[
  {
    "path": ".binny.yaml",
    "content": "tools:\n  # we want to use a pinned version of binny to manage the toolchain (so binny manages itself!)\n  - name: binny\n    version:\n      want: v0.12.0\n    method: github-release\n    with:\n      repo: anchore/binny\n\n  # used to produce SBOMs during release\n  - name: syft\n    version:\n      want: latest\n    method: github-release\n    with:\n      repo: anchore/syft\n\n  # used to sign mac binaries at release\n  - name: quill\n    version:\n      want: v0.7.1\n    method: github-release\n    with:\n      repo: anchore/quill\n\n  # used for linting\n  - name: golangci-lint\n    version:\n      want: v2.11.3\n    method: github-release\n    with:\n      repo: golangci/golangci-lint\n\n  # used for showing the changelog at release\n  - name: glow\n    version:\n      want: v2.1.1\n    method: github-release\n    with:\n      repo: charmbracelet/glow\n\n  # used for signing the checksums file at release\n  - name: cosign\n    version:\n      want: v3.0.5\n    method: github-release\n    with:\n      repo: sigstore/cosign\n\n  # used to release all artifacts\n  - name: goreleaser\n    version:\n      want: v2.14.3\n    method: github-release\n    with:\n      repo: goreleaser/goreleaser\n\n  # used for organizing imports during static analysis\n  - name: gosimports\n    version:\n      want: v0.3.8\n    method: github-release\n    with:\n      repo: rinchsan/gosimports\n\n  # used at release to generate the changelog\n  - name: chronicle\n    version:\n      want: v0.8.0\n    method: github-release\n    with:\n      repo: anchore/chronicle\n\n  # used during static analysis for license compliance\n  - name: bouncer\n    version:\n      want: v0.4.0\n    method: github-release\n    with:\n      repo: wagoodman/go-bouncer\n\n  # used for running all local and CI tasks\n  - name: task\n    version:\n      want: v3.49.1\n    method: github-release\n    with:\n      repo: go-task/task\n\n  # used for triggering a release\n  - name: gh\n    version:\n      want: v2.88.1\n    method: github-release\n    with:\n      repo: cli/cli\n\n  # used for integration tests\n  - name: skopeo\n    version:\n      want: v1.22.0\n    method: go-install\n    with:\n      module: github.com/containers/skopeo\n      entrypoint: cmd/skopeo\n      args:\n        - \"-tags\"\n        - containers_image_openpgp\n      env:\n        - CGO_ENABLED=0\n        - GO_DYN_FLAGS=\"\"\n"
  },
  {
    "path": ".bouncer.yaml",
    "content": "permit:\n  - BSD.*\n  - CC0.*\n  - MIT.*\n  - Apache.*\n  - MPL.*\n  - ISC\n  - WTFPL\n\nignore-packages:\n  # packageurl-go is released under the MIT license located in the root of the repo at /mit.LICENSE\n  - github.com/anchore/packageurl-go\n\n  # github.com/gocsaf/csaf is released under the Apache License, version 2.0 (Apache-2.0)\n  # https://github.com/gocsaf/csaf/blob/main/LICENSE-Apache-2.0.txt\n  - github.com/gocsaf/csaf/v3/csaf\n  - github.com/gocsaf/csaf/v3/internal/misc\n  - github.com/gocsaf/csaf/v3/util\n\n  # tools-golang is released under the Apache License, version 2.0 (Apache-2.0)\n  # https://github.com/spdx/tools-golang/blob/main/LICENSE.code\n  - github.com/spdx/tools-golang\n\n  # crypto/internal/boring is released under the openSSL license as a part of the Golang Standard Libary\n  - crypto/internal/boring\n\n  # from: https://github.com/xi2/xz/blob/master/LICENSE\n  # All these files have been put into the public domain.\n  # You can do whatever you want with these files.\n  - github.com/xi2/xz\n\n  # from: https://github.com/owenrumney/go-sarif/blob/main/LICENSE\n  # This is released into the public domain using the \"Unlicense license\"\n  - github.com/owenrumney/go-sarif\n\n  # github.com/sorairolake/lzip-go is released under the Apache License, version 2.0 (Apache-2.0)\n  # https://github.com/sorairolake/lzip-go/blob/develop/LICENSE-APACHE\n  - github.com/sorairolake/lzip-go\n\n  # from: https://gitlab.com/cznic/sqlite/-/blob/v1.66.3/LICENSE\n  # This is a BSD-3-Clause license\n  - modernc.org/libc\n  - modernc.org/libc/errno\n  - modernc.org/libc/fcntl\n  - modernc.org/libc/fts\n  - modernc.org/libc/grp\n  - modernc.org/libc/langinfo\n  - modernc.org/libc/limits\n  - modernc.org/libc/netdb\n  - modernc.org/libc/netinet/in\n  - modernc.org/libc/poll\n  - modernc.org/libc/pthread\n  - modernc.org/libc/pwd\n  - modernc.org/libc/signal\n  - modernc.org/libc/stdio\n  - modernc.org/libc/stdlib\n  - modernc.org/libc/sys/socket\n  - modernc.org/libc/sys/stat\n  - modernc.org/libc/sys/types\n  - modernc.org/libc/termios\n  - modernc.org/libc/time\n  - modernc.org/libc/unistd\n  - modernc.org/libc/utime\n  - modernc.org/libc/uuid/uuid\n  - modernc.org/libc/wctype\n  - modernc.org/mathutil\n  - modernc.org/memory\n\n"
  },
  {
    "path": ".chronicle.yaml",
    "content": "enforce-v0: true # don't make breaking-change label bump major version before 1.0.\ntitle: \"\"\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: bug\nassignees: ''\n\n---\n\n**What happened**:\n\n**What you expected to happen**:\n\n**How to reproduce it (as minimally and precisely as possible)**:\n\n<!--\nIf possible, please include a link to an artifact grype can scan, instructions to make\none, or upload it on this issue. Some suggestions:\n\n1. Link to Dockerhub, GitHub, GitLab, maven central, quay.io, etc to a public\n   artifact we can try scanning\n2. A Dockerfile that we can build and scan\n3. A simple script that creates a directory exhibiting the issue, for example a\n   list of `npm install` commands\n\nPlease also include the grype command and any configuration used.\n-->\n**Anything else we need to know?**:\n\n**Environment**:\n- Output of `grype version`:\n- OS (e.g: `cat /etc/os-release` or similar):\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "contact_links:\n\n  - name: Join our Discourse community 💬\n    # link to our community Discourse site\n    url: https://anchore.com/discourse\n    about: 'Come chat with us! Ask for help, join our software development efforts, or just give us feedback!'\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: enhancement\nassignees: ''\n\n---\n\n**What would you like to be added**:\n\n**Why is this needed**:\n\n**Additional context**:\n<!-- Add any other context or screenshots about the feature request here. -->\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/match_issue.md",
    "content": "---\nname: Vulnerability Match Issue\nabout: Report an issue with vulnerability matching\ntitle: ''\nlabels: [ bug, false-positive ]\nassignees: ''\n---\n\n**Vulnerability ID**:\n<!-- CVE, GHSA, etc. -->\n\n**Package URL or steps to reproduce**:\n<!--\nRunning `grype <PackageURL>` should show the matching issue. If it does, please provide\nthe full Package URL, e.g.: pkg:apk/alpine/alpine-baselayout@3.7.0-r0?arch=x86_64&distro=alpine-3.22.2\nmake sure to include all parameters, including the distro, where applicable.\n\nIf the issue can't be reproduced with a Package URL, please link to a public artifact\ngrype can scan or provide other instructions to reproduce.\nSome suggestions:\n1. Link to Dockerhub, GitHub, GitLab, maven central, quay.io, etc to a public\n   artifact we can try scanning\n2. A Dockerfile that we can build and scan\n3. A simple script that creates a directory exhibiting the issue, for example a\n   list of `npm install` commands\n\nPlease also include the grype command and any configuration used.\n-->\n\n**Anything else we need to know?**:\n<!-- Add additional information here:\nSome suggestions:\n1. Links to the GHSA or CVE page(s)\n2. Explanation why a vulnerability is or isn't applicable\n-->\n\n**Environment**:\n- Output of `grype version`:\n- OS (e.g: `cat /etc/os-release` or similar):\n"
  },
  {
    "path": ".github/actions/bootstrap/action.yaml",
    "content": "name: \"Bootstrap\"\ndescription: \"Bootstrap all tools and dependencies\"\ninputs:\n  go-version:\n    description: \"Go version to install\"\n    required: true\n    default: \"1.25.x\"\n  python-version:\n    description: \"Python version to install\"\n    required: true\n    default: \"3.11\"\n  go-dependencies:\n    description: \"Download go dependencies\"\n    required: true\n    default: \"true\"\n  cache-key-prefix:\n    description: \"Prefix all cache keys with this value\"\n    required: true\n    default: \"1ac8281053\"\n  compute-fingerprints:\n    description: \"Compute test fixture fingerprints\"\n    required: true\n    default: \"true\"\n  tools:\n    description: \"whether to install tools\"\n    default: \"true\"\n  bootstrap-apt-packages:\n    description: \"Space delimited list of tools to install via apt\"\n    default: \"libxml2-utils\"\n\nruns:\n  using: \"composite\"\n  steps:\n    # note: go mod and build is automatically cached on default with v4+\n    - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0\n      if: inputs.go-version != ''\n      with:\n        go-version: ${{ inputs.go-version }}\n        check-latest: true\n\n    - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0\n      with:\n        python-version: ${{ inputs.python-version }}\n\n    - name: Restore tool cache\n      id: tool-cache\n      uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3\n      if: inputs.tools == 'true'\n      with:\n        path: ${{ github.workspace }}/.tool\n        key: ${{ inputs.cache-key-prefix }}-${{ runner.os }}-tool-${{ hashFiles('.binny.yaml') }}\n\n    - name: Install project tools\n      if: inputs.tools == 'true'\n      shell: bash\n      run: |\n        make tools\n        .tool/binny list\n        .tool/binny check\n\n    - name: Install go dependencies\n      if: inputs.go-dependencies == 'true'\n      shell: bash\n      run: make ci-bootstrap-go\n\n    - name: Install apt packages\n      if: inputs.bootstrap-apt-packages != ''\n      shell: bash\n      run: |\n        read -ra PACKAGES <<< \"$BOOTSTRAP_APT_PACKAGES\"\n        DEBIAN_FRONTEND=noninteractive sudo apt update && sudo -E apt install -y \"${PACKAGES[@]}\"\n      env:\n        BOOTSTRAP_APT_PACKAGES: ${{ inputs.bootstrap-apt-packages }}\n\n    - name: Create all cache fingerprints\n      if: inputs.compute-fingerprints == 'true'\n      shell: bash\n      run: make fingerprints\n"
  },
  {
    "path": ".github/dependabot.yaml",
    "content": "version: 2\nupdates:\n  - package-ecosystem: \"github-actions\"\n    directory: \"/\"\n    schedule:\n      interval: daily\n    cooldown:\n      default-days: 7\n\n  - package-ecosystem: \"github-actions\"\n    directory: \"/.github/actions/bootstrap\"\n    schedule:\n      interval: \"daily\"\n    open-pull-requests-limit: 10\n    labels:\n      - \"dependencies\"\n    cooldown:\n      default-days: 7\n\n  - package-ecosystem: \"gomod\"\n    directory: \"/\"\n    schedule:\n      interval: daily\n    cooldown:\n      default-days: 7\n"
  },
  {
    "path": ".github/scripts/check-syft-version-is-release.sh",
    "content": "#!/usr/bin/env bash\nset -e\n\nversion=$(grep -E \"github.com/anchore/syft\" go.mod | awk '{print $NF}')\n\n# ensure that the version is a release version (not a commit hash)\n# a release in this case means that the go tooling resolved the version to a tag\n# this does not guarantee that the tag has a github release associated with it\nif [[ ! $version =~ ^v[0-9]+\\.[0-9]+\\.[0-9]?$ ]]; then\n    echo \"syft version in go.mod is not a release version: $version\"\n    echo \"please update the version in go.mod to a release version and try again\"\n    exit 1\nelse\n    echo \"syft version in go.mod is a release version: $version\"\nfi\n"
  },
  {
    "path": ".github/scripts/ci-check.sh",
    "content": "#!/usr/bin/env bash\n\nred=$(tput setaf 1)\nbold=$(tput bold)\nnormal=$(tput sgr0)\n\n# assert we are running in CI (or die!)\nif [[ -z \"$CI\" ]]; then\n    echo \"${bold}${red}This step should ONLY be run in CI. Exiting...${normal}\"\n    exit 1\nfi\n\n"
  },
  {
    "path": ".github/scripts/coverage.py",
    "content": "#!/usr/bin/env python3\nimport subprocess\nimport sys\nimport shlex\n\n\nclass bcolors:\n    HEADER = '\\033[95m'\n    OKBLUE = '\\033[94m'\n    OKCYAN = '\\033[96m'\n    OKGREEN = '\\033[92m'\n    WARNING = '\\033[93m'\n    FAIL = '\\033[91m'\n    ENDC = '\\033[0m'\n    BOLD = '\\033[1m'\n    UNDERLINE = '\\033[4m'\n\n\nif len(sys.argv) < 3:\n    print(\"Usage: coverage.py [threshold] [go-coverage-report]\")\n    sys.exit(1)\n\n\nthreshold = float(sys.argv[1])\nreport = sys.argv[2]\n\n\nargs = shlex.split(f\"go tool cover -func {report}\")\np = subprocess.run(args, capture_output=True, text=True)\n\npercent_coverage = float(p.stdout.splitlines()[-1].split()[-1].replace(\"%\", \"\"))\nprint(f\"{bcolors.BOLD}Coverage: {percent_coverage}%{bcolors.ENDC}\")\n\nif percent_coverage < threshold:\n    print(f\"{bcolors.BOLD}{bcolors.FAIL}Coverage below threshold of {threshold}%{bcolors.ENDC}\")\n    sys.exit(1)\n"
  },
  {
    "path": ".github/scripts/db-schema-drift-check.sh",
    "content": "#!/usr/bin/env bash\nset -u\n\nif [ \"$(git status --porcelain | wc -l)\" -ne \"0\" ]; then\n  echo \"  🔴 there are uncommitted changes, please commit them before running this check\"\n  exit 1\nfi\n\nif ! make generate-db-schema; then\n  echo \"Generating database blob schemas failed\"\n  exit 1\nfi\n\nif [ \"$(git status --porcelain | wc -l)\" -ne \"0\" ]; then\n  echo \"  🔴 database blob schemas have uncommitted changes\"\n  echo \"  Run 'task generate-db-schema' and commit the changes\"\n  echo \"\"\n  git status --porcelain\n  echo \"\"\n  git diff schema/grype/db/\n  exit 1\nfi\n\necho \"✅ Database blob schemas are up to date\"\n"
  },
  {
    "path": ".github/scripts/go-mod-tidy-check.sh",
    "content": "#!/usr/bin/env bash\nset -eu\n\nORIGINAL_STATE_DIR=$(mktemp -d \"TEMP-original-state-XXXXXXXXX\")\nTIDY_STATE_DIR=$(mktemp -d \"TEMP-tidy-state-XXXXXXXXX\")\n\ntrap \"cp -p ${ORIGINAL_STATE_DIR}/* ./ && git update-index -q --refresh && rm -fR ${ORIGINAL_STATE_DIR} ${TIDY_STATE_DIR}\" EXIT\n\n# capturing original state of files...\ncp go.mod go.sum \"${ORIGINAL_STATE_DIR}\"\n\n# capturing state of go.mod and go.sum after running go mod tidy...\ngo mod tidy\ncp go.mod go.sum \"${TIDY_STATE_DIR}\"\n\nset +e\n\n# detect difference between the git HEAD state and the go mod tidy state\nDIFF_MOD=$(diff -u \"${ORIGINAL_STATE_DIR}/go.mod\" \"${TIDY_STATE_DIR}/go.mod\")\nDIFF_SUM=$(diff -u \"${ORIGINAL_STATE_DIR}/go.sum\" \"${TIDY_STATE_DIR}/go.sum\")\n\nif [[ -n \"${DIFF_MOD}\" || -n \"${DIFF_SUM}\" ]]; then\n    echo \"go.mod diff:\"\n    echo \"${DIFF_MOD}\"\n    echo \"go.sum diff:\"\n    echo \"${DIFF_SUM}\"\n    echo \"\"\n    printf \"FAILED! go.mod and/or go.sum are NOT tidy; please run 'go mod tidy'.\\n\\n\"\n    exit 1\nfi\n"
  },
  {
    "path": ".github/scripts/json-schema-drift-check.sh",
    "content": "#!/usr/bin/env bash\nset -u\n\nif [ \"$(git status --porcelain | wc -l)\" -ne \"0\" ]; then\n  echo \"  🔴 there are uncommitted changes, please commit them before running this check\"\n  exit 1\nfi\n\nif ! make generate-json-schema; then\n  echo \"Generating json schema failed\"\n  exit 1\nfi\n\nif [ \"$(git status --porcelain | wc -l)\" -ne \"0\" ]; then\n  echo \"  🔴 there are uncommitted changes, please commit them before running this check\"\n  exit 1\nfi\n"
  },
  {
    "path": ".github/scripts/trigger-release.sh",
    "content": "#!/usr/bin/env bash\nset -eu\n\nbold=$(tput bold)\nnormal=$(tput sgr0)\n\nGH_CLI=.tool/gh\n\nif ! [ -x \"$(command -v $GH_CLI)\" ]; then\n    echo \"The GitHub CLI could not be found. run: make bootstrap\"\n    exit 1\nfi\n\n# we want to stop the release as early as possible if the version is not a release version\n./.github/scripts/check-syft-version-is-release.sh\n\n$GH_CLI auth status\n\n# set the default repo in cases where multiple remotes are defined\n$GH_CLI repo set-default anchore/grype\n\nexport GITHUB_TOKEN=\"${GITHUB_TOKEN-\"$($GH_CLI auth token)\"}\"\n\n# we need all of the git state to determine the next version. Since tagging is done by\n# the release pipeline it is possible to not have all of the tags from previous releases.\ngit fetch --tags\n\n# populates the CHANGELOG.md and VERSION files\necho \"${bold}Generating changelog...${normal}\"\nmake changelog 2> /dev/null\n\nNEXT_VERSION=$(cat VERSION)\n\nif [[ \"$NEXT_VERSION\" == \"\" ||  \"${NEXT_VERSION}\" == \"(Unreleased)\" ]]; then\n    echo \"Could not determine the next version to release. Exiting...\"\n    exit 1\nfi\n\nwhile true; do\n    read -p \"${bold}Do you want to trigger a release for version '${NEXT_VERSION}'?${normal} [y/n] \" yn\n    case $yn in\n        [Yy]* ) echo; break;;\n        [Nn]* ) echo; echo \"Cancelling release...\"; exit;;\n        * ) echo \"Please answer yes or no.\";;\n    esac\ndone\n\necho \"${bold}Kicking off release for ${NEXT_VERSION}${normal}...\"\necho\n$GH_CLI workflow run release.yaml -f version=${NEXT_VERSION}\n\necho\necho \"${bold}Waiting for release to start...${normal}\"\nsleep 10\n\nset +e\n\necho \"${bold}Head to the release workflow to monitor the release:${normal} $($GH_CLI run list --workflow=release.yaml --limit=1 --json url --jq '.[].url')\"\nid=$($GH_CLI run list --workflow=release.yaml --limit=1 --json databaseId --jq '.[].databaseId')\n$GH_CLI run watch $id --exit-status || (echo ; echo \"${bold}Logs of failed step:${normal}\" && GH_PAGER=\"\" $GH_CLI run view $id --log-failed)\n"
  },
  {
    "path": ".github/workflows/codeql-analysis.yml",
    "content": "name: CodeQL Security Scan\n\non:\n  workflow_dispatch:\n  push:\n    paths:\n     - '**'\n     - '!**.md'\n     - '!LICENSE'\n     - '!test/**'\n    branches: [ main ]\n\n  schedule:\n    - cron: '0 14 * * 4'\n\njobs:\n  CodeQL:\n    uses: anchore/workflows/.github/workflows/codeql-go.yaml@main\n    with:\n      entrypoint: \"./cmd/${{ github.event.repository.name }}\"\n    permissions:\n      security-events: write\n      contents: read\n"
  },
  {
    "path": ".github/workflows/dependabot-automation.yaml",
    "content": "name: Dependabot Automation\non:\n  pull_request:\n\npermissions:\n  pull-requests: write\n\njobs:\n  run:\n    uses: anchore/workflows/.github/workflows/dependabot-automation.yaml@main\n"
  },
  {
    "path": ".github/workflows/oss-project-board-add.yaml",
    "content": "name: Add to OSS board\n\npermissions:\n  contents: read\n\non:\n  issues:\n    types:\n      - opened\n      - reopened\n      - transferred\n      - labeled\n\njobs:\n\n  run:\n    uses: \"anchore/workflows/.github/workflows/oss-project-board-add.yaml@main\"\n    secrets:\n      token: ${{ secrets.OSS_PROJECT_GH_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/release.yaml",
    "content": "name: \"Release\"\non:\n  workflow_dispatch:\n    inputs:\n      version:\n        description: tag the latest commit on main with the given version (prefixed with v)\n        required: true\n      skip_quality_gate:\n        description: skip quality gate and proceed directly to releasing (for emergency releases)\n        type: boolean\n        default: false\n\npermissions:\n  contents: read\n\njobs:\n  quality-gate:\n    if: ${{ !inputs.skip_quality_gate }}\n    environment: release\n    runs-on: ubuntu-24.04\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2\n        with:\n          persist-credentials: false\n\n      - name: Check if running on main\n        if: github.ref != 'refs/heads/main'\n        # we are using the following flag when running `cosign blob-verify` for checksum signature verification:\n        #   --certificate-identity-regexp \"https://github.com/anchore/.github/workflows/release.yaml@refs/heads/main\"\n        # if we are not on the main branch, the signature will not be verifiable since the suffix requires the main branch\n        # at the time of when the OIDC token was issued on the Github Actions runner.\n        run: echo \"This can only be run on the main branch otherwise releases produced will not be verifiable with cosign\" && exit 1\n\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2\n        with:\n          persist-credentials: false\n\n      - name: Check if pinned syft is a release version\n        run: .github/scripts/check-syft-version-is-release.sh\n\n      - name: Check if tag already exists\n        # note: this will fail if the tag already exists\n        run: |\n          [[ \"$VERSION\" == v* ]] || (echo \"version '$VERSION' does not have a 'v' prefix\" && exit 1)\n          git tag \"$VERSION\"\n        env:\n          VERSION: ${{ github.event.inputs.version }}\n\n      - name: Check static analysis results\n        uses: fountainhead/action-wait-for-check@5a908a24814494009c4bb27c242ea38c93c593be # v1.2.0\n        id: static-analysis\n        with:\n          token: ${{ secrets.GITHUB_TOKEN }}\n          # This check name is defined as the github action job name (in .github/workflows/testing.yaml)\n          checkName: \"Static analysis\"\n          ref: ${{ github.event.pull_request.head.sha || github.sha }}\n\n      - name: Check unit test results\n        uses: fountainhead/action-wait-for-check@5a908a24814494009c4bb27c242ea38c93c593be # v1.2.0\n        id: unit\n        with:\n          token: ${{ secrets.GITHUB_TOKEN }}\n          # This check name is defined as the github action job name (in .github/workflows/testing.yaml)\n          checkName: \"Unit tests\"\n          ref: ${{ github.event.pull_request.head.sha || github.sha }}\n\n      - name: Check integration test results\n        uses: fountainhead/action-wait-for-check@5a908a24814494009c4bb27c242ea38c93c593be # v1.2.0\n        id: integration\n        with:\n          token: ${{ secrets.GITHUB_TOKEN }}\n          # This check name is defined as the github action job name (in .github/workflows/testing.yaml)\n          checkName: \"Integration tests\"\n          timeoutSeconds: 1200 # 20 minutes, it sometimes takes that long\n          ref: ${{ github.event.pull_request.head.sha || github.sha }}\n\n      - name: Check integration test results\n        uses: fountainhead/action-wait-for-check@5a908a24814494009c4bb27c242ea38c93c593be # v1.2.0\n        id: quality_tests\n        with:\n          token: ${{ secrets.GITHUB_TOKEN }}\n          # This check name is defined as the github action job name (in .github/workflows/testing.yaml)\n          checkName: \"Quality tests\"\n          timeoutSeconds: 1200 # 20 minutes, it sometimes takes that long\n          ref: ${{ github.event.pull_request.head.sha || github.sha }}\n\n      - name: Check acceptance test results (linux)\n        uses: fountainhead/action-wait-for-check@5a908a24814494009c4bb27c242ea38c93c593be # v1.2.0\n        id: acceptance-linux\n        with:\n          token: ${{ secrets.GITHUB_TOKEN }}\n          # This check name is defined as the github action job name (in .github/workflows/testing.yaml)\n          checkName: \"Acceptance tests (Linux)\"\n          ref: ${{ github.event.pull_request.head.sha || github.sha }}\n\n      - name: Check acceptance test results (mac)\n        uses: fountainhead/action-wait-for-check@5a908a24814494009c4bb27c242ea38c93c593be # v1.2.0\n        id: acceptance-mac\n        with:\n          token: ${{ secrets.GITHUB_TOKEN }}\n          # This check name is defined as the github action job name (in .github/workflows/testing.yaml)\n          checkName: \"Acceptance tests (Mac)\"\n          ref: ${{ github.event.pull_request.head.sha || github.sha }}\n\n      - name: Check cli test results (linux)\n        uses: fountainhead/action-wait-for-check@5a908a24814494009c4bb27c242ea38c93c593be # v1.2.0\n        id: cli-linux\n        with:\n          token: ${{ secrets.GITHUB_TOKEN }}\n          # This check name is defined as the github action job name (in .github/workflows/testing.yaml)\n          checkName: \"CLI tests (Linux)\"\n          ref: ${{ github.event.pull_request.head.sha || github.sha }}\n\n      - name: Quality gate\n        if: steps.static-analysis.outputs.conclusion != 'success' || steps.unit.outputs.conclusion != 'success' || steps.integration.outputs.conclusion != 'success' || steps.quality_tests.outputs.conclusion != 'success' || steps.cli-linux.outputs.conclusion != 'success' || steps.acceptance-linux.outputs.conclusion != 'success' || steps.acceptance-mac.outputs.conclusion != 'success'\n        env:\n          STATIC_ANALYSIS_STATUS: ${{ steps.static-analysis.conclusion }}\n          UNIT_TEST_STATUS: ${{ steps.unit.outputs.conclusion }}\n          INTEGRATION_TEST_STATUS: ${{ steps.integration.outputs.conclusion }}\n          QUALITY_TEST_STATUS: ${{ steps.quality_tests.outputs.conclusion }}\n          ACCEPTANCE_LINUX_STATUS: ${{ steps.acceptance-linux.outputs.conclusion }}\n          ACCEPTANCE_MAC_STATUS: ${{ steps.acceptance-mac.outputs.conclusion }}\n          CLI_LINUX_STATUS: ${{ steps.cli-linux.outputs.conclusion }}\n        run: |\n          echo \"Static Analysis Status: $STATIC_ANALYSIS_STATUS\"\n          echo \"Unit Test Status: $UNIT_TEST_STATUS\"\n          echo \"Integration Test Status: $INTEGRATION_TEST_STATUS\"\n          echo \"Quality Test Status: $QUALITY_TEST_STATUS\"\n          echo \"Acceptance Test (Linux) Status: $ACCEPTANCE_LINUX_STATUS\"\n          echo \"Acceptance Test (Mac) Status: $ACCEPTANCE_MAC_STATUS\"\n          echo \"CLI Test (Linux) Status: $CLI_LINUX_STATUS\"\n          false\n\n  # only release core assets within the \"release\" job. Any other assets not already under the purview of the\n  # goreleaser configuration should be added as separate jobs to allow for debugging separately from the release workflow\n  # as well as not accidentally be re-run as a step multiple times (as could be done within the release workflow) as\n  # not all actions are guaranteed to be idempotent.\n  release:\n    needs: [quality-gate]\n    if: ${{ always() && (needs.quality-gate.result == 'success' || inputs.skip_quality_gate) }}\n    runs-on: ubuntu-24.04\n    permissions:\n      contents: write\n      packages: write\n      id-token: write\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2\n        with:\n          fetch-depth: 0\n          persist-credentials: true\n\n      - name: Bootstrap environment\n        uses: ./.github/actions/bootstrap\n        with:\n          # use the same cache we used for building snapshots\n          build-cache-key-prefix: \"snapshot\"\n\n      - name: Login to Docker Hub\n        uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2  #v4.0.0\n        with:\n          username: ${{ secrets.ANCHOREOSSWRITE_DH_USERNAME }}\n          password: ${{ secrets.ANCHOREOSSWRITE_DH_PAT }}\n\n      - name: Login to GitHub Container Registry\n        uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2  #v4.0.0\n        with:\n          registry: ghcr.io\n          username: ${{ github.actor }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Tag release\n        run: |\n          git config user.name \"anchoreci\"\n          git config user.email \"anchoreci@users.noreply.github.com\"\n          git tag -a \"$VERSION\" -m \"Release $VERSION\"\n          git push origin --tags\n        env:\n          VERSION: ${{ github.event.inputs.version }}\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Build & publish release artifacts\n        run: make ci-release\n        env:\n          # for mac signing and notarization...\n          QUILL_SIGN_P12: ${{ secrets.ANCHORE_APPLE_DEVELOPER_ID_CERT_CHAIN }}\n          QUILL_SIGN_PASSWORD: ${{ secrets.ANCHORE_APPLE_DEVELOPER_ID_CERT_PASS }}\n          QUILL_NOTARY_ISSUER: ${{ secrets.APPLE_NOTARY_ISSUER }}\n          QUILL_NOTARY_KEY_ID: ${{ secrets.APPLE_NOTARY_KEY_ID }}\n          QUILL_NOTARY_KEY: ${{ secrets.APPLE_NOTARY_KEY }}\n          # for creating the release (requires write access to packages and content)\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          # for updating brew formula in anchore/homebrew-syft\n          GITHUB_BREW_TOKEN: ${{ secrets.ANCHOREOPS_GITHUB_OSS_WRITE_TOKEN }}\n\n      - uses: anchore/sbom-action@57aae528053a48a3f6235f2d9461b05fbcb7366d # v0.23.1\n        continue-on-error: true\n        with:\n          artifact-name: sbom.spdx.json\n\n      - name: Notify Slack of new release\n        uses: slackapi/slack-github-action@91efab103c0de0a537f72a35f6b8cda0ee76bf0a #v2.1.1\n        continue-on-error: true\n        with:\n          webhook: ${{ secrets.SLACK_TOOLBOX_WEBHOOK_URL }}\n          webhook-type: incoming-webhook\n          payload: |\n            text: \"A new Grype release has been published: https://github.com/anchore/grype/releases/tag/${{ github.event.inputs.version }}\"\n            blocks:\n              - type: section\n                text:\n                  type: mrkdwn\n                  text: |\n                    *A new Grype release has been published* :rocket:\n                    • Release: <https://github.com/anchore/grype/releases/tag/${{ github.event.inputs.version }}|${{ github.event.inputs.version }}>\n                    • Repo: `${{ github.repository }}`\n                    • Workflow: `${{ github.workflow }}`\n                    • Event: `${{ github.event_name }}`\n        if: ${{ success() }}\n\n  release-install-script:\n    needs: [release]\n    if: ${{ needs.release.result == 'success' }}\n    uses: \"anchore/workflows/.github/workflows/release-install-script.yaml@main\"\n    with:\n      tag: ${{ github.event.inputs.version }}\n    secrets:\n      # needed for r2...\n      R2_INSTALL_ACCESS_KEY_ID: ${{ secrets.OSS_R2_INSTALL_ACCESS_KEY_ID }}\n      R2_INSTALL_SECRET_ACCESS_KEY: ${{ secrets.OSS_R2_INSTALL_SECRET_ACCESS_KEY }}\n      R2_ENDPOINT: ${{ secrets.TOOLBOX_CLOUDFLARE_R2_ENDPOINT }}\n      # needed for s3...\n      S3_INSTALL_AWS_ACCESS_KEY_ID: ${{ secrets.TOOLBOX_AWS_ACCESS_KEY_ID }}\n      S3_INSTALL_AWS_SECRET_ACCESS_KEY: ${{ secrets.TOOLBOX_AWS_SECRET_ACCESS_KEY }}"
  },
  {
    "path": ".github/workflows/remove-awaiting-response-label.yaml",
    "content": "name: \"Manage Awaiting Response Label\"\n\non:\n  issue_comment:\n    types: [created]\n\njobs:\n  run:\n    permissions:\n      issues: write\n      pull-requests: write\n    uses: \"anchore/workflows/.github/workflows/remove-awaiting-response-label.yaml@main\"\n    secrets:\n      token: ${{ secrets.OSS_PROJECT_GH_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/scorecards.yml",
    "content": "name: Scorecards supply-chain security\non:\n  # Only the default branch is supported.\n  branch_protection_rule:\n  push:\n    branches: [ \"main\" ]\n\n# Declare default permissions as read only.\npermissions: read-all\n\njobs:\n  analysis:\n    name: Scorecards analysis\n    runs-on: ubuntu-latest\n    permissions:\n      # Needed to upload the results to code-scanning dashboard.\n      security-events: write\n      # Used to receive a badge.\n      id-token: write\n\n    steps:\n      - name: \"Checkout code\"\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          persist-credentials: false\n\n      - name: \"Run analysis\"\n        uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3\n        with:\n          results_file: results.sarif\n          results_format: sarif\n\n          # Publish the results for public repositories to enable scorecard badges. For more details, see\n          # https://github.com/ossf/scorecard-action#publishing-results.\n          # For private repositories, `publish_results` will automatically be set to `false`, regardless\n          # of the value entered here.\n          publish_results: true\n\n      # Upload the results to GitHub's code scanning dashboard.\n      - name: \"Upload to code-scanning\"\n        uses: github/codeql-action/upload-sarif@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6\n        with:\n          sarif_file: results.sarif\n"
  },
  {
    "path": ".github/workflows/update-anchore-dependencies.yml",
    "content": "name: PR to update Anchore dependencies\non:\n  workflow_dispatch:\n    inputs:\n      repos:\n        description: \"List of dependencies to update\"\n        required: true\n        type: string\n\npermissions:\n  contents: read\n\njobs:\n  update:\n    runs-on: ubuntu-latest\n    if: github.repository_owner == 'anchore' # only run for main repo (not forks)\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2\n        with:\n          persist-credentials: false\n\n      - name: Bootstrap environment\n        uses: ./.github/actions/bootstrap\n        with:\n          tools: false\n          bootstrap-apt-packages: \"\"\n\n      - name: Update dependencies\n        id: update\n        uses: anchore/workflows/.github/actions/update-go-dependencies@main\n        with:\n          repos: ${{ github.event.inputs.repos }}\n\n      - uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a #v2.1.0\n        id: generate-token\n        with:\n          app_id: ${{ secrets.TOKEN_APP_ID }}\n          private_key: ${{ secrets.TOKEN_APP_PRIVATE_KEY }}\n\n      - uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 #v8.0.0\n        with:\n          signoff: true\n          delete-branch: true\n          draft: ${{ steps.update.outputs.draft }}\n          # do not change this branch, as other workflows depend on it\n          branch: auto/integration\n          labels: dependencies,pre-release\n          commit-message: \"chore(deps): update anchore dependencies\"\n          title: \"chore(deps): update anchore dependencies\"\n          body: ${{ steps.update.outputs.summary }}\n          token: ${{ steps.generate-token.outputs.token }}\n"
  },
  {
    "path": ".github/workflows/update-bootstrap-tools.yml",
    "content": "name: PR for latest versions of tools\non:\n  schedule:\n    - cron: \"0 8 * * *\" # 3 AM EST\n\n  workflow_dispatch:\n\npermissions:\n  contents: read\n\njobs:\n  update-bootstrap-tools:\n    runs-on: ubuntu-latest\n    if: github.repository == 'anchore/grype' # only run for main repo\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2\n        with:\n          persist-credentials: false\n\n      - name: Bootstrap environment\n        uses: ./.github/actions/bootstrap\n        with:\n          bootstrap-apt-packages: \"\"\n          compute-fingerprints: \"false\"\n          go-dependencies: false\n\n      - name: \"Update tool versions\"\n        id: latest-versions\n        run: |\n          make update-tools\n          make list-tools\n\n          export NO_COLOR=1\n          delimiter=\"$(openssl rand -hex 8)\"\n\n          {\n            echo \"status<<${delimiter}\"\n            make list-tool-updates\n            echo \"${delimiter}\"\n          } >> $GITHUB_OUTPUT\n\n          {\n            echo \"### Tool version status\"\n            echo \"\\`\\`\\`\"\n            make list-tool-updates\n            echo \"\\`\\`\\`\"\n          } >> $GITHUB_STEP_SUMMARY\n\n      - uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a #v2.1.0\n        id: generate-token\n        with:\n          app_id: ${{ secrets.TOKEN_APP_ID }}\n          private_key: ${{ secrets.TOKEN_APP_PRIVATE_KEY }}\n\n      - uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 #v8.0.0\n        with:\n          signoff: true\n          delete-branch: true\n          branch: auto/latest-tools\n          labels: dependencies\n          commit-message: 'chore(deps): update tools to latest versions'\n          title: 'chore(deps): update tools to latest versions'\n          body: |\n            ```\n            ${{ steps.latest-versions.outputs.status }}\n            ```\n            This is an auto-generated pull request to update all of the tools to the latest versions.\n          token: ${{ steps.generate-token.outputs.token }}\n"
  },
  {
    "path": ".github/workflows/update-generated-code.yml",
    "content": "name: PR to update OS codename generated code\non:\n  schedule:\n    - cron: \"0 1 * * 1\" # every Monday at 1 AM UTC\n\n  workflow_dispatch:\n\npermissions:\n  contents: read\n\nenv:\n  SLACK_NOTIFICATIONS: true\n\njobs:\n  run-code-gen:\n    name: \"Run code generation\"\n    runs-on: ubuntu-latest\n    if: github.repository == 'anchore/grype' # only run for main repo\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2\n        with:\n          persist-credentials: false\n\n      - name: Bootstrap environment\n        uses: ./.github/actions/bootstrap\n        with:\n          bootstrap-apt-packages: \"\"\n          compute-fingerprints: \"false\"\n          go-dependencies: true\n\n      - name: \"Generate codename data\"\n        run: |\n          make generate-codename-data\n\n      - uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a #v2.1.0\n        id: generate-token\n        with:\n          app_id: ${{ secrets.TOKEN_APP_ID }}\n          private_key: ${{ secrets.TOKEN_APP_PRIVATE_KEY }}\n\n      - uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 #v8.1.0\n        with:\n          signoff: true\n          delete-branch: true\n          branch: auto/latest-codename-data\n          labels: dependencies\n          commit-message: \"chore(deps): update OS codename generated code\"\n          title: \"chore(deps): update OS codename generated code\"\n          body: |\n            Update OS codename data from endoflife.date\n          token: ${{ steps.generate-token.outputs.token }}\n\n      - name: Notify Slack on failure\n        uses: slackapi/slack-github-action@91efab103c0de0a537f72a35f6b8cda0ee76bf0a #v2.1.1\n        if: ${{ failure() && env.SLACK_NOTIFICATIONS == 'true' }}\n        with:\n          webhook: ${{ secrets.SLACK_TOOLBOX_WEBHOOK_URL }}\n          webhook-type: incoming-webhook\n          payload: |\n            text: \"Grype OS codename code generation failed\"\n            blocks:\n              - type: section\n                text:\n                  type: mrkdwn\n                  text: |\n                    *Grype OS codename code generation failed*\n                    • Workflow: `${{ github.workflow }}`\n                    • Event: `${{ github.event_name }}`\n                    • Job Status: `${{ job.status }}`\n                    • <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View Run>\n"
  },
  {
    "path": ".github/workflows/update-quality-gate-db.yml",
    "content": "name: PR for upgrading quality gate test DB\non:\n  schedule:\n    - cron: \"0 16 1 * *\" # first day of each month @ 11 AM EST\n\n  workflow_dispatch:\n\npermissions:\n  contents: read\n\njobs:\n  update-test-db-url:\n    runs-on: ubuntu-latest\n    if: github.repository == 'anchore/grype' # only run for main repo\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2\n        with:\n          persist-credentials: false\n\n      - name: \"Update quality DB\"\n        run: |\n          make update-quality-gate-db\n\n      - uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a #v2.1.0\n        id: generate-token\n        with:\n          app_id: ${{ secrets.TOKEN_APP_ID }}\n          private_key: ${{ secrets.TOKEN_APP_PRIVATE_KEY }}\n\n      - uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 #v8.0.0\n        with:\n          signoff: true\n          delete-branch: true\n          branch: auto/update-quality-test-db\n          labels: test, changelog-ignore\n          commit-message: 'test: update quality gate db to latest version'\n          title: 'test: update quality gate db to latest version'\n          body: |\n            This is an auto-generated pull request to update the quality gate db to latest version\n          token: ${{ steps.generate-token.outputs.token }}\n"
  },
  {
    "path": ".github/workflows/validate-github-actions.yaml",
    "content": "name: \"Validate GitHub Actions\"\n\non:\n  pull_request:\n    paths:\n      - '.github/workflows/**'\n      - '.github/actions/**'\n  push:\n    branches:\n      - main\n    paths:\n      - '.github/workflows/**'\n      - '.github/actions/**'\n\npermissions:\n  contents: read\n\njobs:\n  zizmor:\n    name: \"Lint\"\n    runs-on: ubuntu-latest\n    permissions:\n      contents: read\n      security-events: write  # for uploading SARIF results\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          persist-credentials: false\n\n      - name: \"Run zizmor\"\n        uses: zizmorcore/zizmor-action@0dce2577a4760a2749d8cfb7a84b7d5585ebcb7d # v0.5.0\n        with:\n          config: .github/zizmor.yml\n          # Disable SARIF upload so the step is a simple pass/fail gate\n          advanced-security: false\n          inputs: .github\n"
  },
  {
    "path": ".github/workflows/validations.yaml",
    "content": "name: \"Validations\"\n\non:\n  workflow_dispatch:\n  pull_request:\n  push:\n    branches:\n      - main\n\npermissions:\n  contents: read\n\njobs:\n  Static-Analysis:\n    # Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline\n    name: \"Static analysis\"\n    runs-on: ubuntu-24.04\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          persist-credentials: false\n\n      - name: Bootstrap environment\n        uses: ./.github/actions/bootstrap\n\n      - name: Run static analysis\n        run: make static-analysis\n\n  Unit-Test:\n    # Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline\n    name: \"Unit tests\"\n    runs-on: ubuntu-24.04\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          persist-credentials: false\n\n      - name: Bootstrap environment\n        uses: ./.github/actions/bootstrap\n      \n      - name: Run unit tests\n        run: make unit\n\n  Quality-Test:\n    # Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline\n    name: \"Quality tests\"\n    runs-on: ubuntu-22.04-4core-16gb\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          submodules: true\n          persist-credentials: false\n\n      - name: Bootstrap environment\n        uses: ./.github/actions/bootstrap\n\n      - name: Run quality tests\n        run: make quality\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Archive the provider state\n        if: ${{ failure() }}\n        run: tar -czvf qg-capture-state.tar.gz -C test/quality --exclude tools --exclude labels .yardstick.yaml .yardstick\n\n      - name: Upload the provider state archive\n        if: ${{ failure() }}\n        uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0\n        with:\n          name: qg-capture-state\n          path: qg-capture-state.tar.gz\n\n      - name: Show instructions to debug\n        if: ${{ failure() }}\n        run: |\n          ARCHIVE_BASENAME=qg-capture-state\n          ARCHIVE_NAME=$ARCHIVE_BASENAME.zip\n\n          cat << EOF >> $GITHUB_STEP_SUMMARY\n          ## Troubleshooting failed run\n\n          Download the artifact from this workflow run: \\`$ARCHIVE_NAME\\`\n\n          Then run the following commands to debug:\n          \\`\\`\\`bash\n          # copy the archive to the tests/quality directory\n          cd test/quality\n          unzip $ARCHIVE_NAME && tar -xzf $ARCHIVE_BASENAME.tar.gz\n          \\`\\`\\`\n\n          Now you can debug the with yardstick:\n          \\`\\`\\`bash\n          poetry shell\n          yardstick result list\n          yardstick label explore\n          \\`\\`\\`\n          EOF\n\n\n  Integration-Test:\n    # Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline\n    name: \"Integration tests\"\n    runs-on: ubuntu-24.04\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          persist-credentials: false\n\n      - name: Bootstrap environment\n        uses: ./.github/actions/bootstrap\n\n      - name: Restore integration test cache\n        uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 #v5.0.3\n        with:\n          path: ${{ github.workspace }}/test/integration/testdata/cache\n          key: ${{ runner.os }}-integration-test-cache-${{ hashFiles('test/integration/testdata/cache.fingerprint') }}\n\n      - name: Run integration tests\n        run: make integration\n\n  Build-Snapshot-Artifacts:\n    name: \"Build snapshot artifacts\"\n    runs-on: ubuntu-24.04\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          persist-credentials: false\n\n      - name: Bootstrap environment\n        uses: ./.github/actions/bootstrap\n        with:\n          # why have another build cache key? We don't want unit/integration/etc test build caches to replace\n          # the snapshot build cache, which includes builds for all OSs and architectures. As long as this key is\n          # unique from the build-cache-key-prefix in other CI jobs, we should be fine.\n          #\n          # note: ideally this value should match what is used in release (just to help with build times).\n          build-cache-key-prefix: \"snapshot\"\n          bootstrap-apt-packages: \"\"\n\n      - name: Build snapshot artifacts\n        run: make snapshot\n\n      # why not use actions/upload-artifact? It is very slow (3 minutes to upload ~600MB of data, vs 10 seconds with this approach).\n      # see https://github.com/actions/upload-artifact/issues/199 for more info\n      - name: Upload snapshot artifacts\n        uses: actions/cache/save@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 #v5.0.3\n        with:\n          path: snapshot\n          key: snapshot-build-${{ github.run_id }}\n\n  Upload-Snapshot-Artifacts:\n    name: \"Upload snapshot artifacts\"\n    needs: [Build-Snapshot-Artifacts]\n    runs-on: ubuntu-24.04\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2\n        with:\n          persist-credentials: false\n\n      - name: Download snapshot build\n        uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 #v5.0.3\n        with:\n          path: snapshot\n          key: snapshot-build-${{ github.run_id }}\n\n      - run: npm install @actions/artifact@2.2.2\n\n      - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd #v8.0.0\n        with:\n          script: |\n            const { readdirSync } = require('fs')\n            const { DefaultArtifactClient } = require('@actions/artifact')\n            const artifact = new DefaultArtifactClient()\n            const ls = d => readdirSync(d, { withFileTypes: true })\n            const baseDir = \"./snapshot\"\n            const dirs = ls(baseDir).filter(f => f.isDirectory()).map(f => f.name)\n            const uploads = []\n            for (const dir of dirs) {\n              // uploadArtifact returns Promise<{id, size}>\n              uploads.push(artifact.uploadArtifact(\n                // name of the archive:\n                `${dir}`,\n                // array of all files to include:\n                ls(`${baseDir}/${dir}`).map(f => `${baseDir}/${dir}/${f.name}`),\n                // base directory to trim from entries:\n                `${baseDir}/${dir}`,\n                { retentionDays: 30 }\n              ))\n            }\n            // wait for all uploads to finish\n            Promise.all(uploads)\n\n  Acceptance-Linux:\n    # Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline\n    name: \"Acceptance tests (Linux)\"\n    needs: [Build-Snapshot-Artifacts]\n    runs-on: ubuntu-24.04\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2\n        with:\n          persist-credentials: false\n\n      - name: Download snapshot build\n        uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 #v5.0.3\n        with:\n          path: snapshot\n          key: snapshot-build-${{ github.run_id }}\n\n      - name: Restore install.sh test image cache\n        id: install-test-image-cache\n        uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 #v5.0.3\n        with:\n          path: ${{ github.workspace }}/test/install/cache\n          key: ${{ runner.os }}-install-test-image-cache-${{ hashFiles('test/install/cache.fingerprint') }}\n\n      - name: Load test image cache\n        if: steps.install-test-image-cache.outputs.cache-hit == 'true'\n        run: make install-test-cache-load\n\n      - name: Run install.sh tests (Linux)\n        run: make install-test\n\n      - name: (cache-miss) Create test image cache\n        if: steps.install-test-image-cache.outputs.cache-hit != 'true'\n        run: make install-test-cache-save\n\n  Acceptance-Mac:\n    # Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline\n    name: \"Acceptance tests (Mac)\"\n    needs: [Build-Snapshot-Artifacts]\n    runs-on: macos-latest\n    steps:\n      - name: Install Cosign\n        uses: sigstore/cosign-installer@ba7bc0a3fef59531c69a25acd34668d6d3fe6f22 #v4.1.0\n\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2\n        with:\n          persist-credentials: false\n\n      - name: Download snapshot build\n        uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 #v5.0.3\n        with:\n          path: snapshot\n          key: snapshot-build-${{ github.run_id }}\n\n      - name: Restore docker image cache for compare testing\n        id: mac-compare-testing-cache\n        uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 #v5.0.3\n        with:\n          path: image.tar\n          key: ${{ runner.os }}-${{ hashFiles('test/compare/mac.sh') }}\n\n      - name: Run install.sh tests (Mac)\n        run: make install-test-ci-mac\n\n\n  Cli-Linux:\n    # Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline\n    name: \"CLI tests (Linux)\"\n    needs: [Build-Snapshot-Artifacts]\n    runs-on: ubuntu-24.04\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2\n        with:\n          persist-credentials: false\n\n      - name: Bootstrap environment\n        uses: ./.github/actions/bootstrap\n\n      - name: Restore CLI test-fixture cache\n        uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 #v5.0.3\n        with:\n          path: ${{ github.workspace }}/test/cli/testdata/cache\n          key: ${{ runner.os }}-cli-test-cache-${{ hashFiles('test/cli/testdata/cache.fingerprint') }}\n\n      - name: Download snapshot build\n        uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 #v5.0.3\n        with:\n          path: snapshot\n          key: snapshot-build-${{ github.run_id }}\n\n      - name: Run CLI Tests (Linux)\n        run: make cli\n        env:\n          GRYPE_SNAPSHOT_PREBUILT: \"true\"\n          GRYPE_BINARY_LOCATION: ${{ github.workspace }}/snapshot/linux-build_linux_amd64_v1/grype\n\n  Cleanup-Cache:\n    name: \"Cleanup snapshot cache\"\n    if: github.event.pull_request.head.repo.full_name == github.repository\n    runs-on: ubuntu-24.04\n    permissions:\n      actions: write\n    needs:\n      - Acceptance-Linux\n      - Acceptance-Mac\n      - Cli-Linux\n      - Upload-Snapshot-Artifacts\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2\n        with:\n          persist-credentials: false\n\n      - name: Delete snapshot cache\n        run: gh cache delete \"snapshot-build-${{ github.run_id }}\" || echo \"Cache deletion failed or cache not found - continuing\"\n        env:\n          GH_TOKEN: ${{ github.token }}\n"
  },
  {
    "path": ".github/zizmor.yml",
    "content": "rules:\n  unpinned-uses:\n    config:\n      policies:\n        # anchore/workflows is an internal repository; using @main is acceptable\n        anchore/*: any\n"
  },
  {
    "path": ".gitignore",
    "content": "# AI\n.claude\nCLAUDE.MD\n\n# local development tailoring\ngo.work\ngo.work.sum\n.tool-versions\n.jj/\nmise.toml\nspecs/\n*.xxh64\n\n# AI and LLM related files\nCLAUDE.md\n.claude\n\n# app configuration\n/.grype.yaml\n\n# tool and bin directories\n.tmp/\nbin/\n/bin\n/.bin\n/build\n/dist\n/snapshot\n/.tool\n/.task\n\n# changelog generation\n/CHANGELOG.md\n/VERSION\n\n# IDE configuration\n.vscode/\n.idea/\n.server/\n.history/\n\n# test related\n*.fingerprint\n/test/results\ncoverage.txt\n*.log\n.server\n\n# grype-db related\n/metadata.json\n/listing.json\n*.db\n*.db-journal\n!**/testdata/**/*.db\n!**/testdata/**/bin/\n!**/testdata/**/*.jar\n\n# probable archives\n.images\n*.tar\n*.jar\n*.war\n*.ear\n*.jpi\n*.hpi\n*.zip\n*.iml\n\n# Binaries for programs and plugins\n*.exe\n*.exe~\n*.dll\n*.so\n*.dylib\n\n# Test binary, build with `go test -c`\n*.test\n\n# OS files\n.DS_Store\n\n*.profile\n\n# Binaries for programs and plugins\n*.exe\n*.exe~\n*.dll\n*.so\n*.dylib\n/grype/grype\nmain\n\n# Test binary, build with `go test -c`\n*.test\n\n# Output of the go coverage tool, specifically when used with LiteIDE\n*.out\n"
  },
  {
    "path": ".gitmodules",
    "content": "[submodule \"test/quality/vulnerability-match-labels\"]\n\tpath = test/quality/vulnerability-match-labels\n\turl = https://github.com/anchore/vulnerability-match-labels.git\n\tbranch = main\n"
  },
  {
    "path": ".golangci.yaml",
    "content": "version: \"2\"\nlinters:\n    # inverted configuration with `enable-all` and `disable` is not scalable during updates of golangci-lint\n  default: none\n  enable:\n    - asciicheck\n    - bodyclose\n    - copyloopvar\n    - dogsled\n    - dupl\n    - errcheck\n    - funlen\n    - gocognit\n    - goconst\n    - gocritic\n    - gocyclo\n    - goprintffuncname\n    - gosec\n    - govet\n    - ineffassign\n    - misspell\n    - nakedret\n    - revive\n    - staticcheck\n    - unconvert\n    - unparam\n    - unused\n    - whitespace\n  settings:\n    funlen:\n      lines: 70\n      statements: 50\n  exclusions:\n    generated: lax\n    presets:\n      - comments\n      - common-false-positives\n      - legacy\n      - std-error-handling\n    rules:\n      # we have multiple packages in grype that might overlap with the stblib; their names reflect their purpose\n      - linters:\n          - revive\n        text: \"var-naming: avoid package names that conflict\"\n\n    paths:\n      - third_party$\n      - builtin$\n      - examples$\n\n# do not enable...\n#    - deadcode          # The owner seems to have abandoned the linter. Replaced by \"unused\".\n#    - depguard          # we need to setup a configuration for this\n#    - goprintffuncname  # does not catch all cases and there are exceptions\n#    - nakedret          # does not catch all cases and should not fail a build\n#    - gochecknoglobals\n#    - gochecknoinits    # this is too aggressive\n#    - rowserrcheck disabled per generics https://github.com/golangci/golangci-lint/issues/2649\n#    - godot\n#    - godox\n#    - goerr113\n#    - goimports   # we're using gosimports now instead to account for extra whitespaces (see https://github.com/golang/go/issues/20818)\n#    - golint      # deprecated\n#    - gomnd       # this is too aggressive\n#    - interfacer  # this is a good idea, but is no longer supported and is prone to false positives\n#    - lll         # without a way to specify per-line exception cases, this is not usable\n#    - maligned    # this is an excellent linter, but tricky to optimize and we are not sensitive to memory layout optimizations\n#    - nestif\n#    - 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)\n#    - prealloc     # following this rule isn't consistently a good idea, as it sometimes forces unnecessary allocations that result in less idiomatic code\n#    - rowserrcheck # not in a repo with sql, so this is not useful\n#    - scopelint    # deprecated\n#    - structcheck  # The owner seems to have abandoned the linter. Replaced by \"unused\".\n#    - testpackage\n#    - varcheck     # The owner seems to have abandoned the linter. Replaced by \"unused\".\n#    - wsl          # this doens't have an auto-fixer yet and is pretty noisy (https://github.com/bombsimon/wsl/issues/90)\n\nissues:\n  max-same-issues: 25\n  uniq-by-line: false\n\n# TODO: enable this when we have coverage on docstring comments\n#  # The list of ids of default excludes to include or disable.\n#  include:\n#    - EXC0002 # disable excluding of issues about comments from golint\n\nformatters:\n  enable:\n    - gofmt\n    - goimports\n  exclusions:\n    generated: lax\n    paths:\n      - third_party$\n      - builtin$\n      - examples$\n"
  },
  {
    "path": ".goreleaser.yaml",
    "content": "version: 2\n\nrelease:\n  prerelease: auto\n  draft: false\n\nenv:\n  # required to support multi architecture docker builds\n  - DOCKER_CLI_EXPERIMENTAL=enabled\n  - CGO_ENABLED=0\n\nbuilds:\n  - id: linux-build\n    dir: ./cmd/grype\n    binary: grype\n    goos:\n      - linux\n    goarch:\n      - amd64\n      - arm64\n      - ppc64le\n      - s390x\n    # set the modified timestamp on the output binary to the git timestamp to ensure a reproducible build\n    mod_timestamp: &build-timestamp '{{ .CommitTimestamp }}'\n    ldflags: &build-ldflags |\n      -w\n      -s\n      -extldflags '-static'\n      -X main.version={{.Version}}\n      -X main.gitCommit={{.Commit}}\n      -X main.buildDate={{.Date}}\n      -X main.gitDescription={{.Summary}}\n\n  - id: darwin-build\n    dir: ./cmd/grype\n    binary: grype\n    goos:\n      - darwin\n    goarch:\n      - amd64\n      - arm64\n    mod_timestamp: *build-timestamp\n    ldflags: *build-ldflags\n    hooks:\n      post:\n        - cmd: .tool/quill sign-and-notarize \"{{ .Path }}\" --dry-run={{ .IsSnapshot }} --ad-hoc={{ .IsSnapshot }} -vv\n          env:\n            - QUILL_LOG_FILE=/tmp/quill-{{ .Target }}.log\n\n  - id: windows-build\n    dir: ./cmd/grype\n    binary: grype\n    goos:\n      - windows\n    goarch:\n      - amd64\n    mod_timestamp: *build-timestamp\n    ldflags: *build-ldflags\n\narchives:\n  - id: linux-archives\n    ids:\n      - linux-build\n\n  - id: darwin-archives\n    ids:\n      - darwin-build\n\n  - id: windows-archives\n    formats: [zip]\n    ids:\n      - windows-build\n\nnfpms:\n  - license: \"Apache 2.0\"\n    maintainer: \"Anchore, Inc\"\n    homepage: &website \"https://github.com/anchore/grype\"\n    description: &description \"A vulnerability scanner for container images and filesystems\"\n    formats:\n      - rpm\n      - deb\n\nhomebrew_casks:\n  - repository:\n      owner: anchore\n      name: homebrew-grype\n      token: \"{{.Env.GITHUB_BREW_TOKEN}}\"\n    ids:\n      - darwin-archives\n      - linux-archives\n    homepage: *website\n    description: *description\n    license: \"Apache License 2.0\"\n    conflicts:\n      # claim that the previous grype formula (in the root of homebrew-grype) conflicts with the new cask\n      # see https://goreleaser.com/deprecations/#brews for more information\n      - formula: grype\n\ndockers:\n  # production images...\n  - image_templates:\n      - anchore/grype:{{.Tag}}-amd64\n      - ghcr.io/anchore/grype:{{.Tag}}-amd64\n    goarch: amd64\n    dockerfile: Dockerfile\n    use: buildx\n    build_flag_templates:\n      - \"--platform=linux/amd64\"\n      - \"--provenance=false\"\n      - \"--build-arg=BUILD_DATE={{.Date}}\"\n      - \"--build-arg=BUILD_VERSION={{.Version}}\"\n      - \"--build-arg=VCS_REF={{.FullCommit}}\"\n      - \"--build-arg=VCS_URL={{.GitURL}}\"\n\n  - image_templates:\n      - anchore/grype:{{.Tag}}-arm64v8\n      - ghcr.io/anchore/grype:{{.Tag}}-arm64v8\n    goarch: arm64\n    dockerfile: Dockerfile\n    use: buildx\n    build_flag_templates:\n      - \"--platform=linux/arm64/v8\"\n      - \"--provenance=false\"\n      - \"--build-arg=BUILD_DATE={{.Date}}\"\n      - \"--build-arg=BUILD_VERSION={{.Version}}\"\n      - \"--build-arg=VCS_REF={{.FullCommit}}\"\n      - \"--build-arg=VCS_URL={{.GitURL}}\"\n\n  - image_templates:\n      - anchore/grype:{{.Tag}}-ppc64le\n      - ghcr.io/anchore/grype:{{.Tag}}-ppc64le\n    goarch: ppc64le\n    dockerfile: Dockerfile\n    use: buildx\n    build_flag_templates:\n      - \"--platform=linux/ppc64le\"\n      - \"--provenance=false\"\n      - \"--build-arg=BUILD_DATE={{.Date}}\"\n      - \"--build-arg=BUILD_VERSION={{.Version}}\"\n      - \"--build-arg=VCS_REF={{.FullCommit}}\"\n      - \"--build-arg=VCS_URL={{.GitURL}}\"\n\n  - image_templates:\n      - anchore/grype:{{.Tag}}-s390x\n      - ghcr.io/anchore/grype:{{.Tag}}-s390x\n    goarch: s390x\n    dockerfile: Dockerfile\n    use: buildx\n    build_flag_templates:\n      - \"--platform=linux/s390x\"\n      - \"--provenance=false\"\n      - \"--build-arg=BUILD_DATE={{.Date}}\"\n      - \"--build-arg=BUILD_VERSION={{.Version}}\"\n      - \"--build-arg=VCS_REF={{.FullCommit}}\"\n      - \"--build-arg=VCS_URL={{.GitURL}}\"\n\n  # nonroot images...\n  - image_templates:\n      - anchore/grype:{{.Tag}}-nonroot-amd64\n      - ghcr.io/anchore/grype:{{.Tag}}-nonroot-amd64\n    goarch: amd64\n    dockerfile: Dockerfile.nonroot\n    use: buildx\n    build_flag_templates:\n      - \"--platform=linux/amd64\"\n      - \"--provenance=false\"\n      - \"--build-arg=BUILD_DATE={{.Date}}\"\n      - \"--build-arg=BUILD_VERSION={{.Version}}\"\n      - \"--build-arg=VCS_REF={{.FullCommit}}\"\n      - \"--build-arg=VCS_URL={{.GitURL}}\"\n\n  - image_templates:\n      - anchore/grype:{{.Tag}}-nonroot-arm64v8\n      - ghcr.io/anchore/grype:{{.Tag}}-nonroot-arm64v8\n    goarch: arm64\n    dockerfile: Dockerfile.nonroot\n    use: buildx\n    build_flag_templates:\n      - \"--platform=linux/arm64/v8\"\n      - \"--provenance=false\"\n      - \"--build-arg=BUILD_DATE={{.Date}}\"\n      - \"--build-arg=BUILD_VERSION={{.Version}}\"\n      - \"--build-arg=VCS_REF={{.FullCommit}}\"\n      - \"--build-arg=VCS_URL={{.GitURL}}\"\n\n  - image_templates:\n      - anchore/grype:{{.Tag}}-nonroot-ppc64le\n      - ghcr.io/anchore/grype:{{.Tag}}-nonroot-ppc64le\n    goarch: ppc64le\n    dockerfile: Dockerfile.nonroot\n    use: buildx\n    build_flag_templates:\n      - \"--platform=linux/ppc64le\"\n      - \"--provenance=false\"\n      - \"--build-arg=BUILD_DATE={{.Date}}\"\n      - \"--build-arg=BUILD_VERSION={{.Version}}\"\n      - \"--build-arg=VCS_REF={{.FullCommit}}\"\n      - \"--build-arg=VCS_URL={{.GitURL}}\"\n\n  - image_templates:\n      - anchore/grype:{{.Tag}}-nonroot-s390x\n      - ghcr.io/anchore/grype:{{.Tag}}-nonroot-s390x\n    goarch: s390x\n    dockerfile: Dockerfile.nonroot\n    use: buildx\n    build_flag_templates:\n      - \"--platform=linux/s390x\"\n      - \"--provenance=false\"\n      - \"--build-arg=BUILD_DATE={{.Date}}\"\n      - \"--build-arg=BUILD_VERSION={{.Version}}\"\n      - \"--build-arg=VCS_REF={{.FullCommit}}\"\n      - \"--build-arg=VCS_URL={{.GitURL}}\"\n\n  # debug images...\n  - image_templates:\n      - anchore/grype:{{.Tag}}-debug-amd64\n      - ghcr.io/anchore/grype:{{.Tag}}-debug-amd64\n    goarch: amd64\n    dockerfile: Dockerfile.debug\n    use: buildx\n    build_flag_templates:\n      - \"--platform=linux/amd64\"\n      - \"--provenance=false\"\n      - \"--build-arg=BUILD_DATE={{.Date}}\"\n      - \"--build-arg=BUILD_VERSION={{.Version}}\"\n      - \"--build-arg=VCS_REF={{.FullCommit}}\"\n      - \"--build-arg=VCS_URL={{.GitURL}}\"\n\n  - image_templates:\n      - anchore/grype:{{.Tag}}-debug-arm64v8\n      - ghcr.io/anchore/grype:{{.Tag}}-debug-arm64v8\n    goarch: arm64\n    dockerfile: Dockerfile.debug\n    use: buildx\n    build_flag_templates:\n      - \"--platform=linux/arm64/v8\"\n      - \"--provenance=false\"\n      - \"--build-arg=BUILD_DATE={{.Date}}\"\n      - \"--build-arg=BUILD_VERSION={{.Version}}\"\n      - \"--build-arg=VCS_REF={{.FullCommit}}\"\n      - \"--build-arg=VCS_URL={{.GitURL}}\"\n\n  - image_templates:\n      - anchore/grype:{{.Tag}}-debug-ppc64le\n      - ghcr.io/anchore/grype:{{.Tag}}-debug-ppc64le\n    goarch: ppc64le\n    dockerfile: Dockerfile.debug\n    use: buildx\n    build_flag_templates:\n      - \"--platform=linux/ppc64le\"\n      - \"--provenance=false\"\n      - \"--build-arg=BUILD_DATE={{.Date}}\"\n      - \"--build-arg=BUILD_VERSION={{.Version}}\"\n      - \"--build-arg=VCS_REF={{.FullCommit}}\"\n      - \"--build-arg=VCS_URL={{.GitURL}}\"\n\n  - image_templates:\n      - anchore/grype:{{.Tag}}-debug-s390x\n      - ghcr.io/anchore/grype:{{.Tag}}-debug-s390x\n    goarch: s390x\n    dockerfile: Dockerfile.debug\n    use: buildx\n    build_flag_templates:\n      - \"--platform=linux/s390x\"\n      - \"--provenance=false\"\n      - \"--build-arg=BUILD_DATE={{.Date}}\"\n      - \"--build-arg=BUILD_VERSION={{.Version}}\"\n      - \"--build-arg=VCS_REF={{.FullCommit}}\"\n      - \"--build-arg=VCS_URL={{.GitURL}}\"\n\ndocker_manifests:\n  - name_template: anchore/grype:latest\n    image_templates:\n      - anchore/grype:{{.Tag}}-amd64\n      - anchore/grype:{{.Tag}}-arm64v8\n      - anchore/grype:{{.Tag}}-ppc64le\n      - anchore/grype:{{.Tag}}-s390x\n\n  - name_template: ghcr.io/anchore/grype:latest\n    image_templates:\n      - ghcr.io/anchore/grype:{{.Tag}}-amd64\n      - ghcr.io/anchore/grype:{{.Tag}}-arm64v8\n      - ghcr.io/anchore/grype:{{.Tag}}-ppc64le\n      - ghcr.io/anchore/grype:{{.Tag}}-s390x\n\n  - name_template: anchore/grype:{{.Tag}}\n    image_templates:\n      - anchore/grype:{{.Tag}}-amd64\n      - anchore/grype:{{.Tag}}-arm64v8\n      - anchore/grype:{{.Tag}}-ppc64le\n      - anchore/grype:{{.Tag}}-s390x\n\n  - name_template: ghcr.io/anchore/grype:{{.Tag}}\n    image_templates:\n      - ghcr.io/anchore/grype:{{.Tag}}-amd64\n      - ghcr.io/anchore/grype:{{.Tag}}-arm64v8\n      - ghcr.io/anchore/grype:{{.Tag}}-ppc64le\n      - ghcr.io/anchore/grype:{{.Tag}}-s390x\n\n  # nonroot images...\n  - name_template: anchore/grype:nonroot\n    image_templates:\n      - anchore/grype:{{.Tag}}-nonroot-amd64\n      - anchore/grype:{{.Tag}}-nonroot-arm64v8\n      - anchore/grype:{{.Tag}}-nonroot-ppc64le\n      - anchore/grype:{{.Tag}}-nonroot-s390x\n\n  - name_template: ghcr.io/anchore/grype:nonroot\n    image_templates:\n      - ghcr.io/anchore/grype:{{.Tag}}-nonroot-amd64\n      - ghcr.io/anchore/grype:{{.Tag}}-nonroot-arm64v8\n      - ghcr.io/anchore/grype:{{.Tag}}-nonroot-ppc64le\n      - ghcr.io/anchore/grype:{{.Tag}}-nonroot-s390x\n\n  - name_template: anchore/grype:{{.Tag}}-nonroot\n    image_templates:\n      - anchore/grype:{{.Tag}}-nonroot-amd64\n      - anchore/grype:{{.Tag}}-nonroot-arm64v8\n      - anchore/grype:{{.Tag}}-nonroot-ppc64le\n      - anchore/grype:{{.Tag}}-nonroot-s390x\n\n  - name_template: ghcr.io/anchore/grype:{{.Tag}}-nonroot\n    image_templates:\n      - ghcr.io/anchore/grype:{{.Tag}}-nonroot-amd64\n      - ghcr.io/anchore/grype:{{.Tag}}-nonroot-arm64v8\n      - ghcr.io/anchore/grype:{{.Tag}}-nonroot-ppc64le\n      - ghcr.io/anchore/grype:{{.Tag}}-nonroot-s390x\n\n  # debug images...\n  - name_template: anchore/grype:debug\n    image_templates:\n      - anchore/grype:{{.Tag}}-debug-amd64\n      - anchore/grype:{{.Tag}}-debug-arm64v8\n      - anchore/grype:{{.Tag}}-debug-ppc64le\n      - anchore/grype:{{.Tag}}-debug-s390x\n\n  - name_template: ghcr.io/anchore/grype:debug\n    image_templates:\n      - ghcr.io/anchore/grype:{{.Tag}}-debug-amd64\n      - ghcr.io/anchore/grype:{{.Tag}}-debug-arm64v8\n      - ghcr.io/anchore/grype:{{.Tag}}-debug-ppc64le\n      - ghcr.io/anchore/grype:{{.Tag}}-debug-s390x\n\n  - name_template: anchore/grype:{{.Tag}}-debug\n    image_templates:\n      - anchore/grype:{{.Tag}}-debug-amd64\n      - anchore/grype:{{.Tag}}-debug-arm64v8\n      - anchore/grype:{{.Tag}}-debug-ppc64le\n      - anchore/grype:{{.Tag}}-debug-s390x\n\n  - name_template: ghcr.io/anchore/grype:{{.Tag}}-debug\n    image_templates:\n      - ghcr.io/anchore/grype:{{.Tag}}-debug-amd64\n      - ghcr.io/anchore/grype:{{.Tag}}-debug-arm64v8\n      - ghcr.io/anchore/grype:{{.Tag}}-debug-ppc64le\n      - ghcr.io/anchore/grype:{{.Tag}}-debug-s390x\n\nsigns:\n  - cmd: .tool/cosign\n    signature: \"${artifact}.sig\"\n    certificate: \"${artifact}.pem\"\n    args:\n      - \"sign-blob\"\n      - \"--use-signing-config=false\"\n      - \"--oidc-issuer=https://token.actions.githubusercontent.com\"\n      - \"--output-certificate=${certificate}\"\n      - \"--output-signature=${signature}\"\n      - \"${artifact}\"\n      - \"--yes\"\n    artifacts: checksum\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Code of Conduct\n\nAll contributors for any Anchore project must follow the [Contributor Covenant Code of Conduct](https://oss.anchore.com/docs/contributing/code-of-conduct/).\n\n**TLDR:** Be kind, be respectful, and assume good intentions. We're all here to build great software together.\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing\n\nThank you for your interest in contributing to Grype!\n\nPlease see the [contribution guide](https://oss.anchore.com/docs/contributing/grype/) for development requirements and helpful tips to get started developing in the repo. For a deeper dive, please see the [architecture docs for grype](https://oss.anchore.com/docs/architecture/grype/) and the sister project [grype-db](https://oss.anchore.com/docs/architecture/grype-db/).\n\n**Have a question or need help?** Check out our [issues and discussions guide](https://oss.anchore.com/docs/contributing/issues-and-discussions/) to find the right place to start a conversation.\n\n**Ready to submit code?** Our [pull request guide](https://oss.anchore.com/docs/contributing/pull-requests/) covers everything from title conventions to the review process. Don't forget that ***all commits require a [sign-off](https://oss.anchore.com/docs/contributing/sign-off/)***.\n\n**Found a security issue?** Please do **not** open a public issue. Instead, see our [security policy](https://oss.anchore.com/docs/contributing/security/) for how to report vulnerabilities responsibly.\n\n**Want to help improve the docs?** Check out the [anchore/oss-docs](https://github.com/anchore/oss-docs) repository.\n"
  },
  {
    "path": "Dockerfile",
    "content": "FROM gcr.io/distroless/static-debian12:latest AS build\n\nFROM scratch\n# needed for version check HTTPS request\nCOPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt\n\n# create the /tmp dir, which is needed for image content cache\nWORKDIR /tmp\n\nCOPY grype /\n\nARG BUILD_DATE\nARG BUILD_VERSION\nARG VCS_REF\nARG VCS_URL\n\nLABEL org.opencontainers.image.created=$BUILD_DATE\nLABEL org.opencontainers.image.title=\"grype\"\nLABEL org.opencontainers.image.description=\"A vulnerability scanner for container images and filesystems\"\nLABEL org.opencontainers.image.source=$VCS_URL\nLABEL org.opencontainers.image.revision=$VCS_REF\nLABEL org.opencontainers.image.vendor=\"Anchore, Inc.\"\nLABEL org.opencontainers.image.version=$BUILD_VERSION\nLABEL org.opencontainers.image.licenses=\"Apache-2.0\"\nLABEL io.artifacthub.package.readme-url=\"https://raw.githubusercontent.com/anchore/grype/main/README.md\"\nLABEL io.artifacthub.package.logo-url=\"https://user-images.githubusercontent.com/5199289/136855393-d0a9eef9-ccf1-4e2b-9d7c-7aad16a567e5.png\"\nLABEL io.artifacthub.package.license=\"Apache-2.0\"\n\nENTRYPOINT [\"/grype\"]\n"
  },
  {
    "path": "Dockerfile.debug",
    "content": "FROM gcr.io/distroless/static-debian12:debug-nonroot\n\n# create the /tmp dir, which is needed for image content cache\nWORKDIR /tmp\n\nCOPY grype /\n\nARG BUILD_DATE\nARG BUILD_VERSION\nARG VCS_REF\nARG VCS_URL\n\nLABEL org.opencontainers.image.created=$BUILD_DATE\nLABEL org.opencontainers.image.title=\"grype\"\nLABEL org.opencontainers.image.description=\"A vulnerability scanner for container images and filesystems\"\nLABEL org.opencontainers.image.source=$VCS_URL\nLABEL org.opencontainers.image.revision=$VCS_REF\nLABEL org.opencontainers.image.vendor=\"Anchore, Inc.\"\nLABEL org.opencontainers.image.version=$BUILD_VERSION\nLABEL org.opencontainers.image.licenses=\"Apache-2.0\"\nLABEL io.artifacthub.package.readme-url=\"https://raw.githubusercontent.com/anchore/grype/main/README.md\"\nLABEL io.artifacthub.package.logo-url=\"https://user-images.githubusercontent.com/5199289/136855393-d0a9eef9-ccf1-4e2b-9d7c-7aad16a567e5.png\"\nLABEL io.artifacthub.package.license=\"Apache-2.0\"\n\nENTRYPOINT [\"/grype\"]\n"
  },
  {
    "path": "Dockerfile.nonroot",
    "content": "FROM gcr.io/distroless/static-debian12:nonroot\n\n# create the /tmp dir, which is needed for image content cache\nWORKDIR /tmp\n\nCOPY grype /\n\nARG BUILD_DATE\nARG BUILD_VERSION\nARG VCS_REF\nARG VCS_URL\n\nLABEL org.opencontainers.image.created=$BUILD_DATE\nLABEL org.opencontainers.image.title=\"grype\"\nLABEL org.opencontainers.image.description=\"A vulnerability scanner for container images and filesystems\"\nLABEL org.opencontainers.image.source=$VCS_URL\nLABEL org.opencontainers.image.revision=$VCS_REF\nLABEL org.opencontainers.image.vendor=\"Anchore, Inc.\"\nLABEL org.opencontainers.image.version=$BUILD_VERSION\nLABEL org.opencontainers.image.licenses=\"Apache-2.0\"\nLABEL io.artifacthub.package.readme-url=\"https://raw.githubusercontent.com/anchore/grype/main/README.md\"\nLABEL io.artifacthub.package.logo-url=\"https://user-images.githubusercontent.com/5199289/136855393-d0a9eef9-ccf1-4e2b-9d7c-7aad16a567e5.png\"\nLABEL io.artifacthub.package.license=\"Apache-2.0\"\n\nENTRYPOINT [\"/grype\"]\n"
  },
  {
    "path": "LICENSE",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "Makefile",
    "content": "TOOL_DIR = .tool\nBINNY = $(TOOL_DIR)/binny\nTASK = $(TOOL_DIR)/task\n\n.DEFAULT_GOAL := make-default\n\n## Bootstrapping targets #################################\n\n# note: we need to assume that binny and task have not already been installed\n$(BINNY):\n\t@mkdir -p $(TOOL_DIR)\n\t@curl -sSfL https://get.anchore.io/binny | sh -s -- -b $(TOOL_DIR)\n\n# note: we need to assume that binny and task have not already been installed\n.PHONY: task\n$(TASK) task: $(BINNY)\n\t@$(BINNY) install task -q\n\n.PHONY: ci-bootstrap-go\nci-bootstrap-go:\n\tgo mod download\n\n# this is a bootstrapping catch-all, where if the target doesn't exist, we'll ensure the tools are installed and then try again\n%:\n\tmake $(TASK)\n\t$(TASK) $@\n\n## Shim targets #################################\n\n.PHONY: make-default\nmake-default: $(TASK)\n\t@# run the default task in the taskfile\n\t@$(TASK)\n\n# for those of us that can't seem to kick the habit of typing `make ...` lets wrap the superior `task` tool\nTASKS := $(shell bash -c \"test -f $(TASK) && NO_COLOR=1 $(TASK) -l | grep '^\\* ' | cut -d' ' -f2 | tr -d ':' | tr '\\n' ' '\" ) $(shell bash -c \"test -f $(TASK) && NO_COLOR=1 $(TASK) -l | grep 'aliases:' | cut -d ':' -f 3 | tr '\\n' ' ' | tr -d ','\")\n\n.PHONY: $(TASKS)\n$(TASKS): $(TASK)\n\t@$(TASK) $@\n\nhelp: $(TASK)\n\t@$(TASK) -l\n"
  },
  {
    "path": "README.md",
    "content": "<p align=\"center\">\n    <img alt=\"Grype logo\" src=\"https://user-images.githubusercontent.com/5199289/136855393-d0a9eef9-ccf1-4e2b-9d7c-7aad16a567e5.png\" width=\"234\">\n</p>\n\n# Grype\n\n**A vulnerability scanner for container images and filesystems.**\n\n<p align=\"center\">\n    &nbsp;<a href=\"https://github.com/anchore/grype/actions?query=workflow%3A%22Static+Analysis+%2B+Unit+%2B+Integration%22\"><img src=\"https://github.com/anchore/grype/workflows/Static%20Analysis%20+%20Unit%20+%20Integration/badge.svg\" alt=\"Static Analysis + Unit + Integration\"></a>&nbsp;\n    &nbsp;<a href=\"https://github.com/anchore/grype/actions/workflows/validations.yaml\"><img src=\"https://github.com/anchore/grype/workflows/Validations/badge.svg\" alt=\"Validations\"></a>&nbsp;\n    &nbsp;<a href=\"https://goreportcard.com/report/github.com/anchore/grype\"><img src=\"https://goreportcard.com/badge/github.com/anchore/grype\" alt=\"Go Report Card\"></a>&nbsp;\n    &nbsp;<a href=\"https://github.com/anchore/grype/releases/latest\"><img src=\"https://img.shields.io/github/release/anchore/grype.svg\" alt=\"GitHub release\"></a>&nbsp;\n    &nbsp;<a href=\"https://github.com/anchore/grype\"><img src=\"https://img.shields.io/github/go-mod/go-version/anchore/grype.svg\" alt=\"GitHub go.mod Go version\"></a>&nbsp;\n    &nbsp;<a href=\"https://github.com/anchore/grype/blob/main/LICENSE\"><img src=\"https://img.shields.io/badge/License-Apache%202.0-blue.svg\" alt=\"License: Apache-2.0\"></a>&nbsp;\n    &nbsp;<a href=\"https://anchore.com/discourse\"><img src=\"https://img.shields.io/badge/Discourse-Join-blue?logo=discourse\" alt=\"Join our Discourse\"></a>&nbsp;\n    &nbsp;<a rel=\"me\" href=\"https://fosstodon.org/@grype\"><img src=\"https://img.shields.io/badge/Mastodon-Follow-blue?logoColor=white&logo=mastodon\" alt=\"Follow on Mastodon\"></a>&nbsp;\n</p>\n\n![grype-demo](https://user-images.githubusercontent.com/590471/90276236-9868f300-de31-11ea-8068-4268b6b68529.gif)\n\n## Features\n\n- Scan **container images**, **filesystems**, and **SBOMs** for known vulnerabilities (see the docs for a full list of [supported scan targets](https://oss.anchore.com/docs/guides/vulnerability/scan-targets/))\n- Supports major OS package ecosystems (Alpine, Debian, Ubuntu, RHEL, Oracle Linux, Amazon Linux, and [more](https://oss.anchore.com/docs/capabilities/all-os/))\n- Supports language-specific packages (Ruby, Java, JavaScript, Python, .NET, Go, PHP, Rust, and [more](https://oss.anchore.com/docs/capabilities/all-packages/))\n- Supports Docker, OCI, and [Singularity](https://github.com/sylabs/singularity) image formats\n- Threat & risk prioritization with **EPSS**, **KEV**, and **risk scoring** (see [interpreting the results docs](https://oss.anchore.com/docs/guides/vulnerability/interpreting-results/))\n- [OpenVEX](https://github.com/openvex) support for filtering and augmenting scan results\n\n> [!TIP]\n> New to Grype? Check out the [Getting Started guide](https://oss.anchore.com/docs/guides/vulnerability/getting-started/) for a walkthrough!\n\n## Installation\n\nThe quickest way to get up and going:\n```bash\ncurl -sSfL https://get.anchore.io/grype | sudo sh -s -- -b /usr/local/bin\n```\n\n> [!TIP]\n> See [Installation docs](https://oss.anchore.com/docs/installation/grype/) for more ways to get Grype, including Homebrew, Docker, Chocolatey, MacPorts, and more!\n\n## The basics\n\nScan a container image or directory for vulnerabilities:\n\n```bash\n# container image\ngrype alpine:latest\n\n# directory\ngrype ./my-project\n```\n\nScan an SBOM for even faster vulnerability detection:\n\n```bash\n# scan a Syft SBOM\ngrype sbom:./sbom.json\n\n# pipe an SBOM into Grype\ncat ./sbom.json | grype\n```\n\n> [!TIP]\n> Check out the [Getting Started guide](https://oss.anchore.com/docs/guides/vulnerability/getting-started/) to explore all of the capabilities and features.\n>\n> Want to know all of the ins-and-outs of Grype? Check out the [CLI docs](https://oss.anchore.com/docs/reference/grype/cli/) and [configuration docs](https://oss.anchore.com/docs/reference/grype/configuration/).\n\n## Contributing\n\nWe encourage users to help make these tools better by [submitting issues](https://github.com/anchore/grype/issues) when you find a bug or want a new feature.\nCheck out our [contributing overview](https://oss.anchore.com/docs/contributing/) and [developer-specific documentation](https://oss.anchore.com/docs/contributing/grype/) if you are interested in providing code contributions.\n\n<p xmlns:cc=\"http://creativecommons.org/ns#\" xmlns:dct=\"http://purl.org/dc/terms/\">\n  Grype development is sponsored by <a href=\"https://anchore.com/\">Anchore</a>, and is released under the <a href=\"https://github.com/anchore/grype?tab=Apache-2.0-1-ov-file\">Apache-2.0 License</a>.\n  The <a property=\"dct:title\" rel=\"cc:attributionURL\" href=\"https://anchore.com/wp-content/uploads/2024/11/grype-logo.svg\">Grype logo</a> by <a rel=\"cc:attributionURL dct:creator\" property=\"cc:attributionName\" href=\"https://anchore.com/\">Anchore</a> is licensed under <a href=\"https://creativecommons.org/licenses/by/4.0/\" target=\"_blank\" rel=\"license noopener noreferrer\" style=\"display:inline-block;\">CC BY 4.0<img style=\"height:22px!important;margin-left:3px;vertical-align:text-bottom;\" src=\"https://mirrors.creativecommons.org/presskit/icons/cc.svg\" alt=\"\"><img style=\"height:22px!important;margin-left:3px;vertical-align:text-bottom;\" src=\"https://mirrors.creativecommons.org/presskit/icons/by.svg\" alt=\"\"></a>\n</p>\n\nFor commercial support options with Syft or Grype, please [contact Anchore](https://get.anchore.com/contact/).\n\n## Come talk to us!\n\nThe Grype Team holds regular community meetings online. All are welcome to join to bring topics for discussion.\n- Check the [calendar](https://calendar.google.com/calendar/u/0/r?cid=Y182OTM4dGt0MjRtajI0NnNzOThiaGtnM29qNEBncm91cC5jYWxlbmRhci5nb29nbGUuY29t) for the next meeting date.\n- Add items to the [agenda](https://docs.google.com/document/d/1ZtSAa6fj2a6KRWviTn3WoJm09edvrNUp4Iz_dOjjyY8/edit?usp=sharing) (join [this group](https://groups.google.com/g/anchore-oss-community) for write access to the [agenda](https://docs.google.com/document/d/1ZtSAa6fj2a6KRWviTn3WoJm09edvrNUp4Iz_dOjjyY8/edit?usp=sharing))\n- See you there!\n"
  },
  {
    "path": "RELEASE.md",
    "content": "# Release\n\nA release of grype comprises:\n- a new semver git tag from the current tip of the main branch\n- a new [github release](https://github.com/anchore/grype/releases) with a changelog and archived binary assets\n- docker images published to `ghcr.io` and `dockerhub`, including multi architecture images + manifest\n- [`anchore/homebrew-grype`](https://github.com/anchore/homebrew-grype) tap updated to point to assets in the latest github release\n\nIdeally releasing should be done often with small increments when possible. Unless a\nbreaking change is blocking the release, or no fixes/features have been merged, a good\ntarget release cadence is between every 1 or 2 weeks.\n\n\n## Creating a release\n\nThis release process itself should be as automated as possible, and has only a few steps:\n\n1. **Trigger a new release with `make release`**. At this point you'll see a preview\n   changelog in the terminal. If you're happy with the changelog, press `y` to continue, otherwise\n   you can abort and adjust the labels on the PRs and issues to be included in the release and\n   re-run the release trigger command.\n\n1. A release admin must approve the release on the GitHub Actions [release pipeline](https://github.com/anchore/grype/actions/workflows/release.yaml) run page.\n   Once approved, the release pipeline will generate all assets and publish a GitHub Release.\n\n\n## Retracting a release\n\nIf a release is found to be problematic, it can be retracted with the following steps:\n\n- Deleting the GitHub Release\n- Untag the docker images in the `ghcr.io` and `docker.io` registries\n- Revert the brew formula in [`anchore/homebrew-grype`](https://github.com/anchore/homebrew-grype) to point to the previous release\n- Add a new `retract` entry in the go.mod for the versioned release\n\n**Note**: do not delete release tags from the git repository since there may already be references to the release\nin the go proxy, which will cause confusion when trying to reuse the tag later (the H1 hash will not match and there\nwill be a warning when users try to pull the new release).\n\n"
  },
  {
    "path": "SECURITY.md",
    "content": "# Security Policy\n\n## Supported Versions\n\nSecurity updates are applied only to the most recent release, try to always be up to date.\n\n## Reporting a Vulnerability\n\nTo report a security issue, please email\n[security@anchore.com](mailto:security@anchore.com)\nwith a description of the issue, the steps you took to create the issue,\naffected versions, and, if known, mitigations for the issue.\n\nAll support will be made on a best effort basis, so please indicate the \"urgency level\" of the vulnerability as Critical, High, Medium or Low.\n\nFor more details, see our [security policy documentation](https://oss.anchore.com/docs/contributing/security/).\n"
  },
  {
    "path": "Taskfile.yaml",
    "content": "version: \"3\"\nvars:\n  OWNER: anchore\n  PROJECT: grype\n\n  # static file dirs\n  TOOL_DIR: .tool\n  TMP_DIR: .tmp\n\n  # used for changelog generation\n  CHANGELOG: CHANGELOG.md\n  NEXT_VERSION: VERSION\n\n  # used for snapshot builds\n  OS:\n    sh: uname -s | tr '[:upper:]' '[:lower:]'\n  ARCH:\n    sh: |\n      [ \"$(uname -m)\" = \"x86_64\" ] && echo \"amd64_v1\" || echo $(uname -m)\n  PROJECT_ROOT:\n    sh: echo $PWD\n  # note: the snapshot dir must be a relative path starting with ./\n  SNAPSHOT_DIR: ./snapshot\n  SNAPSHOT_BIN: \"{{ .PROJECT_ROOT }}/{{ .SNAPSHOT_DIR }}/{{ .OS }}-build_{{ .OS }}_{{ .ARCH }}/{{ .PROJECT }}\"\n  SNAPSHOT_CMD: \"{{ .TOOL_DIR }}/goreleaser release --config {{ .TMP_DIR }}/goreleaser.yaml --clean --snapshot --skip=publish --skip=sign\"\n  BUILD_CMD:    \"{{ .TOOL_DIR }}/goreleaser build   --config {{ .TMP_DIR }}/goreleaser.yaml --clean --snapshot --single-target\"\n  RELEASE_CMD:  \"{{ .TOOL_DIR }}/goreleaser release --clean --release-notes {{ .CHANGELOG }}\"\n  VERSION:\n    sh: git describe --dirty --always --tags\n\n  # used for install and acceptance testing\n  COMPARE_DIR: ./test/compare\n  COMPARE_TEST_IMAGE: centos:8.2.2004\n\nenv:\n  SYFT_CHECK_FOR_APP_UPDATE: false\n  GRYPE_CHECK_FOR_APP_UPDATE: false\n\ntasks:\n\n  ## High-level tasks #################################\n\n  # note: the default task should not run levels of test, only build the project.\n  default:\n    desc: Build the project\n    cmds:\n      - task: build\n\n  validate:\n    desc: Run all validation tasks\n    aliases:\n      - pr-validations\n      - validations\n    cmds:\n      - task: static-analysis\n      - task: test\n      - task: install-test\n\n  static-analysis:\n    desc: Run all static analysis tasks\n    cmds:\n      - task: check-go-mod-tidy\n      - task: check-licenses\n      - task: lint\n      - task: check-json-schema-drift\n      - task: check-db-schema-drift\n# TODO: while developing v6, we need to disable this check (since v5 and v6 are imported in the same codebase)\n#      - task: validate-grype-db-schema\n\n  test:\n    desc: Run all levels of test\n    cmds:\n      - task: unit\n      - task: integration\n      - task: cli\n\n  ## Bootstrap tasks #################################\n\n  binny:\n    internal: true\n    # desc: Get the binny tool\n    generates:\n      - \"{{ .TOOL_DIR }}/binny\"\n    status:\n      - \"test -f {{ .TOOL_DIR }}/binny\"\n    cmd: \"curl -sSfL https://get.anchore.io/binny | sh -s -- -b .tool\"\n    silent: true\n\n  tools:\n    desc: Install all tools needed for CI and local development\n    deps: [binny]\n    aliases:\n      - bootstrap\n    generates:\n      - \".binny.yaml\"\n      - \"{{ .TOOL_DIR }}/*\"\n    status:\n      - \"{{ .TOOL_DIR }}/binny check -v\"\n    cmd: \"{{ .TOOL_DIR }}/binny install -v\"\n    silent: true\n\n  update-tools:\n    desc: Update pinned versions of all tools to their latest available versions\n    deps: [binny]\n    generates:\n      - \".binny.yaml\"\n      - \"{{ .TOOL_DIR }}/*\"\n    cmd: \"{{ .TOOL_DIR }}/binny update -v\"\n    silent: true\n\n  update-quality-gate-db:\n    desc: Update pinned version of quality gate database\n    cmds:\n      - cmd: \"go run cmd/grype/main.go db list -o json | jq -r '\\\"https://grype.anchore.io/databases/v6/\\\" + .[0].path' > test/quality/test-db\"\n        silent: true\n\n  list-tools:\n    desc: List all tools needed for CI and local development\n    deps: [binny]\n    cmd: \"{{ .TOOL_DIR }}/binny list\"\n    silent: true\n\n  list-tool-updates:\n    desc: List all tools that are not up to date relative to the binny config\n    deps: [binny]\n    cmd: \"{{ .TOOL_DIR }}/binny list --updates\"\n    silent: true\n\n  tmpdir:\n    silent: true\n    generates:\n      - \"{{ .TMP_DIR }}\"\n    cmd: \"mkdir -p {{ .TMP_DIR }}\"\n\n  ## Static analysis tasks #################################\n\n  format:\n    desc: Auto-format all source code\n    deps: [tools]\n    cmds:\n      - gofmt -w -s .\n      - \"{{ .TOOL_DIR }}/gosimports -local github.com/anchore -w .\"\n      - go mod tidy\n\n  lint-fix:\n    desc: Auto-format all source code + run golangci lint fixers\n    deps: [tools]\n    cmds:\n      - task: format\n      - \"{{ .TOOL_DIR }}/golangci-lint run --tests=false --fix\"\n\n  lint:\n    desc: Run gofmt + golangci lint checks\n    vars:\n      BAD_FMT_FILES:\n        sh: gofmt -l -s .\n      BAD_FILE_NAMES:\n        sh: \"find . | grep -e ':' | grep -v -e 'test/quality/.yardstick' -e 'test/quality/vulnerability-match-labels' || true\"\n    deps: [tools]\n    cmds:\n      # ensure there are no go fmt differences\n      - cmd: 'test -z \"{{ .BAD_FMT_FILES }}\" || (echo \"files with gofmt issues: [{{ .BAD_FMT_FILES }}]\"; exit 1)'\n        silent: true\n      # ensure there are no files with \":\" in it (a known back case in the go ecosystem)\n      - cmd: 'test -z \"{{ .BAD_FILE_NAMES }}\" || (echo \"files with bad names: [{{ .BAD_FILE_NAMES }}]\"; exit 1)'\n        silent: true\n      # run linting\n      - \"{{ .TOOL_DIR }}/golangci-lint run --tests=false\"\n\n  check-licenses:\n    # desc: Ensure transitive dependencies are compliant with the current license policy\n    deps: [tools]\n    cmd: \"{{ .TOOL_DIR }}/bouncer check ./...\"\n\n  check-go-mod-tidy:\n    # desc: Ensure go.mod and go.sum are up to date\n    cmds:\n      - cmd: .github/scripts/go-mod-tidy-check.sh && echo \"go.mod and go.sum are tidy!\"\n        silent: true\n\n  check-json-schema-drift:\n    desc: Ensure there is no drift between the JSON schema and the code\n    cmds:\n      - .github/scripts/json-schema-drift-check.sh\n\n  check-db-schema-drift:\n    desc: Ensure there is no drift between the database blob schemas and the code\n    cmds:\n      - .github/scripts/db-schema-drift-check.sh\n\n  validate-grype-db-schema:\n    desc: Ensure the codebase is only referencing a single grype-db schema version (multiple is not allowed)\n    cmds:\n      - python test/validate-grype-db-schema.py\n\n\n  ## Testing tasks #################################\n\n  unit:\n    desc: Run unit tests\n    deps:\n      - tmpdir\n    vars:\n      TEST_PKGS:\n        sh: \"go list ./... | grep -v {{ .OWNER }}/{{ .PROJECT }}/test | grep -v {{ .OWNER }}/{{ .PROJECT }}/internal/test | tr '\\n' ' '\"\n\n      # unit test coverage threshold (in % coverage)\n      COVERAGE_THRESHOLD: 47\n    cmds:\n      - \"go test -coverprofile {{ .TMP_DIR }}/unit-coverage-details.txt {{ .TEST_PKGS }}\"\n      - cmd: \".github/scripts/coverage.py {{ .COVERAGE_THRESHOLD }} {{ .TMP_DIR }}/unit-coverage-details.txt\"\n        silent: true\n\n  integration:\n    desc: Run integration tests\n    cmds:\n      - \"go test -v ./test/integration\"\n      # update database outside race detector, since doing so with\n      # race detector on is very slow\n      - \"go run cmd/{{ .PROJECT }}/main.go db update\"\n      # exercise most of the CLI with the data race detector enabled\n      - \"go run -race cmd/{{ .PROJECT }}/main.go alpine:latest\"\n\n  _ensure-snapshot:\n    # Ensure the snapshot binary is available, building only if needed.\n    # In CI, set GRYPE_SNAPSHOT_PREBUILT=true to skip the rebuild when the binary was restored from cache.\n    # Locally, this falls through to the snapshot task which uses checksum-based staleness detection\n    # so that code changes always produce a fresh binary.\n    internal: true\n    status:\n      - test -n \"$GRYPE_SNAPSHOT_PREBUILT\" -a -f \"{{ .SNAPSHOT_BIN }}\"\n    cmds:\n      - task: snapshot\n\n  cli:\n    desc: Run CLI tests\n    deps: [tools, _ensure-snapshot]\n    sources:\n      - \"{{ .SNAPSHOT_BIN }}\"\n      - ./test/cli/**\n      - ./**/*.go\n    cmds:\n      - cmd: \"echo 'testing binary: {{ .SNAPSHOT_BIN }}'\"\n        silent: true\n\n      - cmd: \"test -f {{ .SNAPSHOT_BIN }} || (find {{ .SNAPSHOT_DIR }} && echo '\\nno snapshot found' && false)\"\n        silent: true\n\n      - \"go test -count=1 -timeout=15m -v ./test/cli\"\n\n  quality:\n    desc: Run quality tests\n    cmds:\n      - \"cd test/quality && make\"\n\n\n  ## Test-fixture-related targets #################################\n\n  fingerprints:\n    desc: Generate test fixture fingerprints\n    generates:\n      - test/integration/testdata/cache.fingerprint\n      - test/install/cache.fingerprint\n      - test/cli/testdata/cache.fingerprint\n    cmds:\n      # for IMAGE integration test fixtures\n      - \"cd test/integration/testdata && make cache.fingerprint\"\n      # for INSTALL integration test fixtures\n      - \"cd test/install && make cache.fingerprint\"\n      # for CLI test fixtures\n      - \"cd test/cli/testdata && make cache.fingerprint\"\n\n  show-test-image-cache:\n    silent: true\n    cmds:\n      - \"echo '\\nDocker daemon cache:'\"\n      - \"docker images --format '{{`{{.ID}}`}} {{`{{.Repository}}`}}:{{`{{.Tag}}`}}' | grep stereoscope-fixture- | sort\"\n      - \"echo '\\nTar cache:'\"\n      - 'find . -type f -wholename \"**/testdata/snapshot/*\" | sort'\n\n\n  ## install.sh testing targets #################################\n\n  install-test:\n    cmds:\n      - \"cd test/install && make\"\n\n  install-test-cache-save:\n    cmds:\n      - \"cd test/install && make save\"\n\n  install-test-cache-load:\n    cmds:\n      - \"cd test/install && make load\"\n\n  install-test-ci-mac:\n    cmds:\n      - \"cd test/install && make ci-test-mac\"\n\n  generate-compare-file:\n    cmd: \"go run ./cmd/{{ .PROJECT }} {{ .COMPARE_TEST_IMAGE }} -o json > {{ .COMPARE_DIR }}/testdata/acceptance-{{ .COMPARE_TEST_IMAGE }}.json\"\n\n  compare-mac:\n    deps: [tmpdir]\n    cmd: |\n      {{ .COMPARE_DIR }}/mac.sh \\\n        {{ .SNAPSHOT_DIR }} \\\n        {{ .COMPARE_DIR }} \\\n        {{ .COMPARE_TEST_IMAGE }} \\\n        {{ .TMP_DIR }}\n\n  compare-linux:\n    cmds:\n      - task: compare-test-deb-package-install\n      - task: compare-test-rpm-package-install\n\n  compare-test-deb-package-install:\n    deps: [tmpdir]\n    cmd: |\n      {{ .COMPARE_DIR }}/deb.sh \\\n        {{ .SNAPSHOT_DIR }} \\\n        {{ .COMPARE_DIR }} \\\n        {{ .COMPARE_TEST_IMAGE }} \\\n        {{ .TMP_DIR }}\n\n  compare-test-rpm-package-install:\n    deps: [tmpdir]\n    cmd: |\n      {{ .COMPARE_DIR }}/rpm.sh \\\n        {{ .SNAPSHOT_DIR }} \\\n        {{ .COMPARE_DIR }} \\\n        {{ .COMPARE_TEST_IMAGE }} \\\n        {{ .TMP_DIR }}\n\n\n  ## Code and data generation targets #################################\n\n  generate:\n    desc: Run code and data generation tasks\n    cmds:\n      - task: generate-json-schema\n      - task: generate-db-schema\n      - task: generate-codename-data\n\n  generate-json-schema:\n    desc: Generate a new JSON schema\n    cmds:\n      # re-generate package metadata\n      - \"cd grype/internal && go generate\"\n      # generate the JSON schema for the CLI output\n      - \"cd cmd/grype/cli/commands/internal/jsonschema && go run .\"\n\n  generate-db-schema:\n    desc: Generate database blob JSON schemas\n    cmds:\n      # generate the JSON schema for database blob types\n      - \"cd grype/db/v6/schema && go run .\"\n\n  generate-codename-data:\n    desc: Generate OS codename lookup data\n    cmds:\n      - \"go generate ./grype/db\"\n      - task: format\n\n\n  ## Build-related targets #################################\n\n  build:\n    desc: Build the project\n    deps: [tools, tmpdir]\n    generates:\n      - \"{{ .PROJECT }}\"\n    cmds:\n      - silent: true\n        cmd: |\n          echo \"dist: {{ .SNAPSHOT_DIR }}\" > {{ .TMP_DIR }}/goreleaser.yaml\n          cat .goreleaser.yaml >> {{ .TMP_DIR }}/goreleaser.yaml\n\n      - \"{{ .BUILD_CMD }}\"\n\n  snapshot:\n    desc: Create a snapshot release\n    aliases:\n      - build\n    deps: [tools, tmpdir]\n    sources:\n      - cmd/**/*.go\n      - \"{{ .PROJECT }}/**/*.go\"\n      - internal/**/*.go\n    method: checksum\n    generates:\n      - \"{{ .SNAPSHOT_BIN }}\"\n    cmds:\n      - silent: true\n        cmd: |\n          echo \"dist: {{ .SNAPSHOT_DIR }}\" > {{ .TMP_DIR }}/goreleaser.yaml\n          cat .goreleaser.yaml >> {{ .TMP_DIR }}/goreleaser.yaml\n\n      - \"{{ .SNAPSHOT_CMD }}\"\n\n  changelog:\n    desc: Generate a changelog\n    deps: [tools]\n    generates:\n      - \"{{ .CHANGELOG }}\"\n      - \"{{ .NEXT_VERSION }}\"\n    cmds:\n      - \"{{ .TOOL_DIR }}/chronicle -vv -n --version-file {{ .NEXT_VERSION }} > {{ .CHANGELOG }}\"\n      - \"{{ .TOOL_DIR }}/glow -w 0 {{ .CHANGELOG }}\"\n\n\n  ## Release targets #################################\n\n  release:\n    desc: Create a release\n    interactive: true\n    deps: [tools]\n    cmds:\n      - cmd: .github/scripts/trigger-release.sh\n        silent: true\n\n\n  ## CI-only targets #################################\n\n  ci-check:\n    # desc: \"[CI only] Are you in CI?\"\n    cmds:\n      - cmd: .github/scripts/ci-check.sh\n        silent: true\n\n  ci-release:\n    # desc: \"[CI only] Create a release\"\n    deps: [tools]\n    cmds:\n      - task: ci-check\n      - \"{{ .TOOL_DIR }}/chronicle -vvv > CHANGELOG.md\"\n      - cmd: \"cat CHANGELOG.md\"\n        silent: true\n      - \"{{ .RELEASE_CMD }}\"\n\n  ci-validate-test-config:\n    # desc: \"[CI only] Ensure the update URL is not overridden (not pointing to staging)\"\n    silent: true\n    cmd: |\n      bash -c '\\\n        grep -q \"update-url\" test/grype-test-config.yaml; \\\n        if [ $? -eq 0 ]; then \\\n          echo \"Found \\\"update-url\\\" in CLI testing config. Cannot release if previous CLI testing did not use production (default) values\"; \\\n        else\n          echo \"Test configuration valid\"\n        fi'\n\n\n  ## Cleanup targets #################################\n\n  clean-snapshot:\n    desc: Remove any snapshot builds\n    cmds:\n      - \"rm -rf {{ .SNAPSHOT_DIR }}\"\n      - \"rm -rf {{ .TMP_DIR }}/goreleaser.yaml\"\n\n  clean-cache:\n    desc: Remove all docker cache and local image tar cache\n    cmds:\n      - 'find . -type f -wholename \"**/testdata/cache/stereoscope-fixture-*.tar\" -delete'\n      - \"docker images --format '{{`{{.ID}}`}} {{`{{.Repository}}`}}' | grep stereoscope-fixture- | awk '{print $$1}' | uniq | xargs -r docker rmi --force\"\n"
  },
  {
    "path": "artifacthub-repo.yml",
    "content": "# See documentation here: https://github.com/artifacthub/hub/blob/v1.6.0/docs/metadata/artifacthub-repo.yml\nrepositoryID: 8ff93ef9-2a75-40b6-945e-514e936fe9bb\nowners:\n  - name: wagoodman\n    email: wagoodman@gmail.com\n"
  },
  {
    "path": "cmd/grype/cli/cli.go",
    "content": "package cli\n\nimport (\n\t\"errors\"\n\t\"os\"\n\t\"runtime/debug\"\n\t\"strings\"\n\n\t\"github.com/charmbracelet/lipgloss\"\n\t\"github.com/muesli/termenv\"\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/anchore/clio\"\n\t\"github.com/anchore/grype/cmd/grype/cli/commands\"\n\tgrypeHandler \"github.com/anchore/grype/cmd/grype/cli/ui\"\n\t\"github.com/anchore/grype/cmd/grype/internal/ui\"\n\tv6 \"github.com/anchore/grype/grype/db/v6\"\n\t\"github.com/anchore/grype/grype/grypeerr\"\n\t\"github.com/anchore/grype/internal/bus\"\n\t\"github.com/anchore/grype/internal/log\"\n\t\"github.com/anchore/grype/internal/redact\"\n\t\"github.com/anchore/stereoscope\"\n\tsyftHandler \"github.com/anchore/syft/cmd/syft/cli/ui\"\n\t\"github.com/anchore/syft/syft\"\n)\n\nfunc Application(id clio.Identification) clio.Application {\n\tapp, _ := create(id)\n\treturn app\n}\n\nfunc Command(id clio.Identification) *cobra.Command {\n\t_, cmd := create(id)\n\treturn cmd\n}\n\nfunc SetupConfig(id clio.Identification) *clio.SetupConfig {\n\treturn clio.NewSetupConfig(id).\n\t\tWithGlobalConfigFlag().   // add persistent -c <path> for reading an application config from\n\t\tWithGlobalLoggingFlags(). // add persistent -v and -q flags tied to the logging config\n\t\tWithConfigInRootHelp().   // --help on the root command renders the full application config in the help text\n\t\tWithUIConstructor(\n\t\t\t// select a UI based on the logging configuration and state of stdin (if stdin is a tty)\n\t\t\tfunc(cfg clio.Config) (*clio.UICollection, error) {\n\t\t\t\t// remove CI var from consideration when determining if we should use the UI\n\t\t\t\tlipgloss.SetDefaultRenderer(lipgloss.NewRenderer(os.Stdout, termenv.WithEnvironment(environWithoutCI{})))\n\n\t\t\t\t// setup the UIs\n\t\t\t\tnoUI := ui.None(cfg.Log.Quiet)\n\t\t\t\tif !cfg.Log.AllowUI(os.Stdin) || cfg.Log.Quiet {\n\t\t\t\t\treturn clio.NewUICollection(noUI), nil\n\t\t\t\t}\n\n\t\t\t\treturn clio.NewUICollection(\n\t\t\t\t\tui.New(cfg.Log.Quiet,\n\t\t\t\t\t\tgrypeHandler.New(grypeHandler.DefaultHandlerConfig()),\n\t\t\t\t\t\tsyftHandler.New(syftHandler.DefaultHandlerConfig()),\n\t\t\t\t\t),\n\t\t\t\t\tnoUI,\n\t\t\t\t), nil\n\t\t\t},\n\t\t).\n\t\tWithInitializers(\n\t\t\tfunc(state *clio.State) error {\n\t\t\t\t// clio is setting up and providing the bus, redact store, and logger to the application. Once loaded,\n\t\t\t\t// we can hoist them into the internal packages for global use.\n\t\t\t\tstereoscope.SetBus(state.Bus)\n\t\t\t\tsyft.SetBus(state.Bus)\n\t\t\t\tbus.Set(state.Bus)\n\n\t\t\t\tredact.Set(state.RedactStore)\n\n\t\t\t\tlog.Set(state.Logger)\n\t\t\t\tsyft.SetLogger(state.Logger.Nested(\"from\", \"syft\"))\n\t\t\t\tstereoscope.SetLogger(state.Logger.Nested(\"from\", \"stereoscope\"))\n\n\t\t\t\treturn nil\n\t\t\t},\n\t\t).\n\t\tWithPostRuns(func(_ *clio.State, _ error) {\n\t\t\tstereoscope.Cleanup() //nolint:staticcheck\n\t\t}).\n\t\tWithMapExitCode(func(err error) int {\n\t\t\t// return exit code 2 to indicate when a vulnerability severity is discovered\n\t\t\t// that is equal or above the given --fail-on severity value.\n\t\t\tif errors.Is(err, grypeerr.ErrAboveSeverityThreshold) {\n\t\t\t\treturn 2\n\t\t\t}\n\t\t\t// return exit code 100 to indicate a DB upgrade is available (cmd: db check).\n\t\t\tif errors.Is(err, grypeerr.ErrDBUpgradeAvailable) {\n\t\t\t\treturn 100\n\t\t\t}\n\t\t\treturn 1\n\t\t})\n}\n\nfunc create(id clio.Identification) (clio.Application, *cobra.Command) {\n\tclioCfg := SetupConfig(id)\n\n\tapp := clio.New(*clioCfg)\n\n\trootCmd := commands.Root(app)\n\n\t// add sub-commands\n\trootCmd.AddCommand(\n\t\tcommands.DB(app),\n\t\tcommands.Completion(app),\n\t\tcommands.Explain(app),\n\t\tclio.VersionCommand(id, syftVersion, dbVersion),\n\t\tclio.ConfigCommand(app, nil),\n\t)\n\n\treturn app, rootCmd\n}\n\nfunc syftVersion() (string, any) {\n\tbuildInfo, ok := debug.ReadBuildInfo()\n\tif !ok {\n\t\tlog.Debug(\"unable to find the buildinfo section of the binary (syft version is unknown)\")\n\t\treturn \"\", \"\"\n\t}\n\n\tfor _, d := range buildInfo.Deps {\n\t\tif d.Path == \"github.com/anchore/syft\" {\n\t\t\treturn \"Syft Version\", d.Version\n\t\t}\n\t}\n\n\tlog.Debug(\"unable to find 'github.com/anchore/syft' from the buildinfo section of the binary\")\n\treturn \"\", \"\"\n}\n\nfunc dbVersion() (string, any) {\n\treturn \"Supported DB Schema\", v6.ModelVersion\n}\n\ntype environWithoutCI struct {\n}\n\nfunc (e environWithoutCI) Environ() []string {\n\tvar out []string\n\tfor _, s := range os.Environ() {\n\t\tif strings.HasPrefix(s, \"CI=\") {\n\t\t\tcontinue\n\t\t}\n\t\tout = append(out, s)\n\t}\n\treturn out\n}\n\nfunc (e environWithoutCI) Getenv(s string) string {\n\tif s == \"CI\" {\n\t\treturn \"\"\n\t}\n\treturn os.Getenv(s)\n}\n"
  },
  {
    "path": "cmd/grype/cli/cli_test.go",
    "content": "package cli\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/clio\"\n)\n\nfunc Test_Command(t *testing.T) {\n\troot := Command(clio.Identification{\n\t\tName:    \"test-name\",\n\t\tVersion: \"test-version\",\n\t})\n\n\trequire.Equal(t, root.Name(), \"test-name\")\n\trequire.NotEmpty(t, root.Commands())\n}\n"
  },
  {
    "path": "cmd/grype/cli/commands/completion.go",
    "content": "package commands\n\nimport (\n\t\"context\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/docker/docker/api/types/filters\"\n\t\"github.com/docker/docker/api/types/image\"\n\t\"github.com/docker/docker/client\"\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/anchore/clio\"\n)\n\n// Completion returns a command to provide completion to various terminal shells\nfunc Completion(app clio.Application) *cobra.Command {\n\treturn &cobra.Command{\n\t\tUse:   \"completion [bash|zsh|fish]\",\n\t\tShort: \"Generate a shell completion for Grype (listing local docker images)\",\n\t\tLong: `To load completions (docker image list):\n\nBash:\n\n$ source <(grype completion bash)\n\n# To load completions for each session, execute once:\nLinux:\n  $ grype completion bash > /etc/bash_completion.d/grype\nMacOS:\n  $ grype completion bash > /usr/local/etc/bash_completion.d/grype\n\nZsh:\n\n# If shell completion is not already enabled in your environment you will need\n# to enable it.  You can execute the following once:\n\n$ echo \"autoload -U compinit; compinit\" >> ~/.zshrc\n\n# To load completions for each session, execute once:\n$ grype completion zsh > \"${fpath[1]}/_grype\"\n\n# You will need to start a new shell for this setup to take effect.\n\nFish:\n\n$ grype completion fish | source\n\n# To load completions for each session, execute once:\n$ grype completion fish > ~/.config/fish/completions/grype.fish\n`,\n\t\tDisableFlagsInUseLine: true,\n\t\tValidArgs:             []string{\"bash\", \"fish\", \"zsh\"},\n\t\tPreRunE:               disableUI(app),\n\t\tArgs:                  cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\tvar err error\n\t\t\tswitch args[0] {\n\t\t\tcase \"zsh\":\n\t\t\t\terr = cmd.Root().GenZshCompletion(os.Stdout)\n\t\t\tcase \"bash\":\n\t\t\t\terr = cmd.Root().GenBashCompletion(os.Stdout)\n\t\t\tcase \"fish\":\n\t\t\t\terr = cmd.Root().GenFishCompletion(os.Stdout, true)\n\t\t\t}\n\t\t\treturn err\n\t\t},\n\t}\n}\n\nfunc listLocalDockerImages(prefix string) ([]string, error) {\n\tvar repoTags = make([]string, 0)\n\tctx := context.Background()\n\tcli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())\n\tif err != nil {\n\t\treturn repoTags, err\n\t}\n\n\t// Only want to return tagged images\n\timageListArgs := filters.NewArgs()\n\timageListArgs.Add(\"dangling\", \"false\")\n\timages, err := cli.ImageList(ctx, image.ListOptions{All: false, Filters: imageListArgs})\n\tif err != nil {\n\t\treturn repoTags, err\n\t}\n\n\tfor _, image := range images {\n\t\t// image may have multiple tags\n\t\tfor _, tag := range image.RepoTags {\n\t\t\tif strings.HasPrefix(tag, prefix) {\n\t\t\t\trepoTags = append(repoTags, tag)\n\t\t\t}\n\t\t}\n\t}\n\treturn repoTags, nil\n}\n\nfunc dockerImageValidArgsFunction(_ *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) {\n\t// Since we use ValidArgsFunction, Cobra will call this AFTER having parsed all flags and arguments provided\n\tdockerImageRepoTags, err := listLocalDockerImages(toComplete)\n\tif err != nil {\n\t\t// Indicates that an error occurred and completions should be ignored\n\t\treturn []string{\"completion failed\"}, cobra.ShellCompDirectiveError\n\t}\n\tif len(dockerImageRepoTags) == 0 {\n\t\treturn []string{\"no docker images found\"}, cobra.ShellCompDirectiveError\n\t}\n\t// ShellCompDirectiveDefault indicates that the shell will perform its default behavior after completions have\n\t// been provided (without implying other possible directives)\n\treturn dockerImageRepoTags, cobra.ShellCompDirectiveDefault\n}\n"
  },
  {
    "path": "cmd/grype/cli/commands/db.go",
    "content": "package commands\n\nimport (\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/anchore/clio\"\n)\n\nconst (\n\tjsonOutputFormat  = \"json\"\n\ttableOutputFormat = \"table\"\n\ttextOutputFormat  = \"text\"\n)\n\nfunc DB(app clio.Application) *cobra.Command {\n\tdb := &cobra.Command{\n\t\tUse:   \"db\",\n\t\tShort: \"vulnerability database operations\",\n\t}\n\n\tdb.AddCommand(\n\t\tDBCheck(app),\n\t\tDBDelete(app),\n\t\tDBImport(app),\n\t\tDBList(app),\n\t\tDBStatus(app),\n\t\tDBUpdate(app),\n\t\tDBSearch(app),\n\t\tDBProviders(app),\n\t)\n\n\treturn db\n}\n"
  },
  {
    "path": "cmd/grype/cli/commands/db_check.go",
    "content": "package commands\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/anchore/clio\"\n\t\"github.com/anchore/grype/cmd/grype/cli/options\"\n\tdb \"github.com/anchore/grype/grype/db/v6\"\n\t\"github.com/anchore/grype/grype/db/v6/distribution\"\n\t\"github.com/anchore/grype/grype/grypeerr\"\n\t\"github.com/anchore/grype/internal/log\"\n)\n\ntype dbCheckOptions struct {\n\tOutput                  string `yaml:\"output\" json:\"output\" mapstructure:\"output\"`\n\toptions.DatabaseCommand `yaml:\",inline\" mapstructure:\",squash\"`\n}\n\nvar _ clio.FlagAdder = (*dbCheckOptions)(nil)\n\nfunc (d *dbCheckOptions) AddFlags(flags clio.FlagSet) {\n\tflags.StringVarP(&d.Output, \"output\", \"o\", \"format to display results (available=[text, json])\")\n}\n\nfunc DBCheck(app clio.Application) *cobra.Command {\n\topts := &dbCheckOptions{\n\t\tOutput:          textOutputFormat,\n\t\tDatabaseCommand: *options.DefaultDatabaseCommand(app.ID()),\n\t}\n\n\tcmd := &cobra.Command{\n\t\tUse:   \"check\",\n\t\tShort: \"Check to see if there is a database update available\",\n\t\tPreRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\t// DB commands should not opt into the low-pass check filter\n\t\t\topts.DB.MaxUpdateCheckFrequency = 0\n\t\t\treturn disableUI(app)(cmd, args)\n\t\t},\n\t\tArgs: cobra.ExactArgs(0),\n\t\tRunE: func(_ *cobra.Command, _ []string) error {\n\t\t\treturn runDBCheck(*opts)\n\t\t},\n\t}\n\n\t// prevent from being shown in the grype config\n\ttype configWrapper struct {\n\t\tHidden                   *dbCheckOptions `json:\"-\" yaml:\"-\" mapstructure:\"-\"`\n\t\t*options.DatabaseCommand `yaml:\",inline\" mapstructure:\",squash\"`\n\t}\n\n\treturn app.SetupCommand(cmd, &configWrapper{Hidden: opts, DatabaseCommand: &opts.DatabaseCommand})\n}\n\nfunc runDBCheck(opts dbCheckOptions) error {\n\tclient, err := distribution.NewClient(opts.ToClientConfig())\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to create distribution client: %w\", err)\n\t}\n\n\tcfg := opts.ToCuratorConfig()\n\n\tcurrent, err := db.ReadDescription(cfg.DBFilePath())\n\tif err != nil {\n\t\tlog.WithFields(\"error\", err).Debug(\"unable to read current database metadata\")\n\t\tcurrent = nil\n\t}\n\n\tarchive, err := client.IsUpdateAvailable(current)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to check for vulnerability database update: %w\", err)\n\t}\n\n\tupdateAvailable := archive != nil\n\n\tif err := presentNewDBCheck(opts.Output, os.Stdout, updateAvailable, current, archive); err != nil {\n\t\treturn err\n\t}\n\n\tif updateAvailable {\n\t\treturn grypeerr.ErrDBUpgradeAvailable\n\t}\n\treturn nil\n}\n\ntype dbCheckJSON struct {\n\tCurrentDB       *db.Description       `json:\"currentDB\"`\n\tCandidateDB     *distribution.Archive `json:\"candidateDB\"`\n\tUpdateAvailable bool                  `json:\"updateAvailable\"`\n}\n\nfunc presentNewDBCheck(format string, writer io.Writer, updateAvailable bool, current *db.Description, candidate *distribution.Archive) error {\n\tswitch format {\n\tcase textOutputFormat:\n\t\tif current != nil {\n\t\t\tfmt.Fprintf(writer, \"Installed DB version %s was built on %s\\n\", current.SchemaVersion, current.Built.String())\n\t\t} else {\n\t\t\tfmt.Fprintln(writer, \"No installed DB version found\")\n\t\t}\n\n\t\tif !updateAvailable {\n\t\t\tfmt.Fprintln(writer, \"No update available\")\n\t\t\treturn nil\n\t\t}\n\n\t\tfmt.Fprintf(writer, \"Updated DB version %s was built on %s\\n\", candidate.SchemaVersion, candidate.Built.String())\n\t\tfmt.Fprintln(writer, \"You can run 'grype db update' to update to the latest db\")\n\tcase jsonOutputFormat:\n\t\tdata := dbCheckJSON{\n\t\t\tCurrentDB:       current,\n\t\t\tCandidateDB:     candidate,\n\t\t\tUpdateAvailable: updateAvailable,\n\t\t}\n\n\t\tenc := json.NewEncoder(writer)\n\t\tenc.SetEscapeHTML(false)\n\t\tenc.SetIndent(\"\", \" \")\n\t\tif err := enc.Encode(&data); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to db listing information: %+v\", err)\n\t\t}\n\tdefault:\n\t\treturn fmt.Errorf(\"unsupported output format: %s\", format)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "cmd/grype/cli/commands/db_check_test.go",
    "content": "package commands\n\nimport (\n\t\"bytes\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\tdb \"github.com/anchore/grype/grype/db/v6\"\n\t\"github.com/anchore/grype/grype/db/v6/distribution\"\n\t\"github.com/anchore/grype/internal/schemaver\"\n)\n\nfunc TestPresentNewDBCheck(t *testing.T) {\n\tcurrentDB := &db.Description{\n\t\tSchemaVersion: schemaver.New(6, 0, 0),\n\t\tBuilt:         db.Time{Time: time.Date(2023, 11, 25, 12, 0, 0, 0, time.UTC)},\n\t}\n\n\tcandidateDB := &distribution.Archive{\n\t\tDescription: db.Description{\n\t\t\tSchemaVersion: schemaver.New(6, 0, 1),\n\t\t\tBuilt:         db.Time{Time: time.Date(2023, 11, 26, 12, 0, 0, 0, time.UTC)},\n\t\t},\n\t\tPath:     \"vulnerability-db_6.0.1_2023-11-26T12:00:00Z_6238463.tar.gz\",\n\t\tChecksum: \"sha256:1234561234567890345674561234567890345678\",\n\t}\n\ttests := []struct {\n\t\tname            string\n\t\tformat          string\n\t\tupdateAvailable bool\n\t\tcurrent         *db.Description\n\t\tcandidate       *distribution.Archive\n\t\texpectedText    string\n\t\texpectErr       require.ErrorAssertionFunc\n\t}{\n\t\t{\n\t\t\tname:            \"text format with update available\",\n\t\t\tformat:          textOutputFormat,\n\t\t\tupdateAvailable: true,\n\t\t\tcurrent:         currentDB,\n\t\t\tcandidate:       candidateDB,\n\t\t\texpectedText: `\nInstalled DB version v6.0.0 was built on 2023-11-25T12:00:00Z\nUpdated DB version v6.0.1 was built on 2023-11-26T12:00:00Z\nYou can run 'grype db update' to update to the latest db\n`,\n\t\t},\n\t\t{\n\t\t\tname:            \"text format without update available\",\n\t\t\tformat:          textOutputFormat,\n\t\t\tupdateAvailable: false,\n\t\t\tcurrent:         currentDB,\n\t\t\tcandidate:       nil,\n\t\t\texpectedText: `\nInstalled DB version v6.0.0 was built on 2023-11-25T12:00:00Z\nNo update available\n`,\n\t\t},\n\t\t{\n\t\t\tname:            \"json format with update available\",\n\t\t\tformat:          jsonOutputFormat,\n\t\t\tupdateAvailable: true,\n\t\t\tcurrent:         currentDB,\n\t\t\tcandidate:       candidateDB,\n\t\t\texpectedText: `\n{\n \"currentDB\": {\n  \"schemaVersion\": \"v6.0.0\",\n  \"built\": \"2023-11-25T12:00:00Z\"\n },\n \"candidateDB\": {\n  \"schemaVersion\": \"v6.0.1\",\n  \"built\": \"2023-11-26T12:00:00Z\",\n  \"path\": \"vulnerability-db_6.0.1_2023-11-26T12:00:00Z_6238463.tar.gz\",\n  \"checksum\": \"sha256:1234561234567890345674561234567890345678\"\n },\n \"updateAvailable\": true\n}\n`,\n\t\t},\n\t\t{\n\t\t\tname:            \"json format without update available\",\n\t\t\tformat:          jsonOutputFormat,\n\t\t\tupdateAvailable: false,\n\t\t\tcurrent:         currentDB,\n\t\t\tcandidate:       nil,\n\t\t\texpectedText: `\n{\n \"currentDB\": {\n  \"schemaVersion\": \"v6.0.0\",\n  \"built\": \"2023-11-25T12:00:00Z\"\n },\n \"candidateDB\": null,\n \"updateAvailable\": false\n}\n`,\n\t\t},\n\t\t{\n\t\t\tname:      \"unsupported format\",\n\t\t\tformat:    \"xml\",\n\t\t\texpectErr: requireErrorContains(\"unsupported output format: xml\"),\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif tt.expectErr == nil {\n\t\t\t\ttt.expectErr = require.NoError\n\t\t\t}\n\t\t\tbuf := &bytes.Buffer{}\n\t\t\terr := presentNewDBCheck(tt.format, buf, tt.updateAvailable, tt.current, tt.candidate)\n\n\t\t\ttt.expectErr(t, err)\n\t\t\tif err != nil {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.Equal(t, strings.TrimSpace(tt.expectedText), strings.TrimSpace(buf.String()))\n\t\t})\n\t}\n}\n\nfunc requireErrorContains(expected string) require.ErrorAssertionFunc {\n\treturn func(t require.TestingT, err error, msgAndArgs ...interface{}) {\n\t\trequire.Error(t, err)\n\t\tassert.Contains(t, err.Error(), expected)\n\t}\n}\n"
  },
  {
    "path": "cmd/grype/cli/commands/db_delete.go",
    "content": "package commands\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/anchore/clio\"\n\t\"github.com/anchore/grype/cmd/grype/cli/options\"\n\t\"github.com/anchore/grype/grype/db/v6/distribution\"\n\t\"github.com/anchore/grype/grype/db/v6/installation\"\n)\n\nfunc DBDelete(app clio.Application) *cobra.Command {\n\topts := options.DefaultDatabaseCommand(app.ID())\n\n\tcmd := &cobra.Command{\n\t\tUse:     \"delete\",\n\t\tShort:   \"Delete the vulnerability database\",\n\t\tArgs:    cobra.ExactArgs(0),\n\t\tPreRunE: disableUI(app),\n\t\tRunE: func(_ *cobra.Command, _ []string) error {\n\t\t\treturn runDBDelete(*opts)\n\t\t},\n\t}\n\n\t// prevent from being shown in the grype config\n\ttype configWrapper struct {\n\t\t*options.DatabaseCommand `yaml:\",inline\" mapstructure:\",squash\"`\n\t}\n\n\treturn app.SetupCommand(cmd, &configWrapper{opts})\n}\n\nfunc runDBDelete(opts options.DatabaseCommand) error {\n\tclient, err := distribution.NewClient(opts.ToClientConfig())\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to create distribution client: %w\", err)\n\t}\n\tc, err := installation.NewCurator(opts.ToCuratorConfig(), client)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to create curator: %w\", err)\n\t}\n\n\tif err := c.Delete(); err != nil {\n\t\treturn fmt.Errorf(\"unable to delete vulnerability database: %+v\", err)\n\t}\n\n\treturn stderrPrintLnf(\"Vulnerability database deleted\")\n}\n"
  },
  {
    "path": "cmd/grype/cli/commands/db_import.go",
    "content": "package commands\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/anchore/clio\"\n\t\"github.com/anchore/grype/cmd/grype/cli/options\"\n\t\"github.com/anchore/grype/grype/db/v6/distribution\"\n\t\"github.com/anchore/grype/grype/db/v6/installation\"\n\t\"github.com/anchore/grype/internal/log\"\n)\n\nfunc DBImport(app clio.Application) *cobra.Command {\n\topts := options.DefaultDatabaseCommand(app.ID())\n\n\tcmd := &cobra.Command{\n\t\tUse:   \"import FILE | URL\",\n\t\tShort: \"Import a vulnerability database or archive from a local file or URL\",\n\t\tLong:  fmt.Sprintf(\"import a vulnerability database archive from a local FILE or URL.\\nDB archives can be obtained from %q (or running `db list`). If the URL has a `checksum` query parameter with a fully qualified digest (e.g. 'sha256:abc728...') then the archive/DB will be verified against this value.\", opts.DB.UpdateURL),\n\t\tArgs:  cobra.ExactArgs(1),\n\t\tRunE: func(_ *cobra.Command, args []string) error {\n\t\t\treturn runDBImport(*opts, args[0])\n\t\t},\n\t}\n\n\t// prevent from being shown in the grype config\n\ttype configWrapper struct {\n\t\t*options.DatabaseCommand `yaml:\",inline\" mapstructure:\",squash\"`\n\t}\n\n\treturn app.SetupCommand(cmd, &configWrapper{opts})\n}\n\nfunc runDBImport(opts options.DatabaseCommand, reference string) error {\n\t// TODO: tui update? better logging?\n\tclient, err := distribution.NewClient(opts.ToClientConfig())\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to create distribution client: %w\", err)\n\t}\n\tc, err := installation.NewCurator(opts.ToCuratorConfig(), client)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to create curator: %w\", err)\n\t}\n\n\tlog.WithFields(\"reference\", reference).Infof(\"importing vulnerability database archive\")\n\tif err := c.Import(reference); err != nil {\n\t\treturn fmt.Errorf(\"unable to import vulnerability database: %w\", err)\n\t}\n\n\ts := c.Status()\n\tlog.WithFields(\"built\", s.Built.String(), \"status\", renderStoreValidation(s)).Info(\"vulnerability database imported\")\n\treturn nil\n}\n"
  },
  {
    "path": "cmd/grype/cli/commands/db_list.go",
    "content": "package commands\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/url\"\n\t\"os\"\n\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/anchore/clio\"\n\t\"github.com/anchore/grype/cmd/grype/cli/options\"\n\t\"github.com/anchore/grype/grype/db/v6/distribution\"\n)\n\ntype dbListOptions struct {\n\tOutput                  string `yaml:\"output\" json:\"output\" mapstructure:\"output\"`\n\toptions.DatabaseCommand `yaml:\",inline\" mapstructure:\",squash\"`\n}\n\nvar _ clio.FlagAdder = (*dbListOptions)(nil)\n\nfunc (d *dbListOptions) AddFlags(flags clio.FlagSet) {\n\tflags.StringVarP(&d.Output, \"output\", \"o\", \"format to display results (available=[text, raw, json])\")\n}\n\nfunc DBList(app clio.Application) *cobra.Command {\n\topts := &dbListOptions{\n\t\tOutput:          textOutputFormat,\n\t\tDatabaseCommand: *options.DefaultDatabaseCommand(app.ID()),\n\t}\n\n\tcmd := &cobra.Command{\n\t\tUse:     \"list\",\n\t\tShort:   \"List all DBs available according to the listing URL\",\n\t\tPreRunE: disableUI(app),\n\t\tArgs:    cobra.ExactArgs(0),\n\t\tRunE: func(_ *cobra.Command, _ []string) error {\n\t\t\treturn runDBList(*opts)\n\t\t},\n\t}\n\n\t// prevent from being shown in the grype config\n\ttype configWrapper struct {\n\t\tHidden                   *dbListOptions `json:\"-\" yaml:\"-\" mapstructure:\"-\"`\n\t\t*options.DatabaseCommand `yaml:\",inline\" mapstructure:\",squash\"`\n\t}\n\n\treturn app.SetupCommand(cmd, &configWrapper{Hidden: opts, DatabaseCommand: &opts.DatabaseCommand})\n}\n\nfunc runDBList(opts dbListOptions) error {\n\tc, err := distribution.NewClient(opts.ToClientConfig())\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to create distribution client: %w\", err)\n\t}\n\n\tlatest, err := c.Latest()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to get database listing: %w\", err)\n\t}\n\n\tu, err := c.ResolveArchiveURL(latest.Archive)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to resolve database URL: %w\", err)\n\t}\n\n\treturn presentDBList(opts.Output, u, opts.DB.UpdateURL, os.Stdout, latest)\n}\n\nfunc presentDBList(format string, archiveURL, listingURL string, writer io.Writer, latest *distribution.LatestDocument) error {\n\tif latest == nil {\n\t\treturn fmt.Errorf(\"no database listing found\")\n\t}\n\n\t// remove query params\n\tarchiveURLObj, err := url.Parse(archiveURL)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to parse db URL %q: %w\", archiveURL, err)\n\t}\n\n\tarchiveURLObj.RawQuery = \"\"\n\n\tif listingURL == distribution.DefaultConfig().LatestURL {\n\t\t// append on the schema\n\t\tlistingURL = fmt.Sprintf(\"%s/v%v/%s\", listingURL, latest.SchemaVersion.Model, distribution.LatestFileName)\n\t}\n\n\tswitch format {\n\tcase textOutputFormat:\n\t\tfmt.Fprintf(writer, \"Status:   %s\\n\", latest.Status)\n\t\tfmt.Fprintf(writer, \"Schema:   %s\\n\", latest.SchemaVersion.String())\n\t\tfmt.Fprintf(writer, \"Built:    %s\\n\", latest.Built.String())\n\t\tfmt.Fprintf(writer, \"Listing:  %s\\n\", listingURL)\n\t\tfmt.Fprintf(writer, \"DB URL:   %s\\n\", archiveURLObj.String())\n\t\tfmt.Fprintf(writer, \"Checksum: %s\\n\", latest.Checksum)\n\tcase jsonOutputFormat, \"raw\":\n\t\tenc := json.NewEncoder(writer)\n\t\tenc.SetEscapeHTML(false)\n\t\tenc.SetIndent(\"\", \" \")\n\t\t// why make an array? We are reserving the right to list additional entries in the future without the\n\t\t// need to change from an object to an array at that point in time. This will be useful if we implement\n\t\t// the history.json functionality for grabbing historical database listings.\n\t\tif err := enc.Encode([]any{latest}); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to db listing information: %+v\", err)\n\t\t}\n\tdefault:\n\t\treturn fmt.Errorf(\"unsupported output format: %s\", format)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "cmd/grype/cli/commands/db_list_test.go",
    "content": "package commands\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/clio\"\n\t\"github.com/anchore/grype/cmd/grype/cli/options\"\n\tdb \"github.com/anchore/grype/grype/db/v6\"\n\t\"github.com/anchore/grype/grype/db/v6/distribution\"\n\t\"github.com/anchore/grype/internal/schemaver\"\n)\n\nfunc Test_ListingUserAgent(t *testing.T) {\n\n\tt.Run(\"new\", func(t *testing.T) {\n\t\tlistingFile := \"/latest.json\"\n\n\t\tgot := \"\"\n\n\t\t// setup mock\n\t\thandler := http.NewServeMux()\n\t\thandler.HandleFunc(listingFile, func(w http.ResponseWriter, r *http.Request) {\n\t\t\tgot = r.Header.Get(\"User-Agent\")\n\t\t\tw.WriteHeader(http.StatusOK)\n\t\t\t_ = json.NewEncoder(w).Encode(&distribution.LatestDocument{\n\t\t\t\tStatus: \"active\",\n\t\t\t\tArchive: distribution.Archive{\n\t\t\t\t\tDescription: db.Description{\n\t\t\t\t\t\tSchemaVersion: schemaver.New(6, 0, 0),\n\t\t\t\t\t\tBuilt:         db.Time{Time: time.Now()},\n\t\t\t\t\t},\n\t\t\t\t\tPath:     \"vulnerability-db_v6.0.0.tar.gz\",\n\t\t\t\t\tChecksum: \"sha256:dummychecksum\",\n\t\t\t\t},\n\t\t\t})\n\t\t})\n\t\tmockSrv := httptest.NewServer(handler)\n\t\tdefer mockSrv.Close()\n\n\t\tdbOptions := *options.DefaultDatabaseCommand(clio.Identification{\n\t\t\tName:    \"new-app\",\n\t\t\tVersion: \"v4.0.0\",\n\t\t})\n\t\tdbOptions.DB.RequireUpdateCheck = true\n\t\tdbOptions.DB.UpdateURL = mockSrv.URL + listingFile\n\n\t\terr := runDBList(dbListOptions{\n\t\t\tOutput:          textOutputFormat,\n\t\t\tDatabaseCommand: dbOptions,\n\t\t})\n\t\trequire.NoError(t, err)\n\n\t\tif got != \"new-app v4.0.0\" {\n\t\t\tt.Errorf(\"expected User-Agent header to match, got: %v\", got)\n\t\t}\n\t})\n\n}\n\nfunc TestPresentDBList(t *testing.T) {\n\tlatestDoc := &distribution.LatestDocument{\n\t\tStatus: \"active\",\n\t\tArchive: distribution.Archive{\n\t\t\tDescription: db.Description{\n\t\t\t\tSchemaVersion: schemaver.New(6, 0, 0),\n\t\t\t\tBuilt:         db.Time{Time: time.Date(2024, 11, 27, 14, 43, 17, 0, time.UTC)},\n\t\t\t},\n\t\t\tPath:     \"vulnerability-db_v6.0.0_2024-11-25T01:31:56Z_1732718597.tar.zst\",\n\t\t\tChecksum: \"sha256:16bcb6551c748056f752f299fcdb4fa50fe61589d086be3889e670261ff21ca4\",\n\t\t},\n\t}\n\n\ttests := []struct {\n\t\tname         string\n\t\tformat       string\n\t\tbaseURL      string\n\t\tarchiveURL   string\n\t\tlatest       *distribution.LatestDocument\n\t\texpectedText string\n\t\texpectedErr  require.ErrorAssertionFunc\n\t}{\n\t\t{\n\t\t\tname:       \"valid text format\",\n\t\t\tformat:     textOutputFormat,\n\t\t\tlatest:     latestDoc,\n\t\t\tbaseURL:    \"http://localhost:8000/latest.json\",\n\t\t\tarchiveURL: \"http://localhost:8000/vulnerability-db_v6.0.0_2024-11-25T01:31:56Z_1732718597.tar.zst\",\n\t\t\texpectedText: `Status:   active\nSchema:   v6.0.0\nBuilt:    2024-11-27T14:43:17Z\nListing:  http://localhost:8000/latest.json\nDB URL:   http://localhost:8000/vulnerability-db_v6.0.0_2024-11-25T01:31:56Z_1732718597.tar.zst\nChecksum: sha256:16bcb6551c748056f752f299fcdb4fa50fe61589d086be3889e670261ff21ca4\n`,\n\t\t\texpectedErr: require.NoError,\n\t\t},\n\t\t{\n\t\t\tname:       \"complete default values\",\n\t\t\tformat:     textOutputFormat,\n\t\t\tlatest:     latestDoc,\n\t\t\tbaseURL:    \"https://grype.anchore.io/databases\",\n\t\t\tarchiveURL: \"https://grype.anchore.io/databases/v6/vulnerability-db_v6.0.0_2024-11-25T01:31:56Z_1732718597.tar.zst\",\n\t\t\texpectedText: `Status:   active\nSchema:   v6.0.0\nBuilt:    2024-11-27T14:43:17Z\nListing:  https://grype.anchore.io/databases/v6/latest.json\nDB URL:   https://grype.anchore.io/databases/v6/vulnerability-db_v6.0.0_2024-11-25T01:31:56Z_1732718597.tar.zst\nChecksum: sha256:16bcb6551c748056f752f299fcdb4fa50fe61589d086be3889e670261ff21ca4\n`,\n\t\t\texpectedErr: require.NoError,\n\t\t},\n\t\t{\n\t\t\tname:   \"valid JSON format\",\n\t\t\tformat: jsonOutputFormat,\n\t\t\tlatest: latestDoc,\n\t\t\texpectedText: `[\n {\n  \"status\": \"active\",\n  \"schemaVersion\": \"v6.0.0\",\n  \"built\": \"2024-11-27T14:43:17Z\",\n  \"path\": \"vulnerability-db_v6.0.0_2024-11-25T01:31:56Z_1732718597.tar.zst\",\n  \"checksum\": \"sha256:16bcb6551c748056f752f299fcdb4fa50fe61589d086be3889e670261ff21ca4\"\n }\n]\n`,\n\t\t\texpectedErr: require.NoError,\n\t\t},\n\t\t{\n\t\t\tname:        \"nil latest document\",\n\t\t\tformat:      textOutputFormat,\n\t\t\tlatest:      nil,\n\t\t\texpectedErr: requireErrorContains(\"no database listing found\"),\n\t\t},\n\t\t{\n\t\t\tname:        \"unsupported format\",\n\t\t\tformat:      \"unsupported\",\n\t\t\tlatest:      latestDoc,\n\t\t\texpectedErr: requireErrorContains(\"unsupported output format\"),\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\twriter := &bytes.Buffer{}\n\n\t\t\terr := presentDBList(tt.format, tt.archiveURL, tt.baseURL, writer, tt.latest)\n\t\t\tif tt.expectedErr == nil {\n\t\t\t\ttt.expectedErr = require.NoError\n\t\t\t}\n\t\t\ttt.expectedErr(t, err)\n\n\t\t\tif err != nil {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\trequire.Equal(t, strings.TrimSpace(tt.expectedText), strings.TrimSpace(writer.String()))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "cmd/grype/cli/commands/db_providers.go",
    "content": "package commands\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/anchore/clio\"\n\t\"github.com/anchore/grype/cmd/grype/cli/options\"\n\tv6 \"github.com/anchore/grype/grype/db/v6\"\n\t\"github.com/anchore/grype/grype/db/v6/distribution\"\n\t\"github.com/anchore/grype/grype/db/v6/installation\"\n\t\"github.com/anchore/grype/internal/bus\"\n)\n\ntype dbProvidersOptions struct {\n\tOutput                  string `yaml:\"output\" json:\"output\"`\n\toptions.DatabaseCommand `yaml:\",inline\" mapstructure:\",squash\"`\n}\n\nvar _ clio.FlagAdder = (*dbProvidersOptions)(nil)\n\nfunc (d *dbProvidersOptions) AddFlags(flags clio.FlagSet) {\n\tflags.StringVarP(&d.Output, \"output\", \"o\", \"format to display results (available=[table, json])\")\n}\n\nfunc DBProviders(app clio.Application) *cobra.Command {\n\topts := &dbProvidersOptions{\n\t\tOutput:          tableOutputFormat,\n\t\tDatabaseCommand: *options.DefaultDatabaseCommand(app.ID()),\n\t}\n\n\tcmd := &cobra.Command{\n\t\tUse:   \"providers\",\n\t\tShort: \"List vulnerability providers that are in the database\",\n\t\tArgs:  cobra.ExactArgs(0),\n\t\tRunE: func(_ *cobra.Command, _ []string) error {\n\t\t\treturn runDBProviders(opts)\n\t\t},\n\t}\n\n\t// prevent from being shown in the grype config\n\ttype configWrapper struct {\n\t\tHidden                   *dbProvidersOptions `json:\"-\" yaml:\"-\" mapstructure:\"-\"`\n\t\t*options.DatabaseCommand `yaml:\",inline\" mapstructure:\",squash\"`\n\t}\n\n\treturn app.SetupCommand(cmd, &configWrapper{Hidden: opts, DatabaseCommand: &opts.DatabaseCommand})\n}\n\nfunc runDBProviders(opts *dbProvidersOptions) error {\n\tclient, err := distribution.NewClient(opts.ToClientConfig())\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to create distribution client: %w\", err)\n\t}\n\tc, err := installation.NewCurator(opts.ToCuratorConfig(), client)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to create curator: %w\", err)\n\t}\n\n\treader, err := c.Reader()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to get providers: %w\", err)\n\t}\n\n\tproviderModels, err := reader.AllProviders()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to get providers: %w\", err)\n\t}\n\n\tsb := &strings.Builder{}\n\n\tswitch opts.Output {\n\tcase tableOutputFormat, textOutputFormat:\n\t\terr = displayDBProvidersTable(toProviders(providerModels), sb)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\tcase jsonOutputFormat:\n\t\terr = displayDBProvidersJSON(toProviders(providerModels), sb)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\tdefault:\n\t\treturn fmt.Errorf(\"unsupported output format: %s\", opts.Output)\n\t}\n\tbus.Report(sb.String())\n\n\treturn nil\n}\n\ntype provider struct {\n\tName         string     `json:\"name\"`\n\tVersion      string     `json:\"version\"`\n\tProcessor    string     `json:\"processor\"`\n\tDateCaptured *time.Time `json:\"dateCaptured\"`\n\tInputDigest  string     `json:\"inputDigest\"`\n}\n\nfunc toProviders(providers []v6.Provider) []provider {\n\tvar res []provider\n\tfor _, p := range providers {\n\t\tres = append(res, provider{\n\t\t\tName:         p.ID,\n\t\t\tVersion:      p.Version,\n\t\t\tProcessor:    p.Processor,\n\t\t\tDateCaptured: p.DateCaptured,\n\t\t\tInputDigest:  p.InputDigest,\n\t\t})\n\t}\n\treturn res\n}\n\nfunc displayDBProvidersTable(providers []provider, output io.Writer) error {\n\trows := [][]string{}\n\tfor _, p := range providers {\n\t\trows = append(rows, []string{p.Name, p.Version, p.Processor, p.DateCaptured.String(), p.InputDigest})\n\t}\n\n\ttable := newTable(output, []string{\"Name\", \"Version\", \"Processor\", \"Date Captured\", \"Input Digest\"})\n\n\tif err := table.Bulk(rows); err != nil {\n\t\treturn fmt.Errorf(\"failed to add table rows: %w\", err)\n\t}\n\treturn table.Render()\n}\n\nfunc displayDBProvidersJSON(providers []provider, output io.Writer) error {\n\tencoder := json.NewEncoder(output)\n\tencoder.SetEscapeHTML(false)\n\tencoder.SetIndent(\"\", \" \")\n\terr := encoder.Encode(providers)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"cannot display json: %w\", err)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "cmd/grype/cli/commands/db_providers_test.go",
    "content": "package commands\n\nimport (\n\t\"bytes\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestDisplayDBProvidersTable(t *testing.T) {\n\tproviders := []provider{\n\t\t{\n\t\t\tName:         \"provider1\",\n\t\t\tVersion:      \"1.0.0\",\n\t\t\tProcessor:    \"vunnel@3.2\",\n\t\t\tDateCaptured: timeRef(time.Date(2024, 11, 25, 14, 30, 0, 0, time.UTC)),\n\t\t\tInputDigest:  \"xxh64:1234567834567\",\n\t\t},\n\t\t{\n\t\t\tName:         \"provider2\",\n\t\t\tVersion:      \"2.0.0\",\n\t\t\tProcessor:    \"vunnel@3.2\",\n\t\t\tDateCaptured: timeRef(time.Date(2024, 11, 26, 10, 15, 0, 0, time.UTC)),\n\t\t\tInputDigest:  \"xxh64:9876543212345\",\n\t\t},\n\t}\n\n\texpectedOutput := `NAME       VERSION  PROCESSOR   DATE CAPTURED                  INPUT DIGEST         \nprovider1  1.0.0    vunnel@3.2  2024-11-25 14:30:00 +0000 UTC  xxh64:1234567834567  \nprovider2  2.0.0    vunnel@3.2  2024-11-26 10:15:00 +0000 UTC  xxh64:9876543212345  \n`\n\n\tvar output bytes.Buffer\n\trequire.NoError(t, displayDBProvidersTable(providers, &output))\n\n\trequire.Equal(t, expectedOutput, output.String())\n}\n\nfunc TestDisplayDBProvidersJSON(t *testing.T) {\n\tproviders := []provider{\n\t\t{\n\t\t\tName:         \"provider1\",\n\t\t\tVersion:      \"1.0.0\",\n\t\t\tProcessor:    \"vunnel@3.2\",\n\t\t\tDateCaptured: timeRef(time.Date(2024, 11, 25, 14, 30, 0, 0, time.UTC)),\n\t\t\tInputDigest:  \"xxh64:1234567834567\",\n\t\t},\n\t\t{\n\t\t\tName:         \"provider2\",\n\t\t\tVersion:      \"2.0.0\",\n\t\t\tProcessor:    \"vunnel@3.2\",\n\t\t\tDateCaptured: timeRef(time.Date(2024, 11, 26, 10, 15, 0, 0, time.UTC)),\n\t\t\tInputDigest:  \"xxh64:9876543212345\",\n\t\t},\n\t}\n\n\texpectedJSON := `[\n {\n  \"name\": \"provider1\",\n  \"version\": \"1.0.0\",\n  \"processor\": \"vunnel@3.2\",\n  \"dateCaptured\": \"2024-11-25T14:30:00Z\",\n  \"inputDigest\": \"xxh64:1234567834567\"\n },\n {\n  \"name\": \"provider2\",\n  \"version\": \"2.0.0\",\n  \"processor\": \"vunnel@3.2\",\n  \"dateCaptured\": \"2024-11-26T10:15:00Z\",\n  \"inputDigest\": \"xxh64:9876543212345\"\n }\n]\n`\n\n\tvar output bytes.Buffer\n\terr := displayDBProvidersJSON(providers, &output)\n\trequire.NoError(t, err)\n\n\trequire.JSONEq(t, expectedJSON, output.String())\n}\n\nfunc timeRef(t time.Time) *time.Time {\n\treturn &t\n}\n"
  },
  {
    "path": "cmd/grype/cli/commands/db_search.go",
    "content": "package commands\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"regexp\"\n\t\"sort\"\n\t\"strings\"\n\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/anchore/clio\"\n\t\"github.com/anchore/grype/cmd/grype/cli/commands/internal/dbsearch\"\n\t\"github.com/anchore/grype/cmd/grype/cli/options\"\n\tv6 \"github.com/anchore/grype/grype/db/v6\"\n\t\"github.com/anchore/grype/grype/db/v6/distribution\"\n\t\"github.com/anchore/grype/grype/db/v6/installation\"\n\t\"github.com/anchore/grype/internal/bus\"\n\t\"github.com/anchore/grype/internal/log\"\n)\n\ntype dbSearchMatchOptions struct {\n\tFormat        options.DBSearchFormat          `yaml:\",inline\" mapstructure:\",squash\"`\n\tVulnerability options.DBSearchVulnerabilities `yaml:\",inline\" mapstructure:\",squash\"`\n\tPackage       options.DBSearchPackages        `yaml:\",inline\" mapstructure:\",squash\"`\n\tOS            options.DBSearchOSs             `yaml:\",inline\" mapstructure:\",squash\"`\n\tBounds        options.DBSearchBounds          `yaml:\",inline\" mapstructure:\",squash\"`\n\n\toptions.DatabaseCommand `yaml:\",inline\" mapstructure:\",squash\"`\n}\n\nvar alasPattern = regexp.MustCompile(`^alas[\\w]*-\\d+-\\d+$`)\n\nfunc (o *dbSearchMatchOptions) applyArgs(args []string) error {\n\tfor _, arg := range args {\n\t\tlowerArg := strings.ToLower(arg)\n\t\tswitch {\n\t\tcase hasAnyPrefix(lowerArg, \"cpe:\", \"purl:\"):\n\t\t\t// this is explicitly a package...\n\t\t\tlog.WithFields(\"value\", arg).Trace(\"assuming arg is a package specifier\")\n\t\t\to.Package.Packages = append(o.Package.Packages, arg)\n\t\tcase hasAnyPrefix(lowerArg, \"cve-\", \"ghsa-\", \"elsa-\", \"rhsa-\") || alasPattern.MatchString(lowerArg):\n\t\t\t// this is a vulnerability...\n\t\t\tlog.WithFields(\"value\", arg).Trace(\"assuming arg is a vulnerability ID\")\n\t\t\to.Vulnerability.VulnerabilityIDs = append(o.Vulnerability.VulnerabilityIDs, arg)\n\t\tdefault:\n\t\t\t// assume this is a package name\n\t\t\tlog.WithFields(\"value\", arg).Trace(\"assuming arg is a package name\")\n\t\t\to.Package.Packages = append(o.Package.Packages, arg)\n\t\t}\n\t}\n\n\tif err := o.Vulnerability.PostLoad(); err != nil {\n\t\treturn err\n\t}\n\n\tif err := o.Package.PostLoad(); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc hasAnyPrefix(s string, prefixes ...string) bool {\n\tfor _, prefix := range prefixes {\n\t\tif strings.HasPrefix(s, prefix) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc DBSearch(app clio.Application) *cobra.Command {\n\topts := &dbSearchMatchOptions{\n\t\tFormat: options.DefaultDBSearchFormat(),\n\t\tVulnerability: options.DBSearchVulnerabilities{\n\t\t\tUseVulnIDFlag: true,\n\t\t},\n\t\tBounds:          options.DefaultDBSearchBounds(),\n\t\tDatabaseCommand: *options.DefaultDatabaseCommand(app.ID()),\n\t}\n\n\tcmd := &cobra.Command{\n\t\tUse:   \"search\",\n\t\tShort: \"Search the DB for vulnerabilities or affected packages\",\n\t\tExample: `\n  Search for affected packages by vulnerability ID:\n\n    $ grype db search --vuln ELSA-2023-12205\n\n  Search for affected packages by package name:\n\n    $ grype db search --pkg log4j\n\n  Search for affected packages by package name, filtering down to a specific vulnerability:\n\n    $ grype db search --pkg log4j --vuln CVE-2021-44228\n\n  Search for affected packages by PURL (note: version is not considered):\n\n    $ grype db search --pkg 'pkg:rpm/redhat/openssl' # or: '--ecosystem rpm --pkg openssl\n\n  Search for affected packages by CPE (note: version/update is not considered):\n\n    $ grype db search --pkg 'cpe:2.3:a:jetty:jetty_http_server:*:*:*:*:*:*:*:*'\n    $ grype db search --pkg 'cpe:/a:jetty:jetty_http_server'`,\n\t\tPreRunE: disableUI(app),\n\t\tRunE: func(cmd *cobra.Command, args []string) (err error) {\n\t\t\tif len(args) > 0 {\n\t\t\t\t// try to stay backwards compatible with v5 search command (which takes args)\n\t\t\t\tif err := opts.applyArgs(args); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\t\t\terr = runDBSearchMatches(*opts)\n\t\t\tif err != nil {\n\t\t\t\tif errors.Is(err, dbsearch.ErrNoSearchCriteria) {\n\t\t\t\t\t_ = cmd.Usage()\n\t\t\t\t}\n\t\t\t\treturn err\n\t\t\t}\n\t\t\treturn nil\n\t\t},\n\t}\n\n\tcmd.AddCommand(\n\t\tDBSearchVulnerabilities(app),\n\t)\n\n\t// prevent from being shown in the grype config\n\ttype configWrapper struct {\n\t\tHidden                   *dbSearchMatchOptions `json:\"-\" yaml:\"-\" mapstructure:\"-\"`\n\t\t*options.DatabaseCommand `yaml:\",inline\" mapstructure:\",squash\"`\n\t}\n\n\treturn app.SetupCommand(cmd, &configWrapper{Hidden: opts, DatabaseCommand: &opts.DatabaseCommand})\n}\n\nfunc runDBSearchMatches(opts dbSearchMatchOptions) error {\n\tclient, err := distribution.NewClient(opts.ToClientConfig())\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to create distribution client: %w\", err)\n\t}\n\n\tcurator, err := installation.NewCurator(opts.ToCuratorConfig(), client)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to create curator: %w\", err)\n\t}\n\n\treader, err := curator.Reader()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to get providers: %w\", err)\n\t}\n\n\tif err := validateProvidersFilter(reader, opts.Vulnerability.Providers); err != nil {\n\t\treturn err\n\t}\n\n\trows, queryErr := dbsearch.FindMatches(reader, dbsearch.AffectedPackagesOptions{\n\t\tVulnerability:         opts.Vulnerability.Specs,\n\t\tPackage:               opts.Package.PkgSpecs,\n\t\tCPE:                   opts.Package.CPESpecs,\n\t\tOS:                    opts.OS.Specs,\n\t\tAllowBroadCPEMatching: opts.Package.AllowBroadCPEMatching,\n\t\tRecordLimit:           opts.Bounds.RecordLimit,\n\t\tFixedStates:           opts.Vulnerability.FixedState,\n\t})\n\tif queryErr != nil {\n\t\tif !errors.Is(queryErr, v6.ErrLimitReached) {\n\t\t\treturn queryErr\n\t\t}\n\t}\n\n\tsb := &strings.Builder{}\n\terr = presentDBSearchMatches(opts.Format.Output, rows, sb)\n\trep := sb.String()\n\tif rep != \"\" {\n\t\tbus.Report(rep)\n\t}\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to present search results: %w\", err)\n\t}\n\n\treturn queryErr\n}\n\nfunc presentDBSearchMatches(outputFormat string, structuredRows dbsearch.Matches, output io.Writer) error {\n\tswitch outputFormat {\n\tcase tableOutputFormat:\n\t\tif len(structuredRows) == 0 {\n\t\t\tbus.Notify(\"No results found\")\n\t\t\treturn nil\n\t\t}\n\t\trows := renderDBSearchPackagesTableRows(structuredRows.Flatten())\n\n\t\ttable := newTable(output, []string{\"Vulnerability\", \"Package\", \"Ecosystem\", \"Namespace\", \"Version Constraint\"})\n\n\t\tif err := table.Bulk(rows); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to add table rows: %+v\", err)\n\t\t}\n\t\treturn table.Render()\n\tcase jsonOutputFormat:\n\t\tif structuredRows == nil {\n\t\t\t// always allocate the top level collection\n\t\t\tstructuredRows = dbsearch.Matches{}\n\t\t}\n\t\tenc := json.NewEncoder(output)\n\t\tenc.SetEscapeHTML(false)\n\t\tenc.SetIndent(\"\", \" \")\n\t\tif err := enc.Encode(structuredRows); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to encode diff information: %+v\", err)\n\t\t}\n\tdefault:\n\t\treturn fmt.Errorf(\"unsupported output format: %s\", outputFormat)\n\t}\n\treturn nil\n}\n\nfunc renderDBSearchPackagesTableRows(structuredRows []dbsearch.AffectedPackage) [][]string {\n\tvar rows [][]string\n\tfor _, rr := range structuredRows {\n\t\tvar pkgOrCPE, ecosystem string\n\t\tif rr.Package != nil {\n\t\t\tpkgOrCPE = rr.Package.Name\n\t\t\tecosystem = rr.Package.Ecosystem\n\t\t} else if rr.CPE != nil {\n\t\t\tpkgOrCPE = rr.CPE.String()\n\t\t\tecosystem = rr.CPE.TargetSoftware\n\t\t}\n\n\t\tvar ranges []string\n\t\tfor _, ra := range rr.Detail.Ranges {\n\t\t\tranges = append(ranges, ra.Version.Constraint)\n\t\t}\n\t\trangeStr := strings.Join(ranges, \" || \")\n\t\trows = append(rows, []string{rr.Vulnerability.ID, pkgOrCPE, ecosystem, mimicV5Namespace(rr), rangeStr})\n\t}\n\n\t// sort rows by each column\n\tsort.Slice(rows, func(i, j int) bool {\n\t\tfor k := range rows[i] {\n\t\t\tif rows[i][k] != rows[j][k] {\n\t\t\t\treturn rows[i][k] < rows[j][k]\n\t\t\t}\n\t\t}\n\t\treturn false\n\t})\n\n\treturn rows\n}\n\nfunc mimicV5Namespace(row dbsearch.AffectedPackage) string {\n\tnamespace := v6.MimicV5Namespace(&row.Vulnerability.Model, row.Model)\n\n\tif row.Model != nil && row.Model.OperatingSystem != nil && row.Model.OperatingSystem.Channel != \"\" {\n\t\treturn namespace + \":\" + row.Model.OperatingSystem.Channel\n\t}\n\n\treturn namespace\n}\n"
  },
  {
    "path": "cmd/grype/cli/commands/db_search_test.go",
    "content": "package commands\n\nimport (\n\t\"testing\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/google/go-cmp/cmp/cmpopts\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/grype/cmd/grype/cli/commands/internal/dbsearch\"\n\t\"github.com/anchore/grype/cmd/grype/cli/options\"\n\tv6 \"github.com/anchore/grype/grype/db/v6\"\n)\n\nfunc TestDBSearchMatchOptionsApplyArgs(t *testing.T) {\n\ttestCases := []struct {\n\t\tname               string\n\t\targs               []string\n\t\texpectedPackages   []string\n\t\texpectedVulnIDs    []string\n\t\texpectedErrMessage string\n\t}{\n\t\t{\n\t\t\tname:             \"empty arguments\",\n\t\t\targs:             []string{},\n\t\t\texpectedPackages: []string{},\n\t\t\texpectedVulnIDs:  []string{},\n\t\t},\n\t\t{\n\t\t\tname: \"valid cpe\",\n\t\t\targs: []string{\"cpe:2.3:a:vendor:product:1.0:*:*:*:*:*:*:*\"},\n\t\t\texpectedPackages: []string{\n\t\t\t\t\"cpe:2.3:a:vendor:product:1.0:*:*:*:*:*:*:*\",\n\t\t\t},\n\t\t\texpectedVulnIDs: []string{},\n\t\t},\n\t\t{\n\t\t\tname: \"valid purl\",\n\t\t\targs: []string{\"pkg:npm/package-name@1.0.0\"},\n\t\t\texpectedPackages: []string{\n\t\t\t\t\"pkg:npm/package-name@1.0.0\",\n\t\t\t},\n\t\t\texpectedVulnIDs: []string{},\n\t\t},\n\t\t{\n\t\t\tname:             \"valid vulnerability IDs\",\n\t\t\targs:             []string{\"CVE-2023-0001\", \"GHSA-1234\", \"ALAS-2023-1234\"},\n\t\t\texpectedPackages: []string{},\n\t\t\texpectedVulnIDs: []string{\n\t\t\t\t\"CVE-2023-0001\",\n\t\t\t\t\"GHSA-1234\",\n\t\t\t\t\"ALAS-2023-1234\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"mixed package and vulns\",\n\t\t\targs: []string{\"cpe:2.3:a:vendor:product:1.0:*:*:*:*:*:*:*\", \"CVE-2023-0001\"},\n\t\t\texpectedPackages: []string{\n\t\t\t\t\"cpe:2.3:a:vendor:product:1.0:*:*:*:*:*:*:*\",\n\t\t\t},\n\t\t\texpectedVulnIDs: []string{\n\t\t\t\t\"CVE-2023-0001\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"plain package name\",\n\t\t\targs: []string{\"package-name\"},\n\t\t\texpectedPackages: []string{\n\t\t\t\t\"package-name\",\n\t\t\t},\n\t\t\texpectedVulnIDs: []string{},\n\t\t},\n\t\t{\n\t\t\tname: \"invalid PostLoad error for Package\",\n\t\t\targs: []string{\"pkg:npm/package-name@1.0.0\", \"cpe:invalid\"},\n\t\t\texpectedPackages: []string{\n\t\t\t\t\"pkg:npm/package-name@1.0.0\",\n\t\t\t},\n\t\t\texpectedErrMessage: \"invalid CPE\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\topts := &dbSearchMatchOptions{\n\t\t\t\tVulnerability: options.DBSearchVulnerabilities{},\n\t\t\t\tPackage:       options.DBSearchPackages{},\n\t\t\t}\n\n\t\t\terr := opts.applyArgs(tc.args)\n\n\t\t\tif tc.expectedErrMessage != \"\" {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\trequire.Contains(t, err.Error(), tc.expectedErrMessage)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\trequire.NoError(t, err)\n\t\t\tif d := cmp.Diff(tc.expectedPackages, opts.Package.Packages, cmpopts.EquateEmpty()); d != \"\" {\n\t\t\t\tt.Errorf(\"unexpected package specifiers: %s\", d)\n\t\t\t}\n\t\t\tif d := cmp.Diff(tc.expectedVulnIDs, opts.Vulnerability.VulnerabilityIDs, cmpopts.EquateEmpty()); d != \"\" {\n\t\t\t\tt.Errorf(\"unexpected vulnerability specifiers: %s\", d)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestMimicV5Namespace(t *testing.T) {\n\ttests := []struct {\n\t\tname      string\n\t\tosName    string\n\t\tosChannel string\n\t\texpected  string\n\t}{\n\t\t{\n\t\t\tname:      \"distro namespace without channel\",\n\t\t\tosName:    \"redhat\",\n\t\t\tosChannel: \"\",\n\t\t\texpected:  \"redhat:distro:redhat:8\",\n\t\t},\n\t\t{\n\t\t\tname:      \"distro namespace with channel\",\n\t\t\tosName:    \"redhat\",\n\t\t\tosChannel: \"eus\",\n\t\t\texpected:  \"redhat:distro:redhat:8:eus\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\trow := dbsearch.AffectedPackage{\n\t\t\t\tVulnerability: dbsearch.VulnerabilityInfo{\n\t\t\t\t\tModel: v6.VulnerabilityHandle{\n\t\t\t\t\t\tProvider: &v6.Provider{ID: \"rhel\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tAffectedPackageInfo: dbsearch.AffectedPackageInfo{\n\t\t\t\t\tModel: &v6.AffectedPackageHandle{\n\t\t\t\t\t\tOperatingSystem: &v6.OperatingSystem{\n\t\t\t\t\t\t\tName:         tt.osName,\n\t\t\t\t\t\t\tMajorVersion: \"8\",\n\t\t\t\t\t\t\tMinorVersion: \"6\",\n\t\t\t\t\t\t\tChannel:      tt.osChannel,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tPackage: &v6.Package{\n\t\t\t\t\t\t\tName:      \"test-package\",\n\t\t\t\t\t\t\tEcosystem: \"rpm\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\n\t\t\tresult := mimicV5Namespace(row)\n\t\t\trequire.Equal(t, tt.expected, result)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "cmd/grype/cli/commands/db_search_vuln.go",
    "content": "package commands\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"sort\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/hashicorp/go-multierror\"\n\t\"github.com/scylladb/go-set/strset\"\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/anchore/clio\"\n\t\"github.com/anchore/grype/cmd/grype/cli/commands/internal/dbsearch\"\n\t\"github.com/anchore/grype/cmd/grype/cli/options\"\n\tv6 \"github.com/anchore/grype/grype/db/v6\"\n\t\"github.com/anchore/grype/grype/db/v6/distribution\"\n\t\"github.com/anchore/grype/grype/db/v6/installation\"\n\t\"github.com/anchore/grype/internal/bus\"\n)\n\ntype dbSearchVulnerabilityOptions struct {\n\tFormat        options.DBSearchFormat          `yaml:\",inline\" mapstructure:\",squash\"`\n\tVulnerability options.DBSearchVulnerabilities `yaml:\",inline\" mapstructure:\",squash\"`\n\tBounds        options.DBSearchBounds          `yaml:\",inline\" mapstructure:\",squash\"`\n\n\toptions.DatabaseCommand `yaml:\",inline\" mapstructure:\",squash\"`\n}\n\nfunc DBSearchVulnerabilities(app clio.Application) *cobra.Command {\n\topts := &dbSearchVulnerabilityOptions{\n\t\tFormat: options.DefaultDBSearchFormat(),\n\t\tVulnerability: options.DBSearchVulnerabilities{\n\t\t\tUseVulnIDFlag: false, // we input this through the args\n\t\t},\n\t\tBounds:          options.DefaultDBSearchBounds(),\n\t\tDatabaseCommand: *options.DefaultDatabaseCommand(app.ID()),\n\t}\n\n\tcmd := &cobra.Command{\n\t\tUse:     \"vuln ID...\",\n\t\tAliases: []string{\"vulnerability\", \"vulnerabilities\", \"vulns\"},\n\t\tShort:   \"Search for vulnerabilities within the DB (supports DB schema v6+ only)\",\n\t\tArgs: func(_ *cobra.Command, args []string) error {\n\t\t\tif len(args) == 0 {\n\t\t\t\treturn fmt.Errorf(\"must specify at least one vulnerability ID\")\n\t\t\t}\n\t\t\topts.Vulnerability.VulnerabilityIDs = args\n\t\t\treturn nil\n\t\t},\n\t\tRunE: func(_ *cobra.Command, _ []string) (err error) {\n\t\t\treturn runDBSearchVulnerabilities(*opts)\n\t\t},\n\t}\n\n\t// prevent from being shown in the grype config\n\ttype configWrapper struct {\n\t\tHidden                   *dbSearchVulnerabilityOptions `json:\"-\" yaml:\"-\" mapstructure:\"-\"`\n\t\t*options.DatabaseCommand `yaml:\",inline\" mapstructure:\",squash\"`\n\t}\n\n\treturn app.SetupCommand(cmd, &configWrapper{Hidden: opts, DatabaseCommand: &opts.DatabaseCommand})\n}\n\nfunc runDBSearchVulnerabilities(opts dbSearchVulnerabilityOptions) error {\n\tclient, err := distribution.NewClient(opts.ToClientConfig())\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to create distribution client: %w\", err)\n\t}\n\n\tc, err := installation.NewCurator(opts.ToCuratorConfig(), client)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to create curator: %w\", err)\n\t}\n\n\treader, err := c.Reader()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to get providers: %w\", err)\n\t}\n\n\tif err := validateProvidersFilter(reader, opts.Vulnerability.Providers); err != nil {\n\t\treturn err\n\t}\n\n\trows, err := dbsearch.FindVulnerabilities(reader, dbsearch.VulnerabilitiesOptions{\n\t\tVulnerability: opts.Vulnerability.Specs,\n\t\tRecordLimit:   opts.Bounds.RecordLimit,\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tsb := &strings.Builder{}\n\terr = presentDBSearchVulnerabilities(opts.Format.Output, rows, sb)\n\trep := sb.String()\n\tif rep != \"\" {\n\t\tbus.Report(rep)\n\t}\n\n\treturn err\n}\n\nfunc validateProvidersFilter(reader v6.Reader, providers []string) error {\n\tif len(providers) == 0 {\n\t\treturn nil\n\t}\n\tavailableProviders, err := reader.AllProviders()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to get providers: %w\", err)\n\t}\n\tactiveProviders := strset.New()\n\tfor _, p := range availableProviders {\n\t\tactiveProviders.Add(p.ID)\n\t}\n\n\tprovSet := strset.New(providers...)\n\n\tdiff := strset.Difference(provSet, activeProviders)\n\tdiffList := diff.List()\n\tsort.Strings(diffList)\n\tvar errs error\n\tfor _, p := range diffList {\n\t\terrs = multierror.Append(errs, fmt.Errorf(\"provider not found: %q\", p))\n\t}\n\n\treturn errs\n}\n\nfunc presentDBSearchVulnerabilities(outputFormat string, structuredRows []dbsearch.Vulnerability, output io.Writer) error {\n\tswitch outputFormat {\n\tcase tableOutputFormat:\n\t\tif len(structuredRows) == 0 {\n\t\t\tbus.Notify(\"No results found\")\n\t\t\treturn nil\n\t\t}\n\n\t\trows := renderDBSearchVulnerabilitiesTableRows(structuredRows)\n\n\t\ttable := newTable(output, []string{\"ID\", \"Provider\", \"Published\", \"Severity\", \"Reference\"})\n\n\t\tif err := table.Bulk(rows); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to add table rows: %+v\", err)\n\t\t}\n\t\treturn table.Render()\n\tcase jsonOutputFormat:\n\t\tif structuredRows == nil {\n\t\t\t// always allocate the top level collection\n\t\t\tstructuredRows = []dbsearch.Vulnerability{}\n\t\t}\n\t\tenc := json.NewEncoder(output)\n\t\tenc.SetEscapeHTML(false)\n\t\tenc.SetIndent(\"\", \" \")\n\t\tif err := enc.Encode(structuredRows); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to encode diff information: %+v\", err)\n\t\t}\n\tdefault:\n\t\treturn fmt.Errorf(\"unsupported output format: %s\", outputFormat)\n\t}\n\treturn nil\n}\n\nfunc renderDBSearchVulnerabilitiesTableRows(structuredRows []dbsearch.Vulnerability) [][]string {\n\ttype row struct {\n\t\tVuln                    string\n\t\tProviderWithoutVersions string\n\t\tPublishedDate           string\n\t\tSeverity                string\n\t\tReference               string\n\t}\n\n\tversionsByRow := make(map[row][]string)\n\tfor _, rr := range structuredRows {\n\t\tr := row{\n\t\t\tVuln:                    rr.ID,\n\t\t\tProviderWithoutVersions: rr.Provider,\n\t\t\tPublishedDate:           getDate(rr.PublishedDate),\n\t\t\tSeverity:                rr.Severity,\n\t\t\tReference:               getPrimaryReference(rr.References),\n\t\t}\n\t\tversionsByRow[r] = append(versionsByRow[r], getOSVersions(rr.OperatingSystems)...)\n\t}\n\n\tvar rows [][]string\n\tfor r, versions := range versionsByRow {\n\t\tprov := r.ProviderWithoutVersions\n\t\tif len(versions) > 0 {\n\t\t\tsort.Strings(versions)\n\t\t\tprov = fmt.Sprintf(\"%s (%s)\", r.ProviderWithoutVersions, strings.Join(versions, \", \"))\n\t\t}\n\t\trows = append(rows, []string{r.Vuln, prov, r.PublishedDate, r.Severity, r.Reference})\n\t}\n\n\t// sort rows by each column\n\tsort.Slice(rows, func(i, j int) bool {\n\t\tfor k := range rows[i] {\n\t\t\tif rows[i][k] != rows[j][k] {\n\t\t\t\treturn rows[i][k] < rows[j][k]\n\t\t\t}\n\t\t}\n\t\treturn false\n\t})\n\n\treturn rows\n}\n\nfunc getOSVersions(oss []dbsearch.OperatingSystem) []string {\n\tvar versions []string\n\tfor _, os := range oss {\n\t\tversions = append(versions, os.Version)\n\t}\n\treturn versions\n}\n\nfunc getPrimaryReference(refs []v6.Reference) string {\n\tif len(refs) > 0 {\n\t\treturn refs[0].URL\n\t}\n\n\treturn \"\"\n}\n\nfunc getDate(t *time.Time) string {\n\tif t != nil && !t.IsZero() {\n\t\treturn t.Format(\"2006-01-02\")\n\t}\n\treturn \"\"\n}\n"
  },
  {
    "path": "cmd/grype/cli/commands/db_search_vuln_test.go",
    "content": "package commands\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/grype/cmd/grype/cli/commands/internal/dbsearch\"\n\tv6 \"github.com/anchore/grype/grype/db/v6\"\n)\n\nfunc TestGetOSVersions(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    []dbsearch.OperatingSystem\n\t\texpected []string\n\t}{\n\t\t{\n\t\t\tname:     \"empty list\",\n\t\t\tinput:    []dbsearch.OperatingSystem{},\n\t\t\texpected: nil,\n\t\t},\n\t\t{\n\t\t\tname: \"single os\",\n\t\t\tinput: []dbsearch.OperatingSystem{\n\t\t\t\t{\n\t\t\t\t\tName:    \"debian\",\n\t\t\t\t\tVersion: \"11\",\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: []string{\"11\"},\n\t\t},\n\t\t{\n\t\t\tname: \"multiple os\",\n\t\t\tinput: []dbsearch.OperatingSystem{\n\t\t\t\t{\n\t\t\t\t\tName:    \"ubuntu\",\n\t\t\t\t\tVersion: \"16.04\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tName:    \"ubuntu\",\n\t\t\t\t\tVersion: \"22.04\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tName:    \"ubuntu\",\n\t\t\t\t\tVersion: \"24.04\",\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: []string{\"16.04\", \"22.04\", \"24.04\"},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tactual := getOSVersions(tt.input)\n\t\t\trequire.Equal(t, tt.expected, actual)\n\t\t})\n\t}\n}\n\nfunc TestGetPrimaryReference(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    []v6.Reference\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:     \"empty list\",\n\t\t\tinput:    []v6.Reference{},\n\t\t\texpected: \"\",\n\t\t},\n\t\t{\n\t\t\tname: \"single reference\",\n\t\t\tinput: []v6.Reference{\n\t\t\t\t{\n\t\t\t\t\tURL:  \"https://example.com/vuln/123\",\n\t\t\t\t\tTags: []string{\"primary\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: \"https://example.com/vuln/123\",\n\t\t},\n\t\t{\n\t\t\tname: \"multiple references\",\n\t\t\tinput: []v6.Reference{\n\t\t\t\t{\n\t\t\t\t\tURL:  \"https://example.com/vuln/123\",\n\t\t\t\t\tTags: []string{\"primary\"},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tURL:  \"https://example.com/advisory/123\",\n\t\t\t\t\tTags: []string{\"secondary\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: \"https://example.com/vuln/123\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tactual := getPrimaryReference(tt.input)\n\t\t\trequire.Equal(t, tt.expected, actual)\n\t\t})\n\t}\n}\n\nfunc TestGetDate(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    *time.Time\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:     \"nil time\",\n\t\t\tinput:    nil,\n\t\t\texpected: \"\",\n\t\t},\n\t\t{\n\t\t\tname:     \"zero time\",\n\t\t\tinput:    &time.Time{},\n\t\t\texpected: \"\",\n\t\t},\n\t\t{\n\t\t\tname:     \"valid time\",\n\t\t\tinput:    timePtr(time.Date(2023, 5, 15, 0, 0, 0, 0, time.UTC)),\n\t\t\texpected: \"2023-05-15\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tactual := getDate(tt.input)\n\t\t\trequire.Equal(t, tt.expected, actual)\n\t\t})\n\t}\n}\n\nfunc timePtr(t time.Time) *time.Time {\n\treturn &t\n}\n"
  },
  {
    "path": "cmd/grype/cli/commands/db_status.go",
    "content": "package commands\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/anchore/clio\"\n\t\"github.com/anchore/grype/cmd/grype/cli/options\"\n\t\"github.com/anchore/grype/grype/db/v6/distribution\"\n\t\"github.com/anchore/grype/grype/db/v6/installation\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n)\n\ntype dbStatusOptions struct {\n\tOutput                  string `yaml:\"output\" json:\"output\" mapstructure:\"output\"`\n\toptions.DatabaseCommand `yaml:\",inline\" mapstructure:\",squash\"`\n}\n\nvar _ clio.FlagAdder = (*dbStatusOptions)(nil)\n\nfunc (d *dbStatusOptions) AddFlags(flags clio.FlagSet) {\n\tflags.StringVarP(&d.Output, \"output\", \"o\", \"format to display results (available=[text, json])\")\n}\n\nfunc DBStatus(app clio.Application) *cobra.Command {\n\topts := &dbStatusOptions{\n\t\tOutput:          textOutputFormat,\n\t\tDatabaseCommand: *options.DefaultDatabaseCommand(app.ID()),\n\t}\n\n\tcmd := &cobra.Command{\n\t\tUse:     \"status\",\n\t\tShort:   \"Display database status and metadata\",\n\t\tArgs:    cobra.ExactArgs(0),\n\t\tPreRunE: disableUI(app),\n\t\tRunE: func(_ *cobra.Command, _ []string) error {\n\t\t\treturn runDBStatus(*opts)\n\t\t},\n\t}\n\n\t// prevent from being shown in the grype config\n\ttype configWrapper struct {\n\t\tHidden                   *dbStatusOptions `json:\"-\" yaml:\"-\" mapstructure:\"-\"`\n\t\t*options.DatabaseCommand `yaml:\",inline\" mapstructure:\",squash\"`\n\t}\n\n\treturn app.SetupCommand(cmd, &configWrapper{Hidden: opts, DatabaseCommand: &opts.DatabaseCommand})\n}\n\nfunc runDBStatus(opts dbStatusOptions) error {\n\tclient, err := distribution.NewClient(opts.ToClientConfig())\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to create distribution client: %w\", err)\n\t}\n\tc, err := installation.NewCurator(opts.ToCuratorConfig(), client)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to create distribution client: %w\", err)\n\t}\n\n\tstatus := c.Status()\n\n\tif err := presentDBStatus(opts.Output, os.Stdout, status); err != nil {\n\t\treturn fmt.Errorf(\"failed to present db status information: %+v\", err)\n\t}\n\n\treturn status.Error\n}\n\nfunc presentDBStatus(format string, writer io.Writer, status vulnerability.ProviderStatus) error {\n\tswitch format {\n\tcase textOutputFormat:\n\t\tfmt.Fprintln(writer, \"Path:     \", status.Path)\n\t\tfmt.Fprintln(writer, \"Schema:   \", status.SchemaVersion)\n\t\tfmt.Fprintln(writer, \"Built:    \", status.Built.Format(time.RFC3339))\n\t\tif status.From != \"\" {\n\t\t\tfmt.Fprintln(writer, \"From:     \", status.From)\n\t\t}\n\t\tfmt.Fprintln(writer, \"Status:   \", renderStoreValidation(status))\n\tcase jsonOutputFormat:\n\t\tenc := json.NewEncoder(writer)\n\t\tenc.SetEscapeHTML(false)\n\t\tenc.SetIndent(\"\", \" \")\n\t\tif err := enc.Encode(&status); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to db status information: %+v\", err)\n\t\t}\n\tdefault:\n\t\treturn fmt.Errorf(\"unsupported output format: %s\", format)\n\t}\n\n\treturn nil\n}\n\nfunc renderStoreValidation(status vulnerability.ProviderStatus) string {\n\tif status.Error != nil {\n\t\treturn \"invalid\"\n\t}\n\treturn \"valid\"\n}\n"
  },
  {
    "path": "cmd/grype/cli/commands/db_status_test.go",
    "content": "package commands\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/grype/grype/vulnerability\"\n)\n\nfunc TestPresentDBStatus(t *testing.T) {\n\tvalidStatus := vulnerability.ProviderStatus{\n\t\tPath:          \"/Users/test/Library/Caches/grype/db/6/vulnerability.db\",\n\t\tFrom:          \"https://grype.anchore.io/databases/v6/vulnerability-db_v6.0.2_2025-03-14T01:31:06Z_1741925227.tar.zst?checksum=sha256%3Ad4654e3b212f1d8a1aaab979599691099af541568d687c4a7c4e7c1da079b9b8\",\n\t\tSchemaVersion: \"6.0.0\",\n\t\tBuilt:         time.Date(2024, 11, 27, 14, 43, 17, 0, time.UTC),\n\t\tError:         nil,\n\t}\n\n\tinvalidStatus := vulnerability.ProviderStatus{\n\t\tPath:          \"/Users/test/Library/Caches/grype/db/6/vulnerability.db\",\n\t\tFrom:          \"https://grype.anchore.io/databases/v6/vulnerability-db_v6.0.2_2025-03-14T01:31:06Z_1741925227.tar.zst?checksum=sha256%3Ad4654e3b212f1d8a1aaab979599691099af541568d687c4a7c4e7c1da079b9b8\",\n\t\tSchemaVersion: \"6.0.0\",\n\t\tBuilt:         time.Date(2024, 11, 27, 14, 43, 17, 0, time.UTC),\n\t\tError:         errors.New(\"checksum mismatch\"),\n\t}\n\n\ttests := []struct {\n\t\tname         string\n\t\tformat       string\n\t\tstatus       vulnerability.ProviderStatus\n\t\texpectedText string\n\t\texpectedErr  require.ErrorAssertionFunc\n\t}{\n\t\t{\n\t\t\tname:   \"valid status, text format\",\n\t\t\tformat: textOutputFormat,\n\t\t\tstatus: validStatus,\n\t\t\texpectedText: `Path:      /Users/test/Library/Caches/grype/db/6/vulnerability.db\nSchema:    6.0.0\nBuilt:     2024-11-27T14:43:17Z\nFrom:      https://grype.anchore.io/databases/v6/vulnerability-db_v6.0.2_2025-03-14T01:31:06Z_1741925227.tar.zst?checksum=sha256%3Ad4654e3b212f1d8a1aaab979599691099af541568d687c4a7c4e7c1da079b9b8\nStatus:    valid\n`,\n\t\t\texpectedErr: require.NoError,\n\t\t},\n\t\t{\n\t\t\tname:   \"invalid status, text format\",\n\t\t\tformat: textOutputFormat,\n\t\t\tstatus: invalidStatus,\n\t\t\texpectedText: `Path:      /Users/test/Library/Caches/grype/db/6/vulnerability.db\nSchema:    6.0.0\nBuilt:     2024-11-27T14:43:17Z\nFrom:      https://grype.anchore.io/databases/v6/vulnerability-db_v6.0.2_2025-03-14T01:31:06Z_1741925227.tar.zst?checksum=sha256%3Ad4654e3b212f1d8a1aaab979599691099af541568d687c4a7c4e7c1da079b9b8\nStatus:    invalid\n`,\n\t\t\texpectedErr: require.NoError,\n\t\t},\n\t\t{\n\t\t\tname:   \"valid status, JSON format\",\n\t\t\tformat: jsonOutputFormat,\n\t\t\tstatus: validStatus,\n\t\t\texpectedText: `{\n \"schemaVersion\": \"6.0.0\",\n \"from\": \"https://grype.anchore.io/databases/v6/vulnerability-db_v6.0.2_2025-03-14T01:31:06Z_1741925227.tar.zst?checksum=sha256%3Ad4654e3b212f1d8a1aaab979599691099af541568d687c4a7c4e7c1da079b9b8\",\n \"built\": \"2024-11-27T14:43:17Z\",\n \"path\": \"/Users/test/Library/Caches/grype/db/6/vulnerability.db\",\n \"valid\": true\n}\n`,\n\t\t\texpectedErr: require.NoError,\n\t\t},\n\t\t{\n\t\t\tname:   \"invalid status, JSON format\",\n\t\t\tformat: jsonOutputFormat,\n\t\t\tstatus: invalidStatus,\n\t\t\texpectedText: `{\n \"schemaVersion\": \"6.0.0\",\n \"from\": \"https://grype.anchore.io/databases/v6/vulnerability-db_v6.0.2_2025-03-14T01:31:06Z_1741925227.tar.zst?checksum=sha256%3Ad4654e3b212f1d8a1aaab979599691099af541568d687c4a7c4e7c1da079b9b8\",\n \"built\": \"2024-11-27T14:43:17Z\",\n \"path\": \"/Users/test/Library/Caches/grype/db/6/vulnerability.db\",\n \"valid\": false,\n \"error\": \"checksum mismatch\"\n}\n`,\n\t\t\texpectedErr: require.NoError,\n\t\t},\n\t\t{\n\t\t\tname:        \"unsupported format\",\n\t\t\tformat:      \"unsupported\",\n\t\t\tstatus:      validStatus,\n\t\t\texpectedErr: requireErrorContains(\"unsupported output format\"),\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif tt.expectedErr == nil {\n\t\t\t\ttt.expectedErr = require.NoError\n\t\t\t}\n\t\t\twriter := &bytes.Buffer{}\n\n\t\t\terr := presentDBStatus(tt.format, writer, tt.status)\n\t\t\ttt.expectedErr(t, err)\n\t\t\tif err != nil {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.Equal(t, strings.TrimSpace(tt.expectedText), strings.TrimSpace(writer.String()))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "cmd/grype/cli/commands/db_update.go",
    "content": "package commands\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/anchore/clio\"\n\t\"github.com/anchore/grype/cmd/grype/cli/options\"\n\t\"github.com/anchore/grype/grype/db/v6/distribution\"\n\t\"github.com/anchore/grype/grype/db/v6/installation\"\n\t\"github.com/anchore/grype/internal/bus\"\n\t\"github.com/anchore/grype/internal/log\"\n)\n\nfunc DBUpdate(app clio.Application) *cobra.Command {\n\topts := options.DefaultDatabaseCommand(app.ID())\n\n\tcmd := &cobra.Command{\n\t\tUse:   \"update\",\n\t\tShort: \"Download and install the latest vulnerability database\",\n\t\tArgs:  cobra.ExactArgs(0),\n\t\tPreRunE: func(_ *cobra.Command, _ []string) error {\n\t\t\t// DB commands should not opt into the low-pass check filter\n\t\t\topts.DB.MaxUpdateCheckFrequency = 0\n\t\t\treturn nil\n\t\t},\n\t\tRunE: func(_ *cobra.Command, _ []string) error {\n\t\t\treturn runDBUpdate(*opts)\n\t\t},\n\t}\n\n\t// prevent from being shown in the grype config\n\ttype configWrapper struct {\n\t\t*options.DatabaseCommand `yaml:\",inline\" mapstructure:\",squash\"`\n\t}\n\n\treturn app.SetupCommand(cmd, &configWrapper{opts})\n}\n\nfunc runDBUpdate(opts options.DatabaseCommand) error {\n\tcfg := opts.ToClientConfig()\n\t// we need to have this set to true to force the update call to try to update\n\t// regardless of what the user provided in order for update checks to fail\n\tif !cfg.RequireUpdateCheck {\n\t\tlog.Warn(\"overriding db update check\")\n\t\tcfg.RequireUpdateCheck = true\n\t}\n\tclient, err := distribution.NewClient(cfg)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to create distribution client: %w\", err)\n\t}\n\tc, err := installation.NewCurator(opts.ToCuratorConfig(), client)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to create curator: %w\", err)\n\t}\n\n\tupdated, err := c.Update()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to update vulnerability database: %w\", err)\n\t}\n\n\tresult := \"No vulnerability database update available\\n\"\n\tif updated {\n\t\tresult = \"Vulnerability database updated to latest version!\\n\"\n\t}\n\n\tlog.Debugf(\"completed db update check with result: %s\", result)\n\n\tbus.Report(result)\n\n\treturn nil\n}\n"
  },
  {
    "path": "cmd/grype/cli/commands/explain.go",
    "content": "package commands\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/anchore/clio\"\n\t\"github.com/anchore/grype/grype/presenter/explain\"\n\t\"github.com/anchore/grype/grype/presenter/models\"\n\t\"github.com/anchore/grype/internal\"\n\t\"github.com/anchore/grype/internal/log\"\n)\n\ntype explainOptions struct {\n\tCVEIDs []string `yaml:\"cve-ids\" json:\"cve-ids\" mapstructure:\"cve-ids\"`\n}\n\nvar _ clio.FlagAdder = (*explainOptions)(nil)\n\nfunc (d *explainOptions) AddFlags(flags clio.FlagSet) {\n\tflags.StringArrayVarP(&d.CVEIDs, \"id\", \"\", \"CVE IDs to explain\")\n}\n\nfunc Explain(app clio.Application) *cobra.Command {\n\topts := &explainOptions{}\n\n\tcmd := &cobra.Command{\n\t\tUse:     \"explain --id [VULNERABILITY ID]\",\n\t\tShort:   \"Ask grype to explain a set of findings\",\n\t\tPreRunE: disableUI(app),\n\t\tRunE: func(_ *cobra.Command, _ []string) error {\n\t\t\tlog.Warn(\"grype explain is a prototype feature and is subject to change\")\n\t\t\tisStdinPipeOrRedirect, err := internal.IsStdinPipeOrRedirect()\n\t\t\tif err != nil {\n\t\t\t\tlog.Warnf(\"unable to determine if there is piped input: %+v\", err)\n\t\t\t\tisStdinPipeOrRedirect = false\n\t\t\t}\n\t\t\tif isStdinPipeOrRedirect {\n\t\t\t\t// TODO: eventually detect different types of input; for now assume grype json\n\t\t\t\tvar parseResult models.Document\n\t\t\t\tdecoder := json.NewDecoder(os.Stdin)\n\t\t\t\terr := decoder.Decode(&parseResult)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"unable to parse piped input: %+v\", err)\n\t\t\t\t}\n\t\t\t\texplainer := explain.NewVulnerabilityExplainer(os.Stdout, &parseResult)\n\t\t\t\treturn explainer.ExplainByID(opts.CVEIDs)\n\t\t\t}\n\t\t\t// perform a scan, then explain requested CVEs\n\t\t\t// TODO: implement\n\t\t\treturn fmt.Errorf(\"requires grype json on stdin, please run 'grype -o json ... | grype explain ...'\")\n\t\t},\n\t}\n\n\t// prevent from being shown in the grype config\n\ttype configWrapper struct {\n\t\tOpts *explainOptions `json:\"-\" yaml:\"-\" mapstructure:\"-\"`\n\t}\n\n\treturn app.SetupCommand(cmd, &configWrapper{opts})\n}\n"
  },
  {
    "path": "cmd/grype/cli/commands/internal/dbsearch/affected_packages.go",
    "content": "package dbsearch\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\n\tv6 \"github.com/anchore/grype/grype/db/v6\"\n\t\"github.com/anchore/grype/internal/log\"\n\t\"github.com/anchore/syft/syft/cpe\"\n)\n\nvar ErrNoSearchCriteria = errors.New(\"must provide at least one of vulnerability or package to search for\")\n\n// AffectedPackage represents a package affected by a vulnerability\ntype AffectedPackage struct {\n\t// Vulnerability is the core advisory record for a single known vulnerability from a specific provider.\n\tVulnerability VulnerabilityInfo `json:\"vulnerability\"`\n\n\t// AffectedPackageInfo is the detailed information about the affected package\n\tAffectedPackageInfo `json:\",inline\"`\n}\n\ntype AffectedPackageInfo struct {\n\t// TODO: remove this when namespace is no longer used\n\tModel *v6.AffectedPackageHandle `json:\"-\"` // tracking package handle info is necessary for namespace lookup (note CPE handles are not tracked)\n\n\t// OS identifies the operating system release that the affected package is released for\n\tOS *OperatingSystem `json:\"os,omitempty\"`\n\n\t// Package identifies the name of the package in a specific ecosystem affected by the vulnerability\n\tPackage *Package `json:\"package,omitempty\"`\n\n\t// CPE is a Common Platform Enumeration that is affected by the vulnerability\n\tCPE *CPE `json:\"cpe,omitempty\"`\n\n\t// Namespace is a holdover value from the v5 DB schema that combines provider and search methods into a single value\n\t//\n\t// Deprecated: this field will be removed in a later version of the search schema\n\tNamespace string `json:\"namespace\"`\n\n\t// Detail is the detailed information about the affected package\n\tDetail v6.PackageBlob `json:\"detail\"`\n}\n\n// Package represents a package name within a known ecosystem, such as \"python\" or \"golang\".\ntype Package struct {\n\t// Name is the name of the package within the ecosystem\n\tName string `json:\"name\"`\n\n\t// Ecosystem is the tooling and language ecosystem that the package is released within\n\tEcosystem string `json:\"ecosystem\"`\n}\n\n// CPE is a Common Platform Enumeration that identifies a package\ntype CPE v6.Cpe\n\nfunc (c *CPE) MarshalJSON() ([]byte, error) {\n\treturn []byte(fmt.Sprintf(\"%q\", c.String())), nil\n}\n\nfunc (c *CPE) String() string {\n\tif c == nil {\n\t\treturn \"\"\n\t}\n\n\treturn v6.Cpe(*c).String()\n}\n\ntype AffectedPackagesOptions struct {\n\tVulnerability         v6.VulnerabilitySpecifiers\n\tPackage               v6.PackageSpecifiers\n\tCPE                   v6.PackageSpecifiers\n\tOS                    v6.OSSpecifiers\n\tAllowBroadCPEMatching bool\n\tRecordLimit           int\n\tFixedStates           []string\n}\n\ntype affectedPackageWithDecorations struct {\n\tv6.AffectedPackageHandle\n\tvulnerabilityDecorations\n}\n\nfunc (a *affectedPackageWithDecorations) getCVEs() []string {\n\tif a == nil {\n\t\treturn nil\n\t}\n\treturn getCVEs(a.Vulnerability)\n}\n\ntype affectedCPEWithDecorations struct {\n\tv6.AffectedCPEHandle\n\tvulnerabilityDecorations\n}\n\nfunc (a *affectedCPEWithDecorations) getCVEs() []string {\n\tif a == nil {\n\t\treturn nil\n\t}\n\treturn getCVEs(a.Vulnerability)\n}\n\nfunc newAffectedPackageRows(affectedPkgs []affectedPackageWithDecorations, affectedCPEs []affectedCPEWithDecorations) (rows []AffectedPackage) {\n\tfor i := range affectedPkgs {\n\t\tpkg := affectedPkgs[i]\n\t\tvar detail v6.PackageBlob\n\t\tif pkg.BlobValue != nil {\n\t\t\tdetail = *pkg.BlobValue\n\t\t}\n\t\tif pkg.Vulnerability == nil {\n\t\t\tlog.Errorf(\"affected package record missing vulnerability: %+v\", pkg)\n\t\t\tcontinue\n\t\t}\n\n\t\trows = append(rows, AffectedPackage{\n\t\t\tVulnerability: newVulnerabilityInfo(*pkg.Vulnerability, pkg.vulnerabilityDecorations),\n\t\t\tAffectedPackageInfo: AffectedPackageInfo{\n\t\t\t\tModel:     &pkg.AffectedPackageHandle,\n\t\t\t\tOS:        toOS(pkg.OperatingSystem),\n\t\t\t\tPackage:   toPackage(pkg.Package),\n\t\t\t\tNamespace: v6.MimicV5Namespace(pkg.Vulnerability, &pkg.AffectedPackageHandle),\n\t\t\t\tDetail:    detail,\n\t\t\t},\n\t\t})\n\t}\n\n\tfor _, ac := range affectedCPEs {\n\t\tvar detail v6.PackageBlob\n\t\tif ac.BlobValue != nil {\n\t\t\tdetail = *ac.BlobValue\n\t\t}\n\t\tif ac.Vulnerability == nil {\n\t\t\tlog.Errorf(\"affected CPE record missing vulnerability: %+v\", ac)\n\t\t\tcontinue\n\t\t}\n\n\t\tvar c *CPE\n\t\tif ac.CPE != nil {\n\t\t\tcv := CPE(*ac.CPE)\n\t\t\tc = &cv\n\t\t}\n\n\t\trows = append(rows, AffectedPackage{\n\t\t\t// tracking model information is not possible with CPE handles\n\t\t\tVulnerability: newVulnerabilityInfo(*ac.Vulnerability, ac.vulnerabilityDecorations),\n\t\t\tAffectedPackageInfo: AffectedPackageInfo{\n\t\t\t\tCPE:       c,\n\t\t\t\tNamespace: v6.MimicV5Namespace(ac.Vulnerability, nil), // no affected package will default to NVD\n\t\t\t\tDetail:    detail,\n\t\t\t},\n\t\t})\n\t}\n\n\treturn rows\n}\n\nfunc toPackage(pkg *v6.Package) *Package {\n\tif pkg == nil {\n\t\treturn nil\n\t}\n\treturn &Package{\n\t\tName:      pkg.Name,\n\t\tEcosystem: pkg.Ecosystem,\n\t}\n}\n\nfunc toOS(os *v6.OperatingSystem) *OperatingSystem {\n\tif os == nil {\n\t\treturn nil\n\t}\n\n\treturn &OperatingSystem{\n\t\tName:    os.Name,\n\t\tVersion: os.Version(),\n\t}\n}\n\nfunc FindAffectedPackages(reader interface {\n\tv6.AffectedPackageStoreReader\n\tv6.AffectedCPEStoreReader\n\tv6.VulnerabilityDecoratorStoreReader\n}, criteria AffectedPackagesOptions,\n) ([]AffectedPackage, error) {\n\tallAffectedPkgs, allAffectedCPEs, err := findAffectedPackages(reader, criteria)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif len(criteria.FixedStates) > 0 {\n\t\tallAffectedPkgs = filterByFixedStateForPackages(allAffectedPkgs, criteria.FixedStates)\n\t\tallAffectedCPEs = filterByFixedStateForCPEs(allAffectedCPEs, criteria.FixedStates)\n\t}\n\n\treturn newAffectedPackageRows(allAffectedPkgs, allAffectedCPEs), nil\n}\n\nfunc findAffectedPackages(reader interface { //nolint:funlen,gocognit\n\tv6.AffectedPackageStoreReader\n\tv6.AffectedCPEStoreReader\n\tv6.VulnerabilityDecoratorStoreReader\n}, config AffectedPackagesOptions,\n) ([]affectedPackageWithDecorations, []affectedCPEWithDecorations, error) {\n\tvar allAffectedPkgs []affectedPackageWithDecorations\n\tvar allAffectedCPEs []affectedCPEWithDecorations\n\n\tpkgSpecs := config.Package\n\tcpeSpecs := config.CPE\n\tosSpecs := config.OS\n\tvulnSpecs := config.Vulnerability\n\n\tif config.RecordLimit == 0 {\n\t\tlog.Warn(\"no record limit set! For queries with large result sets this may result in performance issues\")\n\t}\n\n\tif len(vulnSpecs) == 0 && len(pkgSpecs) == 0 && len(cpeSpecs) == 0 {\n\t\treturn nil, nil, ErrNoSearchCriteria\n\t}\n\n\t// don't allow for searching by any package AND any CPE AND any vulnerability AND any OS. Since these searches\n\t// are oriented by primarily package, we only want to have ANY package/CPE when there is a vulnerability or OS specified.\n\tif len(vulnSpecs) > 0 || !osSpecs.IsAny() {\n\t\tif len(pkgSpecs) == 0 {\n\t\t\tpkgSpecs = []*v6.PackageSpecifier{v6.AnyPackageSpecified}\n\t\t}\n\n\t\tif len(cpeSpecs) == 0 {\n\t\t\tcpeSpecs = []*v6.PackageSpecifier{v6.AnyPackageSpecified}\n\t\t}\n\t}\n\n\t// we have multiple return points that return actual values, using a defer to decorate any given results\n\t// ensures that all paths are handled the same way.\n\tdefer func() {\n\t\tfor i := range allAffectedPkgs {\n\t\t\tdecorateVulnerabilities(reader, &allAffectedPkgs[i])\n\t\t}\n\n\t\tfor i := range allAffectedCPEs {\n\t\t\tdecorateVulnerabilities(reader, &allAffectedCPEs[i])\n\t\t}\n\t}()\n\n\tfor i := range pkgSpecs {\n\t\tpkgSpec := pkgSpecs[i]\n\n\t\tlog.WithFields(\"vuln\", vulnSpecs, \"pkg\", pkgSpec, \"os\", osSpecs).Debug(\"searching for affected packages\")\n\n\t\taffectedPkgs, err := reader.GetAffectedPackages(pkgSpec, &v6.GetPackageOptions{\n\t\t\tPreloadOS:             true,\n\t\t\tPreloadPackage:        true,\n\t\t\tPreloadPackageCPEs:    false,\n\t\t\tPreloadVulnerability:  true,\n\t\t\tPreloadBlob:           true,\n\t\t\tOSs:                   osSpecs,\n\t\t\tVulnerabilities:       vulnSpecs,\n\t\t\tAllowBroadCPEMatching: config.AllowBroadCPEMatching,\n\t\t\tLimit:                 config.RecordLimit,\n\t\t})\n\n\t\tfor i := range affectedPkgs {\n\t\t\tallAffectedPkgs = append(allAffectedPkgs, affectedPackageWithDecorations{\n\t\t\t\tAffectedPackageHandle: affectedPkgs[i],\n\t\t\t})\n\t\t}\n\n\t\tif err != nil {\n\t\t\tif errors.Is(err, v6.ErrLimitReached) {\n\t\t\t\treturn allAffectedPkgs, allAffectedCPEs, err\n\t\t\t}\n\t\t\treturn nil, nil, fmt.Errorf(\"unable to get affected packages for %s: %w\", vulnSpecs, err)\n\t\t}\n\t}\n\n\tif osSpecs.IsAny() {\n\t\tfor i := range cpeSpecs {\n\t\t\tcpeSpec := cpeSpecs[i]\n\t\t\tvar searchCPE *cpe.Attributes\n\t\t\tif cpeSpec != nil {\n\t\t\t\tsearchCPE = cpeSpec.CPE\n\t\t\t}\n\n\t\t\tlog.WithFields(\"vuln\", vulnSpecs, \"cpe\", cpeSpec).Debug(\"searching for affected packages\")\n\n\t\t\taffectedCPEs, err := reader.GetAffectedCPEs(searchCPE, &v6.GetCPEOptions{\n\t\t\t\tPreloadCPE:            true,\n\t\t\t\tPreloadVulnerability:  true,\n\t\t\t\tPreloadBlob:           true,\n\t\t\t\tVulnerabilities:       vulnSpecs,\n\t\t\t\tAllowBroadCPEMatching: config.AllowBroadCPEMatching,\n\t\t\t\tLimit:                 config.RecordLimit,\n\t\t\t})\n\n\t\t\tfor i := range affectedCPEs {\n\t\t\t\tallAffectedCPEs = append(allAffectedCPEs, affectedCPEWithDecorations{\n\t\t\t\t\tAffectedCPEHandle: affectedCPEs[i],\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif err != nil {\n\t\t\t\tif errors.Is(err, v6.ErrLimitReached) {\n\t\t\t\t\treturn allAffectedPkgs, allAffectedCPEs, err\n\t\t\t\t}\n\t\t\t\treturn nil, nil, fmt.Errorf(\"unable to get affected cpes for %s: %w\", vulnSpecs, err)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn allAffectedPkgs, allAffectedCPEs, nil\n}\n"
  },
  {
    "path": "cmd/grype/cli/commands/internal/dbsearch/affected_packages_test.go",
    "content": "package dbsearch\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/mock\"\n\t\"github.com/stretchr/testify/require\"\n\n\tv6 \"github.com/anchore/grype/grype/db/v6\"\n\t\"github.com/anchore/syft/syft/cpe\"\n)\n\nfunc TestAffectedPackageTableRowMarshalJSON(t *testing.T) {\n\trow := AffectedPackage{\n\t\tVulnerability: VulnerabilityInfo{\n\t\t\tVulnerabilityBlob: v6.VulnerabilityBlob{\n\t\t\t\tID:          \"CVE-1234-5678\",\n\t\t\t\tDescription: \"Test vulnerability\",\n\t\t\t},\n\t\t\tProvider:      \"provider1\",\n\t\t\tStatus:        \"active\",\n\t\t\tPublishedDate: ptr(time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC)),\n\t\t\tModifiedDate:  ptr(time.Date(2023, 2, 1, 0, 0, 0, 0, time.UTC)),\n\t\t\tKnownExploited: []KnownExploited{\n\t\t\t\t{\n\t\t\t\t\tCVE:                        \"CVE-1234-5678\",\n\t\t\t\t\tVendorProject:              \"LinuxFoundation\",\n\t\t\t\t\tProduct:                    \"Linux\",\n\t\t\t\t\tDateAdded:                  \"2025-02-02\",\n\t\t\t\t\tRequiredAction:             \"Yes\",\n\t\t\t\t\tDueDate:                    \"2025-02-02\",\n\t\t\t\t\tKnownRansomwareCampaignUse: \"Known\",\n\t\t\t\t\tNotes:                      \"note!\",\n\t\t\t\t\tURLs:                       []string{\"https://example.com\"},\n\t\t\t\t\tCWEs:                       []string{\"CWE-1234\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tEPSS: []EPSS{\n\t\t\t\t{\n\t\t\t\t\tCVE:        \"CVE-1234-5678\",\n\t\t\t\t\tEPSS:       0.893,\n\t\t\t\t\tPercentile: 0.99,\n\t\t\t\t\tDate:       \"2025-02-24\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tAffectedPackageInfo: AffectedPackageInfo{\n\t\t\tPackage:   &Package{Name: \"pkg1\", Ecosystem: \"ecosystem1\"},\n\t\t\tCPE:       &CPE{Part: \"a\", Vendor: \"vendor1\", Product: \"product1\"},\n\t\t\tNamespace: \"namespace1\",\n\t\t\tDetail: v6.PackageBlob{\n\t\t\t\tCVEs: []string{\"CVE-1234-5678\"},\n\t\t\t\tQualifiers: &v6.PackageQualifiers{\n\t\t\t\t\tRpmModularity: ptr(\"modularity\"),\n\t\t\t\t\tPlatformCPEs:  []string{\"platform-cpe-1\"},\n\t\t\t\t},\n\t\t\t\tRanges: []v6.Range{\n\t\t\t\t\t{\n\t\t\t\t\t\tVersion: v6.Version{\n\t\t\t\t\t\t\tType:       \"semver\",\n\t\t\t\t\t\t\tConstraint: \">=1.0.0, <2.0.0\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tFix: &v6.Fix{\n\t\t\t\t\t\t\tVersion: \"1.2.0\",\n\t\t\t\t\t\t\tState:   \"fixed\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tbuf := bytes.Buffer{}\n\tenc := json.NewEncoder(&buf)\n\tenc.SetIndent(\"\", \"  \")\n\tenc.SetEscapeHTML(false)\n\terr := enc.Encode(row)\n\trequire.NoError(t, err)\n\n\texpectedJSON := `{\n  \"vulnerability\": {\n    \"id\": \"CVE-1234-5678\",\n    \"description\": \"Test vulnerability\",\n    \"provider\": \"provider1\",\n    \"status\": \"active\",\n    \"published_date\": \"2023-01-01T00:00:00Z\",\n    \"modified_date\": \"2023-02-01T00:00:00Z\",\n    \"known_exploited\": [\n      {\n        \"cve\": \"CVE-1234-5678\",\n        \"vendor_project\": \"LinuxFoundation\",\n        \"product\": \"Linux\",\n        \"date_added\": \"2025-02-02\",\n        \"required_action\": \"Yes\",\n        \"due_date\": \"2025-02-02\",\n        \"known_ransomware_campaign_use\": \"Known\",\n        \"notes\": \"note!\",\n        \"urls\": [\n          \"https://example.com\"\n        ],\n        \"cwes\": [\n          \"CWE-1234\"\n        ]\n      }\n    ],\n    \"epss\": [\n      {\n        \"cve\": \"CVE-1234-5678\",\n        \"epss\": 0.893,\n        \"percentile\": 0.99,\n        \"date\": \"2025-02-24\"\n      }\n    ]\n  },\n  \"package\": {\n    \"name\": \"pkg1\",\n    \"ecosystem\": \"ecosystem1\"\n  },\n  \"cpe\": \"cpe:2.3:a:vendor1:product1:*:*:*:*:*:*:*:*\",\n  \"namespace\": \"namespace1\",\n  \"detail\": {\n    \"cves\": [\n      \"CVE-1234-5678\"\n    ],\n    \"qualifiers\": {\n      \"rpm_modularity\": \"modularity\",\n      \"platform_cpes\": [\n        \"platform-cpe-1\"\n      ]\n    },\n    \"ranges\": [\n      {\n        \"version\": {\n          \"type\": \"semver\",\n          \"constraint\": \">=1.0.0, <2.0.0\"\n        },\n        \"fix\": {\n          \"version\": \"1.2.0\",\n          \"state\": \"fixed\"\n        }\n      }\n    ]\n  }\n}\n`\n\n\tif diff := cmp.Diff(expectedJSON, buf.String()); diff != \"\" {\n\t\tt.Errorf(\"unexpected JSON (-want +got):\\n%s\", diff)\n\t}\n}\n\nfunc TestNewAffectedPackageRows(t *testing.T) {\n\taffectedPkgs := []affectedPackageWithDecorations{\n\t\t{\n\t\t\tAffectedPackageHandle: v6.AffectedPackageHandle{\n\t\t\t\tPackage: &v6.Package{Name: \"pkg1\", Ecosystem: \"ecosystem1\"},\n\t\t\t\tOperatingSystem: &v6.OperatingSystem{\n\t\t\t\t\tName:         \"Linux\",\n\t\t\t\t\tMajorVersion: \"5\",\n\t\t\t\t\tMinorVersion: \"10\",\n\t\t\t\t},\n\t\t\t\tVulnerability: &v6.VulnerabilityHandle{\n\t\t\t\t\tName:          \"CVE-1234-5678\",\n\t\t\t\t\tProvider:      &v6.Provider{ID: \"provider1\"},\n\t\t\t\t\tStatus:        \"active\",\n\t\t\t\t\tPublishedDate: ptr(time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC)),\n\t\t\t\t\tModifiedDate:  ptr(time.Date(2023, 2, 1, 0, 0, 0, 0, time.UTC)),\n\t\t\t\t\tBlobValue: &v6.VulnerabilityBlob{\n\t\t\t\t\t\tDescription: \"Test vulnerability\",\n\t\t\t\t\t\tSeverities: []v6.Severity{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tScheme: \"CVSS_V3\",\n\t\t\t\t\t\t\t\tValue: CVSSSeverity{\n\t\t\t\t\t\t\t\t\tVector:  \"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H\",\n\t\t\t\t\t\t\t\t\tVersion: \"3.1\",\n\t\t\t\t\t\t\t\t\tMetrics: CvssMetrics{\n\t\t\t\t\t\t\t\t\t\tBaseScore: 9.8,\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tSource: \"nvd@nist.gov\",\n\t\t\t\t\t\t\t\tRank:   1,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tBlobValue: &v6.PackageBlob{\n\t\t\t\t\tCVEs: []string{\"CVE-1234-5678\"},\n\t\t\t\t\tQualifiers: &v6.PackageQualifiers{\n\t\t\t\t\t\tRpmModularity: ptr(\"modularity\"),\n\t\t\t\t\t\tPlatformCPEs:  []string{\"platform-cpe-1\"},\n\t\t\t\t\t},\n\t\t\t\t\tRanges: []v6.Range{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tVersion: v6.Version{\n\t\t\t\t\t\t\t\tType:       \"semver\",\n\t\t\t\t\t\t\t\tConstraint: \">=1.0.0, <2.0.0\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tFix: &v6.Fix{\n\t\t\t\t\t\t\t\tVersion: \"1.2.0\",\n\t\t\t\t\t\t\t\tState:   \"fixed\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tvulnerabilityDecorations: vulnerabilityDecorations{\n\t\t\t\tKnownExploited: []KnownExploited{\n\t\t\t\t\t{\n\t\t\t\t\t\tCVE:                        \"CVE-1234-5678\",\n\t\t\t\t\t\tVendorProject:              \"LinuxFoundation\",\n\t\t\t\t\t\tProduct:                    \"Linux\",\n\t\t\t\t\t\tDateAdded:                  \"2025-02-02\",\n\t\t\t\t\t\tRequiredAction:             \"Yes\",\n\t\t\t\t\t\tDueDate:                    \"2025-02-02\",\n\t\t\t\t\t\tKnownRansomwareCampaignUse: \"Known\",\n\t\t\t\t\t\tNotes:                      \"note!\",\n\t\t\t\t\t\tURLs:                       []string{\"https://example.com\"},\n\t\t\t\t\t\tCWEs:                       []string{\"CWE-1234\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tEPSS: []EPSS{\n\t\t\t\t\t{\n\t\t\t\t\t\tCVE:        \"CVE-1234-5678\",\n\t\t\t\t\t\tEPSS:       0.893,\n\t\t\t\t\t\tPercentile: 0.99,\n\t\t\t\t\t\tDate:       \"2025-02-24\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\taffectedCPEs := []affectedCPEWithDecorations{\n\t\t{\n\t\t\tAffectedCPEHandle: v6.AffectedCPEHandle{\n\t\t\t\tCPE: &v6.Cpe{Part: \"a\", Vendor: \"vendor1\", Product: \"product1\"},\n\t\t\t\tVulnerability: &v6.VulnerabilityHandle{\n\t\t\t\t\tName:      \"CVE-9876-5432\",\n\t\t\t\t\tProvider:  &v6.Provider{ID: \"provider2\"},\n\t\t\t\t\tBlobValue: &v6.VulnerabilityBlob{Description: \"CPE vulnerability description\"},\n\t\t\t\t},\n\t\t\t\tBlobValue: &v6.PackageBlob{\n\t\t\t\t\tCVEs: []string{\"CVE-9876-5432\"},\n\t\t\t\t\tRanges: []v6.Range{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tVersion: v6.Version{\n\t\t\t\t\t\t\t\tType:       \"rpm\",\n\t\t\t\t\t\t\t\tConstraint: \">=2.0.0, <3.0.0\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tFix: &v6.Fix{\n\t\t\t\t\t\t\t\tVersion: \"2.5.0\",\n\t\t\t\t\t\t\t\tState:   \"fixed\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tvulnerabilityDecorations: vulnerabilityDecorations{\n\t\t\t\tKnownExploited: []KnownExploited{\n\t\t\t\t\t{\n\t\t\t\t\t\tCVE:                        \"CVE-9876-5432\",\n\t\t\t\t\t\tVendorProject:              \"vendor1\",\n\t\t\t\t\t\tProduct:                    \"product1\",\n\t\t\t\t\t\tDateAdded:                  \"2025-02-03\",\n\t\t\t\t\t\tRequiredAction:             \"Yes\",\n\t\t\t\t\t\tDueDate:                    \"2025-02-03\",\n\t\t\t\t\t\tKnownRansomwareCampaignUse: \"Known\",\n\t\t\t\t\t\tNotes:                      \"note!\",\n\t\t\t\t\t\tURLs:                       []string{\"https://example.com\"},\n\t\t\t\t\t\tCWEs:                       []string{\"CWE-5678\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tEPSS: []EPSS{\n\t\t\t\t\t{\n\t\t\t\t\t\tCVE:        \"CVE-9876-5432\",\n\t\t\t\t\t\tEPSS:       0.938,\n\t\t\t\t\t\tPercentile: 0.9222,\n\t\t\t\t\t\tDate:       \"2025-02-25\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\trows := newAffectedPackageRows(affectedPkgs, affectedCPEs)\n\texpected := []AffectedPackage{\n\t\t{\n\t\t\tVulnerability: VulnerabilityInfo{\n\t\t\t\tVulnerabilityBlob: v6.VulnerabilityBlob{\n\t\t\t\t\tDescription: \"Test vulnerability\",\n\t\t\t\t\tSeverities: []v6.Severity{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tScheme: \"CVSS_V3\",\n\t\t\t\t\t\t\tValue: CVSSSeverity{\n\t\t\t\t\t\t\t\tVector:  \"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H\",\n\t\t\t\t\t\t\t\tVersion: \"3.1\",\n\t\t\t\t\t\t\t\tMetrics: CvssMetrics{\n\t\t\t\t\t\t\t\t\tBaseScore: 9.8,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tSource: \"nvd@nist.gov\",\n\t\t\t\t\t\t\tRank:   1,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tSeverity:      \"critical\",\n\t\t\t\tProvider:      \"provider1\",\n\t\t\t\tStatus:        \"active\",\n\t\t\t\tPublishedDate: ptr(time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC)),\n\t\t\t\tModifiedDate:  ptr(time.Date(2023, 2, 1, 0, 0, 0, 0, time.UTC)),\n\t\t\t\tKnownExploited: []KnownExploited{\n\t\t\t\t\t{\n\t\t\t\t\t\tCVE:                        \"CVE-1234-5678\",\n\t\t\t\t\t\tVendorProject:              \"LinuxFoundation\",\n\t\t\t\t\t\tProduct:                    \"Linux\",\n\t\t\t\t\t\tDateAdded:                  \"2025-02-02\",\n\t\t\t\t\t\tRequiredAction:             \"Yes\",\n\t\t\t\t\t\tDueDate:                    \"2025-02-02\",\n\t\t\t\t\t\tKnownRansomwareCampaignUse: \"Known\",\n\t\t\t\t\t\tNotes:                      \"note!\",\n\t\t\t\t\t\tURLs:                       []string{\"https://example.com\"},\n\t\t\t\t\t\tCWEs:                       []string{\"CWE-1234\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tEPSS: []EPSS{\n\t\t\t\t\t{\n\t\t\t\t\t\tCVE:        \"CVE-1234-5678\",\n\t\t\t\t\t\tEPSS:       0.893,\n\t\t\t\t\t\tPercentile: 0.99,\n\t\t\t\t\t\tDate:       \"2025-02-24\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tAffectedPackageInfo: AffectedPackageInfo{\n\t\t\t\tOS:        &OperatingSystem{Name: \"Linux\", Version: \"5.10\"},\n\t\t\t\tPackage:   &Package{Name: \"pkg1\", Ecosystem: \"ecosystem1\"},\n\t\t\t\tNamespace: \"provider1:distro:Linux:5.10\",\n\t\t\t\tDetail: v6.PackageBlob{\n\t\t\t\t\tCVEs: []string{\"CVE-1234-5678\"},\n\t\t\t\t\tQualifiers: &v6.PackageQualifiers{\n\t\t\t\t\t\tRpmModularity: ptr(\"modularity\"),\n\t\t\t\t\t\tPlatformCPEs:  []string{\"platform-cpe-1\"},\n\t\t\t\t\t},\n\t\t\t\t\tRanges: []v6.Range{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tVersion: v6.Version{\n\t\t\t\t\t\t\t\tType:       \"semver\",\n\t\t\t\t\t\t\t\tConstraint: \">=1.0.0, <2.0.0\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tFix: &v6.Fix{\n\t\t\t\t\t\t\t\tVersion: \"1.2.0\",\n\t\t\t\t\t\t\t\tState:   \"fixed\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tVulnerability: VulnerabilityInfo{\n\t\t\t\tVulnerabilityBlob: v6.VulnerabilityBlob{Description: \"CPE vulnerability description\"},\n\t\t\t\tSeverity:          \"unknown\",\n\t\t\t\tProvider:          \"provider2\",\n\t\t\t\tKnownExploited: []KnownExploited{\n\t\t\t\t\t{\n\t\t\t\t\t\tCVE:                        \"CVE-9876-5432\",\n\t\t\t\t\t\tVendorProject:              \"vendor1\",\n\t\t\t\t\t\tProduct:                    \"product1\",\n\t\t\t\t\t\tDateAdded:                  \"2025-02-03\",\n\t\t\t\t\t\tRequiredAction:             \"Yes\",\n\t\t\t\t\t\tDueDate:                    \"2025-02-03\",\n\t\t\t\t\t\tKnownRansomwareCampaignUse: \"Known\",\n\t\t\t\t\t\tNotes:                      \"note!\",\n\t\t\t\t\t\tURLs:                       []string{\"https://example.com\"},\n\t\t\t\t\t\tCWEs:                       []string{\"CWE-5678\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tEPSS: []EPSS{\n\t\t\t\t\t{\n\t\t\t\t\t\tCVE:        \"CVE-9876-5432\",\n\t\t\t\t\t\tEPSS:       0.938,\n\t\t\t\t\t\tPercentile: 0.9222,\n\t\t\t\t\t\tDate:       \"2025-02-25\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tAffectedPackageInfo: AffectedPackageInfo{\n\t\t\t\tCPE:       &CPE{Part: \"a\", Vendor: \"vendor1\", Product: \"product1\"},\n\t\t\t\tNamespace: \"provider2:cpe\",\n\t\t\t\tDetail: v6.PackageBlob{\n\t\t\t\t\tCVEs: []string{\"CVE-9876-5432\"},\n\t\t\t\t\tRanges: []v6.Range{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tVersion: v6.Version{\n\t\t\t\t\t\t\t\tType:       \"rpm\",\n\t\t\t\t\t\t\t\tConstraint: \">=2.0.0, <3.0.0\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tFix: &v6.Fix{\n\t\t\t\t\t\t\t\tVersion: \"2.5.0\",\n\t\t\t\t\t\t\t\tState:   \"fixed\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tif diff := cmp.Diff(expected, rows, cmpOpts()...); diff != \"\" {\n\t\tt.Errorf(\"unexpected rows (-want +got):\\n%s\", diff)\n\t}\n}\n\nfunc TestAffectedPackages(t *testing.T) {\n\tmockReader := new(affectedMockReader)\n\n\tmockReader.On(\"GetAffectedPackages\", mock.Anything, mock.Anything).Return([]v6.AffectedPackageHandle{\n\t\t{\n\t\t\tPackage: &v6.Package{Name: \"pkg1\", Ecosystem: \"ecosystem1\"},\n\t\t\tOperatingSystem: &v6.OperatingSystem{\n\t\t\t\tName:         \"Linux\",\n\t\t\t\tMajorVersion: \"5\",\n\t\t\t\tMinorVersion: \"10\",\n\t\t\t},\n\t\t\tVulnerability: &v6.VulnerabilityHandle{\n\t\t\t\tName:          \"CVE-1234-5678\",\n\t\t\t\tProvider:      &v6.Provider{ID: \"provider1\"},\n\t\t\t\tStatus:        \"active\",\n\t\t\t\tPublishedDate: ptr(time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC)),\n\t\t\t\tModifiedDate:  ptr(time.Date(2023, 2, 1, 0, 0, 0, 0, time.UTC)),\n\t\t\t\tBlobValue:     &v6.VulnerabilityBlob{Description: \"Test vulnerability\"},\n\t\t\t},\n\t\t\tBlobValue: &v6.PackageBlob{\n\t\t\t\tCVEs: []string{\"CVE-1234-5678\"},\n\t\t\t\tRanges: []v6.Range{\n\t\t\t\t\t{\n\t\t\t\t\t\tVersion: v6.Version{\n\t\t\t\t\t\t\tType:       \"semver\",\n\t\t\t\t\t\t\tConstraint: \">=1.0.0, <2.0.0\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tFix: &v6.Fix{\n\t\t\t\t\t\t\tVersion: \"1.2.0\",\n\t\t\t\t\t\t\tState:   \"fixed\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}, nil)\n\n\tmockReader.On(\"GetAffectedCPEs\", mock.Anything, mock.Anything).Return([]v6.AffectedCPEHandle{\n\t\t{\n\t\t\tCPE: &v6.Cpe{Part: \"a\", Vendor: \"vendor1\", Product: \"product1\"},\n\t\t\tVulnerability: &v6.VulnerabilityHandle{\n\t\t\t\tName:      \"CVE-9876-5432\",\n\t\t\t\tProvider:  &v6.Provider{ID: \"provider2\"},\n\t\t\t\tBlobValue: &v6.VulnerabilityBlob{Description: \"CPE vulnerability description\"},\n\t\t\t},\n\t\t\tBlobValue: &v6.PackageBlob{\n\t\t\t\tCVEs: []string{\"CVE-9876-5432\"},\n\t\t\t\tRanges: []v6.Range{\n\t\t\t\t\t{\n\t\t\t\t\t\tVersion: v6.Version{\n\t\t\t\t\t\t\tType:       \"rpm\",\n\t\t\t\t\t\t\tConstraint: \">=2.0.0, <3.0.0\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tFix: &v6.Fix{\n\t\t\t\t\t\t\tVersion: \"2.5.0\",\n\t\t\t\t\t\t\tState:   \"fixed\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}, nil)\n\n\tmockReader.On(\"GetKnownExploitedVulnerabilities\", \"CVE-1234-5678\").Return([]v6.KnownExploitedVulnerabilityHandle{\n\t\t{\n\t\t\tCve: \"CVE-1234-5678\",\n\t\t\tBlobValue: &v6.KnownExploitedVulnerabilityBlob{\n\t\t\t\tCve:                        \"CVE-1234-5678\",\n\t\t\t\tVendorProject:              \"LinuxFoundation\",\n\t\t\t\tProduct:                    \"Linux\",\n\t\t\t\tDateAdded:                  ptr(time.Date(2025, 2, 2, 0, 0, 0, 0, time.UTC)),\n\t\t\t\tRequiredAction:             \"Yes\",\n\t\t\t\tDueDate:                    ptr(time.Date(2025, 2, 2, 0, 0, 0, 0, time.UTC)),\n\t\t\t\tKnownRansomwareCampaignUse: \"Known\",\n\t\t\t\tNotes:                      \"note!\",\n\t\t\t\tURLs:                       []string{\"https://example.com\"},\n\t\t\t\tCWEs:                       []string{\"CWE-1234\"},\n\t\t\t},\n\t\t},\n\t}, nil)\n\n\tmockReader.On(\"GetKnownExploitedVulnerabilities\", \"CVE-9876-5432\").Return([]v6.KnownExploitedVulnerabilityHandle{\n\t\t{\n\t\t\tCve: \"CVE-9876-5432\",\n\t\t\tBlobValue: &v6.KnownExploitedVulnerabilityBlob{\n\t\t\t\tCve:                        \"CVE-9876-5432\",\n\t\t\t\tVendorProject:              \"vendor1\",\n\t\t\t\tProduct:                    \"product1\",\n\t\t\t\tDateAdded:                  ptr(time.Date(2025, 2, 3, 0, 0, 0, 0, time.UTC)),\n\t\t\t\tRequiredAction:             \"Yes\",\n\t\t\t\tDueDate:                    ptr(time.Date(2025, 2, 3, 0, 0, 0, 0, time.UTC)),\n\t\t\t\tKnownRansomwareCampaignUse: \"Known\",\n\t\t\t\tNotes:                      \"note!\",\n\t\t\t\tURLs:                       []string{\"https://example.com\"},\n\t\t\t\tCWEs:                       []string{\"CWE-5678\"},\n\t\t\t},\n\t\t},\n\t}, nil)\n\n\tmockReader.On(\"GetEpss\", \"CVE-1234-5678\").Return([]v6.EpssHandle{\n\t\t{\n\t\t\tCve:        \"CVE-1234-5678\",\n\t\t\tEpss:       0.893,\n\t\t\tPercentile: 0.99,\n\t\t\tDate:       time.Date(2025, 2, 24, 0, 0, 0, 0, time.UTC),\n\t\t},\n\t}, nil)\n\n\tmockReader.On(\"GetEpss\", \"CVE-9876-5432\").Return([]v6.EpssHandle{\n\t\t{\n\t\t\tCve:        \"CVE-9876-5432\",\n\t\t\tEpss:       0.938,\n\t\t\tPercentile: 0.9222,\n\t\t\tDate:       time.Date(2025, 2, 25, 0, 0, 0, 0, time.UTC),\n\t\t},\n\t}, nil)\n\n\tcriteria := AffectedPackagesOptions{\n\t\tVulnerability: v6.VulnerabilitySpecifiers{\n\t\t\t{Name: \"CVE-1234-5678\"},\n\t\t},\n\t}\n\n\tresults, err := FindAffectedPackages(mockReader, criteria)\n\trequire.NoError(t, err)\n\n\texpected := []AffectedPackage{\n\t\t{\n\t\t\tVulnerability: VulnerabilityInfo{\n\t\t\t\tVulnerabilityBlob: v6.VulnerabilityBlob{Description: \"Test vulnerability\"},\n\t\t\t\tSeverity:          \"unknown\",\n\t\t\t\tProvider:          \"provider1\",\n\t\t\t\tStatus:            \"active\",\n\t\t\t\tPublishedDate:     ptr(time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC)),\n\t\t\t\tModifiedDate:      ptr(time.Date(2023, 2, 1, 0, 0, 0, 0, time.UTC)),\n\t\t\t\tKnownExploited: []KnownExploited{\n\t\t\t\t\t{\n\t\t\t\t\t\tCVE:                        \"CVE-1234-5678\",\n\t\t\t\t\t\tVendorProject:              \"LinuxFoundation\",\n\t\t\t\t\t\tProduct:                    \"Linux\",\n\t\t\t\t\t\tDateAdded:                  \"2025-02-02\",\n\t\t\t\t\t\tRequiredAction:             \"Yes\",\n\t\t\t\t\t\tDueDate:                    \"2025-02-02\",\n\t\t\t\t\t\tKnownRansomwareCampaignUse: \"Known\",\n\t\t\t\t\t\tNotes:                      \"note!\",\n\t\t\t\t\t\tURLs:                       []string{\"https://example.com\"},\n\t\t\t\t\t\tCWEs:                       []string{\"CWE-1234\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tEPSS: []EPSS{\n\t\t\t\t\t{\n\t\t\t\t\t\tCVE:        \"CVE-1234-5678\",\n\t\t\t\t\t\tEPSS:       0.893,\n\t\t\t\t\t\tPercentile: 0.99,\n\t\t\t\t\t\tDate:       \"2025-02-24\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tAffectedPackageInfo: AffectedPackageInfo{\n\t\t\t\tOS:        &OperatingSystem{Name: \"Linux\", Version: \"5.10\"},\n\t\t\t\tPackage:   &Package{Name: \"pkg1\", Ecosystem: \"ecosystem1\"},\n\t\t\t\tNamespace: \"provider1:distro:Linux:5.10\",\n\t\t\t\tDetail: v6.PackageBlob{\n\t\t\t\t\tCVEs: []string{\"CVE-1234-5678\"},\n\t\t\t\t\tRanges: []v6.Range{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tVersion: v6.Version{\n\t\t\t\t\t\t\t\tType:       \"semver\",\n\t\t\t\t\t\t\t\tConstraint: \">=1.0.0, <2.0.0\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tFix: &v6.Fix{\n\t\t\t\t\t\t\t\tVersion: \"1.2.0\",\n\t\t\t\t\t\t\t\tState:   \"fixed\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tVulnerability: VulnerabilityInfo{\n\t\t\t\tVulnerabilityBlob: v6.VulnerabilityBlob{Description: \"CPE vulnerability description\"},\n\t\t\t\tSeverity:          \"unknown\",\n\t\t\t\tProvider:          \"provider2\",\n\t\t\t\tKnownExploited: []KnownExploited{\n\t\t\t\t\t{\n\t\t\t\t\t\tCVE:                        \"CVE-9876-5432\",\n\t\t\t\t\t\tVendorProject:              \"vendor1\",\n\t\t\t\t\t\tProduct:                    \"product1\",\n\t\t\t\t\t\tDateAdded:                  \"2025-02-03\",\n\t\t\t\t\t\tRequiredAction:             \"Yes\",\n\t\t\t\t\t\tDueDate:                    \"2025-02-03\",\n\t\t\t\t\t\tKnownRansomwareCampaignUse: \"Known\",\n\t\t\t\t\t\tNotes:                      \"note!\",\n\t\t\t\t\t\tURLs:                       []string{\"https://example.com\"},\n\t\t\t\t\t\tCWEs:                       []string{\"CWE-5678\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tEPSS: []EPSS{\n\t\t\t\t\t{\n\t\t\t\t\t\tCVE:        \"CVE-9876-5432\",\n\t\t\t\t\t\tEPSS:       0.938,\n\t\t\t\t\t\tPercentile: 0.9222,\n\t\t\t\t\t\tDate:       \"2025-02-25\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tAffectedPackageInfo: AffectedPackageInfo{\n\t\t\t\tCPE:       &CPE{Part: \"a\", Vendor: \"vendor1\", Product: \"product1\"},\n\t\t\t\tNamespace: \"provider2:cpe\",\n\t\t\t\tDetail: v6.PackageBlob{\n\t\t\t\t\tCVEs: []string{\"CVE-9876-5432\"},\n\t\t\t\t\tRanges: []v6.Range{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tVersion: v6.Version{\n\t\t\t\t\t\t\t\tType:       \"rpm\",\n\t\t\t\t\t\t\t\tConstraint: \">=2.0.0, <3.0.0\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tFix: &v6.Fix{\n\t\t\t\t\t\t\t\tVersion: \"2.5.0\",\n\t\t\t\t\t\t\t\tState:   \"fixed\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tif diff := cmp.Diff(expected, results, cmpOpts()...); diff != \"\" {\n\t\tt.Errorf(\"unexpected results (-want +got):\\n%s\", diff)\n\t}\n}\n\nfunc TestFindAffectedPackages(t *testing.T) {\n\t// this test is not meant to check the correctness of the results relative to the reader but instead make certain\n\t// that the correct calls are made to the reader based on the search criteria (we're wired up correctly).\n\t// Additional verifications are made to check that the combinations of different specs are handled correctly.\n\ttype pkgCall struct {\n\t\tpkg     *v6.PackageSpecifier\n\t\toptions *v6.GetPackageOptions\n\t}\n\n\ttype cpeCall struct {\n\t\tcpe     *cpe.Attributes\n\t\toptions *v6.GetCPEOptions\n\t}\n\n\ttestCases := []struct {\n\t\tname             string\n\t\tconfig           AffectedPackagesOptions\n\t\texpectedPkgCalls []pkgCall\n\t\texpectedCPECalls []cpeCall\n\t\texpectedErr      error\n\t}{\n\t\t{\n\t\t\tname:        \"no search criteria\",\n\t\t\tconfig:      AffectedPackagesOptions{},\n\t\t\texpectedErr: ErrNoSearchCriteria,\n\t\t},\n\t\t{\n\t\t\tname: \"os spec alone is not enough\",\n\t\t\tconfig: AffectedPackagesOptions{\n\t\t\t\tOS: v6.OSSpecifiers{\n\t\t\t\t\t{Name: \"ubuntu\", MajorVersion: \"20\", MinorVersion: \"04\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedErr: ErrNoSearchCriteria,\n\t\t},\n\t\t{\n\t\t\tname: \"vuln spec provided\",\n\t\t\tconfig: AffectedPackagesOptions{\n\t\t\t\tVulnerability: v6.VulnerabilitySpecifiers{\n\t\t\t\t\t{Name: \"CVE-2023-0001\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedPkgCalls: []pkgCall{\n\t\t\t\t{\n\t\t\t\t\tpkg: nil,\n\t\t\t\t\toptions: &v6.GetPackageOptions{\n\t\t\t\t\t\tPreloadOS:            true,\n\t\t\t\t\t\tPreloadPackage:       true,\n\t\t\t\t\t\tPreloadVulnerability: true,\n\t\t\t\t\t\tPreloadBlob:          true,\n\t\t\t\t\t\tVulnerabilities: v6.VulnerabilitySpecifiers{\n\t\t\t\t\t\t\t{Name: \"CVE-2023-0001\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tLimit: 0,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedCPECalls: []cpeCall{\n\t\t\t\t{\n\t\t\t\t\tcpe: nil,\n\t\t\t\t\toptions: &v6.GetCPEOptions{\n\t\t\t\t\t\tPreloadCPE:           true,\n\t\t\t\t\t\tPreloadVulnerability: true,\n\t\t\t\t\t\tPreloadBlob:          true,\n\t\t\t\t\t\tVulnerabilities: v6.VulnerabilitySpecifiers{\n\t\t\t\t\t\t\t{Name: \"CVE-2023-0001\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tLimit: 0,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"only cpe spec provided\",\n\t\t\tconfig: AffectedPackagesOptions{\n\t\t\t\tPackage: v6.PackageSpecifiers{\n\t\t\t\t\t{CPE: &cpe.Attributes{Part: \"a\", Vendor: \"vendor1\", Product: \"product1\"}},\n\t\t\t\t},\n\t\t\t\tCPE: v6.PackageSpecifiers{\n\t\t\t\t\t{CPE: &cpe.Attributes{Part: \"a\", Vendor: \"vendor2\", Product: \"product2\"}},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedPkgCalls: []pkgCall{\n\t\t\t\t{\n\t\t\t\t\tpkg: &v6.PackageSpecifier{CPE: &cpe.Attributes{Part: \"a\", Vendor: \"vendor1\", Product: \"product1\"}},\n\t\t\t\t\toptions: &v6.GetPackageOptions{\n\t\t\t\t\t\tPreloadOS:            true,\n\t\t\t\t\t\tPreloadPackage:       true,\n\t\t\t\t\t\tPreloadVulnerability: true,\n\t\t\t\t\t\tPreloadBlob:          true,\n\t\t\t\t\t\tVulnerabilities:      nil,\n\t\t\t\t\t\tLimit:                0,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedCPECalls: []cpeCall{\n\t\t\t\t{\n\t\t\t\t\tcpe: &cpe.Attributes{Part: \"a\", Vendor: \"vendor2\", Product: \"product2\"},\n\t\t\t\t\toptions: &v6.GetCPEOptions{\n\t\t\t\t\t\tPreloadCPE:           true,\n\t\t\t\t\t\tPreloadVulnerability: true,\n\t\t\t\t\t\tPreloadBlob:          true,\n\t\t\t\t\t\tVulnerabilities:      nil,\n\t\t\t\t\t\tLimit:                0,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedErr: nil,\n\t\t},\n\t\t{\n\t\t\tname: \"cpe + os spec provided\",\n\t\t\tconfig: AffectedPackagesOptions{\n\t\t\t\tPackage: v6.PackageSpecifiers{\n\t\t\t\t\t{CPE: &cpe.Attributes{Part: \"a\", Vendor: \"vendor1\", Product: \"product1\"}},\n\t\t\t\t},\n\t\t\t\tCPE: v6.PackageSpecifiers{\n\t\t\t\t\t{CPE: &cpe.Attributes{Part: \"a\", Vendor: \"vendor2\", Product: \"product2\"}},\n\t\t\t\t},\n\t\t\t\tOS: v6.OSSpecifiers{\n\t\t\t\t\t{Name: \"debian\", MajorVersion: \"10\"}, // this prevents an agnostic CPE search\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedPkgCalls: []pkgCall{\n\t\t\t\t{\n\t\t\t\t\tpkg: &v6.PackageSpecifier{CPE: &cpe.Attributes{Part: \"a\", Vendor: \"vendor1\", Product: \"product1\"}},\n\t\t\t\t\toptions: &v6.GetPackageOptions{\n\t\t\t\t\t\tPreloadOS:            true,\n\t\t\t\t\t\tPreloadPackage:       true,\n\t\t\t\t\t\tPreloadVulnerability: true,\n\t\t\t\t\t\tPreloadBlob:          true,\n\t\t\t\t\t\tVulnerabilities:      nil,\n\t\t\t\t\t\tOSs: v6.OSSpecifiers{\n\t\t\t\t\t\t\t{Name: \"debian\", MajorVersion: \"10\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tLimit: 0,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedCPECalls: nil,\n\t\t\texpectedErr:      nil,\n\t\t},\n\t\t{\n\t\t\tname: \"pkg spec provided\",\n\t\t\tconfig: AffectedPackagesOptions{\n\t\t\t\tPackage: v6.PackageSpecifiers{\n\t\t\t\t\t{Name: \"test-package\", Ecosystem: \"npm\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedPkgCalls: []pkgCall{\n\t\t\t\t{\n\t\t\t\t\tpkg: &v6.PackageSpecifier{Name: \"test-package\", Ecosystem: \"npm\"},\n\t\t\t\t\toptions: &v6.GetPackageOptions{\n\t\t\t\t\t\tPreloadOS:            true,\n\t\t\t\t\t\tPreloadPackage:       true,\n\t\t\t\t\t\tPreloadVulnerability: true,\n\t\t\t\t\t\tPreloadBlob:          true,\n\t\t\t\t\t\tVulnerabilities:      nil,\n\t\t\t\t\t\tLimit:                0,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedCPECalls: nil,\n\t\t},\n\n\t\t{\n\t\t\tname: \"pkg and os specs provided\",\n\t\t\tconfig: AffectedPackagesOptions{\n\t\t\t\tPackage: v6.PackageSpecifiers{\n\t\t\t\t\t{Name: \"test-package\", Ecosystem: \"npm\"},\n\t\t\t\t},\n\t\t\t\tOS: v6.OSSpecifiers{\n\t\t\t\t\t{Name: \"debian\", MajorVersion: \"10\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedPkgCalls: []pkgCall{\n\t\t\t\t{\n\t\t\t\t\tpkg: &v6.PackageSpecifier{Name: \"test-package\", Ecosystem: \"npm\"},\n\t\t\t\t\toptions: &v6.GetPackageOptions{\n\t\t\t\t\t\tPreloadOS:            true,\n\t\t\t\t\t\tPreloadPackage:       true,\n\t\t\t\t\t\tPreloadVulnerability: true,\n\t\t\t\t\t\tPreloadBlob:          true,\n\t\t\t\t\t\tOSs: v6.OSSpecifiers{\n\t\t\t\t\t\t\t{Name: \"debian\", MajorVersion: \"10\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tLimit: 0,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedCPECalls: nil,\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tm := new(affectedMockReader)\n\t\t\tdefer m.AssertExpectations(t)\n\n\t\t\tfor _, expected := range tc.expectedPkgCalls {\n\t\t\t\tm.On(\"GetAffectedPackages\", expected.pkg, mock.MatchedBy(func(actual *v6.GetPackageOptions) bool {\n\t\t\t\t\treturn cmp.Equal(actual, expected.options)\n\t\t\t\t})).Return([]v6.AffectedPackageHandle{}, nil).Once()\n\t\t\t}\n\n\t\t\tfor _, expected := range tc.expectedCPECalls {\n\t\t\t\tm.On(\"GetAffectedCPEs\", expected.cpe, mock.MatchedBy(func(actual *v6.GetCPEOptions) bool {\n\t\t\t\t\treturn cmp.Equal(actual, expected.options)\n\t\t\t\t})).Return([]v6.AffectedCPEHandle{}, nil).Once()\n\t\t\t}\n\n\t\t\t_, _, err := findAffectedPackages(m, tc.config)\n\n\t\t\tif tc.expectedErr != nil {\n\t\t\t\trequire.ErrorIs(t, err, tc.expectedErr)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\ntype affectedMockReader struct {\n\tmock.Mock\n}\n\nfunc (m *affectedMockReader) GetAffectedPackages(pkgSpec *v6.PackageSpecifier, options *v6.GetPackageOptions) ([]v6.AffectedPackageHandle, error) {\n\targs := m.Called(pkgSpec, options)\n\treturn args.Get(0).([]v6.AffectedPackageHandle), args.Error(1)\n}\n\nfunc (m *affectedMockReader) GetAffectedCPEs(cpeSpec *cpe.Attributes, options *v6.GetCPEOptions) ([]v6.AffectedCPEHandle, error) {\n\targs := m.Called(cpeSpec, options)\n\treturn args.Get(0).([]v6.AffectedCPEHandle), args.Error(1)\n}\n\nfunc (m *affectedMockReader) GetKnownExploitedVulnerabilities(cve string) ([]v6.KnownExploitedVulnerabilityHandle, error) {\n\targs := m.Called(cve)\n\treturn args.Get(0).([]v6.KnownExploitedVulnerabilityHandle), args.Error(1)\n}\n\nfunc (m *affectedMockReader) GetEpss(cve string) ([]v6.EpssHandle, error) {\n\targs := m.Called(cve)\n\treturn args.Get(0).([]v6.EpssHandle), args.Error(1)\n}\n\nfunc (m *affectedMockReader) GetCWEs(cve string) ([]v6.CWEHandle, error) {\n\targs := m.Called(cve)\n\treturn args.Get(0).([]v6.CWEHandle), args.Error(1)\n}\n\nfunc ptr[T any](t T) *T {\n\treturn &t\n}\n\nfunc TestGetFixStateFromPackageBlob(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tblob     *v6.PackageBlob\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:     \"nil blob returns unknown\",\n\t\t\tblob:     nil,\n\t\t\texpected: \"unknown\",\n\t\t},\n\t\t{\n\t\t\tname:     \"empty blob returns unknown\",\n\t\t\tblob:     &v6.PackageBlob{},\n\t\t\texpected: \"unknown\",\n\t\t},\n\t\t{\n\t\t\tname: \"blob with fixed status\",\n\t\t\tblob: &v6.PackageBlob{\n\t\t\t\tRanges: []v6.Range{\n\t\t\t\t\t{\n\t\t\t\t\t\tFix: &v6.Fix{\n\t\t\t\t\t\t\tState:   v6.FixedStatus,\n\t\t\t\t\t\t\tVersion: \"1.2.3\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: \"fixed\",\n\t\t},\n\t\t{\n\t\t\tname: \"blob with not-fixed status\",\n\t\t\tblob: &v6.PackageBlob{\n\t\t\t\tRanges: []v6.Range{\n\t\t\t\t\t{\n\t\t\t\t\t\tFix: &v6.Fix{\n\t\t\t\t\t\t\tState: v6.NotFixedStatus,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: \"not-fixed\",\n\t\t},\n\t\t{\n\t\t\tname: \"blob with wont-fix status\",\n\t\t\tblob: &v6.PackageBlob{\n\t\t\t\tRanges: []v6.Range{\n\t\t\t\t\t{\n\t\t\t\t\t\tFix: &v6.Fix{\n\t\t\t\t\t\t\tState: v6.WontFixStatus,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: \"wont-fix\",\n\t\t},\n\t\t{\n\t\t\tname: \"blob with no fix returns unknown\",\n\t\t\tblob: &v6.PackageBlob{\n\t\t\t\tRanges: []v6.Range{\n\t\t\t\t\t{\n\t\t\t\t\t\tFix: nil,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: \"unknown\",\n\t\t},\n\t\t{\n\t\t\tname: \"blob with mixed statuses prefers fixed\",\n\t\t\tblob: &v6.PackageBlob{\n\t\t\t\tRanges: []v6.Range{\n\t\t\t\t\t{\n\t\t\t\t\t\tFix: &v6.Fix{\n\t\t\t\t\t\t\tState: v6.NotFixedStatus,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tFix: &v6.Fix{\n\t\t\t\t\t\t\tState:   v6.FixedStatus,\n\t\t\t\t\t\t\tVersion: \"2.0.0\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: \"fixed\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := getFixStateFromPackageBlob(tt.blob)\n\t\t\tassert.Equal(t, tt.expected, result)\n\t\t})\n\t}\n}\n\nfunc TestFilterByFixedStateForPackages(t *testing.T) {\n\ttests := []struct {\n\t\tname        string\n\t\tpackages    []affectedPackageWithDecorations\n\t\tfixedStates []string\n\t\texpectedLen int\n\t}{\n\t\t{\n\t\t\tname: \"empty fixed states returns all packages\",\n\t\t\tpackages: []affectedPackageWithDecorations{\n\t\t\t\t{AffectedPackageHandle: v6.AffectedPackageHandle{BlobValue: &v6.PackageBlob{}}},\n\t\t\t\t{AffectedPackageHandle: v6.AffectedPackageHandle{BlobValue: &v6.PackageBlob{}}},\n\t\t\t},\n\t\t\tfixedStates: []string{},\n\t\t\texpectedLen: 2,\n\t\t},\n\t\t{\n\t\t\tname: \"filter by fixed state\",\n\t\t\tpackages: []affectedPackageWithDecorations{\n\t\t\t\tmakeAffectedPackageWithFixState(v6.FixedStatus),\n\t\t\t\tmakeAffectedPackageWithFixState(v6.NotFixedStatus),\n\t\t\t},\n\t\t\tfixedStates: []string{\"fixed\"},\n\t\t\texpectedLen: 1,\n\t\t},\n\t\t{\n\t\t\tname: \"filter by multiple states\",\n\t\t\tpackages: []affectedPackageWithDecorations{\n\t\t\t\tmakeAffectedPackageWithFixState(v6.FixedStatus),\n\t\t\t\tmakeAffectedPackageWithFixState(v6.NotFixedStatus),\n\t\t\t\tmakeAffectedPackageWithFixState(v6.WontFixStatus),\n\t\t\t},\n\t\t\tfixedStates: []string{\"fixed\", \"wont-fix\"},\n\t\t\texpectedLen: 2,\n\t\t},\n\t\t{\n\t\t\tname: \"filter with no matches\",\n\t\t\tpackages: []affectedPackageWithDecorations{\n\t\t\t\tmakeAffectedPackageWithFixState(v6.NotFixedStatus),\n\t\t\t},\n\t\t\tfixedStates: []string{\"fixed\"},\n\t\t\texpectedLen: 0,\n\t\t},\n\t\t{\n\t\t\tname: \"packages with nil blob are filtered out\",\n\t\t\tpackages: []affectedPackageWithDecorations{\n\t\t\t\tmakeAffectedPackageWithFixState(v6.FixedStatus),\n\t\t\t\t{AffectedPackageHandle: v6.AffectedPackageHandle{BlobValue: nil}},\n\t\t\t},\n\t\t\tfixedStates: []string{\"fixed\"},\n\t\t\texpectedLen: 1,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := filterByFixedStateForPackages(tt.packages, tt.fixedStates)\n\t\t\tassert.Equal(t, tt.expectedLen, len(result))\n\t\t})\n\t}\n}\n\nfunc TestFilterByFixedStateForCPEs(t *testing.T) {\n\ttests := []struct {\n\t\tname        string\n\t\tcpes        []affectedCPEWithDecorations\n\t\tfixedStates []string\n\t\texpectedLen int\n\t}{\n\t\t{\n\t\t\tname: \"empty fixed states returns all CPEs\",\n\t\t\tcpes: []affectedCPEWithDecorations{\n\t\t\t\t{AffectedCPEHandle: v6.AffectedCPEHandle{BlobValue: &v6.PackageBlob{}}},\n\t\t\t\t{AffectedCPEHandle: v6.AffectedCPEHandle{BlobValue: &v6.PackageBlob{}}},\n\t\t\t},\n\t\t\tfixedStates: []string{},\n\t\t\texpectedLen: 2,\n\t\t},\n\t\t{\n\t\t\tname: \"filter by fixed state\",\n\t\t\tcpes: []affectedCPEWithDecorations{\n\t\t\t\tmakeAffectedCPEWithFixState(v6.FixedStatus),\n\t\t\t\tmakeAffectedCPEWithFixState(v6.NotFixedStatus),\n\t\t\t},\n\t\t\tfixedStates: []string{\"fixed\"},\n\t\t\texpectedLen: 1,\n\t\t},\n\t\t{\n\t\t\tname: \"filter by multiple states\",\n\t\t\tcpes: []affectedCPEWithDecorations{\n\t\t\t\tmakeAffectedCPEWithFixState(v6.FixedStatus),\n\t\t\t\tmakeAffectedCPEWithFixState(v6.NotFixedStatus),\n\t\t\t\tmakeAffectedCPEWithFixState(v6.WontFixStatus),\n\t\t\t},\n\t\t\tfixedStates: []string{\"not-fixed\", \"wont-fix\"},\n\t\t\texpectedLen: 2,\n\t\t},\n\t\t{\n\t\t\tname: \"CPEs with nil blob are filtered out\",\n\t\t\tcpes: []affectedCPEWithDecorations{\n\t\t\t\tmakeAffectedCPEWithFixState(v6.FixedStatus),\n\t\t\t\t{AffectedCPEHandle: v6.AffectedCPEHandle{BlobValue: nil}},\n\t\t\t},\n\t\t\tfixedStates: []string{\"fixed\"},\n\t\t\texpectedLen: 1,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := filterByFixedStateForCPEs(tt.cpes, tt.fixedStates)\n\t\t\tassert.Equal(t, tt.expectedLen, len(result))\n\t\t})\n\t}\n}\n\nfunc makeAffectedPackageWithFixState(state v6.FixStatus) affectedPackageWithDecorations {\n\treturn affectedPackageWithDecorations{\n\t\tAffectedPackageHandle: v6.AffectedPackageHandle{\n\t\t\tBlobValue: &v6.PackageBlob{\n\t\t\t\tRanges: []v6.Range{\n\t\t\t\t\t{\n\t\t\t\t\t\tFix: &v6.Fix{\n\t\t\t\t\t\t\tState:   state,\n\t\t\t\t\t\t\tVersion: \"1.0.0\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n}\n\nfunc makeAffectedCPEWithFixState(state v6.FixStatus) affectedCPEWithDecorations {\n\treturn affectedCPEWithDecorations{\n\t\tAffectedCPEHandle: v6.AffectedCPEHandle{\n\t\t\tBlobValue: &v6.PackageBlob{\n\t\t\t\tRanges: []v6.Range{\n\t\t\t\t\t{\n\t\t\t\t\t\tFix: &v6.Fix{\n\t\t\t\t\t\t\tState:   state,\n\t\t\t\t\t\t\tVersion: \"1.0.0\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n}\n"
  },
  {
    "path": "cmd/grype/cli/commands/internal/dbsearch/common.go",
    "content": "package dbsearch\n\nimport v6 \"github.com/anchore/grype/grype/db/v6\"\n\nconst (\n\tfixStateFixed    = \"fixed\"\n\tfixStateNotFixed = \"not-fixed\"\n\tfixStateWontFix  = \"wont-fix\"\n\tfixStateUnknown  = \"unknown\"\n)\n\n// getFixStateFromPackageBlob determines the overall fix state for a package blob.\n// When multiple ranges exist with different fix states, precedence is applied:\n// fixed > wont-fix > not-fixed > unknown\n// This ensures that if ANY range has a fix available, the package is considered fixable.\nfunc getFixStateFromPackageBlob(blob *v6.PackageBlob) string {\n\tif blob == nil {\n\t\treturn fixStateUnknown\n\t}\n\n\thasFixed := false\n\thasNotFixed := false\n\thasWontFix := false\n\n\tfor _, r := range blob.Ranges {\n\t\tif r.Fix == nil {\n\t\t\tcontinue\n\t\t}\n\t\tswitch r.Fix.State {\n\t\tcase v6.FixedStatus:\n\t\t\thasFixed = true\n\t\tcase v6.WontFixStatus:\n\t\t\thasWontFix = true\n\t\tcase v6.NotFixedStatus:\n\t\t\thasNotFixed = true\n\t\t}\n\t}\n\n\tif hasFixed {\n\t\treturn fixStateFixed\n\t}\n\tif hasWontFix {\n\t\treturn fixStateWontFix\n\t}\n\tif hasNotFixed {\n\t\treturn fixStateNotFixed\n\t}\n\n\treturn fixStateUnknown\n}\n\nfunc filterByFixedStateForPackages(packages []affectedPackageWithDecorations, fixedStates []string) []affectedPackageWithDecorations {\n\tif len(fixedStates) == 0 {\n\t\treturn packages\n\t}\n\n\tstateSet := make(map[string]bool)\n\tfor _, state := range fixedStates {\n\t\tstateSet[state] = true\n\t}\n\n\tvar filtered []affectedPackageWithDecorations\n\tfor _, pkg := range packages {\n\t\tif pkg.BlobValue == nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tfixState := getFixStateFromPackageBlob(pkg.BlobValue)\n\t\tif stateSet[fixState] {\n\t\t\tfiltered = append(filtered, pkg)\n\t\t}\n\t}\n\n\treturn filtered\n}\n\nfunc filterByFixedStateForCPEs(cpes []affectedCPEWithDecorations, fixedStates []string) []affectedCPEWithDecorations {\n\tif len(fixedStates) == 0 {\n\t\treturn cpes\n\t}\n\n\tstateSet := make(map[string]bool)\n\tfor _, state := range fixedStates {\n\t\tstateSet[state] = true\n\t}\n\n\tvar filtered []affectedCPEWithDecorations\n\tfor _, cpe := range cpes {\n\t\tif cpe.BlobValue == nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tfixState := getFixStateFromPackageBlob(cpe.BlobValue)\n\t\tif stateSet[fixState] {\n\t\t\tfiltered = append(filtered, cpe)\n\t\t}\n\t}\n\n\treturn filtered\n}\n"
  },
  {
    "path": "cmd/grype/cli/commands/internal/dbsearch/matches.go",
    "content": "package dbsearch\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"sort\"\n\n\t\"github.com/hashicorp/go-multierror\"\n\n\tv6 \"github.com/anchore/grype/grype/db/v6\"\n)\n\n// Matches is the JSON document for the `db search` command\ntype Matches []Match\n\n// Match represents a pairing of a vulnerability advisory with the packages affected by the vulnerability.\ntype Match struct {\n\t// Vulnerability is the core advisory record for a single known vulnerability from a specific provider.\n\tVulnerability VulnerabilityInfo `json:\"vulnerability\"`\n\n\t// AffectedPackages is the list of packages affected by the vulnerability.\n\tAffectedPackages []AffectedPackageInfo `json:\"packages\"`\n}\n\nfunc (m Match) Flatten() []AffectedPackage {\n\tvar rows []AffectedPackage\n\tfor _, pkg := range m.AffectedPackages {\n\t\trows = append(rows, AffectedPackage{\n\t\t\tVulnerability:       m.Vulnerability,\n\t\t\tAffectedPackageInfo: pkg,\n\t\t})\n\t}\n\treturn rows\n}\n\nfunc (m Matches) Flatten() []AffectedPackage {\n\tvar rows []AffectedPackage\n\tfor _, r := range m {\n\t\trows = append(rows, r.Flatten()...)\n\t}\n\treturn rows\n}\n\nfunc newMatchesRows(affectedPkgs []affectedPackageWithDecorations, affectedCPEs []affectedCPEWithDecorations) (rows []Match, retErr error) { // nolint:funlen\n\tvar affectedPkgsByVuln = make(map[v6.ID][]AffectedPackageInfo)\n\tvar vulnsByID = make(map[v6.ID]v6.VulnerabilityHandle)\n\tvar decorationsByID = make(map[v6.ID]vulnerabilityDecorations)\n\n\tfor i := range affectedPkgs {\n\t\tpkg := affectedPkgs[i]\n\t\tvar detail v6.PackageBlob\n\t\tif pkg.BlobValue != nil {\n\t\t\tdetail = *pkg.BlobValue\n\t\t}\n\t\tif pkg.Vulnerability == nil {\n\t\t\tretErr = multierror.Append(retErr, fmt.Errorf(\"affected package record missing vulnerability: %+v\", pkg))\n\t\t\tcontinue\n\t\t}\n\t\tif _, ok := vulnsByID[pkg.Vulnerability.ID]; !ok {\n\t\t\tvulnsByID[pkg.Vulnerability.ID] = *pkg.Vulnerability\n\t\t\tdecorationsByID[pkg.Vulnerability.ID] = pkg.vulnerabilityDecorations\n\t\t}\n\n\t\taff := AffectedPackageInfo{\n\t\t\tModel:     &pkg.AffectedPackageHandle,\n\t\t\tOS:        toOS(pkg.OperatingSystem),\n\t\t\tPackage:   toPackage(pkg.Package),\n\t\t\tNamespace: v6.MimicV5Namespace(pkg.Vulnerability, &pkg.AffectedPackageHandle),\n\t\t\tDetail:    detail,\n\t\t}\n\n\t\taffectedPkgsByVuln[pkg.Vulnerability.ID] = append(affectedPkgsByVuln[pkg.Vulnerability.ID], aff)\n\t}\n\n\tfor _, ac := range affectedCPEs {\n\t\tvar detail v6.PackageBlob\n\t\tif ac.BlobValue != nil {\n\t\t\tdetail = *ac.BlobValue\n\t\t}\n\t\tif ac.Vulnerability == nil {\n\t\t\tretErr = multierror.Append(retErr, fmt.Errorf(\"affected CPE record missing vulnerability: %+v\", ac))\n\t\t\tcontinue\n\t\t}\n\n\t\tvar c *CPE\n\t\tif ac.CPE != nil {\n\t\t\tcv := CPE(*ac.CPE)\n\t\t\tc = &cv\n\t\t}\n\n\t\tif _, ok := vulnsByID[ac.Vulnerability.ID]; !ok {\n\t\t\tvulnsByID[ac.Vulnerability.ID] = *ac.Vulnerability\n\t\t\tdecorationsByID[ac.Vulnerability.ID] = ac.vulnerabilityDecorations\n\t\t}\n\n\t\taff := AffectedPackageInfo{\n\t\t\t// tracking model information is not possible with CPE handles\n\t\t\tCPE:       c,\n\t\t\tNamespace: v6.MimicV5Namespace(ac.Vulnerability, nil), // no affected package will default to NVD\n\t\t\tDetail:    detail,\n\t\t}\n\n\t\taffectedPkgsByVuln[ac.Vulnerability.ID] = append(affectedPkgsByVuln[ac.Vulnerability.ID], aff)\n\t}\n\n\tfor vulnID, vuln := range vulnsByID {\n\t\trows = append(rows, Match{\n\t\t\tVulnerability:    newVulnerabilityInfo(vuln, decorationsByID[vulnID]),\n\t\t\tAffectedPackages: affectedPkgsByVuln[vulnID],\n\t\t})\n\t}\n\n\tsort.Slice(rows, func(i, j int) bool {\n\t\treturn rows[i].Vulnerability.ID < rows[j].Vulnerability.ID\n\t})\n\n\treturn rows, retErr\n}\n\nfunc FindMatches(reader interface {\n\tv6.AffectedPackageStoreReader\n\tv6.AffectedCPEStoreReader\n\tv6.VulnerabilityDecoratorStoreReader\n}, criteria AffectedPackagesOptions) (Matches, error) {\n\tallAffectedPkgs, allAffectedCPEs, fetchErr := findAffectedPackages(reader, criteria)\n\n\tif fetchErr != nil {\n\t\tif !errors.Is(fetchErr, v6.ErrLimitReached) {\n\t\t\treturn nil, fetchErr\n\t\t}\n\t}\n\n\tif len(criteria.FixedStates) > 0 {\n\t\tallAffectedPkgs = filterByFixedStateForPackages(allAffectedPkgs, criteria.FixedStates)\n\t\tallAffectedCPEs = filterByFixedStateForCPEs(allAffectedCPEs, criteria.FixedStates)\n\t}\n\n\trows, presErr := newMatchesRows(allAffectedPkgs, allAffectedCPEs)\n\tif presErr != nil {\n\t\treturn nil, presErr\n\t}\n\treturn rows, fetchErr\n}\n"
  },
  {
    "path": "cmd/grype/cli/commands/internal/dbsearch/matches_test.go",
    "content": "package dbsearch\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\n\tv6 \"github.com/anchore/grype/grype/db/v6\"\n)\n\nfunc TestGetFixStateFromBlob(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tblob     *v6.PackageBlob\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:     \"nil blob returns unknown\",\n\t\t\tblob:     nil,\n\t\t\texpected: \"unknown\",\n\t\t},\n\t\t{\n\t\t\tname:     \"empty blob returns unknown\",\n\t\t\tblob:     &v6.PackageBlob{},\n\t\t\texpected: \"unknown\",\n\t\t},\n\t\t{\n\t\t\tname: \"blob with fixed status\",\n\t\t\tblob: &v6.PackageBlob{\n\t\t\t\tRanges: []v6.Range{\n\t\t\t\t\t{\n\t\t\t\t\t\tFix: &v6.Fix{\n\t\t\t\t\t\t\tState:   v6.FixedStatus,\n\t\t\t\t\t\t\tVersion: \"1.2.3\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: \"fixed\",\n\t\t},\n\t\t{\n\t\t\tname: \"blob with not-fixed status\",\n\t\t\tblob: &v6.PackageBlob{\n\t\t\t\tRanges: []v6.Range{\n\t\t\t\t\t{\n\t\t\t\t\t\tFix: &v6.Fix{\n\t\t\t\t\t\t\tState: v6.NotFixedStatus,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: \"not-fixed\",\n\t\t},\n\t\t{\n\t\t\tname: \"blob with wont-fix status\",\n\t\t\tblob: &v6.PackageBlob{\n\t\t\t\tRanges: []v6.Range{\n\t\t\t\t\t{\n\t\t\t\t\t\tFix: &v6.Fix{\n\t\t\t\t\t\t\tState: v6.WontFixStatus,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: \"wont-fix\",\n\t\t},\n\t\t{\n\t\t\tname: \"blob with no fix returns unknown\",\n\t\t\tblob: &v6.PackageBlob{\n\t\t\t\tRanges: []v6.Range{\n\t\t\t\t\t{\n\t\t\t\t\t\tFix: nil,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: \"unknown\",\n\t\t},\n\t\t{\n\t\t\tname: \"blob with mixed statuses prefers fixed\",\n\t\t\tblob: &v6.PackageBlob{\n\t\t\t\tRanges: []v6.Range{\n\t\t\t\t\t{\n\t\t\t\t\t\tFix: &v6.Fix{\n\t\t\t\t\t\t\tState: v6.NotFixedStatus,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tFix: &v6.Fix{\n\t\t\t\t\t\t\tState:   v6.FixedStatus,\n\t\t\t\t\t\t\tVersion: \"2.0.0\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: \"fixed\",\n\t\t},\n\t\t{\n\t\t\tname: \"blob with wont-fix and not-fixed prefers wont-fix\",\n\t\t\tblob: &v6.PackageBlob{\n\t\t\t\tRanges: []v6.Range{\n\t\t\t\t\t{\n\t\t\t\t\t\tFix: &v6.Fix{\n\t\t\t\t\t\t\tState: v6.NotFixedStatus,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tFix: &v6.Fix{\n\t\t\t\t\t\t\tState: v6.WontFixStatus,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: \"wont-fix\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := getFixStateFromPackageBlob(tt.blob)\n\t\t\tassert.Equal(t, tt.expected, result)\n\t\t})\n\t}\n}\n\nfunc TestFilterByFixedState(t *testing.T) {\n\ttests := []struct {\n\t\tname         string\n\t\tpackages     []affectedPackageWithDecorations\n\t\tfixedStates  []string\n\t\texpectedLen  int\n\t\texpectedStrs []string\n\t}{\n\t\t{\n\t\t\tname:         \"empty fixed states returns all packages\",\n\t\t\tpackages:     makeTestPackages(3),\n\t\t\tfixedStates:  []string{},\n\t\t\texpectedLen:  3,\n\t\t\texpectedStrs: nil,\n\t\t},\n\t\t{\n\t\t\tname: \"filter by fixed state\",\n\t\t\tpackages: []affectedPackageWithDecorations{\n\t\t\t\tmakePackageWithFixState(v6.FixedStatus),\n\t\t\t\tmakePackageWithFixState(v6.NotFixedStatus),\n\t\t\t\tmakePackageWithFixState(v6.WontFixStatus),\n\t\t\t},\n\t\t\tfixedStates:  []string{\"fixed\"},\n\t\t\texpectedLen:  1,\n\t\t\texpectedStrs: []string{\"fixed\"},\n\t\t},\n\t\t{\n\t\t\tname: \"filter by multiple states\",\n\t\t\tpackages: []affectedPackageWithDecorations{\n\t\t\t\tmakePackageWithFixState(v6.FixedStatus),\n\t\t\t\tmakePackageWithFixState(v6.NotFixedStatus),\n\t\t\t\tmakePackageWithFixState(v6.WontFixStatus),\n\t\t\t},\n\t\t\tfixedStates:  []string{\"fixed\", \"wont-fix\"},\n\t\t\texpectedLen:  2,\n\t\t\texpectedStrs: []string{\"fixed\", \"wont-fix\"},\n\t\t},\n\t\t{\n\t\t\tname: \"filter with no matches\",\n\t\t\tpackages: []affectedPackageWithDecorations{\n\t\t\t\tmakePackageWithFixState(v6.NotFixedStatus),\n\t\t\t\tmakePackageWithFixState(v6.WontFixStatus),\n\t\t\t},\n\t\t\tfixedStates:  []string{\"fixed\"},\n\t\t\texpectedLen:  0,\n\t\t\texpectedStrs: nil,\n\t\t},\n\t\t{\n\t\t\tname: \"packages with nil blob are filtered out\",\n\t\t\tpackages: []affectedPackageWithDecorations{\n\t\t\t\tmakePackageWithFixState(v6.FixedStatus),\n\t\t\t\t{AffectedPackageHandle: v6.AffectedPackageHandle{BlobValue: nil}},\n\t\t\t},\n\t\t\tfixedStates:  []string{\"fixed\", \"unknown\"},\n\t\t\texpectedLen:  1,\n\t\t\texpectedStrs: []string{\"fixed\"},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := filterByFixedStateForPackages(tt.packages, tt.fixedStates)\n\t\t\tassert.Equal(t, tt.expectedLen, len(result))\n\n\t\t\tif tt.expectedStrs != nil {\n\t\t\t\tvar resultStates []string\n\t\t\t\tfor _, pkg := range result {\n\t\t\t\t\tresultStates = append(resultStates, getFixStateFromPackageBlob(pkg.BlobValue))\n\t\t\t\t}\n\t\t\t\tassert.ElementsMatch(t, tt.expectedStrs, resultStates)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestFilterCPEsByFixedState(t *testing.T) {\n\ttests := []struct {\n\t\tname         string\n\t\tcpes         []affectedCPEWithDecorations\n\t\tfixedStates  []string\n\t\texpectedLen  int\n\t\texpectedStrs []string\n\t}{\n\t\t{\n\t\t\tname:         \"empty fixed states returns all CPEs\",\n\t\t\tcpes:         makeTestCPEs(3),\n\t\t\tfixedStates:  []string{},\n\t\t\texpectedLen:  3,\n\t\t\texpectedStrs: nil,\n\t\t},\n\t\t{\n\t\t\tname: \"filter by fixed state\",\n\t\t\tcpes: []affectedCPEWithDecorations{\n\t\t\t\tmakeCPEWithFixState(v6.FixedStatus),\n\t\t\t\tmakeCPEWithFixState(v6.NotFixedStatus),\n\t\t\t\tmakeCPEWithFixState(v6.WontFixStatus),\n\t\t\t},\n\t\t\tfixedStates:  []string{\"fixed\"},\n\t\t\texpectedLen:  1,\n\t\t\texpectedStrs: []string{\"fixed\"},\n\t\t},\n\t\t{\n\t\t\tname: \"filter by multiple states\",\n\t\t\tcpes: []affectedCPEWithDecorations{\n\t\t\t\tmakeCPEWithFixState(v6.FixedStatus),\n\t\t\t\tmakeCPEWithFixState(v6.NotFixedStatus),\n\t\t\t\tmakeCPEWithFixState(v6.WontFixStatus),\n\t\t\t},\n\t\t\tfixedStates:  []string{\"not-fixed\", \"wont-fix\"},\n\t\t\texpectedLen:  2,\n\t\t\texpectedStrs: []string{\"not-fixed\", \"wont-fix\"},\n\t\t},\n\t\t{\n\t\t\tname: \"CPEs with nil blob are filtered out\",\n\t\t\tcpes: []affectedCPEWithDecorations{\n\t\t\t\tmakeCPEWithFixState(v6.FixedStatus),\n\t\t\t\t{AffectedCPEHandle: v6.AffectedCPEHandle{BlobValue: nil}},\n\t\t\t},\n\t\t\tfixedStates:  []string{\"fixed\", \"unknown\"},\n\t\t\texpectedLen:  1,\n\t\t\texpectedStrs: []string{\"fixed\"},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := filterByFixedStateForCPEs(tt.cpes, tt.fixedStates)\n\t\t\tassert.Equal(t, tt.expectedLen, len(result))\n\n\t\t\tif tt.expectedStrs != nil {\n\t\t\t\tvar resultStates []string\n\t\t\t\tfor _, cpe := range result {\n\t\t\t\t\tresultStates = append(resultStates, getFixStateFromPackageBlob(cpe.BlobValue))\n\t\t\t\t}\n\t\t\t\tassert.ElementsMatch(t, tt.expectedStrs, resultStates)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc makeTestPackages(count int) []affectedPackageWithDecorations {\n\tpackages := make([]affectedPackageWithDecorations, count)\n\tfor i := 0; i < count; i++ {\n\t\tpackages[i] = affectedPackageWithDecorations{\n\t\t\tAffectedPackageHandle: v6.AffectedPackageHandle{\n\t\t\t\tBlobValue: &v6.PackageBlob{},\n\t\t\t},\n\t\t}\n\t}\n\treturn packages\n}\n\nfunc makePackageWithFixState(state v6.FixStatus) affectedPackageWithDecorations {\n\treturn affectedPackageWithDecorations{\n\t\tAffectedPackageHandle: v6.AffectedPackageHandle{\n\t\t\tBlobValue: &v6.PackageBlob{\n\t\t\t\tRanges: []v6.Range{\n\t\t\t\t\t{\n\t\t\t\t\t\tFix: &v6.Fix{\n\t\t\t\t\t\t\tState:   state,\n\t\t\t\t\t\t\tVersion: \"1.0.0\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n}\n\nfunc makeTestCPEs(count int) []affectedCPEWithDecorations {\n\tcpes := make([]affectedCPEWithDecorations, count)\n\tfor i := 0; i < count; i++ {\n\t\tcpes[i] = affectedCPEWithDecorations{\n\t\t\tAffectedCPEHandle: v6.AffectedCPEHandle{\n\t\t\t\tBlobValue: &v6.PackageBlob{},\n\t\t\t},\n\t\t}\n\t}\n\treturn cpes\n}\n\nfunc makeCPEWithFixState(state v6.FixStatus) affectedCPEWithDecorations {\n\treturn affectedCPEWithDecorations{\n\t\tAffectedCPEHandle: v6.AffectedCPEHandle{\n\t\t\tBlobValue: &v6.PackageBlob{\n\t\t\t\tRanges: []v6.Range{\n\t\t\t\t\t{\n\t\t\t\t\t\tFix: &v6.Fix{\n\t\t\t\t\t\t\tState:   state,\n\t\t\t\t\t\t\tVersion: \"1.0.0\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n}\n"
  },
  {
    "path": "cmd/grype/cli/commands/internal/dbsearch/versions.go",
    "content": "package dbsearch\n\nconst (\n\t// MatchesSchemaVersion is the schema version for the `db search` command\n\tMatchesSchemaVersion = \"1.1.3\"\n\n\t// MatchesSchemaVersion Changelog:\n\t// 1.0.0 - Initial schema 🎉\n\t// 1.0.1 - Add KEV and EPSS data to vulnerability matches\n\t// 1.0.2 - Add v5 namespace emulation for affected packages\n\t// 1.0.3 - Add severity string field to vulnerability object\n\t// 1.1.0 - Add fix available date information to vulnerability range object. This removes existing unused git-commit and date fields from the schema, but is a non-breaking change.\n\t// 1.1.1 - Add unaffected package and unaffected cpe to output\n\t// 1.1.2 - Add CWE IDs to vulnerability output\n\t// 1.1.3 - Add ID field to Reference (for advisory IDs like RHSA-2023:5455)\n\n\t// VulnerabilitiesSchemaVersion is the schema version for the `db search vuln` command\n\tVulnerabilitiesSchemaVersion = \"1.0.5\"\n\n\t// VulnerabilitiesSchemaVersion\n\t// 1.0.0 - Initial schema 🎉\n\t// 1.0.1 - Add KEV and EPSS data to vulnerability\n\t// 1.0.3 - Add severity string field to vulnerability object\n\t// 1.0.4 - Add CWE IDs to vulnerability output\n\t// 1.0.5 - Add ID field to Reference (for advisory IDs like RHSA-2023:5455)\n)\n"
  },
  {
    "path": "cmd/grype/cli/commands/internal/dbsearch/vulnerabilities.go",
    "content": "package dbsearch\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"sort\"\n\t\"time\"\n\n\tv6 \"github.com/anchore/grype/grype/db/v6\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n\t\"github.com/anchore/grype/internal/cvss\"\n\t\"github.com/anchore/grype/internal/log\"\n)\n\n// Vulnerabilities is the JSON document for the `db search vuln` command\ntype Vulnerabilities []Vulnerability\n\n// Vulnerability represents the core advisory record for a single known vulnerability from a specific provider.\ntype Vulnerability struct {\n\tVulnerabilityInfo `json:\",inline\"`\n\n\t// OperatingSystems is a list of operating systems affected by the vulnerability\n\tOperatingSystems []OperatingSystem `json:\"operating_systems\"`\n\n\t// AffectedPackages is the number of packages affected by the vulnerability\n\tAffectedPackages int `json:\"affected_packages\"`\n}\n\ntype VulnerabilityInfo struct {\n\t// TODO: remove this when namespace is no longer used\n\tModel v6.VulnerabilityHandle `json:\"-\"` // tracking package handle info is necessary for namespace lookup\n\n\tv6.VulnerabilityBlob `json:\",inline\"`\n\n\t// Severity is the single string representation of the vulnerability's severity based on the set of available severity values\n\tSeverity string `json:\"severity,omitempty\"`\n\n\t// Provider is the upstream data processor (usually Vunnel) that is responsible for vulnerability records. Each provider\n\t// should be scoped to a specific vulnerability dataset, for instance, the \"ubuntu\" provider for all records from\n\t// Canonicals' Ubuntu Security Notices (for all Ubuntu distro versions).\n\tProvider string `json:\"provider\"`\n\n\t// Status conveys the actionability of the current record (one of \"active\", \"analyzing\", \"rejected\", \"disputed\")\n\tStatus string `json:\"status\"`\n\n\t// PublishedDate is the date the vulnerability record was first published\n\tPublishedDate *time.Time `json:\"published_date,omitempty\"`\n\n\t// ModifiedDate is the date the vulnerability record was last modified\n\tModifiedDate *time.Time `json:\"modified_date,omitempty\"`\n\n\t// WithdrawnDate is the date the vulnerability record was withdrawn\n\tWithdrawnDate *time.Time `json:\"withdrawn_date,omitempty\"`\n\n\t// KnownExploited is a list of known exploited vulnerabilities from the CISA KEV dataset\n\tKnownExploited []KnownExploited `json:\"known_exploited,omitempty\"`\n\n\t// EPSS is a list of Exploit Prediction Scoring System (EPSS) scores for the vulnerability\n\tEPSS []EPSS `json:\"epss,omitempty\"`\n\n\t// CWEs is a list of Common Weakness Enumeration (CWE) identifiers for the vulnerability\n\tCWEs []CWE `json:\"cwes,omitempty\"`\n}\n\n// OperatingSystem represents specific release of an operating system.\ntype OperatingSystem struct {\n\t// Name is the operating system family name (e.g. \"debian\")\n\tName string `json:\"name\"`\n\n\t// Version is the semver-ish or codename for the release of the operating system\n\tVersion string `json:\"version\"`\n}\n\ntype KnownExploited struct {\n\tCVE                        string   `json:\"cve\"`\n\tVendorProject              string   `json:\"vendor_project,omitempty\"`\n\tProduct                    string   `json:\"product,omitempty\"`\n\tDateAdded                  string   `json:\"date_added,omitempty\"`\n\tRequiredAction             string   `json:\"required_action,omitempty\"`\n\tDueDate                    string   `json:\"due_date,omitempty\"`\n\tKnownRansomwareCampaignUse string   `json:\"known_ransomware_campaign_use\"`\n\tNotes                      string   `json:\"notes,omitempty\"`\n\tURLs                       []string `json:\"urls,omitempty\"`\n\tCWEs                       []string `json:\"cwes,omitempty\"`\n}\n\ntype EPSS struct {\n\tCVE        string  `json:\"cve\"`\n\tEPSS       float64 `json:\"epss\"`\n\tPercentile float64 `json:\"percentile\"`\n\tDate       string  `json:\"date\"`\n}\n\ntype CWE struct {\n\tCve    string `json:\"cve\"`\n\tCWE    string `json:\"cwe\"`\n\tSource string `json:\"source\"`\n\tType   string `json:\"type\"`\n}\n\ntype CVSSSeverity struct {\n\t// Vector is the CVSS assessment as a parameterized string\n\tVector string `json:\"vector\"`\n\n\t// Version is the CVSS version (e.g. \"3.0\")\n\tVersion string `json:\"version,omitempty\"`\n\n\t// Metrics is the CVSS quantitative assessment based on the vector\n\tMetrics CvssMetrics `json:\"metrics\"`\n}\n\ntype CvssMetrics struct {\n\tBaseScore           float64  `json:\"baseScore\"`\n\tExploitabilityScore *float64 `json:\"exploitabilityScore,omitempty\"`\n\tImpactScore         *float64 `json:\"impactScore,omitempty\"`\n}\n\ntype vulnerabilityAffectedPackageJoin struct {\n\tVulnerability    v6.VulnerabilityHandle\n\tOperatingSystems []v6.OperatingSystem\n\tAffectedPackages int\n\tvulnerabilityDecorations\n}\n\ntype VulnerabilitiesOptions struct {\n\tVulnerability v6.VulnerabilitySpecifiers\n\tRecordLimit   int\n}\n\nfunc newVulnerabilityRows(vaps ...vulnerabilityAffectedPackageJoin) (rows []Vulnerability) {\n\tfor _, vap := range vaps {\n\t\trows = append(rows, Vulnerability{\n\t\t\tVulnerabilityInfo: newVulnerabilityInfo(vap.Vulnerability, vap.vulnerabilityDecorations),\n\t\t\tOperatingSystems:  newOperatingSystems(vap.OperatingSystems),\n\t\t\tAffectedPackages:  vap.AffectedPackages,\n\t\t})\n\t}\n\treturn rows\n}\n\nfunc newVulnerabilityInfo(vuln v6.VulnerabilityHandle, vc vulnerabilityDecorations) VulnerabilityInfo {\n\tvar blob v6.VulnerabilityBlob\n\tif vuln.BlobValue != nil {\n\t\tblob = *vuln.BlobValue\n\t}\n\tpatchCVSSMetrics(&blob)\n\treturn VulnerabilityInfo{\n\t\tModel:             vuln,\n\t\tVulnerabilityBlob: blob,\n\t\tSeverity:          getSeverity(blob.Severities),\n\t\tProvider:          vuln.Provider.ID,\n\t\tStatus:            string(vuln.Status),\n\t\tPublishedDate:     vuln.PublishedDate,\n\t\tModifiedDate:      vuln.ModifiedDate,\n\t\tWithdrawnDate:     vuln.WithdrawnDate,\n\t\tKnownExploited:    vc.KnownExploited,\n\t\tEPSS:              vc.EPSS,\n\t}\n}\n\nfunc patchCVSSMetrics(blob *v6.VulnerabilityBlob) {\n\tfor i := range blob.Severities {\n\t\tsev := &blob.Severities[i]\n\t\tif val, ok := sev.Value.(v6.CVSSSeverity); ok {\n\t\t\tmet, err := cvss.ParseMetricsFromVector(val.Vector)\n\t\t\tif err != nil {\n\t\t\t\tlog.WithFields(\"vector\", val.Vector, \"error\", err).Debug(\"unable to parse CVSS vector\")\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tnewSev := CVSSSeverity{\n\t\t\t\tVector:  val.Vector,\n\t\t\t\tVersion: val.Version,\n\t\t\t\tMetrics: CvssMetrics{\n\t\t\t\t\tBaseScore:           met.BaseScore,\n\t\t\t\t\tExploitabilityScore: met.ExploitabilityScore,\n\t\t\t\t\tImpactScore:         met.ImpactScore,\n\t\t\t\t},\n\t\t\t}\n\t\t\tsev.Value = newSev\n\t\t}\n\t}\n}\n\nfunc newOperatingSystems(oss []v6.OperatingSystem) (os []OperatingSystem) {\n\tfor _, o := range oss {\n\t\tos = append(os, OperatingSystem{\n\t\t\tName:    o.Name,\n\t\t\tVersion: o.Version(),\n\t\t})\n\t}\n\treturn os\n}\n\nfunc FindVulnerabilities(reader interface { //nolint:funlen\n\tv6.VulnerabilityStoreReader\n\tv6.AffectedPackageStoreReader\n\tv6.VulnerabilityDecoratorStoreReader\n}, config VulnerabilitiesOptions,\n) ([]Vulnerability, error) {\n\tlog.WithFields(\"vulnSpecs\", len(config.Vulnerability)).Debug(\"fetching vulnerabilities\")\n\n\tif config.RecordLimit == 0 {\n\t\tlog.Warn(\"no record limit set! For queries with large result sets this may result in performance issues\")\n\t}\n\n\tvar vulns []v6.VulnerabilityHandle\n\tvar limitReached bool\n\tfor _, vulnSpec := range config.Vulnerability {\n\t\tvs, err := reader.GetVulnerabilities(&vulnSpec, &v6.GetVulnerabilityOptions{\n\t\t\tPreload: true,\n\t\t\tLimit:   config.RecordLimit,\n\t\t})\n\t\tif err != nil {\n\t\t\tif !errors.Is(err, v6.ErrLimitReached) {\n\t\t\t\treturn nil, fmt.Errorf(\"unable to get vulnerabilities: %w\", err)\n\t\t\t}\n\t\t\tlimitReached = true\n\t\t\tbreak\n\t\t}\n\n\t\tvulns = append(vulns, vs...)\n\t}\n\n\tlog.WithFields(\"vulns\", len(vulns)).Debug(\"fetching affected packages\")\n\n\t// find all affected packages for this vulnerability, so we can gather os information\n\tvar pairs []vulnerabilityAffectedPackageJoin\n\tfor _, vuln := range vulns {\n\t\taffected, fetchErr := reader.GetAffectedPackages(nil, &v6.GetPackageOptions{\n\t\t\tPreloadOS: true,\n\t\t\tVulnerabilities: []v6.VulnerabilitySpecifier{\n\t\t\t\t{\n\t\t\t\t\tID: vuln.ID,\n\t\t\t\t},\n\t\t\t},\n\t\t\tLimit: config.RecordLimit,\n\t\t})\n\t\tif fetchErr != nil {\n\t\t\tif !errors.Is(fetchErr, v6.ErrLimitReached) {\n\t\t\t\treturn nil, fmt.Errorf(\"unable to get affected packages: %w\", fetchErr)\n\t\t\t}\n\t\t\tlimitReached = true\n\t\t}\n\n\t\tdistros := make(map[v6.ID]v6.OperatingSystem)\n\t\tfor _, a := range affected {\n\t\t\tif a.OperatingSystem != nil {\n\t\t\t\tif _, ok := distros[a.OperatingSystem.ID]; !ok {\n\t\t\t\t\tdistros[a.OperatingSystem.ID] = *a.OperatingSystem\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tvar distrosSlice []v6.OperatingSystem\n\t\tfor _, d := range distros {\n\t\t\tdistrosSlice = append(distrosSlice, d)\n\t\t}\n\n\t\tsort.Slice(distrosSlice, func(i, j int) bool {\n\t\t\treturn distrosSlice[i].ID < distrosSlice[j].ID\n\t\t})\n\n\t\tpairs = append(pairs, vulnerabilityAffectedPackageJoin{\n\t\t\tVulnerability:    vuln,\n\t\t\tOperatingSystems: distrosSlice,\n\t\t\tAffectedPackages: len(affected),\n\t\t})\n\n\t\tif errors.Is(fetchErr, v6.ErrLimitReached) {\n\t\t\tbreak\n\t\t}\n\t}\n\n\tfor i := range pairs {\n\t\tdecorateVulnerabilities(reader, &pairs[i])\n\t}\n\n\tvar err error\n\tif limitReached {\n\t\terr = v6.ErrLimitReached\n\t}\n\n\treturn newVulnerabilityRows(pairs...), err\n}\n\nfunc getSeverity(sevs []v6.Severity) string {\n\tif len(sevs) == 0 {\n\t\treturn vulnerability.UnknownSeverity.String()\n\t}\n\t// get the first severity value (which is ranked highest)\n\tswitch v := sevs[0].Value.(type) {\n\tcase string:\n\t\treturn v\n\tcase CVSSSeverity:\n\t\treturn cvss.SeverityFromBaseScore(v.Metrics.BaseScore).String()\n\t}\n\n\treturn fmt.Sprintf(\"%v\", sevs[0].Value)\n}\n"
  },
  {
    "path": "cmd/grype/cli/commands/internal/dbsearch/vulnerabilities_test.go",
    "content": "package dbsearch\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/google/go-cmp/cmp/cmpopts\"\n\t\"github.com/stretchr/testify/mock\"\n\t\"github.com/stretchr/testify/require\"\n\n\tv6 \"github.com/anchore/grype/grype/db/v6\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n)\n\nfunc TestGetSeverity(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    []v6.Severity\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:     \"empty list\",\n\t\t\tinput:    []v6.Severity{},\n\t\t\texpected: vulnerability.UnknownSeverity.String(),\n\t\t},\n\t\t{\n\t\t\tname: \"string severity\",\n\t\t\tinput: []v6.Severity{\n\t\t\t\t{\n\t\t\t\t\tScheme: \"HML\",\n\t\t\t\t\tValue:  \"high\",\n\t\t\t\t\tSource: \"nvd@nist.gov\",\n\t\t\t\t\tRank:   1,\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: \"high\",\n\t\t},\n\t\t{\n\t\t\tname: \"CVSS severity\",\n\t\t\tinput: []v6.Severity{\n\t\t\t\t{\n\t\t\t\t\tScheme: \"CVSS_V3\",\n\t\t\t\t\tValue: CVSSSeverity{\n\t\t\t\t\t\tVector:  \"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H\",\n\t\t\t\t\t\tVersion: \"3.1\",\n\t\t\t\t\t\tMetrics: CvssMetrics{\n\t\t\t\t\t\t\tBaseScore: 9.8,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tSource: \"nvd@nist.gov\",\n\t\t\t\t\tRank:   1,\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: \"critical\",\n\t\t},\n\t\t{\n\t\t\tname: \"other value type\",\n\t\t\tinput: []v6.Severity{\n\t\t\t\t{\n\t\t\t\t\tScheme: \"OTHER\",\n\t\t\t\t\tValue:  42.0,\n\t\t\t\t\tSource: \"custom\",\n\t\t\t\t\tRank:   1,\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: \"42\",\n\t\t},\n\t\t{\n\t\t\tname: \"multiple severities\",\n\t\t\tinput: []v6.Severity{\n\t\t\t\t{\n\t\t\t\t\tScheme: \"HML\",\n\t\t\t\t\tValue:  \"high\",\n\t\t\t\t\tSource: \"nvd@nist.gov\",\n\t\t\t\t\tRank:   1,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tScheme: \"CVSS_V3\",\n\t\t\t\t\tValue: CVSSSeverity{\n\t\t\t\t\t\tVector:  \"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H\",\n\t\t\t\t\t\tVersion: \"3.1\",\n\t\t\t\t\t\tMetrics: CvssMetrics{\n\t\t\t\t\t\t\tBaseScore: 9.8,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tSource: \"nvd@nist.gov\",\n\t\t\t\t\tRank:   2,\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: \"high\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tactual := getSeverity(tt.input)\n\t\t\trequire.Equal(t, tt.expected, actual)\n\t\t})\n\t}\n}\n\nfunc TestNewVulnerabilityRows(t *testing.T) {\n\tvap := vulnerabilityAffectedPackageJoin{\n\t\tVulnerability: v6.VulnerabilityHandle{\n\t\t\tID:            1,\n\t\t\tName:          \"CVE-1234-5678\",\n\t\t\tStatus:        \"active\",\n\t\t\tPublishedDate: ptr(time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC)),\n\t\t\tModifiedDate:  ptr(time.Date(2023, 2, 1, 0, 0, 0, 0, time.UTC)),\n\t\t\tWithdrawnDate: nil,\n\t\t\tProvider:      &v6.Provider{ID: \"provider1\"},\n\t\t\tBlobValue: &v6.VulnerabilityBlob{\n\t\t\t\tDescription: \"Test description\",\n\t\t\t\tSeverities: []v6.Severity{\n\t\t\t\t\t{\n\t\t\t\t\t\tScheme: \"CVSS_V3\",\n\t\t\t\t\t\tValue: CVSSSeverity{\n\t\t\t\t\t\t\tVector:  \"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H\",\n\t\t\t\t\t\t\tVersion: \"3.1\",\n\t\t\t\t\t\t\tMetrics: CvssMetrics{\n\t\t\t\t\t\t\t\tBaseScore: 9.8,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tSource: \"nvd@nist.gov\",\n\t\t\t\t\t\tRank:   1,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tOperatingSystems: []v6.OperatingSystem{\n\t\t\t{Name: \"Linux\", MajorVersion: \"5\", MinorVersion: \"10\"},\n\t\t},\n\t\tAffectedPackages: 5,\n\t\tvulnerabilityDecorations: vulnerabilityDecorations{\n\t\t\tKnownExploited: []KnownExploited{\n\t\t\t\t{\n\t\t\t\t\tCVE:                        \"CVE-1234-5678\",\n\t\t\t\t\tVendorProject:              \"LinuxFoundation\",\n\t\t\t\t\tProduct:                    \"Linux\",\n\t\t\t\t\tDateAdded:                  \"2025-02-02\",\n\t\t\t\t\tRequiredAction:             \"Yes\",\n\t\t\t\t\tDueDate:                    \"2025-02-02\",\n\t\t\t\t\tKnownRansomwareCampaignUse: \"Known\",\n\t\t\t\t\tNotes:                      \"note!\",\n\t\t\t\t\tURLs:                       []string{\"https://example.com\"},\n\t\t\t\t\tCWEs:                       []string{\"CWE-1234\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tEPSS: []EPSS{\n\t\t\t\t{\n\t\t\t\t\tCVE:        \"CVE-1234-5678\",\n\t\t\t\t\tEPSS:       0.893,\n\t\t\t\t\tPercentile: 0.99,\n\t\t\t\t\tDate:       \"2025-02-24\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\trows := newVulnerabilityRows(vap)\n\texpected := []Vulnerability{\n\t\t{\n\t\t\tVulnerabilityInfo: VulnerabilityInfo{\n\t\t\t\tVulnerabilityBlob: v6.VulnerabilityBlob{\n\t\t\t\t\tDescription: \"Test description\",\n\t\t\t\t\tSeverities: []v6.Severity{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tScheme: \"CVSS_V3\",\n\t\t\t\t\t\t\tValue: CVSSSeverity{\n\t\t\t\t\t\t\t\tVector:  \"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H\",\n\t\t\t\t\t\t\t\tVersion: \"3.1\",\n\t\t\t\t\t\t\t\tMetrics: CvssMetrics{\n\t\t\t\t\t\t\t\t\tBaseScore: 9.8,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tSource: \"nvd@nist.gov\",\n\t\t\t\t\t\t\tRank:   1,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tSeverity:      \"critical\",\n\t\t\t\tProvider:      \"provider1\",\n\t\t\t\tStatus:        \"active\",\n\t\t\t\tPublishedDate: ptr(time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC)),\n\t\t\t\tModifiedDate:  ptr(time.Date(2023, 2, 1, 0, 0, 0, 0, time.UTC)),\n\t\t\t\tWithdrawnDate: nil,\n\t\t\t\tKnownExploited: []KnownExploited{\n\t\t\t\t\t{\n\t\t\t\t\t\tCVE:                        \"CVE-1234-5678\",\n\t\t\t\t\t\tVendorProject:              \"LinuxFoundation\",\n\t\t\t\t\t\tProduct:                    \"Linux\",\n\t\t\t\t\t\tDateAdded:                  \"2025-02-02\",\n\t\t\t\t\t\tRequiredAction:             \"Yes\",\n\t\t\t\t\t\tDueDate:                    \"2025-02-02\",\n\t\t\t\t\t\tKnownRansomwareCampaignUse: \"Known\",\n\t\t\t\t\t\tNotes:                      \"note!\",\n\t\t\t\t\t\tURLs:                       []string{\"https://example.com\"},\n\t\t\t\t\t\tCWEs:                       []string{\"CWE-1234\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tEPSS: []EPSS{\n\t\t\t\t\t{\n\t\t\t\t\t\tCVE:        \"CVE-1234-5678\",\n\t\t\t\t\t\tEPSS:       0.893,\n\t\t\t\t\t\tPercentile: 0.99,\n\t\t\t\t\t\tDate:       \"2025-02-24\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tOperatingSystems: []OperatingSystem{\n\t\t\t\t{Name: \"Linux\", Version: \"5.10\"},\n\t\t\t},\n\t\t\tAffectedPackages: 5,\n\t\t},\n\t}\n\n\tif diff := cmp.Diff(expected, rows, cmpOpts()...); diff != \"\" {\n\t\tt.Errorf(\"unexpected rows (-want +got):\\n%s\", diff)\n\t}\n}\n\nfunc TestVulnerabilities(t *testing.T) {\n\tmockReader := new(mockVulnReader)\n\tvulnSpecs := v6.VulnerabilitySpecifiers{\n\t\t{Name: \"CVE-1234-5678\"},\n\t}\n\n\tmockReader.On(\"GetVulnerabilities\", mock.Anything, mock.Anything).Return([]v6.VulnerabilityHandle{\n\t\t{\n\t\t\tID:            1,\n\t\t\tName:          \"CVE-1234-5678\",\n\t\t\tStatus:        \"active\",\n\t\t\tPublishedDate: ptr(time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC)),\n\t\t\tModifiedDate:  ptr(time.Date(2023, 2, 1, 0, 0, 0, 0, time.UTC)),\n\t\t\tProvider:      &v6.Provider{ID: \"provider1\"},\n\t\t\tBlobValue: &v6.VulnerabilityBlob{\n\t\t\t\tDescription: \"Test description\",\n\t\t\t\tSeverities: []v6.Severity{\n\t\t\t\t\t{\n\t\t\t\t\t\tScheme: v6.SeveritySchemeCVSS,\n\t\t\t\t\t\tValue: v6.CVSSSeverity{\n\t\t\t\t\t\t\tVector:  \"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H\",\n\t\t\t\t\t\t\tVersion: \"3.1\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tSource: \"nvd\",\n\t\t\t\t\t\tRank:   1,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}, nil)\n\n\tmockReader.On(\"GetAffectedPackages\", mock.Anything, mock.Anything).Return([]v6.AffectedPackageHandle{\n\t\t{\n\t\t\tOperatingSystem: &v6.OperatingSystem{Name: \"Linux\", MajorVersion: \"5\", MinorVersion: \"10\"},\n\t\t},\n\t}, nil)\n\n\tmockReader.On(\"GetKnownExploitedVulnerabilities\", \"CVE-1234-5678\").Return([]v6.KnownExploitedVulnerabilityHandle{\n\t\t{\n\t\t\tCve: \"CVE-1234-5678\",\n\t\t\tBlobValue: &v6.KnownExploitedVulnerabilityBlob{\n\t\t\t\tCve:                        \"CVE-1234-5678\",\n\t\t\t\tVendorProject:              \"LinuxFoundation\",\n\t\t\t\tProduct:                    \"Linux\",\n\t\t\t\tDateAdded:                  ptr(time.Date(2025, 2, 2, 0, 0, 0, 0, time.UTC)),\n\t\t\t\tRequiredAction:             \"Yes\",\n\t\t\t\tDueDate:                    ptr(time.Date(2025, 2, 2, 0, 0, 0, 0, time.UTC)),\n\t\t\t\tKnownRansomwareCampaignUse: \"Known\",\n\t\t\t\tNotes:                      \"note!\",\n\t\t\t\tURLs:                       []string{\"https://example.com\"},\n\t\t\t\tCWEs:                       []string{\"CWE-1234\"},\n\t\t\t},\n\t\t},\n\t}, nil)\n\n\tmockReader.On(\"GetEpss\", \"CVE-1234-5678\").Return([]v6.EpssHandle{\n\t\t{\n\t\t\tCve:        \"CVE-1234-5678\",\n\t\t\tEpss:       0.893,\n\t\t\tPercentile: 0.99,\n\t\t\tDate:       time.Date(2025, 2, 24, 0, 0, 0, 0, time.UTC),\n\t\t},\n\t}, nil)\n\n\tresults, err := FindVulnerabilities(mockReader, VulnerabilitiesOptions{Vulnerability: vulnSpecs})\n\trequire.NoError(t, err)\n\n\texpected := []Vulnerability{\n\t\t{\n\t\t\tVulnerabilityInfo: VulnerabilityInfo{\n\t\t\t\tVulnerabilityBlob: v6.VulnerabilityBlob{\n\t\t\t\t\tDescription: \"Test description\",\n\t\t\t\t\tSeverities: []v6.Severity{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tScheme: \"CVSS\",\n\t\t\t\t\t\t\tValue: CVSSSeverity{\n\t\t\t\t\t\t\t\tVector:  \"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H\",\n\t\t\t\t\t\t\t\tVersion: \"3.1\",\n\t\t\t\t\t\t\t\tMetrics: CvssMetrics{\n\t\t\t\t\t\t\t\t\tBaseScore:           7.5,\n\t\t\t\t\t\t\t\t\tExploitabilityScore: ptr(3.9),\n\t\t\t\t\t\t\t\t\tImpactScore:         ptr(3.6),\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tSource: \"nvd\",\n\t\t\t\t\t\t\tRank:   1,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tSeverity:      \"high\",\n\t\t\t\tProvider:      \"provider1\",\n\t\t\t\tStatus:        \"active\",\n\t\t\t\tPublishedDate: ptr(time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC)),\n\t\t\t\tModifiedDate:  ptr(time.Date(2023, 2, 1, 0, 0, 0, 0, time.UTC)),\n\t\t\t\tWithdrawnDate: nil,\n\t\t\t\tKnownExploited: []KnownExploited{\n\t\t\t\t\t{\n\t\t\t\t\t\tCVE:                        \"CVE-1234-5678\",\n\t\t\t\t\t\tVendorProject:              \"LinuxFoundation\",\n\t\t\t\t\t\tProduct:                    \"Linux\",\n\t\t\t\t\t\tDateAdded:                  \"2025-02-02\",\n\t\t\t\t\t\tRequiredAction:             \"Yes\",\n\t\t\t\t\t\tDueDate:                    \"2025-02-02\",\n\t\t\t\t\t\tKnownRansomwareCampaignUse: \"Known\",\n\t\t\t\t\t\tNotes:                      \"note!\",\n\t\t\t\t\t\tURLs:                       []string{\"https://example.com\"},\n\t\t\t\t\t\tCWEs:                       []string{\"CWE-1234\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tEPSS: []EPSS{\n\t\t\t\t\t{\n\t\t\t\t\t\tCVE:        \"CVE-1234-5678\",\n\t\t\t\t\t\tEPSS:       0.893,\n\t\t\t\t\t\tPercentile: 0.99,\n\t\t\t\t\t\tDate:       \"2025-02-24\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tOperatingSystems: []OperatingSystem{\n\t\t\t\t{Name: \"Linux\", Version: \"5.10\"},\n\t\t\t},\n\t\t\tAffectedPackages: 1,\n\t\t},\n\t}\n\n\tif diff := cmp.Diff(expected, results, cmpOpts()...); diff != \"\" {\n\t\tt.Errorf(\"unexpected results (-want +got):\\n%s\", diff)\n\t}\n}\n\nfunc TestFindVulnerabilities_DecorationErrors(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\tkevErr  error\n\t\tepssErr error\n\t}{\n\t\t{\n\t\t\tname:    \"EPSS error is not fatal\",\n\t\t\tepssErr: fmt.Errorf(\"unable to fetch EPSS metadata: record not found\"),\n\t\t},\n\t\t{\n\t\t\tname:   \"KEV error is not fatal\",\n\t\t\tkevErr: fmt.Errorf(\"unable to fetch KEV records: record not found\"),\n\t\t},\n\t\t{\n\t\t\tname:    \"both EPSS and KEV errors are not fatal\",\n\t\t\tkevErr:  fmt.Errorf(\"unable to fetch KEV records: record not found\"),\n\t\t\tepssErr: fmt.Errorf(\"unable to fetch EPSS metadata: record not found\"),\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tmockReader := new(mockVulnReader)\n\n\t\t\tmockReader.On(\"GetVulnerabilities\", mock.Anything, mock.Anything).Return([]v6.VulnerabilityHandle{\n\t\t\t\t{\n\t\t\t\t\tID:       1,\n\t\t\t\t\tName:     \"CVE-2021-22947\",\n\t\t\t\t\tStatus:   \"active\",\n\t\t\t\t\tProvider: &v6.Provider{ID: \"nvd\"},\n\t\t\t\t\tBlobValue: &v6.VulnerabilityBlob{\n\t\t\t\t\t\tDescription: \"Test vuln\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}, nil)\n\n\t\t\tmockReader.On(\"GetAffectedPackages\", mock.Anything, mock.Anything).Return([]v6.AffectedPackageHandle{}, nil)\n\n\t\t\tmockReader.On(\"GetKnownExploitedVulnerabilities\", \"CVE-2021-22947\").Return(\n\t\t\t\t[]v6.KnownExploitedVulnerabilityHandle{}, tt.kevErr,\n\t\t\t)\n\n\t\t\tmockReader.On(\"GetEpss\", \"CVE-2021-22947\").Return(\n\t\t\t\t[]v6.EpssHandle{}, tt.epssErr,\n\t\t\t)\n\n\t\t\tresults, err := FindVulnerabilities(mockReader, VulnerabilitiesOptions{\n\t\t\t\tVulnerability: v6.VulnerabilitySpecifiers{{Name: \"CVE-2021-22947\"}},\n\t\t\t})\n\n\t\t\trequire.NoError(t, err, \"decoration errors should not propagate as fatal errors\")\n\t\t\trequire.Len(t, results, 1)\n\t\t\trequire.Equal(t, \"Test vuln\", results[0].VulnerabilityBlob.Description)\n\t\t\trequire.Empty(t, results[0].KnownExploited)\n\t\t\trequire.Empty(t, results[0].EPSS)\n\t\t})\n\t}\n}\n\ntype mockVulnReader struct {\n\tmock.Mock\n}\n\nfunc (m *mockVulnReader) GetVulnerabilities(vuln *v6.VulnerabilitySpecifier, config *v6.GetVulnerabilityOptions) ([]v6.VulnerabilityHandle, error) {\n\targs := m.Called(vuln, config)\n\treturn args.Get(0).([]v6.VulnerabilityHandle), args.Error(1)\n}\n\nfunc (m *mockVulnReader) GetAffectedPackages(pkg *v6.PackageSpecifier, config *v6.GetPackageOptions) ([]v6.AffectedPackageHandle, error) {\n\targs := m.Called(pkg, config)\n\treturn args.Get(0).([]v6.AffectedPackageHandle), args.Error(1)\n}\n\nfunc (m *mockVulnReader) GetKnownExploitedVulnerabilities(cve string) ([]v6.KnownExploitedVulnerabilityHandle, error) {\n\targs := m.Called(cve)\n\treturn args.Get(0).([]v6.KnownExploitedVulnerabilityHandle), args.Error(1)\n}\n\nfunc (m *mockVulnReader) GetEpss(cve string) ([]v6.EpssHandle, error) {\n\targs := m.Called(cve)\n\treturn args.Get(0).([]v6.EpssHandle), args.Error(1)\n}\n\nfunc (m *mockVulnReader) GetCWEs(cve string) ([]v6.CWEHandle, error) {\n\targs := m.Called(cve)\n\treturn args.Get(0).([]v6.CWEHandle), args.Error(1)\n}\n\nfunc cmpOpts() []cmp.Option {\n\treturn []cmp.Option{\n\t\tcmpopts.IgnoreFields(AffectedPackageInfo{}, \"Model\"),\n\t\tcmpopts.IgnoreFields(VulnerabilityInfo{}, \"Model\"),\n\t}\n}\n"
  },
  {
    "path": "cmd/grype/cli/commands/internal/dbsearch/vulnerability_decorations.go",
    "content": "package dbsearch\n\nimport (\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/hashicorp/go-multierror\"\n\t\"github.com/scylladb/go-set/strset\"\n\n\tv6 \"github.com/anchore/grype/grype/db/v6\"\n\t\"github.com/anchore/grype/internal/log\"\n)\n\ntype canonicalVulnerability interface {\n\tgetCVEs() []string\n\tdecorate(kevs []KnownExploited, epss []EPSS)\n}\n\n// vulnerabilityDecorations are separate model elements (not from VulnerabilityHandle) that is fetched on\n// the provider.GetMetadata() path instead of the provider.GetVulnerabilities() path. I hope for these two paths\n// to be merged into the same path come grype 1.0, in which case these elements would already be on the\n// store get methods when crafting a vulnerability.\ntype vulnerabilityDecorations struct {\n\tKnownExploited []KnownExploited `json:\"knownExploited,omitempty\"`\n\tEPSS           []EPSS           `json:\"epss,omitempty\"`\n}\n\nfunc decorateVulnerabilities(reader v6.VulnerabilityDecoratorStoreReader, cvs ...canonicalVulnerability) {\n\tfor _, cv := range cvs {\n\t\tcves := cv.getCVEs()\n\t\tif len(cves) == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tknownExploited, err := fetchKnownExploited(reader, cves)\n\t\tif err != nil {\n\t\t\tlog.WithFields(\"error\", err).Debug(\"unable to get known exploited vulnerabilities\")\n\t\t}\n\n\t\tepss, err := fetchEpss(reader, cves)\n\t\tif err != nil {\n\t\t\tlog.WithFields(\"error\", err).Debug(\"unable to get EPSS scores\")\n\t\t}\n\n\t\tcv.decorate(knownExploited, epss)\n\t}\n}\n\nfunc (afj *vulnerabilityAffectedPackageJoin) getCVEs() []string {\n\tif afj == nil {\n\t\treturn nil\n\t}\n\treturn getCVEs(&afj.Vulnerability)\n}\n\nfunc getCVEs(v *v6.VulnerabilityHandle) []string {\n\tvar cves []string\n\tset := strset.New()\n\n\taddCVE := func(id string) {\n\t\tlower := strings.ToLower(id)\n\t\tif strings.HasPrefix(lower, \"cve-\") {\n\t\t\tif !set.Has(lower) {\n\t\t\t\tcves = append(cves, id)\n\t\t\t\tset.Add(lower)\n\t\t\t}\n\t\t}\n\t}\n\n\tif v == nil {\n\t\treturn cves\n\t}\n\n\taddCVE(v.Name)\n\n\tif v.BlobValue == nil {\n\t\treturn cves\n\t}\n\n\taddCVE(v.BlobValue.ID)\n\n\tfor _, alias := range v.BlobValue.Aliases {\n\t\taddCVE(alias)\n\t}\n\n\treturn cves\n}\n\nfunc (vd *vulnerabilityDecorations) decorate(kevs []KnownExploited, epss []EPSS) {\n\tif vd == nil {\n\t\treturn\n\t}\n\n\tvd.KnownExploited = kevs\n\tvd.EPSS = epss\n}\n\nfunc fetchKnownExploited(reader v6.VulnerabilityDecoratorStoreReader, cves []string) ([]KnownExploited, error) {\n\tvar out []KnownExploited\n\tvar errs error\n\tfor _, cve := range cves {\n\t\tkevs, err := reader.GetKnownExploitedVulnerabilities(cve)\n\t\tif err != nil {\n\t\t\terrs = multierror.Append(errs, err)\n\t\t\tcontinue\n\t\t}\n\t\tfor _, kev := range kevs {\n\t\t\tout = append(out, KnownExploited{\n\t\t\t\tCVE:                        kev.Cve,\n\t\t\t\tVendorProject:              kev.BlobValue.VendorProject,\n\t\t\t\tProduct:                    kev.BlobValue.Product,\n\t\t\t\tDateAdded:                  kev.BlobValue.DateAdded.Format(time.DateOnly),\n\t\t\t\tRequiredAction:             kev.BlobValue.RequiredAction,\n\t\t\t\tDueDate:                    kev.BlobValue.DueDate.Format(time.DateOnly),\n\t\t\t\tKnownRansomwareCampaignUse: kev.BlobValue.KnownRansomwareCampaignUse,\n\t\t\t\tNotes:                      kev.BlobValue.Notes,\n\t\t\t\tURLs:                       kev.BlobValue.URLs,\n\t\t\t\tCWEs:                       kev.BlobValue.CWEs,\n\t\t\t})\n\t\t}\n\t}\n\treturn out, errs\n}\n\nfunc fetchEpss(reader v6.VulnerabilityDecoratorStoreReader, cves []string) ([]EPSS, error) {\n\tvar out []EPSS\n\tvar errs error\n\tfor _, cve := range cves {\n\t\tentries, err := reader.GetEpss(cve)\n\t\tif err != nil {\n\t\t\terrs = multierror.Append(errs, err)\n\t\t\tcontinue\n\t\t}\n\t\tfor _, entry := range entries {\n\t\t\tout = append(out, EPSS{\n\t\t\t\tCVE:        entry.Cve,\n\t\t\t\tEPSS:       entry.Epss,\n\t\t\t\tPercentile: entry.Percentile,\n\t\t\t\tDate:       entry.Date.Format(time.DateOnly),\n\t\t\t})\n\t\t}\n\t}\n\treturn out, errs\n}\n"
  },
  {
    "path": "cmd/grype/cli/commands/internal/jsonschema/main.go",
    "content": "package main\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"go/ast\"\n\t\"io\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"reflect\"\n\t\"strings\"\n\n\t\"github.com/invopop/jsonschema\"\n\t\"golang.org/x/tools/go/packages\"\n\n\t\"github.com/anchore/grype/cmd/grype/cli/commands/internal/dbsearch\"\n)\n\nfunc main() {\n\tpkgPatterns := []string{\"../dbsearch\", \"../../../../../../grype/db/v6\"}\n\n\tcomments := parseCommentsFromPackages(pkgPatterns)\n\tfmt.Printf(\"Extracted field comments from %d structs\\n\", len(comments))\n\n\tcompose(dbsearch.Matches{}, \"db-search\", dbsearch.MatchesSchemaVersion, comments)\n\tcompose(dbsearch.Vulnerabilities{}, \"db-search-vuln\", dbsearch.VulnerabilitiesSchemaVersion, comments)\n}\n\nfunc compose(document any, component, version string, comments map[string]map[string]string) {\n\twrite(encode(build(document, component, version, comments)), component, version)\n}\n\nfunc write(schema []byte, component, version string) {\n\tparent := filepath.Join(repoRoot(), \"schema\", \"grype\", component, \"json\")\n\tschemaPath := filepath.Join(parent, fmt.Sprintf(\"schema-%s.json\", version))\n\tlatestSchemaPath := filepath.Join(parent, \"schema-latest.json\")\n\n\tif _, err := os.Stat(schemaPath); !os.IsNotExist(err) {\n\t\t// check if the schema is the same...\n\t\texistingFh, err := os.Open(schemaPath)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\n\t\texistingSchemaBytes, err := io.ReadAll(existingFh)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\n\t\tif bytes.Equal(existingSchemaBytes, schema) {\n\t\t\t// the generated schema is the same, bail with no error :)\n\t\t\tfmt.Printf(\"No change to the existing %q schema!\\n\", component)\n\t\t\treturn\n\t\t}\n\n\t\t// the generated schema is different, bail with error :(\n\t\tfmt.Printf(\"Cowardly refusing to overwrite existing %q schema (%s)!\\nSee the README.md for how to increment\\n\", component, schemaPath)\n\t\tos.Exit(1)\n\t}\n\n\tfh, err := os.Create(schemaPath)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tdefer fh.Close()\n\n\t_, err = fh.Write(schema)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tlatestFile, err := os.Create(latestSchemaPath)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tdefer latestFile.Close()\n\n\t_, err = latestFile.Write(schema)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Printf(\"Wrote new %q schema to %q\\n\", component, schemaPath)\n}\n\nfunc encode(schema *jsonschema.Schema) []byte {\n\tnewSchemaBuffer := new(bytes.Buffer)\n\tenc := json.NewEncoder(newSchemaBuffer)\n\t// prevent > and < from being escaped in the payload\n\tenc.SetEscapeHTML(false)\n\tenc.SetIndent(\"\", \"  \")\n\terr := enc.Encode(&schema)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn newSchemaBuffer.Bytes()\n}\n\nfunc build(document any, component, version string, comments map[string]map[string]string) *jsonschema.Schema {\n\treflector := &jsonschema.Reflector{\n\t\tBaseSchemaID:              schemaID(component, version),\n\t\tAllowAdditionalProperties: true,\n\t\tNamer: func(r reflect.Type) string {\n\t\t\treturn strings.TrimPrefix(r.Name(), \"JSON\")\n\t\t},\n\t}\n\n\tdocumentSchema := reflector.ReflectFromType(reflect.TypeOf(document))\n\n\tfor structName, fields := range comments {\n\t\tif structSchema, exists := documentSchema.Definitions[structName]; exists {\n\t\t\tif structSchema.Definitions == nil {\n\t\t\t\tstructSchema.Definitions = make(map[string]*jsonschema.Schema)\n\t\t\t}\n\t\t\tfor fieldName, comment := range fields {\n\t\t\t\tif fieldName == \"\" {\n\t\t\t\t\t// struct-level comment\n\t\t\t\t\tstructSchema.Description = comment\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\t// field level comment\n\t\t\t\tif comment == \"\" {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif _, exists := structSchema.Properties.Get(fieldName); exists {\n\t\t\t\t\tfieldSchema, exists := structSchema.Definitions[fieldName]\n\t\t\t\t\tif exists {\n\t\t\t\t\t\tfieldSchema.Description = comment\n\t\t\t\t\t} else {\n\t\t\t\t\t\tfieldSchema = &jsonschema.Schema{\n\t\t\t\t\t\t\tDescription: comment,\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tstructSchema.Definitions[fieldName] = fieldSchema\n\t\t\t\t}\n\t\t\t}\n\t\t\tdocumentSchema.Definitions[structName] = structSchema\n\t\t}\n\t}\n\n\treturn documentSchema\n}\n\n// parseCommentsFromPackages scans multiple packages and collects field comments for structs.\nfunc parseCommentsFromPackages(pkgPatterns []string) map[string]map[string]string {\n\tcommentMap := make(map[string]map[string]string)\n\n\tcfg := &packages.Config{\n\t\tMode: packages.NeedFiles | packages.NeedSyntax | packages.NeedDeps | packages.NeedImports,\n\t}\n\tpkgs, err := packages.Load(cfg, pkgPatterns...)\n\tif err != nil {\n\t\tpanic(fmt.Errorf(\"failed to load packages: %w\", err))\n\t}\n\n\tfor _, pkg := range pkgs {\n\t\tfor _, file := range pkg.Syntax {\n\t\t\tfileComments := parseFileComments(file)\n\t\t\tfor structName, fields := range fileComments {\n\t\t\t\tif _, exists := commentMap[structName]; !exists {\n\t\t\t\t\tcommentMap[structName] = fields\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn commentMap\n}\n\n// parseFileComments extracts comments for structs and their fields in a single file.\nfunc parseFileComments(node *ast.File) map[string]map[string]string {\n\tcommentMap := make(map[string]map[string]string)\n\n\tast.Inspect(node, func(n ast.Node) bool {\n\t\tts, ok := n.(*ast.TypeSpec)\n\t\tif !ok {\n\t\t\treturn true\n\t\t}\n\t\tst, ok := ts.Type.(*ast.StructType)\n\t\tif !ok {\n\t\t\treturn true\n\t\t}\n\n\t\tstructName := ts.Name.Name\n\t\tfieldComments := make(map[string]string)\n\n\t\t// extract struct-level comment\n\t\tif ts.Doc != nil {\n\t\t\tstructComment := strings.TrimSpace(ts.Doc.Text())\n\t\t\tif !strings.Contains(structComment, \"TODO:\") {\n\t\t\t\tfieldComments[\"\"] = cleanComment(structComment)\n\t\t\t}\n\t\t}\n\n\t\t// extract field-level comments\n\t\tfor _, field := range st.Fields.List {\n\t\t\tif len(field.Names) == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tfieldName := field.Names[0].Name\n\t\t\tjsonTag := getJSONTag(field)\n\n\t\t\tif field.Doc != nil {\n\t\t\t\tcomment := strings.TrimSpace(field.Doc.Text())\n\t\t\t\tif strings.Contains(comment, \"TODO:\") {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif jsonTag != \"\" {\n\t\t\t\t\tfieldComments[jsonTag] = cleanComment(comment)\n\t\t\t\t} else {\n\t\t\t\t\tfieldComments[fieldName] = cleanComment(comment)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif len(fieldComments) > 0 {\n\t\t\tcommentMap[structName] = fieldComments\n\t\t}\n\t\treturn true\n\t})\n\n\treturn commentMap\n}\n\nfunc cleanComment(comment string) string {\n\t// remove the first word, since that is the field name (if following go-doc patterns)\n\tsplit := strings.SplitN(comment, \" \", 2)\n\tif len(split) > 1 {\n\t\tcomment = split[1]\n\t}\n\n\treturn strings.TrimSpace(strings.ReplaceAll(comment, \"\\\"\", \"'\"))\n}\n\nfunc getJSONTag(field *ast.Field) string {\n\tif field.Tag != nil {\n\t\ttagValue := strings.Trim(field.Tag.Value, \"`\")\n\t\tstructTag := reflect.StructTag(tagValue)\n\t\tif jsonTag, ok := structTag.Lookup(\"json\"); ok {\n\t\t\tjsonParts := strings.Split(jsonTag, \",\")\n\t\t\treturn strings.TrimSpace(jsonParts[0])\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc schemaID(component, version string) jsonschema.ID {\n\treturn jsonschema.ID(fmt.Sprintf(\"anchore.io/schema/grype/%s/json/%s\", component, version))\n}\n\nfunc repoRoot() string {\n\troot, err := exec.Command(\"git\", \"rev-parse\", \"--show-toplevel\").Output()\n\tif err != nil {\n\t\tpanic(fmt.Errorf(\"unable to find repo root dir: %+v\", err))\n\t}\n\tabsRepoRoot, err := filepath.Abs(strings.TrimSpace(string(root)))\n\tif err != nil {\n\t\tpanic(fmt.Errorf(\"unable to get abs path to repo root: %w\", err))\n\t}\n\treturn absRepoRoot\n}\n"
  },
  {
    "path": "cmd/grype/cli/commands/root.go",
    "content": "package commands\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/spf13/cobra\"\n\t\"github.com/wagoodman/go-partybus\"\n\n\t\"github.com/anchore/clio\"\n\t\"github.com/anchore/grype/cmd/grype/cli/options\"\n\t\"github.com/anchore/grype/grype\"\n\t\"github.com/anchore/grype/grype/distro\"\n\t\"github.com/anchore/grype/grype/event\"\n\t\"github.com/anchore/grype/grype/event/parsers\"\n\t\"github.com/anchore/grype/grype/grypeerr\"\n\t\"github.com/anchore/grype/grype/match\"\n\t\"github.com/anchore/grype/grype/matcher\"\n\t\"github.com/anchore/grype/grype/matcher/dotnet\"\n\t\"github.com/anchore/grype/grype/matcher/dpkg\"\n\t\"github.com/anchore/grype/grype/matcher/golang\"\n\t\"github.com/anchore/grype/grype/matcher/hex\"\n\t\"github.com/anchore/grype/grype/matcher/java\"\n\t\"github.com/anchore/grype/grype/matcher/javascript\"\n\t\"github.com/anchore/grype/grype/matcher/python\"\n\t\"github.com/anchore/grype/grype/matcher/rpm\"\n\t\"github.com/anchore/grype/grype/matcher/ruby\"\n\t\"github.com/anchore/grype/grype/matcher/stock\"\n\t\"github.com/anchore/grype/grype/pkg\"\n\t\"github.com/anchore/grype/grype/presenter/models\"\n\t\"github.com/anchore/grype/grype/version\"\n\t\"github.com/anchore/grype/grype/vex\"\n\tvexStatus \"github.com/anchore/grype/grype/vex/status\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n\t\"github.com/anchore/grype/internal\"\n\t\"github.com/anchore/grype/internal/bus\"\n\t\"github.com/anchore/grype/internal/format\"\n\t\"github.com/anchore/grype/internal/log\"\n\t\"github.com/anchore/grype/internal/stringutil\"\n\t\"github.com/anchore/syft/syft\"\n\t\"github.com/anchore/syft/syft/cataloging\"\n\tsyftPkg \"github.com/anchore/syft/syft/pkg\"\n\t\"github.com/anchore/syft/syft/sbom\"\n)\n\nfunc Root(app clio.Application) *cobra.Command {\n\topts := options.DefaultGrype(app.ID())\n\n\treturn app.SetupRootCommand(&cobra.Command{\n\t\tUse:   fmt.Sprintf(\"%s [IMAGE]\", app.ID().Name),\n\t\tShort: \"A vulnerability scanner for container images, filesystems, and SBOMs\",\n\t\tLong: stringutil.Tprintf(`A vulnerability scanner for container images, filesystems, and SBOMs.\n\nSupports the following image sources:\n    {{.appName}} yourrepo/yourimage:tag             defaults to using images from a Docker daemon\n    {{.appName}} path/to/yourproject                a Docker tar, OCI tar, OCI directory, SIF container, or generic filesystem directory\n\nYou can also explicitly specify the scheme to use:\n    {{.appName}} podman:yourrepo/yourimage:tag          explicitly use the Podman daemon\n    {{.appName}} docker:yourrepo/yourimage:tag          explicitly use the Docker daemon\n    {{.appName}} docker-archive:path/to/yourimage.tar   use a tarball from disk for archives created from \"docker save\"\n    {{.appName}} oci-archive:path/to/yourimage.tar      use a tarball from disk for OCI archives (from Podman or otherwise)\n    {{.appName}} oci-dir:path/to/yourimage              read directly from a path on disk for OCI layout directories (from Skopeo or otherwise)\n    {{.appName}} singularity:path/to/yourimage.sif      read directly from a Singularity Image Format (SIF) container on disk\n    {{.appName}} dir:path/to/yourproject                read directly from a path on disk (any directory)\n    {{.appName}} file:path/to/yourfile                  read directly from a file on disk\n    {{.appName}} sbom:path/to/syft.json                 read Syft JSON from path on disk\n    {{.appName}} registry:yourrepo/yourimage:tag        pull image directly from a registry (no container runtime required)\n    {{.appName}} purl:path/to/purl/file                 read a newline separated file of package URLs from a path on disk\n    {{.appName}} PURL                                   read a single package PURL directly (e.g. pkg:apk/openssl@3.2.1?distro=alpine-3.20.3)\n    {{.appName}} cpes:path/to/cpes/file                 read a newline separated file of package CPEs from a path on disk\n    {{.appName}} CPE                                    read a single CPE directly (e.g. cpe:2.3:a:openssl:openssl:3.0.14:*:*:*:*:*)\n\nYou can also pipe in Syft JSON directly:\n\tsyft yourimage:tag -o json | {{.appName}}\n\n`, map[string]interface{}{\n\t\t\t\"appName\": app.ID().Name,\n\t\t}),\n\t\tArgs:          validateRootArgs,\n\t\tSilenceUsage:  true,\n\t\tSilenceErrors: true,\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\tuserInput := \"\"\n\t\t\tif len(args) > 0 {\n\t\t\t\tuserInput = args[0]\n\t\t\t}\n\t\t\treturn runGrype(cmd.Context(), app, opts, userInput)\n\t\t},\n\t\tValidArgsFunction: dockerImageValidArgsFunction,\n\t}, opts)\n}\n\nvar ignoreNonFixedMatches = []match.IgnoreRule{\n\t{FixState: string(vulnerability.FixStateNotFixed)},\n\t{FixState: string(vulnerability.FixStateWontFix)},\n\t{FixState: string(vulnerability.FixStateUnknown)},\n}\n\nvar ignoreFixedMatches = []match.IgnoreRule{\n\t{FixState: string(vulnerability.FixStateFixed)},\n}\n\nvar ignoreVEXFixedNotAffected = []match.IgnoreRule{\n\t{VexStatus: string(vexStatus.NotAffected)},\n\t{VexStatus: string(vexStatus.Fixed)},\n}\n\nvar ignoreLinuxKernelHeaders = []match.IgnoreRule{\n\t{Package: match.IgnoreRulePackage{Name: \"kernel-headers\", UpstreamName: \"kernel\", Type: string(syftPkg.RpmPkg)}, MatchType: match.ExactIndirectMatch},\n\t{Package: match.IgnoreRulePackage{Name: \"linux(-.*)?-headers-.*\", UpstreamName: \"linux.*\", Type: string(syftPkg.DebPkg)}, MatchType: match.ExactIndirectMatch},\n\t{Package: match.IgnoreRulePackage{Name: \"linux-libc-dev\", UpstreamName: \"linux\", Type: string(syftPkg.DebPkg)}, MatchType: match.ExactIndirectMatch},\n}\n\n//nolint:funlen\nfunc runGrype(ctx context.Context, app clio.Application, opts *options.Grype, userInput string) (errs error) {\n\twriter, err := format.MakeScanResultWriter(opts.Outputs, opts.File, format.PresentationConfig{\n\t\tTemplateFilePath: opts.OutputTemplateFile,\n\t\tShowSuppressed:   opts.ShowSuppressed,\n\t\tPretty:           opts.Pretty,\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar vp vulnerability.Provider\n\tvar status *vulnerability.ProviderStatus\n\tvar packages []pkg.Package\n\tvar s *sbom.SBOM\n\tvar pkgContext pkg.Context\n\n\tif opts.OnlyFixed {\n\t\topts.Ignore = append(opts.Ignore, ignoreNonFixedMatches...)\n\t}\n\n\tif opts.OnlyNotFixed {\n\t\topts.Ignore = append(opts.Ignore, ignoreFixedMatches...)\n\t}\n\n\tif !opts.MatchUpstreamKernelHeaders {\n\t\topts.Ignore = append(opts.Ignore, ignoreLinuxKernelHeaders...)\n\t}\n\n\tfor _, ignoreState := range stringutil.SplitCommaSeparatedString(opts.IgnoreStates) {\n\t\tswitch vulnerability.FixState(ignoreState) {\n\t\tcase vulnerability.FixStateUnknown, vulnerability.FixStateFixed, vulnerability.FixStateNotFixed, vulnerability.FixStateWontFix:\n\t\t\topts.Ignore = append(opts.Ignore, match.IgnoreRule{FixState: ignoreState})\n\t\tdefault:\n\t\t\treturn fmt.Errorf(\"unknown fix state %s was supplied for --ignore-states\", ignoreState)\n\t\t}\n\t}\n\n\terr = parallel(\n\t\tfunc() error {\n\t\t\tcheckForAppUpdate(app.ID(), opts)\n\t\t\treturn nil\n\t\t},\n\t\tfunc() (err error) {\n\t\t\tstartTime := time.Now()\n\n\t\t\tdefer func() {\n\t\t\t\tvalidStr := \"valid\"\n\t\t\t\tif err != nil {\n\t\t\t\t\tvalidStr = \"invalid\"\n\t\t\t\t}\n\t\t\t\tlog.WithFields(\"time\", time.Since(startTime), \"status\", validStr).Info(\"loaded DB\")\n\t\t\t\tif status != nil {\n\t\t\t\t\tlog.WithFields(\"schema\", status.SchemaVersion).Debug(\"├──\")\n\t\t\t\t\tlog.WithFields(\"built\", status.Built.UTC().Format(time.RFC3339)).Debug(\"├──\")\n\t\t\t\t\tlog.WithFields(\"from\", status.From).Debug(\"├──\")\n\t\t\t\t\tlog.WithFields(\"path\", status.Path).Debug(\"└──\")\n\t\t\t\t}\n\t\t\t}()\n\t\t\tlog.Debug(\"loading DB\")\n\t\t\tvp, status, err = grype.LoadVulnerabilityDB(opts.ToClientConfig(), opts.ToCuratorConfig(), opts.DB.AutoUpdate)\n\n\t\t\treturn validateDBLoad(err, status)\n\t\t},\n\t\tfunc() (err error) {\n\t\t\tstartTime := time.Now()\n\n\t\t\tdefer func() {\n\t\t\t\tlog.WithFields(\"time\", time.Since(startTime), \"packages\", len(packages)).Info(\"gathered packages\")\n\t\t\t}()\n\n\t\t\tlog.Debugf(\"gathering packages\")\n\t\t\t// packages are grype.Package, not syft.Package\n\t\t\t// the SBOM is returned for downstream formatting concerns\n\t\t\t// grype uses the SBOM in combination with syft formatters to produce cycloneDX\n\t\t\t// with vulnerability information appended\n\t\t\tpackages, pkgContext, s, err = pkg.Provide(userInput, getProviderConfig(opts))\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to catalog: %w\", err)\n\t\t\t}\n\n\t\t\treturn nil\n\t\t},\n\t)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tdefer log.CloseAndLogError(vp, status.Path)\n\n\twarnWhenDistroHintNeeded(packages, &pkgContext)\n\n\tif err = applyVexRules(opts); err != nil {\n\t\treturn fmt.Errorf(\"applying vex rules: %w\", err)\n\t}\n\n\tstartTime := time.Now()\n\n\tvexProcessor, err := vex.NewProcessor(vex.ProcessorOptions{\n\t\tDocuments:   opts.VexDocuments,\n\t\tIgnoreRules: opts.Ignore,\n\t})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create VEX processor: %w\", err)\n\t}\n\n\tvulnMatcher := grype.VulnerabilityMatcher{\n\t\tVulnerabilityProvider: vp,\n\t\tIgnoreRules:           opts.Ignore,\n\t\tNormalizeByCVE:        opts.ByCVE,\n\t\tFailSeverity:          opts.FailOnSeverity(),\n\t\tMatchers:              getMatchers(opts),\n\t\tVexProcessor:          vexProcessor,\n\t\tAlerts: grype.AlertsConfig{\n\t\t\tEnableEOLDistroWarnings: opts.Alerts.EnableEOLDistroWarnings,\n\t\t},\n\t}\n\n\tremainingMatches, ignoredMatches, err := vulnMatcher.FindMatchesContext(ctx, packages, pkgContext)\n\tif err != nil {\n\t\tif !errors.Is(err, grypeerr.ErrAboveSeverityThreshold) {\n\t\t\treturn err\n\t\t}\n\t\terrs = appendErrors(errs, err)\n\t}\n\n\tlog.WithFields(\"time\", time.Since(startTime)).Info(\"found vulnerability matches\")\n\tstartTime = time.Now()\n\n\t// clear out the registry auth information to avoid including possibly sensitive information in the report\n\topts.Registry.Auth = nil\n\n\t// collect distro alert data from the vulnerability matcher (if enabled)\n\tvar distroAlertData *models.DistroAlertData\n\tif opts.Alerts.EnableEOLDistroWarnings {\n\t\tdistroAlertData = &models.DistroAlertData{\n\t\t\tEOLDistroPackages: vulnMatcher.EOLDistroPackages(),\n\t\t}\n\t\twarnDistroAlerts(distroAlertData)\n\t}\n\n\tmodel, err := models.NewDocument(app.ID(), packages, pkgContext, *remainingMatches, ignoredMatches, vp, opts, dbInfo(status, vp), models.SortStrategy(opts.SortBy.Criteria), opts.Timestamp, distroAlertData)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create document: %w\", err)\n\t}\n\n\tif err = writer.Write(models.PresenterConfig{\n\t\tID:       app.ID(),\n\t\tDocument: model,\n\t\tSBOM:     s,\n\t\tPretty:   opts.Pretty,\n\t}); err != nil {\n\t\terrs = appendErrors(errs, err)\n\t}\n\n\tlog.WithFields(\"time\", time.Since(startTime)).Trace(\"wrote vulnerability report\")\n\n\treturn errs\n}\n\nfunc warnWhenDistroHintNeeded(pkgs []pkg.Package, context *pkg.Context) {\n\thasOSPackageWithoutDistro := false\n\tfor _, p := range pkgs {\n\t\tswitch p.Type {\n\t\tcase syftPkg.AlpmPkg, syftPkg.DebPkg, syftPkg.RpmPkg, syftPkg.KbPkg:\n\t\t\tif p.Distro == nil {\n\t\t\t\thasOSPackageWithoutDistro = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\tif context.Distro == nil && hasOSPackageWithoutDistro {\n\t\tlog.Warnf(\"Unable to determine the OS distribution of some packages. This may result in missing vulnerabilities. \" +\n\t\t\t\"You may specify a distro using: --distro <distro>:<version>\")\n\t}\n}\n\nfunc warnDistroAlerts(data *models.DistroAlertData) {\n\tif data == nil {\n\t\treturn\n\t}\n\n\t// warn about EOL distro packages\n\tfor distroName, count := range countPackagesByDistro(data.EOLDistroPackages) {\n\t\tmsg := fmt.Sprintf(\"%d packages from EOL distro %q - vulnerability data may be incomplete or outdated; consider upgrading to a supported version\", count, distroName)\n\t\tbus.Notify(msg)\n\t}\n}\n\nfunc countPackagesByDistro(packages []pkg.Package) map[string]int {\n\tcounts := make(map[string]int)\n\tfor _, p := range packages {\n\t\tdistroName := \"unknown\"\n\t\tif p.Distro != nil {\n\t\t\tdistroName = p.Distro.String()\n\t\t}\n\t\tcounts[distroName]++\n\t}\n\treturn counts\n}\n\nfunc dbInfo(status *vulnerability.ProviderStatus, vp vulnerability.Provider) any {\n\tvar providers map[string]vulnerability.DataProvenance\n\n\tif vp != nil {\n\t\tproviders = make(map[string]vulnerability.DataProvenance)\n\t\tif dpr, ok := vp.(vulnerability.StoreMetadataProvider); ok {\n\t\t\tdps, err := dpr.DataProvenance()\n\t\t\t// ignore errors here\n\t\t\tif err == nil {\n\t\t\t\tproviders = dps\n\t\t\t}\n\t\t}\n\t}\n\n\treturn struct {\n\t\tStatus    *vulnerability.ProviderStatus           `json:\"status\"`\n\t\tProviders map[string]vulnerability.DataProvenance `json:\"providers\"`\n\t}{\n\t\tStatus:    status,\n\t\tProviders: providers,\n\t}\n}\n\nfunc checkForAppUpdate(id clio.Identification, opts *options.Grype) {\n\tif !opts.CheckForAppUpdate {\n\t\treturn\n\t}\n\n\tisAvailable, newVersion, err := isUpdateAvailable(id)\n\tif err != nil {\n\t\tlog.Errorf(err.Error())\n\t}\n\tif isAvailable {\n\t\tlog.Infof(\"new version of %s is available: %s (currently running: %s)\", id.Name, newVersion, id.Version)\n\n\t\tbus.Publish(partybus.Event{\n\t\t\tType: event.CLIAppUpdateAvailable,\n\t\t\tValue: parsers.UpdateCheck{\n\t\t\t\tNew:     newVersion,\n\t\t\t\tCurrent: id.Version,\n\t\t\t},\n\t\t})\n\t} else {\n\t\tlog.Debugf(\"no new %s application update available\", id.Name)\n\t}\n}\n\nfunc getMatcherConfig(opts *options.Grype) matcher.Config {\n\treturn matcher.Config{\n\t\tJava: java.MatcherConfig{\n\t\t\tExternalSearchConfig: opts.ExternalSources.ToJavaMatcherConfig(),\n\t\t\tUseCPEs:              opts.Match.Java.UseCPEs,\n\t\t},\n\t\tRuby:       ruby.MatcherConfig(opts.Match.Ruby),\n\t\tPython:     python.MatcherConfig(opts.Match.Python),\n\t\tDotnet:     dotnet.MatcherConfig(opts.Match.Dotnet),\n\t\tJavascript: javascript.MatcherConfig(opts.Match.Javascript),\n\t\tGolang: golang.MatcherConfig{\n\t\t\tUseCPEs:                                opts.Match.Golang.UseCPEs,\n\t\t\tAlwaysUseCPEForStdlib:                  opts.Match.Golang.AlwaysUseCPEForStdlib,\n\t\t\tAllowMainModulePseudoVersionComparison: opts.Match.Golang.AllowMainModulePseudoVersionComparison,\n\t\t},\n\t\tHex:   hex.MatcherConfig(opts.Match.Hex),\n\t\tStock: stock.MatcherConfig(opts.Match.Stock),\n\t\tDpkg: dpkg.MatcherConfig{\n\t\t\tMissingEpochStrategy: opts.Match.Dpkg.MissingEpochStrategy,\n\t\t\tUseCPEsForEOL:        opts.Match.Dpkg.UseCPEsForEOL,\n\t\t},\n\t\tRpm: rpm.MatcherConfig{\n\t\t\tMissingEpochStrategy: opts.Match.Rpm.MissingEpochStrategy,\n\t\t\tUseCPEsForEOL:        opts.Match.Rpm.UseCPEsForEOL,\n\t\t},\n\t}\n}\n\nfunc getMatchers(opts *options.Grype) []match.Matcher {\n\treturn matcher.NewDefaultMatchers(getMatcherConfig(opts))\n}\n\nfunc getProviderConfig(opts *options.Grype) pkg.ProviderConfig {\n\tcfg := syft.DefaultCreateSBOMConfig()\n\tcfg.Packages.JavaArchive.IncludeIndexedArchives = opts.Search.IncludeIndexedArchives\n\tcfg.Packages.JavaArchive.IncludeUnindexedArchives = opts.Search.IncludeUnindexedArchives\n\n\t// when we run into a package with missing information like version, then this is not useful in the context\n\t// of vulnerability matching. Though there will be downstream processing to handle this case, we can still\n\t// save us the effort of ever attempting to match with these packages as early as possible.\n\tcfg.Compliance.MissingVersion = cataloging.ComplianceActionDrop\n\n\treturn pkg.ProviderConfig{\n\t\tSyftProviderConfig: pkg.SyftProviderConfig{\n\t\t\tRegistryOptions:        opts.Registry.ToOptions(),\n\t\t\tExclusions:             opts.Exclusions,\n\t\t\tSBOMOptions:            cfg,\n\t\t\tPlatform:               opts.Platform,\n\t\t\tName:                   opts.Name,\n\t\t\tDefaultImagePullSource: opts.DefaultImagePullSource,\n\t\t\tSources:                opts.From,\n\t\t},\n\t\tSynthesisConfig: pkg.SynthesisConfig{\n\t\t\tGenerateMissingCPEs: opts.GenerateMissingCPEs,\n\t\t\tDistro: pkg.DistroConfig{\n\t\t\t\tOverride:    applyDistroHint(opts.Distro),\n\t\t\t\tFixChannels: getFixChannels(opts.FixChannel),\n\t\t\t},\n\t\t},\n\t}\n}\n\nfunc getFixChannels(fixChannelOpts options.FixChannels) distro.FixChannels {\n\t// use the API defaults as a starting point, then overlay the application options\n\teusOptions := distro.DefaultFixChannels().Get(\"eus\")\n\n\tif eusOptions == nil {\n\t\tpanic(\"default fix channels do not contain Red Hat EUS channel\")\n\t}\n\n\teusOptions.Apply = distro.FixChannelEnabled(fixChannelOpts.RedHatEUS.Apply)\n\tif fixChannelOpts.RedHatEUS.Versions != \"\" {\n\t\teusOptions.Versions = version.MustGetConstraint(fixChannelOpts.RedHatEUS.Versions, version.SemanticFormat)\n\t}\n\n\treturn []distro.FixChannel{\n\t\t{\n\t\t\t// information inherent to the channel (part of the API defaults)\n\t\t\tName: \"eus\",\n\t\t\tIDs:  eusOptions.IDs,\n\n\t\t\t// user configurable options\n\t\t\tVersions: eusOptions.Versions,\n\t\t\tApply:    eusOptions.Apply,\n\t\t},\n\t}\n}\n\nfunc applyDistroHint(hint string) *distro.Distro {\n\tif hint == \"\" {\n\t\treturn nil\n\t}\n\n\tname, version := distro.ParseDistroString(hint)\n\treturn distro.NewFromNameVersion(name, version)\n}\n\nfunc validateDBLoad(loadErr error, status *vulnerability.ProviderStatus) error {\n\tif loadErr != nil {\n\t\t// notify the user about grype db delete to fix checksum errors\n\t\tif strings.Contains(loadErr.Error(), \"checksum\") {\n\t\t\tbus.Notify(\"Database checksum invalid, run `grype db delete` to remove it and `grype db update` to update.\")\n\t\t}\n\t\tif strings.Contains(loadErr.Error(), \"import.json\") {\n\t\t\tbus.Notify(\"Unable to find database import metadata, run `grype db delete` to remove the existing database and `grype db update` to update.\")\n\t\t}\n\t\treturn fmt.Errorf(\"failed to load vulnerability db: %w\", loadErr)\n\t}\n\tif status == nil {\n\t\treturn fmt.Errorf(\"unable to determine the status of the vulnerability db\")\n\t}\n\tif status.Error != nil {\n\t\treturn fmt.Errorf(\"db could not be loaded: %w\", status.Error)\n\t}\n\treturn nil\n}\n\nfunc validateRootArgs(cmd *cobra.Command, args []string) error {\n\tisStdinPipeOrRedirect, err := internal.IsStdinPipeOrRedirect()\n\tif err != nil {\n\t\tlog.Warnf(\"unable to determine if there is piped input: %+v\", err)\n\t\tisStdinPipeOrRedirect = false\n\t}\n\n\tif len(args) == 0 && !isStdinPipeOrRedirect {\n\t\t// in the case that no arguments are given and there is no piped input we want to show the help text and return with a non-0 return code.\n\t\tif err := cmd.Help(); err != nil {\n\t\t\treturn fmt.Errorf(\"unable to display help: %w\", err)\n\t\t}\n\t\treturn fmt.Errorf(\"an image/directory argument is required\")\n\t}\n\n\t// in the case that a single empty string argument (\"\") is given and there is no piped input we want to show the help text and return with a non-0 return code.\n\tif len(args) != 0 && args[0] == \"\" && !isStdinPipeOrRedirect {\n\t\tif err := cmd.Help(); err != nil {\n\t\t\treturn fmt.Errorf(\"unable to display help: %w\", err)\n\t\t}\n\t\treturn fmt.Errorf(\"an image/directory argument is required\")\n\t}\n\n\treturn cobra.MaximumNArgs(1)(cmd, args)\n}\n\nfunc applyVexRules(opts *options.Grype) error {\n\t// If any vex documents are provided, assume the user intends to ignore vulnerabilities that those\n\t// vex documents list as \"fixed\" or \"not_affected\".\n\tif len(opts.VexDocuments) > 0 {\n\t\topts.Ignore = append(opts.Ignore, ignoreVEXFixedNotAffected...)\n\t}\n\n\tfor _, status := range opts.VexAdd {\n\t\tswitch status {\n\t\tcase string(vexStatus.Affected):\n\t\t\topts.Ignore = append(\n\t\t\t\topts.Ignore, match.IgnoreRule{VexStatus: string(vexStatus.Affected)},\n\t\t\t)\n\t\tcase string(vexStatus.UnderInvestigation):\n\t\t\topts.Ignore = append(\n\t\t\t\topts.Ignore, match.IgnoreRule{VexStatus: string(vexStatus.UnderInvestigation)},\n\t\t\t)\n\t\tdefault:\n\t\t\treturn fmt.Errorf(\"invalid VEX status in vex-add setting: %s\", status)\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "cmd/grype/cli/commands/root_test.go",
    "content": "package commands\n\nimport (\n\t\"testing\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/google/go-cmp/cmp/cmpopts\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/clio\"\n\t\"github.com/anchore/grype/cmd/grype/cli/options\"\n\t\"github.com/anchore/grype/grype/distro\"\n\t\"github.com/anchore/grype/grype/match\"\n\t\"github.com/anchore/grype/grype/matcher\"\n\t\"github.com/anchore/grype/grype/matcher/dotnet\"\n\t\"github.com/anchore/grype/grype/matcher/dpkg\"\n\t\"github.com/anchore/grype/grype/matcher/golang\"\n\t\"github.com/anchore/grype/grype/matcher/hex\"\n\t\"github.com/anchore/grype/grype/matcher/java\"\n\t\"github.com/anchore/grype/grype/matcher/javascript\"\n\t\"github.com/anchore/grype/grype/matcher/python\"\n\t\"github.com/anchore/grype/grype/matcher/rpm\"\n\t\"github.com/anchore/grype/grype/matcher/ruby\"\n\t\"github.com/anchore/grype/grype/matcher/stock\"\n\t\"github.com/anchore/grype/grype/pkg\"\n\t\"github.com/anchore/grype/grype/version\"\n\tvexStatus \"github.com/anchore/grype/grype/vex/status\"\n\t\"github.com/anchore/stereoscope/pkg/image\"\n\t\"github.com/anchore/syft/syft\"\n\t\"github.com/anchore/syft/syft/cataloging\"\n\t\"github.com/anchore/syft/syft/pkg/cataloger/binary\"\n)\n\nfunc Test_getProviderConfig(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\topts *options.Grype\n\t\twant pkg.ProviderConfig\n\t}{\n\t\t{\n\t\t\tname: \"syft default api options are used\",\n\t\t\topts: options.DefaultGrype(clio.Identification{\n\t\t\t\tName:    \"test\",\n\t\t\t\tVersion: \"1.0\",\n\t\t\t}),\n\t\t\twant: pkg.ProviderConfig{\n\t\t\t\tSyftProviderConfig: pkg.SyftProviderConfig{\n\t\t\t\t\tSBOMOptions: func() *syft.CreateSBOMConfig {\n\t\t\t\t\t\tcfg := syft.DefaultCreateSBOMConfig()\n\t\t\t\t\t\tcfg.Compliance.MissingVersion = cataloging.ComplianceActionDrop\n\t\t\t\t\t\treturn cfg\n\t\t\t\t\t}(),\n\t\t\t\t\tRegistryOptions: &image.RegistryOptions{\n\t\t\t\t\t\tCredentials: []image.RegistryCredentials{},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tSynthesisConfig: pkg.SynthesisConfig{\n\t\t\t\t\tGenerateMissingCPEs: false,\n\t\t\t\t\tDistro: pkg.DistroConfig{\n\t\t\t\t\t\tOverride: nil,\n\t\t\t\t\t\tFixChannels: []distro.FixChannel{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tName:     \"eus\",\n\t\t\t\t\t\t\t\tIDs:      []string{\"rhel\"},\n\t\t\t\t\t\t\t\tApply:    \"auto\",\n\t\t\t\t\t\t\t\tVersions: version.MustGetConstraint(\">= 8.0\", version.SemanticFormat),\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\topts := cmp.Options{\n\t\t\t\tcmpopts.IgnoreFields(binary.Classifier{}, \"EvidenceMatcher\"),\n\t\t\t\tcmpopts.IgnoreUnexported(syft.CreateSBOMConfig{}),\n\t\t\t}\n\t\t\tif d := cmp.Diff(tt.want, getProviderConfig(tt.opts), opts...); d != \"\" {\n\t\t\t\tt.Errorf(\"getProviderConfig() mismatch (-want +got):\\n%s\", d)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_getMatcherConfig(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\topts *options.Grype\n\t\twant matcher.Config\n\t}{\n\t\t{\n\t\t\tname: \"default options\",\n\t\t\topts: options.DefaultGrype(clio.Identification{\n\t\t\t\tName:    \"test\",\n\t\t\t\tVersion: \"1.0\",\n\t\t\t}),\n\t\t\twant: matcher.Config{\n\t\t\t\tJava: java.MatcherConfig{\n\t\t\t\t\tExternalSearchConfig: java.ExternalSearchConfig{\n\t\t\t\t\t\tSearchMavenUpstream: false,\n\t\t\t\t\t\tMavenBaseURL:        \"https://search.maven.org/solrsearch/select\",\n\t\t\t\t\t\tMavenRateLimit:      300000000, // 300ms in nanoseconds\n\t\t\t\t\t},\n\t\t\t\t\tUseCPEs: false,\n\t\t\t\t},\n\t\t\t\tRuby:       ruby.MatcherConfig{},\n\t\t\t\tPython:     python.MatcherConfig{},\n\t\t\t\tDotnet:     dotnet.MatcherConfig{},\n\t\t\t\tJavascript: javascript.MatcherConfig{},\n\t\t\t\tGolang: golang.MatcherConfig{\n\t\t\t\t\tUseCPEs:                                false,\n\t\t\t\t\tAlwaysUseCPEForStdlib:                  true,\n\t\t\t\t\tAllowMainModulePseudoVersionComparison: false,\n\t\t\t\t},\n\t\t\t\tHex:   hex.MatcherConfig{},\n\t\t\t\tStock: stock.MatcherConfig{UseCPEs: true},\n\t\t\t\tRpm: rpm.MatcherConfig{\n\t\t\t\t\tMissingEpochStrategy: \"auto\",\n\t\t\t\t},\n\t\t\t\tDpkg: dpkg.MatcherConfig{\n\t\t\t\t\tMissingEpochStrategy: \"zero\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"rpm missing-epoch-strategy set to zero\",\n\t\t\topts: func() *options.Grype {\n\t\t\t\topts := options.DefaultGrype(clio.Identification{Name: \"test\", Version: \"1.0\"})\n\t\t\t\topts.Match.Rpm.MissingEpochStrategy = \"zero\"\n\t\t\t\treturn opts\n\t\t\t}(),\n\t\t\twant: matcher.Config{\n\t\t\t\tJava: java.MatcherConfig{\n\t\t\t\t\tExternalSearchConfig: java.ExternalSearchConfig{\n\t\t\t\t\t\tSearchMavenUpstream: false,\n\t\t\t\t\t\tMavenBaseURL:        \"https://search.maven.org/solrsearch/select\",\n\t\t\t\t\t\tMavenRateLimit:      300000000,\n\t\t\t\t\t},\n\t\t\t\t\tUseCPEs: false,\n\t\t\t\t},\n\t\t\t\tRuby:       ruby.MatcherConfig{},\n\t\t\t\tPython:     python.MatcherConfig{},\n\t\t\t\tDotnet:     dotnet.MatcherConfig{},\n\t\t\t\tJavascript: javascript.MatcherConfig{},\n\t\t\t\tGolang: golang.MatcherConfig{\n\t\t\t\t\tUseCPEs:                                false,\n\t\t\t\t\tAlwaysUseCPEForStdlib:                  true,\n\t\t\t\t\tAllowMainModulePseudoVersionComparison: false,\n\t\t\t\t},\n\t\t\t\tHex:   hex.MatcherConfig{},\n\t\t\t\tStock: stock.MatcherConfig{UseCPEs: true},\n\t\t\t\tRpm: rpm.MatcherConfig{\n\t\t\t\t\tMissingEpochStrategy: \"zero\",\n\t\t\t\t},\n\t\t\t\tDpkg: dpkg.MatcherConfig{\n\t\t\t\t\tMissingEpochStrategy: \"zero\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"dpkg missing-epoch-strategy set to auto\",\n\t\t\topts: func() *options.Grype {\n\t\t\t\topts := options.DefaultGrype(clio.Identification{Name: \"test\", Version: \"1.0\"})\n\t\t\t\topts.Match.Dpkg.MissingEpochStrategy = \"auto\"\n\t\t\t\treturn opts\n\t\t\t}(),\n\t\t\twant: matcher.Config{\n\t\t\t\tJava: java.MatcherConfig{\n\t\t\t\t\tExternalSearchConfig: java.ExternalSearchConfig{\n\t\t\t\t\t\tSearchMavenUpstream: false,\n\t\t\t\t\t\tMavenBaseURL:        \"https://search.maven.org/solrsearch/select\",\n\t\t\t\t\t\tMavenRateLimit:      300000000,\n\t\t\t\t\t},\n\t\t\t\t\tUseCPEs: false,\n\t\t\t\t},\n\t\t\t\tRuby:       ruby.MatcherConfig{},\n\t\t\t\tPython:     python.MatcherConfig{},\n\t\t\t\tDotnet:     dotnet.MatcherConfig{},\n\t\t\t\tJavascript: javascript.MatcherConfig{},\n\t\t\t\tGolang: golang.MatcherConfig{\n\t\t\t\t\tUseCPEs:                                false,\n\t\t\t\t\tAlwaysUseCPEForStdlib:                  true,\n\t\t\t\t\tAllowMainModulePseudoVersionComparison: false,\n\t\t\t\t},\n\t\t\t\tHex:   hex.MatcherConfig{},\n\t\t\t\tStock: stock.MatcherConfig{UseCPEs: true},\n\t\t\t\tRpm: rpm.MatcherConfig{\n\t\t\t\t\tMissingEpochStrategy: \"auto\",\n\t\t\t\t},\n\t\t\t\tDpkg: dpkg.MatcherConfig{\n\t\t\t\t\tMissingEpochStrategy: \"auto\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif d := cmp.Diff(tt.want, getMatcherConfig(tt.opts)); d != \"\" {\n\t\t\t\tt.Errorf(\"getMatcherConfig() mismatch (-want +got):\\n%s\", d)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_applyVexRules(t *testing.T) {\n\ttests := []struct {\n\t\tname                   string\n\t\tinitialIgnoreRules     []match.IgnoreRule\n\t\tvexDocuments           []string\n\t\tvexAdd                 []string\n\t\texpectedIgnoreRules    []match.IgnoreRule\n\t\texpectError            bool\n\t\texpectedErrorSubstring string\n\t}{\n\t\t{\n\t\t\tname:                \"no VEX documents provided - no rules added\",\n\t\t\tinitialIgnoreRules:  []match.IgnoreRule{},\n\t\t\tvexDocuments:        []string{},\n\t\t\tvexAdd:              []string{},\n\t\t\texpectedIgnoreRules: []match.IgnoreRule{},\n\t\t\texpectError:         false,\n\t\t},\n\t\t{\n\t\t\tname:               \"VEX documents provided with empty ignore rules - automatic rules added\",\n\t\t\tinitialIgnoreRules: []match.IgnoreRule{},\n\t\t\tvexDocuments:       []string{\"path/to/vex.json\"},\n\t\t\tvexAdd:             []string{},\n\t\t\texpectedIgnoreRules: []match.IgnoreRule{\n\t\t\t\t{VexStatus: string(vexStatus.NotAffected)},\n\t\t\t\t{VexStatus: string(vexStatus.Fixed)},\n\t\t\t},\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname: \"VEX documents provided with existing ignore rules - automatic rules still added\",\n\t\t\tinitialIgnoreRules: []match.IgnoreRule{\n\t\t\t\t{Vulnerability: \"CVE-2023-1234\"},\n\t\t\t},\n\t\t\tvexDocuments: []string{\"path/to/vex.json\"},\n\t\t\tvexAdd:       []string{},\n\t\t\texpectedIgnoreRules: []match.IgnoreRule{\n\t\t\t\t{Vulnerability: \"CVE-2023-1234\"},\n\t\t\t\t{VexStatus: string(vexStatus.NotAffected)},\n\t\t\t\t{VexStatus: string(vexStatus.Fixed)},\n\t\t\t},\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname:               \"vex-add with valid statuses\",\n\t\t\tinitialIgnoreRules: []match.IgnoreRule{},\n\t\t\tvexDocuments:       []string{\"path/to/vex.json\"},\n\t\t\tvexAdd:             []string{\"affected\", \"under_investigation\"},\n\t\t\texpectedIgnoreRules: []match.IgnoreRule{\n\t\t\t\t{VexStatus: string(vexStatus.NotAffected)},\n\t\t\t\t{VexStatus: string(vexStatus.Fixed)},\n\t\t\t\t{VexStatus: string(vexStatus.Affected)},\n\t\t\t\t{VexStatus: string(vexStatus.UnderInvestigation)},\n\t\t\t},\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname:                   \"vex-add with invalid status\",\n\t\t\tinitialIgnoreRules:     []match.IgnoreRule{},\n\t\t\tvexDocuments:           []string{\"path/to/vex.json\"},\n\t\t\tvexAdd:                 []string{\"invalid_status\"},\n\t\t\texpectedIgnoreRules:    nil,\n\t\t\texpectError:            true,\n\t\t\texpectedErrorSubstring: \"invalid VEX status in vex-add setting: invalid_status\",\n\t\t},\n\t\t{\n\t\t\tname:                   \"vex-add attempting to use fixed status\",\n\t\t\tinitialIgnoreRules:     []match.IgnoreRule{},\n\t\t\tvexDocuments:           []string{\"path/to/vex.json\"},\n\t\t\tvexAdd:                 []string{\"fixed\"},\n\t\t\texpectedIgnoreRules:    nil,\n\t\t\texpectError:            true,\n\t\t\texpectedErrorSubstring: \"invalid VEX status in vex-add setting: fixed\",\n\t\t},\n\t\t{\n\t\t\tname: \"multiple VEX documents with existing rules\",\n\t\t\tinitialIgnoreRules: []match.IgnoreRule{\n\t\t\t\t{Vulnerability: \"CVE-2023-1234\"},\n\t\t\t\t{FixState: \"unknown\"},\n\t\t\t},\n\t\t\tvexDocuments: []string{\"vex1.json\", \"vex2.json\"},\n\t\t\tvexAdd:       []string{\"affected\"},\n\t\t\texpectedIgnoreRules: []match.IgnoreRule{\n\t\t\t\t{Vulnerability: \"CVE-2023-1234\"},\n\t\t\t\t{FixState: \"unknown\"},\n\t\t\t\t{VexStatus: string(vexStatus.NotAffected)},\n\t\t\t\t{VexStatus: string(vexStatus.Fixed)},\n\t\t\t\t{VexStatus: string(vexStatus.Affected)},\n\t\t\t},\n\t\t\texpectError: false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\topts := &options.Grype{\n\t\t\t\tIgnore:       append([]match.IgnoreRule{}, tt.initialIgnoreRules...),\n\t\t\t\tVexDocuments: tt.vexDocuments,\n\t\t\t\tVexAdd:       tt.vexAdd,\n\t\t\t}\n\n\t\t\terr := applyVexRules(opts)\n\n\t\t\tif tt.expectError {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\tassert.Contains(t, err.Error(), tt.expectedErrorSubstring)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, tt.expectedIgnoreRules, opts.Ignore)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "cmd/grype/cli/commands/testdata/provider-metadata.json",
    "content": "{\n  \"providers\": [\n    {\n      \"name\": \"provider1\",\n      \"lastSuccessfulRun\": \"2024-10-16T01:33:16.844201Z\"\n    },\n    {\n      \"name\": \"provider2\",\n      \"lastSuccessfulRun\": \"2024-10-16T01:32:43.516596Z\"\n    }\n  ]\n}"
  },
  {
    "path": "cmd/grype/cli/commands/update.go",
    "content": "package commands\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"strings\"\n\n\t\"github.com/anchore/clio\"\n\thashiVersion \"github.com/anchore/go-version\"\n\t\"github.com/anchore/grype/cmd/grype/internal\"\n)\n\nvar latestAppVersionURL = struct {\n\thost string\n\tpath string\n}{\n\thost: \"https://toolbox-data.anchore.io\",\n\tpath: \"/grype/releases/latest/VERSION\",\n}\n\nfunc isProductionBuild(version string) bool {\n\tif strings.Contains(version, \"SNAPSHOT\") || strings.Contains(version, internal.NotProvided) {\n\t\treturn false\n\t}\n\treturn true\n}\n\nfunc isUpdateAvailable(id clio.Identification) (bool, string, error) {\n\tif !isProductionBuild(id.Version) {\n\t\t// don't allow for non-production builds to check for a version.\n\t\treturn false, \"\", nil\n\t}\n\tcurrentVersion, err := hashiVersion.NewVersion(id.Version)\n\tif err != nil {\n\t\treturn false, \"\", fmt.Errorf(\"failed to parse current application version: %w\", err)\n\t}\n\n\tlatestVersion, err := fetchLatestApplicationVersion(id)\n\tif err != nil {\n\t\treturn false, \"\", err\n\t}\n\n\tif latestVersion.GreaterThan(currentVersion) {\n\t\treturn true, latestVersion.String(), nil\n\t}\n\n\treturn false, \"\", nil\n}\n\nfunc fetchLatestApplicationVersion(id clio.Identification) (*hashiVersion.Version, error) {\n\treq, err := http.NewRequest(http.MethodGet, latestAppVersionURL.host+latestAppVersionURL.path, nil)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create request for latest version: %w\", err)\n\t}\n\n\treq.Header.Add(\"User-Agent\", fmt.Sprintf(\"%v %v\", id.Name, id.Version))\n\n\tclient := http.Client{}\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to fetch latest version: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn nil, fmt.Errorf(\"HTTP %d on fetching latest version: %s\", resp.StatusCode, resp.Status)\n\t}\n\n\tversionBytes, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to read latest version: %w\", err)\n\t}\n\n\tversionStr := strings.TrimSuffix(string(versionBytes), \"\\n\")\n\tif len(versionStr) > 50 {\n\t\treturn nil, fmt.Errorf(\"version too long: %q\", versionStr[:50])\n\t}\n\n\treturn hashiVersion.NewVersion(versionStr)\n}\n"
  },
  {
    "path": "cmd/grype/cli/commands/update_test.go",
    "content": "package commands\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/anchore/clio\"\n\thashiVersion \"github.com/anchore/go-version\"\n\t\"github.com/anchore/grype/cmd/grype/internal\"\n)\n\nfunc TestIsUpdateAvailable(t *testing.T) {\n\ttests := []struct {\n\t\tname          string\n\t\tbuildVersion  string\n\t\tlatestVersion string\n\t\tcode          int\n\t\tisAvailable   bool\n\t\tnewVersion    string\n\t\terr           bool\n\t}{\n\t\t{\n\t\t\tname:          \"equal\",\n\t\t\tbuildVersion:  \"1.0.0\",\n\t\t\tlatestVersion: \"1.0.0\",\n\t\t\tcode:          200,\n\t\t\tisAvailable:   false,\n\t\t\tnewVersion:    \"\",\n\t\t\terr:           false,\n\t\t},\n\t\t{\n\t\t\tname:          \"hasUpdate\",\n\t\t\tbuildVersion:  \"1.0.0\",\n\t\t\tlatestVersion: \"1.2.0\",\n\t\t\tcode:          200,\n\t\t\tisAvailable:   true,\n\t\t\tnewVersion:    \"1.2.0\",\n\t\t\terr:           false,\n\t\t},\n\t\t{\n\t\t\tname:          \"aheadOfLatest\",\n\t\t\tbuildVersion:  \"1.2.0\",\n\t\t\tlatestVersion: \"1.0.0\",\n\t\t\tcode:          200,\n\t\t\tisAvailable:   false,\n\t\t\tnewVersion:    \"\",\n\t\t\terr:           false,\n\t\t},\n\t\t{\n\t\t\tname:          \"EmptyUpdate\",\n\t\t\tbuildVersion:  \"1.0.0\",\n\t\t\tlatestVersion: \"\",\n\t\t\tcode:          200,\n\t\t\tisAvailable:   false,\n\t\t\tnewVersion:    \"\",\n\t\t\terr:           true,\n\t\t},\n\t\t{\n\t\t\tname:          \"GarbageUpdate\",\n\t\t\tbuildVersion:  \"1.0.0\",\n\t\t\tlatestVersion: \"hdfjksdhfhkj\",\n\t\t\tcode:          200,\n\t\t\tisAvailable:   false,\n\t\t\tnewVersion:    \"\",\n\t\t\terr:           true,\n\t\t},\n\t\t{\n\t\t\tname:          \"BadUpdate\",\n\t\t\tbuildVersion:  \"1.0.0\",\n\t\t\tlatestVersion: \"1.0.\",\n\t\t\tcode:          500,\n\t\t\tisAvailable:   false,\n\t\t\tnewVersion:    \"\",\n\t\t\terr:           true,\n\t\t},\n\t\t{\n\t\t\tname:          \"NoBuildVersion\",\n\t\t\tbuildVersion:  internal.NotProvided,\n\t\t\tlatestVersion: \"1.0.0\",\n\t\t\tcode:          200,\n\t\t\tisAvailable:   false,\n\t\t\tnewVersion:    \"\",\n\t\t\terr:           false,\n\t\t},\n\t\t{\n\t\t\tname:          \"BadUpdateValidVersion\",\n\t\t\tbuildVersion:  \"1.0.0\",\n\t\t\tlatestVersion: \"2.0.0\",\n\t\t\tcode:          404,\n\t\t\tisAvailable:   false,\n\t\t\tnewVersion:    \"\",\n\t\t\terr:           true,\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\t// setup mocks\n\t\t\t// local...\n\t\t\tversion := test.buildVersion\n\t\t\t// remote...\n\t\t\thandler := http.NewServeMux()\n\t\t\thandler.HandleFunc(latestAppVersionURL.path, func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\tw.WriteHeader(test.code)\n\t\t\t\t_, _ = w.Write([]byte(test.latestVersion))\n\t\t\t})\n\t\t\tmockSrv := httptest.NewServer(handler)\n\t\t\tlatestAppVersionURL.host = mockSrv.URL\n\t\t\tdefer mockSrv.Close()\n\n\t\t\tisAvailable, newVersion, err := isUpdateAvailable(clio.Identification{Version: version})\n\t\t\tif err != nil && !test.err {\n\t\t\t\tt.Fatalf(\"got error but expected none: %+v\", err)\n\t\t\t} else if err == nil && test.err {\n\t\t\t\tt.Fatalf(\"expected error but got none\")\n\t\t\t}\n\n\t\t\tif newVersion != test.newVersion {\n\t\t\t\tt.Errorf(\"unexpected NEW version: %+v\", newVersion)\n\t\t\t}\n\n\t\t\tif isAvailable != test.isAvailable {\n\t\t\t\tt.Errorf(\"unexpected result: %+v\", isAvailable)\n\t\t\t}\n\t\t})\n\t}\n\n}\n\nfunc TestFetchLatestApplicationVersion(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tresponse string\n\t\tcode     int\n\t\terr      bool\n\t\texpected *hashiVersion.Version\n\t}{\n\t\t{\n\t\t\tname:     \"gocase\",\n\t\t\tresponse: \"1.0.0\",\n\t\t\tcode:     200,\n\t\t\texpected: hashiVersion.Must(hashiVersion.NewVersion(\"1.0.0\")),\n\t\t},\n\t\t{\n\t\t\tname:     \"garbage\",\n\t\t\tresponse: \"garbage\",\n\t\t\tcode:     200,\n\t\t\texpected: nil,\n\t\t\terr:      true,\n\t\t},\n\t\t{\n\t\t\tname:     \"http 500\",\n\t\t\tresponse: \"1.0.0\",\n\t\t\tcode:     500,\n\t\t\texpected: nil,\n\t\t\terr:      true,\n\t\t},\n\t\t{\n\t\t\tname:     \"http 404\",\n\t\t\tresponse: \"1.0.0\",\n\t\t\tcode:     404,\n\t\t\texpected: nil,\n\t\t\terr:      true,\n\t\t},\n\t\t{\n\t\t\tname:     \"empty\",\n\t\t\tresponse: \"\",\n\t\t\tcode:     200,\n\t\t\texpected: nil,\n\t\t\terr:      true,\n\t\t},\n\t\t{\n\t\t\tname:     \"too long\",\n\t\t\tresponse: \"this is really long this is really long this is really long this is really long this is really long this is really long this is really long this is really long \",\n\t\t\tcode:     200,\n\t\t\texpected: nil,\n\t\t\terr:      true,\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\t// setup mock\n\t\t\thandler := http.NewServeMux()\n\t\t\thandler.HandleFunc(latestAppVersionURL.path, func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\tw.WriteHeader(test.code)\n\t\t\t\t_, _ = w.Write([]byte(test.response))\n\t\t\t})\n\t\t\tmockSrv := httptest.NewServer(handler)\n\t\t\tlatestAppVersionURL.host = mockSrv.URL\n\t\t\tdefer mockSrv.Close()\n\n\t\t\tactual, err := fetchLatestApplicationVersion(clio.Identification{})\n\t\t\tif err != nil && !test.err {\n\t\t\t\tt.Fatalf(\"got error but expected none: %+v\", err)\n\t\t\t} else if err == nil && test.err {\n\t\t\t\tt.Fatalf(\"expected error but got none\")\n\t\t\t}\n\n\t\t\tif err != nil {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif actual.String() != test.expected.String() {\n\t\t\t\tt.Errorf(\"unexpected version: %+v\", actual.String())\n\t\t\t}\n\t\t})\n\t}\n\n}\n\nfunc Test_UserAgent(t *testing.T) {\n\tgot := \"\"\n\n\t// setup mock\n\thandler := http.NewServeMux()\n\thandler.HandleFunc(latestAppVersionURL.path, func(w http.ResponseWriter, r *http.Request) {\n\t\tgot = r.Header.Get(\"User-Agent\")\n\t\tw.WriteHeader(http.StatusOK)\n\t\t_, _ = w.Write([]byte(\"1.0.0\"))\n\t})\n\tmockSrv := httptest.NewServer(handler)\n\tlatestAppVersionURL.host = mockSrv.URL\n\tdefer mockSrv.Close()\n\n\tfetchLatestApplicationVersion(clio.Identification{\n\t\tName:    \"the-app\",\n\t\tVersion: \"v3.2.1\",\n\t})\n\n\tif got != \"the-app v3.2.1\" {\n\t\tt.Errorf(\"expected User-Agent header to match, got: %v\", got)\n\t}\n}\n"
  },
  {
    "path": "cmd/grype/cli/commands/util.go",
    "content": "package commands\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/hashicorp/go-multierror\"\n\t\"github.com/olekukonko/tablewriter\"\n\t\"github.com/olekukonko/tablewriter/renderer\"\n\t\"github.com/olekukonko/tablewriter/tw\"\n\t\"github.com/spf13/cobra\"\n\t\"golang.org/x/exp/maps\"\n\n\t\"github.com/anchore/clio\"\n\t\"github.com/anchore/grype/cmd/grype/internal/ui\"\n)\n\nfunc disableUI(app clio.Application) func(*cobra.Command, []string) error {\n\treturn func(_ *cobra.Command, _ []string) error {\n\t\ttype Stater interface {\n\t\t\tState() *clio.State\n\t\t}\n\n\t\tstate := app.(Stater).State()\n\t\tstate.UI = clio.NewUICollection(ui.None(state.Config.Log.Quiet))\n\n\t\treturn nil\n\t}\n}\n\nfunc stderrPrintLnf(message string, args ...interface{}) error {\n\tif !strings.HasSuffix(message, \"\\n\") {\n\t\tmessage += \"\\n\"\n\t}\n\t_, err := fmt.Fprintf(os.Stderr, message, args...)\n\treturn err\n}\n\n// parallel takes a set of functions and runs them in parallel, capturing all errors returned and\n// returning the single error returned by one of the parallel funcs, or a multierror.Error with all\n// the errors if more than one\nfunc parallel(funcs ...func() error) error {\n\terrs := parallelMapped(funcs...)\n\tif len(errs) > 0 {\n\t\tvalues := maps.Values(errs)\n\t\tif len(values) == 1 {\n\t\t\treturn values[0]\n\t\t}\n\t\treturn multierror.Append(nil, values...)\n\t}\n\treturn nil\n}\n\n// parallelMapped takes a set of functions and runs them in parallel, capturing all errors returned in\n// a map indicating which func, by index returned which error\nfunc parallelMapped(funcs ...func() error) map[int]error {\n\terrs := map[int]error{}\n\terrorLock := &sync.Mutex{}\n\twg := &sync.WaitGroup{}\n\twg.Add(len(funcs))\n\tfor i, fn := range funcs {\n\t\tgo func(i int, fn func() error) {\n\t\t\tdefer wg.Done()\n\t\t\terr := fn()\n\t\t\tif err != nil {\n\t\t\t\terrorLock.Lock()\n\t\t\t\tdefer errorLock.Unlock()\n\t\t\t\terrs[i] = err\n\t\t\t}\n\t\t}(i, fn)\n\t}\n\twg.Wait()\n\treturn errs\n}\n\nfunc appendErrors(errs error, err ...error) error {\n\tif errs == nil {\n\t\tswitch len(err) {\n\t\tcase 0:\n\t\t\treturn nil\n\t\tcase 1:\n\t\t\treturn err[0]\n\t\t}\n\t}\n\treturn multierror.Append(errs, err...)\n}\n\nfunc newTable(output io.Writer, columns []string) *tablewriter.Table {\n\treturn tablewriter.NewTable(output,\n\t\ttablewriter.WithHeader(columns),\n\t\ttablewriter.WithHeaderAlignment(tw.AlignLeft),\n\t\ttablewriter.WithHeaderAutoWrap(tw.WrapNone),\n\t\ttablewriter.WithRowAutoWrap(tw.WrapNone),\n\t\ttablewriter.WithAutoHide(tw.On),\n\t\ttablewriter.WithRenderer(renderer.NewBlueprint()),\n\t\ttablewriter.WithBehavior(\n\t\t\ttw.Behavior{\n\t\t\t\tTrimSpace: tw.On,\n\t\t\t\tAutoHide:  tw.On,\n\t\t\t},\n\t\t),\n\t\ttablewriter.WithPadding(\n\t\t\ttw.Padding{\n\t\t\t\tRight: \"  \",\n\t\t\t},\n\t\t),\n\t\ttablewriter.WithRendition(\n\t\t\ttw.Rendition{\n\t\t\t\tSymbols: tw.NewSymbols(tw.StyleNone),\n\t\t\t\tSettings: tw.Settings{\n\t\t\t\t\tLines: tw.Lines{\n\t\t\t\t\t\tShowTop:        tw.Off,\n\t\t\t\t\t\tShowBottom:     tw.Off,\n\t\t\t\t\t\tShowHeaderLine: tw.Off,\n\t\t\t\t\t\tShowFooterLine: tw.Off,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t),\n\t)\n}\n"
  },
  {
    "path": "cmd/grype/cli/commands/util_test.go",
    "content": "package commands\n\nimport (\n\t\"fmt\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"testing\"\n\n\t\"github.com/hashicorp/go-multierror\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst lotsaParallel = 100\n\nfunc Test_lotsaLotsaParallel(t *testing.T) {\n\tfuncs := []func() error{}\n\tfor i := 0; i < lotsaParallel; i++ {\n\t\tfuncs = append(funcs, func() error {\n\t\t\tTest_lotsaParallel(t)\n\t\t\treturn nil\n\t\t})\n\t}\n\terr := parallel(funcs...)\n\trequire.NoError(t, err)\n}\n\nfunc Test_lotsaParallel(t *testing.T) {\n\tfor i := 0; i < lotsaParallel; i++ {\n\t\tTest_parallel(t)\n\t}\n}\n\n// Test_parallel tests the parallel function by executing a set of functions that can only execute in a specific\n// order if they are actually running in parallel.\nfunc Test_parallel(t *testing.T) {\n\tcount := atomic.Int32{}\n\tcount.Store(0)\n\n\twg1 := sync.WaitGroup{}\n\twg1.Add(1)\n\n\twg2 := sync.WaitGroup{}\n\twg2.Add(1)\n\n\twg3 := sync.WaitGroup{}\n\twg3.Add(1)\n\n\terr1 := fmt.Errorf(\"error-1\")\n\terr2 := fmt.Errorf(\"error-2\")\n\terr3 := fmt.Errorf(\"error-3\")\n\n\torder := \"\"\n\n\tgot := parallel(\n\t\tfunc() error {\n\t\t\twg1.Wait()\n\t\t\tcount.Add(1)\n\t\t\torder = order + \"_0\"\n\t\t\treturn nil\n\t\t},\n\t\tfunc() error {\n\t\t\twg3.Wait()\n\t\t\tdefer wg2.Done()\n\t\t\tcount.Add(10)\n\t\t\torder = order + \"_1\"\n\t\t\treturn err1\n\t\t},\n\t\tfunc() error {\n\t\t\twg2.Wait()\n\t\t\tdefer wg1.Done()\n\t\t\tcount.Add(100)\n\t\t\torder = order + \"_2\"\n\t\t\treturn err2\n\t\t},\n\t\tfunc() error {\n\t\t\tdefer wg3.Done()\n\t\t\tcount.Add(1000)\n\t\t\torder = order + \"_3\"\n\t\t\treturn err3\n\t\t},\n\t)\n\trequire.Equal(t, int32(1111), count.Load())\n\trequire.Equal(t, \"_3_1_2_0\", order)\n\n\terrs := got.(*multierror.Error).Errors\n\n\t// cannot check equality to a slice with err1,2,3 because the functions above are running in parallel, for example:\n\t// after func()#4 returns and the `wg3.Done()` has executed, the thread could immediately pause\n\t// and the remaining functions execute first and err3 becomes the last in the list instead of the first\n\trequire.Contains(t, errs, err1)\n\trequire.Contains(t, errs, err2)\n\trequire.Contains(t, errs, err3)\n}\n\nfunc Test_parallelMapped(t *testing.T) {\n\terr0 := fmt.Errorf(\"error-0\")\n\terr1 := fmt.Errorf(\"error-1\")\n\terr2 := fmt.Errorf(\"error-2\")\n\n\ttests := []struct {\n\t\tname     string\n\t\tfuncs    []func() error\n\t\texpected map[int]error\n\t}{\n\t\t{\n\t\t\tname: \"basic\",\n\t\t\tfuncs: []func() error{\n\t\t\t\tfunc() error {\n\t\t\t\t\treturn nil\n\t\t\t\t},\n\t\t\t\tfunc() error {\n\t\t\t\t\treturn err1\n\t\t\t\t},\n\t\t\t\tfunc() error {\n\t\t\t\t\treturn nil\n\t\t\t\t},\n\t\t\t\tfunc() error {\n\t\t\t\t\treturn err2\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: map[int]error{\n\t\t\t\t1: err1,\n\t\t\t\t3: err2,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"no errors\",\n\t\t\tfuncs: []func() error{\n\t\t\t\tfunc() error {\n\t\t\t\t\treturn nil\n\t\t\t\t},\n\t\t\t\tfunc() error {\n\t\t\t\t\treturn nil\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: map[int]error{},\n\t\t},\n\t\t{\n\t\t\tname: \"all errors\",\n\t\t\tfuncs: []func() error{\n\t\t\t\tfunc() error {\n\t\t\t\t\treturn err0\n\t\t\t\t},\n\t\t\t\tfunc() error {\n\t\t\t\t\treturn err1\n\t\t\t\t},\n\t\t\t\tfunc() error {\n\t\t\t\t\treturn err2\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: map[int]error{\n\t\t\t\t0: err0,\n\t\t\t\t1: err1,\n\t\t\t\t2: err2,\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tgot := parallelMapped(test.funcs...)\n\t\t\trequire.Equal(t, test.expected, got)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "cmd/grype/cli/options/alerts.go",
    "content": "package options\n\nimport \"github.com/anchore/clio\"\n\n// Alerts configures how alerts are generated and displayed.\ntype Alerts struct {\n\t// EnableEOLDistroWarnings enables warnings about packages from end-of-life distros\n\tEnableEOLDistroWarnings bool `yaml:\"enable-eol-distro-warnings\" json:\"enable-eol-distro-warnings\" mapstructure:\"enable-eol-distro-warnings\"`\n}\n\nvar _ clio.FieldDescriber = (*Alerts)(nil)\n\nfunc defaultAlerts() Alerts {\n\treturn Alerts{\n\t\tEnableEOLDistroWarnings: true,\n\t}\n}\n\nfunc (a *Alerts) DescribeFields(descriptions clio.FieldDescriptionSet) {\n\tdescriptions.Add(&a.EnableEOLDistroWarnings, `enable/disable warnings about packages from end-of-life (EOL) distros. When enabled, grype will track and report packages that come from distros that have reached their end-of-life date.`)\n}\n"
  },
  {
    "path": "cmd/grype/cli/options/alerts_test.go",
    "content": "package options\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestDefaultAlerts(t *testing.T) {\n\talerts := defaultAlerts()\n\n\t// EOL distro warnings should be enabled by default\n\tassert.True(t, alerts.EnableEOLDistroWarnings, \"EnableEOLDistroWarnings should be true by default\")\n}\n"
  },
  {
    "path": "cmd/grype/cli/options/database.go",
    "content": "package options\n\nimport (\n\t\"time\"\n\n\t\"github.com/anchore/clio\"\n\t\"github.com/anchore/go-homedir\"\n\t\"github.com/anchore/grype/grype/db/v6/distribution\"\n\t\"github.com/anchore/grype/grype/db/v6/installation\"\n)\n\ntype Database struct {\n\tID                      clio.Identification `yaml:\"-\" json:\"-\" mapstructure:\"-\"`\n\tDir                     string              `yaml:\"cache-dir\" json:\"cache-dir\" mapstructure:\"cache-dir\"`\n\tUpdateURL               string              `yaml:\"update-url\" json:\"update-url\" mapstructure:\"update-url\"`\n\tCACert                  string              `yaml:\"ca-cert\" json:\"ca-cert\" mapstructure:\"ca-cert\"`\n\tAutoUpdate              bool                `yaml:\"auto-update\" json:\"auto-update\" mapstructure:\"auto-update\"`\n\tValidateByHashOnStart   bool                `yaml:\"validate-by-hash-on-start\" json:\"validate-by-hash-on-start\" mapstructure:\"validate-by-hash-on-start\"`\n\tValidateAge             bool                `yaml:\"validate-age\" json:\"validate-age\" mapstructure:\"validate-age\"`\n\tMaxAllowedBuiltAge      time.Duration       `yaml:\"max-allowed-built-age\" json:\"max-allowed-built-age\" mapstructure:\"max-allowed-built-age\"`\n\tRequireUpdateCheck      bool                `yaml:\"require-update-check\" json:\"require-update-check\" mapstructure:\"require-update-check\"`\n\tUpdateAvailableTimeout  time.Duration       `yaml:\"update-available-timeout\" json:\"update-available-timeout\" mapstructure:\"update-available-timeout\"`\n\tUpdateDownloadTimeout   time.Duration       `yaml:\"update-download-timeout\" json:\"update-download-timeout\" mapstructure:\"update-download-timeout\"`\n\tMaxUpdateCheckFrequency time.Duration       `yaml:\"max-update-check-frequency\" json:\"max-update-check-frequency\" mapstructure:\"max-update-check-frequency\"`\n}\n\nvar _ interface {\n\tclio.FieldDescriber\n\tclio.PostLoader\n} = (*Database)(nil)\n\nfunc DefaultDatabase(id clio.Identification) Database {\n\tdistConfig := distribution.DefaultConfig()\n\tinstallConfig := installation.DefaultConfig(id)\n\treturn Database{\n\t\tID:          id,\n\t\tDir:         installConfig.DBRootDir,\n\t\tUpdateURL:   distConfig.LatestURL,\n\t\tAutoUpdate:  true,\n\t\tValidateAge: installConfig.ValidateAge,\n\t\t// After this period (5 days) the db data is considered stale\n\t\tMaxAllowedBuiltAge:      installConfig.MaxAllowedBuiltAge,\n\t\tRequireUpdateCheck:      distConfig.RequireUpdateCheck,\n\t\tValidateByHashOnStart:   installConfig.ValidateChecksum,\n\t\tUpdateAvailableTimeout:  distConfig.CheckTimeout,\n\t\tUpdateDownloadTimeout:   distConfig.UpdateTimeout,\n\t\tMaxUpdateCheckFrequency: installConfig.UpdateCheckMaxFrequency,\n\t\tCACert:                  distConfig.CACert,\n\t}\n}\n\nfunc (cfg *Database) DescribeFields(descriptions clio.FieldDescriptionSet) {\n\tdescriptions.Add(&cfg.Dir, `location to write the vulnerability database cache`)\n\tdescriptions.Add(&cfg.UpdateURL, `URL of the vulnerability database`)\n\tdescriptions.Add(&cfg.CACert, `certificate to trust download the database and listing file`)\n\tdescriptions.Add(&cfg.AutoUpdate, `check for database updates on execution`)\n\tdescriptions.Add(&cfg.ValidateAge, `ensure db build is no older than the max-allowed-built-age`)\n\tdescriptions.Add(&cfg.ValidateByHashOnStart, `validate the database matches the known hash each execution`)\n\tdescriptions.Add(&cfg.MaxAllowedBuiltAge, `Max allowed age for vulnerability database,\nage being the time since it was built\nDefault max age is 120h (or five days)`)\n\tdescriptions.Add(&cfg.RequireUpdateCheck, `fail the scan if unable to check for database updates`)\n\tdescriptions.Add(&cfg.UpdateAvailableTimeout, `Timeout for downloading GRYPE_DB_UPDATE_URL to see if the database needs to be downloaded\nThis file is ~156KiB as of 2024-04-17 so the download should be quick; adjust as needed`)\n\tdescriptions.Add(&cfg.UpdateDownloadTimeout, `Timeout for downloading actual vulnerability DB\nThe DB is ~156MB as of 2024-04-17 so slower connections may exceed the default timeout; adjust as needed`)\n\tdescriptions.Add(&cfg.MaxUpdateCheckFrequency, `Maximum frequency to check for vulnerability database updates`)\n}\n\nfunc (cfg *Database) PostLoad() error {\n\tvar err error\n\tcfg.Dir, err = homedir.Expand(cfg.Dir)\n\treturn err\n}\n"
  },
  {
    "path": "cmd/grype/cli/options/database_command.go",
    "content": "package options\n\nimport (\n\t\"github.com/anchore/clio\"\n\t\"github.com/anchore/grype/grype/db/v6/distribution\"\n\t\"github.com/anchore/grype/grype/db/v6/installation\"\n)\n\ntype DatabaseCommand struct {\n\tDB           Database     `yaml:\"db\" json:\"db\" mapstructure:\"db\"`\n\tExperimental Experimental `yaml:\"exp\" json:\"exp\" mapstructure:\"exp\"`\n\tDeveloper    developer    `yaml:\"dev\" json:\"dev\" mapstructure:\"dev\"`\n}\n\nfunc DefaultDatabaseCommand(id clio.Identification) *DatabaseCommand {\n\tdbDefaults := DefaultDatabase(id)\n\t// by default, require update check success for db operations which check for updates\n\tdbDefaults.RequireUpdateCheck = true\n\t// we want to validate by hash during Status checks\n\tdbDefaults.ValidateByHashOnStart = true\n\treturn &DatabaseCommand{\n\t\tDB: dbDefaults,\n\t}\n}\n\nfunc (cfg DatabaseCommand) ToCuratorConfig() installation.Config {\n\treturn installation.Config{\n\t\tDBRootDir:               cfg.DB.Dir,\n\t\tValidateAge:             cfg.DB.ValidateAge,\n\t\tValidateChecksum:        cfg.DB.ValidateByHashOnStart,\n\t\tMaxAllowedBuiltAge:      cfg.DB.MaxAllowedBuiltAge,\n\t\tUpdateCheckMaxFrequency: cfg.DB.MaxUpdateCheckFrequency,\n\t\tDebug:                   cfg.Developer.DB.Debug,\n\t}\n}\n\nfunc (cfg DatabaseCommand) ToClientConfig() distribution.Config {\n\treturn distribution.Config{\n\t\tID:                 cfg.DB.ID,\n\t\tLatestURL:          cfg.DB.UpdateURL,\n\t\tCACert:             cfg.DB.CACert,\n\t\tRequireUpdateCheck: cfg.DB.RequireUpdateCheck,\n\t\tCheckTimeout:       cfg.DB.UpdateAvailableTimeout,\n\t\tUpdateTimeout:      cfg.DB.UpdateDownloadTimeout,\n\t}\n}\n"
  },
  {
    "path": "cmd/grype/cli/options/database_search_bounds.go",
    "content": "package options\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/anchore/clio\"\n)\n\ntype DBSearchBounds struct {\n\tRecordLimit int `yaml:\"limit\" json:\"limit\" mapstructure:\"limit\"`\n}\n\nfunc DefaultDBSearchBounds() DBSearchBounds {\n\treturn DBSearchBounds{\n\t\tRecordLimit: 5000,\n\t}\n}\n\nfunc (o *DBSearchBounds) AddFlags(flags clio.FlagSet) {\n\tflags.IntVarP(&o.RecordLimit, \"limit\", \"\", \"limit the number of results returned, use 0 for no limit\")\n}\n\nfunc (o *DBSearchBounds) PostLoad() error {\n\tif o.RecordLimit < 0 {\n\t\treturn fmt.Errorf(\"limit must be a positive integer\")\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "cmd/grype/cli/options/database_search_format.go",
    "content": "package options\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/scylladb/go-set/strset\"\n\n\t\"github.com/anchore/clio\"\n)\n\ntype DBSearchFormat struct {\n\tOutput    string   `yaml:\"output\" json:\"output\" mapstructure:\"output\"`\n\tAllowable []string `yaml:\"-\" json:\"-\" mapstructure:\"-\"`\n}\n\nfunc DefaultDBSearchFormat() DBSearchFormat {\n\treturn DBSearchFormat{\n\t\tOutput:    \"table\",\n\t\tAllowable: []string{\"table\", \"json\"},\n\t}\n}\n\nfunc (c *DBSearchFormat) AddFlags(flags clio.FlagSet) {\n\tavailable := strings.Join(c.Allowable, \", \")\n\tflags.StringVarP(&c.Output, \"output\", \"o\", fmt.Sprintf(\"format to display results (available=[%s])\", available))\n}\n\nfunc (c *DBSearchFormat) PostLoad() error {\n\tif len(c.Allowable) > 0 {\n\t\tif !strset.New(c.Allowable...).Has(c.Output) {\n\t\t\treturn fmt.Errorf(\"invalid output format: %s (expected one of: %s)\", c.Output, strings.Join(c.Allowable, \", \"))\n\t\t}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "cmd/grype/cli/options/database_search_os.go",
    "content": "package options\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"unicode\"\n\n\t\"github.com/anchore/clio\"\n\tv6 \"github.com/anchore/grype/grype/db/v6\"\n\t\"github.com/anchore/grype/grype/distro\"\n)\n\ntype DBSearchOSs struct {\n\tOSs   []string        `yaml:\"distro\" json:\"distro\" mapstructure:\"distro\"`\n\tSpecs v6.OSSpecifiers `yaml:\"-\" json:\"-\" mapstructure:\"-\"`\n}\n\nfunc (o *DBSearchOSs) AddFlags(flags clio.FlagSet) {\n\t// consistent with grype --distro flag today\n\tflags.StringArrayVarP(&o.OSs, \"distro\", \"\", \"refine to results with the given operating system (format: 'name', 'name[-:@]version', 'name[-:@]maj.min', 'name[-:@]codename')\")\n}\n\nfunc (o *DBSearchOSs) PostLoad() error {\n\tif len(o.OSs) == 0 {\n\t\to.Specs = []*v6.OSSpecifier{v6.AnyOSSpecified}\n\t\treturn nil\n\t}\n\n\tvar specs []*v6.OSSpecifier\n\tfor _, osValue := range o.OSs {\n\t\tspec, err := parseOSString(osValue)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tspecs = append(specs, spec)\n\t}\n\to.Specs = specs\n\n\treturn nil\n}\n\nfunc parseOSString(osValue string) (*v6.OSSpecifier, error) {\n\t// Check for multiple @ separators in the original input (not allowed)\n\tif strings.Count(osValue, \"@\") > 1 {\n\t\treturn nil, fmt.Errorf(\"invalid distro name@version: %q\", osValue)\n\t}\n\n\t// Use the shared parsing logic from grype/distro package\n\tname, version := distro.ParseDistroString(osValue)\n\n\tif name == \"\" {\n\t\treturn nil, fmt.Errorf(\"invalid distro input provided: %q\", osValue)\n\t}\n\n\t// Check if there was a separator but no version (e.g., \"ubuntu@\")\n\t// This can be detected by checking if the original string ends with a separator\n\toriginalTrimmed := strings.TrimSpace(osValue)\n\tif len(originalTrimmed) > 0 && version == \"\" {\n\t\tlastChar := originalTrimmed[len(originalTrimmed)-1]\n\t\tif lastChar == '-' || lastChar == ':' || lastChar == '@' {\n\t\t\treturn nil, fmt.Errorf(\"invalid distro version provided\")\n\t\t}\n\t}\n\n\t// No version specified\n\tif version == \"\" {\n\t\treturn &v6.OSSpecifier{Name: name}, nil\n\t}\n\n\t// parse the version (major.minor, major, or codename)\n\t// if starts with a number, then it is a version\n\tif unicode.IsDigit(rune(version[0])) {\n\t\tversionParts := strings.Split(version, \".\")\n\t\tvar major, minor string\n\t\tswitch len(versionParts) {\n\t\tcase 1:\n\t\t\tmajor = versionParts[0]\n\t\tcase 2:\n\t\t\tmajor = versionParts[0]\n\t\t\tminor = versionParts[1]\n\t\tcase 3:\n\t\t\treturn nil, fmt.Errorf(\"invalid distro version provided: patch version ignored: %q\", version)\n\t\tdefault:\n\t\t\treturn nil, fmt.Errorf(\"invalid distro version provided: %q\", version)\n\t\t}\n\n\t\treturn &v6.OSSpecifier{Name: name, MajorVersion: major, MinorVersion: minor}, nil\n\t}\n\n\t// is codename / label\n\treturn &v6.OSSpecifier{Name: name, LabelVersion: version}, nil\n}\n"
  },
  {
    "path": "cmd/grype/cli/options/database_search_os_test.go",
    "content": "package options\n\nimport (\n\t\"testing\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/stretchr/testify/require\"\n\n\tv6 \"github.com/anchore/grype/grype/db/v6\"\n)\n\nfunc TestDBSearchOSsPostLoad(t *testing.T) {\n\ttestCases := []struct {\n\t\tname           string\n\t\tinput          DBSearchOSs\n\t\texpectedSpecs  v6.OSSpecifiers\n\t\texpectedErrMsg string\n\t}{\n\t\t{\n\t\t\tname:          \"no OS input (any OS)\",\n\t\t\tinput:         DBSearchOSs{},\n\t\t\texpectedSpecs: []*v6.OSSpecifier{v6.AnyOSSpecified},\n\t\t},\n\t\t{\n\t\t\tname: \"valid OS name only\",\n\t\t\tinput: DBSearchOSs{\n\t\t\t\tOSs: []string{\"ubuntu\"},\n\t\t\t},\n\t\t\texpectedSpecs: []*v6.OSSpecifier{\n\t\t\t\t{Name: \"ubuntu\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"valid OS with major version\",\n\t\t\tinput: DBSearchOSs{\n\t\t\t\tOSs: []string{\"ubuntu@20\"},\n\t\t\t},\n\t\t\texpectedSpecs: []*v6.OSSpecifier{\n\t\t\t\t{Name: \"ubuntu\", MajorVersion: \"20\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"valid OS with major and minor version\",\n\t\t\tinput: DBSearchOSs{\n\t\t\t\tOSs: []string{\"ubuntu@20.04\"},\n\t\t\t},\n\t\t\texpectedSpecs: []*v6.OSSpecifier{\n\t\t\t\t{Name: \"ubuntu\", MajorVersion: \"20\", MinorVersion: \"04\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"valid OS with codename\",\n\t\t\tinput: DBSearchOSs{\n\t\t\t\tOSs: []string{\"ubuntu@focal\"},\n\t\t\t},\n\t\t\texpectedSpecs: []*v6.OSSpecifier{\n\t\t\t\t{Name: \"ubuntu\", LabelVersion: \"focal\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"invalid OS version (too many parts)\",\n\t\t\tinput: DBSearchOSs{\n\t\t\t\tOSs: []string{\"ubuntu@20.04.1\"},\n\t\t\t},\n\t\t\texpectedErrMsg: \"invalid distro version provided: patch version ignored\",\n\t\t},\n\t\t{\n\t\t\tname: \"invalid OS format with colon\",\n\t\t\tinput: DBSearchOSs{\n\t\t\t\tOSs: []string{\"ubuntu:20\"},\n\t\t\t},\n\t\t\texpectedSpecs: []*v6.OSSpecifier{\n\t\t\t\t{Name: \"ubuntu\", MajorVersion: \"20\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"invalid OS with empty version\",\n\t\t\tinput: DBSearchOSs{\n\t\t\t\tOSs: []string{\"ubuntu@\"},\n\t\t\t},\n\t\t\texpectedErrMsg: \"invalid distro version provided\",\n\t\t},\n\t\t{\n\t\t\tname: \"invalid OS name@version format\",\n\t\t\tinput: DBSearchOSs{\n\t\t\t\tOSs: []string{\"ubuntu@20@04\"},\n\t\t\t},\n\t\t\texpectedErrMsg: \"invalid distro name@version\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\terr := tc.input.PostLoad()\n\n\t\t\tif tc.expectedErrMsg != \"\" {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\trequire.ErrorContains(t, err, tc.expectedErrMsg)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\trequire.NoError(t, err)\n\t\t\tif d := cmp.Diff(tc.expectedSpecs, tc.input.Specs); d != \"\" {\n\t\t\t\tt.Errorf(\"unexpected OS specifiers (-want +got):\\n%s\", d)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "cmd/grype/cli/options/database_search_packages.go",
    "content": "package options\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/anchore/clio\"\n\tv6 \"github.com/anchore/grype/grype/db/v6\"\n\t\"github.com/anchore/grype/internal/log\"\n\t\"github.com/anchore/packageurl-go\"\n\t\"github.com/anchore/syft/syft/cpe\"\n)\n\ntype DBSearchPackages struct {\n\tAllowBroadCPEMatching bool                 `yaml:\"allow-broad-cpe-matching\" json:\"allow-broad-cpe-matching\" mapstructure:\"allow-broad-cpe-matching\"`\n\tPackages              []string             `yaml:\"packages\" json:\"packages\" mapstructure:\"packages\"`\n\tEcosystem             string               `yaml:\"ecosystem\" json:\"ecosystem\" mapstructure:\"ecosystem\"`\n\tPkgSpecs              v6.PackageSpecifiers `yaml:\"-\" json:\"-\" mapstructure:\"-\"`\n\tCPESpecs              v6.PackageSpecifiers `yaml:\"-\" json:\"-\" mapstructure:\"-\"`\n}\n\nfunc (o *DBSearchPackages) AddFlags(flags clio.FlagSet) {\n\tflags.StringArrayVarP(&o.Packages, \"pkg\", \"\", \"package name/CPE/PURL to search for\")\n\tflags.StringVarP(&o.Ecosystem, \"ecosystem\", \"\", \"ecosystem of the package to search within\")\n\tflags.BoolVarP(&o.AllowBroadCPEMatching, \"broad-cpe-matching\", \"\", \"allow for specific package CPE attributes to match with '*' values on the vulnerability\")\n}\n\nfunc (o *DBSearchPackages) PostLoad() error {\n\t// note: this may be called multiple times, so we need to reset the specs each time\n\to.PkgSpecs = nil\n\to.CPESpecs = nil\n\n\tfor _, p := range o.Packages {\n\t\tswitch {\n\t\tcase strings.HasPrefix(p, \"cpe:\"):\n\t\t\tc, err := cpe.NewAttributes(p)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"invalid CPE from %q: %w\", o.Packages, err)\n\t\t\t}\n\n\t\t\tif c.Version != \"\" || c.Update != \"\" {\n\t\t\t\tlog.Warnf(\"ignoring version and update values for %q\", p)\n\t\t\t\tc.Version = \"\"\n\t\t\t\tc.Update = \"\"\n\t\t\t}\n\n\t\t\ts := &v6.PackageSpecifier{CPE: &c}\n\t\t\to.CPESpecs = append(o.CPESpecs, s)\n\t\t\to.PkgSpecs = append(o.PkgSpecs, s)\n\t\tcase strings.HasPrefix(p, \"pkg:\"):\n\t\t\tif o.Ecosystem != \"\" {\n\t\t\t\treturn errors.New(\"cannot specify both package URL and ecosystem\")\n\t\t\t}\n\n\t\t\tpurl, err := packageurl.FromString(p)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"invalid package URL from %q: %w\", o.Packages, err)\n\t\t\t}\n\n\t\t\tif purl.Version != \"\" || len(purl.Qualifiers) > 0 {\n\t\t\t\tlog.Warnf(\"ignoring version and qualifiers for package URL %q\", purl)\n\t\t\t}\n\n\t\t\to.PkgSpecs = append(o.PkgSpecs, &v6.PackageSpecifier{Name: purl.Name, Ecosystem: purl.Type})\n\t\t\to.CPESpecs = append(o.CPESpecs, &v6.PackageSpecifier{CPE: &cpe.Attributes{Part: \"a\", Product: purl.Name, TargetSW: purl.Type}})\n\n\t\tdefault:\n\t\t\to.PkgSpecs = append(o.PkgSpecs, &v6.PackageSpecifier{Name: p, Ecosystem: o.Ecosystem})\n\t\t\to.CPESpecs = append(o.CPESpecs, &v6.PackageSpecifier{\n\t\t\t\tCPE: &cpe.Attributes{Part: \"a\", Product: p},\n\t\t\t})\n\t\t}\n\t}\n\n\tif len(o.Packages) == 0 {\n\t\tif o.Ecosystem != \"\" {\n\t\t\to.PkgSpecs = append(o.PkgSpecs, &v6.PackageSpecifier{Ecosystem: o.Ecosystem})\n\t\t\to.CPESpecs = append(o.CPESpecs, &v6.PackageSpecifier{CPE: &cpe.Attributes{TargetSW: o.Ecosystem}})\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "cmd/grype/cli/options/database_search_packages_test.go",
    "content": "package options\n\nimport (\n\t\"testing\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/stretchr/testify/require\"\n\n\tv6 \"github.com/anchore/grype/grype/db/v6\"\n\t\"github.com/anchore/syft/syft/cpe\"\n)\n\nfunc TestDBSearchPackagesPostLoad(t *testing.T) {\n\ttestCases := []struct {\n\t\tname           string\n\t\tinput          DBSearchPackages\n\t\texpectedPkg    v6.PackageSpecifiers\n\t\texpectedCPE    v6.PackageSpecifiers\n\t\texpectedErrMsg string\n\t}{\n\t\t{\n\t\t\tname: \"valid CPE\",\n\t\t\tinput: DBSearchPackages{\n\t\t\t\tPackages: []string{\"cpe:2.3:a:vendor:product:1.0:*:*:*:*:*:*:*\"},\n\t\t\t},\n\t\t\texpectedPkg: v6.PackageSpecifiers{\n\t\t\t\t{CPE: &cpe.Attributes{Part: \"a\", Vendor: \"vendor\", Product: \"product\"}},\n\t\t\t},\n\t\t\texpectedCPE: v6.PackageSpecifiers{\n\t\t\t\t{CPE: &cpe.Attributes{Part: \"a\", Vendor: \"vendor\", Product: \"product\"}},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"valid PURL\",\n\t\t\tinput: DBSearchPackages{\n\t\t\t\tPackages: []string{\"pkg:npm/package-name@1.0.0\"},\n\t\t\t},\n\t\t\texpectedPkg: v6.PackageSpecifiers{\n\t\t\t\t{Name: \"package-name\", Ecosystem: \"npm\"},\n\t\t\t},\n\t\t\texpectedCPE: v6.PackageSpecifiers{\n\t\t\t\t{CPE: &cpe.Attributes{Part: \"a\", Product: \"package-name\", TargetSW: \"npm\"}},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"plain package name\",\n\t\t\tinput: DBSearchPackages{\n\t\t\t\tPackages: []string{\"package-name\"},\n\t\t\t},\n\t\t\texpectedPkg: v6.PackageSpecifiers{\n\t\t\t\t{Name: \"package-name\"},\n\t\t\t},\n\t\t\texpectedCPE: v6.PackageSpecifiers{\n\t\t\t\t{CPE: &cpe.Attributes{Part: \"a\", Product: \"package-name\"}},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"ecosystem without packages\",\n\t\t\tinput: DBSearchPackages{\n\t\t\t\tEcosystem: \"npm\",\n\t\t\t},\n\t\t\texpectedPkg: v6.PackageSpecifiers{\n\t\t\t\t{Ecosystem: \"npm\"},\n\t\t\t},\n\t\t\texpectedCPE: v6.PackageSpecifiers{\n\t\t\t\t{CPE: &cpe.Attributes{TargetSW: \"npm\"}},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"conflicting PURL and ecosystem\",\n\t\t\tinput: DBSearchPackages{\n\t\t\t\tPackages:  []string{\"pkg:npm/package-name@1.0.0\"},\n\t\t\t\tEcosystem: \"npm\",\n\t\t\t},\n\t\t\texpectedErrMsg: \"cannot specify both package URL and ecosystem\",\n\t\t},\n\t\t{\n\t\t\tname: \"invalid CPE\",\n\t\t\tinput: DBSearchPackages{\n\t\t\t\tPackages: []string{\"cpe:2.3:a:$%&^*%\"},\n\t\t\t},\n\t\t\texpectedErrMsg: \"invalid CPE\",\n\t\t},\n\t\t{\n\t\t\tname: \"invalid PURL\",\n\t\t\tinput: DBSearchPackages{\n\t\t\t\tPackages: []string{\"pkg:invalid\"},\n\t\t\t},\n\t\t\texpectedErrMsg: \"invalid package URL\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\terr := tc.input.PostLoad()\n\n\t\t\tif tc.expectedErrMsg != \"\" {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\trequire.ErrorContains(t, err, tc.expectedErrMsg)\n\t\t\t\treturn\n\t\t\t}\n\t\t\trequire.NoError(t, err)\n\t\t\tif d := cmp.Diff(tc.expectedPkg, tc.input.PkgSpecs); d != \"\" {\n\t\t\t\tt.Errorf(\"unexpected package specifiers (-want +got):\\n%s\", d)\n\t\t\t}\n\t\t\tif d := cmp.Diff(tc.expectedCPE, tc.input.CPESpecs); d != \"\" {\n\t\t\t\tt.Errorf(\"unexpected CPE specifiers (-want +got):\\n%s\", d)\n\t\t\t}\n\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "cmd/grype/cli/options/database_search_vulnerabilities.go",
    "content": "package options\n\nimport (\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/araddon/dateparse\"\n\n\t\"github.com/anchore/clio\"\n\tv6 \"github.com/anchore/grype/grype/db/v6\"\n)\n\ntype DBSearchVulnerabilities struct {\n\tVulnerabilityIDs []string `yaml:\"vulnerability-ids\" json:\"vulnerability-ids\" mapstructure:\"vulnerability-ids\"`\n\tUseVulnIDFlag    bool     `yaml:\"-\" json:\"-\" mapstructure:\"-\"`\n\n\tPublishedAfter string `yaml:\"published-after\" json:\"published-after\" mapstructure:\"published-after\"`\n\tModifiedAfter  string `yaml:\"modified-after\" json:\"modified-after\" mapstructure:\"modified-after\"`\n\n\tProviders  []string `yaml:\"providers\" json:\"providers\" mapstructure:\"providers\"`\n\tFixedState []string `yaml:\"fixed-state\" json:\"fixed-state\" mapstructure:\"fixed-state\"`\n\n\tSpecs v6.VulnerabilitySpecifiers `yaml:\"-\" json:\"-\" mapstructure:\"-\"`\n}\n\nfunc (c *DBSearchVulnerabilities) AddFlags(flags clio.FlagSet) {\n\tif c.UseVulnIDFlag {\n\t\tflags.StringArrayVarP(&c.VulnerabilityIDs, \"vuln\", \"\", \"only show results for the given vulnerability ID\")\n\t}\n\tflags.StringVarP(&c.PublishedAfter, \"published-after\", \"\", \"only show vulnerabilities originally published after the given date (format: YYYY-MM-DD)\")\n\tflags.StringVarP(&c.ModifiedAfter, \"modified-after\", \"\", \"only show vulnerabilities originally published or modified since the given date (format: YYYY-MM-DD)\")\n\tflags.StringArrayVarP(&c.Providers, \"provider\", \"\", \"only show vulnerabilities from the given provider\")\n\tflags.StringArrayVarP(&c.FixedState, \"fixed-state\", \"\", \"only show vulnerabilities with the given fix state (fixed, not-fixed, unknown, wont-fix)\")\n}\n\nfunc (c *DBSearchVulnerabilities) PostLoad() error {\n\t// note: this may be called multiple times, so we need to reset the specs each time\n\tc.Specs = nil\n\n\thandleTimeOption := func(val string, flag string) (*time.Time, error) {\n\t\tif val == \"\" {\n\t\t\treturn nil, nil\n\t\t}\n\t\tparsed, err := dateparse.ParseIn(val, time.UTC)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"invalid date format for %s=%q: %w\", flag, val, err)\n\t\t}\n\t\treturn &parsed, nil\n\t}\n\n\tif c.PublishedAfter != \"\" && c.ModifiedAfter != \"\" {\n\t\treturn fmt.Errorf(\"only one of --published-after or --modified-after can be set\")\n\t}\n\n\tvalidFixStates := map[string]bool{\n\t\t\"fixed\":     true,\n\t\t\"not-fixed\": true,\n\t\t\"unknown\":   true,\n\t\t\"wont-fix\":  true,\n\t}\n\tfor _, fs := range c.FixedState {\n\t\tif !validFixStates[fs] {\n\t\t\treturn fmt.Errorf(\"invalid fixed-state value: %q (valid values: fixed, not-fixed, unknown, wont-fix)\", fs)\n\t\t}\n\t}\n\n\tvar publishedAfter, modifiedAfter *time.Time\n\tvar err error\n\tpublishedAfter, err = handleTimeOption(c.PublishedAfter, \"published-after\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"invalid date format for published-after field: %w\", err)\n\t}\n\tmodifiedAfter, err = handleTimeOption(c.ModifiedAfter, \"modified-after\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"invalid date format for modified-after field: %w\", err)\n\t}\n\n\tvar specs []v6.VulnerabilitySpecifier\n\tfor _, vulnID := range c.VulnerabilityIDs {\n\t\tspecs = append(specs, v6.VulnerabilitySpecifier{\n\t\t\tName:           vulnID,\n\t\t\tPublishedAfter: publishedAfter,\n\t\t\tModifiedAfter:  modifiedAfter,\n\t\t\tProviders:      c.Providers,\n\t\t})\n\t}\n\n\tif len(specs) == 0 {\n\t\tif c.PublishedAfter != \"\" || c.ModifiedAfter != \"\" || len(c.Providers) > 0 {\n\t\t\tspecs = append(specs, v6.VulnerabilitySpecifier{\n\t\t\t\tPublishedAfter: publishedAfter,\n\t\t\t\tModifiedAfter:  modifiedAfter,\n\t\t\t\tProviders:      c.Providers,\n\t\t\t})\n\t\t}\n\t}\n\n\tc.Specs = specs\n\n\treturn nil\n}\n"
  },
  {
    "path": "cmd/grype/cli/options/database_search_vulnerabilities_test.go",
    "content": "package options\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/stretchr/testify/require\"\n\n\tv6 \"github.com/anchore/grype/grype/db/v6\"\n)\n\nfunc TestDBSearchVulnerabilitiesPostLoad(t *testing.T) {\n\ttestCases := []struct {\n\t\tname           string\n\t\tinput          DBSearchVulnerabilities\n\t\texpectedSpecs  v6.VulnerabilitySpecifiers\n\t\texpectedErrMsg string\n\t}{\n\t\t{\n\t\t\tname: \"single vulnerability ID\",\n\t\t\tinput: DBSearchVulnerabilities{\n\t\t\t\tVulnerabilityIDs: []string{\"CVE-2023-0001\"},\n\t\t\t},\n\t\t\texpectedSpecs: v6.VulnerabilitySpecifiers{\n\t\t\t\t{Name: \"CVE-2023-0001\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"multiple vulnerability IDs\",\n\t\t\tinput: DBSearchVulnerabilities{\n\t\t\t\tVulnerabilityIDs: []string{\"CVE-2023-0001\", \"GHSA-1234\"},\n\t\t\t},\n\t\t\texpectedSpecs: v6.VulnerabilitySpecifiers{\n\t\t\t\t{Name: \"CVE-2023-0001\"},\n\t\t\t\t{Name: \"GHSA-1234\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"published-after set\",\n\t\t\tinput: DBSearchVulnerabilities{\n\t\t\t\tPublishedAfter: \"2023-01-01\",\n\t\t\t},\n\t\t\texpectedSpecs: v6.VulnerabilitySpecifiers{\n\t\t\t\t{PublishedAfter: parseTime(\"2023-01-01\")},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"modified-after set\",\n\t\t\tinput: DBSearchVulnerabilities{\n\t\t\t\tModifiedAfter: \"2023-02-01\",\n\t\t\t},\n\t\t\texpectedSpecs: v6.VulnerabilitySpecifiers{\n\t\t\t\t{ModifiedAfter: parseTime(\"2023-02-01\")},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"both published-after and modified-after set\",\n\t\t\tinput: DBSearchVulnerabilities{\n\t\t\t\tPublishedAfter: \"2023-01-01\",\n\t\t\t\tModifiedAfter:  \"2023-02-01\",\n\t\t\t},\n\t\t\texpectedErrMsg: \"only one of --published-after or --modified-after can be set\",\n\t\t},\n\t\t{\n\t\t\tname: \"invalid date for published-after\",\n\t\t\tinput: DBSearchVulnerabilities{\n\t\t\t\tPublishedAfter: \"invalid-date\",\n\t\t\t},\n\t\t\texpectedErrMsg: \"invalid date format for published-after\",\n\t\t},\n\t\t{\n\t\t\tname: \"invalid date for modified-after\",\n\t\t\tinput: DBSearchVulnerabilities{\n\t\t\t\tModifiedAfter: \"invalid-date\",\n\t\t\t},\n\t\t\texpectedErrMsg: \"invalid date format for modified-after\",\n\t\t},\n\t\t{\n\t\t\tname: \"vulnerability ID with providers\",\n\t\t\tinput: DBSearchVulnerabilities{\n\t\t\t\tVulnerabilityIDs: []string{\"CVE-2023-0001\"},\n\t\t\t\tProviders:        []string{\"provider1\"},\n\t\t\t},\n\t\t\texpectedSpecs: v6.VulnerabilitySpecifiers{\n\t\t\t\t{Name: \"CVE-2023-0001\", Providers: []string{\"provider1\"}},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"providers without vulnerability IDs\",\n\t\t\tinput: DBSearchVulnerabilities{\n\t\t\t\tProviders: []string{\"provider1\", \"provider2\"},\n\t\t\t},\n\t\t\texpectedSpecs: v6.VulnerabilitySpecifiers{\n\t\t\t\t{Providers: []string{\"provider1\", \"provider2\"}},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"valid fixed-state: fixed\",\n\t\t\tinput: DBSearchVulnerabilities{\n\t\t\t\tFixedState: []string{\"fixed\"},\n\t\t\t},\n\t\t\texpectedSpecs: nil,\n\t\t},\n\t\t{\n\t\t\tname: \"valid fixed-state: multiple values\",\n\t\t\tinput: DBSearchVulnerabilities{\n\t\t\t\tFixedState: []string{\"fixed\", \"not-fixed\", \"wont-fix\", \"unknown\"},\n\t\t\t},\n\t\t\texpectedSpecs: nil,\n\t\t},\n\t\t{\n\t\t\tname: \"invalid fixed-state\",\n\t\t\tinput: DBSearchVulnerabilities{\n\t\t\t\tFixedState: []string{\"invalid-state\"},\n\t\t\t},\n\t\t\texpectedErrMsg: \"invalid fixed-state value: \\\"invalid-state\\\"\",\n\t\t},\n\t\t{\n\t\t\tname: \"mixed valid and invalid fixed-state\",\n\t\t\tinput: DBSearchVulnerabilities{\n\t\t\t\tFixedState: []string{\"fixed\", \"bad-state\"},\n\t\t\t},\n\t\t\texpectedErrMsg: \"invalid fixed-state value: \\\"bad-state\\\"\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\terr := tc.input.PostLoad()\n\n\t\t\tif tc.expectedErrMsg != \"\" {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\trequire.ErrorContains(t, err, tc.expectedErrMsg)\n\t\t\t\treturn\n\t\t\t}\n\t\t\trequire.NoError(t, err)\n\t\t\tif d := cmp.Diff(tc.expectedSpecs, tc.input.Specs); d != \"\" {\n\t\t\t\tt.Errorf(\"unexpected vulnerability specifiers (-want +got):\\n%s\", d)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc parseTime(value string) *time.Time {\n\tt, _ := time.Parse(\"2006-01-02\", value)\n\treturn &t\n}\n"
  },
  {
    "path": "cmd/grype/cli/options/datasources.go",
    "content": "package options\n\nimport (\n\t\"time\"\n\n\t\"github.com/anchore/clio\"\n\t\"github.com/anchore/grype/grype/matcher/java\"\n)\n\nconst (\n\tdefaultMavenBaseURL = \"https://search.maven.org/solrsearch/select\"\n)\n\ntype externalSources struct {\n\tEnable bool  `yaml:\"enable\" json:\"enable\" mapstructure:\"enable\"`\n\tMaven  maven `yaml:\"maven\" json:\"maven\" mapstructure:\"maven\"`\n}\n\nvar _ interface {\n\tclio.FieldDescriber\n} = (*externalSources)(nil)\n\ntype maven struct {\n\tSearchUpstreamBySha1 bool          `yaml:\"search-upstream\" json:\"searchUpstreamBySha1\" mapstructure:\"search-maven-upstream\"`\n\tBaseURL              string        `yaml:\"base-url\" json:\"baseUrl\" mapstructure:\"base-url\"`\n\tRateLimit            time.Duration `yaml:\"rate-limit\" json:\"rateLimit\" mapstructure:\"rate-limit\"`\n}\n\nfunc defaultExternalSources() externalSources {\n\treturn externalSources{\n\t\tMaven: maven{\n\t\t\tSearchUpstreamBySha1: true,\n\t\t\tBaseURL:              defaultMavenBaseURL,\n\t\t\tRateLimit:            300 * time.Millisecond,\n\t\t},\n\t}\n}\n\nfunc (cfg externalSources) ToJavaMatcherConfig() java.ExternalSearchConfig {\n\t// always respect if global config is disabled\n\tsmu := cfg.Maven.SearchUpstreamBySha1\n\tif !cfg.Enable {\n\t\tsmu = cfg.Enable\n\t}\n\treturn java.ExternalSearchConfig{\n\t\tSearchMavenUpstream: smu,\n\t\tMavenBaseURL:        cfg.Maven.BaseURL,\n\t\tMavenRateLimit:      cfg.Maven.RateLimit,\n\t}\n}\n\nfunc (cfg *externalSources) DescribeFields(descriptions clio.FieldDescriptionSet) {\n\tdescriptions.Add(&cfg.Enable, `enable Grype searching network source for additional information`)\n\tdescriptions.Add(&cfg.Maven.SearchUpstreamBySha1, `search for Maven artifacts by SHA1`)\n\tdescriptions.Add(&cfg.Maven.BaseURL, `base URL of the Maven repository to search`)\n}\n"
  },
  {
    "path": "cmd/grype/cli/options/experimental.go",
    "content": "package options\n\n// Experimental options are opt-in features that are...\n// ...not stable\n// ...not yet fully supported\n// ...not necessarily tested\n// ...not ready for production use\n// these may go away at any moment, do not depend on them\ntype Experimental struct {\n}\n"
  },
  {
    "path": "cmd/grype/cli/options/fix_channels.go",
    "content": "package options\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/anchore/clio\"\n\t\"github.com/anchore/grype/grype/distro\"\n)\n\ntype FixChannelEnabled string\n\ntype FixChannels struct {\n\t// TODO: in the future we may want to support more channels, as well as have a default-apply value here that can be overridden within each channel configuration\n\n\t// EUS is the Extended Update Support channel for RHEL\n\tRedHatEUS FixChannel `yaml:\"redhat-eus\" json:\"redhat-eus\" mapstructure:\"redhat-eus\"`\n}\n\ntype FixChannel struct {\n\t// Apply indicates how the channel should be applied to the distro\n\tApply string `yaml:\"apply\" json:\"apply\" mapstructure:\"apply\"`\n\n\t// Versions specifies a constraint string indicating which versions of the distro this channel applies to (e.g. \">= 8.0\" for RHEL 8 and above)\n\tVersions string `yaml:\"versions\" json:\"versions\" mapstructure:\"versions\"`\n}\n\nfunc (o *FixChannel) PostLoad() error {\n\tif o.Apply == \"\" {\n\t\to.Apply = string(distro.ChannelConditionallyEnabled)\n\t}\n\n\tswitch strings.ToLower(o.Apply) {\n\tcase string(distro.ChannelNeverEnabled), string(distro.ChannelAlwaysEnabled), string(distro.ChannelConditionallyEnabled):\n\t\treturn nil\n\tdefault:\n\t\treturn fmt.Errorf(\"apply %q valid values are 'never', 'always', or 'auto' (conditionally applied based on SBOM data)\", o.Apply)\n\t}\n}\n\nfunc DefaultFixChannels() FixChannels {\n\trhelEUS := distro.DefaultFixChannels().Get(\"eus\")\n\n\tif rhelEUS == nil {\n\t\tpanic(\"default fix channels do not contain Red Hat EUS channel\")\n\t}\n\n\t// use API defaults for the CLI configuration\n\treturn FixChannels{\n\t\tRedHatEUS: FixChannel{\n\t\t\tApply:    string(rhelEUS.Apply),\n\t\t\tVersions: rhelEUS.Versions.Value(),\n\t\t},\n\t}\n}\n\nfunc (o *FixChannels) DescribeFields(descriptions clio.FieldDescriptionSet) {\n\tdescriptions.Add(&o.RedHatEUS, `whether to always enable, disable, or automatically detect when to use Red Hat Extended Update Support (EUS) vulnerability data`)\n\tdescriptions.Add(&o.RedHatEUS.Apply, `whether fixes from this channel should be considered, options are \"never\", \"always\", or \"auto\" (conditionally applied based on SBOM data)`)\n}\n"
  },
  {
    "path": "cmd/grype/cli/options/grype.go",
    "content": "package options\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/anchore/clio\"\n\t\"github.com/anchore/grype/grype/match\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n\t\"github.com/anchore/grype/internal/format\"\n\t\"github.com/anchore/syft/syft/source\"\n)\n\ntype Grype struct {\n\tOutputs                    []string           `yaml:\"output\" json:\"output\" mapstructure:\"output\"` // -o, <presenter>=<file> the Presenter hint string to use for report formatting and the output file\n\tFile                       string             `yaml:\"file\" json:\"file\" mapstructure:\"file\"`       // --file, the file to write report output to\n\tPretty                     bool               `yaml:\"pretty\" json:\"pretty\" mapstructure:\"pretty\"`\n\tDistro                     string             `yaml:\"distro\" json:\"distro\" mapstructure:\"distro\"`                                           // --distro, specify a distro to explicitly use\n\tGenerateMissingCPEs        bool               `yaml:\"add-cpes-if-none\" json:\"add-cpes-if-none\" mapstructure:\"add-cpes-if-none\"`             // --add-cpes-if-none, automatically generate CPEs if they are not present in import (e.g. from a 3rd party SPDX document)\n\tOutputTemplateFile         string             `yaml:\"output-template-file\" json:\"output-template-file\" mapstructure:\"output-template-file\"` // -t, the template file to use for formatting the final report\n\tCheckForAppUpdate          bool               `yaml:\"check-for-app-update\" json:\"check-for-app-update\" mapstructure:\"check-for-app-update\"` // whether to check for an application update on start up or not\n\tOnlyFixed                  bool               `yaml:\"only-fixed\" json:\"only-fixed\" mapstructure:\"only-fixed\"`                               // only fail if detected vulns have a fix\n\tOnlyNotFixed               bool               `yaml:\"only-notfixed\" json:\"only-notfixed\" mapstructure:\"only-notfixed\"`                      // only fail if detected vulns don't have a fix\n\tIgnoreStates               string             `yaml:\"ignore-states\" json:\"ignore-wontfix\" mapstructure:\"ignore-wontfix\"`                    // ignore detections for vulnerabilities matching these comma-separated fix states\n\tPlatform                   string             `yaml:\"platform\" json:\"platform\" mapstructure:\"platform\"`                                     // --platform, override the target platform for a container image\n\tSearch                     search             `yaml:\"search\" json:\"search\" mapstructure:\"search\"`\n\tIgnore                     []match.IgnoreRule `yaml:\"ignore\" json:\"ignore\" mapstructure:\"ignore\"`\n\tExclusions                 []string           `yaml:\"exclude\" json:\"exclude\" mapstructure:\"exclude\"`\n\tExternalSources            externalSources    `yaml:\"external-sources\" json:\"externalSources\" mapstructure:\"external-sources\"`\n\tMatch                      matchConfig        `yaml:\"match\" json:\"match\" mapstructure:\"match\"`\n\tFailOn                     string             `yaml:\"fail-on-severity\" json:\"fail-on-severity\" mapstructure:\"fail-on-severity\"`\n\tRegistry                   registry           `yaml:\"registry\" json:\"registry\" mapstructure:\"registry\"`\n\tShowSuppressed             bool               `yaml:\"show-suppressed\" json:\"show-suppressed\" mapstructure:\"show-suppressed\"`\n\tByCVE                      bool               `yaml:\"by-cve\" json:\"by-cve\" mapstructure:\"by-cve\"` // --by-cve, indicates if the original match vulnerability IDs should be preserved or the CVE should be used instead\n\tSortBy                     SortBy             `yaml:\",inline\" json:\",inline\" mapstructure:\",squash\"`\n\tName                       string             `yaml:\"name\" json:\"name\" mapstructure:\"name\"`\n\tDefaultImagePullSource     string             `yaml:\"default-image-pull-source\" json:\"default-image-pull-source\" mapstructure:\"default-image-pull-source\"`\n\tFrom                       []string           `yaml:\"from\" json:\"from\" mapstructure:\"from\"`\n\tVexDocuments               []string           `yaml:\"vex-documents\" json:\"vex-documents\" mapstructure:\"vex-documents\"`\n\tVexAdd                     []string           `yaml:\"vex-add\" json:\"vex-add\" mapstructure:\"vex-add\"`                                                                   // GRYPE_VEX_ADD\n\tMatchUpstreamKernelHeaders bool               `yaml:\"match-upstream-kernel-headers\" json:\"match-upstream-kernel-headers\" mapstructure:\"match-upstream-kernel-headers\"` // Show matches on kernel-headers packages where the match is on kernel upstream instead of marking them as ignored, default=false\n\tFixChannel                 FixChannels        `yaml:\"fix-channel\" json:\"fix-channel\" mapstructure:\"fix-channel\"`                                                       // the fix channels to apply to the distro when matching\n\tTimestamp                  bool               `yaml:\"timestamp\" json:\"timestamp\" mapstructure:\"timestamp\"`\n\tAlerts                     Alerts             `yaml:\"alerts\" json:\"alerts\" mapstructure:\"alerts\"`\n\tDatabaseCommand            `yaml:\",inline\" json:\",inline\" mapstructure:\",squash\"`\n}\n\ntype developer struct {\n\tDB databaseDeveloper `yaml:\"db\" json:\"db\" mapstructure:\"db\"`\n}\n\ntype databaseDeveloper struct {\n\tDebug bool `yaml:\"debug\" json:\"debug\" mapstructure:\"debug\"`\n}\n\nvar _ interface {\n\tclio.FlagAdder\n\tclio.PostLoader\n\tclio.FieldDescriber\n} = (*Grype)(nil)\n\nfunc DefaultGrype(id clio.Identification) *Grype {\n\treturn &Grype{\n\t\tSearch:     defaultSearch(source.SquashedScope),\n\t\tFixChannel: DefaultFixChannels(),\n\t\tDatabaseCommand: DatabaseCommand{\n\t\t\tDB: DefaultDatabase(id),\n\t\t},\n\t\tMatch:                      defaultMatchConfig(),\n\t\tExternalSources:            defaultExternalSources(),\n\t\tCheckForAppUpdate:          true,\n\t\tVexAdd:                     []string{},\n\t\tMatchUpstreamKernelHeaders: false,\n\t\tSortBy:                     defaultSortBy(),\n\t\tTimestamp:                  true,\n\t\tAlerts:                     defaultAlerts(),\n\t}\n}\n\n// nolint:funlen\nfunc (o *Grype) AddFlags(flags clio.FlagSet) {\n\tflags.StringVarP(&o.Search.Scope,\n\t\t\"scope\", \"s\",\n\t\tfmt.Sprintf(\"selection of layers to analyze, options=%v\", source.AllScopes),\n\t)\n\n\tflags.StringArrayVarP(&o.Outputs,\n\t\t\"output\", \"o\",\n\t\tfmt.Sprintf(\"report output formatter, formats=%v, deprecated formats=%v\", format.AvailableFormats, format.DeprecatedFormats),\n\t)\n\n\tflags.StringVarP(&o.File,\n\t\t\"file\", \"\",\n\t\t\"file to write the default report output to (default is STDOUT)\",\n\t)\n\n\tflags.StringVarP(&o.Name,\n\t\t\"name\", \"\",\n\t\t\"set the name of the target being analyzed\",\n\t)\n\n\tflags.StringVarP(&o.Distro,\n\t\t\"distro\", \"\",\n\t\t\"distro to match against in the format: <distro>[-:@]<version>\",\n\t)\n\n\tflags.BoolVarP(&o.GenerateMissingCPEs,\n\t\t\"add-cpes-if-none\", \"\",\n\t\t\"generate CPEs for packages with no CPE data\",\n\t)\n\n\tflags.StringVarP(&o.OutputTemplateFile,\n\t\t\"template\", \"t\",\n\t\t\"specify the path to a Go template file (requires 'template' output to be selected)\")\n\n\tflags.StringVarP(&o.FailOn,\n\t\t\"fail-on\", \"f\",\n\t\tfmt.Sprintf(\"set the return code to 2 if a vulnerability is found with a severity >= the given severity, options=%v\", vulnerability.AllSeverities()),\n\t)\n\n\tflags.BoolVarP(&o.OnlyFixed,\n\t\t\"only-fixed\", \"\",\n\t\t\"ignore matches for vulnerabilities that are not fixed\",\n\t)\n\n\tflags.BoolVarP(&o.OnlyNotFixed,\n\t\t\"only-notfixed\", \"\",\n\t\t\"ignore matches for vulnerabilities that are fixed\",\n\t)\n\n\tflags.StringVarP(&o.IgnoreStates,\n\t\t\"ignore-states\", \"\",\n\t\tfmt.Sprintf(\"ignore matches for vulnerabilities with specified comma separated fix states, options=%v\", vulnerability.AllFixStates()),\n\t)\n\n\tflags.BoolVarP(&o.ByCVE,\n\t\t\"by-cve\", \"\",\n\t\t\"orient results by CVE instead of the original vulnerability ID when possible\",\n\t)\n\n\tflags.BoolVarP(&o.ShowSuppressed,\n\t\t\"show-suppressed\", \"\",\n\t\t\"show suppressed/ignored vulnerabilities in the output (only supported with table output format)\",\n\t)\n\n\tflags.StringArrayVarP(&o.Exclusions,\n\t\t\"exclude\", \"\",\n\t\t\"exclude paths from being scanned using a glob expression\",\n\t)\n\n\tflags.StringVarP(&o.Platform,\n\t\t\"platform\", \"\",\n\t\t\"an optional platform specifier for container image sources (e.g. 'linux/arm64', 'linux/arm64/v8', 'arm64', 'linux')\",\n\t)\n\n\tflags.StringArrayVarP(&o.From,\n\t\t\"from\", \"\",\n\t\t\"specify the source behavior to use (e.g. docker, registry, podman, oci-dir, ...)\",\n\t)\n\n\tflags.StringArrayVarP(&o.VexDocuments,\n\t\t\"vex\", \"\",\n\t\t\"a list of VEX documents to consider when producing scanning results\",\n\t)\n}\n\nfunc (o *Grype) PostLoad() error {\n\to.From = flatten(o.From)\n\n\tif o.FailOn != \"\" {\n\t\tfailOnSeverity := *o.FailOnSeverity()\n\t\tif failOnSeverity == vulnerability.UnknownSeverity {\n\t\t\treturn fmt.Errorf(\"bad --fail-on severity value '%s'\", o.FailOn)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (o *Grype) DescribeFields(descriptions clio.FieldDescriptionSet) {\n\tdescriptions.Add(&o.CheckForAppUpdate, `enable/disable checking for application updates on startup`)\n\tdescriptions.Add(&o.DefaultImagePullSource, `allows users to specify which image source should be used to generate the sbom\nvalid values are: registry, docker, podman`)\n\tdescriptions.Add(&o.Name, `same as --name; set the name of the target being analyzed`)\n\tdescriptions.Add(&o.Exclusions, `a list of globs to exclude from scanning, for example:\n  - '/etc/**'\n  - './out/**/*.json'\nsame as --exclude`)\n\tdescriptions.Add(&o.File, `if using template output, you must provide a path to a Go template file\nsee https://github.com/anchore/grype#using-templates for more information on template output\nthe default path to the template file is the current working directory\noutput-template-file: .grype/html.tmpl\n\nwrite output report to a file (default is to write to stdout)`)\n\tdescriptions.Add(&o.Outputs, `the output format of the vulnerability report (options: table, template, json, cyclonedx)\nwhen using template as the output type, you must also provide a value for 'output-template-file'`)\n\tdescriptions.Add(&o.Pretty, `pretty-print output`)\n\tdescriptions.Add(&o.FailOn, `upon scanning, if a severity is found at or above the given severity then the return code will be 1\ndefault is unset which will skip this validation (options: negligible, low, medium, high, critical)`)\n\tdescriptions.Add(&o.Ignore, `A list of vulnerability ignore rules, one or more property may be specified and all matching vulnerabilities will be ignored.\nThis is the full set of supported rule fields:\n  - vulnerability: CVE-2008-4318\n    fix-state: unknown\n    package:\n      name: libcurl\n      version: 1.5.1\n      type: npm\n      location: \"/usr/local/lib/node_modules/**\"\n\nVEX fields apply when Grype reads vex data:\n  - vex-status: not_affected\n    vex-justification: vulnerable_code_not_present\n`)\n\tdescriptions.Add(&o.VexAdd, `VEX statuses to consider as ignored rules`)\n\tdescriptions.Add(&o.MatchUpstreamKernelHeaders, `match kernel-header packages with upstream kernel as kernel vulnerabilities`)\n}\n\nfunc (o Grype) FailOnSeverity() *vulnerability.Severity {\n\tseverity := vulnerability.ParseSeverity(o.FailOn)\n\treturn &severity\n}\n\n// flatten takes a list of comma-separated entries and returns a flattened list of trimmed values (preserving order)\nfunc flatten(commaSeparatedEntries []string) []string {\n\tvar out []string\n\tfor _, v := range commaSeparatedEntries {\n\t\tfor _, s := range strings.Split(v, \",\") {\n\t\t\tout = append(out, strings.TrimSpace(s))\n\t\t}\n\t}\n\treturn out\n}\n"
  },
  {
    "path": "cmd/grype/cli/options/grype_test.go",
    "content": "package options\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc Test_flatten(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    []string\n\t\texpected []string\n\t}{\n\t\t{\n\t\t\tname:     \"single value\",\n\t\t\tinput:    []string{\"docker\"},\n\t\t\texpected: []string{\"docker\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"comma-separated values\",\n\t\t\tinput:    []string{\"docker,registry\"},\n\t\t\texpected: []string{\"docker\", \"registry\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"multiple entries with commas\",\n\t\t\tinput:    []string{\"docker,registry\", \"podman\"},\n\t\t\texpected: []string{\"docker\", \"registry\", \"podman\"}, // preserves order\n\t\t},\n\t\t{\n\t\t\tname:     \"whitespace trimming\",\n\t\t\tinput:    []string{\" docker , registry \"},\n\t\t\texpected: []string{\"docker\", \"registry\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"empty input\",\n\t\t\tinput:    []string{},\n\t\t\texpected: nil,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot := flatten(tt.input)\n\t\t\tassert.Equal(t, tt.expected, got)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "cmd/grype/cli/options/match.go",
    "content": "package options\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/anchore/clio\"\n\t\"github.com/anchore/grype/grype/version\"\n)\n\n// matchConfig contains all matching-related configuration options available to the user via the application config.\ntype matchConfig struct {\n\tJava       matcherConfig `yaml:\"java\" json:\"java\" mapstructure:\"java\"`                   // settings for the java matcher\n\tJVM        matcherConfig `yaml:\"jvm\" json:\"jvm\" mapstructure:\"jvm\"`                      // settings for the jvm matcher\n\tDotnet     matcherConfig `yaml:\"dotnet\" json:\"dotnet\" mapstructure:\"dotnet\"`             // settings for the dotnet matcher\n\tGolang     golangConfig  `yaml:\"golang\" json:\"golang\" mapstructure:\"golang\"`             // settings for the golang matcher\n\tJavascript matcherConfig `yaml:\"javascript\" json:\"javascript\" mapstructure:\"javascript\"` // settings for the javascript matcher\n\tPython     matcherConfig `yaml:\"python\" json:\"python\" mapstructure:\"python\"`             // settings for the python matcher\n\tRuby       matcherConfig `yaml:\"ruby\" json:\"ruby\" mapstructure:\"ruby\"`                   // settings for the ruby matcher\n\tRust       matcherConfig `yaml:\"rust\" json:\"rust\" mapstructure:\"rust\"`                   // settings for the rust matcher\n\tHex        matcherConfig `yaml:\"hex\" json:\"hex\" mapstructure:\"hex\"`                      // settings for the hex matcher (Elixir/Erlang)\n\tStock      matcherConfig `yaml:\"stock\" json:\"stock\" mapstructure:\"stock\"`                // settings for the default/stock matcher\n\tDpkg       dpkgConfig    `yaml:\"dpkg\" json:\"dpkg\" mapstructure:\"dpkg\"`                   // settings for the dpkg matcher\n\tRpm        rpmConfig     `yaml:\"rpm\" json:\"rpm\" mapstructure:\"rpm\"`                      // settings for the rpm matcher\n}\n\nvar _ interface {\n\tclio.FieldDescriber\n\tclio.PostLoader\n} = (*matchConfig)(nil)\n\ntype matcherConfig struct {\n\tUseCPEs bool `yaml:\"using-cpes\" json:\"using-cpes\" mapstructure:\"using-cpes\"` // if CPEs should be used during matching\n}\n\ntype golangConfig struct {\n\tmatcherConfig                          `yaml:\",inline\" mapstructure:\",squash\"`\n\tAlwaysUseCPEForStdlib                  bool `yaml:\"always-use-cpe-for-stdlib\" json:\"always-use-cpe-for-stdlib\" mapstructure:\"always-use-cpe-for-stdlib\"`                                                       // if CPEs should be used during matching\n\tAllowMainModulePseudoVersionComparison bool `yaml:\"allow-main-module-pseudo-version-comparison\" json:\"allow-main-module-pseudo-version-comparison\" mapstructure:\"allow-main-module-pseudo-version-comparison\"` // if pseudo versions should be compared\n}\n\n// dpkgConfig contains configuration for the dpkg matcher.\ntype dpkgConfig struct {\n\tmatcherConfig `yaml:\",inline\" mapstructure:\",squash\"`\n\t// MissingEpochStrategy controls how missing epochs in package versions are handled\n\t// during vulnerability matching.\n\t//\n\t// Valid values:\n\t//   - \"zero\" (default): Treat missing epochs as 0\n\t//   - \"auto\": Assume missing epoch matches the constraint's epoch\n\t//\n\t// The \"zero\" strategy follows dpkg specification guidance and maintains backward\n\t// compatibility with existing Grype behavior. The \"auto\" strategy reduces false\n\t// positives by recognizing that distros rarely track multiple epochs of the same\n\t// package in the same release.\n\t//\n\t// Example:\n\t//   Package version: 2.0.0 (no epoch)\n\t//   Constraint: < 1:1.5.0 (epoch 1)\n\t//\n\t//   With \"zero\": Treat package as 0:2.0.0 → MATCH (0 < 1)\n\t//   With \"auto\": Treat package as 1:2.0.0 → NO MATCH (2.0.0 > 1.5.0)\n\tMissingEpochStrategy version.MissingEpochStrategy `yaml:\"missing-epoch-strategy\" json:\"missing-epoch-strategy\" mapstructure:\"missing-epoch-strategy\"`\n\tUseCPEsForEOL        bool                         `yaml:\"use-cpes-for-eol\" json:\"use-cpes-for-eol\" mapstructure:\"use-cpes-for-eol\"` // if CPEs should be used for EOL distro packages\n}\n\n// rpmConfig contains configuration for the RPM matcher.\ntype rpmConfig struct {\n\tmatcherConfig `yaml:\",inline\" mapstructure:\",squash\"`\n\t// MissingEpochStrategy controls how missing epochs in package versions are handled\n\t// during vulnerability matching.\n\t//\n\t// Valid values:\n\t//   - \"zero\" (default): Treat missing epochs as 0\n\t//   - \"auto\": Assume missing epoch matches the constraint's epoch\n\t//\n\t// The \"zero\" strategy follows RPM specification guidance and maintains backward\n\t// compatibility with existing Grype behavior. The \"auto\" strategy reduces false\n\t// positives by recognizing that distros rarely track multiple epochs of the same\n\t// package in the same release.\n\t//\n\t// Example:\n\t//   Package version: 2.0.0 (no epoch)\n\t//   Constraint: < 1:1.5.0 (epoch 1)\n\t//\n\t//   With \"zero\": Treat package as 0:2.0.0 → MATCH (0 < 1)\n\t//   With \"auto\": Treat package as 1:2.0.0 → NO MATCH (2.0.0 > 1.5.0)\n\tMissingEpochStrategy version.MissingEpochStrategy `yaml:\"missing-epoch-strategy\" json:\"missing-epoch-strategy\" mapstructure:\"missing-epoch-strategy\"`\n\tUseCPEsForEOL        bool                         `yaml:\"use-cpes-for-eol\" json:\"use-cpes-for-eol\" mapstructure:\"use-cpes-for-eol\"` // if CPEs should be used for EOL distro packages\n}\n\nfunc defaultGolangConfig() golangConfig {\n\treturn golangConfig{\n\t\tmatcherConfig: matcherConfig{\n\t\t\tUseCPEs: false,\n\t\t},\n\t\tAlwaysUseCPEForStdlib:                  true,\n\t\tAllowMainModulePseudoVersionComparison: false,\n\t}\n}\n\nfunc defaultRpmConfig() rpmConfig {\n\treturn rpmConfig{\n\t\tmatcherConfig:        matcherConfig{UseCPEs: false},\n\t\tMissingEpochStrategy: version.MissingEpochStrategyAuto,\n\t\tUseCPEsForEOL:        false,\n\t}\n}\n\nfunc defaultDpkgConfig() dpkgConfig {\n\treturn dpkgConfig{\n\t\tmatcherConfig:        matcherConfig{UseCPEs: false},\n\t\tMissingEpochStrategy: version.MissingEpochStrategyZero,\n\t\tUseCPEsForEOL:        false,\n\t}\n}\n\nfunc defaultMatchConfig() matchConfig {\n\tuseCpe := matcherConfig{UseCPEs: true}\n\tdontUseCpe := matcherConfig{UseCPEs: false}\n\treturn matchConfig{\n\t\tJava:       dontUseCpe,\n\t\tJVM:        useCpe,\n\t\tDotnet:     dontUseCpe,\n\t\tGolang:     defaultGolangConfig(),\n\t\tJavascript: dontUseCpe,\n\t\tPython:     dontUseCpe,\n\t\tRuby:       dontUseCpe,\n\t\tRust:       dontUseCpe,\n\t\tHex:        dontUseCpe,\n\t\tStock:      useCpe,\n\t\tDpkg:       defaultDpkgConfig(),\n\t\tRpm:        defaultRpmConfig(),\n\t}\n}\n\nfunc (cfg *matchConfig) PostLoad() error {\n\tif err := cfg.Rpm.PostLoad(); err != nil {\n\t\treturn err\n\t}\n\tif err := cfg.Dpkg.PostLoad(); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\n// PostLoad validates the RPM configuration.\nfunc (cfg *rpmConfig) PostLoad() error {\n\tif cfg.MissingEpochStrategy != version.MissingEpochStrategyZero && cfg.MissingEpochStrategy != version.MissingEpochStrategyAuto {\n\t\treturn fmt.Errorf(\"invalid rpm.missing-epoch-strategy: %q (allowable: %s, %s)\",\n\t\t\tcfg.MissingEpochStrategy, version.MissingEpochStrategyZero, version.MissingEpochStrategyAuto)\n\t}\n\treturn nil\n}\n\n// PostLoad validates the dpkg configuration.\nfunc (cfg *dpkgConfig) PostLoad() error {\n\tif cfg.MissingEpochStrategy != version.MissingEpochStrategyZero && cfg.MissingEpochStrategy != version.MissingEpochStrategyAuto {\n\t\treturn fmt.Errorf(\"invalid dpkg.missing-epoch-strategy: %q (allowable: %s, %s)\",\n\t\t\tcfg.MissingEpochStrategy, version.MissingEpochStrategyZero, version.MissingEpochStrategyAuto)\n\t}\n\treturn nil\n}\n\nfunc (cfg *matchConfig) DescribeFields(descriptions clio.FieldDescriptionSet) {\n\tusingCpeDescription := `use CPE matching to find vulnerabilities`\n\tdescriptions.Add(&cfg.Java.UseCPEs, usingCpeDescription)\n\tdescriptions.Add(&cfg.Dotnet.UseCPEs, usingCpeDescription)\n\tdescriptions.Add(&cfg.Golang.UseCPEs, usingCpeDescription)\n\tdescriptions.Add(&cfg.Golang.AlwaysUseCPEForStdlib, usingCpeDescription+\" for the Go standard library\")\n\tdescriptions.Add(&cfg.Golang.AllowMainModulePseudoVersionComparison, `allow comparison between main module pseudo-versions (e.g. v0.0.0-20240413-2b432cf643...)`)\n\tdescriptions.Add(&cfg.Javascript.UseCPEs, usingCpeDescription)\n\tdescriptions.Add(&cfg.Python.UseCPEs, usingCpeDescription)\n\tdescriptions.Add(&cfg.Ruby.UseCPEs, usingCpeDescription)\n\tdescriptions.Add(&cfg.Rust.UseCPEs, usingCpeDescription)\n\tdescriptions.Add(&cfg.Hex.UseCPEs, usingCpeDescription)\n\tdescriptions.Add(&cfg.Stock.UseCPEs, usingCpeDescription)\n\tdescriptions.Add(&cfg.Dpkg.MissingEpochStrategy,\n\t\t`strategy for handling missing epochs in dpkg package versions during matching (options: zero, auto)`)\n\tdescriptions.Add(&cfg.Rpm.MissingEpochStrategy,\n\t\t`strategy for handling missing epochs in RPM package versions during matching (options: zero, auto)`)\n\n\teolCpeDescription := `use CPE matching for packages from end-of-life distributions`\n\tdescriptions.Add(&cfg.Dpkg.UseCPEsForEOL, eolCpeDescription)\n\tdescriptions.Add(&cfg.Rpm.UseCPEsForEOL, eolCpeDescription)\n}\n"
  },
  {
    "path": "cmd/grype/cli/options/match_test.go",
    "content": "package options\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/grype/grype/version\"\n)\n\nfunc TestRpmConfig_PostLoad(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\tcfg     rpmConfig\n\t\twantErr bool\n\t\terrMsg  string\n\t}{\n\t\t{\n\t\t\tname:    \"valid zero strategy\",\n\t\t\tcfg:     rpmConfig{MissingEpochStrategy: \"zero\"},\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"valid auto strategy\",\n\t\t\tcfg:     rpmConfig{MissingEpochStrategy: \"auto\"},\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"invalid strategy\",\n\t\t\tcfg:     rpmConfig{MissingEpochStrategy: \"garbage\"},\n\t\t\twantErr: true,\n\t\t\terrMsg:  `invalid rpm.missing-epoch-strategy: \"garbage\" (allowable: zero, auto)`,\n\t\t},\n\t\t{\n\t\t\tname:    \"empty strategy fails validation\",\n\t\t\tcfg:     rpmConfig{MissingEpochStrategy: \"\"},\n\t\t\twantErr: true,\n\t\t\terrMsg:  `invalid rpm.missing-epoch-strategy: \"\" (allowable: zero, auto)`,\n\t\t},\n\t\t{\n\t\t\tname:    \"case sensitive - Zero is invalid\",\n\t\t\tcfg:     rpmConfig{MissingEpochStrategy: \"Zero\"},\n\t\t\twantErr: true,\n\t\t},\n\t\t{\n\t\t\tname:    \"case sensitive - AUTO is invalid\",\n\t\t\tcfg:     rpmConfig{MissingEpochStrategy: \"AUTO\"},\n\t\t\twantErr: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := tt.cfg.PostLoad()\n\t\t\tif tt.wantErr {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\tif tt.errMsg != \"\" {\n\t\t\t\t\tassert.Contains(t, err.Error(), tt.errMsg)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDpkgConfig_PostLoad(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\tcfg     dpkgConfig\n\t\twantErr bool\n\t\terrMsg  string\n\t}{\n\t\t{\n\t\t\tname:    \"valid zero strategy\",\n\t\t\tcfg:     dpkgConfig{MissingEpochStrategy: \"zero\"},\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"valid auto strategy\",\n\t\t\tcfg:     dpkgConfig{MissingEpochStrategy: \"auto\"},\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"invalid strategy\",\n\t\t\tcfg:     dpkgConfig{MissingEpochStrategy: \"invalid\"},\n\t\t\twantErr: true,\n\t\t\terrMsg:  `invalid dpkg.missing-epoch-strategy: \"invalid\" (allowable: zero, auto)`,\n\t\t},\n\t\t{\n\t\t\tname:    \"empty strategy fails validation\",\n\t\t\tcfg:     dpkgConfig{MissingEpochStrategy: \"\"},\n\t\t\twantErr: true,\n\t\t\terrMsg:  `invalid dpkg.missing-epoch-strategy: \"\" (allowable: zero, auto)`,\n\t\t},\n\t\t{\n\t\t\tname:    \"whitespace strategy is invalid\",\n\t\t\tcfg:     dpkgConfig{MissingEpochStrategy: \"  zero  \"},\n\t\t\twantErr: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := tt.cfg.PostLoad()\n\t\t\tif tt.wantErr {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\tif tt.errMsg != \"\" {\n\t\t\t\t\tassert.Contains(t, err.Error(), tt.errMsg)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestMatchConfig_PostLoad(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\tcfg     matchConfig\n\t\twantErr bool\n\t\terrMsg  string\n\t}{\n\t\t{\n\t\t\tname: \"valid rpm and dpkg configs\",\n\t\t\tcfg: matchConfig{\n\t\t\t\tRpm:  rpmConfig{MissingEpochStrategy: \"zero\"},\n\t\t\t\tDpkg: dpkgConfig{MissingEpochStrategy: \"auto\"},\n\t\t\t},\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"invalid rpm config\",\n\t\t\tcfg: matchConfig{\n\t\t\t\tRpm:  rpmConfig{MissingEpochStrategy: \"bad\"},\n\t\t\t\tDpkg: dpkgConfig{MissingEpochStrategy: \"zero\"},\n\t\t\t},\n\t\t\twantErr: true,\n\t\t\terrMsg:  \"rpm.missing-epoch-strategy\",\n\t\t},\n\t\t{\n\t\t\tname: \"invalid dpkg config\",\n\t\t\tcfg: matchConfig{\n\t\t\t\tRpm:  rpmConfig{MissingEpochStrategy: \"zero\"},\n\t\t\t\tDpkg: dpkgConfig{MissingEpochStrategy: \"bad\"},\n\t\t\t},\n\t\t\twantErr: true,\n\t\t\terrMsg:  \"dpkg.missing-epoch-strategy\",\n\t\t},\n\t\t{\n\t\t\tname: \"both invalid\",\n\t\t\tcfg: matchConfig{\n\t\t\t\tRpm:  rpmConfig{MissingEpochStrategy: \"bad\"},\n\t\t\t\tDpkg: dpkgConfig{MissingEpochStrategy: \"bad\"},\n\t\t\t},\n\t\t\twantErr: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := tt.cfg.PostLoad()\n\t\t\tif tt.wantErr {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\tif tt.errMsg != \"\" {\n\t\t\t\t\tassert.Contains(t, err.Error(), tt.errMsg)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDefaultRpmConfig(t *testing.T) {\n\tcfg := defaultRpmConfig()\n\tassert.Equal(t, version.MissingEpochStrategyAuto, cfg.MissingEpochStrategy, \"default should be auto for backward compatibility (RPM ignores one-sided epochs)\")\n\tassert.False(t, cfg.UseCPEs, \"rpm matcher should not use CPEs by default\")\n\n\t// Ensure default is valid\n\terr := cfg.PostLoad()\n\trequire.NoError(t, err, \"default config should be valid\")\n}\n\nfunc TestDefaultDpkgConfig(t *testing.T) {\n\tcfg := defaultDpkgConfig()\n\tassert.Equal(t, version.MissingEpochStrategyZero, cfg.MissingEpochStrategy, \"default should be zero for backward compatibility\")\n\tassert.False(t, cfg.UseCPEs, \"dpkg matcher should not use CPEs by default\")\n\n\t// Ensure default is valid\n\terr := cfg.PostLoad()\n\trequire.NoError(t, err, \"default config should be valid\")\n}\n\nfunc TestDefaultMatchConfig(t *testing.T) {\n\tcfg := defaultMatchConfig()\n\n\t// Verify RPM defaults (auto preserves legacy behavior where one-sided epochs are ignored)\n\tassert.Equal(t, version.MissingEpochStrategyAuto, cfg.Rpm.MissingEpochStrategy)\n\tassert.False(t, cfg.Rpm.UseCPEs)\n\n\t// Verify dpkg defaults (zero preserves legacy behavior where missing epoch = 0)\n\tassert.Equal(t, version.MissingEpochStrategyZero, cfg.Dpkg.MissingEpochStrategy)\n\tassert.False(t, cfg.Dpkg.UseCPEs)\n\n\t// Ensure the entire default config is valid\n\terr := cfg.PostLoad()\n\trequire.NoError(t, err, \"default match config should be valid\")\n}\n"
  },
  {
    "path": "cmd/grype/cli/options/registry.go",
    "content": "package options\n\nimport (\n\t\"os\"\n\n\t\"github.com/anchore/clio\"\n\t\"github.com/anchore/stereoscope/pkg/image\"\n)\n\ntype RegistryCredentials struct {\n\tAuthority string `yaml:\"authority\" json:\"authority\" mapstructure:\"authority\"`\n\t// IMPORTANT: do not show the username, password, or token in any output (sensitive information)\n\tUsername secret `yaml:\"username\" json:\"username\" mapstructure:\"username\"`\n\tPassword secret `yaml:\"password\" json:\"password\" mapstructure:\"password\"`\n\tToken    secret `yaml:\"token\" json:\"token\" mapstructure:\"token\"`\n\n\tTLSCert string `yaml:\"tls-cert,omitempty\" json:\"tls-cert,omitempty\" mapstructure:\"tls-cert\"`\n\tTLSKey  string `yaml:\"tls-key,omitempty\" json:\"tls-key,omitempty\" mapstructure:\"tls-key\"`\n}\n\ntype registry struct {\n\tInsecureSkipTLSVerify bool                  `yaml:\"insecure-skip-tls-verify\" json:\"insecure-skip-tls-verify\" mapstructure:\"insecure-skip-tls-verify\"`\n\tInsecureUseHTTP       bool                  `yaml:\"insecure-use-http\" json:\"insecure-use-http\" mapstructure:\"insecure-use-http\"`\n\tAuth                  []RegistryCredentials `yaml:\"auth\" json:\"auth,omitempty\" mapstructure:\"auth\"`\n\tCACert                string                `yaml:\"ca-cert\" json:\"ca-cert\" mapstructure:\"ca-cert\"`\n}\n\nvar _ interface {\n\tclio.PostLoader\n\tclio.FieldDescriber\n} = (*registry)(nil)\n\nfunc (cfg *registry) PostLoad() error {\n\t// there may be additional credentials provided by env var that should be appended to the set of credentials\n\tauthority, username, password, token, tlsCert, tlsKey :=\n\t\tos.Getenv(\"GRYPE_REGISTRY_AUTH_AUTHORITY\"),\n\t\tos.Getenv(\"GRYPE_REGISTRY_AUTH_USERNAME\"),\n\t\tos.Getenv(\"GRYPE_REGISTRY_AUTH_PASSWORD\"),\n\t\tos.Getenv(\"GRYPE_REGISTRY_AUTH_TOKEN\"),\n\t\tos.Getenv(\"GRYPE_REGISTRY_AUTH_TLS_CERT\"),\n\t\tos.Getenv(\"GRYPE_REGISTRY_AUTH_TLS_KEY\")\n\n\tif hasNonEmptyCredentials(username, password, token, tlsCert, tlsKey) {\n\t\t// note: we prepend the credentials such that the environment variables take precedence over on-disk configuration.\n\t\tcfg.Auth = append([]RegistryCredentials{\n\t\t\t{\n\t\t\t\tAuthority: authority,\n\t\t\t\tUsername:  secret(username),\n\t\t\t\tPassword:  secret(password),\n\t\t\t\tToken:     secret(token),\n\t\t\t\tTLSCert:   tlsCert,\n\t\t\t\tTLSKey:    tlsKey,\n\t\t\t},\n\t\t}, cfg.Auth...)\n\t}\n\treturn nil\n}\n\nfunc (cfg *registry) DescribeFields(descriptions clio.FieldDescriptionSet) {\n\tdescriptions.Add(&cfg.InsecureSkipTLSVerify, \"skip TLS verification when communicating with the registry\")\n\tdescriptions.Add(&cfg.InsecureUseHTTP, \"use http instead of https when connecting to the registry\")\n\tdescriptions.Add(&cfg.CACert, \"filepath to a CA certificate (or directory containing *.crt, *.cert, *.pem) used to generate the client certificate\")\n\tdescriptions.Add(&cfg.Auth, `Authentication credentials for specific registries. Each entry describes authentication for a specific authority:\n-\tauthority: the registry authority URL the URL to the registry (e.g. \"docker.io\", \"localhost:5000\", etc.) (env: SYFT_REGISTRY_AUTH_AUTHORITY)\n\tusername: a username if using basic credentials (env: SYFT_REGISTRY_AUTH_USERNAME)\n\tpassword: a corresponding password (env: SYFT_REGISTRY_AUTH_PASSWORD)\n\ttoken: a token if using token-based authentication, mutually exclusive with username/password (env: SYFT_REGISTRY_AUTH_TOKEN)\n\ttls-cert: filepath to the client certificate used for TLS authentication to the registry (env: SYFT_REGISTRY_AUTH_TLS_CERT)\n\ttls-key: filepath to the client key used for TLS authentication to the registry (env: SYFT_REGISTRY_AUTH_TLS_KEY)\n`)\n}\n\nfunc hasNonEmptyCredentials(username, password, token, tlsCert, tlsKey string) bool {\n\thasUserPass := username != \"\" && password != \"\"\n\thasToken := token != \"\"\n\thasTLSMaterial := tlsCert != \"\" && tlsKey != \"\"\n\treturn hasUserPass || hasToken || hasTLSMaterial\n}\n\nfunc (cfg *registry) ToOptions() *image.RegistryOptions {\n\tvar auth = make([]image.RegistryCredentials, len(cfg.Auth))\n\tfor i, a := range cfg.Auth {\n\t\tauth[i] = image.RegistryCredentials{\n\t\t\tAuthority:  a.Authority,\n\t\t\tUsername:   string(a.Username),\n\t\t\tPassword:   string(a.Password),\n\t\t\tToken:      string(a.Token),\n\t\t\tClientCert: a.TLSCert,\n\t\t\tClientKey:  a.TLSKey,\n\t\t}\n\t}\n\n\treturn &image.RegistryOptions{\n\t\tInsecureSkipTLSVerify: cfg.InsecureSkipTLSVerify,\n\t\tInsecureUseHTTP:       cfg.InsecureUseHTTP,\n\t\tCredentials:           auth,\n\t\tCAFileOrDir:           cfg.CACert,\n\t}\n}\n"
  },
  {
    "path": "cmd/grype/cli/options/registry_test.go",
    "content": "package options\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\n\t\"github.com/anchore/stereoscope/pkg/image\"\n)\n\nfunc TestHasNonEmptyCredentials(t *testing.T) {\n\ttests := []struct {\n\t\tusername, password, token, cert, key string\n\t\texpected                             bool\n\t}{\n\n\t\t{\n\t\t\t\"\", \"\", \"\", \"\", \"\",\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"user\", \"\", \"\", \"\", \"\",\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"\", \"pass\", \"\", \"\", \"\",\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"\", \"pass\", \"tok\", \"\", \"\",\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"user\", \"\", \"tok\", \"\", \"\",\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"\", \"\", \"tok\", \"\", \"\",\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"user\", \"pass\", \"tok\", \"\", \"\",\n\t\t\ttrue,\n\t\t},\n\n\t\t{\n\t\t\t\"user\", \"pass\", \"\", \"\", \"\",\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"\", \"\", \"\", \"cert\", \"key\",\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"\", \"\", \"\", \"cert\", \"\",\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"\", \"\", \"\", \"\", \"key\",\n\t\t\tfalse,\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(fmt.Sprintf(\"%+v\", test), func(t *testing.T) {\n\t\t\tassert.Equal(t, test.expected, hasNonEmptyCredentials(test.username, test.password, test.token, test.cert, test.key))\n\t\t})\n\t}\n}\n\nfunc Test_registry_ToOptions(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    registry\n\t\texpected image.RegistryOptions\n\t}{\n\t\t{\n\t\t\tname:  \"no registry options\",\n\t\t\tinput: registry{},\n\t\t\texpected: image.RegistryOptions{\n\t\t\t\tCredentials: []image.RegistryCredentials{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"set InsecureSkipTLSVerify\",\n\t\t\tinput: registry{\n\t\t\t\tInsecureSkipTLSVerify: true,\n\t\t\t},\n\t\t\texpected: image.RegistryOptions{\n\t\t\t\tInsecureSkipTLSVerify: true,\n\t\t\t\tCredentials:           []image.RegistryCredentials{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"set InsecureUseHTTP\",\n\t\t\tinput: registry{\n\t\t\t\tInsecureUseHTTP: true,\n\t\t\t},\n\t\t\texpected: image.RegistryOptions{\n\t\t\t\tInsecureUseHTTP: true,\n\t\t\t\tCredentials:     []image.RegistryCredentials{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"set all bool options\",\n\t\t\tinput: registry{\n\t\t\t\tInsecureSkipTLSVerify: true,\n\t\t\t\tInsecureUseHTTP:       true,\n\t\t\t},\n\t\t\texpected: image.RegistryOptions{\n\t\t\t\tInsecureSkipTLSVerify: true,\n\t\t\t\tInsecureUseHTTP:       true,\n\t\t\t\tCredentials:           []image.RegistryCredentials{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"provide all tls configuration\",\n\t\t\tinput: registry{\n\t\t\t\tCACert:                \"ca.crt\",\n\t\t\t\tInsecureSkipTLSVerify: true,\n\t\t\t\tAuth: []RegistryCredentials{\n\t\t\t\t\t{\n\t\t\t\t\t\tTLSCert: \"client.crt\",\n\t\t\t\t\t\tTLSKey:  \"client.key\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: image.RegistryOptions{\n\t\t\t\tCAFileOrDir:           \"ca.crt\",\n\t\t\t\tInsecureSkipTLSVerify: true,\n\t\t\t\tCredentials: []image.RegistryCredentials{\n\t\t\t\t\t{\n\t\t\t\t\t\tClientCert: \"client.crt\",\n\t\t\t\t\t\tClientKey:  \"client.key\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tassert.Equal(t, &test.expected, test.input.ToOptions())\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "cmd/grype/cli/options/search.go",
    "content": "package options\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/anchore/clio\"\n\t\"github.com/anchore/syft/syft/cataloging\"\n\t\"github.com/anchore/syft/syft/source\"\n)\n\ntype search struct {\n\tScope                    string `yaml:\"scope\" json:\"scope\" mapstructure:\"scope\"`\n\tIncludeUnindexedArchives bool   `yaml:\"unindexed-archives\" json:\"unindexed-archives\" mapstructure:\"unindexed-archives\"`\n\tIncludeIndexedArchives   bool   `yaml:\"indexed-archives\" json:\"indexed-archives\" mapstructure:\"indexed-archives\"`\n}\n\nvar _ interface {\n\tclio.PostLoader\n\tclio.FieldDescriber\n} = (*search)(nil)\n\nfunc defaultSearch(scope source.Scope) search {\n\tc := cataloging.DefaultArchiveSearchConfig()\n\treturn search{\n\t\tScope:                    scope.String(),\n\t\tIncludeUnindexedArchives: c.IncludeUnindexedArchives,\n\t\tIncludeIndexedArchives:   c.IncludeIndexedArchives,\n\t}\n}\n\nfunc (cfg *search) PostLoad() error {\n\tscopeOption := cfg.GetScope()\n\tif scopeOption == source.UnknownScope {\n\t\treturn fmt.Errorf(\"bad scope value %q\", cfg.Scope)\n\t}\n\treturn nil\n}\n\nfunc (cfg *search) DescribeFields(descriptions clio.FieldDescriptionSet) {\n\tdescriptions.Add(&cfg.IncludeIndexedArchives, `search within archives that do contain a file index to search against (zip)\nnote: for now this only applies to the java package cataloger`)\n\tdescriptions.Add(&cfg.IncludeUnindexedArchives, `search within archives that do not contain a file index to search against (tar, tar.gz, tar.bz2, etc)\nnote: enabling this may result in a performance impact since all discovered compressed tars will be decompressed\nnote: for now this only applies to the java package cataloger`)\n}\n\nfunc (cfg search) GetScope() source.Scope {\n\treturn source.ParseScope(cfg.Scope)\n}\n"
  },
  {
    "path": "cmd/grype/cli/options/secret.go",
    "content": "package options\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/anchore/clio\"\n\t\"github.com/anchore/grype/internal/redact\"\n)\n\ntype secret string\n\nvar _ interface {\n\tfmt.Stringer\n\tclio.PostLoader\n} = (*secret)(nil)\n\n// PostLoad needs to use a pointer receiver, even if it's not modifying the value\nfunc (r *secret) PostLoad() error {\n\tredact.Add(string(*r))\n\treturn nil\n}\n\nfunc (r secret) String() string {\n\tif r == \"\" {\n\t\treturn \"\"\n\t}\n\t// match the redactor's behavior, replacing with 7 asterisks\n\treturn \"*******\"\n}\n\nfunc (r secret) MarshalText() ([]byte, error) {\n\treturn []byte(r.String()), nil\n}\n"
  },
  {
    "path": "cmd/grype/cli/options/sort_by.go",
    "content": "package options\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/scylladb/go-set/strset\"\n\n\t\"github.com/anchore/clio\"\n\t\"github.com/anchore/fangs\"\n\t\"github.com/anchore/grype/grype/presenter/models\"\n)\n\nvar _ interface {\n\tfangs.FlagAdder\n\tfangs.PostLoader\n} = (*SortBy)(nil)\n\ntype SortBy struct {\n\tCriteria         string   `yaml:\"sort-by\" json:\"sort-by\" mapstructure:\"sort-by\"`\n\tAllowableOptions []string `yaml:\"-\" json:\"-\" mapstructure:\"-\"`\n}\n\nfunc defaultSortBy() SortBy {\n\tvar strategies []string\n\tfor _, s := range models.SortStrategies() {\n\t\tstrategies = append(strategies, strings.ToLower(s.String()))\n\t}\n\treturn SortBy{\n\t\tCriteria:         models.DefaultSortStrategy.String(),\n\t\tAllowableOptions: strategies,\n\t}\n}\n\nfunc (o *SortBy) AddFlags(flags clio.FlagSet) {\n\tflags.StringVarP(&o.Criteria,\n\t\t\"sort-by\", \"\",\n\t\tfmt.Sprintf(\"sort the match results with the given strategy, options=%v\", o.AllowableOptions),\n\t)\n}\n\nfunc (o *SortBy) PostLoad() error {\n\tif !strset.New(o.AllowableOptions...).Has(strings.ToLower(o.Criteria)) {\n\t\treturn fmt.Errorf(\"invalid sort-by criteria: %q (allowable: %s)\", o.Criteria, strings.Join(o.AllowableOptions, \", \"))\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "cmd/grype/cli/ui/__snapshots__/handle_database_diff_started_test.snap",
    "content": "\n[TestHandler_handleDatabaseDiffStarted/DB_diff_started - 1]\n ⠋ Comparing Vulnerability DBs     ━━━━━━━━━━━━━━━━━━━━  [current]  \n---\n\n[TestHandler_handleDatabaseDiffStarted/DB_diff_complete - 1]\n ✔ Compared Vulnerability DBs      [20 differences found]  \n---\n"
  },
  {
    "path": "cmd/grype/cli/ui/__snapshots__/handle_update_vulnerability_database_test.snap",
    "content": "\n[TestHandler_handleUpdateVulnerabilityDatabase/downloading_DB - 1]\n ⠋ Vulnerability DB                ━━━━━━━━━━━━━━━━━━━━  [current]  \n---\n\n[TestHandler_handleUpdateVulnerabilityDatabase/DB_download_complete - 1]\n ✔ Vulnerability DB                [current]  \n---\n"
  },
  {
    "path": "cmd/grype/cli/ui/__snapshots__/handle_vulnerability_scanning_started_test.snap",
    "content": "\n[TestHandler_handleVulnerabilityScanningStarted/vulnerability_scanning_in_progress/task_line - 1]\n ⠋ Scanning for vulnerabilities    [36 vulnerability matches]  \n---\n\n[TestHandler_handleVulnerabilityScanningStarted/vulnerability_scanning_in_progress/tree - 1]\n   ├── by severity: 1 critical, 2 high, 3 medium, 4 low, 5 negligible (6 unknown)\n   └── by status:   30 fixed, 10 not-fixed, 4 ignored (2 dropped)\n\n---\n\n[TestHandler_handleVulnerabilityScanningStarted/vulnerability_scanning_complete/task_line - 1]\n ✔ Scanned for vulnerabilities     [40 vulnerability matches]  \n---\n\n[TestHandler_handleVulnerabilityScanningStarted/vulnerability_scanning_complete/tree - 1]\n   ├── by severity: 1 critical, 2 high, 3 medium, 4 low, 5 negligible (6 unknown)\n   └── by status:   35 fixed, 10 not-fixed, 5 ignored (3 dropped)\n\n---\n"
  },
  {
    "path": "cmd/grype/cli/ui/handle_database_diff_started.go",
    "content": "package ui\n\nimport (\n\t\"fmt\"\n\n\ttea \"github.com/charmbracelet/bubbletea\"\n\t\"github.com/wagoodman/go-partybus\"\n\t\"github.com/wagoodman/go-progress\"\n\n\t\"github.com/anchore/bubbly/bubbles/taskprogress\"\n\t\"github.com/anchore/grype/grype/event/monitor\"\n\t\"github.com/anchore/grype/grype/event/parsers\"\n\t\"github.com/anchore/grype/internal/log\"\n)\n\ntype dbDiffProgressStager struct {\n\tmonitor *monitor.DBDiff\n}\n\nfunc (p dbDiffProgressStager) Stage() string {\n\tif progress.IsErrCompleted(p.monitor.StageProgress.Error()) {\n\t\treturn fmt.Sprintf(\"%d differences found\", p.monitor.DifferencesDiscovered.Current())\n\t}\n\treturn p.monitor.Stager.Stage()\n}\n\nfunc (p dbDiffProgressStager) Current() int64 {\n\treturn p.monitor.StageProgress.Current()\n}\n\nfunc (p dbDiffProgressStager) Error() error {\n\treturn p.monitor.StageProgress.Error()\n}\n\nfunc (p dbDiffProgressStager) Size() int64 {\n\treturn p.monitor.StageProgress.Size()\n}\n\nfunc (m *Handler) handleDatabaseDiffStarted(e partybus.Event) ([]tea.Model, tea.Cmd) {\n\tmon, err := parsers.ParseDatabaseDiffingStarted(e)\n\tif err != nil {\n\t\tlog.WithFields(\"error\", err).Warn(\"unable to parse event\")\n\t\treturn nil, nil\n\t}\n\n\ttsk := m.newTaskProgress(\n\t\ttaskprogress.Title{\n\t\t\tDefault: \"Compare Vulnerability DBs\",\n\t\t\tRunning: \"Comparing Vulnerability DBs\",\n\t\t\tSuccess: \"Compared Vulnerability DBs\",\n\t\t},\n\t\ttaskprogress.WithStagedProgressable(dbDiffProgressStager{monitor: mon}),\n\t)\n\n\ttsk.HideStageOnSuccess = false\n\n\treturn []tea.Model{tsk}, nil\n}\n"
  },
  {
    "path": "cmd/grype/cli/ui/handle_database_diff_started_test.go",
    "content": "package ui\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\ttea \"github.com/charmbracelet/bubbletea\"\n\t\"github.com/gkampitakis/go-snaps/snaps\"\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/wagoodman/go-partybus\"\n\t\"github.com/wagoodman/go-progress\"\n\n\t\"github.com/anchore/bubbly/bubbles/taskprogress\"\n\t\"github.com/anchore/grype/grype/event\"\n\t\"github.com/anchore/grype/grype/event/monitor\"\n)\n\nfunc TestHandler_handleDatabaseDiffStarted(t *testing.T) {\n\n\ttests := []struct {\n\t\tname       string\n\t\teventFn    func(*testing.T) partybus.Event\n\t\titerations int\n\t}{\n\t\t{\n\t\t\tname: \"DB diff started\",\n\t\t\teventFn: func(t *testing.T) partybus.Event {\n\t\t\t\tprog := &progress.Manual{}\n\t\t\t\tprog.SetTotal(100)\n\t\t\t\tprog.Set(50)\n\n\t\t\t\tdiffs := &progress.Manual{}\n\t\t\t\tdiffs.Set(20)\n\n\t\t\t\tmon := monitor.DBDiff{\n\t\t\t\t\tStager:                &progress.Stage{Current: \"current\"},\n\t\t\t\t\tStageProgress:         prog,\n\t\t\t\t\tDifferencesDiscovered: diffs,\n\t\t\t\t}\n\n\t\t\t\treturn partybus.Event{\n\t\t\t\t\tType:  event.DatabaseDiffingStarted,\n\t\t\t\t\tValue: mon,\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"DB diff complete\",\n\t\t\teventFn: func(t *testing.T) partybus.Event {\n\t\t\t\tprog := &progress.Manual{}\n\t\t\t\tprog.SetTotal(100)\n\t\t\t\tprog.Set(100)\n\t\t\t\tprog.SetCompleted()\n\n\t\t\t\tdiffs := &progress.Manual{}\n\t\t\t\tdiffs.Set(20)\n\n\t\t\t\tmon := monitor.DBDiff{\n\t\t\t\t\tStager:                &progress.Stage{Current: \"current\"},\n\t\t\t\t\tStageProgress:         prog,\n\t\t\t\t\tDifferencesDiscovered: diffs,\n\t\t\t\t}\n\n\t\t\t\treturn partybus.Event{\n\t\t\t\t\tType:  event.DatabaseDiffingStarted,\n\t\t\t\t\tValue: mon,\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\te := tt.eventFn(t)\n\t\t\thandler := New(DefaultHandlerConfig())\n\t\t\thandler.WindowSize = tea.WindowSizeMsg{\n\t\t\t\tWidth:  100,\n\t\t\t\tHeight: 80,\n\t\t\t}\n\n\t\t\tmodels, _ := handler.Handle(e)\n\t\t\trequire.Len(t, models, 1)\n\t\t\tmodel := models[0]\n\n\t\t\ttsk, ok := model.(taskprogress.Model)\n\t\t\trequire.True(t, ok)\n\n\t\t\tgot := runModel(t, tsk, tt.iterations, taskprogress.TickMsg{\n\t\t\t\tTime:     time.Now(),\n\t\t\t\tSequence: tsk.Sequence(),\n\t\t\t\tID:       tsk.ID(),\n\t\t\t})\n\t\t\tt.Log(got)\n\t\t\tsnaps.MatchSnapshot(t, got)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "cmd/grype/cli/ui/handle_update_vulnerability_database.go",
    "content": "package ui\n\nimport (\n\t\"fmt\"\n\n\ttea \"github.com/charmbracelet/bubbletea\"\n\t\"github.com/dustin/go-humanize\"\n\t\"github.com/wagoodman/go-partybus\"\n\t\"github.com/wagoodman/go-progress\"\n\n\t\"github.com/anchore/bubbly/bubbles/taskprogress\"\n\t\"github.com/anchore/grype/grype/event/parsers\"\n\t\"github.com/anchore/grype/internal/log\"\n)\n\ntype dbDownloadProgressStager struct {\n\tprog progress.StagedProgressable\n}\n\nfunc (s dbDownloadProgressStager) Stage() string {\n\tstage := s.prog.Stage()\n\tif stage == \"downloading\" {\n\t\t// note: since validation is baked into the download progress there is no visibility into this stage.\n\t\t// for that reason we report \"validating\" on the last byte being downloaded (which tends to be the longest\n\t\t// since go-downloader is doing this work).\n\t\tif s.prog.Current() >= s.prog.Size()-1 {\n\t\t\treturn \"validating\"\n\t\t}\n\t\t// show intermediate progress of the download\n\t\treturn fmt.Sprintf(\"%s / %s\",\n\t\t\thumanize.Bytes(safeConvertInt64ToUint64(s.prog.Current())),\n\t\t\thumanize.Bytes(safeConvertInt64ToUint64(s.prog.Size())),\n\t\t)\n\t}\n\treturn stage\n}\n\nfunc (m *Handler) handleUpdateVulnerabilityDatabase(e partybus.Event) ([]tea.Model, tea.Cmd) {\n\tprog, err := parsers.ParseUpdateVulnerabilityDatabase(e)\n\tif err != nil {\n\t\tlog.WithFields(\"error\", err).Warn(\"unable to parse event\")\n\t\treturn nil, nil\n\t}\n\n\ttsk := m.newTaskProgress(\n\t\ttaskprogress.Title{\n\t\t\tDefault: \"Vulnerability DB\",\n\t\t},\n\t\ttaskprogress.WithStagedProgressable(prog), // ignore the static stage provided by the event\n\t\ttaskprogress.WithStager(dbDownloadProgressStager{prog: prog}),\n\t)\n\n\ttsk.HideStageOnSuccess = false\n\n\treturn []tea.Model{tsk}, nil\n}\n\nfunc safeConvertInt64ToUint64(i int64) uint64 {\n\tif i < 0 {\n\t\treturn 0\n\t}\n\treturn uint64(i)\n}\n"
  },
  {
    "path": "cmd/grype/cli/ui/handle_update_vulnerability_database_test.go",
    "content": "package ui\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\ttea \"github.com/charmbracelet/bubbletea\"\n\t\"github.com/gkampitakis/go-snaps/snaps\"\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/wagoodman/go-partybus\"\n\t\"github.com/wagoodman/go-progress\"\n\n\t\"github.com/anchore/bubbly/bubbles/taskprogress\"\n\t\"github.com/anchore/grype/grype/event\"\n)\n\nfunc TestHandler_handleUpdateVulnerabilityDatabase(t *testing.T) {\n\n\ttests := []struct {\n\t\tname       string\n\t\teventFn    func(*testing.T) partybus.Event\n\t\titerations int\n\t}{\n\t\t{\n\t\t\tname: \"downloading DB\",\n\t\t\teventFn: func(t *testing.T) partybus.Event {\n\t\t\t\tprog := &progress.Manual{}\n\t\t\t\tprog.SetTotal(100)\n\t\t\t\tprog.Set(50)\n\n\t\t\t\tmon := struct {\n\t\t\t\t\tprogress.Progressable\n\t\t\t\t\tprogress.Stager\n\t\t\t\t}{\n\t\t\t\t\tProgressable: prog,\n\t\t\t\t\tStager: &progress.Stage{\n\t\t\t\t\t\tCurrent: \"current\",\n\t\t\t\t\t},\n\t\t\t\t}\n\n\t\t\t\treturn partybus.Event{\n\t\t\t\t\tType:  event.UpdateVulnerabilityDatabase,\n\t\t\t\t\tValue: mon,\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"DB download complete\",\n\t\t\teventFn: func(t *testing.T) partybus.Event {\n\t\t\t\tprog := &progress.Manual{}\n\t\t\t\tprog.SetTotal(100)\n\t\t\t\tprog.Set(100)\n\t\t\t\tprog.SetCompleted()\n\n\t\t\t\tmon := struct {\n\t\t\t\t\tprogress.Progressable\n\t\t\t\t\tprogress.Stager\n\t\t\t\t}{\n\t\t\t\t\tProgressable: prog,\n\t\t\t\t\tStager: &progress.Stage{\n\t\t\t\t\t\tCurrent: \"current\",\n\t\t\t\t\t},\n\t\t\t\t}\n\n\t\t\t\treturn partybus.Event{\n\t\t\t\t\tType:  event.UpdateVulnerabilityDatabase,\n\t\t\t\t\tValue: mon,\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\te := tt.eventFn(t)\n\t\t\thandler := New(DefaultHandlerConfig())\n\t\t\thandler.WindowSize = tea.WindowSizeMsg{\n\t\t\t\tWidth:  100,\n\t\t\t\tHeight: 80,\n\t\t\t}\n\n\t\t\tmodels, _ := handler.Handle(e)\n\t\t\trequire.Len(t, models, 1)\n\t\t\tmodel := models[0]\n\n\t\t\ttsk, ok := model.(taskprogress.Model)\n\t\t\trequire.True(t, ok)\n\n\t\t\tgot := runModel(t, tsk, tt.iterations, taskprogress.TickMsg{\n\t\t\t\tTime:     time.Now(),\n\t\t\t\tSequence: tsk.Sequence(),\n\t\t\t\tID:       tsk.ID(),\n\t\t\t})\n\t\t\tt.Log(got)\n\t\t\tsnaps.MatchSnapshot(t, got)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "cmd/grype/cli/ui/handle_vulnerability_scanning_started.go",
    "content": "package ui\n\nimport (\n\t\"fmt\"\n\t\"sort\"\n\t\"strings\"\n\t\"time\"\n\n\ttea \"github.com/charmbracelet/bubbletea\"\n\t\"github.com/charmbracelet/lipgloss\"\n\t\"github.com/wagoodman/go-partybus\"\n\t\"github.com/wagoodman/go-progress\"\n\n\t\"github.com/anchore/bubbly/bubbles/taskprogress\"\n\t\"github.com/anchore/grype/grype/event/monitor\"\n\t\"github.com/anchore/grype/grype/event/parsers\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n\t\"github.com/anchore/grype/internal/log\"\n)\n\nconst (\n\tbranch = \"├──\"\n\tend    = \"└──\"\n)\n\nvar _ progress.StagedProgressable = (*vulnerabilityScanningAdapter)(nil)\n\ntype vulnerabilityProgressTree struct {\n\tmon        *monitor.Matching\n\twindowSize tea.WindowSizeMsg\n\n\tcountBySeverity map[vulnerability.Severity]int64\n\tunknownCount    int64\n\tfixedCount      int64\n\tignoredCount    int64\n\tdroppedCount    int64\n\ttotalCount      int64\n\tseverities      []vulnerability.Severity\n\n\tid       uint32\n\tsequence int\n\n\tupdateDuration time.Duration\n\ttextStyle      lipgloss.Style\n}\n\nfunc newVulnerabilityProgressTree(monitor *monitor.Matching, textStyle lipgloss.Style) vulnerabilityProgressTree {\n\tallSeverities := vulnerability.AllSeverities()\n\tsort.Sort(sort.Reverse(vulnerability.Severities(allSeverities)))\n\n\treturn vulnerabilityProgressTree{\n\t\tmon:             monitor,\n\t\tcountBySeverity: make(map[vulnerability.Severity]int64),\n\t\tseverities:      allSeverities,\n\t\ttextStyle:       textStyle,\n\t}\n}\n\n// vulnerabilityProgressTreeTickMsg indicates that the timer has ticked and we should render a frame.\ntype vulnerabilityProgressTreeTickMsg struct {\n\tTime     time.Time\n\tSequence int\n\tID       uint32\n}\n\ntype vulnerabilityScanningAdapter struct {\n\tmon *monitor.Matching\n}\n\nfunc (p vulnerabilityScanningAdapter) Current() int64 {\n\treturn p.mon.PackagesProcessed.Current()\n}\n\nfunc (p vulnerabilityScanningAdapter) Error() error {\n\treturn p.mon.MatchesDiscovered.Error()\n}\n\nfunc (p vulnerabilityScanningAdapter) Size() int64 {\n\treturn p.mon.PackagesProcessed.Size()\n}\n\nfunc (p vulnerabilityScanningAdapter) Stage() string {\n\treturn fmt.Sprintf(\"%d vulnerability matches\", p.mon.MatchesDiscovered.Current()-p.mon.Ignored.Current())\n}\n\nfunc (m *Handler) handleVulnerabilityScanningStarted(e partybus.Event) ([]tea.Model, tea.Cmd) {\n\tmon, err := parsers.ParseVulnerabilityScanningStarted(e)\n\tif err != nil {\n\t\tlog.WithFields(\"error\", err).Warn(\"unable to parse event\")\n\t\treturn nil, nil\n\t}\n\n\ttsk := m.newTaskProgress(\n\t\ttaskprogress.Title{\n\t\t\tDefault: \"Scan for vulnerabilities\",\n\t\t\tRunning: \"Scanning for vulnerabilities\",\n\t\t\tSuccess: \"Scanned for vulnerabilities\",\n\t\t},\n\t\ttaskprogress.WithStagedProgressable(vulnerabilityScanningAdapter{mon: mon}),\n\t)\n\n\ttsk.HideStageOnSuccess = false\n\n\ttextStyle := tsk.HintStyle\n\n\treturn []tea.Model{\n\t\ttsk,\n\t\tnewVulnerabilityProgressTree(mon, textStyle),\n\t}, nil\n}\n\nfunc (l vulnerabilityProgressTree) Init() tea.Cmd {\n\t// this is the periodic update of state information\n\treturn func() tea.Msg {\n\t\treturn vulnerabilityProgressTreeTickMsg{\n\t\t\t// The time at which the tick occurred.\n\t\t\tTime: time.Now(),\n\n\t\t\t// The ID of the log frame that this message belongs to. This can be\n\t\t\t// helpful when routing messages, however bear in mind that log frames\n\t\t\t// will ignore messages that don't contain ID by default.\n\t\t\tID: l.id,\n\n\t\t\tSequence: l.sequence,\n\t\t}\n\t}\n}\n\nfunc (l vulnerabilityProgressTree) Update(msg tea.Msg) (tea.Model, tea.Cmd) {\n\tswitch msg := msg.(type) {\n\tcase tea.WindowSizeMsg:\n\t\tl.windowSize = msg\n\t\treturn l, nil\n\n\tcase vulnerabilityProgressTreeTickMsg:\n\t\t// update the model\n\t\tl.totalCount = l.mon.MatchesDiscovered.Current()\n\t\tl.fixedCount = l.mon.Fixed.Current()\n\t\tl.ignoredCount = l.mon.Ignored.Current()\n\t\tl.droppedCount = l.mon.Dropped.Current()\n\t\tl.unknownCount = l.mon.BySeverity[vulnerability.UnknownSeverity].Current()\n\t\tfor _, sev := range l.severities {\n\t\t\tl.countBySeverity[sev] = l.mon.BySeverity[sev].Current()\n\t\t}\n\n\t\t// kick off the next tick\n\t\ttickCmd := l.handleTick(msg)\n\n\t\treturn l, tickCmd\n\t}\n\n\treturn l, nil\n}\n\nfunc (l vulnerabilityProgressTree) View() string {\n\tsb := strings.Builder{}\n\n\tfor idx, sev := range l.severities {\n\t\tcount := l.countBySeverity[sev]\n\t\tfmt.Fprintf(&sb, \"%d %s\", count, sev)\n\t\tif idx < len(l.severities)-1 {\n\t\t\tsb.WriteString(\", \")\n\t\t}\n\t}\n\tif l.unknownCount > 0 {\n\t\tunknownStr := fmt.Sprintf(\" (%d unknown)\", l.unknownCount)\n\t\tsb.WriteString(unknownStr)\n\t}\n\n\tstatus := sb.String()\n\tsb.Reset()\n\n\tsevStr := l.textStyle.Render(fmt.Sprintf(\"   %s by severity: %s\", branch, status))\n\n\tsb.WriteString(sevStr)\n\n\tdropped := \"\"\n\tif l.droppedCount > 0 {\n\t\tdropped = fmt.Sprintf(\"(%d dropped)\", l.droppedCount)\n\t}\n\n\tfixedStr := l.textStyle.Render(\n\t\tfmt.Sprintf(\"   %s by status:   %d fixed, %d not-fixed, %d ignored %s\",\n\t\t\tend, l.fixedCount, l.totalCount-l.fixedCount, l.ignoredCount, dropped,\n\t\t),\n\t)\n\tsb.WriteString(\"\\n\" + fixedStr)\n\tsb.WriteString(\"\\n\")\n\n\treturn sb.String()\n}\n\nfunc (l vulnerabilityProgressTree) queueNextTick() tea.Cmd {\n\treturn tea.Tick(l.updateDuration, func(t time.Time) tea.Msg {\n\t\treturn vulnerabilityProgressTreeTickMsg{\n\t\t\tTime:     t,\n\t\t\tID:       l.id,\n\t\t\tSequence: l.sequence,\n\t\t}\n\t})\n}\n\nfunc (l *vulnerabilityProgressTree) handleTick(msg vulnerabilityProgressTreeTickMsg) tea.Cmd {\n\t// If an ID is set, and the ID doesn't belong to this log frame, reject the message.\n\tif msg.ID > 0 && msg.ID != l.id {\n\t\treturn nil\n\t}\n\n\t// If a sequence is set, and it's not the one we expect, reject the message.\n\t// This prevents the log frame from receiving too many messages and\n\t// thus updating too frequently.\n\tif msg.Sequence > 0 && msg.Sequence != l.sequence {\n\t\treturn nil\n\t}\n\n\tl.sequence++\n\n\t// note: even if the log is completed we should still respond to stage changes and window size events\n\treturn l.queueNextTick()\n}\n"
  },
  {
    "path": "cmd/grype/cli/ui/handle_vulnerability_scanning_started_test.go",
    "content": "package ui\n\nimport (\n\t\"sort\"\n\t\"testing\"\n\t\"time\"\n\n\ttea \"github.com/charmbracelet/bubbletea\"\n\t\"github.com/gkampitakis/go-snaps/snaps\"\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/wagoodman/go-partybus\"\n\t\"github.com/wagoodman/go-progress\"\n\n\t\"github.com/anchore/bubbly/bubbles/taskprogress\"\n\t\"github.com/anchore/grype/grype/event\"\n\t\"github.com/anchore/grype/grype/event/monitor\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n)\n\nfunc TestHandler_handleVulnerabilityScanningStarted(t *testing.T) {\n\ttests := []struct {\n\t\tname       string\n\t\teventFn    func(*testing.T) partybus.Event\n\t\titerations int\n\t}{\n\t\t{\n\t\t\tname: \"vulnerability scanning in progress\",\n\t\t\teventFn: func(t *testing.T) partybus.Event {\n\t\t\t\treturn partybus.Event{\n\t\t\t\t\tType:  event.VulnerabilityScanningStarted,\n\t\t\t\t\tValue: getVulnerabilityMonitor(false),\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"vulnerability scanning complete\",\n\t\t\teventFn: func(t *testing.T) partybus.Event {\n\t\t\t\treturn partybus.Event{\n\t\t\t\t\tType:  event.VulnerabilityScanningStarted,\n\t\t\t\t\tValue: getVulnerabilityMonitor(true),\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\te := tt.eventFn(t)\n\t\t\thandler := New(DefaultHandlerConfig())\n\t\t\thandler.WindowSize = tea.WindowSizeMsg{\n\t\t\t\tWidth:  100,\n\t\t\t\tHeight: 80,\n\t\t\t}\n\n\t\t\tmodels, _ := handler.Handle(e)\n\t\t\trequire.Len(t, models, 2)\n\n\t\t\tt.Run(\"task line\", func(t *testing.T) {\n\t\t\t\ttsk, ok := models[0].(taskprogress.Model)\n\t\t\t\trequire.True(t, ok)\n\n\t\t\t\tgot := runModel(t, tsk, tt.iterations, taskprogress.TickMsg{\n\t\t\t\t\tTime:     time.Now(),\n\t\t\t\t\tSequence: tsk.Sequence(),\n\t\t\t\t\tID:       tsk.ID(),\n\t\t\t\t})\n\t\t\t\tt.Log(got)\n\t\t\t\tsnaps.MatchSnapshot(t, got)\n\t\t\t})\n\n\t\t\tt.Run(\"tree\", func(t *testing.T) {\n\t\t\t\tlog, ok := models[1].(vulnerabilityProgressTree)\n\t\t\t\trequire.True(t, ok)\n\t\t\t\tgot := runModel(t, log, tt.iterations, vulnerabilityProgressTreeTickMsg{\n\t\t\t\t\tTime:     time.Now(),\n\t\t\t\t\tSequence: log.sequence,\n\t\t\t\t\tID:       log.id,\n\t\t\t\t})\n\t\t\t\tt.Log(got)\n\t\t\t\tsnaps.MatchSnapshot(t, got)\n\t\t\t})\n\n\t\t})\n\t}\n}\n\nfunc getVulnerabilityMonitor(completed bool) monitor.Matching {\n\tpkgs := &progress.Manual{}\n\tpkgs.SetTotal(-1)\n\tif completed {\n\t\tpkgs.Set(2000)\n\t\tpkgs.SetCompleted()\n\t} else {\n\t\tpkgs.Set(300)\n\t}\n\n\tvulns := &progress.Manual{}\n\tvulns.SetTotal(-1)\n\tif completed {\n\t\tvulns.Set(45)\n\t\tvulns.SetCompleted()\n\t} else {\n\t\tvulns.Set(40)\n\t}\n\n\tfixed := &progress.Manual{}\n\tfixed.SetTotal(-1)\n\tif completed {\n\t\tfixed.Set(35)\n\t\tfixed.SetCompleted()\n\t} else {\n\t\tfixed.Set(30)\n\t}\n\n\tignored := &progress.Manual{}\n\tignored.SetTotal(-1)\n\tif completed {\n\t\tignored.Set(5)\n\t\tignored.SetCompleted()\n\t} else {\n\t\tignored.Set(4)\n\t}\n\n\tdropped := &progress.Manual{}\n\tdropped.SetTotal(-1)\n\tif completed {\n\t\tdropped.Set(3)\n\t\tdropped.SetCompleted()\n\t} else {\n\t\tdropped.Set(2)\n\t}\n\n\tbySeverityWriter := map[vulnerability.Severity]*progress.Manual{\n\t\tvulnerability.CriticalSeverity:   {},\n\t\tvulnerability.HighSeverity:       {},\n\t\tvulnerability.MediumSeverity:     {},\n\t\tvulnerability.LowSeverity:        {},\n\t\tvulnerability.NegligibleSeverity: {},\n\t\tvulnerability.UnknownSeverity:    {},\n\t}\n\n\tallSeverities := vulnerability.AllSeverities()\n\tsort.Sort(sort.Reverse(vulnerability.Severities(allSeverities)))\n\n\tvar count int64 = 1\n\tfor _, sev := range allSeverities {\n\t\tbySeverityWriter[sev].Add(count)\n\t\tcount++\n\t}\n\tbySeverityWriter[vulnerability.UnknownSeverity].Add(count)\n\n\tbySeverity := map[vulnerability.Severity]progress.Monitorable{}\n\n\tfor k, v := range bySeverityWriter {\n\t\tbySeverity[k] = v\n\t}\n\n\treturn monitor.Matching{\n\t\tPackagesProcessed: pkgs,\n\t\tMatchesDiscovered: vulns,\n\t\tFixed:             fixed,\n\t\tIgnored:           ignored,\n\t\tDropped:           dropped,\n\t\tBySeverity:        bySeverity,\n\t}\n}\n"
  },
  {
    "path": "cmd/grype/cli/ui/handler.go",
    "content": "package ui\n\nimport (\n\t\"sync\"\n\n\ttea \"github.com/charmbracelet/bubbletea\"\n\t\"github.com/wagoodman/go-partybus\"\n\n\t\"github.com/anchore/bubbly\"\n\t\"github.com/anchore/bubbly/bubbles/taskprogress\"\n\t\"github.com/anchore/grype/grype/event\"\n)\n\nvar _ interface {\n\tbubbly.EventHandler\n\tbubbly.MessageListener\n\tbubbly.HandleWaiter\n} = (*Handler)(nil)\n\ntype HandlerConfig struct {\n\tTitleWidth        int\n\tAdjustDefaultTask func(taskprogress.Model) taskprogress.Model\n}\n\ntype Handler struct {\n\tWindowSize tea.WindowSizeMsg\n\tRunning    *sync.WaitGroup\n\tConfig     HandlerConfig\n\n\tbubbly.EventHandler\n}\n\nfunc DefaultHandlerConfig() HandlerConfig {\n\treturn HandlerConfig{\n\t\tTitleWidth: 30,\n\t}\n}\n\nfunc New(cfg HandlerConfig) *Handler {\n\td := bubbly.NewEventDispatcher()\n\n\th := &Handler{\n\t\tEventHandler: d,\n\t\tRunning:      &sync.WaitGroup{},\n\t\tConfig:       cfg,\n\t}\n\n\t// register all supported event types with the respective handler functions\n\td.AddHandlers(map[partybus.EventType]bubbly.EventHandlerFn{\n\t\tevent.UpdateVulnerabilityDatabase:  h.handleUpdateVulnerabilityDatabase,\n\t\tevent.VulnerabilityScanningStarted: h.handleVulnerabilityScanningStarted,\n\t\tevent.DatabaseDiffingStarted:       h.handleDatabaseDiffStarted,\n\t})\n\n\treturn h\n}\n\nfunc (m *Handler) OnMessage(msg tea.Msg) {\n\tif msg, ok := msg.(tea.WindowSizeMsg); ok {\n\t\tm.WindowSize = msg\n\t}\n}\n\nfunc (m *Handler) Wait() {\n\tm.Running.Wait()\n}\n"
  },
  {
    "path": "cmd/grype/cli/ui/new_task_progress.go",
    "content": "package ui\n\nimport \"github.com/anchore/bubbly/bubbles/taskprogress\"\n\nfunc (m Handler) newTaskProgress(title taskprogress.Title, opts ...taskprogress.Option) taskprogress.Model {\n\ttsk := taskprogress.New(m.Running, opts...)\n\n\ttsk.HideProgressOnSuccess = true\n\ttsk.HideStageOnSuccess = true\n\ttsk.WindowSize = m.WindowSize\n\ttsk.TitleWidth = m.Config.TitleWidth\n\ttsk.TitleOptions = title\n\n\tif m.Config.AdjustDefaultTask != nil {\n\t\ttsk = m.Config.AdjustDefaultTask(tsk)\n\t}\n\n\treturn tsk\n}\n"
  },
  {
    "path": "cmd/grype/cli/ui/util_test.go",
    "content": "package ui\n\nimport (\n\t\"reflect\"\n\t\"sync\"\n\t\"testing\"\n\t\"unsafe\"\n\n\ttea \"github.com/charmbracelet/bubbletea\"\n)\n\nfunc runModel(t testing.TB, m tea.Model, iterations int, message tea.Msg, wgs ...*sync.WaitGroup) string {\n\tt.Helper()\n\tif iterations == 0 {\n\t\titerations = 1\n\t}\n\tm.Init()\n\tvar cmd tea.Cmd = func() tea.Msg {\n\t\treturn message\n\t}\n\n\tfor _, wg := range wgs {\n\t\tif wg != nil {\n\t\t\twg.Wait()\n\t\t}\n\t}\n\n\tfor i := 0; cmd != nil && i < iterations; i++ {\n\t\tmsgs := flatten(cmd())\n\t\tvar nextCmds []tea.Cmd\n\t\tvar next tea.Cmd\n\t\tfor _, msg := range msgs {\n\t\t\tt.Logf(\"Message: %+v %+v\\n\", reflect.TypeOf(msg), msg)\n\t\t\tm, next = m.Update(msg)\n\t\t\tnextCmds = append(nextCmds, next)\n\t\t}\n\t\tcmd = tea.Batch(nextCmds...)\n\t}\n\treturn m.View()\n}\n\nfunc flatten(p tea.Msg) (msgs []tea.Msg) {\n\tif reflect.TypeOf(p).Name() == \"batchMsg\" {\n\t\tpartials := extractBatchMessages(p)\n\t\tfor _, m := range partials {\n\t\t\tmsgs = append(msgs, flatten(m)...)\n\t\t}\n\t} else {\n\t\tmsgs = []tea.Msg{p}\n\t}\n\treturn msgs\n}\n\nfunc extractBatchMessages(m tea.Msg) (ret []tea.Msg) {\n\tsliceMsgType := reflect.SliceOf(reflect.TypeOf(tea.Cmd(nil)))\n\tvalue := reflect.ValueOf(m) // note: this is technically unaddressable\n\n\t// make our own instance that is addressable\n\tvalueCopy := reflect.New(value.Type()).Elem()\n\tvalueCopy.Set(value)\n\n\tcmds := reflect.NewAt(sliceMsgType, unsafe.Pointer(valueCopy.UnsafeAddr())).Elem()\n\tfor i := 0; i < cmds.Len(); i++ {\n\t\titem := cmds.Index(i)\n\t\tr := item.Call(nil)\n\t\tret = append(ret, r[0].Interface().(tea.Msg))\n\t}\n\treturn ret\n}\n"
  },
  {
    "path": "cmd/grype/internal/constants.go",
    "content": "package internal\n\nconst (\n\tNotProvided = \"[not provided]\"\n)\n"
  },
  {
    "path": "cmd/grype/internal/ui/__snapshots__/post_ui_event_writer_test.snap",
    "content": "\n[Test_postUIEventWriter_write/no_events/stdout - 1]\n\n---\n\n[Test_postUIEventWriter_write/no_events/stderr - 1]\n\n---\n\n[Test_postUIEventWriter_write/all_events/stdout - 1]\n\n\n<my --\n-\n-\nreport 1!!>\n<report 2>\n\n---\n\n[Test_postUIEventWriter_write/all_events/stderr - 1]\n                    \n                    \n<my notification 1!!\n...still notifying> \n                    \n                    \n<notification 2>\n<notification 3>\nA newer version of grype is available for download: v0.33.0 (installed version is [not provided])\n\n---\n\n[Test_postUIEventWriter_write/quiet_only_shows_report/stdout - 1]\n<report 1>\n\n---\n\n[Test_postUIEventWriter_write/quiet_only_shows_report/stderr - 1]\n\n---\n"
  },
  {
    "path": "cmd/grype/internal/ui/no_ui.go",
    "content": "package ui\n\nimport (\n\t\"os\"\n\n\t\"github.com/wagoodman/go-partybus\"\n\n\t\"github.com/anchore/clio\"\n\t\"github.com/anchore/grype/grype/event\"\n)\n\nvar _ clio.UI = (*NoUI)(nil)\n\ntype NoUI struct {\n\tfinalizeEvents []partybus.Event\n\tsubscription   partybus.Unsubscribable\n\tquiet          bool\n}\n\nfunc None(quiet bool) *NoUI {\n\treturn &NoUI{\n\t\tquiet: quiet,\n\t}\n}\n\nfunc (n *NoUI) Setup(subscription partybus.Unsubscribable) error {\n\tn.subscription = subscription\n\treturn nil\n}\n\nfunc (n *NoUI) Handle(e partybus.Event) error {\n\tswitch e.Type {\n\tcase event.CLIReport, event.CLINotification:\n\t\t// keep these for when the UI is terminated to show to the screen (or perform other events)\n\t\tn.finalizeEvents = append(n.finalizeEvents, e)\n\t}\n\treturn nil\n}\n\nfunc (n NoUI) Teardown(_ bool) error {\n\treturn newPostUIEventWriter(os.Stdout, os.Stderr).write(n.quiet, n.finalizeEvents...)\n}\n"
  },
  {
    "path": "cmd/grype/internal/ui/post_ui_event_writer.go",
    "content": "package ui\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"strings\"\n\n\t\"github.com/charmbracelet/lipgloss\"\n\t\"github.com/hashicorp/go-multierror\"\n\t\"github.com/wagoodman/go-partybus\"\n\n\t\"github.com/anchore/grype/grype/event\"\n\t\"github.com/anchore/grype/grype/event/parsers\"\n\t\"github.com/anchore/grype/internal/log\"\n)\n\ntype postUIEventWriter struct {\n\thandles []postUIHandle\n}\n\ntype postUIHandle struct {\n\trespectQuiet bool\n\tevent        partybus.EventType\n\twriter       io.Writer\n\tdispatch     eventWriter\n}\n\ntype eventWriter func(io.Writer, ...partybus.Event) error\n\nfunc newPostUIEventWriter(stdout, stderr io.Writer) *postUIEventWriter {\n\treturn &postUIEventWriter{\n\t\thandles: []postUIHandle{\n\t\t\t{\n\t\t\t\tevent:        event.CLIReport,\n\t\t\t\trespectQuiet: false,\n\t\t\t\twriter:       stdout,\n\t\t\t\tdispatch:     writeReports,\n\t\t\t},\n\t\t\t{\n\t\t\t\tevent:        event.CLINotification,\n\t\t\t\trespectQuiet: true,\n\t\t\t\twriter:       stderr,\n\t\t\t\tdispatch:     writeNotifications,\n\t\t\t},\n\t\t\t{\n\t\t\t\tevent:        event.CLIAppUpdateAvailable,\n\t\t\t\trespectQuiet: true,\n\t\t\t\twriter:       stderr,\n\t\t\t\tdispatch:     writeAppUpdate,\n\t\t\t},\n\t\t},\n\t}\n}\n\nfunc (w postUIEventWriter) write(quiet bool, events ...partybus.Event) error {\n\tvar errs error\n\tfor _, h := range w.handles {\n\t\tif quiet && h.respectQuiet {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, e := range events {\n\t\t\tif e.Type != h.event {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif err := h.dispatch(h.writer, e); err != nil {\n\t\t\t\terrs = multierror.Append(errs, err)\n\t\t\t}\n\t\t}\n\t}\n\treturn errs\n}\n\nfunc writeReports(writer io.Writer, events ...partybus.Event) error {\n\tvar reports []string\n\tfor _, e := range events {\n\t\t_, report, err := parsers.ParseCLIReport(e)\n\t\tif err != nil {\n\t\t\tlog.WithFields(\"error\", err).Warn(\"failed to gather final report\")\n\t\t\tcontinue\n\t\t}\n\n\t\t// remove all whitespace padding from the end of the report\n\t\treports = append(reports, strings.TrimRight(report, \"\\n \")+\"\\n\")\n\t}\n\n\t// prevent the double new-line at the end of the report\n\treport := strings.Join(reports, \"\\n\")\n\n\tif _, err := fmt.Fprint(writer, report); err != nil {\n\t\treturn fmt.Errorf(\"failed to write final report to stdout: %w\", err)\n\t}\n\treturn nil\n}\n\nfunc writeNotifications(writer io.Writer, events ...partybus.Event) error {\n\t// 13 = high intensity magenta (ANSI 16 bit code)\n\tstyle := lipgloss.NewStyle().Foreground(lipgloss.Color(\"13\"))\n\n\tfor _, e := range events {\n\t\t_, notification, err := parsers.ParseCLINotification(e)\n\t\tif err != nil {\n\t\t\tlog.WithFields(\"error\", err).Warn(\"failed to parse notification\")\n\t\t\tcontinue\n\t\t}\n\n\t\tif _, err := fmt.Fprintln(writer, style.Render(notification)); err != nil {\n\t\t\t// don't let this be fatal\n\t\t\tlog.WithFields(\"error\", err).Warn(\"failed to write final notifications\")\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc writeAppUpdate(writer io.Writer, events ...partybus.Event) error {\n\t// 13 = high intensity magenta (ANSI 16 bit code) + italics\n\tstyle := lipgloss.NewStyle().Foreground(lipgloss.Color(\"13\")).Italic(true)\n\n\tfor _, e := range events {\n\t\tversion, err := parsers.ParseCLIAppUpdateAvailable(e)\n\t\tif err != nil {\n\t\t\tlog.WithFields(\"error\", err).Warn(\"failed to parse app update notification\")\n\t\t\tcontinue\n\t\t}\n\n\t\tif version.New == \"\" {\n\t\t\tcontinue\n\t\t}\n\n\t\tnotice := fmt.Sprintf(\"A newer version of grype is available for download: %s (installed version is %s)\", version.New, version.Current)\n\n\t\tif _, err := fmt.Fprintln(writer, style.Render(notice)); err != nil {\n\t\t\t// don't let this be fatal\n\t\t\tlog.WithFields(\"error\", err).Warn(\"failed to write app update notification\")\n\t\t}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "cmd/grype/internal/ui/post_ui_event_writer_test.go",
    "content": "package ui\n\nimport (\n\t\"bytes\"\n\t\"testing\"\n\n\t\"github.com/gkampitakis/go-snaps/snaps\"\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/wagoodman/go-partybus\"\n\n\t\"github.com/anchore/grype/grype/event\"\n\t\"github.com/anchore/grype/grype/event/parsers\"\n)\n\nfunc Test_postUIEventWriter_write(t *testing.T) {\n\n\ttests := []struct {\n\t\tname    string\n\t\tquiet   bool\n\t\tevents  []partybus.Event\n\t\twantErr require.ErrorAssertionFunc\n\t}{\n\t\t{\n\t\t\tname: \"no events\",\n\t\t},\n\t\t{\n\t\t\tname: \"all events\",\n\t\t\tevents: []partybus.Event{\n\t\t\t\t{\n\t\t\t\t\tType:  event.CLINotification,\n\t\t\t\t\tValue: \"\\n\\n<my notification 1!!\\n...still notifying>\\n\\n\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tType:  event.CLINotification,\n\t\t\t\t\tValue: \"<notification 2>\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tType: event.CLIAppUpdateAvailable,\n\t\t\t\t\tValue: parsers.UpdateCheck{\n\t\t\t\t\t\tNew:     \"v0.33.0\",\n\t\t\t\t\t\tCurrent: \"[not provided]\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tType:  event.CLINotification,\n\t\t\t\t\tValue: \"<notification 3>\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tType:  event.CLIReport,\n\t\t\t\t\tValue: \"\\n\\n<my --\\n-\\n-\\nreport 1!!>\\n\\n\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tType:  event.CLIReport,\n\t\t\t\t\tValue: \"<report 2>\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"quiet only shows report\",\n\t\t\tquiet: true,\n\t\t\tevents: []partybus.Event{\n\n\t\t\t\t{\n\t\t\t\t\tType:  event.CLINotification,\n\t\t\t\t\tValue: \"<notification 1>\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tType: event.CLIAppUpdateAvailable,\n\t\t\t\t\tValue: parsers.UpdateCheck{\n\t\t\t\t\t\tNew:     \"<new version>\",\n\t\t\t\t\t\tCurrent: \"<current version>\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tType:  event.CLIReport,\n\t\t\t\t\tValue: \"<report 1>\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif tt.wantErr == nil {\n\t\t\t\ttt.wantErr = require.NoError\n\t\t\t}\n\n\t\t\tstdout := &bytes.Buffer{}\n\t\t\tstderr := &bytes.Buffer{}\n\t\t\tw := newPostUIEventWriter(stdout, stderr)\n\n\t\t\ttt.wantErr(t, w.write(tt.quiet, tt.events...))\n\n\t\t\tt.Run(\"stdout\", func(t *testing.T) {\n\t\t\t\tsnaps.MatchSnapshot(t, stdout.String())\n\t\t\t})\n\n\t\t\tt.Run(\"stderr\", func(t *testing.T) {\n\t\t\t\tsnaps.MatchSnapshot(t, stderr.String())\n\t\t\t})\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "cmd/grype/internal/ui/ui.go",
    "content": "package ui\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"sync\"\n\t\"time\"\n\n\ttea \"github.com/charmbracelet/bubbletea\"\n\t\"github.com/wagoodman/go-partybus\"\n\n\t\"github.com/anchore/bubbly\"\n\t\"github.com/anchore/bubbly/bubbles/frame\"\n\t\"github.com/anchore/clio\"\n\t\"github.com/anchore/go-logger\"\n\t\"github.com/anchore/grype/grype/event\"\n\t\"github.com/anchore/grype/internal/bus\"\n\t\"github.com/anchore/grype/internal/log\"\n)\n\nvar _ interface {\n\ttea.Model\n\tpartybus.Responder\n\tclio.UI\n} = (*UI)(nil)\n\ntype UI struct {\n\tprogram        *tea.Program\n\trunning        *sync.WaitGroup\n\tquiet          bool\n\tsubscription   partybus.Unsubscribable\n\tfinalizeEvents []partybus.Event\n\n\thandler *bubbly.HandlerCollection\n\tframe   tea.Model\n}\n\nfunc New(quiet bool, handlers ...bubbly.EventHandler) *UI {\n\treturn &UI{\n\t\thandler: bubbly.NewHandlerCollection(handlers...),\n\t\tframe:   frame.New(),\n\t\trunning: &sync.WaitGroup{},\n\t\tquiet:   quiet,\n\t}\n}\n\nfunc (m *UI) Setup(subscription partybus.Unsubscribable) error {\n\t// we still want to collect log messages, however, we also the logger shouldn't write to the screen directly\n\tif logWrapper, ok := log.Get().(logger.Controller); ok {\n\t\tlogWrapper.SetOutput(m.frame.(*frame.Frame).Footer())\n\t}\n\n\tm.subscription = subscription\n\tm.program = tea.NewProgram(m, tea.WithOutput(os.Stderr), tea.WithInput(os.Stdin))\n\tm.running.Add(1)\n\n\tgo func() {\n\t\tdefer m.running.Done()\n\t\tif _, err := m.program.Run(); err != nil {\n\t\t\tlog.Errorf(\"unable to start UI: %+v\", err)\n\t\t\tbus.ExitWithInterrupt()\n\t\t}\n\t}()\n\n\treturn nil\n}\n\nfunc (m *UI) Handle(e partybus.Event) error {\n\tif m.program != nil {\n\t\tm.program.Send(e)\n\t}\n\treturn nil\n}\n\nfunc (m *UI) Teardown(force bool) error {\n\tdefer func() {\n\t\t// allow for traditional logging to resume now that the UI is shutting down\n\t\tif logWrapper, ok := log.Get().(logger.Controller); ok {\n\t\t\tlogWrapper.SetOutput(os.Stderr)\n\t\t}\n\t}()\n\n\tif !force {\n\t\tm.handler.Wait()\n\t\tm.program.Quit()\n\t\t// typically in all cases we would want to wait for the UI to finish. However there are still error cases\n\t\t// that are not accounted for, resulting in hangs. For now, we'll just wait for the UI to finish in the\n\t\t// happy path only. There will always be an indication of the problem to the user via reporting the error\n\t\t// string from the worker (outside of the UI after teardown).\n\t\tm.running.Wait()\n\t} else {\n\t\t_ = runWithTimeout(250*time.Millisecond, func() error {\n\t\t\tm.handler.Wait()\n\t\t\treturn nil\n\t\t})\n\n\t\t// it may be tempting to use Kill() however it has been found that this can cause the terminal to be left in\n\t\t// a bad state (where Ctrl+C and other control characters no longer works for future processes in that terminal).\n\t\tm.program.Quit()\n\n\t\t_ = runWithTimeout(250*time.Millisecond, func() error {\n\t\t\tm.running.Wait()\n\t\t\treturn nil\n\t\t})\n\t}\n\n\t// TODO: allow for writing out the full log output to the screen (only a partial log is shown currently)\n\t// this needs coordination to know what the last frame event is to change the state accordingly (which isn't possible now)\n\n\treturn newPostUIEventWriter(os.Stdout, os.Stderr).write(m.quiet, m.finalizeEvents...)\n}\n\n// bubbletea.Model functions\n\nfunc (m UI) Init() tea.Cmd {\n\treturn m.frame.Init()\n}\n\nfunc (m UI) RespondsTo() []partybus.EventType {\n\treturn append([]partybus.EventType{\n\t\tevent.CLIReport,\n\t\tevent.CLINotification,\n\t\tevent.CLIAppUpdateAvailable,\n\t}, m.handler.RespondsTo()...)\n}\n\nfunc (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {\n\t// note: we need a pointer receiver such that the same instance of UI used in Teardown is referenced (to keep finalize events)\n\n\tvar cmds []tea.Cmd\n\n\t// allow for non-partybus UI updates (such as window size events). Note: these must not affect existing models,\n\t// that is the responsibility of the frame object on this UI object. The handler is a factory of models\n\t// which the frame is responsible for the lifecycle of. This update allows for injecting the initial state\n\t// of the world when creating those models.\n\tm.handler.OnMessage(msg)\n\n\tswitch msg := msg.(type) {\n\tcase tea.KeyMsg:\n\t\tswitch msg.String() {\n\t\t// today we treat esc and ctrl+c the same, but in the future when the worker has a graceful way to\n\t\t// cancel in-flight work via a context, we can wire up esc to this path with bus.Exit()\n\t\tcase \"esc\", \"ctrl+c\":\n\t\t\tbus.ExitWithInterrupt()\n\t\t\treturn m, tea.Quit\n\t\t}\n\n\tcase partybus.Event:\n\t\tlog.WithFields(\"component\", \"ui\").Tracef(\"event: %q\", msg.Type)\n\n\t\tswitch msg.Type {\n\t\tcase event.CLIReport, event.CLINotification, event.CLIAppUpdateAvailable:\n\t\t\t// keep these for when the UI is terminated to show to the screen (or perform other events)\n\t\t\tm.finalizeEvents = append(m.finalizeEvents, msg)\n\n\t\t\t// why not return tea.Quit here for exit events? because there may be UI components that still need the update-render loop.\n\t\t\t// for this reason we'll let the event loop call Teardown() which will explicitly wait for these components\n\t\t\treturn m, nil\n\t\t}\n\n\t\tmodels, cmd := m.handler.Handle(msg)\n\t\tif cmd != nil {\n\t\t\tcmds = append(cmds, cmd)\n\t\t}\n\t\tfor _, newModel := range models {\n\t\t\tif newModel == nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tcmds = append(cmds, newModel.Init())\n\t\t\tm.frame.(*frame.Frame).AppendModel(newModel)\n\t\t}\n\t\t// intentionally fallthrough to update the frame model\n\t}\n\n\tframeModel, cmd := m.frame.Update(msg)\n\tm.frame = frameModel\n\tcmds = append(cmds, cmd)\n\n\treturn m, tea.Batch(cmds...)\n}\n\nfunc (m UI) View() string {\n\treturn m.frame.View()\n}\n\nfunc runWithTimeout(timeout time.Duration, fn func() error) (err error) {\n\tc := make(chan struct{}, 1)\n\tgo func() {\n\t\terr = fn()\n\t\tc <- struct{}{}\n\t}()\n\tselect {\n\tcase <-c:\n\tcase <-time.After(timeout):\n\t\treturn fmt.Errorf(\"timed out after %v\", timeout)\n\t}\n\treturn err\n}\n"
  },
  {
    "path": "cmd/grype/main.go",
    "content": "package main\n\nimport (\n\t_ \"github.com/glebarez/sqlite\"\n\n\t\"github.com/anchore/clio\"\n\t\"github.com/anchore/grype/cmd/grype/cli\"\n\t\"github.com/anchore/grype/cmd/grype/internal\"\n)\n\n// applicationName is the non-capitalized name of the application (do not change this)\nconst applicationName = \"grype\"\n\n// all variables here are provided as build-time arguments, with clear default values\nvar (\n\tversion        = internal.NotProvided\n\tbuildDate      = internal.NotProvided\n\tgitCommit      = internal.NotProvided\n\tgitDescription = internal.NotProvided\n)\n\nfunc main() {\n\tapp := cli.Application(\n\t\tclio.Identification{\n\t\t\tName:           applicationName,\n\t\t\tVersion:        version,\n\t\t\tBuildDate:      buildDate,\n\t\t\tGitCommit:      gitCommit,\n\t\t\tGitDescription: gitDescription,\n\t\t},\n\t)\n\n\tapp.Run()\n}\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/anchore/grype\n\ngo 1.25.8\n\nrequire (\n\tgithub.com/CycloneDX/cyclonedx-go v0.10.0\n\tgithub.com/Masterminds/semver/v3 v3.4.0\n\tgithub.com/Masterminds/sprig/v3 v3.3.0\n\tgithub.com/OneOfOne/xxhash v1.2.8\n\tgithub.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d\n\tgithub.com/adrg/xdg v0.5.3\n\tgithub.com/anchore/bubbly v0.0.0-20250717181826-8a411f9d8cbf\n\tgithub.com/anchore/clio v0.0.0-20250715152405-a0fa658e5084\n\tgithub.com/anchore/fangs v0.0.0-20250716230140-94c22408c232\n\tgithub.com/anchore/go-collections v0.0.0-20251016125210-a3c352120e8c\n\tgithub.com/anchore/go-homedir v0.0.0-20250319154043-c29668562e4d\n\tgithub.com/anchore/go-logger v0.0.0-20250318195838-07ae343dd722\n\tgithub.com/anchore/go-version v1.2.2-0.20210903204242-51efa5b487c4\n\tgithub.com/anchore/packageurl-go v0.1.1-0.20250220190351-d62adb6e1115\n\tgithub.com/anchore/stereoscope v0.1.22\n\tgithub.com/anchore/syft v1.42.3\n\tgithub.com/aquasecurity/go-pep440-version v0.0.1\n\tgithub.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de\n\tgithub.com/bitnami/go-version v0.0.0-20250505154626-452e8c5ee607\n\tgithub.com/bmatcuk/doublestar/v2 v2.0.4\n\tgithub.com/charmbracelet/bubbletea v1.3.10\n\tgithub.com/charmbracelet/lipgloss v1.1.0\n\tgithub.com/dave/jennifer v1.7.1\n\tgithub.com/docker/docker v28.5.2+incompatible\n\tgithub.com/dustin/go-humanize v1.0.1\n\tgithub.com/facebookincubator/nvdtools v0.1.5\n\tgithub.com/gabriel-vasile/mimetype v1.4.13\n\tgithub.com/gkampitakis/go-snaps v0.5.20\n\tgithub.com/glebarez/sqlite v1.11.0\n\tgithub.com/go-test/deep v1.1.1\n\tgithub.com/go-viper/mapstructure/v2 v2.5.0\n\tgithub.com/gocsaf/csaf/v3 v3.5.1\n\tgithub.com/gohugoio/hashstructure v0.6.0\n\tgithub.com/google/go-cmp v0.7.0\n\tgithub.com/google/go-containerregistry v0.21.2\n\tgithub.com/google/osv-scanner v1.9.2\n\tgithub.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510\n\tgithub.com/google/uuid v1.6.0\n\tgithub.com/gookit/color v1.6.0\n\tgithub.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b\n\tgithub.com/hashicorp/go-cleanhttp v0.5.2\n\tgithub.com/hashicorp/go-getter v1.8.5\n\tgithub.com/hashicorp/go-multierror v1.1.1\n\tgithub.com/iancoleman/strcase v0.3.0\n\tgithub.com/invopop/jsonschema v0.13.0\n\tgithub.com/jinzhu/copier v0.4.0\n\tgithub.com/klauspost/compress v1.18.4\n\tgithub.com/knqyf263/go-apk-version v0.0.0-20200609155635-041fdbb8563f\n\tgithub.com/knqyf263/go-deb-version v0.0.0-20241115132648-6f4aee6ccd23\n\tgithub.com/masahiro331/go-mvn-version v0.0.0-20250131095131-f4974fa13b8a\n\tgithub.com/mholt/archives v0.1.5\n\tgithub.com/muesli/termenv v0.16.0\n\tgithub.com/olekukonko/tablewriter v1.1.4\n\tgithub.com/openvex/go-vex v0.2.7\n\tgithub.com/owenrumney/go-sarif v1.1.2-0.20231003122901-1000f5e05554\n\tgithub.com/pandatix/go-cvss v0.6.2\n\t// pinned to pull in 386 arch fix: https://github.com/scylladb/go-set/commit/cc7b2070d91ebf40d233207b633e28f5bd8f03a5\n\tgithub.com/scylladb/go-set v1.0.3-0.20200225121959-cc7b2070d91e\n\tgithub.com/sergi/go-diff v1.4.0\n\tgithub.com/spf13/afero v1.15.0\n\tgithub.com/spf13/cobra v1.10.2\n\tgithub.com/stretchr/testify v1.11.1\n\tgithub.com/ulikunitz/xz v0.5.15\n\tgithub.com/umisama/go-cpe v0.0.0-20190323060751-cdd6c3c28a23\n\tgithub.com/wagoodman/go-partybus v0.0.0-20230516145632-8ccac152c651\n\tgithub.com/wagoodman/go-presenter v0.0.0-20211015174752-f9c01afc824b\n\tgithub.com/wagoodman/go-progress v0.0.0-20260303201901-10176f79b2c0\n\tgithub.com/xi2/xz v0.0.0-20171230120015-48954b6210f8\n\tgolang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546\n\tgolang.org/x/text v0.35.0\n\tgolang.org/x/time v0.15.0\n\tgolang.org/x/tools v0.43.0\n\tgopkg.in/yaml.v3 v3.0.1\n\tgorm.io/gorm v1.31.1\n)\n\nrequire (\n\tcel.dev/expr v0.25.1 // indirect\n\tcloud.google.com/go v0.123.0 // indirect\n\tcloud.google.com/go/auth v0.18.1 // indirect\n\tcloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect\n\tcloud.google.com/go/compute/metadata v0.9.0 // indirect\n\tcloud.google.com/go/iam v1.5.3 // indirect\n\tcloud.google.com/go/monitoring v1.24.3 // indirect\n\tcloud.google.com/go/storage v1.60.0 // indirect\n\tcyphar.com/go-pathrs v0.2.1 // indirect\n\tdario.cat/mergo v1.0.2 // indirect\n\tgithub.com/BurntSushi/toml v1.6.0 // indirect\n\tgithub.com/DataDog/zstd v1.5.7 // indirect\n\tgithub.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 // indirect\n\tgithub.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 // indirect\n\tgithub.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 // indirect\n\tgithub.com/Intevation/gval v1.3.0 // indirect\n\tgithub.com/Intevation/jsonpath v0.2.1 // indirect\n\tgithub.com/Masterminds/goutils v1.1.1 // indirect\n\tgithub.com/Microsoft/go-winio v0.6.2 // indirect\n\tgithub.com/Microsoft/hcsshim v0.14.0-rc.1 // indirect\n\tgithub.com/ProtonMail/go-crypto v1.4.0 // indirect\n\tgithub.com/STARRY-S/zip v0.2.3 // indirect\n\tgithub.com/acobaugh/osrelease v0.1.0 // indirect\n\tgithub.com/agext/levenshtein v1.2.3 // indirect\n\tgithub.com/anchore/go-lzo v0.1.0 // indirect\n\tgithub.com/anchore/go-macholibre v0.0.0-20250320151634-807da7ad2331 // indirect\n\tgithub.com/anchore/go-rpmdb v0.0.0-20250516171929-f77691e1faec // indirect\n\tgithub.com/anchore/go-struct-converter v0.1.0 // indirect\n\tgithub.com/anchore/go-sync v0.0.0-20250714163430-add63db73ad1 // indirect\n\tgithub.com/andybalholm/brotli v1.2.0 // indirect\n\tgithub.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect\n\tgithub.com/aquasecurity/go-version v0.0.1 // indirect\n\tgithub.com/aws/aws-sdk-go-v2 v1.41.2 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/config v1.32.10 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/credentials v1.19.10 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/internal/v4a v1.4.17 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.8 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.17 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/s3 v1.96.0 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/signin v1.0.6 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/sso v1.30.11 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/sts v1.41.7 // indirect\n\tgithub.com/aws/smithy-go v1.24.1 // indirect\n\tgithub.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect\n\tgithub.com/bahlo/generic-list-go v0.2.0 // indirect\n\tgithub.com/becheran/wildmatch-go v1.0.0 // indirect\n\tgithub.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect\n\tgithub.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb // indirect\n\tgithub.com/bmatcuk/doublestar/v4 v4.10.0 // indirect\n\tgithub.com/bodgit/plumbing v1.3.0 // indirect\n\tgithub.com/bodgit/sevenzip v1.6.1 // indirect\n\tgithub.com/bodgit/windows v1.0.1 // indirect\n\tgithub.com/buger/jsonparser v1.1.2 // indirect\n\tgithub.com/cespare/xxhash/v2 v2.3.0 // indirect\n\tgithub.com/charmbracelet/bubbles v1.0.0 // indirect\n\tgithub.com/charmbracelet/colorprofile v0.4.1 // indirect\n\tgithub.com/charmbracelet/harmonica v0.2.0 // indirect\n\tgithub.com/charmbracelet/x/ansi v0.11.6 // indirect\n\tgithub.com/charmbracelet/x/cellbuf v0.0.15 // indirect\n\tgithub.com/charmbracelet/x/term v0.2.2 // indirect\n\tgithub.com/clipperhouse/displaywidth v0.10.0 // indirect\n\tgithub.com/clipperhouse/uax29/v2 v2.6.0 // indirect\n\tgithub.com/cloudflare/circl v1.6.3 // indirect\n\tgithub.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 // indirect\n\tgithub.com/containerd/cgroups/v3 v3.1.2 // indirect\n\tgithub.com/containerd/containerd/api v1.10.0 // indirect\n\tgithub.com/containerd/containerd/v2 v2.2.1 // indirect\n\tgithub.com/containerd/continuity v0.4.5 // indirect\n\tgithub.com/containerd/errdefs v1.0.0 // indirect\n\tgithub.com/containerd/errdefs/pkg v0.3.0 // indirect\n\tgithub.com/containerd/fifo v1.1.0 // indirect\n\tgithub.com/containerd/log v0.1.0 // indirect\n\tgithub.com/containerd/platforms v1.0.0-rc.2 // indirect\n\tgithub.com/containerd/plugin v1.0.0 // indirect\n\tgithub.com/containerd/stargz-snapshotter/estargz v0.18.2 // indirect\n\tgithub.com/containerd/ttrpc v1.2.7 // indirect\n\tgithub.com/containerd/typeurl/v2 v2.2.3 // indirect\n\tgithub.com/cyphar/filepath-securejoin v0.6.0 // indirect\n\tgithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect\n\tgithub.com/deitch/magic v0.0.0-20240306090643-c67ab88f10cb // indirect\n\tgithub.com/diskfs/go-diskfs v1.7.0 // indirect\n\tgithub.com/distribution/reference v0.6.0 // indirect\n\tgithub.com/docker/cli v29.3.0+incompatible // indirect\n\tgithub.com/docker/distribution v2.8.3+incompatible // indirect\n\tgithub.com/docker/docker-credential-helpers v0.9.5 // indirect\n\tgithub.com/docker/go-connections v0.6.0 // indirect\n\tgithub.com/docker/go-units v0.5.0 // indirect\n\tgithub.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect\n\tgithub.com/elliotchance/phpserialize v1.4.0 // indirect\n\tgithub.com/emirpasic/gods v1.18.1 // indirect\n\tgithub.com/envoyproxy/go-control-plane/envoy v1.36.0 // indirect\n\tgithub.com/envoyproxy/protoc-gen-validate v1.3.0 // indirect\n\tgithub.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect\n\tgithub.com/fatih/color v1.18.0 // indirect\n\tgithub.com/felixge/fgprof v0.9.5 // indirect\n\tgithub.com/felixge/httpsnoop v1.0.4 // indirect\n\tgithub.com/fsnotify/fsnotify v1.9.0 // indirect\n\tgithub.com/github/go-spdx/v2 v2.4.0 // indirect\n\tgithub.com/gkampitakis/ciinfo v0.3.2 // indirect\n\tgithub.com/glebarez/go-sqlite v1.22.0 // indirect\n\tgithub.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect\n\tgithub.com/go-git/go-billy/v5 v5.8.0 // indirect\n\tgithub.com/go-git/go-git/v5 v5.17.0 // indirect\n\tgithub.com/go-jose/go-jose/v4 v4.1.3 // indirect\n\tgithub.com/go-logr/logr v1.4.3 // indirect\n\tgithub.com/go-logr/stdr v1.2.2 // indirect\n\tgithub.com/go-restruct/restruct v1.2.0-alpha // indirect\n\tgithub.com/goccy/go-yaml v1.19.2 // indirect\n\tgithub.com/gogo/protobuf v1.3.2 // indirect\n\tgithub.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect\n\tgithub.com/google/licensecheck v0.3.1 // indirect\n\tgithub.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5 // indirect\n\tgithub.com/google/s2a-go v0.1.9 // indirect\n\tgithub.com/googleapis/enterprise-certificate-proxy v0.3.11 // indirect\n\tgithub.com/googleapis/gax-go/v2 v2.17.0 // indirect\n\tgithub.com/gpustack/gguf-parser-go v0.24.0 // indirect\n\tgithub.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.70 // indirect\n\tgithub.com/hashicorp/errwrap v1.1.0 // indirect\n\tgithub.com/hashicorp/go-version v1.8.0 // indirect\n\tgithub.com/hashicorp/golang-lru/v2 v2.0.7 // indirect\n\tgithub.com/hashicorp/hcl/v2 v2.24.0 // indirect\n\tgithub.com/henvic/httpretty v0.1.4 // indirect\n\tgithub.com/huandu/xstrings v1.5.0 // indirect\n\tgithub.com/inconshreveable/mousetrap v1.1.0 // indirect\n\tgithub.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect\n\tgithub.com/jinzhu/inflection v1.0.0 // indirect\n\tgithub.com/jinzhu/now v1.1.5 // indirect\n\tgithub.com/json-iterator/go v1.1.12 // indirect\n\tgithub.com/kastenhq/goversion v0.0.0-20230811215019-93b2f8823953 // indirect\n\tgithub.com/kevinburke/ssh_config v1.2.0 // indirect\n\tgithub.com/klauspost/pgzip v1.2.6 // indirect\n\tgithub.com/kr/pretty v0.3.1 // indirect\n\tgithub.com/kr/text v0.2.0 // indirect\n\tgithub.com/lucasb-eyer/go-colorful v1.3.0 // indirect\n\tgithub.com/mailru/easyjson v0.9.0 // indirect\n\tgithub.com/maruel/natural v1.1.1 // indirect\n\tgithub.com/mattn/go-colorable v0.1.14 // indirect\n\tgithub.com/mattn/go-isatty v0.0.20 // indirect\n\tgithub.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75 // indirect\n\tgithub.com/mattn/go-runewidth v0.0.19 // indirect\n\tgithub.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect\n\tgithub.com/mikelolasagasti/xz v1.0.1 // indirect\n\tgithub.com/minio/minlz v1.0.1 // indirect\n\tgithub.com/mitchellh/copystructure v1.2.0 // indirect\n\tgithub.com/mitchellh/go-homedir v1.1.0 // indirect\n\tgithub.com/mitchellh/go-wordwrap v1.0.1 // indirect\n\tgithub.com/mitchellh/reflectwalk v1.0.2 // indirect\n\tgithub.com/moby/docker-image-spec v1.3.1 // indirect\n\tgithub.com/moby/locker v1.0.1 // indirect\n\tgithub.com/moby/moby/api v1.54.0 // indirect\n\tgithub.com/moby/moby/client v0.3.0 // indirect\n\tgithub.com/moby/sys/mountinfo v0.7.2 // indirect\n\tgithub.com/moby/sys/sequential v0.6.0 // indirect\n\tgithub.com/moby/sys/signal v0.7.1 // indirect\n\tgithub.com/moby/sys/user v0.4.0 // indirect\n\tgithub.com/moby/sys/userns v0.1.0 // indirect\n\tgithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect\n\tgithub.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect\n\tgithub.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect\n\tgithub.com/muesli/cancelreader v0.2.2 // indirect\n\tgithub.com/ncruces/go-strftime v1.0.0 // indirect\n\tgithub.com/nix-community/go-nix v0.0.0-20250101154619-4bdde671e0a1 // indirect\n\tgithub.com/nwaples/rardecode/v2 v2.2.0 // indirect\n\tgithub.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect\n\tgithub.com/olekukonko/errors v1.2.0 // indirect\n\tgithub.com/olekukonko/ll v0.1.6 // indirect\n\tgithub.com/opencontainers/go-digest v1.0.0 // indirect\n\tgithub.com/opencontainers/image-spec v1.1.1 // indirect\n\tgithub.com/opencontainers/runtime-spec v1.3.0 // indirect\n\tgithub.com/opencontainers/selinux v1.13.1 // indirect\n\tgithub.com/package-url/packageurl-go v0.1.3 // indirect\n\tgithub.com/pborman/indent v1.2.1 // indirect\n\tgithub.com/pelletier/go-toml v1.9.5 // indirect\n\tgithub.com/pelletier/go-toml/v2 v2.2.4 // indirect\n\tgithub.com/pierrec/lz4/v4 v4.1.22 // indirect\n\tgithub.com/pjbgf/sha1cd v0.4.0 // indirect\n\tgithub.com/pkg/errors v0.9.1 // indirect\n\tgithub.com/pkg/profile v1.7.0 // indirect\n\tgithub.com/pkg/xattr v0.4.12 // indirect\n\tgithub.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect\n\tgithub.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect\n\tgithub.com/rivo/uniseg v0.4.7 // indirect\n\tgithub.com/rogpeppe/go-internal v1.14.1 // indirect\n\tgithub.com/rust-secure-code/go-rustaudit v0.0.0-20250226111315-e20ec32e963c // indirect\n\tgithub.com/sagikazarmark/locafero v0.11.0 // indirect\n\tgithub.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d // indirect\n\tgithub.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect\n\tgithub.com/sassoftware/go-rpmutils v0.4.0 // indirect\n\tgithub.com/shopspring/decimal v1.4.0 // indirect\n\tgithub.com/sirupsen/logrus v1.9.4 // indirect\n\tgithub.com/skeema/knownhosts v1.3.1 // indirect\n\tgithub.com/smallnest/ringbuffer v0.0.0-20241116012123-461381446e3d // indirect\n\tgithub.com/sorairolake/lzip-go v0.3.8 // indirect\n\tgithub.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect\n\tgithub.com/spdx/gordf v0.0.0-20250128162952-000978ccd6fb // indirect\n\tgithub.com/spdx/tools-golang v0.5.7 // indirect\n\tgithub.com/spf13/cast v1.10.0 // indirect\n\tgithub.com/spf13/pflag v1.0.10 // indirect\n\tgithub.com/spf13/viper v1.21.0 // indirect\n\tgithub.com/spiffe/go-spiffe/v2 v2.6.0 // indirect\n\tgithub.com/stretchr/objx v0.5.2 // indirect\n\tgithub.com/subosito/gotenv v1.6.0 // indirect\n\tgithub.com/sylabs/sif/v2 v2.24.0 // indirect\n\tgithub.com/sylabs/squashfs v1.0.6 // indirect\n\tgithub.com/therootcompany/xz v1.0.1 // indirect\n\tgithub.com/tidwall/gjson v1.18.0 // indirect\n\tgithub.com/tidwall/match v1.1.1 // indirect\n\tgithub.com/tidwall/pretty v1.2.1 // indirect\n\tgithub.com/tidwall/sjson v1.2.5 // indirect\n\tgithub.com/vbatts/go-mtree v0.7.0 // indirect\n\tgithub.com/vbatts/tar-split v0.12.2 // indirect\n\tgithub.com/vifraa/gopom v1.0.0 // indirect\n\tgithub.com/wk8/go-ordered-map/v2 v2.1.8 // indirect\n\tgithub.com/xanzy/ssh-agent v0.3.3 // indirect\n\tgithub.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect\n\tgithub.com/zclconf/go-cty v1.16.3 // indirect\n\tgithub.com/zyedidia/generic v1.2.2-0.20230320175451-4410d2372cb1 // indirect\n\tgo.etcd.io/bbolt v1.4.3 // indirect\n\tgo.opencensus.io v0.24.0 // indirect\n\tgo.opentelemetry.io/auto/sdk v1.2.1 // indirect\n\tgo.opentelemetry.io/contrib/detectors/gcp v1.39.0 // indirect\n\tgo.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect\n\tgo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 // indirect\n\tgo.opentelemetry.io/otel v1.40.0 // indirect\n\tgo.opentelemetry.io/otel/metric v1.40.0 // indirect\n\tgo.opentelemetry.io/otel/sdk v1.40.0 // indirect\n\tgo.opentelemetry.io/otel/sdk/metric v1.40.0 // indirect\n\tgo.opentelemetry.io/otel/trace v1.40.0 // indirect\n\tgo.yaml.in/yaml/v3 v3.0.4 // indirect\n\tgo4.org v0.0.0-20230225012048-214862532bf5 // indirect\n\tgolang.org/x/crypto v0.49.0 // indirect\n\tgolang.org/x/mod v0.34.0 // indirect\n\tgolang.org/x/net v0.52.0 // indirect\n\tgolang.org/x/oauth2 v0.36.0 // indirect\n\tgolang.org/x/sync v0.20.0 // indirect\n\tgolang.org/x/sys v0.42.0 // indirect\n\tgolang.org/x/term v0.41.0 // indirect\n\tgolang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect\n\tgonum.org/v1/gonum v0.16.0 // indirect\n\tgoogle.golang.org/api v0.267.0 // indirect\n\tgoogle.golang.org/genproto v0.0.0-20260128011058-8636f8732409 // indirect\n\tgoogle.golang.org/genproto/googleapis/api v0.0.0-20260203192932-546029d2fa20 // indirect\n\tgoogle.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 // indirect\n\tgoogle.golang.org/grpc v1.79.3 // indirect\n\tgoogle.golang.org/protobuf v1.36.11 // indirect\n\tgopkg.in/warnings.v0 v0.1.2 // indirect\n\tmodernc.org/libc v1.67.6 // indirect\n\tmodernc.org/mathutil v1.7.1 // indirect\n\tmodernc.org/memory v1.11.0 // indirect\n\tmodernc.org/sqlite v1.46.1 // indirect\n)\n"
  },
  {
    "path": "go.sum",
    "content": "cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=\ncel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=\ncloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ncloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ncloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=\ncloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=\ncloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=\ncloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=\ncloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=\ncloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=\ncloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=\ncloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=\ncloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=\ncloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=\ncloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=\ncloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=\ncloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=\ncloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=\ncloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=\ncloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg=\ncloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8=\ncloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0=\ncloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY=\ncloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM=\ncloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY=\ncloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ=\ncloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI=\ncloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4=\ncloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc=\ncloud.google.com/go v0.98.0/go.mod h1:ua6Ush4NALrHk5QXDWnjvZHN93OuF0HfuEPq9I1X0cM=\ncloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA=\ncloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE=\ncloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU=\ncloud.google.com/go/auth v0.18.1 h1:IwTEx92GFUo2pJ6Qea0EU3zYvKnTAeRCODxfA/G5UWs=\ncloud.google.com/go/auth v0.18.1/go.mod h1:GfTYoS9G3CWpRA3Va9doKN9mjPGRS+v41jmZAhBzbrA=\ncloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=\ncloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=\ncloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=\ncloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=\ncloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=\ncloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=\ncloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=\ncloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=\ncloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=\ncloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=\ncloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=\ncloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=\ncloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY=\ncloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc=\ncloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU=\ncloud.google.com/go/logging v1.13.1 h1:O7LvmO0kGLaHY/gq8cV7T0dyp6zJhYAOtZPX4TF3QtY=\ncloud.google.com/go/logging v1.13.1/go.mod h1:XAQkfkMBxQRjQek96WLPNze7vsOmay9H5PqfsNYDqvw=\ncloud.google.com/go/longrunning v0.8.0 h1:LiKK77J3bx5gDLi4SMViHixjD2ohlkwBi+mKA7EhfW8=\ncloud.google.com/go/longrunning v0.8.0/go.mod h1:UmErU2Onzi+fKDg2gR7dusz11Pe26aknR4kHmJJqIfk=\ncloud.google.com/go/monitoring v1.24.3 h1:dde+gMNc0UhPZD1Azu6at2e79bfdztVDS5lvhOdsgaE=\ncloud.google.com/go/monitoring v1.24.3/go.mod h1:nYP6W0tm3N9H/bOw8am7t62YTzZY+zUeQ+Bi6+2eonI=\ncloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=\ncloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=\ncloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=\ncloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=\ncloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=\ncloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=\ncloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=\ncloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=\ncloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=\ncloud.google.com/go/storage v1.60.0 h1:oBfZrSOCimggVNz9Y/bXY35uUcts7OViubeddTTVzQ8=\ncloud.google.com/go/storage v1.60.0/go.mod h1:q+5196hXfejkctrnx+VYU8RKQr/L3c0cBIlrjmiAKE0=\ncloud.google.com/go/trace v1.11.7 h1:kDNDX8JkaAG3R2nq1lIdkb7FCSi1rCmsEtKVsty7p+U=\ncloud.google.com/go/trace v1.11.7/go.mod h1:TNn9d5V3fQVf6s4SCveVMIBS2LJUqo73GACmq/Tky0s=\ncyphar.com/go-pathrs v0.2.1 h1:9nx1vOgwVvX1mNBWDu93+vaceedpbsDqo+XuBGL40b8=\ncyphar.com/go-pathrs v0.2.1/go.mod h1:y8f1EMG7r+hCuFf/rXsKqMJrJAUoADZGNh5/vZPKcGc=\ndario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=\ndario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=\ndmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=\ngithub.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk=\ngithub.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=\ngithub.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=\ngithub.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=\ngithub.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=\ngithub.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=\ngithub.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=\ngithub.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=\ngithub.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=\ngithub.com/CycloneDX/cyclonedx-go v0.10.0 h1:7xyklU7YD+CUyGzSFIARG18NYLsKVn4QFg04qSsu+7Y=\ngithub.com/CycloneDX/cyclonedx-go v0.10.0/go.mod h1:vUvbCXQsEm48OI6oOlanxstwNByXjCZ2wuleUlwGEO8=\ngithub.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=\ngithub.com/DataDog/zstd v1.5.7 h1:ybO8RBeh29qrxIhCA9E8gKY6xfONU9T6G6aP9DTKfLE=\ngithub.com/DataDog/zstd v1.5.7/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw=\ngithub.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 h1:sBEjpZlNHzK1voKq9695PJSX2o5NEXl7/OL3coiIY0c=\ngithub.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0=\ngithub.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 h1:UnDZ/zFfG1JhH/DqxIZYU/1CUAlTUScoXD/LcM2Ykk8=\ngithub.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0/go.mod h1:IA1C1U7jO/ENqm/vhi7V9YYpBsp+IMyqNrEN94N7tVc=\ngithub.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0 h1:7t/qx5Ost0s0wbA/VDrByOooURhp+ikYwv20i9Y07TQ=\ngithub.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0/go.mod h1:vB2GH9GAYYJTO3mEn8oYwzEdhlayZIdQz6zdzgUIRvA=\ngithub.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 h1:0s6TxfCu2KHkkZPnBfsQ2y5qia0jl3MMrmBhu3nCOYk=\ngithub.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc=\ngithub.com/Intevation/gval v1.3.0 h1:+Ze5sft5MmGbZrHj06NVUbcxCb67l9RaPTLMNr37mjw=\ngithub.com/Intevation/gval v1.3.0/go.mod h1:xmGyGpP5be12EL0P12h+dqiYG8qn2j3PJxIgkoOHO5o=\ngithub.com/Intevation/jsonpath v0.2.1 h1:rINNQJ0Pts5XTFEG+zamtdL7l9uuE1z0FBA+r55Sw+A=\ngithub.com/Intevation/jsonpath v0.2.1/go.mod h1:WnZ8weMmwAx/fAO3SutjYFU+v7DFreNYnibV7CiaYIw=\ngithub.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=\ngithub.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=\ngithub.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=\ngithub.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=\ngithub.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs=\ngithub.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=\ngithub.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=\ngithub.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=\ngithub.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=\ngithub.com/Microsoft/hcsshim v0.14.0-rc.1 h1:qAPXKwGOkVn8LlqgBN8GS0bxZ83hOJpcjxzmlQKxKsQ=\ngithub.com/Microsoft/hcsshim v0.14.0-rc.1/go.mod h1:hTKFGbnDtQb1wHiOWv4v0eN+7boSWAHyK/tNAaYZL0c=\ngithub.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=\ngithub.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8=\ngithub.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q=\ngithub.com/ProtonMail/go-crypto v1.4.0 h1:Zq/pbM3F5DFgJiMouxEdSVY44MVoQNEKp5d5QxIQceQ=\ngithub.com/ProtonMail/go-crypto v1.4.0/go.mod h1:e1OaTyu5SYVrO9gKOEhTc+5UcXtTUa+P3uLudwcgPqo=\ngithub.com/STARRY-S/zip v0.2.3 h1:luE4dMvRPDOWQdeDdUxUoZkzUIpTccdKdhHHsQJ1fm4=\ngithub.com/STARRY-S/zip v0.2.3/go.mod h1:lqJ9JdeRipyOQJrYSOtpNAiaesFO6zVDsE8GIGFaoSk=\ngithub.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8=\ngithub.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo=\ngithub.com/acobaugh/osrelease v0.1.0 h1:Yb59HQDGGNhCj4suHaFQQfBps5wyoKLSSX/J/+UifRE=\ngithub.com/acobaugh/osrelease v0.1.0/go.mod h1:4bFEs0MtgHNHBrmHCt67gNisnabCRAlzdVasCEGHTWY=\ngithub.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78=\ngithub.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ=\ngithub.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo=\ngithub.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=\ngithub.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=\ngithub.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=\ngithub.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=\ngithub.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=\ngithub.com/anchore/bubbly v0.0.0-20250717181826-8a411f9d8cbf h1:UY7SQkfVVaeGUpPZrJxqmTc8M0ZSWc5ChiKF6I6aL3I=\ngithub.com/anchore/bubbly v0.0.0-20250717181826-8a411f9d8cbf/go.mod h1:w8Br1ZKk1Nk82YRSh10pcD7LO7avPyFmNnaY1TRPgs0=\ngithub.com/anchore/clio v0.0.0-20250715152405-a0fa658e5084 h1:7DUAXEdAxoANPlDgxYiaSRKnWnTygvdrrWhnmvEjNLg=\ngithub.com/anchore/clio v0.0.0-20250715152405-a0fa658e5084/go.mod h1:42dWox8z4//b898OIELsQnSdYq9q1aCXkwp5fKF+BEU=\ngithub.com/anchore/fangs v0.0.0-20250716230140-94c22408c232 h1:aVC6r9h5wGNh8BYTW3CXxOdPoZzY/bBRWne1NvSTlO8=\ngithub.com/anchore/fangs v0.0.0-20250716230140-94c22408c232/go.mod h1:Zees1AEKNpXIRgdVAMYWITncarLFiPOtEQ7rl45V/h0=\ngithub.com/anchore/go-collections v0.0.0-20251016125210-a3c352120e8c h1:eoJXyC0n7DZ4YvySG/ETdYkTar2Due7eH+UmLK6FbrA=\ngithub.com/anchore/go-collections v0.0.0-20251016125210-a3c352120e8c/go.mod h1:1aiktV46ATCkuVg0O573ZrH56BUawTECPETbZyBcqT8=\ngithub.com/anchore/go-homedir v0.0.0-20250319154043-c29668562e4d h1:gT69osH9AsdpOfqxbRwtxcNnSZ1zg4aKy2BevO3ZBdc=\ngithub.com/anchore/go-homedir v0.0.0-20250319154043-c29668562e4d/go.mod h1:PhSnuFYknwPZkOWKB1jXBNToChBA+l0FjwOxtViIc50=\ngithub.com/anchore/go-logger v0.0.0-20250318195838-07ae343dd722 h1:2SqmFgE7h+Ql4VyBzhjLkRF/3gDrcpUBj8LjvvO6OOM=\ngithub.com/anchore/go-logger v0.0.0-20250318195838-07ae343dd722/go.mod h1:oFuE8YuTCM+spgMXhePGzk3asS94yO9biUfDzVTFqNw=\ngithub.com/anchore/go-lzo v0.1.0 h1:NgAacnzqPeGH49Ky19QKLBZEuFRqtTG9cdaucc3Vncs=\ngithub.com/anchore/go-lzo v0.1.0/go.mod h1:3kLx0bve2oN1iDwgM1U5zGku1Tfbdb0No5qp1eL1fIk=\ngithub.com/anchore/go-macholibre v0.0.0-20250320151634-807da7ad2331 h1:fWPHXkH3FQGVCyPkFMqNvMjQvdNMfkylBTsDqZC4lE4=\ngithub.com/anchore/go-macholibre v0.0.0-20250320151634-807da7ad2331/go.mod h1:DYvTRnWrlJ//6YOR83SiewmJiNFDEMRaOTnrzgco9FA=\ngithub.com/anchore/go-rpmdb v0.0.0-20250516171929-f77691e1faec h1:SjjPMOXTzpuU1ZME4XeoHyek+dry3/C7I8gzaCo02eg=\ngithub.com/anchore/go-rpmdb v0.0.0-20250516171929-f77691e1faec/go.mod h1:eQVa6QFGzKy0qMcnW2pez0XBczvgwSjw9vA23qifEyU=\ngithub.com/anchore/go-struct-converter v0.1.0 h1:2rDRssAl6mgKBSLNiVCMADgZRhoqtw9dedlWa0OhD30=\ngithub.com/anchore/go-struct-converter v0.1.0/go.mod h1:rYqSE9HbjzpHTI74vwPvae4ZVYZd1lue2ta6xHPdblA=\ngithub.com/anchore/go-sync v0.0.0-20250714163430-add63db73ad1 h1:UK1SWZf2xD5jq8QVeDdpt6wW31cO3RckBvPmGlDrTkg=\ngithub.com/anchore/go-sync v0.0.0-20250714163430-add63db73ad1/go.mod h1:hd0Ol9qFM8tRDdF50a+DpZEoB0HFNaEnCp/BSVyBRlg=\ngithub.com/anchore/go-version v1.2.2-0.20210903204242-51efa5b487c4 h1:rmZG77uXgE+o2gozGEBoUMpX27lsku+xrMwlmBZJtbg=\ngithub.com/anchore/go-version v1.2.2-0.20210903204242-51efa5b487c4/go.mod h1:Bkc+JYWjMCF8OyZ340IMSIi2Ebf3uwByOk6ho4wne1E=\ngithub.com/anchore/packageurl-go v0.1.1-0.20250220190351-d62adb6e1115 h1:ZyRCmiEjnoGJZ1+Ah0ZZ/mKKqNhGcUZBl0s7PTTDzvY=\ngithub.com/anchore/packageurl-go v0.1.1-0.20250220190351-d62adb6e1115/go.mod h1:KoYIv7tdP5+CC9VGkeZV4/vGCKsY55VvoG+5dadg4YI=\ngithub.com/anchore/stereoscope v0.1.22 h1:L807G/kk0WZzOCGuRGF7knxMKzwW2PGdbPVRystryd8=\ngithub.com/anchore/stereoscope v0.1.22/go.mod h1:FikPtAb/WnbqwgLHAvQA9O+fWez0K4pbjxzghz++iy4=\ngithub.com/anchore/syft v1.42.3 h1:eIeeGyqfXm/C8wpBWU50xFbOjdL37VbLatMj9nEJ6n4=\ngithub.com/anchore/syft v1.42.3/go.mod h1:i2PZ+276IdPcnd/n32aeIv849iO/QqdjRknbIc39yL0=\ngithub.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=\ngithub.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=\ngithub.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=\ngithub.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=\ngithub.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=\ngithub.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=\ngithub.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY=\ngithub.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4=\ngithub.com/aquasecurity/go-pep440-version v0.0.1 h1:8VKKQtH2aV61+0hovZS3T//rUF+6GDn18paFTVS0h0M=\ngithub.com/aquasecurity/go-pep440-version v0.0.1/go.mod h1:3naPe+Bp6wi3n4l5iBFCZgS0JG8vY6FT0H4NGhFJ+i4=\ngithub.com/aquasecurity/go-version v0.0.1 h1:4cNl516agK0TCn5F7mmYN+xVs1E3S45LkgZk3cbaW2E=\ngithub.com/aquasecurity/go-version v0.0.1/go.mod h1:s1UU6/v2hctXcOa3OLwfj5d9yoXHa3ahf+ipSwEvGT0=\ngithub.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de h1:FxWPpzIjnTlhPwqqXc4/vE0f7GvRjuAsbW+HOIe8KnA=\ngithub.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de/go.mod h1:DCaWoUhZrYW9p1lxo/cm8EmUOOzAPSEZNGF2DK1dJgw=\ngithub.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=\ngithub.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=\ngithub.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc=\ngithub.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=\ngithub.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=\ngithub.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=\ngithub.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=\ngithub.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=\ngithub.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=\ngithub.com/aws/aws-sdk-go-v2 v1.41.2 h1:LuT2rzqNQsauaGkPK/7813XxcZ3o3yePY0Iy891T2ls=\ngithub.com/aws/aws-sdk-go-v2 v1.41.2/go.mod h1:IvvlAZQXvTXznUPfRVfryiG1fbzE2NGK6m9u39YQ+S4=\ngithub.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 h1:489krEF9xIGkOaaX3CE/Be2uWjiXrkCH6gUX+bZA/BU=\ngithub.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4/go.mod h1:IOAPF6oT9KCsceNTvvYMNHy0+kMF8akOjeDvPENWxp4=\ngithub.com/aws/aws-sdk-go-v2/config v1.32.10 h1:9DMthfO6XWZYLfzZglAgW5Fyou2nRI5CuV44sTedKBI=\ngithub.com/aws/aws-sdk-go-v2/config v1.32.10/go.mod h1:2rUIOnA2JaiqYmSKYmRJlcMWy6qTj1vuRFscppSBMcw=\ngithub.com/aws/aws-sdk-go-v2/credentials v1.19.10 h1:EEhmEUFCE1Yhl7vDhNOI5OCL/iKMdkkYFTRpZXNw7m8=\ngithub.com/aws/aws-sdk-go-v2/credentials v1.19.10/go.mod h1:RnnlFCAlxQCkN2Q379B67USkBMu1PipEEiibzYN5UTE=\ngithub.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18 h1:Ii4s+Sq3yDfaMLpjrJsqD6SmG/Wq/P5L/hw2qa78UAY=\ngithub.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18/go.mod h1:6x81qnY++ovptLE6nWQeWrpXxbnlIex+4H4eYYGcqfc=\ngithub.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18 h1:F43zk1vemYIqPAwhjTjYIz0irU2EY7sOb/F5eJ3HuyM=\ngithub.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18/go.mod h1:w1jdlZXrGKaJcNoL+Nnrj+k5wlpGXqnNrKoP22HvAug=\ngithub.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18 h1:xCeWVjj0ki0l3nruoyP2slHsGArMxeiiaoPN5QZH6YQ=\ngithub.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18/go.mod h1:r/eLGuGCBw6l36ZRWiw6PaZwPXb6YOj+i/7MizNl5/k=\ngithub.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=\ngithub.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=\ngithub.com/aws/aws-sdk-go-v2/internal/v4a v1.4.17 h1:JqcdRG//czea7Ppjb+g/n4o8i/R50aTBHkA7vu0lK+k=\ngithub.com/aws/aws-sdk-go-v2/internal/v4a v1.4.17/go.mod h1:CO+WeGmIdj/MlPel2KwID9Gt7CNq4M65HUfBW97liM0=\ngithub.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5 h1:CeY9LUdur+Dxoeldqoun6y4WtJ3RQtzk0JMP2gfUay0=\ngithub.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5/go.mod h1:AZLZf2fMaahW5s/wMRciu1sYbdsikT/UHwbUjOdEVTc=\ngithub.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.8 h1:Z5EiPIzXKewUQK0QTMkutjiaPVeVYXX7KIqhXu/0fXs=\ngithub.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.8/go.mod h1:FsTpJtvC4U1fyDXk7c71XoDv3HlRm8V3NiYLeYLh5YE=\ngithub.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18 h1:LTRCYFlnnKFlKsyIQxKhJuDuA3ZkrDQMRYm6rXiHlLY=\ngithub.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18/go.mod h1:XhwkgGG6bHSd00nO/mexWTcTjgd6PjuvWQMqSn2UaEk=\ngithub.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.17 h1:bGeHBsGZx0Dvu/eJC0Lh9adJa3M1xREcndxLNZlve2U=\ngithub.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.17/go.mod h1:dcW24lbU0CzHusTE8LLHhRLI42ejmINN8Lcr22bwh/g=\ngithub.com/aws/aws-sdk-go-v2/service/s3 v1.96.0 h1:oeu8VPlOre74lBA/PMhxa5vewaMIMmILM+RraSyB8KA=\ngithub.com/aws/aws-sdk-go-v2/service/s3 v1.96.0/go.mod h1:5jggDlZ2CLQhwJBiZJb4vfk4f0GxWdEDruWKEJ1xOdo=\ngithub.com/aws/aws-sdk-go-v2/service/signin v1.0.6 h1:MzORe+J94I+hYu2a6XmV5yC9huoTv8NRcCrUNedDypQ=\ngithub.com/aws/aws-sdk-go-v2/service/signin v1.0.6/go.mod h1:hXzcHLARD7GeWnifd8j9RWqtfIgxj4/cAtIVIK7hg8g=\ngithub.com/aws/aws-sdk-go-v2/service/sso v1.30.11 h1:7oGD8KPfBOJGXiCoRKrrrQkbvCp8N++u36hrLMPey6o=\ngithub.com/aws/aws-sdk-go-v2/service/sso v1.30.11/go.mod h1:0DO9B5EUJQlIDif+XJRWCljZRKsAFKh3gpFz7UnDtOo=\ngithub.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15 h1:edCcNp9eGIUDUCrzoCu1jWAXLGFIizeqkdkKgRlJwWc=\ngithub.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15/go.mod h1:lyRQKED9xWfgkYC/wmmYfv7iVIM68Z5OQ88ZdcV1QbU=\ngithub.com/aws/aws-sdk-go-v2/service/sts v1.41.7 h1:NITQpgo9A5NrDZ57uOWj+abvXSb83BbyggcUBVksN7c=\ngithub.com/aws/aws-sdk-go-v2/service/sts v1.41.7/go.mod h1:sks5UWBhEuWYDPdwlnRFn1w7xWdH29Jcpe+/PJQefEs=\ngithub.com/aws/smithy-go v1.24.1 h1:VbyeNfmYkWoxMVpGUAbQumkODcYmfMRfZ8yQiH30SK0=\ngithub.com/aws/smithy-go v1.24.1/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=\ngithub.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=\ngithub.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=\ngithub.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=\ngithub.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=\ngithub.com/becheran/wildmatch-go v1.0.0 h1:mE3dGGkTmpKtT4Z+88t8RStG40yN9T+kFEGj2PZFSzA=\ngithub.com/becheran/wildmatch-go v1.0.0/go.mod h1:gbMvj0NtVdJ15Mg/mH9uxk2R1QCistMyU7d9KFzroX4=\ngithub.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=\ngithub.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=\ngithub.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=\ngithub.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1UJrqV3uuy861HCTo708pDMbjHHdCas=\ngithub.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4=\ngithub.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=\ngithub.com/bitnami/go-version v0.0.0-20250505154626-452e8c5ee607 h1:lBg3tHGquFySSblLi9zNi2iGNmVLRHBzVal2fqphCM8=\ngithub.com/bitnami/go-version v0.0.0-20250505154626-452e8c5ee607/go.mod h1:9iglf1GG4oNRJ39bZ5AZrjgAFD2RwQbXw6Qf7Cs47wo=\ngithub.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb h1:m935MPodAbYS46DG4pJSv7WO+VECIWUQ7OJYSoTrMh4=\ngithub.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb/go.mod h1:PkYb9DJNAwrSvRx5DYA+gUcOIgTGVMNkfSCbZM8cWpI=\ngithub.com/bmatcuk/doublestar/v2 v2.0.4 h1:6I6oUiT/sU27eE2OFcWqBhL1SwjyvQuOssxT4a1yidI=\ngithub.com/bmatcuk/doublestar/v2 v2.0.4/go.mod h1:QMmcs3H2AUQICWhfzLXz+IYln8lRQmTZRptLie8RgRw=\ngithub.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs=\ngithub.com/bmatcuk/doublestar/v4 v4.10.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=\ngithub.com/bodgit/plumbing v1.3.0 h1:pf9Itz1JOQgn7vEOE7v7nlEfBykYqvUYioC61TwWCFU=\ngithub.com/bodgit/plumbing v1.3.0/go.mod h1:JOTb4XiRu5xfnmdnDJo6GmSbSbtSyufrsyZFByMtKEs=\ngithub.com/bodgit/sevenzip v1.6.1 h1:kikg2pUMYC9ljU7W9SaqHXhym5HyKm8/M/jd31fYan4=\ngithub.com/bodgit/sevenzip v1.6.1/go.mod h1:GVoYQbEVbOGT8n2pfqCIMRUaRjQ8F9oSqoBEqZh5fQ8=\ngithub.com/bodgit/windows v1.0.1 h1:tF7K6KOluPYygXa3Z2594zxlkbKPAOvqr97etrGNIz4=\ngithub.com/bodgit/windows v1.0.1/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM=\ngithub.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oMMlVBbn9M=\ngithub.com/bradleyjkemp/cupaloy/v2 v2.8.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0=\ngithub.com/buger/jsonparser v1.1.2 h1:frqHqw7otoVbk5M8LlE/L7HTnIq2v9RX6EJ48i9AxJk=\ngithub.com/buger/jsonparser v1.1.2/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=\ngithub.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=\ngithub.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=\ngithub.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=\ngithub.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=\ngithub.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=\ngithub.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=\ngithub.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc=\ngithub.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E=\ngithub.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=\ngithub.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=\ngithub.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk=\ngithub.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk=\ngithub.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ=\ngithub.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=\ngithub.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=\ngithub.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=\ngithub.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=\ngithub.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=\ngithub.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=\ngithub.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=\ngithub.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=\ngithub.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=\ngithub.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=\ngithub.com/chromedp/chromedp v0.9.2/go.mod h1:LkSXJKONWTCHAfQasKFUZI+mxqS4tZqhmtGzzhLsnLs=\ngithub.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww=\ngithub.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=\ngithub.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=\ngithub.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=\ngithub.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk=\ngithub.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=\ngithub.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=\ngithub.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=\ngithub.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=\ngithub.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=\ngithub.com/clipperhouse/displaywidth v0.10.0 h1:GhBG8WuerxjFQQYeuZAeVTuyxuX+UraiZGD4HJQ3Y8g=\ngithub.com/clipperhouse/displaywidth v0.10.0/go.mod h1:XqJajYsaiEwkxOj4bowCTMcT1SgvHo9flfF3jQasdbs=\ngithub.com/clipperhouse/uax29/v2 v2.6.0 h1:z0cDbUV+aPASdFb2/ndFnS9ts/WNXgTNNGFoKXuhpos=\ngithub.com/clipperhouse/uax29/v2 v2.6.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=\ngithub.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=\ngithub.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=\ngithub.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=\ngithub.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=\ngithub.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=\ngithub.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=\ngithub.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=\ngithub.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=\ngithub.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=\ngithub.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=\ngithub.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=\ngithub.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=\ngithub.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w=\ngithub.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI=\ngithub.com/containerd/cgroups/v3 v3.1.2 h1:OSosXMtkhI6Qove637tg1XgK4q+DhR0mX8Wi8EhrHa4=\ngithub.com/containerd/cgroups/v3 v3.1.2/go.mod h1:PKZ2AcWmSBsY/tJUVhtS/rluX0b1uq1GmPO1ElCmbOw=\ngithub.com/containerd/containerd/api v1.10.0 h1:5n0oHYVBwN4VhoX9fFykCV9dF1/BvAXeg2F8W6UYq1o=\ngithub.com/containerd/containerd/api v1.10.0/go.mod h1:NBm1OAk8ZL+LG8R0ceObGxT5hbUYj7CzTmR3xh0DlMM=\ngithub.com/containerd/containerd/v2 v2.2.1 h1:TpyxcY4AL5A+07dxETevunVS5zxqzuq7ZqJXknM11yk=\ngithub.com/containerd/containerd/v2 v2.2.1/go.mod h1:NR70yW1iDxe84F2iFWbR9xfAN0N2F0NcjTi1OVth4nU=\ngithub.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4=\ngithub.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE=\ngithub.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=\ngithub.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=\ngithub.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=\ngithub.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=\ngithub.com/containerd/fifo v1.1.0 h1:4I2mbh5stb1u6ycIABlBw9zgtlK8viPI9QkQNRQEEmY=\ngithub.com/containerd/fifo v1.1.0/go.mod h1:bmC4NWMbXlt2EZ0Hc7Fx7QzTFxgPID13eH0Qu+MAb2o=\ngithub.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=\ngithub.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=\ngithub.com/containerd/platforms v1.0.0-rc.2 h1:0SPgaNZPVWGEi4grZdV8VRYQn78y+nm6acgLGv/QzE4=\ngithub.com/containerd/platforms v1.0.0-rc.2/go.mod h1:J71L7B+aiM5SdIEqmd9wp6THLVRzJGXfNuWCZCllLA4=\ngithub.com/containerd/plugin v1.0.0 h1:c8Kf1TNl6+e2TtMHZt+39yAPDbouRH9WAToRjex483Y=\ngithub.com/containerd/plugin v1.0.0/go.mod h1:hQfJe5nmWfImiqT1q8Si3jLv3ynMUIBB47bQ+KexvO8=\ngithub.com/containerd/stargz-snapshotter/estargz v0.18.2 h1:yXkZFYIzz3eoLwlTUZKz2iQ4MrckBxJjkmD16ynUTrw=\ngithub.com/containerd/stargz-snapshotter/estargz v0.18.2/go.mod h1:XyVU5tcJ3PRpkA9XS2T5us6Eg35yM0214Y+wvrZTBrY=\ngithub.com/containerd/ttrpc v1.2.7 h1:qIrroQvuOL9HQ1X6KHe2ohc7p+HP/0VE6XPU7elJRqQ=\ngithub.com/containerd/ttrpc v1.2.7/go.mod h1:YCXHsb32f+Sq5/72xHubdiJRQY9inL4a4ZQrAbN1q9o=\ngithub.com/containerd/typeurl/v2 v2.2.3 h1:yNA/94zxWdvYACdYO8zofhrTVuQY73fFU1y++dYSw40=\ngithub.com/containerd/typeurl/v2 v2.2.3/go.mod h1:95ljDnPfD3bAbDJRugOiShd/DlAAsxGtUBhJxIn7SCk=\ngithub.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=\ngithub.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=\ngithub.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=\ngithub.com/cyphar/filepath-securejoin v0.6.0 h1:BtGB77njd6SVO6VztOHfPxKitJvd/VPT+OFBFMOi1Is=\ngithub.com/cyphar/filepath-securejoin v0.6.0/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=\ngithub.com/dave/jennifer v1.7.1 h1:B4jJJDHelWcDhlRQxWeo0Npa/pYKBLrirAQoTN45txo=\ngithub.com/dave/jennifer v1.7.1/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3RmGZc=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/deitch/magic v0.0.0-20240306090643-c67ab88f10cb h1:4W/2rQ3wzEimF5s+J6OY3ODiQtJZ5W1sForSgogVXkY=\ngithub.com/deitch/magic v0.0.0-20240306090643-c67ab88f10cb/go.mod h1:B3tI9iGHi4imdLi4Asdha1Sc6feLMTfPLXh9IUYmysk=\ngithub.com/dgrijalva/jwt-go/v4 v4.0.0-preview1/go.mod h1:+hnT3ywWDTAFrW5aE+u2Sa/wT555ZqwoCS+pk3p6ry4=\ngithub.com/diskfs/go-diskfs v1.7.0 h1:vonWmt5CMowXwUc79jWyGrf2DIMeoOjkLlMnQYGVOs8=\ngithub.com/diskfs/go-diskfs v1.7.0/go.mod h1:LhQyXqOugWFRahYUSw47NyZJPezFzB9UELwhpszLP/k=\ngithub.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=\ngithub.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=\ngithub.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=\ngithub.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=\ngithub.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=\ngithub.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=\ngithub.com/docker/cli v29.3.0+incompatible h1:z3iWveU7h19Pqx7alZES8j+IeFQZ1lhTwb2F+V9SVvk=\ngithub.com/docker/cli v29.3.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=\ngithub.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=\ngithub.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=\ngithub.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM=\ngithub.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=\ngithub.com/docker/docker-credential-helpers v0.9.5 h1:EFNN8DHvaiK8zVqFA2DT6BjXE0GzfLOZ38ggPTKePkY=\ngithub.com/docker/docker-credential-helpers v0.9.5/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c=\ngithub.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=\ngithub.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=\ngithub.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=\ngithub.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=\ngithub.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 h1:2tV76y6Q9BB+NEBasnqvs7e49aEBFI8ejC89PSnWH+4=\ngithub.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s=\ngithub.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=\ngithub.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=\ngithub.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=\ngithub.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=\ngithub.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=\ngithub.com/elliotchance/phpserialize v1.4.0 h1:cAp/9+KSnEbUC8oYCE32n2n84BeW8HOY3HMDI8hG2OY=\ngithub.com/elliotchance/phpserialize v1.4.0/go.mod h1:gt7XX9+ETUcLXbtTKEuyrqW3lcLUAeS/AnGZ2e49TZs=\ngithub.com/elliotwutingfeng/asciiset v0.0.0-20230602022725-51bbb787efab h1:h1UgjJdAAhj+uPL68n7XASS6bU+07ZX1WJvVS2eyoeY=\ngithub.com/elliotwutingfeng/asciiset v0.0.0-20230602022725-51bbb787efab/go.mod h1:GLo/8fDswSAniFG+BFIaiSPcK610jyzgEhWYPQwuQdw=\ngithub.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=\ngithub.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=\ngithub.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=\ngithub.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=\ngithub.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=\ngithub.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=\ngithub.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=\ngithub.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=\ngithub.com/envoyproxy/go-control-plane v0.10.1/go.mod h1:AY7fTTXNdv/aJ2O5jwpxAPOWUZ7hQAEvzN5Pf27BkQQ=\ngithub.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA=\ngithub.com/envoyproxy/go-control-plane v0.14.0/go.mod h1:NcS5X47pLl/hfqxU70yPwL9ZMkUlwlKxtAohpi2wBEU=\ngithub.com/envoyproxy/go-control-plane/envoy v1.36.0 h1:yg/JjO5E7ubRyKX3m07GF3reDNEnfOboJ0QySbH736g=\ngithub.com/envoyproxy/go-control-plane/envoy v1.36.0/go.mod h1:ty89S1YCCVruQAm9OtKeEkQLTb+Lkz0k8v9W0Oxsv98=\ngithub.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI=\ngithub.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4=\ngithub.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=\ngithub.com/envoyproxy/protoc-gen-validate v0.6.2/go.mod h1:2t7qjJNvHPx8IjnBOzl9E9/baC+qXE/TeeyBRzgJDws=\ngithub.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg5VPuZ0uONDT6eb4=\ngithub.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA=\ngithub.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=\ngithub.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=\ngithub.com/facebookincubator/flog v0.0.0-20190930132826-d2511d0ce33c/go.mod h1:QGzNH9ujQ2ZUr/CjDGZGWeDAVStrWNjHeEcjJL96Nuk=\ngithub.com/facebookincubator/nvdtools v0.1.5 h1:jbmDT1nd6+k+rlvKhnkgMokrCAzHoASWE5LtHbX2qFQ=\ngithub.com/facebookincubator/nvdtools v0.1.5/go.mod h1:Kh55SAWnjckS96TBSrXI99KrEKH4iB0OJby3N8GRJO4=\ngithub.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=\ngithub.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=\ngithub.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=\ngithub.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=\ngithub.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=\ngithub.com/fatih/set v0.2.1 h1:nn2CaJyknWE/6txyUDGwysr3G5QC6xWB/PtVjPBbeaA=\ngithub.com/fatih/set v0.2.1/go.mod h1:+RKtMCH+favT2+3YecHGxcc0b4KyVWA1QWWJUs4E0CI=\ngithub.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw=\ngithub.com/felixge/fgprof v0.9.5 h1:8+vR6yu2vvSKn08urWyEuxx75NWPEvybbkBirEpsbVY=\ngithub.com/felixge/fgprof v0.9.5/go.mod h1:yKl+ERSa++RYOs32d8K6WEXCB4uXdLls4ZaZPpayhMM=\ngithub.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=\ngithub.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=\ngithub.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=\ngithub.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=\ngithub.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=\ngithub.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=\ngithub.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=\ngithub.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=\ngithub.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=\ngithub.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=\ngithub.com/github/go-spdx/v2 v2.4.0 h1:+4IwVwJJbm3rzvrQ6P1nI9BDMcy3la4RchRy5uehV/M=\ngithub.com/github/go-spdx/v2 v2.4.0/go.mod h1:/5rwgS0txhGtRdUZwc02bTglzg6HK3FfuEbECKlK2Sg=\ngithub.com/gkampitakis/ciinfo v0.3.2 h1:JcuOPk8ZU7nZQjdUhctuhQofk7BGHuIy0c9Ez8BNhXs=\ngithub.com/gkampitakis/ciinfo v0.3.2/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo=\ngithub.com/gkampitakis/go-snaps v0.5.20 h1:FGKonEeQPJ12t7RQj6cTPa881fl5c8HYarMLv5vP7sg=\ngithub.com/gkampitakis/go-snaps v0.5.20/go.mod h1:gC3YqxQTPyIXvQrw/Vpt3a8VqR1MO8sVpZFWN4DGwNs=\ngithub.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=\ngithub.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=\ngithub.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=\ngithub.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=\ngithub.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=\ngithub.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=\ngithub.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=\ngithub.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=\ngithub.com/go-git/go-billy/v5 v5.8.0 h1:I8hjc3LbBlXTtVuFNJuwYuMiHvQJDq1AT6u4DwDzZG0=\ngithub.com/go-git/go-billy/v5 v5.8.0/go.mod h1:RpvI/rw4Vr5QA+Z60c6d6LXH0rYJo0uD5SqfmrrheCY=\ngithub.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=\ngithub.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=\ngithub.com/go-git/go-git/v5 v5.17.0 h1:AbyI4xf+7DsjINHMu35quAh4wJygKBKBuXVjV/pxesM=\ngithub.com/go-git/go-git/v5 v5.17.0/go.mod h1:f82C4YiLx+Lhi8eHxltLeGC5uBTXSFa6PC5WW9o4SjI=\ngithub.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=\ngithub.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=\ngithub.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=\ngithub.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=\ngithub.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=\ngithub.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=\ngithub.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=\ngithub.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=\ngithub.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=\ngithub.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=\ngithub.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=\ngithub.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=\ngithub.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=\ngithub.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=\ngithub.com/go-restruct/restruct v1.2.0-alpha h1:2Lp474S/9660+SJjpVxoKuWX09JsXHSrdV7Nv3/gkvc=\ngithub.com/go-restruct/restruct v1.2.0-alpha/go.mod h1:KqrpKpn4M8OLznErihXTGLlsXFGeLxHUrLRRI/1YjGk=\ngithub.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=\ngithub.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=\ngithub.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=\ngithub.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=\ngithub.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=\ngithub.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=\ngithub.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=\ngithub.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=\ngithub.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=\ngithub.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=\ngithub.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=\ngithub.com/gocsaf/csaf/v3 v3.5.1 h1:jTA1fLrK0/JIczPs7itTD53qANoO4tn2VaGvUeitePc=\ngithub.com/gocsaf/csaf/v3 v3.5.1/go.mod h1:pga89lE+iWJm7smTdzYcXuetYUbgY8caXfaIP4BJG98=\ngithub.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=\ngithub.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=\ngithub.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=\ngithub.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=\ngithub.com/gohugoio/hashstructure v0.6.0 h1:7wMB/2CfXoThFYhdWRGv3u3rUM761Cq29CxUW+NltUg=\ngithub.com/gohugoio/hashstructure v0.6.0/go.mod h1:lapVLk9XidheHG1IQ4ZSbyYrXcaILU1ZEP/+vno5rBQ=\ngithub.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=\ngithub.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=\ngithub.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=\ngithub.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=\ngithub.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=\ngithub.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=\ngithub.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=\ngithub.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=\ngithub.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=\ngithub.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=\ngithub.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=\ngithub.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=\ngithub.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=\ngithub.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=\ngithub.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=\ngithub.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=\ngithub.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=\ngithub.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=\ngithub.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=\ngithub.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=\ngithub.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=\ngithub.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=\ngithub.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=\ngithub.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=\ngithub.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=\ngithub.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=\ngithub.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=\ngithub.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=\ngithub.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=\ngithub.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=\ngithub.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=\ngithub.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=\ngithub.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\ngithub.com/google/go-containerregistry v0.21.2 h1:vYaMU4nU55JJGFC9JR/s8NZcTjbE9DBBbvusTW9NeS0=\ngithub.com/google/go-containerregistry v0.21.2/go.mod h1:ctO5aCaewH4AK1AumSF5DPW+0+R+d2FmylMJdp5G7p0=\ngithub.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=\ngithub.com/google/licensecheck v0.3.1 h1:QoxgoDkaeC4nFrtGN1jV7IPmDCHFNIVh54e5hSt6sPs=\ngithub.com/google/licensecheck v0.3.1/go.mod h1:ORkR35t/JjW+emNKtfJDII0zlciG9JgbT7SmsohlHmY=\ngithub.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no=\ngithub.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=\ngithub.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=\ngithub.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=\ngithub.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk=\ngithub.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc=\ngithub.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0=\ngithub.com/google/osv-scanner v1.9.2 h1:N5Arl9SA75afbjmX8mKURgOIaKyuK3NUjCaxDlj1KHI=\ngithub.com/google/osv-scanner v1.9.2/go.mod h1:ZTL8Dp9z/7Jr9kkQSOGqo8z6Csqt83qMIr58aZVx+pM=\ngithub.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=\ngithub.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=\ngithub.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg=\ngithub.com/google/pprof v0.0.0-20240227163752-401108e1b7e7/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=\ngithub.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5 h1:xhMrHhTJ6zxu3gA4enFM9MLn9AY7613teCdFnlUVbSQ=\ngithub.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA=\ngithub.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=\ngithub.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=\ngithub.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=\ngithub.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=\ngithub.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=\ngithub.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/googleapis/enterprise-certificate-proxy v0.3.11 h1:vAe81Msw+8tKUxi2Dqh/NZMz7475yUvmRIkXr4oN2ao=\ngithub.com/googleapis/enterprise-certificate-proxy v0.3.11/go.mod h1:RFV7MUdlb7AgEq2v7FmMCfeSMCllAzWxFgRdusoGks8=\ngithub.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=\ngithub.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=\ngithub.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0=\ngithub.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM=\ngithub.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ1J6SMc=\ngithub.com/googleapis/gax-go/v2 v2.17.0/go.mod h1:mzaqghpQp4JDh3HvADwrat+6M3MOIDp5YKHhb9PAgDY=\ngithub.com/gookit/assert v0.1.1 h1:lh3GcawXe/p+cU7ESTZ5Ui3Sm/x8JWpIis4/1aF0mY0=\ngithub.com/gookit/assert v0.1.1/go.mod h1:jS5bmIVQZTIwk42uXl4lyj4iaaxx32tqH16CFj0VX2E=\ngithub.com/gookit/color v1.2.5/go.mod h1:AhIE+pS6D4Ql0SQWbBeXPHw7gY0/sjHoA4s/n1KB7xg=\ngithub.com/gookit/color v1.6.0 h1:JjJXBTk1ETNyqyilJhkTXJYYigHG24TM9Xa2M1xAhRA=\ngithub.com/gookit/color v1.6.0/go.mod h1:9ACFc7/1IpHGBW8RwuDm/0YEnhg3dwwXpoMsmtyHfjs=\ngithub.com/gpustack/gguf-parser-go v0.24.0 h1:tdJceXYp9e5RhE9RwVYIuUpir72Jz2D68NEtDXkKCKc=\ngithub.com/gpustack/gguf-parser-go v0.24.0/go.mod h1:y4TwTtDqFWTK+xvprOjRUh+dowgU2TKCX37vRKvGiZ0=\ngithub.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo=\ngithub.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=\ngithub.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 h1:e9Rjr40Z98/clHv5Yg79Is0NtosR5LXRvdr7o/6NwbA=\ngithub.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1/go.mod h1:tIxuGz/9mpox++sgp9fJjHO0+q1X9/UOWd798aAm22M=\ngithub.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b h1:wDUNC2eKiL35DbLvsDhiblTUXHxcOPwQSCzi7xpQUN4=\ngithub.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b/go.mod h1:VzxiSdG6j1pi7rwGm/xYI5RbtpBgM8sARDXlvEvxlu0=\ngithub.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.70 h1:0HADrxxqaQkGycO1JoUUA+B4FnIkuo8d2bz/hSaTFFQ=\ngithub.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.70/go.mod h1:fm2FdDCzJdtbXF7WKAMvBb5NEPouXPHFbGNYs9ShFns=\ngithub.com/hashicorp/consul/api v1.11.0/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M=\ngithub.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms=\ngithub.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=\ngithub.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=\ngithub.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=\ngithub.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=\ngithub.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=\ngithub.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=\ngithub.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=\ngithub.com/hashicorp/go-getter v1.8.5 h1:DMPV5CSw5JrNg/IK7kDZt3+l2REKXOi3oAw7uYLh2NM=\ngithub.com/hashicorp/go-getter v1.8.5/go.mod h1:WIffejwAyDSJhoVptc3UEshEMkR9O63rw34V7k43O3Q=\ngithub.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=\ngithub.com/hashicorp/go-hclog v1.0.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=\ngithub.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=\ngithub.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=\ngithub.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=\ngithub.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=\ngithub.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=\ngithub.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=\ngithub.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=\ngithub.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=\ngithub.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=\ngithub.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=\ngithub.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=\ngithub.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=\ngithub.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=\ngithub.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4=\ngithub.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=\ngithub.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=\ngithub.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=\ngithub.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=\ngithub.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=\ngithub.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=\ngithub.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=\ngithub.com/hashicorp/hcl/v2 v2.24.0 h1:2QJdZ454DSsYGoaE6QheQZjtKZSUs9Nh2izTWiwQxvE=\ngithub.com/hashicorp/hcl/v2 v2.24.0/go.mod h1:oGoO1FIQYfn/AgyOhlg9qLC6/nOJPX3qGbkZpYAcqfM=\ngithub.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=\ngithub.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY=\ngithub.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc=\ngithub.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE=\ngithub.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE=\ngithub.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk=\ngithub.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4=\ngithub.com/henvic/httpretty v0.1.4 h1:Jo7uwIRWVFxkqOnErcoYfH90o3ddQyVrSANeS4cxYmU=\ngithub.com/henvic/httpretty v0.1.4/go.mod h1:Dn60sQTZfbt2dYsdUSNsCljyF4AfdqnuJFDLJA1I4AM=\ngithub.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=\ngithub.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=\ngithub.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=\ngithub.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI=\ngithub.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=\ngithub.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=\ngithub.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=\ngithub.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=\ngithub.com/ianlancetaylor/demangle v0.0.0-20230524184225-eabc099b10ab/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw=\ngithub.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=\ngithub.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=\ngithub.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=\ngithub.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E=\ngithub.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=\ngithub.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=\ngithub.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=\ngithub.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=\ngithub.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=\ngithub.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=\ngithub.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=\ngithub.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=\ngithub.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=\ngithub.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=\ngithub.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=\ngithub.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=\ngithub.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=\ngithub.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=\ngithub.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=\ngithub.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=\ngithub.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=\ngithub.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=\ngithub.com/kastenhq/goversion v0.0.0-20230811215019-93b2f8823953 h1:WdAeg/imY2JFPc/9CST4bZ80nNJbiBFCAdSZCSgrS5Y=\ngithub.com/kastenhq/goversion v0.0.0-20230811215019-93b2f8823953/go.mod h1:6o+UrvuZWc4UTyBhQf0LGjW9Ld7qJxLz/OqvSOWWlEc=\ngithub.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=\ngithub.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=\ngithub.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=\ngithub.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=\ngithub.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=\ngithub.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=\ngithub.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=\ngithub.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=\ngithub.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=\ngithub.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=\ngithub.com/knqyf263/go-apk-version v0.0.0-20200609155635-041fdbb8563f h1:GvCU5GXhHq+7LeOzx/haG7HSIZokl3/0GkoUFzsRJjg=\ngithub.com/knqyf263/go-apk-version v0.0.0-20200609155635-041fdbb8563f/go.mod h1:q59u9px8b7UTj0nIjEjvmTWekazka6xIt6Uogz5Dm+8=\ngithub.com/knqyf263/go-deb-version v0.0.0-20241115132648-6f4aee6ccd23 h1:dWzdsqjh1p2gNtRKqNwuBvKqMNwnLOPLzVZT1n6DK7s=\ngithub.com/knqyf263/go-deb-version v0.0.0-20241115132648-6f4aee6ccd23/go.mod h1:lUaIXCWzf7BRKTY5iEcrYy1TfgbYLYVIS/B2vPkJzOc=\ngithub.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=\ngithub.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=\ngithub.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=\ngithub.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=\ngithub.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=\ngithub.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=\ngithub.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=\ngithub.com/lyft/protoc-gen-star v0.5.3/go.mod h1:V0xaHgaf5oCCqmcxYcWiDfTiKsZsRc87/1qhoTACD8w=\ngithub.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=\ngithub.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=\ngithub.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=\ngithub.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=\ngithub.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=\ngithub.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=\ngithub.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo=\ngithub.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg=\ngithub.com/masahiro331/go-mvn-version v0.0.0-20250131095131-f4974fa13b8a h1:eLvAzVoRfHEOl64OxFhepPf3vj7SKvXY/tFc3BS0b7s=\ngithub.com/masahiro331/go-mvn-version v0.0.0-20250131095131-f4974fa13b8a/go.mod h1:jZ3F25l7DbD7l7DcA8aj7eo1EZ84nbzcQHBB4lCSrI8=\ngithub.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=\ngithub.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=\ngithub.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=\ngithub.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=\ngithub.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=\ngithub.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=\ngithub.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=\ngithub.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=\ngithub.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=\ngithub.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=\ngithub.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=\ngithub.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=\ngithub.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=\ngithub.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=\ngithub.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\ngithub.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75 h1:P8UmIzZMYDR+NGImiFvErt6VWfIRPuGM+vyjiEdkmIw=\ngithub.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=\ngithub.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=\ngithub.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=\ngithub.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=\ngithub.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=\ngithub.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=\ngithub.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=\ngithub.com/mholt/archives v0.1.5 h1:Fh2hl1j7VEhc6DZs2DLMgiBNChUux154a1G+2esNvzQ=\ngithub.com/mholt/archives v0.1.5/go.mod h1:3TPMmBLPsgszL+1As5zECTuKwKvIfj6YcwWPpeTAXF4=\ngithub.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=\ngithub.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=\ngithub.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=\ngithub.com/mikelolasagasti/xz v1.0.1 h1:Q2F2jX0RYJUG3+WsM+FJknv+6eVjsjXNDV0KJXZzkD0=\ngithub.com/mikelolasagasti/xz v1.0.1/go.mod h1:muAirjiOUxPRXwm9HdDtB3uoRPrGnL85XHtokL9Hcgc=\ngithub.com/minio/minlz v1.0.1 h1:OUZUzXcib8diiX+JYxyRLIdomyZYzHct6EShOKtQY2A=\ngithub.com/minio/minlz v1.0.1/go.mod h1:qT0aEB35q79LLornSzeDH75LBf3aH1MV+jB5w9Wasec=\ngithub.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI=\ngithub.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=\ngithub.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=\ngithub.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=\ngithub.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=\ngithub.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=\ngithub.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=\ngithub.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=\ngithub.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=\ngithub.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=\ngithub.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=\ngithub.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=\ngithub.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=\ngithub.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=\ngithub.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=\ngithub.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg=\ngithub.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc=\ngithub.com/moby/moby/api v1.54.0 h1:7kbUgyiKcoBhm0UrWbdrMs7RX8dnwzURKVbZGy2GnL0=\ngithub.com/moby/moby/api v1.54.0/go.mod h1:8mb+ReTlisw4pS6BRzCMts5M49W5M7bKt1cJy/YbAqc=\ngithub.com/moby/moby/client v0.3.0 h1:UUGL5okry+Aomj3WhGt9Aigl3ZOxZGqR7XPo+RLPlKs=\ngithub.com/moby/moby/client v0.3.0/go.mod h1:HJgFbJRvogDQjbM8fqc1MCEm4mIAGMLjXbgwoZp6jCQ=\ngithub.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=\ngithub.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=\ngithub.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg=\ngithub.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4=\ngithub.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=\ngithub.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=\ngithub.com/moby/sys/signal v0.7.1 h1:PrQxdvxcGijdo6UXXo/lU/TvHUWyPhj7UOpSo8tuvk0=\ngithub.com/moby/sys/signal v0.7.1/go.mod h1:Se1VGehYokAkrSQwL4tDzHvETwUZlnY7S5XtQ50mQp8=\ngithub.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=\ngithub.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=\ngithub.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=\ngithub.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=\ngithub.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=\ngithub.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=\ngithub.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=\ngithub.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=\ngithub.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=\ngithub.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8=\ngithub.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=\ngithub.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=\ngithub.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=\ngithub.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=\ngithub.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=\ngithub.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=\ngithub.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=\ngithub.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=\ngithub.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=\ngithub.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=\ngithub.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=\ngithub.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=\ngithub.com/nix-community/go-nix v0.0.0-20250101154619-4bdde671e0a1 h1:kpt9ZfKcm+EDG4s40hMwE//d5SBgDjUOrITReV2u4aA=\ngithub.com/nix-community/go-nix v0.0.0-20250101154619-4bdde671e0a1/go.mod h1:qgCw4bBKZX8qMgGeEZzGFVT3notl42dBjNqO2jut0M0=\ngithub.com/nsf/jsondiff v0.0.0-20210926074059-1e845ec5d249 h1:NHrXEjTNQY7P0Zfx1aMrNhpgxHmow66XQtm0aQLY0AE=\ngithub.com/nsf/jsondiff v0.0.0-20210926074059-1e845ec5d249/go.mod h1:mpRZBD8SJ55OIICQ3iWH0Yz3cjzA61JdqMLoWXeB2+8=\ngithub.com/nwaples/rardecode/v2 v2.2.0 h1:4ufPGHiNe1rYJxYfehALLjup4Ls3ck42CWwjKiOqu0A=\ngithub.com/nwaples/rardecode/v2 v2.2.0/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw=\ngithub.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc=\ngithub.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0=\ngithub.com/olekukonko/errors v1.2.0 h1:10Zcn4GeV59t/EGqJc8fUjtFT/FuUh5bTMzZ1XwmCRo=\ngithub.com/olekukonko/errors v1.2.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=\ngithub.com/olekukonko/ll v0.1.6 h1:lGVTHO+Qc4Qm+fce/2h2m5y9LvqaW+DCN7xW9hsU3uA=\ngithub.com/olekukonko/ll v0.1.6/go.mod h1:NVUmjBb/aCtUpjKk75BhWrOlARz3dqsM+OtszpY4o88=\ngithub.com/olekukonko/tablewriter v1.1.4 h1:ORUMI3dXbMnRlRggJX3+q7OzQFDdvgbN9nVWj1drm6I=\ngithub.com/olekukonko/tablewriter v1.1.4/go.mod h1:+kedxuyTtgoZLwif3P1Em4hARJs+mVnzKxmsCL/C5RY=\ngithub.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=\ngithub.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=\ngithub.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=\ngithub.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=\ngithub.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=\ngithub.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=\ngithub.com/opencontainers/runtime-spec v1.3.0 h1:YZupQUdctfhpZy3TM39nN9Ika5CBWT5diQ8ibYCRkxg=\ngithub.com/opencontainers/runtime-spec v1.3.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=\ngithub.com/opencontainers/selinux v1.13.1 h1:A8nNeceYngH9Ow++M+VVEwJVpdFmrlxsN22F+ISDCJE=\ngithub.com/opencontainers/selinux v1.13.1/go.mod h1:S10WXZ/osk2kWOYKy1x2f/eXF5ZHJoUs8UU/2caNRbg=\ngithub.com/openvex/go-vex v0.2.7 h1:/pN3bqvS4QOc6WkkL0hbKzJuAtsUD9vmvk9IZkzD3Zc=\ngithub.com/openvex/go-vex v0.2.7/go.mod h1:ZyQC3NXl9jjS53JOpBG3LAUXySkW8IlJ/GIhsnf5D54=\ngithub.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=\ngithub.com/owenrumney/go-sarif v1.1.2-0.20231003122901-1000f5e05554 h1:FvA4bwjKpPqik5WsQ8+4z4DKWgA1tO1RTTtNKr5oYNA=\ngithub.com/owenrumney/go-sarif v1.1.2-0.20231003122901-1000f5e05554/go.mod h1:n73K/hcuJ50MiVznXyN4rde6fZY7naGKWBXOLFTyc94=\ngithub.com/package-url/packageurl-go v0.1.3 h1:4juMED3hHiz0set3Vq3KeQ75KD1avthoXLtmE3I0PLs=\ngithub.com/package-url/packageurl-go v0.1.3/go.mod h1:nKAWB8E6uk1MHqiS/lQb9pYBGH2+mdJ2PJc2s50dQY0=\ngithub.com/pandatix/go-cvss v0.6.2 h1:TFiHlzUkT67s6UkelHmK6s1INKVUG7nlKYiWWDTITGI=\ngithub.com/pandatix/go-cvss v0.6.2/go.mod h1:jDXYlQBZrc8nvrMUVVvTG8PhmuShOnKrxP53nOFkt8Q=\ngithub.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=\ngithub.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=\ngithub.com/pborman/indent v1.2.1 h1:lFiviAbISHv3Rf0jcuh489bi06hj98JsVMtIDZQb9yM=\ngithub.com/pborman/indent v1.2.1/go.mod h1:FitS+t35kIYtB5xWTZAPhnmrxcciEEOdbyrrpz5K6Vw=\ngithub.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=\ngithub.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=\ngithub.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=\ngithub.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=\ngithub.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=\ngithub.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=\ngithub.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=\ngithub.com/pjbgf/sha1cd v0.4.0 h1:NXzbL1RvjTUi6kgYZCX3fPwwl27Q1LJndxtUDVfJGRY=\ngithub.com/pjbgf/sha1cd v0.4.0/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=\ngithub.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=\ngithub.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA=\ngithub.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo=\ngithub.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=\ngithub.com/pkg/xattr v0.4.12 h1:rRTkSyFNTRElv6pkA3zpjHpQ90p/OdHQC1GmGh1aTjM=\ngithub.com/pkg/xattr v0.4.12/go.mod h1:di8WF84zAKk8jzR1UBTEWh9AUlIZZ7M/JNt8e9B6ktU=\ngithub.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=\ngithub.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=\ngithub.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=\ngithub.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=\ngithub.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=\ngithub.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=\ngithub.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=\ngithub.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=\ngithub.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=\ngithub.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=\ngithub.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=\ngithub.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=\ngithub.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=\ngithub.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=\ngithub.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=\ngithub.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=\ngithub.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=\ngithub.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=\ngithub.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=\ngithub.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=\ngithub.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=\ngithub.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=\ngithub.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=\ngithub.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=\ngithub.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=\ngithub.com/rust-secure-code/go-rustaudit v0.0.0-20250226111315-e20ec32e963c h1:8gOLsYwaY2JwlTMT4brS5/9XJdrdIbmk2obvQ748CC0=\ngithub.com/rust-secure-code/go-rustaudit v0.0.0-20250226111315-e20ec32e963c/go.mod h1:kwM/7r/rVluTE8qJbHAffduuqmSv4knVQT2IajGvSiA=\ngithub.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=\ngithub.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=\ngithub.com/sagikazarmark/crypt v0.3.0/go.mod h1:uD/D+6UF4SrIR1uGEv7bBNkNqLGqUr43MRiaGWX1Nig=\ngithub.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=\ngithub.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=\ngithub.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA=\ngithub.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=\ngithub.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d h1:hrujxIzL1woJ7AwssoOcM/tq5JjjG2yYOc8odClEiXA=\ngithub.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU=\ngithub.com/sanity-io/litter v1.5.8 h1:uM/2lKrWdGbRXDrIq08Lh9XtVYoeGtcQxk9rtQ7+rYg=\ngithub.com/sanity-io/litter v1.5.8/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U=\ngithub.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ=\ngithub.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=\ngithub.com/sassoftware/go-rpmutils v0.4.0 h1:ojND82NYBxgwrV+mX1CWsd5QJvvEZTKddtCdFLPWhpg=\ngithub.com/sassoftware/go-rpmutils v0.4.0/go.mod h1:3goNWi7PGAT3/dlql2lv3+MSN5jNYPjT5mVcQcIsYzI=\ngithub.com/scylladb/go-set v1.0.3-0.20200225121959-cc7b2070d91e h1:7q6NSFZDeGfvvtIRwBrU/aegEYJYmvev0cHAwo17zZQ=\ngithub.com/scylladb/go-set v1.0.3-0.20200225121959-cc7b2070d91e/go.mod h1:DkpGd78rljTxKAnTDPFqXSGxvETQnJyuSOQwsHycqfs=\ngithub.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4/go.mod h1:C1a7PQSMz9NShzorzCiG2fk9+xuCgLkPeCvMHYR2OWg=\ngithub.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=\ngithub.com/sebdah/goldie/v2 v2.8.0 h1:dZb9wR8q5++oplmEiJT+U/5KyotVD+HNGCAc5gNr8rc=\ngithub.com/sebdah/goldie/v2 v2.8.0/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI=\ngithub.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=\ngithub.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=\ngithub.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=\ngithub.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=\ngithub.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=\ngithub.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=\ngithub.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=\ngithub.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=\ngithub.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=\ngithub.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=\ngithub.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=\ngithub.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=\ngithub.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8=\ngithub.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=\ngithub.com/smallnest/ringbuffer v0.0.0-20241116012123-461381446e3d h1:3VwvTjiRPA7cqtgOWddEL+JrcijMlXUmj99c/6YyZoY=\ngithub.com/smallnest/ringbuffer v0.0.0-20241116012123-461381446e3d/go.mod h1:tAG61zBM1DYRaGIPloumExGvScf08oHuo0kFoOqdbT0=\ngithub.com/sorairolake/lzip-go v0.3.8 h1:j5Q2313INdTA80ureWYRhX+1K78mUXfMoPZCw/ivWik=\ngithub.com/sorairolake/lzip-go v0.3.8/go.mod h1:JcBqGMV0frlxwrsE9sMWXDjqn3EeVf0/54YPsw66qkU=\ngithub.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=\ngithub.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=\ngithub.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=\ngithub.com/spdx/gordf v0.0.0-20250128162952-000978ccd6fb h1:7G2Czq97VORM5xNRrD8tSQdhoXPRs8s+Otlc7st9TS0=\ngithub.com/spdx/gordf v0.0.0-20250128162952-000978ccd6fb/go.mod h1:uKWaldnbMnjsSAXRurWqqrdyZen1R7kxl8TkmWk2OyM=\ngithub.com/spdx/tools-golang v0.5.7 h1:+sWcKGnhwp3vLdMqPcLdA6QK679vd86cK9hQWH3AwCg=\ngithub.com/spdx/tools-golang v0.5.7/go.mod h1:jg7w0LOpoNAw6OxKEzCoqPC2GCTj45LyTlVmXubDsYw=\ngithub.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4=\ngithub.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=\ngithub.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=\ngithub.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=\ngithub.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=\ngithub.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=\ngithub.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=\ngithub.com/spf13/cobra v1.3.0/go.mod h1:BrRVncBjOJa/eUcVVm9CE+oC6as8k+VYr4NY7WCi9V4=\ngithub.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=\ngithub.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=\ngithub.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=\ngithub.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=\ngithub.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/spf13/viper v1.10.0/go.mod h1:SoyBPwAtKDzypXNDFKN5kzH7ppppbGZtls1UpIy5AsM=\ngithub.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=\ngithub.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=\ngithub.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo=\ngithub.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=\ngithub.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=\ngithub.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=\ngithub.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=\ngithub.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=\ngithub.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=\ngithub.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=\ngithub.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=\ngithub.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=\ngithub.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=\ngithub.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngithub.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=\ngithub.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=\ngithub.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=\ngithub.com/sylabs/sif/v2 v2.24.0 h1:1wB5uMDUQYjk8AckTySaDcP9YnpMb1LyDRr1Jt9A10w=\ngithub.com/sylabs/sif/v2 v2.24.0/go.mod h1:DbXWqWZ1hdLSU+K9ipdds5AmZeHWsyxCOj/oQakBa88=\ngithub.com/sylabs/squashfs v1.0.6 h1:PvJcDzxr+vIm2kH56mEMbaOzvGu79gK7P7IX+R7BDZI=\ngithub.com/sylabs/squashfs v1.0.6/go.mod h1:DlDeUawVXLWAsSRa085Eo0ZenGzAB32JdAUFaB0LZfE=\ngithub.com/terminalstatic/go-xsd-validate v0.1.6 h1:TenYeQ3eY631qNi1/cTmLH/s2slHPRKTTHT+XSHkepo=\ngithub.com/terminalstatic/go-xsd-validate v0.1.6/go.mod h1:18lsvYFofBflqCrvo1umpABZ99+GneNTw2kEEc8UPJw=\ngithub.com/therootcompany/xz v1.0.1 h1:CmOtsn1CbtmyYiusbfmhmkpAAETj0wBIH6kCYaX+xzw=\ngithub.com/therootcompany/xz v1.0.1/go.mod h1:3K3UH1yCKgBneZYhuQUvJ9HPD19UEXEI0BWbMn8qNMY=\ngithub.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=\ngithub.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=\ngithub.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=\ngithub.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=\ngithub.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=\ngithub.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=\ngithub.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=\ngithub.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=\ngithub.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=\ngithub.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=\ngithub.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=\ngithub.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=\ngithub.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=\ngithub.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=\ngithub.com/umisama/go-cpe v0.0.0-20190323060751-cdd6c3c28a23 h1:+168JmE638t0OxroPRx7BUbkB91hF3GWS1OkvITgdT0=\ngithub.com/umisama/go-cpe v0.0.0-20190323060751-cdd6c3c28a23/go.mod h1:Jv/KoYWD3+46wW8r3pEwISwtgv5Q8NTfFto2wFRKvoA=\ngithub.com/vbatts/go-mtree v0.7.0 h1:ytmOc3MTRidZiBi9VBCyZ2BHe4fZS47L5v7BVXDWW4E=\ngithub.com/vbatts/go-mtree v0.7.0/go.mod h1:EjdpFC+LZy1TXbRGNa1MKKgjQ+7ew3foMFJK8o4/TdY=\ngithub.com/vbatts/tar-split v0.12.2 h1:w/Y6tjxpeiFMR47yzZPlPj/FcPLpXbTUi/9H7d3CPa4=\ngithub.com/vbatts/tar-split v0.12.2/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA=\ngithub.com/vifraa/gopom v1.0.0 h1:L9XlKbyvid8PAIK8nr0lihMApJQg/12OBvMA28BcWh0=\ngithub.com/vifraa/gopom v1.0.0/go.mod h1:oPa1dcrGrtlO37WPDBm5SqHAT+wTgF8An1Q71Z6Vv4o=\ngithub.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc=\ngithub.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=\ngithub.com/wagoodman/go-partybus v0.0.0-20230516145632-8ccac152c651 h1:jIVmlAFIqV3d+DOxazTR9v+zgj8+VYuQBzPgBZvWBHA=\ngithub.com/wagoodman/go-partybus v0.0.0-20230516145632-8ccac152c651/go.mod h1:b26F2tHLqaoRQf8DywqzVaV1MQ9yvjb0OMcNl7Nxu20=\ngithub.com/wagoodman/go-presenter v0.0.0-20211015174752-f9c01afc824b h1:uWNQ0khA6RdFzODOMwKo9XXu7fuewnnkHykUtuKru8s=\ngithub.com/wagoodman/go-presenter v0.0.0-20211015174752-f9c01afc824b/go.mod h1:ewlIKbKV8l+jCj8rkdXIs361ocR5x3qGyoCSca47Gx8=\ngithub.com/wagoodman/go-progress v0.0.0-20260303201901-10176f79b2c0 h1:EHsPe0Q0ANoLOZff1dBLAyeWLTA4sbPTpGI+2zb0FnM=\ngithub.com/wagoodman/go-progress v0.0.0-20260303201901-10176f79b2c0/go.mod h1:g/D9uEUFp5YLyciwCpVsSOZOm56hfv4rzGJod6MlqIM=\ngithub.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=\ngithub.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=\ngithub.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=\ngithub.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=\ngithub.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=\ngithub.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=\ngithub.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=\ngithub.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=\ngithub.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=\ngithub.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=\ngithub.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=\ngithub.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=\ngithub.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=\ngithub.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=\ngithub.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=\ngithub.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=\ngithub.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=\ngithub.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=\ngithub.com/zclconf/go-cty v1.14.0/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE=\ngithub.com/zclconf/go-cty v1.16.3 h1:osr++gw2T61A8KVYHoQiFbFd1Lh3JOCXc/jFLJXKTxk=\ngithub.com/zclconf/go-cty v1.16.3/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE=\ngithub.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo=\ngithub.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM=\ngithub.com/zyedidia/generic v1.2.2-0.20230320175451-4410d2372cb1 h1:V+UsotZpAVvfj3X/LMoEytoLzSiP6Lg0F7wdVyu9gGg=\ngithub.com/zyedidia/generic v1.2.2-0.20230320175451-4410d2372cb1/go.mod h1:ly2RBz4mnz1yeuVbQA/VFwGjK3mnHGRj1JuoG336Bis=\ngo.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=\ngo.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=\ngo.etcd.io/etcd/api/v3 v3.5.1/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=\ngo.etcd.io/etcd/client/pkg/v3 v3.5.1/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=\ngo.etcd.io/etcd/client/v2 v2.305.1/go.mod h1:pMEacxZW7o8pg4CrFE7pquyCJJzZvkvdD2RibOCCCGs=\ngo.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=\ngo.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=\ngo.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=\ngo.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=\ngo.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=\ngo.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=\ngo.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=\ngo.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=\ngo.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=\ngo.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=\ngo.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=\ngo.opentelemetry.io/contrib/detectors/gcp v1.39.0 h1:kWRNZMsfBHZ+uHjiH4y7Etn2FK26LAGkNFw7RHv1DhE=\ngo.opentelemetry.io/contrib/detectors/gcp v1.39.0/go.mod h1:t/OGqzHBa5v6RHZwrDBJ2OirWc+4q/w2fTbLZwAKjTk=\ngo.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo=\ngo.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 h1:Hf9xI/XLML9ElpiHVDNwvqI0hIFlzV8dgIr35kV1kRU=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0/go.mod h1:NfchwuyNoMcZ5MLHwPrODwUF1HWCXWrL31s8gSAdIKY=\ngo.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=\ngo.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 h1:xJ2qHD0C1BeYVTLLR9sX12+Qb95kfeD/byKj6Ky1pXg=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0/go.mod h1:u5BF1xyjstDowA1R5QAO9JHzqK+ublenEW/dyqTjBVk=\ngo.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.39.0 h1:5gn2urDL/FBnK8OkCfD1j3/ER79rUuTYmCvlXBKeYL8=\ngo.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.39.0/go.mod h1:0fBG6ZJxhqByfFZDwSwpZGzJU671HkwpWaNe2t4VUPI=\ngo.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=\ngo.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=\ngo.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8=\ngo.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE=\ngo.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw=\ngo.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg=\ngo.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=\ngo.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=\ngo.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=\ngo.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4=\ngo.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE=\ngo.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=\ngo.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=\ngo.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=\ngo.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=\ngo.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=\ngo.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=\ngo.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=\ngo4.org v0.0.0-20230225012048-214862532bf5 h1:nifaUDeh+rPaBCMPMQHZmvJf+QdpLFnuQPwx+LxVmtc=\ngo4.org v0.0.0-20230225012048-214862532bf5/go.mod h1:F57wTi5Lrj6WLyswp5EYV1ncrEbFGHD4hhz6S1ZYeaU=\ngolang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=\ngolang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=\ngolang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=\ngolang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=\ngolang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=\ngolang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=\ngolang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=\ngolang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=\ngolang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=\ngolang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=\ngolang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=\ngolang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=\ngolang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=\ngolang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=\ngolang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=\ngolang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=\ngolang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=\ngolang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=\ngolang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=\ngolang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=\ngolang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=\ngolang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=\ngolang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=\ngolang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=\ngolang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=\ngolang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=\ngolang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=\ngolang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=\ngolang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=\ngolang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=\ngolang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=\ngolang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=\ngolang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=\ngolang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=\ngolang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=\ngolang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=\ngolang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=\ngolang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=\ngolang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=\ngolang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=\ngolang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=\ngolang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=\ngolang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=\ngolang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=\ngolang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=\ngolang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=\ngolang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=\ngolang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=\ngolang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=\ngolang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=\ngolang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=\ngolang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=\ngolang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=\ngolang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=\ngolang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=\ngolang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=\ngolang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=\ngolang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=\ngolang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=\ngolang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=\ngolang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=\ngolang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=\ngolang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=\ngolang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=\ngolang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=\ngolang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=\ngolang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=\ngolang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=\ngolang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=\ngolang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=\ngolang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=\ngolang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=\ngolang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=\ngolang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=\ngolang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=\ngolang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=\ngolang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=\ngolang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=\ngolang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY=\ngolang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=\ngonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=\ngonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=\ngoogle.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=\ngoogle.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=\ngoogle.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=\ngoogle.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=\ngoogle.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=\ngoogle.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=\ngoogle.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=\ngoogle.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=\ngoogle.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=\ngoogle.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=\ngoogle.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=\ngoogle.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=\ngoogle.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=\ngoogle.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=\ngoogle.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU=\ngoogle.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94=\ngoogle.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo=\ngoogle.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4=\ngoogle.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw=\ngoogle.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU=\ngoogle.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k=\ngoogle.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=\ngoogle.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=\ngoogle.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI=\ngoogle.golang.org/api v0.59.0/go.mod h1:sT2boj7M9YJxZzgeZqXogmhfmRWDtPzT31xkieUbuZU=\ngoogle.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I=\ngoogle.golang.org/api v0.62.0/go.mod h1:dKmwPCydfsad4qCH08MSdgWjfHOyfpd4VtDGgRFdavw=\ngoogle.golang.org/api v0.267.0 h1:w+vfWPMPYeRs8qH1aYYsFX68jMls5acWl/jocfLomwE=\ngoogle.golang.org/api v0.267.0/go.mod h1:Jzc0+ZfLnyvXma3UtaTl023TdhZu6OMBP9tJ+0EmFD0=\ngoogle.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=\ngoogle.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=\ngoogle.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=\ngoogle.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=\ngoogle.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=\ngoogle.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=\ngoogle.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=\ngoogle.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=\ngoogle.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=\ngoogle.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=\ngoogle.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=\ngoogle.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=\ngoogle.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=\ngoogle.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=\ngoogle.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=\ngoogle.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=\ngoogle.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A=\ngoogle.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=\ngoogle.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=\ngoogle.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=\ngoogle.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24=\ngoogle.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=\ngoogle.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=\ngoogle.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=\ngoogle.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=\ngoogle.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w=\ngoogle.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=\ngoogle.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=\ngoogle.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=\ngoogle.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=\ngoogle.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=\ngoogle.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=\ngoogle.golang.org/genproto v0.0.0-20211008145708-270636b82663/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=\ngoogle.golang.org/genproto v0.0.0-20211028162531-8db9c33dc351/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=\ngoogle.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=\ngoogle.golang.org/genproto v0.0.0-20211129164237-f09f9a12af12/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=\ngoogle.golang.org/genproto v0.0.0-20211203200212-54befc351ae9/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=\ngoogle.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=\ngoogle.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=\ngoogle.golang.org/genproto v0.0.0-20260128011058-8636f8732409 h1:VQZ/yAbAtjkHgH80teYd2em3xtIkkHd7ZhqfH2N9CsM=\ngoogle.golang.org/genproto v0.0.0-20260128011058-8636f8732409/go.mod h1:rxKD3IEILWEu3P44seeNOAwZN4SaoKaQ/2eTg4mM6EM=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20260203192932-546029d2fa20 h1:7ei4lp52gK1uSejlA8AZl5AJjeLUOHBQscRQZUgAcu0=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20260203192932-546029d2fa20/go.mod h1:ZdbssH/1SOVnjnDlXzxDHK2MCidiqXtbYccJNzNYPEE=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 h1:Jr5R2J6F6qWyzINc+4AM8t5pfUz6beZpHp678GNrMbE=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=\ngoogle.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=\ngoogle.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=\ngoogle.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=\ngoogle.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=\ngoogle.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=\ngoogle.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=\ngoogle.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=\ngoogle.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=\ngoogle.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=\ngoogle.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=\ngoogle.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=\ngoogle.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=\ngoogle.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=\ngoogle.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=\ngoogle.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=\ngoogle.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=\ngoogle.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=\ngoogle.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=\ngoogle.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=\ngoogle.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=\ngoogle.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=\ngoogle.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=\ngoogle.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=\ngoogle.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=\ngoogle.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=\ngoogle.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=\ngoogle.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=\ngoogle.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=\ngoogle.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=\ngoogle.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=\ngoogle.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=\ngoogle.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=\ngoogle.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=\ngoogle.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=\ngoogle.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=\ngoogle.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=\ngoogle.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=\ngoogle.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=\ngoogle.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=\ngopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\ngopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=\ngopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=\ngopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=\ngopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=\ngopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=\ngorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=\ngotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=\ngotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=\nhonnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=\nhonnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=\nhonnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=\nmodernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=\nmodernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=\nmodernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc=\nmodernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM=\nmodernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=\nmodernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=\nmodernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=\nmodernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=\nmodernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE=\nmodernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=\nmodernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=\nmodernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=\nmodernc.org/libc v1.67.6 h1:eVOQvpModVLKOdT+LvBPjdQqfrZq+pC39BygcT+E7OI=\nmodernc.org/libc v1.67.6/go.mod h1:JAhxUVlolfYDErnwiqaLvUqc8nfb2r6S6slAgZOnaiE=\nmodernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=\nmodernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=\nmodernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=\nmodernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=\nmodernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=\nmodernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=\nmodernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=\nmodernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=\nmodernc.org/sqlite v1.46.1 h1:eFJ2ShBLIEnUWlLy12raN0Z1plqmFX9Qe3rjQTKt6sU=\nmodernc.org/sqlite v1.46.1/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA=\nmodernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=\nmodernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=\nmodernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=\nmodernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=\npgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk=\npgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04=\nrsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=\nrsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=\nrsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=\n"
  },
  {
    "path": "grype/cpe/cpe.go",
    "content": "package cpe\n\nimport (\n\t\"github.com/facebookincubator/nvdtools/wfn\"\n\n\t\"github.com/anchore/grype/internal/log\"\n\t\"github.com/anchore/syft/syft/cpe\"\n)\n\nfunc NewSlice(cpeStrs ...string) ([]cpe.CPE, error) {\n\tvar cpes []cpe.CPE\n\tfor _, c := range cpeStrs {\n\t\tvalue, err := cpe.New(c, \"\")\n\t\tif err != nil {\n\t\t\tlog.Warnf(\"excluding invalid CPE %q: %v\", c, err)\n\t\t\tcontinue\n\t\t}\n\n\t\tcpes = append(cpes, value)\n\t}\n\treturn cpes, nil\n}\n\nfunc MatchWithoutVersion(c cpe.CPE, candidates []cpe.CPE) []cpe.CPE {\n\tmatches := make([]cpe.CPE, 0)\n\ta := wfn.Attributes(c.Attributes)\n\ta.Update = wfn.Any\n\tfor _, candidate := range candidates {\n\t\tcanCopy := wfn.Attributes(candidate.Attributes)\n\t\tcanCopy.Update = wfn.Any\n\t\tif a.MatchWithoutVersion(&canCopy) {\n\t\t\tmatches = append(matches, candidate)\n\t\t}\n\t}\n\treturn matches\n}\n"
  },
  {
    "path": "grype/cpe/cpe_test.go",
    "content": "package cpe\n\nimport (\n\t\"testing\"\n\n\t\"github.com/sergi/go-diff/diffmatchpatch\"\n\n\t\"github.com/anchore/syft/syft/cpe\"\n)\n\nfunc TestMatchWithoutVersion(t *testing.T) {\n\ttests := []struct {\n\t\tname       string\n\t\tcompare    cpe.CPE\n\t\tcandidates []cpe.CPE\n\t\texpected   []cpe.CPE\n\t}{\n\t\t{\n\t\t\tname:    \"GoCase\",\n\t\t\tcompare: cpe.Must(\"cpe:2.3:*:python-requests:requests:2.3.0:*:*:*:*:python:*:*\", \"\"),\n\t\t\tcandidates: []cpe.CPE{\n\t\t\t\tcpe.Must(\"cpe:2.3:a:python-requests:requests:2.2.1:*:*:*:*:*:*:*\", \"\"),\n\t\t\t},\n\t\t\texpected: []cpe.CPE{\n\t\t\t\tcpe.Must(\"cpe:2.3:a:python-requests:requests:2.2.1:*:*:*:*:*:*:*\", \"\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:    \"IgnoreVersion\",\n\t\t\tcompare: cpe.Must(\"cpe:2.3:*:name:name:3.2:*:*:*:*:java:*:*\", \"\"),\n\t\t\tcandidates: []cpe.CPE{\n\t\t\t\tcpe.Must(\"cpe:2.3:*:name:name:3.2:*:*:*:*:java:*:*\", \"\"),\n\t\t\t\tcpe.Must(\"cpe:2.3:*:name:name:3.3:*:*:*:*:java:*:*\", \"\"),\n\t\t\t\tcpe.Must(\"cpe:2.3:*:name:name:5.5:*:*:*:*:java:*:*\", \"\"),\n\t\t\t},\n\t\t\texpected: []cpe.CPE{\n\t\t\t\tcpe.Must(\"cpe:2.3:*:name:name:3.2:*:*:*:*:java:*:*\", \"\"),\n\t\t\t\tcpe.Must(\"cpe:2.3:*:name:name:3.3:*:*:*:*:java:*:*\", \"\"),\n\t\t\t\tcpe.Must(\"cpe:2.3:*:name:name:5.5:*:*:*:*:java:*:*\", \"\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:    \"MatchByTargetSW\",\n\t\t\tcompare: cpe.Must(\"cpe:2.3:*:name:name:3.2:*:*:*:*:java:*:*\", \"\"),\n\t\t\tcandidates: []cpe.CPE{\n\t\t\t\tcpe.Must(\"cpe:2.3:*:name:name:3.2:*:*:*:*:java:*:*\", \"\"),\n\t\t\t\tcpe.Must(\"cpe:2.3:*:name:name:3.2:*:*:*:*:maven:*:*\", \"\"),\n\t\t\t\tcpe.Must(\"cpe:2.3:*:name:name:3.2:*:*:*:*:jenkins:*:*\", \"\"),\n\t\t\t\tcpe.Must(\"cpe:2.3:*:name:name:3.2:*:*:*:*:cloudbees_jenkins:*:*\", \"\"),\n\t\t\t\tcpe.Must(\"cpe:2.3:*:name:name:3.2:*:*:*:*:*:*:*\", \"\"),\n\t\t\t},\n\t\t\texpected: []cpe.CPE{\n\t\t\t\tcpe.Must(\"cpe:2.3:*:name:name:3.2:*:*:*:*:java:*:*\", \"\"),\n\t\t\t\tcpe.Must(\"cpe:2.3:*:name:name:3.2:*:*:*:*:*:*:*\", \"\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:    \"MatchByName\",\n\t\t\tcompare: cpe.Must(\"cpe:2.3:*:name:name5:3.2:*:*:*:*:java:*:*\", \"\"),\n\t\t\tcandidates: []cpe.CPE{\n\t\t\t\tcpe.Must(\"cpe:2.3:*:name:name1:3.2:*:*:*:*:java:*:*\", \"\"),\n\t\t\t\tcpe.Must(\"cpe:2.3:*:name:name2:3.2:*:*:*:*:java:*:*\", \"\"),\n\t\t\t\tcpe.Must(\"cpe:2.3:*:name:name3:3.2:*:*:*:*:java:*:*\", \"\"),\n\t\t\t\tcpe.Must(\"cpe:2.3:*:name:name4:3.2:*:*:*:*:java:*:*\", \"\"),\n\t\t\t\tcpe.Must(\"cpe:2.3:*:name:name5:3.2:*:*:*:*:*:*:*\", \"\"),\n\t\t\t},\n\t\t\texpected: []cpe.CPE{\n\t\t\t\tcpe.Must(\"cpe:2.3:*:name:name5:3.2:*:*:*:*:*:*:*\", \"\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:    \"MatchByVendor\",\n\t\t\tcompare: cpe.Must(\"cpe:2.3:*:name3:name:3.2:*:*:*:*:java:*:*\", \"\"),\n\t\t\tcandidates: []cpe.CPE{\n\t\t\t\tcpe.Must(\"cpe:2.3:*:name1:name:3.2:*:*:*:*:java:*:*\", \"\"),\n\t\t\t\tcpe.Must(\"cpe:2.3:*:name3:name:3.2:*:*:*:*:jaba-no-bother:*:*\", \"\"),\n\t\t\t\tcpe.Must(\"cpe:2.3:*:name3:name:3.2:*:*:*:*:java:*:*\", \"\"),\n\t\t\t\tcpe.Must(\"cpe:2.3:*:name4:name:3.2:*:*:*:*:java:*:*\", \"\"),\n\t\t\t\tcpe.Must(\"cpe:2.3:*:name5:name:3.2:*:*:*:*:*:*:*\", \"\"),\n\t\t\t},\n\t\t\texpected: []cpe.CPE{\n\t\t\t\tcpe.Must(\"cpe:2.3:*:name3:name:3.2:*:*:*:*:java:*:*\", \"\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:    \"MatchAnyVendorOrTargetSW\",\n\t\t\tcompare: cpe.Must(\"cpe:2.3:*:*:name:3.2:*:*:*:*:*:*:*\", \"\"),\n\t\t\tcandidates: []cpe.CPE{\n\t\t\t\tcpe.Must(\"cpe:2.3:*:name1:name:3.2:*:*:*:*:java:*:*\", \"\"),\n\t\t\t\tcpe.Must(\"cpe:2.3:*:name3:name:3.2:*:*:*:*:jaba-no-bother:*:*\", \"\"),\n\t\t\t\tcpe.Must(\"cpe:2.3:*:name3:name:3.2:*:*:*:*:java:*:*\", \"\"),\n\t\t\t\tcpe.Must(\"cpe:2.3:*:name4:name:3.2:*:*:*:*:java:*:*\", \"\"),\n\t\t\t\tcpe.Must(\"cpe:2.3:*:name5:name:3.2:*:*:*:*:*:*:*\", \"\"),\n\t\t\t\tcpe.Must(\"cpe:2.3:*:name5:NOMATCH:3.2:*:*:*:*:*:*:*\", \"\"),\n\t\t\t},\n\t\t\texpected: []cpe.CPE{\n\t\t\t\tcpe.Must(\"cpe:2.3:*:name1:name:3.2:*:*:*:*:java:*:*\", \"\"),\n\t\t\t\tcpe.Must(\"cpe:2.3:*:name3:name:3.2:*:*:*:*:jaba-no-bother:*:*\", \"\"),\n\t\t\t\tcpe.Must(\"cpe:2.3:*:name3:name:3.2:*:*:*:*:java:*:*\", \"\"),\n\t\t\t\tcpe.Must(\"cpe:2.3:*:name4:name:3.2:*:*:*:*:java:*:*\", \"\"),\n\t\t\t\tcpe.Must(\"cpe:2.3:*:name5:name:3.2:*:*:*:*:*:*:*\", \"\"),\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tactual := MatchWithoutVersion(test.compare, test.candidates)\n\n\t\t\tif len(actual) != len(test.expected) {\n\t\t\t\tfor _, e := range actual {\n\t\t\t\t\tt.Errorf(\"   unexpected entry: %+v\", e.Attributes.BindToFmtString())\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"unexpected number of entries: %d\", len(actual))\n\t\t\t}\n\n\t\t\tfor idx, a := range actual {\n\t\t\t\te := test.expected[idx]\n\t\t\t\tif a.Attributes.BindToFmtString() != e.Attributes.BindToFmtString() {\n\t\t\t\t\tdmp := diffmatchpatch.New()\n\t\t\t\t\tdiffs := dmp.DiffMain(a.Attributes.BindToFmtString(), e.Attributes.BindToFmtString(), true)\n\t\t\t\t\tt.Errorf(\"mismatched entries @ %d:\\n\\texpected:%+v\\n\\t  actual:%+v\\n\\t    diff:%+v\\n\", idx, e.Attributes.BindToFmtString(), a.Attributes.BindToFmtString(), dmp.DiffPrettyText(diffs))\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/db/build.go",
    "content": "package db\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"sort\"\n\t\"time\"\n\n\t\"github.com/dustin/go-humanize\"\n\t\"github.com/spf13/afero\"\n\n\t\"github.com/anchore/grype/grype/db/data\"\n\t\"github.com/anchore/grype/grype/db/internal/provider/unmarshal\"\n\t\"github.com/anchore/grype/grype/db/provider\"\n\t\"github.com/anchore/grype/grype/db/provider/entry\"\n\tgrypeDBv5 \"github.com/anchore/grype/grype/db/v5\"\n\tv5 \"github.com/anchore/grype/grype/db/v5/build\"\n\tgrypeDBv6 \"github.com/anchore/grype/grype/db/v6\"\n\tv6 \"github.com/anchore/grype/grype/db/v6/build\"\n\t\"github.com/anchore/grype/internal/log\"\n)\n\n// DefaultBatchSize is the default number of database operations to batch together\n// before flushing to disk. This value balances throughput with memory usage.\nconst DefaultBatchSize = 2000\n\ntype BuildConfig struct {\n\tSchemaVersion        int\n\tDirectory            string\n\tStates               provider.States\n\tTimestamp            time.Time\n\tIncludeCPEParts      []string\n\tInferNVDFixVersions  bool\n\tHydrate              bool\n\tFailOnMissingFixDate bool // any fixes found without at least one available date will cause a build failure\n\tBatchSize            int  // number of operations to batch before committing\n}\n\nfunc Build(cfg BuildConfig) error {\n\tlog.WithFields(\n\t\t\"schema\", cfg.SchemaVersion,\n\t\t\"build-directory\", cfg.Directory,\n\t\t\"providers\", cfg.States.Names()).\n\t\tInfo(\"building database\")\n\n\tprocessors, err := getProcessors(cfg)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\twriter, err := getWriter(cfg)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar openers []providerResults\n\tfor _, sd := range cfg.States {\n\t\tsdOpeners, count, err := entry.Openers(sd.Store, sd.ResultPaths())\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to open provider result files: %w\", err)\n\t\t}\n\t\topeners = append(openers, providerResults{\n\t\t\topeners:  sdOpeners,\n\t\t\tprovider: sd,\n\t\t\tcount:    count,\n\t\t})\n\t}\n\n\tif err := build(openers, writer, processors...); err != nil {\n\t\treturn err\n\t}\n\n\tif err := writer.Close(); err != nil {\n\t\treturn err\n\t}\n\n\tif cfg.Hydrate && cfg.SchemaVersion > 5 {\n\t\tif err := hydrate(cfg); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\ntype providerResults struct {\n\topeners  <-chan entry.Opener\n\tprovider provider.State\n\tcount    int64\n}\n\nfunc getProcessors(cfg BuildConfig) ([]data.Processor, error) {\n\tswitch cfg.SchemaVersion {\n\tcase grypeDBv5.SchemaVersion:\n\t\treturn v5.Processors(v5.NewConfig(v5.WithCPEParts(cfg.IncludeCPEParts), v5.WithInferNVDFixVersions(cfg.InferNVDFixVersions))), nil\n\tcase grypeDBv6.ModelVersion:\n\t\treturn v6.Processors(v6.NewConfig(v6.WithCPEParts(cfg.IncludeCPEParts), v6.WithInferNVDFixVersions(cfg.InferNVDFixVersions))), nil\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unable to create processor: unsupported schema version: %+v\", cfg.SchemaVersion)\n\t}\n}\n\nfunc getWriter(cfg BuildConfig) (data.Writer, error) {\n\t// Use default if not configured\n\tbatchSize := cfg.BatchSize\n\tif batchSize == 0 {\n\t\tbatchSize = DefaultBatchSize\n\t}\n\n\tswitch cfg.SchemaVersion {\n\tcase grypeDBv5.SchemaVersion:\n\t\treturn v5.NewWriter(cfg.Directory, cfg.Timestamp, cfg.States, batchSize)\n\tcase grypeDBv6.ModelVersion:\n\t\treturn v6.NewWriter(cfg.Directory, cfg.States, cfg.FailOnMissingFixDate, batchSize)\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unable to create writer: unsupported schema version: %+v\", cfg.SchemaVersion)\n\t}\n}\n\nfunc build(results []providerResults, writer data.Writer, processors ...data.Processor) error { // nolint:funlen\n\tlastUpdate := time.Now()\n\tvar totalRecords int\n\tfor _, result := range results {\n\t\ttotalRecords += int(result.count)\n\t}\n\tlog.WithFields(\"total\", humanize.Comma(int64(totalRecords))).Info(\"processing all records\")\n\n\t// for exponential moving average, choose an alpha between 0 and 1, where 1 biases towards the most recent sample\n\t// and 0 biases towards the average of all samples.\n\trateWindow := newEMA(0.4)\n\n\tvar recordsProcessed, recordsObserved, dropped int\n\tdroppedElementsByProvider := make(map[string]int)\n\tdroppedSchemaElements := make(map[string]int)\n\n\tfor _, result := range results {\n\t\tlog.WithFields(\"provider\", result.provider.Provider, \"total\", humanize.Comma(result.count)).Info(\"processing provider records\")\n\t\tproviderRecordsObserved := 0\n\t\trecordsObservedInStatusCycle := 0\n\t\tfor opener := range result.openers {\n\t\t\tproviderRecordsObserved++\n\t\t\trecordsObserved++\n\t\t\trecordsObservedInStatusCycle++\n\t\t\tvar processor data.Processor\n\n\t\t\tif time.Since(lastUpdate) > 3*time.Second {\n\t\t\t\tr := recordsPerSecond(recordsObservedInStatusCycle, lastUpdate)\n\t\t\t\trateWindow.Add(r)\n\n\t\t\t\tlog.WithFields(\n\t\t\t\t\t\"provider\", fmt.Sprintf(\"%q %1.0f/s (%1.2f%%)\", result.provider.Provider, r, percent(providerRecordsObserved, int(result.count))),\n\t\t\t\t\t\"overall\", fmt.Sprintf(\"%1.2f%%\", percent(recordsObserved, totalRecords)),\n\t\t\t\t\t\"eta\", eta(recordsObserved, totalRecords, rateWindow.Average()).String(),\n\t\t\t\t).Debug(\"status\")\n\t\t\t\tlastUpdate = time.Now()\n\t\t\t\trecordsObservedInStatusCycle = 0\n\t\t\t}\n\n\t\t\tf, err := opener.Open()\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to open cache entry %q: %w\", opener.String(), err)\n\t\t\t}\n\t\t\tenvelope, err := unmarshal.Envelope(f)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to unmarshal cache entry %q: %w\", opener.String(), err)\n\t\t\t}\n\n\t\t\tfor _, candidate := range processors {\n\t\t\t\tif candidate.IsSupported(envelope.Schema) {\n\t\t\t\t\tprocessor = candidate\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t\tif processor == nil {\n\t\t\t\tdroppedElementsByProvider[result.provider.Provider]++\n\t\t\t\tdroppedSchemaElements[envelope.Schema]++\n\t\t\t\tdropped++\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\trecordsProcessed++\n\n\t\t\tentries, err := processor.Process(bytes.NewReader(envelope.Item), result.provider)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to process cache entry %q: %w\", opener.String(), err)\n\t\t\t}\n\n\t\t\tif err := writer.Write(entries...); err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to write records to the DB for cache entry %q: %w\", opener.String(), err)\n\t\t\t}\n\t\t}\n\t}\n\n\tlogDropped(droppedElementsByProvider, droppedSchemaElements)\n\n\tlog.WithFields(\"processed\", recordsProcessed, \"dropped\", dropped, \"observed\", recordsObserved).Debugf(\"wrote all provider state\")\n\n\tif recordsProcessed == 0 {\n\t\treturn fmt.Errorf(\"no records were processed\")\n\t}\n\n\treturn nil\n}\n\nfunc hydrate(cfg BuildConfig) error {\n\thydrator := grypeDBv6.Hydrater()\n\tfs := afero.NewOsFs()\n\n\tif err := hydrator(cfg.Directory); err != nil {\n\t\treturn fmt.Errorf(\"failed to hydrate db: %w\", err)\n\t}\n\n\tdoc, err := grypeDBv6.WriteImportMetadata(fs, cfg.Directory, \"grype db build\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to write checksums file: %w\", err)\n\t}\n\n\tlog.WithFields(\"digest\", doc.Digest).Trace(\"captured DB digest\")\n\n\treturn nil\n}\n\nfunc logDropped(droppedElementsByProvider, droppedSchemaElements map[string]int) {\n\tsortedKeys := func(m map[string]int) []string {\n\t\tvar keys []string\n\t\tfor k := range m {\n\t\t\tkeys = append(keys, k)\n\t\t}\n\t\tsort.Strings(keys)\n\t\treturn keys\n\t}\n\tsortedProviders := sortedKeys(droppedElementsByProvider)\n\tfor _, p := range sortedProviders {\n\t\tlog.WithFields(\"provider\", p, \"count\", droppedElementsByProvider[p]).Warn(\"dropped records for provider\")\n\t}\n\n\tsortedSchemas := sortedKeys(droppedSchemaElements)\n\tfor _, s := range sortedSchemas {\n\t\tlog.WithFields(\"schema\", s, \"count\", droppedSchemaElements[s]).Warn(\"dropped records by schema\")\n\t}\n}\n\ntype expMovingAverage struct {\n\talpha float64\n\tvalue float64\n\tcount int\n}\n\nfunc newEMA(alpha float64) *expMovingAverage {\n\treturn &expMovingAverage{alpha: alpha}\n}\n\nfunc (e *expMovingAverage) Add(sample float64) {\n\tif e.count == 0 {\n\t\te.value = sample // initialize with the first sample\n\t} else {\n\t\te.value = e.alpha*sample + (1-e.alpha)*e.value\n\t}\n\te.count++\n}\n\nfunc (e *expMovingAverage) Average() float64 {\n\treturn e.value\n}\n\nfunc recordsPerSecond(idx int, lastUpdate time.Time) float64 {\n\tsec := time.Since(lastUpdate).Seconds()\n\tif sec == 0 {\n\t\treturn 0\n\t}\n\treturn float64(idx) / sec\n}\n\nfunc percent(idx, total int) float64 {\n\tif total == 0 {\n\t\treturn 0\n\t}\n\treturn float64(idx) / float64(total) * 100\n}\n\nfunc eta(idx, total int, rate float64) time.Duration {\n\tif rate == 0 {\n\t\treturn 0\n\t}\n\treturn time.Duration(float64(total-idx)/rate) * time.Second\n}\n"
  },
  {
    "path": "grype/db/data/entry.go",
    "content": "package data\n\n// Entry is a data structure responsible for capturing an individual writable entry from a data.Processor (written by a data.Writer).\ntype Entry struct {\n\tDBSchemaVersion int\n\t// Data is the specific payload that should be written (usually a grype-db v*.Entry struct)\n\tData interface{}\n}\n"
  },
  {
    "path": "grype/db/data/processor.go",
    "content": "package data\n\nimport (\n\t\"io\"\n\n\t\"github.com/anchore/grype/grype/db/provider\"\n)\n\n// Processor takes individual feed group cache files (for select feed groups) and is responsible to producing\n// data.Entry objects to be written to the DB.\ntype Processor interface {\n\tIsSupported(schemaURL string) bool\n\tProcess(reader io.Reader, state provider.State) ([]Entry, error)\n}\n"
  },
  {
    "path": "grype/db/data/severity.go",
    "content": "package data\n\nimport \"strings\"\n\ntype Severity string\n\nconst (\n\tSeverityUnknown    Severity = \"Unknown\"\n\tSeverityNegligible Severity = \"Negligible\"\n\tSeverityLow        Severity = \"Low\"\n\tSeverityMedium     Severity = \"Medium\"\n\tSeverityHigh       Severity = \"High\"\n\tSeverityCritical   Severity = \"Critical\"\n)\n\nfunc ParseSeverity(s string) Severity {\n\tclean := strings.TrimSpace(strings.ToLower(s))\n\tswitch clean {\n\tcase \"unknown\", \"\":\n\t\treturn SeverityUnknown\n\tcase \"negligible\":\n\t\treturn SeverityNegligible\n\tcase \"low\":\n\t\treturn SeverityLow\n\tcase \"medium\":\n\t\treturn SeverityMedium\n\tcase \"high\":\n\t\treturn SeverityHigh\n\tcase \"critical\":\n\t\treturn SeverityCritical\n\tdefault:\n\t\treturn SeverityUnknown\n\t}\n}\n"
  },
  {
    "path": "grype/db/data/severity_test.go",
    "content": "package data\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestParseSeverity(t *testing.T) {\n\ttests := []struct {\n\t\tinput string\n\t\twant  Severity\n\t}{\n\t\t{\n\t\t\tinput: \"negLIGible\",\n\t\t\twant:  SeverityNegligible,\n\t\t},\n\t\t{\n\t\t\tinput: \"loW\",\n\t\t\twant:  SeverityLow,\n\t\t},\n\t\t{\n\t\t\tinput: \"meDIum\",\n\t\t\twant:  SeverityMedium,\n\t\t},\n\t\t{\n\t\t\tinput: \"  hiGH\",\n\t\t\twant:  SeverityHigh,\n\t\t},\n\t\t{\n\t\t\tinput: \"cRiTical  \",\n\t\t\twant:  SeverityCritical,\n\t\t},\n\t\t{\n\t\t\tinput: \"unKNOWN\",\n\t\t\twant:  SeverityUnknown,\n\t\t},\n\t\t{\n\t\t\tinput: \"\",\n\t\t\twant:  SeverityUnknown,\n\t\t},\n\t\t{\n\t\t\tinput: \"  \",\n\t\t\twant:  SeverityUnknown,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.input, func(t *testing.T) {\n\t\t\tassert.Equal(t, tt.want, ParseSeverity(tt.input))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/db/data/transformers.go",
    "content": "package data\n\nimport (\n\t\"github.com/anchore/grype/grype/db/internal/provider/unmarshal\"\n\t\"github.com/anchore/grype/grype/db/provider\"\n)\n\n// Transformers are functions that know how ta take individual data shapes defined in the unmarshal package and\n// reshape the data into data.Entry objects that are writable by a data.Writer. Transformers are dependency-injected\n// into commonly-shared data.Processors in the individual process.v* packages.\n\n// all v1 transformers (schema v1 - v5)\n\ntype GitHubTransformer func(entry unmarshal.GitHubAdvisory) ([]Entry, error)\ntype MSRCTransformer func(entry unmarshal.MSRCVulnerability) ([]Entry, error)\ntype NVDTransformer func(entry unmarshal.NVDVulnerability) ([]Entry, error)\ntype OSTransformer func(entry unmarshal.OSVulnerability) ([]Entry, error)\ntype MatchExclusionTransformer func(entry unmarshal.MatchExclusion) ([]Entry, error)\n\n// all v2 transformers (schema v6+)\n\ntype GitHubTransformerV2 func(entry unmarshal.GitHubAdvisory, state provider.State) ([]Entry, error)\ntype MSRCTransformerV2 func(entry unmarshal.MSRCVulnerability, state provider.State) ([]Entry, error)\ntype NVDTransformerV2 func(entry unmarshal.NVDVulnerability, state provider.State) ([]Entry, error)\ntype OSTransformerV2 func(entry unmarshal.OSVulnerability, state provider.State) ([]Entry, error)\ntype MatchExclusionTransformerV2 func(entry unmarshal.MatchExclusion, state provider.State) ([]Entry, error)\n\ntype KnownExploitedVulnerabilityTransformerV2 func(entry unmarshal.KnownExploitedVulnerability, state provider.State) ([]Entry, error)\ntype EPSSTransformerV2 func(entry unmarshal.EPSS, state provider.State) ([]Entry, error)\ntype OSVTransformerV2 func(entry unmarshal.OSVVulnerability, state provider.State) ([]Entry, error)\ntype OpenVEXTransformerV2 func(entry unmarshal.OpenVEXVulnerability, state provider.State) ([]Entry, error)\ntype AnnotatedOpenVEXTransformerV2 func(entry unmarshal.AnnotatedOpenVEXVulnerability, state provider.State) ([]Entry, error)\ntype EOLTransformerV2 func(entry unmarshal.EndOfLifeDateRelease, state provider.State) ([]Entry, error)\n"
  },
  {
    "path": "grype/db/data/writer.go",
    "content": "package data\n\n// Writer knows how to persist one or more data.Entry objects to a database. Note that the backing implementations\n// may take advantage of bulk writes when possible (positively improving performance), which is why multiple\n// entries can be written at once.\ntype Writer interface {\n\tWrite(...Entry) error\n\tClose() error\n}\n"
  },
  {
    "path": "grype/db/default_schema_version.go",
    "content": "package db\n\nimport db \"github.com/anchore/grype/grype/db/v6\"\n\nconst DefaultSchemaVersion = db.ModelVersion\n"
  },
  {
    "path": "grype/db/generate.go",
    "content": "package db\n\n//go:generate go run ./internal/codename/generate/main.go\n"
  },
  {
    "path": "grype/db/internal/codename/codename.go",
    "content": "package codename\n\nimport \"strings\"\n\nfunc LookupOS(osName, majorVersion, minorVersion string) string {\n\tmajorVersion = strings.TrimLeft(majorVersion, \"0\")\n\tif minorVersion != \"0\" {\n\t\tminorVersion = strings.TrimLeft(minorVersion, \"0\")\n\t}\n\n\t// try to find the most specific match (major and minor version)\n\tif versions, ok := normalizedOSCodenames[osName]; ok {\n\t\tif minorMap, ok := versions[majorVersion]; ok {\n\t\t\tif codename, ok := minorMap[minorVersion]; ok {\n\t\t\t\treturn codename\n\t\t\t}\n\t\t\t// fall back to the least specific match (only major version, allowing for any minor version explicitly)\n\t\t\tif codename, ok := minorMap[\"*\"]; ok {\n\t\t\t\treturn codename\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n"
  },
  {
    "path": "grype/db/internal/codename/codename_test.go",
    "content": "package codename\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestLookupOSCodename(t *testing.T) {\n\ttests := []struct {\n\t\tName             string\n\t\tOSName           string\n\t\tMajorVersion     string\n\t\tMinorVersion     string\n\t\tExpectedCodename string\n\t}{\n\t\t{Name: \"Ubuntu 20.04 exact\", OSName: \"ubuntu\", MajorVersion: \"20\", MinorVersion: \"04\", ExpectedCodename: \"focal\"},\n\t\t{Name: \"Ubuntu 20.4 exact\", OSName: \"ubuntu\", MajorVersion: \"20\", MinorVersion: \"4\", ExpectedCodename: \"focal\"},\n\t\t{Name: \"Ubuntu 0 (non existent) minor\", OSName: \"ubuntu\", MajorVersion: \"20\", MinorVersion: \"0\", ExpectedCodename: \"\"},\n\t\t{Name: \"Ubuntu empty minor\", OSName: \"ubuntu\", MajorVersion: \"10\", MinorVersion: \"\", ExpectedCodename: \"\"},\n\t\t{Name: \"Debian empty minor\", OSName: \"debian\", MajorVersion: \"10\", MinorVersion: \"\", ExpectedCodename: \"buster\"},\n\t\t{Name: \"Ubuntu leading zeros in major\", OSName: \"ubuntu\", MajorVersion: \"020\", MinorVersion: \"04\", ExpectedCodename: \"focal\"},\n\t\t{Name: \"Debian leading zeros in major\", OSName: \"debian\", MajorVersion: \"010\", MinorVersion: \"\", ExpectedCodename: \"buster\"},\n\t\t{Name: \"Debian bad minor\", OSName: \"debian\", MajorVersion: \"11\", MinorVersion: \"99\", ExpectedCodename: \"bullseye\"},\n\t\t{Name: \"Ubuntu bad minor\", OSName: \"ubuntu\", MajorVersion: \"22\", MinorVersion: \"99\", ExpectedCodename: \"\"},\n\t\t{Name: \"Ubuntu 6.10 exact (legacy)\", OSName: \"ubuntu\", MajorVersion: \"6\", MinorVersion: \"10\", ExpectedCodename: \"edgy\"},\n\t\t{Name: \"Ubuntu 6.6 exact (legacy)\", OSName: \"ubuntu\", MajorVersion: \"6\", MinorVersion: \"6\", ExpectedCodename: \"dapper\"},\n\t\t{Name: \"Debian 2.1 exact\", OSName: \"debian\", MajorVersion: \"2\", MinorVersion: \"1\", ExpectedCodename: \"slink\"},\n\t\t{Name: \"Debian 2 fallback to *\", OSName: \"debian\", MajorVersion: \"2\", MinorVersion: \"0\", ExpectedCodename: \"hamm\"},\n\t\t{Name: \"Invalid OS name\", OSName: \"nonexistentOS\", MajorVersion: \"10\", MinorVersion: \"04\", ExpectedCodename: \"\"},\n\t\t{Name: \"Invalid major version\", OSName: \"ubuntu\", MajorVersion: \"99\", MinorVersion: \"04\", ExpectedCodename: \"\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.Name, func(t *testing.T) {\n\t\t\tactualCodename := LookupOS(tt.OSName, tt.MajorVersion, tt.MinorVersion)\n\t\t\tassert.Equal(t, tt.ExpectedCodename, actualCodename)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/db/internal/codename/codenames_generated.go",
    "content": "// DO NOT EDIT: generated by grype/db/internal/codename/generate/main.go\n\npackage codename\n\nvar normalizedOSCodenames = map[string]map[string]map[string]string{\n\t\"debian\": {\n\t\t\"1\": {\n\t\t\t\"1\": \"buzz\",\n\t\t\t\"2\": \"rex\",\n\t\t\t\"3\": \"bo\",\n\t\t},\n\t\t\"10\": {\"*\": \"buster\"},\n\t\t\"11\": {\"*\": \"bullseye\"},\n\t\t\"12\": {\"*\": \"bookworm\"},\n\t\t\"13\": {\"*\": \"trixie\"},\n\t\t\"2\": {\n\t\t\t\"0\": \"hamm\",\n\t\t\t\"1\": \"slink\",\n\t\t\t\"2\": \"potato\",\n\t\t},\n\t\t\"3\": {\n\t\t\t\"0\": \"woody\",\n\t\t\t\"1\": \"sarge\",\n\t\t},\n\t\t\"4\": {\"*\": \"etch\"},\n\t\t\"5\": {\"*\": \"lenny\"},\n\t\t\"6\": {\"*\": \"squeeze\"},\n\t\t\"7\": {\"*\": \"wheezy\"},\n\t\t\"8\": {\"*\": \"jessie\"},\n\t\t\"9\": {\"*\": \"stretch\"},\n\t},\n\t\"ubuntu\": {\n\t\t\"10\": {\n\t\t\t\"10\": \"maverick\",\n\t\t\t\"4\":  \"lucid\",\n\t\t},\n\t\t\"11\": {\n\t\t\t\"10\": \"oneiric\",\n\t\t\t\"4\":  \"natty\",\n\t\t},\n\t\t\"12\": {\n\t\t\t\"10\": \"quantal\",\n\t\t\t\"4\":  \"precise\",\n\t\t},\n\t\t\"13\": {\n\t\t\t\"10\": \"saucy\",\n\t\t\t\"4\":  \"raring\",\n\t\t},\n\t\t\"14\": {\n\t\t\t\"10\": \"utopic\",\n\t\t\t\"4\":  \"trusty\",\n\t\t},\n\t\t\"15\": {\n\t\t\t\"10\": \"wily\",\n\t\t\t\"4\":  \"vivid\",\n\t\t},\n\t\t\"16\": {\n\t\t\t\"10\": \"yakkety\",\n\t\t\t\"4\":  \"xenial\",\n\t\t},\n\t\t\"17\": {\n\t\t\t\"10\": \"artful\",\n\t\t\t\"4\":  \"zesty\",\n\t\t},\n\t\t\"18\": {\n\t\t\t\"10\": \"cosmic\",\n\t\t\t\"4\":  \"bionic\",\n\t\t},\n\t\t\"19\": {\n\t\t\t\"10\": \"eoan\",\n\t\t\t\"4\":  \"disco\",\n\t\t},\n\t\t\"20\": {\n\t\t\t\"10\": \"groovy\",\n\t\t\t\"4\":  \"focal\",\n\t\t},\n\t\t\"21\": {\n\t\t\t\"10\": \"impish\",\n\t\t\t\"4\":  \"hirsute\",\n\t\t},\n\t\t\"22\": {\n\t\t\t\"10\": \"kinetic\",\n\t\t\t\"4\":  \"jammy\",\n\t\t},\n\t\t\"23\": {\n\t\t\t\"10\": \"mantic\",\n\t\t\t\"4\":  \"lunar\",\n\t\t},\n\t\t\"24\": {\n\t\t\t\"10\": \"oracular\",\n\t\t\t\"4\":  \"noble\",\n\t\t},\n\t\t\"25\": {\n\t\t\t\"10\": \"questing\",\n\t\t\t\"4\":  \"plucky\",\n\t\t},\n\t\t\"4\": {\"10\": \"warty\"},\n\t\t\"5\": {\n\t\t\t\"10\": \"breezy\",\n\t\t\t\"4\":  \"hoary\",\n\t\t},\n\t\t\"6\": {\n\t\t\t\"10\": \"edgy\",\n\t\t\t\"6\":  \"dapper\",\n\t\t},\n\t\t\"7\": {\n\t\t\t\"10\": \"gutsy\",\n\t\t\t\"4\":  \"feisty\",\n\t\t},\n\t\t\"8\": {\n\t\t\t\"10\": \"intrepid\",\n\t\t\t\"4\":  \"hardy\",\n\t\t},\n\t\t\"9\": {\n\t\t\t\"10\": \"karmic\",\n\t\t\t\"4\":  \"jaunty\",\n\t\t},\n\t},\n}\n"
  },
  {
    "path": "grype/db/internal/codename/generate/main.go",
    "content": "package main\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/dave/jennifer/jen\"\n)\n\nconst (\n\toutputPackage = \"grype/db/internal/codename\"\n\toutputPath    = \"internal/codename/codenames_generated.go\" // relative to where go generate is called\n)\n\ntype Version struct {\n\tCycle    string `json:\"cycle\"`\n\tCodename string `json:\"codename\"`\n}\n\nfunc main() {\n\tosCodenames := make(map[string]map[string]map[string]string)\n\n\tfmt.Println(\"Fetching and parsing data for operating system codenames\")\n\n\tfmt.Println(\"ubuntu:\")\n\tosCodenames[\"ubuntu\"] = fetchAndParse(\"https://endoflife.date/api/ubuntu.json\", ubuntuHandler)\n\n\tfmt.Println(\"debian:\")\n\tosCodenames[\"debian\"] = fetchAndParse(\"https://endoflife.date/api/debian.json\", lowercaseHandler)\n\n\tfmt.Printf(\"Generating code for %d operating system codenames\\n\", len(osCodenames))\n\n\tf := jen.NewFile(\"codename\")\n\tf.HeaderComment(\"DO NOT EDIT: generated by grype/db/internal/codename/generate/main.go\")\n\tf.ImportName(outputPackage, \"pkg\")\n\tf.Var().Id(\"normalizedOSCodenames\").Op(\"=\").Map(jen.String()).Map(jen.String()).Map(jen.String()).String().Values(jen.DictFunc(func(d jen.Dict) {\n\t\tfor osName, versions := range osCodenames {\n\t\t\tmajorMap := jen.Dict{}\n\t\t\tfor major, minors := range versions {\n\t\t\t\tminorMap := jen.Dict{}\n\t\t\t\tfor minor, codename := range minors {\n\t\t\t\t\tminorMap[jen.Lit(minor)] = jen.Lit(codename)\n\t\t\t\t}\n\t\t\t\tmajorMap[jen.Lit(major)] = jen.Values(minorMap)\n\t\t\t}\n\t\t\td[jen.Lit(osName)] = jen.Values(majorMap)\n\t\t}\n\t}))\n\n\trendered := fmt.Sprintf(\"%#v\", f)\n\n\tfile, err := os.OpenFile(outputPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)\n\tif err != nil {\n\t\tpanic(fmt.Errorf(\"unable to open file: %w\", err))\n\t}\n\tdefer file.Close()\n\n\tif _, err := file.WriteString(rendered); err != nil {\n\t\tpanic(fmt.Errorf(\"unable to write file: %w\", err))\n\t}\n\n\tfmt.Printf(\"Code generation completed and written to %s\\n\", outputPath)\n}\n\n// fetchAndParse fetches the JSON data from a URL, parses it, and organizes it into a map.\nfunc fetchAndParse(url string, handler func(string) string) map[string]map[string]string {\n\tresp, err := http.Get(url) //nolint:gosec\n\tif err != nil {\n\t\tpanic(fmt.Errorf(\"error fetching data from %s: %w\", url, err))\n\t}\n\tdefer resp.Body.Close()\n\n\tdata, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\tpanic(fmt.Errorf(\"error reading response: %w\", err))\n\t}\n\n\tvar versions []Version\n\tif err := json.Unmarshal(data, &versions); err != nil {\n\t\tpanic(fmt.Errorf(\"error parsing JSON: %w\", err))\n\t}\n\n\tparsedData := make(map[string]map[string]string)\n\tfor _, version := range versions {\n\t\tmajor, minor := parseVersion(version.Cycle)\n\t\tif parsedData[major] == nil {\n\t\t\tparsedData[major] = make(map[string]string)\n\t\t}\n\t\tcodename := handler(version.Codename)\n\t\tfmt.Printf(\"  adding %s.%s --> %s\\n\", major, minor, codename)\n\t\tparsedData[major][minor] = codename\n\t}\n\n\treturn parsedData\n}\n\nfunc lowercaseHandler(codename string) string {\n\treturn strings.ToLower(codename)\n}\n\nfunc ubuntuHandler(codename string) string {\n\treturn strings.ToLower(strings.Split(codename, \" \")[0])\n}\n\n// parseVersion splits a version string like \"20.04\" into major \"20\" and minor \"04\".\nfunc parseVersion(version string) (string, string) {\n\tparts := strings.Split(version, \".\")\n\tmajor := strings.TrimLeft(parts[0], \"0\")\n\tminor := \"*\"\n\tif len(parts) > 1 {\n\t\tif parts[1] == \"0\" {\n\t\t\tminor = parts[1]\n\t\t} else {\n\t\t\tminor = strings.TrimLeft(parts[1], \"0\")\n\t\t}\n\t}\n\treturn major, minor\n}\n"
  },
  {
    "path": "grype/db/internal/gormadapter/logger.go",
    "content": "package gormadapter\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"gorm.io/gorm/logger\"\n\n\tanchoreLogger \"github.com/anchore/go-logger\"\n\t\"github.com/anchore/grype/internal/log\"\n)\n\n// logAdapter is meant to adapt the gorm logger interface (see https://github.com/go-gorm/gorm/blob/v1.25.12/logger/logger.go)\n// to the anchore logger interface.\ntype logAdapter struct {\n\tdebug         bool\n\tslowThreshold time.Duration\n\tlevel         logger.LogLevel\n}\n\n// LogMode sets the log level for the logger and returns a new instance\nfunc (l *logAdapter) LogMode(level logger.LogLevel) logger.Interface {\n\tnewlogger := *l\n\tnewlogger.level = level\n\treturn &newlogger\n}\n\nfunc (l logAdapter) Info(_ context.Context, fmt string, v ...interface{}) {\n\tif l.level >= logger.Info {\n\t\tif l.debug {\n\t\t\tlog.Infof(\"[sql] \"+fmt, v...)\n\t\t}\n\t}\n}\n\nfunc (l logAdapter) Warn(_ context.Context, fmt string, v ...interface{}) {\n\tif l.level >= logger.Warn {\n\t\tlog.Warnf(\"[sql] \"+fmt, v...)\n\t}\n}\n\nfunc (l logAdapter) Error(_ context.Context, fmt string, v ...interface{}) {\n\tif l.level >= logger.Error {\n\t\tlog.Errorf(\"[sql] \"+fmt, v...)\n\t}\n}\n\n// Trace logs the SQL statement and the duration it took to run the statement\nfunc (l logAdapter) Trace(_ context.Context, t time.Time, fn func() (sql string, rowsAffected int64), _ error) {\n\tif l.level <= logger.Silent {\n\t\treturn\n\t}\n\n\tif l.debug {\n\t\tsql, rowsAffected := fn()\n\t\telapsed := time.Since(t)\n\t\tfields := anchoreLogger.Fields{\n\t\t\t\"rows\":     rowsAffected,\n\t\t\t\"duration\": elapsed,\n\t\t}\n\n\t\tisSlow := l.slowThreshold != 0 && elapsed > l.slowThreshold\n\t\tif isSlow {\n\t\t\tfields[\"is-slow\"] = isSlow\n\t\t\tfields[\"slow-threshold\"] = fmt.Sprintf(\"> %s\", l.slowThreshold)\n\t\t\tlog.WithFields(fields).Warnf(\"[sql] %s\", sql)\n\t\t} else {\n\t\t\tlog.WithFields(fields).Tracef(\"[sql] %s\", sql)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "grype/db/internal/gormadapter/open.go",
    "content": "package gormadapter\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/glebarez/sqlite\"\n\t\"gorm.io/gorm\"\n\n\t\"github.com/anchore/grype/internal/log\"\n)\n\nvar commonStatements = []string{\n\t`PRAGMA foreign_keys = ON`, // needed for v6+\n}\n\nvar writerStatements = []string{\n\t// performance improvements (note: will result in lost data on write interruptions)\n\t`PRAGMA synchronous = OFF`,     // minimize the amount of syncing to disk, prioritizing write performance over durability\n\t`PRAGMA journal_mode = MEMORY`, // do not write the journal to disk (maximizing write performance); OFF is faster but less safe in terms of DB consistency\n}\n\nvar heavyWriteStatements = []string{\n\t`PRAGMA cache_size = -1073741824`, // ~1 GB (negative means treat as bytes not page count); one caveat is to not pick a value that risks swapping behavior, negating performance gains\n\t`PRAGMA mmap_size = 1073741824`,   // ~1 GB; the maximum size of the memory-mapped I/O buffer (to access the database file as if it were a part of the process’s virtual memory)\n\t`PRAGMA defer_foreign_keys = ON`,  // defer enforcement of foreign key constraints until the end of the transaction (to avoid the overhead of checking constraints for each row)\n}\n\nvar readConnectionOptions = []string{\n\t\"immutable=1\",  // indicates that the database file is guaranteed not to change during the connection’s lifetime (slight performance benefit for read-only cases)\n\t\"mode=ro\",      // opens the database in as read-only (an enforcement mechanism to allow immutable=1 to be effective)\n\t\"cache=shared\", // multiple database connections within the same process share a single page cache\n}\n\ntype config struct {\n\tdebug                     bool\n\tpath                      string\n\twritable                  bool\n\ttruncate                  bool\n\tallowLargeMemoryFootprint bool\n\tmodels                    []any\n\tinitialData               []any\n\tmemory                    bool\n\tstatements                []string\n}\n\ntype Option func(*config)\n\nfunc WithDebug(debug bool) Option {\n\treturn func(c *config) {\n\t\tc.debug = debug\n\t}\n}\n\nfunc WithTruncate(truncate bool, models []any, initialData []any) Option {\n\treturn func(c *config) {\n\t\tc.truncate = truncate\n\t\tif truncate {\n\t\t\tc.writable = true\n\t\t\tc.models = models\n\t\t\tc.initialData = initialData\n\t\t\tc.allowLargeMemoryFootprint = true\n\t\t}\n\t}\n}\n\nfunc WithStatements(statements ...string) Option {\n\treturn func(c *config) {\n\t\tc.statements = append(c.statements, statements...)\n\t}\n}\n\nfunc WithModels(models []any) Option {\n\treturn func(c *config) {\n\t\tc.models = append(c.models, models...)\n\t}\n}\n\nfunc WithWritable(write bool, models []any) Option {\n\treturn func(c *config) {\n\t\tc.writable = write\n\t\tc.models = models\n\t}\n}\n\nfunc WithLargeMemoryFootprint(largeFootprint bool) Option {\n\treturn func(c *config) {\n\t\tc.allowLargeMemoryFootprint = largeFootprint\n\t}\n}\n\nfunc newConfig(path string, opts []Option) config {\n\tc := config{}\n\tc.apply(path, opts)\n\treturn c\n}\n\nfunc (c *config) apply(path string, opts []Option) {\n\tfor _, o := range opts {\n\t\to(c)\n\t}\n\tc.memory = len(path) == 0\n\tc.path = path\n}\n\nfunc (c config) connectionString() string {\n\tvar conn string\n\tif c.path == \"\" {\n\t\tconn = \":memory:\"\n\t} else {\n\t\tconn = fmt.Sprintf(\"file:%s?cache=shared\", c.path)\n\t}\n\n\tif !c.writable && !c.memory {\n\t\tif !strings.Contains(conn, \"?\") {\n\t\t\tconn += \"?\"\n\t\t}\n\t\tfor _, o := range readConnectionOptions {\n\t\t\tconn += fmt.Sprintf(\"&%s\", o)\n\t\t}\n\t}\n\treturn conn\n}\n\n// Open a new connection to a sqlite3 database file\nfunc Open(path string, options ...Option) (*gorm.DB, error) {\n\tcfg := newConfig(path, options)\n\n\tif cfg.truncate && !cfg.writable {\n\t\treturn nil, fmt.Errorf(\"cannot truncate a read-only DB\")\n\t}\n\n\tif cfg.truncate {\n\t\tif err := deleteDB(path); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tdbObj, err := gorm.Open(sqlite.Open(cfg.connectionString()), &gorm.Config{Logger: &logAdapter{\n\t\tdebug:         cfg.debug,\n\t\tslowThreshold: 400 * time.Millisecond,\n\t}})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to connect to DB: %w\", err)\n\t}\n\n\treturn cfg.prepareDB(dbObj)\n}\n\nfunc (c config) prepareDB(dbObj *gorm.DB) (*gorm.DB, error) {\n\tif c.writable {\n\t\tlog.WithFields(\"path\", c.path).Debug(\"using writable DB statements\")\n\t\tif err := c.applyStatements(dbObj, writerStatements); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"unable to apply DB writer statements: %w\", err)\n\t\t}\n\t}\n\n\tif c.truncate && c.allowLargeMemoryFootprint {\n\t\tlog.WithFields(\"path\", c.path).Debug(\"using large memory footprint DB statements\")\n\t\tif err := c.applyStatements(dbObj, heavyWriteStatements); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"unable to apply DB heavy writer statements: %w\", err)\n\t\t}\n\t}\n\n\tif len(commonStatements) > 0 {\n\t\tif err := c.applyStatements(dbObj, commonStatements); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"unable to apply DB common statements: %w\", err)\n\t\t}\n\t}\n\n\tif len(c.statements) > 0 {\n\t\tif err := c.applyStatements(dbObj, c.statements); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"unable to apply DB custom statements: %w\", err)\n\t\t}\n\t}\n\n\tif len(c.models) > 0 && c.writable {\n\t\tlog.WithFields(\"path\", c.path).Debug(\"applying DB migrations\")\n\t\tif err := dbObj.AutoMigrate(c.models...); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"unable to migrate: %w\", err)\n\t\t}\n\t\t// now that there are potentially new models and indexes, analyze the DB to ensure the query planner is up-to-date\n\t\tif err := dbObj.Exec(\"ANALYZE\").Error; err != nil {\n\t\t\treturn nil, fmt.Errorf(\"unable to analyze DB: %w\", err)\n\t\t}\n\t}\n\n\tif len(c.initialData) > 0 && c.truncate {\n\t\tlog.WithFields(\"path\", c.path).Debug(\"writing initial data\")\n\t\tfor _, d := range c.initialData {\n\t\t\tif err := dbObj.Create(d).Error; err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"unable to create initial data: %w\", err)\n\t\t\t}\n\t\t}\n\t}\n\n\tif c.debug {\n\t\tdbObj = dbObj.Debug()\n\t}\n\n\treturn dbObj, nil\n}\n\nfunc (c config) applyStatements(db *gorm.DB, statements []string) error {\n\tfor _, sqlStmt := range statements {\n\t\tif err := db.Exec(sqlStmt).Error; err != nil {\n\t\t\treturn fmt.Errorf(\"unable to execute (%s): %w\", sqlStmt, err)\n\t\t}\n\t\tif strings.HasPrefix(sqlStmt, \"PRAGMA\") {\n\t\t\tname, value, err := c.pragmaNameValue(sqlStmt)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"unable to parse PRAGMA statement: %w\", err)\n\t\t\t}\n\n\t\t\tvar result string\n\t\t\tif err := db.Raw(\"PRAGMA \" + name + \";\").Scan(&result).Error; err != nil {\n\t\t\t\treturn fmt.Errorf(\"unable to verify PRAGMA %q: %w\", name, err)\n\t\t\t}\n\n\t\t\tif !strings.EqualFold(result, value) {\n\t\t\t\tif value == \"ON\" && result == \"1\" {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif value == \"OFF\" && result == \"0\" {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\treturn fmt.Errorf(\"PRAGMA %q was not set to %q (%q)\", name, value, result)\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (c config) pragmaNameValue(sqlStmt string) (string, string, error) {\n\tsqlStmt = strings.TrimSuffix(strings.TrimSpace(sqlStmt), \";\") // remove the trailing semicolon\n\tif strings.Count(sqlStmt, \";\") > 0 {\n\t\treturn \"\", \"\", fmt.Errorf(\"PRAGMA statements should not contain semicolons: %q\", sqlStmt)\n\t}\n\n\t// check if the pragma was set, parse the pragma name and value from the statement. This is because\n\t// sqlite will not return errors when there are issues with the pragma key or value, but it will\n\t// be inconsistent with the expected value if you explicitly check\n\tvar name, value string\n\n\tclean := strings.TrimPrefix(sqlStmt, \"PRAGMA\")\n\tfields := strings.SplitN(clean, \"=\", 2)\n\tif len(fields) == 2 {\n\t\tname = strings.ToLower(strings.TrimSpace(fields[0]))\n\t\tvalue = strings.TrimSpace(fields[1])\n\t} else {\n\t\treturn \"\", \"\", fmt.Errorf(\"unable to parse PRAGMA statement: %q\", sqlStmt)\n\t}\n\n\tif c.memory && name == \"mmap_size\" {\n\t\t// memory only DBs do not have mmap capability\n\t\tvalue = \"\"\n\t}\n\n\tif name == \"\" {\n\t\treturn \"\", \"\", fmt.Errorf(\"unable to parse name from PRAGMA statement: %q\", sqlStmt)\n\t}\n\n\treturn name, value, nil\n}\n\nfunc deleteDB(path string) error {\n\tif _, err := os.Stat(path); err == nil {\n\t\tif err := os.Remove(path); err != nil {\n\t\t\treturn fmt.Errorf(\"unable to remove existing DB file: %w\", err)\n\t\t}\n\t}\n\n\tparent := filepath.Dir(path)\n\tif err := os.MkdirAll(parent, 0700); err != nil {\n\t\treturn fmt.Errorf(\"unable to create parent directory %q for DB file: %w\", parent, err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "grype/db/internal/gormadapter/open_test.go",
    "content": "package gormadapter\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestConfigApply(t *testing.T) {\n\ttests := []struct {\n\t\tname           string\n\t\tpath           string\n\t\toptions        []Option\n\t\texpectedPath   string\n\t\texpectedMemory bool\n\t}{\n\t\t{\n\t\t\tname:           \"apply with path\",\n\t\t\tpath:           \"test.db\",\n\t\t\toptions:        []Option{},\n\t\t\texpectedPath:   \"test.db\",\n\t\t\texpectedMemory: false,\n\t\t},\n\t\t{\n\t\t\tname:           \"apply with empty path (memory)\",\n\t\t\tpath:           \"\",\n\t\t\toptions:        []Option{},\n\t\t\texpectedPath:   \"\",\n\t\t\texpectedMemory: true,\n\t\t},\n\t\t{\n\t\t\tname:           \"apply with truncate option\",\n\t\t\tpath:           \"test.db\",\n\t\t\toptions:        []Option{WithTruncate(true, nil, nil)}, // migration and initial data don't matter\n\t\t\texpectedPath:   \"test.db\",\n\t\t\texpectedMemory: false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tc := newConfig(tt.path, tt.options)\n\n\t\t\trequire.Equal(t, tt.expectedPath, c.path)\n\t\t\trequire.Equal(t, tt.expectedMemory, c.memory)\n\t\t})\n\t}\n}\n\nfunc TestConfigConnectionString(t *testing.T) {\n\ttests := []struct {\n\t\tname            string\n\t\tpath            string\n\t\twrite           bool\n\t\tmemory          bool\n\t\texpectedConnStr string\n\t}{\n\t\t{\n\t\t\tname:            \"writable path\",\n\t\t\tpath:            \"test.db\",\n\t\t\twrite:           true,\n\t\t\texpectedConnStr: \"file:test.db?cache=shared\",\n\t\t},\n\t\t{\n\t\t\tname:            \"read-only path\",\n\t\t\tpath:            \"test.db\",\n\t\t\twrite:           false,\n\t\t\texpectedConnStr: \"file:test.db?cache=shared&immutable=1&mode=ro&cache=shared\",\n\t\t},\n\t\t{\n\t\t\tname:            \"in-memory mode\",\n\t\t\tpath:            \"\",\n\t\t\twrite:           false,\n\t\t\tmemory:          true,\n\t\t\texpectedConnStr: \":memory:\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tc := config{\n\t\t\t\tpath:     tt.path,\n\t\t\t\twritable: tt.write,\n\t\t\t\tmemory:   tt.memory,\n\t\t\t}\n\t\t\trequire.Equal(t, tt.expectedConnStr, c.connectionString())\n\t\t})\n\t}\n}\n\nfunc TestPrepareWritableDB(t *testing.T) {\n\n\tt.Run(\"creates new directory and file when path does not exist\", func(t *testing.T) {\n\t\ttempDir := t.TempDir()\n\t\tdbPath := filepath.Join(tempDir, \"newdir\", \"test.db\")\n\n\t\terr := deleteDB(dbPath)\n\t\trequire.NoError(t, err)\n\n\t\t_, err = os.Stat(filepath.Dir(dbPath))\n\t\trequire.NoError(t, err)\n\t})\n\n\tt.Run(\"removes existing file at path\", func(t *testing.T) {\n\t\ttempDir := t.TempDir()\n\t\tdbPath := filepath.Join(tempDir, \"test.db\")\n\n\t\t_, err := os.Create(dbPath)\n\t\trequire.NoError(t, err)\n\n\t\t_, err = os.Stat(dbPath)\n\t\trequire.NoError(t, err)\n\n\t\terr = deleteDB(dbPath)\n\t\trequire.NoError(t, err)\n\n\t\t_, err = os.Stat(dbPath)\n\t\trequire.True(t, os.IsNotExist(err))\n\t})\n\n\tt.Run(\"returns error if unable to create parent directory\", func(t *testing.T) {\n\t\tinvalidDir := filepath.Join(\"/root\", \"invalidDir\", \"test.db\")\n\t\terr := deleteDB(invalidDir)\n\t\trequire.Error(t, err)\n\t\trequire.Contains(t, err.Error(), \"unable to create parent directory\")\n\t})\n}\n\nfunc TestPragmaNameValue(t *testing.T) {\n\ttests := []struct {\n\t\tname      string\n\t\tcfg       config\n\t\tinput     string\n\t\twantName  string\n\t\twantValue string\n\t\twantErr   require.ErrorAssertionFunc\n\t}{\n\t\t{\n\t\t\tname:      \"basic pragma\",\n\t\t\tcfg:       config{memory: false},\n\t\t\tinput:     \"PRAGMA journal_mode=WAL\",\n\t\t\twantName:  \"journal_mode\",\n\t\t\twantValue: \"WAL\",\n\t\t},\n\t\t{\n\t\t\tname:      \"pragma with spaces\",\n\t\t\tcfg:       config{memory: false},\n\t\t\tinput:     \"PRAGMA   cache_size  =   2000  \",\n\t\t\twantName:  \"cache_size\",\n\t\t\twantValue: \"2000\",\n\t\t},\n\t\t{\n\t\t\tname:      \"pragma with trailing semicolon\",\n\t\t\tcfg:       config{memory: false},\n\t\t\tinput:     \"PRAGMA synchronous=NORMAL;\",\n\t\t\twantName:  \"synchronous\",\n\t\t\twantValue: \"NORMAL\",\n\t\t},\n\t\t{\n\t\t\tname:    \"pragma with multiple semicolons\",\n\t\t\tcfg:     config{memory: false},\n\t\t\tinput:   \"PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL;\",\n\t\t\twantErr: require.Error,\n\t\t},\n\t\t{\n\t\t\tname:    \"invalid pragma format\",\n\t\t\tcfg:     config{memory: false},\n\t\t\tinput:   \"PRAGMA invalid_format\",\n\t\t\twantErr: require.Error,\n\t\t},\n\t\t{\n\t\t\tname:      \"mmap_size pragma with memory DB\",\n\t\t\tcfg:       config{memory: true},\n\t\t\tinput:     \"PRAGMA mmap_size=1000\",\n\t\t\twantName:  \"mmap_size\",\n\t\t\twantValue: \"\", // should be empty for memory DB\n\t\t},\n\t\t{\n\t\t\tname:      \"mmap_size pragma with regular DB\",\n\t\t\tcfg:       config{memory: false},\n\t\t\tinput:     \"PRAGMA mmap_size=1000\",\n\t\t\twantName:  \"mmap_size\",\n\t\t\twantValue: \"1000\",\n\t\t},\n\t\t{\n\t\t\tname:      \"pragma with numeric value\",\n\t\t\tcfg:       config{memory: false},\n\t\t\tinput:     \"PRAGMA page_size=4096\",\n\t\t\twantName:  \"page_size\",\n\t\t\twantValue: \"4096\",\n\t\t},\n\t\t{\n\t\t\tname:      \"pragma with mixed case\",\n\t\t\tcfg:       config{memory: false},\n\t\t\tinput:     \"PRAGMA Journal_Mode=WAL\",\n\t\t\twantName:  \"journal_mode\",\n\t\t\twantValue: \"WAL\",\n\t\t},\n\t\t{\n\t\t\tname:    \"empty pragma\",\n\t\t\tcfg:     config{memory: false},\n\t\t\tinput:   \"PRAGMA =value\",\n\t\t\twantErr: require.Error,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif tt.wantErr == nil {\n\t\t\t\ttt.wantErr = require.NoError\n\t\t\t}\n\n\t\t\tgotName, gotValue, err := tt.cfg.pragmaNameValue(tt.input)\n\t\t\ttt.wantErr(t, err)\n\n\t\t\tif err == nil {\n\t\t\t\trequire.Equal(t, tt.wantName, gotName)\n\t\t\t\trequire.Equal(t, tt.wantValue, gotValue)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/db/internal/provider/unmarshal/annotated_openvex_vulnerability.go",
    "content": "package unmarshal\n\nimport (\n\t\"io\"\n\n\tgovex \"github.com/openvex/go-vex/pkg/vex\"\n)\n\ntype AnnotatedOpenVEXVulnerability struct {\n\tDocument govex.Statement       `json:\"document\"`\n\tFixes    []AnnotatedOpenVEXFix `json:\"fixes\"`\n}\n\ntype AnnotatedOpenVEXFix struct {\n\tAvailable AnnotatedOpenVEXFixAvailability `json:\"available\"`\n\tProduct   string                          `json:\"product\"`\n}\n\ntype AnnotatedOpenVEXFixAvailability struct {\n\tDate string `json:\"date\"`\n\tKind string `json:\"kind\"`\n}\n\nfunc AnnotatedOpenVEXVulnerabilityEntries(reader io.Reader) ([]AnnotatedOpenVEXVulnerability, error) {\n\treturn unmarshalSingleOrMulti[AnnotatedOpenVEXVulnerability](reader)\n}\n"
  },
  {
    "path": "grype/db/internal/provider/unmarshal/eol.go",
    "content": "package unmarshal\n\nimport \"io\"\n\n// EndOfLifeDateRelease represents a single release entry from the endoflife.date API v1.\n// This matches the ProductRelease schema with camelCase field names.\n// Ref: https://endoflife.date/api/v1/products/{product}\n//\n// Note: Product and Identifiers are denormalized from the parent product by vunnel.\ntype EndOfLifeDateRelease struct {\n\t// Denormalized fields added by vunnel\n\tProduct     string                    `json:\"product\"`\n\tIdentifiers []EndOfLifeDateIdentifier `json:\"identifiers\"`\n\n\t// Fields from endoflife.date ProductRelease schema\n\tName         string                 `json:\"name\"`\n\tCodename     *string                `json:\"codename\"`\n\tLabel        string                 `json:\"label\"`\n\tReleaseDate  *string                `json:\"releaseDate\"`\n\tIsLTS        bool                   `json:\"isLts\"`\n\tLTSFrom      *string                `json:\"ltsFrom\"`\n\tIsEOAS       bool                   `json:\"isEoas\"`\n\tEOASFrom     *string                `json:\"eoasFrom\"`\n\tIsEOL        bool                   `json:\"isEol\"`\n\tEOLFrom      *string                `json:\"eolFrom\"`\n\tIsMaintained bool                   `json:\"isMaintained\"`\n\tLatest       *EndOfLifeDateLatest   `json:\"latest\"`\n\tCustom       map[string]interface{} `json:\"custom\"`\n}\n\n// EndOfLifeDateLatest represents the latest release info nested within a release.\ntype EndOfLifeDateLatest struct {\n\tName string  `json:\"name\"`\n\tDate *string `json:\"date\"`\n\tLink *string `json:\"link\"`\n}\n\n// EndOfLifeDateIdentifier represents a CPE, PURL, or Repology identifier.\ntype EndOfLifeDateIdentifier struct {\n\tType string `json:\"type\"`\n\tID   string `json:\"id\"`\n}\n\n// EndOfLifeDateLabels contains custom labels for EOL-related dates.\ntype EndOfLifeDateLabels struct {\n\tEOAS         *string `json:\"eoas\"`\n\tDiscontinued *string `json:\"discontinued\"`\n\tEOL          *string `json:\"eol\"`\n\tEOES         *string `json:\"eoes\"`\n}\n\n// EndOfLifeDateLinks contains URLs for the product.\ntype EndOfLifeDateLinks struct {\n\tIcon          *string `json:\"icon\"`\n\tHTML          *string `json:\"html\"`\n\tReleasePolicy *string `json:\"releasePolicy\"`\n}\n\n// EndOfLifeDateResult represents the result object within a product response.\ntype EndOfLifeDateResult struct {\n\tName           string                    `json:\"name\"`\n\tAliases        []string                  `json:\"aliases\"`\n\tLabel          string                    `json:\"label\"`\n\tCategory       string                    `json:\"category\"`\n\tTags           []string                  `json:\"tags\"`\n\tVersionCommand *string                   `json:\"versionCommand\"`\n\tIdentifiers    []EndOfLifeDateIdentifier `json:\"identifiers\"`\n\tLabels         EndOfLifeDateLabels       `json:\"labels\"`\n\tLinks          EndOfLifeDateLinks        `json:\"links\"`\n\tReleases       []EndOfLifeDateRelease    `json:\"releases\"`\n}\n\n// EndOfLifeDateProduct represents the full product response from the endoflife.date API v1.\ntype EndOfLifeDateProduct struct {\n\tSchemaVersion string              `json:\"schema_version\"`\n\tGeneratedAt   string              `json:\"generated_at\"`\n\tLastModified  string              `json:\"last_modified\"`\n\tResult        EndOfLifeDateResult `json:\"result\"`\n}\n\n// IsEmpty returns true if the release has no meaningful data.\nfunc (e EndOfLifeDateRelease) IsEmpty() bool {\n\treturn e.Product == \"\"\n}\n\n// ProductName returns the product name from the release.\nfunc (e EndOfLifeDateRelease) ProductName() string {\n\treturn e.Product\n}\n\n// EndOfLifeDateReleaseEntries unmarshals EndOfLifeDateRelease records from a reader.\nfunc EndOfLifeDateReleaseEntries(reader io.Reader) ([]EndOfLifeDateRelease, error) {\n\treturn unmarshalSingleOrMulti[EndOfLifeDateRelease](reader)\n}\n"
  },
  {
    "path": "grype/db/internal/provider/unmarshal/epss.go",
    "content": "package unmarshal\n\nimport \"io\"\n\ntype EPSS struct {\n\tCVE        string  `json:\"cve\"`\n\tEPSS       float64 `json:\"epss\"`\n\tPercentile float64 `json:\"percentile\"`\n\tDate       string  `json:\"date\"`\n}\n\nfunc (o EPSS) IsEmpty() bool {\n\treturn o.CVE == \"\"\n}\n\nfunc EPSSEntries(reader io.Reader) ([]EPSS, error) {\n\treturn unmarshalSingleOrMulti[EPSS](reader)\n}\n"
  },
  {
    "path": "grype/db/internal/provider/unmarshal/errors.go",
    "content": "package unmarshal\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n)\n\nfunc handleJSONUnmarshalError(err error) error {\n\tif ute, ok := err.(*json.UnmarshalTypeError); ok { //nolint: errorlint\n\t\treturn fmt.Errorf(\"unmarshal type error: expected=%v, got=%v, field=%v, offset=%v\", ute.Type, ute.Value, ute.Field, ute.Offset)\n\t} else if se, ok := err.(*json.SyntaxError); ok { //nolint: errorlint\n\t\treturn fmt.Errorf(\"syntax error: offset=%v, error=%w\", se.Offset, se)\n\t}\n\treturn err\n}\n"
  },
  {
    "path": "grype/db/internal/provider/unmarshal/github_advisory.go",
    "content": "package unmarshal\n\nimport (\n\t\"io\"\n)\n\ntype GitHubAdvisory struct {\n\tAdvisory struct {\n\t\tClassification string\n\t\tCVE            []string `json:\"CVE\"`\n\t\tCVSS           *struct {\n\t\t\tBaseMetrics struct {\n\t\t\t\tBaseScore           float64 `json:\"base_score\"`\n\t\t\t\tBaseSeverity        string  `json:\"base_severity\"`\n\t\t\t\tExploitabilityScore float64 `json:\"exploitability_score\"`\n\t\t\t\tImpactScore         float64 `json:\"impact_score\"`\n\t\t\t} `json:\"base_metrics\"`\n\t\t\tStatus       string `json:\"status\"`\n\t\t\tVectorString string `json:\"vector_string\"`\n\t\t\tVersion      string `json:\"version\"`\n\t\t} `json:\"CVSS\"`\n\t\tCVSSSeverities []*struct {\n\t\t\tVector  string `json:\"vector\"`\n\t\t\tVersion string `json:\"version\"`\n\t\t} `json:\"cvss_severities\"`\n\t\tFixedIn  []GithubFixedIn `json:\"FixedIn\"`\n\t\tMetadata struct {\n\t\t\tCVE []string `json:\"CVE\"`\n\t\t} `json:\"Metadata\"`\n\t\tSeverity   string `json:\"Severity\"`\n\t\tSummary    string `json:\"Summary\"`\n\t\tGhsaID     string `json:\"ghsaId\"`\n\t\tNamespace  string `json:\"namespace\"`\n\t\tURL        string `json:\"url\"`\n\t\tPublished  string `json:\"published\"`\n\t\tUpdated    string `json:\"updated\"`\n\t\tWithdrawn  string `json:\"withdrawn\"`\n\t\tReferences []*struct {\n\t\t\tURL string `json:\"url\"`\n\t\t} `json:\"references\"`\n\t} `json:\"Advisory\"`\n}\n\nfunc (g GitHubAdvisory) IsEmpty() bool {\n\treturn g.Advisory.GhsaID == \"\"\n}\n\nfunc GitHubAdvisoryEntries(reader io.Reader) ([]GitHubAdvisory, error) {\n\treturn unmarshalSingleOrMulti[GitHubAdvisory](reader)\n}\n\ntype GithubFixedIn struct {\n\tEcosystem  string `json:\"ecosystem\"`\n\tIdentifier string `json:\"identifier\"`\n\tName       string `json:\"name\"`\n\tNamespace  string `json:\"namespace\"`\n\tRange      string `json:\"range\"`\n\tAvailable  struct {\n\t\tDate string `json:\"date,omitempty\"`\n\t\tKind string `json:\"kind,omitempty\"`\n\t} `json:\"available,omitempty\"`\n}\n"
  },
  {
    "path": "grype/db/internal/provider/unmarshal/items_envelope.go",
    "content": "package unmarshal\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n)\n\ntype ItemsEnvelope struct {\n\tSchema     string          `yaml:\"schema\" json:\"schema\" mapstructure:\"schema\"`\n\tIdentifier string          `yaml:\"identifier\" json:\"identifier\" mapstructure:\"identifier\"`\n\tItem       json.RawMessage `yaml:\"item\" json:\"item\" mapstructure:\"item\"`\n}\n\nfunc Envelope(reader io.Reader) (*ItemsEnvelope, error) {\n\tvar envelope ItemsEnvelope\n\tdec := json.NewDecoder(reader)\n\terr := dec.Decode(&envelope)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to open envelope: %w\", err)\n\t}\n\treturn &envelope, nil\n}\n"
  },
  {
    "path": "grype/db/internal/provider/unmarshal/known_exploited_vulnerability.go",
    "content": "package unmarshal\n\nimport \"io\"\n\ntype KnownExploitedVulnerability struct {\n\tCveID                      string   `json:\"cveID\"`\n\tVendorProject              string   `json:\"vendorProject\"`\n\tProduct                    string   `json:\"product\"`\n\tVulnerabilityName          string   `json:\"vulnerabilityName\"`\n\tDateAdded                  string   `json:\"dateAdded\"`\n\tShortDescription           string   `json:\"shortDescription\"`\n\tRequiredAction             string   `json:\"requiredAction\"`\n\tDueDate                    string   `json:\"dueDate\"`\n\tKnownRansomwareCampaignUse string   `json:\"knownRansomwareCampaignUse\"`\n\tNotes                      string   `json:\"notes\"`\n\tCWEs                       []string `json:\"cwes\"`\n}\n\nfunc (g KnownExploitedVulnerability) IsEmpty() bool {\n\treturn g.CveID == \"\"\n}\n\nfunc KnownExploitedVulnerabilityEntries(reader io.Reader) ([]KnownExploitedVulnerability, error) {\n\treturn unmarshalSingleOrMulti[KnownExploitedVulnerability](reader)\n}\n"
  },
  {
    "path": "grype/db/internal/provider/unmarshal/match_exclusion.go",
    "content": "package unmarshal\n\nimport (\n\t\"io\"\n)\n\ntype MatchExclusion struct {\n\tID          string `json:\"id\"`\n\tConstraints []struct {\n\t\tVulnerability struct {\n\t\t\tNamespace string `json:\"namespace,omitempty\"`\n\t\t\tFixState  string `json:\"fix_state,omitempty\"`\n\t\t} `json:\"vulnerability,omitempty\"`\n\t\tPackage struct {\n\t\t\tLanguage string `json:\"language,omitempty\"`\n\t\t\tType     string `json:\"type,omitempty\"`\n\t\t\tName     string `json:\"name,omitempty\"`\n\t\t\tVersion  string `json:\"version,omitempty\"`\n\t\t\tLocation string `json:\"location,omitempty\"`\n\t\t} `json:\"package,omitempty\"`\n\t} `json:\"constraints,omitempty\"`\n\tJustification string `json:\"justification\"`\n}\n\nfunc (m MatchExclusion) IsEmpty() bool {\n\treturn m.ID == \"\"\n}\n\nfunc MatchExclusions(reader io.Reader) ([]MatchExclusion, error) {\n\treturn unmarshalSingleOrMulti[MatchExclusion](reader)\n}\n"
  },
  {
    "path": "grype/db/internal/provider/unmarshal/msrc_vulnerability.go",
    "content": "package unmarshal\n\nimport (\n\t\"io\"\n)\n\n// MSRCVulnerability represents a single Msrc entry with vulnerability metadata\ntype MSRCVulnerability struct {\n\tCvss struct {\n\t\tBaseScore     float64 `json:\"base_score\"`\n\t\tTemporalScore float64 `json:\"temporal_score\"`\n\t\tVector        string  `json:\"vector\"`\n\t} `json:\"cvss\"`\n\tFixedIn []struct {\n\t\tID        string   `json:\"id\"`\n\t\tIsFirst   bool     `json:\"is_first\"`\n\t\tIsLatest  bool     `json:\"is_latest\"`\n\t\tLinks     []string `json:\"links\"`\n\t\tAvailable struct {\n\t\t\tDate string `json:\"date,omitempty\"`\n\t\t\tKind string `json:\"kind,omitempty\"`\n\t\t} `json:\"available,omitempty\"`\n\t} `json:\"fixed_in\"`\n\tID      string `json:\"id\"`\n\tLink    string `json:\"link\"`\n\tProduct struct {\n\t\tFamily string `json:\"family\"`\n\t\tID     string `json:\"id\"`\n\t\tName   string `json:\"name\"`\n\t} `json:\"product\"`\n\tSeverity   string   `json:\"severity\"`\n\tSummary    string   `json:\"summary\"`\n\tVulnerable []string `json:\"vulnerable\"`\n}\n\nfunc (o MSRCVulnerability) IsEmpty() bool {\n\treturn o.ID == \"\"\n}\n\nfunc MSRCVulnerabilityEntries(reader io.Reader) ([]MSRCVulnerability, error) {\n\treturn unmarshalSingleOrMulti[MSRCVulnerability](reader)\n}\n"
  },
  {
    "path": "grype/db/internal/provider/unmarshal/nvd/cve.go",
    "content": "package nvd\n\nimport (\n\t\"sort\"\n\n\t\"github.com/Masterminds/semver/v3\"\n\t\"github.com/jinzhu/copier\"\n\t\"golang.org/x/text/cases\"\n\t\"golang.org/x/text/language\"\n\n\t\"github.com/anchore/grype/grype/db/internal/provider/unmarshal/nvd/cvss20\"\n\t\"github.com/anchore/grype/grype/db/internal/provider/unmarshal/nvd/cvss30\"\n\t\"github.com/anchore/grype/grype/db/internal/provider/unmarshal/nvd/cvss31\"\n\t\"github.com/anchore/grype/grype/db/internal/provider/unmarshal/nvd/cvss40\"\n)\n\n// note: this was autogenerated with some manual tweaking (see schema/nvd/cve-api-json/README.md)\n\ntype Operator string\n\nconst (\n\tAnd Operator = \"AND\"\n\tOr  Operator = \"OR\"\n)\n\nconst englishLanguage = \"en\"\n\n// this is the struct to use when unmarshalling directly from the API (which grype-db is NOT doing)\n// type APIResults struct {\n//\tFormat          string          `json:\"format\"`\n//\tResultsPerPage  int64           `json:\"resultsPerPage\"`\n//\tStartIndex      int64           `json:\"startIndex\"`\n//\tTimestamp       string          `json:\"timestamp\"`\n//\tTotalResults    int64           `json:\"totalResults\"`\n//\tVersion         string          `json:\"version\"`\n//\tVulnerabilities []Vulnerability `json:\"vulnerabilities\"`\n//}\n\ntype Vulnerability struct {\n\tCve CveItem `json:\"cve\"`\n}\n\ntype CveItem struct {\n\tID string `json:\"id\"`\n\t// CisaActionDue         *string         `json:\"cisaActionDue,omitempty\"`\n\t// CisaExploitAdd        *string         `json:\"cisaExploitAdd,omitempty\"`\n\t// CisaRequiredAction    *string         `json:\"cisaRequiredAction,omitempty\"`\n\t// CisaVulnerabilityName *string         `json:\"cisaVulnerabilityName,omitempty\"`\n\tConfigurations []Configuration `json:\"configurations,omitempty\"`\n\tDescriptions   []LangString    `json:\"descriptions\"`\n\t// EvaluatorComment      *string         `json:\"evaluatorComment,omitempty\"`\n\t// EvaluatorImpact       *string         `json:\"evaluatorImpact,omitempty\"`\n\t// EvaluatorSolution     *string         `json:\"evaluatorSolution,omitempty\"`\n\tLastModified     string      `json:\"lastModified\"`\n\tMetrics          *Metrics    `json:\"metrics,omitempty\"`\n\tPublished        string      `json:\"published\"`\n\tReferences       []Reference `json:\"references\"`\n\tSourceIdentifier *string     `json:\"sourceIdentifier,omitempty\"`\n\t// VendorComments        []VendorComment `json:\"vendorComments,omitempty\"`\n\tVulnStatus *string    `json:\"vulnStatus,omitempty\"`\n\tWeaknesses []Weakness `json:\"weaknesses,omitempty\"`\n}\n\ntype Configuration struct {\n\tNegate   *bool     `json:\"negate,omitempty\"`\n\tNodes    []Node    `json:\"nodes\"`\n\tOperator *Operator `json:\"operator,omitempty\"`\n}\n\ntype Node struct {\n\tCpeMatch []CpeMatch `json:\"cpeMatch\"`\n\tNegate   *bool      `json:\"negate,omitempty\"`\n\tOperator Operator   `json:\"operator\"`\n}\n\ntype FixInfo struct {\n\tVersion string `json:\"version\"`\n\tDate    string `json:\"date\"`\n\tKind    string `json:\"kind\"`\n}\n\ntype CpeMatch struct {\n\tCriteria              string   `json:\"criteria\"`\n\tMatchCriteriaID       string   `json:\"matchCriteriaId\"`\n\tVersionEndExcluding   *string  `json:\"versionEndExcluding,omitempty\"`\n\tVersionEndIncluding   *string  `json:\"versionEndIncluding,omitempty\"`\n\tVersionStartExcluding *string  `json:\"versionStartExcluding,omitempty\"`\n\tVersionStartIncluding *string  `json:\"versionStartIncluding,omitempty\"`\n\tVulnerable            bool     `json:\"vulnerable\"`\n\tFix                   *FixInfo `json:\"fix,omitempty\"`\n}\n\ntype LangString struct {\n\tLang  string `json:\"lang\"`\n\tValue string `json:\"value\"`\n}\n\n// Metrics scores for a vulnerability as found on NVD.\ntype Metrics struct {\n\tCvssMetricV2  []CvssV2  `json:\"cvssMetricV2,omitempty\"`  // CVSS V2.0 score.\n\tCvssMetricV30 []CvssV30 `json:\"cvssMetricV30,omitempty\"` // CVSS V3.0 score.\n\tCvssMetricV31 []CvssV31 `json:\"cvssMetricV31,omitempty\"` // CVSS V3.1 score.\n\tCvssMetricV40 []CvssV40 `json:\"cvssMetricV40,omitempty\"` // CVSS V4.1 score.\n}\n\ntype CvssV2 struct {\n\t// ACInsufInfo             *bool         `json:\"acInsufInfo,omitempty\"`\n\tBaseSeverity        *string       `json:\"baseSeverity,omitempty\"`\n\tCvssData            cvss20.Cvss20 `json:\"cvssData\"`\n\tExploitabilityScore *float64      `json:\"exploitabilityScore,omitempty\"`\n\tImpactScore         *float64      `json:\"impactScore,omitempty\"`\n\t// ObtainAllPrivilege      *bool         `json:\"obtainAllPrivilege,omitempty\"`\n\t// ObtainOtherPrivilege    *bool         `json:\"obtainOtherPrivilege,omitempty\"`\n\t// ObtainUserPrivilege     *bool         `json:\"obtainUserPrivilege,omitempty\"`\n\tSource string   `json:\"source\"`\n\tType   CvssType `json:\"type\"`\n\t// UserInteractionRequired *bool         `json:\"userInteractionRequired,omitempty\"`\n}\n\ntype CvssV30 struct {\n\tCvssData            cvss30.Cvss30 `json:\"cvssData\"`\n\tExploitabilityScore *float64      `json:\"exploitabilityScore,omitempty\"`\n\tImpactScore         *float64      `json:\"impactScore,omitempty\"`\n\tSource              string        `json:\"source\"`\n\tType                CvssType      `json:\"type\"`\n}\n\ntype CvssV31 struct {\n\tCvssData            cvss31.Cvss31 `json:\"cvssData\"`\n\tExploitabilityScore *float64      `json:\"exploitabilityScore,omitempty\"`\n\tImpactScore         *float64      `json:\"impactScore,omitempty\"`\n\tSource              string        `json:\"source\"`\n\tType                CvssType      `json:\"type\"`\n}\n\ntype CvssV40 struct {\n\tCvssData            cvss40.Cvss40 `json:\"cvssData\"`\n\tExploitabilityScore *float64      `json:\"exploitabilityScore,omitempty\"`\n\tImpactScore         *float64      `json:\"impactScore,omitempty\"`\n\tSource              string        `json:\"source\"`\n\tType                CvssType      `json:\"type\"`\n}\n\n// CvssType relative to the NVD docs: \"type identifies whether the organization is a primary or secondary source.\n// Primary sources include the NVD and CNA who have reached the provider level in CVMAP. 10% of provider level\n// submissions are audited by the NVD. If a submission has been audited the NVD will appear as the primary source\n// and the provider level CNA will appear as the secondary source.\"\ntype CvssType string\n\nconst (\n\tPrimary   CvssType = \"Primary\"\n\tSecondary CvssType = \"Secondary\"\n)\n\ntype Reference struct {\n\tSource *string  `json:\"source,omitempty\"`\n\tTags   []string `json:\"tags,omitempty\"`\n\tURL    string   `json:\"url\"`\n}\n\n//\ttype VendorComment struct {\n//\t\tComment      string `json:\"comment\"`\n//\t\tLastModified string `json:\"lastModified\"`\n//\t\tOrganization string `json:\"organization\"`\n//\t}\ntype Weakness struct {\n\tDescription []LangString `json:\"description\"`\n\tSource      string       `json:\"source\"`\n\tType        string       `json:\"type\"`\n}\n\nfunc (o CveItem) Description() string {\n\tfor _, d := range o.Descriptions {\n\t\tif d.Lang == englishLanguage {\n\t\t\treturn d.Value\n\t\t}\n\t}\n\treturn \"\"\n}\n\ntype CvssSummary struct {\n\tSource              string\n\tType                CvssType\n\tVersion             string\n\tVector              string\n\tBaseScore           float64\n\tExploitabilityScore *float64\n\tImpactScore         *float64\n\tbaseSeverity        *string\n}\n\nfunc (o CvssSummary) Severity() string {\n\tif o.baseSeverity != nil {\n\t\treturn cases.Title(language.English).String(*o.baseSeverity)\n\t}\n\treturn \"\"\n}\n\nfunc (o CvssSummary) version() *semver.Version {\n\tv, err := semver.NewVersion(o.Version)\n\tif err != nil {\n\t\treturn semver.MustParse(\"2.0\")\n\t}\n\treturn v\n}\n\ntype CvssSummaries []CvssSummary\n\nfunc (o CvssSummaries) Len() int {\n\treturn len(o)\n}\n\nfunc (o CvssSummaries) Less(i, j int) bool {\n\tiEntry := o[i]\n\tjEntry := o[j]\n\n\t// first compare by type (Primary/Secondary)\n\tif iEntry.Type != jEntry.Type {\n\t\treturn iEntry.Type == Secondary\n\t}\n\n\t// then compare by source (NVD preferred, then lexicographic)\n\tif iEntry.Source != jEntry.Source {\n\t\tif iEntry.Source == \"nvd@nist.gov\" {\n\t\t\treturn false\n\t\t}\n\t\tif jEntry.Source == \"nvd@nist.gov\" {\n\t\t\treturn true\n\t\t}\n\t\t// for non-NVD sources, use lexicographic ordering (descending for Reverse sort)\n\t\treturn iEntry.Source > jEntry.Source\n\t}\n\n\t// finally, compare by version when type and source are the same (v4 > v3 > v2 > v1)\n\tiV := iEntry.version()\n\tjV := jEntry.version()\n\treturn iV.LessThan(jV)\n}\n\nfunc (o CvssSummaries) Swap(i, j int) {\n\to[i], o[j] = o[j], o[i]\n}\n\nfunc (o CvssSummaries) Severity() string {\n\tfor _, c := range o {\n\t\tsev := c.Severity()\n\t\tif sev != \"\" {\n\t\t\treturn sev\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc (o CvssSummaries) Sorted() CvssSummaries {\n\tvar n CvssSummaries\n\tif err := copier.Copy(&n, &o); err != nil {\n\t\tpanic(err)\n\t}\n\tsort.Sort(sort.Reverse(n))\n\treturn n\n}\n\nfunc (o CveItem) CVSS() []CvssSummary {\n\tif o.Metrics == nil {\n\t\treturn nil\n\t}\n\n\tvar results CvssSummaries\n\n\tfor _, c := range o.Metrics.CvssMetricV2 {\n\t\tresults = append(results,\n\t\t\tCvssSummary{\n\t\t\t\tSource:              c.Source,\n\t\t\t\tType:                c.Type,\n\t\t\t\tVersion:             c.CvssData.Version,\n\t\t\t\tVector:              c.CvssData.VectorString,\n\t\t\t\tBaseScore:           c.CvssData.BaseScore,\n\t\t\t\tExploitabilityScore: c.ExploitabilityScore,\n\t\t\t\tImpactScore:         c.ImpactScore,\n\t\t\t\tbaseSeverity:        c.BaseSeverity,\n\t\t\t},\n\t\t)\n\t}\n\tfor _, c := range o.Metrics.CvssMetricV30 {\n\t\tsev := string(c.CvssData.BaseSeverity)\n\t\tresults = append(results,\n\t\t\tCvssSummary{\n\t\t\t\tSource:              c.Source,\n\t\t\t\tType:                c.Type,\n\t\t\t\tVersion:             c.CvssData.Version,\n\t\t\t\tVector:              c.CvssData.VectorString,\n\t\t\t\tBaseScore:           c.CvssData.BaseScore,\n\t\t\t\tExploitabilityScore: c.ExploitabilityScore,\n\t\t\t\tImpactScore:         c.ImpactScore,\n\t\t\t\tbaseSeverity:        &sev,\n\t\t\t},\n\t\t)\n\t}\n\tfor _, c := range o.Metrics.CvssMetricV31 {\n\t\tsev := string(c.CvssData.BaseSeverity)\n\t\tresults = append(results,\n\t\t\tCvssSummary{\n\t\t\t\tSource:              c.Source,\n\t\t\t\tType:                c.Type,\n\t\t\t\tVersion:             c.CvssData.Version,\n\t\t\t\tVector:              c.CvssData.VectorString,\n\t\t\t\tBaseScore:           c.CvssData.BaseScore,\n\t\t\t\tExploitabilityScore: c.ExploitabilityScore,\n\t\t\t\tImpactScore:         c.ImpactScore,\n\t\t\t\tbaseSeverity:        &sev,\n\t\t\t},\n\t\t)\n\t}\n\n\tfor _, c := range o.Metrics.CvssMetricV40 {\n\t\tsev := string(c.CvssData.BaseSeverity)\n\t\tresults = append(results,\n\t\t\tCvssSummary{\n\t\t\t\tSource:              c.Source,\n\t\t\t\tType:                c.Type,\n\t\t\t\tVersion:             c.CvssData.Version,\n\t\t\t\tVector:              c.CvssData.VectorString,\n\t\t\t\tBaseScore:           c.CvssData.BaseScore,\n\t\t\t\tExploitabilityScore: c.ExploitabilityScore,\n\t\t\t\tImpactScore:         c.ImpactScore,\n\t\t\t\tbaseSeverity:        &sev,\n\t\t\t},\n\t\t)\n\t}\n\n\treturn results\n}\n\nfunc (o Vulnerability) IsEmpty() bool {\n\treturn o.Cve.ID == \"\"\n}\n"
  },
  {
    "path": "grype/db/internal/provider/unmarshal/nvd/cve_test.go",
    "content": "package nvd\n\nimport (\n\t\"testing\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/google/go-cmp/cmp/cmpopts\"\n)\n\nfunc TestCvssSummariesSorted(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    CvssSummaries\n\t\texpected CvssSummaries\n\t}{\n\t\t{\n\t\t\tname: \"primary types sorted by version descending\",\n\t\t\tinput: CvssSummaries{\n\t\t\t\t{Type: Primary, Version: \"2.0\", Source: \"same-source\"},\n\t\t\t\t{Type: Primary, Version: \"3.1\", Source: \"same-source\"},\n\t\t\t\t{Type: Primary, Version: \"3.0\", Source: \"same-source\"},\n\t\t\t\t{Type: Primary, Version: \"4.0\", Source: \"same-source\"},\n\t\t\t},\n\t\t\texpected: CvssSummaries{\n\t\t\t\t{Type: Primary, Version: \"4.0\", Source: \"same-source\"},\n\t\t\t\t{Type: Primary, Version: \"3.1\", Source: \"same-source\"},\n\t\t\t\t{Type: Primary, Version: \"3.0\", Source: \"same-source\"},\n\t\t\t\t{Type: Primary, Version: \"2.0\", Source: \"same-source\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"secondary types sorted by version descending\",\n\t\t\tinput: CvssSummaries{\n\t\t\t\t{Type: Secondary, Version: \"2.0\", Source: \"same-source\"},\n\t\t\t\t{Type: Secondary, Version: \"3.1\", Source: \"same-source\"},\n\t\t\t\t{Type: Secondary, Version: \"3.0\", Source: \"same-source\"},\n\t\t\t},\n\t\t\texpected: CvssSummaries{\n\t\t\t\t{Type: Secondary, Version: \"3.1\", Source: \"same-source\"},\n\t\t\t\t{Type: Secondary, Version: \"3.0\", Source: \"same-source\"},\n\t\t\t\t{Type: Secondary, Version: \"2.0\", Source: \"same-source\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"primary types before secondary types\",\n\t\t\tinput: CvssSummaries{\n\t\t\t\t{Type: Secondary, Version: \"3.1\", Source: \"G\"},\n\t\t\t\t{Type: Primary, Version: \"2.0\", Source: \"H\"},\n\t\t\t\t{Type: Secondary, Version: \"2.0\", Source: \"I\"},\n\t\t\t\t{Type: Primary, Version: \"3.0\", Source: \"J\"},\n\t\t\t},\n\t\t\texpected: CvssSummaries{\n\t\t\t\t{Type: Primary, Version: \"2.0\", Source: \"H\"},\n\t\t\t\t{Type: Primary, Version: \"3.0\", Source: \"J\"},\n\t\t\t\t{Type: Secondary, Version: \"3.1\", Source: \"G\"},\n\t\t\t\t{Type: Secondary, Version: \"2.0\", Source: \"I\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"mix of versions and types\",\n\t\t\tinput: CvssSummaries{\n\t\t\t\t{Type: Secondary, Version: \"3.1\", Source: \"K\"},\n\t\t\t\t{Type: Primary, Version: \"3.1\", Source: \"L\"},\n\t\t\t\t{Type: Primary, Version: \"2.0\", Source: \"M\"},\n\t\t\t\t{Type: Secondary, Version: \"2.0\", Source: \"N\"},\n\t\t\t\t{Type: Primary, Version: \"3.0\", Source: \"O\"},\n\t\t\t\t{Type: Secondary, Version: \"3.0\", Source: \"P\"},\n\t\t\t},\n\t\t\texpected: CvssSummaries{\n\t\t\t\t{Type: Primary, Version: \"3.1\", Source: \"L\"},\n\t\t\t\t{Type: Primary, Version: \"2.0\", Source: \"M\"},\n\t\t\t\t{Type: Primary, Version: \"3.0\", Source: \"O\"},\n\t\t\t\t{Type: Secondary, Version: \"3.1\", Source: \"K\"},\n\t\t\t\t{Type: Secondary, Version: \"2.0\", Source: \"N\"},\n\t\t\t\t{Type: Secondary, Version: \"3.0\", Source: \"P\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"nvd source preferred within same type and version\",\n\t\t\tinput: CvssSummaries{\n\t\t\t\t{Type: Primary, Version: \"3.0\", Source: \"random-source\"},\n\t\t\t\t{Type: Primary, Version: \"3.0\", Source: \"nvd@nist.gov\"},\n\t\t\t},\n\t\t\texpected: CvssSummaries{\n\t\t\t\t{Type: Primary, Version: \"3.0\", Source: \"nvd@nist.gov\"},\n\t\t\t\t{Type: Primary, Version: \"3.0\", Source: \"random-source\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"nvd source preferred but type takes precedence\",\n\t\t\tinput: CvssSummaries{\n\t\t\t\t{Type: Secondary, Version: \"3.0\", Source: \"nvd@nist.gov\"},\n\t\t\t\t{Type: Primary, Version: \"3.0\", Source: \"random-source\"},\n\t\t\t},\n\t\t\texpected: CvssSummaries{\n\t\t\t\t{Type: Primary, Version: \"3.0\", Source: \"random-source\"},\n\t\t\t\t{Type: Secondary, Version: \"3.0\", Source: \"nvd@nist.gov\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"multiple nvd sources sorted by version\",\n\t\t\tinput: CvssSummaries{\n\t\t\t\t{Type: Primary, Version: \"2.0\", Source: \"nvd@nist.gov\"},\n\t\t\t\t{Type: Primary, Version: \"3.1\", Source: \"nvd@nist.gov\"},\n\t\t\t\t{Type: Primary, Version: \"3.0\", Source: \"nvd@nist.gov\"},\n\t\t\t},\n\t\t\texpected: CvssSummaries{\n\t\t\t\t{Type: Primary, Version: \"3.1\", Source: \"nvd@nist.gov\"},\n\t\t\t\t{Type: Primary, Version: \"3.0\", Source: \"nvd@nist.gov\"},\n\t\t\t\t{Type: Primary, Version: \"2.0\", Source: \"nvd@nist.gov\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"complex sorting with types, versions, and sources\",\n\t\t\tinput: CvssSummaries{\n\t\t\t\t{Type: Secondary, Version: \"3.1\", Source: \"nvd@nist.gov\"},\n\t\t\t\t{Type: Primary, Version: \"2.0\", Source: \"random-source\"},\n\t\t\t\t{Type: Primary, Version: \"3.0\", Source: \"nvd@nist.gov\"},\n\t\t\t\t{Type: Primary, Version: \"3.0\", Source: \"other-source\"},\n\t\t\t\t{Type: Secondary, Version: \"2.0\", Source: \"other-source\"},\n\t\t\t\t{Type: Secondary, Version: \"3.0\", Source: \"nvd@nist.gov\"},\n\t\t\t},\n\t\t\texpected: CvssSummaries{\n\t\t\t\t{Type: Primary, Version: \"3.0\", Source: \"nvd@nist.gov\"},\n\t\t\t\t{Type: Primary, Version: \"3.0\", Source: \"other-source\"},\n\t\t\t\t{Type: Primary, Version: \"2.0\", Source: \"random-source\"},\n\t\t\t\t{Type: Secondary, Version: \"3.1\", Source: \"nvd@nist.gov\"},\n\t\t\t\t{Type: Secondary, Version: \"3.0\", Source: \"nvd@nist.gov\"},\n\t\t\t\t{Type: Secondary, Version: \"2.0\", Source: \"other-source\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:     \"empty input\",\n\t\t\tinput:    CvssSummaries{},\n\t\t\texpected: CvssSummaries{},\n\t\t},\n\t\t{\n\t\t\tname: \"invalid version handling\",\n\t\t\tinput: CvssSummaries{\n\t\t\t\t{Type: Primary, Version: \"invalid\", Source: \"Q\"},\n\t\t\t\t{Type: Primary, Version: \"3.0\", Source: \"R\"},\n\t\t\t},\n\t\t\texpected: CvssSummaries{\n\t\t\t\t{Type: Primary, Version: \"invalid\", Source: \"Q\"}, // sorted by source (Q < R)\n\t\t\t\t{Type: Primary, Version: \"3.0\", Source: \"R\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"source takes priority over version, then version as tiebreaker\",\n\t\t\tinput: CvssSummaries{\n\t\t\t\t{Type: Primary, Version: \"4.0\", Source: \"other-source\"},\n\t\t\t\t{Type: Primary, Version: \"3.0\", Source: \"nvd@nist.gov\"},\n\t\t\t\t{Type: Primary, Version: \"2.0\", Source: \"nvd@nist.gov\"},\n\t\t\t\t{Type: Primary, Version: \"3.0\", Source: \"source-a\"},\n\t\t\t},\n\t\t\texpected: CvssSummaries{\n\t\t\t\t{Type: Primary, Version: \"3.0\", Source: \"nvd@nist.gov\"},\n\t\t\t\t{Type: Primary, Version: \"2.0\", Source: \"nvd@nist.gov\"},\n\t\t\t\t{Type: Primary, Version: \"4.0\", Source: \"other-source\"},\n\t\t\t\t{Type: Primary, Version: \"3.0\", Source: \"source-a\"},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tresult := tc.input.Sorted()\n\n\t\t\tif d := cmp.Diff(tc.expected, result, cmpopts.IgnoreUnexported(CvssSummary{})); d != \"\" {\n\t\t\t\tt.Errorf(\"unexpected result (-want +got):\\n%s\", d)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestCvssSummaryVersion(t *testing.T) {\n\ttests := []struct {\n\t\tinput    string\n\t\texpected string\n\t}{\n\t\t{\"4.0\", \"4.0.0\"},\n\t\t{\"3.1\", \"3.1.0\"},\n\t\t{\"3.0\", \"3.0.0\"},\n\t\t{\"2.0\", \"2.0.0\"},\n\t\t{\"invalid\", \"2.0.0\"}, // default to 2.0 for invalid versions\n\t\t{\"3.1.5\", \"3.1.5\"},\n\t\t{\"\", \"2.0.0\"}, // empty string is invalid\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.input, func(t *testing.T) {\n\t\t\tsummary := CvssSummary{Version: tc.input}\n\t\t\tversion := summary.version()\n\t\t\tif version.String() != tc.expected {\n\t\t\t\tt.Errorf(\"Expected version %s, got %s\", tc.expected, version.String())\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/db/internal/provider/unmarshal/nvd/cvss20/cvss20.go",
    "content": "package cvss20\n\n// note: this was autogenerated with some manual tweaking\n\ntype Cvss20 struct {\n\t// AccessComplexity           *AccessComplexityType          `json:\"accessComplexity,omitempty\"`\n\t// AccessVector               *AccessVectorType              `json:\"accessVector,omitempty\"`\n\t// Authentication             *AuthenticationType            `json:\"authentication,omitempty\"`\n\t// AvailabilityImpact         *CiaType                       `json:\"availabilityImpact,omitempty\"`\n\t// AvailabilityRequirement    *CiaRequirementType            `json:\"availabilityRequirement,omitempty\"`\n\tBaseScore float64 `json:\"baseScore\"`\n\t// CollateralDamagePotential  *CollateralDamagePotentialType `json:\"collateralDamagePotential,omitempty\"`\n\t// ConfidentialityImpact      *CiaType                       `json:\"confidentialityImpact,omitempty\"`\n\t// ConfidentialityRequirement *CiaRequirementType            `json:\"confidentialityRequirement,omitempty\"`\n\t// EnvironmentalScore         *float64                       `json:\"environmentalScore,omitempty\"`\n\t// Exploitability             *ExploitabilityType            `json:\"exploitability,omitempty\"`\n\t// IntegrityImpact            *CiaType                       `json:\"integrityImpact,omitempty\"`\n\t// IntegrityRequirement       *CiaRequirementType            `json:\"integrityRequirement,omitempty\"`\n\t// RemediationLevel           *RemediationLevelType          `json:\"remediationLevel,omitempty\"`\n\t// ReportConfidence           *ReportConfidenceType          `json:\"reportConfidence,omitempty\"`\n\t// TargetDistribution         *TargetDistributionType        `json:\"targetDistribution,omitempty\"`\n\t// TemporalScore              *float64                       `json:\"temporalScore,omitempty\"`\n\tVectorString string `json:\"vectorString\"`\n\tVersion      string `json:\"version\"` // CVSS Version\n}\n\n// type AccessComplexityType string\n//\n// const (\n//\tAccessComplexityTypeHIGH   AccessComplexityType = \"HIGH\"\n//\tAccessComplexityTypeLOW    AccessComplexityType = \"LOW\"\n//\tAccessComplexityTypeMEDIUM AccessComplexityType = \"MEDIUM\"\n//)\n//\n// type AccessVectorType string\n//\n// const (\n//\tAdjacentNetwork AccessVectorType = \"ADJACENT_NETWORK\"\n//\tLocal           AccessVectorType = \"LOCAL\"\n//\tNetwork         AccessVectorType = \"NETWORK\"\n//)\n//\n// type AuthenticationType string\n//\n// const (\n//\tAuthenticationTypeNONE AuthenticationType = \"NONE\"\n//\tMultiple               AuthenticationType = \"MULTIPLE\"\n//\tSingle                 AuthenticationType = \"SINGLE\"\n//)\n//\n// type CiaType string\n//\n// const (\n//\tCiaTypeNONE CiaType = \"NONE\"\n//\tComplete    CiaType = \"COMPLETE\"\n//\tPartial     CiaType = \"PARTIAL\"\n//)\n//\n// type CiaRequirementType string\n//\n// const (\n//\tCiaRequirementTypeHIGH       CiaRequirementType = \"HIGH\"\n//\tCiaRequirementTypeLOW        CiaRequirementType = \"LOW\"\n//\tCiaRequirementTypeMEDIUM     CiaRequirementType = \"MEDIUM\"\n//\tCiaRequirementTypeNOTDEFINED CiaRequirementType = \"NOT_DEFINED\"\n//)\n//\n// type CollateralDamagePotentialType string\n//\n// const (\n//\tCollateralDamagePotentialTypeHIGH       CollateralDamagePotentialType = \"HIGH\"\n//\tCollateralDamagePotentialTypeLOW        CollateralDamagePotentialType = \"LOW\"\n//\tCollateralDamagePotentialTypeNONE       CollateralDamagePotentialType = \"NONE\"\n//\tCollateralDamagePotentialTypeNOTDEFINED CollateralDamagePotentialType = \"NOT_DEFINED\"\n//\tLowMedium                               CollateralDamagePotentialType = \"LOW_MEDIUM\"\n//\tMediumHigh                              CollateralDamagePotentialType = \"MEDIUM_HIGH\"\n//)\n//\n// type ExploitabilityType string\n//\n// const (\n//\tExploitabilityTypeHIGH       ExploitabilityType = \"HIGH\"\n//\tExploitabilityTypeNOTDEFINED ExploitabilityType = \"NOT_DEFINED\"\n//\tFunctional                   ExploitabilityType = \"FUNCTIONAL\"\n//\tProofOfConcept               ExploitabilityType = \"PROOF_OF_CONCEPT\"\n//\tUnproven                     ExploitabilityType = \"UNPROVEN\"\n//)\n//\n// type RemediationLevelType string\n//\n// const (\n//\tOfficialFix                    RemediationLevelType = \"OFFICIAL_FIX\"\n//\tRemediationLevelTypeNOTDEFINED RemediationLevelType = \"NOT_DEFINED\"\n//\tTemporaryFix                   RemediationLevelType = \"TEMPORARY_FIX\"\n//\tUnavailable                    RemediationLevelType = \"UNAVAILABLE\"\n//\tWorkaround                     RemediationLevelType = \"WORKAROUND\"\n//)\n//\n// type ReportConfidenceType string\n//\n// const (\n//\tConfirmed                      ReportConfidenceType = \"CONFIRMED\"\n//\tReportConfidenceTypeNOTDEFINED ReportConfidenceType = \"NOT_DEFINED\"\n//\tUnconfirmed                    ReportConfidenceType = \"UNCONFIRMED\"\n//\tUncorroborated                 ReportConfidenceType = \"UNCORROBORATED\"\n//)\n//\n// type TargetDistributionType string\n//\n// const (\n//\tTargetDistributionTypeHIGH       TargetDistributionType = \"HIGH\"\n//\tTargetDistributionTypeLOW        TargetDistributionType = \"LOW\"\n//\tTargetDistributionTypeMEDIUM     TargetDistributionType = \"MEDIUM\"\n//\tTargetDistributionTypeNONE       TargetDistributionType = \"NONE\"\n//\tTargetDistributionTypeNOTDEFINED TargetDistributionType = \"NOT_DEFINED\"\n//)\n"
  },
  {
    "path": "grype/db/internal/provider/unmarshal/nvd/cvss30/cvss30.go",
    "content": "package cvss30\n\n// note: this was autogenerated with some manual tweaking\n\ntype Cvss30 struct {\n\t// AttackComplexity              *AttackComplexityType         `json:\"attackComplexity,omitempty\"`\n\t// AttackVector                  *AttackVectorType             `json:\"attackVector,omitempty\"`\n\t// AvailabilityImpact            *Type                         `json:\"availabilityImpact,omitempty\"`\n\t// AvailabilityRequirement       *CiaRequirementType           `json:\"availabilityRequirement,omitempty\"`\n\tBaseScore    float64      `json:\"baseScore\"`\n\tBaseSeverity SeverityType `json:\"baseSeverity\"`\n\t// ConfidentialityImpact         *Type                         `json:\"confidentialityImpact,omitempty\"`\n\t// ConfidentialityRequirement    *CiaRequirementType           `json:\"confidentialityRequirement,omitempty\"`\n\t// EnvironmentalScore            *float64                      `json:\"environmentalScore,omitempty\"`\n\t// EnvironmentalSeverity         *SeverityType                 `json:\"environmentalSeverity,omitempty\"`\n\t// ExploitCodeMaturity           *ExploitCodeMaturityType      `json:\"exploitCodeMaturity,omitempty\"`\n\t// IntegrityImpact               *Type                         `json:\"integrityImpact,omitempty\"`\n\t// IntegrityRequirement          *CiaRequirementType           `json:\"integrityRequirement,omitempty\"`\n\t// ModifiedAttackComplexity      *ModifiedAttackComplexityType `json:\"modifiedAttackComplexity,omitempty\"`\n\t// ModifiedAttackVector          *ModifiedAttackVectorType     `json:\"modifiedAttackVector,omitempty\"`\n\t// ModifiedAvailabilityImpact    *ModifiedType                 `json:\"modifiedAvailabilityImpact,omitempty\"`\n\t// ModifiedConfidentialityImpact *ModifiedType                 `json:\"modifiedConfidentialityImpact,omitempty\"`\n\t// ModifiedIntegrityImpact       *ModifiedType                 `json:\"modifiedIntegrityImpact,omitempty\"`\n\t// ModifiedPrivilegesRequired    *ModifiedType                 `json:\"modifiedPrivilegesRequired,omitempty\"`\n\t// ModifiedScope                 *ModifiedScopeType            `json:\"modifiedScope,omitempty\"`\n\t// ModifiedUserInteraction       *ModifiedUserInteractionType  `json:\"modifiedUserInteraction,omitempty\"`\n\t// PrivilegesRequired            *Type                         `json:\"privilegesRequired,omitempty\"`\n\t// RemediationLevel              *RemediationLevelType         `json:\"remediationLevel,omitempty\"`\n\t// ReportConfidence              *ConfidenceType               `json:\"reportConfidence,omitempty\"`\n\t// Scope                         *ScopeType                    `json:\"scope,omitempty\"`\n\t// TemporalScore                 *float64                      `json:\"temporalScore,omitempty\"`\n\t// TemporalSeverity              *SeverityType                 `json:\"temporalSeverity,omitempty\"`\n\t// UserInteraction               *UserInteractionType          `json:\"userInteraction,omitempty\"`\n\tVectorString string `json:\"vectorString\"`\n\tVersion      string `json:\"version\"` // CVSS Version\n}\n\n// type AttackComplexityType string\n//\n// const (\n//\tAttackComplexityTypeHIGH AttackComplexityType = \"HIGH\"\n//\tAttackComplexityTypeLOW  AttackComplexityType = \"LOW\"\n//)\n//\n// type AttackVectorType string\n//\n// const (\n//\tAttackVectorTypeADJACENTNETWORK AttackVectorType = \"ADJACENT_NETWORK\"\n//\tAttackVectorTypeLOCAL           AttackVectorType = \"LOCAL\"\n//\tAttackVectorTypeNETWORK         AttackVectorType = \"NETWORK\"\n//\tAttackVectorTypePHYSICAL        AttackVectorType = \"PHYSICAL\"\n//)\n//\n// type Type string\n//\n// const (\n//\tTypeHIGH Type = \"HIGH\"\n//\tTypeLOW  Type = \"LOW\"\n//\tTypeNONE Type = \"NONE\"\n//)\n//\n// type CiaRequirementType string\n//\n// const (\n//\tCiaRequirementTypeHIGH       CiaRequirementType = \"HIGH\"\n//\tCiaRequirementTypeLOW        CiaRequirementType = \"LOW\"\n//\tCiaRequirementTypeMEDIUM     CiaRequirementType = \"MEDIUM\"\n//\tCiaRequirementTypeNOTDEFINED CiaRequirementType = \"NOT_DEFINED\"\n//)\n\ntype SeverityType string\n\nconst (\n\tCritical           SeverityType = \"CRITICAL\"\n\tSeverityTypeHIGH   SeverityType = \"HIGH\"\n\tSeverityTypeLOW    SeverityType = \"LOW\"\n\tSeverityTypeMEDIUM SeverityType = \"MEDIUM\"\n\tSeverityTypeNONE   SeverityType = \"NONE\"\n)\n\n//\n// type ExploitCodeMaturityType string\n//\n// const (\n//\tExploitCodeMaturityTypeHIGH       ExploitCodeMaturityType = \"HIGH\"\n//\tExploitCodeMaturityTypeNOTDEFINED ExploitCodeMaturityType = \"NOT_DEFINED\"\n//\tFunctional                        ExploitCodeMaturityType = \"FUNCTIONAL\"\n//\tProofOfConcept                    ExploitCodeMaturityType = \"PROOF_OF_CONCEPT\"\n//\tUnproven                          ExploitCodeMaturityType = \"UNPROVEN\"\n//)\n//\n// type ModifiedAttackComplexityType string\n//\n// const (\n//\tModifiedAttackComplexityTypeHIGH       ModifiedAttackComplexityType = \"HIGH\"\n//\tModifiedAttackComplexityTypeLOW        ModifiedAttackComplexityType = \"LOW\"\n//\tModifiedAttackComplexityTypeNOTDEFINED ModifiedAttackComplexityType = \"NOT_DEFINED\"\n//)\n//\n// type ModifiedAttackVectorType string\n//\n// const (\n//\tModifiedAttackVectorTypeADJACENTNETWORK ModifiedAttackVectorType = \"ADJACENT_NETWORK\"\n//\tModifiedAttackVectorTypeLOCAL           ModifiedAttackVectorType = \"LOCAL\"\n//\tModifiedAttackVectorTypeNETWORK         ModifiedAttackVectorType = \"NETWORK\"\n//\tModifiedAttackVectorTypeNOTDEFINED      ModifiedAttackVectorType = \"NOT_DEFINED\"\n//\tModifiedAttackVectorTypePHYSICAL        ModifiedAttackVectorType = \"PHYSICAL\"\n//)\n//\n// type ModifiedType string\n//\n// const (\n//\tModifiedTypeHIGH       ModifiedType = \"HIGH\"\n//\tModifiedTypeLOW        ModifiedType = \"LOW\"\n//\tModifiedTypeNONE       ModifiedType = \"NONE\"\n//\tModifiedTypeNOTDEFINED ModifiedType = \"NOT_DEFINED\"\n//)\n//\n// type ModifiedScopeType string\n//\n// const (\n//\tModifiedScopeTypeCHANGED    ModifiedScopeType = \"CHANGED\"\n//\tModifiedScopeTypeNOTDEFINED ModifiedScopeType = \"NOT_DEFINED\"\n//\tModifiedScopeTypeUNCHANGED  ModifiedScopeType = \"UNCHANGED\"\n//)\n//\n// type ModifiedUserInteractionType string\n//\n// const (\n//\tModifiedUserInteractionTypeNONE       ModifiedUserInteractionType = \"NONE\"\n//\tModifiedUserInteractionTypeNOTDEFINED ModifiedUserInteractionType = \"NOT_DEFINED\"\n//\tModifiedUserInteractionTypeREQUIRED   ModifiedUserInteractionType = \"REQUIRED\"\n//)\n//\n// type RemediationLevelType string\n//\n// const (\n//\tOfficialFix                    RemediationLevelType = \"OFFICIAL_FIX\"\n//\tRemediationLevelTypeNOTDEFINED RemediationLevelType = \"NOT_DEFINED\"\n//\tTemporaryFix                   RemediationLevelType = \"TEMPORARY_FIX\"\n//\tUnavailable                    RemediationLevelType = \"UNAVAILABLE\"\n//\tWorkaround                     RemediationLevelType = \"WORKAROUND\"\n//)\n//\n// type ConfidenceType string\n//\n// const (\n//\tConfidenceTypeNOTDEFINED ConfidenceType = \"NOT_DEFINED\"\n//\tConfirmed                ConfidenceType = \"CONFIRMED\"\n//\tReasonable               ConfidenceType = \"REASONABLE\"\n//\tUnknown                  ConfidenceType = \"UNKNOWN\"\n//)\n//\n// type ScopeType string\n//\n// const (\n//\tScopeTypeCHANGED   ScopeType = \"CHANGED\"\n//\tScopeTypeUNCHANGED ScopeType = \"UNCHANGED\"\n//)\n//\n// type UserInteractionType string\n//\n// const (\n//\tUserInteractionTypeNONE     UserInteractionType = \"NONE\"\n//\tUserInteractionTypeREQUIRED UserInteractionType = \"REQUIRED\"\n//)\n"
  },
  {
    "path": "grype/db/internal/provider/unmarshal/nvd/cvss31/cvss31.go",
    "content": "package cvss31\n\n// note: this was autogenerated with some manual tweaking\n\ntype Cvss31 struct {\n\t// AttackComplexity              *AttackComplexityType         `json:\"attackComplexity,omitempty\"`\n\t// AttackVector                  *AttackVectorType             `json:\"attackVector,omitempty\"`\n\t// AvailabilityImpact            *Type                         `json:\"availabilityImpact,omitempty\"`\n\t// AvailabilityRequirement       *CiaRequirementType           `json:\"availabilityRequirement,omitempty\"`\n\tBaseScore    float64      `json:\"baseScore\"`\n\tBaseSeverity SeverityType `json:\"baseSeverity\"`\n\t// ConfidentialityImpact         *Type                         `json:\"confidentialityImpact,omitempty\"`\n\t// ConfidentialityRequirement    *CiaRequirementType           `json:\"confidentialityRequirement,omitempty\"`\n\t// EnvironmentalScore            *float64                      `json:\"environmentalScore,omitempty\"`\n\t// EnvironmentalSeverity         *SeverityType                 `json:\"environmentalSeverity,omitempty\"`\n\t// ExploitCodeMaturity           *ExploitCodeMaturityType      `json:\"exploitCodeMaturity,omitempty\"`\n\t// IntegrityImpact               *Type                         `json:\"integrityImpact,omitempty\"`\n\t// IntegrityRequirement          *CiaRequirementType           `json:\"integrityRequirement,omitempty\"`\n\t// ModifiedAttackComplexity      *ModifiedAttackComplexityType `json:\"modifiedAttackComplexity,omitempty\"`\n\t// ModifiedAttackVector          *ModifiedAttackVectorType     `json:\"modifiedAttackVector,omitempty\"`\n\t// ModifiedAvailabilityImpact    *ModifiedType                 `json:\"modifiedAvailabilityImpact,omitempty\"`\n\t// ModifiedConfidentialityImpact *ModifiedType                 `json:\"modifiedConfidentialityImpact,omitempty\"`\n\t// ModifiedIntegrityImpact       *ModifiedType                 `json:\"modifiedIntegrityImpact,omitempty\"`\n\t// ModifiedPrivilegesRequired    *ModifiedType                 `json:\"modifiedPrivilegesRequired,omitempty\"`\n\t// ModifiedScope                 *ModifiedScopeType            `json:\"modifiedScope,omitempty\"`\n\t// ModifiedUserInteraction       *ModifiedUserInteractionType  `json:\"modifiedUserInteraction,omitempty\"`\n\t// PrivilegesRequired            *Type                         `json:\"privilegesRequired,omitempty\"`\n\t// RemediationLevel              *RemediationLevelType         `json:\"remediationLevel,omitempty\"`\n\t// ReportConfidence              *ConfidenceType               `json:\"reportConfidence,omitempty\"`\n\t// Scope                         *ScopeType                    `json:\"scope,omitempty\"`\n\t// TemporalScore                 *float64                      `json:\"temporalScore,omitempty\"`\n\t// TemporalSeverity              *SeverityType                 `json:\"temporalSeverity,omitempty\"`\n\t// UserInteraction               *UserInteractionType          `json:\"userInteraction,omitempty\"`\n\tVectorString string `json:\"vectorString\"`\n\tVersion      string `json:\"version\"` // CVSS Version\n}\n\n// type AttackComplexityType string\n//\n// const (\n//\tAttackComplexityTypeHIGH AttackComplexityType = \"HIGH\"\n//\tAttackComplexityTypeLOW  AttackComplexityType = \"LOW\"\n//)\n//\n// type AttackVectorType string\n//\n// const (\n//\tAttackVectorTypeADJACENTNETWORK AttackVectorType = \"ADJACENT_NETWORK\"\n//\tAttackVectorTypeLOCAL           AttackVectorType = \"LOCAL\"\n//\tAttackVectorTypeNETWORK         AttackVectorType = \"NETWORK\"\n//\tAttackVectorTypePHYSICAL        AttackVectorType = \"PHYSICAL\"\n//)\n//\n// type Type string\n//\n// const (\n//\tTypeHIGH Type = \"HIGH\"\n//\tTypeLOW  Type = \"LOW\"\n//\tTypeNONE Type = \"NONE\"\n//)\n//\n// type CiaRequirementType string\n//\n// const (\n//\tCiaRequirementTypeHIGH       CiaRequirementType = \"HIGH\"\n//\tCiaRequirementTypeLOW        CiaRequirementType = \"LOW\"\n//\tCiaRequirementTypeMEDIUM     CiaRequirementType = \"MEDIUM\"\n//\tCiaRequirementTypeNOTDEFINED CiaRequirementType = \"NOT_DEFINED\"\n//)\n\ntype SeverityType string\n\nconst (\n\tCritical           SeverityType = \"CRITICAL\"\n\tSeverityTypeHIGH   SeverityType = \"HIGH\"\n\tSeverityTypeLOW    SeverityType = \"LOW\"\n\tSeverityTypeMEDIUM SeverityType = \"MEDIUM\"\n\tSeverityTypeNONE   SeverityType = \"NONE\"\n)\n\n// type ExploitCodeMaturityType string\n//\n// const (\n//\tExploitCodeMaturityTypeHIGH       ExploitCodeMaturityType = \"HIGH\"\n//\tExploitCodeMaturityTypeNOTDEFINED ExploitCodeMaturityType = \"NOT_DEFINED\"\n//\tFunctional                        ExploitCodeMaturityType = \"FUNCTIONAL\"\n//\tProofOfConcept                    ExploitCodeMaturityType = \"PROOF_OF_CONCEPT\"\n//\tUnproven                          ExploitCodeMaturityType = \"UNPROVEN\"\n//)\n//\n// type ModifiedAttackComplexityType string\n//\n// const (\n//\tModifiedAttackComplexityTypeHIGH       ModifiedAttackComplexityType = \"HIGH\"\n//\tModifiedAttackComplexityTypeLOW        ModifiedAttackComplexityType = \"LOW\"\n//\tModifiedAttackComplexityTypeNOTDEFINED ModifiedAttackComplexityType = \"NOT_DEFINED\"\n//)\n//\n// type ModifiedAttackVectorType string\n//\n// const (\n//\tModifiedAttackVectorTypeADJACENTNETWORK ModifiedAttackVectorType = \"ADJACENT_NETWORK\"\n//\tModifiedAttackVectorTypeLOCAL           ModifiedAttackVectorType = \"LOCAL\"\n//\tModifiedAttackVectorTypeNETWORK         ModifiedAttackVectorType = \"NETWORK\"\n//\tModifiedAttackVectorTypeNOTDEFINED      ModifiedAttackVectorType = \"NOT_DEFINED\"\n//\tModifiedAttackVectorTypePHYSICAL        ModifiedAttackVectorType = \"PHYSICAL\"\n//)\n//\n// type ModifiedType string\n//\n// const (\n//\tModifiedTypeHIGH       ModifiedType = \"HIGH\"\n//\tModifiedTypeLOW        ModifiedType = \"LOW\"\n//\tModifiedTypeNONE       ModifiedType = \"NONE\"\n//\tModifiedTypeNOTDEFINED ModifiedType = \"NOT_DEFINED\"\n//)\n//\n// type ModifiedScopeType string\n//\n// const (\n//\tModifiedScopeTypeCHANGED    ModifiedScopeType = \"CHANGED\"\n//\tModifiedScopeTypeNOTDEFINED ModifiedScopeType = \"NOT_DEFINED\"\n//\tModifiedScopeTypeUNCHANGED  ModifiedScopeType = \"UNCHANGED\"\n//)\n//\n// type ModifiedUserInteractionType string\n//\n// const (\n//\tModifiedUserInteractionTypeNONE       ModifiedUserInteractionType = \"NONE\"\n//\tModifiedUserInteractionTypeNOTDEFINED ModifiedUserInteractionType = \"NOT_DEFINED\"\n//\tModifiedUserInteractionTypeREQUIRED   ModifiedUserInteractionType = \"REQUIRED\"\n//)\n//\n// type RemediationLevelType string\n//\n// const (\n//\tOfficialFix                    RemediationLevelType = \"OFFICIAL_FIX\"\n//\tRemediationLevelTypeNOTDEFINED RemediationLevelType = \"NOT_DEFINED\"\n//\tTemporaryFix                   RemediationLevelType = \"TEMPORARY_FIX\"\n//\tUnavailable                    RemediationLevelType = \"UNAVAILABLE\"\n//\tWorkaround                     RemediationLevelType = \"WORKAROUND\"\n//)\n//\n// type ConfidenceType string\n//\n// const (\n//\tConfidenceTypeNOTDEFINED ConfidenceType = \"NOT_DEFINED\"\n//\tConfirmed                ConfidenceType = \"CONFIRMED\"\n//\tReasonable               ConfidenceType = \"REASONABLE\"\n//\tUnknown                  ConfidenceType = \"UNKNOWN\"\n//)\n//\n// type ScopeType string\n//\n// const (\n//\tScopeTypeCHANGED   ScopeType = \"CHANGED\"\n//\tScopeTypeUNCHANGED ScopeType = \"UNCHANGED\"\n//)\n//\n// type UserInteractionType string\n//\n// const (\n//\tUserInteractionTypeNONE     UserInteractionType = \"NONE\"\n//\tUserInteractionTypeREQUIRED UserInteractionType = \"REQUIRED\"\n//)\n"
  },
  {
    "path": "grype/db/internal/provider/unmarshal/nvd/cvss40/cvss40.go",
    "content": "package cvss40\n\n// note: this was autogenerated with some manual tweaking\n\ntype Cvss40 struct {\n\tVersion      string       `json:\"version\"`\n\tVectorString string       `json:\"vectorString\"`\n\tBaseScore    float64      `json:\"baseScore\"`\n\tBaseSeverity SeverityType `json:\"baseSeverity\"`\n\t// AttackVector                      string  `json:\"attackVector\"`\n\t// AttackComplexity                  string  `json:\"attackComplexity\"`\n\t// AttackRequirements                string  `json:\"attackRequirements\"`\n\t// PrivilegesRequired                string  `json:\"privilegesRequired\"`\n\t// UserInteraction                   string  `json:\"userInteraction\"`\n\t// VulnConfidentialityImpact         string  `json:\"vulnConfidentialityImpact\"`\n\t// VulnIntegrityImpact               string  `json:\"vulnIntegrityImpact\"`\n\t// VulnAvailabilityImpact            string  `json:\"vulnAvailabilityImpact\"`\n\t// SubConfidentialityImpact          string  `json:\"subConfidentialityImpact\"`\n\t// SubIntegrityImpact                string  `json:\"subIntegrityImpact\"`\n\t// SubAvailabilityImpact             string  `json:\"subAvailabilityImpact\"`\n\t// ExploitMaturity                   string  `json:\"exploitMaturity\"`\n\t// ConfidentialityRequirement        string  `json:\"confidentialityRequirement\"`\n\t// IntegrityRequirement              string  `json:\"integrityRequirement\"`\n\t// AvailabilityRequirement           string  `json:\"availabilityRequirement\"`\n\t// ModifiedAttackVector              string  `json:\"modifiedAttackVector\"`\n\t// ModifiedAttackComplexity          string  `json:\"modifiedAttackComplexity\"`\n\t// ModifiedAttackRequirements        string  `json:\"modifiedAttackRequirements\"`\n\t// ModifiedPrivilegesRequired        string  `json:\"modifiedPrivilegesRequired\"`\n\t// ModifiedUserInteraction           string  `json:\"modifiedUserInteraction\"`\n\t// ModifiedVulnConfidentialityImpact string  `json:\"modifiedVulnConfidentialityImpact\"`\n\t// ModifiedVulnIntegrityImpact       string  `json:\"modifiedVulnIntegrityImpact\"`\n\t// ModifiedVulnAvailabilityImpact    string  `json:\"modifiedVulnAvailabilityImpact\"`\n\t// ModifiedSubConfidentialityImpact  string  `json:\"modifiedSubConfidentialityImpact\"`\n\t// ModifiedSubIntegrityImpact        string  `json:\"modifiedSubIntegrityImpact\"`\n\t// ModifiedSubAvailabilityImpact     string  `json:\"modifiedSubAvailabilityImpact\"`\n\t// Safety                            string  `json:\"Safety\"`\n\t// Automatable                       string  `json:\"Automatable\"`\n\t// Recovery                          string  `json:\"Recovery\"`\n\t// ValueDensity                      string  `json:\"valueDensity\"`\n\t// VulnerabilityResponseEffort       string  `json:\"vulnerabilityResponseEffort\"`\n\t// ProviderUrgency                   string  `json:\"providerUrgency\"`\n}\n\ntype SeverityType string\n\nconst (\n\tSeverityTypeCritical SeverityType = \"CRITICAL\"\n\tSeverityTypeHIGH     SeverityType = \"HIGH\"\n\tSeverityTypeLOW      SeverityType = \"LOW\"\n\tSeverityTypeMEDIUM   SeverityType = \"MEDIUM\"\n\tSeverityTypeNONE     SeverityType = \"NONE\"\n)\n"
  },
  {
    "path": "grype/db/internal/provider/unmarshal/nvd_vulnerability.go",
    "content": "package unmarshal\n\nimport (\n\t\"io\"\n\n\t\"github.com/anchore/grype/grype/db/internal/provider/unmarshal/nvd\"\n)\n\ntype (\n\tNVDVulnerability = nvd.CveItem\n)\n\nfunc NvdVulnerabilityEntries(reader io.Reader) ([]nvd.Vulnerability, error) {\n\treturn unmarshalSingleOrMulti[nvd.Vulnerability](reader)\n}\n"
  },
  {
    "path": "grype/db/internal/provider/unmarshal/openvex_vulnerability.go",
    "content": "package unmarshal\n\nimport (\n\t\"io\"\n\n\tgovex \"github.com/openvex/go-vex/pkg/vex\"\n)\n\ntype OpenVEXVulnerability = govex.Statement\n\nfunc OpenVEXVulnerabilityEntries(reader io.Reader) ([]OpenVEXVulnerability, error) {\n\treturn unmarshalSingleOrMulti[OpenVEXVulnerability](reader)\n}\n"
  },
  {
    "path": "grype/db/internal/provider/unmarshal/os_vulnerability.go",
    "content": "package unmarshal\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"sort\"\n\t\"strings\"\n\t\"unicode\"\n\n\t\"github.com/anchore/grype/grype/version\"\n)\n\ntype OSFixedIn struct {\n\tModule         *string `json:\"Module,omitempty\"`\n\tName           string  `json:\"Name\"`\n\tNamespaceName  string  `json:\"NamespaceName\"`\n\tVendorAdvisory struct {\n\t\tAdvisorySummary []struct {\n\t\t\tID   string `json:\"ID\"`\n\t\t\tLink string `json:\"Link\"`\n\t\t} `json:\"AdvisorySummary\"`\n\t\tNoAdvisory bool `json:\"NoAdvisory\"`\n\t} `json:\"VendorAdvisory\"`\n\tVersion         string `json:\"Version\"`\n\tVersionFormat   string `json:\"VersionFormat\"`\n\tVulnerableRange string `json:\"VulnerableRange\"`\n\tAvailable       struct {\n\t\tDate string `json:\"Date,omitempty\"`\n\t\tKind string `json:\"Kind,omitempty\"`\n\t} `json:\"Available,omitempty\"`\n}\n\ntype OSFixedIns []OSFixedIn\n\ntype OSVulnerability struct {\n\tVulnerability struct {\n\t\tCVSS []struct {\n\t\t\tBaseMetrics struct {\n\t\t\t\tBaseScore           float64 `json:\"base_score\"`\n\t\t\t\tBaseSeverity        string  `json:\"base_severity\"`\n\t\t\t\tExploitabilityScore float64 `json:\"exploitability_score\"`\n\t\t\t\tImpactScore         float64 `json:\"impact_score\"`\n\t\t\t} `json:\"base_metrics\"`\n\t\t\tStatus       string `json:\"status\"`\n\t\t\tVectorString string `json:\"vector_string\"`\n\t\t\tVersion      string `json:\"version\"`\n\t\t} `json:\"CVSS\"`\n\t\tDescription string `json:\"Description\"`\n\t\tFixedIn     OSFixedIns\n\t\tLink        string `json:\"Link\"`\n\t\tMetadata    struct {\n\t\t\tIssued  string `json:\"Issued\"`\n\t\t\tUpdated string `json:\"Updated\"`\n\t\t\tRefID   string `json:\"RefId\"`\n\t\t\tCVE     []struct {\n\t\t\t\tName string `json:\"Name\"`\n\t\t\t\tLink string `json:\"Link\"`\n\t\t\t} `json:\"CVE\"`\n\t\t\tNVD struct {\n\t\t\t\tCVSSv2 struct {\n\t\t\t\t\tScore   float64 `json:\"Score\"`\n\t\t\t\t\tVectors string  `json:\"Vectors\"`\n\t\t\t\t} `json:\"CVSSv2\"`\n\t\t\t} `json:\"NVD\"`\n\t\t} `json:\"Metadata\"`\n\t\tName          string `json:\"Name\"`\n\t\tNamespaceName string `json:\"NamespaceName\"`\n\t\tSeverity      string `json:\"Severity\"`\n\t} `json:\"Vulnerability\"`\n}\n\nfunc (o OSVulnerability) IsEmpty() bool {\n\treturn o.Vulnerability.Name == \"\"\n}\n\nfunc OSVulnerabilityEntries(reader io.Reader) ([]OSVulnerability, error) {\n\treturn unmarshalSingleOrMulti[OSVulnerability](reader)\n}\n\n// FilterToHighestModularity returns a new distinct set of fixes, keeping only the highest version module fix.\n// In cases where there is no modularity the fix is kept.\n//\n\nfunc (fixes OSFixedIns) FilterToHighestModularity() OSFixedIns {\n\tif len(fixes) < 2 {\n\t\treturn fixes\n\t}\n\n\ttype moduleFix struct {\n\t\tconstraint version.Constraint\n\t\tfix        OSFixedIn\n\t}\n\n\tvar keep []OSFixedIn\n\tmoduleHighestFixes := make(map[string]moduleFix)\n\n\tfor _, f := range fixes {\n\t\tvalidModule, moduleName, v, c := moduleNameAndVersion(f.Module)\n\t\tif !validModule {\n\t\t\tkeep = append(keep, f)\n\t\t\tcontinue\n\t\t}\n\n\t\tk := fmt.Sprintf(\"%s|%s|%s\", f.Name, f.NamespaceName, moduleName)\n\n\t\tif m, exists := moduleHighestFixes[k]; exists {\n\t\t\tsatisfied, err := m.constraint.Satisfied(v)\n\t\t\tif err != nil {\n\t\t\t\tkeep = append(keep, f)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif !satisfied {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\t\tmoduleHighestFixes[k] = moduleFix{\n\t\t\tconstraint: *c,\n\t\t\tfix:        f,\n\t\t}\n\t}\n\n\t// To ensure stable output ordering for tests\n\tvar orderedKeys []string\n\tfor k := range moduleHighestFixes {\n\t\torderedKeys = append(orderedKeys, k)\n\t}\n\tsort.Strings(orderedKeys)\n\n\tfor _, k := range orderedKeys {\n\t\tkeep = append(keep, moduleHighestFixes[k].fix)\n\t}\n\n\treturn keep\n}\n\nfunc moduleNameAndVersion(module *string) (bool, string, *version.Version, *version.Constraint) {\n\tif module == nil || *module == \"\" {\n\t\treturn false, \"\", nil, nil\n\t}\n\n\tmoduleComponents := strings.Split(*module, \":\")\n\n\tif len(moduleComponents) < 2 {\n\t\treturn false, \"\", nil, nil\n\t}\n\n\tmoduleName := strings.Join(moduleComponents[0:len(moduleComponents)-1], \":\")\n\tmoduleVersion := moduleComponents[len(moduleComponents)-1]\n\tisPotentiallyVersionedModule := len(moduleVersion) > 0 && unicode.IsDigit(rune(moduleVersion[0]))\n\n\tv, c := moduleVersionConstraint(moduleVersion)\n\tif v == nil || c == nil {\n\t\treturn false, \"\", nil, nil\n\t}\n\n\treturn isPotentiallyVersionedModule, moduleName, v, c\n}\n\nfunc moduleVersionConstraint(moduleVersion string) (*version.Version, *version.Constraint) {\n\tv := version.New(moduleVersion, version.UnknownFormat)\n\n\tif v == nil {\n\t\treturn nil, nil\n\t}\n\n\tc := version.MustGetConstraint(fmt.Sprintf(\"> %s\", moduleVersion), version.UnknownFormat)\n\treturn v, &c\n}\n"
  },
  {
    "path": "grype/db/internal/provider/unmarshal/os_vulnerability_test.go",
    "content": "package unmarshal\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc Test_OSFixedIns_FilterToHighestModularity(t *testing.T) {\n\n\tkeepAll := []OSFixedIn{\n\t\t{\n\t\t\tModule:        nil,\n\t\t\tName:          \"name\",\n\t\t\tNamespaceName: \"namespace\",\n\t\t\tVersion:       \"v1.0.2\",\n\t\t\tVersionFormat: \"semver\",\n\t\t},\n\t\t{\n\t\t\tModule: func() *string {\n\t\t\t\tx := \"\"\n\t\t\t\treturn &x\n\t\t\t}(),\n\t\t\tName:          \"name\",\n\t\t\tNamespaceName: \"namespace\",\n\t\t\tVersion:       \"v1.0.2\",\n\t\t\tVersionFormat: \"semver\",\n\t\t},\n\t}\n\n\ttable := []struct {\n\t\tname   string\n\t\tstart  OSFixedIns\n\t\texpect OSFixedIns\n\t}{\n\t\t{\n\t\t\tname:   \"go case: no filtering\",\n\t\t\tstart:  keepAll,\n\t\t\texpect: keepAll,\n\t\t},\n\t\t{\n\t\t\tname: \"keep the highest version of a module\",\n\t\t\tstart: []OSFixedIn{\n\t\t\t\t{\n\t\t\t\t\tModule: func() *string {\n\t\t\t\t\t\tx := \"name:1.0.2\"\n\t\t\t\t\t\treturn &x\n\t\t\t\t\t}(),\n\t\t\t\t\tName:          \"name\",\n\t\t\t\t\tNamespaceName: \"namespace\",\n\t\t\t\t\tVersion:       \"v1.0.2\",\n\t\t\t\t\tVersionFormat: \"semver\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tModule: func() *string {\n\t\t\t\t\t\tx := \"name:1.0.3\"\n\t\t\t\t\t\treturn &x\n\t\t\t\t\t}(),\n\t\t\t\t\tName:          \"name\",\n\t\t\t\t\tNamespaceName: \"namespace\",\n\t\t\t\t\tVersion:       \"v1.0.3\",\n\t\t\t\t\tVersionFormat: \"semver\",\n\t\t\t\t},\n\t\t\t},\n\t\t\texpect: []OSFixedIn{\n\t\t\t\t{\n\t\t\t\t\tModule: func() *string {\n\t\t\t\t\t\tx := \"name:1.0.3\"\n\t\t\t\t\t\treturn &x\n\t\t\t\t\t}(),\n\t\t\t\t\tName:          \"name\",\n\t\t\t\t\tNamespaceName: \"namespace\",\n\t\t\t\t\tVersion:       \"v1.0.3\",\n\t\t\t\t\tVersionFormat: \"semver\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"keep the highest version of a module (version processing flipped)\",\n\t\t\tstart: []OSFixedIn{\n\n\t\t\t\t{\n\t\t\t\t\tModule: func() *string {\n\t\t\t\t\t\tx := \"name:1.0.3\"\n\t\t\t\t\t\treturn &x\n\t\t\t\t\t}(),\n\t\t\t\t\tName:          \"name\",\n\t\t\t\t\tNamespaceName: \"namespace\",\n\t\t\t\t\tVersion:       \"v1.0.3\",\n\t\t\t\t\tVersionFormat: \"semver\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tModule: func() *string {\n\t\t\t\t\t\tx := \"name:1.0.2\"\n\t\t\t\t\t\treturn &x\n\t\t\t\t\t}(),\n\t\t\t\t\tName:          \"name\",\n\t\t\t\t\tNamespaceName: \"namespace\",\n\t\t\t\t\tVersion:       \"v1.0.2\",\n\t\t\t\t\tVersionFormat: \"semver\",\n\t\t\t\t},\n\t\t\t},\n\t\t\texpect: []OSFixedIn{\n\t\t\t\t{\n\t\t\t\t\tModule: func() *string {\n\t\t\t\t\t\tx := \"name:1.0.3\"\n\t\t\t\t\t\treturn &x\n\t\t\t\t\t}(),\n\t\t\t\t\tName:          \"name\",\n\t\t\t\t\tNamespaceName: \"namespace\",\n\t\t\t\t\tVersion:       \"v1.0.3\",\n\t\t\t\t\tVersionFormat: \"semver\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"keep distinct module names (even though the package info is the same)\",\n\t\t\tstart: []OSFixedIn{\n\t\t\t\t{\n\t\t\t\t\tModule: func() *string {\n\t\t\t\t\t\tx := \"name-1:1.0.2\" // <-- important\n\t\t\t\t\t\treturn &x\n\t\t\t\t\t}(),\n\t\t\t\t\tName:          \"name\",\n\t\t\t\t\tNamespaceName: \"namespace\",\n\t\t\t\t\tVersion:       \"v1.0.2\",\n\t\t\t\t\tVersionFormat: \"semver\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tModule: func() *string {\n\t\t\t\t\t\tx := \"name:1.0.3\"\n\t\t\t\t\t\treturn &x\n\t\t\t\t\t}(),\n\t\t\t\t\tName:          \"name\",\n\t\t\t\t\tNamespaceName: \"namespace\",\n\t\t\t\t\tVersion:       \"v1.0.3\",\n\t\t\t\t\tVersionFormat: \"semver\",\n\t\t\t\t},\n\t\t\t},\n\t\t\texpect: []OSFixedIn{\n\t\t\t\t{\n\t\t\t\t\tModule: func() *string {\n\t\t\t\t\t\tx := \"name:1.0.3\"\n\t\t\t\t\t\treturn &x\n\t\t\t\t\t}(),\n\t\t\t\t\tName:          \"name\",\n\t\t\t\t\tNamespaceName: \"namespace\",\n\t\t\t\t\tVersion:       \"v1.0.3\",\n\t\t\t\t\tVersionFormat: \"semver\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tModule: func() *string {\n\t\t\t\t\t\tx := \"name-1:1.0.2\"\n\t\t\t\t\t\treturn &x\n\t\t\t\t\t}(),\n\t\t\t\t\tName:          \"name\",\n\t\t\t\t\tNamespaceName: \"namespace\",\n\t\t\t\t\tVersion:       \"v1.0.2\",\n\t\t\t\t\tVersionFormat: \"semver\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"keep distinct namespaces\",\n\t\t\tstart: []OSFixedIn{\n\t\t\t\t{\n\t\t\t\t\tModule: func() *string {\n\t\t\t\t\t\tx := \"name:1.0.2\"\n\t\t\t\t\t\treturn &x\n\t\t\t\t\t}(),\n\t\t\t\t\tName:          \"name\",\n\t\t\t\t\tNamespaceName: \"namespace-1\", // <-- important\n\t\t\t\t\tVersion:       \"v1.0.2\",\n\t\t\t\t\tVersionFormat: \"semver\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tModule: func() *string {\n\t\t\t\t\t\tx := \"name:1.0.3\"\n\t\t\t\t\t\treturn &x\n\t\t\t\t\t}(),\n\t\t\t\t\tName:          \"name\",\n\t\t\t\t\tNamespaceName: \"namespace\",\n\t\t\t\t\tVersion:       \"v1.0.3\",\n\t\t\t\t\tVersionFormat: \"semver\",\n\t\t\t\t},\n\t\t\t},\n\t\t\texpect: []OSFixedIn{\n\t\t\t\t{\n\t\t\t\t\tModule: func() *string {\n\t\t\t\t\t\tx := \"name:1.0.2\"\n\t\t\t\t\t\treturn &x\n\t\t\t\t\t}(),\n\t\t\t\t\tName:          \"name\",\n\t\t\t\t\tNamespaceName: \"namespace-1\",\n\t\t\t\t\tVersion:       \"v1.0.2\",\n\t\t\t\t\tVersionFormat: \"semver\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tModule: func() *string {\n\t\t\t\t\t\tx := \"name:1.0.3\"\n\t\t\t\t\t\treturn &x\n\t\t\t\t\t}(),\n\t\t\t\t\tName:          \"name\",\n\t\t\t\t\tNamespaceName: \"namespace\",\n\t\t\t\t\tVersion:       \"v1.0.3\",\n\t\t\t\t\tVersionFormat: \"semver\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"keep module with no numeric versions\",\n\t\t\tstart: []OSFixedIn{\n\t\t\t\t{\n\t\t\t\t\tModule: func() *string {\n\t\t\t\t\t\tx := \"name:prefix1.0.2\" // <-- important\n\t\t\t\t\t\treturn &x\n\t\t\t\t\t}(),\n\t\t\t\t\tName:          \"name\",\n\t\t\t\t\tNamespaceName: \"namespace\",\n\t\t\t\t\tVersion:       \"v1.0.2\",\n\t\t\t\t\tVersionFormat: \"semver\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tModule: func() *string {\n\t\t\t\t\t\tx := \"name:1.0.3\"\n\t\t\t\t\t\treturn &x\n\t\t\t\t\t}(),\n\t\t\t\t\tName:          \"name\",\n\t\t\t\t\tNamespaceName: \"namespace\",\n\t\t\t\t\tVersion:       \"v1.0.3\",\n\t\t\t\t\tVersionFormat: \"semver\",\n\t\t\t\t},\n\t\t\t},\n\t\t\texpect: []OSFixedIn{\n\t\t\t\t{\n\t\t\t\t\tModule: func() *string {\n\t\t\t\t\t\tx := \"name:prefix1.0.2\"\n\t\t\t\t\t\treturn &x\n\t\t\t\t\t}(),\n\t\t\t\t\tName:          \"name\",\n\t\t\t\t\tNamespaceName: \"namespace\",\n\t\t\t\t\tVersion:       \"v1.0.2\",\n\t\t\t\t\tVersionFormat: \"semver\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tModule: func() *string {\n\t\t\t\t\t\tx := \"name:1.0.3\"\n\t\t\t\t\t\treturn &x\n\t\t\t\t\t}(),\n\t\t\t\t\tName:          \"name\",\n\t\t\t\t\tNamespaceName: \"namespace\",\n\t\t\t\t\tVersion:       \"v1.0.3\",\n\t\t\t\t\tVersionFormat: \"semver\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range table {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot := tt.start.FilterToHighestModularity()\n\t\t\tassert.Equal(t, tt.expect, got)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/db/internal/provider/unmarshal/osv_vulnerability.go",
    "content": "package unmarshal\n\nimport (\n\t\"io\"\n\n\t\"github.com/google/osv-scanner/pkg/models\"\n)\n\ntype OSVVulnerability = models.Vulnerability\n\nfunc OSVVulnerabilityEntries(reader io.Reader) ([]OSVVulnerability, error) {\n\treturn unmarshalSingleOrMulti[OSVVulnerability](reader)\n}\n"
  },
  {
    "path": "grype/db/internal/provider/unmarshal/single_or_multi.go",
    "content": "package unmarshal\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n)\n\nfunc unmarshalSingleOrMulti[T interface{}](reader io.Reader) ([]T, error) {\n\tvar entry T\n\n\tvar buf bytes.Buffer\n\tr := io.TeeReader(reader, &buf)\n\n\tdec := json.NewDecoder(r)\n\terr := dec.Decode(&entry)\n\tif err == nil {\n\t\treturn []T{entry}, nil\n\t}\n\n\t// TODO: enhance the error handling to return the original error if the item is found to not be an array of items\n\n\tvar entries []T\n\tdec = json.NewDecoder(io.MultiReader(&buf, reader))\n\n\tif err = dec.Decode(&entries); err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to decode vulnerability: %w\", handleJSONUnmarshalError(err))\n\t}\n\treturn entries, nil\n}\n"
  },
  {
    "path": "grype/db/internal/sqlite/nullable_types.go",
    "content": "package sqlite\n\nimport (\n\t\"database/sql\"\n\t\"encoding/json\"\n)\n\ntype NullString struct {\n\tsql.NullString\n}\n\nfunc NewNullString(s string, valid bool) NullString {\n\treturn NullString{\n\t\tsql.NullString{\n\t\t\tString: s,\n\t\t\tValid:  valid,\n\t\t},\n\t}\n}\n\nfunc ToNullString(v any) NullString {\n\tnullString := NullString{}\n\tnullString.Valid = false\n\n\tif v != nil {\n\t\tvar stringValue string\n\n\t\tif s, ok := v.(string); ok {\n\t\t\tstringValue = s\n\t\t} else {\n\t\t\tvBytes, err := json.Marshal(v)\n\t\t\tif err != nil {\n\t\t\t\t// TODO: just no\n\t\t\t\tpanic(err)\n\t\t\t}\n\n\t\t\tstringValue = string(vBytes)\n\t\t}\n\n\t\tif stringValue != \"null\" {\n\t\t\tnullString.String = stringValue\n\t\t\tnullString.Valid = true\n\t\t}\n\t}\n\n\treturn nullString\n}\n\nfunc (v NullString) ToByteSlice() []byte {\n\tif v.Valid {\n\t\treturn []byte(v.String)\n\t}\n\n\treturn []byte(\"null\")\n}\n\nfunc (v NullString) MarshalJSON() ([]byte, error) {\n\tif v.Valid {\n\t\treturn json.Marshal(v.String)\n\t}\n\n\treturn json.Marshal(nil)\n}\n\nfunc (v *NullString) UnmarshalJSON(data []byte) error {\n\tif data != nil && string(data) != \"null\" {\n\t\tv.Valid = true\n\t\tv.String = string(data)\n\t} else {\n\t\tv.Valid = false\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "grype/db/internal/sqlite/nullable_types_test.go",
    "content": "package sqlite\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestToNullString(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    any\n\t\texpected NullString\n\t}{\n\t\t{\n\t\t\tname:     \"Nil input\",\n\t\t\tinput:    nil,\n\t\t\texpected: NullString{},\n\t\t},\n\t\t{\n\t\t\tname:     \"String null\",\n\t\t\tinput:    \"null\",\n\t\t\texpected: NullString{},\n\t\t},\n\t\t{\n\t\t\tname:     \"Other string\",\n\t\t\tinput:    \"Hello there {}\",\n\t\t\texpected: NewNullString(\"Hello there {}\", true),\n\t\t},\n\t\t{\n\t\t\tname: \"Single struct with all fields populated\",\n\t\t\tinput: struct {\n\t\t\t\tBoolean     bool   `json:\"boolean\"`\n\t\t\t\tString      string `json:\"string\"`\n\t\t\t\tInteger     int    `json:\"integer\"`\n\t\t\t\tInnerStruct struct {\n\t\t\t\t\tStringList []string `json:\"string_list\"`\n\t\t\t\t} `json:\"inner_struct\"`\n\t\t\t}{\n\t\t\t\tBoolean: true,\n\t\t\t\tString:  \"{}\",\n\t\t\t\tInteger: 1034,\n\t\t\t\tInnerStruct: struct {\n\t\t\t\t\tStringList []string `json:\"string_list\"`\n\t\t\t\t}{\n\t\t\t\t\tStringList: []string{\"a\", \"b\", \"c\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: NewNullString(`{\"boolean\":true,\"string\":\"{}\",\"integer\":1034,\"inner_struct\":{\"string_list\":[\"a\",\"b\",\"c\"]}}`, true),\n\t\t},\n\t\t{\n\t\t\tname: \"Single struct with one field populated\",\n\t\t\tinput: struct {\n\t\t\t\tBoolean     bool   `json:\"boolean\"`\n\t\t\t\tString      string `json:\"string\"`\n\t\t\t\tInteger     int    `json:\"integer\"`\n\t\t\t\tInnerStruct struct {\n\t\t\t\t\tStringList []string `json:\"string_list\"`\n\t\t\t\t} `json:\"inner_struct\"`\n\t\t\t}{\n\t\t\t\tBoolean: true,\n\t\t\t},\n\t\t\texpected: NewNullString(`{\"boolean\":true,\"string\":\"\",\"integer\":0,\"inner_struct\":{\"string_list\":null}}`, true),\n\t\t},\n\t\t{\n\t\t\tname: \"Single struct with one field populated omit empty\",\n\t\t\tinput: struct {\n\t\t\t\tBoolean     bool   `json:\"boolean,omitempty\"`\n\t\t\t\tString      string `json:\"string,omitempty\"`\n\t\t\t\tInteger     int    `json:\"integer,omitempty\"`\n\t\t\t\tInnerStruct struct {\n\t\t\t\t\tStringList []string `json:\"string_list,omitempty\"`\n\t\t\t\t} `json:\"inner_struct,omitempty\"`\n\t\t\t}{\n\t\t\t\tBoolean: true,\n\t\t\t},\n\t\t\texpected: NewNullString(`{\"boolean\":true,\"inner_struct\":{}}`, true),\n\t\t},\n\t\t{\n\t\t\tname: \"Array of structs\",\n\t\t\tinput: []struct {\n\t\t\t\tBoolean bool   `json:\"boolean,omitempty\"`\n\t\t\t\tString  string `json:\"string,omitempty\"`\n\t\t\t\tInteger int    `json:\"integer,omitempty\"`\n\t\t\t}{\n\t\t\t\t{\n\t\t\t\t\tBoolean: true,\n\t\t\t\t\tString:  \"{}\",\n\t\t\t\t\tInteger: 1034,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tString: \"[{}]\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tInteger: -5000,\n\t\t\t\t\tBoolean: false,\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: NewNullString(`[{\"boolean\":true,\"string\":\"{}\",\"integer\":1034},{\"string\":\"[{}]\"},{\"integer\":-5000}]`, true),\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tresult := ToNullString(test.input)\n\t\t\tassert.Equal(t, test.expected, result)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/db/internal/tarutil/file_entry.go",
    "content": "package tarutil\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n)\n\nvar _ Entry = (*FileEntry)(nil)\n\ntype FileEntry struct {\n\tPath string\n}\n\nfunc NewEntryFromFilePath(path string) Entry {\n\treturn FileEntry{\n\t\tPath: path,\n\t}\n}\n\nfunc NewEntryFromFilePaths(paths ...string) []Entry {\n\tvar entries []Entry\n\tfor _, path := range paths {\n\t\tentries = append(entries, NewEntryFromFilePath(path))\n\t}\n\treturn entries\n}\n\nfunc (t FileEntry) writeEntry(tw lowLevelWriter) error {\n\tfi, err := os.Lstat(t.Path)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to stat file %q: %w\", t.Path, err)\n\t}\n\treturn writeEntry(tw, t.Path, fi, func() (io.Reader, error) {\n\t\treturn os.Open(t.Path)\n\t})\n}\n"
  },
  {
    "path": "grype/db/internal/tarutil/file_entry_test.go",
    "content": "package tarutil\n\nimport (\n\t\"archive/tar\"\n\t\"bytes\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/google/uuid\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nvar _ lowLevelWriter = (*mockTarWriter)(nil)\n\ntype mockTarWriter struct {\n\theaders     []*tar.Header\n\tbuffers     []*bytes.Buffer\n\tcloseCalled bool\n\tflushCalled bool\n\tcloseErr    error\n\tflushErr    error\n}\n\nfunc (m *mockTarWriter) Flush() error {\n\tm.flushCalled = true\n\treturn m.flushErr\n}\n\nfunc (m *mockTarWriter) Close() error {\n\tm.closeCalled = true\n\treturn m.closeErr\n}\n\nfunc (m *mockTarWriter) WriteHeader(header *tar.Header) error {\n\tm.headers = append(m.headers, header)\n\tm.buffers = append(m.buffers, &bytes.Buffer{})\n\treturn nil\n}\n\nfunc (m *mockTarWriter) Write(b []byte) (int, error) {\n\treturn m.buffers[len(m.buffers)-1].Write(b)\n}\n\nfunc TestFileEntry_writeEntry(t *testing.T) {\n\ttestStr := \"hello world\"\n\ttests := []struct {\n\t\tname    string\n\t\tfile    func(t *testing.T) string\n\t\twantErr require.ErrorAssertionFunc\n\t}{\n\t\t{\n\t\t\tname: \"valid file\",\n\t\t\tfile: func(t *testing.T) string {\n\t\t\t\tdir := t.TempDir()\n\t\t\t\tdest := filepath.Join(dir, \"file.txt\")\n\t\t\t\trequire.NoError(t, os.WriteFile(dest, []byte(testStr), 0644))\n\t\t\t\treturn dest\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"invalid file\",\n\t\t\tfile: func(t *testing.T) string {\n\t\t\t\treturn filepath.Join(\"/tmp/invalid/path\", uuid.New().String())\n\t\t\t},\n\t\t\twantErr: require.Error,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif tt.wantErr == nil {\n\t\t\t\ttt.wantErr = require.NoError\n\t\t\t}\n\n\t\t\texpectedName := tt.file(t)\n\t\t\tfe := NewEntryFromFilePath(expectedName)\n\t\t\ttw := &mockTarWriter{}\n\n\t\t\terr := fe.writeEntry(tw)\n\t\t\ttt.wantErr(t, err)\n\t\t\tif err != nil {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.NoError(t, err)\n\t\t\trequire.Len(t, tw.headers, 1)\n\t\t\tassert.Equal(t, expectedName, tw.headers[0].Name)\n\t\t\tassert.Equal(t, int64(len(testStr)), tw.headers[0].Size)\n\t\t\tassert.Equal(t, testStr, tw.buffers[0].String())\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/db/internal/tarutil/populate.go",
    "content": "package tarutil\n\n// PopulateWithPaths creates a compressed tar from the given paths.\nfunc PopulateWithPaths(tarPath string, filePaths ...string) error {\n\treturn PopulateWithPathsAndCompressors(tarPath, nil, filePaths...)\n}\n\n// PopulateWithPathsAndCompressors creates a compressed tar from the given paths using custom compressor commands.\nfunc PopulateWithPathsAndCompressors(tarPath string, compressorCommands map[string]string, filePaths ...string) error {\n\tw, err := NewWriterWithCompressors(tarPath, compressorCommands)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer w.Close()\n\n\tfor _, entry := range NewEntryFromFilePaths(filePaths...) {\n\t\tif err := w.WriteEntry(entry); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "grype/db/internal/tarutil/populate_test.go",
    "content": "package tarutil\n\nimport (\n\t\"archive/tar\"\n\t\"compress/gzip\"\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/klauspost/compress/zstd\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestPopulateWithPaths(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\ttarPath string\n\t\twantErr bool\n\t}{\n\t\t{\n\t\t\tname:    \"plain tar\",\n\t\t\ttarPath: \"foo.tar\",\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"tar gz\",\n\t\t\ttarPath: \"foo.tar.gz\",\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"tar zst\",\n\t\t\ttarPath: \"foo.tar.zst\",\n\t\t\twantErr: false,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tdir := t.TempDir()\n\t\t\ttempPath := filepath.Join(dir, \"some-path.txt\")\n\t\t\tf, err := os.Create(tempPath)\n\t\t\trequire.NoError(t, err)\n\t\t\t_, err = f.Write([]byte(\"hello world\\n\"))\n\t\t\trequire.NoError(t, err)\n\t\t\tarchivePath := filepath.Join(dir, tt.tarPath)\n\t\t\terr = PopulateWithPaths(archivePath, tempPath)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tvar r io.Reader\n\t\t\tf, err = os.Open(archivePath)\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer f.Close()\n\n\t\t\tswitch {\n\t\t\tcase strings.HasSuffix(archivePath, \".tar.gz\"):\n\t\t\t\tr, err = gzip.NewReader(f)\n\t\t\t\trequire.NoError(t, err)\n\t\t\tcase strings.HasSuffix(archivePath, \".tar.zst\"):\n\t\t\t\tr, err = zstd.NewReader(f)\n\t\t\t\trequire.NoError(t, err)\n\t\t\tcase strings.HasSuffix(archivePath, \".tar\"):\n\t\t\t\tr = f\n\t\t\tdefault:\n\t\t\t\tt.Fatalf(\"unsupported archive type: %s\", archivePath)\n\t\t\t}\n\n\t\t\ttr := tar.NewReader(r)\n\t\t\th, err := tr.Next()\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, h.Name, tempPath)\n\t\t\tb, err := io.ReadAll(tr)\n\t\t\tassert.Equal(t, []byte(\"hello world\\n\"), b)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/db/internal/tarutil/reader_entry.go",
    "content": "package tarutil\n\nimport (\n\t\"archive/tar\"\n\t\"bytes\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\n\t\"github.com/anchore/grype/internal/log\"\n)\n\nvar _ Entry = (*ReaderEntry)(nil)\n\ntype ReaderEntry struct {\n\tReader   io.Reader\n\tFilename string\n\tFileInfo os.FileInfo\n}\n\nfunc NewEntryFromBytes(by []byte, filename string, fileInfo os.FileInfo) Entry {\n\treturn ReaderEntry{\n\t\tReader:   bytes.NewReader(by),\n\t\tFilename: filename,\n\t\tFileInfo: fileInfo,\n\t}\n}\n\nfunc (t ReaderEntry) writeEntry(tw lowLevelWriter) error {\n\tlog.WithFields(\"path\", t.Filename).Trace(\"adding stream to archive\")\n\treturn writeEntry(tw, t.Filename, t.FileInfo, func() (io.Reader, error) {\n\t\treturn t.Reader, nil\n\t})\n}\n\n// autoDeleteFile wraps an *os.File and deletes it when closed.\ntype autoDeleteFile struct {\n\t*os.File\n}\n\nfunc (f *autoDeleteFile) Close() error {\n\tname := f.Name()\n\terr := f.File.Close()\n\tif removeErr := os.Remove(name); removeErr != nil && err == nil {\n\t\terr = removeErr\n\t}\n\treturn err\n}\n\n// readerWithSize determines the size of the reader's content without reading the entire content into memory.\n// For known reader types (bytes.Reader, os.File), it queries the size directly.\n// For unknown types, it copies to a temp file to avoid loading into memory.\n// Returns the size, a ReadCloser for the content (may be different from input), and any error.\nfunc readerWithSize(reader io.Reader) (int64, io.ReadCloser, error) {\n\tswitch r := reader.(type) {\n\tcase *bytes.Reader:\n\t\t// For bytes.Reader (used by NewEntryFromBytes), get actual size\n\t\treturn r.Size(), io.NopCloser(reader), nil\n\tcase interface{ Stat() (os.FileInfo, error) }:\n\t\t// For *os.File, use Stat to get size\n\t\tstat, err := r.Stat()\n\t\tif err != nil {\n\t\t\treturn 0, nil, err\n\t\t}\n\t\t// Check if it's already a ReadCloser\n\t\tif rc, ok := reader.(io.ReadCloser); ok {\n\t\t\treturn stat.Size(), rc, nil\n\t\t}\n\t\treturn 0, nil, fmt.Errorf(\"reader with Stat() must implement io.ReadCloser\")\n\tdefault:\n\t\t// Fallback for unknown reader types: copy to temp file to avoid loading into memory\n\t\ttmpFile, err := os.CreateTemp(\"\", \"grype-db-tar-*\")\n\t\tif err != nil {\n\t\t\treturn 0, nil, fmt.Errorf(\"unable to create temp file: %w\", err)\n\t\t}\n\n\t\tsize, err := io.Copy(tmpFile, reader)\n\t\tif err != nil {\n\t\t\ttmpFile.Close()\n\t\t\tos.Remove(tmpFile.Name())\n\t\t\treturn 0, nil, fmt.Errorf(\"unable to copy to temp file: %w\", err)\n\t\t}\n\n\t\t// Seek back to beginning for reading\n\t\tif _, err := tmpFile.Seek(0, 0); err != nil {\n\t\t\ttmpFile.Close()\n\t\t\tos.Remove(tmpFile.Name())\n\t\t\treturn 0, nil, fmt.Errorf(\"unable to seek temp file: %w\", err)\n\t\t}\n\n\t\treturn size, &autoDeleteFile{File: tmpFile}, nil\n\t}\n}\n\nfunc writeEntry(tw lowLevelWriter, filename string, fileInfo os.FileInfo, opener func() (io.Reader, error)) error {\n\tlog.WithFields(\"path\", filename).Trace(\"adding file to archive\")\n\n\theader, err := tar.FileInfoHeader(fileInfo, \"\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\theader.Name = filename\n\tswitch fileInfo.Mode() & os.ModeType {\n\tcase os.ModeDir:\n\t\theader.Size = 0\n\t\terr = tw.WriteHeader(header)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn nil\n\n\tcase os.ModeSymlink:\n\t\tlinkTarget, err := os.Readlink(filename)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\theader.Linkname = linkTarget\n\t\theader.Size = 0\n\t\terr = tw.WriteHeader(header)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn nil\n\n\tdefault:\n\t\treader, err := opener()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tsize, readCloser, err := readerWithSize(reader)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tdefer readCloser.Close()\n\n\t\theader.Size = size\n\n\t\tif err := tw.WriteHeader(header); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t// Stream the file contents directly to the tar writer\n\t\tif _, err := io.Copy(tw, readCloser); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t// ensure proper alignment in the tar archive (padding with zeros)\n\t\tif err := tw.Flush(); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "grype/db/internal/tarutil/reader_entry_test.go",
    "content": "package tarutil\n\nimport (\n\t\"archive/tar\"\n\t\"bytes\"\n\t\"io\"\n\t\"io/fs\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/spf13/afero\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nvar _ lowLevelWriter = (*mockTarWriter)(nil)\n\nvar _ os.FileInfo = (*mockFileInfo)(nil)\n\ntype mockFileInfo struct {\n\tname    string\n\tsize    int64\n\tmode    fs.FileMode\n\tmodTime time.Time\n\tisDir   bool\n\tsys     any\n}\n\nfunc (m mockFileInfo) Name() string {\n\treturn m.name\n}\n\nfunc (m mockFileInfo) Size() int64 {\n\treturn m.size\n}\n\nfunc (m mockFileInfo) Mode() fs.FileMode {\n\treturn m.mode\n}\n\nfunc (m mockFileInfo) ModTime() time.Time {\n\treturn m.modTime\n}\n\nfunc (m mockFileInfo) IsDir() bool {\n\treturn m.isDir\n}\n\nfunc (m mockFileInfo) Sys() any {\n\treturn m.sys\n}\n\nfunc TestReaderEntry_writeEntry(t *testing.T) {\n\td := t.TempDir()\n\tfile := filepath.Join(d, \"file.txt\")\n\trequire.NoError(t, os.WriteFile(file, []byte(\"hello world\"), 0644))\n\n\tlink := filepath.Join(d, \"link\")\n\trequire.NoError(t, os.Symlink(file, link))\n\n\tdir := filepath.Join(d, \"dir\")\n\trequire.NoError(t, os.Mkdir(dir, 0755))\n\n\ttests := []struct {\n\t\tname        string\n\t\ttypeFlag    byte\n\t\tbytes       []byte\n\t\tfilename    string\n\t\tfileinfo    os.FileInfo\n\t\twantErr     require.ErrorAssertionFunc\n\t\texpectFlush bool\n\t\tfs          afero.Fs\n\t}{\n\t\t{\n\t\t\tname:        \"valid file\",\n\t\t\ttypeFlag:    tar.TypeReg,\n\t\t\tbytes:       []byte(\"hello world\"),\n\t\t\tfilename:    file,\n\t\t\texpectFlush: true,\n\t\t\tfileinfo: &mockFileInfo{\n\t\t\t\tname:    file,\n\t\t\t\tsize:    11,\n\t\t\t\tmode:    0644,\n\t\t\t\tmodTime: time.Now(),\n\t\t\t\tisDir:   false,\n\t\t\t\tsys:     nil,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:        \"symlink\",\n\t\t\ttypeFlag:    tar.TypeSymlink,\n\t\t\tbytes:       nil,\n\t\t\tfilename:    link,\n\t\t\texpectFlush: false,\n\t\t\tfileinfo: &mockFileInfo{\n\t\t\t\tname:    link,\n\t\t\t\tsize:    0,\n\t\t\t\tmode:    os.ModeSymlink,\n\t\t\t\tmodTime: time.Now(),\n\t\t\t\tisDir:   false,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:        \"directory\",\n\t\t\ttypeFlag:    tar.TypeDir,\n\t\t\tbytes:       nil,\n\t\t\tfilename:    dir,\n\t\t\texpectFlush: false,\n\t\t\tfileinfo: &mockFileInfo{\n\t\t\t\tname:    dir,\n\t\t\t\tsize:    0,\n\t\t\t\tmode:    os.ModeDir,\n\t\t\t\tmodTime: time.Now(),\n\t\t\t\tisDir:   true,\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif tt.wantErr == nil {\n\t\t\t\ttt.wantErr = require.NoError\n\t\t\t}\n\n\t\t\tfe := NewEntryFromBytes(tt.bytes, tt.filename, tt.fileinfo)\n\t\t\ttw := &mockTarWriter{}\n\n\t\t\terr := fe.writeEntry(tw)\n\t\t\ttt.wantErr(t, err)\n\t\t\tif err != nil {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.NoError(t, err)\n\t\t\trequire.Len(t, tw.headers, 1)\n\t\t\tassert.Equal(t, tt.typeFlag, tw.headers[0].Typeflag)\n\t\t\tassert.Equal(t, tt.filename, tw.headers[0].Name)\n\t\t\tassert.Equal(t, int64(len(tt.bytes)), tw.headers[0].Size)\n\t\t\tassert.Equal(t, string(tt.bytes), tw.buffers[0].String())\n\t\t\tassert.Equal(t, tt.expectFlush, tw.flushCalled)\n\t\t})\n\t}\n}\n\nfunc Test_readerWithSize(t *testing.T) {\n\ttestData := \"hello world from test\"\n\n\ttests := []struct {\n\t\tname     string\n\t\treader   func(t *testing.T) io.Reader\n\t\twantSize int64\n\t\twantErr  require.ErrorAssertionFunc\n\t}{\n\t\t{\n\t\t\tname: \"bytes.Reader\",\n\t\t\treader: func(t *testing.T) io.Reader {\n\t\t\t\treturn bytes.NewReader([]byte(testData))\n\t\t\t},\n\t\t\twantSize: int64(len(testData)),\n\t\t},\n\t\t{\n\t\t\tname: \"os.File success\",\n\t\t\treader: func(t *testing.T) io.Reader {\n\t\t\t\tdir := t.TempDir()\n\t\t\t\tpath := filepath.Join(dir, \"test.txt\")\n\t\t\t\trequire.NoError(t, os.WriteFile(path, []byte(testData), 0644))\n\t\t\t\tf, err := os.Open(path)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tt.Cleanup(func() { f.Close() })\n\t\t\t\treturn f\n\t\t\t},\n\t\t\twantSize: int64(len(testData)),\n\t\t},\n\t\t{\n\t\t\tname: \"os.File stat fails\",\n\t\t\treader: func(t *testing.T) io.Reader {\n\t\t\t\tdir := t.TempDir()\n\t\t\t\tpath := filepath.Join(dir, \"test.txt\")\n\t\t\t\trequire.NoError(t, os.WriteFile(path, []byte(testData), 0644))\n\t\t\t\tf, err := os.Open(path)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tf.Close()\n\t\t\t\treturn f\n\t\t\t},\n\t\t\twantErr: require.Error,\n\t\t},\n\t\t{\n\t\t\tname: \"unknown reader creates temp file\",\n\t\t\treader: func(t *testing.T) io.Reader {\n\t\t\t\treturn strings.NewReader(testData)\n\t\t\t},\n\t\t\twantSize: int64(len(testData)),\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif tt.wantErr == nil {\n\t\t\t\ttt.wantErr = require.NoError\n\t\t\t}\n\n\t\t\treader := tt.reader(t)\n\t\t\tsize, rc, err := readerWithSize(reader)\n\t\t\ttt.wantErr(t, err)\n\t\t\tif err != nil {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tdefer rc.Close()\n\n\t\t\tassert.Equal(t, tt.wantSize, size)\n\n\t\t\tcontent, err := io.ReadAll(rc)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, testData, string(content))\n\t\t})\n\t}\n}\n\nfunc Test_autoDeleteFile(t *testing.T) {\n\tdir := t.TempDir()\n\tpath := filepath.Join(dir, \"test.txt\")\n\trequire.NoError(t, os.WriteFile(path, []byte(\"test content\"), 0644))\n\n\tf, err := os.Open(path)\n\trequire.NoError(t, err)\n\n\tadf := &autoDeleteFile{File: f}\n\n\t_, err = os.Stat(path)\n\trequire.NoError(t, err)\n\n\terr = adf.Close()\n\trequire.NoError(t, err)\n\n\t_, err = os.Stat(path)\n\tassert.True(t, os.IsNotExist(err))\n}\n"
  },
  {
    "path": "grype/db/internal/tarutil/tar.go",
    "content": "package tarutil\n\nimport (\n\t\"archive/tar\"\n\t\"io\"\n)\n\n// Writer represents a facade for writing entries to a tar file.\ntype Writer interface {\n\tWriteEntry(Entry) error\n\tio.Closer\n}\n\n// lowLevelWriter abstracts the *tar.Writer from the standard library.\ntype lowLevelWriter interface {\n\tWriteHeader(*tar.Header) error\n\tFlush() error\n\tio.WriteCloser\n}\n\n// Entry represents an entry that can be written to a tar file via a tar.Writer from the standard library.\ntype Entry interface {\n\twriteEntry(writer lowLevelWriter) error\n}\n"
  },
  {
    "path": "grype/db/internal/tarutil/writer.go",
    "content": "package tarutil\n\nimport (\n\t\"archive/tar\"\n\t\"bufio\"\n\t\"compress/gzip\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n\n\t\"github.com/google/shlex\"\n\t\"github.com/klauspost/compress/flate\"\n\n\t\"github.com/anchore/grype/internal/log\"\n)\n\nvar ErrUnsupportedArchiveSuffix = fmt.Errorf(\"archive name has an unsupported suffix\")\n\nvar _ Writer = (*writer)(nil)\n\ntype writer struct {\n\tcompressor io.WriteCloser\n\twriter     *tar.Writer\n}\n\n// NewWriter creates a new tar writer that writes to the specified archive path. Supports .tar.gz, .tar.zst, .tar.xz, and .tar file extensions.\nfunc NewWriter(archivePath string) (Writer, error) {\n\treturn NewWriterWithCompressors(archivePath, nil)\n}\n\n// NewWriterWithCompressors creates a new tar writer with custom compressor commands. If compressorCommands is nil or empty, it uses default commands.\nfunc NewWriterWithCompressors(archivePath string, compressorCommands map[string]string) (Writer, error) {\n\tw, err := newCompressorWithCommands(archivePath, compressorCommands)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\ttw := tar.NewWriter(w)\n\n\treturn &writer{\n\t\tcompressor: w,\n\t\twriter:     tw,\n\t}, nil\n}\n\nfunc newCompressorWithCommands(archivePath string, compressorCommands map[string]string) (io.WriteCloser, error) {\n\tarchive, err := os.Create(archivePath)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// check for custom compressor command first\n\tfor ext, cmd := range compressorCommands {\n\t\tif strings.HasSuffix(archivePath, \".\"+ext) {\n\t\t\tlog.Debugf(\"using custom compressor command for %s: %s\", ext, cmd)\n\t\t\treturn newShellCompressor(cmd, archive)\n\t\t}\n\t}\n\tlog.Debugf(\"no custom compressor command found for %s, using default\", archivePath)\n\n\t// fall back to default compressor commands\n\tswitch {\n\tcase strings.HasSuffix(archivePath, \".tar.gz\"):\n\t\treturn gzip.NewWriterLevel(archive, flate.BestCompression)\n\tcase strings.HasSuffix(archivePath, \".tar.zst\"):\n\t\t// note: since we're using --ultra this tends to have a high memory usage at decompression time\n\t\t// For ~700 MB payload that is compressing down to ~60 MB, that would need ~130 MB of memory (--ultra -22)\n\t\t// for the same payload compressing down to ~65MB, that would need ~70MB of memory (--ultra -21)\n\t\treturn newShellCompressor(\"zstd -T0 -22 --ultra -c -vv\", archive)\n\tcase strings.HasSuffix(archivePath, \".tar.xz\"):\n\t\treturn newShellCompressor(\"xz -9 --threads=0 -c -vv\", archive)\n\tcase strings.HasSuffix(archivePath, \".tar\"):\n\t\treturn archive, nil\n\t}\n\treturn nil, ErrUnsupportedArchiveSuffix\n}\n\n// shellCompressor wraps the stdin pipe of an external compression process and ensures proper cleanup.\ntype shellCompressor struct {\n\tcmd  *exec.Cmd\n\tpipe io.WriteCloser\n}\n\nfunc newShellCompressor(c string, archive io.Writer) (*shellCompressor, error) {\n\targs, err := shlex.Split(c)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to parse command: %w\", err)\n\t}\n\tbinary := args[0]\n\n\tbinPath, err := exec.LookPath(binary)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to find binary %q: %w\", binary, err)\n\t}\n\tif binPath == \"\" {\n\t\treturn nil, fmt.Errorf(\"unable to find binary %q in PATH\", binary)\n\t}\n\n\targs = args[1:]\n\tcmd := exec.Command(binary, args...)\n\tlog.Debug(strings.Join(cmd.Args, \" \"))\n\tcmd.Stdout = archive\n\n\tstderrPipe, err := cmd.StderrPipe()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create stderr pipe: %w\", err)\n\t}\n\n\tpipe, err := cmd.StdinPipe()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create stdin pipe: %w\", err)\n\t}\n\n\tif err := cmd.Start(); err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to start process: %w\", err)\n\t}\n\n\tgo func() {\n\t\tscanner := bufio.NewScanner(stderrPipe)\n\t\tfor scanner.Scan() {\n\t\t\tlog.Debugf(\"[%s] %s\", binary, scanner.Text())\n\t\t}\n\t\tif err := scanner.Err(); err != nil {\n\t\t\tlog.Errorf(\"[%s] error reading stderr: %v\", binary, err)\n\t\t}\n\t}()\n\n\treturn &shellCompressor{\n\t\tcmd:  cmd,\n\t\tpipe: pipe,\n\t}, nil\n}\n\nfunc (sc *shellCompressor) Write(p []byte) (int, error) {\n\treturn sc.pipe.Write(p)\n}\n\nfunc (sc *shellCompressor) Close() error {\n\tif err := sc.pipe.Close(); err != nil {\n\t\treturn fmt.Errorf(\"unable to close compression stdin pipe: %w\", err)\n\t}\n\tif err := sc.cmd.Wait(); err != nil {\n\t\treturn fmt.Errorf(\"compression process error: %w\", err)\n\t}\n\treturn nil\n}\n\nfunc (w *writer) WriteEntry(entry Entry) error {\n\treturn entry.writeEntry(w.writer)\n}\n\nfunc (w *writer) Close() error {\n\tif w.writer != nil {\n\t\terr := w.writer.Close()\n\t\tw.writer = nil\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"unable to close tar writer: %w\", err)\n\t\t}\n\t}\n\n\tif w.compressor != nil {\n\t\terr := w.compressor.Close()\n\t\tw.compressor = nil\n\t\treturn err\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "grype/db/internal/tarutil/writer_test.go",
    "content": "package tarutil\n\nimport (\n\t\"archive/tar\"\n\t\"compress/gzip\"\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/klauspost/compress/zstd\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestNewWriter(t *testing.T) {\n\ttests := []struct {\n\t\tname        string\n\t\tarchivePath string\n\t\twantErr     bool\n\t}{\n\t\t{\n\t\t\tname:        \"tar.gz compressor\",\n\t\t\tarchivePath: \"test.tar.gz\",\n\t\t\twantErr:     false,\n\t\t},\n\t\t{\n\t\t\tname:        \"tar.zst compressor\",\n\t\t\tarchivePath: \"test.tar.zst\",\n\t\t\twantErr:     false,\n\t\t},\n\t\t{\n\t\t\tname:        \"tar compressor\",\n\t\t\tarchivePath: \"test.tar\",\n\t\t\twantErr:     false,\n\t\t},\n\t\t{\n\t\t\tname:        \"unsupported compressor\",\n\t\t\tarchivePath: \"test.txt\",\n\t\t\twantErr:     true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tdir := t.TempDir()\n\t\t\trequire.NoError(t, os.Chdir(dir))\n\t\t\ttestFilePath := \"testfile\"\n\t\t\ttestString := \"hello world\"\n\t\t\trequire.NoError(t, os.WriteFile(testFilePath, []byte(testString), 0644))\n\n\t\t\tarchivePath := filepath.Join(dir, tt.archivePath)\n\n\t\t\tw, err := NewWriter(archivePath)\n\t\t\tif tt.wantErr {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\treturn\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\n\t\t\tentry := NewEntryFromFilePath(testFilePath)\n\n\t\t\terr = w.WriteEntry(entry)\n\t\t\trequire.NoError(t, err)\n\n\t\t\terr = w.Close()\n\t\t\trequire.NoError(t, err)\n\n\t\t\t_, err = os.Stat(archivePath)\n\t\t\tassert.NoError(t, err)\n\n\t\t\tvar r io.Reader\n\t\t\tf, err := os.Open(archivePath)\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer f.Close()\n\n\t\t\tswitch {\n\t\t\tcase strings.HasSuffix(archivePath, \".tar.gz\"):\n\t\t\t\tr, err = gzip.NewReader(f)\n\t\t\t\trequire.NoError(t, err)\n\t\t\tcase strings.HasSuffix(archivePath, \".tar.zst\"):\n\t\t\t\tr, err = zstd.NewReader(f)\n\t\t\t\trequire.NoError(t, err)\n\t\t\tcase strings.HasSuffix(archivePath, \".tar\"):\n\t\t\t\tr = f\n\t\t\tdefault:\n\t\t\t\tt.Fatalf(\"unsupported archive type: %s\", archivePath)\n\t\t\t}\n\n\t\t\ttr := tar.NewReader(r)\n\t\t\thdr, err := tr.Next()\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.Equal(t, testFilePath, hdr.Name)\n\t\t\tassert.Equal(t, int64(len(testString)), hdr.Size)\n\n\t\t\tcontent, err := io.ReadAll(tr)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, testString, string(content))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/db/internal/testutil/utils.go",
    "content": "package testutil\n\nimport (\n\t\"log\"\n\t\"os\"\n)\n\nfunc CloseFile(f *os.File) {\n\terr := f.Close()\n\n\tif err != nil {\n\t\tlog.Fatal(\"error closing file\")\n\t}\n}\n"
  },
  {
    "path": "grype/db/internal/versionutil/clean_fixed_in_version.go",
    "content": "package versionutil\n\nimport \"strings\"\n\nfunc CleanFixedInVersion(version string) string {\n\tswitch strings.TrimSpace(strings.ToLower(version)) {\n\tcase \"none\", \"\":\n\t\treturn \"\"\n\tdefault:\n\t\treturn version\n\t}\n}\n"
  },
  {
    "path": "grype/db/internal/versionutil/constraint.go",
    "content": "package versionutil\n\nimport (\n\t\"regexp\"\n\t\"strings\"\n)\n\n// match examples:\n// >= 5.0.0\n// <= 6.1.2.beta\n// >= 5.0.0\n// < 6.1\n// > 5.0.0\n// >=5\n// <6\nvar forceSemVerPattern = regexp.MustCompile(`[><=]+\\s*[^<>=]+`)\n\nfunc EnforceSemVerConstraint(constraint string) string {\n\tconstraint = CleanConstraint(constraint)\n\tif constraint == \"\" {\n\t\treturn \"\"\n\t}\n\treturn strings.ReplaceAll(strings.Join(forceSemVerPattern.FindAllString(constraint, -1), \", \"), \" \", \"\")\n}\n\nfunc AndConstraints(c ...string) string {\n\treturn strings.Join(c, \" \")\n}\n\nfunc OrConstraints(c ...string) string {\n\treturn strings.Join(c, \" || \")\n}\n\nfunc CleanConstraint(constraint string) string {\n\tif strings.ToLower(constraint) == \"none\" {\n\t\treturn \"\"\n\t}\n\treturn constraint\n}\n"
  },
  {
    "path": "grype/db/internal/versionutil/constraint_test.go",
    "content": "package versionutil\n\nimport \"testing\"\n\nfunc TestEnforceSemVerConstraint(t *testing.T) {\n\ttests := []struct {\n\t\tvalue    string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tvalue:    \" >=  5.0.0<7.1 \",\n\t\t\texpected: \">=5.0.0,<7.1\",\n\t\t},\n\t\t{\n\t\t\tvalue:    \"None\",\n\t\t\texpected: \"\",\n\t\t},\n\t\t{\n\t\t\tvalue:    \"\",\n\t\t\texpected: \"\",\n\t\t},\n\t}\n\tfor _, test := range tests {\n\t\tt.Run(test.value, func(t *testing.T) {\n\t\t\tactual := EnforceSemVerConstraint(test.value)\n\t\t\tif actual != test.expected {\n\t\t\t\tt.Errorf(\"mismatch: '%s'!='%s'\", actual, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/db/package.go",
    "content": "package db\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\n\tgrypeDBLegacyDistribution \"github.com/anchore/grype/grype/db/v5/distribution\"\n\tv6process \"github.com/anchore/grype/grype/db/v6/build\"\n)\n\nfunc Package(dbDir, publishBaseURL, overrideArchiveExtension string, compressorCommands map[string]string) error {\n\t// check if metadata file exists, if so, then this\n\tif _, err := os.Stat(filepath.Join(dbDir, grypeDBLegacyDistribution.MetadataFileName)); os.IsNotExist(err) {\n\t\t// TODO: detect from disk which version of the DB is present\n\t\treturn v6process.CreateArchive(dbDir, overrideArchiveExtension, compressorCommands)\n\t}\n\treturn packageLegacyDB(dbDir, publishBaseURL, overrideArchiveExtension, compressorCommands)\n}\n"
  },
  {
    "path": "grype/db/package_legacy.go",
    "content": "package db\n\nimport (\n\t\"fmt\"\n\t\"net/url\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/scylladb/go-set/strset\"\n\t\"github.com/spf13/afero\"\n\n\t\"github.com/anchore/grype/grype/db/internal/tarutil\"\n\tgrypeDBLegacy \"github.com/anchore/grype/grype/db/v5\"\n\tgrypeDBLegacyDistribution \"github.com/anchore/grype/grype/db/v5/distribution\"\n\tgrypeDBLegacyStore \"github.com/anchore/grype/grype/db/v5/store\"\n\t\"github.com/anchore/grype/internal/log\"\n)\n\n// listingFiles is a set of files that should not be included in the archive\nvar listingFiles = strset.New(\"listing.json\", \"latest.json\", \"history.json\")\n\nfunc packageLegacyDB(dbDir, publishBaseURL, overrideArchiveExtension string, compressorCommands map[string]string) error { //nolint:funlen\n\tlog.WithFields(\"from\", dbDir, \"url\", publishBaseURL, \"extension-override\", overrideArchiveExtension).Info(\"packaging database\")\n\n\tfs := afero.NewOsFs()\n\tmetadata, err := grypeDBLegacyDistribution.NewMetadataFromDir(fs, dbDir)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif metadata == nil {\n\t\treturn fmt.Errorf(\"no metadata found in %q\", dbDir)\n\t}\n\n\ts, err := grypeDBLegacyStore.New(filepath.Join(dbDir, grypeDBLegacy.VulnerabilityStoreFileName), false)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to open vulnerability store: %w\", err)\n\t}\n\n\tid, err := s.GetID()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to get vulnerability store ID: %w\", err)\n\t}\n\n\tif id.SchemaVersion != metadata.Version {\n\t\treturn fmt.Errorf(\"metadata version %d does not match vulnerability store version %d\", metadata.Version, id.SchemaVersion)\n\t}\n\n\tu, err := url.Parse(publishBaseURL)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// we need a well-ordered string to append to the archive name to ensure uniqueness (to avoid overwriting\n\t// existing archives in the CDN) as well as to ensure that multiple archives created in the same day are\n\t// put in the correct order in the listing file. The DB timestamp represents the age of the data in the DB\n\t// not when the DB was created. The trailer represents the time the DB was packaged.\n\ttrailer := fmt.Sprintf(\"%d\", secondsSinceEpoch())\n\n\tvar extension = \"tar.gz\"\n\tif overrideArchiveExtension != \"\" {\n\t\textension = strings.TrimLeft(overrideArchiveExtension, \".\")\n\t}\n\n\tvar found bool\n\tfor _, valid := range []string{\"tar.zst\", \"tar.gz\"} {\n\t\tif valid == extension {\n\t\t\tfound = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif !found {\n\t\treturn fmt.Errorf(\"invalid archive extension %q\", extension)\n\t}\n\n\t// we attach a random value at the end of the file name to prevent from overwriting DBs in S3 that are already\n\t// cached in the CDN. Ideally this would be based off of the archive checksum but a random string is simpler.\n\ttarName := fmt.Sprintf(\n\t\t\"vulnerability-db_v%d_%s_%s.%s\",\n\t\tmetadata.Version,\n\t\tmetadata.Built.Format(time.RFC3339),\n\t\ttrailer,\n\t\textension,\n\t)\n\ttarPath := path.Join(dbDir, tarName)\n\n\tif err := populateLegacyTar(tarPath, compressorCommands); err != nil {\n\t\treturn err\n\t}\n\n\tlog.WithFields(\"path\", tarPath).Info(\"created database archive\")\n\n\tentry, err := grypeDBLegacyDistribution.NewListingEntryFromArchive(fs, *metadata, tarPath, u)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to create listing entry from archive: %w\", err)\n\t}\n\n\tlisting := grypeDBLegacyDistribution.NewListing(entry)\n\tlistingPath := path.Join(dbDir, grypeDBLegacyDistribution.ListingFileName)\n\tif err = listing.Write(listingPath); err != nil {\n\t\treturn err\n\t}\n\n\tlog.WithFields(\"path\", listingPath).Debug(\"created initial listing file\")\n\n\treturn nil\n}\n\nfunc populateLegacyTar(tarPath string, compressorCommands map[string]string) error {\n\toriginalDir, err := os.Getwd()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to get CWD: %w\", err)\n\t}\n\n\tdbDir, tarName := filepath.Split(tarPath)\n\n\tif dbDir != \"\" {\n\t\tif err = os.Chdir(dbDir); err != nil {\n\t\t\treturn fmt.Errorf(\"unable to cd to build dir: %w\", err)\n\t\t}\n\n\t\tdefer func() {\n\t\t\tif err = os.Chdir(originalDir); err != nil {\n\t\t\t\tlog.Errorf(\"unable to cd to original dir: %v\", err)\n\t\t\t}\n\t\t}()\n\t}\n\n\tfileInfos, err := os.ReadDir(\"./\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to list db directory: %w\", err)\n\t}\n\n\tvar files []string\n\tfor _, fi := range fileInfos {\n\t\tif !listingFiles.Has(fi.Name()) && !strings.Contains(fi.Name(), \".tar.\") {\n\t\t\tfiles = append(files, fi.Name())\n\t\t}\n\t}\n\n\tif err = tarutil.PopulateWithPathsAndCompressors(tarName, compressorCommands, files...); err != nil {\n\t\treturn fmt.Errorf(\"unable to create db archive: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc secondsSinceEpoch() int64 {\n\treturn time.Now().UTC().Unix()\n}\n"
  },
  {
    "path": "grype/db/processors/annotated_openvex_processor.go",
    "content": "package processors // nolint:dupl\n\nimport (\n\t\"io\"\n\n\t\"github.com/anchore/grype/grype/db/data\"\n\t\"github.com/anchore/grype/grype/db/internal/provider/unmarshal\"\n\t\"github.com/anchore/grype/grype/db/provider\"\n\t\"github.com/anchore/grype/internal/log\"\n)\n\ntype annotatedOpenVEXProcessor struct {\n\ttransformer data.AnnotatedOpenVEXTransformerV2\n}\n\nfunc NewV2AnnotatedOpenVEXProcessor(transformer data.AnnotatedOpenVEXTransformerV2) data.Processor {\n\treturn &annotatedOpenVEXProcessor{\n\t\ttransformer: transformer,\n\t}\n}\n\nfunc (p annotatedOpenVEXProcessor) Process(reader io.Reader, state provider.State) ([]data.Entry, error) {\n\tvar results []data.Entry\n\n\tentries, err := unmarshal.AnnotatedOpenVEXVulnerabilityEntries(reader)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfor _, entry := range entries {\n\t\ttransformedEntries, err := p.transformer(entry, state)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tresults = append(results, transformedEntries...)\n\t}\n\n\treturn results, nil\n}\n\nfunc (p annotatedOpenVEXProcessor) IsSupported(schemaURL string) bool {\n\tif !hasSchemaSegment(schemaURL, \"annotated-openvex\") {\n\t\treturn false\n\t}\n\n\tparsedVersion, err := parseVersion(schemaURL)\n\tif err != nil {\n\t\tlog.WithFields(\"schema\", schemaURL, \"error\", err).Error(\"failed to parse annotated OpenVEX schema version\")\n\t\treturn false\n\t}\n\n\treturn parsedVersion.Major == 1\n}\n"
  },
  {
    "path": "grype/db/processors/eol_processor.go",
    "content": "//nolint:dupl\npackage processors\n\nimport (\n\t\"io\"\n\n\t\"github.com/anchore/grype/grype/db/data\"\n\t\"github.com/anchore/grype/grype/db/internal/provider/unmarshal\"\n\t\"github.com/anchore/grype/grype/db/provider\"\n\t\"github.com/anchore/grype/internal/log\"\n)\n\ntype eolProcessor struct {\n\ttransformer data.EOLTransformerV2\n}\n\nfunc NewV2EOLProcessor(transformer data.EOLTransformerV2) data.Processor {\n\treturn &eolProcessor{\n\t\ttransformer: transformer,\n\t}\n}\n\nfunc (p eolProcessor) Process(reader io.Reader, state provider.State) ([]data.Entry, error) {\n\tvar results []data.Entry\n\n\tentries, err := unmarshal.EndOfLifeDateReleaseEntries(reader)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfor _, entry := range entries {\n\t\tif entry.IsEmpty() {\n\t\t\tlog.Warn(\"dropping empty EOL entry\")\n\t\t\tcontinue\n\t\t}\n\n\t\ttransformedEntries, err := p.transformer(entry, state)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tresults = append(results, transformedEntries...)\n\t}\n\n\treturn results, nil\n}\n\nfunc (p eolProcessor) IsSupported(schemaURL string) bool {\n\tif !hasSchemaSegment(schemaURL, \"eol\") {\n\t\treturn false\n\t}\n\n\tparsedVersion, err := parseVersion(schemaURL)\n\tif err != nil {\n\t\tlog.WithFields(\"schema\", schemaURL, \"error\", err).Error(\"failed to parse EOL schema version\")\n\t\treturn false\n\t}\n\n\treturn parsedVersion.Major == 1\n}\n"
  },
  {
    "path": "grype/db/processors/eol_processor_test.go",
    "content": "package processors\n\nimport (\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/grype/grype/db/data\"\n\t\"github.com/anchore/grype/grype/db/internal/provider/unmarshal\"\n\t\"github.com/anchore/grype/grype/db/provider\"\n)\n\nfunc mockEOLProcessorTransform(entry unmarshal.EndOfLifeDateRelease, state provider.State) ([]data.Entry, error) {\n\treturn []data.Entry{\n\t\t{\n\t\t\tDBSchemaVersion: 0,\n\t\t\tData:            entry,\n\t\t},\n\t}, nil\n}\n\nfunc TestEOLProcessor_Process(t *testing.T) {\n\tf, err := os.Open(\"testdata/eol.json\")\n\trequire.NoError(t, err)\n\tdefer f.Close()\n\n\tprocessor := NewV2EOLProcessor(mockEOLProcessorTransform)\n\tentries, err := processor.Process(f, provider.State{\n\t\tProvider: \"eol\",\n\t})\n\n\tassert.NoError(t, err)\n\tassert.Len(t, entries, 2)\n\n\t// Verify first entry is ubuntu\n\tentry0, ok := entries[0].Data.(unmarshal.EndOfLifeDateRelease)\n\trequire.True(t, ok)\n\tassert.Equal(t, \"ubuntu\", entry0.Product)\n\tassert.Equal(t, \"22.04\", entry0.Name)\n\tassert.True(t, entry0.IsLTS)\n\n\t// Verify second entry is debian\n\tentry1, ok := entries[1].Data.(unmarshal.EndOfLifeDateRelease)\n\trequire.True(t, ok)\n\tassert.Equal(t, \"debian\", entry1.Product)\n\tassert.Equal(t, \"11\", entry1.Name)\n}\n\nfunc TestEOLProcessor_Process_EmptyEntry(t *testing.T) {\n\t// Test that empty entries (product == \"\") are filtered out\n\tf, err := os.Open(\"testdata/eol-with-empty.json\")\n\trequire.NoError(t, err)\n\tdefer f.Close()\n\n\tprocessor := NewV2EOLProcessor(mockEOLProcessorTransform)\n\tentries, err := processor.Process(f, provider.State{\n\t\tProvider: \"eol\",\n\t})\n\n\tassert.NoError(t, err)\n\t// Should only have 1 entry (empty one filtered out)\n\tassert.Len(t, entries, 1)\n\tentry, ok := entries[0].Data.(unmarshal.EndOfLifeDateRelease)\n\trequire.True(t, ok)\n\tassert.Equal(t, \"alpine\", entry.Product)\n}\n\nfunc TestEOLProcessor_IsSupported(t *testing.T) {\n\ttc := []struct {\n\t\tname      string\n\t\tschemaURL string\n\t\texpected  bool\n\t}{\n\t\t{\n\t\t\tname:      \"valid schema URL with version 1.0.0\",\n\t\t\tschemaURL: \"https://example.com/vunnel/path/eol/schema-1.0.0.json\",\n\t\t\texpected:  true,\n\t\t},\n\t\t{\n\t\t\tname:      \"valid schema URL with version 1.2.3\",\n\t\t\tschemaURL: \"https://example.com/vunnel/path/eol/schema-1.2.3.json\",\n\t\t\texpected:  true,\n\t\t},\n\t\t{\n\t\t\tname:      \"invalid schema URL with unsupported version\",\n\t\t\tschemaURL: \"https://example.com/vunnel/path/eol/schema-2.0.0.json\",\n\t\t\texpected:  false,\n\t\t},\n\t\t{\n\t\t\tname:      \"invalid schema URL with missing version\",\n\t\t\tschemaURL: \"https://example.com/vunnel/path/eol/schema.json\",\n\t\t\texpected:  false,\n\t\t},\n\t\t{\n\t\t\tname:      \"completely invalid URL\",\n\t\t\tschemaURL: \"https://example.com/invalid/schema/url\",\n\t\t\texpected:  false,\n\t\t},\n\t\t{\n\t\t\tname:      \"invalid schema segment\",\n\t\t\tschemaURL: \"https://example.com/vunnel/path/not-eol/schema-1.0.0.json\",\n\t\t\texpected:  false,\n\t\t},\n\t\t{\n\t\t\tname:      \"epss schema should not match\",\n\t\t\tschemaURL: \"https://example.com/vunnel/path/epss/schema-1.0.0.json\",\n\t\t\texpected:  false,\n\t\t},\n\t}\n\n\tp := eolProcessor{}\n\n\tfor _, tt := range tc {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tassert.Equal(t, tt.expected, p.IsSupported(tt.schemaURL))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/db/processors/epss_processor.go",
    "content": "//nolint:dupl\npackage processors\n\nimport (\n\t\"io\"\n\n\t\"github.com/anchore/grype/grype/db/data\"\n\t\"github.com/anchore/grype/grype/db/internal/provider/unmarshal\"\n\t\"github.com/anchore/grype/grype/db/provider\"\n\t\"github.com/anchore/grype/internal/log\"\n)\n\ntype epssProcessor struct {\n\ttransformer data.EPSSTransformerV2\n}\n\nfunc NewV2EPSSProcessor(transformer data.EPSSTransformerV2) data.Processor {\n\treturn &epssProcessor{\n\t\ttransformer: transformer,\n\t}\n}\n\nfunc (p epssProcessor) Process(reader io.Reader, state provider.State) ([]data.Entry, error) {\n\tvar results []data.Entry\n\n\tentries, err := unmarshal.EPSSEntries(reader)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfor _, entry := range entries {\n\t\tif entry.IsEmpty() {\n\t\t\tlog.Warn(\"dropping empty EPSS entry\")\n\t\t\tcontinue\n\t\t}\n\n\t\ttransformedEntries, err := p.transformer(entry, state)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tresults = append(results, transformedEntries...)\n\t}\n\n\treturn results, nil\n}\n\nfunc (p epssProcessor) IsSupported(schemaURL string) bool {\n\tif !hasSchemaSegment(schemaURL, \"epss\") {\n\t\treturn false\n\t}\n\n\tparsedVersion, err := parseVersion(schemaURL)\n\tif err != nil {\n\t\tlog.WithFields(\"schema\", schemaURL, \"error\", err).Error(\"failed to parse EPSS schema version\")\n\t\treturn false\n\t}\n\n\treturn parsedVersion.Major == 1\n}\n"
  },
  {
    "path": "grype/db/processors/epss_processor_test.go",
    "content": "package processors\n\nimport (\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/grype/grype/db/data\"\n\t\"github.com/anchore/grype/grype/db/internal/provider/unmarshal\"\n\t\"github.com/anchore/grype/grype/db/provider\"\n)\n\nfunc mockEPSSProcessorTransform(entry unmarshal.EPSS, state provider.State) ([]data.Entry, error) {\n\treturn []data.Entry{\n\t\t{\n\t\t\tDBSchemaVersion: 0,\n\t\t\tData:            entry,\n\t\t},\n\t}, nil\n}\n\nfunc TestEPSSProcessor_Process(t *testing.T) {\n\tf, err := os.Open(\"testdata/epss.json\")\n\trequire.NoError(t, err)\n\tdefer f.Close()\n\n\tprocessor := NewV2EPSSProcessor(mockEPSSProcessorTransform)\n\tentries, err := processor.Process(f, provider.State{\n\t\tProvider: \"epss\",\n\t})\n\n\tassert.NoError(t, err)\n\tassert.Len(t, entries, 2)\n}\n\nfunc TestEPSSProcessor_IsSupported(t *testing.T) {\n\ttc := []struct {\n\t\tname      string\n\t\tschemaURL string\n\t\texpected  bool\n\t}{\n\t\t{\n\t\t\tname:      \"valid schema URL with version 1.0.0\",\n\t\t\tschemaURL: \"https://example.com/vunnel/path/vulnerability/epss/schema-1.0.0.json\",\n\t\t\texpected:  true,\n\t\t},\n\t\t{\n\t\t\tname:      \"valid schema URL with version 1.2.3\",\n\t\t\tschemaURL: \"https://example.com/vunnel/path/vulnerability/epss/schema-1.2.3.json\",\n\t\t\texpected:  true,\n\t\t},\n\t\t{\n\t\t\tname:      \"invalid schema URL with unsupported version\",\n\t\t\tschemaURL: \"https://example.com/vunnel/path/vulnerability/epss/schema-2.0.0.json\",\n\t\t\texpected:  false,\n\t\t},\n\t\t{\n\t\t\tname:      \"invalid schema URL with missing version\",\n\t\t\tschemaURL: \"https://example.com/vunnel/path/vulnerability/epss/schema.json\",\n\t\t\texpected:  false,\n\t\t},\n\t\t{\n\t\t\tname:      \"completely invalid URL\",\n\t\t\tschemaURL: \"https://example.com/invalid/schema/url\",\n\t\t\texpected:  false,\n\t\t},\n\t\t{\n\t\t\tname:      \"invalid schema segment\",\n\t\t\tschemaURL: \"https://example.com/vunnel/path/vulnerability/not-epss/schema-1.0.0.json\",\n\t\t\texpected:  false,\n\t\t},\n\t}\n\n\tp := epssProcessor{}\n\n\tfor _, tt := range tc {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tassert.Equal(t, tt.expected, p.IsSupported(tt.schemaURL))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/db/processors/github_processor.go",
    "content": "//nolint:dupl\npackage processors\n\nimport (\n\t\"io\"\n\n\t\"github.com/anchore/grype/grype/db/data\"\n\t\"github.com/anchore/grype/grype/db/internal/provider/unmarshal\"\n\t\"github.com/anchore/grype/grype/db/provider\"\n\t\"github.com/anchore/grype/internal/log\"\n)\n\ntype githubProcessor struct {\n\ttransformer any\n}\n\nfunc NewGitHubProcessor(transformer data.GitHubTransformer) data.Processor {\n\treturn &githubProcessor{\n\t\ttransformer: transformer,\n\t}\n}\n\nfunc NewV2GitHubProcessor(transformer data.GitHubTransformerV2) data.Processor {\n\treturn &githubProcessor{\n\t\ttransformer: transformer,\n\t}\n}\n\nfunc (p githubProcessor) Process(reader io.Reader, state provider.State) ([]data.Entry, error) {\n\tvar results []data.Entry\n\n\tentries, err := unmarshal.GitHubAdvisoryEntries(reader)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar handle func(entry unmarshal.GitHubAdvisory) ([]data.Entry, error)\n\tswitch t := p.transformer.(type) {\n\tcase data.GitHubTransformer:\n\t\thandle = func(entry unmarshal.GitHubAdvisory) ([]data.Entry, error) {\n\t\t\treturn t(entry)\n\t\t}\n\tcase data.GitHubTransformerV2:\n\t\thandle = func(entry unmarshal.GitHubAdvisory) ([]data.Entry, error) {\n\t\t\treturn t(entry, state)\n\t\t}\n\t}\n\n\tfor _, entry := range entries {\n\t\tif entry.IsEmpty() {\n\t\t\tlog.Warn(\"dropping empty GHSA entry\")\n\t\t\tcontinue\n\t\t}\n\n\t\ttransformedEntries, err := handle(entry)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tresults = append(results, transformedEntries...)\n\t}\n\n\treturn results, nil\n}\n\nfunc (p githubProcessor) IsSupported(schemaURL string) bool {\n\tif !hasSchemaSegment(schemaURL, \"github-security-advisory\") {\n\t\treturn false\n\t}\n\n\tparsedVersion, err := parseVersion(schemaURL)\n\tif err != nil {\n\t\tlog.WithFields(\"schema\", schemaURL, \"error\", err).Error(\"failed to parse GHSA schema version\")\n\t\treturn false\n\t}\n\n\treturn parsedVersion.Major == 1\n}\n"
  },
  {
    "path": "grype/db/processors/github_processor_test.go",
    "content": "package processors\n\nimport (\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/grype/grype/db/data\"\n\t\"github.com/anchore/grype/grype/db/internal/provider/unmarshal\"\n\t\"github.com/anchore/grype/grype/db/internal/testutil\"\n\t\"github.com/anchore/grype/grype/db/provider\"\n)\n\nfunc mockGithubProcessorTransform(vulnerability unmarshal.GitHubAdvisory) ([]data.Entry, error) {\n\treturn []data.Entry{\n\t\t{\n\t\t\tDBSchemaVersion: 0,\n\t\t\tData:            vulnerability,\n\t\t},\n\t}, nil\n}\n\nfunc TestGitHubProcessor_Process(t *testing.T) {\n\tf, err := os.Open(\"testdata/github.json\")\n\trequire.NoError(t, err)\n\tdefer testutil.CloseFile(f)\n\n\tprocessor := NewGitHubProcessor(mockGithubProcessorTransform)\n\tentries, err := processor.Process(f, provider.State{\n\t\tProvider: \"github\",\n\t})\n\n\tassert.NoError(t, err)\n\tassert.Len(t, entries, 3)\n}\n\nfunc TestGithubProcessor_IsSupported(t *testing.T) {\n\ttc := []struct {\n\t\tname      string\n\t\tschemaURL string\n\t\texpected  bool\n\t}{\n\t\t{\n\t\t\tname:      \"valid schema URL with version 1.0.0\",\n\t\t\tschemaURL: \"https://example.com/vunnel/path/vulnerability/github-security-advisory/schema-1.0.0.json\",\n\t\t\texpected:  true,\n\t\t},\n\t\t{\n\t\t\tname:      \"valid schema URL with version 1.2.3\",\n\t\t\tschemaURL: \"https://example.com/vunnel/path/vulnerability/github-security-advisory/schema-1.2.3.json\",\n\t\t\texpected:  true,\n\t\t},\n\t\t{\n\t\t\tname:      \"invalid schema URL with unsupported version\",\n\t\t\tschemaURL: \"https://example.com/vunnel/path/vulnerability/github-security-advisory/schema-2.0.0.json\",\n\t\t\texpected:  false,\n\t\t},\n\t\t{\n\t\t\tname:      \"invalid schema URL with missing version\",\n\t\t\tschemaURL: \"https://example.com/vunnel/path/vulnerability/github-security-advisory/schema.json\",\n\t\t\texpected:  false,\n\t\t},\n\t\t{\n\t\t\tname:      \"completely invalid URL\",\n\t\t\tschemaURL: \"https://example.com/invalid/schema/url\",\n\t\t\texpected:  false,\n\t\t},\n\t}\n\n\tp := githubProcessor{}\n\n\tfor _, tt := range tc {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tassert.Equal(t, tt.expected, p.IsSupported(tt.schemaURL))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/db/processors/kev_processor.go",
    "content": "//nolint:dupl\npackage processors\n\nimport (\n\t\"io\"\n\n\t\"github.com/anchore/grype/grype/db/data\"\n\t\"github.com/anchore/grype/grype/db/internal/provider/unmarshal\"\n\t\"github.com/anchore/grype/grype/db/provider\"\n\t\"github.com/anchore/grype/internal/log\"\n)\n\ntype kevProcessor struct {\n\ttransformer data.KnownExploitedVulnerabilityTransformerV2\n}\n\nfunc NewV2KEVProcessor(transformer data.KnownExploitedVulnerabilityTransformerV2) data.Processor {\n\treturn &kevProcessor{\n\t\ttransformer: transformer,\n\t}\n}\n\nfunc (p kevProcessor) Process(reader io.Reader, state provider.State) ([]data.Entry, error) {\n\tvar results []data.Entry\n\n\tentries, err := unmarshal.KnownExploitedVulnerabilityEntries(reader)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfor _, entry := range entries {\n\t\tif entry.IsEmpty() {\n\t\t\tlog.Warn(\"dropping empty KEV entry\")\n\t\t\tcontinue\n\t\t}\n\n\t\ttransformedEntries, err := p.transformer(entry, state)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tresults = append(results, transformedEntries...)\n\t}\n\n\treturn results, nil\n}\n\nfunc (p kevProcessor) IsSupported(schemaURL string) bool {\n\tif !hasSchemaSegment(schemaURL, \"known-exploited\") {\n\t\treturn false\n\t}\n\n\tparsedVersion, err := parseVersion(schemaURL)\n\tif err != nil {\n\t\tlog.WithFields(\"schema\", schemaURL, \"error\", err).Error(\"failed to parse KEV schema version\")\n\t\treturn false\n\t}\n\n\treturn parsedVersion.Major == 1\n}\n"
  },
  {
    "path": "grype/db/processors/kev_processor_test.go",
    "content": "package processors\n\nimport (\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/grype/grype/db/data\"\n\t\"github.com/anchore/grype/grype/db/internal/provider/unmarshal\"\n\t\"github.com/anchore/grype/grype/db/provider\"\n)\n\nfunc mockKEVProcessorTransform(vulnerability unmarshal.KnownExploitedVulnerability, state provider.State) ([]data.Entry, error) {\n\treturn []data.Entry{\n\t\t{\n\t\t\tDBSchemaVersion: 0,\n\t\t\tData:            vulnerability,\n\t\t},\n\t}, nil\n}\n\nfunc TestKEVProcessor_Process(t *testing.T) {\n\tf, err := os.Open(\"testdata/kev.json\")\n\trequire.NoError(t, err)\n\tdefer f.Close()\n\n\tprocessor := NewV2KEVProcessor(mockKEVProcessorTransform)\n\tentries, err := processor.Process(f, provider.State{\n\t\tProvider: \"kev\",\n\t})\n\n\tassert.NoError(t, err)\n\tassert.Len(t, entries, 4)\n}\n\nfunc TestKEVProcessor_IsSupported(t *testing.T) {\n\ttc := []struct {\n\t\tname      string\n\t\tschemaURL string\n\t\texpected  bool\n\t}{\n\t\t{\n\t\t\tname:      \"valid schema URL with version 1.0.0\",\n\t\t\tschemaURL: \"https://example.com/vunnel/path/vulnerability/known-exploited/schema-1.0.0.json\",\n\t\t\texpected:  true,\n\t\t},\n\t\t{\n\t\t\tname:      \"valid schema URL with version 1.2.3\",\n\t\t\tschemaURL: \"https://example.com/vunnel/path/vulnerability/known-exploited/schema-1.2.3.json\",\n\t\t\texpected:  true,\n\t\t},\n\t\t{\n\t\t\tname:      \"invalid schema URL with unsupported version\",\n\t\t\tschemaURL: \"https://example.com/vunnel/path/vulnerability/known-exploited/schema-2.0.0.json\",\n\t\t\texpected:  false,\n\t\t},\n\t\t{\n\t\t\tname:      \"invalid schema URL with missing version\",\n\t\t\tschemaURL: \"https://example.com/vunnel/path/vulnerability/known-exploited/schema.json\",\n\t\t\texpected:  false,\n\t\t},\n\t\t{\n\t\t\tname:      \"completely invalid URL\",\n\t\t\tschemaURL: \"https://example.com/invalid/schema/url\",\n\t\t\texpected:  false,\n\t\t},\n\t\t{\n\t\t\tname:      \"invalid schema segment\",\n\t\t\tschemaURL: \"https://example.com/vunnel/path/vulnerability/not-kev/schema-1.0.0.json\",\n\t\t\texpected:  false,\n\t\t},\n\t}\n\n\tp := kevProcessor{}\n\n\tfor _, tt := range tc {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tassert.Equal(t, tt.expected, p.IsSupported(tt.schemaURL))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/db/processors/match_exclusion_processor.go",
    "content": "//nolint:dupl\npackage processors\n\nimport (\n\t\"io\"\n\n\t\"github.com/anchore/grype/grype/db/data\"\n\t\"github.com/anchore/grype/grype/db/internal/provider/unmarshal\"\n\t\"github.com/anchore/grype/grype/db/provider\"\n\t\"github.com/anchore/grype/internal/log\"\n)\n\ntype matchExclusionProcessor struct {\n\ttransformer data.MatchExclusionTransformer\n}\n\nfunc NewMatchExclusionProcessor(transformer data.MatchExclusionTransformer) data.Processor {\n\treturn &matchExclusionProcessor{\n\t\ttransformer: transformer,\n\t}\n}\n\nfunc (p matchExclusionProcessor) Process(reader io.Reader, _ provider.State) ([]data.Entry, error) {\n\tvar results []data.Entry\n\n\tentries, err := unmarshal.MatchExclusions(reader)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfor _, entry := range entries {\n\t\tif entry.IsEmpty() {\n\t\t\tlog.Warn(\"dropping empty match-exclusion entry\")\n\t\t\tcontinue\n\t\t}\n\n\t\ttransformedEntries, err := p.transformer(entry)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tresults = append(results, transformedEntries...)\n\t}\n\n\treturn results, nil\n}\n\nfunc (p matchExclusionProcessor) IsSupported(schemaURL string) bool {\n\tif !hasSchemaSegment(schemaURL, \"match-exclusion\") {\n\t\treturn false\n\t}\n\n\tparsedVersion, err := parseVersion(schemaURL)\n\tif err != nil {\n\t\tlog.WithFields(\"schema\", schemaURL, \"error\", err).Error(\"failed to parse match-exclusion schema version\")\n\t\treturn false\n\t}\n\n\treturn parsedVersion.Major == 1\n}\n"
  },
  {
    "path": "grype/db/processors/match_exclusion_processor_test.go",
    "content": "package processors\n\nimport (\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/grype/grype/db/data\"\n\t\"github.com/anchore/grype/grype/db/internal/provider/unmarshal\"\n\t\"github.com/anchore/grype/grype/db/internal/testutil\"\n\t\"github.com/anchore/grype/grype/db/provider\"\n)\n\nfunc mockMatchExclusionProcessorTransform(vulnerability unmarshal.MatchExclusion) ([]data.Entry, error) {\n\treturn []data.Entry{\n\t\t{\n\t\t\tDBSchemaVersion: 0,\n\t\t\tData:            vulnerability,\n\t\t},\n\t}, nil\n}\n\nfunc TestMatchExclusionProcessor_Process(t *testing.T) {\n\tf, err := os.Open(\"testdata/exclusions.json\")\n\trequire.NoError(t, err)\n\tdefer testutil.CloseFile(f)\n\n\tprocessor := NewMatchExclusionProcessor(mockMatchExclusionProcessorTransform)\n\tentries, err := processor.Process(f, provider.State{\n\t\tProvider: \"match-exclusions\",\n\t})\n\n\trequire.NoError(t, err)\n\tassert.Len(t, entries, 3)\n}\n\nfunc TestMatchExclusionProcessor_IsSupported(t *testing.T) {\n\ttc := []struct {\n\t\tname      string\n\t\tschemaURL string\n\t\texpected  bool\n\t}{\n\t\t{\n\t\t\tname:      \"valid schema URL with version 1.0.0\",\n\t\t\tschemaURL: \"https://example.com/vunnel/path/match-exclusion/schema-1.0.0.json\",\n\t\t\texpected:  true,\n\t\t},\n\t\t{\n\t\t\tname:      \"valid schema URL with version 1.3.4\",\n\t\t\tschemaURL: \"https://example.com/vunnel/path/match-exclusion/schema-1.3.4.json\",\n\t\t\texpected:  true,\n\t\t},\n\t\t{\n\t\t\tname:      \"invalid schema URL with unsupported version\",\n\t\t\tschemaURL: \"https://example.com/vunnel/path/match-exclusion/schema-2.0.0.json\",\n\t\t\texpected:  false,\n\t\t},\n\t\t{\n\t\t\tname:      \"invalid schema URL with missing version\",\n\t\t\tschemaURL: \"https://example.com/vunnel/path/match-exclusion/schema.json\",\n\t\t\texpected:  false,\n\t\t},\n\t\t{\n\t\t\tname:      \"completely invalid URL\",\n\t\t\tschemaURL: \"https://example.com/invalid/schema/url\",\n\t\t\texpected:  false,\n\t\t},\n\t}\n\n\tp := matchExclusionProcessor{}\n\n\tfor _, tt := range tc {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tassert.Equal(t, tt.expected, p.IsSupported(tt.schemaURL))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/db/processors/msrc_processor.go",
    "content": "//nolint:dupl\npackage processors\n\nimport (\n\t\"io\"\n\n\t\"github.com/anchore/grype/grype/db/data\"\n\t\"github.com/anchore/grype/grype/db/internal/provider/unmarshal\"\n\t\"github.com/anchore/grype/grype/db/provider\"\n\t\"github.com/anchore/grype/internal/log\"\n)\n\n// msrcProcessor defines the regular expression needed to signal what is supported\ntype msrcProcessor struct {\n\ttransformer any\n}\n\n// NewMSRCProcessor creates a new instance of msrcProcessor particular to MSRC\nfunc NewMSRCProcessor(transformer data.MSRCTransformer) data.Processor {\n\treturn &msrcProcessor{\n\t\ttransformer: transformer,\n\t}\n}\n\nfunc NewV2MSRCProcessor(transformer data.MSRCTransformerV2) data.Processor {\n\treturn &msrcProcessor{\n\t\ttransformer: transformer,\n\t}\n}\n\nfunc (p msrcProcessor) Process(reader io.Reader, state provider.State) ([]data.Entry, error) {\n\tvar results []data.Entry\n\n\tentries, err := unmarshal.MSRCVulnerabilityEntries(reader)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar handle func(entry unmarshal.MSRCVulnerability) ([]data.Entry, error)\n\tswitch t := p.transformer.(type) {\n\tcase data.MSRCTransformer:\n\t\thandle = func(entry unmarshal.MSRCVulnerability) ([]data.Entry, error) {\n\t\t\treturn t(entry)\n\t\t}\n\tcase data.MSRCTransformerV2:\n\t\thandle = func(entry unmarshal.MSRCVulnerability) ([]data.Entry, error) {\n\t\t\treturn t(entry, state)\n\t\t}\n\t}\n\n\tfor _, entry := range entries {\n\t\tif entry.IsEmpty() {\n\t\t\tlog.Warn(\"dropping empty MSRC entry\")\n\t\t\tcontinue\n\t\t}\n\n\t\ttransformedEntries, err := handle(entry)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tresults = append(results, transformedEntries...)\n\t}\n\n\treturn results, nil\n}\n\nfunc (p msrcProcessor) IsSupported(schemaURL string) bool {\n\tif !hasSchemaSegment(schemaURL, \"msrc\") {\n\t\treturn false\n\t}\n\n\tparsedVersion, err := parseVersion(schemaURL)\n\tif err != nil {\n\t\tlog.WithFields(\"schema\", schemaURL, \"error\", err).Error(\"failed to parse MSRC schema version\")\n\t\treturn false\n\t}\n\n\treturn parsedVersion.Major == 1\n}\n"
  },
  {
    "path": "grype/db/processors/msrc_processor_test.go",
    "content": "package processors\n\nimport (\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/grype/grype/db/data\"\n\t\"github.com/anchore/grype/grype/db/internal/provider/unmarshal\"\n\t\"github.com/anchore/grype/grype/db/internal/testutil\"\n\t\"github.com/anchore/grype/grype/db/provider\"\n)\n\nfunc mockMSRCProcessorTransform(vulnerability unmarshal.MSRCVulnerability) ([]data.Entry, error) {\n\treturn []data.Entry{\n\t\t{\n\t\t\tDBSchemaVersion: 0,\n\t\t\tData:            vulnerability,\n\t\t},\n\t}, nil\n}\n\nfunc TestMSRCProcessor_Process(t *testing.T) {\n\tf, err := os.Open(\"testdata/msrc.json\")\n\trequire.NoError(t, err)\n\tdefer testutil.CloseFile(f)\n\n\tprocessor := NewMSRCProcessor(mockMSRCProcessorTransform)\n\tentries, err := processor.Process(f, provider.State{\n\t\tProvider: \"msrc\",\n\t})\n\n\trequire.NoError(t, err)\n\tassert.Len(t, entries, 2)\n}\n\nfunc TestMsrcProcessor_IsSupported(t *testing.T) {\n\ttc := []struct {\n\t\tname      string\n\t\tschemaURL string\n\t\texpected  bool\n\t}{\n\t\t{\n\t\t\tname:      \"valid schema URL with version 1.0.0\",\n\t\t\tschemaURL: \"https://example.com/vunnel/path/vulnerability/msrc/schema-1.0.0.json\",\n\t\t\texpected:  true,\n\t\t},\n\t\t{\n\t\t\tname:      \"valid schema URL with version 1.2.3\",\n\t\t\tschemaURL: \"https://example.com/vunnel/path/vulnerability/msrc/schema-1.2.3.json\",\n\t\t\texpected:  true,\n\t\t},\n\t\t{\n\t\t\tname:      \"invalid schema URL with unsupported version\",\n\t\t\tschemaURL: \"https://example.com/vunnel/path/vulnerability/msrc/schema-2.0.0.json\",\n\t\t\texpected:  false,\n\t\t},\n\t\t{\n\t\t\tname:      \"invalid schema URL with missing version\",\n\t\t\tschemaURL: \"https://example.com/vunnel/path/vulnerability/msrc/schema.json\",\n\t\t\texpected:  false,\n\t\t},\n\t\t{\n\t\t\tname:      \"completely invalid URL\",\n\t\t\tschemaURL: \"https://example.com/invalid/schema/url\",\n\t\t\texpected:  false,\n\t\t},\n\t}\n\n\tp := msrcProcessor{}\n\n\tfor _, tt := range tc {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tassert.Equal(t, tt.expected, p.IsSupported(tt.schemaURL))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/db/processors/nvd_processor.go",
    "content": "package processors\n\nimport (\n\t\"io\"\n\n\t\"github.com/anchore/grype/grype/db/data\"\n\t\"github.com/anchore/grype/grype/db/internal/provider/unmarshal\"\n\t\"github.com/anchore/grype/grype/db/provider\"\n\t\"github.com/anchore/grype/internal/log\"\n)\n\ntype nvdProcessor struct {\n\ttransformer any\n}\n\nfunc NewNVDProcessor(transformer data.NVDTransformer) data.Processor {\n\treturn &nvdProcessor{\n\t\ttransformer: transformer,\n\t}\n}\n\nfunc NewV2NVDProcessor(transformer data.NVDTransformerV2) data.Processor {\n\treturn &nvdProcessor{\n\t\ttransformer: transformer,\n\t}\n}\n\nfunc (p nvdProcessor) Process(reader io.Reader, state provider.State) ([]data.Entry, error) {\n\tvar results []data.Entry\n\n\tentries, err := unmarshal.NvdVulnerabilityEntries(reader)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar handle func(entry unmarshal.NVDVulnerability) ([]data.Entry, error)\n\tswitch t := p.transformer.(type) {\n\tcase data.NVDTransformer:\n\t\thandle = func(entry unmarshal.NVDVulnerability) ([]data.Entry, error) {\n\t\t\treturn t(entry)\n\t\t}\n\tcase data.NVDTransformerV2:\n\t\thandle = func(entry unmarshal.NVDVulnerability) ([]data.Entry, error) {\n\t\t\treturn t(entry, state)\n\t\t}\n\t}\n\n\tfor _, entry := range entries {\n\t\tif entry.IsEmpty() {\n\t\t\tlog.Warn(\"dropping empty NVD entry\")\n\t\t\tcontinue\n\t\t}\n\n\t\ttransformedEntries, err := handle(entry.Cve)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tresults = append(results, transformedEntries...)\n\t}\n\n\treturn results, nil\n}\n\nfunc (p nvdProcessor) IsSupported(schemaURL string) bool {\n\tif !hasSchemaSegment(schemaURL, \"nvd\") {\n\t\treturn false\n\t}\n\n\tparsedVersion, err := parseVersion(schemaURL)\n\tif err != nil {\n\t\tlog.WithFields(\"schema\", schemaURL, \"error\", err).Error(\"failed to parse NVD schema version\")\n\t\treturn false\n\t}\n\n\treturn parsedVersion.Major == 1\n}\n"
  },
  {
    "path": "grype/db/processors/nvd_processor_test.go",
    "content": "package processors\n\nimport (\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/grype/grype/db/data\"\n\t\"github.com/anchore/grype/grype/db/internal/provider/unmarshal\"\n\t\"github.com/anchore/grype/grype/db/internal/testutil\"\n\t\"github.com/anchore/grype/grype/db/provider\"\n)\n\nfunc mockNVDProcessorTransform(vulnerability unmarshal.NVDVulnerability) ([]data.Entry, error) {\n\treturn []data.Entry{\n\t\t{\n\t\t\tDBSchemaVersion: 0,\n\t\t\tData:            vulnerability,\n\t\t},\n\t}, nil\n}\n\nfunc TestNVDProcessor_Process(t *testing.T) {\n\tf, err := os.Open(\"testdata/nvd.json\")\n\trequire.NoError(t, err)\n\tdefer testutil.CloseFile(f)\n\n\tprocessor := NewNVDProcessor(mockNVDProcessorTransform)\n\tentries, err := processor.Process(f, provider.State{\n\t\tProvider: \"nvd\",\n\t})\n\n\trequire.NoError(t, err)\n\tassert.Len(t, entries, 3)\n}\n\nfunc TestNvdProcessor_IsSupported(t *testing.T) {\n\ttc := []struct {\n\t\tname      string\n\t\tschemaURL string\n\t\texpected  bool\n\t}{\n\t\t{\n\t\t\tname:      \"valid schema URL with version 1.0.0\",\n\t\t\tschemaURL: \"https://example.com/vunnel/path/vulnerability/nvd/schema-1.0.0.json\",\n\t\t\texpected:  true,\n\t\t},\n\t\t{\n\t\t\tname:      \"valid schema URL with version 1.4.7\",\n\t\t\tschemaURL: \"https://example.com/vunnel/path/vulnerability/nvd/schema-1.4.7.json\",\n\t\t\texpected:  true,\n\t\t},\n\t\t{\n\t\t\tname:      \"invalid schema URL with unsupported version\",\n\t\t\tschemaURL: \"https://example.com/vunnel/path/vulnerability/nvd/schema-2.0.0.json\",\n\t\t\texpected:  false,\n\t\t},\n\t\t{\n\t\t\tname:      \"invalid schema URL with missing version\",\n\t\t\tschemaURL: \"https://example.com/vunnel/path/vulnerability/nvd/schema.json\",\n\t\t\texpected:  false,\n\t\t},\n\t\t{\n\t\t\tname:      \"completely invalid URL\",\n\t\t\tschemaURL: \"https://example.com/invalid/schema/url\",\n\t\t\texpected:  false,\n\t\t},\n\t}\n\n\tp := nvdProcessor{}\n\n\tfor _, tt := range tc {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tassert.Equal(t, tt.expected, p.IsSupported(tt.schemaURL))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/db/processors/openvex_processor.go",
    "content": "package processors\n\nimport (\n\t\"io\"\n\n\t\"github.com/anchore/grype/grype/db/data\"\n\t\"github.com/anchore/grype/grype/db/internal/provider/unmarshal\"\n\t\"github.com/anchore/grype/grype/db/provider\"\n\t\"github.com/anchore/grype/internal/log\"\n)\n\ntype openVEXProcessor struct {\n\ttransformer data.OpenVEXTransformerV2\n}\n\nfunc NewV2OpenVEXProcessor(transformer data.OpenVEXTransformerV2) data.Processor {\n\treturn &openVEXProcessor{\n\t\ttransformer: transformer,\n\t}\n}\n\nfunc (p openVEXProcessor) Process(reader io.Reader, state provider.State) ([]data.Entry, error) {\n\tvar results []data.Entry\n\n\tentries, err := unmarshal.OpenVEXVulnerabilityEntries(reader)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfor _, entry := range entries {\n\t\ttransformedEntries, err := p.transformer(entry, state)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tresults = append(results, transformedEntries...)\n\t}\n\n\treturn results, nil\n}\n\nfunc (p openVEXProcessor) IsSupported(schemaURL string) bool {\n\tif !hasSchemaSegment(schemaURL, \"openvex\") {\n\t\treturn false\n\t}\n\n\tparsedVersion, err := parseVersion(schemaURL)\n\tif err != nil {\n\t\tlog.WithFields(\"schema\", schemaURL, \"error\", err).Error(\"failed to parse OpenVEX schema version\")\n\t\treturn false\n\t}\n\n\t// OpenVEX at 0.2.X (https://github.com/openvex/spec/blob/main/OPENVEX-SPEC.md)\n\treturn parsedVersion.Major == 0 && parsedVersion.Minor >= 2\n}\n"
  },
  {
    "path": "grype/db/processors/openvex_processor_test.go",
    "content": "package processors\n\nimport (\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/grype/grype/db/data\"\n\t\"github.com/anchore/grype/grype/db/internal/provider/unmarshal\"\n\t\"github.com/anchore/grype/grype/db/internal/testutil\"\n\t\"github.com/anchore/grype/grype/db/provider\"\n)\n\nfunc mockOpenVEXProcessorTransform(vulnerability unmarshal.OpenVEXVulnerability, _ provider.State) ([]data.Entry, error) {\n\treturn []data.Entry{\n\t\t{\n\t\t\tDBSchemaVersion: 0,\n\t\t\tData:            vulnerability,\n\t\t},\n\t}, nil\n}\n\nfunc TestV2OpenVEXProcessor_Process(t *testing.T) {\n\tf, err := os.Open(\"testdata/openvex.json\")\n\trequire.NoError(t, err)\n\tdefer testutil.CloseFile(f)\n\n\tprocessor := NewV2OpenVEXProcessor(mockOpenVEXProcessorTransform)\n\tentries, err := processor.Process(f, provider.State{\n\t\tProvider: \"openvex\",\n\t})\n\n\trequire.NoError(t, err)\n\tassert.Len(t, entries, 1)\n}\n\nfunc TestOpenVEXProcessor_IsSupported(t *testing.T) {\n\ttests := []struct {\n\t\tname      string\n\t\tschemaURL string\n\t\twant      bool\n\t}{\n\t\t{\n\t\t\tname:      \"one actually used by vunnel is supported\",\n\t\t\tschemaURL: \"https://github.com/openvex/spec/openvex_json_schema_0.2.0.json\",\n\t\t\twant:      true,\n\t\t},\n\t\t{\n\t\t\tname:      \"openvex schema 0.2.1 is supported\",\n\t\t\tschemaURL: \"https://github.com/openvex/spec/openvex_json_schema_0.2.1.json\",\n\t\t\twant:      true,\n\t\t},\n\t\t{\n\t\t\tname:      \"openvex schema 0.3.1 is supported\",\n\t\t\tschemaURL: \"https://github.com/openvex/spec/openvex_json_schema_0.3.1.json\",\n\t\t\twant:      true,\n\t\t},\n\t\t{\n\t\t\tname:      \"openvex schema 0.1.1 is not supported\",\n\t\t\tschemaURL: \"https://github.com/openvex/spec/openvex_json_schema_0.1.1.json\",\n\t\t\twant:      false,\n\t\t},\n\t\t{\n\t\t\tname:      \"higher schema is not supported\",\n\t\t\tschemaURL: \"https://github.com/openvex/spec/openvex_json_schema_1.2.0.json\",\n\t\t\twant:      false,\n\t\t},\n\t\t{\n\t\t\tname:      \"non-openvex schema is not supported\",\n\t\t\tschemaURL: \"https://example.com/nvd/schema-1.4.0.json\",\n\t\t\twant:      false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tp := NewV2OpenVEXProcessor(mockOpenVEXProcessorTransform)\n\t\t\tassert.Equal(t, tt.want, p.IsSupported(tt.schemaURL))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/db/processors/os_processor.go",
    "content": "//nolint:dupl\npackage processors\n\nimport (\n\t\"io\"\n\n\t\"github.com/anchore/grype/grype/db/data\"\n\t\"github.com/anchore/grype/grype/db/internal/provider/unmarshal\"\n\t\"github.com/anchore/grype/grype/db/provider\"\n\t\"github.com/anchore/grype/internal/log\"\n)\n\ntype osProcessor struct {\n\ttransformer any\n}\n\nfunc NewOSProcessor(transformer data.OSTransformer) data.Processor {\n\treturn &osProcessor{\n\t\ttransformer: transformer,\n\t}\n}\n\nfunc NewV2OSProcessor(transformer data.OSTransformerV2) data.Processor {\n\treturn &osProcessor{\n\t\ttransformer: transformer,\n\t}\n}\n\nfunc (p osProcessor) Process(reader io.Reader, state provider.State) ([]data.Entry, error) {\n\tvar results []data.Entry\n\n\tentries, err := unmarshal.OSVulnerabilityEntries(reader)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar handle func(entry unmarshal.OSVulnerability) ([]data.Entry, error)\n\tswitch t := p.transformer.(type) {\n\tcase data.OSTransformer:\n\t\thandle = func(entry unmarshal.OSVulnerability) ([]data.Entry, error) {\n\t\t\treturn t(entry)\n\t\t}\n\tcase data.OSTransformerV2:\n\t\thandle = func(entry unmarshal.OSVulnerability) ([]data.Entry, error) {\n\t\t\treturn t(entry, state)\n\t\t}\n\t}\n\n\tfor _, entry := range entries {\n\t\tif entry.IsEmpty() {\n\t\t\tlog.Warn(\"dropping empty OS entry\")\n\t\t\tcontinue\n\t\t}\n\n\t\ttransformedEntries, err := handle(entry)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tresults = append(results, transformedEntries...)\n\t}\n\n\treturn results, nil\n}\n\nfunc (p osProcessor) IsSupported(schemaURL string) bool {\n\tif !hasSchemaSegment(schemaURL, \"os\") {\n\t\treturn false\n\t}\n\n\tparsedVersion, err := parseVersion(schemaURL)\n\tif err != nil {\n\t\tlog.WithFields(\"schema\", schemaURL, \"error\", err).Error(\"failed to parse OS schema version\")\n\t\treturn false\n\t}\n\n\treturn parsedVersion.Major == 1\n}\n"
  },
  {
    "path": "grype/db/processors/os_processor_test.go",
    "content": "package processors\n\nimport (\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/grype/grype/db/data\"\n\t\"github.com/anchore/grype/grype/db/internal/provider/unmarshal\"\n\t\"github.com/anchore/grype/grype/db/internal/testutil\"\n\t\"github.com/anchore/grype/grype/db/provider\"\n)\n\nfunc mockOSProcessorTransform(vulnerability unmarshal.OSVulnerability) ([]data.Entry, error) {\n\treturn []data.Entry{\n\t\t{\n\t\t\tDBSchemaVersion: 0,\n\t\t\tData:            vulnerability,\n\t\t},\n\t}, nil\n}\n\nfunc TestOSProcessor_Process(t *testing.T) {\n\tf, err := os.Open(\"testdata/os.json\")\n\trequire.NoError(t, err)\n\tdefer testutil.CloseFile(f)\n\n\tprocessor := NewOSProcessor(mockOSProcessorTransform)\n\tentries, err := processor.Process(f, provider.State{\n\t\tProvider: \"rhel\",\n\t})\n\n\trequire.NoError(t, err)\n\tassert.Len(t, entries, 4)\n}\n\nfunc TestOsProcessor_IsSupported(t *testing.T) {\n\ttc := []struct {\n\t\tname      string\n\t\tschemaURL string\n\t\texpected  bool\n\t}{\n\t\t{\n\t\t\tname:      \"valid schema URL with version 1.0.0\",\n\t\t\tschemaURL: \"https://example.com/vunnel/path/vulnerability/os/schema-1.0.0.json\",\n\t\t\texpected:  true,\n\t\t},\n\t\t{\n\t\t\tname:      \"valid schema URL with version 1.5.2\",\n\t\t\tschemaURL: \"https://example.com/vunnel/path/vulnerability/os/schema-1.5.2.json\",\n\t\t\texpected:  true,\n\t\t},\n\t\t{\n\t\t\tname:      \"invalid schema URL with unsupported version\",\n\t\t\tschemaURL: \"https://example.com/vunnel/path/vulnerability/os/schema-2.0.0.json\",\n\t\t\texpected:  false,\n\t\t},\n\t\t{\n\t\t\tname:      \"invalid schema URL with missing version\",\n\t\t\tschemaURL: \"https://example.com/vunnel/path/vulnerability/os/schema.json\",\n\t\t\texpected:  false,\n\t\t},\n\t\t{\n\t\t\tname:      \"completely invalid URL\",\n\t\t\tschemaURL: \"https://example.com/invalid/schema/url\",\n\t\t\texpected:  false,\n\t\t},\n\t}\n\n\tp := osProcessor{}\n\n\tfor _, tt := range tc {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tassert.Equal(t, tt.expected, p.IsSupported(tt.schemaURL))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/db/processors/osv_processor.go",
    "content": "package processors // nolint:dupl\n\nimport (\n\t\"io\"\n\n\t\"github.com/anchore/grype/grype/db/data\"\n\t\"github.com/anchore/grype/grype/db/internal/provider/unmarshal\"\n\t\"github.com/anchore/grype/grype/db/provider\"\n\t\"github.com/anchore/grype/internal/log\"\n)\n\ntype osvProcessor struct {\n\ttransformer data.OSVTransformerV2\n}\n\nfunc NewV2OSVProcessor(transformer data.OSVTransformerV2) data.Processor {\n\treturn &osvProcessor{\n\t\ttransformer: transformer,\n\t}\n}\n\nfunc (p osvProcessor) Process(reader io.Reader, state provider.State) ([]data.Entry, error) {\n\tvar results []data.Entry\n\n\tentries, err := unmarshal.OSVVulnerabilityEntries(reader)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfor _, entry := range entries {\n\t\ttransformedEntries, err := p.transformer(entry, state)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tresults = append(results, transformedEntries...)\n\t}\n\n\treturn results, nil\n}\n\nfunc (p osvProcessor) IsSupported(schemaURL string) bool {\n\tif !hasSchemaSegment(schemaURL, \"osv\") {\n\t\treturn false\n\t}\n\n\tparsedVersion, err := parseVersion(schemaURL)\n\tif err != nil {\n\t\tlog.WithFields(\"schema\", schemaURL, \"error\", err).Error(\"failed to parse NVD schema version\")\n\t\treturn false\n\t}\n\n\treturn parsedVersion.Major == 1\n}\n"
  },
  {
    "path": "grype/db/processors/osv_processor_test.go",
    "content": "package processors\n\nimport (\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/grype/grype/db/data\"\n\t\"github.com/anchore/grype/grype/db/internal/provider/unmarshal\"\n\t\"github.com/anchore/grype/grype/db/internal/testutil\"\n\t\"github.com/anchore/grype/grype/db/provider\"\n)\n\nfunc mockOSVProcessorTransform(vulnerability unmarshal.OSVVulnerability, state provider.State) ([]data.Entry, error) {\n\treturn []data.Entry{\n\t\t{\n\t\t\tDBSchemaVersion: 0,\n\t\t\tData:            vulnerability,\n\t\t},\n\t}, nil\n}\n\nfunc TestV2OSVProcessor_Process(t *testing.T) {\n\tf, err := os.Open(\"testdata/osv.json\")\n\trequire.NoError(t, err)\n\tdefer testutil.CloseFile(f)\n\n\tprocessor := NewV2OSVProcessor(mockOSVProcessorTransform)\n\tentries, err := processor.Process(f, provider.State{\n\t\tProvider: \"osv\",\n\t})\n\n\trequire.NoError(t, err)\n\tassert.Len(t, entries, 2)\n}\n\nfunc TestOSVProcessor_IsSupported(t *testing.T) {\n\ttests := []struct {\n\t\tname      string\n\t\tschemaURL string\n\t\twant      bool\n\t}{\n\t\t{\n\t\t\tname:      \"one actually used by vunnel is supported\",\n\t\t\tschemaURL: \"https://raw.githubusercontent.com/anchore/vunnel/main/schema/vulnerability/osv/schema-1.5.0.json\",\n\t\t\twant:      true,\n\t\t},\n\t\t{\n\t\t\tname:      \"osv schema 1.6.1 is supported\",\n\t\t\tschemaURL: \"https://example.com/osv/schema-1.6.1.json\",\n\t\t\twant:      true,\n\t\t},\n\t\t{\n\t\t\tname:      \"osv schema 1.5.0 is supported\",\n\t\t\tschemaURL: \"https://example.com/osv/schema-1.5.0.json\",\n\t\t\twant:      true,\n\t\t},\n\t\t{\n\t\t\tname:      \"lower major version is not supported\",\n\t\t\tschemaURL: \"https://example.com/osv/schema-0.4.0.json\",\n\t\t\twant:      false,\n\t\t},\n\t\t{\n\t\t\tname:      \"higher schema is not supported\",\n\t\t\tschemaURL: \"https://example.com/osv/schema-2.4.0.json\",\n\t\t\twant:      false,\n\t\t},\n\t\t{\n\t\t\tname:      \"non-osv schema is not supported\",\n\t\t\tschemaURL: \"https://example.com/nvd/schema-1.4.0.json\",\n\t\t\twant:      false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tp := NewV2OSVProcessor(mockOSVProcessorTransform)\n\t\t\tassert.Equal(t, tt.want, p.IsSupported(tt.schemaURL))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/db/processors/testdata/eol-with-empty.json",
    "content": "[\n  {\n    \"product\": \"\",\n    \"identifiers\": [],\n    \"name\": \"\",\n    \"isEol\": false,\n    \"isMaintained\": false\n  },\n  {\n    \"product\": \"alpine\",\n    \"identifiers\": [],\n    \"name\": \"3.18\",\n    \"codename\": null,\n    \"label\": \"3.18\",\n    \"releaseDate\": \"2023-05-09\",\n    \"isLts\": false,\n    \"ltsFrom\": null,\n    \"isEoas\": false,\n    \"eoasFrom\": null,\n    \"isEol\": false,\n    \"eolFrom\": \"2025-05-09\",\n    \"isMaintained\": true\n  }\n]\n"
  },
  {
    "path": "grype/db/processors/testdata/eol.json",
    "content": "[\n  {\n    \"product\": \"ubuntu\",\n    \"identifiers\": [],\n    \"name\": \"22.04\",\n    \"codename\": \"Jammy Jellyfish\",\n    \"label\": \"22.04 LTS (Jammy Jellyfish)\",\n    \"releaseDate\": \"2022-04-21\",\n    \"isLts\": true,\n    \"ltsFrom\": \"2022-04-21\",\n    \"isEoas\": false,\n    \"eoasFrom\": null,\n    \"isEol\": false,\n    \"eolFrom\": \"2027-04-01\",\n    \"isMaintained\": true\n  },\n  {\n    \"product\": \"debian\",\n    \"identifiers\": [],\n    \"name\": \"11\",\n    \"codename\": \"Bullseye\",\n    \"label\": \"11 (Bullseye)\",\n    \"releaseDate\": \"2021-08-14\",\n    \"isLts\": false,\n    \"ltsFrom\": null,\n    \"isEoas\": false,\n    \"eoasFrom\": null,\n    \"isEol\": false,\n    \"eolFrom\": \"2026-08-14\",\n    \"isMaintained\": true\n  }\n]\n"
  },
  {
    "path": "grype/db/processors/testdata/epss.json",
    "content": "[\n  {\n    \"cve\": \"CVE-2025-0108\",\n    \"epss\": 0.328,\n    \"percentile\": 0.9929,\n    \"date\": \"2025-02-18\"\n  },\n  {\n    \"cve\": \"CVE-2025-0109\",\n    \"epss\": 0.283,\n    \"percentile\": 0.9297,\n    \"date\": \"2025-02-18\"\n  }\n]"
  },
  {
    "path": "grype/db/processors/testdata/exclusions.json",
    "content": "[\n  {\n  },\n  {\n    \"id\": \"CVE-1234-5678\",\n    \"justification\": \"CVE-1234-5678 is imaginary\"\n  },\n  {\n    \"id\": \"CVE-2012-abcxyz\",\n    \"constraints\": [\n      {\n        \"namespaces\": [\n          \"nvd:cpe\",\n          \"abc.xyz:python\"\n        ]\n      }\n    ],\n    \"justification\": \"some reason\"\n  },\n  {\n    \"id\": \"CVE-2015-abc123\",\n    \"constraints\": [\n      {\n        \"ecosystem_constraints\": [\n          {\n            \"language\": \"python\",\n            \"package_constraints\": [\n              {\n                \"package_name\": \"clock\"\n              }\n            ]\n          }\n        ]\n      }\n    ],\n    \"justification\": \"\"\n  }\n]"
  },
  {
    "path": "grype/db/processors/testdata/github.json",
    "content": "[\n  {\n    \"Advisory\": {\n      \"Classification\": \"GENERAL\",\n      \"Severity\": \"Critical\",\n      \"CVSS\": {\n        \"version\": \"3.1\",\n        \"vector_string\": \"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H\",\n        \"base_metrics\": {\n          \"base_score\": 9.8,\n          \"exploitability_score\": 3.9,\n          \"impact_score\": 5.9,\n          \"base_severity\": \"Critical\"\n        },\n        \"status\": \"N/A\"\n      },\n      \"FixedIn\": [\n        {\n          \"name\": \"scratch-vm\",\n          \"identifier\": \"0.2.0-prerelease.20200714185213\",\n          \"ecosystem\": \"npm\",\n          \"namespace\": \"github:npm\",\n          \"range\": \"<= 0.2.0-prerelease.20200709173451\"\n        }\n      ],\n      \"Summary\": \"Remote Code Execution in scratch-vm\",\n      \"url\": \"https://github.com/advisories/GHSA-vc9j-fhvv-8vrf\",\n      \"CVE\": [\n        \"CVE-2020-14000\"\n      ],\n      \"Metadata\": {\n        \"CVE\": [\n          \"CVE-2020-14000\"\n        ]\n      },\n      \"ghsaId\": \"GHSA-vc9j-fhvv-8vrf\",\n      \"published\": \"2020-07-27T19:55:52Z\",\n      \"updated\": \"2023-01-09T05:03:39Z\",\n      \"withdrawn\": null,\n      \"namespace\": \"github:npm\"\n    },\n    \"Vulnerability\": {}\n  },\n  {\n    \"Advisory\": {\n      \"CVE\": [\n        \"CVE-2018-8768\"\n      ],\n      \"FixedIn\": [\n        {\n          \"ecosystem\": \"python\",\n          \"identifier\": \"5.4.1\",\n          \"name\": \"notebook\",\n          \"namespace\": \"github:python\",\n          \"range\": \"< 5.4.1\"\n        }\n      ],\n      \"Metadata\": {\n        \"CVE\": [\n          \"CVE-2018-8768\"\n        ]\n      },\n      \"Severity\": \"Low\",\n      \"Summary\": \"Low severity vulnerability that affects notebook\",\n      \"ghsaId\": \"GHSA-6cwv-x26c-w2q4\",\n      \"namespace\": \"github:python\",\n      \"url\": \"https://github.com/advisories/GHSA-6cwv-x26c-w2q4\",\n      \"withdrawn\": \"2022-01-31T14:32:09Z\"\n    },\n    \"Vulnerability\": {}\n  },\n  {\n    \"Advisory\": {\n      \"CVE\": [\n        \"CVE-2017-5524\"\n      ],\n      \"FixedIn\": [\n        {\n          \"ecosystem\": \"python\",\n          \"identifier\": \"4.3.12\",\n          \"name\": \"Plone\",\n          \"namespace\": \"github:python\",\n          \"range\": \">= 4.0 < 4.3.12\"\n        },\n        {\n          \"ecosystem\": \"python\",\n          \"identifier\": \"5.1b1\",\n          \"name\": \"Plone\",\n          \"namespace\": \"github:python\",\n          \"range\": \">= 5.1a1 < 5.1b1\"\n        },\n        {\n          \"ecosystem\": \"python\",\n          \"identifier\": \"5.0.7\",\n          \"name\": \"Plone-debug\",\n          \"namespace\": \"github:python\",\n          \"range\": \">= 5.0rc1 < 5.0.7\"\n        }\n      ],\n      \"Metadata\": {\n        \"CVE\": [\n          \"CVE-2017-5524\"\n        ]\n      },\n      \"Severity\": \"Medium\",\n      \"Summary\": \"Moderate severity vulnerability that affects Plone\",\n      \"ghsaId\": \"GHSA-p5wr-vp8g-q5p4\",\n      \"namespace\": \"github:python\",\n      \"url\": \"https://github.com/advisories/GHSA-p5wr-vp8g-q5p4\",\n      \"withdrawn\": null\n    },\n    \"Vulnerability\": {}\n  },\n  {\n    \"Advisory\": {\n      \"CVE\": [\n        \"CVE-2017-5524\"\n      ],\n      \"FixedIn\": [\n        {\n          \"ecosystem\": \"python\",\n          \"identifier\": \"4.3.12\",\n          \"name\": \"Plone\",\n          \"namespace\": \"github:python\",\n          \"range\": \">= 4.0 < 4.3.12\"\n        },\n        {\n          \"ecosystem\": \"python\",\n          \"identifier\": \"5.1b1\",\n          \"name\": \"Plone\",\n          \"namespace\": \"github:python\",\n          \"range\": \">= 5.1a1 < 5.1b1\"\n        },\n        {\n          \"ecosystem\": \"python\",\n          \"identifier\": \"5.0.7\",\n          \"name\": \"Plone-debug\",\n          \"namespace\": \"github:python\",\n          \"range\": \">= 5.0rc1 < 5.0.7\"\n        }\n      ],\n      \"Metadata\": {\n        \"CVE\": [\n          \"CVE-2017-5524\"\n        ]\n      },\n      \"Severity\": \"Medium\",\n      \"Summary\": \"Moderate severity vulnerability that affects Plone\",\n      \"ghsaId\": \"\",\n      \"namespace\": \"github:python\",\n      \"url\": \"https://github.com/advisories/GHSA-p5wr-vp8g-q5p4\",\n      \"withdrawn\": null\n    },\n    \"Vulnerability\": {}\n  },\n  {\n    \"Advisory\": {\n      \"CVE\": [\n        \"CVE-2017-5524\"\n      ],\n      \"FixedIn\": [\n        {\n          \"ecosystem\": \"python\",\n          \"identifier\": \"4.3.12\",\n          \"name\": \"Plone\",\n          \"namespace\": \"github:python\",\n          \"range\": \">= 4.0 < 4.3.12\"\n        },\n        {\n          \"ecosystem\": \"python\",\n          \"identifier\": \"5.1b1\",\n          \"name\": \"Plone\",\n          \"namespace\": \"github:python\",\n          \"range\": \">= 5.1a1 < 5.1b1\"\n        },\n        {\n          \"ecosystem\": \"python\",\n          \"identifier\": \"5.0.7\",\n          \"name\": \"Plone-debug\",\n          \"namespace\": \"github:python\",\n          \"range\": \">= 5.0rc1 < 5.0.7\"\n        }\n      ],\n      \"Metadata\": {\n        \"CVE\": [\n          \"CVE-2017-5524\"\n        ]\n      },\n      \"Severity\": \"Medium\",\n      \"Summary\": \"Moderate severity vulnerability that affects Plone\",\n      \"namespace\": \"github:python\",\n      \"url\": \"https://github.com/advisories/GHSA-p5wr-vp8g-q5p4\",\n      \"withdrawn\": null\n    },\n    \"Vulnerability\": {}\n  }\n]\n\n"
  },
  {
    "path": "grype/db/processors/testdata/kev.json",
    "content": "[\n  {\n    \"cveID\": \"CVE-2025-24989\",\n    \"vendorProject\": \"Microsoft\",\n    \"product\": \"Power Pages\",\n    \"vulnerabilityName\": \"Microsoft Power Pages Improper Access Control Vulnerability\",\n    \"dateAdded\": \"2025-02-21\",\n    \"shortDescription\": \"Microsoft Power Pages contains an improper access control vulnerability that allows an unauthorized attacker to elevate privileges over a network potentially bypassing the user registration control.\",\n    \"requiredAction\": \"Apply mitigations per vendor instructions, follow BOD 22-01 guidance for cloud services, or discontinue use of the product if mitigations are unavailable.\",\n    \"dueDate\": \"2025-03-14\",\n    \"knownRansomwareCampaignUse\": \"Unknown\",\n    \"notes\": \"https:\\/\\/msrc.microsoft.com\\/update-guide\\/en-US\\/advisory\\/CVE-2025-24989 ; https:\\/\\/nvd.nist.gov\\/vuln\\/detail\\/CVE-2025-24989\",\n    \"cwes\": [\n      \"CWE-284\"\n    ]\n  },\n  {\n    \"cveID\": \"CVE-2025-0111\",\n    \"vendorProject\": \"Palo Alto Networks\",\n    \"product\": \"PAN-OS\",\n    \"vulnerabilityName\": \"Palo Alto Networks PAN-OS File Read Vulnerability\",\n    \"dateAdded\": \"2025-02-20\",\n    \"shortDescription\": \"Palo Alto Networks PAN-OS contains an external control of file name or path vulnerability. Successful exploitation enables an authenticated attacker with network access to the management web interface to read files on the PAN-OS filesystem that are readable by the \\u201cnobody\\u201d user.\",\n    \"requiredAction\": \"Apply mitigations per vendor instructions or discontinue use of the product if mitigations are unavailable.\",\n    \"dueDate\": \"2025-03-13\",\n    \"knownRansomwareCampaignUse\": \"Unknown\",\n    \"notes\": \"https:\\/\\/security.paloaltonetworks.com\\/CVE-2025-0111 ; https:\\/\\/nvd.nist.gov\\/vuln\\/detail\\/CVE-2025-0111\",\n    \"cwes\": [\n      \"CWE-73\"\n    ]\n  },\n  {\n    \"cveID\": \"CVE-2025-23209\",\n    \"vendorProject\": \"Craft CMS\",\n    \"product\": \"Craft CMS\",\n    \"vulnerabilityName\": \"Craft CMS Code Injection Vulnerability\",\n    \"dateAdded\": \"2025-02-20\",\n    \"shortDescription\": \"Craft CMS contains a code injection vulnerability caused by improper validation of the database backup path, ultimately enabling remote code execution.\",\n    \"requiredAction\": \"Apply mitigations per vendor instructions or discontinue use of the product if mitigations are unavailable.\",\n    \"dueDate\": \"2025-03-13\",\n    \"knownRansomwareCampaignUse\": \"Unknown\",\n    \"notes\": \"https:\\/\\/github.com\\/craftcms\\/cms\\/security\\/advisories\\/GHSA-x684-96hh-833x ; https:\\/\\/nvd.nist.gov\\/vuln\\/detail\\/CVE-2025-23209\",\n    \"cwes\": [\n      \"CWE-94\"\n    ]\n  },\n  {\n    \"cveID\": \"CVE-2025-0108\",\n    \"vendorProject\": \"Palo Alto Networks\",\n    \"product\": \"PAN-OS\",\n    \"vulnerabilityName\": \"Palo Alto Networks PAN-OS Authentication Bypass Vulnerability\",\n    \"dateAdded\": \"2025-02-18\",\n    \"shortDescription\": \"Palo Alto Networks PAN-OS contains an authentication bypass vulnerability in its management web interface. This vulnerability allows an unauthenticated attacker with network access to the management web interface to bypass the authentication normally required and invoke certain PHP scripts.\",\n    \"requiredAction\": \"Apply mitigations per vendor instructions or discontinue use of the product if mitigations are unavailable.\",\n    \"dueDate\": \"2025-03-11\",\n    \"knownRansomwareCampaignUse\": \"Unknown\",\n    \"notes\": \"https:\\/\\/security.paloaltonetworks.com\\/CVE-2025-0108 ; https:\\/\\/nvd.nist.gov\\/vuln\\/detail\\/CVE-2025-0108\",\n    \"cwes\": [\n      \"CWE-306\"\n    ]\n  }\n]"
  },
  {
    "path": "grype/db/processors/testdata/msrc.json",
    "content": "[\n  {\n    \"cvss\": {\n      \"base_score\": 7.8,\n      \"temporal_score\": 7,\n      \"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\"\n    },\n    \"fixed_in\": [\n      {\n        \"id\": \"4493470\",\n        \"is_first\": true,\n        \"is_latest\": false,\n        \"links\": [\n          \"https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4493470\",\n          \"https://support.microsoft.com/help/4493470\"\n        ]\n      },\n      {\n        \"id\": \"4494440\",\n        \"is_first\": false,\n        \"is_latest\": false,\n        \"links\": [\n          \"https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4494440\",\n          \"https://support.microsoft.com/help/4494440\"\n        ]\n      },\n      {\n        \"id\": \"4503267\",\n        \"is_first\": false,\n        \"is_latest\": false,\n        \"links\": [\n          \"https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4503267\",\n          \"https://support.microsoft.com/en-us/help/4503267\"\n        ]\n      },\n      {\n        \"id\": \"4507460\",\n        \"is_first\": false,\n        \"is_latest\": false,\n        \"links\": [\n          \"https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4507460\",\n          \"https://support.microsoft.com/help/4507460\"\n        ]\n      },\n      {\n        \"id\": \"4512517\",\n        \"is_first\": false,\n        \"is_latest\": false,\n        \"links\": [\n          \"https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4512517\",\n          \"https://support.microsoft.com/help/4512517\"\n        ]\n      },\n      {\n        \"id\": \"4516044\",\n        \"is_first\": false,\n        \"is_latest\": true,\n        \"links\": [\n          \"https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4516044\",\n          \"https://support.microsoft.com/help/4516044\"\n        ]\n      }\n    ],\n    \"id\": \"CVE-2019-0671\",\n    \"link\": \"https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2019-0671\",\n    \"product\": {\n      \"family\": \"Windows\",\n      \"id\": \"10852\",\n      \"name\": \"Windows 10 Version 1607 for 32-bit Systems\"\n    },\n    \"severity\": \"High\",\n    \"summary\": \"Microsoft Office Access Connectivity Engine Remote Code Execution Vulnerability\",\n    \"vulnerable\": [\n      \"4480961\",\n      \"4483229\",\n      \"4487026\",\n      \"4489882\"\n    ]\n  },\n  {\n    \"cvss\": {\n      \"base_score\": 4.4,\n      \"temporal_score\": 4,\n      \"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\"\n    },\n    \"fixed_in\": [\n      {\n        \"id\": \"4093119\",\n        \"is_first\": true,\n        \"is_latest\": false,\n        \"links\": [\n          \"https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4093119\"\n        ]\n      },\n      {\n        \"id\": \"4103723\",\n        \"is_first\": false,\n        \"is_latest\": false,\n        \"links\": [\n          \"https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4103723\"\n        ]\n      },\n      {\n        \"id\": \"4284880\",\n        \"is_first\": false,\n        \"is_latest\": false,\n        \"links\": [\n          \"https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4284880\"\n        ]\n      },\n      {\n        \"id\": \"4338814\",\n        \"is_first\": false,\n        \"is_latest\": false,\n        \"links\": [\n          \"https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4338814\"\n        ]\n      },\n      {\n        \"id\": \"4343887\",\n        \"is_first\": false,\n        \"is_latest\": false,\n        \"links\": [\n          \"https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4343887\"\n        ]\n      },\n      {\n        \"id\": \"4345418\",\n        \"is_first\": false,\n        \"is_latest\": true,\n        \"links\": [\n          \"https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4345418\"\n        ]\n      },\n      {\n        \"id\": \"4457131\",\n        \"is_first\": false,\n        \"is_latest\": false,\n        \"links\": [\n          \"https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4457131\"\n        ]\n      },\n      {\n        \"id\": \"4462917\",\n        \"is_first\": false,\n        \"is_latest\": false,\n        \"links\": [\n          \"https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4462917\"\n        ]\n      },\n      {\n        \"id\": \"4467691\",\n        \"is_first\": false,\n        \"is_latest\": false,\n        \"links\": [\n          \"https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4467691\"\n        ]\n      },\n      {\n        \"id\": \"4471321\",\n        \"is_first\": false,\n        \"is_latest\": true,\n        \"links\": [\n          \"https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4471321\"\n        ]\n      }\n    ],\n    \"id\": \"CVE-2018-8116\",\n    \"link\": \"https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2018-8116\",\n    \"product\": {\n      \"family\": \"Windows\",\n      \"id\": \"10852\",\n      \"name\": \"Windows 10 Version 1607 for 32-bit Systems\"\n    },\n    \"severity\": \"Medium\",\n    \"summary\": \"Microsoft Graphics Component Denial of Service Vulnerability\",\n    \"vulnerable\": [\n      \"3213986\",\n      \"4013429\",\n      \"4015217\",\n      \"4019472\",\n      \"4022715\",\n      \"4025339\",\n      \"4034658\",\n      \"4038782\",\n      \"4041691\",\n      \"4048953\",\n      \"4053579\",\n      \"4056890\",\n      \"4074590\",\n      \"4088787\"\n    ]\n  },\n  {\n    \"cvss\": {\n      \"base_score\": 4.4,\n      \"temporal_score\": 4,\n      \"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\"\n    },\n    \"fixed_in\": [\n      {\n        \"id\": \"4093119\",\n        \"is_first\": true,\n        \"is_latest\": false,\n        \"links\": [\n          \"https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4093119\"\n        ]\n      },\n      {\n        \"id\": \"4103723\",\n        \"is_first\": false,\n        \"is_latest\": false,\n        \"links\": [\n          \"https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4103723\"\n        ]\n      },\n      {\n        \"id\": \"4284880\",\n        \"is_first\": false,\n        \"is_latest\": false,\n        \"links\": [\n          \"https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4284880\"\n        ]\n      },\n      {\n        \"id\": \"4338814\",\n        \"is_first\": false,\n        \"is_latest\": false,\n        \"links\": [\n          \"https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4338814\"\n        ]\n      },\n      {\n        \"id\": \"4343887\",\n        \"is_first\": false,\n        \"is_latest\": false,\n        \"links\": [\n          \"https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4343887\"\n        ]\n      },\n      {\n        \"id\": \"4345418\",\n        \"is_first\": false,\n        \"is_latest\": true,\n        \"links\": [\n          \"https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4345418\"\n        ]\n      },\n      {\n        \"id\": \"4457131\",\n        \"is_first\": false,\n        \"is_latest\": false,\n        \"links\": [\n          \"https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4457131\"\n        ]\n      },\n      {\n        \"id\": \"4462917\",\n        \"is_first\": false,\n        \"is_latest\": false,\n        \"links\": [\n          \"https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4462917\"\n        ]\n      },\n      {\n        \"id\": \"4467691\",\n        \"is_first\": false,\n        \"is_latest\": false,\n        \"links\": [\n          \"https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4467691\"\n        ]\n      },\n      {\n        \"id\": \"4471321\",\n        \"is_first\": false,\n        \"is_latest\": true,\n        \"links\": [\n          \"https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4471321\"\n        ]\n      }\n    ],\n    \"id\": \"\",\n    \"link\": \"https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2018-8116\",\n    \"product\": {\n      \"family\": \"Windows\",\n      \"id\": \"10852\",\n      \"name\": \"Windows 10 Version 1607 for 32-bit Systems\"\n    },\n    \"severity\": \"Medium\",\n    \"summary\": \"Microsoft Graphics Component Denial of Service Vulnerability\",\n    \"vulnerable\": [\n      \"3213986\",\n      \"4013429\",\n      \"4015217\",\n      \"4019472\",\n      \"4022715\",\n      \"4025339\",\n      \"4034658\",\n      \"4038782\",\n      \"4041691\",\n      \"4048953\",\n      \"4053579\",\n      \"4056890\",\n      \"4074590\",\n      \"4088787\"\n    ]\n  },\n  {\n    \"cvss\": {\n      \"base_score\": 4.4,\n      \"temporal_score\": 4,\n      \"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\"\n    },\n    \"fixed_in\": [\n      {\n        \"id\": \"4093119\",\n        \"is_first\": true,\n        \"is_latest\": false,\n        \"links\": [\n          \"https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4093119\"\n        ]\n      },\n      {\n        \"id\": \"4103723\",\n        \"is_first\": false,\n        \"is_latest\": false,\n        \"links\": [\n          \"https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4103723\"\n        ]\n      },\n      {\n        \"id\": \"4284880\",\n        \"is_first\": false,\n        \"is_latest\": false,\n        \"links\": [\n          \"https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4284880\"\n        ]\n      },\n      {\n        \"id\": \"4338814\",\n        \"is_first\": false,\n        \"is_latest\": false,\n        \"links\": [\n          \"https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4338814\"\n        ]\n      },\n      {\n        \"id\": \"4343887\",\n        \"is_first\": false,\n        \"is_latest\": false,\n        \"links\": [\n          \"https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4343887\"\n        ]\n      },\n      {\n        \"id\": \"4345418\",\n        \"is_first\": false,\n        \"is_latest\": true,\n        \"links\": [\n          \"https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4345418\"\n        ]\n      },\n      {\n        \"id\": \"4457131\",\n        \"is_first\": false,\n        \"is_latest\": false,\n        \"links\": [\n          \"https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4457131\"\n        ]\n      },\n      {\n        \"id\": \"4462917\",\n        \"is_first\": false,\n        \"is_latest\": false,\n        \"links\": [\n          \"https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4462917\"\n        ]\n      },\n      {\n        \"id\": \"4467691\",\n        \"is_first\": false,\n        \"is_latest\": false,\n        \"links\": [\n          \"https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4467691\"\n        ]\n      },\n      {\n        \"id\": \"4471321\",\n        \"is_first\": false,\n        \"is_latest\": true,\n        \"links\": [\n          \"https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4471321\"\n        ]\n      }\n    ],\n    \"link\": \"https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2018-8116\",\n    \"product\": {\n      \"family\": \"Windows\",\n      \"id\": \"10852\",\n      \"name\": \"Windows 10 Version 1607 for 32-bit Systems\"\n    },\n    \"severity\": \"Medium\",\n    \"summary\": \"Microsoft Graphics Component Denial of Service Vulnerability\",\n    \"vulnerable\": [\n      \"3213986\",\n      \"4013429\",\n      \"4015217\",\n      \"4019472\",\n      \"4022715\",\n      \"4025339\",\n      \"4034658\",\n      \"4038782\",\n      \"4041691\",\n      \"4048953\",\n      \"4053579\",\n      \"4056890\",\n      \"4074590\",\n      \"4088787\"\n    ]\n  }\n]\n"
  },
  {
    "path": "grype/db/processors/testdata/nvd.json",
    "content": "[\n  {\n    \"cve\": {\n      \"id\": \"CVE-1987-1111\",\n      \"sourceIdentifier\": \"cve@mitre.org\",\n      \"published\": \"2016-11-22T17:59:00.180\",\n      \"lastModified\": \"2016-11-28T19:50:59.600\",\n      \"vulnStatus\": \"Modified\",\n      \"descriptions\": [],\n      \"metrics\": {\n        \"cvssMetricV30\": [],\n        \"cvssMetricV2\": []\n      },\n      \"weaknesses\": [],\n      \"configurations\": [\n        {\n          \"nodes\": [\n            {\n              \"operator\": \"OR\",\n              \"negate\": false,\n              \"cpeMatch\": [\n                {\n                  \"vulnerable\": true,\n                  \"criteria\": \"cpe:2.3:a:soap\\\\:\\\\:lite_project:soap\\\\:\\\\:lite:*:*:*:*:*:perl:*:*\",\n                  \"versionEndIncluding\": \"1.14\",\n                  \"matchCriteriaId\": \"FB4DACB9-2E9E-4CBE-825F-FC0303D8CC86\"\n                }\n              ]\n            }\n          ]\n        }\n      ],\n      \"references\": []\n    }\n  },\n  {\n    \"cve\": {\n      \"id\": \"CVE-1987-2222\",\n      \"sourceIdentifier\": \"cve@mitre.org\",\n      \"published\": \"2016-11-22T17:59:00.180\",\n      \"lastModified\": \"2016-11-28T19:50:59.600\",\n      \"vulnStatus\": \"Modified\",\n      \"descriptions\": [],\n      \"metrics\": {\n        \"cvssMetricV30\": [],\n        \"cvssMetricV2\": []\n      },\n      \"weaknesses\": [],\n      \"configurations\": [\n        {\n          \"nodes\": [\n            {\n              \"operator\": \"OR\",\n              \"negate\": false,\n              \"cpeMatch\": [\n                {\n                  \"vulnerable\": true,\n                  \"criteria\": \"cpe:2.3:a:soap\\\\:\\\\:lite_project:soap\\\\:\\\\:lite:*:*:*:*:*:perl:*:*\",\n                  \"versionEndIncluding\": \"1.14\",\n                  \"matchCriteriaId\": \"FB4DACB9-2E9E-4CBE-825F-FC0303D8CC86\"\n                }\n              ]\n            }\n          ]\n        }\n      ],\n      \"references\": []\n    }\n  },\n  {\n    \"cve\": {\n      \"id\": \"CVE-1987-3333\",\n      \"sourceIdentifier\": \"cve@mitre.org\",\n      \"published\": \"2016-11-22T17:59:00.180\",\n      \"lastModified\": \"2016-11-28T19:50:59.600\",\n      \"vulnStatus\": \"Modified\",\n      \"descriptions\": [],\n      \"metrics\": {\n        \"cvssMetricV30\": [],\n        \"cvssMetricV2\": []\n      },\n      \"weaknesses\": [],\n      \"configurations\": [\n        {\n          \"nodes\": [\n            {\n              \"operator\": \"OR\",\n              \"negate\": false,\n              \"cpeMatch\": [\n                {\n                  \"vulnerable\": true,\n                  \"criteria\": \"cpe:2.3:a:soap\\\\:\\\\:lite_project:soap\\\\:\\\\:lite:*:*:*:*:*:perl:*:*\",\n                  \"versionEndIncluding\": \"1.14\",\n                  \"matchCriteriaId\": \"FB4DACB9-2E9E-4CBE-825F-FC0303D8CC86\"\n                }\n              ]\n            }\n          ]\n        }\n      ],\n      \"references\": []\n    }\n  },\n  {\n    \"cve\": {\n      \"id\": \"\",\n      \"sourceIdentifier\": \"^ note... there is no CVE ID in this test!!!\",\n      \"published\": \"2016-11-22T17:59:00.180\",\n      \"lastModified\": \"2016-11-28T19:50:59.600\",\n      \"vulnStatus\": \"Modified\",\n      \"descriptions\": [],\n      \"metrics\": {\n        \"cvssMetricV30\": [],\n        \"cvssMetricV2\": []\n      },\n      \"weaknesses\": [],\n      \"configurations\": [\n        {\n          \"nodes\": [\n            {\n              \"operator\": \"OR\",\n              \"negate\": false,\n              \"cpeMatch\": [\n                {\n                  \"vulnerable\": true,\n                  \"criteria\": \"cpe:2.3:a:soap\\\\:\\\\:lite_project:soap\\\\:\\\\:lite:*:*:*:*:*:perl:*:*\",\n                  \"versionEndIncluding\": \"1.14\",\n                  \"matchCriteriaId\": \"FB4DACB9-2E9E-4CBE-825F-FC0303D8CC86\"\n                }\n              ]\n            }\n          ]\n        }\n      ],\n      \"references\": []\n    }\n  }\n]"
  },
  {
    "path": "grype/db/processors/testdata/openvex.json",
    "content": "{\n  \"@context\": \"https://openvex.dev/ns/v0.2.0\",\n  \"@id\": \"https://openvex.dev/docs/public/vex-9f343308bfdc28ef92df6b952671127b25fbc1aa202e706467fee6ce7f9cb777\",\n  \"author\": \"Unknown Author\",\n  \"timestamp\": \"2025-08-01T15:13:37.914697-07:00\",\n  \"version\": 1,\n  \"statements\": [\n    {\n      \"vulnerability\": {\n        \"name\": \"CVE-2023-45803\"\n      },\n      \"timestamp\": \"2025-08-01T15:13:37.914697-07:00\",\n      \"products\": [\n        {\n          \"identifiers\": {\n            \"purl\": \"pkg:pypi/urllib3@1.26.16+cgr.1\"\n          }\n        }\n      ],\n      \"status\": \"fixed\"\n    }\n  ]\n}"
  },
  {
    "path": "grype/db/processors/testdata/oracle.json",
    "content": "[\n  {\n    \"Vulnerability\": {\n      \"CVSS\": [],\n      \"Description\": \"\",\n      \"FixedIn\": [\n        {\n          \"Name\": \"libexif\",\n          \"NamespaceName\": \"ol:8\",\n          \"Version\": \"0:0.6.21-17.el8_2\",\n          \"VersionFormat\": \"rpm\"\n        },\n        {\n          \"Name\": \"libexif-devel\",\n          \"NamespaceName\": \"ol:8\",\n          \"Version\": \"0:0.6.21-17.el8_2\",\n          \"VersionFormat\": \"rpm\"\n        },\n        {\n          \"Name\": \"libexif-dummy\",\n          \"NamespaceName\": \"ol:8\",\n          \"Version\": \"None\",\n          \"VersionFormat\": \"rpm\"\n        }\n      ],\n      \"Link\": \"http://linux.oracle.com/errata/ELSA-2020-2550.html\",\n      \"Metadata\": {\n        \"CVE\": [\n          {\n            \"Link\": \"http://linux.oracle.com/cve/CVE-2020-13112.html\",\n            \"Name\": \"CVE-2020-13112\"\n          }\n        ],\n        \"Issued\": \"2020-06-15\",\n        \"RefId\": \"ELSA-2020-2550\"\n      },\n      \"Name\": \"ELSA-2020-2550\",\n      \"NamespaceName\": \"ol:8\",\n      \"Severity\": \"Medium\"\n    }\n  },\n  {\n    \"Vulnerability\": {\n      \"CVSS\": [],\n      \"Description\": \"\",\n      \"FixedIn\": [\n        {\n          \"Name\": \"libexif\",\n          \"NamespaceName\": \"ol:8\",\n          \"Version\": \"0:0.6.21-17.el8_2\",\n          \"VersionFormat\": \"rpm\"\n        },\n        {\n          \"Name\": \"libexif-devel\",\n          \"NamespaceName\": \"ol:8\",\n          \"Version\": \"0:0.6.21-17.el8_2\",\n          \"VersionFormat\": \"rpm\"\n        },\n        {\n          \"Name\": \"libexif-dummy\",\n          \"NamespaceName\": \"ol:8\",\n          \"Version\": \"None\",\n          \"VersionFormat\": \"rpm\"\n        }\n      ],\n      \"Link\": \"http://linux.oracle.com/errata/ELSA-2020-2550.html\",\n      \"Metadata\": {\n        \"CVE\": [\n          {\n            \"Link\": \"http://linux.oracle.com/cve/CVE-2020-13112.html\",\n            \"Name\": \"CVE-2020-13112\"\n          }\n        ],\n        \"Issued\": \"2020-06-15\",\n        \"RefId\": \"ELSA-2020-2550\"\n      },\n      \"Name\": \"\",\n      \"NamespaceName\": \"ol:8\",\n      \"Severity\": \"Medium\"\n    }\n  },\n  {\n    \"Vulnerability\": {\n      \"CVSS\": [],\n      \"Description\": \"\",\n      \"FixedIn\": [\n        {\n          \"Name\": \"libexif\",\n          \"NamespaceName\": \"ol:8\",\n          \"Version\": \"0:0.6.21-17.el8_2\",\n          \"VersionFormat\": \"rpm\"\n        },\n        {\n          \"Name\": \"libexif-devel\",\n          \"NamespaceName\": \"ol:8\",\n          \"Version\": \"0:0.6.21-17.el8_2\",\n          \"VersionFormat\": \"rpm\"\n        },\n        {\n          \"Name\": \"libexif-dummy\",\n          \"NamespaceName\": \"ol:8\",\n          \"Version\": \"None\",\n          \"VersionFormat\": \"rpm\"\n        }\n      ],\n      \"Link\": \"http://linux.oracle.com/errata/ELSA-2020-2550.html\",\n      \"Metadata\": {\n        \"CVE\": [\n          {\n            \"Link\": \"http://linux.oracle.com/cve/CVE-2020-13112.html\",\n            \"Name\": \"CVE-2020-13112\"\n          }\n        ],\n        \"Issued\": \"2020-06-15\",\n        \"RefId\": \"ELSA-2020-2550\"\n      },\n      \"NamespaceName\": \"ol:8\",\n      \"Severity\": \"Medium\"\n    }\n  }\n]"
  },
  {
    "path": "grype/db/processors/testdata/os.json",
    "content": "[\n  {\n    \"Vulnerability\": {\n      \"CVSS\": [],\n      \"Description\": \"\",\n      \"FixedIn\": [\n        {\n          \"Name\": \"asterisk\",\n          \"NamespaceName\": \"debian:8\",\n          \"VendorAdvisory\": {\n            \"AdvisorySummary\": [],\n            \"NoAdvisory\": false\n          },\n          \"Version\": \"1:1.6.2.0~rc3-1\",\n          \"VersionFormat\": \"dpkg\"\n        },\n        {\n          \"Name\": \"auth2db\",\n          \"NamespaceName\": \"debian:8\",\n          \"VendorAdvisory\": {\n            \"AdvisorySummary\": [],\n            \"NoAdvisory\": false\n          },\n          \"Version\": \"0.2.5-2+dfsg-1\",\n          \"VersionFormat\": \"dpkg\"\n        },\n        {\n          \"Name\": \"exaile\",\n          \"NamespaceName\": \"debian:8\",\n          \"VendorAdvisory\": {\n            \"AdvisorySummary\": [],\n            \"NoAdvisory\": false\n          },\n          \"Version\": \"0.2.14+debian-2.2\",\n          \"VersionFormat\": \"dpkg\"\n        },\n        {\n          \"Name\": \"wordpress\",\n          \"NamespaceName\": \"debian:8\",\n          \"VendorAdvisory\": {\n            \"AdvisorySummary\": [],\n            \"NoAdvisory\": false\n          },\n          \"Version\": \"\",\n          \"VersionFormat\": \"dpkg\"\n        }\n      ],\n      \"Link\": \"https://security-tracker.debian.org/tracker/CVE-2008-7220\",\n      \"Metadata\": {\n        \"NVD\": {\n          \"CVSSv2\": {\n            \"Score\": 7.5,\n            \"Vectors\": \"AV:N/AC:L/Au:N/C:P/I:P/A:P\"\n          }\n        }\n      },\n      \"Name\": \"CVE-2008-7220\",\n      \"NamespaceName\": \"debian:8\",\n      \"Severity\": \"High\"\n    }\n  },\n  {\n    \"Vulnerability\": {\n      \"CVSS\": [],\n      \"Description\": \"\",\n      \"FixedIn\": [\n        {\n          \"Name\": \"rsyslog\",\n          \"NamespaceName\": \"debian:8\",\n          \"VendorAdvisory\": {\n            \"AdvisorySummary\": [],\n            \"NoAdvisory\": false\n          },\n          \"Version\": \"5.7.4-1\",\n          \"VersionFormat\": \"dpkg\"\n        }\n      ],\n      \"Link\": \"https://security-tracker.debian.org/tracker/CVE-2011-4623\",\n      \"Metadata\": {\n        \"NVD\": {\n          \"CVSSv2\": {\n            \"Score\": 2.1,\n            \"Vectors\": \"AV:L/AC:L/Au:N/C:N/I:N/A:P\"\n          }\n        }\n      },\n      \"Name\": \"CVE-2011-4623\",\n      \"NamespaceName\": \"debian:8\",\n      \"Severity\": \"Low\"\n    }\n  },\n  {\n    \"Vulnerability\": {\n      \"CVSS\": [],\n      \"Description\": \"\",\n      \"FixedIn\": [\n        {\n          \"Name\": \"rsyslog\",\n          \"NamespaceName\": \"debian:8\",\n          \"VendorAdvisory\": {\n            \"AdvisorySummary\": [],\n            \"NoAdvisory\": false\n          },\n          \"Version\": \"3.18.6-1\",\n          \"VersionFormat\": \"dpkg\"\n        }\n      ],\n      \"Link\": \"https://security-tracker.debian.org/tracker/CVE-2008-5618\",\n      \"Metadata\": {\n        \"NVD\": {\n          \"CVSSv2\": {\n            \"Score\": 5,\n            \"Vectors\": \"AV:N/AC:L/Au:N/C:N/I:N/A:P\"\n          }\n        }\n      },\n      \"Name\": \"CVE-2008-5618\",\n      \"NamespaceName\": \"debian:8\",\n      \"Severity\": \"Low\"\n    }\n  },\n  {\n    \"Vulnerability\": {\n      \"Description\": \"\",\n      \"FixedIn\": [\n        {\n          \"Name\": \"389-ds-base\",\n          \"NamespaceName\": \"amzn:2\",\n          \"Version\": \"1.3.8.4-15.amzn2.0.1\",\n          \"VersionFormat\": \"rpm\"\n        },\n        {\n          \"Name\": \"389-ds-base-debuginfo\",\n          \"NamespaceName\": \"amzn:2\",\n          \"Version\": \"1.3.8.4-15.amzn2.0.1\",\n          \"VersionFormat\": \"rpm\"\n        },\n        {\n          \"Name\": \"389-ds-base-devel\",\n          \"NamespaceName\": \"amzn:2\",\n          \"Version\": \"1.3.8.4-15.amzn2.0.1\",\n          \"VersionFormat\": \"rpm\"\n        },\n        {\n          \"Name\": \"389-ds-base-libs\",\n          \"NamespaceName\": \"amzn:2\",\n          \"Version\": \"1.3.8.4-15.amzn2.0.1\",\n          \"VersionFormat\": \"rpm\"\n        },\n        {\n          \"Name\": \"389-ds-base-snmp\",\n          \"NamespaceName\": \"amzn:2\",\n          \"Version\": \"1.3.8.4-15.amzn2.0.1\",\n          \"VersionFormat\": \"rpm\"\n        }\n      ],\n      \"Link\": \"https://alas.aws.amazon.com/AL2/ALAS-2018-1106.html\",\n      \"Metadata\": {\n        \"CVE\": [\n          {\"Name\": \"CVE-2018-14648\"}\n        ]\n      },\n      \"Name\": \"ALAS-2018-1106\",\n      \"NamespaceName\": \"amzn:2\",\n      \"Severity\": \"Medium\"\n    }\n  },\n  {\n    \"Vulnerability\": {\n      \"CVSS\": [],\n      \"Description\": \"\",\n      \"FixedIn\": [],\n      \"Link\": null,\n      \"Metadata\": {},\n      \"Name\": null,\n      \"NamespaceName\": null,\n      \"Severity\": null\n    }\n  },\n  {\n    \"Vulnerability\": {\n      \"CVSS\": [],\n      \"Description\": \"\",\n      \"FixedIn\": [],\n      \"Link\": null,\n      \"Metadata\": {},\n      \"Name\": \"\",\n      \"NamespaceName\": null,\n      \"Severity\": null\n    }\n  },\n  {\n    \"Vulnerability\": {\n      \"CVSS\": [],\n      \"Description\": \"\",\n      \"FixedIn\": [],\n      \"Link\": null,\n      \"Metadata\": {},\n      \"NamespaceName\": null,\n      \"Severity\": null\n    }\n  }\n]"
  },
  {
    "path": "grype/db/processors/testdata/osv.json",
    "content": "[\n  {\n    \"schema_version\": \"1.3.1\",\n    \"id\": \"GO-2023-2412\",\n    \"modified\": \"0001-01-01T00:00:00Z\",\n    \"published\": \"0001-01-01T00:00:00Z\",\n    \"aliases\": [\n      \"GHSA-7ww5-4wqc-m92c\"\n    ],\n    \"summary\": \"RAPL accessibility in github.com/containerd/containerd\",\n    \"details\": \"RAPL accessibility in github.com/containerd/containerd\",\n    \"affected\": [\n      {\n        \"package\": {\n          \"name\": \"github.com/containerd/containerd\",\n          \"ecosystem\": \"Go\"\n        },\n        \"ranges\": [\n          {\n            \"type\": \"SEMVER\",\n            \"events\": [\n              {\n                \"introduced\": \"0\"\n              },\n              {\n                \"fixed\": \"1.6.26\"\n              },\n              {\n                \"introduced\": \"1.7.0\"\n              },\n              {\n                \"fixed\": \"1.7.11\"\n              }\n            ]\n          }\n        ],\n        \"ecosystem_specific\": {\n          \"imports\": [\n            {\n              \"path\": \"github.com/containerd/containerd/contrib/apparmor\",\n              \"symbols\": [\n                \"DumpDefaultProfile\",\n                \"LoadDefaultProfile\",\n                \"generate\"\n              ]\n            }\n          ]\n        }\n      }\n    ],\n    \"references\": [\n      {\n        \"type\": \"ADVISORY\",\n        \"url\": \"https://github.com/containerd/containerd/security/advisories/GHSA-7ww5-4wqc-m92c\"\n      },\n      {\n        \"type\": \"FIX\",\n        \"url\": \"https://github.com/containerd/containerd/commit/67d356cb3095f3e8f8ad7d36f9a733fea1e7e28c\"\n      },\n      {\n        \"type\": \"FIX\",\n        \"url\": \"https://github.com/containerd/containerd/commit/746b910f05855c8bfdb4415a1c0f958b234910e5\"\n      }\n    ],\n    \"database_specific\": {\n      \"url\": \"https://pkg.go.dev/vuln/GO-2023-2412\"\n    }\n  },\n  {\n    \"schema_version\": \"1.3.1\",\n    \"id\": \"GO-2023-2413\",\n    \"modified\": \"0001-01-01T00:00:00Z\",\n    \"published\": \"0001-01-01T00:00:00Z\",\n    \"aliases\": [\n      \"CVE-2023-49922\",\n      \"GHSA-hj4r-2c9c-29h3\"\n    ],\n    \"summary\": \"Sensitive information logged in github.com/elastic/beats/v7\",\n    \"details\": \"Sensitive information logged in github.com/elastic/beats/v7\",\n    \"affected\": [\n      {\n        \"package\": {\n          \"name\": \"github.com/elastic/beats/v7\",\n          \"ecosystem\": \"Go\"\n        },\n        \"ranges\": [\n          {\n            \"type\": \"SEMVER\",\n            \"events\": [\n              {\n                \"introduced\": \"0\"\n              },\n              {\n                \"fixed\": \"7.17.16\"\n              }\n            ]\n          }\n        ],\n        \"ecosystem_specific\": {\n          \"imports\": [\n            {\n              \"path\": \"github.com/elastic/beats/v7/libbeat/processors/script/javascript\",\n              \"symbols\": [\n                \"jsProcessor.Run\",\n                \"session.runProcessFunc\"\n              ]\n            }\n          ]\n        }\n      }\n    ],\n    \"references\": [\n      {\n        \"type\": \"ADVISORY\",\n        \"url\": \"https://nvd.nist.gov/vuln/detail/CVE-2023-49922\"\n      },\n      {\n        \"type\": \"FIX\",\n        \"url\": \"https://github.com/elastic/beats/commit/9bd7de84ab9c31bb4e1c0a348a7b7c26817a0996\"\n      },\n      {\n        \"type\": \"WEB\",\n        \"url\": \"https://discuss.elastic.co/t/beats-and-elastic-agent-8-11-3-7-17-16-security-update-esa-2023-30/349180\"\n      }\n    ],\n    \"database_specific\": {\n      \"url\": \"https://pkg.go.dev/vuln/GO-2023-2413\"\n    }\n  }\n]\n"
  },
  {
    "path": "grype/db/processors/version.go",
    "content": "package processors\n\nimport (\n\t\"fmt\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n)\n\nvar schemaFilePattern = regexp.MustCompile(`schema([-_])(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)\\.json`)\n\ntype version struct {\n\tMajor int\n\tMinor int\n\tPatch int\n}\n\nfunc parseVersion(schemaURL string) (*version, error) {\n\tmatches := schemaFilePattern.FindStringSubmatch(schemaURL)\n\tif matches == nil {\n\t\treturn nil, fmt.Errorf(\"invalid version format in URL: %s\", schemaURL)\n\t}\n\n\tv := &version{}\n\tfor i, name := range schemaFilePattern.SubexpNames() {\n\t\tif name == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tvalue, err := strconv.Atoi(matches[i])\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to parse %s: %v\", name, err)\n\t\t}\n\t\tswitch name {\n\t\tcase \"major\":\n\t\t\tv.Major = value\n\t\tcase \"minor\":\n\t\t\tv.Minor = value\n\t\tcase \"patch\":\n\t\t\tv.Patch = value\n\t\t}\n\t}\n\n\treturn v, nil\n}\n\nfunc hasSchemaSegment(schemaURL string, segment string) bool {\n\treturn strings.Contains(schemaURL, \"/\"+segment+\"/\")\n}\n"
  },
  {
    "path": "grype/db/processors/version_test.go",
    "content": "package processors\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestParseVersion(t *testing.T) {\n\ttests := []struct {\n\t\tname      string\n\t\tschemaURL string\n\t\texpected  *version\n\t\twantErr   bool\n\t}{\n\t\t{\n\t\t\tname:      \"valid version 1.0.0\",\n\t\t\tschemaURL: \"https://example.com/vunnel/path/schema-1.0.0.json\",\n\t\t\texpected:  &version{Major: 1, Minor: 0, Patch: 0},\n\t\t\twantErr:   false,\n\t\t},\n\t\t{\n\t\t\tname:      \"valid version 2.3.4\",\n\t\t\tschemaURL: \"https://example.com/vunnel/path/schema-2.3.4.json\",\n\t\t\texpected:  &version{Major: 2, Minor: 3, Patch: 4},\n\t\t\twantErr:   false,\n\t\t},\n\t\t{\n\t\t\tname:      \"missing patch version\",\n\t\t\tschemaURL: \"https://example.com/vunnel/path/schema-1.0.json\",\n\t\t\texpected:  nil,\n\t\t\twantErr:   true,\n\t\t},\n\t\t{\n\t\t\tname:      \"invalid format\",\n\t\t\tschemaURL: \"https://example.com/vunnel/path/schema.json\",\n\t\t\texpected:  nil,\n\t\t\twantErr:   true,\n\t\t},\n\t\t{\n\t\t\tname:      \"non-numeric version\",\n\t\t\tschemaURL: \"https://example.com/vunnel/path/schema-1.a.0.json\",\n\t\t\texpected:  nil,\n\t\t\twantErr:   true,\n\t\t},\n\t\t{\n\t\t\tname:      \"valid version with extra path\",\n\t\t\tschemaURL: \"https://example.com/vunnel/path/vulnerability/schema_1.2.3.json\",\n\t\t\texpected:  &version{Major: 1, Minor: 2, Patch: 3},\n\t\t\twantErr:   false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult, err := parseVersion(tt.schemaURL)\n\t\t\tif tt.wantErr {\n\t\t\t\tassert.Error(t, err)\n\t\t\t\tassert.Nil(t, result)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.Equal(t, tt.expected, result)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/db/provider/entry/file.go",
    "content": "package entry\n\nimport (\n\t\"io\"\n\t\"os\"\n)\n\ntype fileOpener struct {\n\tpath string\n}\n\nfunc fileOpeners(resultPaths []string) <-chan Opener {\n\topeners := make(chan Opener)\n\tgo func() {\n\t\tdefer close(openers)\n\t\tfor _, p := range resultPaths {\n\t\t\topeners <- fileOpener{path: p}\n\t\t}\n\t}()\n\treturn openers\n}\n\nfunc (e fileOpener) Open() (io.ReadCloser, error) {\n\treturn os.Open(e.path)\n}\n\nfunc (e fileOpener) String() string {\n\treturn e.path\n}\n"
  },
  {
    "path": "grype/db/provider/entry/opener.go",
    "content": "package entry\n\nimport (\n\t\"fmt\"\n\t\"io\"\n)\n\ntype Opener interface {\n\tOpen() (io.ReadCloser, error)\n\tfmt.Stringer\n}\n\nfunc Openers(store string, resultPaths []string) (<-chan Opener, int64, error) {\n\tswitch store {\n\tcase \"flat-file\":\n\t\treturn fileOpeners(resultPaths), int64(len(resultPaths)), nil\n\tcase \"sqlite\":\n\t\treturn sqliteOpeners(resultPaths)\n\t}\n\treturn nil, 0, fmt.Errorf(\"unknown store: %q\", store)\n}\n\nfunc Count(store string, resultPaths []string) (int64, error) {\n\tswitch store {\n\tcase \"flat-file\":\n\t\treturn int64(len(resultPaths)), nil\n\tcase \"sqlite\":\n\t\treturn sqliteEntryCount(resultPaths)\n\t}\n\treturn 0, fmt.Errorf(\"unknown store: %q\", store)\n}\n"
  },
  {
    "path": "grype/db/provider/entry/sqlite.go",
    "content": "package entry\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/glebarez/sqlite\"\n\t\"gorm.io/gorm\"\n\t\"gorm.io/gorm/logger\"\n\n\t\"github.com/anchore/grype/internal/log\"\n)\n\nvar readOptions = []string{\n\t\"immutable=1\",\n\t\"cache=shared\",\n\t\"mode=ro\",\n}\n\n// note: the name of the struct is tied to the table name\ntype results struct {\n\tID     string `gorm:\"column:id\"`\n\tRecord []byte `gorm:\"column:record\"`\n}\n\ntype bytesOpener struct {\n\tcontents []byte\n\tname     string\n}\n\ntype errorOpener struct {\n\terr error\n}\n\nfunc sqliteEntryCount(resultPaths []string) (int64, error) {\n\tvar dbPath string\n\tfor _, p := range resultPaths {\n\t\t// we should only be validating against a single results DB, not any DB in the output\n\t\tif strings.HasSuffix(p, \"results.db\") {\n\t\t\tdbPath = p\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif dbPath == \"\" {\n\t\treturn 0, fmt.Errorf(\"unable to find DB result file\")\n\t}\n\n\tdb, err := openDB(dbPath)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\tvar count int64\n\tdb.Model(&results{}).Count(&count)\n\n\treturn count, nil\n}\n\nfunc sqliteOpeners(resultPaths []string) (<-chan Opener, int64, error) {\n\tvar dbPath string\n\tfor _, p := range resultPaths {\n\t\tif strings.HasSuffix(p, \"results.db\") {\n\t\t\tdbPath = p\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif dbPath == \"\" {\n\t\treturn nil, 0, fmt.Errorf(\"unable to find DB result file\")\n\t}\n\n\tdb, err := openDB(dbPath)\n\tif err != nil {\n\t\treturn nil, 0, err\n\t}\n\n\tvar count int64\n\tdb.Model(&results{}).Count(&count)\n\n\topeners := make(chan Opener)\n\tgo func() {\n\t\tdefer close(openers)\n\n\t\tvar models []results\n\n\t\tcurrent := 0\n\t\tcheck := db.FindInBatches(&models, 100, func(_ *gorm.DB, _ int) error {\n\t\t\tfor _, result := range models {\n\t\t\t\topeners <- bytesOpener{\n\t\t\t\t\tcontents: result.Record,\n\t\t\t\t\tname:     result.ID,\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tcurrent += len(models)\n\n\t\t\t// log.WithFields(\"count\", current).Trace(\"records read from the provider cache DB\")\n\n\t\t\t// note: returning an error will stop future batches\n\t\t\treturn nil\n\t\t})\n\n\t\tif check.Error != nil {\n\t\t\topeners <- errorOpener{err: check.Error}\n\t\t}\n\t}()\n\treturn openers, count, nil\n}\n\nfunc (e bytesOpener) Open() (io.ReadCloser, error) {\n\treturn io.NopCloser(bytes.NewReader(e.contents)), nil\n}\n\nfunc (e bytesOpener) String() string {\n\treturn e.name\n}\n\nfunc (e errorOpener) Open() (io.ReadCloser, error) {\n\treturn nil, e.err\n}\n\nfunc (e errorOpener) String() string {\n\treturn e.err.Error()\n}\n\n// Open a new connection to a sqlite3 database file\nfunc openDB(path string) (*gorm.DB, error) {\n\tconnStr, err := connectionString(path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// &immutable=1&cache=shared&mode=ro\n\tfor _, o := range readOptions {\n\t\tconnStr += fmt.Sprintf(\"&%s\", o)\n\t}\n\n\tdbObj, err := gorm.Open(sqlite.Open(connStr), &gorm.Config{Logger: newLogger()})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to connect to DB: %w\", err)\n\t}\n\n\treturn dbObj, nil\n}\n\n// ConnectionString creates a connection string for sqlite3\nfunc connectionString(path string) (string, error) {\n\tif path == \"\" {\n\t\treturn \"\", fmt.Errorf(\"no db filepath given\")\n\t}\n\treturn fmt.Sprintf(\"file:%s?cache=shared\", path), nil\n}\n\ntype logAdapter struct{}\n\nfunc newLogger() logger.Interface {\n\treturn logAdapter{}\n}\n\nfunc (l logAdapter) LogMode(logger.LogLevel) logger.Interface {\n\treturn l\n}\n\nfunc (l logAdapter) Info(_ context.Context, _ string, _ ...interface{}) {\n\t// unimplemented\n}\n\nfunc (l logAdapter) Warn(_ context.Context, fmt string, v ...interface{}) {\n\tlog.Warnf(\"gorm: \"+fmt, v...)\n}\n\nfunc (l logAdapter) Error(_ context.Context, fmt string, v ...interface{}) {\n\tlog.Errorf(\"gorm: \"+fmt, v...)\n}\n\nfunc (l logAdapter) Trace(_ context.Context, _ time.Time, _ func() (_ string, _ int64), _ error) {\n\t// unimplemented\n}\n"
  },
  {
    "path": "grype/db/provider/file.go",
    "content": "package provider\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/OneOfOne/xxhash\"\n\t\"github.com/spf13/afero\"\n\n\t\"github.com/anchore/grype/internal/file\"\n)\n\ntype File struct {\n\tPath      string `json:\"path\"`\n\tDigest    string `json:\"digest\"`\n\tAlgorithm string `json:\"algorithm\"`\n}\n\ntype Files []File\n\nfunc NewFile(path string) (*File, error) {\n\tdigest, err := file.HashFile(afero.NewOsFs(), path, xxhash.New64())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &File{\n\t\tPath:      path,\n\t\tDigest:    digest,\n\t\tAlgorithm: \"xxh64\",\n\t}, nil\n}\n\nfunc NewFiles(paths ...string) (Files, error) {\n\tvar files []File\n\tfor _, path := range paths {\n\t\tinput, err := NewFile(path)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tfiles = append(files, *input)\n\t}\n\treturn files, nil\n}\n\nfunc (i Files) Paths() []string {\n\tvar paths []string\n\tfor _, input := range i {\n\t\tpaths = append(paths, input.Path)\n\t}\n\treturn paths\n}\n\nfunc NewFilesFromDir(dir string) (Files, error) {\n\tlisting, err := os.ReadDir(dir)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar paths []string\n\tfor _, f := range listing {\n\t\tif f.IsDir() {\n\t\t\tcontinue\n\t\t}\n\t\tpaths = append(paths, filepath.Join(dir, f.Name()))\n\t}\n\n\treturn NewFiles(paths...)\n}\n"
  },
  {
    "path": "grype/db/provider/model.go",
    "content": "package provider\n\nimport (\n\t\"fmt\"\n\n\tdb \"github.com/anchore/grype/grype/db/v6\"\n)\n\nfunc Model(state State) *db.Provider {\n\tvar digest string\n\tif state.Listing != nil {\n\t\tif state.Listing.Algorithm != \"\" && state.Listing.Digest != \"\" {\n\t\t\tdigest = state.Listing.Algorithm + \":\" + state.Listing.Digest\n\t\t}\n\t}\n\treturn &db.Provider{\n\t\tID:           state.Provider,\n\t\tVersion:      fmt.Sprintf(\"%d\", state.Version),\n\t\tProcessor:    state.Processor,\n\t\tDateCaptured: &state.Timestamp,\n\t\tInputDigest:  digest,\n\t}\n}\n"
  },
  {
    "path": "grype/db/provider/model_test.go",
    "content": "package provider\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/require\"\n\n\tdb \"github.com/anchore/grype/grype/db/v6\"\n)\n\nfunc TestProviderModel(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tstate    State\n\t\texpected *db.Provider\n\t}{\n\t\t{\n\t\t\tname: \"valid state with listing\",\n\t\t\tstate: State{\n\t\t\t\tProvider:  \"test-provider\",\n\t\t\t\tVersion:   2,\n\t\t\t\tProcessor: \"test-processor\",\n\t\t\t\tTimestamp: time.Date(2024, 11, 15, 12, 34, 56, 0, time.UTC),\n\t\t\t\tListing: &File{\n\t\t\t\t\tAlgorithm: \"sha256\",\n\t\t\t\t\tDigest:    \"abc123\",\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: &db.Provider{\n\t\t\t\tID:           \"test-provider\",\n\t\t\t\tVersion:      \"2\",\n\t\t\t\tProcessor:    \"test-processor\",\n\t\t\t\tDateCaptured: func() *time.Time { t := time.Date(2024, 11, 15, 12, 34, 56, 0, time.UTC); return &t }(),\n\t\t\t\tInputDigest:  \"sha256:abc123\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"valid state without listing\",\n\t\t\tstate: State{\n\t\t\t\tProvider:  \"test-provider\",\n\t\t\t\tVersion:   1,\n\t\t\t\tProcessor: \"test-processor\",\n\t\t\t\tTimestamp: time.Date(2024, 11, 15, 12, 34, 56, 0, time.UTC),\n\t\t\t\tListing:   nil,\n\t\t\t},\n\t\t\texpected: &db.Provider{\n\t\t\t\tID:           \"test-provider\",\n\t\t\t\tVersion:      \"1\",\n\t\t\t\tProcessor:    \"test-processor\",\n\t\t\t\tDateCaptured: func() *time.Time { t := time.Date(2024, 11, 15, 12, 34, 56, 0, time.UTC); return &t }(),\n\t\t\t\tInputDigest:  \"\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"valid state with empty listing fields\",\n\t\t\tstate: State{\n\t\t\t\tProvider:  \"test-provider\",\n\t\t\t\tVersion:   3,\n\t\t\t\tProcessor: \"test-processor\",\n\t\t\t\tTimestamp: time.Date(2024, 11, 15, 12, 34, 56, 0, time.UTC),\n\t\t\t\tListing: &File{\n\t\t\t\t\tAlgorithm: \"\",\n\t\t\t\t\tDigest:    \"\",\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: &db.Provider{\n\t\t\t\tID:           \"test-provider\",\n\t\t\t\tVersion:      \"3\",\n\t\t\t\tProcessor:    \"test-processor\",\n\t\t\t\tDateCaptured: func() *time.Time { t := time.Date(2024, 11, 15, 12, 34, 56, 0, time.UTC); return &t }(),\n\t\t\t\tInputDigest:  \"\",\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := Model(tt.state)\n\t\t\trequire.Equal(t, tt.expected, result)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/db/provider/provider.go",
    "content": "package provider\n\nimport (\n\t\"context\"\n)\n\ntype Kind string\n\ntype Reader interface {\n\tID() Identifier\n\tState() (*State, error)\n}\n\ntype Writer interface {\n\tUpdate(context.Context) error\n}\n\ntype Identifier struct {\n\tName string `yaml:\"name\" json:\"name\" mapstructure:\"name\"`\n\tKind Kind   `yaml:\"kind,omitempty\" json:\"kind\" mapstructure:\"kind\"`\n}\n\ntype Providers []Reader\n\nfunc (ps Providers) Filter(names ...string) Providers {\n\tvar filtered Providers\n\tfor _, p := range ps {\n\t\tfor _, name := range names {\n\t\t\tif p.ID().Name == name {\n\t\t\t\tfiltered = append(filtered, p)\n\t\t\t}\n\t\t}\n\t}\n\treturn filtered\n}\n\ntype Collection struct {\n\tRoot      string\n\tProviders Providers\n}\n"
  },
  {
    "path": "grype/db/provider/state.go",
    "content": "package provider\n\nimport (\n\t\"bufio\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/spf13/afero\"\n\n\t\"github.com/anchore/grype/internal/file\"\n\t\"github.com/anchore/grype/internal/log\"\n)\n\n// data shape dictated by vunnel \"provider workspace state\" schema definition\n\ntype State struct {\n\tlocation            string\n\troot                string\n\tProvider            string    `json:\"provider\"`\n\tVersion             int       `json:\"version\"`\n\tDistributionVersion int       `json:\"distribution_version\"`\n\tProcessor           string    `json:\"processor\"`\n\tSchema              Schema    `json:\"schema\"`\n\tURLs                []string  `json:\"urls\"`\n\tTimestamp           time.Time `json:\"timestamp\"`\n\tListing             *File     `json:\"listing\"`\n\tStore               string    `json:\"store\"`\n\tStale               bool      `json:\"stale\"`\n\tresultFileStates    []File\n}\n\ntype Schema struct {\n\tVersion string `json:\"version\"`\n\tURL     string `json:\"url\"`\n}\n\ntype States []State\n\nfunc ReadState(location string) (*State, error) {\n\tby, err := os.ReadFile(location)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar sd State\n\tif err := json.Unmarshal(by, &sd); err != nil {\n\t\treturn nil, err\n\t}\n\n\troot := filepath.Dir(location)\n\tsd.root = root\n\tsd.location = location\n\t// we usually have a lot of records (depending on the source)\n\tsd.resultFileStates = make([]File, 0, 300000)\n\n\tstart := time.Now()\n\tif sd.Listing != nil {\n\t\talgorithm := \"xxh64\" // sane default for performance\n\n\t\t// get extension from listing file\n\t\textension := filepath.Ext(sd.Listing.Path)\n\t\tif extension != \"\" {\n\t\t\talgorithm = strings.TrimPrefix(extension, \".\")\n\t\t}\n\n\t\tlistingPath := filepath.Join(root, sd.Listing.Path)\n\t\tf, err := os.Open(listingPath)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"unable to open listing file %q: %w\", listingPath, err)\n\t\t}\n\n\t\t// note: bufio scanner is **much** faster than Fscan\n\t\tscanner := bufio.NewScanner(f)\n\t\tfor scanner.Scan() {\n\t\t\tline := scanner.Text()\n\t\t\tindex := strings.Index(line, \"  \") // faster than strings.Split\n\t\t\tif index != -1 {\n\t\t\t\tsd.resultFileStates = append(sd.resultFileStates,\n\t\t\t\t\tFile{\n\t\t\t\t\t\tPath:      line[index+2:],\n\t\t\t\t\t\tDigest:    line[:index],\n\t\t\t\t\t\tAlgorithm: algorithm,\n\t\t\t\t\t},\n\t\t\t\t)\n\t\t\t}\n\t\t}\n\t}\n\n\tlog.WithFields(\"duration\", time.Since(start), \"entries\", len(sd.resultFileStates)).Trace(\"loaded result listing file\")\n\n\treturn &sd, nil\n}\n\nfunc (sd State) ResultPath(filename string) string {\n\treturn filepath.Join(sd.root, filename)\n}\n\nfunc (sd State) ResultPaths() []string {\n\tvar paths []string\n\tfor _, p := range sd.resultFileStates {\n\t\tpaths = append(paths, sd.ResultPath(p.Path))\n\t}\n\treturn paths\n}\n\nfunc (sd State) Verify(workspaceRoots ...string) error {\n\tif sd.root != \"\" {\n\t\tworkspaceRoots = append(workspaceRoots, sd.root)\n\t}\n\tfor _, workspaceRoot := range workspaceRoots {\n\t\tfor _, resultConfig := range sd.resultFileStates {\n\t\t\tworkspace := NewWorkspaceFromExisting(workspaceRoot)\n\t\t\tpath := filepath.Join(workspace.Path(), resultConfig.Path)\n\n\t\t\tlog.WithFields(\"path\", resultConfig.Path, \"provider\", sd.Provider).Trace(\"validating result file\")\n\n\t\t\tmatches, _, err := file.ValidateByHash(afero.NewOsFs(), path, resultConfig.Algorithm+\":\"+resultConfig.Digest)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"unable to validate result file %q: %w\", path, err)\n\t\t\t}\n\t\t\tif !matches {\n\t\t\t\treturn fmt.Errorf(\"hash mismatch for result file %q\", path)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (s States) Names() []string {\n\tvar names []string\n\tfor _, state := range s {\n\t\tnames = append(names, state.Provider)\n\t}\n\treturn names\n}\n\nfunc (s States) EarliestTimestamp() (time.Time, error) {\n\tif len(s) == 0 {\n\t\treturn time.Time{}, fmt.Errorf(\"cannot find earliest timestamp: no states provided\")\n\t}\n\n\t// special case when there is exactly 1 state, return its timestamp even\n\t// if it is nvd, because otherwise quality gates that pull only nvd deterministically fail.\n\tif len(s) == 1 {\n\t\treturn s[0].Timestamp, nil\n\t}\n\n\tvar earliest time.Time\n\tfor _, curState := range s {\n\t\t// the NVD api is constantly down, so we don't want to consider it for the earliest timestamp\n\t\tif curState.Provider == \"nvd\" {\n\t\t\tlog.WithFields(\"provider\", curState.Provider).Debug(\"not considering data age for provider\")\n\t\t\tcontinue\n\t\t}\n\t\tif earliest.IsZero() {\n\t\t\tearliest = curState.Timestamp\n\t\t\tcontinue\n\t\t}\n\t\tif curState.Timestamp.Before(earliest) {\n\t\t\tearliest = curState.Timestamp\n\t\t}\n\t}\n\n\tif earliest.IsZero() {\n\t\treturn time.Time{}, fmt.Errorf(\"unable to determine earliest timestamp\")\n\t}\n\n\tlog.WithFields(\"timestamp\", earliest).Debug(\"earliest data timestamp\")\n\treturn earliest, nil\n}\n"
  },
  {
    "path": "grype/db/provider/state_test.go",
    "content": "package provider\n\nimport (\n\t\"reflect\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc Test_earliestTimestamp(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\tstates  []State\n\t\twant    time.Time\n\t\twantErr require.ErrorAssertionFunc\n\t}{\n\t\t{\n\t\t\tname: \"happy path\",\n\t\t\tstates: []State{\n\t\t\t\t{\n\t\t\t\t\tTimestamp: time.Date(2021, 1, 2, 0, 0, 0, 0, time.UTC),\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tTimestamp: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC),\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tTimestamp: time.Date(2021, 1, 3, 0, 0, 0, 0, time.UTC),\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC),\n\t\t},\n\t\t{\n\t\t\tname:    \"empty states\",\n\t\t\tstates:  []State{},\n\t\t\twant:    time.Time{},\n\t\t\twantErr: requireErrorContains(\"cannot find earliest timestamp: no states provided\"),\n\t\t},\n\t\t{\n\t\t\tname: \"single state\",\n\t\t\tstates: []State{\n\t\t\t\t{\n\t\t\t\t\tTimestamp: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC),\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC),\n\t\t},\n\t\t{\n\t\t\tname: \"single state, but it's nvd\",\n\t\t\tstates: []State{\n\t\t\t\t{\n\t\t\t\t\tProvider:  \"nvd\",\n\t\t\t\t\tTimestamp: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC),\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC),\n\t\t},\n\t\t{\n\t\t\tname: \"all states have provider nvd\",\n\t\t\tstates: []State{\n\t\t\t\t{\n\t\t\t\t\tProvider:  \"nvd\",\n\t\t\t\t\tTimestamp: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC),\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tProvider:  \"nvd\",\n\t\t\t\t\tTimestamp: time.Date(2021, 1, 2, 0, 0, 0, 0, time.UTC),\n\t\t\t\t},\n\t\t\t},\n\t\t\twant:    time.Time{},\n\t\t\twantErr: requireErrorContains(\"unable to determine earliest timestamp\"),\n\t\t},\n\t\t{\n\t\t\tname: \"mix of nvd and non-nvd providers\",\n\t\t\tstates: []State{\n\t\t\t\t{\n\t\t\t\t\tProvider:  \"nvd\",\n\t\t\t\t\tTimestamp: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC),\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tProvider:  \"other\",\n\t\t\t\t\tTimestamp: time.Date(2021, 1, 3, 0, 0, 0, 0, time.UTC),\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tProvider:  \"other\",\n\t\t\t\t\tTimestamp: time.Date(2021, 1, 2, 0, 0, 0, 0, time.UTC),\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: time.Date(2021, 1, 2, 0, 0, 0, 0, time.UTC),\n\t\t},\n\t\t{\n\t\t\tname: \"timestamps are the same\",\n\t\t\tstates: []State{\n\t\t\t\t{\n\t\t\t\t\tTimestamp: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC),\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tTimestamp: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC),\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tTimestamp: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC),\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC),\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif tt.wantErr == nil {\n\t\t\t\ttt.wantErr = require.NoError\n\t\t\t}\n\t\t\tgot, err := States(tt.states).EarliestTimestamp()\n\t\t\ttt.wantErr(t, err)\n\t\t\tif err != nil {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"earliestTimestamp() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc requireErrorContains(text string) require.ErrorAssertionFunc {\n\treturn func(t require.TestingT, err error, msgAndArgs ...interface{}) {\n\t\trequire.Error(t, err, msgAndArgs...)\n\t\trequire.Contains(t, err.Error(), text, msgAndArgs...)\n\t}\n}\n"
  },
  {
    "path": "grype/db/provider/workspace.go",
    "content": "package provider\n\nimport (\n\t\"path/filepath\"\n)\n\ntype Workspace struct {\n\tRoot string\n\tName string\n}\n\nfunc NewWorkspace(root, name string) Workspace {\n\treturn Workspace{\n\t\tRoot: root,\n\t\tName: name,\n\t}\n}\n\nfunc NewWorkspaceFromExisting(workspacePath string) Workspace {\n\treturn Workspace{\n\t\tRoot: filepath.Dir(workspacePath),\n\t\tName: filepath.Base(workspacePath),\n\t}\n}\n\nfunc (w Workspace) Path() string {\n\treturn filepath.Join(w.Root, w.Name)\n}\n\nfunc (w Workspace) StatePath() string {\n\treturn filepath.Join(w.Path(), \"metadata.json\")\n}\n\nfunc (w Workspace) InputPath() string {\n\treturn filepath.Join(w.Path(), \"input\")\n}\n\nfunc (w Workspace) ResultsPath() string {\n\treturn filepath.Join(w.Path(), \"results\")\n}\n\nfunc (w Workspace) ReadState() (*State, error) {\n\treturn ReadState(w.StatePath())\n}\n"
  },
  {
    "path": "grype/db/v5/advisory.go",
    "content": "package v5\n\n// Advisory represents published statements regarding a vulnerability (and potentially about its resolution).\ntype Advisory struct {\n\tID   string `json:\"id\"`\n\tLink string `json:\"link\"`\n}\n"
  },
  {
    "path": "grype/db/v5/build/processors.go",
    "content": "package v5\n\nimport (\n\t\"github.com/scylladb/go-set/strset\"\n\n\t\"github.com/anchore/grype/grype/db/data\"\n\t\"github.com/anchore/grype/grype/db/processors\"\n\t\"github.com/anchore/grype/grype/db/v5/build/transformers/github\"\n\t\"github.com/anchore/grype/grype/db/v5/build/transformers/matchexclusions\"\n\t\"github.com/anchore/grype/grype/db/v5/build/transformers/msrc\"\n\t\"github.com/anchore/grype/grype/db/v5/build/transformers/nvd\"\n\t\"github.com/anchore/grype/grype/db/v5/build/transformers/os\"\n)\n\ntype Config struct {\n\tNVD nvd.Config\n}\n\ntype Option func(cfg *Config)\n\nfunc WithCPEParts(included []string) Option {\n\treturn func(cfg *Config) {\n\t\tcfg.NVD.CPEParts = strset.New(included...)\n\t}\n}\n\nfunc WithInferNVDFixVersions(infer bool) Option {\n\treturn func(cfg *Config) {\n\t\tcfg.NVD.InferNVDFixVersions = infer\n\t}\n}\n\nfunc NewConfig(options ...Option) Config {\n\tvar cfg Config\n\tfor _, option := range options {\n\t\toption(&cfg)\n\t}\n\n\treturn cfg\n}\n\nfunc Processors(cfg Config) []data.Processor {\n\treturn []data.Processor{\n\t\tprocessors.NewGitHubProcessor(github.Transform),\n\t\tprocessors.NewMSRCProcessor(msrc.Transform),\n\t\tprocessors.NewNVDProcessor(nvd.Transformer(cfg.NVD)),\n\t\tprocessors.NewOSProcessor(os.Transform),\n\t\tprocessors.NewMatchExclusionProcessor(matchexclusions.Transform),\n\t}\n}\n"
  },
  {
    "path": "grype/db/v5/build/transformers/entry.go",
    "content": "package transformers\n\nimport (\n\t\"github.com/anchore/grype/grype/db/data\"\n\tdb \"github.com/anchore/grype/grype/db/v5\"\n)\n\nfunc NewEntries(vs []db.Vulnerability, metadata db.VulnerabilityMetadata) []data.Entry {\n\tentries := []data.Entry{\n\t\t{\n\t\t\tDBSchemaVersion: db.SchemaVersion,\n\t\t\tData:            metadata,\n\t\t},\n\t}\n\tfor _, vuln := range vs {\n\t\tentries = append(entries, data.Entry{\n\t\t\tDBSchemaVersion: db.SchemaVersion,\n\t\t\tData:            vuln,\n\t\t})\n\t}\n\treturn entries\n}\n"
  },
  {
    "path": "grype/db/v5/build/transformers/github/testdata/github-github-npm-0.json",
    "content": "{\n    \"Advisory\": {\n      \"Classification\": \"GENERAL\",\n      \"Severity\": \"Critical\",\n      \"CVSS\": {\n        \"version\": \"3.1\",\n        \"vector_string\": \"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H\",\n        \"base_metrics\": {\n          \"base_score\": 9.8,\n          \"exploitability_score\": 3.9,\n          \"impact_score\": 5.9,\n          \"base_severity\": \"Critical\"\n        },\n        \"status\": \"N/A\"\n      },\n      \"FixedIn\": [\n        {\n          \"name\": \"scratch-vm\",\n          \"identifier\": \"0.2.0-prerelease.20200714185213\",\n          \"ecosystem\": \"npm\",\n          \"namespace\": \"github:npm\",\n          \"range\": \"<= 0.2.0-prerelease.20200709173451\"\n        }\n      ],\n      \"Summary\": \"Remote Code Execution in scratch-vm\",\n      \"url\": \"https://github.com/advisories/GHSA-vc9j-fhvv-8vrf\",\n      \"CVE\": [\n        \"CVE-2020-14000\"\n      ],\n      \"Metadata\": {\n        \"CVE\": [\n          \"CVE-2020-14000\"\n        ]\n      },\n      \"ghsaId\": \"GHSA-vc9j-fhvv-8vrf\",\n      \"published\": \"2020-07-27T19:55:52Z\",\n      \"updated\": \"2023-01-09T05:03:39Z\",\n      \"withdrawn\": null,\n      \"namespace\": \"github:npm\"\n    }\n}"
  },
  {
    "path": "grype/db/v5/build/transformers/github/testdata/github-github-python-0.json",
    "content": "[\n  {\n    \"Advisory\": {\n      \"CVE\": [\n        \"CVE-2018-8768\"\n      ],\n      \"FixedIn\": [\n        {\n          \"ecosystem\": \"python\",\n          \"identifier\": \"5.4.1\",\n          \"name\": \"notebook\",\n          \"namespace\": \"github:python\",\n          \"range\": \"< 5.4.1\"\n        }\n      ],\n      \"Metadata\": {\n        \"CVE\": [\n          \"CVE-2018-8768\"\n        ]\n      },\n      \"Severity\": \"Low\",\n      \"Summary\": \"Low severity vulnerability that affects notebook\",\n      \"ghsaId\": \"GHSA-6cwv-x26c-w2q4\",\n      \"namespace\": \"github:python\",\n      \"url\": \"https://github.com/advisories/GHSA-6cwv-x26c-w2q4\",\n      \"withdrawn\": null\n    },\n    \"Vulnerability\": {}\n  },\n  {\n    \"Advisory\": {\n      \"CVE\": [\n        \"CVE-2017-5524\"\n      ],\n      \"FixedIn\": [\n        {\n          \"ecosystem\": \"python\",\n          \"identifier\": \"4.3.12\",\n          \"name\": \"Plone\",\n          \"namespace\": \"github:python\",\n          \"range\": \">= 4.0 < 4.3.12\"\n        }\n      ],\n      \"Metadata\": {\n        \"CVE\": [\n          \"CVE-2017-5524\"\n        ]\n      },\n      \"Severity\": \"Medium\",\n      \"Summary\": \"Moderate severity vulnerability that affects Plone\",\n      \"ghsaId\": \"GHSA-p5wr-vp8g-q5p4\",\n      \"namespace\": \"github:python\",\n      \"url\": \"https://github.com/advisories/GHSA-p5wr-vp8g-q5p4\",\n      \"withdrawn\": null\n    },\n    \"Vulnerability\": {}\n  }\n]"
  },
  {
    "path": "grype/db/v5/build/transformers/github/testdata/github-github-python-1.json",
    "content": "\n{\n  \"Advisory\": {\n    \"CVE\": [\n      \"CVE-2017-5524\"\n    ],\n    \"FixedIn\": [\n      {\n        \"ecosystem\": \"python\",\n        \"identifier\": \"4.3.12\",\n        \"name\": \"Plone\",\n        \"namespace\": \"github:python\",\n        \"range\": \">= 4.0 < 4.3.12\"\n      },\n      {\n        \"ecosystem\": \"python\",\n        \"identifier\": \"5.1b1\",\n        \"name\": \"Plone\",\n        \"namespace\": \"github:python\",\n        \"range\": \">= 5.1a1 < 5.1b1\"\n      },\n      {\n        \"ecosystem\": \"python\",\n        \"identifier\": \"5.0.7\",\n        \"name\": \"Plone\",\n        \"namespace\": \"github:python\",\n        \"range\": \">= 5.0rc1 < 5.0.7\"\n      }\n    ],\n    \"Metadata\": {\n      \"CVE\": [\n        \"CVE-2017-5524\"\n      ]\n    },\n    \"Severity\": \"Medium\",\n    \"Summary\": \"Moderate severity vulnerability that affects Plone\",\n    \"ghsaId\": \"GHSA-p5wr-vp8g-q5p4\",\n    \"namespace\": \"github:python\",\n    \"url\": \"https://github.com/advisories/GHSA-p5wr-vp8g-q5p4\",\n    \"withdrawn\": null\n  },\n  \"Vulnerability\": {}\n}\n"
  },
  {
    "path": "grype/db/v5/build/transformers/github/testdata/github-withdrawn.json",
    "content": "\n{\n  \"Advisory\": {\n    \"CVE\": [\n      \"CVE-2018-8768\"\n    ],\n    \"FixedIn\": [\n      {\n        \"ecosystem\": \"python\",\n        \"identifier\": \"5.4.1\",\n        \"name\": \"notebook\",\n        \"namespace\": \"github:python\",\n        \"range\": \"< 5.4.1\"\n      }\n    ],\n    \"Metadata\": {\n      \"CVE\": [\n        \"CVE-2018-8768\"\n      ]\n    },\n    \"Severity\": \"Low\",\n    \"Summary\": \"Low severity vulnerability that affects notebook\",\n    \"ghsaId\": \"GHSA-6cwv-x26c-w2q4\",\n    \"namespace\": \"github:python\",\n    \"url\": \"https://github.com/advisories/GHSA-6cwv-x26c-w2q4\",\n    \"withdrawn\": \"2022-01-31T14:32:09Z\"\n  },\n  \"Vulnerability\": {}\n}\n"
  },
  {
    "path": "grype/db/v5/build/transformers/github/testdata/multiple-fixed-in-names.json",
    "content": "\n{\n  \"Advisory\": {\n    \"CVE\": [\n      \"CVE-2017-5524\"\n    ],\n    \"FixedIn\": [\n      {\n        \"ecosystem\": \"python\",\n        \"identifier\": \"4.3.12\",\n        \"name\": \"Plone\",\n        \"namespace\": \"github:python\",\n        \"range\": \">= 4.0 < 4.3.12\"\n      },\n      {\n        \"ecosystem\": \"python\",\n        \"identifier\": \"5.1b1\",\n        \"name\": \"Plone\",\n        \"namespace\": \"github:python\",\n        \"range\": \">= 5.1a1 < 5.1b1\"\n      },\n      {\n        \"ecosystem\": \"python\",\n        \"identifier\": \"5.0.7\",\n        \"name\": \"Plone-debug\",\n        \"namespace\": \"github:python\",\n        \"range\": \">= 5.0rc1 < 5.0.7\"\n      }\n    ],\n    \"Metadata\": {\n      \"CVE\": [\n        \"CVE-2017-5524\"\n      ]\n    },\n    \"Severity\": \"Medium\",\n    \"Summary\": \"Moderate severity vulnerability that affects Plone\",\n    \"ghsaId\": \"GHSA-p5wr-vp8g-q5p4\",\n    \"namespace\": \"github:python\",\n    \"url\": \"https://github.com/advisories/GHSA-p5wr-vp8g-q5p4\",\n    \"withdrawn\": null\n  },\n  \"Vulnerability\": {}\n}\n"
  },
  {
    "path": "grype/db/v5/build/transformers/github/transform.go",
    "content": "package github\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/anchore/grype/grype/db/data\"\n\t\"github.com/anchore/grype/grype/db/internal/provider/unmarshal\"\n\t\"github.com/anchore/grype/grype/db/internal/versionutil\"\n\tdb \"github.com/anchore/grype/grype/db/v5\"\n\t\"github.com/anchore/grype/grype/db/v5/build/transformers\"\n\t\"github.com/anchore/grype/grype/db/v5/namespace\"\n\tsyftPkg \"github.com/anchore/syft/syft/pkg\"\n)\n\nvar errSkip = fmt.Errorf(\"skipping advisory\")\n\nfunc buildGrypeNamespace(group string) (namespace.Namespace, error) {\n\tfeedGroupComponents := strings.Split(group, \":\")\n\n\tif len(feedGroupComponents) < 2 {\n\t\treturn nil, fmt.Errorf(\"unable to determine grype namespace for enterprise namespace=%s\", group)\n\t}\n\n\tfeedGroupLang := feedGroupComponents[1]\n\tsyftLanguage := syftPkg.LanguageByName(feedGroupLang)\n\n\tif syftLanguage == syftPkg.UnknownLanguage {\n\t\tswitch feedGroupLang {\n\t\tcase \"nuget\":\n\t\t\tsyftLanguage = syftPkg.Dotnet\n\t\tcase \"github-action\", \"erlang\":\n\t\t\t// we don't want to error out on this, but grype at this version does not support these ecosystems\n\t\t\treturn nil, errSkip\n\t\tdefault:\n\t\t\treturn nil, fmt.Errorf(\"unable to determine grype namespace for enterprise namespace=%s\", group)\n\t\t}\n\t}\n\n\tns, err := namespace.FromString(fmt.Sprintf(\"github:language:%s\", string(syftLanguage)))\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn ns, nil\n}\n\nfunc Transform(vulnerability unmarshal.GitHubAdvisory) ([]data.Entry, error) {\n\tvar allVulns []db.Vulnerability\n\n\t// Exclude entries marked as withdrawn\n\tif vulnerability.Advisory.Withdrawn != \"\" {\n\t\treturn nil, nil\n\t}\n\n\t// TODO: stop capturing record source in the vulnerability metadata record (now that feed groups are not real)\n\trecordSource := fmt.Sprintf(\"github:%s\", vulnerability.Advisory.Namespace)\n\n\tgrypeNamespace, err := buildGrypeNamespace(vulnerability.Advisory.Namespace)\n\tif err != nil {\n\t\tif errors.Is(err, errSkip) {\n\t\t\treturn nil, nil\n\t\t}\n\t\treturn nil, err\n\t}\n\n\tentryNamespace := grypeNamespace.String()\n\n\t// there may be multiple packages indicated within the FixedIn field, we should make\n\t// separate vulnerability entries (one for each name|namespaces combo) while merging\n\t// constraint ranges as they are found.\n\tfor idx, fixedInEntry := range vulnerability.Advisory.FixedIn {\n\t\tconstraint := versionutil.EnforceSemVerConstraint(fixedInEntry.Range)\n\n\t\tvar versionFormat string\n\t\tswitch entryNamespace {\n\t\tcase \"github:language:python\":\n\t\t\tversionFormat = \"python\"\n\t\tdefault:\n\t\t\tversionFormat = \"unknown\"\n\t\t}\n\n\t\t// create vulnerability entry\n\t\tallVulns = append(allVulns, db.Vulnerability{\n\t\t\tID:                     vulnerability.Advisory.GhsaID,\n\t\t\tVersionConstraint:      constraint,\n\t\t\tVersionFormat:          versionFormat,\n\t\t\tRelatedVulnerabilities: getRelatedVulnerabilities(vulnerability),\n\t\t\tPackageName:            grypeNamespace.Resolver().Normalize(fixedInEntry.Name),\n\t\t\tNamespace:              entryNamespace,\n\t\t\tFix:                    getFix(vulnerability, idx),\n\t\t})\n\t}\n\n\t// create vulnerability metadata entry (a single entry keyed off of the vulnerability ID)\n\tmetadata := db.VulnerabilityMetadata{\n\t\tID:           vulnerability.Advisory.GhsaID,\n\t\tDataSource:   vulnerability.Advisory.URL,\n\t\tNamespace:    entryNamespace,\n\t\tRecordSource: recordSource,\n\t\tSeverity:     vulnerability.Advisory.Severity,\n\t\tURLs:         []string{vulnerability.Advisory.URL},\n\t\tDescription:  vulnerability.Advisory.Summary,\n\t\tCvss:         getCvss(vulnerability),\n\t}\n\n\treturn transformers.NewEntries(allVulns, metadata), nil\n}\n\nfunc getFix(entry unmarshal.GitHubAdvisory, idx int) db.Fix {\n\tfixedInEntry := entry.Advisory.FixedIn[idx]\n\n\tvar fixedInVersions []string\n\tfixedInVersion := versionutil.CleanFixedInVersion(fixedInEntry.Identifier)\n\tif fixedInVersion != \"\" {\n\t\tfixedInVersions = append(fixedInVersions, fixedInVersion)\n\t}\n\n\tfixState := db.NotFixedState\n\tif len(fixedInVersions) > 0 {\n\t\tfixState = db.FixedState\n\t}\n\n\treturn db.Fix{\n\t\tVersions: fixedInVersions,\n\t\tState:    fixState,\n\t}\n}\n\nfunc getRelatedVulnerabilities(entry unmarshal.GitHubAdvisory) []db.VulnerabilityReference {\n\tvulns := make([]db.VulnerabilityReference, len(entry.Advisory.CVE))\n\tfor idx, cve := range entry.Advisory.CVE {\n\t\tvulns[idx] = db.VulnerabilityReference{\n\t\t\tID:        cve,\n\t\t\tNamespace: \"nvd:cpe\",\n\t\t}\n\t}\n\treturn vulns\n}\n\nfunc getCvss(entry unmarshal.GitHubAdvisory) (cvss []db.Cvss) {\n\tif entry.Advisory.CVSS == nil {\n\t\treturn cvss\n\t}\n\n\tcvss = append(cvss, db.Cvss{\n\t\tVersion: entry.Advisory.CVSS.Version,\n\t\tVector:  entry.Advisory.CVSS.VectorString,\n\t\tMetrics: db.NewCvssMetrics(\n\t\t\tentry.Advisory.CVSS.BaseMetrics.BaseScore,\n\t\t\tentry.Advisory.CVSS.BaseMetrics.ExploitabilityScore,\n\t\t\tentry.Advisory.CVSS.BaseMetrics.ImpactScore,\n\t\t),\n\t\tVendorMetadata: transformers.VendorBaseMetrics{\n\t\t\tBaseSeverity: entry.Advisory.CVSS.BaseMetrics.BaseSeverity,\n\t\t\tStatus:       entry.Advisory.CVSS.Status,\n\t\t},\n\t})\n\n\treturn cvss\n}\n"
  },
  {
    "path": "grype/db/v5/build/transformers/github/transform_test.go",
    "content": "package github\n\nimport (\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/grype/grype/db/internal/provider/unmarshal\"\n\t\"github.com/anchore/grype/grype/db/internal/testutil\"\n\tdb \"github.com/anchore/grype/grype/db/v5\"\n\tv5 \"github.com/anchore/grype/grype/db/v5\"\n\t\"github.com/anchore/grype/grype/db/v5/build/transformers\"\n\t\"github.com/anchore/grype/grype/db/v5/namespace\"\n\t\"github.com/anchore/grype/grype/db/v5/namespace/language\"\n\tsyftPkg \"github.com/anchore/syft/syft/pkg\"\n)\n\nfunc TestBuildGrypeNamespace(t *testing.T) {\n\ttests := []struct {\n\t\tgroup     string\n\t\tnamespace namespace.Namespace\n\t\twantErr   require.ErrorAssertionFunc\n\t}{\n\t\t{\n\t\t\tgroup:     \"github:python\",\n\t\t\tnamespace: language.NewNamespace(\"github\", syftPkg.Python, \"\"),\n\t\t},\n\t\t{\n\t\t\tgroup:     \"github:composer\",\n\t\t\tnamespace: language.NewNamespace(\"github\", syftPkg.PHP, \"\"),\n\t\t},\n\t\t{\n\t\t\tgroup:     \"github:gem\",\n\t\t\tnamespace: language.NewNamespace(\"github\", syftPkg.Ruby, \"\"),\n\t\t},\n\t\t{\n\t\t\tgroup:     \"github:npm\",\n\t\t\tnamespace: language.NewNamespace(\"github\", syftPkg.JavaScript, \"\"),\n\t\t},\n\t\t{\n\t\t\tgroup:     \"github:go\",\n\t\t\tnamespace: language.NewNamespace(\"github\", syftPkg.Go, \"\"),\n\t\t},\n\t\t{\n\t\t\tgroup:     \"github:nuget\",\n\t\t\tnamespace: language.NewNamespace(\"github\", syftPkg.Dotnet, \"\"),\n\t\t},\n\t\t{\n\t\t\tgroup:     \"github:rust\",\n\t\t\tnamespace: language.NewNamespace(\"github\", syftPkg.Rust, \"\"),\n\t\t},\n\t\t{\n\t\t\tgroup: \"github:github-action\",\n\t\t\twantErr: func(t require.TestingT, err error, i ...interface{}) {\n\t\t\t\tassert.Error(t, err)\n\t\t\t\tassert.ErrorIs(t, errSkip, err)\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tif test.wantErr == nil {\n\t\t\ttest.wantErr = require.NoError\n\t\t}\n\t\tns, err := buildGrypeNamespace(test.group)\n\t\ttest.wantErr(t, err)\n\t\tif err != nil {\n\t\t\treturn\n\t\t}\n\t\tassert.Equal(t, test.namespace, ns)\n\t}\n}\n\nfunc TestUnmarshalGitHubEntries(t *testing.T) {\n\tf, err := os.Open(\"testdata/github-github-python-0.json\")\n\trequire.NoError(t, err)\n\tdefer testutil.CloseFile(f)\n\n\tentries, err := unmarshal.GitHubAdvisoryEntries(f)\n\trequire.NoError(t, err)\n\n\tassert.Len(t, entries, 2)\n\n}\n\nfunc TestParseGitHubEntry(t *testing.T) {\n\texpectedVulns := []db.Vulnerability{\n\t\t{\n\t\t\tID:                \"GHSA-p5wr-vp8g-q5p4\",\n\t\t\tVersionConstraint: \">=4.0,<4.3.12\",\n\t\t\tVersionFormat:     \"python\",\n\t\t\tRelatedVulnerabilities: []db.VulnerabilityReference{\n\t\t\t\t{\n\t\t\t\t\tID:        \"CVE-2017-5524\",\n\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tPackageName: \"plone\",\n\t\t\tNamespace:   \"github:language:python\",\n\t\t\tFix: db.Fix{\n\t\t\t\tState:    db.FixedState,\n\t\t\t\tVersions: []string{\"4.3.12\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tID:                \"GHSA-p5wr-vp8g-q5p4\",\n\t\t\tVersionConstraint: \">=5.1a1,<5.1b1\",\n\t\t\tVersionFormat:     \"python\",\n\t\t\tRelatedVulnerabilities: []db.VulnerabilityReference{\n\t\t\t\t{\n\t\t\t\t\tID:        \"CVE-2017-5524\",\n\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tPackageName: \"plone\",\n\t\t\tNamespace:   \"github:language:python\",\n\t\t\tFix: db.Fix{\n\t\t\t\tVersions: []string{\"5.1b1\"},\n\t\t\t\tState:    db.FixedState,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tID:                \"GHSA-p5wr-vp8g-q5p4\",\n\t\t\tVersionConstraint: \">=5.0rc1,<5.0.7\",\n\t\t\tVersionFormat:     \"python\",\n\t\t\tRelatedVulnerabilities: []db.VulnerabilityReference{\n\t\t\t\t{\n\t\t\t\t\tID:        \"CVE-2017-5524\",\n\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tPackageName: \"plone\",\n\t\t\tNamespace:   \"github:language:python\",\n\t\t\tFix: db.Fix{\n\t\t\t\tVersions: []string{\"5.0.7\"},\n\t\t\t\tState:    db.FixedState,\n\t\t\t},\n\t\t},\n\t}\n\n\texpectedMetadata := db.VulnerabilityMetadata{\n\t\tID:           \"GHSA-p5wr-vp8g-q5p4\",\n\t\tNamespace:    \"github:language:python\",\n\t\tRecordSource: \"github:github:python\",\n\t\tDataSource:   \"https://github.com/advisories/GHSA-p5wr-vp8g-q5p4\",\n\t\tSeverity:     \"Medium\",\n\t\tURLs:         []string{\"https://github.com/advisories/GHSA-p5wr-vp8g-q5p4\"},\n\t\tDescription:  \"Moderate severity vulnerability that affects Plone\",\n\t}\n\n\tf, err := os.Open(\"testdata/github-github-python-1.json\")\n\trequire.NoError(t, err)\n\tdefer testutil.CloseFile(f)\n\n\tentries, err := unmarshal.GitHubAdvisoryEntries(f)\n\trequire.NoError(t, err)\n\n\trequire.Len(t, entries, 1)\n\n\tentry := entries[0]\n\n\tdataEntries, err := Transform(entry)\n\trequire.NoError(t, err)\n\n\tvar vulns []db.Vulnerability\n\tfor _, entry := range dataEntries {\n\t\tswitch vuln := entry.Data.(type) {\n\t\tcase db.Vulnerability:\n\t\t\tvulns = append(vulns, vuln)\n\t\tcase db.VulnerabilityMetadata:\n\t\t\tassert.Equal(t, expectedMetadata, vuln)\n\t\tdefault:\n\t\t\tt.Fatalf(\"unexpected condition: data entry does not have a vulnerability or a metadata\")\n\t\t}\n\t}\n\n\t// check vulnerability\n\tassert.Len(t, vulns, len(expectedVulns))\n\n\tif diff := cmp.Diff(expectedVulns, vulns); diff != \"\" {\n\t\tt.Errorf(\"vulnerabilities do not match (-want +got):\\n%s\", diff)\n\t}\n}\n\nfunc TestDefaultVersionFormatNpmGitHubEntry(t *testing.T) {\n\texpectedVuln := db.Vulnerability{\n\t\tID:                \"GHSA-vc9j-fhvv-8vrf\",\n\t\tVersionConstraint: \"<=0.2.0-prerelease.20200709173451\",\n\t\tVersionFormat:     \"unknown\", // TODO: this should reference a format, yes? (not a string)\n\t\tRelatedVulnerabilities: []db.VulnerabilityReference{\n\t\t\t{\n\t\t\t\tID:        \"CVE-2020-14000\",\n\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t},\n\t\t},\n\t\tPackageName: \"scratch-vm\",\n\t\tNamespace:   \"github:language:javascript\",\n\t\tFix: db.Fix{\n\t\t\tVersions: []string{\"0.2.0-prerelease.20200714185213\"},\n\t\t\tState:    db.FixedState,\n\t\t},\n\t}\n\n\texpectedMetadata := db.VulnerabilityMetadata{\n\t\tID:           \"GHSA-vc9j-fhvv-8vrf\",\n\t\tNamespace:    \"github:language:javascript\",\n\t\tRecordSource: \"github:github:npm\",\n\t\tDataSource:   \"https://github.com/advisories/GHSA-vc9j-fhvv-8vrf\",\n\t\tSeverity:     \"Critical\",\n\t\tURLs:         []string{\"https://github.com/advisories/GHSA-vc9j-fhvv-8vrf\"},\n\t\tDescription:  \"Remote Code Execution in scratch-vm\",\n\t\tCvss: []db.Cvss{\n\t\t\t{\n\t\t\t\tVendorMetadata: transformers.VendorBaseMetrics{\n\t\t\t\t\tBaseSeverity: \"Critical\",\n\t\t\t\t\tStatus:       \"N/A\",\n\t\t\t\t},\n\t\t\t\tMetrics: v5.NewCvssMetrics(9.8, 3.9, 5.9),\n\t\t\t\tVector:  \"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H\",\n\t\t\t\tVersion: \"3.1\",\n\t\t\t},\n\t\t},\n\t}\n\n\tf, err := os.Open(\"testdata/github-github-npm-0.json\")\n\trequire.NoError(t, err)\n\tdefer testutil.CloseFile(f)\n\n\tentries, err := unmarshal.GitHubAdvisoryEntries(f)\n\trequire.NoError(t, err)\n\n\trequire.Len(t, entries, 1)\n\n\tentry := entries[0]\n\n\tdataEntries, err := Transform(entry)\n\tassert.NoError(t, err)\n\n\tfor _, entry := range dataEntries {\n\t\tswitch vuln := entry.Data.(type) {\n\t\tcase db.Vulnerability:\n\t\t\tassert.Equal(t, expectedVuln, vuln)\n\t\tcase db.VulnerabilityMetadata:\n\t\t\tassert.Equal(t, expectedMetadata, vuln)\n\t\tdefault:\n\t\t\tt.Fatalf(\"unexpected condition: data entry does not have a vulnerability or a metadata\")\n\t\t}\n\t}\n\n\t// check vulnerability\n\tassert.Len(t, dataEntries, 2)\n}\n\nfunc TestFilterWithdrawnEntries(t *testing.T) {\n\tf, err := os.Open(\"testdata/github-withdrawn.json\")\n\trequire.NoError(t, err)\n\tdefer testutil.CloseFile(f)\n\n\tentries, err := unmarshal.GitHubAdvisoryEntries(f)\n\trequire.NoError(t, err)\n\n\trequire.Len(t, entries, 1)\n\n\tentry := entries[0]\n\n\tdataEntries, err := Transform(entry)\n\tassert.NoError(t, err)\n\tassert.Nil(t, dataEntries)\n}\n"
  },
  {
    "path": "grype/db/v5/build/transformers/matchexclusions/transform.go",
    "content": "package matchexclusions\n\nimport (\n\t\"github.com/anchore/grype/grype/db/data\"\n\t\"github.com/anchore/grype/grype/db/internal/provider/unmarshal\"\n\tdb \"github.com/anchore/grype/grype/db/v5\"\n)\n\nfunc Transform(matchExclusion unmarshal.MatchExclusion) ([]data.Entry, error) {\n\texclusion := db.VulnerabilityMatchExclusion{\n\t\tID:            matchExclusion.ID,\n\t\tConstraints:   nil,\n\t\tJustification: matchExclusion.Justification,\n\t}\n\n\tfor _, c := range matchExclusion.Constraints {\n\t\tconstraint := &db.VulnerabilityMatchExclusionConstraint{\n\t\t\tVulnerability: db.VulnerabilityExclusionConstraint{\n\t\t\t\tNamespace: c.Vulnerability.Namespace,\n\t\t\t\tFixState:  db.FixState(c.Vulnerability.FixState),\n\t\t\t},\n\t\t\tPackage: db.PackageExclusionConstraint{\n\t\t\t\tName:     c.Package.Name,\n\t\t\t\tLanguage: c.Package.Language,\n\t\t\t\tType:     c.Package.Type,\n\t\t\t\tVersion:  c.Package.Version,\n\t\t\t\tLocation: c.Package.Location,\n\t\t\t},\n\t\t}\n\n\t\texclusion.Constraints = append(exclusion.Constraints, *constraint)\n\t}\n\n\tentries := []data.Entry{\n\t\t{\n\t\t\tDBSchemaVersion: db.SchemaVersion,\n\t\t\tData:            exclusion,\n\t\t},\n\t}\n\n\treturn entries, nil\n}\n"
  },
  {
    "path": "grype/db/v5/build/transformers/msrc/testdata/microsoft-msrc-0.json",
    "content": "[\n {\n  \"cvss\": {\n   \"base_score\": 7.8,\n   \"temporal_score\": 7,\n   \"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\"\n  },\n  \"fixed_in\": [\n   {\n    \"id\": \"4493470\",\n    \"is_first\": true,\n    \"is_latest\": false,\n    \"links\": [\n     \"https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4493470\",\n     \"https://support.microsoft.com/help/4493470\"\n    ]\n   },\n   {\n    \"id\": \"4494440\",\n    \"is_first\": false,\n    \"is_latest\": false,\n    \"links\": [\n     \"https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4494440\",\n     \"https://support.microsoft.com/help/4494440\"\n    ]\n   },\n   {\n    \"id\": \"4503267\",\n    \"is_first\": false,\n    \"is_latest\": false,\n    \"links\": [\n     \"https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4503267\",\n     \"https://support.microsoft.com/en-us/help/4503267\"\n    ]\n   },\n   {\n    \"id\": \"4507460\",\n    \"is_first\": false,\n    \"is_latest\": false,\n    \"links\": [\n     \"https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4507460\",\n     \"https://support.microsoft.com/help/4507460\"\n    ]\n   },\n   {\n    \"id\": \"4512517\",\n    \"is_first\": false,\n    \"is_latest\": false,\n    \"links\": [\n     \"https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4512517\",\n     \"https://support.microsoft.com/help/4512517\"\n    ]\n   },\n   {\n    \"id\": \"4516044\",\n    \"is_first\": false,\n    \"is_latest\": true,\n    \"links\": [\n     \"https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4516044\",\n     \"https://support.microsoft.com/help/4516044\"\n    ]\n   }\n  ],\n  \"id\": \"CVE-2019-0671\",\n  \"link\": \"https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2019-0671\",\n  \"product\": {\n   \"family\": \"Windows\",\n   \"id\": \"10852\",\n   \"name\": \"Windows 10 Version 1607 for 32-bit Systems\"\n  },\n  \"severity\": \"High\",\n  \"summary\": \"Microsoft Office Access Connectivity Engine Remote Code Execution Vulnerability\",\n  \"vulnerable\": [\n   \"4480961\",\n   \"4483229\",\n   \"4487026\",\n   \"4489882\"\n  ]\n },\n{\n  \"cvss\": {\n   \"base_score\": 4.4,\n   \"temporal_score\": 4,\n   \"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\"\n  },\n  \"fixed_in\": [\n   {\n    \"id\": \"4093119\",\n    \"is_first\": true,\n    \"is_latest\": false,\n    \"links\": [\n     \"https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4093119\"\n    ]\n   },\n   {\n    \"id\": \"4103723\",\n    \"is_first\": false,\n    \"is_latest\": false,\n    \"links\": [\n     \"https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4103723\"\n    ]\n   },\n   {\n    \"id\": \"4284880\",\n    \"is_first\": false,\n    \"is_latest\": false,\n    \"links\": [\n     \"https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4284880\"\n    ]\n   },\n   {\n    \"id\": \"4338814\",\n    \"is_first\": false,\n    \"is_latest\": false,\n    \"links\": [\n     \"https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4338814\"\n    ]\n   },\n   {\n    \"id\": \"4343887\",\n    \"is_first\": false,\n    \"is_latest\": false,\n    \"links\": [\n     \"https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4343887\"\n    ]\n   },\n   {\n    \"id\": \"4345418\",\n    \"is_first\": false,\n    \"is_latest\": true,\n    \"links\": [\n     \"https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4345418\"\n    ]\n   },\n   {\n    \"id\": \"4457131\",\n    \"is_first\": false,\n    \"is_latest\": false,\n    \"links\": [\n     \"https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4457131\"\n    ]\n   },\n   {\n    \"id\": \"4462917\",\n    \"is_first\": false,\n    \"is_latest\": false,\n    \"links\": [\n     \"https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4462917\"\n    ]\n   },\n   {\n    \"id\": \"4467691\",\n    \"is_first\": false,\n    \"is_latest\": false,\n    \"links\": [\n     \"https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4467691\"\n    ]\n   },\n   {\n    \"id\": \"4471321\",\n    \"is_first\": false,\n    \"is_latest\": true,\n    \"links\": [\n     \"https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4471321\"\n    ]\n   }\n  ],\n  \"id\": \"CVE-2018-8116\",\n  \"link\": \"https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2018-8116\",\n  \"product\": {\n   \"family\": \"Windows\",\n   \"id\": \"10852\",\n   \"name\": \"Windows 10 Version 1607 for 32-bit Systems\"\n  },\n  \"severity\": \"Medium\",\n  \"summary\": \"Microsoft Graphics Component Denial of Service Vulnerability\",\n  \"vulnerable\": [\n   \"3213986\",\n   \"4013429\",\n   \"4015217\",\n   \"4019472\",\n   \"4022715\",\n   \"4025339\",\n   \"4034658\",\n   \"4038782\",\n   \"4041691\",\n   \"4048953\",\n   \"4053579\",\n   \"4056890\",\n   \"4074590\",\n   \"4088787\"\n  ]\n }\n]\n"
  },
  {
    "path": "grype/db/v5/build/transformers/msrc/transform.go",
    "content": "package msrc\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/anchore/grype/grype/db/data\"\n\t\"github.com/anchore/grype/grype/db/internal/provider/unmarshal\"\n\t\"github.com/anchore/grype/grype/db/internal/versionutil\"\n\tdb \"github.com/anchore/grype/grype/db/v5\"\n\t\"github.com/anchore/grype/grype/db/v5/build/transformers\"\n\t\"github.com/anchore/grype/grype/db/v5/namespace\"\n\t\"github.com/anchore/grype/grype/distro\"\n)\n\n// Transform gets called by the parser, which consumes entries from the JSON files previously pulled. Each VulnDBVulnerability represents\n// a single unmarshalled entry from the feed service\nfunc Transform(vulnerability unmarshal.MSRCVulnerability) ([]data.Entry, error) {\n\t// TODO: stop capturing record source in the vulnerability metadata record (now that feed groups are not real)\n\trecordSource := fmt.Sprintf(\"microsoft:msrc:%s\", vulnerability.Product.ID)\n\n\tgrypeNamespace, err := namespace.FromString(fmt.Sprintf(\"msrc:distro:%s:%s\", distro.Windows, vulnerability.Product.ID))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tentryNamespace := grypeNamespace.String()\n\n\t// In anchore-enterprise windows analyzer, \"base\" represents unpatched windows images (images with no KBs).\n\t// If a vulnerability exists for a Microsoft Product ID and the image has no KBs (which are patches),\n\t// then the image must be vulnerable to the image.\n\t//nolint:gocritic\n\tversionConstraint := append(vulnerability.Vulnerable, \"base\")\n\n\tallVulns := []db.Vulnerability{\n\t\t{\n\t\t\tID:                vulnerability.ID,\n\t\t\tVersionConstraint: versionutil.OrConstraints(versionConstraint...),\n\t\t\tVersionFormat:     \"kb\",\n\t\t\tPackageName:       grypeNamespace.Resolver().Normalize(vulnerability.Product.ID),\n\t\t\tNamespace:         entryNamespace,\n\t\t\tFix:               getFix(vulnerability),\n\t\t},\n\t}\n\n\t// create vulnerability metadata entry (a single entry keyed off of the vulnerability ID)\n\tmetadata := db.VulnerabilityMetadata{\n\t\tID:           vulnerability.ID,\n\t\tDataSource:   vulnerability.Link,\n\t\tNamespace:    entryNamespace,\n\t\tRecordSource: recordSource,\n\t\tSeverity:     vulnerability.Severity,\n\t\tURLs:         []string{vulnerability.Link},\n\t\t// There is no description for vulnerabilities from the feed service\n\t\t// summary gives something like \"windows information disclosure vulnerability\"\n\t\t//Description:  vulnerability.Summary,\n\t\tCvss: []db.Cvss{\n\t\t\t{\n\t\t\t\tMetrics: db.CvssMetrics{BaseScore: vulnerability.Cvss.BaseScore},\n\t\t\t\tVector:  vulnerability.Cvss.Vector,\n\t\t\t},\n\t\t},\n\t}\n\n\treturn transformers.NewEntries(allVulns, metadata), nil\n}\n\nfunc getFix(entry unmarshal.MSRCVulnerability) db.Fix {\n\tfixedInVersion := fixedInKB(entry)\n\tfixState := db.FixedState\n\n\tif fixedInVersion == \"\" {\n\t\tfixState = db.NotFixedState\n\t}\n\n\treturn db.Fix{\n\t\tVersions: []string{fixedInVersion},\n\t\tState:    fixState,\n\t}\n}\n\n// fixedInKB finds the \"latest\" patch (KB id) amongst the available microsoft patches and returns it\n// if the \"latest\" patch cannot be found, an error is returned\nfunc fixedInKB(vulnerability unmarshal.MSRCVulnerability) string {\n\tfor _, fixedIn := range vulnerability.FixedIn {\n\t\tif fixedIn.IsLatest {\n\t\t\treturn fixedIn.ID\n\t\t}\n\t}\n\treturn \"\"\n}\n"
  },
  {
    "path": "grype/db/v5/build/transformers/msrc/transform_test.go",
    "content": "package msrc\n\nimport (\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/grype/grype/db/internal/provider/unmarshal\"\n\t\"github.com/anchore/grype/grype/db/internal/testutil\"\n\tdb \"github.com/anchore/grype/grype/db/v5\"\n)\n\nfunc TestUnmarshalMsrcVulnerabilities(t *testing.T) {\n\tf, err := os.Open(\"testdata/microsoft-msrc-0.json\")\n\trequire.NoError(t, err)\n\tdefer testutil.CloseFile(f)\n\n\tentries, err := unmarshal.MSRCVulnerabilityEntries(f)\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, len(entries), 2)\n}\n\nfunc TestParseMSRCEntry(t *testing.T) {\n\texpectedVulns := []struct {\n\t\tvulnerability db.Vulnerability\n\t\tmetadata      db.VulnerabilityMetadata\n\t}{\n\t\t{\n\t\t\tvulnerability: db.Vulnerability{\n\t\t\t\tID:                \"CVE-2019-0671\",\n\t\t\t\tVersionConstraint: `4480961 || 4483229 || 4487026 || 4489882 || base`,\n\t\t\t\tVersionFormat:     \"kb\",\n\t\t\t\tPackageName:       \"10852\",\n\t\t\t\tNamespace:         \"msrc:distro:windows:10852\",\n\t\t\t\tFix: db.Fix{\n\t\t\t\t\tVersions: []string{\"4516044\"},\n\t\t\t\t\tState:    db.FixedState,\n\t\t\t\t},\n\t\t\t},\n\t\t\tmetadata: db.VulnerabilityMetadata{\n\t\t\t\tID:           \"CVE-2019-0671\",\n\t\t\t\tSeverity:     \"High\",\n\t\t\t\tDataSource:   \"https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2019-0671\",\n\t\t\t\tURLs:         []string{\"https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2019-0671\"},\n\t\t\t\tDescription:  \"\",\n\t\t\t\tRecordSource: \"microsoft:msrc:10852\",\n\t\t\t\tNamespace:    \"msrc:distro:windows:10852\",\n\t\t\t\tCvss: []db.Cvss{\n\t\t\t\t\t{\n\t\t\t\t\t\tMetrics: db.CvssMetrics{\n\t\t\t\t\t\t\tBaseScore:   7.8,\n\t\t\t\t\t\t\tImpactScore: nil,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tVector: \"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\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tvulnerability: db.Vulnerability{\n\t\t\t\tID:                \"CVE-2018-8116\",\n\t\t\t\tVersionConstraint: `3213986 || 4013429 || 4015217 || 4019472 || 4022715 || 4025339 || 4034658 || 4038782 || 4041691 || 4048953 || 4053579 || 4056890 || 4074590 || 4088787 || base`,\n\t\t\t\tVersionFormat:     \"kb\",\n\t\t\t\tPackageName:       \"10852\",\n\t\t\t\tNamespace:         \"msrc:distro:windows:10852\",\n\t\t\t\tFix: db.Fix{\n\t\t\t\t\tVersions: []string{\"4345418\"},\n\t\t\t\t\tState:    db.FixedState,\n\t\t\t\t},\n\t\t\t},\n\t\t\tmetadata: db.VulnerabilityMetadata{\n\t\t\t\tID:           \"CVE-2018-8116\",\n\t\t\t\tNamespace:    \"msrc:distro:windows:10852\",\n\t\t\t\tDataSource:   \"https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2018-8116\",\n\t\t\t\tRecordSource: \"microsoft:msrc:10852\",\n\t\t\t\tSeverity:     \"Medium\",\n\t\t\t\tURLs:         []string{\"https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2018-8116\"},\n\t\t\t\tDescription:  \"\",\n\t\t\t\tCvss: []db.Cvss{\n\t\t\t\t\t{\n\t\t\t\t\t\tMetrics: db.CvssMetrics{\n\t\t\t\t\t\t\tBaseScore:   4.4,\n\t\t\t\t\t\t\tImpactScore: nil,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tVector: \"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\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tf, err := os.Open(\"testdata/microsoft-msrc-0.json\")\n\trequire.NoError(t, err)\n\tdefer testutil.CloseFile(f)\n\n\tentries, err := unmarshal.MSRCVulnerabilityEntries(f)\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, len(entries), 2)\n\n\tfor idx, entry := range entries {\n\t\tdataEntries, err := Transform(entry)\n\t\tassert.NoError(t, err)\n\t\tassert.Len(t, dataEntries, 2)\n\t\texpected := expectedVulns[idx]\n\t\tfor _, entry := range dataEntries {\n\t\t\tswitch vuln := entry.Data.(type) {\n\t\t\tcase db.Vulnerability:\n\t\t\t\tassert.Equal(t, expected.vulnerability, vuln)\n\t\t\tcase db.VulnerabilityMetadata:\n\t\t\t\tassert.Equal(t, expected.metadata, vuln)\n\t\t\tdefault:\n\t\t\t\tt.Fatalf(\"unexpected condition: data entry does not have a vulnerability or a metadata\")\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "grype/db/v5/build/transformers/nvd/testdata/CVE-2023-45283-platform-cpe-first.json",
    "content": "{\n  \"cve\": {\n    \"id\": \"CVE-2023-45283\",\n    \"sourceIdentifier\": \"security@golang.org\",\n    \"published\": \"2023-11-09T17:15:08.757\",\n    \"lastModified\": \"2023-12-14T10:15:07.947\",\n    \"vulnStatus\": \"Modified\",\n    \"cveTags\": [],\n    \"descriptions\": [\n      {\n        \"lang\": \"en\",\n        \"value\": \"The filepath package does not recognize paths with a \\\\??\\\\ prefix as special. On Windows, a path beginning with \\\\??\\\\ is a Root Local Device path equivalent to a path beginning with \\\\\\\\?\\\\. Paths with a \\\\??\\\\ prefix may be used to access arbitrary locations on the system. For example, the path \\\\??\\\\c:\\\\x is equivalent to the more common path c:\\\\x. Before fix, Clean could convert a rooted path such as \\\\a\\\\..\\\\??\\\\b into the root local device path \\\\??\\\\b. Clean will now convert this to .\\\\??\\\\b. Similarly, Join(\\\\, ??, b) could convert a seemingly innocent sequence of path elements into the root local device path \\\\??\\\\b. Join will now convert this to \\\\.\\\\??\\\\b. In addition, with fix, IsAbs now correctly reports paths beginning with \\\\??\\\\ as absolute, and VolumeName correctly reports the \\\\??\\\\ prefix as a volume name. UPDATE: Go 1.20.11 and Go 1.21.4 inadvertently changed the definition of the volume name in Windows paths starting with \\\\?, resulting in filepath.Clean(\\\\?\\\\c:) returning \\\\?\\\\c: rather than \\\\?\\\\c:\\\\ (among other effects). The previous behavior has been restored.\"\n      },\n      {\n        \"lang\": \"es\",\n        \"value\": \"El paquete filepath no reconoce las rutas con el prefijo \\\\??\\\\ como especiales. En Windows, una ruta que comienza con \\\\??\\\\ es una ruta de dispositivo local raíz equivalente a una ruta que comienza con \\\\\\\\?\\\\. Se pueden utilizar rutas con un prefijo \\\\??\\\\ para acceder a ubicaciones arbitrarias en el sistema. Por ejemplo, la ruta \\\\??\\\\c:\\\\x es equivalente a la ruta más común c:\\\\x. Antes de la solución, Clean podía convertir una ruta raíz como \\\\a\\\\..\\\\??\\\\b en la ruta raíz del dispositivo local \\\\??\\\\b. Clean ahora convertirá esto a .\\\\??\\\\b. De manera similar, Join(\\\\, ??, b) podría convertir una secuencia aparentemente inocente de elementos de ruta en la ruta del dispositivo local raíz \\\\??\\\\b. Unirse ahora convertirá esto a \\\\.\\\\??\\\\b. Además, con la solución, IsAbs ahora informa correctamente las rutas que comienzan con \\\\??\\\\ como absolutas, y VolumeName informa correctamente el prefijo \\\\??\\\\ como nombre de volumen.\"\n      }\n    ],\n    \"metrics\": {\n      \"cvssMetricV31\": [\n        {\n          \"source\": \"nvd@nist.gov\",\n          \"type\": \"Primary\",\n          \"cvssData\": {\n            \"version\": \"3.1\",\n            \"vectorString\": \"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N\",\n            \"attackVector\": \"NETWORK\",\n            \"attackComplexity\": \"LOW\",\n            \"privilegesRequired\": \"NONE\",\n            \"userInteraction\": \"NONE\",\n            \"scope\": \"UNCHANGED\",\n            \"confidentialityImpact\": \"HIGH\",\n            \"integrityImpact\": \"NONE\",\n            \"availabilityImpact\": \"NONE\",\n            \"baseScore\": 7.5,\n            \"baseSeverity\": \"HIGH\"\n          },\n          \"exploitabilityScore\": 3.9,\n          \"impactScore\": 3.6\n        }\n      ]\n    },\n    \"weaknesses\": [\n      {\n        \"source\": \"nvd@nist.gov\",\n        \"type\": \"Primary\",\n        \"description\": [\n          {\n            \"lang\": \"en\",\n            \"value\": \"CWE-22\"\n          }\n        ]\n      }\n    ],\n    \"configurations\": [\n      {\n        \"operator\": \"AND\",\n        \"nodes\": [\n          {\n            \"operator\": \"OR\",\n            \"negate\": false,\n            \"cpeMatch\": [\n              {\n                \"vulnerable\": false,\n                \"criteria\": \"cpe:2.3:o:microsoft:windows:-:*:*:*:*:*:*:*\",\n                \"matchCriteriaId\": \"A2572D17-1DE6-457B-99CC-64AFD54487EA\"\n              }\n            ]\n          },\n          {\n            \"operator\": \"OR\",\n            \"negate\": false,\n            \"cpeMatch\": [\n              {\n                \"vulnerable\": true,\n                \"criteria\": \"cpe:2.3:a:golang:go:*:*:*:*:*:*:*:*\",\n                \"versionEndExcluding\": \"1.20.11\",\n                \"matchCriteriaId\": \"C1E7C289-7484-4AA8-A96B-07D2E2933258\"\n              },\n              {\n                \"vulnerable\": true,\n                \"criteria\": \"cpe:2.3:a:golang:go:*:*:*:*:*:*:*:*\",\n                \"versionStartIncluding\": \"1.21.0-0\",\n                \"versionEndExcluding\": \"1.21.4\",\n                \"matchCriteriaId\": \"4E3FC16C-41B2-4900-901F-48BDA3DC9ED2\"\n              }\n            ]\n          }\n        ]\n      }\n    ],\n    \"references\": [\n      {\n        \"url\": \"http://www.openwall.com/lists/oss-security/2023/12/05/2\",\n        \"source\": \"security@golang.org\"\n      },\n      {\n        \"url\": \"https://go.dev/cl/540277\",\n        \"source\": \"security@golang.org\",\n        \"tags\": [\n          \"Issue Tracking\",\n          \"Vendor Advisory\"\n        ]\n      },\n      {\n        \"url\": \"https://go.dev/cl/541175\",\n        \"source\": \"security@golang.org\"\n      },\n      {\n        \"url\": \"https://go.dev/issue/63713\",\n        \"source\": \"security@golang.org\",\n        \"tags\": [\n          \"Issue Tracking\",\n          \"Vendor Advisory\"\n        ]\n      },\n      {\n        \"url\": \"https://go.dev/issue/64028\",\n        \"source\": \"security@golang.org\"\n      },\n      {\n        \"url\": \"https://groups.google.com/g/golang-announce/c/4tU8LZfBFkY\",\n        \"source\": \"security@golang.org\",\n        \"tags\": [\n          \"Issue Tracking\",\n          \"Mailing List\",\n          \"Vendor Advisory\"\n        ]\n      },\n      {\n        \"url\": \"https://groups.google.com/g/golang-dev/c/6ypN5EjibjM/m/KmLVYH_uAgAJ\",\n        \"source\": \"security@golang.org\"\n      },\n      {\n        \"url\": \"https://pkg.go.dev/vuln/GO-2023-2185\",\n        \"source\": \"security@golang.org\",\n        \"tags\": [\n          \"Issue Tracking\",\n          \"Vendor Advisory\"\n        ]\n      },\n      {\n        \"url\": \"https://security.netapp.com/advisory/ntap-20231214-0008/\",\n        \"source\": \"security@golang.org\"\n      }\n    ]\n  }\n}"
  },
  {
    "path": "grype/db/v5/build/transformers/nvd/testdata/CVE-2023-45283-platform-cpe-last.json",
    "content": "{\n  \"cve\": {\n    \"id\": \"CVE-2023-45283\",\n    \"sourceIdentifier\": \"security@golang.org\",\n    \"published\": \"2023-11-09T17:15:08.757\",\n    \"lastModified\": \"2023-12-14T10:15:07.947\",\n    \"vulnStatus\": \"Modified\",\n    \"descriptions\": [\n      {\n        \"lang\": \"en\",\n        \"value\": \"The filepath package does not recognize paths with a \\\\??\\\\ prefix as special. On Windows, a path beginning with \\\\??\\\\ is a Root Local Device path equivalent to a path beginning with \\\\\\\\?\\\\. Paths with a \\\\??\\\\ prefix may be used to access arbitrary locations on the system. For example, the path \\\\??\\\\c:\\\\x is equivalent to the more common path c:\\\\x. Before fix, Clean could convert a rooted path such as \\\\a\\\\..\\\\??\\\\b into the root local device path \\\\??\\\\b. Clean will now convert this to .\\\\??\\\\b. Similarly, Join(\\\\, ??, b) could convert a seemingly innocent sequence of path elements into the root local device path \\\\??\\\\b. Join will now convert this to \\\\.\\\\??\\\\b. In addition, with fix, IsAbs now correctly reports paths beginning with \\\\??\\\\ as absolute, and VolumeName correctly reports the \\\\??\\\\ prefix as a volume name. UPDATE: Go 1.20.11 and Go 1.21.4 inadvertently changed the definition of the volume name in Windows paths starting with \\\\?, resulting in filepath.Clean(\\\\?\\\\c:) returning \\\\?\\\\c: rather than \\\\?\\\\c:\\\\ (among other effects). The previous behavior has been restored.\"\n      },\n      {\n        \"lang\": \"es\",\n        \"value\": \"El paquete filepath no reconoce las rutas con el prefijo \\\\??\\\\ como especiales. En Windows, una ruta que comienza con \\\\??\\\\ es una ruta de dispositivo local raíz equivalente a una ruta que comienza con \\\\\\\\?\\\\. Se pueden utilizar rutas con un prefijo \\\\??\\\\ para acceder a ubicaciones arbitrarias en el sistema. Por ejemplo, la ruta \\\\??\\\\c:\\\\x es equivalente a la ruta más común c:\\\\x. Antes de la solución, Clean podía convertir una ruta raíz como \\\\a\\\\..\\\\??\\\\b en la ruta raíz del dispositivo local \\\\??\\\\b. Clean ahora convertirá esto a .\\\\??\\\\b. De manera similar, Join(\\\\, ??, b) podría convertir una secuencia aparentemente inocente de elementos de ruta en la ruta del dispositivo local raíz \\\\??\\\\b. Unirse ahora convertirá esto a \\\\.\\\\??\\\\b. Además, con la solución, IsAbs ahora informa correctamente las rutas que comienzan con \\\\??\\\\ como absolutas, y VolumeName informa correctamente el prefijo \\\\??\\\\ como nombre de volumen.\"\n      }\n    ],\n    \"metrics\": {\n      \"cvssMetricV31\": [\n        {\n          \"source\": \"nvd@nist.gov\",\n          \"type\": \"Primary\",\n          \"cvssData\": {\n            \"version\": \"3.1\",\n            \"vectorString\": \"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N\",\n            \"attackVector\": \"NETWORK\",\n            \"attackComplexity\": \"LOW\",\n            \"privilegesRequired\": \"NONE\",\n            \"userInteraction\": \"NONE\",\n            \"scope\": \"UNCHANGED\",\n            \"confidentialityImpact\": \"HIGH\",\n            \"integrityImpact\": \"NONE\",\n            \"availabilityImpact\": \"NONE\",\n            \"baseScore\": 7.5,\n            \"baseSeverity\": \"HIGH\"\n          },\n          \"exploitabilityScore\": 3.9,\n          \"impactScore\": 3.6\n        }\n      ]\n    },\n    \"weaknesses\": [\n      {\n        \"source\": \"nvd@nist.gov\",\n        \"type\": \"Primary\",\n        \"description\": [\n          {\n            \"lang\": \"en\",\n            \"value\": \"CWE-22\"\n          }\n        ]\n      }\n    ],\n    \"configurations\": [\n      {\n        \"operator\": \"AND\",\n        \"nodes\": [\n          {\n            \"operator\": \"OR\",\n            \"negate\": false,\n            \"cpeMatch\": [\n              {\n                \"vulnerable\": true,\n                \"criteria\": \"cpe:2.3:a:golang:go:*:*:*:*:*:*:*:*\",\n                \"versionEndExcluding\": \"1.20.11\",\n                \"matchCriteriaId\": \"C1E7C289-7484-4AA8-A96B-07D2E2933258\"\n              },\n              {\n                \"vulnerable\": true,\n                \"criteria\": \"cpe:2.3:a:golang:go:*:*:*:*:*:*:*:*\",\n                \"versionStartIncluding\": \"1.21.0-0\",\n                \"versionEndExcluding\": \"1.21.4\",\n                \"matchCriteriaId\": \"4E3FC16C-41B2-4900-901F-48BDA3DC9ED2\"\n              }\n            ]\n          },\n          {\n            \"operator\": \"OR\",\n            \"negate\": false,\n            \"cpeMatch\": [\n              {\n                \"vulnerable\": false,\n                \"criteria\": \"cpe:2.3:o:microsoft:windows:-:*:*:*:*:*:*:*\",\n                \"matchCriteriaId\": \"A2572D17-1DE6-457B-99CC-64AFD54487EA\"\n              }\n            ]\n          }\n        ]\n      }\n    ],\n    \"references\": [\n      {\n        \"url\": \"http://www.openwall.com/lists/oss-security/2023/12/05/2\",\n        \"source\": \"security@golang.org\"\n      },\n      {\n        \"url\": \"https://go.dev/cl/540277\",\n        \"source\": \"security@golang.org\",\n        \"tags\": [\n          \"Issue Tracking\",\n          \"Vendor Advisory\"\n        ]\n      },\n      {\n        \"url\": \"https://go.dev/cl/541175\",\n        \"source\": \"security@golang.org\"\n      },\n      {\n        \"url\": \"https://go.dev/issue/63713\",\n        \"source\": \"security@golang.org\",\n        \"tags\": [\n          \"Issue Tracking\",\n          \"Vendor Advisory\"\n        ]\n      },\n      {\n        \"url\": \"https://go.dev/issue/64028\",\n        \"source\": \"security@golang.org\"\n      },\n      {\n        \"url\": \"https://groups.google.com/g/golang-announce/c/4tU8LZfBFkY\",\n        \"source\": \"security@golang.org\",\n        \"tags\": [\n          \"Issue Tracking\",\n          \"Mailing List\",\n          \"Vendor Advisory\"\n        ]\n      },\n      {\n        \"url\": \"https://groups.google.com/g/golang-dev/c/6ypN5EjibjM/m/KmLVYH_uAgAJ\",\n        \"source\": \"security@golang.org\"\n      },\n      {\n        \"url\": \"https://pkg.go.dev/vuln/GO-2023-2185\",\n        \"source\": \"security@golang.org\",\n        \"tags\": [\n          \"Issue Tracking\",\n          \"Vendor Advisory\"\n        ]\n      },\n      {\n        \"url\": \"https://security.netapp.com/advisory/ntap-20231214-0008/\",\n        \"source\": \"security@golang.org\"\n      }\n    ]\n  }\n}"
  },
  {
    "path": "grype/db/v5/build/transformers/nvd/testdata/compound-pkg.json",
    "content": "{\n  \"cve\": {\n    \"id\": \"CVE-2018-10189\",\n    \"sourceIdentifier\": \"cve@mitre.org\",\n    \"published\": \"2018-04-17T20:29:00.410\",\n    \"lastModified\": \"2018-05-23T14:41:49.073\",\n    \"vulnStatus\": \"Analyzed\",\n    \"descriptions\": [\n      {\n        \"lang\": \"en\",\n        \"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.\"\n      },\n      {\n        \"lang\": \"es\",\n        \"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.\"\n      }\n    ],\n    \"metrics\": {\n      \"cvssMetricV30\": [\n        {\n          \"source\": \"nvd@nist.gov\",\n          \"type\": \"Primary\",\n          \"cvssData\": {\n            \"version\": \"3.0\",\n            \"vectorString\": \"CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N\",\n            \"attackVector\": \"NETWORK\",\n            \"attackComplexity\": \"LOW\",\n            \"privilegesRequired\": \"NONE\",\n            \"userInteraction\": \"NONE\",\n            \"scope\": \"UNCHANGED\",\n            \"confidentialityImpact\": \"HIGH\",\n            \"integrityImpact\": \"NONE\",\n            \"availabilityImpact\": \"NONE\",\n            \"baseScore\": 7.5,\n            \"baseSeverity\": \"HIGH\"\n          },\n          \"exploitabilityScore\": 3.9,\n          \"impactScore\": 3.6\n        }\n      ],\n      \"cvssMetricV2\": [\n        {\n          \"source\": \"nvd@nist.gov\",\n          \"type\": \"Primary\",\n          \"cvssData\": {\n            \"version\": \"2.0\",\n            \"vectorString\": \"AV:N/AC:L/Au:N/C:P/I:N/A:N\",\n            \"accessVector\": \"NETWORK\",\n            \"accessComplexity\": \"LOW\",\n            \"authentication\": \"NONE\",\n            \"confidentialityImpact\": \"PARTIAL\",\n            \"integrityImpact\": \"NONE\",\n            \"availabilityImpact\": \"NONE\",\n            \"baseScore\": 5.0\n          },\n          \"baseSeverity\": \"MEDIUM\",\n          \"exploitabilityScore\": 10.0,\n          \"impactScore\": 2.9,\n          \"acInsufInfo\": false,\n          \"obtainAllPrivilege\": false,\n          \"obtainUserPrivilege\": false,\n          \"obtainOtherPrivilege\": false,\n          \"userInteractionRequired\": false\n        }\n      ]\n    },\n    \"weaknesses\": [\n      {\n        \"source\": \"nvd@nist.gov\",\n        \"type\": \"Primary\",\n        \"description\": [\n          {\n            \"lang\": \"en\",\n            \"value\": \"CWE-200\"\n          }\n        ]\n      }\n    ],\n    \"configurations\": [\n      {\n        \"nodes\": [\n          {\n            \"operator\": \"OR\",\n            \"negate\": false,\n            \"cpeMatch\": [\n              {\n                \"vulnerable\": true,\n                \"criteria\": \"cpe:2.3:a:mautic:mautic:*:*:*:*:*:*:*:*\",\n                \"versionStartIncluding\": \"1.0.0\",\n                \"versionEndIncluding\": \"1.4.1\",\n                \"matchCriteriaId\": \"5779710D-099E-40EE-8DF3-55BD3179A50C\"\n              },\n              {\n                \"vulnerable\": true,\n                \"criteria\": \"cpe:2.3:a:mautic:mautic:*:*:*:*:*:*:*:*\",\n                \"versionStartIncluding\": \"2.0.0\",\n                \"versionEndExcluding\": \"2.13.0\",\n                \"matchCriteriaId\": \"4EFAEE48-4AEF-4F8C-95E0-6E8D848D900F\"\n              }\n            ]\n          }\n        ]\n      }\n    ],\n    \"references\": [\n      {\n        \"url\": \"https://github.com/mautic/mautic/releases/tag/2.13.0\",\n        \"source\": \"cve@mitre.org\",\n        \"tags\": [\n          \"Third Party Advisory\"\n        ]\n      }\n    ]\n  }\n}\n"
  },
  {
    "path": "grype/db/v5/build/transformers/nvd/testdata/cve-2020-10729.json",
    "content": "{\n  \"cve\": {\n    \"id\": \"CVE-2020-10729\",\n    \"sourceIdentifier\": \"secalert@redhat.com\",\n    \"published\": \"2021-05-27T19:15:07.880\",\n    \"lastModified\": \"2021-12-10T19:57:06.357\",\n    \"vulnStatus\": \"Analyzed\",\n    \"descriptions\": [\n      {\n        \"lang\": \"en\",\n        \"value\": \"A flaw was found in the use of insufficiently random values in Ansible. Two random password lookups of the same length generate the equal value as the template caching action for the same file since no re-evaluation happens. The highest threat from this vulnerability would be that all passwords are exposed at once for the file. This flaw affects Ansible Engine versions before 2.9.6.\"\n      },\n      {\n        \"lang\": \"es\",\n        \"value\": \"Se encontró un fallo en el uso de valores insuficientemente aleatorios en Ansible.&#xa0;Dos búsquedas de contraseñas aleatorias de la misma longitud generan el mismo valor que la acción de almacenamiento en caché de la plantilla para el mismo archivo, ya que no se realiza una reevaluación.&#xa0;La mayor amenaza de esta vulnerabilidad sería que todas las contraseñas estén expuestas a la vez para el archivo.&#xa0;Este fallo afecta a Ansible Engine versiones anteriores a 2.9.6\"\n      }\n    ],\n    \"metrics\": {\n      \"cvssMetricV31\": [\n        {\n          \"source\": \"nvd@nist.gov\",\n          \"type\": \"Primary\",\n          \"cvssData\": {\n            \"version\": \"3.1\",\n            \"vectorString\": \"CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N\",\n            \"attackVector\": \"LOCAL\",\n            \"attackComplexity\": \"LOW\",\n            \"privilegesRequired\": \"LOW\",\n            \"userInteraction\": \"NONE\",\n            \"scope\": \"UNCHANGED\",\n            \"confidentialityImpact\": \"HIGH\",\n            \"integrityImpact\": \"NONE\",\n            \"availabilityImpact\": \"NONE\",\n            \"baseScore\": 5.5,\n            \"baseSeverity\": \"MEDIUM\"\n          },\n          \"exploitabilityScore\": 1.8,\n          \"impactScore\": 3.6\n        }\n      ],\n      \"cvssMetricV2\": [\n        {\n          \"source\": \"nvd@nist.gov\",\n          \"type\": \"Primary\",\n          \"cvssData\": {\n            \"version\": \"2.0\",\n            \"vectorString\": \"AV:L/AC:L/Au:N/C:P/I:N/A:N\",\n            \"accessVector\": \"LOCAL\",\n            \"accessComplexity\": \"LOW\",\n            \"authentication\": \"NONE\",\n            \"confidentialityImpact\": \"PARTIAL\",\n            \"integrityImpact\": \"NONE\",\n            \"availabilityImpact\": \"NONE\",\n            \"baseScore\": 2.1\n          },\n          \"baseSeverity\": \"LOW\",\n          \"exploitabilityScore\": 3.9,\n          \"impactScore\": 2.9,\n          \"acInsufInfo\": false,\n          \"obtainAllPrivilege\": false,\n          \"obtainUserPrivilege\": false,\n          \"obtainOtherPrivilege\": false,\n          \"userInteractionRequired\": false\n        }\n      ]\n    },\n    \"weaknesses\": [\n      {\n        \"source\": \"nvd@nist.gov\",\n        \"type\": \"Primary\",\n        \"description\": [\n          {\n            \"lang\": \"en\",\n            \"value\": \"CWE-330\"\n          }\n        ]\n      },\n      {\n        \"source\": \"secalert@redhat.com\",\n        \"type\": \"Secondary\",\n        \"description\": [\n          {\n            \"lang\": \"en\",\n            \"value\": \"CWE-330\"\n          }\n        ]\n      }\n    ],\n    \"configurations\": [\n      {\n        \"operator\": \"AND\",\n        \"nodes\": [\n          {\n            \"operator\": \"OR\",\n            \"negate\": false,\n            \"cpeMatch\": [\n              {\n                \"vulnerable\": true,\n                \"criteria\": \"cpe:2.3:a:redhat:ansible_engine:*:*:*:*:*:*:*:*\",\n                \"versionEndExcluding\": \"2.9.6\",\n                \"matchCriteriaId\": \"EDFA8005-6FBE-4032-A499-608B7FA34F56\"\n              }\n            ]\n          },\n          {\n            \"operator\": \"OR\",\n            \"negate\": false,\n            \"cpeMatch\": [\n              {\n                \"vulnerable\": false,\n                \"criteria\": \"cpe:2.3:o:redhat:enterprise_linux:7.0:*:*:*:*:*:*:*\",\n                \"matchCriteriaId\": \"142AD0DD-4CF3-4D74-9442-459CE3347E3A\"\n              },\n              {\n                \"vulnerable\": false,\n                \"criteria\": \"cpe:2.3:o:redhat:enterprise_linux:8.0:*:*:*:*:*:*:*\",\n                \"matchCriteriaId\": \"F4CFF558-3C47-480D-A2F0-BABF26042943\"\n              }\n            ]\n          }\n        ]\n      },\n      {\n        \"nodes\": [\n          {\n            \"operator\": \"OR\",\n            \"negate\": false,\n            \"cpeMatch\": [\n              {\n                \"vulnerable\": true,\n                \"criteria\": \"cpe:2.3:o:debian:debian_linux:10.0:*:*:*:*:*:*:*\",\n                \"matchCriteriaId\": \"07B237A9-69A3-4A9C-9DA0-4E06BD37AE73\"\n              }\n            ]\n          }\n        ]\n      }\n    ],\n    \"references\": [\n      {\n        \"url\": \"https://bugzilla.redhat.com/show_bug.cgi?id=1831089\",\n        \"source\": \"secalert@redhat.com\",\n        \"tags\": [\n          \"Issue Tracking\",\n          \"Vendor Advisory\"\n        ]\n      },\n      {\n        \"url\": \"https://github.com/ansible/ansible/issues/34144\",\n        \"source\": \"secalert@redhat.com\",\n        \"tags\": [\n          \"Exploit\",\n          \"Issue Tracking\",\n          \"Third Party Advisory\"\n        ]\n      },\n      {\n        \"url\": \"https://www.debian.org/security/2021/dsa-4950\",\n        \"source\": \"secalert@redhat.com\",\n        \"tags\": [\n          \"Third Party Advisory\"\n        ]\n      }\n    ]\n  }\n}\n"
  },
  {
    "path": "grype/db/v5/build/transformers/nvd/testdata/cve-2022-0543.json",
    "content": "{\n  \"cve\": {\n    \"id\": \"CVE-2022-0543\",\n    \"sourceIdentifier\": \"security@debian.org\",\n    \"published\": \"2022-02-18T20:15:17.583\",\n    \"lastModified\": \"2023-09-29T15:55:24.533\",\n    \"vulnStatus\": \"Analyzed\",\n    \"cisaExploitAdd\": \"2022-03-28\",\n    \"cisaActionDue\": \"2022-04-18\",\n    \"cisaRequiredAction\": \"Apply updates per vendor instructions.\",\n    \"cisaVulnerabilityName\": \"Debian-specific Redis Server Lua Sandbox Escape Vulnerability\",\n    \"descriptions\": [\n      {\n        \"lang\": \"en\",\n        \"value\": \"It was discovered, that redis, a persistent key-value database, due to a packaging issue, is prone to a (Debian-specific) Lua sandbox escape, which could result in remote code execution.\"\n      },\n      {\n        \"lang\": \"es\",\n        \"value\": \"Se ha detectado que redis, una base de datos persistente de valores clave, debido a un problema de empaquetado, es propenso a un escape del sandbox de Lua (específico de Debian), que podría resultar en una ejecución de código remota\"\n      }\n    ],\n    \"metrics\": {\n      \"cvssMetricV31\": [\n        {\n          \"source\": \"nvd@nist.gov\",\n          \"type\": \"Primary\",\n          \"cvssData\": {\n            \"version\": \"3.1\",\n            \"vectorString\": \"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H\",\n            \"attackVector\": \"NETWORK\",\n            \"attackComplexity\": \"LOW\",\n            \"privilegesRequired\": \"NONE\",\n            \"userInteraction\": \"NONE\",\n            \"scope\": \"CHANGED\",\n            \"confidentialityImpact\": \"HIGH\",\n            \"integrityImpact\": \"HIGH\",\n            \"availabilityImpact\": \"HIGH\",\n            \"baseScore\": 10,\n            \"baseSeverity\": \"CRITICAL\"\n          },\n          \"exploitabilityScore\": 3.9,\n          \"impactScore\": 6\n        }\n      ],\n      \"cvssMetricV2\": [\n        {\n          \"source\": \"nvd@nist.gov\",\n          \"type\": \"Primary\",\n          \"cvssData\": {\n            \"version\": \"2.0\",\n            \"vectorString\": \"AV:N/AC:L/Au:N/C:C/I:C/A:C\",\n            \"accessVector\": \"NETWORK\",\n            \"accessComplexity\": \"LOW\",\n            \"authentication\": \"NONE\",\n            \"confidentialityImpact\": \"COMPLETE\",\n            \"integrityImpact\": \"COMPLETE\",\n            \"availabilityImpact\": \"COMPLETE\",\n            \"baseScore\": 10\n          },\n          \"baseSeverity\": \"HIGH\",\n          \"exploitabilityScore\": 10,\n          \"impactScore\": 10,\n          \"acInsufInfo\": false,\n          \"obtainAllPrivilege\": false,\n          \"obtainUserPrivilege\": false,\n          \"obtainOtherPrivilege\": false,\n          \"userInteractionRequired\": false\n        }\n      ]\n    },\n    \"weaknesses\": [\n      {\n        \"source\": \"nvd@nist.gov\",\n        \"type\": \"Primary\",\n        \"description\": [\n          {\n            \"lang\": \"en\",\n            \"value\": \"CWE-862\"\n          }\n        ]\n      }\n    ],\n    \"configurations\": [\n      {\n        \"operator\": \"AND\",\n        \"nodes\": [\n          {\n            \"operator\": \"OR\",\n            \"negate\": false,\n            \"cpeMatch\": [\n              {\n                \"vulnerable\": true,\n                \"criteria\": \"cpe:2.3:a:redis:redis:-:*:*:*:*:*:*:*\",\n                \"matchCriteriaId\": \"5EBE5E1C-C881-4A76-9E36-4FB7C48427E6\"\n              }\n            ]\n          },\n          {\n            \"operator\": \"OR\",\n            \"negate\": false,\n            \"cpeMatch\": [\n              {\n                \"vulnerable\": false,\n                \"criteria\": \"cpe:2.3:o:canonical:ubuntu_linux:20.04:*:*:*:lts:*:*:*\",\n                \"matchCriteriaId\": \"902B8056-9E37-443B-8905-8AA93E2447FB\"\n              },\n              {\n                \"vulnerable\": false,\n                \"criteria\": \"cpe:2.3:o:canonical:ubuntu_linux:21.10:*:*:*:-:*:*:*\",\n                \"matchCriteriaId\": \"3D94DA3B-FA74-4526-A0A0-A872684598C6\"\n              },\n              {\n                \"vulnerable\": false,\n                \"criteria\": \"cpe:2.3:o:debian:debian_linux:9.0:*:*:*:*:*:*:*\",\n                \"matchCriteriaId\": \"DEECE5FC-CACF-4496-A3E7-164736409252\"\n              },\n              {\n                \"vulnerable\": false,\n                \"criteria\": \"cpe:2.3:o:debian:debian_linux:10.0:*:*:*:*:*:*:*\",\n                \"matchCriteriaId\": \"07B237A9-69A3-4A9C-9DA0-4E06BD37AE73\"\n              },\n              {\n                \"vulnerable\": false,\n                \"criteria\": \"cpe:2.3:o:debian:debian_linux:11.0:*:*:*:*:*:*:*\",\n                \"matchCriteriaId\": \"FA6FEEC2-9F11-4643-8827-749718254FED\"\n              }\n            ]\n          }\n        ]\n      }\n    ],\n    \"references\": [\n      {\n        \"url\": \"http://packetstormsecurity.com/files/166885/Redis-Lua-Sandbox-Escape.html\",\n        \"source\": \"security@debian.org\",\n        \"tags\": [\n          \"Exploit\",\n          \"Third Party Advisory\",\n          \"VDB Entry\"\n        ]\n      },\n      {\n        \"url\": \"https://bugs.debian.org/1005787\",\n        \"source\": \"security@debian.org\",\n        \"tags\": [\n          \"Issue Tracking\",\n          \"Patch\",\n          \"Third Party Advisory\"\n        ]\n      },\n      {\n        \"url\": \"https://lists.debian.org/debian-security-announce/2022/msg00048.html\",\n        \"source\": \"security@debian.org\",\n        \"tags\": [\n          \"Mailing List\",\n          \"Third Party Advisory\"\n        ]\n      },\n      {\n        \"url\": \"https://security.netapp.com/advisory/ntap-20220331-0004/\",\n        \"source\": \"security@debian.org\",\n        \"tags\": [\n          \"Third Party Advisory\"\n        ]\n      },\n      {\n        \"url\": \"https://www.debian.org/security/2022/dsa-5081\",\n        \"source\": \"security@debian.org\",\n        \"tags\": [\n          \"Mailing List\",\n          \"Third Party Advisory\"\n        ]\n      },\n      {\n        \"url\": \"https://www.ubercomp.com/posts/2022-01-20_redis_on_debian_rce\",\n        \"source\": \"security@debian.org\",\n        \"tags\": [\n          \"Third Party Advisory\"\n        ]\n      }\n    ]\n  }\n}\n"
  },
  {
    "path": "grype/db/v5/build/transformers/nvd/testdata/invalid_cpe.json",
    "content": "{\n \"cve\": {\n  \"id\": \"CVE-2015-8978\",\n  \"sourceIdentifier\": \"cve@mitre.org\",\n  \"published\": \"2016-11-22T17:59:00.180\",\n  \"lastModified\": \"2016-11-28T19:50:59.600\",\n  \"vulnStatus\": \"Modified\",\n  \"descriptions\": [\n   {\n    \"lang\": \"en\",\n    \"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.\"\n   },\n   {\n    \"lang\": \"es\",\n    \"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.\"\n   }\n  ],\n  \"metrics\": {\n   \"cvssMetricV30\": [\n    {\n     \"source\": \"nvd@nist.gov\",\n     \"type\": \"Primary\",\n     \"cvssData\": {\n      \"version\": \"3.0\",\n      \"vectorString\": \"CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H\",\n      \"attackVector\": \"NETWORK\",\n      \"attackComplexity\": \"LOW\",\n      \"privilegesRequired\": \"NONE\",\n      \"userInteraction\": \"NONE\",\n      \"scope\": \"UNCHANGED\",\n      \"confidentialityImpact\": \"NONE\",\n      \"integrityImpact\": \"NONE\",\n      \"availabilityImpact\": \"HIGH\",\n      \"baseScore\": 7.5,\n      \"baseSeverity\": \"HIGH\"\n     },\n     \"exploitabilityScore\": 3.9,\n     \"impactScore\": 3.6\n    }\n   ],\n   \"cvssMetricV2\": [\n    {\n     \"source\": \"nvd@nist.gov\",\n     \"type\": \"Primary\",\n     \"cvssData\": {\n      \"version\": \"2.0\",\n      \"vectorString\": \"AV:N/AC:L/Au:N/C:N/I:N/A:P\",\n      \"accessVector\": \"NETWORK\",\n      \"accessComplexity\": \"LOW\",\n      \"authentication\": \"NONE\",\n      \"confidentialityImpact\": \"NONE\",\n      \"integrityImpact\": \"NONE\",\n      \"availabilityImpact\": \"PARTIAL\",\n      \"baseScore\": 5.0\n     },\n     \"baseSeverity\": \"MEDIUM\",\n     \"exploitabilityScore\": 10.0,\n     \"impactScore\": 2.9,\n     \"acInsufInfo\": false,\n     \"obtainAllPrivilege\": false,\n     \"obtainUserPrivilege\": false,\n     \"obtainOtherPrivilege\": false,\n     \"userInteractionRequired\": false\n    }\n   ]\n  },\n  \"weaknesses\": [\n   {\n    \"source\": \"nvd@nist.gov\",\n    \"type\": \"Primary\",\n    \"description\": [\n     {\n      \"lang\": \"en\",\n      \"value\": \"CWE-399\"\n     }\n    ]\n   }\n  ],\n  \"configurations\": [\n   {\n    \"nodes\": [\n     {\n      \"operator\": \"OR\",\n      \"negate\": false,\n      \"cpeMatch\": [\n       {\n        \"vulnerable\": true,\n        \"criteria\": \"cpe:2.3:a:soap::lite_project:soap::lite:*:*:*:*:*:perl:*:*\",\n        \"versionEndIncluding\": \"1.14\",\n        \"matchCriteriaId\": \"FB4DACB9-2E9E-4CBE-825F-FC0303D8CC86\"\n       }\n      ]\n     }\n    ]\n   }\n  ],\n  \"references\": [\n   {\n    \"url\": \"http://cpansearch.perl.org/src/PHRED/SOAP-Lite-1.20/Changes\",\n    \"source\": \"cve@mitre.org\",\n    \"tags\": [\n     \"Vendor Advisory\"\n    ]\n   },\n   {\n    \"url\": \"http://www.securityfocus.com/bid/94487\",\n    \"source\": \"cve@mitre.org\"\n   }\n  ]\n }\n}\n"
  },
  {
    "path": "grype/db/v5/build/transformers/nvd/testdata/multiple-platforms-with-application-cpe.json",
    "content": "{\n  \"cve\": {\n    \"id\": \"CVE-2023-38733\",\n    \"sourceIdentifier\": \"psirt@us.ibm.com\",\n    \"published\": \"2023-08-22T22:15:08.460\",\n    \"lastModified\": \"2023-08-26T02:25:42.957\",\n    \"vulnStatus\": \"Analyzed\",\n    \"descriptions\": [\n      {\n        \"lang\": \"en\",\n        \"value\": \"\\nIBM Robotic Process Automation 21.0.0 through 21.0.7.1 and 23.0.0 through 23.0.1 server could allow an authenticated user to view sensitive information from installation logs.  IBM X-Force Id:  262293.\\n\\n\"\n      }\n    ],\n    \"metrics\": {\n      \"cvssMetricV31\": [\n        {\n          \"source\": \"nvd@nist.gov\",\n          \"type\": \"Primary\",\n          \"cvssData\": {\n            \"version\": \"3.1\",\n            \"vectorString\": \"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N\",\n            \"attackVector\": \"NETWORK\",\n            \"attackComplexity\": \"LOW\",\n            \"privilegesRequired\": \"LOW\",\n            \"userInteraction\": \"NONE\",\n            \"scope\": \"UNCHANGED\",\n            \"confidentialityImpact\": \"LOW\",\n            \"integrityImpact\": \"NONE\",\n            \"availabilityImpact\": \"NONE\",\n            \"baseScore\": 4.3,\n            \"baseSeverity\": \"MEDIUM\"\n          },\n          \"exploitabilityScore\": 2.8,\n          \"impactScore\": 1.4\n        },\n        {\n          \"source\": \"psirt@us.ibm.com\",\n          \"type\": \"Secondary\",\n          \"cvssData\": {\n            \"version\": \"3.1\",\n            \"vectorString\": \"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N\",\n            \"attackVector\": \"NETWORK\",\n            \"attackComplexity\": \"LOW\",\n            \"privilegesRequired\": \"LOW\",\n            \"userInteraction\": \"NONE\",\n            \"scope\": \"UNCHANGED\",\n            \"confidentialityImpact\": \"LOW\",\n            \"integrityImpact\": \"NONE\",\n            \"availabilityImpact\": \"NONE\",\n            \"baseScore\": 4.3,\n            \"baseSeverity\": \"MEDIUM\"\n          },\n          \"exploitabilityScore\": 2.8,\n          \"impactScore\": 1.4\n        }\n      ]\n    },\n    \"weaknesses\": [\n      {\n        \"source\": \"nvd@nist.gov\",\n        \"type\": \"Primary\",\n        \"description\": [\n          {\n            \"lang\": \"en\",\n            \"value\": \"CWE-532\"\n          }\n        ]\n      },\n      {\n        \"source\": \"psirt@us.ibm.com\",\n        \"type\": \"Secondary\",\n        \"description\": [\n          {\n            \"lang\": \"en\",\n            \"value\": \"CWE-532\"\n          }\n        ]\n      }\n    ],\n    \"configurations\": [\n      {\n        \"operator\": \"AND\",\n        \"nodes\": [\n          {\n            \"operator\": \"OR\",\n            \"negate\": false,\n            \"cpeMatch\": [\n              {\n                \"vulnerable\": true,\n                \"criteria\": \"cpe:2.3:a:ibm:robotic_process_automation:*:*:*:*:*:*:*:*\",\n                \"versionStartIncluding\": \"21.0.0\",\n                \"versionEndIncluding\": \"21.0.7.3\",\n                \"matchCriteriaId\": \"DDF503DD-23DC-4B22-8873-BE94BF0F1CD1\"\n              },\n              {\n                \"vulnerable\": true,\n                \"criteria\": \"cpe:2.3:a:ibm:robotic_process_automation:*:*:*:*:*:*:*:*\",\n                \"versionStartIncluding\": \"23.0.0\",\n                \"versionEndIncluding\": \"23.0.3\",\n                \"matchCriteriaId\": \"F513AA2B-F457-408B-8D5F-EBE657439000\"\n              }\n            ]\n          },\n          {\n            \"operator\": \"OR\",\n            \"negate\": false,\n            \"cpeMatch\": [\n              {\n                \"vulnerable\": false,\n                \"criteria\": \"cpe:2.3:a:redhat:openshift:-:*:*:*:*:*:*:*\",\n                \"matchCriteriaId\": \"F08E234C-BDCF-4B41-87B9-96BD5578CBBF\"\n              },\n              {\n                \"vulnerable\": false,\n                \"criteria\": \"cpe:2.3:o:microsoft:windows:-:*:*:*:*:*:*:*\",\n                \"matchCriteriaId\": \"A2572D17-1DE6-457B-99CC-64AFD54487EA\"\n              }\n            ]\n          }\n        ]\n      }\n    ],\n    \"references\": [\n      {\n        \"url\": \"https://exchange.xforce.ibmcloud.com/vulnerabilities/262293\",\n        \"source\": \"psirt@us.ibm.com\",\n        \"tags\": [\n          \"VDB Entry\",\n          \"Vendor Advisory\"\n        ]\n      },\n      {\n        \"url\": \"https://www.ibm.com/support/pages/node/7028223\",\n        \"source\": \"psirt@us.ibm.com\",\n        \"tags\": [\n          \"Patch\",\n          \"Vendor Advisory\"\n        ]\n      }\n    ]\n  }\n}\n"
  },
  {
    "path": "grype/db/v5/build/transformers/nvd/testdata/platform-cpe.json",
    "content": "{\n    \"cve\": {\n      \"id\": \"CVE-2022-26488\",\n      \"sourceIdentifier\": \"cve@mitre.org\",\n      \"published\": \"2022-03-10T17:47:45.383\",\n      \"lastModified\": \"2022-09-03T03:34:19.933\",\n      \"vulnStatus\": \"Analyzed\",\n      \"descriptions\": [\n        {\n          \"lang\": \"en\",\n          \"value\": \"In Python before 3.10.3 on Windows, local users can gain privileges because the search path is inadequately secured. The installer may allow a local attacker to add user-writable directories to the system search path. To exploit, an administrator must have installed Python for all users and enabled PATH entries. A non-administrative user can trigger a repair that incorrectly adds user-writable paths into PATH, enabling search-path hijacking of other users and system services. This affects Python (CPython) through 3.7.12, 3.8.x through 3.8.12, 3.9.x through 3.9.10, and 3.10.x through 3.10.2.\"\n        },\n        {\n          \"lang\": \"es\",\n          \"value\": \"En Python versiones anteriores a 3.10.3 en Windows, los usuarios locales pueden alcanzar privilegios porque la ruta de búsqueda no está asegurada apropiadamente. El instalador puede permitir a un atacante local añadir directorios escribibles por el usuario a la ruta de búsqueda del sistema. Para explotarla, un administrador debe haber instalado Python para todos los usuarios y habilitar las entradas PATH. Un usuario no administrador puede desencadenar una reparación que añada incorrectamente rutas escribibles por el usuario en el PATH, permitiendo el secuestro de la ruta de búsqueda de otros usuarios y servicios del sistema. Esto afecta a Python (CPython) versiones hasta 3.7.12, versiones 3.8.x hasta 3.8.12, versiones 3.9.x hasta 3.9.10, y versiones 3.10.x hasta 3.10.2\"\n        }\n      ],\n      \"metrics\": {\n        \"cvssMetricV31\": [\n          {\n            \"source\": \"nvd@nist.gov\",\n            \"type\": \"Primary\",\n            \"cvssData\": {\n              \"version\": \"3.1\",\n              \"vectorString\": \"CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H\",\n              \"attackVector\": \"LOCAL\",\n              \"attackComplexity\": \"HIGH\",\n              \"privilegesRequired\": \"LOW\",\n              \"userInteraction\": \"NONE\",\n              \"scope\": \"UNCHANGED\",\n              \"confidentialityImpact\": \"HIGH\",\n              \"integrityImpact\": \"HIGH\",\n              \"availabilityImpact\": \"HIGH\",\n              \"baseScore\": 7,\n              \"baseSeverity\": \"HIGH\"\n            },\n            \"exploitabilityScore\": 1,\n            \"impactScore\": 5.9\n          }\n        ],\n        \"cvssMetricV2\": [\n          {\n            \"source\": \"nvd@nist.gov\",\n            \"type\": \"Primary\",\n            \"cvssData\": {\n              \"version\": \"2.0\",\n              \"vectorString\": \"AV:L/AC:M/Au:N/C:P/I:P/A:P\",\n              \"accessVector\": \"LOCAL\",\n              \"accessComplexity\": \"MEDIUM\",\n              \"authentication\": \"NONE\",\n              \"confidentialityImpact\": \"PARTIAL\",\n              \"integrityImpact\": \"PARTIAL\",\n              \"availabilityImpact\": \"PARTIAL\",\n              \"baseScore\": 4.4\n            },\n            \"baseSeverity\": \"MEDIUM\",\n            \"exploitabilityScore\": 3.4,\n            \"impactScore\": 6.4,\n            \"acInsufInfo\": false,\n            \"obtainAllPrivilege\": false,\n            \"obtainUserPrivilege\": false,\n            \"obtainOtherPrivilege\": false,\n            \"userInteractionRequired\": false\n          }\n        ]\n      },\n      \"weaknesses\": [\n        {\n          \"source\": \"nvd@nist.gov\",\n          \"type\": \"Primary\",\n          \"description\": [\n            {\n              \"lang\": \"en\",\n              \"value\": \"CWE-426\"\n            }\n          ]\n        }\n      ],\n      \"configurations\": [\n        {\n          \"operator\": \"AND\",\n          \"nodes\": [\n            {\n              \"operator\": \"OR\",\n              \"negate\": false,\n              \"cpeMatch\": [\n                {\n                  \"vulnerable\": true,\n                  \"criteria\": \"cpe:2.3:a:python:python:*:*:*:*:*:*:*:*\",\n                  \"versionEndIncluding\": \"3.7.12\",\n                  \"matchCriteriaId\": \"1E05F88A-70C2-4DB6-9CCC-1D599AD26D4C\"\n                },\n                {\n                  \"vulnerable\": true,\n                  \"criteria\": \"cpe:2.3:a:python:python:*:*:*:*:*:*:*:*\",\n                  \"versionStartIncluding\": \"3.8.0\",\n                  \"versionEndIncluding\": \"3.8.12\",\n                  \"matchCriteriaId\": \"E80CA0FB-E708-4E92-BF36-7267F799FF8D\"\n                },\n                {\n                  \"vulnerable\": true,\n                  \"criteria\": \"cpe:2.3:a:python:python:*:*:*:*:*:*:*:*\",\n                  \"versionStartIncluding\": \"3.9.0\",\n                  \"versionEndIncluding\": \"3.9.10\",\n                  \"matchCriteriaId\": \"DD4B9F29-F505-4721-A630-C75103942F29\"\n                },\n                {\n                  \"vulnerable\": true,\n                  \"criteria\": \"cpe:2.3:a:python:python:*:*:*:*:*:*:*:*\",\n                  \"versionStartIncluding\": \"3.10.0\",\n                  \"versionEndIncluding\": \"3.10.2\",\n                  \"matchCriteriaId\": \"D5B55D1D-031C-4006-A368-BB66C2057916\"\n                },\n                {\n                  \"vulnerable\": true,\n                  \"criteria\": \"cpe:2.3:a:python:python:3.11.0:alpha1:*:*:*:*:*:*\",\n                  \"matchCriteriaId\": \"514A577E-5E60-40BA-ABD0-A8C5EB28BD90\"\n                },\n                {\n                  \"vulnerable\": true,\n                  \"criteria\": \"cpe:2.3:a:python:python:3.11.0:alpha2:*:*:*:*:*:*\",\n                  \"matchCriteriaId\": \"83B71795-9C81-4E5F-967C-C11808F24B05\"\n                },\n                {\n                  \"vulnerable\": true,\n                  \"criteria\": \"cpe:2.3:a:python:python:3.11.0:alpha3:*:*:*:*:*:*\",\n                  \"matchCriteriaId\": \"3F6F71F3-299E-4A4B-ADD1-EAD5A1D433E2\"\n                },\n                {\n                  \"vulnerable\": true,\n                  \"criteria\": \"cpe:2.3:a:python:python:3.11.0:alpha4:*:*:*:*:*:*\",\n                  \"matchCriteriaId\": \"09BBF4E9-EA54-41B5-948E-8E3D2660B7EF\"\n                },\n                {\n                  \"vulnerable\": true,\n                  \"criteria\": \"cpe:2.3:a:python:python:3.11.0:alpha4:*:*:*:*:*:*\",\n                  \"matchCriteriaId\": \"D9BBF4E9-EA54-41B5-948E-8E3D2660B7EF\"\n                },\n                {\n                  \"vulnerable\": true,\n                  \"criteria\": \"cpe:2.3:a:python:python:3.11.0:alpha5:*:*:*:*:*:*\",\n                  \"matchCriteriaId\": \"AEBFDCE7-81D4-4741-BB88-12C704515F5C\"\n                },\n                {\n                  \"vulnerable\": true,\n                  \"criteria\": \"cpe:2.3:a:python:python:3.11.0:alpha6:*:*:*:*:*:*\",\n                  \"matchCriteriaId\": \"156EB4C2-EFB7-4CEB-804D-93DB62992A63\"\n                }\n              ]\n            },\n            {\n              \"operator\": \"OR\",\n              \"negate\": false,\n              \"cpeMatch\": [\n                {\n                  \"vulnerable\": false,\n                  \"criteria\": \"cpe:2.3:o:microsoft:windows:-:*:*:*:*:*:*:*\",\n                  \"matchCriteriaId\": \"A2572D17-1DE6-457B-99CC-64AFD54487EA\"\n                }\n              ]\n            }\n          ]\n        },\n        {\n          \"operator\": \"AND\",\n          \"nodes\": [\n            {\n              \"operator\": \"OR\",\n              \"negate\": false,\n              \"cpeMatch\": [\n                {\n                  \"vulnerable\": true,\n                  \"criteria\": \"cpe:2.3:a:netapp:active_iq_unified_manager:-:*:*:*:*:windows:*:*\",\n                  \"matchCriteriaId\": \"B55E8D50-99B4-47EC-86F9-699B67D473CE\"\n                },\n                {\n                  \"vulnerable\": true,\n                  \"criteria\": \"cpe:2.3:a:netapp:ontap_select_deploy_administration_utility:-:*:*:*:*:*:*:*\",\n                  \"matchCriteriaId\": \"E7CF3019-975D-40BB-A8A4-894E62BD3797\"\n                }\n              ]\n            }\n          ]\n        }\n      ],\n      \"references\": [\n        {\n          \"url\": \"https://mail.python.org/archives/list/security-announce@python.org/thread/657Z4XULWZNIY5FRP3OWXHYKUSIH6DMN/\",\n          \"source\": \"cve@mitre.org\",\n          \"tags\": [\n            \"Patch\",\n            \"Vendor Advisory\"\n          ]\n        },\n        {\n          \"url\": \"https://security.netapp.com/advisory/ntap-20220419-0005/\",\n          \"source\": \"cve@mitre.org\",\n          \"tags\": [\n            \"Third Party Advisory\"\n          ]\n        }\n      ]\n    }\n  }\n"
  },
  {
    "path": "grype/db/v5/build/transformers/nvd/testdata/single-package-multi-distro.json",
    "content": "{\n  \"cve\": {\n    \"id\": \"CVE-2018-1000222\",\n    \"sourceIdentifier\": \"cve@mitre.org\",\n    \"published\": \"2018-08-20T20:29:01.347\",\n    \"lastModified\": \"2020-03-31T02:15:12.667\",\n    \"vulnStatus\": \"Modified\",\n    \"descriptions\": [\n      {\n        \"lang\": \"en\",\n        \"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.\"\n      },\n      {\n        \"lang\": \"es\",\n        \"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.\"\n      }\n    ],\n    \"metrics\": {\n      \"cvssMetricV30\": [\n        {\n          \"source\": \"nvd@nist.gov\",\n          \"type\": \"Primary\",\n          \"cvssData\": {\n            \"version\": \"3.0\",\n            \"vectorString\": \"CVSS:3.0/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H\",\n            \"attackVector\": \"NETWORK\",\n            \"attackComplexity\": \"LOW\",\n            \"privilegesRequired\": \"NONE\",\n            \"userInteraction\": \"REQUIRED\",\n            \"scope\": \"UNCHANGED\",\n            \"confidentialityImpact\": \"HIGH\",\n            \"integrityImpact\": \"HIGH\",\n            \"availabilityImpact\": \"HIGH\",\n            \"baseScore\": 8.8,\n            \"baseSeverity\": \"HIGH\"\n          },\n          \"exploitabilityScore\": 2.8,\n          \"impactScore\": 5.9\n        }\n      ],\n      \"cvssMetricV2\": [\n        {\n          \"source\": \"nvd@nist.gov\",\n          \"type\": \"Primary\",\n          \"cvssData\": {\n            \"version\": \"2.0\",\n            \"vectorString\": \"AV:N/AC:M/Au:N/C:P/I:P/A:P\",\n            \"accessVector\": \"NETWORK\",\n            \"accessComplexity\": \"MEDIUM\",\n            \"authentication\": \"NONE\",\n            \"confidentialityImpact\": \"PARTIAL\",\n            \"integrityImpact\": \"PARTIAL\",\n            \"availabilityImpact\": \"PARTIAL\",\n            \"baseScore\": 6.8\n          },\n          \"baseSeverity\": \"MEDIUM\",\n          \"exploitabilityScore\": 8.6,\n          \"impactScore\": 6.4,\n          \"acInsufInfo\": false,\n          \"obtainAllPrivilege\": false,\n          \"obtainUserPrivilege\": false,\n          \"obtainOtherPrivilege\": false,\n          \"userInteractionRequired\": true\n        }\n      ]\n    },\n    \"weaknesses\": [\n      {\n        \"source\": \"nvd@nist.gov\",\n        \"type\": \"Primary\",\n        \"description\": [\n          {\n            \"lang\": \"en\",\n            \"value\": \"CWE-415\"\n          }\n        ]\n      }\n    ],\n    \"configurations\": [\n      {\n        \"nodes\": [\n          {\n            \"operator\": \"OR\",\n            \"negate\": false,\n            \"cpeMatch\": [\n              {\n                \"vulnerable\": true,\n                \"criteria\": \"cpe:2.3:a:libgd:libgd:2.2.5:*:*:*:*:*:*:*\",\n                \"matchCriteriaId\": \"C257CC1C-BF6A-4125-AA61-9C2D09096084\"\n              }\n            ]\n          }\n        ]\n      },\n      {\n        \"nodes\": [\n          {\n            \"operator\": \"OR\",\n            \"negate\": false,\n            \"cpeMatch\": [\n              {\n                \"vulnerable\": true,\n                \"criteria\": \"cpe:2.3:o:canonical:ubuntu_linux:14.04:*:*:*:lts:*:*:*\",\n                \"matchCriteriaId\": \"B5A6F2F3-4894-4392-8296-3B8DD2679084\"\n              },\n              {\n                \"vulnerable\": true,\n                \"criteria\": \"cpe:2.3:o:canonical:ubuntu_linux:16.04:*:*:*:lts:*:*:*\",\n                \"matchCriteriaId\": \"F7016A2A-8365-4F1A-89A2-7A19F2BCAE5B\"\n              },\n              {\n                \"vulnerable\": true,\n                \"criteria\": \"cpe:2.3:o:canonical:ubuntu_linux:18.04:*:*:*:lts:*:*:*\",\n                \"matchCriteriaId\": \"23A7C53F-B80F-4E6A-AFA9-58EEA84BE11D\"\n              }\n            ]\n          }\n        ]\n      },\n      {\n        \"nodes\": [\n          {\n            \"operator\": \"OR\",\n            \"negate\": false,\n            \"cpeMatch\": [\n              {\n                \"vulnerable\": true,\n                \"criteria\": \"cpe:2.3:o:debian:debian_linux:8.0:*:*:*:*:*:*:*\",\n                \"matchCriteriaId\": \"C11E6FB0-C8C0-4527-9AA0-CB9B316F8F43\"\n              }\n            ]\n          }\n        ]\n      }\n    ],\n    \"references\": [\n      {\n        \"url\": \"https://github.com/libgd/libgd/issues/447\",\n        \"source\": \"cve@mitre.org\",\n        \"tags\": [\n          \"Issue Tracking\",\n          \"Third Party Advisory\"\n        ]\n      },\n      {\n        \"url\": \"https://lists.debian.org/debian-lts-announce/2019/01/msg00028.html\",\n        \"source\": \"cve@mitre.org\",\n        \"tags\": [\n          \"Mailing List\",\n          \"Third Party Advisory\"\n        ]\n      },\n      {\n        \"url\": \"https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/3CZ2QADQTKRHTGB2AHD7J4QQNDLBEMM6/\",\n        \"source\": \"cve@mitre.org\"\n      },\n      {\n        \"url\": \"https://security.gentoo.org/glsa/201903-18\",\n        \"source\": \"cve@mitre.org\",\n        \"tags\": [\n          \"Third Party Advisory\"\n        ]\n      },\n      {\n        \"url\": \"https://usn.ubuntu.com/3755-1/\",\n        \"source\": \"cve@mitre.org\",\n        \"tags\": [\n          \"Mitigation\",\n          \"Third Party Advisory\"\n        ]\n      }\n    ]\n  }\n}\n"
  },
  {
    "path": "grype/db/v5/build/transformers/nvd/testdata/unmarshal-test.json",
    "content": "{\n  \"cve\": {\n    \"id\": \"CVE-2003-0349\",\n    \"sourceIdentifier\": \"cve@mitre.org\",\n    \"published\": \"2003-07-24T04:00:00.000\",\n    \"lastModified\": \"2018-10-12T21:32:41.083\",\n    \"vulnStatus\": \"Modified\",\n    \"descriptions\": [\n      {\n        \"lang\": \"en\",\n        \"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.\"\n      },\n      {\n        \"lang\": \"es\",\n        \"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.\"\n      }\n    ],\n    \"metrics\": {\n      \"cvssMetricV2\": [\n        {\n          \"source\": \"nvd@nist.gov\",\n          \"type\": \"Primary\",\n          \"cvssData\": {\n            \"version\": \"2.0\",\n            \"vectorString\": \"AV:N/AC:L/Au:N/C:P/I:P/A:P\",\n            \"accessVector\": \"NETWORK\",\n            \"accessComplexity\": \"LOW\",\n            \"authentication\": \"NONE\",\n            \"confidentialityImpact\": \"PARTIAL\",\n            \"integrityImpact\": \"PARTIAL\",\n            \"availabilityImpact\": \"PARTIAL\",\n            \"baseScore\": 7.5\n          },\n          \"baseSeverity\": \"HIGH\",\n          \"exploitabilityScore\": 10.0,\n          \"impactScore\": 6.4,\n          \"acInsufInfo\": false,\n          \"obtainAllPrivilege\": false,\n          \"obtainUserPrivilege\": true,\n          \"obtainOtherPrivilege\": false,\n          \"userInteractionRequired\": false\n        }\n      ]\n    },\n    \"weaknesses\": [\n      {\n        \"source\": \"nvd@nist.gov\",\n        \"type\": \"Primary\",\n        \"description\": [\n          {\n            \"lang\": \"en\",\n            \"value\": \"NVD-CWE-Other\"\n          }\n        ]\n      }\n    ],\n    \"configurations\": [\n      {\n        \"nodes\": [\n          {\n            \"operator\": \"OR\",\n            \"negate\": false,\n            \"cpeMatch\": [\n              {\n                \"vulnerable\": true,\n                \"criteria\": \"cpe:2.3:o:microsoft:windows_2000:*:*:*:*:*:*:*:*\",\n                \"matchCriteriaId\": \"4E545C63-FE9C-4CA1-AF0F-D999D84D2AFD\"\n              }\n            ]\n          }\n        ]\n      }\n    ],\n    \"references\": [\n      {\n        \"url\": \"http://marc.info/?l=bugtraq&m=105665030925504&w=2\",\n        \"source\": \"cve@mitre.org\"\n      },\n      {\n        \"url\": \"http://securitytracker.com/id?1007059\",\n        \"source\": \"cve@mitre.org\"\n      },\n      {\n        \"url\": \"http://www.kb.cert.org/vuls/id/113716\",\n        \"source\": \"cve@mitre.org\",\n        \"tags\": [\n          \"US Government Resource\"\n        ]\n      },\n      {\n        \"url\": \"http://www.ntbugtraq.com/default.asp?pid=36&sid=1&A2=ind0306&L=NTBUGTRAQ&P=R4563\",\n        \"source\": \"cve@mitre.org\",\n        \"tags\": [\n          \"Exploit\",\n          \"Patch\",\n          \"Vendor Advisory\"\n        ]\n      },\n      {\n        \"url\": \"https://docs.microsoft.com/en-us/security-updates/securitybulletins/2003/ms03-022\",\n        \"source\": \"cve@mitre.org\"\n      },\n      {\n        \"url\": \"https://oval.cisecurity.org/repository/search/definition/oval%3Aorg.mitre.oval%3Adef%3A938\",\n        \"source\": \"cve@mitre.org\"\n      }\n    ]\n  }\n}\n"
  },
  {
    "path": "grype/db/v5/build/transformers/nvd/testdata/version-range.json",
    "content": "{\n  \"cve\": {\n    \"id\": \"CVE-2018-5487\",\n    \"sourceIdentifier\": \"security-alert@netapp.com\",\n    \"published\": \"2018-05-24T14:29:00.390\",\n    \"lastModified\": \"2018-07-05T13:52:30.627\",\n    \"vulnStatus\": \"Analyzed\",\n    \"descriptions\": [\n      {\n        \"lang\": \"en\",\n        \"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.\"\n      },\n      {\n        \"lang\": \"es\",\n        \"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.\"\n      }\n    ],\n    \"metrics\": {\n      \"cvssMetricV30\": [\n        {\n          \"source\": \"nvd@nist.gov\",\n          \"type\": \"Primary\",\n          \"cvssData\": {\n            \"version\": \"3.0\",\n            \"vectorString\": \"CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H\",\n            \"attackVector\": \"NETWORK\",\n            \"attackComplexity\": \"LOW\",\n            \"privilegesRequired\": \"NONE\",\n            \"userInteraction\": \"NONE\",\n            \"scope\": \"UNCHANGED\",\n            \"confidentialityImpact\": \"HIGH\",\n            \"integrityImpact\": \"HIGH\",\n            \"availabilityImpact\": \"HIGH\",\n            \"baseScore\": 9.8,\n            \"baseSeverity\": \"CRITICAL\"\n          },\n          \"exploitabilityScore\": 3.9,\n          \"impactScore\": 5.9\n        }\n      ],\n      \"cvssMetricV2\": [\n        {\n          \"source\": \"nvd@nist.gov\",\n          \"type\": \"Primary\",\n          \"cvssData\": {\n            \"version\": \"2.0\",\n            \"vectorString\": \"AV:N/AC:L/Au:N/C:P/I:P/A:P\",\n            \"accessVector\": \"NETWORK\",\n            \"accessComplexity\": \"LOW\",\n            \"authentication\": \"NONE\",\n            \"confidentialityImpact\": \"PARTIAL\",\n            \"integrityImpact\": \"PARTIAL\",\n            \"availabilityImpact\": \"PARTIAL\",\n            \"baseScore\": 7.5\n          },\n          \"baseSeverity\": \"HIGH\",\n          \"exploitabilityScore\": 10.0,\n          \"impactScore\": 6.4,\n          \"acInsufInfo\": true,\n          \"obtainAllPrivilege\": false,\n          \"obtainUserPrivilege\": false,\n          \"obtainOtherPrivilege\": false,\n          \"userInteractionRequired\": false\n        }\n      ]\n    },\n    \"weaknesses\": [\n      {\n        \"source\": \"nvd@nist.gov\",\n        \"type\": \"Primary\",\n        \"description\": [\n          {\n            \"lang\": \"en\",\n            \"value\": \"CWE-20\"\n          }\n        ]\n      }\n    ],\n    \"configurations\": [\n      {\n        \"operator\": \"AND\",\n        \"nodes\": [\n          {\n            \"operator\": \"OR\",\n            \"negate\": false,\n            \"cpeMatch\": [\n              {\n                \"vulnerable\": true,\n                \"criteria\": \"cpe:2.3:a:netapp:oncommand_unified_manager:*:*:*:*:*:*:*:*\",\n                \"versionStartIncluding\": \"7.2\",\n                \"versionEndIncluding\": \"7.3\",\n                \"matchCriteriaId\": \"A5949307-3E9B-441F-B008-81A0E0228DC0\"\n              }\n            ]\n          },\n          {\n            \"operator\": \"OR\",\n            \"negate\": false,\n            \"cpeMatch\": [\n              {\n                \"vulnerable\": false,\n                \"criteria\": \"cpe:2.3:o:linux:linux_kernel:-:*:*:*:*:*:*:*\",\n                \"matchCriteriaId\": \"703AF700-7A70-47E2-BC3A-7FD03B3CA9C1\"\n              }\n            ]\n          }\n        ]\n      }\n    ],\n    \"references\": [\n      {\n        \"url\": \"https://security.netapp.com/advisory/ntap-20180523-0001/\",\n        \"source\": \"security-alert@netapp.com\",\n        \"tags\": [\n          \"Patch\",\n          \"Vendor Advisory\"\n        ]\n      }\n    ]\n  }\n}\n"
  },
  {
    "path": "grype/db/v5/build/transformers/nvd/transform.go",
    "content": "package nvd\n\nimport (\n\t\"slices\"\n\t\"sort\"\n\t\"strings\"\n\n\t\"github.com/scylladb/go-set/strset\"\n\n\t\"github.com/anchore/grype/grype/db/data\"\n\t\"github.com/anchore/grype/grype/db/internal/provider/unmarshal\"\n\t\"github.com/anchore/grype/grype/db/internal/provider/unmarshal/nvd\"\n\tdb \"github.com/anchore/grype/grype/db/v5\"\n\t\"github.com/anchore/grype/grype/db/v5/build/transformers\"\n\t\"github.com/anchore/grype/grype/db/v5/namespace\"\n\t\"github.com/anchore/grype/grype/db/v5/pkg/qualifier\"\n\t\"github.com/anchore/grype/grype/db/v5/pkg/qualifier/platformcpe\"\n\t\"github.com/anchore/grype/grype/pkg\"\n\t\"github.com/anchore/grype/grype/version\"\n\t\"github.com/anchore/syft/syft/cpe\"\n)\n\ntype Config struct {\n\tCPEParts            *strset.Set\n\tInferNVDFixVersions bool\n}\n\nfunc defaultConfig() Config {\n\treturn Config{\n\t\tCPEParts:            strset.New(\"a\"),\n\t\tInferNVDFixVersions: true,\n\t}\n}\n\nfunc Transformer(cfg Config) data.NVDTransformer {\n\tif cfg == (Config{}) {\n\t\tcfg = defaultConfig()\n\t}\n\treturn func(vulnerability unmarshal.NVDVulnerability) ([]data.Entry, error) {\n\t\treturn transform(cfg, vulnerability)\n\t}\n}\n\nfunc transform(cfg Config, vulnerability unmarshal.NVDVulnerability) ([]data.Entry, error) {\n\t// TODO: stop capturing record source in the vulnerability metadata record (now that feed groups are not real)\n\trecordSource := \"nvdv2:nvdv2:cves\"\n\n\tgrypeNamespace, err := namespace.FromString(\"nvd:cpe\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tentryNamespace := grypeNamespace.String()\n\n\tuniquePkgs := findUniquePkgs(cfg, vulnerability.Configurations...)\n\n\t// extract all links\n\tvar links []string\n\tfor _, externalRefs := range vulnerability.References {\n\t\t// TODO: should we capture other information here?\n\t\tif externalRefs.URL != \"\" {\n\t\t\tlinks = append(links, externalRefs.URL)\n\t\t}\n\t}\n\n\t// duplicate the vulnerabilities based on the set of unique packages the vulnerability is for\n\tvar allVulns []db.Vulnerability\n\tfor _, p := range uniquePkgs.All() {\n\t\tvar qualifiers []qualifier.Qualifier\n\t\tmatches := uniquePkgs.Matches(p)\n\t\tcpes := strset.New()\n\t\tfor _, m := range matches {\n\t\t\tcpes.Add(grypeNamespace.Resolver().Normalize(m.Criteria))\n\t\t}\n\n\t\tif p.PlatformCPE != \"\" {\n\t\t\tqualifiers = []qualifier.Qualifier{platformcpe.Qualifier{\n\t\t\t\tKind: \"platform-cpe\",\n\t\t\t\tCPE:  p.PlatformCPE,\n\t\t\t}}\n\t\t}\n\n\t\torderedCPEs := cpes.List()\n\t\tsort.Strings(orderedCPEs)\n\n\t\t// create vulnerability entry\n\t\tallVulns = append(allVulns, db.Vulnerability{\n\t\t\tID:                vulnerability.ID,\n\t\t\tPackageQualifiers: qualifiers,\n\t\t\tVersionConstraint: buildConstraints(matches),\n\t\t\tVersionFormat:     strings.ToLower(getVersionFormat(p.Product, orderedCPEs).String()),\n\t\t\tPackageName:       grypeNamespace.Resolver().Normalize(p.Product),\n\t\t\tNamespace:         entryNamespace,\n\t\t\tCPEs:              orderedCPEs,\n\t\t\tFix:               getFix(matches, cfg.InferNVDFixVersions),\n\t\t})\n\t}\n\n\t// create vulnerability metadata entry (a single entry keyed off of the vulnerability ID)\n\tallCVSS := vulnerability.CVSS()\n\tmetadata := db.VulnerabilityMetadata{\n\t\tID:           vulnerability.ID,\n\t\tDataSource:   \"https://nvd.nist.gov/vuln/detail/\" + vulnerability.ID,\n\t\tNamespace:    entryNamespace,\n\t\tRecordSource: recordSource,\n\t\tSeverity:     nvd.CvssSummaries(allCVSS).Sorted().Severity(),\n\t\tURLs:         links,\n\t\tDescription:  vulnerability.Description(),\n\t\tCvss:         getCvss(allCVSS...),\n\t}\n\n\treturn transformers.NewEntries(allVulns, metadata), nil\n}\n\nfunc getVersionFormat(name string, cpes []string) version.Format {\n\tif pkg.HasJvmPackageName(name) {\n\t\treturn version.JVMFormat\n\t}\n\tfor _, c := range cpes {\n\t\tatt, err := cpe.NewAttributes(c)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tif pkg.HasJvmPackageName(att.Product) {\n\t\t\treturn version.JVMFormat\n\t\t}\n\t}\n\treturn version.UnknownFormat\n}\n\nfunc getFix(matches []nvd.CpeMatch, inferNVDFixVersions bool) db.Fix {\n\tif !inferNVDFixVersions {\n\t\treturn db.Fix{\n\t\t\tState: db.UnknownFixState,\n\t\t}\n\t}\n\n\tpossiblyFixed := strset.New()\n\tknownAffected := strset.New()\n\tunspecifiedSet := strset.New(\"*\", \"-\", \"*\")\n\n\tfor _, match := range matches {\n\t\tif !match.Vulnerable {\n\t\t\tcontinue\n\t\t}\n\n\t\tif match.VersionEndExcluding != nil && !unspecifiedSet.Has(*match.VersionEndExcluding) {\n\t\t\tpossiblyFixed.Add(*match.VersionEndExcluding)\n\t\t}\n\n\t\tif match.VersionStartIncluding != nil && !unspecifiedSet.Has(*match.VersionStartIncluding) {\n\t\t\tknownAffected.Add(*match.VersionStartIncluding)\n\t\t}\n\n\t\tif match.VersionEndIncluding != nil && !unspecifiedSet.Has(*match.VersionEndIncluding) {\n\t\t\tknownAffected.Add(*match.VersionEndIncluding)\n\t\t}\n\n\t\tmatchCPE, err := cpe.New(match.Criteria, cpe.DeclaredSource)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tif !unspecifiedSet.Has(matchCPE.Attributes.Version) {\n\t\t\tknownAffected.Add(matchCPE.Attributes.Version)\n\t\t}\n\t}\n\n\tpossiblyFixed.Remove(knownAffected.List()...)\n\n\tvar fixes []string\n\tfixState := db.UnknownFixState\n\tif possiblyFixed.Size() > 0 {\n\t\tfixState = db.FixedState\n\t\tfixes = possiblyFixed.List()\n\t\tslices.Sort(fixes)\n\t}\n\n\treturn db.Fix{\n\t\tVersions: fixes,\n\t\tState:    fixState,\n\t}\n}\n\nfunc getCvss(cvss ...nvd.CvssSummary) []db.Cvss {\n\tvar results []db.Cvss\n\tfor _, c := range cvss {\n\t\tresults = append(results, db.Cvss{\n\t\t\tSource:  c.Source,\n\t\t\tType:    string(c.Type),\n\t\t\tVersion: c.Version,\n\t\t\tVector:  c.Vector,\n\t\t\tMetrics: db.CvssMetrics{\n\t\t\t\tBaseScore:           c.BaseScore,\n\t\t\t\tExploitabilityScore: c.ExploitabilityScore,\n\t\t\t\tImpactScore:         c.ImpactScore,\n\t\t\t},\n\t\t})\n\t}\n\treturn results\n}\n"
  },
  {
    "path": "grype/db/v5/build/transformers/nvd/transform_test.go",
    "content": "package nvd\n\nimport (\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/go-test/deep\"\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/grype/grype/db/internal/provider/unmarshal\"\n\t\"github.com/anchore/grype/grype/db/internal/provider/unmarshal/nvd\"\n\t\"github.com/anchore/grype/grype/db/internal/testutil\"\n\tdb \"github.com/anchore/grype/grype/db/v5\"\n\t\"github.com/anchore/grype/grype/db/v5/pkg/qualifier\"\n\t\"github.com/anchore/grype/grype/db/v5/pkg/qualifier/platformcpe\"\n\t\"github.com/anchore/grype/grype/version\"\n)\n\nfunc TestUnmarshalNVDVulnerabilitiesEntries(t *testing.T) {\n\tf, err := os.Open(\"testdata/unmarshal-test.json\")\n\trequire.NoError(t, err)\n\tdefer testutil.CloseFile(f)\n\n\tentries, err := unmarshal.NvdVulnerabilityEntries(f)\n\tassert.NoError(t, err)\n\tassert.Len(t, entries, 1)\n}\n\nfunc TestParseAllNVDVulnerabilityEntries(t *testing.T) {\n\n\ttests := []struct {\n\t\tname       string\n\t\tconfig     Config\n\t\tnumEntries int\n\t\tfixture    string\n\t\tvulns      []db.Vulnerability\n\t\tmetadata   db.VulnerabilityMetadata\n\t}{\n\t\t{\n\t\t\tname:       \"AppVersionRange\",\n\t\t\tnumEntries: 1,\n\t\t\tfixture:    \"testdata/version-range.json\",\n\t\t\tvulns: []db.Vulnerability{\n\t\t\t\t{\n\t\t\t\t\tID:          \"CVE-2018-5487\",\n\t\t\t\t\tPackageName: \"oncommand_unified_manager\",\n\t\t\t\t\tPackageQualifiers: []qualifier.Qualifier{platformcpe.Qualifier{\n\t\t\t\t\t\tKind: \"platform-cpe\",\n\t\t\t\t\t\tCPE:  \"cpe:2.3:o:linux:linux_kernel:-:*:*:*:*:*:*:*\",\n\t\t\t\t\t}},\n\t\t\t\t\tVersionConstraint: \">= 7.2, <= 7.3\",\n\t\t\t\t\tVersionFormat:     \"unknown\", // TODO: this should reference a format, yes? (not a string)\n\t\t\t\t\tNamespace:         \"nvd:cpe\",\n\t\t\t\t\tCPEs:              []string{\"cpe:2.3:a:netapp:oncommand_unified_manager:*:*:*:*:*:*:*:*\"},\n\t\t\t\t\tFix: db.Fix{\n\t\t\t\t\t\tState: \"unknown\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tmetadata: db.VulnerabilityMetadata{\n\t\t\t\tID:           \"CVE-2018-5487\",\n\t\t\t\tDataSource:   \"https://nvd.nist.gov/vuln/detail/CVE-2018-5487\",\n\t\t\t\tNamespace:    \"nvd:cpe\",\n\t\t\t\tRecordSource: \"nvdv2:nvdv2:cves\",\n\t\t\t\tSeverity:     \"Critical\",\n\t\t\t\tURLs:         []string{\"https://security.netapp.com/advisory/ntap-20180523-0001/\"},\n\t\t\t\tDescription:  \"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.\",\n\t\t\t\tCvss: []db.Cvss{\n\t\t\t\t\t{\n\t\t\t\t\t\tMetrics: db.NewCvssMetrics(\n\t\t\t\t\t\t\t7.5,\n\t\t\t\t\t\t\t10,\n\t\t\t\t\t\t\t6.4,\n\t\t\t\t\t\t),\n\t\t\t\t\t\tVector:  \"AV:N/AC:L/Au:N/C:P/I:P/A:P\",\n\t\t\t\t\t\tVersion: \"2.0\",\n\t\t\t\t\t\tSource:  \"nvd@nist.gov\",\n\t\t\t\t\t\tType:    \"Primary\",\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tMetrics: db.NewCvssMetrics(\n\t\t\t\t\t\t\t9.8,\n\t\t\t\t\t\t\t3.9,\n\t\t\t\t\t\t\t5.9,\n\t\t\t\t\t\t),\n\t\t\t\t\t\tVector:  \"CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H\",\n\t\t\t\t\t\tVersion: \"3.0\",\n\t\t\t\t\t\tSource:  \"nvd@nist.gov\",\n\t\t\t\t\t\tType:    \"Primary\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:       \"App+OS\",\n\t\t\tnumEntries: 1,\n\t\t\tfixture:    \"testdata/single-package-multi-distro.json\",\n\t\t\tvulns: []db.Vulnerability{\n\t\t\t\t{\n\t\t\t\t\tID:                \"CVE-2018-1000222\",\n\t\t\t\t\tPackageName:       \"libgd\",\n\t\t\t\t\tVersionConstraint: \"= 2.2.5\",\n\t\t\t\t\tVersionFormat:     \"unknown\", // TODO: this should reference a format, yes? (not a string)\n\t\t\t\t\tNamespace:         \"nvd:cpe\",\n\t\t\t\t\tCPEs:              []string{\"cpe:2.3:a:libgd:libgd:2.2.5:*:*:*:*:*:*:*\"},\n\t\t\t\t\tFix: db.Fix{\n\t\t\t\t\t\tState: \"unknown\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t// TODO: Question: should this match also the OS's? (as in the vulnerable_cpes list)... this seems wrong!\n\t\t\t},\n\t\t\tmetadata: db.VulnerabilityMetadata{\n\t\t\t\tID:           \"CVE-2018-1000222\",\n\t\t\t\tDataSource:   \"https://nvd.nist.gov/vuln/detail/CVE-2018-1000222\",\n\t\t\t\tNamespace:    \"nvd:cpe\",\n\t\t\t\tRecordSource: \"nvdv2:nvdv2:cves\",\n\t\t\t\tSeverity:     \"High\",\n\t\t\t\tURLs:         []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/\"},\n\t\t\t\tDescription:  \"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.\",\n\t\t\t\tCvss: []db.Cvss{\n\t\t\t\t\t{\n\t\t\t\t\t\tMetrics: db.NewCvssMetrics(\n\t\t\t\t\t\t\t6.8,\n\t\t\t\t\t\t\t8.6,\n\t\t\t\t\t\t\t6.4,\n\t\t\t\t\t\t),\n\t\t\t\t\t\tVector:  \"AV:N/AC:M/Au:N/C:P/I:P/A:P\",\n\t\t\t\t\t\tVersion: \"2.0\",\n\t\t\t\t\t\tSource:  \"nvd@nist.gov\",\n\t\t\t\t\t\tType:    \"Primary\",\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tMetrics: db.NewCvssMetrics(\n\t\t\t\t\t\t\t8.8,\n\t\t\t\t\t\t\t2.8,\n\t\t\t\t\t\t\t5.9,\n\t\t\t\t\t\t),\n\t\t\t\t\t\tVector:  \"CVSS:3.0/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H\",\n\t\t\t\t\t\tVersion: \"3.0\",\n\t\t\t\t\t\tSource:  \"nvd@nist.gov\",\n\t\t\t\t\t\tType:    \"Primary\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:       \"AppCompoundVersionRange\",\n\t\t\tnumEntries: 1,\n\t\t\tfixture:    \"testdata/compound-pkg.json\",\n\t\t\tvulns: []db.Vulnerability{\n\t\t\t\t{\n\t\t\t\t\tID:                \"CVE-2018-10189\",\n\t\t\t\t\tPackageName:       \"mautic\",\n\t\t\t\t\tVersionConstraint: \">= 1.0.0, <= 1.4.1 || >= 2.0.0, < 2.13.0\",\n\t\t\t\t\tVersionFormat:     \"unknown\",\n\t\t\t\t\tNamespace:         \"nvd:cpe\",\n\t\t\t\t\tCPEs:              []string{\"cpe:2.3:a:mautic:mautic:*:*:*:*:*:*:*:*\"}, // note: entry was dedupicated\n\t\t\t\t\tFix: db.Fix{\n\t\t\t\t\t\tVersions: []string{\"2.13.0\"},\n\t\t\t\t\t\tState:    \"fixed\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tmetadata: db.VulnerabilityMetadata{\n\t\t\t\tID:           \"CVE-2018-10189\",\n\t\t\t\tDataSource:   \"https://nvd.nist.gov/vuln/detail/CVE-2018-10189\",\n\t\t\t\tNamespace:    \"nvd:cpe\",\n\t\t\t\tRecordSource: \"nvdv2:nvdv2:cves\",\n\t\t\t\tSeverity:     \"High\",\n\t\t\t\tURLs:         []string{\"https://github.com/mautic/mautic/releases/tag/2.13.0\"},\n\t\t\t\tDescription:  \"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.\",\n\t\t\t\tCvss: []db.Cvss{\n\t\t\t\t\t{\n\t\t\t\t\t\tMetrics: db.NewCvssMetrics(\n\t\t\t\t\t\t\t5,\n\t\t\t\t\t\t\t10,\n\t\t\t\t\t\t\t2.9,\n\t\t\t\t\t\t),\n\t\t\t\t\t\tVector:  \"AV:N/AC:L/Au:N/C:P/I:N/A:N\",\n\t\t\t\t\t\tVersion: \"2.0\",\n\t\t\t\t\t\tSource:  \"nvd@nist.gov\",\n\t\t\t\t\t\tType:    \"Primary\",\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tMetrics: db.NewCvssMetrics(\n\t\t\t\t\t\t\t7.5,\n\t\t\t\t\t\t\t3.9,\n\t\t\t\t\t\t\t3.6,\n\t\t\t\t\t\t),\n\t\t\t\t\t\tVector:  \"CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N\",\n\t\t\t\t\t\tVersion: \"3.0\",\n\t\t\t\t\t\tSource:  \"nvd@nist.gov\",\n\t\t\t\t\t\tType:    \"Primary\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t// we always keep the metadata even though there are no vulnerability entries for it\n\t\t\tname:       \"InvalidCPE\",\n\t\t\tnumEntries: 1,\n\t\t\tfixture:    \"testdata/invalid_cpe.json\",\n\t\t\tvulns:      nil,\n\t\t\tmetadata: db.VulnerabilityMetadata{\n\t\t\t\tID:           \"CVE-2015-8978\",\n\t\t\t\tNamespace:    \"nvd:cpe\",\n\t\t\t\tDataSource:   \"https://nvd.nist.gov/vuln/detail/CVE-2015-8978\",\n\t\t\t\tRecordSource: \"nvdv2:nvdv2:cves\",\n\t\t\t\tSeverity:     \"High\",\n\t\t\t\tURLs: []string{\n\t\t\t\t\t\"http://cpansearch.perl.org/src/PHRED/SOAP-Lite-1.20/Changes\",\n\t\t\t\t\t\"http://www.securityfocus.com/bid/94487\",\n\t\t\t\t},\n\t\t\t\tDescription: \"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.\",\n\t\t\t\tCvss: []db.Cvss{\n\t\t\t\t\t{\n\t\t\t\t\t\tMetrics: db.NewCvssMetrics(\n\t\t\t\t\t\t\t5,\n\t\t\t\t\t\t\t10,\n\t\t\t\t\t\t\t2.9,\n\t\t\t\t\t\t),\n\t\t\t\t\t\tVector:  \"AV:N/AC:L/Au:N/C:N/I:N/A:P\",\n\t\t\t\t\t\tVersion: \"2.0\",\n\t\t\t\t\t\tSource:  \"nvd@nist.gov\",\n\t\t\t\t\t\tType:    \"Primary\",\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tMetrics: db.NewCvssMetrics(\n\t\t\t\t\t\t\t7.5,\n\t\t\t\t\t\t\t3.9,\n\t\t\t\t\t\t\t3.6,\n\t\t\t\t\t\t),\n\t\t\t\t\t\tVector:  \"CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H\",\n\t\t\t\t\t\tVersion: \"3.0\",\n\t\t\t\t\t\tSource:  \"nvd@nist.gov\",\n\t\t\t\t\t\tType:    \"Primary\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:       \"With Platform CPE\",\n\t\t\tnumEntries: 1,\n\t\t\tfixture:    \"testdata/platform-cpe.json\",\n\t\t\tvulns: []db.Vulnerability{\n\t\t\t\t{\n\t\t\t\t\tID:                \"CVE-2022-26488\",\n\t\t\t\t\tPackageName:       \"active_iq_unified_manager\",\n\t\t\t\t\tVersionConstraint: \"\",\n\t\t\t\t\tVersionFormat:     \"unknown\",\n\t\t\t\t\tNamespace:         \"nvd:cpe\",\n\t\t\t\t\tCPEs:              []string{\"cpe:2.3:a:netapp:active_iq_unified_manager:-:*:*:*:*:windows:*:*\"},\n\t\t\t\t\tFix: db.Fix{\n\t\t\t\t\t\tState: \"unknown\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID:                \"CVE-2022-26488\",\n\t\t\t\t\tPackageName:       \"ontap_select_deploy_administration_utility\",\n\t\t\t\t\tVersionConstraint: \"\",\n\t\t\t\t\tVersionFormat:     \"unknown\",\n\t\t\t\t\tNamespace:         \"nvd:cpe\",\n\t\t\t\t\tCPEs:              []string{\"cpe:2.3:a:netapp:ontap_select_deploy_administration_utility:-:*:*:*:*:*:*:*\"},\n\t\t\t\t\tFix: db.Fix{\n\t\t\t\t\t\tState: \"unknown\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID:          \"CVE-2022-26488\",\n\t\t\t\t\tPackageName: \"python\",\n\t\t\t\t\tPackageQualifiers: []qualifier.Qualifier{platformcpe.Qualifier{\n\t\t\t\t\t\tKind: \"platform-cpe\",\n\t\t\t\t\t\tCPE:  \"cpe:2.3:o:microsoft:windows:-:*:*:*:*:*:*:*\",\n\t\t\t\t\t}},\n\t\t\t\t\tVersionConstraint: \"<= 3.7.12 || >= 3.8.0, <= 3.8.12 || >= 3.9.0, <= 3.9.10 || >= 3.10.0, <= 3.10.2 || = 3.11.0-alpha1 || = 3.11.0-alpha2 || = 3.11.0-alpha3 || = 3.11.0-alpha4 || = 3.11.0-alpha5 || = 3.11.0-alpha6\",\n\t\t\t\t\tVersionFormat:     \"unknown\",\n\t\t\t\t\tNamespace:         \"nvd:cpe\",\n\t\t\t\t\tCPEs: []string{\n\t\t\t\t\t\t\"cpe:2.3:a:python:python:*:*:*:*:*:*:*:*\",\n\t\t\t\t\t\t\"cpe:2.3:a:python:python:3.11.0:alpha1:*:*:*:*:*:*\",\n\t\t\t\t\t\t\"cpe:2.3:a:python:python:3.11.0:alpha2:*:*:*:*:*:*\",\n\t\t\t\t\t\t\"cpe:2.3:a:python:python:3.11.0:alpha3:*:*:*:*:*:*\",\n\t\t\t\t\t\t\"cpe:2.3:a:python:python:3.11.0:alpha4:*:*:*:*:*:*\",\n\t\t\t\t\t\t\"cpe:2.3:a:python:python:3.11.0:alpha5:*:*:*:*:*:*\",\n\t\t\t\t\t\t\"cpe:2.3:a:python:python:3.11.0:alpha6:*:*:*:*:*:*\",\n\t\t\t\t\t},\n\t\t\t\t\tFix: db.Fix{\n\t\t\t\t\t\tState: \"unknown\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tmetadata: db.VulnerabilityMetadata{\n\t\t\t\tID:           \"CVE-2022-26488\",\n\t\t\t\tNamespace:    \"nvd:cpe\",\n\t\t\t\tDataSource:   \"https://nvd.nist.gov/vuln/detail/CVE-2022-26488\",\n\t\t\t\tRecordSource: \"nvdv2:nvdv2:cves\",\n\t\t\t\tSeverity:     \"High\",\n\t\t\t\tURLs: []string{\n\t\t\t\t\t\"https://mail.python.org/archives/list/security-announce@python.org/thread/657Z4XULWZNIY5FRP3OWXHYKUSIH6DMN/\",\n\t\t\t\t\t\"https://security.netapp.com/advisory/ntap-20220419-0005/\",\n\t\t\t\t},\n\t\t\t\tDescription: \"In Python before 3.10.3 on Windows, local users can gain privileges because the search path is inadequately secured. The installer may allow a local attacker to add user-writable directories to the system search path. To exploit, an administrator must have installed Python for all users and enabled PATH entries. A non-administrative user can trigger a repair that incorrectly adds user-writable paths into PATH, enabling search-path hijacking of other users and system services. This affects Python (CPython) through 3.7.12, 3.8.x through 3.8.12, 3.9.x through 3.9.10, and 3.10.x through 3.10.2.\",\n\t\t\t\tCvss: []db.Cvss{\n\t\t\t\t\t{\n\t\t\t\t\t\tMetrics: db.NewCvssMetrics(\n\t\t\t\t\t\t\t4.4,\n\t\t\t\t\t\t\t3.4,\n\t\t\t\t\t\t\t6.4,\n\t\t\t\t\t\t),\n\t\t\t\t\t\tVector:  \"AV:L/AC:M/Au:N/C:P/I:P/A:P\",\n\t\t\t\t\t\tVersion: \"2.0\",\n\t\t\t\t\t\tSource:  \"nvd@nist.gov\",\n\t\t\t\t\t\tType:    \"Primary\",\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tMetrics: db.NewCvssMetrics(\n\t\t\t\t\t\t\t7,\n\t\t\t\t\t\t\t1,\n\t\t\t\t\t\t\t5.9,\n\t\t\t\t\t\t),\n\t\t\t\t\t\tVector:  \"CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H\",\n\t\t\t\t\t\tVersion: \"3.1\",\n\t\t\t\t\t\tSource:  \"nvd@nist.gov\",\n\t\t\t\t\t\tType:    \"Primary\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:       \"CVE-2022-0543 multiple platforms\",\n\t\t\tnumEntries: 1,\n\t\t\tfixture:    \"testdata/cve-2022-0543.json\",\n\t\t\tvulns: []db.Vulnerability{\n\t\t\t\t{\n\t\t\t\t\tID:          \"CVE-2022-0543\",\n\t\t\t\t\tPackageName: \"redis\",\n\t\t\t\t\tNamespace:   \"nvd:cpe\",\n\t\t\t\t\tPackageQualifiers: []qualifier.Qualifier{platformcpe.Qualifier{\n\t\t\t\t\t\tKind: \"platform-cpe\",\n\t\t\t\t\t\tCPE:  \"cpe:2.3:o:canonical:ubuntu_linux:20.04:*:*:*:lts:*:*:*\",\n\t\t\t\t\t}},\n\t\t\t\t\tVersionConstraint:      \"\",\n\t\t\t\t\tVersionFormat:          \"unknown\",\n\t\t\t\t\tCPEs:                   []string{\"cpe:2.3:a:redis:redis:-:*:*:*:*:*:*:*\"},\n\t\t\t\t\tRelatedVulnerabilities: nil,\n\t\t\t\t\tFix:                    db.Fix{State: \"unknown\"},\n\t\t\t\t\tAdvisories:             nil,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID:          \"CVE-2022-0543\",\n\t\t\t\t\tPackageName: \"redis\",\n\t\t\t\t\tNamespace:   \"nvd:cpe\",\n\t\t\t\t\tPackageQualifiers: []qualifier.Qualifier{platformcpe.Qualifier{\n\t\t\t\t\t\tKind: \"platform-cpe\",\n\t\t\t\t\t\tCPE:  \"cpe:2.3:o:canonical:ubuntu_linux:21.10:*:*:*:-:*:*:*\",\n\t\t\t\t\t}},\n\t\t\t\t\tVersionConstraint:      \"\",\n\t\t\t\t\tVersionFormat:          \"unknown\",\n\t\t\t\t\tCPEs:                   []string{\"cpe:2.3:a:redis:redis:-:*:*:*:*:*:*:*\"},\n\t\t\t\t\tRelatedVulnerabilities: nil,\n\t\t\t\t\tFix:                    db.Fix{State: \"unknown\"},\n\t\t\t\t\tAdvisories:             nil,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID:          \"CVE-2022-0543\",\n\t\t\t\t\tPackageName: \"redis\",\n\t\t\t\t\tNamespace:   \"nvd:cpe\",\n\t\t\t\t\tPackageQualifiers: []qualifier.Qualifier{platformcpe.Qualifier{\n\t\t\t\t\t\tKind: \"platform-cpe\",\n\t\t\t\t\t\tCPE:  \"cpe:2.3:o:debian:debian_linux:10.0:*:*:*:*:*:*:*\",\n\t\t\t\t\t}},\n\t\t\t\t\tVersionConstraint:      \"\",\n\t\t\t\t\tVersionFormat:          \"unknown\",\n\t\t\t\t\tCPEs:                   []string{\"cpe:2.3:a:redis:redis:-:*:*:*:*:*:*:*\"},\n\t\t\t\t\tRelatedVulnerabilities: nil,\n\t\t\t\t\tFix:                    db.Fix{State: \"unknown\"},\n\t\t\t\t\tAdvisories:             nil,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID:          \"CVE-2022-0543\",\n\t\t\t\t\tPackageName: \"redis\",\n\t\t\t\t\tNamespace:   \"nvd:cpe\",\n\t\t\t\t\tPackageQualifiers: []qualifier.Qualifier{platformcpe.Qualifier{\n\t\t\t\t\t\tKind: \"platform-cpe\",\n\t\t\t\t\t\tCPE:  \"cpe:2.3:o:debian:debian_linux:11.0:*:*:*:*:*:*:*\",\n\t\t\t\t\t}},\n\t\t\t\t\tVersionConstraint:      \"\",\n\t\t\t\t\tVersionFormat:          \"unknown\",\n\t\t\t\t\tCPEs:                   []string{\"cpe:2.3:a:redis:redis:-:*:*:*:*:*:*:*\"},\n\t\t\t\t\tRelatedVulnerabilities: nil,\n\t\t\t\t\tFix:                    db.Fix{State: \"unknown\"},\n\t\t\t\t\tAdvisories:             nil,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID:          \"CVE-2022-0543\",\n\t\t\t\t\tPackageName: \"redis\",\n\t\t\t\t\tNamespace:   \"nvd:cpe\",\n\t\t\t\t\tPackageQualifiers: []qualifier.Qualifier{platformcpe.Qualifier{\n\t\t\t\t\t\tKind: \"platform-cpe\",\n\t\t\t\t\t\tCPE:  \"cpe:2.3:o:debian:debian_linux:9.0:*:*:*:*:*:*:*\",\n\t\t\t\t\t}},\n\t\t\t\t\tVersionConstraint:      \"\",\n\t\t\t\t\tVersionFormat:          \"unknown\",\n\t\t\t\t\tCPEs:                   []string{\"cpe:2.3:a:redis:redis:-:*:*:*:*:*:*:*\"},\n\t\t\t\t\tRelatedVulnerabilities: nil,\n\t\t\t\t\tFix:                    db.Fix{State: \"unknown\"},\n\t\t\t\t\tAdvisories:             nil,\n\t\t\t\t},\n\t\t\t},\n\t\t\tmetadata: db.VulnerabilityMetadata{\n\t\t\t\tID:           \"CVE-2022-0543\",\n\t\t\t\tNamespace:    \"nvd:cpe\",\n\t\t\t\tDataSource:   \"https://nvd.nist.gov/vuln/detail/CVE-2022-0543\",\n\t\t\t\tRecordSource: \"nvdv2:nvdv2:cves\",\n\t\t\t\tSeverity:     \"Critical\",\n\t\t\t\tURLs: []string{\n\t\t\t\t\t\"http://packetstormsecurity.com/files/166885/Redis-Lua-Sandbox-Escape.html\",\n\t\t\t\t\t\"https://bugs.debian.org/1005787\",\n\t\t\t\t\t\"https://lists.debian.org/debian-security-announce/2022/msg00048.html\",\n\t\t\t\t\t\"https://security.netapp.com/advisory/ntap-20220331-0004/\",\n\t\t\t\t\t\"https://www.debian.org/security/2022/dsa-5081\",\n\t\t\t\t\t\"https://www.ubercomp.com/posts/2022-01-20_redis_on_debian_rce\",\n\t\t\t\t},\n\t\t\t\tDescription: \"It was discovered, that redis, a persistent key-value database, due to a packaging issue, is prone to a (Debian-specific) Lua sandbox escape, which could result in remote code execution.\",\n\t\t\t\tCvss: []db.Cvss{\n\t\t\t\t\t{\n\t\t\t\t\t\tVendorMetadata: nil,\n\t\t\t\t\t\tMetrics:        db.NewCvssMetrics(10, 10, 10),\n\t\t\t\t\t\tVector:         \"AV:N/AC:L/Au:N/C:C/I:C/A:C\",\n\t\t\t\t\t\tVersion:        \"2.0\",\n\t\t\t\t\t\tSource:         \"nvd@nist.gov\",\n\t\t\t\t\t\tType:           \"Primary\",\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tVendorMetadata: nil,\n\t\t\t\t\t\tMetrics:        db.NewCvssMetrics(10, 3.9, 6),\n\t\t\t\t\t\tVector:         \"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H\",\n\t\t\t\t\t\tVersion:        \"3.1\",\n\t\t\t\t\t\tSource:         \"nvd@nist.gov\",\n\t\t\t\t\t\tType:           \"Primary\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:       \"CVE-2020-10729 multiple platforms omitted top level config\",\n\t\t\tnumEntries: 1,\n\t\t\tfixture:    \"testdata/cve-2020-10729.json\",\n\t\t\tvulns: []db.Vulnerability{\n\t\t\t\t{\n\t\t\t\t\tID:          \"CVE-2020-10729\",\n\t\t\t\t\tPackageName: \"ansible_engine\",\n\t\t\t\t\tNamespace:   \"nvd:cpe\",\n\t\t\t\t\tPackageQualifiers: []qualifier.Qualifier{platformcpe.Qualifier{\n\t\t\t\t\t\tKind: \"platform-cpe\",\n\t\t\t\t\t\tCPE:  \"cpe:2.3:o:redhat:enterprise_linux:7.0:*:*:*:*:*:*:*\",\n\t\t\t\t\t}},\n\t\t\t\t\tVersionConstraint:      \"< 2.9.6\",\n\t\t\t\t\tVersionFormat:          \"unknown\",\n\t\t\t\t\tCPEs:                   []string{\"cpe:2.3:a:redhat:ansible_engine:*:*:*:*:*:*:*:*\"},\n\t\t\t\t\tRelatedVulnerabilities: nil,\n\t\t\t\t\tFix: db.Fix{\n\t\t\t\t\t\tVersions: []string{\"2.9.6\"},\n\t\t\t\t\t\tState:    \"fixed\",\n\t\t\t\t\t},\n\t\t\t\t\tAdvisories: nil,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID:          \"CVE-2020-10729\",\n\t\t\t\t\tPackageName: \"ansible_engine\",\n\t\t\t\t\tNamespace:   \"nvd:cpe\",\n\t\t\t\t\tPackageQualifiers: []qualifier.Qualifier{platformcpe.Qualifier{\n\t\t\t\t\t\tKind: \"platform-cpe\",\n\t\t\t\t\t\tCPE:  \"cpe:2.3:o:redhat:enterprise_linux:8.0:*:*:*:*:*:*:*\",\n\t\t\t\t\t}},\n\t\t\t\t\tVersionConstraint:      \"< 2.9.6\",\n\t\t\t\t\tVersionFormat:          \"unknown\",\n\t\t\t\t\tCPEs:                   []string{\"cpe:2.3:a:redhat:ansible_engine:*:*:*:*:*:*:*:*\"},\n\t\t\t\t\tRelatedVulnerabilities: nil,\n\t\t\t\t\tFix: db.Fix{\n\t\t\t\t\t\tVersions: []string{\"2.9.6\"},\n\t\t\t\t\t\tState:    \"fixed\",\n\t\t\t\t\t},\n\t\t\t\t\tAdvisories: nil,\n\t\t\t\t},\n\t\t\t},\n\t\t\tmetadata: db.VulnerabilityMetadata{\n\t\t\t\tID:           \"CVE-2020-10729\",\n\t\t\t\tNamespace:    \"nvd:cpe\",\n\t\t\t\tDataSource:   \"https://nvd.nist.gov/vuln/detail/CVE-2020-10729\",\n\t\t\t\tRecordSource: \"nvdv2:nvdv2:cves\",\n\t\t\t\tSeverity:     \"Medium\",\n\t\t\t\tURLs: []string{\n\t\t\t\t\t\"https://bugzilla.redhat.com/show_bug.cgi?id=1831089\",\n\t\t\t\t\t\"https://github.com/ansible/ansible/issues/34144\",\n\t\t\t\t\t\"https://www.debian.org/security/2021/dsa-4950\",\n\t\t\t\t},\n\t\t\t\tDescription: \"A flaw was found in the use of insufficiently random values in Ansible. Two random password lookups of the same length generate the equal value as the template caching action for the same file since no re-evaluation happens. The highest threat from this vulnerability would be that all passwords are exposed at once for the file. This flaw affects Ansible Engine versions before 2.9.6.\",\n\t\t\t\tCvss: []db.Cvss{\n\t\t\t\t\t{\n\t\t\t\t\t\tVendorMetadata: nil,\n\t\t\t\t\t\tMetrics: db.NewCvssMetrics(\n\t\t\t\t\t\t\t2.1,\n\t\t\t\t\t\t\t3.9,\n\t\t\t\t\t\t\t2.9,\n\t\t\t\t\t\t),\n\t\t\t\t\t\tVector:  \"AV:L/AC:L/Au:N/C:P/I:N/A:N\",\n\t\t\t\t\t\tVersion: \"2.0\",\n\t\t\t\t\t\tSource:  \"nvd@nist.gov\",\n\t\t\t\t\t\tType:    \"Primary\",\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tVendorMetadata: nil,\n\t\t\t\t\t\tMetrics: db.NewCvssMetrics(\n\t\t\t\t\t\t\t5.5,\n\t\t\t\t\t\t\t1.8,\n\t\t\t\t\t\t\t3.6,\n\t\t\t\t\t\t),\n\t\t\t\t\t\tVector:  \"CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N\",\n\t\t\t\t\t\tVersion: \"3.1\",\n\t\t\t\t\t\tSource:  \"nvd@nist.gov\",\n\t\t\t\t\t\tType:    \"Primary\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:       \"multiple platforms some are application\",\n\t\t\tnumEntries: 2,\n\t\t\tfixture:    \"testdata/multiple-platforms-with-application-cpe.json\",\n\t\t\tvulns: []db.Vulnerability{\n\t\t\t\t{\n\t\t\t\t\tID:          \"CVE-2023-38733\",\n\t\t\t\t\tPackageName: \"robotic_process_automation\",\n\t\t\t\t\tNamespace:   \"nvd:cpe\",\n\t\t\t\t\tPackageQualifiers: []qualifier.Qualifier{platformcpe.Qualifier{\n\t\t\t\t\t\tKind: \"platform-cpe\",\n\t\t\t\t\t\tCPE:  \"cpe:2.3:a:redhat:openshift:-:*:*:*:*:*:*:*\",\n\t\t\t\t\t}},\n\t\t\t\t\tVersionConstraint:      \">= 21.0.0, <= 21.0.7.3 || >= 23.0.0, <= 23.0.3\",\n\t\t\t\t\tVersionFormat:          \"unknown\",\n\t\t\t\t\tCPEs:                   []string{\"cpe:2.3:a:ibm:robotic_process_automation:*:*:*:*:*:*:*:*\"},\n\t\t\t\t\tRelatedVulnerabilities: nil,\n\t\t\t\t\tFix: db.Fix{\n\t\t\t\t\t\tState: \"unknown\",\n\t\t\t\t\t},\n\t\t\t\t\tAdvisories: nil,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID:          \"CVE-2023-38733\",\n\t\t\t\t\tPackageName: \"robotic_process_automation\",\n\t\t\t\t\tNamespace:   \"nvd:cpe\",\n\t\t\t\t\tPackageQualifiers: []qualifier.Qualifier{platformcpe.Qualifier{\n\t\t\t\t\t\tKind: \"platform-cpe\",\n\t\t\t\t\t\tCPE:  \"cpe:2.3:o:microsoft:windows:-:*:*:*:*:*:*:*\",\n\t\t\t\t\t}},\n\t\t\t\t\tVersionConstraint:      \">= 21.0.0, <= 21.0.7.3 || >= 23.0.0, <= 23.0.3\",\n\t\t\t\t\tVersionFormat:          \"unknown\",\n\t\t\t\t\tCPEs:                   []string{\"cpe:2.3:a:ibm:robotic_process_automation:*:*:*:*:*:*:*:*\"},\n\t\t\t\t\tRelatedVulnerabilities: nil,\n\t\t\t\t\tFix: db.Fix{\n\t\t\t\t\t\tState: \"unknown\",\n\t\t\t\t\t},\n\t\t\t\t\tAdvisories: nil,\n\t\t\t\t},\n\t\t\t},\n\t\t\tmetadata: db.VulnerabilityMetadata{\n\t\t\t\tID:           \"CVE-2023-38733\",\n\t\t\t\tNamespace:    \"nvd:cpe\",\n\t\t\t\tDataSource:   \"https://nvd.nist.gov/vuln/detail/CVE-2023-38733\",\n\t\t\t\tRecordSource: \"nvdv2:nvdv2:cves\",\n\t\t\t\tSeverity:     \"Medium\",\n\t\t\t\tURLs: []string{\n\t\t\t\t\t\"https://exchange.xforce.ibmcloud.com/vulnerabilities/262293\",\n\t\t\t\t\t\"https://www.ibm.com/support/pages/node/7028223\",\n\t\t\t\t},\n\t\t\t\tDescription: \"\\nIBM Robotic Process Automation 21.0.0 through 21.0.7.1 and 23.0.0 through 23.0.1 server could allow an authenticated user to view sensitive information from installation logs.  IBM X-Force Id:  262293.\\n\\n\",\n\t\t\t\tCvss: []db.Cvss{\n\t\t\t\t\t{\n\t\t\t\t\t\tVendorMetadata: nil,\n\t\t\t\t\t\tMetrics:        db.NewCvssMetrics(4.3, 2.8, 1.4),\n\t\t\t\t\t\tVector:         \"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N\",\n\t\t\t\t\t\tVersion:        \"3.1\",\n\t\t\t\t\t\tSource:         \"nvd@nist.gov\",\n\t\t\t\t\t\tType:           \"Primary\",\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tVendorMetadata: nil,\n\t\t\t\t\t\tMetrics:        db.NewCvssMetrics(4.3, 2.8, 1.4),\n\t\t\t\t\t\tVector:         \"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N\",\n\t\t\t\t\t\tVersion:        \"3.1\",\n\t\t\t\t\t\tSource:         \"psirt@us.ibm.com\",\n\t\t\t\t\t\tType:           \"Secondary\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:       \"Platform CPE first in CPE config list\",\n\t\t\tnumEntries: 1,\n\t\t\tfixture:    \"testdata/CVE-2023-45283-platform-cpe-first.json\",\n\t\t\tvulns: []db.Vulnerability{\n\t\t\t\t{\n\t\t\t\t\tID:          \"CVE-2023-45283\",\n\t\t\t\t\tPackageName: \"go\",\n\t\t\t\t\tNamespace:   \"nvd:cpe\",\n\t\t\t\t\tPackageQualifiers: []qualifier.Qualifier{platformcpe.Qualifier{\n\t\t\t\t\t\tKind: \"platform-cpe\",\n\t\t\t\t\t\tCPE:  \"cpe:2.3:o:microsoft:windows:-:*:*:*:*:*:*:*\",\n\t\t\t\t\t}},\n\t\t\t\t\tVersionConstraint:      \"< 1.20.11 || >= 1.21.0-0, < 1.21.4\",\n\t\t\t\t\tVersionFormat:          \"unknown\",\n\t\t\t\t\tCPEs:                   []string{\"cpe:2.3:a:golang:go:*:*:*:*:*:*:*:*\"},\n\t\t\t\t\tRelatedVulnerabilities: nil,\n\t\t\t\t\tFix: db.Fix{\n\t\t\t\t\t\tVersions: []string{\"1.20.11\", \"1.21.4\"},\n\t\t\t\t\t\tState:    \"fixed\",\n\t\t\t\t\t},\n\t\t\t\t\tAdvisories: nil,\n\t\t\t\t},\n\t\t\t},\n\t\t\tmetadata: db.VulnerabilityMetadata{\n\t\t\t\tID:           \"CVE-2023-45283\",\n\t\t\t\tNamespace:    \"nvd:cpe\",\n\t\t\t\tDataSource:   \"https://nvd.nist.gov/vuln/detail/CVE-2023-45283\",\n\t\t\t\tRecordSource: \"nvdv2:nvdv2:cves\",\n\t\t\t\tSeverity:     \"High\",\n\t\t\t\tURLs: []string{\n\t\t\t\t\t\"http://www.openwall.com/lists/oss-security/2023/12/05/2\",\n\t\t\t\t\t\"https://go.dev/cl/540277\",\n\t\t\t\t\t\"https://go.dev/cl/541175\",\n\t\t\t\t\t\"https://go.dev/issue/63713\",\n\t\t\t\t\t\"https://go.dev/issue/64028\",\n\t\t\t\t\t\"https://groups.google.com/g/golang-announce/c/4tU8LZfBFkY\",\n\t\t\t\t\t\"https://groups.google.com/g/golang-dev/c/6ypN5EjibjM/m/KmLVYH_uAgAJ\",\n\t\t\t\t\t\"https://pkg.go.dev/vuln/GO-2023-2185\",\n\t\t\t\t\t\"https://security.netapp.com/advisory/ntap-20231214-0008/\",\n\t\t\t\t},\n\t\t\t\tDescription: \"The filepath package does not recognize paths with a \\\\??\\\\ prefix as special. On Windows, a path beginning with \\\\??\\\\ is a Root Local Device path equivalent to a path beginning with \\\\\\\\?\\\\. Paths with a \\\\??\\\\ prefix may be used to access arbitrary locations on the system. For example, the path \\\\??\\\\c:\\\\x is equivalent to the more common path c:\\\\x. Before fix, Clean could convert a rooted path such as \\\\a\\\\..\\\\??\\\\b into the root local device path \\\\??\\\\b. Clean will now convert this to .\\\\??\\\\b. Similarly, Join(\\\\, ??, b) could convert a seemingly innocent sequence of path elements into the root local device path \\\\??\\\\b. Join will now convert this to \\\\.\\\\??\\\\b. In addition, with fix, IsAbs now correctly reports paths beginning with \\\\??\\\\ as absolute, and VolumeName correctly reports the \\\\??\\\\ prefix as a volume name. UPDATE: Go 1.20.11 and Go 1.21.4 inadvertently changed the definition of the volume name in Windows paths starting with \\\\?, resulting in filepath.Clean(\\\\?\\\\c:) returning \\\\?\\\\c: rather than \\\\?\\\\c:\\\\ (among other effects). The previous behavior has been restored.\",\n\t\t\t\tCvss: []db.Cvss{\n\t\t\t\t\t{\n\t\t\t\t\t\tVendorMetadata: nil,\n\t\t\t\t\t\tMetrics:        db.NewCvssMetrics(7.5, 3.9, 3.6),\n\t\t\t\t\t\tVector:         \"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N\",\n\t\t\t\t\t\tVersion:        \"3.1\",\n\t\t\t\t\t\tSource:         \"nvd@nist.gov\",\n\t\t\t\t\t\tType:           \"Primary\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:       \"Platform CPE last in CPE config list\",\n\t\t\tnumEntries: 1,\n\t\t\tfixture:    \"testdata/CVE-2023-45283-platform-cpe-last.json\",\n\t\t\tvulns: []db.Vulnerability{\n\t\t\t\t{\n\t\t\t\t\tID:          \"CVE-2023-45283\",\n\t\t\t\t\tPackageName: \"go\",\n\t\t\t\t\tNamespace:   \"nvd:cpe\",\n\t\t\t\t\tPackageQualifiers: []qualifier.Qualifier{platformcpe.Qualifier{\n\t\t\t\t\t\tKind: \"platform-cpe\",\n\t\t\t\t\t\tCPE:  \"cpe:2.3:o:microsoft:windows:-:*:*:*:*:*:*:*\",\n\t\t\t\t\t}},\n\t\t\t\t\tVersionConstraint:      \"< 1.20.11 || >= 1.21.0-0, < 1.21.4\",\n\t\t\t\t\tVersionFormat:          \"unknown\",\n\t\t\t\t\tCPEs:                   []string{\"cpe:2.3:a:golang:go:*:*:*:*:*:*:*:*\"},\n\t\t\t\t\tRelatedVulnerabilities: nil,\n\t\t\t\t\tFix: db.Fix{\n\t\t\t\t\t\tVersions: []string{\"1.20.11\", \"1.21.4\"},\n\t\t\t\t\t\tState:    \"fixed\",\n\t\t\t\t\t},\n\t\t\t\t\tAdvisories: nil,\n\t\t\t\t},\n\t\t\t},\n\t\t\tmetadata: db.VulnerabilityMetadata{\n\t\t\t\tID:           \"CVE-2023-45283\",\n\t\t\t\tNamespace:    \"nvd:cpe\",\n\t\t\t\tDataSource:   \"https://nvd.nist.gov/vuln/detail/CVE-2023-45283\",\n\t\t\t\tRecordSource: \"nvdv2:nvdv2:cves\",\n\t\t\t\tSeverity:     \"High\",\n\t\t\t\tURLs: []string{\n\t\t\t\t\t\"http://www.openwall.com/lists/oss-security/2023/12/05/2\",\n\t\t\t\t\t\"https://go.dev/cl/540277\",\n\t\t\t\t\t\"https://go.dev/cl/541175\",\n\t\t\t\t\t\"https://go.dev/issue/63713\",\n\t\t\t\t\t\"https://go.dev/issue/64028\",\n\t\t\t\t\t\"https://groups.google.com/g/golang-announce/c/4tU8LZfBFkY\",\n\t\t\t\t\t\"https://groups.google.com/g/golang-dev/c/6ypN5EjibjM/m/KmLVYH_uAgAJ\",\n\t\t\t\t\t\"https://pkg.go.dev/vuln/GO-2023-2185\",\n\t\t\t\t\t\"https://security.netapp.com/advisory/ntap-20231214-0008/\",\n\t\t\t\t},\n\t\t\t\tDescription: \"The filepath package does not recognize paths with a \\\\??\\\\ prefix as special. On Windows, a path beginning with \\\\??\\\\ is a Root Local Device path equivalent to a path beginning with \\\\\\\\?\\\\. Paths with a \\\\??\\\\ prefix may be used to access arbitrary locations on the system. For example, the path \\\\??\\\\c:\\\\x is equivalent to the more common path c:\\\\x. Before fix, Clean could convert a rooted path such as \\\\a\\\\..\\\\??\\\\b into the root local device path \\\\??\\\\b. Clean will now convert this to .\\\\??\\\\b. Similarly, Join(\\\\, ??, b) could convert a seemingly innocent sequence of path elements into the root local device path \\\\??\\\\b. Join will now convert this to \\\\.\\\\??\\\\b. In addition, with fix, IsAbs now correctly reports paths beginning with \\\\??\\\\ as absolute, and VolumeName correctly reports the \\\\??\\\\ prefix as a volume name. UPDATE: Go 1.20.11 and Go 1.21.4 inadvertently changed the definition of the volume name in Windows paths starting with \\\\?, resulting in filepath.Clean(\\\\?\\\\c:) returning \\\\?\\\\c: rather than \\\\?\\\\c:\\\\ (among other effects). The previous behavior has been restored.\",\n\t\t\t\tCvss: []db.Cvss{\n\t\t\t\t\t{\n\t\t\t\t\t\tVendorMetadata: nil,\n\t\t\t\t\t\tMetrics:        db.NewCvssMetrics(7.5, 3.9, 3.6),\n\t\t\t\t\t\tVector:         \"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N\",\n\t\t\t\t\t\tVersion:        \"3.1\",\n\t\t\t\t\t\tSource:         \"nvd@nist.gov\",\n\t\t\t\t\t\tType:           \"Primary\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tif test.config == (Config{}) {\n\t\t\t\ttest.config = defaultConfig()\n\t\t\t}\n\t\t\tf, err := os.Open(test.fixture)\n\t\t\trequire.NoError(t, err)\n\t\t\tt.Cleanup(func() {\n\t\t\t\tassert.NoError(t, f.Close())\n\t\t\t})\n\n\t\t\tentries, err := unmarshal.NvdVulnerabilityEntries(f)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tvar vulns []db.Vulnerability\n\t\t\tfor _, entry := range entries {\n\t\t\t\tdataEntries, err := transform(test.config, entry.Cve)\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\tfor _, entry := range dataEntries {\n\t\t\t\t\tswitch vuln := entry.Data.(type) {\n\t\t\t\t\tcase db.Vulnerability:\n\t\t\t\t\t\tvulns = append(vulns, vuln)\n\t\t\t\t\tcase db.VulnerabilityMetadata:\n\t\t\t\t\t\t// check metadata\n\t\t\t\t\t\tif diff := deep.Equal(test.metadata, vuln); diff != nil {\n\t\t\t\t\t\t\tfor _, d := range diff {\n\t\t\t\t\t\t\t\tt.Errorf(\"metadata diff: %+v\", d)\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\tdefault:\n\t\t\t\t\t\tt.Fatalf(\"unexpected condition: data entry does not have a vulnerability or a metadata\")\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif diff := cmp.Diff(test.vulns, vulns); diff != \"\" {\n\t\t\t\tt.Errorf(\"vulnerabilities do not match (-want +got):\\n%s\", diff)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestGetVersionFormat(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    string\n\t\tcpes     []string\n\t\texpected version.Format\n\t}{\n\t\t{\n\t\t\tname:     \"detects JVM format from name\",\n\t\t\tinput:    \"java_se\",\n\t\t\tcpes:     []string{},\n\t\t\texpected: version.JVMFormat,\n\t\t},\n\t\t{\n\t\t\tname:     \"detects JVM format from CPEs\",\n\t\t\tinput:    \"other_product\",\n\t\t\tcpes:     []string{\"cpe:2.3:a:oracle:openjdk:11:update53:*:*:*:*:*:*\"},\n\t\t\texpected: version.JVMFormat,\n\t\t},\n\t\t{\n\t\t\tname:     \"detects JVM format from another CPE (zulu)\",\n\t\t\tinput:    \"other_product\",\n\t\t\tcpes:     []string{\"cpe:2.3:a:zula:zulu:15:*:*:*:*:*:*:*\"},\n\t\t\texpected: version.JVMFormat,\n\t\t},\n\t\t{\n\t\t\tname:     \"detects JVM format from another CPE (jdk)\",\n\t\t\tinput:    \"other_product\",\n\t\t\tcpes:     []string{\"cpe:2.3:a:oracle:jdk:11.0:*:*:*:*:*:*:*\"},\n\t\t\texpected: version.JVMFormat,\n\t\t},\n\t\t{\n\t\t\tname:     \"detects JVM format from another CPE (jre)\",\n\t\t\tinput:    \"other_product\",\n\t\t\tcpes:     []string{\"cpe:2.3:a:oracle:jre:11.0:*:*:*:*:*:*:*\"},\n\t\t\texpected: version.JVMFormat,\n\t\t},\n\t\t{\n\t\t\tname:     \"returns unknown format for non-JVM product and non-JVM CPEs\",\n\t\t\tinput:    \"non_jvm_product\",\n\t\t\tcpes:     []string{\"cpe:2.3:a:some_other_product:product_name:1.0:*:*:*:*:*:*\"},\n\t\t\texpected: version.UnknownFormat,\n\t\t},\n\t\t{\n\t\t\tname:     \"handles invalid CPE gracefully\",\n\t\t\tinput:    \"non_jvm_product\",\n\t\t\tcpes:     []string{\"invalid_cpe_format\"},\n\t\t\texpected: version.UnknownFormat,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tformat := getVersionFormat(tt.input, tt.cpes)\n\t\t\tassert.Equal(t, tt.expected, format)\n\t\t})\n\t}\n}\n\nfunc TestGetFix(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tmatches  []nvd.CpeMatch\n\t\texpected db.Fix\n\t}{\n\t\t{\n\t\t\tname: \"Equals\",\n\t\t\tmatches: []nvd.CpeMatch{\n\t\t\t\t{\n\t\t\t\t\tCriteria:   \"cpe:2.3:a:vendor:product:2.2.0:*:*:*:*:target:*:*\",\n\t\t\t\t\tVulnerable: true,\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: db.Fix{\n\t\t\t\tVersions: nil,\n\t\t\t\tState:    \"unknown\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"VersionEndExcluding\",\n\t\t\tmatches: []nvd.CpeMatch{\n\t\t\t\t{\n\t\t\t\t\tCriteria:            \"cpe:2.3:a:vendor:product:*:*:*:*:*:target:*:*\",\n\t\t\t\t\tVersionEndExcluding: strRef(\"2.3.0\"),\n\t\t\t\t\tVulnerable:          true,\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: db.Fix{\n\t\t\t\tVersions: []string{\"2.3.0\"},\n\t\t\t\tState:    \"fixed\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"VersionEndIncluding\",\n\t\t\tmatches: []nvd.CpeMatch{\n\t\t\t\t{\n\t\t\t\t\tCriteria:            \"cpe:2.3:a:vendor:product:*:*:*:*:*:target:*:*\",\n\t\t\t\t\tVersionEndIncluding: strRef(\"2.3.0\"),\n\t\t\t\t\tVulnerable:          true,\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: db.Fix{\n\t\t\t\tVersions: nil,\n\t\t\t\tState:    \"unknown\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"VersionStartExcluding\",\n\t\t\tmatches: []nvd.CpeMatch{\n\t\t\t\t{\n\t\t\t\t\tCriteria:              \"cpe:2.3:a:vendor:product:*:*:*:*:*:target:*:*\",\n\t\t\t\t\tVersionStartExcluding: strRef(\"2.3.0\"),\n\t\t\t\t\tVulnerable:            true,\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: db.Fix{\n\t\t\t\tVersions: nil,\n\t\t\t\tState:    \"unknown\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"VersionStartIncluding\",\n\t\t\tmatches: []nvd.CpeMatch{\n\t\t\t\t{\n\t\t\t\t\tCriteria:              \"cpe:2.3:a:vendor:product:*:*:*:*:*:target:*:*\",\n\t\t\t\t\tVersionStartIncluding: strRef(\"2.3.0\"),\n\t\t\t\t\tVulnerable:            true,\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: db.Fix{\n\t\t\t\tVersions: nil,\n\t\t\t\tState:    \"unknown\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Version Range\",\n\t\t\tmatches: []nvd.CpeMatch{\n\t\t\t\t{\n\t\t\t\t\tCriteria:              \"cpe:2.3:a:vendor:product:*:*:*:*:*:target:*:*\",\n\t\t\t\t\tVersionStartIncluding: strRef(\"2.3.0\"),\n\t\t\t\t\tVersionEndIncluding:   strRef(\"2.5.0\"),\n\t\t\t\t\tVulnerable:            true,\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: db.Fix{\n\t\t\t\tVersions: nil,\n\t\t\t\tState:    \"unknown\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Multiple Version Ranges\",\n\t\t\tmatches: []nvd.CpeMatch{\n\t\t\t\t{\n\t\t\t\t\tCriteria:              \"cpe:2.3:a:vendor:product:*:*:*:*:*:target:*:*\",\n\t\t\t\t\tVersionStartIncluding: strRef(\"2.3.0\"),\n\t\t\t\t\tVersionEndIncluding:   strRef(\"2.5.0\"),\n\t\t\t\t\tVulnerable:            true,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tCriteria:              \"cpe:2.3:a:vendor:product:*:*:*:*:*:target:*:*\",\n\t\t\t\t\tVersionStartExcluding: strRef(\"3.3.0\"),\n\t\t\t\t\tVersionEndExcluding:   strRef(\"3.5.0\"),\n\t\t\t\t\tVulnerable:            true,\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: db.Fix{\n\t\t\t\tVersions: []string{\"3.5.0\"},\n\t\t\t\tState:    \"fixed\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Empty end exclude treated as unknown\",\n\t\t\tmatches: []nvd.CpeMatch{\n\t\t\t\t{\n\t\t\t\t\tCriteria:              \"cpe:2.3:a:vendor:product:*:*:*:*:*:target:*:*\",\n\t\t\t\t\tVersionStartExcluding: strRef(\"3.3.0\"),\n\t\t\t\t\tVersionEndExcluding:   strRef(\"\"),\n\t\t\t\t\tVulnerable:            true,\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: db.Fix{\n\t\t\t\tVersions: nil,\n\t\t\t\tState:    \"unknown\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Multiple fixes with deduplication\",\n\t\t\tmatches: []nvd.CpeMatch{\n\t\t\t\t{\n\t\t\t\t\tCriteria:              \"cpe:2.3:a:vendor:product:*:*:*:*:*:target:*:*\",\n\t\t\t\t\tVersionStartIncluding: strRef(\"3.3.0\"),\n\t\t\t\t\tVersionEndExcluding:   strRef(\"3.5.0\"),\n\t\t\t\t\tVulnerable:            true,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tCriteria:              \"cpe:2.3:a:vendor:product:*:*:*:*:*:target:*:*\",\n\t\t\t\t\tVersionStartIncluding: strRef(\"0\"),\n\t\t\t\t\tVersionEndExcluding:   strRef(\"1.7.0\"),\n\t\t\t\t\tVulnerable:            true,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tCriteria:              \"cpe:2.3:a:vendor:product:*:*:*:*:*:target-2:*:*\",\n\t\t\t\t\tVersionStartIncluding: strRef(\"0\"),\n\t\t\t\t\tVersionEndExcluding:   strRef(\"1.7.0\"),\n\t\t\t\t\tVulnerable:            true,\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: db.Fix{\n\t\t\t\tVersions: []string{\"1.7.0\", \"3.5.0\"},\n\t\t\t\tState:    \"fixed\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"< version as end in a separate affected >= range\",\n\t\t\tmatches: []nvd.CpeMatch{\n\t\t\t\t{\n\t\t\t\t\tCriteria:              \"cpe:2.3:a:vendor:product:*:*:*:*:*:target:*:*\",\n\t\t\t\t\tVersionStartIncluding: strRef(\"2.3.0\"),\n\t\t\t\t\tVersionEndExcluding:   strRef(\"2.5.0\"),\n\t\t\t\t\tVulnerable:            true,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tCriteria:              \"cpe:2.3:a:vendor:product:*:*:*:*:*:target:*:*\",\n\t\t\t\t\tVersionStartIncluding: strRef(\"2.5.0\"),\n\t\t\t\t\tVersionEndExcluding:   strRef(\"3.5.0\"),\n\t\t\t\t\tVulnerable:            true,\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: db.Fix{\n\t\t\t\tVersions: []string{\"3.5.0\"},\n\t\t\t\tState:    \"fixed\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"< version as start in a separate affected <= range\",\n\t\t\tmatches: []nvd.CpeMatch{\n\t\t\t\t{\n\t\t\t\t\tCriteria:              \"cpe:2.3:a:vendor:product:*:*:*:*:*:target:*:*\",\n\t\t\t\t\tVersionStartIncluding: strRef(\"2.3.0\"),\n\t\t\t\t\tVersionEndExcluding:   strRef(\"2.5.0\"),\n\t\t\t\t\tVulnerable:            true,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tCriteria:              \"cpe:2.3:a:vendor:product:*:*:*:*:*:target:*:*\",\n\t\t\t\t\tVersionStartIncluding: strRef(\"2.1.0\"),\n\t\t\t\t\tVersionEndIncluding:   strRef(\"2.5.0\"),\n\t\t\t\t\tVulnerable:            true,\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: db.Fix{\n\t\t\t\tVersions: nil,\n\t\t\t\tState:    \"unknown\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"< range with same version affected == critera\",\n\t\t\tmatches: []nvd.CpeMatch{\n\t\t\t\t{\n\t\t\t\t\tCriteria:              \"cpe:2.3:a:vendor:product:*:*:*:*:*:target:*:*\",\n\t\t\t\t\tVersionStartIncluding: strRef(\"2.3.0\"),\n\t\t\t\t\tVersionEndExcluding:   strRef(\"2.5.0\"),\n\t\t\t\t\tVulnerable:            true,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tCriteria:   \"cpe:2.3:a:vendor:product:2.5.0:*:*:*:*:target:*:*\",\n\t\t\t\t\tVulnerable: true,\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: db.Fix{\n\t\t\t\tVersions: nil,\n\t\t\t\tState:    \"unknown\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"< range with another unaffected entry\",\n\t\t\tmatches: []nvd.CpeMatch{\n\t\t\t\t{\n\t\t\t\t\tCriteria:              \"cpe:2.3:a:vendor:product:*:*:*:*:*:target:*:*\",\n\t\t\t\t\tVersionStartIncluding: strRef(\"2.3.0\"),\n\t\t\t\t\tVersionEndExcluding:   strRef(\"2.5.0\"),\n\t\t\t\t\tVulnerable:            true,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tCriteria:   \"cpe:2.3:a:vendor:product:2.5.0:*:*:*:*:target:*:*\",\n\t\t\t\t\tVulnerable: false,\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: db.Fix{\n\t\t\t\tVersions: []string{\"2.5.0\"},\n\t\t\t\tState:    \"fixed\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"treat * in < as unknown fix state\",\n\t\t\tmatches: []nvd.CpeMatch{\n\t\t\t\t{\n\t\t\t\t\tCriteria:              \"cpe:2.3:a:vendor:product:*:*:*:*:*:target:*:*\",\n\t\t\t\t\tVersionStartIncluding: strRef(\"2.3.0\"),\n\t\t\t\t\tVersionEndExcluding:   strRef(\"*\"),\n\t\t\t\t\tVulnerable:            true,\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: db.Fix{\n\t\t\t\tVersions: nil,\n\t\t\t\tState:    \"unknown\",\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tfix := getFix(tt.matches, true)\n\t\t\tassert.Equal(t, tt.expected, fix)\n\t\t})\n\n\t\tt.Run(tt.name+\" don't infer NVD fixes\", func(t *testing.T) {\n\t\t\tfix := getFix(tt.matches, false)\n\t\t\tassert.Equal(t, db.Fix{\n\t\t\t\tVersions: nil,\n\t\t\t\tState:    \"unknown\",\n\t\t\t}, fix)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/db/v5/build/transformers/nvd/unique_pkg.go",
    "content": "package nvd\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/umisama/go-cpe\"\n\n\t\"github.com/anchore/grype/grype/db/internal/provider/unmarshal/nvd\"\n\t\"github.com/anchore/grype/grype/db/internal/versionutil\"\n\t\"github.com/anchore/grype/internal/log\"\n)\n\nconst (\n\tANY = \"*\"\n\tNA  = \"-\"\n)\n\ntype pkgCandidate struct {\n\tProduct        string\n\tVendor         string\n\tTargetSoftware string\n\tPlatformCPE    string\n}\n\nfunc (p pkgCandidate) String() string {\n\tif p.PlatformCPE == \"\" {\n\t\treturn fmt.Sprintf(\"%s|%s|%s\", p.Vendor, p.Product, p.TargetSoftware)\n\t}\n\n\treturn fmt.Sprintf(\"%s|%s|%s|%s\", p.Vendor, p.Product, p.TargetSoftware, p.PlatformCPE)\n}\n\nfunc newPkgCandidate(tCfg Config, match nvd.CpeMatch, platformCPE string) (*pkgCandidate, error) {\n\t// we are only interested in packages that are vulnerable (not related to secondary match conditioning)\n\tif !match.Vulnerable {\n\t\treturn nil, nil\n\t}\n\n\tc, err := cpe.NewItemFromFormattedString(match.Criteria)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create uniquePkgEntry from '%s': %w\", match.Criteria, err)\n\t}\n\n\t// we are interested in applications, conditionally operating systems, but never hardware\n\tpart := c.Part()\n\tif !tCfg.CPEParts.Has(string(part)) {\n\t\treturn nil, nil\n\t}\n\n\treturn &pkgCandidate{\n\t\tProduct:        c.Product().String(),\n\t\tVendor:         c.Vendor().String(),\n\t\tTargetSoftware: c.TargetSw().String(),\n\t\tPlatformCPE:    platformCPE,\n\t}, nil\n}\n\nfunc findUniquePkgs(tCfg Config, cfgs ...nvd.Configuration) uniquePkgTracker {\n\tset := newUniquePkgTracker()\n\tfor _, c := range cfgs {\n\t\t_findUniquePkgs(tCfg, set, c)\n\t}\n\treturn set\n}\n\nfunc platformPackageCandidates(tCfg Config, set uniquePkgTracker, c nvd.Configuration) bool {\n\tnodes := c.Nodes\n\t/*\n\t\tTurn a configuration like this:\n\t\t(AND\n\t\t\t(OR (cpe:2.3:a:redis:...whatever) (cpe:2.3.:something:...whatever)\n\t\t\t(OR (cpe:2.3:o:debian:9....) (cpe:2.3:o:ubuntu:22..))\n\t\t)\n\t\tInto a configuration like this:\n\t\t(OR\n\t\t\t(AND (cpe:2.3:a:redis:...whatever) (cpe:2.3:o:debian:9...))\n\t\t\t(AND (cpe:2.3:a:redis:...whatever) (cpe:2.3:o:ubuntu:22...))\n\t\t\t(AND (cpe:2.3:a:something:...whatever) (cpe:2.3:o:debian:9...))\n\t\t\t(AND (cpe:2.3:a:something:...whatever) (cpe:2.3:o:ubuntu:22...))\n\t\t)\n\t\tBecause in schema v5, rows in Grype DB can only have zero or one platform CPE\n\t\tconstraint.\n\t*/\n\tif len(nodes) != 2 || c.Operator == nil || *c.Operator != nvd.And {\n\t\treturn false\n\t}\n\tvar platformsNode nvd.Node\n\tvar applicationNode nvd.Node\n\tfor _, n := range nodes {\n\t\tif anyHardwareCPEPresent(n) {\n\t\t\treturn false\n\t\t}\n\t\tif allCPEsVulnerable(n) {\n\t\t\tapplicationNode = n\n\t\t}\n\t\tif noCPEsVulnerable(n) {\n\t\t\tplatformsNode = n\n\t\t}\n\t}\n\tif platformsNode.Operator != nvd.Or {\n\t\treturn false\n\t}\n\tif applicationNode.Operator != nvd.Or {\n\t\treturn false\n\t}\n\tresult := false\n\tmatches := platformsNode.CpeMatch\n\tfor _, application := range applicationNode.CpeMatch {\n\t\tfor _, maybePlatform := range matches {\n\t\t\tplatform := maybePlatform.Criteria\n\t\t\tcandidate, err := newPkgCandidate(tCfg, application, platform)\n\t\t\tif err != nil {\n\t\t\t\tlog.Debugf(\"unable processing uniquePkg with multiple platforms: %v\", err)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif candidate == nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tset.Add(*candidate, application)\n\t\t\tresult = true\n\t\t}\n\t}\n\treturn result\n}\n\nfunc anyHardwareCPEPresent(n nvd.Node) bool {\n\tfor _, c := range n.CpeMatch {\n\t\tparts := strings.Split(c.Criteria, \":\")\n\t\tif len(parts) < 3 || parts[2] == \"h\" {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc allCPEsVulnerable(node nvd.Node) bool {\n\tfor _, c := range node.CpeMatch {\n\t\tif !c.Vulnerable {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\nfunc noCPEsVulnerable(node nvd.Node) bool {\n\tfor _, c := range node.CpeMatch {\n\t\tif c.Vulnerable {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\nfunc _findUniquePkgs(tCfg Config, set uniquePkgTracker, c nvd.Configuration) {\n\tif len(c.Nodes) == 0 {\n\t\treturn\n\t}\n\n\tif platformPackageCandidates(tCfg, set, c) {\n\t\treturn\n\t}\n\n\tfor _, node := range c.Nodes {\n\t\tfor _, match := range node.CpeMatch {\n\t\t\tcandidate, err := newPkgCandidate(tCfg, match, \"\")\n\t\t\tif err != nil {\n\t\t\t\t// Do not halt all execution because of being unable to create\n\t\t\t\t// a PkgCandidate. This can happen when a CPE is invalid which\n\t\t\t\t// could avoid creating a database\n\t\t\t\tlog.Debugf(\"unable processing uniquePkg: %v\", err)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif candidate != nil {\n\t\t\t\tset.Add(*candidate, match)\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc buildConstraints(matches []nvd.CpeMatch) string {\n\tconstraints := make([]string, 0)\n\tfor _, match := range matches {\n\t\tconstraints = append(constraints, buildConstraint(match))\n\t}\n\n\treturn versionutil.OrConstraints(removeDuplicateConstraints(constraints)...)\n}\n\nfunc buildConstraint(match nvd.CpeMatch) string {\n\tconstraints := make([]string, 0)\n\tif match.VersionStartIncluding != nil && *match.VersionStartIncluding != \"\" {\n\t\tconstraints = append(constraints, fmt.Sprintf(\">= %s\", *match.VersionStartIncluding))\n\t} else if match.VersionStartExcluding != nil && *match.VersionStartExcluding != \"\" {\n\t\tconstraints = append(constraints, fmt.Sprintf(\"> %s\", *match.VersionStartExcluding))\n\t}\n\n\tif match.VersionEndIncluding != nil && *match.VersionEndIncluding != \"\" {\n\t\tconstraints = append(constraints, fmt.Sprintf(\"<= %s\", *match.VersionEndIncluding))\n\t} else if match.VersionEndExcluding != nil && *match.VersionEndExcluding != \"\" {\n\t\tconstraints = append(constraints, fmt.Sprintf(\"< %s\", *match.VersionEndExcluding))\n\t}\n\n\tif len(constraints) == 0 {\n\t\tc, err := cpe.NewItemFromFormattedString(match.Criteria)\n\t\tif err != nil {\n\t\t\treturn \"\"\n\t\t}\n\t\tversion := c.Version().String()\n\t\tupdate := c.Update().String()\n\t\tif version != ANY && version != NA {\n\t\t\tif update != ANY && update != NA {\n\t\t\t\tversion = fmt.Sprintf(\"%s-%s\", version, update)\n\t\t\t}\n\n\t\t\tconstraints = append(constraints, fmt.Sprintf(\"= %s\", version))\n\t\t}\n\t}\n\n\treturn strings.Join(constraints, \", \")\n}\n\nfunc removeDuplicateConstraints(constraints []string) []string {\n\tconstraintMap := make(map[string]struct{})\n\tvar uniqueConstraints []string\n\tfor _, constraint := range constraints {\n\t\tif _, exists := constraintMap[constraint]; !exists {\n\t\t\tconstraintMap[constraint] = struct{}{}\n\t\t\tuniqueConstraints = append(uniqueConstraints, constraint)\n\t\t}\n\t}\n\treturn uniqueConstraints\n}\n"
  },
  {
    "path": "grype/db/v5/build/transformers/nvd/unique_pkg_test.go",
    "content": "package nvd\n\nimport (\n\t\"testing\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/scylladb/go-set/strset\"\n\t\"github.com/sergi/go-diff/diffmatchpatch\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/grype/grype/db/internal/provider/unmarshal/nvd\"\n)\n\nfunc newUniquePkgTrackerFromSlice(candidates []pkgCandidate) uniquePkgTracker {\n\tset := newUniquePkgTracker()\n\tfor _, c := range candidates {\n\t\tset[c] = nil\n\t}\n\treturn set\n}\n\nfunc TestFindUniquePkgs(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tconfig   Config\n\t\tnodes    []nvd.Node\n\t\toperator *nvd.Operator\n\t\texpected uniquePkgTracker\n\t}{\n\t\t{\n\t\t\tname: \"simple-match\",\n\t\t\tnodes: []nvd.Node{\n\t\t\t\t{\n\t\t\t\t\tCpeMatch: []nvd.CpeMatch{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tCriteria:   \"cpe:2.3:a:vendor:product:2.2.0:*:*:*:*:target:*:*\",\n\t\t\t\t\t\t\tVulnerable: true,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: newUniquePkgTrackerFromSlice(\n\t\t\t\t[]pkgCandidate{\n\t\t\t\t\t{\n\t\t\t\t\t\tProduct:        \"product\",\n\t\t\t\t\t\tVendor:         \"vendor\",\n\t\t\t\t\t\tTargetSoftware: \"target\",\n\t\t\t\t\t},\n\t\t\t\t}),\n\t\t},\n\t\t{\n\t\t\tname: \"skip-hw\",\n\t\t\tnodes: []nvd.Node{\n\t\t\t\t{\n\t\t\t\t\tCpeMatch: []nvd.CpeMatch{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tCriteria:   \"cpe:2.3:h:vendor:product:2.2.0:*:*:*:*:target:*:*\",\n\t\t\t\t\t\t\tVulnerable: true,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: newUniquePkgTrackerFromSlice([]pkgCandidate{}),\n\t\t},\n\t\t{\n\t\t\tname: \"skip-os-by-default\",\n\t\t\tnodes: []nvd.Node{\n\t\t\t\t{\n\t\t\t\t\tCpeMatch: []nvd.CpeMatch{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tCriteria:   \"cpe:2.3:o:vendor:product:2.2.0:*:*:*:*:target:*:*\",\n\t\t\t\t\t\t\tVulnerable: true,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: newUniquePkgTrackerFromSlice([]pkgCandidate{}),\n\t\t},\n\t\t{\n\t\t\tname: \"include-os-explicitly\",\n\t\t\tconfig: Config{\n\t\t\t\tCPEParts: strset.New(\"a\", \"o\"),\n\t\t\t},\n\t\t\tnodes: []nvd.Node{\n\t\t\t\t{\n\t\t\t\t\tCpeMatch: []nvd.CpeMatch{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tCriteria:   \"cpe:2.3:o:vendor:product:2.2.0:*:*:*:*:target:*:*\",\n\t\t\t\t\t\t\tVulnerable: true,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: newUniquePkgTrackerFromSlice([]pkgCandidate{\n\t\t\t\t{\n\t\t\t\t\tProduct:        \"product\",\n\t\t\t\t\tVendor:         \"vendor\",\n\t\t\t\t\tTargetSoftware: \"target\",\n\t\t\t\t},\n\t\t\t}),\n\t\t},\n\t\t{\n\t\t\tname: \"duplicate-by-product\",\n\t\t\tnodes: []nvd.Node{\n\t\t\t\t{\n\t\t\t\t\tCpeMatch: []nvd.CpeMatch{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tCriteria:   \"cpe:2.3:a:vendor:productA:3.3.3:*:*:*:*:target:*:*\",\n\t\t\t\t\t\t\tVulnerable: true,\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tCriteria:   \"cpe:2.3:a:vendor:productB:2.2.0:*:*:*:*:target:*:*\",\n\t\t\t\t\t\t\tVulnerable: true,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tOperator: \"OR\",\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: newUniquePkgTrackerFromSlice(\n\t\t\t\t[]pkgCandidate{\n\t\t\t\t\t{\n\t\t\t\t\t\tProduct:        \"productA\",\n\t\t\t\t\t\tVendor:         \"vendor\",\n\t\t\t\t\t\tTargetSoftware: \"target\",\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tProduct:        \"productB\",\n\t\t\t\t\t\tVendor:         \"vendor\",\n\t\t\t\t\t\tTargetSoftware: \"target\",\n\t\t\t\t\t},\n\t\t\t\t}),\n\t\t},\n\t\t{\n\t\t\tname: \"duplicate-by-target\",\n\t\t\tnodes: []nvd.Node{\n\t\t\t\t{\n\t\t\t\t\tCpeMatch: []nvd.CpeMatch{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tCriteria:   \"cpe:2.3:a:vendor:product:3.3.3:*:*:*:*:targetA:*:*\",\n\t\t\t\t\t\t\tVulnerable: true,\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tCriteria:   \"cpe:2.3:a:vendor:product:2.2.0:*:*:*:*:targetB:*:*\",\n\t\t\t\t\t\t\tVulnerable: true,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tOperator: \"OR\",\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: newUniquePkgTrackerFromSlice(\n\t\t\t\t[]pkgCandidate{\n\t\t\t\t\t{\n\t\t\t\t\t\tProduct:        \"product\",\n\t\t\t\t\t\tVendor:         \"vendor\",\n\t\t\t\t\t\tTargetSoftware: \"targetA\",\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tProduct:        \"product\",\n\t\t\t\t\t\tVendor:         \"vendor\",\n\t\t\t\t\t\tTargetSoftware: \"targetB\",\n\t\t\t\t\t},\n\t\t\t\t}),\n\t\t},\n\t\t{\n\t\t\tname: \"duplicate-by-vendor\",\n\t\t\tnodes: []nvd.Node{\n\t\t\t\t{\n\t\t\t\t\tCpeMatch: []nvd.CpeMatch{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tCriteria:   \"cpe:2.3:a:vendorA:product:3.3.3:*:*:*:*:target:*:*\",\n\t\t\t\t\t\t\tVulnerable: true,\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tCriteria:   \"cpe:2.3:a:vendorB:product:2.2.0:*:*:*:*:target:*:*\",\n\t\t\t\t\t\t\tVulnerable: true,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tOperator: \"OR\",\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: newUniquePkgTrackerFromSlice(\n\t\t\t\t[]pkgCandidate{\n\t\t\t\t\t{\n\t\t\t\t\t\tProduct:        \"product\",\n\t\t\t\t\t\tVendor:         \"vendorA\",\n\t\t\t\t\t\tTargetSoftware: \"target\",\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tProduct:        \"product\",\n\t\t\t\t\t\tVendor:         \"vendorB\",\n\t\t\t\t\t\tTargetSoftware: \"target\",\n\t\t\t\t\t},\n\t\t\t\t}),\n\t\t},\n\t\t{\n\t\t\tname: \"de-duplicate-case\",\n\t\t\tnodes: []nvd.Node{\n\t\t\t\t{\n\t\t\t\t\tCpeMatch: []nvd.CpeMatch{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tCriteria:   \"cpe:2.3:a:vendor:product:3.3.3:A:B:C:D:target:E:F\",\n\t\t\t\t\t\t\tVulnerable: true,\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tCriteria:   \"cpe:2.3:a:vendor:product:2.2.0:Q:R:S:T:target:U:V\",\n\t\t\t\t\t\t\tVulnerable: true,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tOperator: \"OR\",\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: newUniquePkgTrackerFromSlice(\n\t\t\t\t[]pkgCandidate{\n\t\t\t\t\t{\n\t\t\t\t\t\tProduct:        \"product\",\n\t\t\t\t\t\tVendor:         \"vendor\",\n\t\t\t\t\t\tTargetSoftware: \"target\",\n\t\t\t\t\t},\n\t\t\t\t}),\n\t\t},\n\t\t{\n\t\t\tname: \"duplicate-from-nested-nodes\",\n\t\t\tnodes: []nvd.Node{\n\t\t\t\t{\n\t\t\t\t\tCpeMatch: []nvd.CpeMatch{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tCriteria:   \"cpe:2.3:a:vendorB:product:2.2.0:*:*:*:*:target:*:*\",\n\t\t\t\t\t\t\tVulnerable: true,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tOperator: \"OR\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tCpeMatch: []nvd.CpeMatch{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tCriteria:   \"cpe:2.3:a:vendorA:product:2.2.0:*:*:*:*:target:*:*\",\n\t\t\t\t\t\t\tVulnerable: true,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tOperator: \"OR\",\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: newUniquePkgTrackerFromSlice(\n\t\t\t\t[]pkgCandidate{\n\t\t\t\t\t{\n\t\t\t\t\t\tProduct:        \"product\",\n\t\t\t\t\t\tVendor:         \"vendorA\",\n\t\t\t\t\t\tTargetSoftware: \"target\",\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tProduct:        \"product\",\n\t\t\t\t\t\tVendor:         \"vendorB\",\n\t\t\t\t\t\tTargetSoftware: \"target\",\n\t\t\t\t\t},\n\t\t\t\t}),\n\t\t},\n\t\t{\n\t\t\tname:     \"cpe with multiple platforms\",\n\t\t\toperator: opRef(nvd.And),\n\t\t\tnodes: []nvd.Node{\n\t\t\t\t{\n\t\t\t\t\tNegate:   boolRef(false),\n\t\t\t\t\tOperator: nvd.Or,\n\t\t\t\t\tCpeMatch: []nvd.CpeMatch{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tCriteria:        \"cpe:2.3:o:canonical:ubuntu_linux:20.04:*:*:*:lts:*:*:*\",\n\t\t\t\t\t\t\tMatchCriteriaID: \"902B8056-9E37-443B-8905-8AA93E2447FB\",\n\t\t\t\t\t\t\tVulnerable:      false,\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tCriteria:        \"cpe:2.3:o:canonical:ubuntu_linux:21.10:*:*:*:-:*:*:*\",\n\t\t\t\t\t\t\tMatchCriteriaID: \"3D94DA3B-FA74-4526-A0A0-A872684598C6\",\n\t\t\t\t\t\t\tVulnerable:      false,\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tCriteria:        \"cpe:2.3:o:debian:debian_linux:9.0:*:*:*:*:*:*:*\",\n\t\t\t\t\t\t\tMatchCriteriaID: \"DEECE5FC-CACF-4496-A3E7-164736409252\",\n\t\t\t\t\t\t\tVulnerable:      false,\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tCriteria:        \"cpe:2.3:o:debian:debian_linux:10.0:*:*:*:*:*:*:*\",\n\t\t\t\t\t\t\tMatchCriteriaID: \"07B237A9-69A3-4A9C-9DA0-4E06BD37AE73\",\n\t\t\t\t\t\t\tVulnerable:      false,\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tCriteria:        \"cpe:2.3:o:debian:debian_linux:11.0:*:*:*:*:*:*:*\",\n\t\t\t\t\t\t\tMatchCriteriaID: \"FA6FEEC2-9F11-4643-8827-749718254FED\",\n\t\t\t\t\t\t\tVulnerable:      false,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tNegate:   boolRef(false),\n\t\t\t\t\tOperator: nvd.Or,\n\t\t\t\t\tCpeMatch: []nvd.CpeMatch{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tCriteria:        \"cpe:2.3:a:redis:redis:-:*:*:*:*:*:*:*\",\n\t\t\t\t\t\t\tMatchCriteriaID: \"5EBE5E1C-C881-4A76-9E36-4FB7C48427E6\",\n\t\t\t\t\t\t\tVulnerable:      true,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: newUniquePkgTrackerFromSlice([]pkgCandidate{\n\t\t\t\t{\n\t\t\t\t\tProduct:        \"redis\",\n\t\t\t\t\tVendor:         \"redis\",\n\t\t\t\t\tTargetSoftware: ANY,\n\t\t\t\t\tPlatformCPE:    \"cpe:2.3:o:canonical:ubuntu_linux:20.04:*:*:*:lts:*:*:*\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tProduct:        \"redis\",\n\t\t\t\t\tVendor:         \"redis\",\n\t\t\t\t\tTargetSoftware: ANY,\n\t\t\t\t\tPlatformCPE:    \"cpe:2.3:o:canonical:ubuntu_linux:21.10:*:*:*:-:*:*:*\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tProduct:        \"redis\",\n\t\t\t\t\tVendor:         \"redis\",\n\t\t\t\t\tTargetSoftware: ANY,\n\t\t\t\t\tPlatformCPE:    \"cpe:2.3:o:debian:debian_linux:9.0:*:*:*:*:*:*:*\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tProduct:        \"redis\",\n\t\t\t\t\tVendor:         \"redis\",\n\t\t\t\t\tTargetSoftware: ANY,\n\t\t\t\t\tPlatformCPE:    \"cpe:2.3:o:debian:debian_linux:10.0:*:*:*:*:*:*:*\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tProduct:        \"redis\",\n\t\t\t\t\tVendor:         \"redis\",\n\t\t\t\t\tTargetSoftware: ANY,\n\t\t\t\t\tPlatformCPE:    \"cpe:2.3:o:debian:debian_linux:11.0:*:*:*:*:*:*:*\",\n\t\t\t\t},\n\t\t\t}),\n\t\t},\n\t\t{\n\t\t\tname:     \"single platform CPE as first element\",\n\t\t\toperator: opRef(nvd.And),\n\t\t\tnodes: []nvd.Node{\n\t\t\t\t{\n\t\t\t\t\tNegate:   boolRef(false),\n\t\t\t\t\tOperator: nvd.Or,\n\t\t\t\t\tCpeMatch: []nvd.CpeMatch{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tCriteria:        \"cpe:2.3:o:microsoft:windows:-:*:*:*:*:*:*:*\",\n\t\t\t\t\t\t\tMatchCriteriaID: \"902B8056-9E37-443B-8905-8AA93E2447FB\",\n\t\t\t\t\t\t\tVulnerable:      false,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tNegate:   boolRef(false),\n\t\t\t\t\tOperator: nvd.Or,\n\t\t\t\t\tCpeMatch: []nvd.CpeMatch{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tCriteria:              \"cpe:2.3:a:golang:go:*:*:*:*:*:*:*:*\",\n\t\t\t\t\t\t\tVersionEndExcluding:   strRef(\"1.22.2\"),\n\t\t\t\t\t\t\tVersionStartIncluding: strRef(\"1.22\"),\n\t\t\t\t\t\t\tMatchCriteriaID:       \"5EBE5E1C-C881-4A76-9E36-4FB7C48427E6\",\n\t\t\t\t\t\t\tVulnerable:            true,\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tCriteria:            \"cpe:2.3:a:golang:go:*:*:*:*:*:*:*:*\",\n\t\t\t\t\t\t\tVersionEndExcluding: strRef(\"1.21.8\"),\n\t\t\t\t\t\t\tMatchCriteriaID:     \"5EBE5E1C-C881-4A76-9E36-4FB7C48427E6\",\n\t\t\t\t\t\t\tVulnerable:          true,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: newUniquePkgTrackerFromSlice([]pkgCandidate{\n\t\t\t\t{\n\t\t\t\t\tProduct:        \"go\",\n\t\t\t\t\tVendor:         \"golang\",\n\t\t\t\t\tTargetSoftware: ANY,\n\t\t\t\t\tPlatformCPE:    \"cpe:2.3:o:microsoft:windows:-:*:*:*:*:*:*:*\",\n\t\t\t\t},\n\t\t\t}),\n\t\t},\n\t\t{\n\t\t\tname:     \"single platform CPE as last element\",\n\t\t\toperator: opRef(nvd.And),\n\t\t\tnodes: []nvd.Node{\n\t\t\t\t{\n\t\t\t\t\tNegate:   boolRef(false),\n\t\t\t\t\tOperator: nvd.Or,\n\t\t\t\t\tCpeMatch: []nvd.CpeMatch{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tCriteria:              \"cpe:2.3:a:golang:go:*:*:*:*:*:*:*:*\",\n\t\t\t\t\t\t\tVersionEndExcluding:   strRef(\"1.22.2\"),\n\t\t\t\t\t\t\tVersionStartIncluding: strRef(\"1.22\"),\n\t\t\t\t\t\t\tMatchCriteriaID:       \"5EBE5E1C-C881-4A76-9E36-4FB7C48427E6\",\n\t\t\t\t\t\t\tVulnerable:            true,\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tCriteria:            \"cpe:2.3:a:golang:go:*:*:*:*:*:*:*:*\",\n\t\t\t\t\t\t\tVersionEndExcluding: strRef(\"1.21.8\"),\n\t\t\t\t\t\t\tMatchCriteriaID:     \"5EBE5E1C-C881-4A76-9E36-4FB7C48427E6\",\n\t\t\t\t\t\t\tVulnerable:          true,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tNegate:   boolRef(false),\n\t\t\t\t\tOperator: nvd.Or,\n\t\t\t\t\tCpeMatch: []nvd.CpeMatch{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tCriteria:        \"cpe:2.3:o:microsoft:windows:-:*:*:*:*:*:*:*\",\n\t\t\t\t\t\t\tMatchCriteriaID: \"902B8056-9E37-443B-8905-8AA93E2447FB\",\n\t\t\t\t\t\t\tVulnerable:      false,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: newUniquePkgTrackerFromSlice([]pkgCandidate{\n\t\t\t\t{\n\t\t\t\t\tProduct:        \"go\",\n\t\t\t\t\tVendor:         \"golang\",\n\t\t\t\t\tTargetSoftware: ANY,\n\t\t\t\t\tPlatformCPE:    \"cpe:2.3:o:microsoft:windows:-:*:*:*:*:*:*:*\",\n\t\t\t\t},\n\t\t\t}),\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tif test.config == (Config{}) {\n\t\t\t\ttest.config = defaultConfig()\n\t\t\t}\n\t\t\tactual := findUniquePkgs(test.config, nvd.Configuration{Nodes: test.nodes, Operator: test.operator})\n\t\t\tmissing, extra := test.expected.Diff(actual)\n\t\t\tif len(missing) != 0 {\n\t\t\t\tfor _, c := range missing {\n\t\t\t\t\tt.Errorf(\"missing candidate: %+v\", c)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif len(extra) != 0 {\n\t\t\t\tfor _, c := range extra {\n\t\t\t\t\tt.Errorf(\"extra candidate: %+v\", c)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc strRef(s string) *string {\n\treturn &s\n}\n\nfunc TestBuildConstraints(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tmatches  []nvd.CpeMatch\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname: \"Equals\",\n\t\t\tmatches: []nvd.CpeMatch{\n\t\t\t\t{\n\t\t\t\t\tCriteria: \"cpe:2.3:a:vendor:product:2.2.0:*:*:*:*:target:*:*\",\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: \"= 2.2.0\",\n\t\t},\n\t\t{\n\t\t\tname: \"VersionEndExcluding\",\n\t\t\tmatches: []nvd.CpeMatch{\n\t\t\t\t{\n\t\t\t\t\tCriteria:            \"cpe:2.3:a:vendor:product:*:*:*:*:*:target:*:*\",\n\t\t\t\t\tVersionEndExcluding: strRef(\"2.3.0\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: \"< 2.3.0\",\n\t\t},\n\t\t{\n\t\t\tname: \"VersionEndIncluding\",\n\t\t\tmatches: []nvd.CpeMatch{\n\t\t\t\t{\n\t\t\t\t\tCriteria:            \"cpe:2.3:a:vendor:product:*:*:*:*:*:target:*:*\",\n\t\t\t\t\tVersionEndIncluding: strRef(\"2.3.0\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: \"<= 2.3.0\",\n\t\t},\n\t\t{\n\t\t\tname: \"VersionStartExcluding\",\n\t\t\tmatches: []nvd.CpeMatch{\n\t\t\t\t{\n\t\t\t\t\tCriteria:              \"cpe:2.3:a:vendor:product:*:*:*:*:*:target:*:*\",\n\t\t\t\t\tVersionStartExcluding: strRef(\"2.3.0\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: \"> 2.3.0\",\n\t\t},\n\t\t{\n\t\t\tname: \"VersionStartIncluding\",\n\t\t\tmatches: []nvd.CpeMatch{\n\t\t\t\t{\n\t\t\t\t\tCriteria:              \"cpe:2.3:a:vendor:product:*:*:*:*:*:target:*:*\",\n\t\t\t\t\tVersionStartIncluding: strRef(\"2.3.0\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: \">= 2.3.0\",\n\t\t},\n\t\t{\n\t\t\tname: \"Version Range\",\n\t\t\tmatches: []nvd.CpeMatch{\n\t\t\t\t{\n\t\t\t\t\tCriteria:              \"cpe:2.3:a:vendor:product:*:*:*:*:*:target:*:*\",\n\t\t\t\t\tVersionStartIncluding: strRef(\"2.3.0\"),\n\t\t\t\t\tVersionEndIncluding:   strRef(\"2.5.0\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: \">= 2.3.0, <= 2.5.0\",\n\t\t},\n\t\t{\n\t\t\tname: \"Multiple Version Ranges\",\n\t\t\tmatches: []nvd.CpeMatch{\n\t\t\t\t{\n\t\t\t\t\tCriteria:              \"cpe:2.3:a:vendor:product:*:*:*:*:*:target:*:*\",\n\t\t\t\t\tVersionStartIncluding: strRef(\"2.3.0\"),\n\t\t\t\t\tVersionEndIncluding:   strRef(\"2.5.0\"),\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tCriteria:              \"cpe:2.3:a:vendor:product:*:*:*:*:*:target:*:*\",\n\t\t\t\t\tVersionStartExcluding: strRef(\"3.3.0\"),\n\t\t\t\t\tVersionEndExcluding:   strRef(\"3.5.0\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: \">= 2.3.0, <= 2.5.0 || > 3.3.0, < 3.5.0\",\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tactual := buildConstraints(test.matches)\n\n\t\t\tif actual != test.expected {\n\t\t\t\tdmp := diffmatchpatch.New()\n\t\t\t\tdiffs := dmp.DiffMain(actual, test.expected, true)\n\t\t\t\tt.Errorf(\"Expected: %q\", test.expected)\n\t\t\t\tt.Errorf(\"Got     : %q\", actual)\n\t\t\t\tt.Errorf(\"Diff    : %q\", dmp.DiffPrettyText(diffs))\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_UniquePackageTrackerHandlesOnlyPlatformDiff(t *testing.T) {\n\tcandidates := []pkgCandidate{\n\t\t{\n\t\t\tProduct:        \"redis\",\n\t\t\tVendor:         \"redis\",\n\t\t\tTargetSoftware: ANY,\n\t\t\tPlatformCPE:    \"cpe:2.3:o:canonical:ubuntu_linux:20.04:*:*:*:lts:*:*:*\",\n\t\t},\n\t\t{\n\t\t\tProduct:        \"redis\",\n\t\t\tVendor:         \"redis\",\n\t\t\tTargetSoftware: ANY,\n\t\t\tPlatformCPE:    \"cpe:2.3:o:canonical:ubuntu_linux:21.10:*:*:*:-:*:*:*\",\n\t\t},\n\t\t{\n\t\t\tProduct:        \"redis\",\n\t\t\tVendor:         \"redis\",\n\t\t\tTargetSoftware: ANY,\n\t\t\tPlatformCPE:    \"cpe:2.3:o:debian:debian_linux:9.0:*:*:*:*:*:*:*\",\n\t\t},\n\t\t{\n\t\t\tProduct:        \"redis\",\n\t\t\tVendor:         \"redis\",\n\t\t\tTargetSoftware: ANY,\n\t\t\tPlatformCPE:    \"cpe:2.3:o:debian:debian_linux:10.0:*:*:*:*:*:*:*\",\n\t\t},\n\t\t{\n\t\t\tProduct:        \"redis\",\n\t\t\tVendor:         \"redis\",\n\t\t\tTargetSoftware: ANY,\n\t\t\tPlatformCPE:    \"cpe:2.3:o:debian:debian_linux:11.0:*:*:*:*:*:*:*\",\n\t\t},\n\t}\n\tcpeMatch := nvd.CpeMatch{\n\t\tCriteria:        \"cpe:2.3:a:redis:redis:-:*:*:*:*:*:*:*\",\n\t\tMatchCriteriaID: \"5EBE5E1C-C881-4A76-9E36-4FB7C48427E6\",\n\t}\n\tapplicationNode := nvd.CpeMatch{\n\t\tCriteria:        \"cpe:2.3:a:redis:redis:-:*:*:*:*:*:*:*\",\n\t\tMatchCriteriaID: \"some-uuid\",\n\t\tVulnerable:      true,\n\t}\n\ttracker := newUniquePkgTracker()\n\tfor _, c := range candidates {\n\t\tcandidate, err := newPkgCandidate(defaultConfig(), applicationNode, c.PlatformCPE)\n\t\trequire.NoError(t, err)\n\t\ttracker.Add(*candidate, cpeMatch)\n\t}\n\tassert.Len(t, tracker, len(candidates))\n}\n\nfunc TestPlatformPackageCandidates(t *testing.T) {\n\ttype testCase struct {\n\t\tname        string\n\t\tconfig      Config\n\t\tstate       nvd.Configuration\n\t\twantChanged bool\n\t\twantSet     uniquePkgTracker\n\t}\n\ttests := []testCase{\n\t\t{\n\t\t\tname: \"application X platform\",\n\t\t\tstate: nvd.Configuration{\n\t\t\t\tNegate: nil,\n\t\t\t\tNodes: []nvd.Node{\n\t\t\t\t\t{\n\t\t\t\t\t\tCpeMatch: []nvd.CpeMatch{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tVulnerable: true,\n\t\t\t\t\t\t\t\tCriteria:   \"cpe:2.3:a:some-vendor:some-app:*:*:*:*:*:*:*:*\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tVulnerable: true,\n\t\t\t\t\t\t\t\tCriteria:   \"cpe:2.3:a:some-vendor:other-app:*:*:*:*:*:*:*:*\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tNegate:   nil,\n\t\t\t\t\t\tOperator: nvd.Or,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tCpeMatch: []nvd.CpeMatch{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tVulnerable: false,\n\t\t\t\t\t\t\t\tCriteria:   \"cpe:2.3:o:some-vendor:some-platform:*:*:*:*:*:*:*:*\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tVulnerable: false,\n\t\t\t\t\t\t\t\tCriteria:   \"cpe:2.3:o:some-vendor:other-platform:*:*:*:*:*:*:*:*\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tNegate:   nil,\n\t\t\t\t\t\tOperator: nvd.Or,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tOperator: opRef(nvd.And),\n\t\t\t},\n\t\t\twantChanged: true,\n\t\t\twantSet: newUniquePkgTrackerFromSlice(\n\t\t\t\t[]pkgCandidate{\n\t\t\t\t\tmustNewPackage(t, nvd.CpeMatch{\n\t\t\t\t\t\tVulnerable: true,\n\t\t\t\t\t\tCriteria:   \"cpe:2.3:a:some-vendor:some-app:*:*:*:*:*:*:*:*\",\n\t\t\t\t\t}, \"cpe:2.3:o:some-vendor:some-platform:*:*:*:*:*:*:*:*\"),\n\t\t\t\t\tmustNewPackage(t, nvd.CpeMatch{\n\t\t\t\t\t\tVulnerable: true,\n\t\t\t\t\t\tCriteria:   \"cpe:2.3:a:some-vendor:other-app:*:*:*:*:*:*:*:*\",\n\t\t\t\t\t}, \"cpe:2.3:o:some-vendor:some-platform:*:*:*:*:*:*:*:*\"),\n\t\t\t\t\tmustNewPackage(t, nvd.CpeMatch{\n\t\t\t\t\t\tVulnerable: true,\n\t\t\t\t\t\tCriteria:   \"cpe:2.3:a:some-vendor:some-app:*:*:*:*:*:*:*:*\",\n\t\t\t\t\t}, \"cpe:2.3:o:some-vendor:other-platform:*:*:*:*:*:*:*:*\"),\n\t\t\t\t\tmustNewPackage(t, nvd.CpeMatch{\n\t\t\t\t\t\tVulnerable: true,\n\t\t\t\t\t\tCriteria:   \"cpe:2.3:a:some-vendor:other-app:*:*:*:*:*:*:*:*\",\n\t\t\t\t\t}, \"cpe:2.3:o:some-vendor:other-platform:*:*:*:*:*:*:*:*\"),\n\t\t\t\t},\n\t\t\t),\n\t\t},\n\t\t{\n\t\t\tname: \"top-level OR is excluded\",\n\t\t\tstate: nvd.Configuration{\n\t\t\t\tOperator: opRef(nvd.Or),\n\t\t\t},\n\t\t\twantChanged: false,\n\t\t\twantSet:     newUniquePkgTracker(),\n\t\t},\n\t\t{\n\t\t\tname: \"top-level nil op is excluded\",\n\t\t\tstate: nvd.Configuration{\n\t\t\t\tOperator: nil,\n\t\t\t},\n\t\t\twantChanged: false,\n\t\t},\n\t\t{\n\t\t\tname: \"single hardware node results in exclusion\",\n\t\t\tstate: nvd.Configuration{\n\t\t\t\tNegate: nil,\n\t\t\t\tNodes: []nvd.Node{\n\t\t\t\t\t{\n\t\t\t\t\t\tCpeMatch: []nvd.CpeMatch{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tVulnerable: true,\n\t\t\t\t\t\t\t\tCriteria:   \"cpe:2.3:a:some-vendor:some-app:*:*:*:*:*:*:*:*\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tVulnerable: true,\n\t\t\t\t\t\t\t\tCriteria:   \"cpe:2.3:a:some-vendor:other-app:*:*:*:*:*:*:*:*\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tNegate:   nil,\n\t\t\t\t\t\tOperator: nvd.Or,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tCpeMatch: []nvd.CpeMatch{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tVulnerable: false,\n\t\t\t\t\t\t\t\tCriteria:   \"cpe:2.3:o:some-vendor:some-platform:*:*:*:*:*:*:*:*\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tVulnerable: false,\n\t\t\t\t\t\t\t\tCriteria:   \"cpe:2.3:h:some-vendor:some-device:*:*:*:*:*:*:*:*\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tNegate:   nil,\n\t\t\t\t\t\tOperator: nvd.Or,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tOperator: opRef(nvd.And),\n\t\t\t},\n\t\t\twantChanged: false,\n\t\t},\n\t}\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tif tc.config == (Config{}) {\n\t\t\t\ttc.config = defaultConfig()\n\t\t\t}\n\t\t\tset := newUniquePkgTracker()\n\t\t\tresult := platformPackageCandidates(tc.config, set, tc.state)\n\t\t\tassert.Equal(t, result, tc.wantChanged)\n\t\t\tif tc.wantSet == nil {\n\t\t\t\ttc.wantSet = newUniquePkgTracker()\n\t\t\t}\n\t\t\tif diff := cmp.Diff(tc.wantSet.All(), set.All()); diff != \"\" {\n\t\t\t\tt.Errorf(\"unexpected diff (-want +got)\\n%s\", diff)\n\t\t\t}\n\t\t})\n\n\t}\n}\n\nfunc opRef(op nvd.Operator) *nvd.Operator {\n\treturn &op\n}\n\nfunc boolRef(b bool) *bool {\n\treturn &b\n}\n\nfunc mustNewPackage(t *testing.T, match nvd.CpeMatch, platformCPE string, cfg ...Config) pkgCandidate {\n\tvar tCfg *Config\n\tswitch len(cfg) {\n\tcase 0:\n\t\tc := defaultConfig()\n\t\ttCfg = &c\n\tcase 1:\n\t\ttCfg = &cfg[0]\n\tdefault:\n\t\tt.Fatalf(\"too many configs provided\")\n\t}\n\tp, err := newPkgCandidate(*tCfg, match, platformCPE)\n\trequire.NoError(t, err)\n\treturn *p\n}\n"
  },
  {
    "path": "grype/db/v5/build/transformers/nvd/unique_pkg_tracker.go",
    "content": "package nvd\n\nimport (\n\t\"sort\"\n\n\t\"github.com/anchore/grype/grype/db/internal/provider/unmarshal/nvd\"\n)\n\ntype uniquePkgTracker map[pkgCandidate][]nvd.CpeMatch\n\nfunc newUniquePkgTracker() uniquePkgTracker {\n\treturn make(uniquePkgTracker)\n}\n\nfunc (s uniquePkgTracker) Diff(other uniquePkgTracker) (missing []pkgCandidate, extra []pkgCandidate) {\n\tfor k := range s {\n\t\tif !other.Contains(k) {\n\t\t\tmissing = append(missing, k)\n\t\t}\n\t}\n\n\tfor k := range other {\n\t\tif !s.Contains(k) {\n\t\t\textra = append(extra, k)\n\t\t}\n\t}\n\n\treturn\n}\n\nfunc (s uniquePkgTracker) Matches(i pkgCandidate) []nvd.CpeMatch {\n\treturn s[i]\n}\n\nfunc (s uniquePkgTracker) Add(i pkgCandidate, match nvd.CpeMatch) {\n\tif _, ok := s[i]; !ok {\n\t\ts[i] = make([]nvd.CpeMatch, 0)\n\t}\n\ts[i] = append(s[i], match)\n}\n\nfunc (s uniquePkgTracker) Remove(i pkgCandidate) {\n\tdelete(s, i)\n}\n\nfunc (s uniquePkgTracker) Contains(i pkgCandidate) bool {\n\t_, ok := s[i]\n\treturn ok\n}\n\nfunc (s uniquePkgTracker) All() []pkgCandidate {\n\tres := make([]pkgCandidate, len(s))\n\tidx := 0\n\tfor k := range s {\n\t\tres[idx] = k\n\t\tidx++\n\t}\n\n\tsort.SliceStable(res, func(i, j int) bool {\n\t\treturn res[i].String() < res[j].String()\n\t})\n\n\treturn res\n}\n"
  },
  {
    "path": "grype/db/v5/build/transformers/os/testdata/alpine-3.9.json",
    "content": "[\n  {\n    \"Vulnerability\": {\n      \"CVSS\": [],\n      \"Description\": \"\",\n      \"FixedIn\": [\n        {\n          \"Name\": \"xen\",\n          \"NamespaceName\": \"alpine:3.9\",\n          \"Version\": \"4.11.1-r0\",\n          \"VersionFormat\": \"apk\"\n        }\n      ],\n      \"Link\": \"http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-19967\",\n      \"Metadata\": {\n        \"NVD\": {\n          \"CVSSv2\": {\n            \"Score\": 4.9,\n            \"Vectors\": \"AV:L/AC:L/Au:N/C:N/I:N/A:C\"\n          }\n        }\n      },\n      \"Name\": \"CVE-2018-19967\",\n      \"NamespaceName\": \"alpine:3.9\",\n      \"Severity\": \"Medium\"\n    }\n  }\n]"
  },
  {
    "path": "grype/db/v5/build/transformers/os/testdata/amazon-multiple-kernel-advisories.json",
    "content": "[\n  {\n    \"Vulnerability\": {\n      \"Name\": \"ALAS-2021-1704\",\n      \"NamespaceName\": \"amzn:2\",\n      \"Description\": \"\",\n      \"Severity\": \"Medium\",\n      \"Metadata\": {\n        \"CVE\": [\n          {\n            \"Name\": \"CVE-2021-3653\"\n          },\n          {\n            \"Name\": \"CVE-2021-3656\"\n          },\n          {\n            \"Name\": \"CVE-2021-3732\"\n          }\n        ]\n      },\n      \"Link\": \"https://alas.aws.amazon.com/AL2/ALAS-2021-1704.html\",\n      \"FixedIn\": [\n        {\n          \"Name\": \"kernel-headers\",\n          \"NamespaceName\": \"amzn:2\",\n          \"VersionFormat\": \"rpm\",\n          \"Version\": \"4.14.246-187.474.amzn2\"\n        },\n        {\n          \"Name\": \"kernel\",\n          \"NamespaceName\": \"amzn:2\",\n          \"VersionFormat\": \"rpm\",\n          \"Version\": \"4.14.246-187.474.amzn2\"\n        }\n      ]\n    }\n  },\n  {\n    \"Vulnerability\": {\n      \"Name\": \"ALASKERNEL-5.4-2022-007\",\n      \"NamespaceName\": \"amzn:2\",\n      \"Description\": \"\",\n      \"Severity\": \"Medium\",\n      \"Metadata\": {\n        \"CVE\": [\n          {\n            \"Name\": \"CVE-2021-3753\"\n          },\n          {\n            \"Name\": \"CVE-2021-40490\"\n          }\n        ]\n      },\n      \"Link\": \"https://alas.aws.amazon.com/AL2/ALASKERNEL-5.4-2022-007.html\",\n      \"FixedIn\": [\n        {\n          \"Name\": \"kernel-headers\",\n          \"NamespaceName\": \"amzn:2\",\n          \"VersionFormat\": \"rpm\",\n          \"Version\": \"5.4.144-69.257.amzn2\"\n        },\n        {\n          \"Name\": \"kernel\",\n          \"NamespaceName\": \"amzn:2\",\n          \"VersionFormat\": \"rpm\",\n          \"Version\": \"5.4.144-69.257.amzn2\"\n        }\n      ]\n    }\n  },\n  {\n    \"Vulnerability\": {\n      \"Name\": \"ALASKERNEL-5.10-2022-005\",\n      \"NamespaceName\": \"amzn:2\",\n      \"Description\": \"\",\n      \"Severity\": \"Medium\",\n      \"Metadata\": {\n        \"CVE\": [\n          {\n            \"Name\": \"CVE-2021-3753\"\n          },\n          {\n            \"Name\": \"CVE-2021-40490\"\n          }\n        ]\n      },\n      \"Link\": \"https://alas.aws.amazon.com/AL2/ALASKERNEL-5.10-2022-005.html\",\n      \"FixedIn\": [\n        {\n          \"Name\": \"kernel-headers\",\n          \"NamespaceName\": \"amzn:2\",\n          \"VersionFormat\": \"rpm\",\n          \"Version\": \"5.10.62-55.141.amzn2\"\n        },\n        {\n          \"Name\": \"kernel\",\n          \"NamespaceName\": \"amzn:2\",\n          \"VersionFormat\": \"rpm\",\n          \"Version\": \"5.10.62-55.141.amzn2\"\n        }\n      ]\n    }\n  }\n]"
  },
  {
    "path": "grype/db/v5/build/transformers/os/testdata/amzn.json",
    "content": "[\n  {\n    \"Vulnerability\": {\n      \"Description\": \"\",\n      \"FixedIn\": [\n        {\n          \"Name\": \"389-ds-base\",\n          \"NamespaceName\": \"amzn:2\",\n          \"Version\": \"1.3.8.4-15.amzn2.0.1\",\n          \"VersionFormat\": \"rpm\"\n        },\n        {\n          \"Name\": \"389-ds-base-debuginfo\",\n          \"NamespaceName\": \"amzn:2\",\n          \"Version\": \"1.3.8.4-15.amzn2.0.1\",\n          \"VersionFormat\": \"rpm\"\n        },\n        {\n          \"Name\": \"389-ds-base-devel\",\n          \"NamespaceName\": \"amzn:2\",\n          \"Version\": \"1.3.8.4-15.amzn2.0.1\",\n          \"VersionFormat\": \"rpm\"\n        },\n        {\n          \"Name\": \"389-ds-base-libs\",\n          \"NamespaceName\": \"amzn:2\",\n          \"Version\": \"1.3.8.4-15.amzn2.0.1\",\n          \"VersionFormat\": \"rpm\"\n        },\n        {\n          \"Name\": \"389-ds-base-snmp\",\n          \"NamespaceName\": \"amzn:2\",\n          \"Version\": \"1.3.8.4-15.amzn2.0.1\",\n          \"VersionFormat\": \"rpm\"\n        }\n      ],\n      \"Link\": \"https://alas.aws.amazon.com/AL2/ALAS-2018-1106.html\",\n      \"Metadata\": {\n        \"CVE\": [\n          {\n            \"Name\": \"CVE-2018-14648\"\n          }\n        ]\n      },\n      \"Name\": \"ALAS-2018-1106\",\n      \"NamespaceName\": \"amzn:2\",\n      \"Severity\": \"Medium\"\n    }\n  }\n]"
  },
  {
    "path": "grype/db/v5/build/transformers/os/testdata/azure-linux-3.json",
    "content": "[\n  {\n    \"Vulnerability\": {\n      \"Name\": \"CVE-2023-29403\",\n      \"NamespaceName\": \"mariner:3.0\",\n      \"Description\": \"CVE-2023-29403 affecting package golang for versions less than 1.20.7-1. A patched version of the package is available.\",\n      \"Severity\": \"High\",\n      \"Link\": \"https://nvd.nist.gov/vuln/detail/CVE-2023-29403\",\n      \"CVSS\": [],\n      \"FixedIn\": [\n        {\n          \"Name\": \"golang\",\n          \"NamespaceName\": \"mariner:3.0\",\n          \"VersionFormat\": \"rpm\",\n          \"Version\": \"0:1.20.7-1.azl3\",\n          \"Module\": \"\",\n          \"VendorAdvisory\": {\n            \"NoAdvisory\": false,\n            \"AdvisorySummary\": []\n          }\n        }\n      ],\n      \"Metadata\": {}\n    }\n  }\n]\n"
  },
  {
    "path": "grype/db/v5/build/transformers/os/testdata/debian-8-multiple-entries-for-same-package.json",
    "content": "[\n  {\n    \"Vulnerability\": {\n      \"CVSS\": [],\n      \"Description\": \"\",\n      \"FixedIn\": [\n        {\n          \"Name\": \"rsyslog\",\n          \"NamespaceName\": \"debian:8\",\n          \"VendorAdvisory\": {\n            \"AdvisorySummary\": [],\n            \"NoAdvisory\": false\n          },\n          \"Version\": \"5.7.4-1\",\n          \"VersionFormat\": \"dpkg\"\n        }\n      ],\n      \"Link\": \"https://security-tracker.debian.org/tracker/CVE-2011-4623\",\n      \"Metadata\": {\n        \"NVD\": {\n          \"CVSSv2\": {\n            \"Score\": 2.1,\n            \"Vectors\": \"AV:L/AC:L/Au:N/C:N/I:N/A:P\"\n          }\n        }\n      },\n      \"Name\": \"CVE-2011-4623\",\n      \"NamespaceName\": \"debian:8\",\n      \"Severity\": \"Low\"\n    }\n  },\n  {\n    \"Vulnerability\": {\n      \"CVSS\": [],\n      \"Description\": \"\",\n      \"FixedIn\": [\n        {\n          \"Name\": \"rsyslog\",\n          \"NamespaceName\": \"debian:8\",\n          \"VendorAdvisory\": {\n            \"AdvisorySummary\": [],\n            \"NoAdvisory\": false\n          },\n          \"Version\": \"3.18.6-1\",\n          \"VersionFormat\": \"dpkg\"\n        }\n      ],\n      \"Link\": \"https://security-tracker.debian.org/tracker/CVE-2008-5618\",\n      \"Metadata\": {\n        \"NVD\": {\n          \"CVSSv2\": {\n            \"Score\": 5,\n            \"Vectors\": \"AV:N/AC:L/Au:N/C:N/I:N/A:P\"\n          }\n        }\n      },\n      \"Name\": \"CVE-2008-5618\",\n      \"NamespaceName\": \"debian:8\",\n      \"Severity\": \"Low\"\n    }\n  }\n]"
  },
  {
    "path": "grype/db/v5/build/transformers/os/testdata/debian-8.json",
    "content": "[\n  {\n    \"Vulnerability\": {\n      \"CVSS\": [],\n      \"Description\": \"\",\n      \"FixedIn\": [\n        {\n          \"Name\": \"asterisk\",\n          \"NamespaceName\": \"debian:8\",\n          \"VendorAdvisory\": {\n            \"AdvisorySummary\": [],\n            \"NoAdvisory\": false\n          },\n          \"Version\": \"1:1.6.2.0~rc3-1\",\n          \"VersionFormat\": \"dpkg\"\n        },\n        {\n          \"Name\": \"auth2db\",\n          \"NamespaceName\": \"debian:8\",\n          \"VendorAdvisory\": {\n            \"AdvisorySummary\": [],\n            \"NoAdvisory\": false\n          },\n          \"Version\": \"0.2.5-2+dfsg-1\",\n          \"VersionFormat\": \"dpkg\"\n        },\n        {\n          \"Name\": \"exaile\",\n          \"NamespaceName\": \"debian:8\",\n          \"VendorAdvisory\": {\n            \"AdvisorySummary\": [],\n            \"NoAdvisory\": false\n          },\n          \"Version\": \"0.2.14+debian-2.2\",\n          \"VersionFormat\": \"dpkg\"\n        },\n        {\n          \"Name\": \"wordpress\",\n          \"NamespaceName\": \"debian:8\",\n          \"VendorAdvisory\": {\n            \"AdvisorySummary\": [],\n            \"NoAdvisory\": false\n          },\n          \"Version\": \"\",\n          \"VersionFormat\": \"dpkg\"\n        }\n      ],\n      \"Link\": \"https://security-tracker.debian.org/tracker/CVE-2008-7220\",\n      \"Metadata\": {\n        \"NVD\": {\n          \"CVSSv2\": {\n            \"Score\": 7.5,\n            \"Vectors\": \"AV:N/AC:L/Au:N/C:P/I:P/A:P\"\n          }\n        }\n      },\n      \"Name\": \"CVE-2008-7220\",\n      \"NamespaceName\": \"debian:8\",\n      \"Severity\": \"High\"\n    }\n  }\n]"
  },
  {
    "path": "grype/db/v5/build/transformers/os/testdata/mariner-20.json",
    "content": "[\n  {\n    \"Vulnerability\": {\n      \"Name\": \"CVE-2021-37621\",\n      \"NamespaceName\": \"mariner:2.0\",\n      \"Description\": \"CVE-2021-37621 affecting package exiv2 for versions less than 0.27.5-1. An upgraded version of the package is available that resolves this issue.\",\n      \"Severity\": \"Medium\",\n      \"Link\": \"https://nvd.nist.gov/vuln/detail/CVE-2021-37621\",\n      \"CVSS\": [],\n      \"FixedIn\": [\n        {\n          \"Name\": \"exiv2\",\n          \"NamespaceName\": \"mariner:2.0\",\n          \"VersionFormat\": \"rpm\",\n          \"Version\": \"0:0.27.5-1.cm2\",\n          \"Module\": \"\",\n          \"VendorAdvisory\": {\n            \"NoAdvisory\": false,\n            \"AdvisorySummary\": []\n          }\n        }\n      ],\n      \"Metadata\": {}\n    }\n  }\n]\n"
  },
  {
    "path": "grype/db/v5/build/transformers/os/testdata/mariner-range.json",
    "content": "[\n  {\n      \"Vulnerability\": {\n        \"Name\": \"CVE-2023-29404\",\n        \"NamespaceName\": \"mariner:2.0\",\n        \"Description\": \"CVE-2023-29404 affecting package golang for versions less than 1.20.7-1. A patched version of the package is available.\",\n        \"Severity\": \"Critical\",\n        \"Link\": \"https://nvd.nist.gov/vuln/detail/CVE-2023-29404\",\n        \"CVSS\": [],\n        \"FixedIn\": [\n          {\n            \"Name\": \"golang\",\n            \"NamespaceName\": \"mariner:2.0\",\n            \"VersionFormat\": \"rpm\",\n            \"Version\": \"0:1.20.7-1.cm2\",\n            \"Module\": \"\",\n            \"VendorAdvisory\": {\n              \"NoAdvisory\": false,\n              \"AdvisorySummary\": []\n            },\n            \"VulnerableRange\": \"> 0:1.19.0.cm2, < 0:1.20.7-1.cm2\"\n          }\n        ],\n        \"Metadata\": {}\n      }\n    }\n]\n"
  },
  {
    "path": "grype/db/v5/build/transformers/os/testdata/ol-8-modules.json",
    "content": "[\n  {\n    \"Vulnerability\": {\n      \"CVSS\": [],\n      \"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.\",\n      \"FixedIn\": [\n        {\n          \"Module\": \"postgresql:10\",\n          \"Name\": \"postgresql\",\n          \"NamespaceName\": \"ol:8\",\n          \"Version\": \"0:10.14-1.module+el8.2.0+7801+be0fed80\",\n          \"VersionFormat\": \"rpm\"\n        },\n        {\n          \"Module\": \"postgresql:12\",\n          \"Name\": \"postgresql\",\n          \"NamespaceName\": \"ol:8\",\n          \"Version\": \"0:12.5-1.module+el8.3.0+9042+664538f4\",\n          \"VersionFormat\": \"rpm\"\n        },\n        {\n          \"Module\": \"postgresql:9.6\",\n          \"Name\": \"postgresql\",\n          \"NamespaceName\": \"ol:8\",\n          \"Version\": \"0:9.6.20-1.module+el8.3.0+8938+7f0e88b6\",\n          \"VersionFormat\": \"rpm\"\n        }\n      ],\n      \"Link\": \"https://access.redhat.com/security/cve/CVE-2020-14350\",\n      \"Metadata\": {},\n      \"Name\": \"CVE-2020-14350\",\n      \"NamespaceName\": \"ol:8\",\n      \"Severity\": \"Medium\"\n    }\n  }\n]"
  },
  {
    "path": "grype/db/v5/build/transformers/os/testdata/ol-8.json",
    "content": "[\n  {\n    \"Vulnerability\": {\n      \"CVSS\": [],\n      \"Description\": \"\",\n      \"FixedIn\": [\n        {\n          \"Name\": \"libexif\",\n          \"NamespaceName\": \"ol:8\",\n          \"Version\": \"0:0.6.21-17.el8_2\",\n          \"VersionFormat\": \"rpm\"\n        },\n        {\n          \"Name\": \"libexif-devel\",\n          \"NamespaceName\": \"ol:8\",\n          \"Version\": \"0:0.6.21-17.el8_2\",\n          \"VersionFormat\": \"rpm\"\n        },\n        {\n          \"Name\": \"libexif-dummy\",\n          \"NamespaceName\": \"ol:8\",\n          \"Version\": \"None\",\n          \"VersionFormat\": \"rpm\"\n        }\n      ],\n      \"Link\": \"http://linux.oracle.com/errata/ELSA-2020-2550.html\",\n      \"Metadata\": {\n        \"CVE\": [\n          {\n            \"Link\": \"http://linux.oracle.com/cve/CVE-2020-13112.html\",\n            \"Name\": \"CVE-2020-13112\"\n          }\n        ],\n        \"Issued\": \"2020-06-15\",\n        \"RefId\": \"ELSA-2020-2550\"\n      },\n      \"Name\": \"ELSA-2020-2550\",\n      \"NamespaceName\": \"ol:8\",\n      \"Severity\": \"Medium\"\n    }\n  }\n]"
  },
  {
    "path": "grype/db/v5/build/transformers/os/testdata/photon-4.0.json",
    "content": "[\n  {\n    \"Vulnerability\": {\n      \"Description\": \"\",\n      \"FixedIn\": [\n        {\n          \"Name\": \"curl\",\n          \"NamespaceName\": \"photon:4.0\",\n          \"Version\": \"7.88.1-4.ph4\",\n          \"VersionFormat\": \"rpm\",\n          \"VulnerableRange\": \"< 7.88.1-4.ph4\"\n        }\n      ],\n      \"Link\": \"https://nvd.nist.gov/vuln/detail/CVE-2023-38545\",\n      \"Metadata\": {},\n      \"Name\": \"CVE-2023-38545\",\n      \"NamespaceName\": \"photon:4.0\",\n      \"Severity\": \"Critical\",\n      \"CVSS\": []\n    }\n  }\n]\n"
  },
  {
    "path": "grype/db/v5/build/transformers/os/testdata/rhel-8-eus.json",
    "content": "[\n  {\n    \"Vulnerability\": {\n      \"CVSS\": [\n        {\n          \"base_metrics\": {\n            \"base_score\": 8.8,\n            \"base_severity\": \"High\",\n            \"exploitability_score\": 2.8,\n            \"impact_score\": 5.9\n          },\n          \"status\": \"verified\",\n          \"vector_string\": \"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H\",\n          \"version\": \"3.1\"\n        }\n      ],\n      \"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.\",\n      \"FixedIn\": [\n        {\n          \"Name\": \"firefox\",\n          \"NamespaceName\": \"rhel:8+eus\",\n          \"VendorAdvisory\": {\n            \"AdvisorySummary\": [\n              {\n                \"ID\": \"RHSA-2020:1341\",\n                \"Link\": \"https://access.redhat.com/errata/RHSA-2020:1341\"\n              }\n            ],\n            \"NoAdvisory\": false\n          },\n          \"Version\": \"0:68.6.1-1.el8_1\",\n          \"VersionFormat\": \"rpm\"\n        },\n        {\n          \"Name\": \"thunderbird\",\n          \"NamespaceName\": \"rhel:8+eus\",\n          \"VendorAdvisory\": {\n            \"AdvisorySummary\": [\n              {\n                \"ID\": \"RHSA-2020:1495\",\n                \"Link\": \"https://access.redhat.com/errata/RHSA-2020:1495\"\n              }\n            ],\n            \"NoAdvisory\": false\n          },\n          \"Version\": \"0:68.7.0-1.el8_1\",\n          \"VersionFormat\": \"rpm\"\n        }\n      ],\n      \"Link\": \"https://access.redhat.com/security/cve/CVE-2020-6819\",\n      \"Metadata\": {},\n      \"Name\": \"CVE-2020-6819\",\n      \"NamespaceName\": \"rhel:8+eus\",\n      \"Severity\": \"Critical\"\n    }\n  }\n]"
  },
  {
    "path": "grype/db/v5/build/transformers/os/testdata/rhel-8-modules.json",
    "content": "[\n  {\n    \"Vulnerability\": {\n      \"CVSS\": [\n        {\n          \"base_metrics\": {\n            \"base_score\": 7.1,\n            \"base_severity\": \"High\",\n            \"exploitability_score\": 1.2,\n            \"impact_score\": 5.9\n          },\n          \"status\": \"verified\",\n          \"vector_string\": \"CVSS:3.1/AV:N/AC:H/PR:L/UI:R/S:U/C:H/I:H/A:H\",\n          \"version\": \"3.1\"\n        }\n      ],\n      \"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.\",\n      \"FixedIn\": [\n        {\n          \"Module\": \"postgresql:10\",\n          \"Name\": \"postgresql\",\n          \"NamespaceName\": \"rhel:8\",\n          \"VendorAdvisory\": {\n            \"AdvisorySummary\": [\n              {\n                \"ID\": \"RHSA-2020:3669\",\n                \"Link\": \"https://access.redhat.com/errata/RHSA-2020:3669\"\n              }\n            ],\n            \"NoAdvisory\": false\n          },\n          \"Version\": \"0:10.14-1.module+el8.2.0+7801+be0fed80\",\n          \"VersionFormat\": \"rpm\"\n        },\n        {\n          \"Module\": \"postgresql:12\",\n          \"Name\": \"postgresql\",\n          \"NamespaceName\": \"rhel:8\",\n          \"VendorAdvisory\": {\n            \"AdvisorySummary\": [\n              {\n                \"ID\": \"RHSA-2020:5620\",\n                \"Link\": \"https://access.redhat.com/errata/RHSA-2020:5620\"\n              }\n            ],\n            \"NoAdvisory\": false\n          },\n          \"Version\": \"0:12.5-1.module+el8.3.0+9042+664538f4\",\n          \"VersionFormat\": \"rpm\"\n        },\n        {\n          \"Module\": \"postgresql:9.6\",\n          \"Name\": \"postgresql\",\n          \"NamespaceName\": \"rhel:8\",\n          \"VendorAdvisory\": {\n            \"AdvisorySummary\": [\n              {\n                \"ID\": \"RHSA-2020:5619\",\n                \"Link\": \"https://access.redhat.com/errata/RHSA-2020:5619\"\n              }\n            ],\n            \"NoAdvisory\": false\n          },\n          \"Version\": \"0:9.6.20-1.module+el8.3.0+8938+7f0e88b6\",\n          \"VersionFormat\": \"rpm\"\n        }\n      ],\n      \"Link\": \"https://access.redhat.com/security/cve/CVE-2020-14350\",\n      \"Metadata\": {},\n      \"Name\": \"CVE-2020-14350\",\n      \"NamespaceName\": \"rhel:8\",\n      \"Severity\": \"Medium\"\n    }\n  }\n]"
  },
  {
    "path": "grype/db/v5/build/transformers/os/testdata/rhel-8.json",
    "content": "[\n  {\n    \"Vulnerability\": {\n      \"CVSS\": [\n        {\n          \"base_metrics\": {\n            \"base_score\": 8.8,\n            \"base_severity\": \"High\",\n            \"exploitability_score\": 2.8,\n            \"impact_score\": 5.9\n          },\n          \"status\": \"verified\",\n          \"vector_string\": \"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H\",\n          \"version\": \"3.1\"\n        }\n      ],\n      \"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.\",\n      \"FixedIn\": [\n        {\n          \"Name\": \"firefox\",\n          \"NamespaceName\": \"rhel:8\",\n          \"VendorAdvisory\": {\n            \"AdvisorySummary\": [\n              {\n                \"ID\": \"RHSA-2020:1341\",\n                \"Link\": \"https://access.redhat.com/errata/RHSA-2020:1341\"\n              }\n            ],\n            \"NoAdvisory\": false\n          },\n          \"Version\": \"0:68.6.1-1.el8_1\",\n          \"VersionFormat\": \"rpm\"\n        },\n        {\n          \"Name\": \"thunderbird\",\n          \"NamespaceName\": \"rhel:8\",\n          \"VendorAdvisory\": {\n            \"AdvisorySummary\": [\n              {\n                \"ID\": \"RHSA-2020:1495\",\n                \"Link\": \"https://access.redhat.com/errata/RHSA-2020:1495\"\n              }\n            ],\n            \"NoAdvisory\": false\n          },\n          \"Version\": \"0:68.7.0-1.el8_1\",\n          \"VersionFormat\": \"rpm\"\n        }\n      ],\n      \"Link\": \"https://access.redhat.com/security/cve/CVE-2020-6819\",\n      \"Metadata\": {},\n      \"Name\": \"CVE-2020-6819\",\n      \"NamespaceName\": \"rhel:8\",\n      \"Severity\": \"Critical\"\n    }\n  }\n]"
  },
  {
    "path": "grype/db/v5/build/transformers/os/testdata/unmarshal-test.json",
    "content": "[\n  {\n    \"Vulnerability\": {\n      \"Description\": \"\",\n      \"FixedIn\": [\n        {\n          \"Name\": \"389-ds-base\",\n          \"NamespaceName\": \"amzn:2\",\n          \"Version\": \"1.3.8.4-15.amzn2.0.1\",\n          \"VersionFormat\": \"rpm\"\n        },\n        {\n          \"Name\": \"389-ds-base-debuginfo\",\n          \"NamespaceName\": \"amzn:2\",\n          \"Version\": \"1.3.8.4-15.amzn2.0.1\",\n          \"VersionFormat\": \"rpm\"\n        },\n        {\n          \"Name\": \"389-ds-base-devel\",\n          \"NamespaceName\": \"amzn:2\",\n          \"Version\": \"1.3.8.4-15.amzn2.0.1\",\n          \"VersionFormat\": \"rpm\"\n        },\n        {\n          \"Name\": \"389-ds-base-libs\",\n          \"NamespaceName\": \"amzn:2\",\n          \"Version\": \"1.3.8.4-15.amzn2.0.1\",\n          \"VersionFormat\": \"rpm\"\n        },\n        {\n          \"Name\": \"389-ds-base-snmp\",\n          \"NamespaceName\": \"amzn:2\",\n          \"Version\": \"1.3.8.4-15.amzn2.0.1\",\n          \"VersionFormat\": \"rpm\"\n        }\n      ],\n      \"Link\": \"https://alas.aws.amazon.com/AL2/ALAS-2018-1106.html\",\n      \"Metadata\": {\n        \"CVE\": [\n          {\"Name\": \"CVE-2018-14648\"}\n        ]\n      },\n      \"Name\": \"ALAS-2018-1106\",\n      \"NamespaceName\": \"amzn:2\",\n      \"Severity\": \"Medium\"\n    }\n  },\n  {\n    \"Vulnerability\": {\n      \"Description\": \"\",\n      \"FixedIn\": [\n        {\n          \"Name\": \"kernel-livepatch-4.14.173-137.228\",\n          \"NamespaceName\": \"amzn:2\",\n          \"Version\": \"1.0-3.amzn2\",\n          \"VersionFormat\": \"rpm\"\n        },\n        {\n          \"Name\": \"kernel-livepatch-4.14.173-137.228-debuginfo\",\n          \"NamespaceName\": \"amzn:2\",\n          \"Version\": \"1.0-3.amzn2\",\n          \"VersionFormat\": \"rpm\"\n        }\n      ],\n      \"Link\": \"https://alas.aws.amazon.com/AL2/ALASLIVEPATCH-2020-012.html\",\n      \"Metadata\": {\n        \"CVE\": [\n          {\"Name\": \"CVE-2020-12657\"}\n        ]\n      },\n      \"Name\": \"ALASLIVEPATCH-2020-012\",\n      \"NamespaceName\": \"amzn:2\",\n      \"Severity\": \"High\"\n    }\n  },\n  {\n    \"Vulnerability\": {\n      \"Description\": \"\",\n      \"FixedIn\": [\n        {\n          \"Name\": \"kernel-livepatch-4.14.171-136.231\",\n          \"NamespaceName\": \"amzn:2\",\n          \"Version\": \"1.0-5.amzn2\",\n          \"VersionFormat\": \"rpm\"\n        },\n        {\n          \"Name\": \"kernel-livepatch-4.14.171-136.231-debuginfo\",\n          \"NamespaceName\": \"amzn:2\",\n          \"Version\": \"1.0-5.amzn2\",\n          \"VersionFormat\": \"rpm\"\n        }\n      ],\n      \"Link\": \"https://alas.aws.amazon.com/AL2/ALASLIVEPATCH-2020-011.html\",\n      \"Metadata\": {\n        \"CVE\": [\n          {\"Name\": \"CVE-2020-12657\"}\n        ]\n      },\n      \"Name\": \"ALASLIVEPATCH-2020-011\",\n      \"NamespaceName\": \"amzn:2\",\n      \"Severity\": \"High\"\n    }\n  }\n]"
  },
  {
    "path": "grype/db/v5/build/transformers/os/transform.go",
    "content": "package os // nolint:revive\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/anchore/grype/grype/db/data\"\n\t\"github.com/anchore/grype/grype/db/internal/provider/unmarshal\"\n\t\"github.com/anchore/grype/grype/db/internal/versionutil\"\n\tdb \"github.com/anchore/grype/grype/db/v5\"\n\t\"github.com/anchore/grype/grype/db/v5/build/transformers\"\n\t\"github.com/anchore/grype/grype/db/v5/namespace\"\n\t\"github.com/anchore/grype/grype/db/v5/pkg/qualifier\"\n\t\"github.com/anchore/grype/grype/db/v5/pkg/qualifier/rpmmodularity\"\n\t\"github.com/anchore/grype/grype/distro\"\n)\n\nfunc buildGrypeNamespace(group string) (namespace.Namespace, error) {\n\tfeedGroupComponents := strings.Split(group, \":\")\n\n\tif len(feedGroupComponents) < 2 {\n\t\treturn nil, fmt.Errorf(\"unable to determine grype namespace for enterprise namespace=%s\", group)\n\t}\n\n\t// Currently known enterprise feed groups are expected to be of the form {distroID}:{version}\n\tfeedGroupDistroID := feedGroupComponents[0]\n\n\t// secureos and photon are not supported in the grype v5 schema, so the records should be dropped entirely\n\tif feedGroupDistroID == \"secureos\" || feedGroupDistroID == \"photon\" {\n\t\treturn nil, nil\n\t}\n\n\td, ok := distro.IDMapping[feedGroupDistroID]\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"unable to determine grype namespace for enterprise namespace=%s\", group)\n\t}\n\n\tproviderName := d.String()\n\tdistroName := d.String()\n\tver := feedGroupComponents[1]\n\n\tswitch d {\n\tcase distro.OracleLinux:\n\t\tproviderName = \"oracle\"\n\tcase distro.AmazonLinux:\n\t\tproviderName = \"amazon\"\n\tcase distro.Mariner, distro.Azure:\n\t\tproviderName = \"mariner\"\n\t\tif strings.HasPrefix(ver, \"3\") {\n\t\t\tdistroName = distro.Azure.String() // Mariner Linux 3 is known as \"Azure Linux 3\"\n\t\t}\n\t}\n\n\t// distro channels are not supported in the grype v5 schema, so the records should be dropped entirely\n\tif strings.Contains(ver, \"+\") {\n\t\treturn nil, nil\n\t}\n\n\tns, err := namespace.FromString(fmt.Sprintf(\"%s:distro:%s:%s\", providerName, distroName, ver))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn ns, nil\n}\n\nfunc buildPackageQualifiers(fixedInEntry unmarshal.OSFixedIn) (qualifiers []qualifier.Qualifier) {\n\tif fixedInEntry.VersionFormat == \"rpm\" {\n\t\tmodule := \"\"\n\t\tif fixedInEntry.Module != nil {\n\t\t\tmodule = *fixedInEntry.Module\n\t\t}\n\n\t\tqualifiers = []qualifier.Qualifier{rpmmodularity.Qualifier{\n\t\t\tKind:   \"rpm-modularity\",\n\t\t\tModule: module,\n\t\t}}\n\t}\n\n\treturn qualifiers\n}\n\nfunc Transform(vulnerability unmarshal.OSVulnerability) ([]data.Entry, error) {\n\tvar allVulns []db.Vulnerability\n\n\t// TODO: stop capturing record source in the vulnerability metadata record (now that feed groups are not real)\n\trecordSource := fmt.Sprintf(\"vulnerabilities:%s\", vulnerability.Vulnerability.NamespaceName)\n\n\tgrypeNamespace, err := buildGrypeNamespace(vulnerability.Vulnerability.NamespaceName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif grypeNamespace == nil {\n\t\t// this is an enterprise feed group that does not have a corresponding grype namespace, so skip it\n\t\treturn nil, nil\n\t}\n\n\tentryNamespace := grypeNamespace.String()\n\n\t// there may be multiple packages indicated within the FixedIn field, we should make\n\t// separate vulnerability entries (one for each name|namespace combo) while merging\n\t// constraint ranges as they are found.\n\tfor idx, fixedInEntry := range vulnerability.Vulnerability.FixedIn {\n\t\t// create vulnerability entry\n\t\tallVulns = append(allVulns, db.Vulnerability{\n\t\t\tID:                     vulnerability.Vulnerability.Name,\n\t\t\tPackageQualifiers:      buildPackageQualifiers(fixedInEntry),\n\t\t\tVersionConstraint:      enforceConstraint(fixedInEntry.Version, fixedInEntry.VulnerableRange, fixedInEntry.VersionFormat, vulnerability.Vulnerability.Name),\n\t\t\tVersionFormat:          fixedInEntry.VersionFormat,\n\t\t\tPackageName:            grypeNamespace.Resolver().Normalize(fixedInEntry.Name),\n\t\t\tNamespace:              entryNamespace,\n\t\t\tRelatedVulnerabilities: getRelatedVulnerabilities(vulnerability),\n\t\t\tFix:                    getFix(vulnerability, idx),\n\t\t\tAdvisories:             getAdvisories(vulnerability, idx),\n\t\t})\n\t}\n\n\t// create vulnerability metadata entry (a single entry keyed off of the vulnerability ID)\n\tmetadata := db.VulnerabilityMetadata{\n\t\tID:           vulnerability.Vulnerability.Name,\n\t\tNamespace:    entryNamespace,\n\t\tDataSource:   vulnerability.Vulnerability.Link,\n\t\tRecordSource: recordSource,\n\t\tSeverity:     vulnerability.Vulnerability.Severity,\n\t\tURLs:         getLinks(vulnerability),\n\t\tDescription:  vulnerability.Vulnerability.Description,\n\t\tCvss:         getCvss(vulnerability),\n\t}\n\n\treturn transformers.NewEntries(allVulns, metadata), nil\n}\n\nfunc getLinks(entry unmarshal.OSVulnerability) []string {\n\t// find all URLs related to the vulnerability\n\tlinks := []string{entry.Vulnerability.Link}\n\tif entry.Vulnerability.Metadata.CVE != nil {\n\t\tfor _, cve := range entry.Vulnerability.Metadata.CVE {\n\t\t\tif cve.Link != \"\" {\n\t\t\t\tlinks = append(links, cve.Link)\n\t\t\t}\n\t\t}\n\t}\n\treturn links\n}\n\nfunc getCvss(entry unmarshal.OSVulnerability) (cvss []db.Cvss) {\n\tfor _, vendorCvss := range entry.Vulnerability.CVSS {\n\t\tcvss = append(cvss, db.Cvss{\n\t\t\tVersion: vendorCvss.Version,\n\t\t\tVector:  vendorCvss.VectorString,\n\t\t\tMetrics: db.NewCvssMetrics(\n\t\t\t\tvendorCvss.BaseMetrics.BaseScore,\n\t\t\t\tvendorCvss.BaseMetrics.ExploitabilityScore,\n\t\t\t\tvendorCvss.BaseMetrics.ImpactScore,\n\t\t\t),\n\t\t\tVendorMetadata: transformers.VendorBaseMetrics{\n\t\t\t\tBaseSeverity: vendorCvss.BaseMetrics.BaseSeverity,\n\t\t\t\tStatus:       vendorCvss.Status,\n\t\t\t},\n\t\t})\n\t}\n\treturn cvss\n}\n\nfunc getAdvisories(entry unmarshal.OSVulnerability, idx int) (advisories []db.Advisory) {\n\tfixedInEntry := entry.Vulnerability.FixedIn[idx]\n\n\tfor _, advisory := range fixedInEntry.VendorAdvisory.AdvisorySummary {\n\t\tadvisories = append(advisories, db.Advisory{\n\t\t\tID:   advisory.ID,\n\t\t\tLink: advisory.Link,\n\t\t})\n\t}\n\treturn advisories\n}\n\nfunc getFix(entry unmarshal.OSVulnerability, idx int) db.Fix {\n\tfixedInEntry := entry.Vulnerability.FixedIn[idx]\n\n\tvar fixedInVersions []string\n\tfixedInVersion := versionutil.CleanFixedInVersion(fixedInEntry.Version)\n\tif fixedInVersion != \"\" {\n\t\tfixedInVersions = append(fixedInVersions, fixedInVersion)\n\t}\n\n\tfixState := db.NotFixedState\n\tif len(fixedInVersions) > 0 {\n\t\tfixState = db.FixedState\n\t} else if fixedInEntry.VendorAdvisory.NoAdvisory {\n\t\tfixState = db.WontFixState\n\t}\n\n\treturn db.Fix{\n\t\tVersions: fixedInVersions,\n\t\tState:    fixState,\n\t}\n}\n\nfunc getRelatedVulnerabilities(entry unmarshal.OSVulnerability) (vulns []db.VulnerabilityReference) {\n\t// associate related vulnerabilities from the NVD namespace\n\tif strings.HasPrefix(entry.Vulnerability.Name, \"CVE\") {\n\t\tvulns = append(vulns, db.VulnerabilityReference{\n\t\t\tID:        entry.Vulnerability.Name,\n\t\t\tNamespace: \"nvd:cpe\",\n\t\t})\n\t}\n\n\t// 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\n\tfor _, ref := range entry.Vulnerability.Metadata.CVE {\n\t\tvulns = append(vulns, db.VulnerabilityReference{\n\t\t\tID:        ref.Name,\n\t\t\tNamespace: \"nvd:cpe\",\n\t\t})\n\t}\n\treturn vulns\n}\n\nfunc deriveConstraintFromFix(fixVersion, vulnerabilityID string) string {\n\tconstraint := fmt.Sprintf(\"< %s\", fixVersion)\n\n\tif strings.HasPrefix(vulnerabilityID, \"ALASKERNEL-\") {\n\t\t// Amazon advisories of the form ALASKERNEL-5.4-2023-048 should be interpreted as only applying to\n\t\t// the 5.4.x kernel line since Amazon issue a separate advisory per affected line, thus the constraint\n\t\t// should be >= 5.4, < {fix version}.  In the future the vunnel schema for OS vulns should be enhanced\n\t\t// to emit actual constraints rather than fixed-in entries (tracked in https://github.com/anchore/vunnel/issues/266)\n\t\t// at which point this workaround in grype-db can be removed.\n\n\t\tcomponents := strings.Split(vulnerabilityID, \"-\")\n\n\t\tif len(components) == 4 {\n\t\t\tbase := components[1]\n\t\t\tconstraint = fmt.Sprintf(\">= %s, < %s\", base, fixVersion)\n\t\t}\n\t}\n\n\treturn constraint\n}\n\nfunc enforceConstraint(fixedVersion, vulnerableRange, format, vulnerabilityID string) string {\n\tif len(vulnerableRange) > 0 {\n\t\treturn vulnerableRange\n\t}\n\tfixedVersion = versionutil.CleanConstraint(fixedVersion)\n\tif len(fixedVersion) == 0 {\n\t\treturn \"\"\n\t}\n\tswitch strings.ToLower(format) {\n\tcase \"semver\":\n\t\treturn versionutil.EnforceSemVerConstraint(fixedVersion)\n\tdefault:\n\t\t// the passed constraint is a fixed version\n\t\treturn deriveConstraintFromFix(fixedVersion, vulnerabilityID)\n\t}\n}\n"
  },
  {
    "path": "grype/db/v5/build/transformers/os/transform_test.go",
    "content": "package os\n\nimport (\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/grype/grype/db/internal/provider/unmarshal\"\n\t\"github.com/anchore/grype/grype/db/internal/testutil\"\n\tdb \"github.com/anchore/grype/grype/db/v5\"\n\t\"github.com/anchore/grype/grype/db/v5/build/transformers\"\n\t\"github.com/anchore/grype/grype/db/v5/pkg/qualifier\"\n\t\"github.com/anchore/grype/grype/db/v5/pkg/qualifier/rpmmodularity\"\n)\n\nfunc TestUnmarshalOSVulnerabilitiesEntries(t *testing.T) {\n\tf, err := os.Open(\"testdata/unmarshal-test.json\")\n\trequire.NoError(t, err)\n\tdefer testutil.CloseFile(f)\n\n\tentries, err := unmarshal.OSVulnerabilityEntries(f)\n\trequire.NoError(t, err)\n\n\tassert.Len(t, entries, 3)\n\n}\n\nfunc TestParseVulnerabilitiesEntry(t *testing.T) {\n\ttests := []struct {\n\t\tname       string\n\t\tnumEntries int\n\t\tfixture    string\n\t\tvulns      []db.Vulnerability\n\t\tmetadata   db.VulnerabilityMetadata\n\t}{\n\t\t{\n\t\t\tname:       \"Amazon\",\n\t\t\tnumEntries: 1,\n\t\t\tfixture:    \"testdata/amzn.json\",\n\t\t\tvulns: []db.Vulnerability{\n\t\t\t\t{\n\t\t\t\t\tID:                \"ALAS-2018-1106\",\n\t\t\t\t\tVersionConstraint: \"< 1.3.8.4-15.amzn2.0.1\",\n\t\t\t\t\tVersionFormat:     \"rpm\",\n\t\t\t\t\tRelatedVulnerabilities: []db.VulnerabilityReference{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tID:        \"CVE-2018-14648\",\n\t\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tPackageName: \"389-ds-base\",\n\t\t\t\t\tPackageQualifiers: []qualifier.Qualifier{rpmmodularity.Qualifier{\n\t\t\t\t\t\tKind:   \"rpm-modularity\",\n\t\t\t\t\t\tModule: \"\",\n\t\t\t\t\t}},\n\t\t\t\t\tNamespace: \"amazon:distro:amazonlinux:2\",\n\t\t\t\t\tFix: db.Fix{\n\t\t\t\t\t\tVersions: []string{\"1.3.8.4-15.amzn2.0.1\"},\n\t\t\t\t\t\tState:    db.FixedState,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID:                \"ALAS-2018-1106\",\n\t\t\t\t\tVersionConstraint: \"< 1.3.8.4-15.amzn2.0.1\",\n\t\t\t\t\tVersionFormat:     \"rpm\",\n\t\t\t\t\tRelatedVulnerabilities: []db.VulnerabilityReference{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tID:        \"CVE-2018-14648\",\n\t\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tPackageName: \"389-ds-base-debuginfo\",\n\t\t\t\t\tPackageQualifiers: []qualifier.Qualifier{rpmmodularity.Qualifier{\n\t\t\t\t\t\tKind:   \"rpm-modularity\",\n\t\t\t\t\t\tModule: \"\",\n\t\t\t\t\t}},\n\t\t\t\t\tNamespace: \"amazon:distro:amazonlinux:2\",\n\t\t\t\t\tFix: db.Fix{\n\t\t\t\t\t\tVersions: []string{\"1.3.8.4-15.amzn2.0.1\"},\n\t\t\t\t\t\tState:    db.FixedState,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID:                \"ALAS-2018-1106\",\n\t\t\t\t\tVersionConstraint: \"< 1.3.8.4-15.amzn2.0.1\",\n\t\t\t\t\tVersionFormat:     \"rpm\",\n\t\t\t\t\tRelatedVulnerabilities: []db.VulnerabilityReference{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tID:        \"CVE-2018-14648\",\n\t\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tPackageName: \"389-ds-base-devel\",\n\t\t\t\t\tPackageQualifiers: []qualifier.Qualifier{rpmmodularity.Qualifier{\n\t\t\t\t\t\tKind:   \"rpm-modularity\",\n\t\t\t\t\t\tModule: \"\",\n\t\t\t\t\t}},\n\t\t\t\t\tNamespace: \"amazon:distro:amazonlinux:2\",\n\t\t\t\t\tFix: db.Fix{\n\t\t\t\t\t\tVersions: []string{\"1.3.8.4-15.amzn2.0.1\"},\n\t\t\t\t\t\tState:    db.FixedState,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID:                \"ALAS-2018-1106\",\n\t\t\t\t\tVersionConstraint: \"< 1.3.8.4-15.amzn2.0.1\",\n\t\t\t\t\tVersionFormat:     \"rpm\",\n\t\t\t\t\tRelatedVulnerabilities: []db.VulnerabilityReference{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tID:        \"CVE-2018-14648\",\n\t\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tPackageName: \"389-ds-base-libs\",\n\t\t\t\t\tPackageQualifiers: []qualifier.Qualifier{rpmmodularity.Qualifier{\n\t\t\t\t\t\tKind:   \"rpm-modularity\",\n\t\t\t\t\t\tModule: \"\",\n\t\t\t\t\t}},\n\t\t\t\t\tNamespace: \"amazon:distro:amazonlinux:2\",\n\t\t\t\t\tFix: db.Fix{\n\t\t\t\t\t\tVersions: []string{\"1.3.8.4-15.amzn2.0.1\"},\n\t\t\t\t\t\tState:    db.FixedState,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID:                \"ALAS-2018-1106\",\n\t\t\t\t\tVersionConstraint: \"< 1.3.8.4-15.amzn2.0.1\",\n\t\t\t\t\tVersionFormat:     \"rpm\",\n\t\t\t\t\tRelatedVulnerabilities: []db.VulnerabilityReference{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tID:        \"CVE-2018-14648\",\n\t\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tPackageName: \"389-ds-base-snmp\",\n\t\t\t\t\tPackageQualifiers: []qualifier.Qualifier{rpmmodularity.Qualifier{\n\t\t\t\t\t\tKind:   \"rpm-modularity\",\n\t\t\t\t\t\tModule: \"\",\n\t\t\t\t\t}},\n\t\t\t\t\tNamespace: \"amazon:distro:amazonlinux:2\",\n\t\t\t\t\tFix: db.Fix{\n\t\t\t\t\t\tVersions: []string{\"1.3.8.4-15.amzn2.0.1\"},\n\t\t\t\t\t\tState:    db.FixedState,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tmetadata: db.VulnerabilityMetadata{\n\t\t\t\tID:           \"ALAS-2018-1106\",\n\t\t\t\tNamespace:    \"amazon:distro:amazonlinux:2\",\n\t\t\t\tDataSource:   \"https://alas.aws.amazon.com/AL2/ALAS-2018-1106.html\",\n\t\t\t\tRecordSource: \"vulnerabilities:amzn:2\",\n\t\t\t\tSeverity:     \"Medium\",\n\t\t\t\tURLs:         []string{\"https://alas.aws.amazon.com/AL2/ALAS-2018-1106.html\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:       \"Debian\",\n\t\t\tnumEntries: 1,\n\t\t\tfixture:    \"testdata/debian-8.json\",\n\t\t\tvulns: []db.Vulnerability{\n\t\t\t\t{\n\t\t\t\t\tID:                \"CVE-2008-7220\",\n\t\t\t\t\tPackageName:       \"asterisk\",\n\t\t\t\t\tVersionConstraint: \"< 1:1.6.2.0~rc3-1\",\n\t\t\t\t\tVersionFormat:     \"dpkg\",\n\t\t\t\t\tNamespace:         \"debian:distro:debian:8\",\n\t\t\t\t\tRelatedVulnerabilities: []db.VulnerabilityReference{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tID:        \"CVE-2008-7220\",\n\t\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tFix: db.Fix{\n\t\t\t\t\t\tVersions: []string{\"1:1.6.2.0~rc3-1\"},\n\t\t\t\t\t\tState:    db.FixedState,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID:                \"CVE-2008-7220\",\n\t\t\t\t\tPackageName:       \"auth2db\",\n\t\t\t\t\tVersionConstraint: \"< 0.2.5-2+dfsg-1\",\n\t\t\t\t\tVersionFormat:     \"dpkg\",\n\t\t\t\t\tNamespace:         \"debian:distro:debian:8\",\n\t\t\t\t\tRelatedVulnerabilities: []db.VulnerabilityReference{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tID:        \"CVE-2008-7220\",\n\t\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tFix: db.Fix{\n\t\t\t\t\t\tVersions: []string{\"0.2.5-2+dfsg-1\"},\n\t\t\t\t\t\tState:    db.FixedState,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID:                \"CVE-2008-7220\",\n\t\t\t\t\tPackageName:       \"exaile\",\n\t\t\t\t\tVersionConstraint: \"< 0.2.14+debian-2.2\",\n\t\t\t\t\tVersionFormat:     \"dpkg\",\n\t\t\t\t\tNamespace:         \"debian:distro:debian:8\",\n\t\t\t\t\tRelatedVulnerabilities: []db.VulnerabilityReference{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tID:        \"CVE-2008-7220\",\n\t\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tFix: db.Fix{\n\t\t\t\t\t\tVersions: []string{\"0.2.14+debian-2.2\"},\n\t\t\t\t\t\tState:    db.FixedState,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID:          \"CVE-2008-7220\",\n\t\t\t\t\tPackageName: \"wordpress\",\n\t\t\t\t\tRelatedVulnerabilities: []db.VulnerabilityReference{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tID:        \"CVE-2008-7220\",\n\t\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tFix: db.Fix{\n\t\t\t\t\t\tState: db.NotFixedState,\n\t\t\t\t\t},\n\t\t\t\t\tVersionConstraint: \"\",\n\t\t\t\t\tVersionFormat:     \"dpkg\",\n\t\t\t\t\tNamespace:         \"debian:distro:debian:8\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tmetadata: db.VulnerabilityMetadata{\n\t\t\t\tID:           \"CVE-2008-7220\",\n\t\t\t\tNamespace:    \"debian:distro:debian:8\",\n\t\t\t\tDataSource:   \"https://security-tracker.debian.org/tracker/CVE-2008-7220\",\n\t\t\t\tRecordSource: \"vulnerabilities:debian:8\",\n\t\t\t\tSeverity:     \"High\",\n\t\t\t\tURLs:         []string{\"https://security-tracker.debian.org/tracker/CVE-2008-7220\"},\n\t\t\t\tDescription:  \"\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:       \"RHEL\",\n\t\t\tnumEntries: 1,\n\t\t\tfixture:    \"testdata/rhel-8.json\",\n\t\t\tvulns: []db.Vulnerability{\n\t\t\t\t{\n\t\t\t\t\tID:          \"CVE-2020-6819\",\n\t\t\t\t\tPackageName: \"firefox\",\n\t\t\t\t\tPackageQualifiers: []qualifier.Qualifier{rpmmodularity.Qualifier{\n\t\t\t\t\t\tKind:   \"rpm-modularity\",\n\t\t\t\t\t\tModule: \"\",\n\t\t\t\t\t}},\n\t\t\t\t\tVersionConstraint: \"< 0:68.6.1-1.el8_1\",\n\t\t\t\t\tVersionFormat:     \"rpm\",\n\t\t\t\t\tNamespace:         \"redhat:distro:redhat:8\",\n\t\t\t\t\tRelatedVulnerabilities: []db.VulnerabilityReference{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tID:        \"CVE-2020-6819\",\n\t\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tFix: db.Fix{\n\t\t\t\t\t\tVersions: []string{\"0:68.6.1-1.el8_1\"},\n\t\t\t\t\t\tState:    db.FixedState,\n\t\t\t\t\t},\n\t\t\t\t\tAdvisories: []db.Advisory{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tID:   \"RHSA-2020:1341\",\n\t\t\t\t\t\t\tLink: \"https://access.redhat.com/errata/RHSA-2020:1341\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID:          \"CVE-2020-6819\",\n\t\t\t\t\tPackageName: \"thunderbird\",\n\t\t\t\t\tPackageQualifiers: []qualifier.Qualifier{rpmmodularity.Qualifier{\n\t\t\t\t\t\tKind:   \"rpm-modularity\",\n\t\t\t\t\t\tModule: \"\",\n\t\t\t\t\t}},\n\t\t\t\t\tVersionConstraint: \"< 0:68.7.0-1.el8_1\",\n\t\t\t\t\tVersionFormat:     \"rpm\",\n\t\t\t\t\tNamespace:         \"redhat:distro:redhat:8\",\n\t\t\t\t\tRelatedVulnerabilities: []db.VulnerabilityReference{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tID:        \"CVE-2020-6819\",\n\t\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tFix: db.Fix{\n\t\t\t\t\t\tVersions: []string{\"0:68.7.0-1.el8_1\"},\n\t\t\t\t\t\tState:    db.FixedState,\n\t\t\t\t\t},\n\t\t\t\t\tAdvisories: []db.Advisory{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tID:   \"RHSA-2020:1495\",\n\t\t\t\t\t\t\tLink: \"https://access.redhat.com/errata/RHSA-2020:1495\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tmetadata: db.VulnerabilityMetadata{\n\t\t\t\tID:           \"CVE-2020-6819\",\n\t\t\t\tDataSource:   \"https://access.redhat.com/security/cve/CVE-2020-6819\",\n\t\t\t\tNamespace:    \"redhat:distro:redhat:8\",\n\t\t\t\tRecordSource: \"vulnerabilities:rhel:8\",\n\t\t\t\tSeverity:     \"Critical\",\n\t\t\t\tURLs:         []string{\"https://access.redhat.com/security/cve/CVE-2020-6819\"},\n\t\t\t\tDescription:  \"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.\",\n\t\t\t\tCvss: []db.Cvss{\n\t\t\t\t\t{\n\t\t\t\t\t\tVersion: \"3.1\",\n\t\t\t\t\t\tVector:  \"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H\",\n\t\t\t\t\t\tMetrics: db.NewCvssMetrics(\n\t\t\t\t\t\t\t8.8,\n\t\t\t\t\t\t\t2.8,\n\t\t\t\t\t\t\t5.9,\n\t\t\t\t\t\t),\n\t\t\t\t\t\tVendorMetadata: transformers.VendorBaseMetrics{\n\t\t\t\t\t\t\tStatus:       \"verified\",\n\t\t\t\t\t\t\tBaseSeverity: \"High\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:       \"RHEL with modularity\",\n\t\t\tnumEntries: 1,\n\t\t\tfixture:    \"testdata/rhel-8-modules.json\",\n\t\t\tvulns: []db.Vulnerability{\n\t\t\t\t{\n\t\t\t\t\tID:          \"CVE-2020-14350\",\n\t\t\t\t\tPackageName: \"postgresql\",\n\t\t\t\t\tPackageQualifiers: []qualifier.Qualifier{rpmmodularity.Qualifier{\n\t\t\t\t\t\tKind:   \"rpm-modularity\",\n\t\t\t\t\t\tModule: \"postgresql:10\",\n\t\t\t\t\t}},\n\t\t\t\t\tVersionConstraint: \"< 0:10.14-1.module+el8.2.0+7801+be0fed80\",\n\t\t\t\t\tVersionFormat:     \"rpm\",\n\t\t\t\t\tNamespace:         \"redhat:distro:redhat:8\",\n\t\t\t\t\tRelatedVulnerabilities: []db.VulnerabilityReference{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tID:        \"CVE-2020-14350\",\n\t\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tFix: db.Fix{\n\t\t\t\t\t\tVersions: []string{\"0:10.14-1.module+el8.2.0+7801+be0fed80\"},\n\t\t\t\t\t\tState:    db.FixedState,\n\t\t\t\t\t},\n\t\t\t\t\tAdvisories: []db.Advisory{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tID:   \"RHSA-2020:3669\",\n\t\t\t\t\t\t\tLink: \"https://access.redhat.com/errata/RHSA-2020:3669\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID:          \"CVE-2020-14350\",\n\t\t\t\t\tPackageName: \"postgresql\",\n\t\t\t\t\tPackageQualifiers: []qualifier.Qualifier{rpmmodularity.Qualifier{\n\t\t\t\t\t\tKind:   \"rpm-modularity\",\n\t\t\t\t\t\tModule: \"postgresql:12\",\n\t\t\t\t\t}},\n\t\t\t\t\tVersionConstraint: \"< 0:12.5-1.module+el8.3.0+9042+664538f4\",\n\t\t\t\t\tVersionFormat:     \"rpm\",\n\t\t\t\t\tNamespace:         \"redhat:distro:redhat:8\",\n\t\t\t\t\tRelatedVulnerabilities: []db.VulnerabilityReference{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tID:        \"CVE-2020-14350\",\n\t\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tFix: db.Fix{\n\t\t\t\t\t\tVersions: []string{\"0:12.5-1.module+el8.3.0+9042+664538f4\"},\n\t\t\t\t\t\tState:    db.FixedState,\n\t\t\t\t\t},\n\t\t\t\t\tAdvisories: []db.Advisory{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tID:   \"RHSA-2020:5620\",\n\t\t\t\t\t\t\tLink: \"https://access.redhat.com/errata/RHSA-2020:5620\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID:          \"CVE-2020-14350\",\n\t\t\t\t\tPackageName: \"postgresql\",\n\t\t\t\t\tPackageQualifiers: []qualifier.Qualifier{rpmmodularity.Qualifier{\n\t\t\t\t\t\tKind:   \"rpm-modularity\",\n\t\t\t\t\t\tModule: \"postgresql:9.6\",\n\t\t\t\t\t}},\n\t\t\t\t\tVersionConstraint: \"< 0:9.6.20-1.module+el8.3.0+8938+7f0e88b6\",\n\t\t\t\t\tVersionFormat:     \"rpm\",\n\t\t\t\t\tNamespace:         \"redhat:distro:redhat:8\",\n\t\t\t\t\tRelatedVulnerabilities: []db.VulnerabilityReference{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tID:        \"CVE-2020-14350\",\n\t\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tFix: db.Fix{\n\t\t\t\t\t\tVersions: []string{\"0:9.6.20-1.module+el8.3.0+8938+7f0e88b6\"},\n\t\t\t\t\t\tState:    db.FixedState,\n\t\t\t\t\t},\n\t\t\t\t\tAdvisories: []db.Advisory{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tID:   \"RHSA-2020:5619\",\n\t\t\t\t\t\t\tLink: \"https://access.redhat.com/errata/RHSA-2020:5619\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tmetadata: db.VulnerabilityMetadata{\n\t\t\t\tID:           \"CVE-2020-14350\",\n\t\t\t\tDataSource:   \"https://access.redhat.com/security/cve/CVE-2020-14350\",\n\t\t\t\tNamespace:    \"redhat:distro:redhat:8\",\n\t\t\t\tRecordSource: \"vulnerabilities:rhel:8\",\n\t\t\t\tSeverity:     \"Medium\",\n\t\t\t\tURLs:         []string{\"https://access.redhat.com/security/cve/CVE-2020-14350\"},\n\t\t\t\tDescription:  \"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.\",\n\t\t\t\tCvss: []db.Cvss{\n\t\t\t\t\t{\n\t\t\t\t\t\tVersion: \"3.1\",\n\t\t\t\t\t\tVector:  \"CVSS:3.1/AV:N/AC:H/PR:L/UI:R/S:U/C:H/I:H/A:H\",\n\t\t\t\t\t\tMetrics: db.NewCvssMetrics(\n\t\t\t\t\t\t\t7.1,\n\t\t\t\t\t\t\t1.2,\n\t\t\t\t\t\t\t5.9,\n\t\t\t\t\t\t),\n\t\t\t\t\t\tVendorMetadata: transformers.VendorBaseMetrics{\n\t\t\t\t\t\t\tStatus:       \"verified\",\n\t\t\t\t\t\t\tBaseSeverity: \"High\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:       \"RHEL EUS (ignore)\",\n\t\t\tnumEntries: 1,\n\t\t\tfixture:    \"testdata/rhel-8-eus.json\",\n\t\t\t// intentionally creates no vulnerabilities to write to the DB\n\t\t},\n\t\t{\n\t\t\tname:       \"Photon (ignore)\",\n\t\t\tnumEntries: 1,\n\t\t\tfixture:    \"testdata/photon-4.0.json\",\n\t\t\t// photon is not supported in v5, records should be dropped entirely\n\t\t},\n\t\t{\n\t\t\tname:       \"Alpine\",\n\t\t\tnumEntries: 1,\n\t\t\tfixture:    \"testdata/alpine-3.9.json\",\n\t\t\tvulns: []db.Vulnerability{\n\t\t\t\t{\n\t\t\t\t\tID:                \"CVE-2018-19967\",\n\t\t\t\t\tPackageName:       \"xen\",\n\t\t\t\t\tVersionConstraint: \"< 4.11.1-r0\",\n\t\t\t\t\tVersionFormat:     \"apk\",\n\t\t\t\t\tNamespace:         \"alpine:distro:alpine:3.9\",\n\t\t\t\t\tRelatedVulnerabilities: []db.VulnerabilityReference{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tID:        \"CVE-2018-19967\",\n\t\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tFix: db.Fix{\n\t\t\t\t\t\tVersions: []string{\"4.11.1-r0\"},\n\t\t\t\t\t\tState:    db.FixedState,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tmetadata: db.VulnerabilityMetadata{\n\t\t\t\tID:           \"CVE-2018-19967\",\n\t\t\t\tDataSource:   \"http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-19967\",\n\t\t\t\tNamespace:    \"alpine:distro:alpine:3.9\",\n\t\t\t\tRecordSource: \"vulnerabilities:alpine:3.9\",\n\t\t\t\tSeverity:     \"Medium\",\n\t\t\t\tURLs:         []string{\"http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-19967\"},\n\t\t\t\tDescription:  \"\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:       \"Oracle\",\n\t\t\tnumEntries: 1,\n\t\t\tfixture:    \"testdata/ol-8.json\",\n\t\t\tvulns: []db.Vulnerability{\n\t\t\t\t{\n\t\t\t\t\tID:          \"ELSA-2020-2550\",\n\t\t\t\t\tPackageName: \"libexif\",\n\t\t\t\t\tPackageQualifiers: []qualifier.Qualifier{rpmmodularity.Qualifier{\n\t\t\t\t\t\tKind:   \"rpm-modularity\",\n\t\t\t\t\t\tModule: \"\",\n\t\t\t\t\t}},\n\t\t\t\t\tVersionConstraint: \"< 0:0.6.21-17.el8_2\",\n\t\t\t\t\tVersionFormat:     \"rpm\",\n\t\t\t\t\tRelatedVulnerabilities: []db.VulnerabilityReference{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tID:        \"CVE-2020-13112\",\n\t\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tNamespace: \"oracle:distro:oraclelinux:8\",\n\t\t\t\t\tFix: db.Fix{\n\t\t\t\t\t\tVersions: []string{\"0:0.6.21-17.el8_2\"},\n\t\t\t\t\t\tState:    db.FixedState,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID:          \"ELSA-2020-2550\",\n\t\t\t\t\tPackageName: \"libexif-devel\",\n\t\t\t\t\tPackageQualifiers: []qualifier.Qualifier{rpmmodularity.Qualifier{\n\t\t\t\t\t\tKind:   \"rpm-modularity\",\n\t\t\t\t\t\tModule: \"\",\n\t\t\t\t\t}},\n\t\t\t\t\tVersionConstraint: \"< 0:0.6.21-17.el8_2\",\n\t\t\t\t\tVersionFormat:     \"rpm\",\n\t\t\t\t\tRelatedVulnerabilities: []db.VulnerabilityReference{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tID:        \"CVE-2020-13112\",\n\t\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tNamespace: \"oracle:distro:oraclelinux:8\",\n\t\t\t\t\tFix: db.Fix{\n\t\t\t\t\t\tVersions: []string{\"0:0.6.21-17.el8_2\"},\n\t\t\t\t\t\tState:    db.FixedState,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID:          \"ELSA-2020-2550\",\n\t\t\t\t\tPackageName: \"libexif-dummy\",\n\t\t\t\t\tPackageQualifiers: []qualifier.Qualifier{rpmmodularity.Qualifier{\n\t\t\t\t\t\tKind:   \"rpm-modularity\",\n\t\t\t\t\t\tModule: \"\",\n\t\t\t\t\t}},\n\t\t\t\t\tVersionConstraint: \"\",\n\t\t\t\t\tVersionFormat:     \"rpm\",\n\t\t\t\t\tRelatedVulnerabilities: []db.VulnerabilityReference{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tID:        \"CVE-2020-13112\",\n\t\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tNamespace: \"oracle:distro:oraclelinux:8\",\n\t\t\t\t\tFix: db.Fix{\n\t\t\t\t\t\tVersions: nil,\n\t\t\t\t\t\tState:    db.NotFixedState,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tmetadata: db.VulnerabilityMetadata{\n\t\t\t\tID:           \"ELSA-2020-2550\",\n\t\t\t\tDataSource:   \"http://linux.oracle.com/errata/ELSA-2020-2550.html\",\n\t\t\t\tNamespace:    \"oracle:distro:oraclelinux:8\",\n\t\t\t\tRecordSource: \"vulnerabilities:ol:8\",\n\t\t\t\tSeverity:     \"Medium\",\n\t\t\t\tURLs:         []string{\"http://linux.oracle.com/errata/ELSA-2020-2550.html\", \"http://linux.oracle.com/cve/CVE-2020-13112.html\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:       \"Oracle Linux 8 with modularity\",\n\t\t\tnumEntries: 1,\n\t\t\tfixture:    \"testdata/ol-8-modules.json\",\n\t\t\tvulns: []db.Vulnerability{\n\t\t\t\t{\n\t\t\t\t\tID:          \"CVE-2020-14350\",\n\t\t\t\t\tPackageName: \"postgresql\",\n\t\t\t\t\tPackageQualifiers: []qualifier.Qualifier{rpmmodularity.Qualifier{\n\t\t\t\t\t\tKind:   \"rpm-modularity\",\n\t\t\t\t\t\tModule: \"postgresql:10\",\n\t\t\t\t\t}},\n\t\t\t\t\tVersionConstraint: \"< 0:10.14-1.module+el8.2.0+7801+be0fed80\",\n\t\t\t\t\tVersionFormat:     \"rpm\",\n\t\t\t\t\tNamespace:         \"oracle:distro:oraclelinux:8\",\n\t\t\t\t\tRelatedVulnerabilities: []db.VulnerabilityReference{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tID:        \"CVE-2020-14350\",\n\t\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tFix: db.Fix{\n\t\t\t\t\t\tVersions: []string{\"0:10.14-1.module+el8.2.0+7801+be0fed80\"},\n\t\t\t\t\t\tState:    db.FixedState,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID:          \"CVE-2020-14350\",\n\t\t\t\t\tPackageName: \"postgresql\",\n\t\t\t\t\tPackageQualifiers: []qualifier.Qualifier{rpmmodularity.Qualifier{\n\t\t\t\t\t\tKind:   \"rpm-modularity\",\n\t\t\t\t\t\tModule: \"postgresql:12\",\n\t\t\t\t\t}},\n\t\t\t\t\tVersionConstraint: \"< 0:12.5-1.module+el8.3.0+9042+664538f4\",\n\t\t\t\t\tVersionFormat:     \"rpm\",\n\t\t\t\t\tNamespace:         \"oracle:distro:oraclelinux:8\",\n\t\t\t\t\tRelatedVulnerabilities: []db.VulnerabilityReference{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tID:        \"CVE-2020-14350\",\n\t\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tFix: db.Fix{\n\t\t\t\t\t\tVersions: []string{\"0:12.5-1.module+el8.3.0+9042+664538f4\"},\n\t\t\t\t\t\tState:    db.FixedState,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID:          \"CVE-2020-14350\",\n\t\t\t\t\tPackageName: \"postgresql\",\n\t\t\t\t\tPackageQualifiers: []qualifier.Qualifier{rpmmodularity.Qualifier{\n\t\t\t\t\t\tKind:   \"rpm-modularity\",\n\t\t\t\t\t\tModule: \"postgresql:9.6\",\n\t\t\t\t\t}},\n\t\t\t\t\tVersionConstraint: \"< 0:9.6.20-1.module+el8.3.0+8938+7f0e88b6\",\n\t\t\t\t\tVersionFormat:     \"rpm\",\n\t\t\t\t\tNamespace:         \"oracle:distro:oraclelinux:8\",\n\t\t\t\t\tRelatedVulnerabilities: []db.VulnerabilityReference{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tID:        \"CVE-2020-14350\",\n\t\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tFix: db.Fix{\n\t\t\t\t\t\tVersions: []string{\"0:9.6.20-1.module+el8.3.0+8938+7f0e88b6\"},\n\t\t\t\t\t\tState:    db.FixedState,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tmetadata: db.VulnerabilityMetadata{\n\t\t\t\tID:           \"CVE-2020-14350\",\n\t\t\t\tDataSource:   \"https://access.redhat.com/security/cve/CVE-2020-14350\",\n\t\t\t\tNamespace:    \"oracle:distro:oraclelinux:8\",\n\t\t\t\tRecordSource: \"vulnerabilities:ol:8\",\n\t\t\t\tSeverity:     \"Medium\",\n\t\t\t\tURLs:         []string{\"https://access.redhat.com/security/cve/CVE-2020-14350\"},\n\t\t\t\tDescription:  \"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.\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:       \"mariner linux 2.0\",\n\t\t\tnumEntries: 1,\n\t\t\tfixture:    \"testdata/mariner-20.json\",\n\t\t\tvulns: []db.Vulnerability{\n\t\t\t\t{\n\t\t\t\t\tID:          \"CVE-2021-37621\",\n\t\t\t\t\tPackageName: \"exiv2\",\n\t\t\t\t\tNamespace:   \"mariner:distro:mariner:2.0\",\n\t\t\t\t\tPackageQualifiers: []qualifier.Qualifier{\n\t\t\t\t\t\trpmmodularity.Qualifier{\n\t\t\t\t\t\t\tKind: \"rpm-modularity\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tRelatedVulnerabilities: []db.VulnerabilityReference{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tID:        \"CVE-2021-37621\",\n\t\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tVersionConstraint: \"< 0:0.27.5-1.cm2\",\n\t\t\t\t\tVersionFormat:     \"rpm\",\n\t\t\t\t\tFix: db.Fix{\n\t\t\t\t\t\tVersions: []string{\"0:0.27.5-1.cm2\"},\n\t\t\t\t\t\tState:    db.FixedState,\n\t\t\t\t\t},\n\t\t\t\t\tAdvisories: nil,\n\t\t\t\t},\n\t\t\t},\n\t\t\tmetadata: db.VulnerabilityMetadata{\n\t\t\t\tID:           \"CVE-2021-37621\",\n\t\t\t\tNamespace:    \"mariner:distro:mariner:2.0\",\n\t\t\t\tDataSource:   \"https://nvd.nist.gov/vuln/detail/CVE-2021-37621\",\n\t\t\t\tRecordSource: \"vulnerabilities:mariner:2.0\",\n\t\t\t\tSeverity:     \"Medium\",\n\t\t\t\tURLs:         []string{\"https://nvd.nist.gov/vuln/detail/CVE-2021-37621\"},\n\t\t\t\tDescription:  \"CVE-2021-37621 affecting package exiv2 for versions less than 0.27.5-1. An upgraded version of the package is available that resolves this issue.\",\n\t\t\t\tCvss:         nil,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:       \"azure linux 3\",\n\t\t\tnumEntries: 1,\n\t\t\tfixture:    \"testdata/azure-linux-3.json\",\n\t\t\tvulns: []db.Vulnerability{\n\t\t\t\t{\n\t\t\t\t\tID:          \"CVE-2023-29403\",\n\t\t\t\t\tPackageName: \"golang\",\n\t\t\t\t\tNamespace:   \"mariner:distro:azurelinux:3.0\",\n\t\t\t\t\tPackageQualifiers: []qualifier.Qualifier{\n\t\t\t\t\t\trpmmodularity.Qualifier{\n\t\t\t\t\t\t\tKind: \"rpm-modularity\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tRelatedVulnerabilities: []db.VulnerabilityReference{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tID:        \"CVE-2023-29403\",\n\t\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tVersionConstraint: \"< 0:1.20.7-1.azl3\",\n\t\t\t\t\tVersionFormat:     \"rpm\",\n\t\t\t\t\tFix: db.Fix{\n\t\t\t\t\t\tVersions: []string{\"0:1.20.7-1.azl3\"},\n\t\t\t\t\t\tState:    db.FixedState,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tmetadata: db.VulnerabilityMetadata{\n\t\t\t\tID:           \"CVE-2023-29403\",\n\t\t\t\tNamespace:    \"mariner:distro:azurelinux:3.0\",\n\t\t\t\tDataSource:   \"https://nvd.nist.gov/vuln/detail/CVE-2023-29403\",\n\t\t\t\tRecordSource: \"vulnerabilities:mariner:3.0\",\n\t\t\t\tSeverity:     \"High\",\n\t\t\t\tURLs:         []string{\"https://nvd.nist.gov/vuln/detail/CVE-2023-29403\"},\n\t\t\t\tDescription:  \"CVE-2023-29403 affecting package golang for versions less than 1.20.7-1. A patched version of the package is available.\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:       \"mariner entry with version range\",\n\t\t\tnumEntries: 1,\n\t\t\tfixture:    \"testdata/mariner-range.json\",\n\t\t\tvulns: []db.Vulnerability{\n\t\t\t\t{\n\t\t\t\t\tID:          \"CVE-2023-29404\",\n\t\t\t\t\tPackageName: \"golang\",\n\t\t\t\t\tNamespace:   \"mariner:distro:mariner:2.0\",\n\t\t\t\t\tPackageQualifiers: []qualifier.Qualifier{\n\t\t\t\t\t\trpmmodularity.Qualifier{\n\t\t\t\t\t\t\tKind:   \"rpm-modularity\",\n\t\t\t\t\t\t\tModule: \"\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tVersionConstraint: \"> 0:1.19.0.cm2, < 0:1.20.7-1.cm2\",\n\t\t\t\t\tVersionFormat:     \"rpm\",\n\t\t\t\t\tRelatedVulnerabilities: []db.VulnerabilityReference{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tID:        \"CVE-2023-29404\",\n\t\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tFix: db.Fix{\n\t\t\t\t\t\tVersions: []string{\"0:1.20.7-1.cm2\"},\n\t\t\t\t\t\tState:    db.FixedState,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tmetadata: db.VulnerabilityMetadata{\n\t\t\t\tID:           \"CVE-2023-29404\",\n\t\t\t\tNamespace:    \"mariner:distro:mariner:2.0\",\n\t\t\t\tDataSource:   \"https://nvd.nist.gov/vuln/detail/CVE-2023-29404\",\n\t\t\t\tRecordSource: \"vulnerabilities:mariner:2.0\",\n\t\t\t\tSeverity:     \"Critical\",\n\t\t\t\tURLs:         []string{\"https://nvd.nist.gov/vuln/detail/CVE-2023-29404\"},\n\t\t\t\tDescription:  \"CVE-2023-29404 affecting package golang for versions less than 1.20.7-1. A patched version of the package is available.\",\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tf, err := os.Open(test.fixture)\n\t\t\trequire.NoError(t, err)\n\t\t\tt.Cleanup(func() {\n\t\t\t\tassert.NoError(t, f.Close())\n\t\t\t})\n\n\t\t\tentries, err := unmarshal.OSVulnerabilityEntries(f)\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Len(t, entries, 1)\n\n\t\t\tentry := entries[0]\n\n\t\t\tdataEntries, err := Transform(entry)\n\t\t\tassert.NoError(t, err)\n\n\t\t\tvar vulns []db.Vulnerability\n\t\t\tfor _, entry := range dataEntries {\n\t\t\t\tswitch vuln := entry.Data.(type) {\n\t\t\t\tcase db.Vulnerability:\n\t\t\t\t\tvulns = append(vulns, vuln)\n\t\t\t\tcase db.VulnerabilityMetadata:\n\t\t\t\t\tassert.Equal(t, test.metadata, vuln)\n\t\t\t\tdefault:\n\t\t\t\t\tt.Fatalf(\"unexpected condition: data entry does not have a vulnerability or a metadata: %+v\", vuln)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif diff := cmp.Diff(test.vulns, vulns); diff != \"\" {\n\t\t\t\tt.Errorf(\"vulnerabilities do not match (-want +got):\\n%s\", diff)\n\t\t\t}\n\n\t\t})\n\t}\n\n}\n\nfunc TestParseVulnerabilitiesAllEntries(t *testing.T) {\n\ttests := []struct {\n\t\tname       string\n\t\tnumEntries int\n\t\tfixture    string\n\t\tvulns      []db.Vulnerability\n\t}{\n\t\t{\n\t\t\tname:       \"Debian\",\n\t\t\tnumEntries: 2,\n\t\t\tfixture:    \"testdata/debian-8-multiple-entries-for-same-package.json\",\n\t\t\tvulns: []db.Vulnerability{\n\t\t\t\t{\n\t\t\t\t\tID:                \"CVE-2011-4623\",\n\t\t\t\t\tPackageName:       \"rsyslog\",\n\t\t\t\t\tVersionConstraint: \"< 5.7.4-1\",\n\t\t\t\t\tVersionFormat:     \"dpkg\",\n\t\t\t\t\tNamespace:         \"debian:distro:debian:8\",\n\t\t\t\t\tRelatedVulnerabilities: []db.VulnerabilityReference{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tID:        \"CVE-2011-4623\",\n\t\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tFix: db.Fix{\n\t\t\t\t\t\tVersions: []string{\"5.7.4-1\"},\n\t\t\t\t\t\tState:    db.FixedState,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID:                \"CVE-2008-5618\",\n\t\t\t\t\tPackageName:       \"rsyslog\",\n\t\t\t\t\tVersionConstraint: \"< 3.18.6-1\",\n\t\t\t\t\tVersionFormat:     \"dpkg\",\n\t\t\t\t\tNamespace:         \"debian:distro:debian:8\",\n\t\t\t\t\tRelatedVulnerabilities: []db.VulnerabilityReference{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tID:        \"CVE-2008-5618\",\n\t\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tFix: db.Fix{\n\t\t\t\t\t\tVersions: []string{\"3.18.6-1\"},\n\t\t\t\t\t\tState:    db.FixedState,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:       \"Amazon\",\n\t\t\tnumEntries: 3,\n\t\t\tfixture:    \"testdata/amazon-multiple-kernel-advisories.json\",\n\t\t\tvulns: []db.Vulnerability{\n\t\t\t\t{\n\t\t\t\t\tID:          \"ALAS-2021-1704\",\n\t\t\t\t\tPackageName: \"kernel-headers\",\n\t\t\t\t\tPackageQualifiers: []qualifier.Qualifier{rpmmodularity.Qualifier{\n\t\t\t\t\t\tKind:   \"rpm-modularity\",\n\t\t\t\t\t\tModule: \"\",\n\t\t\t\t\t}},\n\t\t\t\t\tVersionConstraint: \"< 4.14.246-187.474.amzn2\",\n\t\t\t\t\tVersionFormat:     \"rpm\",\n\t\t\t\t\tNamespace:         \"amazon:distro:amazonlinux:2\",\n\t\t\t\t\tRelatedVulnerabilities: []db.VulnerabilityReference{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tID:        \"CVE-2021-3653\",\n\t\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tID:        \"CVE-2021-3656\",\n\t\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tID:        \"CVE-2021-3732\",\n\t\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tFix: db.Fix{\n\t\t\t\t\t\tVersions: []string{\"4.14.246-187.474.amzn2\"},\n\t\t\t\t\t\tState:    db.FixedState,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID:          \"ALAS-2021-1704\",\n\t\t\t\t\tPackageName: \"kernel\",\n\t\t\t\t\tPackageQualifiers: []qualifier.Qualifier{rpmmodularity.Qualifier{\n\t\t\t\t\t\tKind:   \"rpm-modularity\",\n\t\t\t\t\t\tModule: \"\",\n\t\t\t\t\t}},\n\t\t\t\t\tVersionConstraint: \"< 4.14.246-187.474.amzn2\",\n\t\t\t\t\tVersionFormat:     \"rpm\",\n\t\t\t\t\tNamespace:         \"amazon:distro:amazonlinux:2\",\n\t\t\t\t\tRelatedVulnerabilities: []db.VulnerabilityReference{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tID:        \"CVE-2021-3653\",\n\t\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tID:        \"CVE-2021-3656\",\n\t\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tID:        \"CVE-2021-3732\",\n\t\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tFix: db.Fix{\n\t\t\t\t\t\tVersions: []string{\"4.14.246-187.474.amzn2\"},\n\t\t\t\t\t\tState:    db.FixedState,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID:          \"ALASKERNEL-5.4-2022-007\",\n\t\t\t\t\tPackageName: \"kernel-headers\",\n\t\t\t\t\tPackageQualifiers: []qualifier.Qualifier{rpmmodularity.Qualifier{\n\t\t\t\t\t\tKind:   \"rpm-modularity\",\n\t\t\t\t\t\tModule: \"\",\n\t\t\t\t\t}},\n\t\t\t\t\tVersionConstraint: \">= 5.4, < 5.4.144-69.257.amzn2\",\n\t\t\t\t\tVersionFormat:     \"rpm\",\n\t\t\t\t\tNamespace:         \"amazon:distro:amazonlinux:2\",\n\t\t\t\t\tRelatedVulnerabilities: []db.VulnerabilityReference{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tID:        \"CVE-2021-3753\",\n\t\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tID:        \"CVE-2021-40490\",\n\t\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tFix: db.Fix{\n\t\t\t\t\t\tVersions: []string{\"5.4.144-69.257.amzn2\"},\n\t\t\t\t\t\tState:    db.FixedState,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID:          \"ALASKERNEL-5.4-2022-007\",\n\t\t\t\t\tPackageName: \"kernel\",\n\t\t\t\t\tPackageQualifiers: []qualifier.Qualifier{rpmmodularity.Qualifier{\n\t\t\t\t\t\tKind:   \"rpm-modularity\",\n\t\t\t\t\t\tModule: \"\",\n\t\t\t\t\t}},\n\t\t\t\t\tVersionConstraint: \">= 5.4, < 5.4.144-69.257.amzn2\",\n\t\t\t\t\tVersionFormat:     \"rpm\",\n\t\t\t\t\tNamespace:         \"amazon:distro:amazonlinux:2\",\n\t\t\t\t\tRelatedVulnerabilities: []db.VulnerabilityReference{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tID:        \"CVE-2021-3753\",\n\t\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tID:        \"CVE-2021-40490\",\n\t\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tFix: db.Fix{\n\t\t\t\t\t\tVersions: []string{\"5.4.144-69.257.amzn2\"},\n\t\t\t\t\t\tState:    db.FixedState,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID:          \"ALASKERNEL-5.10-2022-005\",\n\t\t\t\t\tPackageName: \"kernel-headers\",\n\t\t\t\t\tPackageQualifiers: []qualifier.Qualifier{rpmmodularity.Qualifier{\n\t\t\t\t\t\tKind:   \"rpm-modularity\",\n\t\t\t\t\t\tModule: \"\",\n\t\t\t\t\t}},\n\t\t\t\t\tVersionConstraint: \">= 5.10, < 5.10.62-55.141.amzn2\",\n\t\t\t\t\tVersionFormat:     \"rpm\",\n\t\t\t\t\tNamespace:         \"amazon:distro:amazonlinux:2\",\n\t\t\t\t\tRelatedVulnerabilities: []db.VulnerabilityReference{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tID:        \"CVE-2021-3753\",\n\t\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tID:        \"CVE-2021-40490\",\n\t\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tFix: db.Fix{\n\t\t\t\t\t\tVersions: []string{\"5.10.62-55.141.amzn2\"},\n\t\t\t\t\t\tState:    db.FixedState,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID:          \"ALASKERNEL-5.10-2022-005\",\n\t\t\t\t\tPackageName: \"kernel\",\n\t\t\t\t\tPackageQualifiers: []qualifier.Qualifier{rpmmodularity.Qualifier{\n\t\t\t\t\t\tKind:   \"rpm-modularity\",\n\t\t\t\t\t\tModule: \"\",\n\t\t\t\t\t}},\n\t\t\t\t\tVersionConstraint: \">= 5.10, < 5.10.62-55.141.amzn2\",\n\t\t\t\t\tVersionFormat:     \"rpm\",\n\t\t\t\t\tNamespace:         \"amazon:distro:amazonlinux:2\",\n\t\t\t\t\tRelatedVulnerabilities: []db.VulnerabilityReference{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tID:        \"CVE-2021-3753\",\n\t\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tID:        \"CVE-2021-40490\",\n\t\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tFix: db.Fix{\n\t\t\t\t\t\tVersions: []string{\"5.10.62-55.141.amzn2\"},\n\t\t\t\t\t\tState:    db.FixedState,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tf, err := os.Open(test.fixture)\n\t\t\trequire.NoError(t, err)\n\t\t\tt.Cleanup(func() {\n\t\t\t\tassert.NoError(t, f.Close())\n\t\t\t})\n\n\t\t\tentries, err := unmarshal.OSVulnerabilityEntries(f)\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Len(t, entries, test.numEntries)\n\n\t\t\tvar vulns []db.Vulnerability\n\t\t\tfor _, entry := range entries {\n\t\t\t\tdataEntries, err := Transform(entry)\n\t\t\t\tassert.NoError(t, err)\n\n\t\t\t\tfor _, entry := range dataEntries {\n\t\t\t\t\tswitch vuln := entry.Data.(type) {\n\t\t\t\t\tcase db.Vulnerability:\n\t\t\t\t\t\tvulns = append(vulns, vuln)\n\t\t\t\t\tcase db.VulnerabilityMetadata:\n\t\t\t\t\tdefault:\n\t\t\t\t\t\tt.Fatalf(\"unexpected condition: data entry does not have a vulnerability or a metadata: %+v\", vuln)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif diff := cmp.Diff(test.vulns, vulns); diff != \"\" {\n\t\t\t\tt.Errorf(\"vulnerabilities do not match (-want +got):\\n%s\", diff)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/db/v5/build/transformers/vulnerability_metadata.go",
    "content": "package transformers\n\n// VendorBaseMetrics captures extra metrics that do not fit into a common CVSS\n// struct, like Status and BaseSeverity\ntype VendorBaseMetrics struct {\n\tBaseSeverity string `json:\"base_severity\"`\n\tStatus       string `json:\"status\"`\n}\n"
  },
  {
    "path": "grype/db/v5/build/writer.go",
    "content": "package v5\n\nimport (\n\t\"crypto/sha256\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/spf13/afero\"\n\n\t\"github.com/anchore/grype/grype/db/data\"\n\t\"github.com/anchore/grype/grype/db/provider\"\n\tdb \"github.com/anchore/grype/grype/db/v5\"\n\t\"github.com/anchore/grype/grype/db/v5/distribution\"\n\t\"github.com/anchore/grype/grype/db/v5/store\"\n\t\"github.com/anchore/grype/internal/file\"\n\t\"github.com/anchore/grype/internal/log\"\n)\n\n// TODO: add NVDNamespace const to grype.db package?\nconst (\n\tnvdNamespace             = \"nvd:cpe\"\n\tproviderMetadataFileName = \"provider-metadata.json\"\n)\n\nvar _ data.Writer = (*writer)(nil)\n\ntype writer struct {\n\tdbPath string\n\tstore  db.Store\n\tstates provider.States\n\n\t// Batching infrastructure\n\tbatchSize   int\n\tbatchBuffer []func() error\n\tmu          sync.Mutex // Protect batch state\n\n\t// Metrics\n\ttotalBatches int\n}\n\ntype ProviderMetadata struct {\n\tProviders []Provider `json:\"providers\"`\n}\n\ntype Provider struct {\n\tName              string    `json:\"name\"`\n\tLastSuccessfulRun time.Time `json:\"lastSuccessfulRun\"`\n}\n\nfunc NewWriter(directory string, dataAge time.Time, states provider.States, batchSize int) (data.Writer, error) {\n\tdbPath := path.Join(directory, db.VulnerabilityStoreFileName)\n\ttheStore, err := store.New(dbPath, true)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create store: %w\", err)\n\t}\n\n\tif err := theStore.SetID(db.NewID(dataAge)); err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to set DB ID: %w\", err)\n\t}\n\n\t// Use default if not configured\n\tif batchSize == 0 {\n\t\tbatchSize = 2000\n\t}\n\n\treturn &writer{\n\t\tdbPath:      dbPath,\n\t\tstore:       theStore,\n\t\tstates:      states,\n\t\tbatchSize:   batchSize,\n\t\tbatchBuffer: make([]func() error, 0, batchSize),\n\t}, nil\n}\n\nfunc (w *writer) Write(entries ...data.Entry) error {\n\tlog.WithFields(\"records\", len(entries)).Trace(\"writing records to DB\")\n\tfor _, entry := range entries {\n\t\tif entry.DBSchemaVersion != db.SchemaVersion {\n\t\t\treturn fmt.Errorf(\"wrong schema version: want %+v got %+v\", db.SchemaVersion, entry.DBSchemaVersion)\n\t\t}\n\n\t\tswitch row := entry.Data.(type) {\n\t\tcase db.Vulnerability:\n\t\t\t// Batch the vulnerability write\n\t\t\tvuln := row\n\t\t\tif err := w.addToBatch(func() error {\n\t\t\t\treturn w.store.AddVulnerability(vuln)\n\t\t\t}); err != nil {\n\t\t\t\treturn fmt.Errorf(\"unable to batch vulnerability write: %w\", err)\n\t\t\t}\n\t\tcase db.VulnerabilityMetadata:\n\t\t\t// Normalize severity before batching\n\t\t\tnormalizeSeverity(&row, w.store)\n\t\t\tmetadata := row\n\t\t\tif err := w.addToBatch(func() error {\n\t\t\t\treturn w.store.AddVulnerabilityMetadata(metadata)\n\t\t\t}); err != nil {\n\t\t\t\treturn fmt.Errorf(\"unable to batch vulnerability metadata write: %w\", err)\n\t\t\t}\n\t\tcase db.VulnerabilityMatchExclusion:\n\t\t\t// Batch the exclusion write\n\t\t\texclusion := row\n\t\t\tif err := w.addToBatch(func() error {\n\t\t\t\treturn w.store.AddVulnerabilityMatchExclusion(exclusion)\n\t\t\t}); err != nil {\n\t\t\t\treturn fmt.Errorf(\"unable to batch vulnerability match exclusion write: %w\", err)\n\t\t\t}\n\t\tdefault:\n\t\t\treturn fmt.Errorf(\"data entry is not of type vulnerability, vulnerability metadata, or exclusion: %T\", row)\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// addToBatch adds an operation to the batch buffer and flushes if batch size is reached\nfunc (w *writer) addToBatch(op func() error) error {\n\tw.mu.Lock()\n\tdefer w.mu.Unlock()\n\n\tw.batchBuffer = append(w.batchBuffer, op)\n\n\tif len(w.batchBuffer) >= w.batchSize {\n\t\treturn w.flushUnlocked()\n\t}\n\treturn nil\n}\n\n// Flush executes all pending operations in the batch buffer\nfunc (w *writer) Flush() error {\n\tw.mu.Lock()\n\tdefer w.mu.Unlock()\n\treturn w.flushUnlocked()\n}\n\n// flushUnlocked executes all pending operations without acquiring the lock (must be called with lock held)\nfunc (w *writer) flushUnlocked() error {\n\tif len(w.batchBuffer) == 0 {\n\t\treturn nil\n\t}\n\n\tlog.WithFields(\n\t\t\"operations\", len(w.batchBuffer),\n\t\t\"batch_size\", w.batchSize,\n\t).Debug(\"flushing batch\")\n\n\tfor i, op := range w.batchBuffer {\n\t\tif err := op(); err != nil {\n\t\t\treturn fmt.Errorf(\"batch operation %d failed: %w\", i, err)\n\t\t}\n\t}\n\n\tw.batchBuffer = w.batchBuffer[:0]\n\tw.totalBatches++\n\treturn nil\n}\n\n// metadataAndClose closes the database and returns its metadata.\n// The reason this is a compound action is that getting the built time and\n// schema version from the database is an operation on the open database,\n// but the checksum must be computed after the database is compacted and closed.\nfunc (w *writer) metadataAndClose() (*distribution.Metadata, error) {\n\tstoreID, err := w.store.GetID()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to fetch store ID: %w\", err)\n\t}\n\tw.store.Close()\n\thashStr, err := file.HashFile(afero.NewOsFs(), w.dbPath, sha256.New())\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to hash database file (%s): %w\", w.dbPath, err)\n\t}\n\n\tmetadata := distribution.Metadata{\n\t\tBuilt:    storeID.BuildTimestamp,\n\t\tVersion:  storeID.SchemaVersion,\n\t\tChecksum: \"sha256:\" + hashStr,\n\t}\n\treturn &metadata, nil\n}\n\nfunc NewProviderMetadata() ProviderMetadata {\n\treturn ProviderMetadata{\n\t\tProviders: make([]Provider, 0),\n\t}\n}\n\nfunc (w *writer) ProviderMetadata() *ProviderMetadata {\n\tmetadata := NewProviderMetadata()\n\t// Set provider time from states\n\tfor _, state := range w.states {\n\t\tmetadata.Providers = append(metadata.Providers, Provider{\n\t\t\tName:              state.Provider,\n\t\t\tLastSuccessfulRun: state.Timestamp,\n\t\t})\n\t}\n\treturn &metadata\n}\n\nfunc (w *writer) Close() error {\n\t// Flush any remaining batched operations\n\tif err := w.Flush(); err != nil {\n\t\treturn fmt.Errorf(\"unable to flush pending writes: %w\", err)\n\t}\n\n\tmetadata, err := w.metadataAndClose()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tmetadataPath := path.Join(filepath.Dir(w.dbPath), distribution.MetadataFileName)\n\tif err = metadata.Write(metadataPath); err != nil {\n\t\treturn err\n\t}\n\n\tproviderMetadataPath := path.Join(filepath.Dir(w.dbPath), providerMetadataFileName)\n\tif err = w.ProviderMetadata().Write(providerMetadataPath); err != nil {\n\t\treturn err\n\t}\n\n\tlog.WithFields(\n\t\t\"path\", w.dbPath,\n\t\t\"total_batches\", w.totalBatches,\n\t).Info(\"database created\")\n\tlog.WithFields(\"path\", metadataPath).Debug(\"database metadata created\")\n\tlog.WithFields(\"path\", providerMetadataPath).Debug(\"provider metadata created\")\n\n\treturn nil\n}\n\nfunc normalizeSeverity(metadata *db.VulnerabilityMetadata, reader db.VulnerabilityMetadataStoreReader) {\n\tmetadata.Severity = string(data.ParseSeverity(metadata.Severity))\n\tif metadata.Severity != \"\" && strings.ToLower(metadata.Severity) != \"unknown\" {\n\t\treturn\n\t}\n\tif !strings.HasPrefix(strings.ToLower(metadata.ID), \"cve\") {\n\t\treturn\n\t}\n\tif strings.HasPrefix(metadata.Namespace, nvdNamespace) {\n\t\treturn\n\t}\n\tm, err := reader.GetVulnerabilityMetadata(metadata.ID, nvdNamespace)\n\tif err != nil {\n\t\tlog.WithFields(\"id\", metadata.ID, \"error\", err).Warn(\"error fetching vulnerability metadata from NVD namespace\")\n\t\treturn\n\t}\n\tif m == nil {\n\t\tlog.WithFields(\"id\", metadata.ID).Trace(\"unable to find vulnerability metadata from NVD namespace\")\n\t\treturn\n\t}\n\n\tnewSeverity := string(data.ParseSeverity(m.Severity))\n\tif newSeverity != metadata.Severity {\n\t\tlog.WithFields(\"id\", metadata.ID, \"namespace\", metadata.Namespace, \"sev-from\", metadata.Severity, \"sev-to\", newSeverity).Trace(\"overriding irrelevant severity with data from NVD record\")\n\t}\n\tmetadata.Severity = newSeverity\n}\n\nfunc (p ProviderMetadata) Write(path string) error {\n\tproviderMetadataJSON, err := json.MarshalIndent(p, \"\", \"  \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to marshal provider metadata: %w\", err)\n\t}\n\t//nolint:gosec\n\tif err = os.WriteFile(path, providerMetadataJSON, 0644); err != nil {\n\t\treturn fmt.Errorf(\"unable to write provider metadata: %w\", err)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "grype/db/v5/build/writer_test.go",
    "content": "package v5\n\nimport (\n\t\"errors\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\n\t\"github.com/anchore/grype/grype/db/data\"\n\tdb \"github.com/anchore/grype/grype/db/v5\"\n)\n\nvar _ db.VulnerabilityMetadataStoreReader = (*mockReader)(nil)\n\ntype mockReader struct {\n\tmetadata *db.VulnerabilityMetadata\n\terr      error\n}\n\nfunc newMockReader(sev string) *mockReader {\n\treturn &mockReader{\n\t\tmetadata: &db.VulnerabilityMetadata{\n\t\t\tSeverity:  sev,\n\t\t\tNamespace: \"nvd\",\n\t\t},\n\t}\n}\n\nfunc newDeadMockReader() *mockReader {\n\treturn &mockReader{\n\t\terr: errors.New(\"dead\"),\n\t}\n}\n\nfunc (m mockReader) GetVulnerabilityMetadata(_, _ string) (*db.VulnerabilityMetadata, error) {\n\treturn m.metadata, m.err\n}\n\nfunc (m mockReader) GetAllVulnerabilityMetadata() (*[]db.VulnerabilityMetadata, error) {\n\tpanic(\"implement me\")\n}\n\nfunc Test_normalizeSeverity(t *testing.T) {\n\n\ttests := []struct {\n\t\tname            string\n\t\tinitialSeverity string\n\t\tnamespace       string\n\t\tcveID           string\n\t\treader          db.VulnerabilityMetadataStoreReader\n\t\texpected        data.Severity\n\t}{\n\t\t{\n\t\t\tname:            \"missing severity set to Unknown\",\n\t\t\tinitialSeverity: \"\",\n\t\t\tnamespace:       \"test\",\n\t\t\treader:          &mockReader{},\n\t\t\texpected:        data.SeverityUnknown,\n\t\t},\n\t\t{\n\t\t\tname:            \"non-cve records metadata missing severity set to Unknown\",\n\t\t\tcveID:           \"GHSA-1234-1234-1234\",\n\t\t\tinitialSeverity: \"\",\n\t\t\tnamespace:       \"test\",\n\t\t\treader:          newDeadMockReader(), // should not be used\n\t\t\texpected:        data.SeverityUnknown,\n\t\t},\n\t\t{\n\t\t\tname:            \"non-cve records metadata with severity set should not be overriden\",\n\t\t\tcveID:           \"GHSA-1234-1234-1234\",\n\t\t\tinitialSeverity: \"high\",\n\t\t\tnamespace:       \"test\",\n\t\t\treader:          newMockReader(\"critical\"), // should not be used\n\t\t\texpected:        data.SeverityHigh,\n\t\t},\n\t\t{\n\t\t\tname:            \"override empty severity from NVD\",\n\t\t\tinitialSeverity: \"\",\n\t\t\tnamespace:       \"test\",\n\t\t\treader:          newMockReader(\"low\"),\n\t\t\texpected:        data.SeverityLow,\n\t\t},\n\t\t{\n\t\t\tname:            \"override unknown severity from NVD\",\n\t\t\tinitialSeverity: \"unknown\",\n\t\t\tnamespace:       \"test\",\n\t\t\treader:          newMockReader(\"low\"),\n\t\t\texpected:        data.SeverityLow,\n\t\t},\n\t\t{\n\t\t\tname:            \"ignore record with severity already set\",\n\t\t\tinitialSeverity: \"Low\",\n\t\t\tnamespace:       \"test\",\n\t\t\treader:          newMockReader(\"critical\"), // should not be used\n\t\t\texpected:        data.SeverityLow,\n\t\t},\n\t\t{\n\t\t\tname:            \"ignore nvd records\",\n\t\t\tinitialSeverity: \"Low\",\n\t\t\tnamespace:       \"nvdv2:cves\",\n\t\t\treader:          newDeadMockReader(), // should not be used\n\t\t\texpected:        data.SeverityLow,\n\t\t},\n\t\t{\n\t\t\tname:            \"db errors should not fail or modify the record other than normalizing unset value\",\n\t\t\tinitialSeverity: \"\",\n\t\t\tnamespace:       \"test\",\n\t\t\treader:          newDeadMockReader(),\n\t\t\texpected:        data.SeverityUnknown,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\trecord := &db.VulnerabilityMetadata{\n\t\t\t\tID:        \"cve-2020-0000\",\n\t\t\t\tSeverity:  tt.initialSeverity,\n\t\t\t\tNamespace: tt.namespace,\n\t\t\t}\n\t\t\tif tt.cveID != \"\" {\n\t\t\t\trecord.ID = tt.cveID\n\t\t\t}\n\t\t\tnormalizeSeverity(record, tt.reader)\n\t\t\tassert.Equal(t, string(tt.expected), record.Severity)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/db/v5/cvss.go",
    "content": "package v5\n\nimport (\n\t\"github.com/anchore/grype/grype/vulnerability\"\n)\n\nfunc NewCvss(m []Cvss) []vulnerability.Cvss {\n\t//nolint:prealloc\n\tvar cvss []vulnerability.Cvss\n\tfor _, score := range m {\n\t\tcvss = append(cvss, vulnerability.Cvss{\n\t\t\tSource:  score.Source,\n\t\t\tType:    score.Type,\n\t\t\tVersion: score.Version,\n\t\t\tVector:  score.Vector,\n\t\t\tMetrics: vulnerability.CvssMetrics{\n\t\t\t\tBaseScore:           score.Metrics.BaseScore,\n\t\t\t\tExploitabilityScore: score.Metrics.ExploitabilityScore,\n\t\t\t\tImpactScore:         score.Metrics.ImpactScore,\n\t\t\t},\n\t\t\tVendorMetadata: score.VendorMetadata,\n\t\t})\n\t}\n\treturn cvss\n}\n"
  },
  {
    "path": "grype/db/v5/diff.go",
    "content": "package v5\n\ntype DiffReason = string\n\nconst (\n\tDiffAdded   DiffReason = \"added\"\n\tDiffChanged DiffReason = \"changed\"\n\tDiffRemoved DiffReason = \"removed\"\n)\n\ntype Diff struct {\n\tReason    DiffReason `json:\"reason\"`\n\tID        string     `json:\"id\"`\n\tNamespace string     `json:\"namespace\"`\n\tPackages  []string   `json:\"packages\"`\n}\n"
  },
  {
    "path": "grype/db/v5/differ/differ.go",
    "content": "package differ\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/url\"\n\t\"path\"\n\n\t\"github.com/olekukonko/tablewriter\"\n\t\"github.com/olekukonko/tablewriter/renderer\"\n\t\"github.com/olekukonko/tablewriter/tw\"\n\t\"github.com/wagoodman/go-partybus\"\n\t\"github.com/wagoodman/go-progress\"\n\n\tv5 \"github.com/anchore/grype/grype/db/v5\"\n\tlegacyDistribution \"github.com/anchore/grype/grype/db/v5/distribution\"\n\t\"github.com/anchore/grype/grype/event\"\n\t\"github.com/anchore/grype/internal/bus\"\n\t\"github.com/anchore/grype/internal/log\"\n)\n\ntype Differ struct {\n\tbaseCurator   legacyDistribution.Curator\n\ttargetCurator legacyDistribution.Curator\n}\n\nfunc NewDiffer(config legacyDistribution.Config) (*Differ, error) {\n\tbaseCurator, err := legacyDistribution.NewCurator(legacyDistribution.Config{\n\t\tDBRootDir:           path.Join(config.DBRootDir, \"diff\", \"base\"),\n\t\tListingURL:          config.ListingURL,\n\t\tCACert:              config.CACert,\n\t\tValidateByHashOnGet: config.ValidateByHashOnGet,\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\ttargetCurator, err := legacyDistribution.NewCurator(legacyDistribution.Config{\n\t\tDBRootDir:           path.Join(config.DBRootDir, \"diff\", \"target\"),\n\t\tListingURL:          config.ListingURL,\n\t\tCACert:              config.CACert,\n\t\tValidateByHashOnGet: config.ValidateByHashOnGet,\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &Differ{\n\t\tbaseCurator:   baseCurator,\n\t\ttargetCurator: targetCurator,\n\t}, nil\n}\n\nfunc (d *Differ) SetBaseDB(base string) error {\n\treturn d.setOrDownload(&d.baseCurator, base)\n}\n\nfunc (d *Differ) SetTargetDB(target string) error {\n\treturn d.setOrDownload(&d.targetCurator, target)\n}\n\nfunc (d *Differ) setOrDownload(curator *legacyDistribution.Curator, filenameOrURL string) error {\n\tu, err := url.ParseRequestURI(filenameOrURL)\n\n\tif err != nil || u.Scheme == \"\" {\n\t\t*curator, err = legacyDistribution.NewCurator(legacyDistribution.Config{\n\t\t\tDBRootDir: filenameOrURL,\n\t\t})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t} else {\n\t\tlistings, err := d.baseCurator.ListingFromURL()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tavailable := listings.Available\n\t\tdbs := available[v5.SchemaVersion]\n\n\t\tvar listing *legacyDistribution.ListingEntry\n\n\t\tfor _, d := range dbs {\n\t\t\tdatabase := d\n\t\t\tif *d.URL == *u {\n\t\t\t\tlisting = &database\n\t\t\t}\n\t\t}\n\n\t\tif listing == nil {\n\t\t\treturn fmt.Errorf(\"unable to find listing for url: %s\", filenameOrURL)\n\t\t}\n\n\t\tif err := download(curator, listing); err != nil {\n\t\t\treturn fmt.Errorf(\"unable to download vulnerability database: %+v\", err)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc download(curator *legacyDistribution.Curator, listing *legacyDistribution.ListingEntry) error {\n\t// let consumers know of a monitorable event (download + import stages)\n\timportProgress := progress.NewManual(1)\n\tstage := progress.NewAtomicStage(\"checking available databases\")\n\tdownloadProgress := progress.NewManual(1)\n\taggregateProgress := progress.NewAggregator(progress.DefaultStrategy, downloadProgress, importProgress)\n\n\tbus.Publish(partybus.Event{\n\t\tType: event.UpdateVulnerabilityDatabase,\n\t\tValue: progress.StagedProgressable(&struct {\n\t\t\tprogress.Stager\n\t\t\tprogress.Progressable\n\t\t}{\n\t\t\tStager:       progress.Stager(stage),\n\t\t\tProgressable: progress.Progressable(aggregateProgress),\n\t\t}),\n\t})\n\n\tdefer downloadProgress.SetCompleted()\n\tdefer importProgress.SetCompleted()\n\n\treturn curator.UpdateTo(listing, downloadProgress, importProgress, stage)\n}\n\nfunc (d *Differ) DiffDatabases() (*[]v5.Diff, error) {\n\tbaseStore, err := d.baseCurator.GetStore()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tdefer log.CloseAndLogError(baseStore, d.baseCurator.Status().Location)\n\n\ttargetStore, err := d.targetCurator.GetStore()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tdefer log.CloseAndLogError(targetStore, d.targetCurator.Status().Location)\n\n\treturn baseStore.DiffStore(targetStore)\n}\n\nfunc (d *Differ) DeleteDatabases() error {\n\tif err := d.baseCurator.Delete(); err != nil {\n\t\treturn fmt.Errorf(\"unable to delete vulnerability database: %+v\", err)\n\t}\n\tif err := d.targetCurator.Delete(); err != nil {\n\t\treturn fmt.Errorf(\"unable to delete vulnerability database: %+v\", err)\n\t}\n\treturn nil\n}\n\nfunc (d *Differ) Present(outputFormat string, diff *[]v5.Diff, output io.Writer) error {\n\tif diff == nil {\n\t\treturn nil\n\t}\n\n\tswitch outputFormat {\n\tcase \"table\":\n\t\trows := [][]string{}\n\t\tfor _, d := range *diff {\n\t\t\trows = append(rows, []string{d.ID, d.Namespace, d.Reason})\n\t\t}\n\n\t\ttable := newTable(output, []string{\"ID\", \"Namespace\", \"Reason\"})\n\n\t\tif err := table.Bulk(rows); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to add table rows: %+v\", err)\n\t\t}\n\t\treturn table.Render()\n\tcase \"json\":\n\t\tenc := json.NewEncoder(output)\n\t\tenc.SetEscapeHTML(false)\n\t\tenc.SetIndent(\"\", \" \")\n\t\tif err := enc.Encode(*diff); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to encode diff information: %+v\", err)\n\t\t}\n\tdefault:\n\t\treturn fmt.Errorf(\"unsupported output format: %s\", outputFormat)\n\t}\n\treturn nil\n}\n\nfunc newTable(output io.Writer, columns []string) *tablewriter.Table {\n\treturn tablewriter.NewTable(output,\n\t\ttablewriter.WithHeader(columns),\n\t\ttablewriter.WithHeaderAlignment(tw.AlignLeft),\n\t\ttablewriter.WithHeaderAutoWrap(tw.WrapNone),\n\t\ttablewriter.WithRowAutoWrap(tw.WrapNone),\n\t\ttablewriter.WithAutoHide(tw.On),\n\t\ttablewriter.WithRenderer(renderer.NewBlueprint()),\n\t\ttablewriter.WithBehavior(\n\t\t\ttw.Behavior{\n\t\t\t\tTrimSpace: tw.On,\n\t\t\t\tAutoHide:  tw.On,\n\t\t\t},\n\t\t),\n\t\ttablewriter.WithPadding(\n\t\t\ttw.Padding{\n\t\t\t\tRight: \"  \",\n\t\t\t},\n\t\t),\n\t\ttablewriter.WithRendition(\n\t\t\ttw.Rendition{\n\t\t\t\tSymbols: tw.NewSymbols(tw.StyleNone),\n\t\t\t\tSettings: tw.Settings{\n\t\t\t\t\tLines: tw.Lines{\n\t\t\t\t\t\tShowTop:        tw.Off,\n\t\t\t\t\t\tShowBottom:     tw.Off,\n\t\t\t\t\t\tShowHeaderLine: tw.Off,\n\t\t\t\t\t\tShowFooterLine: tw.Off,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t),\n\t)\n}\n"
  },
  {
    "path": "grype/db/v5/differ/differ_test.go",
    "content": "package differ\n\nimport (\n\t\"bytes\"\n\t\"flag\"\n\t\"strconv\"\n\t\"testing\"\n\n\t\"github.com/sergi/go-diff/diffmatchpatch\"\n\t\"github.com/stretchr/testify/require\"\n\n\tv5 \"github.com/anchore/grype/grype/db/v5\"\n\t\"github.com/anchore/grype/grype/db/v5/distribution\"\n\t\"github.com/anchore/grype/internal/testutils\"\n)\n\nvar update = flag.Bool(\"update\", false, \"update the *.golden files for diff presenter\")\n\nfunc TestNewDiffer(t *testing.T) {\n\t//GIVEN\n\tconfig := distribution.Config{}\n\n\t//WHEN\n\tdiffer, err := NewDiffer(config)\n\n\t//THEN\n\trequire.NoError(t, err)\n\trequire.NotNil(t, differ.baseCurator)\n}\n\nfunc Test_DifferDirectory(t *testing.T) {\n\td, err := NewDiffer(distribution.Config{\n\t\tDBRootDir: \"root-dir\",\n\t})\n\trequire.NoError(t, err)\n\n\terr = d.SetBaseDB(\"testdata/dbs/base\")\n\trequire.NoError(t, err)\n\n\tbaseStatus := d.baseCurator.Status()\n\trequire.Equal(t, \"testdata/dbs/base/\"+strconv.Itoa(v5.SchemaVersion), baseStatus.Location)\n\n\terr = d.SetTargetDB(\"testdata/dbs/target\")\n\trequire.NoError(t, err)\n\n\ttargetStatus := d.targetCurator.Status()\n\trequire.Equal(t, \"testdata/dbs/target/\"+strconv.Itoa(v5.SchemaVersion), targetStatus.Location)\n}\n\nfunc TestPresent_Json(t *testing.T) {\n\t//GIVEN\n\tdiffs := []v5.Diff{\n\t\t{v5.DiffAdded, \"CVE-1\", \"nvd\", []string{\"requests\", \"vault\"}},\n\t\t{v5.DiffRemoved, \"CVE-2\", \"nvd\", []string{\"k8s\"}},\n\t\t{v5.DiffChanged, \"CVE-3\", \"nvd\", []string{}},\n\t}\n\tdiffer := Differ{}\n\tvar buffer bytes.Buffer\n\n\t// WHEN\n\trequire.NoError(t, differ.Present(\"json\", &diffs, &buffer))\n\n\t//THEN\n\tactual := buffer.Bytes()\n\tif *update {\n\t\ttestutils.UpdateGoldenFileContents(t, actual)\n\t}\n\tvar expected = testutils.GetGoldenFileContents(t)\n\tif !bytes.Equal(expected, actual) {\n\t\tdmp := diffmatchpatch.New()\n\t\tdiffs := dmp.DiffMain(string(expected), string(actual), true)\n\t\tt.Errorf(\"mismatched output:\\n%s\", dmp.DiffPrettyText(diffs))\n\t}\n}\n\nfunc TestPresent_Table(t *testing.T) {\n\t//GIVEN\n\tdiffs := []v5.Diff{\n\t\t{v5.DiffAdded, \"CVE-1\", \"nvd\", []string{\"requests\", \"vault\"}},\n\t\t{v5.DiffRemoved, \"CVE-2\", \"nvd\", []string{\"k8s\"}},\n\t\t{v5.DiffChanged, \"CVE-3\", \"nvd\", []string{}},\n\t}\n\tdiffer := Differ{}\n\tvar buffer bytes.Buffer\n\n\t// WHEN\n\trequire.NoError(t, differ.Present(\"table\", &diffs, &buffer))\n\n\t//THEN\n\tactual := buffer.Bytes()\n\tif *update {\n\t\ttestutils.UpdateGoldenFileContents(t, actual)\n\t}\n\tvar expected = testutils.GetGoldenFileContents(t)\n\tif !bytes.Equal(expected, actual) {\n\t\tdmp := diffmatchpatch.New()\n\t\tdiffs := dmp.DiffMain(string(expected), string(actual), true)\n\t\tt.Errorf(\"mismatched output:\\n%s\", dmp.DiffPrettyText(diffs))\n\t}\n}\n\nfunc TestPresent_Invalid(t *testing.T) {\n\t//GIVEN\n\tdiffs := []v5.Diff{\n\t\t{v5.DiffRemoved, \"CVE-2\", \"nvd\", []string{\"k8s\"}},\n\t}\n\tdiffer := Differ{}\n\tvar buffer bytes.Buffer\n\n\t// WHEN\n\terr := differ.Present(\"\", &diffs, &buffer)\n\n\t//THEN\n\trequire.Error(t, err)\n}\n"
  },
  {
    "path": "grype/db/v5/differ/testdata/dbs/base/5/metadata.json",
    "content": "{\n \"built\": \"2022-12-18T08:18:18Z\",\n \"version\": 5,\n \"checksum\": \"sha256:9d979f7320c575e7ac41c4384e9f55d578cdddd701822563cabc6b913fde7b80\"\n}"
  },
  {
    "path": "grype/db/v5/differ/testdata/dbs/target/5/metadata.json",
    "content": "{\n \"built\": \"2022-12-18T08:18:18Z\",\n \"version\": 5,\n \"checksum\": \"sha256:da2141ae7415abe5cf5390faeed60e164c5a919370ee823c8cc3ad9f4698f56e\"\n}"
  },
  {
    "path": "grype/db/v5/differ/testdata/snapshot/TestPresent_Json.golden",
    "content": "[\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-1\",\n  \"namespace\": \"nvd\",\n  \"packages\": [\n   \"requests\",\n   \"vault\"\n  ]\n },\n {\n  \"reason\": \"removed\",\n  \"id\": \"CVE-2\",\n  \"namespace\": \"nvd\",\n  \"packages\": [\n   \"k8s\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-3\",\n  \"namespace\": \"nvd\",\n  \"packages\": []\n }\n]\n"
  },
  {
    "path": "grype/db/v5/differ/testdata/snapshot/TestPresent_Table.golden",
    "content": "ID     NAMESPACE  REASON   \nCVE-1  nvd        added    \nCVE-2  nvd        removed  \nCVE-3  nvd        changed  \n"
  },
  {
    "path": "grype/db/v5/distribution/curator.go",
    "content": "package distribution\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/hako/durafmt\"\n\t\"github.com/hashicorp/go-cleanhttp\"\n\t\"github.com/mholt/archives\"\n\t\"github.com/spf13/afero\"\n\t\"github.com/wagoodman/go-partybus\"\n\t\"github.com/wagoodman/go-progress\"\n\n\t\"github.com/anchore/clio\"\n\tv5 \"github.com/anchore/grype/grype/db/v5\"\n\t\"github.com/anchore/grype/grype/db/v5/store\"\n\t\"github.com/anchore/grype/grype/event\"\n\t\"github.com/anchore/grype/internal/bus\"\n\t\"github.com/anchore/grype/internal/file\"\n\t\"github.com/anchore/grype/internal/log\"\n)\n\nconst (\n\tFileName                = v5.VulnerabilityStoreFileName\n\tlastUpdateCheckFileName = \"last_update_check\"\n)\n\ntype Config struct {\n\tID                      clio.Identification\n\tDBRootDir               string\n\tListingURL              string\n\tCACert                  string\n\tValidateByHashOnGet     bool\n\tValidateAge             bool\n\tMaxAllowedBuiltAge      time.Duration\n\tRequireUpdateCheck      bool\n\tListingFileTimeout      time.Duration\n\tUpdateTimeout           time.Duration\n\tUpdateCheckMaxFrequency time.Duration\n}\n\ntype Curator struct {\n\tfs                      afero.Fs\n\tlistingDownloader       file.Getter\n\tupdateDownloader        file.Getter\n\ttargetSchema            int\n\tdbDir                   string\n\tdbPath                  string\n\tlistingURL              string\n\tvalidateByHashOnGet     bool\n\tvalidateAge             bool\n\tmaxAllowedBuiltAge      time.Duration\n\trequireUpdateCheck      bool\n\tupdateCheckMaxFrequency time.Duration\n}\n\nfunc NewCurator(cfg Config) (Curator, error) {\n\tdbDir := path.Join(cfg.DBRootDir, strconv.Itoa(v5.SchemaVersion))\n\n\tfs := afero.NewOsFs()\n\tlistingClient, err := defaultHTTPClient(fs, cfg.CACert)\n\tif err != nil {\n\t\treturn Curator{}, err\n\t}\n\tlistingClient.Timeout = cfg.ListingFileTimeout\n\n\tdbClient, err := defaultHTTPClient(fs, cfg.CACert)\n\tif err != nil {\n\t\treturn Curator{}, err\n\t}\n\tdbClient.Timeout = cfg.UpdateTimeout\n\n\treturn Curator{\n\t\tfs:                      fs,\n\t\ttargetSchema:            v5.SchemaVersion,\n\t\tlistingDownloader:       file.NewGetter(cfg.ID, listingClient),\n\t\tupdateDownloader:        file.NewGetter(cfg.ID, dbClient),\n\t\tdbDir:                   dbDir,\n\t\tdbPath:                  path.Join(dbDir, FileName),\n\t\tlistingURL:              cfg.ListingURL,\n\t\tvalidateByHashOnGet:     cfg.ValidateByHashOnGet,\n\t\tvalidateAge:             cfg.ValidateAge,\n\t\tmaxAllowedBuiltAge:      cfg.MaxAllowedBuiltAge,\n\t\trequireUpdateCheck:      cfg.RequireUpdateCheck,\n\t\tupdateCheckMaxFrequency: cfg.UpdateCheckMaxFrequency,\n\t}, nil\n}\n\nfunc (c Curator) SupportedSchema() int {\n\treturn c.targetSchema\n}\n\nfunc (c *Curator) GetStore() (v5.StoreReader, error) {\n\t// ensure the DB is ok\n\t_, err := c.validateIntegrity(c.dbDir)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"vulnerability database is invalid (run db update to correct): %+v\", err)\n\t}\n\n\treturn store.New(c.dbPath, false)\n}\n\nfunc (c *Curator) Status() Status {\n\tmetadata, err := NewMetadataFromDir(c.fs, c.dbDir)\n\tif err != nil {\n\t\treturn Status{\n\t\t\tErr: fmt.Errorf(\"failed to parse database metadata (%s): %w\", c.dbDir, err),\n\t\t}\n\t}\n\tif metadata == nil {\n\t\treturn Status{\n\t\t\tErr: fmt.Errorf(\"database metadata not found at %q\", c.dbDir),\n\t\t}\n\t}\n\n\treturn Status{\n\t\tBuilt:         metadata.Built,\n\t\tSchemaVersion: metadata.Version,\n\t\tLocation:      c.dbDir,\n\t\tChecksum:      metadata.Checksum,\n\t\tErr:           nil,\n\t}\n}\n\n// Delete removes the DB and metadata file for this specific schema.\nfunc (c *Curator) Delete() error {\n\treturn c.fs.RemoveAll(c.dbDir)\n}\n\n// Update the existing DB, returning an indication if any action was taken.\nfunc (c *Curator) Update() (bool, error) { // nolint: funlen\n\tif !c.isUpdateCheckAllowed() {\n\t\t// we should not notify the user of an update check if the current configuration and state\n\t\t// indicates we're should be in a low-pass filter mode (and the check frequency is too high).\n\t\t// this should appear to the user as if we never attempted to check for an update at all.\n\t\treturn false, nil\n\t}\n\n\t// let consumers know of a monitorable event (download + import stages)\n\timportProgress := progress.NewManual(1)\n\tstage := progress.NewAtomicStage(\"checking for update\")\n\tdownloadProgress := progress.NewManual(1)\n\taggregateProgress := progress.NewAggregator(progress.DefaultStrategy, downloadProgress, importProgress)\n\n\tbus.Publish(partybus.Event{\n\t\tType: event.UpdateVulnerabilityDatabase,\n\t\tValue: progress.StagedProgressable(&struct {\n\t\t\tprogress.Stager\n\t\t\tprogress.Progressable\n\t\t}{\n\t\t\tStager:       progress.Stager(stage),\n\t\t\tProgressable: progress.Progressable(aggregateProgress),\n\t\t}),\n\t})\n\n\tdefer downloadProgress.SetCompleted()\n\tdefer importProgress.SetCompleted()\n\n\tupdateAvailable, metadata, updateEntry, checkErr := c.IsUpdateAvailable()\n\tif checkErr != nil {\n\t\tif c.requireUpdateCheck {\n\t\t\treturn false, fmt.Errorf(\"check for vulnerability database update failed: %w\", checkErr)\n\t\t}\n\t\tlog.Warnf(\"unable to check for vulnerability database update\")\n\t\tlog.Debugf(\"check for vulnerability update failed: %+v\", checkErr)\n\t}\n\n\tif updateAvailable {\n\t\tlog.Infof(\"downloading new vulnerability DB\")\n\t\terr := c.UpdateTo(updateEntry, downloadProgress, importProgress, stage)\n\t\tif err != nil {\n\t\t\treturn false, fmt.Errorf(\"unable to update vulnerability database: %w\", err)\n\t\t}\n\n\t\t// only set the last successful update check if the update was successful\n\t\tc.setLastSuccessfulUpdateCheck()\n\n\t\tif metadata != nil {\n\t\t\tlog.Infof(\n\t\t\t\t\"updated vulnerability DB from version=%d built=%q to version=%d built=%q\",\n\t\t\t\tmetadata.Version,\n\t\t\t\tmetadata.Built.String(),\n\t\t\t\tupdateEntry.Version,\n\t\t\t\tupdateEntry.Built.String(),\n\t\t\t)\n\t\t\treturn true, nil\n\t\t}\n\n\t\tlog.Infof(\n\t\t\t\"downloaded new vulnerability DB version=%d built=%q\",\n\t\t\tupdateEntry.Version,\n\t\t\tupdateEntry.Built.String(),\n\t\t)\n\t\treturn true, nil\n\t}\n\n\t// there was no update (or any issue while checking for an update)\n\tif checkErr == nil {\n\t\tc.setLastSuccessfulUpdateCheck()\n\t}\n\n\tstage.Set(\"no update available\")\n\treturn false, nil\n}\n\nfunc (c Curator) isUpdateCheckAllowed() bool {\n\tif c.updateCheckMaxFrequency == 0 {\n\t\tlog.Trace(\"no max-frequency set for update check\")\n\t\treturn true\n\t}\n\n\telapsed, err := c.durationSinceUpdateCheck()\n\tif err != nil {\n\t\t// we had an IO error (or similar) trying to read or parse the file, we should not block the update check.\n\t\tlog.WithFields(\"error\", err).Trace(\"unable to determine if update check is allowed\")\n\t\treturn true\n\t}\n\tif elapsed == nil {\n\t\t// there was no last check (this is a first run case), we should not block the update check.\n\t\treturn true\n\t}\n\n\treturn *elapsed > c.updateCheckMaxFrequency\n}\n\nfunc (c Curator) durationSinceUpdateCheck() (*time.Duration, error) {\n\t// open `$dbDir/last_update_check` file and read the timestamp and do now() - timestamp\n\n\tfilePath := path.Join(c.dbDir, lastUpdateCheckFileName)\n\n\tif _, err := c.fs.Stat(filePath); os.IsNotExist(err) {\n\t\tlog.Trace(\"first-run of DB update\")\n\t\treturn nil, nil\n\t}\n\n\tfh, err := c.fs.OpenFile(filePath, os.O_RDONLY, 0)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to read last update check timestamp: %w\", err)\n\t}\n\n\tdefer fh.Close()\n\n\t// read and parse rfc3339 timestamp\n\tvar lastCheckStr string\n\t_, err = fmt.Fscanf(fh, \"%s\", &lastCheckStr)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to read last update check timestamp: %w\", err)\n\t}\n\n\tlastCheck, err := time.Parse(time.RFC3339, lastCheckStr)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to parse last update check timestamp: %w\", err)\n\t}\n\n\tif lastCheck.IsZero() {\n\t\treturn nil, fmt.Errorf(\"empty update check timestamp\")\n\t}\n\n\telapsed := time.Since(lastCheck)\n\treturn &elapsed, nil\n}\n\nfunc (c Curator) setLastSuccessfulUpdateCheck() {\n\t// note: we should always assume the DB dir actually exists, otherwise let this operation fail (since having a DB\n\t// is a prerequisite for a successful update).\n\n\tfilePath := path.Join(c.dbDir, lastUpdateCheckFileName)\n\tfh, err := c.fs.OpenFile(filePath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644)\n\tif err != nil {\n\t\tlog.WithFields(\"error\", err).Trace(\"unable to write last update check timestamp\")\n\t\treturn\n\t}\n\n\tdefer fh.Close()\n\n\t_, _ = fmt.Fprintf(fh, \"%s\", time.Now().UTC().Format(time.RFC3339))\n}\n\n// IsUpdateAvailable indicates if there is a new update available as a boolean, and returns the latest listing information\n// available for this schema.\nfunc (c *Curator) IsUpdateAvailable() (bool, *Metadata, *ListingEntry, error) {\n\tlog.Debugf(\"checking for available database updates\")\n\n\tlisting, err := c.ListingFromURL()\n\tif err != nil {\n\t\treturn false, nil, nil, err\n\t}\n\n\tupdateEntry := listing.BestUpdate(c.targetSchema)\n\tif updateEntry == nil {\n\t\treturn false, nil, nil, fmt.Errorf(\"no db candidates with correct version available (maybe there is an application update available?)\")\n\t}\n\tlog.Debugf(\"found database update candidate: %s\", updateEntry)\n\n\t// compare created data to current db date\n\tcurrent, err := NewMetadataFromDir(c.fs, c.dbDir)\n\tif err != nil {\n\t\treturn false, nil, nil, fmt.Errorf(\"current metadata corrupt: %w\", err)\n\t}\n\n\tif current.IsSupersededBy(updateEntry) {\n\t\tlog.Debugf(\"database update available: %s\", updateEntry)\n\t\treturn true, current, updateEntry, nil\n\t}\n\tlog.Debugf(\"no database update available\")\n\n\treturn false, nil, nil, nil\n}\n\n// UpdateTo updates the existing DB with the specific other version provided from a listing entry.\nfunc (c *Curator) UpdateTo(listing *ListingEntry, downloadProgress, importProgress *progress.Manual, stage *progress.AtomicStage) error {\n\tstage.Set(\"downloading\")\n\t// note: the temp directory is persisted upon download/validation/activation failure to allow for investigation\n\ttempDir, err := c.download(listing, downloadProgress)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tstage.Set(\"validating integrity\")\n\t_, err = c.validateIntegrity(tempDir)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tstage.Set(\"importing\")\n\terr = c.activate(tempDir)\n\tif err != nil {\n\t\treturn err\n\t}\n\tstage.Set(\"updated\")\n\timportProgress.Set(importProgress.Size())\n\timportProgress.SetCompleted()\n\n\treturn c.fs.RemoveAll(tempDir)\n}\n\n// Validate checks the current database to ensure file integrity and if it can be used by this version of the application.\nfunc (c *Curator) Validate() error {\n\tmetadata, err := c.validateIntegrity(c.dbDir)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn c.validateStaleness(metadata)\n}\n\n// ImportFrom takes a DB archive file and imports it into the final DB location.\nfunc (c *Curator) ImportFrom(dbArchivePath string) error {\n\t// note: the temp directory is persisted upon download/validation/activation failure to allow for investigation\n\ttempDir, err := os.MkdirTemp(\"\", \"grype-import\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to create db temp dir: %w\", err)\n\t}\n\n\terr = unarchive(dbArchivePath, tempDir)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t_, err = c.validateIntegrity(tempDir)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = c.activate(tempDir)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn c.fs.RemoveAll(tempDir)\n}\n\nfunc (c *Curator) download(listing *ListingEntry, downloadProgress *progress.Manual) (string, error) {\n\ttempDir, err := os.MkdirTemp(\"\", \"grype-scratch\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"unable to create db temp dir: %w\", err)\n\t}\n\n\t// download the db to the temp dir\n\turl := listing.URL\n\n\t// from go-getter, adding a checksum as a query string will validate the payload after download\n\t// note: the checksum query parameter is not sent to the server\n\tquery := url.Query()\n\tquery.Add(\"checksum\", listing.Checksum)\n\turl.RawQuery = query.Encode()\n\n\t// go-getter will automatically extract all files within the archive to the temp dir\n\terr = c.updateDownloader.GetToDir(tempDir, listing.URL.String(), downloadProgress)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"unable to download db: %w\", err)\n\t}\n\n\treturn tempDir, nil\n}\n\n// validateStaleness ensures the vulnerability database has not passed\n// the max allowed age, calculated from the time it was built until now.\nfunc (c *Curator) validateStaleness(m Metadata) error {\n\tif !c.validateAge {\n\t\treturn nil\n\t}\n\n\t// built time is defined in UTC,\n\t// we should compare it against UTC\n\tnow := time.Now().UTC()\n\n\tage := now.Sub(m.Built)\n\tif age > c.maxAllowedBuiltAge {\n\t\treturn fmt.Errorf(\"the vulnerability database was built %s ago (max allowed age is %s)\", durafmt.ParseShort(age), durafmt.ParseShort(c.maxAllowedBuiltAge))\n\t}\n\n\treturn nil\n}\n\nfunc (c *Curator) validateIntegrity(dbDirPath string) (Metadata, error) {\n\t// check that the disk checksum still matches the db payload\n\tmetadata, err := NewMetadataFromDir(c.fs, dbDirPath)\n\tif err != nil {\n\t\treturn Metadata{}, fmt.Errorf(\"failed to parse database metadata (%s): %w\", dbDirPath, err)\n\t}\n\tif metadata == nil {\n\t\treturn Metadata{}, fmt.Errorf(\"database metadata not found: %s\", dbDirPath)\n\t}\n\n\tif c.validateByHashOnGet {\n\t\tdbPath := path.Join(dbDirPath, FileName)\n\t\tvalid, actualHash, err := file.ValidateByHash(c.fs, dbPath, metadata.Checksum)\n\t\tif err != nil {\n\t\t\treturn Metadata{}, err\n\t\t}\n\t\tif !valid {\n\t\t\treturn Metadata{}, fmt.Errorf(\"bad db checksum (%s): %q vs %q\", dbPath, metadata.Checksum, actualHash)\n\t\t}\n\t}\n\n\tif c.targetSchema != metadata.Version {\n\t\treturn Metadata{}, fmt.Errorf(\"unsupported database version: have=%d want=%d\", metadata.Version, c.targetSchema)\n\t}\n\n\t// TODO: add version checks here to ensure this version of the application can use this database version (relative to what the DB says, not JUST the metadata!)\n\n\treturn *metadata, nil\n}\n\n// activate swaps over the downloaded db to the application directory\nfunc (c *Curator) activate(dbDirPath string) error {\n\t_, err := c.fs.Stat(c.dbDir)\n\tif !os.IsNotExist(err) {\n\t\t// remove any previous databases\n\t\terr = c.Delete()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to purge existing database: %w\", err)\n\t\t}\n\t}\n\n\t// ensure there is an application db directory\n\terr = c.fs.MkdirAll(c.dbDir, 0755)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create db directory: %w\", err)\n\t}\n\n\t// activate the new db cache\n\treturn file.CopyDir(c.fs, dbDirPath, c.dbDir)\n}\n\n// ListingFromURL loads a Listing from a URL.\nfunc (c Curator) ListingFromURL() (Listing, error) {\n\ttempFile, err := afero.TempFile(c.fs, \"\", \"grype-db-listing\")\n\tif err != nil {\n\t\treturn Listing{}, fmt.Errorf(\"unable to create listing temp file: %w\", err)\n\t}\n\tdefer func() {\n\t\tlog.CloseAndLogError(tempFile, tempFile.Name())\n\t\terr := c.fs.RemoveAll(tempFile.Name())\n\t\tif err != nil {\n\t\t\tlog.Errorf(\"failed to remove file (%s): %w\", tempFile.Name(), err)\n\t\t}\n\t}()\n\n\t// download the listing file\n\terr = c.listingDownloader.GetFile(tempFile.Name(), c.listingURL)\n\tif err != nil {\n\t\treturn Listing{}, fmt.Errorf(\"unable to download listing: %w\", err)\n\t}\n\n\t// parse the listing file\n\tlisting, err := NewListingFromFile(c.fs, tempFile.Name())\n\tif err != nil {\n\t\treturn Listing{}, err\n\t}\n\treturn listing, nil\n}\n\nfunc defaultHTTPClient(fs afero.Fs, caCertPath string) (*http.Client, error) {\n\thttpClient := cleanhttp.DefaultClient()\n\thttpClient.Timeout = 30 * time.Second\n\tif caCertPath != \"\" {\n\t\trootCAs := x509.NewCertPool()\n\n\t\tpemBytes, err := afero.ReadFile(fs, caCertPath)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"unable to configure root CAs for curator: %w\", err)\n\t\t}\n\t\trootCAs.AppendCertsFromPEM(pemBytes)\n\n\t\thttpClient.Transport.(*http.Transport).TLSClientConfig = &tls.Config{\n\t\t\tMinVersion: tls.VersionTLS12,\n\t\t\tRootCAs:    rootCAs,\n\t\t}\n\t}\n\treturn httpClient, nil\n}\n\nfunc unarchive(source, destination string) error {\n\tsourceFile, err := os.Open(source)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer sourceFile.Close()\n\n\tformat, stream, err := archives.Identify(context.Background(), source, sourceFile)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\textractor, ok := format.(archives.Extractor)\n\tif !ok {\n\t\treturn fmt.Errorf(\"unable to extract DB file, format not supported: %s\", source)\n\t}\n\n\troot, err := os.OpenRoot(destination)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvisitor := func(_ context.Context, file archives.FileInfo) error {\n\t\tif file.IsDir() || file.LinkTarget != \"\" {\n\t\t\treturn nil\n\t\t}\n\n\t\tfileReader, err := file.Open()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tdefer fileReader.Close()\n\n\t\tfilename := filepath.Clean(file.NameInArchive)\n\n\t\toutputFile, err := root.Create(filename)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tdefer outputFile.Close()\n\n\t\t_, err = io.Copy(outputFile, fileReader)\n\n\t\treturn err\n\t}\n\n\treturn extractor.Extract(context.Background(), stream, visitor)\n}\n"
  },
  {
    "path": "grype/db/v5/distribution/curator_test.go",
    "content": "package distribution\n\nimport (\n\t\"archive/tar\"\n\t\"bufio\"\n\t\"bytes\"\n\t\"compress/gzip\"\n\t\"crypto/sha256\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n\t\"syscall\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/gookit/color\"\n\t\"github.com/mholt/archives\"\n\t\"github.com/spf13/afero\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/mock\"\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/wagoodman/go-progress\"\n\n\t\"github.com/anchore/grype/internal/file\"\n\t\"github.com/anchore/grype/internal/stringutil\"\n)\n\ntype testGetter struct {\n\tfile  map[string]string\n\tdir   map[string]string\n\tcalls stringutil.StringSet\n\tfs    afero.Fs\n}\n\nfunc newTestGetter(fs afero.Fs, f, d map[string]string) *testGetter {\n\treturn &testGetter{\n\t\tfile:  f,\n\t\tdir:   d,\n\t\tcalls: stringutil.NewStringSet(),\n\t\tfs:    fs,\n\t}\n}\n\n// GetFile downloads the give URL into the given path. The URL must reference a single file.\nfunc (g *testGetter) GetFile(dst, src string, _ ...*progress.Manual) error {\n\tg.calls.Add(src)\n\tif _, ok := g.file[src]; !ok {\n\t\treturn fmt.Errorf(\"blerg, no file!\")\n\t}\n\treturn afero.WriteFile(g.fs, dst, []byte(g.file[src]), 0755)\n}\n\n// Get downloads the given URL into the given directory. The directory must already exist.\nfunc (g *testGetter) GetToDir(dst, src string, _ ...*progress.Manual) error {\n\tg.calls.Add(src)\n\tif _, ok := g.dir[src]; !ok {\n\t\treturn fmt.Errorf(\"blerg, no file!\")\n\t}\n\treturn afero.WriteFile(g.fs, dst, []byte(g.dir[src]), 0755)\n}\n\nfunc newTestCurator(tb testing.TB, fs afero.Fs, getter file.Getter, dbDir, metadataUrl string, validateDbHash bool) Curator {\n\tc, err := NewCurator(Config{\n\t\tDBRootDir:           dbDir,\n\t\tListingURL:          metadataUrl,\n\t\tValidateByHashOnGet: validateDbHash,\n\t})\n\n\trequire.NoError(tb, err)\n\n\tc.listingDownloader = getter\n\tc.updateDownloader = getter\n\tc.fs = fs\n\n\treturn c\n}\n\nfunc Test_defaultHTTPClientHasCert(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\thasCert bool\n\t}{\n\t\t{\n\t\t\tname:    \"no custom cert should use default system root certs\",\n\t\t\thasCert: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"should use single custom cert\",\n\t\t\thasCert: true,\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tvar certPath string\n\t\t\tif test.hasCert {\n\t\t\t\tcertPath = generateCertFixture(t)\n\t\t\t}\n\n\t\t\thttpClient, err := defaultHTTPClient(afero.NewOsFs(), certPath)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tif test.hasCert {\n\t\t\t\trequire.NotNil(t, httpClient.Transport.(*http.Transport).TLSClientConfig)\n\t\t\t\tassert.Len(t, httpClient.Transport.(*http.Transport).TLSClientConfig.RootCAs.Subjects(), 1)\n\t\t\t} else {\n\t\t\t\tassert.Nil(t, httpClient.Transport.(*http.Transport).TLSClientConfig)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_defaultHTTPClientTimeout(t *testing.T) {\n\tc, err := defaultHTTPClient(afero.NewMemMapFs(), \"\")\n\trequire.NoError(t, err)\n\tassert.Equal(t, 30*time.Second, c.Timeout)\n}\n\nfunc generateCertFixture(t *testing.T) string {\n\tpath := \"testdata/tls/server.crt\"\n\tif _, err := os.Stat(path); !os.IsNotExist(err) {\n\t\t// fixture already exists...\n\t\treturn path\n\t}\n\n\tt.Log(color.Bold.Sprint(\"Generating Key/Cert Fixture\"))\n\n\tcwd, err := os.Getwd()\n\tif err != nil {\n\t\tt.Errorf(\"unable to get cwd: %+v\", err)\n\t}\n\n\tcmd := exec.Command(\"make\", \"server.crt\")\n\tcmd.Dir = filepath.Join(cwd, \"testdata/tls\")\n\n\tstderr, err := cmd.StderrPipe()\n\tif err != nil {\n\t\tt.Fatalf(\"could not get stderr: %+v\", err)\n\t}\n\tstdout, err := cmd.StdoutPipe()\n\tif err != nil {\n\t\tt.Fatalf(\"could not get stdout: %+v\", err)\n\t}\n\n\terr = cmd.Start()\n\tif err != nil {\n\t\tt.Fatalf(\"failed to start cmd: %+v\", err)\n\t}\n\n\tshow := func(label string, reader io.ReadCloser) {\n\t\tscanner := bufio.NewScanner(reader)\n\t\tscanner.Split(bufio.ScanLines)\n\t\tfor scanner.Scan() {\n\t\t\tt.Logf(\"%s: %s\", label, scanner.Text())\n\t\t}\n\t}\n\tgo show(\"out\", stdout)\n\tgo show(\"err\", stderr)\n\n\tif err := cmd.Wait(); err != nil {\n\t\tif exiterr, ok := err.(*exec.ExitError); ok {\n\t\t\t// The program has exited with an exit code != 0\n\n\t\t\t// This works on both Unix and Windows. Although package\n\t\t\t// syscall is generally platform dependent, WaitStatus is\n\t\t\t// defined for both Unix and Windows and in both cases has\n\t\t\t// an ExitStatus() method with the same signature.\n\t\t\tif status, ok := exiterr.Sys().(syscall.WaitStatus); ok {\n\t\t\t\tif status.ExitStatus() != 0 {\n\t\t\t\t\tt.Fatalf(\"failed to generate fixture: rc=%d\", status.ExitStatus())\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tt.Fatalf(\"unable to get generate fixture result: %+v\", err)\n\t\t}\n\t}\n\treturn path\n}\n\nfunc TestCuratorDownload(t *testing.T) {\n\ttests := []struct {\n\t\tname        string\n\t\tentry       *ListingEntry\n\t\texpectedURL string\n\t\terr         bool\n\t}{\n\t\t{\n\t\t\tname: \"download populates returned tempdir\",\n\t\t\tentry: &ListingEntry{\n\t\t\t\tBuilt:    time.Date(2020, 06, 13, 17, 13, 13, 0, time.UTC),\n\t\t\t\tURL:      mustUrl(url.Parse(\"http://a-url/payload.tar.gz\")),\n\t\t\t\tChecksum: \"sha256:deadbeefcafe\",\n\t\t\t},\n\t\t\texpectedURL: \"http://a-url/payload.tar.gz?checksum=sha256%3Adeadbeefcafe\",\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tmetadataUrl := \"http://metadata.io\"\n\t\t\tcontents := \"CONTENTS!!!\"\n\t\t\tfiles := map[string]string{}\n\t\t\tdirs := map[string]string{\n\t\t\t\ttest.expectedURL: contents,\n\t\t\t}\n\t\t\tfs := afero.NewMemMapFs()\n\t\t\tgetter := newTestGetter(fs, files, dirs)\n\t\t\tcur := newTestCurator(t, fs, getter, \"/tmp/dbdir\", metadataUrl, false)\n\n\t\t\tpath, err := cur.download(test.entry, &progress.Manual{})\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"could not download entry: %+v\", err)\n\t\t\t}\n\n\t\t\tif !getter.calls.Contains(test.expectedURL) {\n\t\t\t\tt.Fatalf(\"never made the appropriate fetch call: %+v\", getter.calls)\n\t\t\t}\n\n\t\t\tf, err := fs.Open(path)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"no db file: %+v\", err)\n\t\t\t}\n\n\t\t\tactual, err := afero.ReadAll(f)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"bad db file read: %+v\", err)\n\t\t\t}\n\n\t\t\tif string(actual) != contents {\n\t\t\t\tt.Fatalf(\"bad contents: %+v\", string(actual))\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestCuratorValidate(t *testing.T) {\n\ttests := []struct {\n\t\tname              string\n\t\tfixture           string\n\t\tconstraint        int\n\t\tcfgValidateDbHash bool\n\t\terr               bool\n\t}{\n\t\t{\n\t\t\tname:              \"good checksum & good constraint\",\n\t\t\tfixture:           \"testdata/curator-validate/good-checksum\",\n\t\t\tcfgValidateDbHash: true,\n\t\t\tconstraint:        1,\n\t\t\terr:               false,\n\t\t},\n\t\t{\n\t\t\tname:              \"good checksum & bad constraint\",\n\t\t\tfixture:           \"testdata/curator-validate/good-checksum\",\n\t\t\tcfgValidateDbHash: true,\n\t\t\tconstraint:        2,\n\t\t\terr:               true,\n\t\t},\n\t\t{\n\t\t\tname:              \"bad checksum & good constraint\",\n\t\t\tfixture:           \"testdata/curator-validate/bad-checksum\",\n\t\t\tcfgValidateDbHash: true,\n\t\t\tconstraint:        1,\n\t\t\terr:               true,\n\t\t},\n\t\t{\n\t\t\tname:              \"bad checksum & bad constraint\",\n\t\t\tfixture:           \"testdata/curator-validate/bad-checksum\",\n\t\t\tcfgValidateDbHash: true,\n\t\t\tconstraint:        2,\n\t\t\terr:               true,\n\t\t},\n\t\t{\n\t\t\tname:              \"bad checksum ignored on config exception\",\n\t\t\tfixture:           \"testdata/curator-validate/bad-checksum\",\n\t\t\tcfgValidateDbHash: false,\n\t\t\tconstraint:        1,\n\t\t\terr:               false,\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tmetadataUrl := \"http://metadata.io\"\n\n\t\t\tfs := afero.NewOsFs()\n\t\t\tgetter := newTestGetter(fs, nil, nil)\n\t\t\tcur := newTestCurator(t, fs, getter, \"/tmp/dbdir\", metadataUrl, test.cfgValidateDbHash)\n\n\t\t\tcur.targetSchema = test.constraint\n\n\t\t\tmd, err := cur.validateIntegrity(test.fixture)\n\n\t\t\tif err == nil && test.err {\n\t\t\t\tt.Errorf(\"expected an error but got none\")\n\t\t\t} else if err != nil && !test.err {\n\t\t\t\tassert.NotZero(t, md)\n\t\t\t\tt.Errorf(\"expected no error, got: %+v\", err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestCuratorDBPathHasSchemaVersion(t *testing.T) {\n\tfs := afero.NewMemMapFs()\n\tdbRootPath := \"/tmp/dbdir\"\n\tcur := newTestCurator(t, fs, nil, dbRootPath, \"http://metadata.io\", false)\n\n\tassert.Equal(t, path.Join(dbRootPath, strconv.Itoa(cur.targetSchema)), cur.dbDir, \"unexpected dir\")\n\tassert.Contains(t, cur.dbPath, path.Join(dbRootPath, strconv.Itoa(cur.targetSchema)), \"unexpected path\")\n}\n\nfunc TestCurator_validateStaleness(t *testing.T) {\n\ttype fields struct {\n\t\tvalidateAge     bool\n\t\tmaxAllowedDBAge time.Duration\n\t\tmd              Metadata\n\t}\n\n\tnow := time.Now().UTC()\n\ttests := []struct {\n\t\tname    string\n\t\tcur     *Curator\n\t\tfields  fields\n\t\twantErr assert.ErrorAssertionFunc\n\t}{\n\t\t{\n\t\t\tname: \"no-validation\",\n\t\t\tfields: fields{\n\t\t\t\tmd: Metadata{Built: now},\n\t\t\t},\n\t\t\twantErr: assert.NoError,\n\t\t},\n\t\t{\n\t\t\tname: \"up-to-date\",\n\t\t\tfields: fields{\n\t\t\t\tmaxAllowedDBAge: 2 * time.Hour,\n\t\t\t\tvalidateAge:     true,\n\t\t\t\tmd:              Metadata{Built: now},\n\t\t\t},\n\t\t\twantErr: assert.NoError,\n\t\t},\n\t\t{\n\t\t\tname: \"stale-data\",\n\t\t\tfields: fields{\n\t\t\t\tmaxAllowedDBAge: time.Hour,\n\t\t\t\tvalidateAge:     true,\n\t\t\t\tmd:              Metadata{Built: now.UTC().Add(-4 * time.Hour)},\n\t\t\t},\n\t\t\twantErr: func(t assert.TestingT, err error, i ...interface{}) bool {\n\t\t\t\treturn assert.ErrorContains(t, err, \"the vulnerability database was built\")\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"stale-data-no-validation\",\n\t\t\tfields: fields{\n\t\t\t\tmaxAllowedDBAge: time.Hour,\n\t\t\t\tvalidateAge:     false,\n\t\t\t\tmd:              Metadata{Built: now.Add(-4 * time.Hour)},\n\t\t\t},\n\t\t\twantErr: assert.NoError,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tc := &Curator{\n\t\t\t\tvalidateAge:        tt.fields.validateAge,\n\t\t\t\tmaxAllowedBuiltAge: tt.fields.maxAllowedDBAge,\n\t\t\t}\n\t\t\ttt.wantErr(t, c.validateStaleness(tt.fields.md), fmt.Sprintf(\"validateStaleness(%v)\", tt.fields.md))\n\t\t})\n\t}\n}\n\nfunc Test_requireUpdateCheck(t *testing.T) {\n\ttoJson := func(listing any) []byte {\n\t\tlistingContents := bytes.Buffer{}\n\t\tenc := json.NewEncoder(&listingContents)\n\t\t_ = enc.Encode(listing)\n\t\treturn listingContents.Bytes()\n\t}\n\tchecksum := func(b []byte) string {\n\t\th := sha256.New()\n\t\th.Write(b)\n\t\treturn hex.EncodeToString(h.Sum(nil))\n\t}\n\tmakeTarGz := func(mod time.Time, contents []byte) []byte {\n\t\tmetadata := toJson(MetadataJSON{\n\t\t\tBuilt:    mod.Format(time.RFC3339),\n\t\t\tVersion:  5,\n\t\t\tChecksum: \"sha256:\" + checksum(contents),\n\t\t})\n\t\ttgz := bytes.Buffer{}\n\t\tgz := gzip.NewWriter(&tgz)\n\t\tw := tar.NewWriter(gz)\n\t\t_ = w.WriteHeader(&tar.Header{\n\t\t\tName: \"metadata.json\",\n\t\t\tSize: int64(len(metadata)),\n\t\t\tMode: 0600,\n\t\t})\n\t\t_, _ = w.Write(metadata)\n\t\t_ = w.WriteHeader(&tar.Header{\n\t\t\tName: \"vulnerability.db\",\n\t\t\tSize: int64(len(contents)),\n\t\t\tMode: 0600,\n\t\t})\n\t\t_, _ = w.Write(contents)\n\t\t_ = w.Close()\n\t\t_ = gz.Close()\n\t\treturn tgz.Bytes()\n\t}\n\n\tnewTime := time.Date(2024, 06, 13, 17, 13, 13, 0, time.UTC)\n\tmidTime := time.Date(2022, 06, 13, 17, 13, 13, 0, time.UTC)\n\toldTime := time.Date(2020, 06, 13, 17, 13, 13, 0, time.UTC)\n\n\tnewDB := makeTarGz(newTime, []byte(\"some-good-contents\"))\n\n\tmidMetadata := toJson(MetadataJSON{\n\t\tBuilt:    midTime.Format(time.RFC3339),\n\t\tVersion:  5,\n\t\tChecksum: \"sha256:deadbeefcafe\",\n\t})\n\n\tvar handlerFunc http.HandlerFunc\n\n\tsrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\thandlerFunc(w, r)\n\t}))\n\tdefer srv.Close()\n\n\tnewDbURI := \"/db.tar.gz\"\n\n\tnewListing := toJson(Listing{Available: map[int][]ListingEntry{5: {ListingEntry{\n\t\tBuilt:    newTime,\n\t\tURL:      mustUrl(url.Parse(srv.URL + newDbURI)),\n\t\tChecksum: \"sha256:\" + checksum(newDB),\n\t}}}})\n\n\toldListing := toJson(Listing{Available: map[int][]ListingEntry{5: {ListingEntry{\n\t\tBuilt:    oldTime,\n\t\tURL:      mustUrl(url.Parse(srv.URL + newDbURI)),\n\t\tChecksum: \"sha256:\" + checksum(newDB),\n\t}}}})\n\n\tnewListingURI := \"/listing.json\"\n\toldListingURI := \"/oldlisting.json\"\n\tbadListingURI := \"/badlisting.json\"\n\n\thandlerFunc = func(response http.ResponseWriter, request *http.Request) {\n\t\tswitch request.RequestURI {\n\t\tcase newListingURI:\n\t\t\tresponse.WriteHeader(http.StatusOK)\n\t\t\t_, _ = response.Write(newListing)\n\t\tcase oldListingURI:\n\t\t\tresponse.WriteHeader(http.StatusOK)\n\t\t\t_, _ = response.Write(oldListing)\n\t\tcase newDbURI:\n\t\t\tresponse.WriteHeader(http.StatusOK)\n\t\t\t_, _ = response.Write(newDB)\n\t\tdefault:\n\t\t\thttp.Error(response, \"not found\", http.StatusNotFound)\n\t\t}\n\t}\n\n\ttests := []struct {\n\t\tname       string\n\t\tconfig     Config\n\t\tdbDir      map[string][]byte\n\t\twantResult bool\n\t\twantErr    require.ErrorAssertionFunc\n\t}{\n\t\t{\n\t\t\tname: \"listing with update\",\n\t\t\tconfig: Config{\n\t\t\t\tListingURL:         srv.URL + newListingURI,\n\t\t\t\tRequireUpdateCheck: true,\n\t\t\t},\n\t\t\tdbDir: map[string][]byte{\n\t\t\t\t\"5/metadata.json\": midMetadata,\n\t\t\t},\n\t\t\twantResult: true,\n\t\t\twantErr:    require.NoError,\n\t\t},\n\t\t{\n\t\t\tname: \"no update\",\n\t\t\tconfig: Config{\n\t\t\t\tListingURL:         srv.URL + oldListingURI,\n\t\t\t\tRequireUpdateCheck: false,\n\t\t\t},\n\t\t\tdbDir: map[string][]byte{\n\t\t\t\t\"5/metadata.json\": midMetadata,\n\t\t\t},\n\t\t\twantResult: false,\n\t\t\twantErr:    require.NoError,\n\t\t},\n\t\t{\n\t\t\tname: \"update error fail\",\n\t\t\tconfig: Config{\n\t\t\t\tListingURL:         srv.URL + badListingURI,\n\t\t\t\tRequireUpdateCheck: true,\n\t\t\t},\n\t\t\twantResult: false,\n\t\t\twantErr:    require.Error,\n\t\t},\n\t\t{\n\t\t\tname: \"update error continue\",\n\t\t\tconfig: Config{\n\t\t\t\tListingURL:         srv.URL + badListingURI,\n\t\t\t\tRequireUpdateCheck: false,\n\t\t\t},\n\t\t\twantResult: false,\n\t\t\twantErr:    require.NoError,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tdbTmpDir := t.TempDir()\n\t\t\ttt.config.DBRootDir = dbTmpDir\n\t\t\ttt.config.ListingFileTimeout = 1 * time.Minute\n\t\t\ttt.config.UpdateTimeout = 1 * time.Minute\n\t\t\tfor filePath, contents := range tt.dbDir {\n\t\t\t\tfullPath := filepath.Join(dbTmpDir, filepath.FromSlash(filePath))\n\t\t\t\terr := os.MkdirAll(filepath.Dir(fullPath), 0700|os.ModeDir)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\terr = os.WriteFile(fullPath, contents, 0700)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\t\t\tc, err := NewCurator(tt.config)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tresult, err := c.Update()\n\t\t\trequire.Equal(t, tt.wantResult, result)\n\t\t\ttt.wantErr(t, err)\n\t\t})\n\t}\n}\n\nfunc TestCuratorTimeoutBehavior(t *testing.T) {\n\tfailAfter := 10 * time.Second\n\tsuccess := make(chan struct{})\n\terrs := make(chan error)\n\ttimeout := time.After(failAfter)\n\n\thangForeverHandler := func(w http.ResponseWriter, r *http.Request) {\n\t\tselect {} // hang forever\n\t}\n\n\tts := httptest.NewServer(http.HandlerFunc(hangForeverHandler))\n\n\tcfg := Config{\n\t\tDBRootDir:           \"\",\n\t\tListingURL:          fmt.Sprintf(\"%s/listing.json\", ts.URL),\n\t\tCACert:              \"\",\n\t\tValidateByHashOnGet: false,\n\t\tValidateAge:         false,\n\t\tMaxAllowedBuiltAge:  0,\n\t\tListingFileTimeout:  400 * time.Millisecond,\n\t\tUpdateTimeout:       400 * time.Millisecond,\n\t}\n\n\tcurator, err := NewCurator(cfg)\n\trequire.NoError(t, err)\n\n\tu, err := url.Parse(fmt.Sprintf(\"%s/some-db.tar.gz\", ts.URL))\n\trequire.NoError(t, err)\n\n\tentry := ListingEntry{\n\t\tBuilt:    time.Now(),\n\t\tVersion:  5,\n\t\tURL:      u,\n\t\tChecksum: \"83b52a2aa6aff35d208520f40dd36144\",\n\t}\n\n\tdownloadProgress := progress.NewManual(10)\n\timportProgress := progress.NewManual(10)\n\tstage := progress.NewAtomicStage(\"some-stage\")\n\n\trunTheTest := func(success chan struct{}, errs chan error) {\n\t\t_, _, _, err = curator.IsUpdateAvailable()\n\t\tif err == nil {\n\t\t\terrs <- errors.New(\"expected timeout error but got nil\")\n\t\t\treturn\n\t\t}\n\t\tif !strings.Contains(err.Error(), \"Timeout exceeded\") {\n\t\t\terrs <- fmt.Errorf(\"expected %q but got %q\", \"Timeout exceeded\", err.Error())\n\t\t\treturn\n\t\t}\n\n\t\terr = curator.UpdateTo(&entry, downloadProgress, importProgress, stage)\n\t\tif err == nil {\n\t\t\terrs <- errors.New(\"expected timeout error but got nil\")\n\t\t\treturn\n\t\t}\n\t\tif !strings.Contains(err.Error(), \"Timeout exceeded\") {\n\t\t\terrs <- fmt.Errorf(\"expected %q but got %q\", \"Timeout exceeded\", err.Error())\n\t\t\treturn\n\t\t}\n\t\tsuccess <- struct{}{}\n\t}\n\tgo runTheTest(success, errs)\n\n\tselect {\n\tcase <-success:\n\t\treturn\n\tcase err := <-errs:\n\t\tt.Error(err)\n\tcase <-timeout:\n\t\tt.Fatalf(\"timeout exceeded (%v)\", failAfter)\n\t}\n}\n\nfunc TestCurator_IsUpdateCheckAllowed(t *testing.T) {\n\tfs := afero.NewOsFs()\n\ttempDir := t.TempDir()\n\n\tcurator := Curator{\n\t\tfs:                      fs,\n\t\tupdateCheckMaxFrequency: 10 * time.Minute,\n\t\tdbDir:                   tempDir,\n\t}\n\n\twriteLastCheckTime := func(t *testing.T, lastCheckTime time.Time) {\n\t\terr := afero.WriteFile(fs, path.Join(tempDir, lastUpdateCheckFileName), []byte(lastCheckTime.Format(time.RFC3339)), 0644)\n\t\trequire.NoError(t, err)\n\t}\n\n\tt.Run(\"first run check (no last check file)\", func(t *testing.T) {\n\t\trequire.True(t, curator.isUpdateCheckAllowed())\n\t})\n\n\tt.Run(\"check not allowed due to frequency\", func(t *testing.T) {\n\t\twriteLastCheckTime(t, time.Now().Add(-5*time.Minute))\n\n\t\trequire.False(t, curator.isUpdateCheckAllowed())\n\t})\n\n\tt.Run(\"check allowed after the frequency period\", func(t *testing.T) {\n\t\twriteLastCheckTime(t, time.Now().Add(-20*time.Minute))\n\n\t\trequire.True(t, curator.isUpdateCheckAllowed())\n\t})\n}\n\nfunc TestCurator_DurationSinceUpdateCheck(t *testing.T) {\n\tfs := afero.NewOsFs()\n\ttempDir := t.TempDir()\n\n\tcurator := Curator{\n\t\tfs:    fs,\n\t\tdbDir: tempDir,\n\t}\n\n\twriteLastCheckTime := func(t *testing.T, lastCheckTime time.Time) {\n\t\terr := afero.WriteFile(fs, path.Join(tempDir, lastUpdateCheckFileName), []byte(lastCheckTime.Format(time.RFC3339)), 0644)\n\t\trequire.NoError(t, err)\n\t}\n\n\tt.Run(\"no last check file\", func(t *testing.T) {\n\t\telapsed, err := curator.durationSinceUpdateCheck()\n\t\trequire.NoError(t, err)\n\t\trequire.Nil(t, elapsed)\n\t})\n\n\tt.Run(\"last check file does not exist\", func(t *testing.T) {\n\t\t// simulate a non-existing file\n\t\t_, err := curator.durationSinceUpdateCheck()\n\t\trequire.NoError(t, err)\n\t})\n\n\tt.Run(\"valid last check file\", func(t *testing.T) {\n\t\twriteLastCheckTime(t, time.Now().Add(-5*time.Minute))\n\n\t\telapsed, err := curator.durationSinceUpdateCheck()\n\t\trequire.NoError(t, err)\n\t\trequire.NotNil(t, elapsed)\n\t\trequire.True(t, *elapsed >= 5*time.Minute)\n\t})\n\n\tt.Run(\"malformed last check file\", func(t *testing.T) {\n\t\terr := afero.WriteFile(fs, path.Join(tempDir, lastUpdateCheckFileName), []byte(\"not a timestamp\"), 0644)\n\t\trequire.NoError(t, err)\n\n\t\t_, err = curator.durationSinceUpdateCheck()\n\t\trequire.Error(t, err)\n\t\trequire.Contains(t, err.Error(), \"unable to parse last update check timestamp\")\n\t})\n}\n\nfunc TestCurator_SetLastSuccessfulUpdateCheck(t *testing.T) {\n\tfs := afero.NewOsFs()\n\ttempDir := t.TempDir()\n\n\tcurator := Curator{\n\t\tfs:    fs,\n\t\tdbDir: tempDir,\n\t}\n\n\tt.Run(\"set last successful update check\", func(t *testing.T) {\n\t\tcurator.setLastSuccessfulUpdateCheck()\n\n\t\tdata, err := afero.ReadFile(fs, path.Join(tempDir, lastUpdateCheckFileName))\n\t\trequire.NoError(t, err)\n\n\t\tlastCheckTime, err := time.Parse(time.RFC3339, string(data))\n\t\trequire.NoError(t, err)\n\t\trequire.WithinDuration(t, time.Now().UTC(), lastCheckTime, time.Second)\n\t})\n\n\tt.Run(\"error writing last successful update check\", func(t *testing.T) {\n\t\tinvalidFs := afero.NewReadOnlyFs(fs) // make it read-only, which should simulate a write error\n\t\tcurator.fs = invalidFs\n\n\t\tcurator.setLastSuccessfulUpdateCheck()\n\t})\n}\n\n// Mock for the file.Getter interface\ntype MockGetter struct {\n\tmock.Mock\n}\n\nfunc (m *MockGetter) GetFile(dst, src string, monitor ...*progress.Manual) error {\n\targs := m.Called(dst, src, monitor)\n\treturn args.Error(0)\n}\n\nfunc (m *MockGetter) GetToDir(dst, src string, monitor ...*progress.Manual) error {\n\targs := m.Called(dst, src, monitor)\n\treturn args.Error(0)\n}\n\nfunc TestCurator_Update_setLastSuccessfulUpdateCheck_notCalled(t *testing.T) {\n\n\tnewCurator := func(t *testing.T) *Curator {\n\t\treturn &Curator{\n\t\t\tfs:                      afero.NewOsFs(),\n\t\t\tdbDir:                   t.TempDir(),\n\t\t\tupdateCheckMaxFrequency: 10 * time.Minute,\n\t\t\tlistingDownloader:       &MockGetter{},\n\t\t\tupdateDownloader:        &MockGetter{},\n\t\t\trequireUpdateCheck:      true,\n\t\t}\n\t}\n\n\tt.Run(\"error checking for update\", func(t *testing.T) {\n\t\tc := newCurator(t)\n\n\t\tc.listingDownloader.(*MockGetter).On(\"GetFile\", mock.Anything, mock.Anything, mock.Anything).Return(errors.New(\"get listing failed\"))\n\n\t\t_, err := c.Update()\n\t\trequire.Error(t, err)\n\t\trequire.ErrorContains(t, err, \"get listing failed\")\n\n\t\trequire.NoFileExists(t, filepath.Join(t.TempDir(), lastUpdateCheckFileName))\n\t})\n\n}\n\nfunc Test_unarchive(t *testing.T) {\n\ttestFile := filepath.Join(t.TempDir(), \"vulnerability.db\")\n\tf, err := os.Create(testFile)\n\trequire.NoError(t, err)\n\tf.Close()\n\n\tfiles, err := archives.FilesFromDisk(t.Context(), nil, map[string]string{\n\t\ttestFile: \"\",\n\t})\n\trequire.NoError(t, err)\n\n\tsource := filepath.Join(t.TempDir(), \"archive.tar.zst\")\n\tout, err := os.Create(source)\n\trequire.NoError(t, err)\n\n\tformat := archives.CompressedArchive{\n\t\tCompression: archives.Zstd{},\n\t\tArchival:    archives.Tar{},\n\t}\n\terr = format.Archive(t.Context(), out, files)\n\trequire.NoError(t, err)\n\n\tdestination := t.TempDir()\n\terr = unarchive(source, destination)\n\trequire.NoError(t, err)\n\n\texpectFile := filepath.Join(destination, \"vulnerability.db\")\n\trequire.FileExists(t, expectFile)\n}\n"
  },
  {
    "path": "grype/db/v5/distribution/listing.go",
    "content": "package distribution\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"sort\"\n\n\t\"github.com/spf13/afero\"\n)\n\nconst ListingFileName = \"listing.json\"\n\n// Listing represents the json file which is served up and made available for applications to download and\n// consume one or more vulnerability db flat files.\ntype Listing struct {\n\tAvailable map[int][]ListingEntry `json:\"available\"`\n}\n\n// NewListing creates a listing from one or more given ListingEntries.\nfunc NewListing(entries ...ListingEntry) Listing {\n\tlisting := Listing{\n\t\tAvailable: make(map[int][]ListingEntry),\n\t}\n\tfor _, entry := range entries {\n\t\tif _, ok := listing.Available[entry.Version]; !ok {\n\t\t\tlisting.Available[entry.Version] = make([]ListingEntry, 0)\n\t\t}\n\t\tlisting.Available[entry.Version] = append(listing.Available[entry.Version], entry)\n\t}\n\n\t// sort each entry descending by date\n\tfor idx := range listing.Available {\n\t\tlistingEntries := listing.Available[idx]\n\t\tsort.SliceStable(listingEntries, func(i, j int) bool {\n\t\t\treturn listingEntries[i].Built.After(listingEntries[j].Built)\n\t\t})\n\t}\n\n\treturn listing\n}\n\n// NewListingFromFile loads a Listing from a given filepath.\nfunc NewListingFromFile(fs afero.Fs, path string) (Listing, error) {\n\tf, err := fs.Open(path)\n\tif err != nil {\n\t\treturn Listing{}, fmt.Errorf(\"unable to open DB listing path: %w\", err)\n\t}\n\tdefer f.Close()\n\n\tvar l Listing\n\terr = json.NewDecoder(f).Decode(&l)\n\tif err != nil {\n\t\treturn Listing{}, fmt.Errorf(\"unable to parse DB listing: %w\", err)\n\t}\n\n\t// sort each entry descending by date\n\tfor idx := range l.Available {\n\t\tlistingEntries := l.Available[idx]\n\t\tsort.SliceStable(listingEntries, func(i, j int) bool {\n\t\t\treturn listingEntries[i].Built.After(listingEntries[j].Built)\n\t\t})\n\t}\n\n\treturn l, nil\n}\n\n// BestUpdate returns the ListingEntry from a Listing that meets the given version constraints.\nfunc (l *Listing) BestUpdate(targetSchema int) *ListingEntry {\n\tif listingEntries, ok := l.Available[targetSchema]; ok {\n\t\tif len(listingEntries) > 0 {\n\t\t\treturn &listingEntries[0]\n\t\t}\n\t}\n\treturn nil\n}\n\n// Write the current listing to the given filepath.\nfunc (l Listing) Write(toPath string) error {\n\tcontents, err := json.MarshalIndent(&l, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to encode listing file: %w\", err)\n\t}\n\n\terr = os.WriteFile(toPath, contents, 0600)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to write listing file: %w\", err)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "grype/db/v5/distribution/listing_entry.go",
    "content": "package distribution\n\nimport (\n\t\"crypto/sha256\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/url\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"time\"\n\n\t\"github.com/spf13/afero\"\n\n\t\"github.com/anchore/grype/internal/file\"\n)\n\n// ListingEntry represents basic metadata about a database archive such as what is in the archive (built/version)\n// as well as how to obtain and verify the archive (URL/checksum).\ntype ListingEntry struct {\n\tBuilt    time.Time // RFC 3339\n\tVersion  int\n\tURL      *url.URL\n\tChecksum string\n}\n\n// ListingEntryJSON is a helper struct for converting a ListingEntry into JSON (or parsing from JSON)\ntype ListingEntryJSON struct {\n\tBuilt    string `json:\"built\"`\n\tVersion  int    `json:\"version\"`\n\tURL      string `json:\"url\"`\n\tChecksum string `json:\"checksum\"`\n}\n\n// NewListingEntryFromArchive creates a new ListingEntry based on the metadata from a database flat file.\nfunc NewListingEntryFromArchive(fs afero.Fs, metadata Metadata, dbArchivePath string, baseURL *url.URL) (ListingEntry, error) {\n\tchecksum, err := file.HashFile(fs, dbArchivePath, sha256.New())\n\tif err != nil {\n\t\treturn ListingEntry{}, fmt.Errorf(\"unable to find db archive checksum: %w\", err)\n\t}\n\n\tdbArchiveName := filepath.Base(dbArchivePath)\n\tfileURL, _ := url.Parse(baseURL.String())\n\tfileURL.Path = path.Join(fileURL.Path, dbArchiveName)\n\n\treturn ListingEntry{\n\t\tBuilt:    metadata.Built,\n\t\tVersion:  metadata.Version,\n\t\tURL:      fileURL,\n\t\tChecksum: \"sha256:\" + checksum,\n\t}, nil\n}\n\n// ToListingEntry converts a ListingEntryJSON to a ListingEntry.\nfunc (l ListingEntryJSON) ToListingEntry() (ListingEntry, error) {\n\tbuild, err := time.Parse(time.RFC3339, l.Built)\n\tif err != nil {\n\t\treturn ListingEntry{}, fmt.Errorf(\"cannot convert built time (%s): %+v\", l.Built, err)\n\t}\n\n\tu, err := url.Parse(l.URL)\n\tif err != nil {\n\t\treturn ListingEntry{}, fmt.Errorf(\"cannot parse url (%s): %+v\", l.URL, err)\n\t}\n\n\treturn ListingEntry{\n\t\tBuilt:    build.UTC(),\n\t\tVersion:  l.Version,\n\t\tURL:      u,\n\t\tChecksum: l.Checksum,\n\t}, nil\n}\n\nfunc (l *ListingEntry) UnmarshalJSON(data []byte) error {\n\tvar lej ListingEntryJSON\n\tif err := json.Unmarshal(data, &lej); err != nil {\n\t\treturn err\n\t}\n\tle, err := lej.ToListingEntry()\n\tif err != nil {\n\t\treturn err\n\t}\n\t*l = le\n\treturn nil\n}\n\nfunc (l *ListingEntry) MarshalJSON() ([]byte, error) {\n\treturn json.Marshal(&ListingEntryJSON{\n\t\tBuilt:    l.Built.Format(time.RFC3339),\n\t\tVersion:  l.Version,\n\t\tChecksum: l.Checksum,\n\t\tURL:      l.URL.String(),\n\t})\n}\n\nfunc (l ListingEntry) String() string {\n\treturn fmt.Sprintf(\"Listing(url=%s)\", l.URL)\n}\n"
  },
  {
    "path": "grype/db/v5/distribution/listing_test.go",
    "content": "package distribution\n\nimport (\n\t\"net/url\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-test/deep\"\n\t\"github.com/spf13/afero\"\n)\n\nfunc mustUrl(u *url.URL, err error) *url.URL {\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn u\n}\n\nfunc TestNewListingFromPath(t *testing.T) {\n\ttests := []struct {\n\t\tfixture  string\n\t\texpected Listing\n\t\terr      bool\n\t}{\n\t\t{\n\t\t\tfixture: \"testdata/listing.json\",\n\t\t\texpected: Listing{\n\t\t\t\tAvailable: map[int][]ListingEntry{\n\t\t\t\t\t1: {\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBuilt:    time.Date(2020, 06, 12, 16, 12, 12, 0, time.UTC),\n\t\t\t\t\t\t\tURL:      mustUrl(url.Parse(\"http://localhost:5000/vulnerability-db-v0.2.0+2020-6-12.tar.gz\")),\n\t\t\t\t\t\t\tVersion:  1,\n\t\t\t\t\t\t\tChecksum: \"sha256:e20c251202948df7f853ddc812f64826bdcd6a285c839a7c65939e68609dfc6e\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\t2: {\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBuilt:    time.Date(2020, 06, 13, 17, 13, 13, 0, time.UTC),\n\t\t\t\t\t\t\tURL:      mustUrl(url.Parse(\"http://localhost:5000/vulnerability-db-v1.1.0+2020-6-13.tar.gz\")),\n\t\t\t\t\t\t\tVersion:  2,\n\t\t\t\t\t\t\tChecksum: \"sha256:dcd6a285c839a7c65939e20c251202912f64826be68609dfc6e48df7f853ddc8\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tfixture: \"testdata/listing-sorted.json\",\n\t\t\texpected: Listing{\n\t\t\t\tAvailable: map[int][]ListingEntry{\n\t\t\t\t\t1: {\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBuilt:    time.Date(2020, 06, 13, 17, 13, 13, 0, time.UTC),\n\t\t\t\t\t\t\tURL:      mustUrl(url.Parse(\"http://localhost:5000/vulnerability-db_v1_2020-6-13.tar.gz\")),\n\t\t\t\t\t\t\tVersion:  1,\n\t\t\t\t\t\t\tChecksum: \"sha256:dcd6a285c839a7c65939e20c251202912f64826be68609dfc6e48df7f853ddc8\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBuilt:    time.Date(2020, 06, 12, 16, 12, 12, 0, time.UTC),\n\t\t\t\t\t\t\tURL:      mustUrl(url.Parse(\"http://localhost:5000/vulnerability-db_v1_2020-6-12.tar.gz\")),\n\t\t\t\t\t\t\tVersion:  1,\n\t\t\t\t\t\t\tChecksum: \"sha256:e20c251202948df7f853ddc812f64826bdcd6a285c839a7c65939e68609dfc6e\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tfixture: \"testdata/listing-unsorted.json\",\n\t\t\texpected: Listing{\n\t\t\t\tAvailable: map[int][]ListingEntry{\n\t\t\t\t\t1: {\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBuilt:    time.Date(2020, 06, 13, 17, 13, 13, 0, time.UTC),\n\t\t\t\t\t\t\tURL:      mustUrl(url.Parse(\"http://localhost:5000/vulnerability-db_v1_2020-6-13.tar.gz\")),\n\t\t\t\t\t\t\tVersion:  1,\n\t\t\t\t\t\t\tChecksum: \"sha256:dcd6a285c839a7c65939e20c251202912f64826be68609dfc6e48df7f853ddc8\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tBuilt:    time.Date(2020, 06, 12, 16, 12, 12, 0, time.UTC),\n\t\t\t\t\t\t\tURL:      mustUrl(url.Parse(\"http://localhost:5000/vulnerability-db_v1_2020-6-12.tar.gz\")),\n\t\t\t\t\t\t\tVersion:  1,\n\t\t\t\t\t\t\tChecksum: \"sha256:e20c251202948df7f853ddc812f64826bdcd6a285c839a7c65939e68609dfc6e\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.fixture, func(t *testing.T) {\n\t\t\tlisting, err := NewListingFromFile(afero.NewOsFs(), test.fixture)\n\t\t\tif err != nil && !test.err {\n\t\t\t\tt.Fatalf(\"failed to get metadata: %+v\", err)\n\t\t\t} else if err == nil && test.err {\n\t\t\t\tt.Fatalf(\"expected errer but got none\")\n\t\t\t}\n\n\t\t\tfor _, diff := range deep.Equal(listing, test.expected) {\n\t\t\t\tt.Errorf(\"listing difference: %s\", diff)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestListingBestUpdate(t *testing.T) {\n\ttests := []struct {\n\t\tfixture    string\n\t\tconstraint int\n\t\texpected   *ListingEntry\n\t}{\n\t\t{\n\t\t\tfixture:    \"testdata/listing.json\",\n\t\t\tconstraint: 2,\n\t\t\texpected: &ListingEntry{\n\t\t\t\tBuilt:    time.Date(2020, 06, 13, 17, 13, 13, 0, time.UTC),\n\t\t\t\tURL:      mustUrl(url.Parse(\"http://localhost:5000/vulnerability-db-v1.1.0+2020-6-13.tar.gz\")),\n\t\t\t\tVersion:  2,\n\t\t\t\tChecksum: \"sha256:dcd6a285c839a7c65939e20c251202912f64826be68609dfc6e48df7f853ddc8\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tfixture:    \"testdata/listing.json\",\n\t\t\tconstraint: 1,\n\t\t\texpected: &ListingEntry{\n\t\t\t\tBuilt:    time.Date(2020, 06, 12, 16, 12, 12, 0, time.UTC),\n\t\t\t\tURL:      mustUrl(url.Parse(\"http://localhost:5000/vulnerability-db-v0.2.0+2020-6-12.tar.gz\")),\n\t\t\t\tVersion:  1,\n\t\t\t\tChecksum: \"sha256:e20c251202948df7f853ddc812f64826bdcd6a285c839a7c65939e68609dfc6e\",\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.fixture, func(t *testing.T) {\n\t\t\tlisting, err := NewListingFromFile(afero.NewOsFs(), test.fixture)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"failed to get metadata: %+v\", err)\n\t\t\t}\n\n\t\t\tactual := listing.BestUpdate(test.constraint)\n\t\t\tif actual == nil && test.expected != nil || actual != nil && test.expected == nil {\n\t\t\t\tt.Fatalf(\"mismatched best candidate expectations\")\n\t\t\t}\n\n\t\t\tfor _, diff := range deep.Equal(actual, test.expected) {\n\t\t\t\tt.Errorf(\"listing entry difference: %s\", diff)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/db/v5/distribution/metadata.go",
    "content": "package distribution\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"path\"\n\t\"time\"\n\n\t\"github.com/spf13/afero\"\n\n\t\"github.com/anchore/grype/internal/file\"\n\t\"github.com/anchore/grype/internal/log\"\n)\n\nconst MetadataFileName = \"metadata.json\"\n\n// Metadata represents the basic identifying information of a database flat file (built/version) and a way to\n// verify the contents (checksum).\ntype Metadata struct {\n\tBuilt    time.Time\n\tVersion  int\n\tChecksum string\n}\n\n// MetadataJSON is a helper struct for parsing and assembling Metadata objects to and from JSON.\ntype MetadataJSON struct {\n\tBuilt    string `json:\"built\"` // RFC 3339\n\tVersion  int    `json:\"version\"`\n\tChecksum string `json:\"checksum\"`\n}\n\n// ToMetadata converts a MetadataJSON object to a Metadata object.\nfunc (m MetadataJSON) ToMetadata() (Metadata, error) {\n\tbuild, err := time.Parse(time.RFC3339, m.Built)\n\tif err != nil {\n\t\treturn Metadata{}, fmt.Errorf(\"cannot convert built time (%s): %+v\", m.Built, err)\n\t}\n\n\tmetadata := Metadata{\n\t\tBuilt:    build.UTC(),\n\t\tVersion:  m.Version,\n\t\tChecksum: m.Checksum,\n\t}\n\n\treturn metadata, nil\n}\n\nfunc metadataPath(dir string) string {\n\treturn path.Join(dir, MetadataFileName)\n}\n\n// NewMetadataFromDir generates a Metadata object from a directory containing a vulnerability.db flat file.\nfunc NewMetadataFromDir(fs afero.Fs, dir string) (*Metadata, error) {\n\tmetadataFilePath := metadataPath(dir)\n\texists, err := file.Exists(fs, metadataFilePath)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to check if DB metadata path exists (%s): %w\", metadataFilePath, err)\n\t}\n\tif !exists {\n\t\treturn nil, nil\n\t}\n\tf, err := fs.Open(metadataFilePath)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to open DB metadata path (%s): %w\", metadataFilePath, err)\n\t}\n\tdefer f.Close()\n\n\tvar m Metadata\n\terr = json.NewDecoder(f).Decode(&m)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to parse DB metadata (%s): %w\", metadataFilePath, err)\n\t}\n\treturn &m, nil\n}\n\nfunc (m *Metadata) UnmarshalJSON(data []byte) error {\n\tvar mj MetadataJSON\n\tif err := json.Unmarshal(data, &mj); err != nil {\n\t\treturn err\n\t}\n\tme, err := mj.ToMetadata()\n\tif err != nil {\n\t\treturn err\n\t}\n\t*m = me\n\treturn nil\n}\n\n// IsSupersededBy takes a ListingEntry and determines if the entry candidate is newer than what is hinted at\n// in the current Metadata object.\nfunc (m *Metadata) IsSupersededBy(entry *ListingEntry) bool {\n\tif m == nil {\n\t\tlog.Debugf(\"cannot find existing metadata, using update...\")\n\t\t// any valid update beats no database, use it!\n\t\treturn true\n\t}\n\n\tif entry.Version > m.Version {\n\t\tlog.Debugf(\"update is a newer version than the current database, using update...\")\n\t\t// the listing is newer than the existing db, use it!\n\t\treturn true\n\t}\n\n\tif entry.Built.After(m.Built) {\n\t\tlog.Debugf(\"existing database (%s) is older than candidate update (%s), using update...\", m.Built.String(), entry.Built.String())\n\t\t// the listing is newer than the existing db, use it!\n\t\treturn true\n\t}\n\n\tlog.Debugf(\"existing database is already up to date\")\n\treturn false\n}\n\nfunc (m Metadata) String() string {\n\treturn fmt.Sprintf(\"Metadata(built=%s version=%d checksum=%s)\", m.Built, m.Version, m.Checksum)\n}\n\n// Write out a Metadata object to the given path.\nfunc (m Metadata) Write(toPath string) error {\n\tmetadata := MetadataJSON{\n\t\tBuilt:    m.Built.UTC().Format(time.RFC3339),\n\t\tVersion:  m.Version,\n\t\tChecksum: m.Checksum,\n\t}\n\n\tcontents, err := json.MarshalIndent(&metadata, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to encode metadata file: %w\", err)\n\t}\n\n\terr = os.WriteFile(toPath, contents, 0600)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to write metadata file: %w\", err)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "grype/db/v5/distribution/metadata_test.go",
    "content": "package distribution\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-test/deep\"\n\t\"github.com/spf13/afero\"\n)\n\nfunc TestMetadataParse(t *testing.T) {\n\ttests := []struct {\n\t\tfixture  string\n\t\texpected *Metadata\n\t\terr      bool\n\t}{\n\t\t{\n\t\t\tfixture: \"testdata/metadata-gocase\",\n\t\t\texpected: &Metadata{\n\t\t\t\tBuilt:    time.Date(2020, 06, 15, 14, 02, 36, 0, time.UTC),\n\t\t\t\tVersion:  2,\n\t\t\t\tChecksum: \"sha256:dcd6a285c839a7c65939e20c251202912f64826be68609dfc6e48df7f853ddc8\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tfixture: \"testdata/metadata-edt-timezone\",\n\t\t\texpected: &Metadata{\n\t\t\t\tBuilt:    time.Date(2020, 06, 15, 18, 02, 36, 0, time.UTC),\n\t\t\t\tVersion:  2,\n\t\t\t\tChecksum: \"sha256:dcd6a285c839a7c65939e20c251202912f64826be68609dfc6e48df7f853ddc8\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tfixture: \"/dev/null/impossible\",\n\t\t\terr:     true,\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.fixture, func(t *testing.T) {\n\t\t\tmetadata, err := NewMetadataFromDir(afero.NewOsFs(), test.fixture)\n\t\t\tif err != nil && !test.err {\n\t\t\t\tt.Fatalf(\"failed to get metadata: %+v\", err)\n\t\t\t} else if err == nil && test.err {\n\t\t\t\tt.Fatalf(\"expected error but got none\")\n\t\t\t} else if metadata == nil && test.expected != nil {\n\t\t\t\tt.Fatalf(\"metadata not found: %+v\", test.fixture)\n\t\t\t}\n\n\t\t\tif metadata != nil && test.expected != nil {\n\t\t\t\tfor _, diff := range deep.Equal(*metadata, *test.expected) {\n\t\t\t\t\tt.Errorf(\"metadata difference: %s\", diff)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestMetadataIsSupercededBy(t *testing.T) {\n\ttests := []struct {\n\t\tname                string\n\t\tcurrent             *Metadata\n\t\tupdate              *ListingEntry\n\t\texpectedToSupercede bool\n\t}{\n\t\t{\n\t\t\tname:                \"prefer updated versions over later dates\",\n\t\t\texpectedToSupercede: true,\n\t\t\tcurrent: &Metadata{\n\t\t\t\tBuilt:   time.Date(2020, 06, 15, 14, 02, 36, 0, time.UTC),\n\t\t\t\tVersion: 2,\n\t\t\t},\n\t\t\tupdate: &ListingEntry{\n\t\t\t\tBuilt:   time.Date(2020, 06, 13, 17, 13, 13, 0, time.UTC),\n\t\t\t\tVersion: 3,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:                \"prefer later dates when version is the same\",\n\t\t\texpectedToSupercede: false,\n\t\t\tcurrent: &Metadata{\n\t\t\t\tBuilt:   time.Date(2020, 06, 15, 14, 02, 36, 0, time.UTC),\n\t\t\t\tVersion: 1,\n\t\t\t},\n\t\t\tupdate: &ListingEntry{\n\t\t\t\tBuilt:   time.Date(2020, 06, 13, 17, 13, 13, 0, time.UTC),\n\t\t\t\tVersion: 1,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:                \"prefer something over nothing\",\n\t\t\texpectedToSupercede: true,\n\t\t\tcurrent:             nil,\n\t\t\tupdate: &ListingEntry{\n\t\t\t\tBuilt:   time.Date(2020, 06, 13, 17, 13, 13, 0, time.UTC),\n\t\t\t\tVersion: 1,\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tactual := test.current.IsSupersededBy(test.update)\n\n\t\t\tif test.expectedToSupercede != actual {\n\t\t\t\tt.Errorf(\"failed supercede assertion: got %+v\", actual)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/db/v5/distribution/status.go",
    "content": "package distribution\n\nimport \"time\"\n\ntype Status struct {\n\tBuilt         time.Time `json:\"built\"`\n\tSchemaVersion int       `json:\"schemaVersion\"`\n\tLocation      string    `json:\"location\"`\n\tChecksum      string    `json:\"checksum\"`\n\tErr           error     `json:\"error\"`\n}\n\nfunc (s Status) Status() string {\n\tif s.Err != nil {\n\t\treturn \"invalid\"\n\t}\n\treturn \"valid\"\n}\n"
  },
  {
    "path": "grype/db/v5/distribution/testdata/curator-validate/bad-checksum/metadata.json",
    "content": "{\n    \"built\": \"2020-06-15T14:02:36Z\",\n    \"version\": 1,\n    \"checksum\": \"sha256:deadbeefcafe\"\n}"
  },
  {
    "path": "grype/db/v5/distribution/testdata/curator-validate/good-checksum/metadata.json",
    "content": "{\n    \"built\": \"2020-06-15T14:02:36Z\",\n    \"version\": 1,\n    \"checksum\": \"sha256:3baf9c50c94e7f1e65bafac2e6a6d559fb177461dd25bf8fca7e6e9e9c266cb4\"\n}"
  },
  {
    "path": "grype/db/v5/distribution/testdata/listing-sorted.json",
    "content": "{\n  \"available\": {\n    \"1\": [\n      {\n        \"built\": \"2020-06-13T13:13:13-04:00\",\n        \"version\": 1,\n        \"url\": \"http://localhost:5000/vulnerability-db_v1_2020-6-13.tar.gz\",\n        \"checksum\": \"sha256:dcd6a285c839a7c65939e20c251202912f64826be68609dfc6e48df7f853ddc8\"\n      },\n      {\n        \"built\": \"2020-06-12T12:12:12-04:00\",\n        \"version\": 1,\n        \"url\": \"http://localhost:5000/vulnerability-db_v1_2020-6-12.tar.gz\",\n        \"checksum\": \"sha256:e20c251202948df7f853ddc812f64826bdcd6a285c839a7c65939e68609dfc6e\"\n      }\n    ]\n  }\n}\n"
  },
  {
    "path": "grype/db/v5/distribution/testdata/listing-unsorted.json",
    "content": "{\n  \"available\": {\n    \"1\": [\n      {\n        \"built\": \"2020-06-12T12:12:12-04:00\",\n        \"version\": 1,\n        \"url\": \"http://localhost:5000/vulnerability-db_v1_2020-6-12.tar.gz\",\n        \"checksum\": \"sha256:e20c251202948df7f853ddc812f64826bdcd6a285c839a7c65939e68609dfc6e\"\n      },\n      {\n        \"built\": \"2020-06-13T13:13:13-04:00\",\n        \"version\": 1,\n        \"url\": \"http://localhost:5000/vulnerability-db_v1_2020-6-13.tar.gz\",\n        \"checksum\": \"sha256:dcd6a285c839a7c65939e20c251202912f64826be68609dfc6e48df7f853ddc8\"\n      }\n    ]\n  }\n}\n"
  },
  {
    "path": "grype/db/v5/distribution/testdata/listing.json",
    "content": "{\n    \"available\": {\n        \"1\": [\n            {\n                \"built\": \"2020-06-12T12:12:12-04:00\",\n                \"version\": 1,\n                \"url\": \"http://localhost:5000/vulnerability-db-v0.2.0+2020-6-12.tar.gz\",\n                \"checksum\": \"sha256:e20c251202948df7f853ddc812f64826bdcd6a285c839a7c65939e68609dfc6e\"\n            }\n        ],\n        \"2\": [\n            {\n                \"built\": \"2020-06-13T13:13:13-04:00\",\n                \"version\": 2,\n                \"url\": \"http://localhost:5000/vulnerability-db-v1.1.0+2020-6-13.tar.gz\",\n                \"checksum\": \"sha256:dcd6a285c839a7c65939e20c251202912f64826be68609dfc6e48df7f853ddc8\"\n            }\n        ]\n    }\n}\n"
  },
  {
    "path": "grype/db/v5/distribution/testdata/metadata-edt-timezone/metadata.json",
    "content": "{\n    \"built\": \"2020-06-15T14:02:36-04:00\",\n    \"updated\": \"2020-06-15T14:02:36-04:00\",\n    \"last-check\": \"2020-06-15T14:02:36-04:00\",\n    \"version\": 2,\n    \"checksum\": \"sha256:dcd6a285c839a7c65939e20c251202912f64826be68609dfc6e48df7f853ddc8\"\n}\n"
  },
  {
    "path": "grype/db/v5/distribution/testdata/metadata-gocase/metadata.json",
    "content": "{\n    \"built\": \"2020-06-15T14:02:36Z\",\n    \"version\": 2,\n    \"checksum\": \"sha256:dcd6a285c839a7c65939e20c251202912f64826be68609dfc6e48df7f853ddc8\"\n}"
  },
  {
    "path": "grype/db/v5/distribution/testdata/tls/.gitignore",
    "content": "server.key\nserver.crt\nwww/\nlisting.json\ndbdir/"
  },
  {
    "path": "grype/db/v5/distribution/testdata/tls/Makefile",
    "content": "all: clean serve\n\n.PHONY: serve\nserve: www/listing.json www/db.tar.gz server.crt\n\tpython3 serve.py\n\n\n.PHONY: grype-test-fail\ngrype-test-fail: clean-dbdir dbdir\n\tGRYPE_DB_CACHE_DIR=$(shell pwd)/dbdir \\\n\tGRYPE_DB_UPDATE_URL=https://$(shell hostname).local/listing.json \\\n\t\tgo run ../../../../cmd/grype -vv alpine:latest\n\n.PHONY: grype-test-pass\ngrype-test-pass: clean-dbdir dbdir\n\tGRYPE_DB_CA_CERT=$(shell pwd)/server.crt \\\n\tGRYPE_DB_CACHE_DIR=$(shell pwd)/dbdir \\\n\tGRYPE_DB_UPDATE_URL=https://$(shell hostname).local/listing.json \\\n\t\tgo run ../../../../cmd/grype -vv alpine:latest\n\ndbdir:\n\tmkdir -p dbdir\n\nserver.crt server.key:\n\t./generate-x509-cert-pair.sh\n\nwww:\n\tmkdir -p www\n\nlisting.json:\n\tcurl -L -O https://toolbox-data.anchore.io/grype/databases/listing.json\n\nwww/listing.json www/db.tar.gz: www listing.json\n\t$(eval location=$(shell python3 listing.py))\n\tcurl -L -o www/db.tar.gz $(location)\n\n.PHONY: clean\nclean: clean-dbdir\n\trm -rf www\n\trm -f server.crt\n\trm -f server.key\n\n.PHONY: clean-dbdir\nclean-dbdir:\n\trm -rf dbdir/"
  },
  {
    "path": "grype/db/v5/distribution/testdata/tls/README.md",
    "content": "# TLS test utils\n\nNote: Makefile, server.crt, and server.key are used in automated testing, the remaining files are for convenience in manual verification.\n\nYou will require Python 3 to run these utils.\n\nTo stand up a test server:\n```\nmake serve\n```\n\nTo test grype against this server:\n```\n# without the custom cert configured (thus will fail)\nmake grype-test-fail\n\n# with the custom cert configured\nmake grype-test-pass\n```\n\nTo remove all temp files:\n```\nmake clean\n```"
  },
  {
    "path": "grype/db/v5/distribution/testdata/tls/generate-x509-cert-pair.sh",
    "content": "#!/usr/bin/env bash\nset -eux\n\n# we want to still use this on systems where there could be invalid characters in the hostname (e.g. ' or \" characters)\nHOSTNAME=$(hostname | sed \"s/['']/'/g\" | sed 's/[^a-zA-Z0-9.-]/-/g')\n\n# create private key\nopenssl genrsa -out server.key 2048\n\n# generate self-signed public key (cert) based on the private key\nopenssl req -new -x509 -sha256 \\\n    -key server.key \\\n    -out server.crt \\\n    -days 3650 \\\n    -reqexts SAN \\\n    -extensions SAN \\\n    -config <(cat /etc/ssl/openssl.cnf <(printf \"[SAN]\\nsubjectAltName=DNS:$HOSTNAME.local\")) \\\n    -subj \"/C=US/ST=Test/L=Test/O=Test/CN=$HOSTNAME.local\"\n"
  },
  {
    "path": "grype/db/v5/distribution/testdata/tls/listing.py",
    "content": "import urllib.request\nimport json\nimport os\n\nwith open('listing.json', 'r') as fh:\n    data = json.loads(fh.read())\n\nentry = data[\"available\"][\"3\"][-1]\n\nhostname = os.popen('hostname').read().strip()\n\nwith open('www/listing.json', 'w') as fh:\n    json.dump(\n        {\n            \"available\": {\n                entry[\"version\"]: [\n                    {\n                        \"built\": entry[\"built\"],\n                        \"version\": entry[\"version\"],\n                        \"url\": f\"https://{hostname}.local/db.tar.gz\",\n                        \"checksum\": entry[\"checksum\"]\n                    }\n                ]\n            }\n        }, fh)\n\nprint(entry[\"url\"])\n"
  },
  {
    "path": "grype/db/v5/distribution/testdata/tls/serve.py",
    "content": "from http.server import HTTPServer, SimpleHTTPRequestHandler\nimport ssl\nimport logging\n\nport = 443\ndirectory = \"www\"\n\n\nclass Handler(SimpleHTTPRequestHandler):\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, directory=directory, **kwargs)\n\n    def do_GET(self):\n        logging.error(self.headers)\n        SimpleHTTPRequestHandler.do_GET(self)\n\n\nhttpd = HTTPServer(('0.0.0.0', port), Handler)\nsslctx = ssl.SSLContext()\nsslctx.options |= ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1\nsslctx.load_cert_chain(certfile='server.crt', keyfile=\"server.key\")\nhttpd.socket = sslctx.wrap_socket(httpd.socket, server_side=True)\n\nprint(f\"Server running on https://0.0.0.0:{port}\")\nhttpd.serve_forever()"
  },
  {
    "path": "grype/db/v5/fix.go",
    "content": "package v5\n\ntype FixState string\n\nconst (\n\tUnknownFixState FixState = \"unknown\"\n\tFixedState      FixState = \"fixed\"\n\tNotFixedState   FixState = \"not-fixed\"\n\tWontFixState    FixState = \"wont-fix\"\n)\n\n// Fix represents all information about known fixes for a stated vulnerability.\ntype Fix struct {\n\tVersions []string `json:\"versions\"` // The version(s) which this particular vulnerability was fixed in\n\tState    FixState `json:\"state\"`\n}\n"
  },
  {
    "path": "grype/db/v5/id.go",
    "content": "package v5\n\nimport (\n\t\"time\"\n)\n\n// ID represents identifying information for a DB and the data it contains.\ntype ID struct {\n\t// BuildTimestamp is the timestamp used to define the age of the DB, ideally including the age of the data\n\t// contained in the DB, not just when the DB file was created.\n\tBuildTimestamp time.Time `json:\"build_timestamp\"`\n\tSchemaVersion  int       `json:\"schema_version\"`\n}\n\ntype IDReader interface {\n\tGetID() (*ID, error)\n}\n\ntype IDWriter interface {\n\tSetID(ID) error\n}\n\nfunc NewID(age time.Time) ID {\n\treturn ID{\n\t\tBuildTimestamp: age.UTC(),\n\t\tSchemaVersion:  SchemaVersion,\n\t}\n}\n"
  },
  {
    "path": "grype/db/v5/match_exclusion_provider.go",
    "content": "package v5\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/anchore/grype/grype/match\"\n)\n\nvar _ match.ExclusionProvider = (*MatchExclusionProvider)(nil)\n\ntype MatchExclusionProvider struct {\n\treader VulnerabilityMatchExclusionStoreReader\n}\n\nfunc NewMatchExclusionProvider(reader VulnerabilityMatchExclusionStoreReader) *MatchExclusionProvider {\n\treturn &MatchExclusionProvider{\n\t\treader: reader,\n\t}\n}\n\nfunc buildIgnoreRulesFromMatchExclusion(e VulnerabilityMatchExclusion) []match.IgnoreRule {\n\tvar ignoreRules []match.IgnoreRule\n\n\tif len(e.Constraints) == 0 {\n\t\tignoreRules = append(ignoreRules, match.IgnoreRule{Vulnerability: e.ID})\n\t\treturn ignoreRules\n\t}\n\n\tfor _, c := range e.Constraints {\n\t\tignoreRules = append(ignoreRules, match.IgnoreRule{\n\t\t\tVulnerability: e.ID,\n\t\t\tNamespace:     c.Vulnerability.Namespace,\n\t\t\tFixState:      string(c.Vulnerability.FixState),\n\t\t\tPackage: match.IgnoreRulePackage{\n\t\t\t\tName:     c.Package.Name,\n\t\t\t\tLanguage: c.Package.Language,\n\t\t\t\tType:     c.Package.Type,\n\t\t\t\tLocation: c.Package.Location,\n\t\t\t\tVersion:  c.Package.Version,\n\t\t\t},\n\t\t})\n\t}\n\n\treturn ignoreRules\n}\n\nfunc (pr *MatchExclusionProvider) IgnoreRules(vulnerabilityID string) ([]match.IgnoreRule, error) {\n\tmatchExclusions, err := pr.reader.GetVulnerabilityMatchExclusion(vulnerabilityID)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"match exclusion provider failed to fetch records for vulnerability id='%s': %w\", vulnerabilityID, err)\n\t}\n\n\tvar ignoreRules []match.IgnoreRule\n\n\tfor _, e := range matchExclusions {\n\t\trules := buildIgnoreRulesFromMatchExclusion(e)\n\t\tignoreRules = append(ignoreRules, rules...)\n\t}\n\n\treturn ignoreRules, nil\n}\n"
  },
  {
    "path": "grype/db/v5/namespace/cpe/namespace.go",
    "content": "package cpe\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/anchore/grype/grype/db/v5/pkg/resolver\"\n\t\"github.com/anchore/grype/grype/db/v5/pkg/resolver/stock\"\n)\n\nconst ID = \"cpe\"\n\ntype Namespace struct {\n\tprovider string\n\tresolver resolver.Resolver\n}\n\nfunc NewNamespace(provider string) *Namespace {\n\treturn &Namespace{\n\t\tprovider: provider,\n\t\tresolver: &stock.Resolver{},\n\t}\n}\n\nfunc FromString(namespaceStr string) (*Namespace, error) {\n\tif namespaceStr == \"\" {\n\t\treturn nil, errors.New(\"unable to create CPE namespace from empty string\")\n\t}\n\n\tcomponents := strings.Split(namespaceStr, \":\")\n\treturn FromComponents(components)\n}\n\nfunc FromComponents(components []string) (*Namespace, error) {\n\tif len(components) != 2 {\n\t\treturn nil, fmt.Errorf(\"unable to create CPE namespace from %s: incorrect number of components\", strings.Join(components, \":\"))\n\t}\n\n\tif components[1] != ID {\n\t\treturn nil, fmt.Errorf(\"unable to create CPE namespace from %s: type %s is incorrect\", strings.Join(components, \":\"), components[1])\n\t}\n\n\treturn NewNamespace(components[0]), nil\n}\n\nfunc (n *Namespace) Provider() string {\n\treturn n.provider\n}\n\nfunc (n *Namespace) Resolver() resolver.Resolver {\n\treturn n.resolver\n}\n\nfunc (n Namespace) String() string {\n\treturn fmt.Sprintf(\"%s:%s\", n.provider, ID)\n}\n"
  },
  {
    "path": "grype/db/v5/namespace/cpe/namespace_test.go",
    "content": "package cpe\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestFromString(t *testing.T) {\n\tsuccessTests := []struct {\n\t\tnamespaceString string\n\t\tresult          *Namespace\n\t}{\n\t\t{\n\t\t\tnamespaceString: \"abc.xyz:cpe\",\n\t\t\tresult:          NewNamespace(\"abc.xyz\"),\n\t\t},\n\t}\n\n\tfor _, test := range successTests {\n\t\tresult, _ := FromString(test.namespaceString)\n\t\tassert.Equal(t, result, test.result)\n\t}\n\n\terrorTests := []struct {\n\t\tnamespaceString string\n\t\terrorMessage    string\n\t}{\n\t\t{\n\t\t\tnamespaceString: \"\",\n\t\t\terrorMessage:    \"unable to create CPE namespace from empty string\",\n\t\t},\n\t\t{\n\t\t\tnamespaceString: \"single-component\",\n\t\t\terrorMessage:    \"unable to create CPE namespace from single-component: incorrect number of components\",\n\t\t},\n\t\t{\n\t\t\tnamespaceString: \"too:many:components\",\n\t\t\terrorMessage:    \"unable to create CPE namespace from too:many:components: incorrect number of components\",\n\t\t},\n\t\t{\n\t\t\tnamespaceString: \"wrong:namespace_type\",\n\t\t\terrorMessage:    \"unable to create CPE namespace from wrong:namespace_type: type namespace_type is incorrect\",\n\t\t},\n\t}\n\n\tfor _, test := range errorTests {\n\t\t_, err := FromString(test.namespaceString)\n\t\tassert.EqualError(t, err, test.errorMessage)\n\t}\n}\n"
  },
  {
    "path": "grype/db/v5/namespace/distro/namespace.go",
    "content": "package distro\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/anchore/grype/grype/db/v5/pkg/resolver\"\n\t\"github.com/anchore/grype/grype/db/v5/pkg/resolver/stock\"\n\t\"github.com/anchore/grype/grype/distro\"\n)\n\nconst ID = \"distro\"\n\ntype Namespace struct {\n\tprovider   string\n\tdistroType distro.Type\n\tversion    string\n\tresolver   resolver.Resolver\n}\n\nfunc NewNamespace(provider string, distroType distro.Type, version string) *Namespace {\n\treturn &Namespace{\n\t\tprovider:   provider,\n\t\tdistroType: distroType,\n\t\tversion:    version,\n\t\tresolver:   &stock.Resolver{},\n\t}\n}\n\nfunc FromString(namespaceStr string) (*Namespace, error) {\n\tif namespaceStr == \"\" {\n\t\treturn nil, errors.New(\"unable to create distro namespace from empty string\")\n\t}\n\n\tcomponents := strings.Split(namespaceStr, \":\")\n\treturn FromComponents(components)\n}\n\nfunc FromComponents(components []string) (*Namespace, error) {\n\tif len(components) != 4 {\n\t\treturn nil, fmt.Errorf(\"unable to create distro namespace from %s: incorrect number of components\", strings.Join(components, \":\"))\n\t}\n\n\tif components[1] != ID {\n\t\treturn nil, fmt.Errorf(\"unable to create distro namespace from %s: type %s is incorrect\", strings.Join(components, \":\"), components[1])\n\t}\n\n\treturn NewNamespace(components[0], distro.Type(components[2]), components[3]), nil\n}\n\nfunc (n *Namespace) Provider() string {\n\treturn n.provider\n}\n\nfunc (n *Namespace) DistroType() distro.Type {\n\treturn n.distroType\n}\n\nfunc (n *Namespace) Version() string {\n\treturn n.version\n}\n\nfunc (n *Namespace) Resolver() resolver.Resolver {\n\treturn n.resolver\n}\n\nfunc (n Namespace) String() string {\n\treturn fmt.Sprintf(\"%s:%s:%s:%s\", n.provider, ID, n.distroType, n.version)\n}\n"
  },
  {
    "path": "grype/db/v5/namespace/distro/namespace_test.go",
    "content": "package distro\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\n\tgrypeDistro \"github.com/anchore/grype/grype/distro\"\n)\n\nfunc TestFromString(t *testing.T) {\n\tsuccessTests := []struct {\n\t\tnamespaceString string\n\t\tresult          *Namespace\n\t}{\n\t\t{\n\t\t\tnamespaceString: \"alpine:distro:alpine:3.15\",\n\t\t\tresult:          NewNamespace(\"alpine\", grypeDistro.Alpine, \"3.15\"),\n\t\t},\n\t\t{\n\t\t\tnamespaceString: \"redhat:distro:redhat:8\",\n\t\t\tresult:          NewNamespace(\"redhat\", grypeDistro.RedHat, \"8\"),\n\t\t},\n\t\t{\n\t\t\tnamespaceString: \"abc.xyz:distro:unknown:abcd~~~\",\n\t\t\tresult:          NewNamespace(\"abc.xyz\", grypeDistro.Type(\"unknown\"), \"abcd~~~\"),\n\t\t},\n\t\t{\n\t\t\tnamespaceString: \"msrc:distro:windows:10111\",\n\t\t\tresult:          NewNamespace(\"msrc\", grypeDistro.Type(\"windows\"), \"10111\"),\n\t\t},\n\t\t{\n\t\t\tnamespaceString: \"amazon:distro:amazonlinux:2022\",\n\t\t\tresult:          NewNamespace(\"amazon\", grypeDistro.AmazonLinux, \"2022\"),\n\t\t},\n\t\t{\n\t\t\tnamespaceString: \"amazon:distro:amazonlinux:2\",\n\t\t\tresult:          NewNamespace(\"amazon\", grypeDistro.AmazonLinux, \"2\"),\n\t\t},\n\t\t{\n\t\t\tnamespaceString: \"wolfi:distro:wolfi:rolling\",\n\t\t\tresult:          NewNamespace(\"wolfi\", grypeDistro.Wolfi, \"rolling\"),\n\t\t},\n\t\t{\n\t\t\tnamespaceString: \"echo:distro:echo:rolling\",\n\t\t\tresult:          NewNamespace(\"echo\", grypeDistro.Echo, \"rolling\"),\n\t\t},\n\t\t{\n\t\t\tnamespaceString: \"minimos:distro:minimos:rolling\",\n\t\t\tresult:          NewNamespace(\"minimos\", grypeDistro.MinimOS, \"rolling\"),\n\t\t},\n\t}\n\n\tfor _, test := range successTests {\n\t\tresult, _ := FromString(test.namespaceString)\n\t\tassert.Equal(t, result, test.result)\n\t}\n\n\terrorTests := []struct {\n\t\tnamespaceString string\n\t\terrorMessage    string\n\t}{\n\t\t{\n\t\t\tnamespaceString: \"\",\n\t\t\terrorMessage:    \"unable to create distro namespace from empty string\",\n\t\t},\n\t\t{\n\t\t\tnamespaceString: \"single-component\",\n\t\t\terrorMessage:    \"unable to create distro namespace from single-component: incorrect number of components\",\n\t\t},\n\t\t{\n\t\t\tnamespaceString: \"two:components\",\n\t\t\terrorMessage:    \"unable to create distro namespace from two:components: incorrect number of components\",\n\t\t},\n\t\t{\n\t\t\tnamespaceString: \"still:not:enough\",\n\t\t\terrorMessage:    \"unable to create distro namespace from still:not:enough: incorrect number of components\",\n\t\t},\n\t\t{\n\t\t\tnamespaceString: \"too:many:components:a:b\",\n\t\t\terrorMessage:    \"unable to create distro namespace from too:many:components:a:b: incorrect number of components\",\n\t\t},\n\t\t{\n\t\t\tnamespaceString: \"wrong:namespace_type:a:b\",\n\t\t\terrorMessage:    \"unable to create distro namespace from wrong:namespace_type:a:b: type namespace_type is incorrect\",\n\t\t},\n\t}\n\n\tfor _, test := range errorTests {\n\t\t_, err := FromString(test.namespaceString)\n\t\tassert.EqualError(t, err, test.errorMessage)\n\t}\n}\n"
  },
  {
    "path": "grype/db/v5/namespace/from_string.go",
    "content": "package namespace\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/anchore/grype/grype/db/v5/namespace/cpe\"\n\t\"github.com/anchore/grype/grype/db/v5/namespace/distro\"\n\t\"github.com/anchore/grype/grype/db/v5/namespace/language\"\n)\n\nfunc FromString(namespaceStr string) (Namespace, error) {\n\tif namespaceStr == \"\" {\n\t\treturn nil, errors.New(\"unable to create namespace from empty string\")\n\t}\n\n\tcomponents := strings.Split(namespaceStr, \":\")\n\n\tif len(components) < 2 {\n\t\treturn nil, fmt.Errorf(\"unable to create namespace from %s: incorrect number of components\", namespaceStr)\n\t}\n\n\tswitch components[1] {\n\tcase cpe.ID:\n\t\treturn cpe.FromComponents(components)\n\tcase distro.ID:\n\t\treturn distro.FromComponents(components)\n\tcase language.ID:\n\t\treturn language.FromComponents(components)\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unable to create namespace from %s: unknown type %s\", namespaceStr, components[1])\n\t}\n}\n"
  },
  {
    "path": "grype/db/v5/namespace/from_string_test.go",
    "content": "package namespace\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\n\t\"github.com/anchore/grype/grype/db/v5/namespace/cpe\"\n\t\"github.com/anchore/grype/grype/db/v5/namespace/distro\"\n\t\"github.com/anchore/grype/grype/db/v5/namespace/language\"\n\tgrypeDistro \"github.com/anchore/grype/grype/distro\"\n\tsyftPkg \"github.com/anchore/syft/syft/pkg\"\n)\n\nfunc TestFromString(t *testing.T) {\n\ttests := []struct {\n\t\tnamespaceString string\n\t\tresult          Namespace\n\t}{\n\t\t{\n\t\t\tnamespaceString: \"github:language:python\",\n\t\t\tresult:          language.NewNamespace(\"github\", syftPkg.Python, \"\"),\n\t\t},\n\t\t{\n\t\t\tnamespaceString: \"github:language:python:python\",\n\t\t\tresult:          language.NewNamespace(\"github\", syftPkg.Python, syftPkg.PythonPkg),\n\t\t},\n\t\t{\n\t\t\tnamespaceString: \"debian:distro:debian:8\",\n\t\t\tresult:          distro.NewNamespace(\"debian\", grypeDistro.Debian, \"8\"),\n\t\t},\n\t\t{\n\t\t\tnamespaceString: \"unknown:distro:amazonlinux:2022.15\",\n\t\t\tresult:          distro.NewNamespace(\"unknown\", grypeDistro.AmazonLinux, \"2022.15\"),\n\t\t},\n\t\t{\n\t\t\tnamespaceString: \"ns-1:distro:unknowndistro:abcdefg~~~\",\n\t\t\tresult:          distro.NewNamespace(\"ns-1\", grypeDistro.Type(\"unknowndistro\"), \"abcdefg~~~\"),\n\t\t},\n\t\t{\n\t\t\tnamespaceString: \"abc.xyz:cpe\",\n\t\t\tresult:          cpe.NewNamespace(\"abc.xyz\"),\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tresult, _ := FromString(test.namespaceString)\n\t\tassert.Equal(t, result, test.result)\n\t}\n}\n"
  },
  {
    "path": "grype/db/v5/namespace/language/namespace.go",
    "content": "package language\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/anchore/grype/grype/db/v5/pkg/resolver\"\n\tsyftPkg \"github.com/anchore/syft/syft/pkg\"\n)\n\nconst ID = \"language\"\n\ntype Namespace struct {\n\tprovider    string\n\tlanguage    syftPkg.Language\n\tpackageType syftPkg.Type\n\tresolver    resolver.Resolver\n}\n\nfunc NewNamespace(provider string, language syftPkg.Language, packageType syftPkg.Type) *Namespace {\n\tr, _ := resolver.FromLanguage(language)\n\n\treturn &Namespace{\n\t\tprovider:    provider,\n\t\tlanguage:    language,\n\t\tpackageType: packageType,\n\t\tresolver:    r,\n\t}\n}\n\nfunc FromString(namespaceStr string) (*Namespace, error) {\n\tif namespaceStr == \"\" {\n\t\treturn nil, errors.New(\"unable to create language namespace from empty string\")\n\t}\n\n\tcomponents := strings.Split(namespaceStr, \":\")\n\treturn FromComponents(components)\n}\n\nfunc FromComponents(components []string) (*Namespace, error) {\n\tif len(components) != 3 && len(components) != 4 {\n\t\treturn nil, fmt.Errorf(\"unable to create language namespace from %s: incorrect number of components\", strings.Join(components, \":\"))\n\t}\n\n\tif components[1] != ID {\n\t\treturn nil, fmt.Errorf(\"unable to create language namespace from %s: type %s is incorrect\", strings.Join(components, \":\"), components[1])\n\t}\n\n\tpackageType := \"\"\n\n\tif len(components) == 4 {\n\t\tpackageType = components[3]\n\t}\n\n\treturn NewNamespace(components[0], syftPkg.Language(components[2]), syftPkg.Type(packageType)), nil\n}\n\nfunc (n *Namespace) Provider() string {\n\treturn n.provider\n}\n\nfunc (n *Namespace) Language() syftPkg.Language {\n\treturn n.language\n}\n\nfunc (n *Namespace) PackageType() syftPkg.Type {\n\treturn n.packageType\n}\n\nfunc (n *Namespace) Resolver() resolver.Resolver {\n\treturn n.resolver\n}\n\nfunc (n Namespace) String() string {\n\tif n.packageType != \"\" {\n\t\treturn fmt.Sprintf(\"%s:%s:%s:%s\", n.provider, ID, n.language, n.packageType)\n\t}\n\n\treturn fmt.Sprintf(\"%s:%s:%s\", n.provider, ID, n.language)\n}\n"
  },
  {
    "path": "grype/db/v5/namespace/language/namespace_test.go",
    "content": "package language\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\n\tsyftPkg \"github.com/anchore/syft/syft/pkg\"\n)\n\nfunc TestFromString(t *testing.T) {\n\tsuccessTests := []struct {\n\t\tnamespaceString string\n\t\tresult          *Namespace\n\t}{\n\t\t{\n\t\t\tnamespaceString: \"github:language:python\",\n\t\t\tresult:          NewNamespace(\"github\", syftPkg.Python, \"\"),\n\t\t},\n\t\t{\n\t\t\tnamespaceString: \"github:language:ruby\",\n\t\t\tresult:          NewNamespace(\"github\", syftPkg.Ruby, \"\"),\n\t\t},\n\t\t{\n\t\t\tnamespaceString: \"github:language:java\",\n\t\t\tresult:          NewNamespace(\"github\", syftPkg.Java, \"\"),\n\t\t},\n\t\t{\n\t\t\tnamespaceString: \"github:language:rust\",\n\t\t\tresult:          NewNamespace(\"github\", syftPkg.Rust, \"\"),\n\t\t},\n\t\t{\n\t\t\tnamespaceString: \"abc.xyz:language:something\",\n\t\t\tresult:          NewNamespace(\"abc.xyz\", syftPkg.Language(\"something\"), \"\"),\n\t\t},\n\t\t{\n\t\t\tnamespaceString: \"abc.xyz:language:something:another-package-manager\",\n\t\t\tresult:          NewNamespace(\"abc.xyz\", syftPkg.Language(\"something\"), syftPkg.Type(\"another-package-manager\")),\n\t\t},\n\t}\n\n\tfor _, test := range successTests {\n\t\tresult, _ := FromString(test.namespaceString)\n\t\tassert.Equal(t, result, test.result)\n\t}\n\n\terrorTests := []struct {\n\t\tnamespaceString string\n\t\terrorMessage    string\n\t}{\n\t\t{\n\t\t\tnamespaceString: \"\",\n\t\t\terrorMessage:    \"unable to create language namespace from empty string\",\n\t\t},\n\t\t{\n\t\t\tnamespaceString: \"single-component\",\n\t\t\terrorMessage:    \"unable to create language namespace from single-component: incorrect number of components\",\n\t\t},\n\t\t{\n\t\t\tnamespaceString: \"two:components\",\n\t\t\terrorMessage:    \"unable to create language namespace from two:components: incorrect number of components\",\n\t\t},\n\t\t{\n\t\t\tnamespaceString: \"too:many:components:a:b\",\n\t\t\terrorMessage:    \"unable to create language namespace from too:many:components:a:b: incorrect number of components\",\n\t\t},\n\t\t{\n\t\t\tnamespaceString: \"wrong:namespace_type:a:b\",\n\t\t\terrorMessage:    \"unable to create language namespace from wrong:namespace_type:a:b: type namespace_type is incorrect\",\n\t\t},\n\t}\n\n\tfor _, test := range errorTests {\n\t\t_, err := FromString(test.namespaceString)\n\t\tassert.EqualError(t, err, test.errorMessage)\n\t}\n}\n"
  },
  {
    "path": "grype/db/v5/namespace/namespace.go",
    "content": "package namespace\n\nimport (\n\t\"github.com/anchore/grype/grype/db/v5/pkg/resolver\"\n)\n\ntype Namespace interface {\n\tProvider() string\n\tResolver() resolver.Resolver\n\tString() string\n}\n"
  },
  {
    "path": "grype/db/v5/pkg/qualifier/from_json.go",
    "content": "package qualifier\n\nimport (\n\t\"encoding/json\"\n\n\t\"github.com/go-viper/mapstructure/v2\"\n\n\t\"github.com/anchore/grype/grype/db/v5/pkg/qualifier/platformcpe\"\n\t\"github.com/anchore/grype/grype/db/v5/pkg/qualifier/rpmmodularity\"\n\t\"github.com/anchore/grype/internal/log\"\n)\n\nfunc FromJSON(data []byte) ([]Qualifier, error) {\n\tvar records []map[string]interface{}\n\tif err := json.Unmarshal(data, &records); err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar qualifiers []Qualifier\n\n\tfor _, r := range records {\n\t\tk, ok := r[\"kind\"]\n\n\t\tif !ok {\n\t\t\tlog.Warn(\"Skipping qualifier with no kind specified\")\n\t\t\tcontinue\n\t\t}\n\n\t\t// create the specific kind of Qualifier\n\t\tswitch k {\n\t\tcase \"rpm-modularity\":\n\t\t\tvar q rpmmodularity.Qualifier\n\t\t\tif err := mapstructure.Decode(r, &q); err != nil {\n\t\t\t\tlog.Warn(\"Error decoding rpm-modularity package qualifier:  (%v)\", err)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tqualifiers = append(qualifiers, q)\n\t\tcase \"platform-cpe\":\n\t\t\tvar q platformcpe.Qualifier\n\t\t\tif err := mapstructure.Decode(r, &q); err != nil {\n\t\t\t\tlog.Warn(\"Error decoding platform-cpe package qualifier:  (%v)\", err)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tqualifiers = append(qualifiers, q)\n\t\tdefault:\n\t\t\tlog.Debug(\"Skipping unsupported package qualifier: %s\", k)\n\t\t\tcontinue\n\t\t}\n\t}\n\n\treturn qualifiers, nil\n}\n"
  },
  {
    "path": "grype/db/v5/pkg/qualifier/platformcpe/qualifier.go",
    "content": "package platformcpe\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/anchore/grype/grype/pkg/qualifier\"\n\t\"github.com/anchore/grype/grype/pkg/qualifier/platformcpe\"\n)\n\ntype Qualifier struct {\n\tKind string `json:\"kind\" mapstructure:\"kind\"`                   // Kind of qualifier\n\tCPE  string `json:\"cpe,omitempty\" mapstructure:\"cpe,omitempty\"` // CPE\n}\n\nfunc (q Qualifier) Parse() qualifier.Qualifier {\n\treturn platformcpe.New(q.CPE)\n}\n\nfunc (q Qualifier) String() string {\n\treturn fmt.Sprintf(\"kind: %s, cpe: %q\", q.Kind, q.CPE)\n}\n"
  },
  {
    "path": "grype/db/v5/pkg/qualifier/qualifier.go",
    "content": "package qualifier\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/anchore/grype/grype/pkg/qualifier\"\n)\n\ntype Qualifier interface {\n\tfmt.Stringer\n\tParse() qualifier.Qualifier\n}\n"
  },
  {
    "path": "grype/db/v5/pkg/qualifier/rpmmodularity/qualifier.go",
    "content": "package rpmmodularity\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/anchore/grype/grype/pkg/qualifier\"\n\t\"github.com/anchore/grype/grype/pkg/qualifier/rpmmodularity\"\n)\n\ntype Qualifier struct {\n\tKind   string `json:\"kind\" mapstructure:\"kind\"`                         // Kind of qualifier\n\tModule string `json:\"module,omitempty\" mapstructure:\"module,omitempty\"` // Modularity label\n}\n\nfunc (q Qualifier) Parse() qualifier.Qualifier {\n\treturn rpmmodularity.New(q.Module)\n}\n\nfunc (q Qualifier) String() string {\n\treturn fmt.Sprintf(\"kind: %s, module: %q\", q.Kind, q.Module)\n}\n"
  },
  {
    "path": "grype/db/v5/pkg/resolver/java/resolver.go",
    "content": "package java\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\tgrypePkg \"github.com/anchore/grype/grype/pkg\"\n\t\"github.com/anchore/grype/internal/log\"\n\t\"github.com/anchore/grype/internal/stringutil\"\n\t\"github.com/anchore/packageurl-go\"\n)\n\ntype Resolver struct {\n}\n\nfunc (r *Resolver) Normalize(name string) string {\n\treturn strings.ToLower(name)\n}\n\nfunc (r *Resolver) Resolve(p grypePkg.Package) []string {\n\tnames := stringutil.NewStringSet()\n\n\t// The current default for the Java ecosystem is to use a Maven-like identifier of the form\n\t// \"<group-name>:<artifact-name>\"\n\tif metadata, ok := p.Metadata.(grypePkg.JavaMetadata); ok {\n\t\tif metadata.PomGroupID != \"\" {\n\t\t\tif metadata.PomArtifactID != \"\" {\n\t\t\t\tnames.Add(r.Normalize(fmt.Sprintf(\"%s:%s\", metadata.PomGroupID, metadata.PomArtifactID)))\n\t\t\t}\n\t\t\tif metadata.ManifestName != \"\" {\n\t\t\t\tnames.Add(r.Normalize(fmt.Sprintf(\"%s:%s\", metadata.PomGroupID, metadata.ManifestName)))\n\t\t\t}\n\t\t}\n\t}\n\n\tif p.PURL != \"\" {\n\t\tpurl, err := packageurl.FromString(p.PURL)\n\t\tif err != nil {\n\t\t\tlog.Warnf(\"unable to resolve java package identifier from purl=%q: %+v\", p.PURL, err)\n\t\t} else {\n\t\t\tnames.Add(r.Normalize(fmt.Sprintf(\"%s:%s\", purl.Namespace, purl.Name)))\n\t\t}\n\t}\n\n\treturn names.ToSlice()\n}\n"
  },
  {
    "path": "grype/db/v5/pkg/resolver/java/resolver_test.go",
    "content": "package java\n\nimport (\n\t\"testing\"\n\n\t\"github.com/google/uuid\"\n\t\"github.com/stretchr/testify/assert\"\n\n\tgrypePkg \"github.com/anchore/grype/grype/pkg\"\n)\n\nfunc TestResolver_Normalize(t *testing.T) {\n\ttests := []struct {\n\t\tname       string\n\t\tnormalized string\n\t}{\n\t\t{\n\t\t\tname:       \"PyYAML\",\n\t\t\tnormalized: \"pyyaml\",\n\t\t},\n\t\t{\n\t\t\tname:       \"oslo.concurrency\",\n\t\t\tnormalized: \"oslo.concurrency\",\n\t\t},\n\t\t{\n\t\t\tname:       \"\",\n\t\t\tnormalized: \"\",\n\t\t},\n\t\t{\n\t\t\tname:       \"test---1\",\n\t\t\tnormalized: \"test---1\",\n\t\t},\n\t\t{\n\t\t\tname:       \"AbCd.-__.--.-___.__.--1234____----....XyZZZ\",\n\t\t\tnormalized: \"abcd.-__.--.-___.__.--1234____----....xyzzz\",\n\t\t},\n\t}\n\n\tresolver := Resolver{}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tresolvedNames := resolver.Normalize(test.name)\n\t\t\tassert.Equal(t, resolvedNames, test.normalized)\n\t\t})\n\t}\n}\n\nfunc TestResolver_Resolve(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tpkg      grypePkg.Package\n\t\tresolved []string\n\t}{\n\t\t{\n\t\t\tname: \"both artifact and manifest 1\",\n\t\t\tpkg: grypePkg.Package{\n\t\t\t\tName:     \"ABCD\",\n\t\t\t\tVersion:  \"1.2.3.4\",\n\t\t\t\tLanguage: \"java\",\n\t\t\t\tMetadata: grypePkg.JavaMetadata{\n\t\t\t\t\tVirtualPath:   \"virtual-path-info\",\n\t\t\t\t\tPomArtifactID: \"pom-ARTIFACT-ID-info\",\n\t\t\t\t\tPomGroupID:    \"pom-group-ID-info\",\n\t\t\t\t\tManifestName:  \"main-section-name-info\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tresolved: []string{\"pom-group-id-info:pom-artifact-id-info\", \"pom-group-id-info:main-section-name-info\"},\n\t\t},\n\t\t{\n\t\t\tname: \"both artifact and manifest 2\",\n\t\t\tpkg: grypePkg.Package{\n\t\t\t\tID:   grypePkg.ID(uuid.NewString()),\n\t\t\t\tName: \"a-name\",\n\t\t\t\tMetadata: grypePkg.JavaMetadata{\n\t\t\t\t\tVirtualPath:   \"v-path\",\n\t\t\t\t\tPomArtifactID: \"art-id\",\n\t\t\t\t\tPomGroupID:    \"g-id\",\n\t\t\t\t\tManifestName:  \"man-name\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tresolved: []string{\n\t\t\t\t\"g-id:art-id\",\n\t\t\t\t\"g-id:man-name\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"no group id\",\n\t\t\tpkg: grypePkg.Package{\n\t\t\t\tID:   grypePkg.ID(uuid.NewString()),\n\t\t\t\tName: \"a-name\",\n\t\t\t\tMetadata: grypePkg.JavaMetadata{\n\t\t\t\t\tVirtualPath:   \"v-path\",\n\t\t\t\t\tPomArtifactID: \"art-id\",\n\t\t\t\t\tManifestName:  \"man-name\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tresolved: []string{},\n\t\t},\n\t\t{\n\t\t\tname: \"only manifest\",\n\t\t\tpkg: grypePkg.Package{\n\t\t\t\tID:   grypePkg.ID(uuid.NewString()),\n\t\t\t\tName: \"a-name\",\n\t\t\t\tMetadata: grypePkg.JavaMetadata{\n\t\t\t\t\tVirtualPath:  \"v-path\",\n\t\t\t\t\tPomGroupID:   \"g-id\",\n\t\t\t\t\tManifestName: \"man-name\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tresolved: []string{\n\t\t\t\t\"g-id:man-name\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"only artifact\",\n\t\t\tpkg: grypePkg.Package{\n\t\t\t\tID:   grypePkg.ID(uuid.NewString()),\n\t\t\t\tName: \"a-name\",\n\t\t\t\tMetadata: grypePkg.JavaMetadata{\n\t\t\t\t\tVirtualPath:   \"v-path\",\n\t\t\t\t\tPomArtifactID: \"art-id\",\n\t\t\t\t\tPomGroupID:    \"g-id\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tresolved: []string{\n\t\t\t\t\"g-id:art-id\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"no artifact or manifest\",\n\t\t\tpkg: grypePkg.Package{\n\t\t\t\tID:   grypePkg.ID(uuid.NewString()),\n\t\t\t\tName: \"a-name\",\n\t\t\t\tMetadata: grypePkg.JavaMetadata{\n\t\t\t\t\tVirtualPath: \"v-path\",\n\t\t\t\t\tPomGroupID:  \"g-id\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tresolved: []string{},\n\t\t},\n\t\t{\n\t\t\tname: \"with valid purl\",\n\t\t\tpkg: grypePkg.Package{\n\t\t\t\tID:   grypePkg.ID(uuid.NewString()),\n\t\t\t\tName: \"a-name\",\n\t\t\t\tPURL: \"pkg:maven/org.anchore/b-name@0.2\",\n\t\t\t},\n\t\t\tresolved: []string{\"org.anchore:b-name\"},\n\t\t},\n\t\t{\n\t\t\tname: \"ignore invalid pURLs\",\n\t\t\tpkg: grypePkg.Package{\n\t\t\t\tID:   grypePkg.ID(uuid.NewString()),\n\t\t\t\tName: \"a-name\",\n\t\t\t\tPURL: \"pkg:BAD/\",\n\t\t\t\tMetadata: grypePkg.JavaMetadata{\n\t\t\t\t\tVirtualPath:   \"v-path\",\n\t\t\t\t\tPomArtifactID: \"art-id\",\n\t\t\t\t\tPomGroupID:    \"g-id\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tresolved: []string{\n\t\t\t\t\"g-id:art-id\",\n\t\t\t},\n\t\t},\n\t}\n\n\tresolver := Resolver{}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tresolvedNames := resolver.Resolve(test.pkg)\n\t\t\tassert.ElementsMatch(t, resolvedNames, test.resolved)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/db/v5/pkg/resolver/python/resolver.go",
    "content": "package python\n\nimport (\n\t\"regexp\"\n\t\"strings\"\n\n\tgrypePkg \"github.com/anchore/grype/grype/pkg\"\n)\n\ntype Resolver struct {\n}\n\nfunc (r *Resolver) Normalize(name string) string {\n\t// Canonical naming of packages within python is defined by PEP 503 at\n\t// https://peps.python.org/pep-0503/#normalized-names, and this code is derived from\n\t// the official python implementation of canonical naming at\n\t// https://packaging.pypa.io/en/latest/_modules/packaging/utils.html#canonicalize_name\n\n\treturn strings.ToLower(regexp.MustCompile(`[-_.]+`).ReplaceAllString(name, \"-\"))\n}\n\nfunc (r *Resolver) Resolve(p grypePkg.Package) []string {\n\t// Canonical naming of packages within python is defined by PEP 503 at\n\t// https://peps.python.org/pep-0503/#normalized-names, and this code is derived from\n\t// the official python implementation of canonical naming at\n\t// https://packaging.pypa.io/en/latest/_modules/packaging/utils.html#canonicalize_name\n\n\treturn []string{r.Normalize(p.Name)}\n}\n"
  },
  {
    "path": "grype/db/v5/pkg/resolver/python/resolver_test.go",
    "content": "package python\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestResolver_Normalize(t *testing.T) {\n\ttests := []struct {\n\t\tname       string\n\t\tnormalized string\n\t}{\n\t\t{\n\t\t\tname:       \"PyYAML\",\n\t\t\tnormalized: \"pyyaml\",\n\t\t},\n\t\t{\n\t\t\tname:       \"oslo.concurrency\",\n\t\t\tnormalized: \"oslo-concurrency\",\n\t\t},\n\t\t{\n\t\t\tname:       \"\",\n\t\t\tnormalized: \"\",\n\t\t},\n\t\t{\n\t\t\tname:       \"test---1\",\n\t\t\tnormalized: \"test-1\",\n\t\t},\n\t\t{\n\t\t\tname:       \"AbCd.-__.--.-___.__.--1234____----....XyZZZ\",\n\t\t\tnormalized: \"abcd-1234-xyzzz\",\n\t\t},\n\t}\n\n\tresolver := Resolver{}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tresolvedNames := resolver.Normalize(test.name)\n\t\t\tassert.Equal(t, resolvedNames, test.normalized)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/db/v5/pkg/resolver/resolver.go",
    "content": "package resolver\n\nimport (\n\t\"github.com/anchore/grype/grype/db/v5/pkg/resolver/java\"\n\t\"github.com/anchore/grype/grype/db/v5/pkg/resolver/python\"\n\t\"github.com/anchore/grype/grype/db/v5/pkg/resolver/stock\"\n\tgrypePkg \"github.com/anchore/grype/grype/pkg\"\n\tsyftPkg \"github.com/anchore/syft/syft/pkg\"\n)\n\ntype Resolver interface {\n\tNormalize(string) string\n\tResolve(p grypePkg.Package) []string\n}\n\nfunc FromLanguage(language syftPkg.Language) (Resolver, error) {\n\tvar r Resolver\n\n\tswitch language {\n\tcase syftPkg.Python:\n\t\tr = &python.Resolver{}\n\tcase syftPkg.Java:\n\t\tr = &java.Resolver{}\n\tdefault:\n\t\tr = &stock.Resolver{}\n\t}\n\n\treturn r, nil\n}\n\nfunc PackageNames(p grypePkg.Package) []string {\n\tnames := []string{p.Name}\n\tr, _ := FromLanguage(p.Language)\n\tif r != nil {\n\t\tparts := r.Resolve(p)\n\t\tif len(parts) > 0 {\n\t\t\tnames = parts\n\t\t}\n\t}\n\treturn names\n}\n"
  },
  {
    "path": "grype/db/v5/pkg/resolver/resolver_test.go",
    "content": "package resolver\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\n\t\"github.com/anchore/grype/grype/db/v5/pkg/resolver/java\"\n\t\"github.com/anchore/grype/grype/db/v5/pkg/resolver/python\"\n\t\"github.com/anchore/grype/grype/db/v5/pkg/resolver/stock\"\n\tsyftPkg \"github.com/anchore/syft/syft/pkg\"\n)\n\nfunc TestFromLanguage(t *testing.T) {\n\ttests := []struct {\n\t\tlanguage syftPkg.Language\n\t\tresult   Resolver\n\t}{\n\t\t{\n\t\t\tlanguage: syftPkg.Python,\n\t\t\tresult:   &python.Resolver{},\n\t\t},\n\t\t{\n\t\t\tlanguage: syftPkg.Java,\n\t\t\tresult:   &java.Resolver{},\n\t\t},\n\t\t{\n\t\t\tlanguage: syftPkg.Ruby,\n\t\t\tresult:   &stock.Resolver{},\n\t\t},\n\t\t{\n\t\t\tlanguage: syftPkg.Dart,\n\t\t\tresult:   &stock.Resolver{},\n\t\t},\n\t\t{\n\t\t\tlanguage: syftPkg.Rust,\n\t\t\tresult:   &stock.Resolver{},\n\t\t},\n\t\t{\n\t\t\tlanguage: syftPkg.Go,\n\t\t\tresult:   &stock.Resolver{},\n\t\t},\n\t\t{\n\t\t\tlanguage: syftPkg.JavaScript,\n\t\t\tresult:   &stock.Resolver{},\n\t\t},\n\t\t{\n\t\t\tlanguage: syftPkg.Dotnet,\n\t\t\tresult:   &stock.Resolver{},\n\t\t},\n\t\t{\n\t\t\tlanguage: syftPkg.PHP,\n\t\t\tresult:   &stock.Resolver{},\n\t\t},\n\t\t{\n\t\t\tlanguage: syftPkg.Ruby,\n\t\t\tresult:   &stock.Resolver{},\n\t\t},\n\t\t{\n\t\t\tlanguage: syftPkg.Language(\"something-new\"),\n\t\t\tresult:   &stock.Resolver{},\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tresult, err := FromLanguage(test.language)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, result, test.result)\n\t}\n}\n"
  },
  {
    "path": "grype/db/v5/pkg/resolver/stock/resolver.go",
    "content": "package stock\n\nimport (\n\t\"strings\"\n\n\tgrypePkg \"github.com/anchore/grype/grype/pkg\"\n)\n\ntype Resolver struct {\n}\n\nfunc (r *Resolver) Normalize(name string) string {\n\treturn strings.ToLower(name)\n}\n\nfunc (r *Resolver) Resolve(p grypePkg.Package) []string {\n\treturn []string{r.Normalize(p.Name)}\n}\n"
  },
  {
    "path": "grype/db/v5/pkg/resolver/stock/resolver_test.go",
    "content": "package stock\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestResolver_Normalize(t *testing.T) {\n\ttests := []struct {\n\t\tpackageName string\n\t\tnormalized  string\n\t}{\n\t\t{\n\t\t\tpackageName: \"PyYAML\",\n\t\t\tnormalized:  \"pyyaml\",\n\t\t},\n\t\t{\n\t\t\tpackageName: \"oslo.concurrency\",\n\t\t\tnormalized:  \"oslo.concurrency\",\n\t\t},\n\t\t{\n\t\t\tpackageName: \"\",\n\t\t\tnormalized:  \"\",\n\t\t},\n\t\t{\n\t\t\tpackageName: \"test---1\",\n\t\t\tnormalized:  \"test---1\",\n\t\t},\n\t\t{\n\t\t\tpackageName: \"AbCd.-__.--.-___.__.--1234____----....XyZZZ\",\n\t\t\tnormalized:  \"abcd.-__.--.-___.__.--1234____----....xyzzz\",\n\t\t},\n\t}\n\n\tresolver := Resolver{}\n\n\tfor _, test := range tests {\n\t\tresolvedNames := resolver.Normalize(test.packageName)\n\t\tassert.Equal(t, resolvedNames, test.normalized)\n\t}\n}\n"
  },
  {
    "path": "grype/db/v5/provider_store.go",
    "content": "package v5\n\nimport (\n\t\"github.com/anchore/grype/grype/match\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n)\n\ntype ProviderStore struct {\n\tvulnerability.Provider\n\tmatch.ExclusionProvider\n}\n"
  },
  {
    "path": "grype/db/v5/schema_version.go",
    "content": "package v5\n\nconst SchemaVersion = 5\n"
  },
  {
    "path": "grype/db/v5/store/diff.go",
    "content": "package store\n\nimport (\n\t\"github.com/wagoodman/go-partybus\"\n\t\"github.com/wagoodman/go-progress\"\n\n\tv5 \"github.com/anchore/grype/grype/db/v5\"\n\t\"github.com/anchore/grype/grype/event\"\n\t\"github.com/anchore/grype/grype/event/monitor\"\n\t\"github.com/anchore/grype/internal/bus\"\n)\n\ntype storeKey struct {\n\tid          string\n\tnamespace   string\n\tpackageName string\n}\n\ntype PkgMap = map[storeKey][]string\n\ntype storeVulnerabilityList struct {\n\titems map[storeKey][]storeVulnerability\n\tseen  bool\n}\ntype storeVulnerability struct {\n\titem *v5.Vulnerability\n\tseen bool\n}\ntype storeMetadata struct {\n\titem *v5.VulnerabilityMetadata\n\tseen bool\n}\n\n// create manual progress bars for tracking the database diff's progress\nfunc trackDiff(total int64) (*progress.Manual, *progress.Manual, *progress.Stage) {\n\tstageProgress := &progress.Manual{}\n\tstageProgress.SetTotal(total)\n\tdifferencesDiscovered := &progress.Manual{}\n\tstager := &progress.Stage{}\n\n\tbus.Publish(partybus.Event{\n\t\tType: event.DatabaseDiffingStarted,\n\t\tValue: monitor.DBDiff{\n\t\t\tStager:                stager,\n\t\t\tStageProgress:         progress.Progressable(stageProgress),\n\t\t\tDifferencesDiscovered: progress.Monitorable(differencesDiscovered),\n\t\t},\n\t})\n\treturn stageProgress, differencesDiscovered, stager\n}\n\n// creates a map from an unpackaged key to a list of all packages associated with it\nfunc buildVulnerabilityPkgsMap(models *[]v5.Vulnerability) *map[storeKey][]string {\n\tstoreMap := make(map[storeKey][]string)\n\tfor _, m := range *models {\n\t\tmodel := m\n\t\tk := getVulnerabilityParentKey(model)\n\t\tif storeVuln, exists := storeMap[k]; exists {\n\t\t\tstoreMap[k] = append(storeVuln, model.PackageName)\n\t\t} else {\n\t\t\tstoreMap[k] = []string{model.PackageName}\n\t\t}\n\t}\n\treturn &storeMap\n}\n\n// creates a diff from the given key using the package maps information to populate\n// the relevant packages affected by the update\nfunc createDiff(baseStore, targetStore *PkgMap, key storeKey, reason v5.DiffReason) *v5.Diff {\n\tpkgMap := make(map[string]struct{})\n\n\tkey.packageName = \"\"\n\tif baseStore != nil {\n\t\tif basePkgs, exists := (*baseStore)[key]; exists {\n\t\t\tfor _, pkg := range basePkgs {\n\t\t\t\tpkgMap[pkg] = struct{}{}\n\t\t\t}\n\t\t}\n\t}\n\tif targetStore != nil {\n\t\tif targetPkgs, exists := (*targetStore)[key]; exists {\n\t\t\tfor _, pkg := range targetPkgs {\n\t\t\t\tpkgMap[pkg] = struct{}{}\n\t\t\t}\n\t\t}\n\t}\n\tpkgs := []string{}\n\tfor pkg := range pkgMap {\n\t\tpkgs = append(pkgs, pkg)\n\t}\n\n\treturn &v5.Diff{\n\t\tReason:    reason,\n\t\tID:        key.id,\n\t\tNamespace: key.namespace,\n\t\tPackages:  pkgs,\n\t}\n}\n\n// gets an unpackaged key from a vulnerability\nfunc getVulnerabilityParentKey(vuln v5.Vulnerability) storeKey {\n\treturn storeKey{vuln.ID, vuln.Namespace, \"\"}\n}\n\n// gets a packaged key from a vulnerability\nfunc getVulnerabilityKey(vuln v5.Vulnerability) storeKey {\n\treturn storeKey{vuln.ID, vuln.Namespace, vuln.PackageName}\n}\n\ntype VulnerabilitySet struct {\n\tdata map[storeKey]*storeVulnerabilityList\n}\n\nfunc NewVulnerabilitySet(models *[]v5.Vulnerability) *VulnerabilitySet {\n\tm := make(map[storeKey]*storeVulnerabilityList, len(*models))\n\tfor _, mm := range *models {\n\t\tmodel := mm\n\t\tparentKey := getVulnerabilityParentKey(model)\n\t\tvulnKey := getVulnerabilityKey(model)\n\t\tif storeVuln, exists := m[parentKey]; exists {\n\t\t\tif kk, exists := storeVuln.items[vulnKey]; exists {\n\t\t\t\tstoreVuln.items[vulnKey] = append(kk, storeVulnerability{\n\t\t\t\t\titem: &model,\n\t\t\t\t\tseen: false,\n\t\t\t\t})\n\t\t\t} else {\n\t\t\t\tstoreVuln.items[vulnKey] = []storeVulnerability{{&model, false}}\n\t\t\t}\n\t\t} else {\n\t\t\tvuln := storeVulnerabilityList{\n\t\t\t\titems: make(map[storeKey][]storeVulnerability),\n\t\t\t\tseen:  false,\n\t\t\t}\n\t\t\tvuln.items[vulnKey] = []storeVulnerability{{&model, false}}\n\t\t\tm[parentKey] = &vuln\n\t\t}\n\t}\n\treturn &VulnerabilitySet{\n\t\tdata: m,\n\t}\n}\n\nfunc (v *VulnerabilitySet) in(item v5.Vulnerability) bool {\n\t_, exists := v.data[getVulnerabilityParentKey(item)]\n\treturn exists\n}\n\nfunc (v *VulnerabilitySet) match(item v5.Vulnerability) bool {\n\tif parent, exists := v.data[getVulnerabilityParentKey(item)]; exists {\n\t\tparent.seen = true\n\t\tkey := getVulnerabilityKey(item)\n\t\tif children, exists := parent.items[key]; exists {\n\t\t\tfor idx, child := range children {\n\t\t\t\tif item.Equal(*child.item) {\n\t\t\t\t\tchildren[idx].seen = true\n\t\t\t\t\treturn true\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn false\n}\n\nfunc (v *VulnerabilitySet) getUnmatched() ([]storeKey, []storeKey) {\n\tnotSeen := []storeKey{}\n\tnotEntirelySeen := []storeKey{}\n\tfor k, item := range v.data {\n\t\tif !item.seen {\n\t\t\tnotSeen = append(notSeen, k)\n\t\t\tcontinue\n\t\t}\n\tcomponentLoop:\n\t\tfor _, components := range item.items {\n\t\t\tfor _, component := range components {\n\t\t\t\tif !component.seen {\n\t\t\t\t\tnotEntirelySeen = append(notEntirelySeen, k)\n\t\t\t\t\tbreak componentLoop\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn notSeen, notEntirelySeen\n}\n\nfunc diffVulnerabilities(baseModels, targetModels *[]v5.Vulnerability, basePkgsMap, targetPkgsMap *PkgMap, differentItems *progress.Manual) *map[string]*v5.Diff {\n\tdiffs := make(map[string]*v5.Diff)\n\tm := NewVulnerabilitySet(baseModels)\n\n\tfor _, tModel := range *targetModels {\n\t\ttargetModel := tModel\n\t\tk := getVulnerabilityKey(targetModel)\n\t\tif m.in(targetModel) {\n\t\t\tmatched := m.match(targetModel)\n\t\t\tif !matched {\n\t\t\t\tif _, exists := diffs[k.id+k.namespace]; exists {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tdiffs[k.id+k.namespace] = createDiff(basePkgsMap, targetPkgsMap, k, v5.DiffChanged)\n\t\t\t\tdifferentItems.Increment()\n\t\t\t}\n\t\t} else {\n\t\t\tif _, exists := diffs[k.id+k.namespace]; exists {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tdiffs[k.id+k.namespace] = createDiff(nil, targetPkgsMap, k, v5.DiffAdded)\n\t\t\tdifferentItems.Increment()\n\t\t}\n\t}\n\tnotSeen, partialSeen := m.getUnmatched()\n\tfor _, k := range partialSeen {\n\t\tif _, exists := diffs[k.id+k.namespace]; exists {\n\t\t\tcontinue\n\t\t}\n\t\tdiffs[k.id+k.namespace] = createDiff(basePkgsMap, targetPkgsMap, k, v5.DiffChanged)\n\t\tdifferentItems.Increment()\n\t}\n\tfor _, k := range notSeen {\n\t\tif _, exists := diffs[k.id+k.namespace]; exists {\n\t\t\tcontinue\n\t\t}\n\t\tdiffs[k.id+k.namespace] = createDiff(basePkgsMap, nil, k, v5.DiffRemoved)\n\t\tdifferentItems.Increment()\n\t}\n\n\treturn &diffs\n}\n\ntype MetadataSet struct {\n\tdata map[storeKey]*storeMetadata\n}\n\nfunc NewMetadataSet(models *[]v5.VulnerabilityMetadata) *MetadataSet {\n\tm := make(map[storeKey]*storeMetadata, len(*models))\n\tfor _, mm := range *models {\n\t\tmodel := mm\n\t\tm[getMetadataKey(model)] = &storeMetadata{\n\t\t\titem: &model,\n\t\t\tseen: false,\n\t\t}\n\t}\n\treturn &MetadataSet{\n\t\tdata: m,\n\t}\n}\n\nfunc (v *MetadataSet) in(item v5.VulnerabilityMetadata) bool {\n\t_, exists := v.data[getMetadataKey(item)]\n\treturn exists\n}\n\nfunc (v *MetadataSet) match(item v5.VulnerabilityMetadata) bool {\n\tif baseModel, exists := v.data[getMetadataKey(item)]; exists {\n\t\tbaseModel.seen = true\n\t\treturn baseModel.item.Equal(item)\n\t}\n\treturn false\n}\n\nfunc (v *MetadataSet) getUnmatched() []storeKey {\n\tnotSeen := []storeKey{}\n\tfor k, item := range v.data {\n\t\tif !item.seen {\n\t\t\tnotSeen = append(notSeen, k)\n\t\t}\n\t}\n\treturn notSeen\n}\n\nfunc diffVulnerabilityMetadata(baseModels, targetModels *[]v5.VulnerabilityMetadata, basePkgsMap, targetPkgsMap *PkgMap, differentItems *progress.Manual) *map[string]*v5.Diff {\n\tdiffs := make(map[string]*v5.Diff)\n\tm := NewMetadataSet(baseModels)\n\n\tfor _, tModel := range *targetModels {\n\t\ttargetModel := tModel\n\t\tk := getMetadataKey(targetModel)\n\t\tif m.in(targetModel) {\n\t\t\tif !m.match(targetModel) {\n\t\t\t\tif _, exists := diffs[k.id+k.namespace]; exists {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tdiffs[k.id+k.namespace] = createDiff(basePkgsMap, targetPkgsMap, k, v5.DiffChanged)\n\t\t\t\tdifferentItems.Increment()\n\t\t\t}\n\t\t} else {\n\t\t\tif _, exists := diffs[k.id+k.namespace]; exists {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tdiffs[k.id+k.namespace] = createDiff(nil, targetPkgsMap, k, v5.DiffAdded)\n\t\t\tdifferentItems.Increment()\n\t\t}\n\t}\n\tfor _, k := range m.getUnmatched() {\n\t\tif _, exists := diffs[k.id+k.namespace]; exists {\n\t\t\tcontinue\n\t\t}\n\t\tdiffs[k.id+k.namespace] = createDiff(basePkgsMap, nil, k, v5.DiffRemoved)\n\t\tdifferentItems.Increment()\n\t}\n\n\treturn &diffs\n}\n\nfunc getMetadataKey(metadata v5.VulnerabilityMetadata) storeKey {\n\treturn storeKey{metadata.ID, metadata.Namespace, \"\"}\n}\n"
  },
  {
    "path": "grype/db/v5/store/diff_test.go",
    "content": "package store\n\nimport (\n\t\"os\"\n\t\"sort\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\n\tv5 \"github.com/anchore/grype/grype/db/v5\"\n)\n\nfunc Test_GetAllVulnerabilities(t *testing.T) {\n\t//GIVEN\n\tdbTempFile := t.TempDir()\n\n\ts, err := New(dbTempFile, true)\n\tif err != nil {\n\t\tt.Fatalf(\"could not create store: %+v\", err)\n\t}\n\n\t//WHEN\n\tresult, err := s.GetAllVulnerabilities()\n\n\t//THEN\n\tassert.NotNil(t, result)\n\tassert.NoError(t, err)\n}\n\nfunc Test_GetAllVulnerabilityMetadata(t *testing.T) {\n\t//GIVEN\n\tdbTempFile := t.TempDir()\n\n\tdefer os.Remove(dbTempFile)\n\n\ts, err := New(dbTempFile, true)\n\tif err != nil {\n\t\tt.Fatalf(\"could not create store: %+v\", err)\n\t}\n\n\t//WHEN\n\tresult, err := s.GetAllVulnerabilityMetadata()\n\n\t//THEN\n\tassert.NotNil(t, result)\n\tassert.NoError(t, err)\n}\n\nfunc Test_Diff_Vulnerabilities(t *testing.T) {\n\t//GIVEN\n\tdbTempFile := t.TempDir()\n\n\ts1, err := New(dbTempFile, true)\n\tif err != nil {\n\t\tt.Fatalf(\"could not create store: %+v\", err)\n\t}\n\tdbTempFile = t.TempDir()\n\n\ts2, err := New(dbTempFile, true)\n\tif err != nil {\n\t\tt.Fatalf(\"could not create store: %+v\", err)\n\t}\n\n\tbaseVulns := []v5.Vulnerability{\n\t\t{\n\t\t\tNamespace:         \"github:language:python\",\n\t\t\tID:                \"CVE-123-4567\",\n\t\t\tPackageName:       \"pypi:requests\",\n\t\t\tVersionConstraint: \"< 2.0 >= 1.29\",\n\t\t\tCPEs:              []string{\"cpe:2.3:pypi:requests:*:*:*:*:*:*\"},\n\t\t},\n\t\t{\n\t\t\tNamespace:         \"github:language:python\",\n\t\t\tID:                \"CVE-123-4567\",\n\t\t\tPackageName:       \"pypi:requests\",\n\t\t\tVersionConstraint: \"< 3.0 >= 2.17\",\n\t\t\tCPEs:              []string{\"cpe:2.3:pypi:requests:*:*:*:*:*:*\"},\n\t\t},\n\t\t{\n\t\t\tNamespace:         \"npm\",\n\t\t\tID:                \"CVE-123-7654\",\n\t\t\tPackageName:       \"npm:axios\",\n\t\t\tVersionConstraint: \"< 3.0 >= 2.17\",\n\t\t\tCPEs:              []string{\"cpe:2.3:npm:axios:*:*:*:*:*:*\"},\n\t\t\tFix: v5.Fix{\n\t\t\t\tState: v5.UnknownFixState,\n\t\t\t},\n\t\t},\n\t}\n\ttargetVulns := []v5.Vulnerability{\n\t\t{\n\t\t\tNamespace:         \"github:language:python\",\n\t\t\tID:                \"CVE-123-4567\",\n\t\t\tPackageName:       \"pypi:requests\",\n\t\t\tVersionConstraint: \"< 2.0 >= 1.29\",\n\t\t\tCPEs:              []string{\"cpe:2.3:pypi:requests:*:*:*:*:*:*\"},\n\t\t},\n\t\t{\n\t\t\tNamespace:         \"github:language:go\",\n\t\t\tID:                \"GHSA-....-....\",\n\t\t\tPackageName:       \"hashicorp:nomad\",\n\t\t\tVersionConstraint: \"< 3.0 >= 2.17\",\n\t\t\tCPEs:              []string{\"cpe:2.3:golang:hashicorp:nomad:*:*:*:*:*\"},\n\t\t},\n\t\t{\n\t\t\tNamespace:         \"npm\",\n\t\t\tID:                \"CVE-123-7654\",\n\t\t\tPackageName:       \"npm:axios\",\n\t\t\tVersionConstraint: \"< 3.0 >= 2.17\",\n\t\t\tCPEs:              []string{\"cpe:2.3:npm:axios:*:*:*:*:*:*\"},\n\t\t\tFix: v5.Fix{\n\t\t\t\tState: v5.WontFixState,\n\t\t\t},\n\t\t},\n\t}\n\texpectedDiffs := []v5.Diff{\n\t\t{\n\t\t\tReason:    v5.DiffChanged,\n\t\t\tID:        \"CVE-123-4567\",\n\t\t\tNamespace: \"github:language:python\",\n\t\t\tPackages:  []string{\"pypi:requests\"},\n\t\t},\n\t\t{\n\t\t\tReason:    v5.DiffChanged,\n\t\t\tID:        \"CVE-123-7654\",\n\t\t\tNamespace: \"npm\",\n\t\t\tPackages:  []string{\"npm:axios\"},\n\t\t},\n\t\t{\n\t\t\tReason:    v5.DiffAdded,\n\t\t\tID:        \"GHSA-....-....\",\n\t\t\tNamespace: \"github:language:go\",\n\t\t\tPackages:  []string{\"hashicorp:nomad\"},\n\t\t},\n\t}\n\n\tfor _, vuln := range baseVulns {\n\t\ts1.AddVulnerability(vuln)\n\t}\n\tfor _, vuln := range targetVulns {\n\t\ts2.AddVulnerability(vuln)\n\t}\n\n\t//WHEN\n\tresult, err := s1.DiffStore(s2)\n\tsort.SliceStable(*result, func(i, j int) bool {\n\t\treturn (*result)[i].ID < (*result)[j].ID\n\t})\n\n\t//THEN\n\tassert.NoError(t, err)\n\tassert.Equal(t, expectedDiffs, *result)\n}\n\nfunc Test_Diff_Metadata(t *testing.T) {\n\t//GIVEN\n\tdbTempFile := t.TempDir()\n\ts1, err := New(dbTempFile, true)\n\tif err != nil {\n\t\tt.Fatalf(\"could not create store: %+v\", err)\n\t}\n\tdbTempFile = t.TempDir()\n\ts2, err := New(dbTempFile, true)\n\tif err != nil {\n\t\tt.Fatalf(\"could not create store: %+v\", err)\n\t}\n\n\tbaseVulns := []v5.VulnerabilityMetadata{\n\t\t{\n\t\t\tNamespace:  \"github:language:python\",\n\t\t\tID:         \"CVE-123-4567\",\n\t\t\tDataSource: \"nvd\",\n\t\t},\n\t\t{\n\t\t\tNamespace:  \"github:language:python\",\n\t\t\tID:         \"CVE-123-4567\",\n\t\t\tDataSource: \"nvd\",\n\t\t},\n\t\t{\n\t\t\tNamespace:  \"npm\",\n\t\t\tID:         \"CVE-123-7654\",\n\t\t\tDataSource: \"nvd\",\n\t\t},\n\t}\n\ttargetVulns := []v5.VulnerabilityMetadata{\n\t\t{\n\t\t\tNamespace:  \"github:language:go\",\n\t\t\tID:         \"GHSA-....-....\",\n\t\t\tDataSource: \"nvd\",\n\t\t},\n\t\t{\n\t\t\tNamespace:  \"npm\",\n\t\t\tID:         \"CVE-123-7654\",\n\t\t\tDataSource: \"vulndb\",\n\t\t},\n\t}\n\texpectedDiffs := []v5.Diff{\n\t\t{\n\t\t\tReason:    v5.DiffRemoved,\n\t\t\tID:        \"CVE-123-4567\",\n\t\t\tNamespace: \"github:language:python\",\n\t\t\tPackages:  []string{},\n\t\t},\n\t\t{\n\t\t\tReason:    v5.DiffChanged,\n\t\t\tID:        \"CVE-123-7654\",\n\t\t\tNamespace: \"npm\",\n\t\t\tPackages:  []string{},\n\t\t},\n\t\t{\n\t\t\tReason:    v5.DiffAdded,\n\t\t\tID:        \"GHSA-....-....\",\n\t\t\tNamespace: \"github:language:go\",\n\t\t\tPackages:  []string{},\n\t\t},\n\t}\n\n\tfor _, vuln := range baseVulns {\n\t\ts1.AddVulnerabilityMetadata(vuln)\n\t}\n\tfor _, vuln := range targetVulns {\n\t\ts2.AddVulnerabilityMetadata(vuln)\n\t}\n\n\t//WHEN\n\tresult, err := s1.DiffStore(s2)\n\n\t//THEN\n\tsort.SliceStable(*result, func(i, j int) bool {\n\t\treturn (*result)[i].ID < (*result)[j].ID\n\t})\n\n\tassert.NoError(t, err)\n\tassert.Equal(t, expectedDiffs, *result)\n}\n"
  },
  {
    "path": "grype/db/v5/store/model/id.go",
    "content": "package model\n\nimport (\n\t\"fmt\"\n\t\"time\"\n\n\tv5 \"github.com/anchore/grype/grype/db/v5\"\n)\n\nconst (\n\tIDTableName = \"id\"\n)\n\ntype IDModel struct {\n\tBuildTimestamp string `gorm:\"column:build_timestamp\"`\n\tSchemaVersion  int    `gorm:\"column:schema_version\"`\n}\n\nfunc NewIDModel(id v5.ID) IDModel {\n\treturn IDModel{\n\t\tBuildTimestamp: id.BuildTimestamp.Format(time.RFC3339Nano),\n\t\tSchemaVersion:  id.SchemaVersion,\n\t}\n}\n\nfunc (IDModel) TableName() string {\n\treturn IDTableName\n}\n\nfunc (m *IDModel) Inflate() (v5.ID, error) {\n\tbuildTime, err := time.Parse(time.RFC3339Nano, m.BuildTimestamp)\n\tif err != nil {\n\t\treturn v5.ID{}, fmt.Errorf(\"unable to parse build timestamp (%+v): %w\", m.BuildTimestamp, err)\n\t}\n\n\treturn v5.ID{\n\t\tBuildTimestamp: buildTime,\n\t\tSchemaVersion:  m.SchemaVersion,\n\t}, nil\n}\n"
  },
  {
    "path": "grype/db/v5/store/model/vulnerability.go",
    "content": "package model\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\n\tsqlite \"github.com/anchore/grype/grype/db/internal/sqlite\"\n\tv5 \"github.com/anchore/grype/grype/db/v5\"\n\t\"github.com/anchore/grype/grype/db/v5/pkg/qualifier\"\n)\n\nconst (\n\tVulnerabilityTableName    = \"vulnerability\"\n\tGetVulnerabilityIndexName = \"get_vulnerability_index\"\n)\n\n// VulnerabilityModel is a struct used to serialize db.Vulnerability information into a sqlite3 DB.\ntype VulnerabilityModel struct {\n\tPK                     uint64            `gorm:\"primary_key;auto_increment;\"`\n\tID                     string            `gorm:\"column:id\"`\n\tPackageName            string            `gorm:\"column:package_name; index:get_vulnerability_index\"`\n\tNamespace              string            `gorm:\"column:namespace; index:get_vulnerability_index\"`\n\tPackageQualifiers      sqlite.NullString `gorm:\"column:package_qualifiers\"`\n\tVersionConstraint      string            `gorm:\"column:version_constraint\"`\n\tVersionFormat          string            `gorm:\"column:version_format\"`\n\tCPEs                   sqlite.NullString `gorm:\"column:cpes; default:null\"`\n\tRelatedVulnerabilities sqlite.NullString `gorm:\"column:related_vulnerabilities; default:null\"`\n\tFixedInVersions        sqlite.NullString `gorm:\"column:fixed_in_versions; default:null\"`\n\tFixState               string            `gorm:\"column:fix_state\"`\n\tAdvisories             sqlite.NullString `gorm:\"column:advisories; default:null\"`\n}\n\n// NewVulnerabilityModel generates a new model from a db.Vulnerability struct.\nfunc NewVulnerabilityModel(vulnerability v5.Vulnerability) VulnerabilityModel {\n\treturn VulnerabilityModel{\n\t\tID:                     vulnerability.ID,\n\t\tPackageName:            vulnerability.PackageName,\n\t\tNamespace:              vulnerability.Namespace,\n\t\tPackageQualifiers:      sqlite.ToNullString(vulnerability.PackageQualifiers),\n\t\tVersionConstraint:      vulnerability.VersionConstraint,\n\t\tVersionFormat:          vulnerability.VersionFormat,\n\t\tFixedInVersions:        sqlite.ToNullString(vulnerability.Fix.Versions),\n\t\tFixState:               string(vulnerability.Fix.State),\n\t\tAdvisories:             sqlite.ToNullString(vulnerability.Advisories),\n\t\tCPEs:                   sqlite.ToNullString(vulnerability.CPEs),\n\t\tRelatedVulnerabilities: sqlite.ToNullString(vulnerability.RelatedVulnerabilities),\n\t}\n}\n\n// TableName returns the table which all db.Vulnerability model instances are stored into.\nfunc (VulnerabilityModel) TableName() string {\n\treturn VulnerabilityTableName\n}\n\n// Inflate generates a db.Vulnerability object from the serialized model instance.\nfunc (m *VulnerabilityModel) Inflate() (v5.Vulnerability, error) {\n\tvar cpes []string\n\terr := json.Unmarshal(m.CPEs.ToByteSlice(), &cpes)\n\tif err != nil {\n\t\treturn v5.Vulnerability{}, fmt.Errorf(\"unable to unmarshal CPEs (%+v): %w\", m.CPEs, err)\n\t}\n\n\tvar related []v5.VulnerabilityReference\n\terr = json.Unmarshal(m.RelatedVulnerabilities.ToByteSlice(), &related)\n\tif err != nil {\n\t\treturn v5.Vulnerability{}, fmt.Errorf(\"unable to unmarshal related vulnerabilities (%+v): %w\", m.RelatedVulnerabilities, err)\n\t}\n\n\tvar advisories []v5.Advisory\n\n\terr = json.Unmarshal(m.Advisories.ToByteSlice(), &advisories)\n\tif err != nil {\n\t\treturn v5.Vulnerability{}, fmt.Errorf(\"unable to unmarshal advisories (%+v): %w\", m.Advisories, err)\n\t}\n\n\tvar versions []string\n\terr = json.Unmarshal(m.FixedInVersions.ToByteSlice(), &versions)\n\tif err != nil {\n\t\treturn v5.Vulnerability{}, fmt.Errorf(\"unable to unmarshal versions (%+v): %w\", m.FixedInVersions, err)\n\t}\n\n\tpkgQualifiers, err := qualifier.FromJSON(m.PackageQualifiers.ToByteSlice())\n\tif err != nil {\n\t\treturn v5.Vulnerability{}, fmt.Errorf(\"unable to unmarshal package_qualifiers (%+v): %w\", m.PackageQualifiers, err)\n\t}\n\n\treturn v5.Vulnerability{\n\t\tID:                     m.ID,\n\t\tPackageName:            m.PackageName,\n\t\tPackageQualifiers:      pkgQualifiers,\n\t\tNamespace:              m.Namespace,\n\t\tVersionConstraint:      m.VersionConstraint,\n\t\tVersionFormat:          m.VersionFormat,\n\t\tCPEs:                   cpes,\n\t\tRelatedVulnerabilities: related,\n\t\tFix: v5.Fix{\n\t\t\tVersions: versions,\n\t\t\tState:    v5.FixState(m.FixState),\n\t\t},\n\t\tAdvisories: advisories,\n\t}, nil\n}\n"
  },
  {
    "path": "grype/db/v5/store/model/vulnerability_match_exclusion.go",
    "content": "package model\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\n\t\"github.com/anchore/grype/grype/db/internal/sqlite\"\n\tv5 \"github.com/anchore/grype/grype/db/v5\"\n\t\"github.com/anchore/grype/internal/log\"\n)\n\nconst (\n\tVulnerabilityMatchExclusionTableName    = \"vulnerability_match_exclusion\"\n\tGetVulnerabilityMatchExclusionIndexName = \"get_vulnerability_match_exclusion_index\"\n)\n\n// VulnerabilityMatchExclusionModel is a struct used to serialize db.VulnerabilityMatchExclusion information into a sqlite3 DB.\ntype VulnerabilityMatchExclusionModel struct {\n\tPK            uint64            `gorm:\"primary_key;auto_increment;\"`\n\tID            string            `gorm:\"column:id; index:get_vulnerability_match_exclusion_index\"`\n\tConstraints   sqlite.NullString `gorm:\"column:constraints; default:null\"`\n\tJustification string            `gorm:\"column:justification\"`\n}\n\n// NewVulnerabilityMatchExclusionModel generates a new model from a db.VulnerabilityMatchExclusion struct.\nfunc NewVulnerabilityMatchExclusionModel(v v5.VulnerabilityMatchExclusion) VulnerabilityMatchExclusionModel {\n\treturn VulnerabilityMatchExclusionModel{\n\t\tID:            v.ID,\n\t\tConstraints:   sqlite.ToNullString(v.Constraints),\n\t\tJustification: v.Justification,\n\t}\n}\n\n// TableName returns the table which all db.VulnerabilityMatchExclusion model instances are stored into.\nfunc (VulnerabilityMatchExclusionModel) TableName() string {\n\treturn VulnerabilityMatchExclusionTableName\n}\n\n// Inflate generates a db.VulnerabilityMatchExclusion object from the serialized model instance.\nfunc (m *VulnerabilityMatchExclusionModel) Inflate() (*v5.VulnerabilityMatchExclusion, error) {\n\t// It's important that we only utilise exclusion constraints that are compatible with this version of Grype,\n\t// so if any unknown fields are encountered then ignore that constraint.\n\n\tvar constraints []v5.VulnerabilityMatchExclusionConstraint\n\terr := json.Unmarshal(m.Constraints.ToByteSlice(), &constraints)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to unmarshal vulnerability match exclusion constraints (%+v): %w\", m.Constraints, err)\n\t}\n\n\tvar compatibleConstraints []v5.VulnerabilityMatchExclusionConstraint\n\n\tif len(constraints) > 0 {\n\t\tfor _, c := range constraints {\n\t\t\tif !c.Usable() {\n\t\t\t\tlog.Debugf(\"skipping incompatible vulnerability match constraint for vuln id=%s, constraint=%+v\", m.ID, c)\n\t\t\t} else {\n\t\t\t\tcompatibleConstraints = append(compatibleConstraints, c)\n\t\t\t}\n\t\t}\n\n\t\t// If there were constraints and none were compatible, the entire record is not usable by this version of Grype\n\t\tif len(compatibleConstraints) == 0 {\n\t\t\treturn nil, nil\n\t\t}\n\t}\n\n\treturn &v5.VulnerabilityMatchExclusion{\n\t\tID:            m.ID,\n\t\tConstraints:   compatibleConstraints,\n\t\tJustification: m.Justification,\n\t}, nil\n}\n"
  },
  {
    "path": "grype/db/v5/store/model/vulnerability_match_exclusion_test.go",
    "content": "package model\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\n\t\"github.com/anchore/grype/grype/db/internal/sqlite\"\n\tv5 \"github.com/anchore/grype/grype/db/v5\"\n)\n\nfunc TestVulnerabilityMatchExclusionModel_Inflate(t *testing.T) {\n\ttests := []struct {\n\t\tname   string\n\t\trecord *VulnerabilityMatchExclusionModel\n\t\tresult *v5.VulnerabilityMatchExclusion\n\t}{\n\t\t{\n\t\t\tname: \"Nil constraint\",\n\t\t\trecord: &VulnerabilityMatchExclusionModel{\n\t\t\t\tPK:            0,\n\t\t\t\tID:            \"CVE-12345\",\n\t\t\t\tConstraints:   sqlite.ToNullString(nil),\n\t\t\t\tJustification: \"Who really knows?\",\n\t\t\t},\n\t\t\tresult: &v5.VulnerabilityMatchExclusion{\n\t\t\t\tID:            \"CVE-12345\",\n\t\t\t\tConstraints:   nil,\n\t\t\t\tJustification: \"Who really knows?\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Empty constraint array\",\n\t\t\trecord: &VulnerabilityMatchExclusionModel{\n\t\t\t\tPK:            0,\n\t\t\t\tID:            \"CVE-919\",\n\t\t\t\tConstraints:   sqlite.NewNullString(`[]`, true),\n\t\t\t\tJustification: \"Always ignore\",\n\t\t\t},\n\t\t\tresult: &v5.VulnerabilityMatchExclusion{\n\t\t\t\tID:            \"CVE-919\",\n\t\t\t\tConstraints:   nil,\n\t\t\t\tJustification: \"Always ignore\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Single constraint\",\n\t\t\trecord: &VulnerabilityMatchExclusionModel{\n\t\t\t\tPK:            0,\n\t\t\t\tID:            \"CVE-919\",\n\t\t\t\tConstraints:   sqlite.NewNullString(`[{\"vulnerability\":{\"namespace\":\"nvd:cpe\"},\"package\":{\"language\":\"python\"}}]`, true),\n\t\t\t\tJustification: \"Python packages are not vulnerable\",\n\t\t\t},\n\t\t\tresult: &v5.VulnerabilityMatchExclusion{\n\t\t\t\tID: \"CVE-919\",\n\t\t\t\tConstraints: []v5.VulnerabilityMatchExclusionConstraint{\n\t\t\t\t\t{\n\t\t\t\t\t\tVulnerability: v5.VulnerabilityExclusionConstraint{\n\t\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tPackage: v5.PackageExclusionConstraint{\n\t\t\t\t\t\t\tLanguage: \"python\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tJustification: \"Python packages are not vulnerable\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Single unusable constraint with unknown vulnerability constraint field\",\n\t\t\trecord: &VulnerabilityMatchExclusionModel{\n\t\t\t\tPK:            0,\n\t\t\t\tID:            \"CVE-919\",\n\t\t\t\tConstraints:   sqlite.NewNullString(`[{\"vulnerability\":{\"namespace\":\"nvd:cpe\",\"something_new\":\"1234\"}}]`, true),\n\t\t\t\tJustification: \"Python packages are not vulnerable\",\n\t\t\t},\n\t\t\tresult: nil,\n\t\t},\n\t\t{\n\t\t\tname: \"Single unusable constraint with unknown package constraint fields\",\n\t\t\trecord: &VulnerabilityMatchExclusionModel{\n\t\t\t\tPK:            0,\n\t\t\t\tID:            \"CVE-919\",\n\t\t\t\tConstraints:   sqlite.NewNullString(`[{\"package\":{\"name\":\"jim\",\"another_field\":\"1234\",\"x_y_z\":\"abc\"}}]`, true),\n\t\t\t\tJustification: \"Python packages are not vulnerable\",\n\t\t\t},\n\t\t\tresult: nil,\n\t\t},\n\t\t{\n\t\t\tname: \"Single unusable constraint with unknown root-level constraint fields\",\n\t\t\trecord: &VulnerabilityMatchExclusionModel{\n\t\t\t\tPK:            0,\n\t\t\t\tID:            \"CVE-919\",\n\t\t\t\tConstraints:   sqlite.NewNullString(`[{\"x_y_z\":{\"name\":\"jim\",\"another_field\":\"1234\",\"x_y_z\":\"abc\"},\"package\":{\"name\":\"jim\",\"another_field\":\"1234\",\"x_y_z\":\"abc\"}}]`, true),\n\t\t\t\tJustification: \"Python packages are not vulnerable\",\n\t\t\t},\n\t\t\tresult: nil,\n\t\t},\n\t\t{\n\t\t\tname: \"Multiple usable constraints\",\n\t\t\trecord: &VulnerabilityMatchExclusionModel{\n\t\t\t\tPK:            0,\n\t\t\t\tID:            \"CVE-2025-152345\",\n\t\t\t\tConstraints:   sqlite.NewNullString(`[{\"vulnerability\":{\"namespace\":\"abc.xyz:language:ruby\",\"fix_state\":\"wont-fix\"},\"package\":{\"language\":\"ruby\",\"type\":\"not-gem\"}},{\"package\":{\"language\":\"python\",\"version\":\"1000.0.1\"}},{\"vulnerability\":{\"namespace\":\"nvd:cpe\"}},{\"vulnerability\":{\"namespace\":\"nvd:cpe\"},\"package\":{\"name\":\"x\"}},{\"package\":{\"location\":\"/bin/x\"}}]`, true),\n\t\t\t\tJustification: \"Python packages are not vulnerable\",\n\t\t\t},\n\t\t\tresult: &v5.VulnerabilityMatchExclusion{\n\t\t\t\tID: \"CVE-2025-152345\",\n\t\t\t\tConstraints: []v5.VulnerabilityMatchExclusionConstraint{\n\t\t\t\t\t{\n\t\t\t\t\t\tVulnerability: v5.VulnerabilityExclusionConstraint{\n\t\t\t\t\t\t\tNamespace: \"abc.xyz:language:ruby\",\n\t\t\t\t\t\t\tFixState:  \"wont-fix\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tPackage: v5.PackageExclusionConstraint{\n\t\t\t\t\t\t\tLanguage: \"ruby\",\n\t\t\t\t\t\t\tType:     \"not-gem\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tPackage: v5.PackageExclusionConstraint{\n\t\t\t\t\t\t\tLanguage: \"python\",\n\t\t\t\t\t\t\tVersion:  \"1000.0.1\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tVulnerability: v5.VulnerabilityExclusionConstraint{\n\t\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tVulnerability: v5.VulnerabilityExclusionConstraint{\n\t\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tPackage: v5.PackageExclusionConstraint{\n\t\t\t\t\t\t\tName: \"x\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tPackage: v5.PackageExclusionConstraint{\n\t\t\t\t\t\t\tLocation: \"/bin/x\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tJustification: \"Python packages are not vulnerable\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Multiple constraints with some unusable\",\n\t\t\trecord: &VulnerabilityMatchExclusionModel{\n\t\t\t\tPK:            0,\n\t\t\t\tID:            \"CVE-2025-152345\",\n\t\t\t\tConstraints:   sqlite.NewNullString(`[{\"a_b_c\": \"x\",\"vulnerability\":{\"namespace\":\"abc.xyz:language:ruby\",\"fix_state\":\"wont-fix\"},\"package\":{\"language\":\"ruby\",\"type\":\"not-gem\"}},{\"package\":{\"language\":\"python\",\"version\":\"1000.0.1\"}},{\"vulnerability\":{\"namespace\":\"nvd:cpe\"}},{\"vulnerability\":{\"namespace\":\"nvd:cpe\"},\"package\":{\"name\":\"x\"}},{\"package\":{\"location\":\"/bin/x\",\"nnnn\":\"no\"}}]`, true),\n\t\t\t\tJustification: \"Python packages are not vulnerable\",\n\t\t\t},\n\t\t\tresult: &v5.VulnerabilityMatchExclusion{\n\t\t\t\tID: \"CVE-2025-152345\",\n\t\t\t\tConstraints: []v5.VulnerabilityMatchExclusionConstraint{\n\t\t\t\t\t{\n\t\t\t\t\t\tPackage: v5.PackageExclusionConstraint{\n\t\t\t\t\t\t\tLanguage: \"python\",\n\t\t\t\t\t\t\tVersion:  \"1000.0.1\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tVulnerability: v5.VulnerabilityExclusionConstraint{\n\t\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tVulnerability: v5.VulnerabilityExclusionConstraint{\n\t\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tPackage: v5.PackageExclusionConstraint{\n\t\t\t\t\t\t\tName: \"x\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tJustification: \"Python packages are not vulnerable\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Multiple constraints all unusable\",\n\t\t\trecord: &VulnerabilityMatchExclusionModel{\n\t\t\t\tPK:            0,\n\t\t\t\tID:            \"CVE-2025-152345\",\n\t\t\t\tConstraints:   sqlite.NewNullString(`[{\"a_b_c\": \"x\",\"vulnerability\":{\"namespace\":\"abc.xyz:language:ruby\",\"fix_state\":\"wont-fix\"},\"package\":{\"language\":\"ruby\",\"type\":\"not-gem\"}},{\"a_b_c\": \"x\",\"package\":{\"language\":\"python\",\"version\":\"1000.0.1\"}},{\"a_b_c\": \"x\",\"vulnerability\":{\"namespace\":\"nvd:cpe\"}},{\"a_b_c\": \"x\",\"vulnerability\":{\"namespace\":\"nvd:cpe\"},\"package\":{\"name\":\"x\"}},{\"package\":{\"location\":\"/bin/x\",\"nnnn\":\"no\"}}]`, true),\n\t\t\t\tJustification: \"Python packages are not vulnerable\",\n\t\t\t},\n\t\t\tresult: nil,\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tresult, err := test.record.Inflate()\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, test.result, result)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/db/v5/store/model/vulnerability_metadata.go",
    "content": "package model\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\n\tsqlite \"github.com/anchore/grype/grype/db/internal/sqlite\"\n\tv5 \"github.com/anchore/grype/grype/db/v5\"\n)\n\nconst (\n\tVulnerabilityMetadataTableName = \"vulnerability_metadata\"\n)\n\n// VulnerabilityMetadataModel is a struct used to serialize db.VulnerabilityMetadata information into a sqlite3 DB.\ntype VulnerabilityMetadataModel struct {\n\tID           string            `gorm:\"primary_key; column:id;\"`\n\tNamespace    string            `gorm:\"primary_key; column:namespace;\"`\n\tDataSource   string            `gorm:\"column:data_source\"`\n\tRecordSource string            `gorm:\"column:record_source\"`\n\tSeverity     string            `gorm:\"column:severity\"`\n\tURLs         sqlite.NullString `gorm:\"column:urls; default:null\"`\n\tDescription  string            `gorm:\"column:description\"`\n\tCvss         sqlite.NullString `gorm:\"column:cvss; default:null\"`\n}\n\n// NewVulnerabilityMetadataModel generates a new model from a db.VulnerabilityMetadata struct.\nfunc NewVulnerabilityMetadataModel(metadata v5.VulnerabilityMetadata) VulnerabilityMetadataModel {\n\tif metadata.Cvss == nil {\n\t\tmetadata.Cvss = make([]v5.Cvss, 0)\n\t}\n\n\treturn VulnerabilityMetadataModel{\n\t\tID:           metadata.ID,\n\t\tNamespace:    metadata.Namespace,\n\t\tDataSource:   metadata.DataSource,\n\t\tRecordSource: metadata.RecordSource,\n\t\tSeverity:     metadata.Severity,\n\t\tURLs:         sqlite.ToNullString(metadata.URLs),\n\t\tDescription:  metadata.Description,\n\t\tCvss:         sqlite.ToNullString(metadata.Cvss),\n\t}\n}\n\n// TableName returns the table which all db.VulnerabilityMetadata model instances are stored into.\nfunc (VulnerabilityMetadataModel) TableName() string {\n\treturn VulnerabilityMetadataTableName\n}\n\n// Inflate generates a db.VulnerabilityMetadataModel object from the serialized model instance.\nfunc (m *VulnerabilityMetadataModel) Inflate() (v5.VulnerabilityMetadata, error) {\n\tvar links []string\n\tvar cvss []v5.Cvss\n\n\tif err := json.Unmarshal(m.URLs.ToByteSlice(), &links); err != nil {\n\t\treturn v5.VulnerabilityMetadata{}, fmt.Errorf(\"unable to unmarshal URLs (%+v): %w\", m.URLs, err)\n\t}\n\n\terr := json.Unmarshal(m.Cvss.ToByteSlice(), &cvss)\n\tif err != nil {\n\t\treturn v5.VulnerabilityMetadata{}, fmt.Errorf(\"unable to unmarshal cvss data (%+v): %w\", m.Cvss, err)\n\t}\n\n\treturn v5.VulnerabilityMetadata{\n\t\tID:           m.ID,\n\t\tNamespace:    m.Namespace,\n\t\tDataSource:   m.DataSource,\n\t\tRecordSource: m.RecordSource,\n\t\tSeverity:     m.Severity,\n\t\tURLs:         links,\n\t\tDescription:  m.Description,\n\t\tCvss:         cvss,\n\t}, nil\n}\n"
  },
  {
    "path": "grype/db/v5/store/model/vulnerability_test.go",
    "content": "package model\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\n\t\"github.com/anchore/grype/grype/db/internal/sqlite\"\n\tv5 \"github.com/anchore/grype/grype/db/v5\"\n\t\"github.com/anchore/grype/grype/db/v5/pkg/qualifier\"\n\t\"github.com/anchore/grype/grype/db/v5/pkg/qualifier/platformcpe\"\n\t\"github.com/anchore/grype/grype/db/v5/pkg/qualifier/rpmmodularity\"\n)\n\nfunc TestVulnerabilityModel_Inflate(t *testing.T) {\n\ttests := []struct {\n\t\tname   string\n\t\trecord *VulnerabilityModel\n\t\tresult v5.Vulnerability\n\t}{\n\t\t{\n\t\t\tname: \"nil package_qualifiers\",\n\t\t\trecord: &VulnerabilityModel{\n\t\t\t\tPK:                0,\n\t\t\t\tID:                \"CVE-12345\",\n\t\t\t\tPackageQualifiers: sqlite.ToNullString(nil),\n\t\t\t},\n\t\t\tresult: v5.Vulnerability{\n\t\t\t\tID: \"CVE-12345\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Empty package_qualifiers array\",\n\t\t\trecord: &VulnerabilityModel{\n\t\t\t\tPK:                0,\n\t\t\t\tID:                \"CVE-919\",\n\t\t\t\tPackageQualifiers: sqlite.NewNullString(`[]`, true),\n\t\t\t},\n\t\t\tresult: v5.Vulnerability{\n\t\t\t\tID: \"CVE-919\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Single rpmmodularity package qualifier with no module specified\",\n\t\t\trecord: &VulnerabilityModel{\n\t\t\t\tPK:                0,\n\t\t\t\tID:                \"CVE-919\",\n\t\t\t\tPackageQualifiers: sqlite.NewNullString(`[{\"kind\": \"rpm-modularity\"}]`, true),\n\t\t\t},\n\t\t\tresult: v5.Vulnerability{\n\t\t\t\tID: \"CVE-919\",\n\t\t\t\tPackageQualifiers: []qualifier.Qualifier{\n\t\t\t\t\trpmmodularity.Qualifier{\n\t\t\t\t\t\tKind:   \"rpm-modularity\",\n\t\t\t\t\t\tModule: \"\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Single rpmmodularity package qualifier with empty string module specified\",\n\t\t\trecord: &VulnerabilityModel{\n\t\t\t\tPK:                0,\n\t\t\t\tID:                \"CVE-919\",\n\t\t\t\tPackageQualifiers: sqlite.NewNullString(`[{\"kind\": \"rpm-modularity\", \"module\": \"\"}]`, true),\n\t\t\t},\n\t\t\tresult: v5.Vulnerability{\n\t\t\t\tID: \"CVE-919\",\n\t\t\t\tPackageQualifiers: []qualifier.Qualifier{\n\t\t\t\t\trpmmodularity.Qualifier{\n\t\t\t\t\t\tKind:   \"rpm-modularity\",\n\t\t\t\t\t\tModule: \"\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Single rpmmodularity package qualifier with module specified\",\n\t\t\trecord: &VulnerabilityModel{\n\t\t\t\tPK:                0,\n\t\t\t\tID:                \"CVE-919\",\n\t\t\t\tPackageQualifiers: sqlite.NewNullString(`[{\"kind\": \"rpm-modularity\", \"module\": \"x.y.z:2000\"}]`, true),\n\t\t\t},\n\t\t\tresult: v5.Vulnerability{\n\t\t\t\tID: \"CVE-919\",\n\t\t\t\tPackageQualifiers: []qualifier.Qualifier{\n\t\t\t\t\trpmmodularity.Qualifier{\n\t\t\t\t\t\tKind:   \"rpm-modularity\",\n\t\t\t\t\t\tModule: \"x.y.z:2000\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Single platformcpe package qualifier with cpe specified\",\n\t\t\trecord: &VulnerabilityModel{\n\t\t\t\tPK:                0,\n\t\t\t\tID:                \"CVE-919\",\n\t\t\t\tPackageQualifiers: sqlite.NewNullString(`[{\"kind\": \"platform-cpe\", \"cpe\": \"cpe:2.3:o:canonical:ubuntu_linux:19.10:*:*:*:*:*:*:*\"}]`, true),\n\t\t\t},\n\t\t\tresult: v5.Vulnerability{\n\t\t\t\tID: \"CVE-919\",\n\t\t\t\tPackageQualifiers: []qualifier.Qualifier{\n\t\t\t\t\tplatformcpe.Qualifier{\n\t\t\t\t\t\tKind: \"platform-cpe\",\n\t\t\t\t\t\tCPE:  \"cpe:2.3:o:canonical:ubuntu_linux:19.10:*:*:*:*:*:*:*\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Single unrecognized package qualifier\",\n\t\t\trecord: &VulnerabilityModel{\n\t\t\t\tPK:                0,\n\t\t\t\tID:                \"CVE-919\",\n\t\t\t\tPackageQualifiers: sqlite.NewNullString(`[{\"kind\": \"unknown\", \"some-random-slice\": [{\"x\": true}]}]`, true),\n\t\t\t},\n\t\t\tresult: v5.Vulnerability{\n\t\t\t\tID: \"CVE-919\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Single package qualifier without kind specified\",\n\t\t\trecord: &VulnerabilityModel{\n\t\t\t\tPK:                0,\n\t\t\t\tID:                \"CVE-919\",\n\t\t\t\tPackageQualifiers: sqlite.NewNullString(`[{\"some-random-slice\": [{\"x\": true}]}]`, true),\n\t\t\t},\n\t\t\tresult: v5.Vulnerability{\n\t\t\t\tID: \"CVE-919\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Multiple package qualifiers\",\n\t\t\trecord: &VulnerabilityModel{\n\t\t\t\tPK:                0,\n\t\t\t\tID:                \"CVE-919\",\n\t\t\t\tPackageQualifiers: sqlite.NewNullString(`[{\"kind\": \"rpm-modularity\"},{\"kind\": \"rpm-modularity\", \"module\": \"\"},{\"kind\": \"rpm-modularity\", \"module\": \"x.y.z:2000\"},{\"kind\": \"unknown\", \"some-random-slice\": [{\"x\": true}]}]`, true),\n\t\t\t},\n\t\t\tresult: v5.Vulnerability{\n\t\t\t\tID: \"CVE-919\",\n\t\t\t\tPackageQualifiers: []qualifier.Qualifier{\n\t\t\t\t\trpmmodularity.Qualifier{\n\t\t\t\t\t\tKind:   \"rpm-modularity\",\n\t\t\t\t\t\tModule: \"\",\n\t\t\t\t\t},\n\t\t\t\t\trpmmodularity.Qualifier{\n\t\t\t\t\t\tKind:   \"rpm-modularity\",\n\t\t\t\t\t\tModule: \"\",\n\t\t\t\t\t},\n\t\t\t\t\trpmmodularity.Qualifier{\n\t\t\t\t\t\tKind:   \"rpm-modularity\",\n\t\t\t\t\t\tModule: \"x.y.z:2000\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tresult, err := test.record.Inflate()\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, test.result, result)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/db/v5/store/store.go",
    "content": "package store\n\nimport (\n\t\"fmt\"\n\t\"sort\"\n\n\t_ \"github.com/glebarez/sqlite\" // provide the sqlite dialect to gorm via import\n\t\"github.com/go-test/deep\"\n\t\"gorm.io/gorm\"\n\n\t\"github.com/anchore/grype/grype/db/internal/gormadapter\"\n\tv5 \"github.com/anchore/grype/grype/db/v5\"\n\t\"github.com/anchore/grype/grype/db/v5/store/model\"\n\t\"github.com/anchore/grype/internal/log\"\n\t\"github.com/anchore/grype/internal/stringutil\"\n)\n\n// store holds an instance of the database connection\ntype store struct {\n\tdb *gorm.DB\n}\n\nfunc models() []any {\n\treturn []any{\n\t\tmodel.IDModel{},\n\t\tmodel.VulnerabilityModel{},\n\t\tmodel.VulnerabilityMetadataModel{},\n\t\tmodel.VulnerabilityMatchExclusionModel{},\n\t}\n}\n\n// New creates a new instance of the store.\nfunc New(dbFilePath string, overwrite bool) (v5.Store, error) {\n\tdb, err := gormadapter.Open(dbFilePath, gormadapter.WithTruncate(overwrite, models(), nil))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &store{\n\t\tdb: db,\n\t}, nil\n}\n\n// GetID fetches the metadata about the databases schema version and build time.\nfunc (s *store) GetID() (*v5.ID, error) {\n\tvar models []model.IDModel\n\tresult := s.db.Find(&models)\n\tif result.Error != nil {\n\t\treturn nil, result.Error\n\t}\n\n\tswitch {\n\tcase len(models) > 1:\n\t\treturn nil, fmt.Errorf(\"found multiple DB IDs\")\n\tcase len(models) == 1:\n\t\tid, err := models[0].Inflate()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn &id, nil\n\t}\n\n\treturn nil, nil\n}\n\n// SetID stores the databases schema version and build time.\nfunc (s *store) SetID(id v5.ID) error {\n\tvar ids []model.IDModel\n\n\t// replace the existing ID with the given one\n\ts.db.Find(&ids).Delete(&ids)\n\n\tm := model.NewIDModel(id)\n\tresult := s.db.Create(&m)\n\n\tif result.RowsAffected != 1 {\n\t\treturn fmt.Errorf(\"unable to add id (%d rows affected)\", result.RowsAffected)\n\t}\n\n\treturn result.Error\n}\n\n// GetVulnerabilityNamespaces retrieves all possible namespaces from the database.\nfunc (s *store) GetVulnerabilityNamespaces() ([]string, error) {\n\tvar names []string\n\tresult := s.db.Model(&model.VulnerabilityMetadataModel{}).Distinct().Pluck(\"namespace\", &names)\n\treturn names, result.Error\n}\n\n// GetVulnerability retrieves vulnerabilities by namespace and id\nfunc (s *store) GetVulnerability(namespace, id string) ([]v5.Vulnerability, error) {\n\tvar models []model.VulnerabilityModel\n\n\tquery := s.db.Where(\"id = ?\", id)\n\n\tif namespace != \"\" {\n\t\tquery = query.Where(\"namespace = ?\", namespace)\n\t}\n\n\tresult := query.Find(&models)\n\n\tvulnerabilities := make([]v5.Vulnerability, len(models))\n\tfor idx, m := range models {\n\t\tvulnerability, err := m.Inflate()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tvulnerabilities[idx] = vulnerability\n\t}\n\n\treturn vulnerabilities, result.Error\n}\n\n// SearchForVulnerabilities retrieves vulnerabilities by namespace and package\nfunc (s *store) SearchForVulnerabilities(namespace, packageName string) ([]v5.Vulnerability, error) {\n\tvar models []model.VulnerabilityModel\n\n\tresult := s.db.Where(\"namespace = ? AND package_name = ?\", namespace, packageName).Find(&models)\n\n\tvulnerabilities := make([]v5.Vulnerability, len(models))\n\tfor idx, m := range models {\n\t\tvulnerability, err := m.Inflate()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tvulnerabilities[idx] = vulnerability\n\t}\n\n\treturn vulnerabilities, result.Error\n}\n\n// AddVulnerability saves one or more vulnerabilities into the sqlite3 store.\nfunc (s *store) AddVulnerability(vulnerabilities ...v5.Vulnerability) error {\n\tfor _, vulnerability := range vulnerabilities {\n\t\tm := model.NewVulnerabilityModel(vulnerability)\n\n\t\tresult := s.db.Create(&m)\n\t\tif result.Error != nil {\n\t\t\treturn result.Error\n\t\t}\n\n\t\tif result.RowsAffected != 1 {\n\t\t\treturn fmt.Errorf(\"unable to add vulnerability (%d rows affected)\", result.RowsAffected)\n\t\t}\n\t}\n\treturn nil\n}\n\n// GetVulnerabilityMetadata retrieves metadata for the given vulnerability ID relative to a specific record source.\nfunc (s *store) GetVulnerabilityMetadata(id, namespace string) (*v5.VulnerabilityMetadata, error) {\n\tvar models []model.VulnerabilityMetadataModel\n\n\tresult := s.db.Where(&model.VulnerabilityMetadataModel{ID: id, Namespace: namespace}).Find(&models)\n\tif result.Error != nil {\n\t\treturn nil, result.Error\n\t}\n\n\tswitch {\n\tcase len(models) > 1:\n\t\treturn nil, fmt.Errorf(\"found multiple metadatas for single ID=%q Namespace=%q\", id, namespace)\n\tcase len(models) == 1:\n\t\tmetadata, err := models[0].Inflate()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\treturn &metadata, nil\n\t}\n\n\treturn nil, nil\n}\n\n// AddVulnerabilityMetadata stores one or more vulnerability metadata models into the sqlite DB.\n//\n//nolint:gocognit\nfunc (s *store) AddVulnerabilityMetadata(metadata ...v5.VulnerabilityMetadata) error {\n\tfor _, m := range metadata {\n\t\texisting, err := s.GetVulnerabilityMetadata(m.ID, m.Namespace)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to verify existing entry: %w\", err)\n\t\t}\n\n\t\tif existing != nil {\n\t\t\t// merge with the existing entry\n\n\t\t\tswitch {\n\t\t\tcase existing.Severity != m.Severity:\n\t\t\t\treturn fmt.Errorf(\"existing metadata has mismatched severity (%q!=%q)\", existing.Severity, m.Severity)\n\t\t\tcase existing.Description != m.Description:\n\t\t\t\treturn fmt.Errorf(\"existing metadata has mismatched description (%q!=%q)\", existing.Description, m.Description)\n\t\t\t}\n\n\t\tincoming:\n\t\t\t// go through all incoming CVSS and see if they are already stored.\n\t\t\t// If they exist already in the database then skip adding them,\n\t\t\t// preventing a duplicate\n\t\t\tfor _, incomingCvss := range m.Cvss {\n\t\t\t\tfor _, existingCvss := range existing.Cvss {\n\t\t\t\t\tif len(deep.Equal(incomingCvss, existingCvss)) == 0 {\n\t\t\t\t\t\t// duplicate found, so incoming CVSS shouldn't get added\n\t\t\t\t\t\tcontinue incoming\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t// a duplicate CVSS entry wasn't found, so append the incoming CVSS\n\t\t\t\texisting.Cvss = append(existing.Cvss, incomingCvss)\n\t\t\t}\n\n\t\t\tlinks := stringutil.NewStringSetFromSlice(existing.URLs)\n\t\t\tfor _, l := range m.URLs {\n\t\t\t\tlinks.Add(l)\n\t\t\t}\n\n\t\t\texisting.URLs = links.ToSlice()\n\t\t\tsort.Strings(existing.URLs)\n\n\t\t\tnewModel := model.NewVulnerabilityMetadataModel(*existing)\n\t\t\tresult := s.db.Save(&newModel)\n\n\t\t\tif result.RowsAffected != 1 {\n\t\t\t\treturn fmt.Errorf(\"unable to merge vulnerability metadata (%d rows affected)\", result.RowsAffected)\n\t\t\t}\n\n\t\t\tif result.Error != nil {\n\t\t\t\treturn result.Error\n\t\t\t}\n\t\t} else {\n\t\t\t// this is a new entry\n\t\t\tnewModel := model.NewVulnerabilityMetadataModel(m)\n\t\t\tresult := s.db.Create(&newModel)\n\t\t\tif result.Error != nil {\n\t\t\t\treturn result.Error\n\t\t\t}\n\n\t\t\tif result.RowsAffected != 1 {\n\t\t\t\treturn fmt.Errorf(\"unable to add vulnerability metadata (%d rows affected)\", result.RowsAffected)\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\n// GetVulnerabilityMatchExclusion retrieves one or more vulnerability match exclusion records given a vulnerability identifier.\nfunc (s *store) GetVulnerabilityMatchExclusion(id string) ([]v5.VulnerabilityMatchExclusion, error) {\n\tvar models []model.VulnerabilityMatchExclusionModel\n\n\tresult := s.db.Where(\"id = ?\", id).Find(&models)\n\n\tvar exclusions []v5.VulnerabilityMatchExclusion\n\tfor _, m := range models {\n\t\texclusion, err := m.Inflate()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tif exclusion != nil {\n\t\t\texclusions = append(exclusions, *exclusion)\n\t\t}\n\t}\n\n\treturn exclusions, result.Error\n}\n\n// AddVulnerabilityMatchExclusion saves one or more vulnerability match exclusion records into the sqlite3 store.\nfunc (s *store) AddVulnerabilityMatchExclusion(exclusions ...v5.VulnerabilityMatchExclusion) error {\n\tfor _, exclusion := range exclusions {\n\t\tm := model.NewVulnerabilityMatchExclusionModel(exclusion)\n\n\t\tresult := s.db.Create(&m)\n\t\tif result.Error != nil {\n\t\t\treturn result.Error\n\t\t}\n\n\t\tif result.RowsAffected != 1 {\n\t\t\treturn fmt.Errorf(\"unable to add vulnerability match exclusion (%d rows affected)\", result.RowsAffected)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (s *store) Close() error {\n\tlog.Debug(\"optimizing database settings for memory-efficient VACUUM\")\n\n\t// Reduce memory footprint for VACUUM operation\n\tmemoryEfficientStatements := []string{\n\t\t\"PRAGMA cache_size = -32768\",     // 32MB instead of 1GB\n\t\t\"PRAGMA temp_store = FILE\",       // Use disk for temp storage\n\t\t\"PRAGMA mmap_size = 67108864\",    // 64MB instead of 1GB\n\t\t\"PRAGMA journal_mode = TRUNCATE\", // Disk-based journal, no directory modifications\n\t}\n\n\tfor _, stmt := range memoryEfficientStatements {\n\t\tif err := s.db.Exec(stmt).Error; err != nil {\n\t\t\tlog.WithFields(\"statement\", stmt, \"error\", err).Warn(\"failed to apply memory optimization\")\n\t\t} else {\n\t\t\tlog.WithFields(\"statement\", stmt).Debug(\"applied memory optimization\")\n\t\t}\n\t}\n\n\tlog.Debug(\"starting database VACUUM operation\")\n\ts.db.Exec(\"VACUUM;\")\n\tlog.Debug(\"database VACUUM operation completed\")\n\n\tsqlDB, _ := s.db.DB()\n\tif sqlDB != nil {\n\t\t_ = sqlDB.Close()\n\t}\n\n\treturn nil\n}\n\n// GetAllVulnerabilities gets all vulnerabilities in the database\nfunc (s *store) GetAllVulnerabilities() (*[]v5.Vulnerability, error) {\n\tvar models []model.VulnerabilityModel\n\tif result := s.db.Find(&models); result.Error != nil {\n\t\treturn nil, result.Error\n\t}\n\tvulns := make([]v5.Vulnerability, len(models))\n\tfor idx, m := range models {\n\t\tvuln, err := m.Inflate()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tvulns[idx] = vuln\n\t}\n\treturn &vulns, nil\n}\n\n// GetAllVulnerabilityMetadata gets all vulnerability metadata in the database\nfunc (s *store) GetAllVulnerabilityMetadata() (*[]v5.VulnerabilityMetadata, error) {\n\tvar models []model.VulnerabilityMetadataModel\n\tif result := s.db.Find(&models); result.Error != nil {\n\t\treturn nil, result.Error\n\t}\n\tmetadata := make([]v5.VulnerabilityMetadata, len(models))\n\tfor idx, m := range models {\n\t\tdata, err := m.Inflate()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tmetadata[idx] = data\n\t}\n\treturn &metadata, nil\n}\n\n// DiffStore creates a diff between the current sql database and the given store\nfunc (s *store) DiffStore(targetStore v5.StoreReader) (*[]v5.Diff, error) {\n\t// 7 stages, one for each step of the diff process (stages)\n\trowsProgress, diffItems, stager := trackDiff(7)\n\n\tstager.Current = \"reading target vulnerabilities\"\n\ttargetVulns, err := targetStore.GetAllVulnerabilities()\n\trowsProgress.Increment()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tstager.Current = \"reading base vulnerabilities\"\n\tbaseVulns, err := s.GetAllVulnerabilities()\n\trowsProgress.Increment()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tstager.Current = \"preparing\"\n\tbaseVulnPkgMap := buildVulnerabilityPkgsMap(baseVulns)\n\ttargetVulnPkgMap := buildVulnerabilityPkgsMap(targetVulns)\n\n\tstager.Current = \"comparing vulnerabilities\"\n\tallDiffsMap := diffVulnerabilities(baseVulns, targetVulns, baseVulnPkgMap, targetVulnPkgMap, diffItems)\n\n\tstager.Current = \"reading base metadata\"\n\tbaseMetadata, err := s.GetAllVulnerabilityMetadata()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\trowsProgress.Increment()\n\n\tstager.Current = \"reading target metadata\"\n\ttargetMetadata, err := targetStore.GetAllVulnerabilityMetadata()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\trowsProgress.Increment()\n\n\tstager.Current = \"comparing metadata\"\n\tmetaDiffsMap := diffVulnerabilityMetadata(baseMetadata, targetMetadata, baseVulnPkgMap, targetVulnPkgMap, diffItems)\n\tfor k, diff := range *metaDiffsMap {\n\t\t(*allDiffsMap)[k] = diff\n\t}\n\tallDiffs := []v5.Diff{}\n\tfor _, diff := range *allDiffsMap {\n\t\tallDiffs = append(allDiffs, *diff)\n\t}\n\n\trowsProgress.SetCompleted()\n\tdiffItems.SetCompleted()\n\n\treturn &allDiffs, nil\n}\n"
  },
  {
    "path": "grype/db/v5/store/store_test.go",
    "content": "package store\n\nimport (\n\t\"encoding/json\"\n\t\"sort\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-test/deep\"\n\t\"github.com/stretchr/testify/assert\"\n\n\tv5 \"github.com/anchore/grype/grype/db/v5\"\n\t\"github.com/anchore/grype/grype/db/v5/store/model\"\n)\n\nfunc assertIDReader(t *testing.T, reader v5.IDReader, expected v5.ID) {\n\tt.Helper()\n\tif actual, err := reader.GetID(); err != nil {\n\t\tt.Fatalf(\"failed to get ID: %+v\", err)\n\t} else {\n\t\tdiffs := deep.Equal(&expected, actual)\n\t\tif len(diffs) > 0 {\n\t\t\tfor _, d := range diffs {\n\t\t\t\tt.Errorf(\"Diff: %+v\", d)\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc TestStore_GetID_SetID(t *testing.T) {\n\tdbTempFile := t.TempDir()\n\n\ts, err := New(dbTempFile, true)\n\tif err != nil {\n\t\tt.Fatalf(\"could not create store: %+v\", err)\n\t}\n\n\texpected := v5.ID{\n\t\tBuildTimestamp: time.Now().UTC(),\n\t\tSchemaVersion:  2,\n\t}\n\n\tif err = s.SetID(expected); err != nil {\n\t\tt.Fatalf(\"failed to set ID: %+v\", err)\n\t}\n\n\tassertIDReader(t, s, expected)\n\n}\n\nfunc assertVulnerabilityReader(t *testing.T, reader v5.VulnerabilityStoreReader, namespace, name string, expected []v5.Vulnerability) {\n\tif actual, err := reader.SearchForVulnerabilities(namespace, name); err != nil {\n\t\tt.Fatalf(\"failed to get Vulnerability: %+v\", err)\n\t} else {\n\t\tif len(actual) != len(expected) {\n\t\t\tt.Fatalf(\"unexpected number of vulns: %d\", len(actual))\n\t\t}\n\t\tfor idx := range actual {\n\t\t\tdiffs := deep.Equal(expected[idx], actual[idx])\n\t\t\tif len(diffs) > 0 {\n\t\t\t\tfor _, d := range diffs {\n\t\t\t\t\tt.Errorf(\"Diff: %+v\", d)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc TestStore_GetVulnerability_SetVulnerability(t *testing.T) {\n\tdbTempFile := t.TempDir()\n\ts, err := New(dbTempFile, true)\n\tif err != nil {\n\t\tt.Fatalf(\"could not create store: %+v\", err)\n\t}\n\n\textra := []v5.Vulnerability{\n\t\t{\n\t\t\tID:                \"my-cve-33333\",\n\t\t\tPackageName:       \"package-name-2\",\n\t\t\tNamespace:         \"my-namespace\",\n\t\t\tVersionConstraint: \"< 1.0\",\n\t\t\tVersionFormat:     \"semver\",\n\t\t\tCPEs:              []string{\"a-cool-cpe\"},\n\t\t\tRelatedVulnerabilities: []v5.VulnerabilityReference{\n\t\t\t\t{\n\t\t\t\t\tID:        \"another-cve\",\n\t\t\t\t\tNamespace: \"nvd\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID:        \"an-other-cve\",\n\t\t\t\t\tNamespace: \"nvd\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tFix: v5.Fix{\n\t\t\t\tVersions: []string{\"2.0.1\"},\n\t\t\t\tState:    v5.FixedState,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tID:                \"my-other-cve-33333\",\n\t\t\tPackageName:       \"package-name-3\",\n\t\t\tNamespace:         \"my-namespace\",\n\t\t\tVersionConstraint: \"< 509.2.2\",\n\t\t\tVersionFormat:     \"semver\",\n\t\t\tCPEs:              []string{\"a-cool-cpe\"},\n\t\t\tRelatedVulnerabilities: []v5.VulnerabilityReference{\n\t\t\t\t{\n\t\t\t\t\tID:        \"another-cve\",\n\t\t\t\t\tNamespace: \"nvd\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID:        \"an-other-cve\",\n\t\t\t\t\tNamespace: \"nvd\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tFix: v5.Fix{\n\t\t\t\tState: v5.NotFixedState,\n\t\t\t},\n\t\t},\n\t}\n\n\texpected := []v5.Vulnerability{\n\t\t{\n\t\t\tID:                \"my-cve\",\n\t\t\tPackageName:       \"package-name\",\n\t\t\tNamespace:         \"my-namespace\",\n\t\t\tVersionConstraint: \"< 1.0\",\n\t\t\tVersionFormat:     \"semver\",\n\t\t\tCPEs:              []string{\"a-cool-cpe\"},\n\t\t\tRelatedVulnerabilities: []v5.VulnerabilityReference{\n\t\t\t\t{\n\t\t\t\t\tID:        \"another-cve\",\n\t\t\t\t\tNamespace: \"nvd\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID:        \"an-other-cve\",\n\t\t\t\t\tNamespace: \"nvd\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tFix: v5.Fix{\n\t\t\t\tVersions: []string{\"1.0.1\"},\n\t\t\t\tState:    v5.FixedState,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tID:                \"my-other-cve\",\n\t\t\tPackageName:       \"package-name\",\n\t\t\tNamespace:         \"my-namespace\",\n\t\t\tVersionConstraint: \"< 509.2.2\",\n\t\t\tVersionFormat:     \"semver\",\n\t\t\tCPEs:              nil,\n\t\t\tRelatedVulnerabilities: []v5.VulnerabilityReference{\n\t\t\t\t{\n\t\t\t\t\tID:        \"another-cve\",\n\t\t\t\t\tNamespace: \"nvd\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID:        \"an-other-cve\",\n\t\t\t\t\tNamespace: \"nvd\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tFix: v5.Fix{\n\t\t\t\tVersions: []string{\"4.0.5\"},\n\t\t\t\tState:    v5.FixedState,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tID:                     \"yet-another-cve\",\n\t\t\tPackageName:            \"package-name\",\n\t\t\tNamespace:              \"my-namespace\",\n\t\t\tVersionConstraint:      \"< 1000.0.0\",\n\t\t\tVersionFormat:          \"semver\",\n\t\t\tCPEs:                   nil,\n\t\t\tRelatedVulnerabilities: nil,\n\t\t\tFix: v5.Fix{\n\t\t\t\tVersions: []string{\"1000.0.1\"},\n\t\t\t\tState:    v5.FixedState,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tID:                     \"yet-another-cve-with-advisories\",\n\t\t\tPackageName:            \"package-name\",\n\t\t\tNamespace:              \"my-namespace\",\n\t\t\tVersionConstraint:      \"< 1000.0.0\",\n\t\t\tVersionFormat:          \"semver\",\n\t\t\tCPEs:                   nil,\n\t\t\tRelatedVulnerabilities: nil,\n\t\t\tFix: v5.Fix{\n\t\t\t\tVersions: []string{\"1000.0.1\"},\n\t\t\t\tState:    v5.FixedState,\n\t\t\t},\n\t\t\tAdvisories: []v5.Advisory{{ID: \"ABC-12345\", Link: \"https://abc.xyz\"}},\n\t\t},\n\t}\n\n\ttotal := append(expected, extra...)\n\n\tif err = s.AddVulnerability(total...); err != nil {\n\t\tt.Fatalf(\"failed to set Vulnerability: %+v\", err)\n\t}\n\n\tvar allEntries []model.VulnerabilityModel\n\ts.(*store).db.Find(&allEntries)\n\tif len(allEntries) != len(total) {\n\t\tt.Fatalf(\"unexpected number of entries: %d\", len(allEntries))\n\t}\n\n\tassertVulnerabilityReader(t, s, expected[0].Namespace, expected[0].PackageName, expected)\n\n}\n\nfunc assertVulnerabilityMetadataReader(t *testing.T, reader v5.VulnerabilityMetadataStoreReader, id, namespace string, expected v5.VulnerabilityMetadata) {\n\tif actual, err := reader.GetVulnerabilityMetadata(id, namespace); err != nil {\n\t\tt.Fatalf(\"failed to get metadata: %+v\", err)\n\t} else if actual == nil {\n\t\tt.Fatalf(\"no metadata returned for id=%q namespace=%q\", id, namespace)\n\t} else {\n\t\tsortMetadataCvss(actual.Cvss)\n\t\tsortMetadataCvss(expected.Cvss)\n\n\t\t// make sure they both have the same number of CVSS entries - preventing a panic on later assertions\n\t\tassert.Len(t, expected.Cvss, len(actual.Cvss))\n\t\tfor idx, actualCvss := range actual.Cvss {\n\t\t\tassert.Equal(t, actualCvss.Vector, expected.Cvss[idx].Vector)\n\t\t\tassert.Equal(t, actualCvss.Version, expected.Cvss[idx].Version)\n\t\t\tassert.Equal(t, actualCvss.Metrics, expected.Cvss[idx].Metrics)\n\n\t\t\tactualVendor, err := json.Marshal(actualCvss.VendorMetadata)\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"unable to marshal vendor metadata: %q\", err)\n\t\t\t}\n\t\t\texpectedVendor, err := json.Marshal(expected.Cvss[idx].VendorMetadata)\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"unable to marshal vendor metadata: %q\", err)\n\t\t\t}\n\t\t\tassert.Equal(t, string(actualVendor), string(expectedVendor))\n\n\t\t}\n\n\t\t// nil the Cvss field because it is an interface - verification of Cvss\n\t\t// has already happened at this point\n\t\texpected.Cvss = nil\n\t\tactual.Cvss = nil\n\t\tassert.Equal(t, &expected, actual)\n\t}\n\n}\n\nfunc sortMetadataCvss(cvss []v5.Cvss) {\n\tsort.Slice(cvss, func(i, j int) bool {\n\t\t// first, sort by Vector\n\t\tif cvss[i].Vector > cvss[j].Vector {\n\t\t\treturn true\n\t\t}\n\t\tif cvss[i].Vector < cvss[j].Vector {\n\t\t\treturn false\n\t\t}\n\t\t// then try to sort by BaseScore if Vector is the same\n\t\treturn cvss[i].Metrics.BaseScore < cvss[j].Metrics.BaseScore\n\t})\n}\n\n// CustomMetadata is effectively a noop, its values aren't meaningful and are\n// mostly useful to ensure that any type can be stored and then retrieved for\n// assertion in these test cases where custom vendor CVSS scores are used\ntype CustomMetadata struct {\n\tSuperScore string\n\tVendor     string\n}\n\nfunc TestStore_GetVulnerabilityMetadata_SetVulnerabilityMetadata(t *testing.T) {\n\tdbTempFile := t.TempDir()\n\n\ts, err := New(dbTempFile, true)\n\tif err != nil {\n\t\tt.Fatalf(\"could not create store: %+v\", err)\n\t}\n\n\ttotal := []v5.VulnerabilityMetadata{\n\t\t{\n\t\t\tID:           \"my-cve\",\n\t\t\tRecordSource: \"record-source\",\n\t\t\tNamespace:    \"namespace\",\n\t\t\tSeverity:     \"pretty bad\",\n\t\t\tURLs:         []string{\"https://ancho.re\"},\n\t\t\tDescription:  \"best description ever\",\n\t\t\tCvss: []v5.Cvss{\n\t\t\t\t{\n\t\t\t\t\tVendorMetadata: CustomMetadata{\n\t\t\t\t\t\tVendor:     \"redhat\",\n\t\t\t\t\t\tSuperScore: \"1000\",\n\t\t\t\t\t},\n\t\t\t\t\tVersion: \"2.0\",\n\t\t\t\t\tMetrics: v5.NewCvssMetrics(\n\t\t\t\t\t\t1.1,\n\t\t\t\t\t\t2.2,\n\t\t\t\t\t\t3.3,\n\t\t\t\t\t),\n\t\t\t\t\tVector: \"AV:N/AC:L/Au:N/C:P/I:P/A:P--NOT\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tVersion: \"3.0\",\n\t\t\t\t\tMetrics: v5.NewCvssMetrics(\n\t\t\t\t\t\t1.3,\n\t\t\t\t\t\t2.1,\n\t\t\t\t\t\t3.2,\n\t\t\t\t\t),\n\t\t\t\t\tVector:         \"AV:N/AC:L/Au:N/C:P/I:P/A:P--NICE\",\n\t\t\t\t\tVendorMetadata: nil,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tID:           \"my-other-cve\",\n\t\t\tRecordSource: \"record-source\",\n\t\t\tNamespace:    \"namespace\",\n\t\t\tSeverity:     \"pretty bad\",\n\t\t\tURLs:         []string{\"https://ancho.re\"},\n\t\t\tDescription:  \"worst description ever\",\n\t\t\tCvss: []v5.Cvss{\n\t\t\t\t{\n\t\t\t\t\tVersion: \"2.0\",\n\t\t\t\t\tMetrics: v5.NewCvssMetrics(\n\t\t\t\t\t\t4.1,\n\t\t\t\t\t\t5.2,\n\t\t\t\t\t\t6.3,\n\t\t\t\t\t),\n\t\t\t\t\tVector: \"AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tVersion: \"3.0\",\n\t\t\t\t\tMetrics: v5.NewCvssMetrics(\n\t\t\t\t\t\t1.4,\n\t\t\t\t\t\t2.5,\n\t\t\t\t\t\t3.6,\n\t\t\t\t\t),\n\t\t\t\t\tVector: \"AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tif err = s.AddVulnerabilityMetadata(total...); err != nil {\n\t\tt.Fatalf(\"failed to set metadata: %+v\", err)\n\t}\n\n\tvar allEntries []model.VulnerabilityMetadataModel\n\ts.(*store).db.Find(&allEntries)\n\tif len(allEntries) != len(total) {\n\t\tt.Fatalf(\"unexpected number of entries: %d\", len(allEntries))\n\t}\n\n}\n\nfunc TestStore_MergeVulnerabilityMetadata(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tadd      []v5.VulnerabilityMetadata\n\t\texpected v5.VulnerabilityMetadata\n\t\terr      bool\n\t}{\n\t\t{\n\t\t\tname: \"go-case\",\n\t\t\tadd: []v5.VulnerabilityMetadata{\n\t\t\t\t{\n\t\t\t\t\tID:           \"my-cve\",\n\t\t\t\t\tRecordSource: \"record-source\",\n\t\t\t\t\tNamespace:    \"namespace\",\n\t\t\t\t\tSeverity:     \"pretty bad\",\n\t\t\t\t\tURLs:         []string{\"https://ancho.re\"},\n\t\t\t\t\tDescription:  \"worst description ever\",\n\t\t\t\t\tCvss: []v5.Cvss{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tVersion: \"2.0\",\n\t\t\t\t\t\t\tMetrics: v5.NewCvssMetrics(\n\t\t\t\t\t\t\t\t4.1,\n\t\t\t\t\t\t\t\t5.2,\n\t\t\t\t\t\t\t\t6.3,\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\tVector: \"AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tVersion: \"3.0\",\n\t\t\t\t\t\t\tMetrics: v5.NewCvssMetrics(\n\t\t\t\t\t\t\t\t1.4,\n\t\t\t\t\t\t\t\t2.5,\n\t\t\t\t\t\t\t\t3.6,\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\tVector: \"AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: v5.VulnerabilityMetadata{\n\t\t\t\tID:           \"my-cve\",\n\t\t\t\tRecordSource: \"record-source\",\n\t\t\t\tNamespace:    \"namespace\",\n\t\t\t\tSeverity:     \"pretty bad\",\n\t\t\t\tURLs:         []string{\"https://ancho.re\"},\n\t\t\t\tDescription:  \"worst description ever\",\n\t\t\t\tCvss: []v5.Cvss{\n\t\t\t\t\t{\n\t\t\t\t\t\tVersion: \"2.0\",\n\t\t\t\t\t\tMetrics: v5.NewCvssMetrics(\n\t\t\t\t\t\t\t4.1,\n\t\t\t\t\t\t\t5.2,\n\t\t\t\t\t\t\t6.3,\n\t\t\t\t\t\t),\n\t\t\t\t\t\tVector: \"AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY\",\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tVersion: \"3.0\",\n\t\t\t\t\t\tMetrics: v5.NewCvssMetrics(\n\t\t\t\t\t\t\t1.4,\n\t\t\t\t\t\t\t2.5,\n\t\t\t\t\t\t\t3.6,\n\t\t\t\t\t\t),\n\t\t\t\t\t\tVector: \"AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"merge-links\",\n\t\t\tadd: []v5.VulnerabilityMetadata{\n\t\t\t\t{\n\t\t\t\t\tID:           \"my-cve\",\n\t\t\t\t\tRecordSource: \"record-source\",\n\t\t\t\t\tNamespace:    \"namespace\",\n\t\t\t\t\tSeverity:     \"pretty bad\",\n\t\t\t\t\tURLs:         []string{\"https://ancho.re\"},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID:           \"my-cve\",\n\t\t\t\t\tRecordSource: \"record-source\",\n\t\t\t\t\tNamespace:    \"namespace\",\n\t\t\t\t\tSeverity:     \"pretty bad\",\n\t\t\t\t\tURLs:         []string{\"https://google.com\"},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID:           \"my-cve\",\n\t\t\t\t\tRecordSource: \"record-source\",\n\t\t\t\t\tNamespace:    \"namespace\",\n\t\t\t\t\tSeverity:     \"pretty bad\",\n\t\t\t\t\tURLs:         []string{\"https://yahoo.com\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: v5.VulnerabilityMetadata{\n\t\t\t\tID:           \"my-cve\",\n\t\t\t\tRecordSource: \"record-source\",\n\t\t\t\tNamespace:    \"namespace\",\n\t\t\t\tSeverity:     \"pretty bad\",\n\t\t\t\tURLs:         []string{\"https://ancho.re\", \"https://google.com\", \"https://yahoo.com\"},\n\t\t\t\tCvss:         []v5.Cvss{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"bad-severity\",\n\t\t\tadd: []v5.VulnerabilityMetadata{\n\t\t\t\t{\n\t\t\t\t\tID:           \"my-cve\",\n\t\t\t\t\tRecordSource: \"record-source\",\n\t\t\t\t\tNamespace:    \"namespace\",\n\t\t\t\t\tSeverity:     \"pretty bad\",\n\t\t\t\t\tURLs:         []string{\"https://ancho.re\"},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID:           \"my-cve\",\n\t\t\t\t\tRecordSource: \"record-source\",\n\t\t\t\t\tNamespace:    \"namespace\",\n\t\t\t\t\tSeverity:     \"meh, push that for next tuesday...\",\n\t\t\t\t\tURLs:         []string{\"https://redhat.com\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\terr: true,\n\t\t},\n\t\t{\n\t\t\tname: \"mismatch-description\",\n\t\t\terr:  true,\n\t\t\tadd: []v5.VulnerabilityMetadata{\n\t\t\t\t{\n\n\t\t\t\t\tID:           \"my-cve\",\n\t\t\t\t\tRecordSource: \"record-source\",\n\t\t\t\t\tNamespace:    \"namespace\",\n\t\t\t\t\tSeverity:     \"pretty bad\",\n\t\t\t\t\tURLs:         []string{\"https://ancho.re\"},\n\t\t\t\t\tDescription:  \"best description ever\",\n\t\t\t\t\tCvss: []v5.Cvss{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tVersion: \"2.0\",\n\t\t\t\t\t\t\tMetrics: v5.NewCvssMetrics(\n\t\t\t\t\t\t\t\t4.1,\n\t\t\t\t\t\t\t\t5.2,\n\t\t\t\t\t\t\t\t6.3,\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\tVector: \"AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tVersion: \"3.0\",\n\t\t\t\t\t\t\tMetrics: v5.NewCvssMetrics(\n\t\t\t\t\t\t\t\t1.4,\n\t\t\t\t\t\t\t\t2.5,\n\t\t\t\t\t\t\t\t3.6,\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\tVector: \"AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID:           \"my-cve\",\n\t\t\t\t\tRecordSource: \"record-source\",\n\t\t\t\t\tNamespace:    \"namespace\",\n\t\t\t\t\tSeverity:     \"pretty bad\",\n\t\t\t\t\tURLs:         []string{\"https://ancho.re\"},\n\t\t\t\t\tDescription:  \"worst description ever\",\n\t\t\t\t\tCvss: []v5.Cvss{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tVersion: \"2.0\",\n\t\t\t\t\t\t\tMetrics: v5.NewCvssMetrics(\n\t\t\t\t\t\t\t\t4.1,\n\t\t\t\t\t\t\t\t5.2,\n\t\t\t\t\t\t\t\t6.3,\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\tVector: \"AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tVersion: \"3.0\",\n\t\t\t\t\t\t\tMetrics: v5.NewCvssMetrics(\n\t\t\t\t\t\t\t\t1.4,\n\t\t\t\t\t\t\t\t2.5,\n\t\t\t\t\t\t\t\t3.6,\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\tVector: \"AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"mismatch-cvss2\",\n\t\t\terr:  false,\n\t\t\tadd: []v5.VulnerabilityMetadata{\n\t\t\t\t{\n\t\t\t\t\tID:           \"my-cve\",\n\t\t\t\t\tRecordSource: \"record-source\",\n\t\t\t\t\tNamespace:    \"namespace\",\n\t\t\t\t\tSeverity:     \"pretty bad\",\n\t\t\t\t\tURLs:         []string{\"https://ancho.re\"},\n\t\t\t\t\tDescription:  \"best description ever\",\n\t\t\t\t\tCvss: []v5.Cvss{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tVersion: \"2.0\",\n\t\t\t\t\t\t\tMetrics: v5.NewCvssMetrics(\n\t\t\t\t\t\t\t\t4.1,\n\t\t\t\t\t\t\t\t5.2,\n\t\t\t\t\t\t\t\t6.3,\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\tVector: \"AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tVersion: \"3.0\",\n\t\t\t\t\t\t\tMetrics: v5.NewCvssMetrics(\n\t\t\t\t\t\t\t\t1.4,\n\t\t\t\t\t\t\t\t2.5,\n\t\t\t\t\t\t\t\t3.6,\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\tVector: \"AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID:           \"my-cve\",\n\t\t\t\t\tRecordSource: \"record-source\",\n\t\t\t\t\tNamespace:    \"namespace\",\n\t\t\t\t\tSeverity:     \"pretty bad\",\n\t\t\t\t\tURLs:         []string{\"https://ancho.re\"},\n\t\t\t\t\tDescription:  \"best description ever\",\n\t\t\t\t\tCvss: []v5.Cvss{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tVersion: \"2.0\",\n\t\t\t\t\t\t\tMetrics: v5.NewCvssMetrics(\n\t\t\t\t\t\t\t\t4.1,\n\t\t\t\t\t\t\t\t5.2,\n\t\t\t\t\t\t\t\t6.3,\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\tVector: \"AV:P--VERY\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tVersion: \"3.0\",\n\t\t\t\t\t\t\tMetrics: v5.NewCvssMetrics(\n\t\t\t\t\t\t\t\t1.4,\n\t\t\t\t\t\t\t\t2.5,\n\t\t\t\t\t\t\t\t3.6,\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\tVector: \"AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: v5.VulnerabilityMetadata{\n\t\t\t\tID:           \"my-cve\",\n\t\t\t\tRecordSource: \"record-source\",\n\t\t\t\tNamespace:    \"namespace\",\n\t\t\t\tSeverity:     \"pretty bad\",\n\t\t\t\tURLs:         []string{\"https://ancho.re\"},\n\t\t\t\tDescription:  \"best description ever\",\n\t\t\t\tCvss: []v5.Cvss{\n\t\t\t\t\t{\n\t\t\t\t\t\tVersion: \"2.0\",\n\t\t\t\t\t\tMetrics: v5.NewCvssMetrics(\n\t\t\t\t\t\t\t4.1,\n\t\t\t\t\t\t\t5.2,\n\t\t\t\t\t\t\t6.3,\n\t\t\t\t\t\t),\n\t\t\t\t\t\tVector: \"AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY\",\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tVersion: \"3.0\",\n\t\t\t\t\t\tMetrics: v5.NewCvssMetrics(\n\t\t\t\t\t\t\t1.4,\n\t\t\t\t\t\t\t2.5,\n\t\t\t\t\t\t\t3.6,\n\t\t\t\t\t\t),\n\t\t\t\t\t\tVector: \"AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD\",\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tVersion: \"2.0\",\n\t\t\t\t\t\tMetrics: v5.NewCvssMetrics(\n\t\t\t\t\t\t\t4.1,\n\t\t\t\t\t\t\t5.2,\n\t\t\t\t\t\t\t6.3,\n\t\t\t\t\t\t),\n\t\t\t\t\t\tVector: \"AV:P--VERY\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"mismatch-cvss3\",\n\t\t\terr:  false,\n\t\t\tadd: []v5.VulnerabilityMetadata{\n\t\t\t\t{\n\t\t\t\t\tID:           \"my-cve\",\n\t\t\t\t\tRecordSource: \"record-source\",\n\t\t\t\t\tNamespace:    \"namespace\",\n\t\t\t\t\tSeverity:     \"pretty bad\",\n\t\t\t\t\tURLs:         []string{\"https://ancho.re\"},\n\t\t\t\t\tDescription:  \"best description ever\",\n\t\t\t\t\tCvss: []v5.Cvss{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tVersion: \"2.0\",\n\t\t\t\t\t\t\tMetrics: v5.NewCvssMetrics(\n\t\t\t\t\t\t\t\t4.1,\n\t\t\t\t\t\t\t\t5.2,\n\t\t\t\t\t\t\t\t6.3,\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\tVector: \"AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tVersion: \"3.0\",\n\t\t\t\t\t\t\tMetrics: v5.NewCvssMetrics(\n\t\t\t\t\t\t\t\t1.4,\n\t\t\t\t\t\t\t\t2.5,\n\t\t\t\t\t\t\t\t3.6,\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\tVector: \"AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID:           \"my-cve\",\n\t\t\t\t\tRecordSource: \"record-source\",\n\t\t\t\t\tNamespace:    \"namespace\",\n\t\t\t\t\tSeverity:     \"pretty bad\",\n\t\t\t\t\tURLs:         []string{\"https://ancho.re\"},\n\t\t\t\t\tDescription:  \"best description ever\",\n\t\t\t\t\tCvss: []v5.Cvss{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tVersion: \"2.0\",\n\t\t\t\t\t\t\tMetrics: v5.NewCvssMetrics(\n\t\t\t\t\t\t\t\t4.1,\n\t\t\t\t\t\t\t\t5.2,\n\t\t\t\t\t\t\t\t6.3,\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\tVector: \"AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tVersion: \"3.0\",\n\t\t\t\t\t\t\tMetrics: v5.NewCvssMetrics(\n\t\t\t\t\t\t\t\t1.4,\n\t\t\t\t\t\t\t\t0,\n\t\t\t\t\t\t\t\t3.6,\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\tVector: \"AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: v5.VulnerabilityMetadata{\n\t\t\t\tID:           \"my-cve\",\n\t\t\t\tRecordSource: \"record-source\",\n\t\t\t\tNamespace:    \"namespace\",\n\t\t\t\tSeverity:     \"pretty bad\",\n\t\t\t\tURLs:         []string{\"https://ancho.re\"},\n\t\t\t\tDescription:  \"best description ever\",\n\t\t\t\tCvss: []v5.Cvss{\n\t\t\t\t\t{\n\t\t\t\t\t\tVersion: \"2.0\",\n\t\t\t\t\t\tMetrics: v5.NewCvssMetrics(\n\t\t\t\t\t\t\t4.1,\n\t\t\t\t\t\t\t5.2,\n\t\t\t\t\t\t\t6.3,\n\t\t\t\t\t\t),\n\t\t\t\t\t\tVector: \"AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY\",\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tVersion: \"3.0\",\n\t\t\t\t\t\tMetrics: v5.NewCvssMetrics(\n\t\t\t\t\t\t\t1.4,\n\t\t\t\t\t\t\t2.5,\n\t\t\t\t\t\t\t3.6,\n\t\t\t\t\t\t),\n\t\t\t\t\t\tVector: \"AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD\",\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tVersion: \"3.0\",\n\t\t\t\t\t\tMetrics: v5.NewCvssMetrics(\n\t\t\t\t\t\t\t1.4,\n\t\t\t\t\t\t\t0,\n\t\t\t\t\t\t\t3.6,\n\t\t\t\t\t\t),\n\t\t\t\t\t\tVector: \"AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tdbTempDir := t.TempDir()\n\n\t\t\ts, err := New(dbTempDir, true)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"could not create store: %+v\", err)\n\t\t\t}\n\n\t\t\t// add each metadata in order\n\t\t\tvar theErr error\n\t\t\tfor _, metadata := range test.add {\n\t\t\t\terr = s.AddVulnerabilityMetadata(metadata)\n\t\t\t\tif err != nil {\n\t\t\t\t\ttheErr = err\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif test.err && theErr == nil {\n\t\t\t\tt.Fatalf(\"expected error but did not get one\")\n\t\t\t} else if !test.err && theErr != nil {\n\t\t\t\tt.Fatalf(\"expected no error but got one: %+v\", theErr)\n\t\t\t} else if test.err && theErr != nil {\n\t\t\t\t// test pass...\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// ensure there is exactly one entry\n\t\t\tvar allEntries []model.VulnerabilityMetadataModel\n\t\t\ts.(*store).db.Find(&allEntries)\n\t\t\tif len(allEntries) != 1 {\n\t\t\t\tt.Fatalf(\"unexpected number of entries: %d\", len(allEntries))\n\t\t\t}\n\n\t\t\t// get the resulting metadata object\n\t\t\tif actual, err := s.GetVulnerabilityMetadata(test.expected.ID, test.expected.Namespace); err != nil {\n\t\t\t\tt.Fatalf(\"failed to get metadata: %+v\", err)\n\t\t\t} else {\n\t\t\t\tdiffs := deep.Equal(&test.expected, actual)\n\t\t\t\tif len(diffs) > 0 {\n\t\t\t\t\tfor _, d := range diffs {\n\t\t\t\t\t\tt.Errorf(\"Diff: %+v\", d)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestCvssScoresInMetadata(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tadd      []v5.VulnerabilityMetadata\n\t\texpected v5.VulnerabilityMetadata\n\t}{\n\t\t{\n\t\t\tname: \"append-cvss\",\n\t\t\tadd: []v5.VulnerabilityMetadata{\n\t\t\t\t{\n\t\t\t\t\tID:           \"my-cve\",\n\t\t\t\t\tRecordSource: \"record-source\",\n\t\t\t\t\tNamespace:    \"namespace\",\n\t\t\t\t\tSeverity:     \"pretty bad\",\n\t\t\t\t\tURLs:         []string{\"https://ancho.re\"},\n\t\t\t\t\tDescription:  \"worst description ever\",\n\t\t\t\t\tCvss: []v5.Cvss{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tVersion: \"2.0\",\n\t\t\t\t\t\t\tMetrics: v5.NewCvssMetrics(\n\t\t\t\t\t\t\t\t4.1,\n\t\t\t\t\t\t\t\t5.2,\n\t\t\t\t\t\t\t\t6.3,\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\tVector: \"AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID:           \"my-cve\",\n\t\t\t\t\tRecordSource: \"record-source\",\n\t\t\t\t\tNamespace:    \"namespace\",\n\t\t\t\t\tSeverity:     \"pretty bad\",\n\t\t\t\t\tURLs:         []string{\"https://ancho.re\"},\n\t\t\t\t\tDescription:  \"worst description ever\",\n\t\t\t\t\tCvss: []v5.Cvss{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tVersion: \"3.0\",\n\t\t\t\t\t\t\tMetrics: v5.NewCvssMetrics(\n\t\t\t\t\t\t\t\t1.4,\n\t\t\t\t\t\t\t\t2.5,\n\t\t\t\t\t\t\t\t3.6,\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\tVector: \"AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: v5.VulnerabilityMetadata{\n\t\t\t\tID:           \"my-cve\",\n\t\t\t\tRecordSource: \"record-source\",\n\t\t\t\tNamespace:    \"namespace\",\n\t\t\t\tSeverity:     \"pretty bad\",\n\t\t\t\tURLs:         []string{\"https://ancho.re\"},\n\t\t\t\tDescription:  \"worst description ever\",\n\t\t\t\tCvss: []v5.Cvss{\n\t\t\t\t\t{\n\t\t\t\t\t\tVersion: \"2.0\",\n\t\t\t\t\t\tMetrics: v5.NewCvssMetrics(\n\t\t\t\t\t\t\t4.1,\n\t\t\t\t\t\t\t5.2,\n\t\t\t\t\t\t\t6.3,\n\t\t\t\t\t\t),\n\t\t\t\t\t\tVector: \"AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY\",\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tVersion: \"3.0\",\n\t\t\t\t\t\tMetrics: v5.NewCvssMetrics(\n\t\t\t\t\t\t\t1.4,\n\t\t\t\t\t\t\t2.5,\n\t\t\t\t\t\t\t3.6,\n\t\t\t\t\t\t),\n\t\t\t\t\t\tVector: \"AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"append-vendor-cvss\",\n\t\t\tadd: []v5.VulnerabilityMetadata{\n\t\t\t\t{\n\t\t\t\t\tID:           \"my-cve\",\n\t\t\t\t\tRecordSource: \"record-source\",\n\t\t\t\t\tNamespace:    \"namespace\",\n\t\t\t\t\tSeverity:     \"pretty bad\",\n\t\t\t\t\tURLs:         []string{\"https://ancho.re\"},\n\t\t\t\t\tDescription:  \"worst description ever\",\n\t\t\t\t\tCvss: []v5.Cvss{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tVersion: \"2.0\",\n\t\t\t\t\t\t\tMetrics: v5.NewCvssMetrics(\n\t\t\t\t\t\t\t\t4.1,\n\t\t\t\t\t\t\t\t5.2,\n\t\t\t\t\t\t\t\t6.3,\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\tVector: \"AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID:           \"my-cve\",\n\t\t\t\t\tRecordSource: \"record-source\",\n\t\t\t\t\tNamespace:    \"namespace\",\n\t\t\t\t\tSeverity:     \"pretty bad\",\n\t\t\t\t\tURLs:         []string{\"https://ancho.re\"},\n\t\t\t\t\tDescription:  \"worst description ever\",\n\t\t\t\t\tCvss: []v5.Cvss{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tVersion: \"2.0\",\n\t\t\t\t\t\t\tMetrics: v5.NewCvssMetrics(\n\t\t\t\t\t\t\t\t4.1,\n\t\t\t\t\t\t\t\t5.2,\n\t\t\t\t\t\t\t\t6.3,\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\tVector: \"AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY\",\n\t\t\t\t\t\t\tVendorMetadata: CustomMetadata{\n\t\t\t\t\t\t\t\tSuperScore: \"100\",\n\t\t\t\t\t\t\t\tVendor:     \"debian\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: v5.VulnerabilityMetadata{\n\t\t\t\tID:           \"my-cve\",\n\t\t\t\tRecordSource: \"record-source\",\n\t\t\t\tNamespace:    \"namespace\",\n\t\t\t\tSeverity:     \"pretty bad\",\n\t\t\t\tURLs:         []string{\"https://ancho.re\"},\n\t\t\t\tDescription:  \"worst description ever\",\n\t\t\t\tCvss: []v5.Cvss{\n\t\t\t\t\t{\n\t\t\t\t\t\tVersion: \"2.0\",\n\t\t\t\t\t\tMetrics: v5.NewCvssMetrics(\n\t\t\t\t\t\t\t4.1,\n\t\t\t\t\t\t\t5.2,\n\t\t\t\t\t\t\t6.3,\n\t\t\t\t\t\t),\n\t\t\t\t\t\tVector: \"AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY\",\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tVersion: \"2.0\",\n\t\t\t\t\t\tMetrics: v5.NewCvssMetrics(\n\t\t\t\t\t\t\t4.1,\n\t\t\t\t\t\t\t5.2,\n\t\t\t\t\t\t\t6.3,\n\t\t\t\t\t\t),\n\t\t\t\t\t\tVector: \"AV:N/AC:L/Au:N/C:P/I:P/A:P--VERY\",\n\t\t\t\t\t\tVendorMetadata: CustomMetadata{\n\t\t\t\t\t\t\tSuperScore: \"100\",\n\t\t\t\t\t\t\tVendor:     \"debian\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"avoids-duplicate-cvss\",\n\t\t\tadd: []v5.VulnerabilityMetadata{\n\t\t\t\t{\n\t\t\t\t\tID:           \"my-cve\",\n\t\t\t\t\tRecordSource: \"record-source\",\n\t\t\t\t\tNamespace:    \"namespace\",\n\t\t\t\t\tSeverity:     \"pretty bad\",\n\t\t\t\t\tURLs:         []string{\"https://ancho.re\"},\n\t\t\t\t\tDescription:  \"worst description ever\",\n\t\t\t\t\tCvss: []v5.Cvss{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tVersion: \"3.0\",\n\t\t\t\t\t\t\tMetrics: v5.NewCvssMetrics(\n\t\t\t\t\t\t\t\t1.4,\n\t\t\t\t\t\t\t\t2.5,\n\t\t\t\t\t\t\t\t3.6,\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\tVector: \"AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID:           \"my-cve\",\n\t\t\t\t\tRecordSource: \"record-source\",\n\t\t\t\t\tNamespace:    \"namespace\",\n\t\t\t\t\tSeverity:     \"pretty bad\",\n\t\t\t\t\tURLs:         []string{\"https://ancho.re\"},\n\t\t\t\t\tDescription:  \"worst description ever\",\n\t\t\t\t\tCvss: []v5.Cvss{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tVersion: \"3.0\",\n\t\t\t\t\t\t\tMetrics: v5.NewCvssMetrics(\n\t\t\t\t\t\t\t\t1.4,\n\t\t\t\t\t\t\t\t2.5,\n\t\t\t\t\t\t\t\t3.6,\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\tVector: \"AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: v5.VulnerabilityMetadata{\n\t\t\t\tID:           \"my-cve\",\n\t\t\t\tRecordSource: \"record-source\",\n\t\t\t\tNamespace:    \"namespace\",\n\t\t\t\tSeverity:     \"pretty bad\",\n\t\t\t\tURLs:         []string{\"https://ancho.re\"},\n\t\t\t\tDescription:  \"worst description ever\",\n\t\t\t\tCvss: []v5.Cvss{\n\t\t\t\t\t{\n\t\t\t\t\t\tVersion: \"3.0\",\n\t\t\t\t\t\tMetrics: v5.NewCvssMetrics(\n\t\t\t\t\t\t\t1.4,\n\t\t\t\t\t\t\t2.5,\n\t\t\t\t\t\t\t3.6,\n\t\t\t\t\t\t),\n\t\t\t\t\t\tVector: \"AV:N/AC:L/Au:N/C:P/I:P/A:P--GOOD\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tdbTempDir := t.TempDir()\n\n\t\t\ts, err := New(dbTempDir, true)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"could not create s: %+v\", err)\n\t\t\t}\n\n\t\t\t// add each metadata in order\n\t\t\tfor _, metadata := range test.add {\n\t\t\t\terr = s.AddVulnerabilityMetadata(metadata)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"unable to s vulnerability metadata: %+v\", err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// ensure there is exactly one entry\n\t\t\tvar allEntries []model.VulnerabilityMetadataModel\n\t\t\ts.(*store).db.Find(&allEntries)\n\t\t\tif len(allEntries) != 1 {\n\t\t\t\tt.Fatalf(\"unexpected number of entries: %d\", len(allEntries))\n\t\t\t}\n\n\t\t\tassertVulnerabilityMetadataReader(t, s, test.expected.ID, test.expected.Namespace, test.expected)\n\t\t})\n\t}\n}\n\nfunc assertVulnerabilityMatchExclusionReader(t *testing.T, reader v5.VulnerabilityMatchExclusionStoreReader, id string, expected []v5.VulnerabilityMatchExclusion) {\n\tif actual, err := reader.GetVulnerabilityMatchExclusion(id); err != nil {\n\t\tt.Fatalf(\"failed to get Vulnerability Match Exclusion: %+v\", err)\n\t} else {\n\t\tt.Logf(\"%+v\", actual)\n\t\tif len(actual) != len(expected) {\n\t\t\tt.Fatalf(\"unexpected number of vulnerability match exclusions: expected=%d, actual=%d\", len(expected), len(actual))\n\t\t}\n\t\tfor idx := range actual {\n\t\t\tdiffs := deep.Equal(expected[idx], actual[idx])\n\t\t\tif len(diffs) > 0 {\n\t\t\t\tfor _, d := range diffs {\n\t\t\t\t\tt.Errorf(\"Diff: %+v\", d)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc TestStore_GetVulnerabilityMatchExclusion_SetVulnerabilityMatchExclusion(t *testing.T) {\n\tdbTempFile := t.TempDir()\n\n\ts, err := New(dbTempFile, true)\n\tif err != nil {\n\t\tt.Fatalf(\"could not create store: %+v\", err)\n\t}\n\n\textra := []v5.VulnerabilityMatchExclusion{\n\t\t{\n\t\t\tID: \"CVE-1234-14567\",\n\t\t\tConstraints: []v5.VulnerabilityMatchExclusionConstraint{\n\t\t\t\t{\n\t\t\t\t\tVulnerability: v5.VulnerabilityExclusionConstraint{\n\t\t\t\t\t\tNamespace: \"extra-namespace:cpe\",\n\t\t\t\t\t},\n\t\t\t\t\tPackage: v5.PackageExclusionConstraint{\n\t\t\t\t\t\tName:     \"abc\",\n\t\t\t\t\t\tLanguage: \"ruby\",\n\t\t\t\t\t\tVersion:  \"1.2.3\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tVulnerability: v5.VulnerabilityExclusionConstraint{\n\t\t\t\t\t\tNamespace: \"extra-namespace:cpe\",\n\t\t\t\t\t},\n\t\t\t\t\tPackage: v5.PackageExclusionConstraint{\n\t\t\t\t\t\tName:     \"abc\",\n\t\t\t\t\t\tLanguage: \"ruby\",\n\t\t\t\t\t\tVersion:  \"4.5.6\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tVulnerability: v5.VulnerabilityExclusionConstraint{\n\t\t\t\t\t\tNamespace: \"extra-namespace:cpe\",\n\t\t\t\t\t},\n\t\t\t\t\tPackage: v5.PackageExclusionConstraint{\n\t\t\t\t\t\tName:     \"time-1\",\n\t\t\t\t\t\tLanguage: \"ruby\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tVulnerability: v5.VulnerabilityExclusionConstraint{\n\t\t\t\t\t\tNamespace: \"extra-namespace:cpe\",\n\t\t\t\t\t},\n\t\t\t\t\tPackage: v5.PackageExclusionConstraint{\n\t\t\t\t\t\tName: \"abc.xyz:nothing-of-interest\",\n\t\t\t\t\t\tType: \"java-archive\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tJustification: \"Because I said so.\",\n\t\t},\n\t\t{\n\t\t\tID:            \"CVE-1234-10\",\n\t\t\tConstraints:   nil,\n\t\t\tJustification: \"Because I said so.\",\n\t\t},\n\t}\n\n\texpected := []v5.VulnerabilityMatchExclusion{\n\t\t{\n\t\t\tID: \"CVE-1234-9999999\",\n\t\t\tConstraints: []v5.VulnerabilityMatchExclusionConstraint{\n\t\t\t\t{\n\t\t\t\t\tVulnerability: v5.VulnerabilityExclusionConstraint{\n\t\t\t\t\t\tNamespace: \"old-namespace:cpe\",\n\t\t\t\t\t},\n\t\t\t\t\tPackage: v5.PackageExclusionConstraint{\n\t\t\t\t\t\tLanguage: \"python\",\n\t\t\t\t\t\tName:     \"abc\",\n\t\t\t\t\t\tVersion:  \"1.2.3\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tVulnerability: v5.VulnerabilityExclusionConstraint{\n\t\t\t\t\t\tNamespace: \"old-namespace:cpe\",\n\t\t\t\t\t},\n\t\t\t\t\tPackage: v5.PackageExclusionConstraint{\n\t\t\t\t\t\tLanguage: \"python\",\n\t\t\t\t\t\tName:     \"abc\",\n\t\t\t\t\t\tVersion:  \"4.5.6\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tVulnerability: v5.VulnerabilityExclusionConstraint{\n\t\t\t\t\t\tNamespace: \"old-namespace:cpe\",\n\t\t\t\t\t},\n\t\t\t\t\tPackage: v5.PackageExclusionConstraint{\n\t\t\t\t\t\tLanguage: \"python\",\n\t\t\t\t\t\tName:     \"time-245\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tVulnerability: v5.VulnerabilityExclusionConstraint{\n\t\t\t\t\t\tNamespace: \"old-namespace:cpe\",\n\t\t\t\t\t},\n\t\t\t\t\tPackage: v5.PackageExclusionConstraint{\n\t\t\t\t\t\tType: \"npm\",\n\t\t\t\t\t\tName: \"everything\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tJustification: \"This is a false positive\",\n\t\t},\n\t\t{\n\t\t\tID: \"CVE-1234-9999999\",\n\t\t\tConstraints: []v5.VulnerabilityMatchExclusionConstraint{\n\t\t\t\t{\n\t\t\t\t\tVulnerability: v5.VulnerabilityExclusionConstraint{\n\t\t\t\t\t\tNamespace: \"old-namespace:cpe\",\n\t\t\t\t\t},\n\t\t\t\t\tPackage: v5.PackageExclusionConstraint{\n\t\t\t\t\t\tLanguage: \"go\",\n\t\t\t\t\t\tType:     \"go-module\",\n\t\t\t\t\t\tName:     \"abc\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tVulnerability: v5.VulnerabilityExclusionConstraint{\n\t\t\t\t\t\tNamespace: \"some-other-namespace:cpe\",\n\t\t\t\t\t},\n\t\t\t\t\tPackage: v5.PackageExclusionConstraint{\n\t\t\t\t\t\tLanguage: \"go\",\n\t\t\t\t\t\tType:     \"go-module\",\n\t\t\t\t\t\tName:     \"abc\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tVulnerability: v5.VulnerabilityExclusionConstraint{\n\t\t\t\t\t\tFixState: \"wont-fix\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tJustification: \"This is also a false positive\",\n\t\t},\n\t\t{\n\t\t\tID:            \"CVE-1234-9999999\",\n\t\t\tJustification: \"global exclude\",\n\t\t},\n\t}\n\n\ttotal := append(expected, extra...)\n\n\tif err = s.AddVulnerabilityMatchExclusion(total...); err != nil {\n\t\tt.Fatalf(\"failed to set Vulnerability Match Exclusion: %+v\", err)\n\t}\n\n\tvar allEntries []model.VulnerabilityMatchExclusionModel\n\ts.(*store).db.Find(&allEntries)\n\tif len(allEntries) != len(total) {\n\t\tt.Fatalf(\"unexpected number of entries: %d\", len(allEntries))\n\t}\n\tassertVulnerabilityMatchExclusionReader(t, s, expected[0].ID, expected)\n}\n\nfunc Test_DiffStore(t *testing.T) {\n\t//GIVEN\n\tdbTempFile := t.TempDir()\n\ts1, err := New(dbTempFile, true)\n\tif err != nil {\n\t\tt.Fatalf(\"could not create store: %+v\", err)\n\t}\n\tdbTempFile = t.TempDir()\n\ts2, err := New(dbTempFile, true)\n\tif err != nil {\n\t\tt.Fatalf(\"could not create store: %+v\", err)\n\t}\n\n\tbaseVulns := []v5.Vulnerability{\n\t\t{\n\t\t\tNamespace:         \"github:language:python\",\n\t\t\tID:                \"CVE-123-4567\",\n\t\t\tPackageName:       \"pypi:requests\",\n\t\t\tVersionConstraint: \"< 2.0 >= 1.29\",\n\t\t\tCPEs:              []string{\"cpe:2.3:pypi:requests:*:*:*:*:*:*\"},\n\t\t},\n\t\t{\n\t\t\tNamespace:         \"github:language:python\",\n\t\t\tID:                \"CVE-123-4567\",\n\t\t\tPackageName:       \"pypi:requests\",\n\t\t\tVersionConstraint: \"< 3.0 >= 2.17\",\n\t\t\tCPEs:              []string{\"cpe:2.3:pypi:requests:*:*:*:*:*:*\"},\n\t\t},\n\t\t{\n\t\t\tNamespace:         \"npm\",\n\t\t\tID:                \"CVE-123-7654\",\n\t\t\tPackageName:       \"npm:axios\",\n\t\t\tVersionConstraint: \"< 3.0 >= 2.17\",\n\t\t\tCPEs:              []string{\"cpe:2.3:npm:axios:*:*:*:*:*:*\"},\n\t\t\tFix: v5.Fix{\n\t\t\t\tState: v5.UnknownFixState,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tNamespace:         \"nuget\",\n\t\t\tID:                \"GHSA-****-******\",\n\t\t\tPackageName:       \"nuget:net\",\n\t\t\tVersionConstraint: \"< 3.0 >= 2.17\",\n\t\t\tCPEs:              []string{\"cpe:2.3:nuget:net:*:*:*:*:*:*\"},\n\t\t\tFix: v5.Fix{\n\t\t\t\tState: v5.UnknownFixState,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tNamespace:         \"hex\",\n\t\t\tID:                \"GHSA-^^^^-^^^^^^\",\n\t\t\tPackageName:       \"hex:esbuild\",\n\t\t\tVersionConstraint: \"< 3.0 >= 2.17\",\n\t\t\tCPEs:              []string{\"cpe:2.3:hex:esbuild:*:*:*:*:*:*\"},\n\t\t},\n\t}\n\tbaseMetadata := []v5.VulnerabilityMetadata{\n\t\t{\n\t\t\tNamespace:  \"nuget\",\n\t\t\tID:         \"GHSA-****-******\",\n\t\t\tDataSource: \"nvd\",\n\t\t},\n\t}\n\ttargetVulns := []v5.Vulnerability{\n\t\t{\n\t\t\tNamespace:         \"github:language:python\",\n\t\t\tID:                \"CVE-123-4567\",\n\t\t\tPackageName:       \"pypi:requests\",\n\t\t\tVersionConstraint: \"< 2.0 >= 1.29\",\n\t\t\tCPEs:              []string{\"cpe:2.3:pypi:requests:*:*:*:*:*:*\"},\n\t\t},\n\t\t{\n\t\t\tNamespace:         \"github:language:go\",\n\t\t\tID:                \"GHSA-....-....\",\n\t\t\tPackageName:       \"hashicorp:nomad\",\n\t\t\tVersionConstraint: \"< 3.0 >= 2.17\",\n\t\t\tCPEs:              []string{\"cpe:2.3:golang:hashicorp:nomad:*:*:*:*:*\"},\n\t\t},\n\t\t{\n\t\t\tNamespace:         \"github:language:go\",\n\t\t\tID:                \"GHSA-....-....\",\n\t\t\tPackageName:       \"hashicorp:n\",\n\t\t\tVersionConstraint: \"< 2.0 >= 1.17\",\n\t\t\tCPEs:              []string{\"cpe:2.3:golang:hashicorp:n:*:*:*:*:*\"},\n\t\t},\n\t\t{\n\t\t\tNamespace:         \"npm\",\n\t\t\tID:                \"CVE-123-7654\",\n\t\t\tPackageName:       \"npm:axios\",\n\t\t\tVersionConstraint: \"< 3.0 >= 2.17\",\n\t\t\tCPEs:              []string{\"cpe:2.3:npm:axios:*:*:*:*:*:*\"},\n\t\t\tFix: v5.Fix{\n\t\t\t\tState: v5.WontFixState,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tNamespace:         \"nuget\",\n\t\t\tID:                \"GHSA-****-******\",\n\t\t\tPackageName:       \"nuget:net\",\n\t\t\tVersionConstraint: \"< 3.0 >= 2.17\",\n\t\t\tCPEs:              []string{\"cpe:2.3:nuget:net:*:*:*:*:*:*\"},\n\t\t\tFix: v5.Fix{\n\t\t\t\tState: v5.UnknownFixState,\n\t\t\t},\n\t\t},\n\t}\n\texpectedDiffs := []v5.Diff{\n\t\t{\n\t\t\tReason:    v5.DiffChanged,\n\t\t\tID:        \"CVE-123-4567\",\n\t\t\tNamespace: \"github:language:python\",\n\t\t\tPackages:  []string{\"pypi:requests\"},\n\t\t},\n\t\t{\n\t\t\tReason:    v5.DiffChanged,\n\t\t\tID:        \"CVE-123-7654\",\n\t\t\tNamespace: \"npm\",\n\t\t\tPackages:  []string{\"npm:axios\"},\n\t\t},\n\t\t{\n\t\t\tReason:    v5.DiffRemoved,\n\t\t\tID:        \"GHSA-****-******\",\n\t\t\tNamespace: \"nuget\",\n\t\t\tPackages:  []string{\"nuget:net\"},\n\t\t},\n\t\t{\n\t\t\tReason:    v5.DiffAdded,\n\t\t\tID:        \"GHSA-....-....\",\n\t\t\tNamespace: \"github:language:go\",\n\t\t\tPackages:  []string{\"hashicorp:n\", \"hashicorp:nomad\"},\n\t\t},\n\t\t{\n\t\t\tReason:    v5.DiffRemoved,\n\t\t\tID:        \"GHSA-^^^^-^^^^^^\",\n\t\t\tNamespace: \"hex\",\n\t\t\tPackages:  []string{\"hex:esbuild\"},\n\t\t},\n\t}\n\n\tfor _, vuln := range baseVulns {\n\t\ts1.AddVulnerability(vuln)\n\t}\n\tfor _, vuln := range targetVulns {\n\t\ts2.AddVulnerability(vuln)\n\t}\n\tfor _, meta := range baseMetadata {\n\t\ts1.AddVulnerabilityMetadata(meta)\n\t}\n\n\t//WHEN\n\tresult, err := s1.DiffStore(s2)\n\n\t//THEN\n\tsort.SliceStable(*result, func(i, j int) bool {\n\t\treturn (*result)[i].ID < (*result)[j].ID\n\t})\n\tfor i := range *result {\n\t\tsort.Strings((*result)[i].Packages)\n\t}\n\n\tassert.NoError(t, err)\n\tassert.Equal(t, expectedDiffs, *result)\n}\n"
  },
  {
    "path": "grype/db/v5/store.go",
    "content": "package v5\n\nimport \"io\"\n\ntype Store interface {\n\tStoreReader\n\tStoreWriter\n}\n\ntype StoreReader interface {\n\tIDReader\n\tDiffReader\n\tVulnerabilityStoreReader\n\tVulnerabilityMetadataStoreReader\n\tVulnerabilityMatchExclusionStoreReader\n\tio.Closer\n}\n\ntype StoreWriter interface {\n\tIDWriter\n\tVulnerabilityStoreWriter\n\tVulnerabilityMetadataStoreWriter\n\tVulnerabilityMatchExclusionStoreWriter\n\tio.Closer\n}\n\ntype DiffReader interface {\n\tDiffStore(s StoreReader) (*[]Diff, error)\n}\n"
  },
  {
    "path": "grype/db/v5/vulnerability.go",
    "content": "package v5\n\nimport (\n\t\"fmt\"\n\t\"sort\"\n\t\"strings\"\n\n\tqualifierV5 \"github.com/anchore/grype/grype/db/v5/pkg/qualifier\"\n\t\"github.com/anchore/grype/grype/pkg/qualifier\"\n\t\"github.com/anchore/grype/grype/version\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n\t\"github.com/anchore/grype/internal/log\"\n\t\"github.com/anchore/syft/syft/cpe\"\n)\n\n// Vulnerability represents the minimum data fields necessary to perform package-to-vulnerability matching. This can represent a CVE, 3rd party advisory, or any source that relates back to a CVE.\ntype Vulnerability struct {\n\tID                     string                   `json:\"id\"`                      // The identifier of the vulnerability or advisory\n\tPackageName            string                   `json:\"package_name\"`            // The name of the package that is vulnerable\n\tNamespace              string                   `json:\"namespace\"`               // The ecosystem where the package resides\n\tPackageQualifiers      []qualifierV5.Qualifier  `json:\"package_qualifiers\"`      // The qualifiers for determining if a package is vulnerable\n\tVersionConstraint      string                   `json:\"version_constraint\"`      // The version range which the given package is vulnerable\n\tVersionFormat          string                   `json:\"version_format\"`          // The format which all version fields should be interpreted as\n\tCPEs                   []string                 `json:\"cpes\"`                    // The CPEs which are considered vulnerable\n\tRelatedVulnerabilities []VulnerabilityReference `json:\"related_vulnerabilities\"` // Other Vulnerabilities that are related to this one (e.g. GHSA relate to CVEs, or how distro CVE relates to NVD record)\n\tFix                    Fix                      `json:\"fix\"`                     // All information about fixed versions\n\tAdvisories             []Advisory               `json:\"advisories\"`              // Any vendor advisories about fixes or other notifications about this vulnerability\n}\n\ntype VulnerabilityReference struct {\n\tID        string `json:\"id\"`\n\tNamespace string `json:\"namespace\"`\n}\n\n//nolint:gocognit\nfunc (v *Vulnerability) Equal(vv Vulnerability) bool {\n\tequal := v.ID == vv.ID &&\n\t\tv.PackageName == vv.PackageName &&\n\t\tv.Namespace == vv.Namespace &&\n\t\tlen(v.PackageQualifiers) == len(vv.PackageQualifiers) &&\n\t\tv.VersionConstraint == vv.VersionConstraint &&\n\t\tv.VersionFormat == vv.VersionFormat &&\n\t\tlen(v.CPEs) == len(vv.CPEs) &&\n\t\tlen(v.RelatedVulnerabilities) == len(vv.RelatedVulnerabilities) &&\n\t\tlen(v.Advisories) == len(vv.Advisories) &&\n\t\tv.Fix.State == vv.Fix.State &&\n\t\tlen(v.Fix.Versions) == len(vv.Fix.Versions)\n\n\tif !equal {\n\t\treturn false\n\t}\n\n\tsort.Strings(v.CPEs)\n\tsort.Strings(vv.CPEs)\n\tfor idx, cpe := range v.CPEs {\n\t\tif cpe != vv.CPEs[idx] {\n\t\t\treturn false\n\t\t}\n\t}\n\n\tsortedBaseRelVulns, sortedTargetRelVulns := sortRelatedVulns(v.RelatedVulnerabilities), sortRelatedVulns(vv.RelatedVulnerabilities)\n\tfor idx, item := range sortedBaseRelVulns {\n\t\tif item != sortedTargetRelVulns[idx] {\n\t\t\treturn false\n\t\t}\n\t}\n\tsortedBaseAdvisories, sortedTargetAdvisories := sortAdvisories(v.Advisories), sortAdvisories(vv.Advisories)\n\tfor idx, item := range sortedBaseAdvisories {\n\t\tif item != sortedTargetAdvisories[idx] {\n\t\t\treturn false\n\t\t}\n\t}\n\tsortedBasePkgQualifiers, sortedTargetPkgQualifiers := sortPackageQualifiers(v.PackageQualifiers), sortPackageQualifiers(vv.PackageQualifiers)\n\tfor idx, item := range sortedBasePkgQualifiers {\n\t\tif item != sortedTargetPkgQualifiers[idx] {\n\t\t\treturn false\n\t\t}\n\t}\n\n\tsort.Strings(v.Fix.Versions)\n\tsort.Strings(vv.Fix.Versions)\n\tfor idx, item := range v.Fix.Versions {\n\t\tif item != vv.Fix.Versions[idx] {\n\t\t\treturn false\n\t\t}\n\t}\n\n\treturn true\n}\n\nfunc sortRelatedVulns(vulns []VulnerabilityReference) []VulnerabilityReference {\n\tsort.SliceStable(vulns, func(i, j int) bool {\n\t\tb1, b2 := strings.Builder{}, strings.Builder{}\n\t\tb1.WriteString(vulns[i].ID)\n\t\tb1.WriteString(vulns[i].Namespace)\n\t\tb2.WriteString(vulns[j].ID)\n\t\tb2.WriteString(vulns[j].Namespace)\n\t\treturn b1.String() < b2.String()\n\t})\n\treturn vulns\n}\n\nfunc sortAdvisories(advisories []Advisory) []Advisory {\n\tsort.SliceStable(advisories, func(i, j int) bool {\n\t\tb1, b2 := strings.Builder{}, strings.Builder{}\n\t\tb1.WriteString(advisories[i].ID)\n\t\tb1.WriteString(advisories[i].Link)\n\t\tb2.WriteString(advisories[j].ID)\n\t\tb2.WriteString(advisories[j].Link)\n\t\treturn b1.String() < b2.String()\n\t})\n\treturn advisories\n}\n\nfunc sortPackageQualifiers(qualifiers []qualifierV5.Qualifier) []qualifierV5.Qualifier {\n\tsort.SliceStable(qualifiers, func(i, j int) bool {\n\t\treturn qualifiers[i].String() < qualifiers[j].String()\n\t})\n\treturn qualifiers\n}\n\nfunc NewVulnerability(vuln Vulnerability) (*vulnerability.Vulnerability, error) {\n\tformat := version.ParseFormat(vuln.VersionFormat)\n\n\tconstraint, err := version.GetConstraint(vuln.VersionConstraint, format)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to parse constraint='%s' format='%s': %w\", vuln.VersionConstraint, format, err)\n\t}\n\n\tpkgQualifiers := make([]qualifier.Qualifier, len(vuln.PackageQualifiers))\n\tfor idx, q := range vuln.PackageQualifiers {\n\t\tpkgQualifiers[idx] = q.Parse()\n\t}\n\n\tadvisories := make([]vulnerability.Advisory, len(vuln.Advisories))\n\tfor idx, advisory := range vuln.Advisories {\n\t\tadvisories[idx] = vulnerability.Advisory{\n\t\t\tID:   advisory.ID,\n\t\t\tLink: advisory.Link,\n\t\t}\n\t}\n\n\tvar relatedVulnerabilities []vulnerability.Reference\n\tfor _, r := range vuln.RelatedVulnerabilities {\n\t\trelatedVulnerabilities = append(relatedVulnerabilities, vulnerability.Reference{\n\t\t\tID:        r.ID,\n\t\t\tNamespace: r.Namespace,\n\t\t})\n\t}\n\n\tvar cpes []cpe.CPE\n\tfor _, cp := range vuln.CPEs {\n\t\tc, err := cpe.New(cp, \"\")\n\t\tif err != nil {\n\t\t\tlog.WithFields(\"err\", err, \"cpe\", cp).Debug(\"failed to parse CPE\")\n\t\t\tcontinue\n\t\t}\n\t\tcpes = append(cpes, c)\n\t}\n\n\treturn &vulnerability.Vulnerability{\n\t\tPackageName: vuln.PackageName,\n\t\tConstraint:  constraint,\n\t\tReference: vulnerability.Reference{\n\t\t\tID:        vuln.ID,\n\t\t\tNamespace: vuln.Namespace,\n\t\t},\n\t\tCPEs:              cpes,\n\t\tPackageQualifiers: pkgQualifiers,\n\t\tFix: vulnerability.Fix{\n\t\t\tVersions: vuln.Fix.Versions,\n\t\t\tState:    vulnerability.FixState(vuln.Fix.State),\n\t\t},\n\t\tAdvisories:             advisories,\n\t\tRelatedVulnerabilities: relatedVulnerabilities,\n\t}, nil\n}\n"
  },
  {
    "path": "grype/db/v5/vulnerability_match_exclusion.go",
    "content": "package v5\n\nimport (\n\t\"encoding/json\"\n)\n\n// VulnerabilityMatchExclusion represents the minimum data fields necessary to automatically filter certain\n// vulnerabilities from match results based on the specified constraints.\ntype VulnerabilityMatchExclusion struct {\n\tID            string                                  `json:\"id\"`                    // The identifier of the vulnerability or advisory\n\tConstraints   []VulnerabilityMatchExclusionConstraint `json:\"constraints,omitempty\"` // The constraints under which the exclusion applies\n\tJustification string                                  `json:\"justification\"`         // Justification for the exclusion\n}\n\n// VulnerabilityMatchExclusionConstraint describes criteria for which matches should be excluded\ntype VulnerabilityMatchExclusionConstraint struct {\n\tVulnerability VulnerabilityExclusionConstraint `json:\"vulnerability,omitempty\"` // Vulnerability exclusion criteria\n\tPackage       PackageExclusionConstraint       `json:\"package,omitempty\"`       // Package exclusion criteria\n\tExtraFields   map[string]interface{}           `json:\"-\"`\n}\n\nfunc (c VulnerabilityMatchExclusionConstraint) Usable() bool {\n\treturn len(c.ExtraFields) == 0 && c.Vulnerability.Usable() && c.Package.Usable()\n}\n\nfunc (c *VulnerabilityMatchExclusionConstraint) UnmarshalJSON(data []byte) error {\n\t// Create a new type from the target type to avoid recursion.\n\ttype _vulnerabilityMatchExclusionConstraint VulnerabilityMatchExclusionConstraint\n\n\t// Unmarshal into an instance of the new type.\n\tvar _c _vulnerabilityMatchExclusionConstraint\n\tif err := json.Unmarshal(data, &_c); err != nil {\n\t\treturn err\n\t}\n\n\tif err := json.Unmarshal(data, &_c.ExtraFields); err != nil {\n\t\treturn err\n\t}\n\n\tdelete(_c.ExtraFields, \"vulnerability\")\n\tdelete(_c.ExtraFields, \"package\")\n\n\tif len(_c.ExtraFields) == 0 {\n\t\t_c.ExtraFields = nil\n\t}\n\n\t// Cast the new type instance to the original type and assign.\n\t*c = VulnerabilityMatchExclusionConstraint(_c)\n\treturn nil\n}\n\n// VulnerabilityExclusionConstraint describes criteria for excluding a match based on additional vulnerability components\ntype VulnerabilityExclusionConstraint struct {\n\tNamespace   string                 `json:\"namespace,omitempty\"` // Vulnerability namespace\n\tFixState    FixState               `json:\"fix_state,omitempty\"` // Vulnerability fix state\n\tExtraFields map[string]interface{} `json:\"-\"`\n}\n\nfunc (v VulnerabilityExclusionConstraint) Usable() bool {\n\treturn len(v.ExtraFields) == 0\n}\n\nfunc (v *VulnerabilityExclusionConstraint) UnmarshalJSON(data []byte) error {\n\t// Create a new type from the target type to avoid recursion.\n\ttype _vulnerabilityExclusionConstraint VulnerabilityExclusionConstraint\n\n\t// Unmarshal into an instance of the new type.\n\tvar _v _vulnerabilityExclusionConstraint\n\tif err := json.Unmarshal(data, &_v); err != nil {\n\t\treturn err\n\t}\n\n\tif err := json.Unmarshal(data, &_v.ExtraFields); err != nil {\n\t\treturn err\n\t}\n\n\tdelete(_v.ExtraFields, \"namespace\")\n\tdelete(_v.ExtraFields, \"fix_state\")\n\n\tif len(_v.ExtraFields) == 0 {\n\t\t_v.ExtraFields = nil\n\t}\n\n\t// Cast the new type instance to the original type and assign.\n\t*v = VulnerabilityExclusionConstraint(_v)\n\treturn nil\n}\n\n// PackageExclusionConstraint describes criteria for excluding a match based on package components\ntype PackageExclusionConstraint struct {\n\tName        string                 `json:\"name,omitempty\"`     // Package name\n\tLanguage    string                 `json:\"language,omitempty\"` // The language ecosystem for a package\n\tType        string                 `json:\"type,omitempty\"`     // Package type\n\tVersion     string                 `json:\"version,omitempty\"`  // Package version\n\tLocation    string                 `json:\"location,omitempty\"` // Package location\n\tExtraFields map[string]interface{} `json:\"-\"`\n}\n\nfunc (p PackageExclusionConstraint) Usable() bool {\n\treturn len(p.ExtraFields) == 0\n}\n\nfunc (p *PackageExclusionConstraint) UnmarshalJSON(data []byte) error {\n\t// Create a new type from the target type to avoid recursion.\n\ttype _packageExclusionConstraint PackageExclusionConstraint\n\n\t// Unmarshal into an instance of the new type.\n\tvar _p _packageExclusionConstraint\n\tif err := json.Unmarshal(data, &_p); err != nil {\n\t\treturn err\n\t}\n\n\tif err := json.Unmarshal(data, &_p.ExtraFields); err != nil {\n\t\treturn err\n\t}\n\n\tdelete(_p.ExtraFields, \"name\")\n\tdelete(_p.ExtraFields, \"language\")\n\tdelete(_p.ExtraFields, \"type\")\n\tdelete(_p.ExtraFields, \"version\")\n\tdelete(_p.ExtraFields, \"location\")\n\n\tif len(_p.ExtraFields) == 0 {\n\t\t_p.ExtraFields = nil\n\t}\n\n\t// Cast the new type instance to the original type and assign.\n\t*p = PackageExclusionConstraint(_p)\n\treturn nil\n}\n"
  },
  {
    "path": "grype/db/v5/vulnerability_match_exclusion_store.go",
    "content": "package v5\n\ntype VulnerabilityMatchExclusionStore interface {\n\tVulnerabilityMatchExclusionStoreReader\n\tVulnerabilityMatchExclusionStoreWriter\n}\n\ntype VulnerabilityMatchExclusionStoreReader interface {\n\tGetVulnerabilityMatchExclusion(id string) ([]VulnerabilityMatchExclusion, error)\n}\n\ntype VulnerabilityMatchExclusionStoreWriter interface {\n\tAddVulnerabilityMatchExclusion(exclusion ...VulnerabilityMatchExclusion) error\n}\n"
  },
  {
    "path": "grype/db/v5/vulnerability_metadata.go",
    "content": "package v5\n\nimport (\n\t\"reflect\"\n\n\t\"github.com/anchore/grype/grype/vulnerability\"\n)\n\n// VulnerabilityMetadata represents all vulnerability data that is not necessary to perform package-to-vulnerability matching.\ntype VulnerabilityMetadata struct {\n\tID           string   `json:\"id\"`            // The identifier of the vulnerability or advisory\n\tNamespace    string   `json:\"namespace\"`     // Where this entry is valid within\n\tDataSource   string   `json:\"data_source\"`   // A URL where the data was sourced from\n\tRecordSource string   `json:\"record_source\"` // The source of the vulnerability information (relative to the immediate upstream in the enterprise feedgroup)\n\tSeverity     string   `json:\"severity\"`      // How severe the vulnerability is (valid values are defined by upstream sources currently)\n\tURLs         []string `json:\"urls\"`          // URLs to get more information about the vulnerability or advisory\n\tDescription  string   `json:\"description\"`   // Description of the vulnerability\n\tCvss         []Cvss   `json:\"cvss\"`          // Common Vulnerability Scoring System values\n}\n\n// Cvss contains select Common Vulnerability Scoring System fields for a vulnerability.\ntype Cvss struct {\n\t// VendorMetadata captures non-standard CVSS fields that vendors can sometimes\n\t// include when providing CVSS information.  This vendor-specific metadata type\n\t// allows to capture that data for persisting into the database\n\tVendorMetadata interface{} `json:\"vendor_metadata\"`\n\tMetrics        CvssMetrics `json:\"metrics\"`\n\tVector         string      `json:\"vector\"`  // A textual representation of the metric values used to determine the score\n\tVersion        string      `json:\"version\"` // The version of the CVSS spec, for example 2.0, 3.0, or 3.1\n\tSource         string      `json:\"source\"`  // Identifies the organization that provided the score\n\tType           string      `json:\"type\"`    // Whether the source is a `primary` or `secondary` source\n}\n\n// CvssMetrics are the quantitative values that make up a CVSS score.\ntype CvssMetrics struct {\n\t// BaseScore ranges from 0 - 10 and defines qualities intrinsic to the severity of a vulnerability.\n\tBaseScore float64 `json:\"base_score\"`\n\t// ExploitabilityScore is a pointer to avoid having a 0 value by default.\n\t// It is an indicator of how easy it may be for an attacker to exploit\n\t// a vulnerability\n\tExploitabilityScore *float64 `json:\"exploitability_score\"`\n\t// ImpactScore represents the effects of an exploited vulnerability\n\t// relative to compromise in confidentiality, integrity, and availability.\n\t// It is an optional parameter, so that is why it is a pointer instead of\n\t// a regular field\n\tImpactScore *float64 `json:\"impact_score\"`\n}\n\nfunc NewCvssMetrics(baseScore, exploitabilityScore, impactScore float64) CvssMetrics {\n\treturn CvssMetrics{\n\t\tBaseScore:           baseScore,\n\t\tExploitabilityScore: &exploitabilityScore,\n\t\tImpactScore:         &impactScore,\n\t}\n}\n\nfunc (v *VulnerabilityMetadata) Equal(vv VulnerabilityMetadata) bool {\n\tequal := v.ID == vv.ID &&\n\t\tv.Namespace == vv.Namespace &&\n\t\tv.DataSource == vv.DataSource &&\n\t\tv.RecordSource == vv.RecordSource &&\n\t\tv.Severity == vv.Severity &&\n\t\tv.Description == vv.Description &&\n\t\tlen(v.URLs) == len(vv.URLs) &&\n\t\tlen(v.Cvss) == len(vv.Cvss)\n\n\tif !equal {\n\t\treturn false\n\t}\n\tfor idx, cpe := range v.URLs {\n\t\tif cpe != vv.URLs[idx] {\n\t\t\treturn false\n\t\t}\n\t}\n\tfor idx, item := range v.Cvss {\n\t\tif !reflect.DeepEqual(item, vv.Cvss[idx]) {\n\t\t\treturn false\n\t\t}\n\t}\n\n\treturn true\n}\n\nfunc NewMetadata(m *VulnerabilityMetadata) (*vulnerability.Metadata, error) {\n\tif m == nil {\n\t\treturn nil, nil\n\t}\n\treturn &vulnerability.Metadata{\n\t\tID:          m.ID,\n\t\tDataSource:  m.DataSource,\n\t\tNamespace:   m.Namespace,\n\t\tSeverity:    m.Severity,\n\t\tURLs:        m.URLs,\n\t\tDescription: m.Description,\n\t\tCvss:        NewCvss(m.Cvss),\n\t}, nil\n}\n"
  },
  {
    "path": "grype/db/v5/vulnerability_metadata_store.go",
    "content": "package v5\n\ntype VulnerabilityMetadataStore interface {\n\tVulnerabilityMetadataStoreReader\n\tVulnerabilityMetadataStoreWriter\n}\n\ntype VulnerabilityMetadataStoreReader interface {\n\tGetVulnerabilityMetadata(id, namespace string) (*VulnerabilityMetadata, error)\n\tGetAllVulnerabilityMetadata() (*[]VulnerabilityMetadata, error)\n}\n\ntype VulnerabilityMetadataStoreWriter interface {\n\tAddVulnerabilityMetadata(metadata ...VulnerabilityMetadata) error\n}\n"
  },
  {
    "path": "grype/db/v5/vulnerability_store.go",
    "content": "package v5\n\nconst VulnerabilityStoreFileName = \"vulnerability.db\"\n\ntype VulnerabilityStore interface {\n\tVulnerabilityStoreReader\n\tVulnerabilityStoreWriter\n}\n\ntype VulnerabilityStoreReader interface {\n\t// GetVulnerabilityNamespaces retrieves unique list of vulnerability namespaces\n\tGetVulnerabilityNamespaces() ([]string, error)\n\t// GetVulnerability retrieves vulnerabilities by namespace and id\n\tGetVulnerability(namespace, id string) ([]Vulnerability, error)\n\t// SearchForVulnerabilities retrieves vulnerabilities by namespace and package\n\tSearchForVulnerabilities(namespace, packageName string) ([]Vulnerability, error)\n\tGetAllVulnerabilities() (*[]Vulnerability, error)\n}\n\ntype VulnerabilityStoreWriter interface {\n\t// AddVulnerability inserts a new record of a vulnerability into the store\n\tAddVulnerability(vulnerabilities ...Vulnerability) error\n}\n"
  },
  {
    "path": "grype/db/v6/affected_cpe_store.go",
    "content": "package v6\n\nimport (\n\t\"gorm.io/gorm\"\n\n\t\"github.com/anchore/syft/syft/cpe\"\n)\n\ntype AffectedCPEStoreWriter interface {\n\tAddAffectedCPEs(packages ...*AffectedCPEHandle) error\n}\n\ntype AffectedCPEStoreReader interface {\n\tGetAffectedCPEs(cpe *cpe.Attributes, config *GetCPEOptions) ([]AffectedCPEHandle, error)\n}\n\ntype affectedCPEStore struct {\n\tdb        *gorm.DB\n\tblobStore *blobStore\n\tcpeStore  *cpeStore\n}\n\nfunc newAffectedCPEStore(db *gorm.DB, bs *blobStore) *affectedCPEStore {\n\treturn &affectedCPEStore{\n\t\tdb:        db,\n\t\tblobStore: bs,\n\t\tcpeStore:  newCPEStore(db, bs),\n\t}\n}\n\nfunc (s *affectedCPEStore) AddAffectedCPEs(packages ...*AffectedCPEHandle) error {\n\treturn addCPEHandles(s.cpeStore, packages...)\n}\n\nfunc (s *affectedCPEStore) GetAffectedCPEs(cpe *cpe.Attributes, config *GetCPEOptions) ([]AffectedCPEHandle, error) {\n\tresults, err := getCPEHandles[*AffectedCPEHandle](\n\t\ts.cpeStore,\n\t\tcpe,\n\t\tconfig,\n\t\t\"affected_cpe_handles\",\n\t)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tmodels := make([]AffectedCPEHandle, len(results))\n\tfor i, r := range results {\n\t\tmodels[i] = *r\n\t}\n\treturn models, nil\n}\n"
  },
  {
    "path": "grype/db/v6/affected_cpe_store_test.go",
    "content": "package v6\n\nimport (\n\t\"testing\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/syft/syft/cpe\"\n)\n\nfunc TestAffectedCPEStore_AddAffectedCPEs(t *testing.T) {\n\tdb := setupTestStore(t).db\n\tbw := newBlobStore(db)\n\ts := newAffectedCPEStore(db, bw)\n\n\tcpe1 := &AffectedCPEHandle{\n\t\tVulnerability: &VulnerabilityHandle{ // vuln id = 1\n\t\t\tProvider: &Provider{\n\t\t\t\tID: \"nvd\",\n\t\t\t},\n\t\t\tName: \"CVE-2023-5678\",\n\t\t},\n\t\tCPE: &Cpe{\n\t\t\tPart:    \"a\",\n\t\t\tVendor:  \"vendor-1\",\n\t\t\tProduct: \"product-1\",\n\t\t\tEdition: \"edition-1\",\n\t\t},\n\t\tBlobValue: &PackageBlob{\n\t\t\tCVEs: []string{\"CVE-2023-5678\"},\n\t\t},\n\t}\n\n\tcpe2 := testAffectedCPEHandle() // vuln id = 2\n\n\terr := s.AddAffectedCPEs(cpe1, cpe2)\n\trequire.NoError(t, err)\n\n\tvar result1 AffectedCPEHandle\n\terr = db.Where(\"cpe_id = ?\", 1).First(&result1).Error\n\trequire.NoError(t, err)\n\tassert.Equal(t, cpe1.VulnerabilityID, result1.VulnerabilityID)\n\tassert.Equal(t, cpe1.ID, result1.ID)\n\tassert.Equal(t, cpe1.BlobID, result1.BlobID)\n\tassert.Nil(t, result1.BlobValue) // since we're not preloading any fields on the fetch\n\n\tvar result2 AffectedCPEHandle\n\terr = db.Where(\"cpe_id = ?\", 2).First(&result2).Error\n\trequire.NoError(t, err)\n\tassert.Equal(t, cpe2.VulnerabilityID, result2.VulnerabilityID)\n\tassert.Equal(t, cpe2.ID, result2.ID)\n\tassert.Equal(t, cpe2.BlobID, result2.BlobID)\n\tassert.Nil(t, result2.BlobValue) // since we're not preloading any fields on the fetch\n}\n\nfunc TestAffectedCPEStore_GetCPEs(t *testing.T) {\n\tdb := setupTestStore(t).db\n\tbw := newBlobStore(db)\n\ts := newAffectedCPEStore(db, bw)\n\n\tc := testAffectedCPEHandle()\n\terr := s.AddAffectedCPEs(c)\n\trequire.NoError(t, err)\n\n\tresults, err := s.GetAffectedCPEs(cpeFromProduct(c.CPE.Product), nil)\n\trequire.NoError(t, err)\n\n\texpected := []AffectedCPEHandle{*c}\n\trequire.Len(t, results, len(expected))\n\tresult := results[0]\n\tassert.Equal(t, c.CpeID, result.CpeID)\n\tassert.Equal(t, c.ID, result.ID)\n\tassert.Equal(t, c.BlobID, result.BlobID)\n\trequire.Nil(t, result.BlobValue) // since we're not preloading any fields on the fetch\n\n\t// fetch again with blob & cpe preloaded\n\tresults, err = s.GetAffectedCPEs(cpeFromProduct(c.CPE.Product), &GetCPEOptions{PreloadCPE: true, PreloadBlob: true, PreloadVulnerability: true})\n\trequire.NoError(t, err)\n\trequire.Len(t, results, len(expected))\n\tresult = results[0]\n\tassert.NotNil(t, result.BlobValue)\n\tif d := cmp.Diff(*c, result); d != \"\" {\n\t\tt.Errorf(\"unexpected result (-want +got):\\n%s\", d)\n\t}\n}\n\nfunc TestAffectedCPEStore_GetExact(t *testing.T) {\n\tdb := setupTestStore(t).db\n\tbw := newBlobStore(db)\n\ts := newAffectedCPEStore(db, bw)\n\n\tc := testAffectedCPEHandle()\n\terr := s.AddAffectedCPEs(c)\n\trequire.NoError(t, err)\n\n\t// we want to search by all fields to ensure that all are accounted for in the query (since there are string fields referenced in the where clauses)\n\tresults, err := s.GetAffectedCPEs(toCPE(c.CPE), nil)\n\trequire.NoError(t, err)\n\n\texpected := []AffectedCPEHandle{*c}\n\trequire.Len(t, results, len(expected))\n\tresult := results[0]\n\tassert.Equal(t, c.CpeID, result.CpeID)\n\n}\n\nfunc TestAffectedCPEStore_Get_CaseInsensitive(t *testing.T) {\n\tdb := setupTestStore(t).db\n\tbw := newBlobStore(db)\n\ts := newAffectedCPEStore(db, bw)\n\n\tc := testAffectedCPEHandle()\n\terr := s.AddAffectedCPEs(c)\n\trequire.NoError(t, err)\n\n\t// we want to search by all fields to ensure that all are accounted for in the query (since there are string fields referenced in the where clauses)\n\tresults, err := s.GetAffectedCPEs(toCPE(&Cpe{\n\t\tPart:            \"Application\",      // capitalized\n\t\tVendor:          \"Vendor\",           // capitalized\n\t\tProduct:         \"Product\",          // capitalized\n\t\tEdition:         \"Edition\",          // capitalized\n\t\tLanguage:        \"Language\",         // capitalized\n\t\tSoftwareEdition: \"Software_edition\", // capitalized\n\t\tTargetHardware:  \"Target_hardware\",  // capitalized\n\t\tTargetSoftware:  \"Target_software\",  // capitalized\n\t\tOther:           \"Other\",            // capitalized\n\t}), nil)\n\trequire.NoError(t, err)\n\n\texpected := []AffectedCPEHandle{*c}\n\trequire.Len(t, results, len(expected))\n\tresult := results[0]\n\tassert.Equal(t, c.CpeID, result.CpeID)\n}\n\nfunc TestAffectedCPEStore_PreventDuplicateCPEs(t *testing.T) {\n\tdb := setupTestStore(t).db\n\tbw := newBlobStore(db)\n\ts := newAffectedCPEStore(db, bw)\n\n\tcpe1 := &AffectedCPEHandle{\n\t\tVulnerability: &VulnerabilityHandle{ // vuln id = 1\n\t\t\tName: \"CVE-2023-5678\",\n\t\t\tProvider: &Provider{\n\t\t\t\tID: \"nvd\",\n\t\t\t},\n\t\t},\n\t\tCPE: &Cpe{ // ID = 1\n\t\t\tPart:    \"a\",\n\t\t\tVendor:  \"vendor-1\",\n\t\t\tProduct: \"product-1\",\n\t\t\tEdition: \"edition-1\",\n\t\t},\n\t\tBlobValue: &PackageBlob{\n\t\t\tCVEs: []string{\"CVE-2023-5678\"},\n\t\t},\n\t}\n\n\terr := s.AddAffectedCPEs(cpe1)\n\trequire.NoError(t, err)\n\n\t// attempt to add a duplicate CPE with the same values\n\tduplicateCPE := &AffectedCPEHandle{\n\t\tVulnerability: &VulnerabilityHandle{ // vuln id = 2, different VulnerabilityID for testing...\n\t\t\tName: \"CVE-2024-1234\",\n\t\t\tProvider: &Provider{\n\t\t\t\tID: \"nvd\",\n\t\t\t},\n\t\t},\n\t\tCpeID: 2, // for testing explicitly set to 2, but this is unrealistic\n\t\tCPE: &Cpe{\n\t\t\tID:      2,           // different, again, unrealistic but useful for testing\n\t\t\tPart:    \"a\",         // same\n\t\t\tVendor:  \"vendor-1\",  // same\n\t\t\tProduct: \"product-1\", // same\n\t\t\tEdition: \"edition-1\", // same\n\t\t},\n\t\tBlobValue: &PackageBlob{\n\t\t\tCVEs: []string{\"CVE-2024-1234\"},\n\t\t},\n\t}\n\n\terr = s.AddAffectedCPEs(duplicateCPE)\n\trequire.NoError(t, err)\n\n\trequire.Equal(t, cpe1.CpeID, duplicateCPE.CpeID, \"expected the CPE DB ID to be the same\")\n\n\tvar existingCPEs []Cpe\n\terr = db.Find(&existingCPEs).Error\n\trequire.NoError(t, err)\n\trequire.Len(t, existingCPEs, 1, \"expected only one CPE to exist\")\n\n\tactualHandles, err := s.GetAffectedCPEs(cpeFromProduct(cpe1.CPE.Product), &GetCPEOptions{\n\t\tPreloadCPE:           true,\n\t\tPreloadBlob:          true,\n\t\tPreloadVulnerability: true,\n\t})\n\trequire.NoError(t, err)\n\n\t// the CPEs should be the same, and the store should reconcile the IDs\n\tduplicateCPE.CpeID = cpe1.CpeID\n\tduplicateCPE.CPE.ID = cpe1.CPE.ID\n\n\texpected := []AffectedCPEHandle{*cpe1, *duplicateCPE}\n\trequire.Len(t, actualHandles, len(expected), \"expected both handles to be stored\")\n\tif d := cmp.Diff(expected, actualHandles); d != \"\" {\n\t\tt.Errorf(\"unexpected result (-want +got):\\n%s\", d)\n\t}\n}\n\nfunc cpeFromProduct(product string) *cpe.Attributes {\n\treturn &cpe.Attributes{\n\t\tProduct: product,\n\t}\n}\n\nfunc toCPE(c *Cpe) *cpe.Attributes {\n\treturn &cpe.Attributes{\n\t\tPart:      c.Part,\n\t\tVendor:    c.Vendor,\n\t\tProduct:   c.Product,\n\t\tEdition:   c.Edition,\n\t\tLanguage:  c.Language,\n\t\tSWEdition: c.SoftwareEdition,\n\t\tTargetSW:  c.TargetSoftware,\n\t\tTargetHW:  c.TargetHardware,\n\t\tOther:     c.Other,\n\t}\n}\n\nfunc testAffectedCPEHandle() *AffectedCPEHandle {\n\treturn &AffectedCPEHandle{\n\t\tVulnerability: &VulnerabilityHandle{\n\t\t\tName: \"CVE-2024-4321\",\n\t\t\tProvider: &Provider{\n\t\t\t\tID: \"nvd\",\n\t\t\t},\n\t\t},\n\t\tCPE: &Cpe{\n\t\t\tPart:            \"application\",\n\t\t\tVendor:          \"vendor\",\n\t\t\tProduct:         \"product\",\n\t\t\tEdition:         \"edition\",\n\t\t\tLanguage:        \"language\",\n\t\t\tSoftwareEdition: \"software_edition\",\n\t\t\tTargetHardware:  \"target_hardware\",\n\t\t\tTargetSoftware:  \"target_software\",\n\t\t\tOther:           \"other\",\n\t\t},\n\t\tBlobValue: &PackageBlob{\n\t\t\tCVEs: []string{\"CVE-2024-4321\"},\n\t\t},\n\t}\n}\n"
  },
  {
    "path": "grype/db/v6/affected_package_store.go",
    "content": "package v6\n\nimport \"gorm.io/gorm\"\n\ntype AffectedPackageStoreWriter interface {\n\tAddAffectedPackages(packages ...*AffectedPackageHandle) error\n}\n\ntype AffectedPackageStoreReader interface {\n\tGetAffectedPackages(pkg *PackageSpecifier, config *GetPackageOptions) ([]AffectedPackageHandle, error)\n}\n\ntype affectedPackageStore struct {\n\tdb       *gorm.DB\n\tosStore  *operatingSystemStore\n\tpkgStore *packageStore\n}\n\nfunc newAffectedPackageStore(db *gorm.DB, bs *blobStore, oss *operatingSystemStore) *affectedPackageStore {\n\treturn &affectedPackageStore{\n\t\tdb:       db,\n\t\tosStore:  oss,\n\t\tpkgStore: newPackageStore(db, bs, oss),\n\t}\n}\n\nfunc (s *affectedPackageStore) AddAffectedPackages(packages ...*AffectedPackageHandle) error {\n\treturn addPackagesWithOS(s.pkgStore, packages...)\n}\n\nfunc (s *affectedPackageStore) GetAffectedPackages(pkg *PackageSpecifier, config *GetPackageOptions) ([]AffectedPackageHandle, error) {\n\tresults, err := getPackages[*AffectedPackageHandle](\n\t\ts.pkgStore,\n\t\tpkg,\n\t\tconfig,\n\t\t\"affected_package_handles\",\n\t)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tmodels := make([]AffectedPackageHandle, len(results))\n\tfor i, r := range results {\n\t\tmodels[i] = *r\n\t}\n\treturn models, nil\n}\n"
  },
  {
    "path": "grype/db/v6/affected_package_store_test.go",
    "content": "package v6\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/google/go-cmp/cmp/cmpopts\"\n\t\"github.com/scylladb/go-set/strset\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/syft/syft/cpe\"\n)\n\ntype affectedPackageHandlePreloadConfig struct {\n\tname                 string\n\tPreloadOS            bool\n\tPreloadPackage       bool\n\tPreloadBlob          bool\n\tPreloadVulnerability bool\n\tprepExpectations     func(*testing.T, []AffectedPackageHandle) []AffectedPackageHandle\n}\n\nfunc defaultAffectedPackageHandlePreloadCases() []affectedPackageHandlePreloadConfig {\n\treturn []affectedPackageHandlePreloadConfig{\n\t\t{\n\t\t\tname:                 \"preload-all\",\n\t\t\tPreloadOS:            true,\n\t\t\tPreloadPackage:       true,\n\t\t\tPreloadBlob:          true,\n\t\t\tPreloadVulnerability: true,\n\t\t\tprepExpectations: func(t *testing.T, in []AffectedPackageHandle) []AffectedPackageHandle {\n\t\t\t\tfor _, a := range in {\n\t\t\t\t\tif a.OperatingSystemID != nil {\n\t\t\t\t\t\trequire.NotNil(t, a.OperatingSystem)\n\t\t\t\t\t}\n\t\t\t\t\trequire.NotNil(t, a.Package)\n\t\t\t\t\trequire.NotNil(t, a.BlobValue)\n\t\t\t\t\trequire.NotNil(t, a.Vulnerability)\n\t\t\t\t}\n\t\t\t\treturn in\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"preload-none\",\n\t\t\tprepExpectations: func(t *testing.T, in []AffectedPackageHandle) []AffectedPackageHandle {\n\t\t\t\tvar out []AffectedPackageHandle\n\t\t\t\tfor _, a := range in {\n\t\t\t\t\tif a.OperatingSystem == nil && a.BlobValue == nil && a.Package == nil && a.Vulnerability == nil {\n\t\t\t\t\t\tt.Skip(\"preload already matches expectation\")\n\t\t\t\t\t}\n\t\t\t\t\ta.OperatingSystem = nil\n\t\t\t\t\ta.Package = nil\n\t\t\t\t\ta.BlobValue = nil\n\t\t\t\t\ta.Vulnerability = nil\n\t\t\t\t\tout = append(out, a)\n\t\t\t\t}\n\t\t\t\treturn out\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:      \"preload-os-only\",\n\t\t\tPreloadOS: true,\n\t\t\tprepExpectations: func(t *testing.T, in []AffectedPackageHandle) []AffectedPackageHandle {\n\t\t\t\tvar out []AffectedPackageHandle\n\t\t\t\tfor _, a := range in {\n\t\t\t\t\tif a.OperatingSystemID != nil {\n\t\t\t\t\t\trequire.NotNil(t, a.OperatingSystem)\n\t\t\t\t\t}\n\t\t\t\t\tif a.Package == nil && a.BlobValue == nil && a.Vulnerability == nil {\n\t\t\t\t\t\tt.Skip(\"preload already matches expectation\")\n\t\t\t\t\t}\n\t\t\t\t\ta.Package = nil\n\t\t\t\t\ta.BlobValue = nil\n\t\t\t\t\ta.Vulnerability = nil\n\t\t\t\t\tout = append(out, a)\n\t\t\t\t}\n\t\t\t\treturn out\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:           \"preload-package-only\",\n\t\t\tPreloadPackage: true,\n\t\t\tprepExpectations: func(t *testing.T, in []AffectedPackageHandle) []AffectedPackageHandle {\n\t\t\t\tvar out []AffectedPackageHandle\n\t\t\t\tfor _, a := range in {\n\t\t\t\t\trequire.NotNil(t, a.Package)\n\t\t\t\t\tif a.OperatingSystem == nil && a.BlobValue == nil && a.Vulnerability == nil {\n\t\t\t\t\t\tt.Skip(\"preload already matches expectation\")\n\t\t\t\t\t}\n\t\t\t\t\ta.OperatingSystem = nil\n\t\t\t\t\ta.BlobValue = nil\n\t\t\t\t\ta.Vulnerability = nil\n\t\t\t\t\tout = append(out, a)\n\t\t\t\t}\n\t\t\t\treturn out\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:        \"preload-blob-only\",\n\t\t\tPreloadBlob: true,\n\t\t\tprepExpectations: func(t *testing.T, in []AffectedPackageHandle) []AffectedPackageHandle {\n\t\t\t\tvar out []AffectedPackageHandle\n\t\t\t\tfor _, a := range in {\n\t\t\t\t\tif a.OperatingSystem == nil && a.Package == nil && a.Vulnerability == nil {\n\t\t\t\t\t\tt.Skip(\"preload already matches expectation\")\n\t\t\t\t\t}\n\t\t\t\t\ta.OperatingSystem = nil\n\t\t\t\t\ta.Package = nil\n\t\t\t\t\ta.Vulnerability = nil\n\t\t\t\t\tout = append(out, a)\n\t\t\t\t}\n\t\t\t\treturn out\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:                 \"preload-vulnerability-only\",\n\t\t\tPreloadVulnerability: true,\n\t\t\tprepExpectations: func(t *testing.T, in []AffectedPackageHandle) []AffectedPackageHandle {\n\t\t\t\tvar out []AffectedPackageHandle\n\t\t\t\tfor _, a := range in {\n\t\t\t\t\tif a.OperatingSystem == nil && a.Package == nil && a.BlobValue == nil {\n\t\t\t\t\t\tt.Skip(\"preload already matches expectation\")\n\t\t\t\t\t}\n\t\t\t\t\ta.OperatingSystem = nil\n\t\t\t\t\ta.Package = nil\n\t\t\t\t\ta.BlobValue = nil\n\t\t\t\t\tout = append(out, a)\n\t\t\t\t}\n\t\t\t\treturn out\n\t\t\t},\n\t\t},\n\t}\n}\n\nfunc TestAffectedPackageStore_AddAffectedPackages(t *testing.T) {\n\tsetupAffectedPackageStore := func(t *testing.T) *affectedPackageStore {\n\t\tdb := setupTestStore(t).db\n\t\tbs := newBlobStore(db)\n\t\treturn newAffectedPackageStore(db, bs, newOperatingSystemStore(db, bs))\n\t}\n\n\tsetupTestStoreWithPackages := func(t *testing.T) (*AffectedPackageHandle, *AffectedPackageHandle, *affectedPackageStore) {\n\t\tpkg1 := &AffectedPackageHandle{\n\t\t\tVulnerability: &VulnerabilityHandle{\n\t\t\t\tName: \"CVE-2023-1234\",\n\t\t\t\tProvider: &Provider{\n\t\t\t\t\tID: \"provider1\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tPackage: &Package{Name: \"pkg1\", Ecosystem: \"type1\"},\n\t\t\tBlobValue: &PackageBlob{\n\t\t\t\tCVEs: []string{\"CVE-2023-1234\"},\n\t\t\t},\n\t\t}\n\n\t\tpkg2 := testDistro1AffectedPackage2Handle()\n\n\t\treturn pkg1, pkg2, setupAffectedPackageStore(t)\n\t}\n\n\tt.Run(\"no preloading\", func(t *testing.T) {\n\t\tpkg1, pkg2, s := setupTestStoreWithPackages(t)\n\n\t\terr := s.AddAffectedPackages(pkg1, pkg2)\n\t\trequire.NoError(t, err)\n\n\t\tvar result1 AffectedPackageHandle\n\t\terr = s.db.Where(\"package_id = ?\", pkg1.PackageID).First(&result1).Error\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, pkg1.PackageID, result1.PackageID)\n\t\tassert.Equal(t, pkg1.BlobID, result1.BlobID)\n\t\trequire.Nil(t, result1.BlobValue) // no preloading on fetch\n\n\t\tvar result2 AffectedPackageHandle\n\t\terr = s.db.Where(\"package_id = ?\", pkg2.PackageID).First(&result2).Error\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, pkg2.PackageID, result2.PackageID)\n\t\tassert.Equal(t, pkg2.BlobID, result2.BlobID)\n\t\trequire.Nil(t, result2.BlobValue)\n\t})\n\n\tt.Run(\"preloading\", func(t *testing.T) {\n\t\tpkg1, pkg2, s := setupTestStoreWithPackages(t)\n\n\t\terr := s.AddAffectedPackages(pkg1, pkg2)\n\t\trequire.NoError(t, err)\n\n\t\toptions := &GetPackageOptions{\n\t\t\tPreloadOS:      true,\n\t\t\tPreloadPackage: true,\n\t\t\tPreloadBlob:    true,\n\t\t}\n\n\t\tresults, err := s.GetAffectedPackages(pkgFromName(pkg1.Package.Name), options)\n\t\trequire.NoError(t, err)\n\t\trequire.Len(t, results, 1)\n\n\t\tresult := results[0]\n\t\trequire.NotNil(t, result.Package)\n\t\trequire.NotNil(t, result.BlobValue)\n\t\tassert.Nil(t, result.OperatingSystem) // pkg1 has no OS\n\t})\n\n\tt.Run(\"preload CPEs\", func(t *testing.T) {\n\t\tpkg1, _, s := setupTestStoreWithPackages(t)\n\n\t\tc := Cpe{\n\t\t\tPart:    \"a\",\n\t\t\tVendor:  \"vendor1\",\n\t\t\tProduct: \"product1\",\n\t\t}\n\t\tpkg1.Package.CPEs = []Cpe{c}\n\n\t\terr := s.AddAffectedPackages(pkg1)\n\t\trequire.NoError(t, err)\n\n\t\toptions := &GetPackageOptions{\n\t\t\tPreloadPackage:     true,\n\t\t\tPreloadPackageCPEs: true,\n\t\t}\n\n\t\tresults, err := s.GetAffectedPackages(pkgFromName(pkg1.Package.Name), options)\n\t\trequire.NoError(t, err)\n\t\trequire.Len(t, results, 1)\n\n\t\tresult := results[0]\n\t\trequire.NotNil(t, result.Package)\n\n\t\t// the IDs should have been set, and there is only one, so we know the correct values\n\t\tc.ID = 1\n\n\t\tif d := cmp.Diff([]Cpe{c}, result.Package.CPEs); d != \"\" {\n\t\t\tt.Errorf(\"unexpected result (-want +got):\\n%s\", d)\n\t\t}\n\t})\n\n\tt.Run(\"Package deduplication\", func(t *testing.T) {\n\t\tpkg1 := &AffectedPackageHandle{\n\t\t\tVulnerability: &VulnerabilityHandle{\n\t\t\t\tName: \"CVE-2023-1234\",\n\t\t\t\tProvider: &Provider{\n\t\t\t\t\tID: \"provider1\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tPackage: &Package{Name: \"pkg1\", Ecosystem: \"type1\"},\n\t\t\tBlobValue: &PackageBlob{\n\t\t\t\tCVEs: []string{\"CVE-2023-1234\"},\n\t\t\t},\n\t\t}\n\n\t\tpkg2 := &AffectedPackageHandle{\n\t\t\tVulnerability: &VulnerabilityHandle{\n\t\t\t\tName: \"CVE-2023-1234\",\n\t\t\t\tProvider: &Provider{\n\t\t\t\t\tID: \"provider1\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tPackage: &Package{Name: \"pkg1\", Ecosystem: \"type1\"}, // same!\n\t\t\tBlobValue: &PackageBlob{\n\t\t\t\tCVEs: []string{\"CVE-2023-56789\"},\n\t\t\t},\n\t\t}\n\n\t\ts := setupAffectedPackageStore(t)\n\t\terr := s.AddAffectedPackages(pkg1, pkg2)\n\t\trequire.NoError(t, err)\n\n\t\tvar pkgs []Package\n\t\terr = s.db.Find(&pkgs).Error\n\t\trequire.NoError(t, err)\n\n\t\texpected := []Package{\n\t\t\t*pkg1.Package,\n\t\t}\n\n\t\tif d := cmp.Diff(expected, pkgs); d != \"\" {\n\t\t\tt.Errorf(\"unexpected result (-want +got):\\n%s\", d)\n\t\t}\n\t})\n\n\tt.Run(\"same package with multiple CPEs\", func(t *testing.T) {\n\t\tcpe1 := Cpe{\n\t\t\tPart:    \"a\",\n\t\t\tVendor:  \"vendor1\",\n\t\t\tProduct: \"product1\",\n\t\t}\n\n\t\tcpe2 := Cpe{\n\t\t\tPart:    \"a\",\n\t\t\tVendor:  \"vendor2\",\n\t\t\tProduct: \"product2\",\n\t\t}\n\n\t\tpkg1 := &AffectedPackageHandle{\n\t\t\tVulnerability: &VulnerabilityHandle{\n\t\t\t\tName: \"CVE-2023-1234\",\n\t\t\t\tProvider: &Provider{\n\t\t\t\t\tID: \"provider1\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tPackage: &Package{Name: \"pkg1\", Ecosystem: \"type1\", CPEs: []Cpe{cpe1}},\n\t\t\tBlobValue: &PackageBlob{\n\t\t\t\tCVEs: []string{\"CVE-2023-1234\"},\n\t\t\t},\n\t\t}\n\n\t\tpkg2 := &AffectedPackageHandle{\n\t\t\tVulnerability: &VulnerabilityHandle{\n\t\t\t\tName: \"CVE-2023-56789\",\n\t\t\t\tProvider: &Provider{\n\t\t\t\t\tID: \"provider1\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tPackage: &Package{Name: \"pkg1\", Ecosystem: \"type1\", CPEs: []Cpe{cpe1, cpe2}}, // duplicate CPE + additional CPE\n\t\t\tBlobValue: &PackageBlob{\n\t\t\t\tCVEs: []string{\"CVE-2023-56789\"},\n\t\t\t},\n\t\t}\n\n\t\ts := setupAffectedPackageStore(t)\n\t\terr := s.AddAffectedPackages(pkg1, pkg2)\n\t\trequire.NoError(t, err)\n\n\t\tvar pkgs []Package\n\t\terr = s.db.Preload(\"CPEs\").Find(&pkgs).Error\n\t\trequire.NoError(t, err)\n\n\t\texpPkg := *pkg1.Package\n\t\texpPkg.ID = 1\n\t\tcpe1.ID = 1\n\t\tcpe2.ID = 2\n\t\texpPkg.CPEs = []Cpe{cpe1, cpe2}\n\n\t\texpected := []Package{\n\t\t\texpPkg,\n\t\t}\n\n\t\tif d := cmp.Diff(expected, pkgs); d != \"\" {\n\t\t\tt.Errorf(\"unexpected result (-want +got):\\n%s\", d)\n\t\t}\n\n\t\texpectedCPEs := []Cpe{cpe1, cpe2}\n\t\tvar cpeResults []Cpe\n\t\terr = s.db.Find(&cpeResults).Error\n\t\trequire.NoError(t, err)\n\t\tif d := cmp.Diff(expectedCPEs, cpeResults); d != \"\" {\n\t\t\tt.Errorf(\"unexpected result (-want +got):\\n%s\", d)\n\t\t}\n\n\t})\n\n\tt.Run(\"allow same CPE to belong to multiple packages\", func(t *testing.T) {\n\t\tcpe1 := Cpe{\n\t\t\tPart:    \"a\",\n\t\t\tVendor:  \"vendor1\",\n\t\t\tProduct: \"product1\",\n\t\t}\n\n\t\tcpe2 := Cpe{\n\t\t\tPart:    \"a\",\n\t\t\tVendor:  \"vendor2\",\n\t\t\tProduct: \"product2\",\n\t\t}\n\n\t\tpkg1 := &AffectedPackageHandle{\n\t\t\tVulnerability: &VulnerabilityHandle{\n\t\t\t\tName: \"CVE-2023-1234\",\n\t\t\t\tProvider: &Provider{\n\t\t\t\t\tID: \"provider1\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tPackage: &Package{Name: \"pkg1\", Ecosystem: \"type1\", CPEs: []Cpe{cpe1}},\n\t\t\tBlobValue: &PackageBlob{\n\t\t\t\tCVEs: []string{\"CVE-2023-1234\"},\n\t\t\t},\n\t\t}\n\n\t\tpkg2 := &AffectedPackageHandle{\n\t\t\tVulnerability: &VulnerabilityHandle{\n\t\t\t\tName: \"CVE-2023-56789\",\n\t\t\t\tProvider: &Provider{\n\t\t\t\t\tID: \"provider1\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tPackage: &Package{Name: \"pkg2\", Ecosystem: \"type1\", CPEs: []Cpe{cpe1, cpe2}}, // overlapping CPEs for different packages\n\t\t\tBlobValue: &PackageBlob{\n\t\t\t\tCVEs: []string{\"CVE-2023-56789\"},\n\t\t\t},\n\t\t}\n\n\t\ts := setupAffectedPackageStore(t)\n\t\terr := s.AddAffectedPackages(pkg1, pkg2)\n\t\trequire.NoError(t, err)\n\n\t\tvar pkgs []Package\n\t\terr = s.db.Preload(\"CPEs\").Find(&pkgs).Error\n\t\trequire.NoError(t, err)\n\n\t\tcpe1.ID = 1\n\t\tcpe2.ID = 2\n\n\t\texpPkg1 := *pkg1.Package\n\t\texpPkg1.ID = 1\n\t\texpPkg1.CPEs = []Cpe{cpe1}\n\n\t\texpPkg2 := *pkg2.Package\n\t\texpPkg2.ID = 2\n\t\texpPkg2.CPEs = []Cpe{cpe1, cpe2}\n\n\t\texpected := []Package{\n\t\t\texpPkg1,\n\t\t\texpPkg2,\n\t\t}\n\n\t\tif d := cmp.Diff(expected, pkgs); d != \"\" {\n\t\t\tt.Errorf(\"unexpected result (-want +got):\\n%s\", d)\n\t\t}\n\n\t\texpectedCPEs := []Cpe{cpe1, cpe2}\n\t\tvar cpeResults []Cpe\n\t\terr = s.db.Find(&cpeResults).Error\n\t\trequire.NoError(t, err)\n\t\tif d := cmp.Diff(expectedCPEs, cpeResults); d != \"\" {\n\t\t\tt.Errorf(\"unexpected result (-want +got):\\n%s\", d)\n\t\t}\n\t})\n}\n\nfunc TestAffectedPackageStore_GetAffectedPackages_ByCPE(t *testing.T) {\n\tdb := setupTestStore(t).db\n\tbs := newBlobStore(db)\n\toss := newOperatingSystemStore(db, bs)\n\ts := newAffectedPackageStore(db, bs, oss)\n\n\tcpe1 := Cpe{Part: \"a\", Vendor: \"vendor1\", Product: \"product1\"}\n\tcpe2 := Cpe{Part: \"a\", Vendor: \"vendor2\", Product: \"product2\"}\n\tcpe3 := Cpe{Part: \"a\", Vendor: \"vendor2\", Product: \"product2\", TargetSoftware: \"target1\"}\n\tpkg1 := &AffectedPackageHandle{\n\t\tVulnerability: &VulnerabilityHandle{\n\t\t\tName: \"CVE-2023-1234\",\n\t\t\tProvider: &Provider{\n\t\t\t\tID: \"provider1\",\n\t\t\t},\n\t\t},\n\t\tPackage: &Package{Name: \"pkg1\", Ecosystem: \"type1\", CPEs: []Cpe{cpe1}},\n\t\tBlobValue: &PackageBlob{\n\t\t\tCVEs: []string{\"CVE-2023-1234\"},\n\t\t},\n\t}\n\tpkg2 := &AffectedPackageHandle{\n\t\tVulnerability: &VulnerabilityHandle{\n\t\t\tName: \"CVE-2023-5678\",\n\t\t\tProvider: &Provider{\n\t\t\t\tID: \"provider1\",\n\t\t\t},\n\t\t},\n\t\tPackage: &Package{Name: \"pkg2\", Ecosystem: \"type2\", CPEs: []Cpe{cpe2}},\n\t\tBlobValue: &PackageBlob{\n\t\t\tCVEs: []string{\"CVE-2023-5678\"},\n\t\t},\n\t}\n\n\tpkg3 := &AffectedPackageHandle{\n\t\tVulnerability: &VulnerabilityHandle{\n\t\t\tName: \"CVE-2023-5678\",\n\t\t\tProvider: &Provider{\n\t\t\t\tID: \"provider1\",\n\t\t\t},\n\t\t},\n\t\tPackage: &Package{Name: \"pkg3\", Ecosystem: \"type2\", CPEs: []Cpe{cpe3}},\n\t\tBlobValue: &PackageBlob{\n\t\t\tCVEs: []string{\"CVE-2023-5678\"},\n\t\t},\n\t}\n\n\terr := s.AddAffectedPackages(pkg1, pkg2, pkg3)\n\trequire.NoError(t, err)\n\n\ttests := []struct {\n\t\tname     string\n\t\tcpe      cpe.Attributes\n\t\toptions  *GetPackageOptions\n\t\texpected []AffectedPackageHandle\n\t\twantErr  require.ErrorAssertionFunc\n\t}{\n\t\t{\n\t\t\tname: \"full match CPE\",\n\t\t\tcpe: cpe.Attributes{\n\t\t\t\tPart:    \"a\",\n\t\t\t\tVendor:  \"vendor1\",\n\t\t\t\tProduct: \"product1\",\n\t\t\t},\n\t\t\toptions: &GetPackageOptions{\n\t\t\t\tPreloadPackageCPEs:   true,\n\t\t\t\tPreloadPackage:       true,\n\t\t\t\tPreloadBlob:          true,\n\t\t\t\tPreloadVulnerability: true,\n\t\t\t},\n\t\t\texpected: []AffectedPackageHandle{*pkg1},\n\t\t},\n\t\t{\n\t\t\tname: \"partial match CPE\",\n\t\t\tcpe: cpe.Attributes{\n\t\t\t\tPart:   \"a\",\n\t\t\t\tVendor: \"vendor2\",\n\t\t\t},\n\t\t\toptions: &GetPackageOptions{\n\t\t\t\tPreloadPackageCPEs:   true,\n\t\t\t\tPreloadPackage:       true,\n\t\t\t\tPreloadBlob:          true,\n\t\t\t\tPreloadVulnerability: true,\n\t\t\t},\n\t\t\texpected: []AffectedPackageHandle{*pkg2, *pkg3},\n\t\t},\n\t\t{\n\t\t\tname: \"match on any TSW when specific one provided when broad matching enabled\",\n\t\t\tcpe: cpe.Attributes{\n\t\t\t\tPart:     \"a\",\n\t\t\t\tVendor:   \"vendor2\",\n\t\t\t\tTargetSW: \"target1\",\n\t\t\t},\n\t\t\toptions: &GetPackageOptions{\n\t\t\t\tPreloadPackageCPEs:    true,\n\t\t\t\tPreloadPackage:        true,\n\t\t\t\tPreloadBlob:           true,\n\t\t\t\tPreloadVulnerability:  true,\n\t\t\t\tAllowBroadCPEMatching: true,\n\t\t\t},\n\t\t\texpected: []AffectedPackageHandle{*pkg2, *pkg3},\n\t\t},\n\t\t{\n\t\t\tname: \"do NOT match on any TSW when specific one provided when broad matching disabled\",\n\t\t\tcpe: cpe.Attributes{\n\t\t\t\tPart:     \"a\",\n\t\t\t\tVendor:   \"vendor2\",\n\t\t\t\tTargetSW: \"target1\",\n\t\t\t},\n\t\t\toptions: &GetPackageOptions{\n\t\t\t\tPreloadPackageCPEs:    true,\n\t\t\t\tPreloadPackage:        true,\n\t\t\t\tPreloadBlob:           true,\n\t\t\t\tPreloadVulnerability:  true,\n\t\t\t\tAllowBroadCPEMatching: false,\n\t\t\t},\n\t\t\texpected: []AffectedPackageHandle{*pkg3},\n\t\t},\n\t\t{\n\t\t\tname: \"missing attributes\",\n\t\t\tcpe: cpe.Attributes{\n\t\t\t\tPart: \"a\",\n\t\t\t},\n\t\t\toptions: &GetPackageOptions{\n\t\t\t\tPreloadPackageCPEs:   true,\n\t\t\t\tPreloadPackage:       true,\n\t\t\t\tPreloadBlob:          true,\n\t\t\t\tPreloadVulnerability: true,\n\t\t\t},\n\t\t\texpected: []AffectedPackageHandle{*pkg1, *pkg2, *pkg3},\n\t\t},\n\t\t{\n\t\t\tname: \"no matches\",\n\t\t\tcpe: cpe.Attributes{\n\t\t\t\tPart:    \"a\",\n\t\t\t\tVendor:  \"unknown_vendor\",\n\t\t\t\tProduct: \"unknown_product\",\n\t\t\t},\n\t\t\toptions: &GetPackageOptions{\n\t\t\t\tPreloadPackageCPEs:   true,\n\t\t\t\tPreloadPackage:       true,\n\t\t\t\tPreloadBlob:          true,\n\t\t\t\tPreloadVulnerability: true,\n\t\t\t},\n\t\t\texpected: nil,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif tt.wantErr == nil {\n\t\t\t\ttt.wantErr = require.NoError\n\t\t\t}\n\n\t\t\tresult, err := s.GetAffectedPackages(&PackageSpecifier{CPE: &tt.cpe}, tt.options)\n\t\t\ttt.wantErr(t, err)\n\t\t\tif err != nil {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif d := cmp.Diff(tt.expected, result, cmpopts.EquateEmpty()); d != \"\" {\n\t\t\t\tt.Errorf(\"unexpected result: %s\", d)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestAffectedPackageStore_GetAffectedPackages_CaseInsensitive(t *testing.T) {\n\tdb := setupTestStore(t).db\n\tbs := newBlobStore(db)\n\toss := newOperatingSystemStore(db, bs)\n\ts := newAffectedPackageStore(db, bs, oss)\n\n\tcpe1 := Cpe{Part: \"a\", Vendor: \"Vendor1\", Product: \"Product1\"} // capitalized\n\tpkg1 := &AffectedPackageHandle{\n\t\tVulnerability: &VulnerabilityHandle{\n\t\t\tName: \"CVE-2023-1234\",\n\t\t\tProvider: &Provider{\n\t\t\t\tID: \"provider1\",\n\t\t\t},\n\t\t},\n\t\tOperatingSystem: &OperatingSystem{\n\t\t\tName:         \"Ubuntu\", // capitalized\n\t\t\tReleaseID:    \"zubuntu\",\n\t\t\tMajorVersion: \"20\",\n\t\t\tMinorVersion: \"04\", // leading 0\n\t\t\tCodename:     \"focal\",\n\t\t},\n\t\tPackage: &Package{Name: \"Pkg1\", Ecosystem: \"Type1\", CPEs: []Cpe{cpe1}}, // capitalized\n\t\tBlobValue: &PackageBlob{\n\t\t\tCVEs: []string{\"CVE-2023-1234\"},\n\t\t},\n\t}\n\n\tpkg2 := &AffectedPackageHandle{ // this should never register as a match\n\t\tVulnerability: &VulnerabilityHandle{\n\t\t\tName: \"CVE-2222-2222\",\n\t\t\tProvider: &Provider{\n\t\t\t\tID: \"provider2\",\n\t\t\t},\n\t\t},\n\t\tOperatingSystem: &OperatingSystem{\n\t\t\tName:         \"ubuntu\",\n\t\t\tReleaseID:    \"ubuntu\",\n\t\t\tMajorVersion: \"20\",\n\t\t\tMinorVersion: \"10\",\n\t\t},\n\t\tPackage: &Package{Name: \"pkg2\", Ecosystem: \"type2\"},\n\t\tBlobValue: &PackageBlob{\n\t\t\tCVEs: []string{\"CVE-2222-2222\"},\n\t\t},\n\t}\n\n\terr := s.AddAffectedPackages(pkg1, pkg2)\n\trequire.NoError(t, err)\n\n\ttests := []struct {\n\t\tname     string\n\t\tpkgSpec  *PackageSpecifier\n\t\toptions  *GetPackageOptions\n\t\texpected int\n\t}{\n\t\t{\n\t\t\tname:     \"sanity check: search miss\",\n\t\t\tpkgSpec:  pkgFromName(\"does not exist\"),\n\t\t\texpected: 0,\n\t\t},\n\t\t{\n\t\t\tname:     \"get by name\",\n\t\t\tpkgSpec:  pkgFromName(\"pKG1\"),\n\t\t\texpected: 1,\n\t\t},\n\t\t{\n\t\t\tname: \"get by CPE\",\n\t\t\tpkgSpec: &PackageSpecifier{\n\t\t\t\tCPE: &cpe.Attributes{Part: \"a\", Vendor: \"veNDor1\", Product: \"pRODuct1\"},\n\t\t\t},\n\t\t\texpected: 1,\n\t\t},\n\t\t{\n\t\t\tname: \"get by ecosystem\",\n\t\t\tpkgSpec: &PackageSpecifier{\n\t\t\t\tEcosystem: \"tYPE1\",\n\t\t\t},\n\t\t\texpected: 1,\n\t\t},\n\t\t{\n\t\t\tname: \"get by OS name and version (leading 0)\",\n\t\t\toptions: &GetPackageOptions{\n\t\t\t\tOSs: []*OSSpecifier{{\n\t\t\t\t\tName:         \"uBUNtu\",\n\t\t\t\t\tMajorVersion: \"20\",\n\t\t\t\t\tMinorVersion: \"04\",\n\t\t\t\t}},\n\t\t\t},\n\t\t\texpected: 1,\n\t\t},\n\t\t{\n\t\t\tname: \"get by OS name and version\",\n\t\t\toptions: &GetPackageOptions{\n\t\t\t\tOSs: []*OSSpecifier{{\n\t\t\t\t\tName:         \"uBUNtu\",\n\t\t\t\t\tMajorVersion: \"20\",\n\t\t\t\t\tMinorVersion: \"4\",\n\t\t\t\t}},\n\t\t\t},\n\t\t\texpected: 1,\n\t\t},\n\t\t{\n\t\t\tname: \"get by OS release\",\n\t\t\toptions: &GetPackageOptions{\n\t\t\t\tOSs: []*OSSpecifier{{\n\t\t\t\t\tName: \"zUBuntu\",\n\t\t\t\t}},\n\t\t\t},\n\t\t\texpected: 1,\n\t\t},\n\t\t{\n\t\t\tname: \"get by OS codename\",\n\t\t\toptions: &GetPackageOptions{\n\t\t\t\tOSs: []*OSSpecifier{{\n\t\t\t\t\tLabelVersion: \"fOCAL\",\n\t\t\t\t}},\n\t\t\t},\n\t\t\texpected: 1,\n\t\t},\n\t\t{\n\t\t\tname: \"get by vuln ID\",\n\t\t\toptions: &GetPackageOptions{\n\t\t\t\tVulnerabilities: []VulnerabilitySpecifier{{Name: \"cVe-2023-1234\"}},\n\t\t\t},\n\t\t\texpected: 1,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult, err := s.GetAffectedPackages(tt.pkgSpec, tt.options)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Len(t, result, tt.expected)\n\t\t\tif tt.expected > 0 {\n\t\t\t\tassert.Equal(t, pkg1.PackageID, result[0].PackageID)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestAffectedPackageStore_GetAffectedPackages_MultipleVulnerabilitySpecs(t *testing.T) {\n\tdb := setupTestStore(t).db\n\tbs := newBlobStore(db)\n\toss := newOperatingSystemStore(db, bs)\n\ts := newAffectedPackageStore(db, bs, oss)\n\n\tcpe1 := Cpe{Part: \"a\", Vendor: \"vendor1\", Product: \"product1\"}\n\tcpe2 := Cpe{Part: \"a\", Vendor: \"vendor2\", Product: \"product2\"}\n\tpkg1 := &AffectedPackageHandle{\n\t\tVulnerability: &VulnerabilityHandle{\n\t\t\tName: \"CVE-2023-1234\",\n\t\t\tProvider: &Provider{\n\t\t\t\tID: \"provider1\",\n\t\t\t},\n\t\t},\n\t\tPackage: &Package{Name: \"pkg1\", Ecosystem: \"type1\", CPEs: []Cpe{cpe1}},\n\t\tBlobValue: &PackageBlob{\n\t\t\tCVEs: []string{\"CVE-2023-1234\"},\n\t\t},\n\t}\n\tpkg2 := &AffectedPackageHandle{\n\t\tVulnerability: &VulnerabilityHandle{\n\t\t\tName: \"CVE-2023-5678\",\n\t\t\tProvider: &Provider{\n\t\t\t\tID: \"provider1\",\n\t\t\t},\n\t\t},\n\t\tPackage: &Package{Name: \"pkg2\", Ecosystem: \"type2\", CPEs: []Cpe{cpe2}},\n\t\tBlobValue: &PackageBlob{\n\t\t\tCVEs: []string{\"CVE-2023-5678\"},\n\t\t},\n\t}\n\n\terr := s.AddAffectedPackages(pkg1, pkg2)\n\trequire.NoError(t, err)\n\n\tresult, err := s.GetAffectedPackages(nil, &GetPackageOptions{\n\t\tPreloadVulnerability: true,\n\t\tVulnerabilities: []VulnerabilitySpecifier{\n\t\t\t{Name: \"CVE-2023-1234\"},\n\t\t\t{Name: \"CVE-2023-5678\"},\n\t\t},\n\t})\n\trequire.NoError(t, err)\n\n\tactualVulns := strset.New()\n\tfor _, r := range result {\n\t\tactualVulns.Add(r.Vulnerability.Name)\n\t}\n\n\texpectedVulns := strset.New(\"CVE-2023-1234\", \"CVE-2023-5678\")\n\n\tassert.ElementsMatch(t, expectedVulns.List(), actualVulns.List())\n\n}\n\nfunc TestAffectedPackageStore_GetAffectedPackages(t *testing.T) {\n\tdb := setupTestStore(t).db\n\tbs := newBlobStore(db)\n\toss := newOperatingSystemStore(db, bs)\n\ts := newAffectedPackageStore(db, bs, oss)\n\n\tpkg2d1 := testDistro1AffectedPackage2Handle()\n\tpkg2 := testNonDistroAffectedPackage2Handle()\n\tpkg2d2 := testDistro2AffectedPackage2Handle()\n\terr := s.AddAffectedPackages(pkg2d1, pkg2, pkg2d2)\n\trequire.NoError(t, err)\n\n\ttests := []struct {\n\t\tname     string\n\t\tpkg      *PackageSpecifier\n\t\toptions  *GetPackageOptions\n\t\texpected []AffectedPackageHandle\n\t\twantErr  require.ErrorAssertionFunc\n\t}{\n\t\t{\n\t\t\tname: \"specific distro\",\n\t\t\tpkg:  pkgFromName(pkg2d1.Package.Name),\n\t\t\toptions: &GetPackageOptions{\n\t\t\t\tOSs: []*OSSpecifier{{\n\t\t\t\t\tName:         \"ubuntu\",\n\t\t\t\t\tMajorVersion: \"20\",\n\t\t\t\t\tMinorVersion: \"04\",\n\t\t\t\t}},\n\t\t\t},\n\t\t\texpected: []AffectedPackageHandle{*pkg2d1},\n\t\t},\n\t\t{\n\t\t\tname: \"distro major version only\",\n\t\t\tpkg:  pkgFromName(pkg2d1.Package.Name),\n\t\t\toptions: &GetPackageOptions{\n\t\t\t\tOSs: []*OSSpecifier{{\n\t\t\t\t\tName:         \"ubuntu\",\n\t\t\t\t\tMajorVersion: \"20\",\n\t\t\t\t}},\n\t\t\t},\n\t\t\texpected: []AffectedPackageHandle{*pkg2d1, *pkg2d2},\n\t\t},\n\t\t{\n\t\t\tname: \"distro codename\",\n\t\t\tpkg:  pkgFromName(pkg2d1.Package.Name),\n\t\t\toptions: &GetPackageOptions{\n\t\t\t\tOSs: []*OSSpecifier{{\n\t\t\t\t\tName:         \"ubuntu\",\n\t\t\t\t\tLabelVersion: \"groovy\",\n\t\t\t\t}},\n\t\t\t},\n\t\t\texpected: []AffectedPackageHandle{*pkg2d2},\n\t\t},\n\t\t{\n\t\t\tname: \"no distro\",\n\t\t\tpkg:  pkgFromName(pkg2.Package.Name),\n\t\t\toptions: &GetPackageOptions{\n\t\t\t\tOSs: []*OSSpecifier{NoOSSpecified},\n\t\t\t},\n\t\t\texpected: []AffectedPackageHandle{*pkg2},\n\t\t},\n\t\t{\n\t\t\tname: \"any distro\",\n\t\t\tpkg:  pkgFromName(pkg2d1.Package.Name),\n\t\t\toptions: &GetPackageOptions{\n\t\t\t\tOSs: []*OSSpecifier{AnyOSSpecified},\n\t\t\t},\n\t\t\texpected: []AffectedPackageHandle{*pkg2d1, *pkg2, *pkg2d2},\n\t\t},\n\t\t{\n\t\t\tname:     \"package type\",\n\t\t\tpkg:      &PackageSpecifier{Name: pkg2.Package.Name, Ecosystem: \"type2\"},\n\t\t\texpected: []AffectedPackageHandle{*pkg2},\n\t\t},\n\t\t{\n\t\t\tname: \"specific CVE\",\n\t\t\tpkg:  pkgFromName(pkg2d1.Package.Name),\n\t\t\toptions: &GetPackageOptions{\n\t\t\t\tVulnerabilities: []VulnerabilitySpecifier{{\n\t\t\t\t\tName: \"CVE-2023-1234\",\n\t\t\t\t}},\n\t\t\t},\n\t\t\texpected: []AffectedPackageHandle{*pkg2d1},\n\t\t},\n\t\t{\n\t\t\tname: \"any CVE published after a date\",\n\t\t\tpkg:  pkgFromName(pkg2d1.Package.Name),\n\t\t\toptions: &GetPackageOptions{\n\t\t\t\tVulnerabilities: []VulnerabilitySpecifier{{\n\t\t\t\t\tPublishedAfter: func() *time.Time {\n\t\t\t\t\t\tnow := time.Date(2020, 1, 1, 1, 1, 1, 0, time.UTC)\n\t\t\t\t\t\treturn &now\n\t\t\t\t\t}(),\n\t\t\t\t}},\n\t\t\t},\n\t\t\texpected: []AffectedPackageHandle{*pkg2d1, *pkg2d2},\n\t\t},\n\t\t{\n\t\t\tname: \"any CVE modified after a date\",\n\t\t\tpkg:  pkgFromName(pkg2d1.Package.Name),\n\t\t\toptions: &GetPackageOptions{\n\t\t\t\tVulnerabilities: []VulnerabilitySpecifier{{\n\t\t\t\t\tModifiedAfter: func() *time.Time {\n\t\t\t\t\t\tnow := time.Date(2023, 1, 1, 3, 4, 5, 0, time.UTC).Add(time.Hour * 2)\n\t\t\t\t\t\treturn &now\n\t\t\t\t\t}(),\n\t\t\t\t}},\n\t\t\t},\n\t\t\texpected: []AffectedPackageHandle{*pkg2d1},\n\t\t},\n\t\t{\n\t\t\tname: \"any rejected CVE\",\n\t\t\tpkg:  pkgFromName(pkg2d1.Package.Name),\n\t\t\toptions: &GetPackageOptions{\n\t\t\t\tVulnerabilities: []VulnerabilitySpecifier{{\n\t\t\t\t\tStatus: VulnerabilityRejected,\n\t\t\t\t}},\n\t\t\t},\n\t\t\texpected: []AffectedPackageHandle{*pkg2d1},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif tt.wantErr == nil {\n\t\t\t\ttt.wantErr = require.NoError\n\t\t\t}\n\t\t\tfor _, pc := range defaultAffectedPackageHandlePreloadCases() {\n\t\t\t\tt.Run(pc.name, func(t *testing.T) {\n\t\t\t\t\topts := tt.options\n\t\t\t\t\tif opts == nil {\n\t\t\t\t\t\topts = &GetPackageOptions{}\n\t\t\t\t\t}\n\t\t\t\t\topts.PreloadOS = pc.PreloadOS\n\t\t\t\t\topts.PreloadPackage = pc.PreloadPackage\n\t\t\t\t\topts.PreloadBlob = pc.PreloadBlob\n\t\t\t\t\topts.PreloadVulnerability = pc.PreloadVulnerability\n\t\t\t\t\texpected := tt.expected\n\t\t\t\t\tif pc.prepExpectations != nil {\n\t\t\t\t\t\texpected = pc.prepExpectations(t, expected)\n\t\t\t\t\t}\n\t\t\t\t\tresult, err := s.GetAffectedPackages(tt.pkg, opts)\n\t\t\t\t\ttt.wantErr(t, err)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tif d := cmp.Diff(expected, result); d != \"\" {\n\t\t\t\t\t\tt.Errorf(\"unexpected result: %s\", d)\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestAffectedPackageStore_ApplyPackageAlias(t *testing.T) {\n\tdb := setupTestStore(t).db\n\tbs := newBlobStore(db)\n\toss := newOperatingSystemStore(db, bs)\n\ts := newAffectedPackageStore(db, bs, oss)\n\n\ttests := []struct {\n\t\tname     string\n\t\tinput    *PackageSpecifier\n\t\texpected string\n\t}{\n\t\t// positive cases\n\t\t{name: \"alias cocoapods\", input: &PackageSpecifier{Ecosystem: \"cocoapods\"}, expected: \"pod\"},\n\t\t{name: \"alias pub\", input: &PackageSpecifier{Ecosystem: \"pub\"}, expected: \"dart-pub\"},\n\t\t{name: \"alias otp\", input: &PackageSpecifier{Ecosystem: \"otp\"}, expected: \"erlang-otp\"},\n\t\t{name: \"alias github\", input: &PackageSpecifier{Ecosystem: \"github\"}, expected: \"github-action\"},\n\t\t{name: \"alias golang\", input: &PackageSpecifier{Ecosystem: \"golang\"}, expected: \"go-module\"},\n\t\t{name: \"alias maven\", input: &PackageSpecifier{Ecosystem: \"maven\"}, expected: \"java-archive\"},\n\t\t{name: \"alias composer\", input: &PackageSpecifier{Ecosystem: \"composer\"}, expected: \"php-composer\"},\n\t\t{name: \"alias pecl\", input: &PackageSpecifier{Ecosystem: \"pecl\"}, expected: \"php-pecl\"},\n\t\t{name: \"alias pypi\", input: &PackageSpecifier{Ecosystem: \"pypi\"}, expected: \"python\"},\n\t\t{name: \"alias cran\", input: &PackageSpecifier{Ecosystem: \"cran\"}, expected: \"R-package\"},\n\t\t{name: \"alias luarocks\", input: &PackageSpecifier{Ecosystem: \"luarocks\"}, expected: \"lua-rocks\"},\n\t\t{name: \"alias cargo\", input: &PackageSpecifier{Ecosystem: \"cargo\"}, expected: \"rust-crate\"},\n\n\t\t// negative cases\n\t\t{name: \"generic type\", input: &PackageSpecifier{Ecosystem: \"generic/linux-kernel\"}, expected: \"generic/linux-kernel\"},\n\t\t{name: \"empty ecosystem\", input: &PackageSpecifier{Ecosystem: \"\"}, expected: \"\"},\n\t\t{name: \"matching type\", input: &PackageSpecifier{Ecosystem: \"python\"}, expected: \"python\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := s.pkgStore.applyPackageAlias(tt.input)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, tt.expected, tt.input.Ecosystem)\n\t\t})\n\t}\n}\n\nfunc testDistro1AffectedPackage2Handle() *AffectedPackageHandle {\n\tnow := time.Date(2023, 1, 1, 3, 4, 5, 0, time.UTC)\n\tlater := now.Add(time.Hour * 200)\n\treturn &AffectedPackageHandle{\n\t\tPackage: &Package{\n\t\t\tName:      \"pkg2\",\n\t\t\tEcosystem: \"type2d\",\n\t\t},\n\t\tVulnerability: &VulnerabilityHandle{\n\t\t\tName:          \"CVE-2023-1234\",\n\t\t\tStatus:        VulnerabilityRejected,\n\t\t\tPublishedDate: &now,\n\t\t\tModifiedDate:  &later,\n\t\t\tProvider: &Provider{\n\t\t\t\tID: \"ubuntu\",\n\t\t\t},\n\t\t},\n\t\tOperatingSystem: &OperatingSystem{\n\t\t\tName:         \"ubuntu\",\n\t\t\tMajorVersion: \"20\",\n\t\t\tMinorVersion: \"04\",\n\t\t\tLabelVersion: \"focal\",\n\t\t},\n\t\tBlobValue: &PackageBlob{\n\t\t\tCVEs: []string{\"CVE-2023-1234\"},\n\t\t},\n\t}\n}\n\nfunc testDistro2AffectedPackage2Handle() *AffectedPackageHandle {\n\tnow := time.Date(2020, 1, 1, 3, 4, 5, 0, time.UTC)\n\tlater := now.Add(time.Hour * 200)\n\treturn &AffectedPackageHandle{\n\t\tPackage: &Package{\n\t\t\tName:      \"pkg2\",\n\t\t\tEcosystem: \"type2d\",\n\t\t},\n\t\tVulnerability: &VulnerabilityHandle{\n\t\t\tName:          \"CVE-2023-4567\",\n\t\t\tPublishedDate: &now,\n\t\t\tModifiedDate:  &later,\n\t\t\tProvider: &Provider{\n\t\t\t\tID: \"ubuntu\",\n\t\t\t},\n\t\t},\n\t\tOperatingSystem: &OperatingSystem{\n\t\t\tName:         \"ubuntu\",\n\t\t\tMajorVersion: \"20\",\n\t\t\tMinorVersion: \"10\",\n\t\t\tLabelVersion: \"groovy\",\n\t\t},\n\t\tBlobValue: &PackageBlob{\n\t\t\tCVEs: []string{\"CVE-2023-4567\"},\n\t\t},\n\t}\n}\n\nfunc testNonDistroAffectedPackage2Handle() *AffectedPackageHandle {\n\tnow := time.Date(2005, 1, 1, 3, 4, 5, 0, time.UTC)\n\tlater := now.Add(time.Hour * 200)\n\treturn &AffectedPackageHandle{\n\t\tPackage: &Package{\n\t\t\tName:      \"pkg2\",\n\t\t\tEcosystem: \"type2\",\n\t\t},\n\t\tVulnerability: &VulnerabilityHandle{\n\t\t\tName:          \"CVE-2023-4567\",\n\t\t\tPublishedDate: &now,\n\t\t\tModifiedDate:  &later,\n\t\t\tProvider: &Provider{\n\t\t\t\tID: \"wolfi\",\n\t\t\t},\n\t\t},\n\t\tBlobValue: &PackageBlob{\n\t\t\tCVEs: []string{\"CVE-2023-4567\"},\n\t\t},\n\t}\n}\n\nfunc expectErrIs(t *testing.T, expected error) require.ErrorAssertionFunc {\n\tt.Helper()\n\treturn func(t require.TestingT, err error, msgAndArgs ...interface{}) {\n\t\trequire.Error(t, err, msgAndArgs...)\n\t\tassert.ErrorIs(t, err, expected)\n\t}\n}\n\nfunc pkgFromName(name string) *PackageSpecifier {\n\treturn &PackageSpecifier{Name: name}\n}\n"
  },
  {
    "path": "grype/db/v6/blob_store.go",
    "content": "package v6\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"strings\"\n\t\"time\"\n\n\t\"gorm.io/gorm\"\n\n\t\"github.com/anchore/grype/internal/log\"\n)\n\ntype blobable interface {\n\tgetBlobID() ID\n\tgetBlobValue() any\n\tsetBlobID(ID)\n\tsetBlob([]byte) error\n}\n\ntype blobStore struct {\n\tdb          *gorm.DB\n\tidsByDigest map[string]ID\n}\n\nfunc newBlobStore(db *gorm.DB) *blobStore {\n\treturn &blobStore{\n\t\tdb:          db,\n\t\tidsByDigest: make(map[string]ID),\n\t}\n}\n\nfunc (s *blobStore) addBlobable(bs ...blobable) error {\n\tfor i := range bs {\n\t\tb := bs[i]\n\t\tv := b.getBlobValue()\n\t\tif v == nil {\n\t\t\tcontinue\n\t\t}\n\t\tbl := newBlob(v)\n\n\t\tif err := s.addBlobs(bl); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tb.setBlobID(bl.ID)\n\t}\n\treturn nil\n}\n\nfunc (s *blobStore) addBlobs(blobs ...*Blob) error {\n\tfor i := range blobs {\n\t\tv := blobs[i]\n\t\tdigest := v.computeDigest()\n\n\t\tif id, ok := s.idsByDigest[digest]; ok && id != 0 {\n\t\t\tv.ID = id\n\t\t\tcontinue\n\t\t}\n\n\t\tif err := s.db.Create(v).Error; err != nil {\n\t\t\treturn fmt.Errorf(\"failed to create blob: %w\", err)\n\t\t}\n\n\t\tif v.ID != 0 {\n\t\t\ts.idsByDigest[digest] = v.ID\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (s *blobStore) getBlobValues(ids ...ID) ([]Blob, error) {\n\tif len(ids) == 0 {\n\t\treturn nil, nil\n\t}\n\tvar blobs []Blob\n\tif err := s.db.Where(\"id IN ?\", ids).Find(&blobs).Error; err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get blob values: %w\", err)\n\t}\n\treturn blobs, nil\n}\n\nfunc (s *blobStore) attachBlobValue(bs ...blobable) error {\n\tstart := time.Now()\n\tdefer func() {\n\t\tlog.WithFields(\"duration\", time.Since(start), \"count\", len(bs)).Trace(\"attached blob values\")\n\t}()\n\tvar ids []ID\n\tvar setterByID = make(map[ID][]blobable)\n\tfor i := range bs {\n\t\tb := bs[i]\n\n\t\tid := b.getBlobID()\n\n\t\t// skip fetching this blob if there is no blobID, or if we already have this blob\n\t\tif id == 0 || b.getBlobValue() != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tids = append(ids, id)\n\t\tsetterByID[id] = append(setterByID[id], b)\n\t}\n\n\tvs, err := s.getBlobValues(ids...)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get blob value: %w\", err)\n\t}\n\n\tfor _, b := range vs {\n\t\tif b.Value == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tfor _, setter := range setterByID[b.ID] {\n\t\t\tif err := setter.setBlob([]byte(b.Value)); err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to set blob value: %w\", err)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc newBlob(obj any) *Blob {\n\tsb := strings.Builder{}\n\tenc := json.NewEncoder(&sb)\n\tenc.SetEscapeHTML(false)\n\n\tif err := enc.Encode(obj); err != nil {\n\t\tpanic(\"could not marshal object to json\")\n\t}\n\n\treturn &Blob{\n\t\tValue: sb.String(),\n\t}\n}\n"
  },
  {
    "path": "grype/db/v6/blob_store_test.go",
    "content": "package v6\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestBlobWriter_AddBlobs(t *testing.T) {\n\tdb := setupTestStore(t).db\n\twriter := newBlobStore(db)\n\n\tobj1 := map[string]string{\"key\": \"value1\"}\n\tobj2 := map[string]string{\"key\": \"value2\"}\n\n\tblob1 := newBlob(obj1)\n\tblob2 := newBlob(obj2)\n\tblob3 := newBlob(obj1) // same as blob1\n\n\terr := writer.addBlobs(blob1, blob2, blob3)\n\trequire.NoError(t, err)\n\n\trequire.NotZero(t, blob1.ID)\n\trequire.Equal(t, blob1.ID, blob3.ID) // blob3 should have the same ID as blob1 (natural deduplication)\n\n\tvar result1 Blob\n\trequire.NoError(t, db.Where(\"id = ?\", blob1.ID).First(&result1).Error)\n\tassert.Equal(t, blob1.Value, result1.Value)\n\n\tvar result2 Blob\n\trequire.NoError(t, db.Where(\"id = ?\", blob2.ID).First(&result2).Error)\n\tassert.Equal(t, blob2.Value, result2.Value)\n}\n\nfunc TestBlob_computeDigest(t *testing.T) {\n\tassert.Equal(t, \"xxh64:0e6882304e9adbd5\", Blob{Value: \"test content\"}.computeDigest())\n\n\tassert.Equal(t, \"xxh64:ea0c19ae9fbd93b3\", Blob{Value: \"different content\"}.computeDigest())\n}\n"
  },
  {
    "path": "grype/db/v6/blobs.go",
    "content": "package v6\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"strings\"\n\t\"time\"\n)\n\n// VulnerabilityBlob represents the core advisory record for a single known vulnerability from a specific provider.\ntype VulnerabilityBlob struct {\n\t// ID is the lowercase unique string identifier for the vulnerability relative to the provider\n\tID string `json:\"id\"`\n\n\t// Assigners is a list of names, email, or organizations who submitted the vulnerability\n\tAssigners []string `json:\"assigner,omitempty\"`\n\n\t// Description of the vulnerability as provided by the source\n\tDescription string `json:\"description,omitempty\"`\n\n\t// References are URLs to external resources that provide more information about the vulnerability\n\tReferences []Reference `json:\"refs,omitempty\"`\n\n\t// Aliases is a list of IDs of the same vulnerability in other databases, in the form of the ID field. This allows one database to claim that its own entry describes the same vulnerability as one or more entries in other databases.\n\tAliases []string `json:\"aliases,omitempty\"`\n\n\t// Severities is a list of severity indications (quantitative or qualitative) for the vulnerability\n\tSeverities []Severity `json:\"severities,omitempty\"`\n}\n\nfunc (v VulnerabilityBlob) String() string {\n\treturn v.ID\n}\n\n// Reference represents a single external URL and string tags to use for organizational purposes\ntype Reference struct {\n\t// URL is the external resource\n\tURL string `json:\"url\"`\n\n\t// ID is an optional identifier for the reference (e.g., advisory ID like \"RHSA-2023:5455\")\n\tID string `json:\"id,omitempty\"`\n\n\t// Tags is a free-form organizational field to convey additional information about the reference\n\tTags []string `json:\"tags,omitempty\"`\n}\n\n// Severity represents a single string severity record for a vulnerability record\ntype Severity struct {\n\t// Scheme describes the quantitative method used to determine the Score, such as \"CVSS_V3\". Alternatively this makes\n\t// claim that Value is qualitative, for example \"HML\" (High, Medium, Low), CHMLN (critical-high-medium-low-negligible)\n\tScheme SeverityScheme `json:\"scheme\"`\n\n\t// Value is the severity score (e.g. \"7.5\", \"CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N\",  or \"high\" )\n\tValue any `json:\"value\"` // one of CVSSSeverity, HMLSeverity, CHMLNSeverity\n\n\t// Source is the name of the source of the severity score (e.g. \"nvd@nist.gov\" or \"security-advisories@github.com\")\n\tSource string `json:\"source,omitempty\"`\n\n\t// Rank is a free-form organizational field to convey priority over other severities\n\tRank int `json:\"rank\"`\n}\n\ntype severityAlias Severity\n\ntype severityUnmarshalProxy struct {\n\t*severityAlias\n\tValue json.RawMessage `json:\"value\"`\n}\n\n// UnmarshalJSON custom unmarshaller for Severity struct\nfunc (s *Severity) UnmarshalJSON(data []byte) error {\n\taux := &severityUnmarshalProxy{\n\t\tseverityAlias: (*severityAlias)(s),\n\t}\n\n\tif err := json.Unmarshal(data, aux); err != nil {\n\t\treturn err\n\t}\n\n\tvar cvss CVSSSeverity\n\tif err := json.Unmarshal(aux.Value, &cvss); err == nil && cvss.Vector != \"\" {\n\t\ts.Value = cvss\n\t\treturn nil\n\t}\n\n\tvar strSeverity string\n\tif err := json.Unmarshal(aux.Value, &strSeverity); err == nil {\n\t\ts.Value = strSeverity\n\t\treturn nil\n\t}\n\n\treturn fmt.Errorf(\"could not unmarshal severity value to known type: %s\", aux.Value)\n}\n\n// CVSSSeverity represents a single Common Vulnerability Scoring System entry\ntype CVSSSeverity struct {\n\t// Vector is the CVSS assessment as a parameterized string\n\tVector string `json:\"vector\"`\n\n\t// Version is the CVSS version (e.g. \"3.0\")\n\tVersion string `json:\"version,omitempty\"`\n}\n\nfunc (c CVSSSeverity) String() string {\n\tvector := c.Vector\n\tif !strings.HasPrefix(strings.ToLower(c.Vector), \"cvss:\") && c.Version != \"\" {\n\t\tvector = fmt.Sprintf(\"CVSS:%s/%s\", c.Version, c.Vector)\n\t}\n\treturn vector\n}\n\n// PackageBlob represents a package that is affected by a vulnerability.\ntype PackageBlob struct {\n\t// CVEs is a list of Common Vulnerabilities and Exposures (CVE) identifiers related to this vulnerability.\n\tCVEs []string `json:\"cves,omitempty\"`\n\n\t// Qualifiers are package attributes that confirm the package is affected by the vulnerability.\n\tQualifiers *PackageQualifiers `json:\"qualifiers,omitempty\"`\n\n\t// Ranges specifies the affected version ranges and fixes if available.\n\tRanges []Range `json:\"ranges,omitempty\"`\n}\n\nfunc (a PackageBlob) String() string {\n\tvar fields []string\n\n\tif len(a.Ranges) > 0 {\n\t\tvar ranges []string\n\t\tfor _, r := range a.Ranges {\n\t\t\tranges = append(ranges, r.String())\n\t\t}\n\t\tfields = append(fields, fmt.Sprintf(\"ranges=%s\", strings.Join(ranges, \", \")))\n\t}\n\n\tif len(a.CVEs) > 0 {\n\t\tfields = append(fields, fmt.Sprintf(\"cves=%s\", strings.Join(a.CVEs, \", \")))\n\t}\n\n\treturn strings.Join(fields, \", \")\n}\n\n// PackageQualifiers contains package attributes that should hold true to associate a vulnerablity to that package.\ntype PackageQualifiers struct {\n\t// RpmModularity indicates if the package follows RPM modularity for versioning.\n\tRpmModularity *string `json:\"rpm_modularity,omitempty\"`\n\n\t// PlatformCPEs lists Common Platform Enumeration (CPE) identifiers for affected platforms.\n\tPlatformCPEs []string `json:\"platform_cpes,omitempty\"`\n}\n\n// Range defines a specific range of package versions pertaining to a vulnerability.\ntype Range struct {\n\t// Version defines the version constraints for affected software.\n\tVersion Version `json:\"version,omitempty\"`\n\n\t// Fix provides details on the fix version and its state if available.\n\tFix *Fix `json:\"fix,omitempty\"`\n}\n\nfunc (a Range) String() string {\n\treturn fmt.Sprintf(\"%s (%s)\", a.Version, a.Fix)\n}\n\n// Fix conveys availability of a fix for a vulnerability.\ntype Fix struct {\n\t// Version is the version number of the fix.\n\tVersion string `json:\"version,omitempty\"`\n\n\t// State represents the status of the fix (e.g., \"fixed\", \"unaffected\").\n\tState FixStatus `json:\"state,omitempty\"`\n\n\t// Detail provides additional fix information, such as commit details.\n\tDetail *FixDetail `json:\"detail,omitempty\"`\n}\n\nfunc (f Fix) String() string {\n\tswitch f.State {\n\tcase FixedStatus:\n\t\treturn fmt.Sprintf(\"fixed in %s\", f.Version)\n\tcase NotAffectedFixStatus:\n\t\treturn fmt.Sprintf(\"%s is not affected\", f.Version)\n\t}\n\treturn string(f.State)\n}\n\n// FixDetail is additional information about a fix, such as commit details and patch URLs.\ntype FixDetail struct {\n\t// Available indicates when the fix information became available and how it was obtained.\n\tAvailable *FixAvailability `json:\"available,omitempty\"`\n\n\t// References contains URLs or identifiers for additional resources on the fix.\n\tReferences []Reference `json:\"references,omitempty\"`\n}\n\ntype FixAvailability struct {\n\t// Date is the date and time when fix information became available. Note: this might not be when the fix was created, committed or merged.\n\tDate *time.Time `json:\"date,omitempty\"`\n\n\t// Kind describes how this date was obtained (e.g. advisory, release, commit, PR, issue, first-observed-record)\n\tKind string `json:\"kind,omitempty\"`\n}\n\nfunc (f FixAvailability) MarshalJSON() ([]byte, error) {\n\ttype Alias FixAvailability\n\taux := &struct {\n\t\tDate *string `json:\"date,omitempty\"`\n\t\t*Alias\n\t}{\n\t\tAlias: (*Alias)(&f),\n\t}\n\n\t// the JSON marshaller should interpret the time.Time as a Date, not a timestamp\n\tif f.Date != nil {\n\t\tdateStr := f.Date.Format(\"2006-01-02\")\n\t\taux.Date = &dateStr\n\t}\n\n\treturn json.Marshal(aux)\n}\n\nfunc (f *FixAvailability) UnmarshalJSON(data []byte) error {\n\ttype Alias FixAvailability\n\taux := &struct {\n\t\tDate *string `json:\"date,omitempty\"`\n\t\t*Alias\n\t}{\n\t\tAlias: (*Alias)(f),\n\t}\n\n\tif err := json.Unmarshal(data, aux); err != nil {\n\t\treturn err\n\t}\n\n\tif aux.Date != nil {\n\t\tif t, err := time.Parse(\"2006-01-02\", *aux.Date); err == nil {\n\t\t\tf.Date = &t\n\t\t\treturn nil\n\t\t}\n\n\t\tif t, err := time.Parse(time.RFC3339, *aux.Date); err == nil {\n\t\t\tf.Date = &t\n\t\t\treturn nil\n\t\t}\n\n\t\treturn fmt.Errorf(\"unable to parse date %q: expected format YYYY-MM-DD or RFC3339\", *aux.Date)\n\t}\n\n\treturn nil\n}\n\n// Version defines the versioning format and constraints.\ntype Version struct {\n\t// Type specifies the versioning system used (e.g., \"semver\", \"rpm\").\n\tType string `json:\"type,omitempty\"`\n\n\t// Constraint defines the version range constraint for affected versions.\n\tConstraint string `json:\"constraint,omitempty\"`\n}\n\ntype KnownExploitedVulnerabilityBlob struct {\n\tCve                        string     `json:\"cve\"`\n\tVendorProject              string     `json:\"vendor_project,omitempty\"`\n\tProduct                    string     `json:\"product,omitempty\"`\n\tDateAdded                  *time.Time `json:\"date_added,omitempty\"`\n\tRequiredAction             string     `json:\"required_action,omitempty\"`\n\tDueDate                    *time.Time `json:\"due_date,omitempty\"`\n\tKnownRansomwareCampaignUse string     `json:\"known_ransomware_campaign_use,omitempty\"`\n\tNotes                      string     `json:\"notes,omitempty\"`\n\tURLs                       []string   `json:\"urls,omitempty\"`\n\tCWEs                       []string   `json:\"cwes,omitempty\"`\n}\n"
  },
  {
    "path": "grype/db/v6/blobs_test.go",
    "content": "package v6\n\nimport (\n\t\"encoding/json\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestFixAvailability_MarshalJSON(t *testing.T) {\n\ttestTime := time.Date(2022, 4, 9, 15, 30, 45, 0, time.UTC)\n\n\tfixAvail := FixAvailability{\n\t\tDate: &testTime,\n\t\tKind: \"advisory\",\n\t}\n\n\tjsonData, err := json.Marshal(fixAvail)\n\trequire.NoError(t, err)\n\n\texpected := `{\"date\":\"2022-04-09\",\"kind\":\"advisory\"}`\n\tassert.JSONEq(t, expected, string(jsonData))\n}\n\nfunc TestFixAvailability_UnmarshalJSON_SimpleDateFormat(t *testing.T) {\n\tjsonData := `{\"date\":\"2022-04-09\",\"kind\":\"advisory\"}`\n\n\tvar fixAvail FixAvailability\n\terr := json.Unmarshal([]byte(jsonData), &fixAvail)\n\trequire.NoError(t, err)\n\n\texpectedTime := time.Date(2022, 4, 9, 0, 0, 0, 0, time.UTC)\n\tassert.Equal(t, &expectedTime, fixAvail.Date)\n\tassert.Equal(t, \"advisory\", fixAvail.Kind)\n}\n\nfunc TestFixAvailability_UnmarshalJSON_RFC3339Format(t *testing.T) {\n\tjsonData := `{\"date\":\"2022-04-09T00:00:00Z\",\"kind\":\"advisory\"}`\n\n\tvar fixAvail FixAvailability\n\terr := json.Unmarshal([]byte(jsonData), &fixAvail)\n\trequire.NoError(t, err)\n\n\texpectedTime := time.Date(2022, 4, 9, 0, 0, 0, 0, time.UTC)\n\tassert.Equal(t, &expectedTime, fixAvail.Date)\n\tassert.Equal(t, \"advisory\", fixAvail.Kind)\n}\n\nfunc TestFixAvailability_RoundTripMarshalUnmarshal(t *testing.T) {\n\toriginalTime := time.Date(2022, 4, 9, 15, 30, 45, 0, time.UTC)\n\n\toriginal := FixAvailability{\n\t\tDate: &originalTime,\n\t\tKind: \"advisory\",\n\t}\n\n\tjsonData, err := json.Marshal(original)\n\trequire.NoError(t, err)\n\n\tvar unmarshaled FixAvailability\n\terr = json.Unmarshal(jsonData, &unmarshaled)\n\trequire.NoError(t, err)\n\n\t// Time precision is lost during marshaling - only date is preserved\n\texpectedTime := time.Date(2022, 4, 9, 0, 0, 0, 0, time.UTC)\n\tassert.Equal(t, &expectedTime, unmarshaled.Date)\n\tassert.Equal(t, \"advisory\", unmarshaled.Kind)\n}\n\nfunc TestPackageBlob_WithFixAvailability(t *testing.T) {\n\ttestTime := time.Date(2022, 4, 9, 0, 0, 0, 0, time.UTC)\n\n\tblob := PackageBlob{\n\t\tCVEs: []string{\"CVE-2021-3521\"},\n\t\tRanges: []Range{\n\t\t\t{\n\t\t\t\tVersion: Version{\n\t\t\t\t\tType:       \"rpm\",\n\t\t\t\t\tConstraint: \"< 0:4.14.2-15.cm1\",\n\t\t\t\t},\n\t\t\t\tFix: &Fix{\n\t\t\t\t\tVersion: \"0:4.14.2-15.cm1\",\n\t\t\t\t\tState:   FixedStatus,\n\t\t\t\t\tDetail: &FixDetail{\n\t\t\t\t\t\tAvailable: &FixAvailability{\n\t\t\t\t\t\t\tDate: &testTime,\n\t\t\t\t\t\t\tKind: \"advisory\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tjsonData, err := json.Marshal(blob)\n\trequire.NoError(t, err)\n\n\tassert.Contains(t, string(jsonData), `\"date\":\"2022-04-09\"`)\n\tassert.NotContains(t, string(jsonData), `\"date\":\"2022-04-09T`)\n\n\tvar unmarshaledBlob PackageBlob\n\terr = json.Unmarshal(jsonData, &unmarshaledBlob)\n\trequire.NoError(t, err)\n\n\trequire.Len(t, unmarshaledBlob.Ranges, 1)\n\trequire.NotNil(t, unmarshaledBlob.Ranges[0].Fix)\n\trequire.NotNil(t, unmarshaledBlob.Ranges[0].Fix.Detail)\n\trequire.NotNil(t, unmarshaledBlob.Ranges[0].Fix.Detail.Available)\n\n\tassert.Equal(t, &testTime, unmarshaledBlob.Ranges[0].Fix.Detail.Available.Date)\n\tassert.Equal(t, \"advisory\", unmarshaledBlob.Ranges[0].Fix.Detail.Available.Kind)\n}\n\nfunc TestFixAvailability_UnmarshalJSON_InvalidDateFormat(t *testing.T) {\n\tjsonData := `{\"date\":\"invalid-date\",\"kind\":\"advisory\"}`\n\n\tvar fixAvail FixAvailability\n\terr := json.Unmarshal([]byte(jsonData), &fixAvail)\n\n\trequire.Error(t, err)\n\tassert.Contains(t, err.Error(), `unable to parse date \"invalid-date\"`)\n\tassert.Contains(t, err.Error(), \"expected format YYYY-MM-DD or RFC3339\")\n}\n"
  },
  {
    "path": "grype/db/v6/build/archive.go",
    "content": "package v6\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/anchore/grype/grype/db/internal/tarutil\"\n\t\"github.com/anchore/grype/grype/db/provider\"\n\tv6 \"github.com/anchore/grype/grype/db/v6\"\n\tv6Distribution \"github.com/anchore/grype/grype/db/v6/distribution\"\n\t\"github.com/anchore/grype/internal/log\"\n)\n\nfunc CreateArchive(dbDir, overrideArchiveExtension string, compressorCommands map[string]string) error {\n\textension, err := resolveExtension(overrideArchiveExtension)\n\tif err != nil {\n\t\treturn err\n\t}\n\tlog.WithFields(\"from\", dbDir, \"extension\", extension).Info(\"packaging database\")\n\n\tcfg := v6.Config{DBDirPath: dbDir}\n\tr, err := v6.NewReader(cfg)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to open vulnerability store: %w\", err)\n\t}\n\n\tmetadata, err := r.GetDBMetadata()\n\tif err != nil || metadata == nil {\n\t\treturn fmt.Errorf(\"unable to get vulnerability store metadata: %w\", err)\n\t}\n\n\tif metadata.Model != v6.ModelVersion {\n\t\treturn fmt.Errorf(\"metadata model %d does not match vulnerability store model %d\", v6.ModelVersion, metadata.Model)\n\t}\n\n\tproviderModels, err := r.AllProviders()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to get all providers: %w\", err)\n\t}\n\n\tif len(providerModels) == 0 {\n\t\treturn fmt.Errorf(\"no providers found in the vulnerability store\")\n\t}\n\n\teldest, err := toProviders(providerModels).EarliestTimestamp()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// output archive vulnerability-db_VERSION_OLDESTDATADATE_BUILTEPOCH.tar.gz, where:\n\t// - VERSION: schema version in the form of v#.#.#\n\t// - OLDESTDATADATE: RFC3339 formatted value (e.g. 2020-06-18T17:24:53Z) of the oldest date capture date found for all contained providers\n\t// - BUILTEPOCH: linux epoch formatted value of the database metadata built field\n\ttarName := fmt.Sprintf(\n\t\t\"vulnerability-db_v%s_%s_%d.%s\",\n\t\tfmt.Sprintf(\"%d.%d.%d\", metadata.Model, metadata.Revision, metadata.Addition),\n\t\teldest.UTC().Format(time.RFC3339),\n\t\tmetadata.BuildTimestamp.Unix(),\n\t\textension,\n\t)\n\n\ttarPath := filepath.Join(dbDir, tarName)\n\tfiles := []string{v6.VulnerabilityDBFileName}\n\n\tif _, err := os.Stat(path.Join(dbDir, v6.ImportMetadataFileName)); err == nil {\n\t\tfiles = append(files, v6.ImportMetadataFileName)\n\t}\n\n\tif err := populateTar(dbDir, tarName, compressorCommands, files...); err != nil {\n\t\treturn err\n\t}\n\n\tlog.WithFields(\"path\", tarPath).Info(\"created database archive\")\n\n\treturn writeLatestDocument(tarPath, *metadata)\n}\n\nfunc toProviders(states []v6.Provider) provider.States {\n\tvar result provider.States\n\tfor _, state := range states {\n\t\tresult = append(result, provider.State{\n\t\t\tProvider:  state.ID,\n\t\t\tTimestamp: *state.DateCaptured,\n\t\t})\n\t}\n\treturn result\n}\n\nfunc resolveExtension(overrideArchiveExtension string) (string, error) {\n\tvar extension = \"tar.zst\"\n\n\tif overrideArchiveExtension != \"\" {\n\t\textension = strings.TrimLeft(overrideArchiveExtension, \".\")\n\t}\n\n\tvar found bool\n\tfor _, valid := range []string{\"tar.zst\", \"tar.xz\", \"tar.gz\"} {\n\t\tif valid == extension {\n\t\t\tfound = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif !found {\n\t\treturn \"\", fmt.Errorf(\"unsupported archive extension %q\", extension)\n\t}\n\treturn extension, nil\n}\n\nfunc populateTar(dbDir, tarName string, compressorCommands map[string]string, files ...string) error {\n\toriginalDir, err := os.Getwd()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to get CWD: %w\", err)\n\t}\n\n\tif dbDir != \"\" {\n\t\tif err = os.Chdir(dbDir); err != nil {\n\t\t\treturn fmt.Errorf(\"unable to cd to build dir: %w\", err)\n\t\t}\n\n\t\tdefer func() {\n\t\t\tif err = os.Chdir(originalDir); err != nil {\n\t\t\t\tlog.Errorf(\"unable to cd to original dir: %v\", err)\n\t\t\t}\n\t\t}()\n\t}\n\n\tfor _, f := range files {\n\t\t_, err := os.Stat(f)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"unable to stat file %q: %w\", f, err)\n\t\t}\n\t}\n\n\tif err = tarutil.PopulateWithPathsAndCompressors(tarName, compressorCommands, files...); err != nil {\n\t\treturn fmt.Errorf(\"unable to create db archive: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc writeLatestDocument(tarPath string, metadata v6.DBMetadata) error {\n\tarchive, err := v6Distribution.NewArchive(tarPath, *metadata.BuildTimestamp, metadata.Model, metadata.Revision, metadata.Addition)\n\tif err != nil || archive == nil {\n\t\treturn fmt.Errorf(\"unable to create archive: %w\", err)\n\t}\n\n\tdoc := v6Distribution.NewLatestDocument(*archive)\n\tif doc == nil {\n\t\treturn errors.New(\"unable to create latest document\")\n\t}\n\n\tdbDir := filepath.Dir(tarPath)\n\n\tlatestPath := path.Join(dbDir, v6Distribution.LatestFileName)\n\n\tfh, err := os.OpenFile(latestPath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to create latest file: %w\", err)\n\t}\n\n\tif err = doc.Write(fh); err != nil {\n\t\treturn fmt.Errorf(\"unable to write latest document: %w\", err)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "grype/db/v6/build/processors.go",
    "content": "package v6\n\nimport (\n\t\"github.com/scylladb/go-set/strset\"\n\n\t\"github.com/anchore/grype/grype/db/data\"\n\t\"github.com/anchore/grype/grype/db/processors\"\n\t\"github.com/anchore/grype/grype/db/v6/build/transformers/eol\"\n\t\"github.com/anchore/grype/grype/db/v6/build/transformers/epss\"\n\t\"github.com/anchore/grype/grype/db/v6/build/transformers/github\"\n\t\"github.com/anchore/grype/grype/db/v6/build/transformers/kev\"\n\t\"github.com/anchore/grype/grype/db/v6/build/transformers/msrc\"\n\t\"github.com/anchore/grype/grype/db/v6/build/transformers/nvd\"\n\t\"github.com/anchore/grype/grype/db/v6/build/transformers/openvex\"\n\t\"github.com/anchore/grype/grype/db/v6/build/transformers/os\"\n\t\"github.com/anchore/grype/grype/db/v6/build/transformers/osv\"\n)\n\ntype Config struct {\n\tNVD nvd.Config\n}\n\ntype Option func(cfg *Config)\n\nfunc WithCPEParts(included []string) Option {\n\treturn func(cfg *Config) {\n\t\tcfg.NVD.CPEParts = strset.New(included...)\n\t}\n}\n\nfunc WithInferNVDFixVersions(infer bool) Option {\n\treturn func(cfg *Config) {\n\t\tcfg.NVD.InferNVDFixVersions = infer\n\t}\n}\n\nfunc NewConfig(options ...Option) Config {\n\tvar cfg Config\n\tfor _, option := range options {\n\t\toption(&cfg)\n\t}\n\n\treturn cfg\n}\n\nfunc Processors(cfg Config) []data.Processor {\n\treturn []data.Processor{\n\t\tprocessors.NewV2GitHubProcessor(github.Transform),\n\t\tprocessors.NewV2MSRCProcessor(msrc.Transform),\n\t\tprocessors.NewV2NVDProcessor(nvd.Transformer(cfg.NVD)),\n\t\tprocessors.NewV2OSProcessor(os.Transform),\n\t\tprocessors.NewV2OSVProcessor(osv.Transform),\n\t\tprocessors.NewV2KEVProcessor(kev.Transform),\n\t\tprocessors.NewV2EPSSProcessor(epss.Transform),\n\t\tprocessors.NewV2OpenVEXProcessor(openvex.Transform),\n\t\tprocessors.NewV2AnnotatedOpenVEXProcessor(openvex.AnnotatedTransform),\n\t\t// EOL processor must be last to update existing OS records\n\t\tprocessors.NewV2EOLProcessor(eol.Transform),\n\t}\n}\n"
  },
  {
    "path": "grype/db/v6/build/transformers/entry.go",
    "content": "package transformers\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/anchore/grype/grype/db/data\"\n\tdb \"github.com/anchore/grype/grype/db/v6\"\n)\n\ntype RelatedEntries struct {\n\tVulnerabilityHandle *db.VulnerabilityHandle\n\tProvider            *db.Provider\n\tRelated             []any\n}\n\nfunc NewEntries(models ...any) []data.Entry {\n\tvar entry RelatedEntries\n\n\tfor i := range models {\n\t\tmodel := models[i]\n\t\tswitch m := model.(type) {\n\t\tcase db.VulnerabilityHandle:\n\t\t\tentry.VulnerabilityHandle = &m\n\t\tcase db.AffectedPackageHandle, db.UnaffectedPackageHandle, db.AffectedCPEHandle,\n\t\t\tdb.UnaffectedCPEHandle, db.KnownExploitedVulnerabilityHandle, db.EpssHandle, db.CWEHandle,\n\t\t\tdb.OperatingSystemEOLHandle:\n\t\t\tentry.Related = append(entry.Related, m)\n\t\tcase db.Provider:\n\t\t\tentry.Provider = &m\n\t\tdefault:\n\t\t\tpanic(fmt.Sprintf(\"unsupported model type: %T\", m))\n\t\t}\n\t}\n\n\treturn []data.Entry{\n\t\t{\n\t\t\tDBSchemaVersion: db.ModelVersion,\n\t\t\tData:            entry,\n\t\t},\n\t}\n}\n\nfunc (re RelatedEntries) String() string {\n\tvar pkgs []string\n\tfor _, r := range re.Related {\n\t\tswitch v := r.(type) {\n\t\tcase db.AffectedPackageHandle:\n\t\t\tpkgs = append(pkgs, v.Package.String())\n\t\tcase db.AffectedCPEHandle:\n\t\t\tpkgs = append(pkgs, fmt.Sprintf(\"%s/%s\", v.CPE.Vendor, v.CPE.Product))\n\t\tcase db.KnownExploitedVulnerabilityHandle:\n\t\t\tpkgs = append(pkgs, \"kev=\"+v.Cve)\n\t\t}\n\t}\n\tvar fields []string\n\tif re.VulnerabilityHandle != nil {\n\t\tfields = append(fields, fmt.Sprintf(\"vuln=%q\", re.VulnerabilityHandle.Name))\n\t\tfields = append(fields, fmt.Sprintf(\"provider=%q\", re.VulnerabilityHandle.ProviderID))\n\t} else if re.Provider != nil {\n\t\tfields = append(fields, fmt.Sprintf(\"provider=%q\", re.Provider.ID))\n\t}\n\n\tfields = append(fields, fmt.Sprintf(\"entries=%d\", len(re.Related)))\n\n\treturn fmt.Sprintf(\"%s: %s\", strings.Join(fields, \" \"), strings.Join(pkgs, \", \"))\n}\n"
  },
  {
    "path": "grype/db/v6/build/transformers/eol/transform.go",
    "content": "package eol\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/anchore/grype/grype/db/data\"\n\t\"github.com/anchore/grype/grype/db/internal/provider/unmarshal\"\n\t\"github.com/anchore/grype/grype/db/provider\"\n\tdb \"github.com/anchore/grype/grype/db/v6\"\n\t\"github.com/anchore/grype/grype/db/v6/build/transformers\"\n\t\"github.com/anchore/grype/grype/db/v6/build/transformers/internal\"\n\t\"github.com/anchore/grype/internal/log\"\n)\n\n// productNameMapping translates endoflife.date product names to grype distro names.\n// Only includes mappings where the names differ.\nvar productNameMapping = map[string]string{\n\t\"alpine-linux\":  \"alpine\",\n\t\"rhel\":          \"redhat\",\n\t\"amazon-linux\":  \"amazonlinux\",\n\t\"oracle-linux\":  \"oraclelinux\",\n\t\"rocky-linux\":   \"rockylinux\",\n\t\"centos-stream\": \"centos\", // CentOS Stream is separate from classic CentOS\n}\n\n// supportedDistros lists distros we want to import EOL data for.\n// These are distros that grype tracks vulnerability data for.\nvar supportedDistros = map[string]bool{\n\t\"alpine\":      true,\n\t\"amazonlinux\": true,\n\t\"centos\":      true,\n\t\"debian\":      true,\n\t\"fedora\":      true,\n\t\"oraclelinux\": true,\n\t\"redhat\":      true,\n\t\"rockylinux\":  true,\n\t\"almalinux\":   true,\n\t\"sles\":        true,\n\t\"ubuntu\":      true,\n\t\"photon\":      true,\n\t\"mariner\":     true,\n\t\"azurelinux\":  true,\n\t\"wolfi\":       true,\n\t\"chainguard\":  true,\n}\n\n// Transform converts an EOL record into entries for the database.\nfunc Transform(entry unmarshal.EndOfLifeDateRelease, state provider.State) ([]data.Entry, error) {\n\tproductName := entry.ProductName()\n\tdistroName := translateProductName(productName)\n\n\t// Skip non-distro products (software packages, frameworks, etc.)\n\tif !supportedDistros[distroName] {\n\t\tlog.WithFields(\"product\", productName).Trace(\"skipping non-distro EOL record\")\n\t\treturn nil, nil\n\t}\n\n\thandle := getOperatingSystemEOL(entry, distroName)\n\tif handle == nil {\n\t\treturn nil, nil\n\t}\n\n\treturn transformers.NewEntries(*provider.Model(state), *handle), nil\n}\n\n// translateProductName converts endoflife.date product names to grype distro names.\nfunc translateProductName(product string) string {\n\tif mapped, ok := productNameMapping[product]; ok {\n\t\treturn mapped\n\t}\n\treturn product\n}\n\n// getOperatingSystemEOL creates an OperatingSystemEOLHandle from an EOL record.\nfunc getOperatingSystemEOL(entry unmarshal.EndOfLifeDateRelease, distroName string) *db.OperatingSystemEOLHandle {\n\t// Parse version from name (e.g., \"12\", \"22.04\", \"8.5\")\n\tmajorVersion, minorVersion := parseVersion(entry.Name)\n\n\t// Parse EOL dates\n\tvar eolDate, eoasDate *time.Time\n\tif entry.EOLFrom != nil {\n\t\teolDate = internal.ParseTime(*entry.EOLFrom)\n\t}\n\tif entry.EOASFrom != nil {\n\t\teoasDate = internal.ParseTime(*entry.EOASFrom)\n\t}\n\n\t// Skip if no EOL data\n\tif eolDate == nil && eoasDate == nil {\n\t\treturn nil\n\t}\n\n\t// Note: We intentionally don't include codename in the handle because\n\t// endoflife.date uses full names like \"Noble Numbat\" while the DB uses\n\t// short lowercase names like \"noble\". Version matching is sufficient.\n\treturn &db.OperatingSystemEOLHandle{\n\t\tName:         distroName,\n\t\tMajorVersion: majorVersion,\n\t\tMinorVersion: minorVersion,\n\t\tEOLDate:      eolDate,\n\t\tEOASDate:     eoasDate,\n\t}\n}\n\n// parseVersion extracts major and minor version from a cycle string.\n// Normalizes versions by stripping leading zeros (e.g., \"04\" -> \"4\")\n// to match the format used in the vulnerability database.\nfunc parseVersion(cycle string) (major, minor string) {\n\tparts := strings.Split(cycle, \".\")\n\tif len(parts) >= 1 {\n\t\tmajor = normalizeVersion(parts[0])\n\t}\n\tif len(parts) >= 2 {\n\t\tminor = normalizeVersion(parts[1])\n\t}\n\treturn major, minor\n}\n\n// normalizeVersion strips leading zeros from a version string.\nfunc normalizeVersion(v string) string {\n\t// Try to parse as integer to strip leading zeros\n\tif i, err := strconv.Atoi(v); err == nil {\n\t\treturn strconv.Itoa(i)\n\t}\n\treturn v\n}\n"
  },
  {
    "path": "grype/db/v6/build/transformers/eol/transform_test.go",
    "content": "package eol\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestParseVersion(t *testing.T) {\n\ttests := []struct {\n\t\tname      string\n\t\tcycle     string\n\t\twantMajor string\n\t\twantMinor string\n\t}{\n\t\t{\n\t\t\tname:      \"major only\",\n\t\t\tcycle:     \"12\",\n\t\t\twantMajor: \"12\",\n\t\t\twantMinor: \"\",\n\t\t},\n\t\t{\n\t\t\tname:      \"major and minor\",\n\t\t\tcycle:     \"22.04\",\n\t\t\twantMajor: \"22\",\n\t\t\twantMinor: \"4\",\n\t\t},\n\t\t{\n\t\t\tname:      \"major minor patch\",\n\t\t\tcycle:     \"8.5.1\",\n\t\t\twantMajor: \"8\",\n\t\t\twantMinor: \"5\",\n\t\t},\n\t\t{\n\t\t\tname:      \"leading zero in minor\",\n\t\t\tcycle:     \"20.04\",\n\t\t\twantMajor: \"20\",\n\t\t\twantMinor: \"4\",\n\t\t},\n\t\t{\n\t\t\tname:      \"leading zero in major\",\n\t\t\tcycle:     \"08\",\n\t\t\twantMajor: \"8\",\n\t\t\twantMinor: \"\",\n\t\t},\n\t\t{\n\t\t\tname:      \"non-numeric version\",\n\t\t\tcycle:     \"bullseye\",\n\t\t\twantMajor: \"bullseye\",\n\t\t\twantMinor: \"\",\n\t\t},\n\t\t{\n\t\t\tname:      \"mixed numeric and non-numeric\",\n\t\t\tcycle:     \"3.x\",\n\t\t\twantMajor: \"3\",\n\t\t\twantMinor: \"x\",\n\t\t},\n\t\t{\n\t\t\tname:      \"empty string\",\n\t\t\tcycle:     \"\",\n\t\t\twantMajor: \"\",\n\t\t\twantMinor: \"\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgotMajor, gotMinor := parseVersion(tt.cycle)\n\t\t\tassert.Equal(t, tt.wantMajor, gotMajor, \"major version mismatch\")\n\t\t\tassert.Equal(t, tt.wantMinor, gotMinor, \"minor version mismatch\")\n\t\t})\n\t}\n}\n\nfunc TestNormalizeVersion(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\tversion string\n\t\twant    string\n\t}{\n\t\t{\n\t\t\tname:    \"no leading zeros\",\n\t\t\tversion: \"12\",\n\t\t\twant:    \"12\",\n\t\t},\n\t\t{\n\t\t\tname:    \"single leading zero\",\n\t\t\tversion: \"04\",\n\t\t\twant:    \"4\",\n\t\t},\n\t\t{\n\t\t\tname:    \"multiple leading zeros\",\n\t\t\tversion: \"004\",\n\t\t\twant:    \"4\",\n\t\t},\n\t\t{\n\t\t\tname:    \"zero value\",\n\t\t\tversion: \"0\",\n\t\t\twant:    \"0\",\n\t\t},\n\t\t{\n\t\t\tname:    \"non-numeric\",\n\t\t\tversion: \"bullseye\",\n\t\t\twant:    \"bullseye\",\n\t\t},\n\t\t{\n\t\t\tname:    \"mixed content\",\n\t\t\tversion: \"12a\",\n\t\t\twant:    \"12a\",\n\t\t},\n\t\t{\n\t\t\tname:    \"empty string\",\n\t\t\tversion: \"\",\n\t\t\twant:    \"\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot := normalizeVersion(tt.version)\n\t\t\tassert.Equal(t, tt.want, got)\n\t\t})\n\t}\n}\n\nfunc TestTranslateProductName(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\tproduct string\n\t\twant    string\n\t}{\n\t\t{\n\t\t\tname:    \"alpine-linux to alpine\",\n\t\t\tproduct: \"alpine-linux\",\n\t\t\twant:    \"alpine\",\n\t\t},\n\t\t{\n\t\t\tname:    \"rhel to redhat\",\n\t\t\tproduct: \"rhel\",\n\t\t\twant:    \"redhat\",\n\t\t},\n\t\t{\n\t\t\tname:    \"amazon-linux to amazonlinux\",\n\t\t\tproduct: \"amazon-linux\",\n\t\t\twant:    \"amazonlinux\",\n\t\t},\n\t\t{\n\t\t\tname:    \"oracle-linux to oraclelinux\",\n\t\t\tproduct: \"oracle-linux\",\n\t\t\twant:    \"oraclelinux\",\n\t\t},\n\t\t{\n\t\t\tname:    \"rocky-linux to rockylinux\",\n\t\t\tproduct: \"rocky-linux\",\n\t\t\twant:    \"rockylinux\",\n\t\t},\n\t\t{\n\t\t\tname:    \"centos-stream to centos\",\n\t\t\tproduct: \"centos-stream\",\n\t\t\twant:    \"centos\",\n\t\t},\n\t\t{\n\t\t\tname:    \"unmapped product returns as-is\",\n\t\t\tproduct: \"debian\",\n\t\t\twant:    \"debian\",\n\t\t},\n\t\t{\n\t\t\tname:    \"ubuntu returns as-is\",\n\t\t\tproduct: \"ubuntu\",\n\t\t\twant:    \"ubuntu\",\n\t\t},\n\t\t{\n\t\t\tname:    \"unknown product returns as-is\",\n\t\t\tproduct: \"some-unknown-product\",\n\t\t\twant:    \"some-unknown-product\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot := translateProductName(tt.product)\n\t\t\tassert.Equal(t, tt.want, got)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/db/v6/build/transformers/epss/testdata/go-case.json",
    "content": "{\n    \"cve\": \"CVE-2025-0108\",\n    \"epss\": 0.328,\n    \"percentile\": 0.9929,\n    \"date\": \"2025-02-18\"\n}"
  },
  {
    "path": "grype/db/v6/build/transformers/epss/transform.go",
    "content": "package epss\n\nimport (\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/anchore/grype/grype/db/data\"\n\t\"github.com/anchore/grype/grype/db/internal/provider/unmarshal\"\n\t\"github.com/anchore/grype/grype/db/provider\"\n\tdb \"github.com/anchore/grype/grype/db/v6\"\n\t\"github.com/anchore/grype/grype/db/v6/build/transformers\"\n\t\"github.com/anchore/grype/grype/db/v6/build/transformers/internal\"\n)\n\nfunc Transform(entry unmarshal.EPSS, state provider.State) ([]data.Entry, error) {\n\tdate := internal.ParseTime(entry.Date)\n\tif date == nil {\n\t\treturn nil, fmt.Errorf(\"failed to parse date: %q\", entry.Date)\n\t}\n\treturn transformers.NewEntries(*provider.Model(state), getEPSS(entry, *date)), nil\n}\n\nfunc getEPSS(entry unmarshal.EPSS, date time.Time) db.EpssHandle {\n\treturn db.EpssHandle{\n\t\tCve:        entry.CVE,\n\t\tEpss:       entry.EPSS,\n\t\tPercentile: entry.Percentile,\n\t\tDate:       date,\n\t}\n}\n"
  },
  {
    "path": "grype/db/v6/build/transformers/epss/transform_test.go",
    "content": "package epss\n\nimport (\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/grype/grype/db/internal/provider/unmarshal\"\n\t\"github.com/anchore/grype/grype/db/internal/testutil\"\n\t\"github.com/anchore/grype/grype/db/provider\"\n\tdb \"github.com/anchore/grype/grype/db/v6\"\n\t\"github.com/anchore/grype/grype/db/v6/build/transformers\"\n\t\"github.com/anchore/grype/grype/db/v6/build/transformers/internal\"\n)\n\nfunc TestTransform(t *testing.T) {\n\n\tvar timeVal = time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC)\n\tvar listing = provider.File{\n\t\tPath:      \"some\",\n\t\tDigest:    \"123456\",\n\t\tAlgorithm: \"sha256\",\n\t}\n\n\ttests := []struct {\n\t\tname string\n\t\twant []transformers.RelatedEntries\n\t}{\n\t\t{\n\t\t\tname: \"testdata/go-case.json\",\n\t\t\twant: []transformers.RelatedEntries{\n\t\t\t\t{\n\t\t\t\t\tProvider: &db.Provider{\n\t\t\t\t\t\tID:           \"epss\",\n\t\t\t\t\t\tVersion:      \"12\",\n\t\t\t\t\t\tProcessor:    \"vunnel@1.2.3\",\n\t\t\t\t\t\tDateCaptured: &timeVal,\n\t\t\t\t\t\tInputDigest:  \"sha256:123456\",\n\t\t\t\t\t},\n\t\t\t\t\tRelated: epssSlice(\n\t\t\t\t\t\tdb.EpssHandle{\n\t\t\t\t\t\t\tCve:        \"CVE-2025-0108\",\n\t\t\t\t\t\t\tEpss:       0.328,\n\t\t\t\t\t\t\tPercentile: 0.9929,\n\t\t\t\t\t\t\tDate:       *internal.ParseTime(\"2025-02-18\"),\n\t\t\t\t\t\t},\n\t\t\t\t\t),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tentries := loadFixture(t, test.name)\n\n\t\t\tvar actual []transformers.RelatedEntries\n\t\t\tfor _, vuln := range entries {\n\t\t\t\tentries, err := Transform(vuln, provider.State{\n\t\t\t\t\tProvider:  \"epss\",\n\t\t\t\t\tVersion:   12,\n\t\t\t\t\tProcessor: \"vunnel@1.2.3\",\n\t\t\t\t\tTimestamp: timeVal,\n\t\t\t\t\tListing:   &listing,\n\t\t\t\t})\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tfor _, entry := range entries {\n\t\t\t\t\te, ok := entry.Data.(transformers.RelatedEntries)\n\t\t\t\t\trequire.True(t, ok)\n\t\t\t\t\tactual = append(actual, e)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif diff := cmp.Diff(test.want, actual); diff != \"\" {\n\t\t\t\tt.Errorf(\"data entries mismatch (-want +got):\\n%s\", diff)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc epssSlice(a ...db.EpssHandle) []any {\n\tvar r []any\n\tfor _, v := range a {\n\t\tr = append(r, v)\n\t}\n\treturn r\n}\n\nfunc loadFixture(t *testing.T, fixturePath string) []unmarshal.EPSS {\n\tt.Helper()\n\n\tf, err := os.Open(fixturePath)\n\trequire.NoError(t, err)\n\tdefer testutil.CloseFile(f)\n\n\tentries, err := unmarshal.EPSSEntries(f)\n\trequire.NoError(t, err)\n\treturn entries\n}\n"
  },
  {
    "path": "grype/db/v6/build/transformers/github/testdata/GHSA-2wgc-48g2-cj5w.json",
    "content": "{\n  \"Vulnerability\": {},\n  \"Advisory\": {\n    \"Classification\": \"GENERAL\",\n    \"Severity\": \"Medium\",\n    \"CVSS\": {\n      \"version\": \"3.1\",\n      \"vector_string\": \"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:N\",\n      \"base_metrics\": {\n        \"base_score\": 6.5,\n        \"exploitability_score\": 3.9,\n        \"impact_score\": 2.5,\n        \"base_severity\": \"Medium\"\n      },\n      \"status\": \"N/A\"\n    },\n    \"FixedIn\": [\n      {\n        \"name\": \"vantage6\",\n        \"identifier\": \"4.2.0\",\n        \"ecosystem\": \"python\",\n        \"namespace\": \"github:python\",\n        \"range\": \"< 4.2.0\",\n        \"available\": {\n          \"date\": \"2024-01-30T15:00:00Z\",\n          \"kind\": \"advisory\"\n        }\n      }\n    ],\n    \"Summary\": \"vantage6 has insecure SSH configuration for node and server containers\",\n    \"url\": \"https://github.com/advisories/GHSA-2wgc-48g2-cj5w\",\n    \"CVE\": [\n      \"CVE-2024-21653\"\n    ],\n    \"Metadata\": {\n      \"CVE\": [\n        \"CVE-2024-21653\"\n      ]\n    },\n    \"ghsaId\": \"GHSA-2wgc-48g2-cj5w\",\n    \"published\": \"2024-01-30T20:56:46Z\",\n    \"updated\": \"2024-02-08T22:48:31Z\",\n    \"withdrawn\": null,\n    \"namespace\": \"github:python\"\n  }\n}"
  },
  {
    "path": "grype/db/v6/build/transformers/github/testdata/GHSA-3x74-v64j-qc3f.json",
    "content": "{\n  \"Vulnerability\": {},\n  \"Advisory\": {\n    \"Classification\": \"GENERAL\",\n    \"Severity\": \"HIGH\",\n    \"CVSS\": {\n      \"version\": \"3.1\",\n      \"vector_string\": \"CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H\",\n      \"base_metrics\": {\n        \"base_score\": 9.8,\n        \"exploitability_score\": null,\n        \"impact_score\": null,\n        \"base_severity\": \"HIGH\"\n      },\n      \"status\": \"N/A\"\n    },\n    \"FixedIn\": [\n      {\n        \"name\": \"craftcms/cms\",\n        \"identifier\": \"4.4.2\",\n        \"ecosystem\": \"Packagist\",\n        \"namespace\": \"github:Packagist\",\n        \"range\": \"< 4.4.2\"\n      }\n    ],\n    \"Summary\": \"Withdrawn Advisory: CraftCMS Server-Side Template Injection vulnerability\",\n    \"url\": \"https://github.com/advisories/GHSA-3x74-v64j-qc3f\",\n    \"CVE\": [\n      \"CVE-2023-30179\"\n    ],\n    \"Metadata\": {\n      \"CVE\": [\n        \"CVE-2023-30179\"\n      ]\n    },\n    \"ghsaId\": \"GHSA-3x74-v64j-qc3f\",\n    \"published\": \"2023-06-13T18:30:39Z\",\n    \"updated\": \"2024-03-21T17:48:19Z\",\n    \"withdrawn\": \"2023-06-28T23:54:39Z\",\n    \"namespace\": \"github:Packagist\"\n  }\n}\n"
  },
  {
    "path": "grype/db/v6/build/transformers/github/testdata/GHSA-92cp-5422-2mw7.json",
    "content": "{\n  \"Vulnerability\": {},\n  \"Advisory\": {\n    \"Classification\": \"GENERAL\",\n    \"Severity\": \"Low\",\n    \"CVSS\": {\n      \"version\": \"3.1\",\n      \"vector_string\": \"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:L/A:N\",\n      \"base_metrics\": {\n        \"base_score\": 3.7,\n        \"exploitability_score\": 2.2,\n        \"impact_score\": 1.4,\n        \"base_severity\": \"Low\"\n      },\n      \"status\": \"N/A\"\n    },\n    \"FixedIn\": [\n      {\n        \"name\": \"github.com/redis/go-redis/v9\",\n        \"identifier\": \"9.7.3\",\n        \"ecosystem\": \"go\",\n        \"namespace\": \"github:go\",\n        \"range\": \">= 9.7.0-beta.1 < 9.7.3\"\n      },\n      {\n        \"name\": \"github.com/redis/go-redis/v9\",\n        \"identifier\": \"9.6.3\",\n        \"ecosystem\": \"go\",\n        \"namespace\": \"github:go\",\n        \"range\": \">= 9.6.0b1 < 9.6.3\"\n      },\n      {\n        \"name\": \"github.com/redis/go-redis/v9\",\n        \"identifier\": \"9.5.5\",\n        \"ecosystem\": \"go\",\n        \"namespace\": \"github:go\",\n        \"range\": \">= 9.5.1 < 9.5.5\"\n      }\n    ],\n    \"Summary\": \"go-redis allows potential out of order responses when `CLIENT SETINFO` times out during connection establishment\",\n    \"url\": \"https://github.com/advisories/GHSA-92cp-5422-2mw7\",\n    \"CVE\": [\n      \"CVE-2025-29923\"\n    ],\n    \"Metadata\": {\n      \"CVE\": [\n        \"CVE-2025-29923\"\n      ]\n    },\n    \"ghsaId\": \"GHSA-92cp-5422-2mw7\",\n    \"published\": \"2025-03-20T18:49:59Z\",\n    \"updated\": \"2025-03-20T18:50:01Z\",\n    \"withdrawn\": null,\n    \"namespace\": \"github:go\"\n  }\n}\n"
  },
  {
    "path": "grype/db/v6/build/transformers/github/testdata/GHSA-qc55-vm3j-74gp.json",
    "content": "{\n  \"Vulnerability\": {},\n  \"Advisory\": {\n    \"Classification\": \"GENERAL\",\n    \"Severity\": \"High\",\n    \"CVSS\": {\n      \"version\": \"3.0\",\n      \"vector_string\": \"CVSS:3.0/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:N\",\n      \"base_metrics\": {\n        \"base_score\": 5.5,\n        \"exploitability_score\": 1.8,\n        \"impact_score\": 3.6,\n        \"base_severity\": \"Medium\"\n      },\n      \"status\": \"N/A\"\n    },\n    \"cvss_severities\": [\n      {\n        \"version\": \"3.0\",\n        \"vector\": \"CVSS:3.0/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:N\"\n      },\n      {\n        \"version\": \"4.0\",\n        \"vector\": \"CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:N/VI:H/VA:N/SC:N/SI:N/SA:N\"\n      }\n    ],\n    \"FixedIn\": [\n      {\n        \"name\": \"jsnapy\",\n        \"identifier\": \"1.3.0\",\n        \"ecosystem\": \"python\",\n        \"namespace\": \"github:python\",\n        \"range\": \"< 1.3.0\",\n        \"available\": {\n          \"date\": \"2020-07-28\",\n          \"kind\": \"first-observed\"\n        }\n      }\n    ],\n    \"Summary\": \"JSNAPy allows unprivileged local users to alter files under the directory\",\n    \"url\": \"https://github.com/advisories/GHSA-qc55-vm3j-74gp\",\n    \"CVE\": [\n      \"CVE-2018-0023\"\n    ],\n    \"Metadata\": {\n      \"CVE\": [\n        \"CVE-2018-0023\"\n      ]\n    },\n    \"ghsaId\": \"GHSA-qc55-vm3j-74gp\",\n    \"published\": \"2018-07-12T20:30:36Z\",\n    \"updated\": \"2024-09-24T21:02:13Z\",\n    \"withdrawn\": null,\n    \"references\": [\n      {\n        \"url\": \"https://nvd.nist.gov/vuln/detail/CVE-2018-0023\"\n      },\n      {\n        \"url\": \"https://github.com/advisories/GHSA-qc55-vm3j-74gp\"\n      },\n      {\n        \"url\": \"https://kb.juniper.net/JSA10856\"\n      },\n      {\n        \"url\": \"https://github.com/pypa/advisory-database/tree/main/vulns/jsnapy/PYSEC-2018-84.yaml\"\n      },\n      {\n        \"url\": \"https://web.archive.org/web/20200227125151/http://www.securityfocus.com/bid/103745\"\n      }\n    ],\n    \"namespace\": \"github:python\"\n  }\n}\n"
  },
  {
    "path": "grype/db/v6/build/transformers/github/testdata/github-github-npm-0.json",
    "content": "{\n    \"Advisory\": {\n      \"Classification\": \"GENERAL\",\n      \"Severity\": \"Critical\",\n      \"CVSS\": {\n        \"version\": \"3.1\",\n        \"vector_string\": \"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H\",\n        \"base_metrics\": {\n          \"base_score\": 9.8,\n          \"exploitability_score\": 3.9,\n          \"impact_score\": 5.9,\n          \"base_severity\": \"Critical\"\n        },\n        \"status\": \"N/A\"\n      },\n      \"FixedIn\": [\n        {\n          \"name\": \"scratch-vm\",\n          \"identifier\": \"0.2.0-prerelease.20200714185213\",\n          \"ecosystem\": \"npm\",\n          \"namespace\": \"github:npm\",\n          \"range\": \"<= 0.2.0-prerelease.20200709173451\"\n        }\n      ],\n      \"Summary\": \"Remote Code Execution in scratch-vm\",\n      \"url\": \"https://github.com/advisories/GHSA-vc9j-fhvv-8vrf\",\n      \"CVE\": [\n        \"CVE-2020-14000\"\n      ],\n      \"Metadata\": {\n        \"CVE\": [\n          \"CVE-2020-14000\"\n        ]\n      },\n      \"ghsaId\": \"GHSA-vc9j-fhvv-8vrf\",\n      \"published\": \"2020-07-27T19:55:52Z\",\n      \"updated\": \"2023-01-09T05:03:39Z\",\n      \"withdrawn\": null,\n      \"namespace\": \"github:npm\"\n    }\n}"
  },
  {
    "path": "grype/db/v6/build/transformers/github/testdata/github-github-python-0.json",
    "content": "[\n  {\n    \"Advisory\": {\n      \"CVE\": [\n        \"CVE-2018-8768\"\n      ],\n      \"FixedIn\": [\n        {\n          \"ecosystem\": \"python\",\n          \"identifier\": \"5.4.1\",\n          \"name\": \"notebook\",\n          \"namespace\": \"github:python\",\n          \"range\": \"< 5.4.1\"\n        }\n      ],\n      \"Metadata\": {\n        \"CVE\": [\n          \"CVE-2018-8768\"\n        ]\n      },\n      \"Severity\": \"Low\",\n      \"Summary\": \"Low severity vulnerability that affects notebook\",\n      \"ghsaId\": \"GHSA-6cwv-x26c-w2q4\",\n      \"namespace\": \"github:python\",\n      \"url\": \"https://github.com/advisories/GHSA-6cwv-x26c-w2q4\",\n      \"withdrawn\": null\n    },\n    \"Vulnerability\": {}\n  },\n  {\n    \"Advisory\": {\n      \"CVE\": [\n        \"CVE-2017-5524\"\n      ],\n      \"FixedIn\": [\n        {\n          \"ecosystem\": \"python\",\n          \"identifier\": \"4.3.12\",\n          \"name\": \"Plone\",\n          \"namespace\": \"github:python\",\n          \"range\": \">= 4.0 < 4.3.12\"\n        }\n      ],\n      \"Metadata\": {\n        \"CVE\": [\n          \"CVE-2017-5524\"\n        ]\n      },\n      \"Severity\": \"Medium\",\n      \"Summary\": \"Moderate severity vulnerability that affects Plone\",\n      \"ghsaId\": \"GHSA-p5wr-vp8g-q5p4\",\n      \"namespace\": \"github:python\",\n      \"url\": \"https://github.com/advisories/GHSA-p5wr-vp8g-q5p4\",\n      \"withdrawn\": null\n    },\n    \"Vulnerability\": {}\n  }\n]"
  },
  {
    "path": "grype/db/v6/build/transformers/github/testdata/github-withdrawn.json",
    "content": "\n{\n  \"Advisory\": {\n    \"CVE\": [\n      \"CVE-2018-8768\"\n    ],\n    \"FixedIn\": [\n      {\n        \"ecosystem\": \"python\",\n        \"identifier\": \"5.4.1\",\n        \"name\": \"notebook\",\n        \"namespace\": \"github:python\",\n        \"range\": \"< 5.4.1\"\n      }\n    ],\n    \"Metadata\": {\n      \"CVE\": [\n        \"CVE-2018-8768\"\n      ]\n    },\n    \"Severity\": \"Low\",\n    \"Summary\": \"Low severity vulnerability that affects notebook\",\n    \"ghsaId\": \"GHSA-6cwv-x26c-w2q4\",\n    \"namespace\": \"github:python\",\n    \"url\": \"https://github.com/advisories/GHSA-6cwv-x26c-w2q4\",\n    \"withdrawn\": \"2022-01-31T14:32:09Z\"\n  },\n  \"Vulnerability\": {}\n}\n"
  },
  {
    "path": "grype/db/v6/build/transformers/github/testdata/multiple-fixed-in-names.json",
    "content": "\n{\n  \"Advisory\": {\n    \"CVE\": [\n      \"CVE-2017-5524\"\n    ],\n    \"FixedIn\": [\n      {\n        \"ecosystem\": \"python\",\n        \"identifier\": \"4.3.12\",\n        \"name\": \"Plone\",\n        \"namespace\": \"github:python\",\n        \"range\": \">= 4.0 < 4.3.12\",\n        \"available\": {\n          \"date\": \"2017-05-20T10:30:45Z\",\n          \"kind\": \"release\"\n        }\n      },\n      {\n        \"ecosystem\": \"python\",\n        \"identifier\": \"5.1b1\",\n        \"name\": \"Plone\",\n        \"namespace\": \"github:python\",\n        \"range\": \">= 5.1a1 < 5.1b1\",\n        \"available\": {\n          \"date\": \"2017-06-15T14:22:33Z\",\n          \"kind\": \"commit\"\n        }\n      },\n      {\n        \"ecosystem\": \"python\",\n        \"identifier\": \"5.0.7\",\n        \"name\": \"Plone-debug\",\n        \"namespace\": \"github:python\",\n        \"range\": \">= 5.0rc1 < 5.0.7\"\n      }\n    ],\n    \"Metadata\": {\n      \"CVE\": [\n        \"CVE-2017-5524\"\n      ]\n    },\n    \"Severity\": \"Medium\",\n    \"Summary\": \"Moderate severity vulnerability that affects Plone\",\n    \"ghsaId\": \"GHSA-p5wr-vp8g-q5p4\",\n    \"namespace\": \"github:python\",\n    \"url\": \"https://github.com/advisories/GHSA-p5wr-vp8g-q5p4\",\n    \"withdrawn\": null\n  },\n  \"Vulnerability\": {}\n}\n"
  },
  {
    "path": "grype/db/v6/build/transformers/github/transform.go",
    "content": "package github\n\nimport (\n\t\"sort\"\n\t\"strings\"\n\n\t\"github.com/anchore/grype/grype/db/data\"\n\t\"github.com/anchore/grype/grype/db/internal/provider/unmarshal\"\n\t\"github.com/anchore/grype/grype/db/internal/versionutil\"\n\t\"github.com/anchore/grype/grype/db/provider\"\n\tdb \"github.com/anchore/grype/grype/db/v6\"\n\t\"github.com/anchore/grype/grype/db/v6/build/transformers\"\n\t\"github.com/anchore/grype/grype/db/v6/build/transformers/internal\"\n\t\"github.com/anchore/grype/grype/db/v6/name\"\n\t\"github.com/anchore/grype/grype/version\"\n\t\"github.com/anchore/grype/internal/log\"\n\t\"github.com/anchore/syft/syft/pkg\"\n)\n\nfunc Transform(vulnerability unmarshal.GitHubAdvisory, state provider.State) ([]data.Entry, error) {\n\tins := []any{\n\t\tgetVulnerability(vulnerability, state),\n\t}\n\n\tfor _, a := range getAffectedPackage(vulnerability) {\n\t\tins = append(ins, a)\n\t}\n\n\treturn transformers.NewEntries(ins...), nil\n}\n\nfunc getVulnerability(vuln unmarshal.GitHubAdvisory, state provider.State) db.VulnerabilityHandle {\n\treturn db.VulnerabilityHandle{\n\t\tName:          vuln.Advisory.GhsaID,\n\t\tProviderID:    state.Provider,\n\t\tProvider:      provider.Model(state),\n\t\tModifiedDate:  internal.ParseTime(vuln.Advisory.Updated),\n\t\tPublishedDate: internal.ParseTime(vuln.Advisory.Published),\n\t\tWithdrawnDate: internal.ParseTime(vuln.Advisory.Withdrawn),\n\t\tStatus:        getVulnStatus(vuln),\n\t\tBlobValue: &db.VulnerabilityBlob{\n\t\t\tID: vuln.Advisory.GhsaID,\n\t\t\t// it does not appear to be possible to get \"credits\" or any user information from the graphql API\n\t\t\t// for security advisories (see https://docs.github.com/en/graphql/reference/queries#securityadvisories),\n\t\t\t// thus assigner is left empty.\n\t\t\tAssigners:   nil,\n\t\t\tDescription: strings.TrimSpace(vuln.Advisory.Summary),\n\t\t\tReferences:  getReferences(vuln),\n\t\t\tAliases:     getAliases(vuln),\n\t\t\tSeverities:  getSeverities(vuln),\n\t\t},\n\t}\n}\n\nfunc getVulnStatus(vuln unmarshal.GitHubAdvisory) db.VulnerabilityStatus {\n\tif vuln.Advisory.Withdrawn == \"\" {\n\t\treturn db.VulnerabilityActive\n\t}\n\n\treturn db.VulnerabilityRejected\n}\n\nfunc getAffectedPackage(vuln unmarshal.GitHubAdvisory) []db.AffectedPackageHandle {\n\tvar afs []db.AffectedPackageHandle\n\tgroups := groupFixedIns(vuln)\n\thasRangeErr := false\n\tfor group, fixedIns := range groups {\n\t\tfor _, fixedInEntry := range fixedIns {\n\t\t\tranges, rangeErr := getRanges(fixedInEntry)\n\t\t\tif rangeErr != nil {\n\t\t\t\thasRangeErr = true\n\t\t\t}\n\t\t\tafs = append(afs, db.AffectedPackageHandle{\n\t\t\t\tPackage: getPackage(group),\n\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\tCVEs:   getAliases(vuln),\n\t\t\t\t\tRanges: ranges,\n\t\t\t\t},\n\t\t\t})\n\t\t}\n\t}\n\n\t// stable ordering\n\tsort.Sort(internal.ByAffectedPackage(afs))\n\n\tif hasRangeErr {\n\t\tlog.Warnf(\"for %s falling back to fuzzy matching on at least one constraint range\", vuln.Advisory.GhsaID)\n\t}\n\treturn afs\n}\n\nfunc getRanges(fixedInEntry unmarshal.GithubFixedIn) ([]db.Range, error) {\n\tfixedVersion := db.Version{\n\t\tType:       getAffectedVersionFormat(fixedInEntry),\n\t\tConstraint: versionutil.EnforceSemVerConstraint(fixedInEntry.Range),\n\t}\n\terr := validateAffectedVersion(fixedVersion)\n\tif err != nil {\n\t\tlog.Warnf(\"failed to validate affected version: %v\", err)\n\t\tfixedVersion.Type = version.UnknownFormat.String()\n\t}\n\treturn []db.Range{\n\t\t{\n\t\t\tVersion: fixedVersion,\n\t\t\tFix:     getFix(fixedInEntry),\n\t\t},\n\t}, err\n}\n\nfunc validateAffectedVersion(v db.Version) error {\n\tversionFormat := version.ParseFormat(v.Type)\n\tc, err := version.GetConstraint(v.Constraint, versionFormat)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// ensure we can use this version format in a comparison\n\tver := version.New(\"1.0.0\", versionFormat)\n\tif err := ver.Validate(); err != nil {\n\t\t// don't have a good example to use here\n\t\t// TODO: we should consider finding a better way to do this without having to create a valid version for comparison\n\t\treturn nil\n\t}\n\n\t_, err = c.Satisfied(ver)\n\n\treturn err\n}\n\nfunc getAffectedVersionFormat(fixedInEntry unmarshal.GithubFixedIn) string {\n\tversionFormat := strings.ToLower(fixedInEntry.Ecosystem)\n\n\tif versionFormat == \"pip\" {\n\t\tversionFormat = \"python\"\n\t}\n\n\treturn versionFormat\n}\n\nfunc getFix(fixedInEntry unmarshal.GithubFixedIn) *db.Fix {\n\tfixedInVersion := versionutil.CleanFixedInVersion(fixedInEntry.Identifier)\n\n\tfixState := db.NotFixedStatus\n\tif len(fixedInVersion) > 0 {\n\t\tfixState = db.FixedStatus\n\t}\n\n\tvar detail *db.FixDetail\n\tavailability := getFixAvailability(fixedInEntry)\n\tif availability != nil {\n\t\tdetail = &db.FixDetail{\n\t\t\tAvailable: availability,\n\t\t}\n\t}\n\n\treturn &db.Fix{\n\t\tVersion: fixedInVersion,\n\t\tState:   fixState,\n\t\tDetail:  detail,\n\t}\n}\n\nfunc getFixAvailability(fixedInEntry unmarshal.GithubFixedIn) *db.FixAvailability {\n\tif fixedInEntry.Available.Date == \"\" {\n\t\treturn nil\n\t}\n\n\tt := internal.ParseTime(fixedInEntry.Available.Date)\n\tif t == nil {\n\t\tlog.WithFields(\"date\", fixedInEntry.Available.Date).Warn(\"unable to parse fix availability date\")\n\t\treturn nil\n\t}\n\n\treturn &db.FixAvailability{\n\t\tDate: t,\n\t\tKind: fixedInEntry.Available.Kind,\n\t}\n}\n\ntype groupIndex struct {\n\tname      string\n\tecosystem string\n}\n\nfunc groupFixedIns(vuln unmarshal.GitHubAdvisory) map[groupIndex][]unmarshal.GithubFixedIn {\n\tgrouped := make(map[groupIndex][]unmarshal.GithubFixedIn)\n\n\tfor _, fixedIn := range vuln.Advisory.FixedIn {\n\t\tg := groupIndex{\n\t\t\tname:      fixedIn.Name,\n\t\t\tecosystem: fixedIn.Ecosystem,\n\t\t}\n\n\t\tgrouped[g] = append(grouped[g], fixedIn)\n\t}\n\treturn grouped\n}\n\nfunc getPackageType(ecosystem string) pkg.Type {\n\tecosystem = strings.ToLower(ecosystem)\n\tswitch ecosystem {\n\tcase \"composer\":\n\t\treturn pkg.PhpComposerPkg\n\tcase \"rust\", \"cargo\":\n\t\treturn pkg.RustPkg\n\tcase \"dart\":\n\t\treturn pkg.DartPubPkg\n\tcase \"nuget\", \".net\":\n\t\treturn pkg.DotnetPkg\n\tcase \"go\", \"golang\":\n\t\treturn pkg.GoModulePkg\n\tcase \"maven\", \"java\":\n\t\treturn pkg.JavaPkg\n\tcase \"npm\":\n\t\treturn pkg.NpmPkg\n\tcase \"pypi\", \"python\", \"pip\":\n\t\treturn pkg.PythonPkg\n\tcase \"swift\":\n\t\treturn pkg.SwiftPkg\n\tcase \"rubygems\", \"ruby\", \"gem\":\n\t\treturn pkg.GemPkg\n\tcase \"erlang\", \"hex\", \"elixir\":\n\t\treturn pkg.HexPkg\n\tcase \"apk\":\n\t\treturn pkg.ApkPkg\n\tcase \"rpm\":\n\t\treturn pkg.RpmPkg\n\tcase \"deb\":\n\t\treturn pkg.DebPkg\n\tcase \"github-action\":\n\t\treturn pkg.GithubActionPkg\n\t}\n\tty := pkg.TypeByName(ecosystem)\n\tif ty != pkg.UnknownPkg {\n\t\treturn ty\n\t}\n\n\tlog.Warnf(\"using unknown ecosystem intead of syft pkg type (this will probably cause issues when matching): %q\", ecosystem)\n\n\treturn pkg.Type(ecosystem)\n}\n\nfunc getPackage(group groupIndex) *db.Package {\n\tt := getPackageType(group.ecosystem)\n\treturn &db.Package{\n\t\tName:      name.Normalize(group.name, t),\n\t\tEcosystem: string(t),\n\t}\n}\n\nfunc getSeverities(vulnerability unmarshal.GitHubAdvisory) []db.Severity {\n\tvar severities []db.Severity\n\n\t// the string severity and CVSS is not necessarily correlated (nor is CVSS guaranteed to be provided\n\t// at all... see https://github.com/advisories/GHSA-xwg4-93c6-3h42 for example), so we need to keep them separate\n\tcleanSeverity := strings.ToLower(strings.TrimSpace(vulnerability.Advisory.Severity))\n\n\tif cleanSeverity != \"\" {\n\t\tseverities = append(severities, db.Severity{\n\t\t\t// This is the string severity based off of CVSS v3\n\t\t\t// see https://docs.github.com/en/code-security/security-advisories/working-with-global-security-advisories-from-the-github-advisory-database/about-the-github-advisory-database?learn=security_advisories&learnProduct=code-security#about-cvss-levels\n\t\t\tScheme: db.SeveritySchemeCHML,\n\t\t\tValue:  cleanSeverity,\n\t\t})\n\t}\n\n\t// If the new CVSSSeverities field isn't populated, fallback to the old CVSS property\n\tif len(vulnerability.Advisory.CVSSSeverities) == 0 && vulnerability.Advisory.CVSS != nil {\n\t\tseverities = append(severities, db.Severity{\n\t\t\tScheme: db.SeveritySchemeCVSS,\n\t\t\tValue: db.CVSSSeverity{\n\t\t\t\tVector:  vulnerability.Advisory.CVSS.VectorString,\n\t\t\t\tVersion: vulnerability.Advisory.CVSS.Version,\n\t\t\t},\n\t\t})\n\t} else {\n\t\tfor _, cvss := range vulnerability.Advisory.CVSSSeverities {\n\t\t\tseverities = append(severities, db.Severity{\n\t\t\t\tScheme: db.SeveritySchemeCVSS,\n\t\t\t\tValue: db.CVSSSeverity{\n\t\t\t\t\tVector:  cvss.Vector,\n\t\t\t\t\tVersion: cvss.Version,\n\t\t\t\t},\n\t\t\t})\n\t\t}\n\t}\n\n\treturn severities\n}\n\nfunc getAliases(vulnerability unmarshal.GitHubAdvisory) (aliases []string) {\n\taliases = append(aliases, vulnerability.Advisory.CVE...)\n\treturn\n}\n\nfunc getReferences(vulnerability unmarshal.GitHubAdvisory) []db.Reference {\n\t// Capture the GitHub Advisory URL as the first reference\n\trefs := []db.Reference{\n\t\t{\n\t\t\tURL: vulnerability.Advisory.URL,\n\t\t},\n\t}\n\n\tfor _, reference := range vulnerability.Advisory.References {\n\t\tclean := strings.TrimSpace(reference.URL)\n\t\tif clean == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\t// TODO there is other info we could be capturing too (source)\n\t\trefs = append(refs, db.Reference{\n\t\t\tURL: clean,\n\t\t})\n\t}\n\n\treturn transformers.DeduplicateReferences(refs)\n}\n"
  },
  {
    "path": "grype/db/v6/build/transformers/github/transform_test.go",
    "content": "package github\n\nimport (\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/grype/grype/db/internal/provider/unmarshal\"\n\t\"github.com/anchore/grype/grype/db/provider\"\n\tdb \"github.com/anchore/grype/grype/db/v6\"\n\t\"github.com/anchore/grype/grype/db/v6/build/transformers\"\n\t\"github.com/anchore/grype/grype/db/v6/build/transformers/internal\"\n\t\"github.com/anchore/syft/syft/pkg\"\n)\n\nfunc TestTransform(t *testing.T) {\n\ttype counts struct {\n\t\tproviderCount        int\n\t\tvulnerabilityCount   int\n\t\taffectedPackageCount int\n\t}\n\n\ttests := []struct {\n\t\tname       string\n\t\tfixture    string\n\t\tstate      provider.State\n\t\twantCounts counts\n\t}{\n\t\t{\n\t\t\tname:    \"multiple fixed versions for Plone\",\n\t\t\tfixture: \"testdata/multiple-fixed-in-names.json\",\n\t\t\tstate: provider.State{\n\t\t\t\tProvider:  \"github\",\n\t\t\t\tVersion:   1,\n\t\t\t\tTimestamp: time.Date(2024, 03, 01, 12, 0, 0, 0, time.UTC),\n\t\t\t},\n\t\t\twantCounts: counts{\n\t\t\t\tproviderCount:        1,\n\t\t\t\tvulnerabilityCount:   1,\n\t\t\t\taffectedPackageCount: 3,\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tadvisories := loadFixture(t, tt.fixture)\n\t\t\trequire.Len(t, advisories, 1, \"expected exactly one advisory\")\n\t\t\tadvisory := advisories[0]\n\n\t\t\tentries, err := Transform(advisory, tt.state)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Len(t, entries, 1, \"expected exactly one data.Entry\")\n\n\t\t\tentry := entries[0]\n\t\t\trequire.NotNil(t, entry.Data)\n\n\t\t\tdata, ok := entry.Data.(transformers.RelatedEntries)\n\t\t\trequire.True(t, ok, \"expected entry.Data to be of type RelatedEntries\")\n\n\t\t\trequire.NotNil(t, data.VulnerabilityHandle, \"expected a VulnerabilityHandle\")\n\t\t\trequire.Equal(t, tt.wantCounts.vulnerabilityCount, 1)\n\n\t\t\trequire.Len(t, data.Related, tt.wantCounts.affectedPackageCount, \"unexpected number of related entries\")\n\t\t})\n\t}\n}\n\nfunc TestGetVulnerability(t *testing.T) {\n\tnow := time.Date(2024, 03, 01, 12, 0, 0, 0, time.UTC)\n\ttests := []struct {\n\t\tname     string\n\t\texpected []db.VulnerabilityHandle\n\t}{\n\t\t{\n\t\t\tname: \"testdata/GHSA-2wgc-48g2-cj5w.json\",\n\t\t\texpected: []db.VulnerabilityHandle{\n\t\t\t\t{\n\t\t\t\t\tName:       \"GHSA-2wgc-48g2-cj5w\",\n\t\t\t\t\tProviderID: \"github\",\n\t\t\t\t\tProvider: &db.Provider{\n\t\t\t\t\t\tID:           \"github\",\n\t\t\t\t\t\tVersion:      \"1\",\n\t\t\t\t\t\tDateCaptured: &now,\n\t\t\t\t\t},\n\t\t\t\t\tModifiedDate:  internal.ParseTime(\"2024-02-08T22:48:31Z\"),\n\t\t\t\t\tPublishedDate: internal.ParseTime(\"2024-01-30T20:56:46Z\"),\n\t\t\t\t\tWithdrawnDate: nil,\n\t\t\t\t\tStatus:        db.VulnerabilityActive,\n\t\t\t\t\tBlobValue: &db.VulnerabilityBlob{\n\t\t\t\t\t\tID:          \"GHSA-2wgc-48g2-cj5w\",\n\t\t\t\t\t\tDescription: \"vantage6 has insecure SSH configuration for node and server containers\",\n\t\t\t\t\t\tReferences: []db.Reference{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tURL: \"https://github.com/advisories/GHSA-2wgc-48g2-cj5w\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tAliases: []string{\"CVE-2024-21653\"},\n\t\t\t\t\t\tSeverities: []db.Severity{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tScheme: db.SeveritySchemeCHML,\n\t\t\t\t\t\t\t\tValue:  \"medium\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tScheme: db.SeveritySchemeCVSS,\n\t\t\t\t\t\t\t\tValue: db.CVSSSeverity{\n\t\t\t\t\t\t\t\t\tVector:  \"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:N\",\n\t\t\t\t\t\t\t\t\tVersion: \"3.1\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"testdata/GHSA-3x74-v64j-qc3f.json\",\n\t\t\texpected: []db.VulnerabilityHandle{\n\t\t\t\t{\n\t\t\t\t\tName:       \"GHSA-3x74-v64j-qc3f\",\n\t\t\t\t\tProviderID: \"github\",\n\t\t\t\t\tProvider: &db.Provider{\n\t\t\t\t\t\tID:           \"github\",\n\t\t\t\t\t\tVersion:      \"1\",\n\t\t\t\t\t\tDateCaptured: &now,\n\t\t\t\t\t},\n\t\t\t\t\tModifiedDate:  internal.ParseTime(\"2024-03-21T17:48:19Z\"),\n\t\t\t\t\tPublishedDate: internal.ParseTime(\"2023-06-13T18:30:39Z\"),\n\t\t\t\t\tWithdrawnDate: internal.ParseTime(\"2023-06-28T23:54:39Z\"),\n\t\t\t\t\tStatus:        db.VulnerabilityRejected,\n\t\t\t\t\tBlobValue: &db.VulnerabilityBlob{\n\t\t\t\t\t\tID:          \"GHSA-3x74-v64j-qc3f\",\n\t\t\t\t\t\tDescription: \"Withdrawn Advisory: CraftCMS Server-Side Template Injection vulnerability\",\n\t\t\t\t\t\tReferences: []db.Reference{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tURL: \"https://github.com/advisories/GHSA-3x74-v64j-qc3f\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tAliases: []string{\"CVE-2023-30179\"},\n\t\t\t\t\t\tSeverities: []db.Severity{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tScheme: db.SeveritySchemeCHML,\n\t\t\t\t\t\t\t\tValue:  \"high\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tScheme: db.SeveritySchemeCVSS,\n\t\t\t\t\t\t\t\tValue: db.CVSSSeverity{\n\t\t\t\t\t\t\t\t\tVector:  \"CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H\",\n\t\t\t\t\t\t\t\t\tVersion: \"3.1\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"testdata/github-github-npm-0.json\",\n\t\t\texpected: []db.VulnerabilityHandle{\n\t\t\t\t{\n\t\t\t\t\tName:       \"GHSA-vc9j-fhvv-8vrf\",\n\t\t\t\t\tProviderID: \"github\",\n\t\t\t\t\tProvider: &db.Provider{\n\t\t\t\t\t\tID:           \"github\",\n\t\t\t\t\t\tVersion:      \"1\",\n\t\t\t\t\t\tDateCaptured: &now,\n\t\t\t\t\t},\n\t\t\t\t\tModifiedDate:  internal.ParseTime(\"2023-01-09T05:03:39Z\"),\n\t\t\t\t\tPublishedDate: internal.ParseTime(\"2020-07-27T19:55:52Z\"),\n\t\t\t\t\tWithdrawnDate: nil,\n\t\t\t\t\tStatus:        db.VulnerabilityActive,\n\t\t\t\t\tBlobValue: &db.VulnerabilityBlob{\n\t\t\t\t\t\tID:          \"GHSA-vc9j-fhvv-8vrf\",\n\t\t\t\t\t\tDescription: \"Remote Code Execution in scratch-vm\",\n\t\t\t\t\t\tReferences: []db.Reference{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tURL: \"https://github.com/advisories/GHSA-vc9j-fhvv-8vrf\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tAliases: []string{\"CVE-2020-14000\"},\n\t\t\t\t\t\tSeverities: []db.Severity{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tScheme: db.SeveritySchemeCHML,\n\t\t\t\t\t\t\t\tValue:  \"critical\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tScheme: db.SeveritySchemeCVSS,\n\t\t\t\t\t\t\t\tValue: db.CVSSSeverity{\n\t\t\t\t\t\t\t\t\tVector:  \"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H\",\n\t\t\t\t\t\t\t\t\tVersion: \"3.1\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"testdata/github-github-python-0.json\",\n\t\t\texpected: []db.VulnerabilityHandle{\n\t\t\t\t{\n\t\t\t\t\tName:       \"GHSA-6cwv-x26c-w2q4\",\n\t\t\t\t\tProviderID: \"github\",\n\t\t\t\t\tProvider: &db.Provider{\n\t\t\t\t\t\tID:           \"github\",\n\t\t\t\t\t\tVersion:      \"1\",\n\t\t\t\t\t\tDateCaptured: &now,\n\t\t\t\t\t},\n\t\t\t\t\tStatus: \"active\",\n\t\t\t\t\tBlobValue: &db.VulnerabilityBlob{\n\t\t\t\t\t\tID:          \"GHSA-6cwv-x26c-w2q4\",\n\t\t\t\t\t\tDescription: \"Low severity vulnerability that affects notebook\",\n\t\t\t\t\t\tReferences: []db.Reference{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tURL: \"https://github.com/advisories/GHSA-6cwv-x26c-w2q4\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\n\t\t\t\t\t\tAliases: []string{\"CVE-2018-8768\"},\n\n\t\t\t\t\t\tSeverities: []db.Severity{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tScheme: db.SeveritySchemeCHML,\n\t\t\t\t\t\t\t\tValue:  \"low\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tName:       \"GHSA-p5wr-vp8g-q5p4\",\n\t\t\t\t\tProviderID: \"github\",\n\t\t\t\t\tProvider: &db.Provider{\n\t\t\t\t\t\tID:           \"github\",\n\t\t\t\t\t\tVersion:      \"1\",\n\t\t\t\t\t\tDateCaptured: &now,\n\t\t\t\t\t},\n\t\t\t\t\tStatus: \"active\",\n\t\t\t\t\tBlobValue: &db.VulnerabilityBlob{\n\t\t\t\t\t\tID:          \"GHSA-p5wr-vp8g-q5p4\",\n\t\t\t\t\t\tDescription: \"Moderate severity vulnerability that affects Plone\",\n\t\t\t\t\t\tReferences: []db.Reference{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tURL: \"https://github.com/advisories/GHSA-p5wr-vp8g-q5p4\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tAliases: []string{\"CVE-2017-5524\"},\n\t\t\t\t\t\tSeverities: []db.Severity{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tScheme: db.SeveritySchemeCHML,\n\t\t\t\t\t\t\t\tValue:  \"medium\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"testdata/github-withdrawn.json\",\n\t\t\texpected: []db.VulnerabilityHandle{\n\t\t\t\t{\n\t\t\t\t\tName:       \"GHSA-6cwv-x26c-w2q4\",\n\t\t\t\t\tProviderID: \"github\",\n\t\t\t\t\tProvider: &db.Provider{\n\t\t\t\t\t\tID:           \"github\",\n\t\t\t\t\t\tVersion:      \"1\",\n\t\t\t\t\t\tDateCaptured: &now,\n\t\t\t\t\t},\n\t\t\t\t\tModifiedDate:  nil,\n\t\t\t\t\tPublishedDate: nil,\n\t\t\t\t\tWithdrawnDate: internal.ParseTime(\"2022-01-31T14:32:09Z\"),\n\t\t\t\t\tStatus:        db.VulnerabilityRejected,\n\t\t\t\t\tBlobValue: &db.VulnerabilityBlob{\n\t\t\t\t\t\tID:          \"GHSA-6cwv-x26c-w2q4\",\n\t\t\t\t\t\tDescription: \"Low severity vulnerability that affects notebook\",\n\t\t\t\t\t\tReferences: []db.Reference{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tURL: \"https://github.com/advisories/GHSA-6cwv-x26c-w2q4\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tAliases: []string{\"CVE-2018-8768\"},\n\t\t\t\t\t\tSeverities: []db.Severity{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tScheme: db.SeveritySchemeCHML,\n\t\t\t\t\t\t\t\tValue:  \"low\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"testdata/multiple-fixed-in-names.json\",\n\t\t\texpected: []db.VulnerabilityHandle{\n\t\t\t\t{\n\t\t\t\t\tName:       \"GHSA-p5wr-vp8g-q5p4\",\n\t\t\t\t\tProviderID: \"github\",\n\t\t\t\t\tProvider: &db.Provider{\n\t\t\t\t\t\tID:           \"github\",\n\t\t\t\t\t\tVersion:      \"1\",\n\t\t\t\t\t\tDateCaptured: &now,\n\t\t\t\t\t},\n\t\t\t\t\tStatus: db.VulnerabilityActive,\n\t\t\t\t\tBlobValue: &db.VulnerabilityBlob{\n\t\t\t\t\t\tID:          \"GHSA-p5wr-vp8g-q5p4\",\n\t\t\t\t\t\tDescription: \"Moderate severity vulnerability that affects Plone\",\n\t\t\t\t\t\tReferences: []db.Reference{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tURL: \"https://github.com/advisories/GHSA-p5wr-vp8g-q5p4\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tAliases: []string{\"CVE-2017-5524\"},\n\t\t\t\t\t\tSeverities: []db.Severity{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tScheme: db.SeveritySchemeCHML,\n\t\t\t\t\t\t\t\tValue:  \"medium\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"testdata/GHSA-qc55-vm3j-74gp.json\",\n\t\t\texpected: []db.VulnerabilityHandle{\n\t\t\t\t{\n\t\t\t\t\tName:       \"GHSA-qc55-vm3j-74gp\",\n\t\t\t\t\tProviderID: \"github\",\n\t\t\t\t\tProvider: &db.Provider{\n\t\t\t\t\t\tID:           \"github\",\n\t\t\t\t\t\tVersion:      \"1\",\n\t\t\t\t\t\tDateCaptured: &now,\n\t\t\t\t\t},\n\t\t\t\t\tModifiedDate:  internal.ParseTime(\"2024-09-24T21:02:13Z\"),\n\t\t\t\t\tPublishedDate: internal.ParseTime(\"2018-07-12T20:30:36Z\"),\n\t\t\t\t\tWithdrawnDate: nil,\n\t\t\t\t\tStatus:        db.VulnerabilityActive,\n\t\t\t\t\tBlobValue: &db.VulnerabilityBlob{\n\t\t\t\t\t\tID:          \"GHSA-qc55-vm3j-74gp\",\n\t\t\t\t\t\tDescription: \"JSNAPy allows unprivileged local users to alter files under the directory\",\n\t\t\t\t\t\tReferences: []db.Reference{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tURL: \"https://github.com/advisories/GHSA-qc55-vm3j-74gp\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tURL: \"https://nvd.nist.gov/vuln/detail/CVE-2018-0023\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tURL: \"https://kb.juniper.net/JSA10856\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tURL: \"https://github.com/pypa/advisory-database/tree/main/vulns/jsnapy/PYSEC-2018-84.yaml\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tURL: \"https://web.archive.org/web/20200227125151/http://www.securityfocus.com/bid/103745\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tAliases: []string{\"CVE-2018-0023\"},\n\t\t\t\t\t\tSeverities: []db.Severity{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tScheme: db.SeveritySchemeCHML,\n\t\t\t\t\t\t\t\tValue:  \"high\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tScheme: db.SeveritySchemeCVSS,\n\t\t\t\t\t\t\t\tValue: db.CVSSSeverity{\n\t\t\t\t\t\t\t\t\tVector:  \"CVSS:3.0/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:N\",\n\t\t\t\t\t\t\t\t\tVersion: \"3.0\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tScheme: db.SeveritySchemeCVSS,\n\t\t\t\t\t\t\t\tValue: db.CVSSSeverity{\n\t\t\t\t\t\t\t\t\tVector:  \"CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:N/VI:H/VA:N/SC:N/SI:N/SA:N\",\n\t\t\t\t\t\t\t\t\tVersion: \"4.0\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tadvisories := loadFixture(t, tt.name)\n\t\t\tvar results []db.VulnerabilityHandle\n\n\t\t\tfor _, advisory := range advisories {\n\t\t\t\tresult := getVulnerability(advisory, provider.State{Provider: \"github\", Version: 1, Timestamp: now})\n\t\t\t\tresults = append(results, result)\n\t\t\t}\n\t\t\tif d := cmp.Diff(tt.expected, results); d != \"\" {\n\t\t\t\tt.Fatalf(\"unexpected result: %s\", d)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestGetAffectedPackage(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\texpected []db.AffectedPackageHandle\n\t}{\n\t\t{\n\t\t\tname: \"testdata/GHSA-2wgc-48g2-cj5w.json\",\n\t\t\texpected: []db.AffectedPackageHandle{\n\t\t\t\t{\n\t\t\t\t\tPackage: &db.Package{\n\t\t\t\t\t\tName:      \"vantage6\",\n\t\t\t\t\t\tEcosystem: \"python\",\n\t\t\t\t\t},\n\t\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\t\tCVEs: []string{\"CVE-2024-21653\"},\n\t\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tVersion: db.Version{\n\t\t\t\t\t\t\t\t\tType:       \"python\",\n\t\t\t\t\t\t\t\t\tConstraint: \"<4.2.0\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tFix: &db.Fix{\n\t\t\t\t\t\t\t\t\tVersion: \"4.2.0\",\n\t\t\t\t\t\t\t\t\tState:   db.FixedStatus,\n\t\t\t\t\t\t\t\t\tDetail: &db.FixDetail{\n\t\t\t\t\t\t\t\t\t\tAvailable: &db.FixAvailability{\n\t\t\t\t\t\t\t\t\t\t\tDate: internal.ParseTime(\"2024-01-30T15:00:00Z\"),\n\t\t\t\t\t\t\t\t\t\t\tKind: \"advisory\",\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"testdata/GHSA-3x74-v64j-qc3f.json\",\n\t\t\texpected: []db.AffectedPackageHandle{\n\t\t\t\t{\n\t\t\t\t\tPackage: &db.Package{\n\t\t\t\t\t\tName:      \"craftcms/cms\",\n\t\t\t\t\t\tEcosystem: \"packagist\",\n\t\t\t\t\t},\n\t\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\t\tCVEs: []string{\"CVE-2023-30179\"},\n\t\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tVersion: db.Version{\n\t\t\t\t\t\t\t\t\tType:       \"packagist\",\n\t\t\t\t\t\t\t\t\tConstraint: \"<4.4.2\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tFix: &db.Fix{\n\t\t\t\t\t\t\t\t\tVersion: \"4.4.2\",\n\t\t\t\t\t\t\t\t\tState:   db.FixedStatus,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"testdata/github-github-npm-0.json\",\n\t\t\texpected: []db.AffectedPackageHandle{\n\t\t\t\t{\n\t\t\t\t\tPackage: &db.Package{\n\t\t\t\t\t\tName:      \"scratch-vm\",\n\t\t\t\t\t\tEcosystem: \"npm\",\n\t\t\t\t\t},\n\t\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\t\tCVEs: []string{\"CVE-2020-14000\"},\n\t\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tVersion: db.Version{\n\t\t\t\t\t\t\t\t\tType:       \"npm\",\n\t\t\t\t\t\t\t\t\tConstraint: \"<=0.2.0-prerelease.20200709173451\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tFix: &db.Fix{\n\t\t\t\t\t\t\t\t\tVersion: \"0.2.0-prerelease.20200714185213\",\n\t\t\t\t\t\t\t\t\tState:   db.FixedStatus,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"testdata/github-github-python-0.json\",\n\t\t\texpected: []db.AffectedPackageHandle{\n\t\t\t\t{\n\t\t\t\t\tPackage: &db.Package{\n\t\t\t\t\t\tEcosystem: \"python\",\n\t\t\t\t\t\tName:      \"notebook\",\n\t\t\t\t\t},\n\t\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\t\tCVEs:       []string{\"CVE-2018-8768\"},\n\t\t\t\t\t\tQualifiers: nil,\n\t\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tVersion: db.Version{Type: \"python\", Constraint: \"<5.4.1\"},\n\t\t\t\t\t\t\t\tFix:     &db.Fix{Version: \"5.4.1\", State: db.FixedStatus},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tPackage: &db.Package{\n\t\t\t\t\t\tEcosystem: \"python\",\n\t\t\t\t\t\tName:      \"Plone\",\n\t\t\t\t\t},\n\t\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\t\tCVEs: []string{\"CVE-2017-5524\"},\n\t\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tVersion: db.Version{Type: \"python\", Constraint: \">=4.0,<4.3.12\"},\n\t\t\t\t\t\t\t\tFix:     &db.Fix{Version: \"4.3.12\", State: db.FixedStatus},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"testdata/multiple-fixed-in-names.json\",\n\t\t\texpected: []db.AffectedPackageHandle{\n\t\t\t\t{\n\t\t\t\t\tPackage: &db.Package{\n\t\t\t\t\t\tName:      \"Plone\",\n\t\t\t\t\t\tEcosystem: \"python\",\n\t\t\t\t\t},\n\t\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\t\tCVEs: []string{\"CVE-2017-5524\"},\n\t\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tVersion: db.Version{\n\t\t\t\t\t\t\t\t\tType:       \"python\",\n\t\t\t\t\t\t\t\t\tConstraint: \">=4.0,<4.3.12\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tFix: &db.Fix{\n\t\t\t\t\t\t\t\t\tVersion: \"4.3.12\",\n\t\t\t\t\t\t\t\t\tState:   db.FixedStatus,\n\t\t\t\t\t\t\t\t\tDetail: &db.FixDetail{\n\t\t\t\t\t\t\t\t\t\tAvailable: &db.FixAvailability{\n\t\t\t\t\t\t\t\t\t\t\tDate: internal.ParseTime(\"2017-05-20T10:30:45Z\"),\n\t\t\t\t\t\t\t\t\t\t\tKind: \"release\",\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tPackage: &db.Package{\n\t\t\t\t\t\tName:      \"Plone\",\n\t\t\t\t\t\tEcosystem: \"python\",\n\t\t\t\t\t},\n\t\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\t\tCVEs: []string{\"CVE-2017-5524\"},\n\t\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tVersion: db.Version{\n\t\t\t\t\t\t\t\t\tType:       \"python\",\n\t\t\t\t\t\t\t\t\tConstraint: \">=5.1a1,<5.1b1\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tFix: &db.Fix{\n\t\t\t\t\t\t\t\t\tVersion: \"5.1b1\",\n\t\t\t\t\t\t\t\t\tState:   db.FixedStatus,\n\t\t\t\t\t\t\t\t\tDetail: &db.FixDetail{\n\t\t\t\t\t\t\t\t\t\tAvailable: &db.FixAvailability{\n\t\t\t\t\t\t\t\t\t\t\tDate: internal.ParseTime(\"2017-06-15T14:22:33Z\"),\n\t\t\t\t\t\t\t\t\t\t\tKind: \"commit\",\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tPackage: &db.Package{\n\t\t\t\t\t\tName:      \"Plone-debug\",\n\t\t\t\t\t\tEcosystem: \"python\",\n\t\t\t\t\t},\n\t\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\t\tCVEs: []string{\"CVE-2017-5524\"},\n\t\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tVersion: db.Version{\n\t\t\t\t\t\t\t\t\tType:       \"python\",\n\t\t\t\t\t\t\t\t\tConstraint: \">=5.0rc1,<5.0.7\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tFix: &db.Fix{\n\t\t\t\t\t\t\t\t\tVersion: \"5.0.7\",\n\t\t\t\t\t\t\t\t\tState:   db.FixedStatus,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tadvisories := loadFixture(t, tt.name)\n\t\t\tvar results []db.AffectedPackageHandle\n\t\t\tfor _, advisor := range advisories {\n\t\t\t\tresult := getAffectedPackage(advisor)\n\t\t\t\tresults = append(results, result...)\n\t\t\t}\n\t\t\tif d := cmp.Diff(tt.expected, results); d != \"\" {\n\t\t\t\tt.Fatalf(\"unexpected result: %s\", d)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestGetPackageType(t *testing.T) {\n\ttests := []struct {\n\t\tecosystem    string\n\t\texpectedType pkg.Type\n\t}{\n\t\t{\"composer\", pkg.PhpComposerPkg},\n\t\t{\"Composer\", pkg.PhpComposerPkg}, // testing case insensitivity\n\t\t{\"COMPOSER\", pkg.PhpComposerPkg}, // testing case insensitivity\n\t\t{\"rust\", pkg.RustPkg},\n\t\t{\"cargo\", pkg.RustPkg},\n\t\t{\"dart\", pkg.DartPubPkg},\n\t\t{\"nuget\", pkg.DotnetPkg},\n\t\t{\".net\", pkg.DotnetPkg},\n\t\t{\"go\", pkg.GoModulePkg},\n\t\t{\"golang\", pkg.GoModulePkg},\n\t\t{\"maven\", pkg.JavaPkg},\n\t\t{\"java\", pkg.JavaPkg},\n\t\t{\"npm\", pkg.NpmPkg},\n\t\t{\"pypi\", pkg.PythonPkg},\n\t\t{\"python\", pkg.PythonPkg},\n\t\t{\"pip\", pkg.PythonPkg},\n\t\t{\"swift\", pkg.SwiftPkg},\n\t\t{\"rubygems\", pkg.GemPkg},\n\t\t{\"ruby\", pkg.GemPkg},\n\t\t{\"gem\", pkg.GemPkg},\n\t\t{\"apk\", pkg.ApkPkg},\n\t\t{\"rpm\", pkg.RpmPkg},\n\t\t{\"deb\", pkg.DebPkg},\n\t\t{\"github-action\", pkg.GithubActionPkg},\n\n\t\t// test for unknown type fallback\n\t\t{\"unknown-ecosystem\", pkg.Type(\"unknown-ecosystem\")},\n\t\t{\"\", pkg.Type(\"\")},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.ecosystem, func(t *testing.T) {\n\t\t\tgotType := getPackageType(tc.ecosystem)\n\t\t\tif gotType != tc.expectedType {\n\t\t\t\tt.Errorf(\"getPackageType(%q) = %v, want %v\", tc.ecosystem, gotType, tc.expectedType)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestGetRanges(t *testing.T) {\n\tadvisories := loadFixture(t, \"testdata/GHSA-92cp-5422-2mw7.json\")\n\trequire.Len(t, advisories, 1)\n\tadvisory := advisories[0]\n\tvar ranges []db.Range\n\texpectedRanges := []db.Range{\n\t\t{\n\t\t\tVersion: db.Version{\n\t\t\t\tType:       \"go\",\n\t\t\t\tConstraint: \">=9.7.0-beta.1,<9.7.3\",\n\t\t\t},\n\t\t\tFix: &db.Fix{\n\t\t\t\tVersion: \"9.7.3\",\n\t\t\t\tState:   db.FixedStatus,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tVersion: db.Version{\n\t\t\t\t// important: this emits an unknown constraint type,\n\t\t\t\t// triggering fuzzy matching when the input is not\n\t\t\t\t// valid semver\n\t\t\t\tType:       \"Unknown\",\n\t\t\t\tConstraint: \">=9.6.0b1,<9.6.3\",\n\t\t\t},\n\t\t\tFix: &db.Fix{\n\t\t\t\tVersion: \"9.6.3\",\n\t\t\t\tState:   db.FixedStatus,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tVersion: db.Version{\n\t\t\t\tType:       \"go\",\n\t\t\t\tConstraint: \">=9.5.1,<9.5.5\",\n\t\t\t},\n\t\t\tFix: &db.Fix{\n\t\t\t\tVersion: \"9.5.5\",\n\t\t\t\tState:   db.FixedStatus,\n\t\t\t},\n\t\t},\n\t}\n\tvar errors []error\n\tfor _, fixedIn := range advisory.Advisory.FixedIn {\n\t\trng, err := getRanges(fixedIn)\n\t\tif err != nil {\n\t\t\terrors = append(errors, err)\n\t\t}\n\t\tranges = append(ranges, rng...)\n\t}\n\n\trequire.Equal(t, 1, len(errors))\n\tif diff := cmp.Diff(expectedRanges, ranges); diff != \"\" {\n\t\tt.Errorf(\"getRanges() mismatch (-want +got):\\n%s\", diff)\n\t}\n}\n\nfunc TestGetFixAvailability(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tfixture  string\n\t\texpected map[string]*db.FixAvailability // keyed by package identifier for fixture-based testing\n\t}{\n\t\t{\n\t\t\tname:    \"GHSA-2wgc-48g2-cj5w with advisory availability\",\n\t\t\tfixture: \"testdata/GHSA-2wgc-48g2-cj5w.json\",\n\t\t\texpected: map[string]*db.FixAvailability{\n\t\t\t\t\"4.2.0\": {\n\t\t\t\t\tDate: internal.ParseTime(\"2024-01-30T15:00:00Z\"),\n\t\t\t\t\tKind: \"advisory\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:    \"multiple-fixed-in-names with mixed availability\",\n\t\t\tfixture: \"testdata/multiple-fixed-in-names.json\",\n\t\t\texpected: map[string]*db.FixAvailability{\n\t\t\t\t\"4.3.12\": {\n\t\t\t\t\tDate: internal.ParseTime(\"2017-05-20T10:30:45Z\"),\n\t\t\t\t\tKind: \"release\",\n\t\t\t\t},\n\t\t\t\t\"5.1b1\": {\n\t\t\t\t\tDate: internal.ParseTime(\"2017-06-15T14:22:33Z\"),\n\t\t\t\t\tKind: \"commit\",\n\t\t\t\t},\n\t\t\t\t\"5.0.7\": nil, // no availability data in fixture\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tadvisories := loadFixture(t, tt.fixture)\n\t\t\trequire.Len(t, advisories, 1, \"expected exactly one advisory\")\n\n\t\t\tfor _, fixedIn := range advisories[0].Advisory.FixedIn {\n\t\t\t\tresult := getFixAvailability(fixedIn)\n\t\t\t\texpected := tt.expected[fixedIn.Identifier]\n\n\t\t\t\tif expected == nil {\n\t\t\t\t\trequire.Nil(t, result, \"expected nil availability for %s\", fixedIn.Identifier)\n\t\t\t\t} else {\n\t\t\t\t\trequire.NotNil(t, result, \"expected non-nil availability for %s\", fixedIn.Identifier)\n\t\t\t\t\trequire.Equal(t, expected.Kind, result.Kind)\n\t\t\t\t\trequire.Equal(t, expected.Date, result.Date)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n\n\t// keep edge case test for scenarios not covered by fixtures\n\tt.Run(\"invalid date returns nil\", func(t *testing.T) {\n\t\tfixedIn := unmarshal.GithubFixedIn{\n\t\t\tAvailable: struct {\n\t\t\t\tDate string `json:\"date,omitempty\"`\n\t\t\t\tKind string `json:\"kind,omitempty\"`\n\t\t\t}{\n\t\t\t\tDate: \"invalid-date\",\n\t\t\t\tKind: \"commit\",\n\t\t\t},\n\t\t}\n\t\tresult := getFixAvailability(fixedIn)\n\t\trequire.Nil(t, result)\n\t})\n}\n\nfunc TestGetFix(t *testing.T) {\n\t// fixture-based tests\n\ttests := []struct {\n\t\tname     string\n\t\tfixture  string\n\t\texpected map[string]*db.Fix // keyed by package identifier\n\t}{\n\t\t{\n\t\t\tname:    \"GHSA-2wgc-48g2-cj5w with availability\",\n\t\t\tfixture: \"testdata/GHSA-2wgc-48g2-cj5w.json\",\n\t\t\texpected: map[string]*db.Fix{\n\t\t\t\t\"4.2.0\": {\n\t\t\t\t\tVersion: \"4.2.0\",\n\t\t\t\t\tState:   db.FixedStatus,\n\t\t\t\t\tDetail: &db.FixDetail{\n\t\t\t\t\t\tAvailable: &db.FixAvailability{\n\t\t\t\t\t\t\tDate: internal.ParseTime(\"2024-01-30T15:00:00Z\"),\n\t\t\t\t\t\t\tKind: \"advisory\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:    \"multiple-fixed-in-names with mixed availability\",\n\t\t\tfixture: \"testdata/multiple-fixed-in-names.json\",\n\t\t\texpected: map[string]*db.Fix{\n\t\t\t\t\"4.3.12\": {\n\t\t\t\t\tVersion: \"4.3.12\",\n\t\t\t\t\tState:   db.FixedStatus,\n\t\t\t\t\tDetail: &db.FixDetail{\n\t\t\t\t\t\tAvailable: &db.FixAvailability{\n\t\t\t\t\t\t\tDate: internal.ParseTime(\"2017-05-20T10:30:45Z\"),\n\t\t\t\t\t\t\tKind: \"release\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t\"5.1b1\": {\n\t\t\t\t\tVersion: \"5.1b1\",\n\t\t\t\t\tState:   db.FixedStatus,\n\t\t\t\t\tDetail: &db.FixDetail{\n\t\t\t\t\t\tAvailable: &db.FixAvailability{\n\t\t\t\t\t\t\tDate: internal.ParseTime(\"2017-06-15T14:22:33Z\"),\n\t\t\t\t\t\t\tKind: \"commit\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t\"5.0.7\": {\n\t\t\t\t\tVersion: \"5.0.7\",\n\t\t\t\t\tState:   db.FixedStatus,\n\t\t\t\t\tDetail:  nil, // no availability data\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tadvisories := loadFixture(t, tt.fixture)\n\t\t\trequire.Len(t, advisories, 1, \"expected exactly one advisory\")\n\n\t\t\tfor _, fixedIn := range advisories[0].Advisory.FixedIn {\n\t\t\t\tresult := getFix(fixedIn)\n\t\t\t\texpected := tt.expected[fixedIn.Identifier]\n\n\t\t\t\trequire.NotNil(t, expected, \"no expected result for identifier %s\", fixedIn.Identifier)\n\t\t\t\tif d := cmp.Diff(expected, result); d != \"\" {\n\t\t\t\t\tt.Fatalf(\"unexpected result for %s: %s\", fixedIn.Identifier, d)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n\n\t// keep edge case tests\n\tt.Run(\"no fix version and no availability\", func(t *testing.T) {\n\t\tfixedIn := unmarshal.GithubFixedIn{\n\t\t\tIdentifier: \"\",\n\t\t\tAvailable: struct {\n\t\t\t\tDate string `json:\"date,omitempty\"`\n\t\t\t\tKind string `json:\"kind,omitempty\"`\n\t\t\t}{},\n\t\t}\n\t\texpected := &db.Fix{\n\t\t\tVersion: \"\",\n\t\t\tState:   db.NotFixedStatus,\n\t\t\tDetail:  nil,\n\t\t}\n\t\tresult := getFix(fixedIn)\n\t\tif d := cmp.Diff(expected, result); d != \"\" {\n\t\t\tt.Fatalf(\"unexpected result: %s\", d)\n\t\t}\n\t})\n}\n\nfunc loadFixture(t *testing.T, path string) []unmarshal.GitHubAdvisory {\n\tf, err := os.Open(path)\n\tt.Cleanup(func() {\n\t\trequire.NoError(t, f.Close())\n\t})\n\trequire.NoError(t, err)\n\n\tentries, err := unmarshal.GitHubAdvisoryEntries(f)\n\trequire.NoError(t, err)\n\n\treturn entries\n}\n"
  },
  {
    "path": "grype/db/v6/build/transformers/internal/sort.go",
    "content": "package internal\n\nimport db \"github.com/anchore/grype/grype/db/v6\"\n\ntype ByAffectedPackage []db.AffectedPackageHandle\n\nfunc (a ByAffectedPackage) Len() int      { return len(a) }\nfunc (a ByAffectedPackage) Swap(i, j int) { a[i], a[j] = a[j], a[i] }\nfunc (a ByAffectedPackage) Less(i, j int) bool {\n\treturn comparePackageHandles(a[i].Package, a[i].BlobValue.Ranges, a[j].Package, a[j].BlobValue.Ranges)\n}\n\ntype ByUnaffectedPackage []db.UnaffectedPackageHandle\n\nfunc (a ByUnaffectedPackage) Len() int      { return len(a) }\nfunc (a ByUnaffectedPackage) Swap(i, j int) { a[i], a[j] = a[j], a[i] }\nfunc (a ByUnaffectedPackage) Less(i, j int) bool {\n\treturn comparePackageHandles(a[i].Package, a[i].BlobValue.Ranges, a[j].Package, a[j].BlobValue.Ranges)\n}\n\n// comparePackageHandles compares two package handles by name, ecosystem, then version constraints\nfunc comparePackageHandles(pkg1 *db.Package, ranges1 []db.Range, pkg2 *db.Package, ranges2 []db.Range) bool {\n\tif pkg1.Name != pkg2.Name {\n\t\treturn pkg1.Name < pkg2.Name\n\t}\n\tif pkg1.Ecosystem != pkg2.Ecosystem {\n\t\treturn pkg1.Ecosystem < pkg2.Ecosystem\n\t}\n\n\t// compare version constraints\n\tfor _, r1 := range ranges1 {\n\t\tfor _, r2 := range ranges2 {\n\t\t\tif r1.Version.Constraint != r2.Version.Constraint {\n\t\t\t\treturn r1.Version.Constraint < r2.Version.Constraint\n\t\t\t}\n\t\t}\n\t}\n\treturn false\n}\n"
  },
  {
    "path": "grype/db/v6/build/transformers/internal/time.go",
    "content": "package internal\n\nimport (\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/araddon/dateparse\"\n\n\t\"github.com/anchore/grype/internal/log\"\n)\n\nfunc ParseTime(s string) *time.Time {\n\ts = strings.TrimSpace(s)\n\tif s == \"\" {\n\t\treturn nil\n\t}\n\tt, err := time.Parse(time.RFC3339, s)\n\tif err == nil {\n\t\treturn &t\n\t}\n\n\t// check if the timezone information is missing and append UTC if needed\n\tif !strings.Contains(s, \"Z\") && !strings.Contains(s, \"+\") && !strings.Contains(s, \"-\") {\n\t\ts += \"Z\"\n\t\tt, err = time.Parse(time.RFC3339, s)\n\t\tif err == nil {\n\t\t\tt = t.UTC()\n\t\t\treturn &t\n\t\t}\n\t}\n\n\t// handle formats with milliseconds but no timezone\n\tformats := []string{\n\t\t\"2006-01-02T15:04:05.000\",\n\t\t\"2006-01-02T15:04:05.000Z\",\n\t}\n\n\tfor _, format := range formats {\n\t\tt, err = time.Parse(format, s)\n\t\tif err == nil {\n\t\t\tt = t.UTC()\n\t\t\treturn &t\n\t\t}\n\t}\n\n\t// handle a wide variety of other formats\n\tt, err = dateparse.ParseAny(s)\n\tif err == nil {\n\t\tt = t.UTC()\n\t\treturn &t\n\t}\n\n\tlog.WithFields(\"time\", s).Warnf(\"could not parse time: %v\", err)\n\treturn nil\n}\n"
  },
  {
    "path": "grype/db/v6/build/transformers/internal/time_test.go",
    "content": "package internal\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestParseTime(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    string\n\t\texpected *time.Time\n\t}{\n\t\t{\n\t\t\tname:     \"empty string\",\n\t\t\tinput:    \"\",\n\t\t\texpected: nil,\n\t\t},\n\t\t{\n\t\t\tname:  \"valid RFC3339 with Z\",\n\t\t\tinput: \"2024-11-15T12:34:56Z\",\n\t\t\texpected: func() *time.Time {\n\t\t\t\tt, _ := time.Parse(time.RFC3339, \"2024-11-15T12:34:56Z\")\n\t\t\t\treturn &t\n\t\t\t}(),\n\t\t},\n\t\t{\n\t\t\tname:  \"valid RFC3339 without Z\",\n\t\t\tinput: \"2024-11-15T12:34:56\",\n\t\t\texpected: func() *time.Time {\n\t\t\t\tt, _ := time.Parse(time.RFC3339, \"2024-11-15T12:34:56Z\")\n\t\t\t\treturn &t\n\t\t\t}(),\n\t\t},\n\t\t{\n\t\t\tname:  \"valid with milliseconds no timezone\",\n\t\t\tinput: \"2024-11-15T12:34:56.789\",\n\t\t\texpected: func() *time.Time {\n\t\t\t\tt, _ := time.Parse(\"2006-01-02T15:04:05.000\", \"2024-11-15T12:34:56.789\")\n\t\t\t\tutc := t.UTC()\n\t\t\t\treturn &utc\n\t\t\t}(),\n\t\t},\n\t\t{\n\t\t\tname:  \"valid with milliseconds and Z\",\n\t\t\tinput: \"2024-11-15T12:34:56.789Z\",\n\t\t\texpected: func() *time.Time {\n\t\t\t\tt, _ := time.Parse(\"2006-01-02T15:04:05.000Z\", \"2024-11-15T12:34:56.789Z\")\n\t\t\t\tutc := t.UTC()\n\t\t\t\treturn &utc\n\t\t\t}(),\n\t\t},\n\t\t{\n\t\t\tname:  \"valid dateparse format\",\n\t\t\tinput: \"November 15, 2024 12:34 PM UTC\",\n\t\t\texpected: func() *time.Time {\n\t\t\t\tt, _ := time.Parse(time.RFC3339, \"2024-11-15T12:34:00Z\")\n\t\t\t\treturn &t\n\t\t\t}(),\n\t\t},\n\t\t{\n\t\t\tname:  \"valid date only\",\n\t\t\tinput: \"2024-11-15\",\n\t\t\texpected: func() *time.Time {\n\t\t\t\tt, _ := time.Parse(\"2006-01-02\", \"2024-11-15\")\n\t\t\t\tutc := t.UTC()\n\t\t\t\treturn &utc\n\t\t\t}(),\n\t\t},\n\t\t{\n\t\t\tname:  \"valid date with time\",\n\t\t\tinput: \"2024-11-15 01:02:03\",\n\t\t\texpected: func() *time.Time {\n\t\t\t\tt, _ := time.Parse(time.RFC3339, \"2024-11-15T01:02:03Z\")\n\t\t\t\treturn &t\n\t\t\t}(),\n\t\t},\n\t\t{\n\t\t\tname:  \"invalid time format\",\n\t\t\tinput: \"invalid-time\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := ParseTime(tt.input)\n\t\t\tif tt.expected == nil {\n\t\t\t\trequire.Nil(t, result)\n\t\t\t} else {\n\t\t\t\trequire.NotNil(t, result)\n\t\t\t\trequire.Equal(t, tt.expected.UTC(), result.UTC())\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/db/v6/build/transformers/kev/testdata/go-case.json",
    "content": "{\n  \"cveID\": \"CVE-2025-0108\",\n  \"vendorProject\": \"Palo Alto Networks\",\n  \"product\": \"PAN-OS\",\n  \"vulnerabilityName\": \"Palo Alto Networks PAN-OS Authentication Bypass Vulnerability\",\n  \"dateAdded\": \"2025-02-18\",\n  \"shortDescription\": \"Palo Alto Networks PAN-OS contains an authentication bypass vulnerability in its management web interface. This vulnerability allows an unauthenticated attacker with network access to the management web interface to bypass the authentication normally required and invoke certain PHP scripts.\",\n  \"requiredAction\": \"Apply mitigations per vendor instructions [https://www.vendor.com/instructions] or discontinue use of the product if mitigations are unavailable [https:\\/\\/www.vendor.com\\/something-else].\",\n  \"dueDate\": \"2025-03-11\",\n  \"knownRansomwareCampaignUse\": \"Unknown\",\n  \"notes\": \"https:\\/\\/security.paloaltonetworks.com\\/CVE-2025-0108 ; https:\\/\\/nvd.nist.gov\\/vuln\\/detail\\/CVE-2025-0108 ; remaining information\",\n  \"cwes\": [\n    \"CWE-306\"\n  ]\n}\n"
  },
  {
    "path": "grype/db/v6/build/transformers/kev/transform.go",
    "content": "package kev\n\nimport (\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/scylladb/go-set/strset\"\n\n\t\"github.com/anchore/grype/grype/db/data\"\n\t\"github.com/anchore/grype/grype/db/internal/provider/unmarshal\"\n\t\"github.com/anchore/grype/grype/db/provider\"\n\tdb \"github.com/anchore/grype/grype/db/v6\"\n\t\"github.com/anchore/grype/grype/db/v6/build/transformers\"\n\t\"github.com/anchore/grype/grype/db/v6/build/transformers/internal\"\n)\n\nfunc Transform(kev unmarshal.KnownExploitedVulnerability, state provider.State) ([]data.Entry, error) {\n\treturn transformers.NewEntries(*provider.Model(state), getKev(kev)), nil\n}\n\nfunc getKev(kev unmarshal.KnownExploitedVulnerability) db.KnownExploitedVulnerabilityHandle {\n\turls, notes := getURLs([]string{kev.ShortDescription, kev.RequiredAction}, kev.Notes)\n\treturn db.KnownExploitedVulnerabilityHandle{\n\t\tCve: kev.CveID,\n\t\tBlobValue: &db.KnownExploitedVulnerabilityBlob{\n\t\t\tCve:                        kev.CveID,\n\t\t\tVendorProject:              kev.VendorProject,\n\t\t\tProduct:                    kev.Product,\n\t\t\tDateAdded:                  internal.ParseTime(kev.DateAdded),\n\t\t\tRequiredAction:             kev.RequiredAction,\n\t\t\tDueDate:                    internal.ParseTime(kev.DueDate),\n\t\t\tKnownRansomwareCampaignUse: strings.ToLower(kev.KnownRansomwareCampaignUse),\n\t\t\tNotes:                      notes,\n\t\t\tCWEs:                       kev.CWEs,\n\t\t\tURLs:                       urls,\n\t\t},\n\t}\n}\n\nvar bracketURLPattern = regexp.MustCompile(`\\[(https?://[^\\]]+)\\]`)\n\nfunc getURLs(aux []string, notes string) ([]string, string) {\n\t// let's keep the URLs we find in order but also deduplicate them since we're combining URLs from multiple sources\n\turlSet := strset.New()\n\tvar urls []string\n\n\t// add URLs from notes first...\n\tif notes != \"\" {\n\t\tparts := strings.Split(notes, \";\")\n\t\tcleanedParts := make([]string, 0, len(parts))\n\n\t\tfor _, part := range parts {\n\t\t\tpart = strings.TrimSpace(part)\n\n\t\t\tif strings.HasPrefix(strings.ToLower(part), \"http\") {\n\t\t\t\turl := part\n\t\t\t\tif !urlSet.Has(url) {\n\t\t\t\t\turlSet.Add(url)\n\t\t\t\t\turls = append(urls, url)\n\t\t\t\t}\n\t\t\t} else if part != \"\" {\n\t\t\t\tcleanedParts = append(cleanedParts, part)\n\t\t\t}\n\t\t}\n\n\t\tnotes = strings.Join(cleanedParts, \"; \")\n\t}\n\n\t// ...then add URLs from the other fields\n\tfor _, text := range aux {\n\t\tmatches := bracketURLPattern.FindAllStringSubmatch(text, -1)\n\t\tfor _, match := range matches {\n\t\t\tif len(match) > 1 {\n\t\t\t\turl := match[1]\n\t\t\t\tif !urlSet.Has(url) {\n\t\t\t\t\turlSet.Add(url)\n\t\t\t\t\turls = append(urls, url)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn urls, notes\n}\n"
  },
  {
    "path": "grype/db/v6/build/transformers/kev/transform_test.go",
    "content": "package kev\n\nimport (\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/grype/grype/db/internal/provider/unmarshal\"\n\t\"github.com/anchore/grype/grype/db/provider\"\n\tdb \"github.com/anchore/grype/grype/db/v6\"\n\t\"github.com/anchore/grype/grype/db/v6/build/transformers\"\n\t\"github.com/anchore/grype/grype/db/v6/build/transformers/internal\"\n)\n\nfunc TestTransform(t *testing.T) {\n\n\tvar timeVal = time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC)\n\tvar listing = provider.File{\n\t\tPath:      \"some\",\n\t\tDigest:    \"123456\",\n\t\tAlgorithm: \"sha256\",\n\t}\n\n\ttests := []struct {\n\t\tname string\n\t\twant []transformers.RelatedEntries\n\t}{\n\t\t{\n\t\t\tname: \"testdata/go-case.json\",\n\t\t\twant: []transformers.RelatedEntries{\n\t\t\t\t{\n\t\t\t\t\tProvider: &db.Provider{\n\t\t\t\t\t\tID:           \"kev\",\n\t\t\t\t\t\tVersion:      \"12\",\n\t\t\t\t\t\tProcessor:    \"vunnel@1.2.3\",\n\t\t\t\t\t\tDateCaptured: &timeVal,\n\t\t\t\t\t\tInputDigest:  \"sha256:123456\",\n\t\t\t\t\t},\n\t\t\t\t\tRelated: kevSlice(\n\t\t\t\t\t\tdb.KnownExploitedVulnerabilityHandle{\n\t\t\t\t\t\t\tCve: \"CVE-2025-0108\",\n\t\t\t\t\t\t\tBlobValue: &db.KnownExploitedVulnerabilityBlob{\n\t\t\t\t\t\t\t\tCve:                        \"CVE-2025-0108\",\n\t\t\t\t\t\t\t\tVendorProject:              \"Palo Alto Networks\",\n\t\t\t\t\t\t\t\tProduct:                    \"PAN-OS\",\n\t\t\t\t\t\t\t\tDateAdded:                  internal.ParseTime(\"2025-02-18\"),\n\t\t\t\t\t\t\t\tRequiredAction:             \"Apply mitigations per vendor instructions [https://www.vendor.com/instructions] or discontinue use of the product if mitigations are unavailable [https://www.vendor.com/something-else].\",\n\t\t\t\t\t\t\t\tDueDate:                    internal.ParseTime(\"2025-03-11\"),\n\t\t\t\t\t\t\t\tKnownRansomwareCampaignUse: \"unknown\",\n\t\t\t\t\t\t\t\tNotes:                      \"remaining information\",\n\t\t\t\t\t\t\t\tURLs: []string{\n\t\t\t\t\t\t\t\t\t\"https://security.paloaltonetworks.com/CVE-2025-0108\",\n\t\t\t\t\t\t\t\t\t\"https://nvd.nist.gov/vuln/detail/CVE-2025-0108\",\n\t\t\t\t\t\t\t\t\t\"https://www.vendor.com/instructions\",\n\t\t\t\t\t\t\t\t\t\"https://www.vendor.com/something-else\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tCWEs: []string{\"CWE-306\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tentries := loadFixture(t, test.name)\n\n\t\t\tvar actual []transformers.RelatedEntries\n\t\t\tfor _, vuln := range entries {\n\t\t\t\tentries, err := Transform(vuln, provider.State{\n\t\t\t\t\tProvider:  \"kev\",\n\t\t\t\t\tVersion:   12,\n\t\t\t\t\tProcessor: \"vunnel@1.2.3\",\n\t\t\t\t\tTimestamp: timeVal,\n\t\t\t\t\tListing:   &listing,\n\t\t\t\t})\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tfor _, entry := range entries {\n\t\t\t\t\te, ok := entry.Data.(transformers.RelatedEntries)\n\t\t\t\t\trequire.True(t, ok)\n\t\t\t\t\tactual = append(actual, e)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif diff := cmp.Diff(test.want, actual); diff != \"\" {\n\t\t\t\tt.Errorf(\"data entries mismatch (-want +got):\\n%s\", diff)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc kevSlice(a ...db.KnownExploitedVulnerabilityHandle) []any {\n\tvar r []any\n\tfor _, v := range a {\n\t\tr = append(r, v)\n\t}\n\treturn r\n}\n\nfunc loadFixture(t *testing.T, fixturePath string) []unmarshal.KnownExploitedVulnerability {\n\tt.Helper()\n\n\tf, err := os.Open(fixturePath)\n\trequire.NoError(t, err)\n\tdefer func() {\n\t\trequire.NoError(t, f.Close())\n\t}()\n\n\tentries, err := unmarshal.KnownExploitedVulnerabilityEntries(f)\n\trequire.NoError(t, err)\n\treturn entries\n}\n"
  },
  {
    "path": "grype/db/v6/build/transformers/msrc/testdata/microsoft-msrc-0.json",
    "content": "[\n {\n  \"cvss\": {\n   \"base_score\": 7.8,\n   \"temporal_score\": 7,\n   \"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\"\n  },\n  \"fixed_in\": [\n   {\n    \"id\": \"4493470\",\n    \"is_first\": true,\n    \"is_latest\": false,\n    \"links\": [\n     \"https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4493470\",\n     \"https://support.microsoft.com/help/4493470\"\n    ],\n    \"available\": {\n     \"date\": \"2019-11-12\",\n     \"kind\": \"advisory\"\n    }\n   },\n   {\n    \"id\": \"4494440\",\n    \"is_first\": false,\n    \"is_latest\": false,\n    \"links\": [\n     \"https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4494440\",\n     \"https://support.microsoft.com/help/4494440\"\n    ],\n    \"available\": {\n     \"date\": \"2019-11-12\",\n     \"kind\": \"advisory\"\n    }\n   },\n   {\n    \"id\": \"4503267\",\n    \"is_first\": false,\n    \"is_latest\": false,\n    \"links\": [\n     \"https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4503267\",\n     \"https://support.microsoft.com/en-us/help/4503267\"\n    ],\n    \"available\": {\n     \"date\": \"2019-11-12\",\n     \"kind\": \"advisory\"\n    }\n   },\n   {\n    \"id\": \"4507460\",\n    \"is_first\": false,\n    \"is_latest\": false,\n    \"links\": [\n     \"https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4507460\",\n     \"https://support.microsoft.com/help/4507460\"\n    ],\n    \"available\": {\n     \"date\": \"2019-11-12\",\n     \"kind\": \"advisory\"\n    }\n   },\n   {\n    \"id\": \"4512517\",\n    \"is_first\": false,\n    \"is_latest\": false,\n    \"links\": [\n     \"https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4512517\",\n     \"https://support.microsoft.com/help/4512517\"\n    ],\n    \"available\": {\n     \"date\": \"2019-11-12\",\n     \"kind\": \"advisory\"\n    }\n   },\n   {\n    \"id\": \"4516044\",\n    \"is_first\": false,\n    \"is_latest\": true,\n    \"links\": [\n     \"https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4516044\",\n     \"https://support.microsoft.com/help/4516044\"\n    ],\n    \"available\": {\n     \"date\": \"2019-11-12\",\n     \"kind\": \"advisory\"\n    }\n   }\n  ],\n  \"id\": \"CVE-2019-0671\",\n  \"link\": \"https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2019-0671\",\n  \"product\": {\n   \"family\": \"Windows\",\n   \"id\": \"10852\",\n   \"name\": \"Windows 10 Version 1607 for 32-bit Systems\"\n  },\n  \"severity\": \"High\",\n  \"summary\": \"Microsoft Office Access Connectivity Engine Remote Code Execution Vulnerability\",\n  \"vulnerable\": [\n   \"4480961\",\n   \"4483229\",\n   \"4487026\",\n   \"4489882\"\n  ]\n },\n{\n  \"cvss\": {\n   \"base_score\": 4.4,\n   \"temporal_score\": 4,\n   \"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\"\n  },\n  \"fixed_in\": [\n   {\n    \"id\": \"4093119\",\n    \"is_first\": true,\n    \"is_latest\": false,\n    \"links\": [\n     \"https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4093119\"\n    ],\n    \"available\": {\n     \"date\": \"2019-11-12\",\n     \"kind\": \"advisory\"\n    }\n   },\n   {\n    \"id\": \"4103723\",\n    \"is_first\": false,\n    \"is_latest\": false,\n    \"links\": [\n     \"https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4103723\"\n    ],\n    \"available\": {\n     \"date\": \"2019-11-12\",\n     \"kind\": \"advisory\"\n    }\n   },\n   {\n    \"id\": \"4284880\",\n    \"is_first\": false,\n    \"is_latest\": false,\n    \"links\": [\n     \"https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4284880\"\n    ],\n    \"available\": {\n     \"date\": \"2019-11-12\",\n     \"kind\": \"advisory\"\n    }\n   },\n   {\n    \"id\": \"4338814\",\n    \"is_first\": false,\n    \"is_latest\": false,\n    \"links\": [\n     \"https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4338814\"\n    ],\n    \"available\": {\n     \"date\": \"2019-11-12\",\n     \"kind\": \"advisory\"\n    }\n   },\n   {\n    \"id\": \"4343887\",\n    \"is_first\": false,\n    \"is_latest\": false,\n    \"links\": [\n     \"https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4343887\"\n    ],\n    \"available\": {\n     \"date\": \"2019-11-12\",\n     \"kind\": \"advisory\"\n    }\n   },\n   {\n    \"id\": \"4345418\",\n    \"is_first\": false,\n    \"is_latest\": true,\n    \"links\": [\n     \"https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4345418\"\n    ],\n    \"available\": {\n     \"date\": \"2019-11-12\",\n     \"kind\": \"advisory\"\n    }\n   },\n   {\n    \"id\": \"4457131\",\n    \"is_first\": false,\n    \"is_latest\": false,\n    \"links\": [\n     \"https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4457131\"\n    ],\n    \"available\": {\n     \"date\": \"2019-11-12\",\n     \"kind\": \"advisory\"\n    }\n   },\n   {\n    \"id\": \"4462917\",\n    \"is_first\": false,\n    \"is_latest\": false,\n    \"links\": [\n     \"https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4462917\"\n    ],\n    \"available\": {\n     \"date\": \"2019-11-12\",\n     \"kind\": \"advisory\"\n    }\n   },\n   {\n    \"id\": \"4467691\",\n    \"is_first\": false,\n    \"is_latest\": false,\n    \"links\": [\n     \"https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4467691\"\n    ],\n    \"available\": {\n     \"date\": \"2019-11-12\",\n     \"kind\": \"advisory\"\n    }\n   },\n   {\n    \"id\": \"4471321\",\n    \"is_first\": false,\n    \"is_latest\": true,\n    \"links\": [\n     \"https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4471321\"\n    ],\n    \"available\": {\n     \"date\": \"2019-11-12\",\n     \"kind\": \"advisory\"\n    }\n   }\n  ],\n  \"id\": \"CVE-2018-8116\",\n  \"link\": \"https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2018-8116\",\n  \"product\": {\n   \"family\": \"Windows\",\n   \"id\": \"10852\",\n   \"name\": \"Windows 10 Version 1607 for 32-bit Systems\"\n  },\n  \"severity\": \"Medium\",\n  \"summary\": \"Microsoft Graphics Component Denial of Service Vulnerability\",\n  \"vulnerable\": [\n   \"3213986\",\n   \"4013429\",\n   \"4015217\",\n   \"4019472\",\n   \"4022715\",\n   \"4025339\",\n   \"4034658\",\n   \"4038782\",\n   \"4041691\",\n   \"4048953\",\n   \"4053579\",\n   \"4056890\",\n   \"4074590\",\n   \"4088787\"\n  ]\n }\n]\n"
  },
  {
    "path": "grype/db/v6/build/transformers/msrc/transform.go",
    "content": "package msrc\n\nimport (\n\t\"strings\"\n\n\t\"github.com/anchore/grype/grype/db/data\"\n\t\"github.com/anchore/grype/grype/db/internal/provider/unmarshal\"\n\t\"github.com/anchore/grype/grype/db/internal/versionutil\"\n\t\"github.com/anchore/grype/grype/db/provider\"\n\tdb \"github.com/anchore/grype/grype/db/v6\"\n\t\"github.com/anchore/grype/grype/db/v6/build/transformers\"\n\t\"github.com/anchore/grype/grype/db/v6/build/transformers/internal\"\n\t\"github.com/anchore/grype/grype/db/v6/name\"\n\t\"github.com/anchore/syft/syft/pkg\"\n)\n\nfunc Transform(vulnerability unmarshal.MSRCVulnerability, state provider.State) ([]data.Entry, error) {\n\tins := []any{\n\t\tgetVulnerability(vulnerability, state),\n\t}\n\n\tins = append(ins, getAffectedPackage(vulnerability))\n\n\treturn transformers.NewEntries(ins...), nil\n}\n\nfunc getVulnerability(vuln unmarshal.MSRCVulnerability, state provider.State) db.VulnerabilityHandle {\n\treturn db.VulnerabilityHandle{\n\t\tName:       vuln.ID,\n\t\tProviderID: state.Provider,\n\t\tProvider:   provider.Model(state),\n\t\tStatus:     db.VulnerabilityActive,\n\t\tBlobValue: &db.VulnerabilityBlob{\n\t\t\tID:          vuln.ID,\n\t\t\tDescription: strings.TrimSpace(vuln.Summary),\n\t\t\tReferences:  getReferences(vuln),\n\t\t\tSeverities:  getSeverities(vuln),\n\t\t},\n\t}\n}\n\nfunc getAffectedPackage(vuln unmarshal.MSRCVulnerability) db.AffectedPackageHandle {\n\treturn db.AffectedPackageHandle{\n\t\tPackage: getPackage(vuln),\n\t\tBlobValue: &db.PackageBlob{\n\t\t\tRanges: getRanges(vuln),\n\t\t},\n\t}\n}\n\nfunc getPackage(vuln unmarshal.MSRCVulnerability) *db.Package {\n\treturn &db.Package{\n\t\tName:      name.Normalize(vuln.Product.ID, pkg.KbPkg),\n\t\tEcosystem: string(pkg.KbPkg),\n\t}\n}\n\nfunc getRanges(vuln unmarshal.MSRCVulnerability) []db.Range {\n\t// In anchore-enterprise windows analyzer, \"base\" represents unpatched windows images (images with no KBs)\n\t// If a vulnerability exists for a Microsoft Product ID and the image has no KBs (which are patches),\n\t// then the image must be vulnerable to the image.\n\tvuln.Vulnerable = append(vuln.Vulnerable, \"base\")\n\n\treturn []db.Range{\n\t\t{\n\t\t\tVersion: db.Version{\n\t\t\t\tType:       \"kb\",\n\t\t\t\tConstraint: versionutil.OrConstraints(vuln.Vulnerable...),\n\t\t\t},\n\t\t\tFix: getFix(vuln),\n\t\t},\n\t}\n}\n\nfunc getFix(vuln unmarshal.MSRCVulnerability) *db.Fix {\n\tfixedInVersion, fixDetail := fixedInKB(vuln)\n\n\tfixState := db.FixedStatus\n\tif fixedInVersion == \"\" {\n\t\tfixState = db.NotFixedStatus\n\t}\n\n\treturn &db.Fix{\n\t\tVersion: fixedInVersion,\n\t\tState:   fixState,\n\t\tDetail:  fixDetail,\n\t}\n}\n\n// fixedInKB finds the \"latest\" patch (KB id) amongst the available microsoft patches and returns it\n// if the \"latest\" patch cannot be found, an empty string is returned\nfunc fixedInKB(vulnerability unmarshal.MSRCVulnerability) (string, *db.FixDetail) {\n\tfor _, fixedIn := range vulnerability.FixedIn {\n\t\tif fixedIn.IsLatest {\n\t\t\tvar detail *db.FixDetail\n\t\t\tif fixedIn.Available.Date != \"\" {\n\t\t\t\tdetail = &db.FixDetail{\n\t\t\t\t\tAvailable: &db.FixAvailability{\n\t\t\t\t\t\tDate: internal.ParseTime(fixedIn.Available.Date),\n\t\t\t\t\t\tKind: fixedIn.Available.Kind,\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn fixedIn.ID, detail\n\t\t}\n\t}\n\treturn \"\", nil\n}\n\nfunc getReferences(vuln unmarshal.MSRCVulnerability) []db.Reference {\n\trefs := []db.Reference{\n\t\t{\n\t\t\tURL: vuln.Link,\n\t\t},\n\t}\n\n\treturn refs\n}\n\nfunc getSeverities(vuln unmarshal.MSRCVulnerability) []db.Severity {\n\tvar severities []db.Severity\n\n\tcleanSeverity := strings.ToLower(strings.TrimSpace(vuln.Severity))\n\tif cleanSeverity != \"\" {\n\t\tseverities = append(severities, db.Severity{\n\t\t\tScheme: db.SeveritySchemeCHML,\n\t\t\tValue:  cleanSeverity,\n\t\t})\n\t}\n\n\tif vuln.Cvss.Vector != \"\" {\n\t\tseverities = append(severities, db.Severity{\n\t\t\tScheme: db.SeveritySchemeCVSS,\n\t\t\tValue: db.CVSSSeverity{\n\t\t\t\tVector:  vuln.Cvss.Vector,\n\t\t\t\tVersion: \"3.0\", // TODO: assuming CVSS v3, update if different\n\t\t\t},\n\t\t})\n\t}\n\n\treturn severities\n}\n"
  },
  {
    "path": "grype/db/v6/build/transformers/msrc/transform_test.go",
    "content": "package msrc\n\nimport (\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/grype/grype/db/data\"\n\t\"github.com/anchore/grype/grype/db/internal/provider/unmarshal\"\n\t\"github.com/anchore/grype/grype/db/internal/testutil\"\n\t\"github.com/anchore/grype/grype/db/provider\"\n\tdb \"github.com/anchore/grype/grype/db/v6\"\n\t\"github.com/anchore/grype/grype/db/v6/build/transformers\"\n)\n\nfunc TestUnmarshalMsrcVulnerabilities(t *testing.T) {\n\tf, err := os.Open(\"testdata/microsoft-msrc-0.json\")\n\trequire.NoError(t, err)\n\tdefer testutil.CloseFile(f)\n\n\tentries, err := unmarshal.MSRCVulnerabilityEntries(f)\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, len(entries), 2)\n}\n\nfunc TestParseMSRCEntry(t *testing.T) {\n\tx := time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC)\n\n\tproviderState := provider.State{\n\t\tProvider:            \"msrc\",\n\t\tVersion:             1,\n\t\tDistributionVersion: 0,\n\t\tProcessor:           \"\",\n\t\tSchema:              provider.Schema{},\n\t\tURLs:                nil,\n\t\tTimestamp:           x,\n\t\tListing:             nil,\n\t\tStore:               \"\",\n\t\tStale:               false,\n\t}\n\n\texpectedVulns := []data.Entry{\n\t\t{\n\t\t\tDBSchemaVersion: db.ModelVersion,\n\t\t\tData: transformers.RelatedEntries{\n\t\t\t\tVulnerabilityHandle: &db.VulnerabilityHandle{\n\t\t\t\t\tName:       \"CVE-2019-0671\",\n\t\t\t\t\tProviderID: \"msrc\",\n\t\t\t\t\tProvider: &db.Provider{\n\t\t\t\t\t\tID:           \"msrc\",\n\t\t\t\t\t\tVersion:      \"1\",\n\t\t\t\t\t\tDateCaptured: &x,\n\t\t\t\t\t},\n\t\t\t\t\tStatus: db.VulnerabilityActive,\n\t\t\t\t\tBlobValue: &db.VulnerabilityBlob{\n\t\t\t\t\t\tID:          \"CVE-2019-0671\",\n\t\t\t\t\t\tDescription: \"Microsoft Office Access Connectivity Engine Remote Code Execution Vulnerability\",\n\t\t\t\t\t\tReferences: []db.Reference{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tURL: \"https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2019-0671\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tSeverities: []db.Severity{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tScheme: db.SeveritySchemeCHML,\n\t\t\t\t\t\t\t\tValue:  \"high\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tScheme: db.SeveritySchemeCVSS,\n\t\t\t\t\t\t\t\tValue: db.CVSSSeverity{\n\t\t\t\t\t\t\t\t\tVector:  \"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\",\n\t\t\t\t\t\t\t\t\tVersion: \"3.0\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tRelated: []any{\n\t\t\t\t\tdb.AffectedPackageHandle{\n\t\t\t\t\t\tPackage: &db.Package{\n\t\t\t\t\t\t\tName:      \"10852\",\n\t\t\t\t\t\t\tEcosystem: \"msrc-kb\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tVersion: db.Version{\n\t\t\t\t\t\t\t\t\t\tType:       \"kb\",\n\t\t\t\t\t\t\t\t\t\tConstraint: `4480961 || 4483229 || 4487026 || 4489882 || base`,\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\tFix: &db.Fix{\n\t\t\t\t\t\t\t\t\t\tVersion: \"4516044\",\n\t\t\t\t\t\t\t\t\t\tState:   db.FixedStatus,\n\t\t\t\t\t\t\t\t\t\tDetail: &db.FixDetail{\n\t\t\t\t\t\t\t\t\t\t\tAvailable: &db.FixAvailability{\n\t\t\t\t\t\t\t\t\t\t\t\tDate: timePtr(time.Date(2019, 11, 12, 0, 0, 0, 0, time.UTC)),\n\t\t\t\t\t\t\t\t\t\t\t\tKind: \"advisory\",\n\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tDBSchemaVersion: db.ModelVersion,\n\t\t\tData: transformers.RelatedEntries{\n\t\t\t\tVulnerabilityHandle: &db.VulnerabilityHandle{\n\t\t\t\t\tName:       \"CVE-2018-8116\",\n\t\t\t\t\tProviderID: \"msrc\",\n\t\t\t\t\tProvider: &db.Provider{\n\t\t\t\t\t\tID:           \"msrc\",\n\t\t\t\t\t\tVersion:      \"1\",\n\t\t\t\t\t\tDateCaptured: &x,\n\t\t\t\t\t},\n\t\t\t\t\tStatus: db.VulnerabilityActive,\n\t\t\t\t\tBlobValue: &db.VulnerabilityBlob{\n\t\t\t\t\t\tID:          \"CVE-2018-8116\",\n\t\t\t\t\t\tDescription: \"Microsoft Graphics Component Denial of Service Vulnerability\",\n\t\t\t\t\t\tReferences: []db.Reference{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tURL: \"https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2018-8116\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tSeverities: []db.Severity{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tScheme: db.SeveritySchemeCHML,\n\t\t\t\t\t\t\t\tValue:  \"medium\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tScheme: db.SeveritySchemeCVSS,\n\t\t\t\t\t\t\t\tValue: db.CVSSSeverity{\n\t\t\t\t\t\t\t\t\tVector:  \"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\",\n\t\t\t\t\t\t\t\t\tVersion: \"3.0\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tRelated: []any{\n\t\t\t\t\tdb.AffectedPackageHandle{\n\t\t\t\t\t\tPackage: &db.Package{\n\t\t\t\t\t\t\tName:      \"10852\",\n\t\t\t\t\t\t\tEcosystem: \"msrc-kb\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tVersion: db.Version{\n\t\t\t\t\t\t\t\t\t\tType:       \"kb\",\n\t\t\t\t\t\t\t\t\t\tConstraint: `3213986 || 4013429 || 4015217 || 4019472 || 4022715 || 4025339 || 4034658 || 4038782 || 4041691 || 4048953 || 4053579 || 4056890 || 4074590 || 4088787 || base`,\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\tFix: &db.Fix{\n\t\t\t\t\t\t\t\t\t\tVersion: \"4345418\",\n\t\t\t\t\t\t\t\t\t\tState:   db.FixedStatus,\n\t\t\t\t\t\t\t\t\t\tDetail: &db.FixDetail{\n\t\t\t\t\t\t\t\t\t\t\tAvailable: &db.FixAvailability{\n\t\t\t\t\t\t\t\t\t\t\t\tDate: timePtr(time.Date(2019, 11, 12, 0, 0, 0, 0, time.UTC)),\n\t\t\t\t\t\t\t\t\t\t\t\tKind: \"advisory\",\n\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tf, err := os.Open(\"testdata/microsoft-msrc-0.json\")\n\trequire.NoError(t, err)\n\tdefer testutil.CloseFile(f)\n\n\tentries, err := unmarshal.MSRCVulnerabilityEntries(f)\n\trequire.NoError(t, err)\n\trequire.Equal(t, len(entries), 2)\n\n\tfor idx, entry := range entries {\n\t\tdataEntries, err := Transform(entry, providerState)\n\t\trequire.NoError(t, err)\n\t\trequire.Len(t, dataEntries, 1, \"expected a single data entry to be returned\")\n\n\t\tif diff := cmp.Diff(expectedVulns[idx], dataEntries[0]); diff != \"\" {\n\t\t\tt.Errorf(\"data entry mismatch (-expected +actual):\\n%s\", diff)\n\t\t}\n\t}\n}\n\nfunc timePtr(t time.Time) *time.Time {\n\treturn &t\n}\n"
  },
  {
    "path": "grype/db/v6/build/transformers/nvd/affected_range.go",
    "content": "package nvd\n\nimport (\n\t\"fmt\"\n\t\"sort\"\n\t\"strings\"\n\n\t\"github.com/anchore/grype/grype/db/internal/provider/unmarshal/nvd\"\n\t\"github.com/anchore/syft/syft/cpe\"\n)\n\ntype affectedRangeSet map[affectedCPERange]struct{}\n\ntype affectedCPERange struct {\n\tExactVersion          string\n\tExactUpdate           string\n\tVersionStartIncluding string\n\tVersionStartExcluding string\n\tVersionEndIncluding   string\n\tVersionEndExcluding   string\n\tFixInfo               *nvd.FixInfo\n}\n\nfunc newAffectedRanges(rs ...affectedCPERange) affectedRangeSet {\n\ts := make(affectedRangeSet)\n\ts.addRanges(rs...)\n\treturn s\n}\n\nfunc newAffectedRange(match nvd.CpeMatch) affectedCPERange {\n\treturn affectedCPERange{\n\t\tVersionStartIncluding: nonEmptyValue(match.VersionStartIncluding),\n\t\tVersionStartExcluding: nonEmptyValue(match.VersionStartExcluding),\n\t\tVersionEndIncluding:   nonEmptyValue(match.VersionEndIncluding),\n\t\tVersionEndExcluding:   nonEmptyValue(match.VersionEndExcluding),\n\t\tFixInfo:               match.Fix,\n\t}\n}\n\nfunc (s affectedRangeSet) addRanges(rs ...affectedCPERange) {\n\tfor _, r := range rs {\n\t\ts[r] = struct{}{}\n\t}\n}\n\nfunc (s affectedRangeSet) toSlice() []affectedCPERange {\n\tvar result []affectedCPERange\n\tfor r := range s {\n\t\tresult = append(result, r)\n\t}\n\tsort.Slice(result, func(i, j int) bool {\n\t\tif result[i].ExactVersion != result[j].ExactVersion {\n\t\t\treturn result[i].ExactVersion < result[j].ExactVersion\n\t\t}\n\t\tif result[i].ExactUpdate != result[j].ExactUpdate {\n\t\t\treturn result[i].ExactUpdate < result[j].ExactUpdate\n\t\t}\n\t\tif result[i].VersionStartIncluding != result[j].VersionStartIncluding {\n\t\t\treturn result[i].VersionStartIncluding < result[j].VersionStartIncluding\n\t\t}\n\t\tif result[i].VersionStartExcluding != result[j].VersionStartExcluding {\n\t\t\treturn result[i].VersionStartExcluding < result[j].VersionStartExcluding\n\t\t}\n\t\tif result[i].VersionEndIncluding != result[j].VersionEndIncluding {\n\t\t\treturn result[i].VersionEndIncluding < result[j].VersionEndIncluding\n\t\t}\n\t\tif result[i].VersionEndExcluding != result[j].VersionEndExcluding {\n\t\t\treturn result[i].VersionEndExcluding < result[j].VersionEndExcluding\n\t\t}\n\t\treturn false\n\t})\n\treturn result\n}\n\nfunc (r affectedCPERange) String() string {\n\tconstraints := make([]string, 0)\n\tif r.VersionStartIncluding != \"\" {\n\t\tconstraints = append(constraints, fmt.Sprintf(\">= %s\", r.VersionStartIncluding))\n\t} else if r.VersionStartExcluding != \"\" {\n\t\tconstraints = append(constraints, fmt.Sprintf(\"> %s\", r.VersionStartExcluding))\n\t}\n\n\tif r.VersionEndExcluding != \"\" {\n\t\tconstraints = append(constraints, fmt.Sprintf(\"< %s\", r.VersionEndExcluding))\n\t} else if r.VersionEndIncluding != \"\" {\n\t\tconstraints = append(constraints, fmt.Sprintf(\"<= %s\", r.VersionEndIncluding))\n\t}\n\n\tif len(constraints) == 0 {\n\t\tversion := r.ExactVersion\n\t\tupdate := r.ExactUpdate\n\t\tif version != cpe.Any && version != \"-\" {\n\t\t\tif update != cpe.Any && update != \"-\" {\n\t\t\t\tversion = fmt.Sprintf(\"%s-%s\", version, update)\n\t\t\t}\n\n\t\t\tconstraints = append(constraints, fmt.Sprintf(\"= %s\", version))\n\t\t}\n\t}\n\n\treturn strings.Join(constraints, \", \")\n}\n\nfunc nonEmptyValue(value *string) string {\n\tif value == nil {\n\t\treturn \"\"\n\t}\n\treturn *value\n}\n"
  },
  {
    "path": "grype/db/v6/build/transformers/nvd/affected_range_test.go",
    "content": "package nvd\n\nimport (\n\t\"testing\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\n\t\"github.com/anchore/grype/grype/db/internal/provider/unmarshal/nvd\"\n)\n\nfunc Test_AffectedCPERange_String(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    affectedCPERange\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:     \"empty range\",\n\t\t\tinput:    affectedCPERange{},\n\t\t\texpected: \"\",\n\t\t},\n\t\t{\n\t\t\tname: \"exact version match\",\n\t\t\tinput: affectedCPERange{\n\t\t\t\tExactVersion: \"1.0\",\n\t\t\t},\n\t\t\texpected: \"= 1.0\",\n\t\t},\n\t\t{\n\t\t\tname: \"exact version and update match\",\n\t\t\tinput: affectedCPERange{\n\t\t\t\tExactVersion: \"1.0\",\n\t\t\t\tExactUpdate:  \"p1\",\n\t\t\t},\n\t\t\texpected: \"= 1.0-p1\",\n\t\t},\n\t\t{\n\t\t\tname: \"version start including only\",\n\t\t\tinput: affectedCPERange{\n\t\t\t\tVersionStartIncluding: \"1.0\",\n\t\t\t},\n\t\t\texpected: \">= 1.0\",\n\t\t},\n\t\t{\n\t\t\tname: \"version start excluding only\",\n\t\t\tinput: affectedCPERange{\n\t\t\t\tVersionStartExcluding: \"1.0\",\n\t\t\t},\n\t\t\texpected: \"> 1.0\",\n\t\t},\n\t\t{\n\t\t\tname: \"version end including only\",\n\t\t\tinput: affectedCPERange{\n\t\t\t\tVersionEndIncluding: \"2.0\",\n\t\t\t},\n\t\t\texpected: \"<= 2.0\",\n\t\t},\n\t\t{\n\t\t\tname: \"version end excluding only\",\n\t\t\tinput: affectedCPERange{\n\t\t\t\tVersionEndExcluding: \"2.0\",\n\t\t\t},\n\t\t\texpected: \"< 2.0\",\n\t\t},\n\t\t{\n\t\t\tname: \"version range with start and end including\",\n\t\t\tinput: affectedCPERange{\n\t\t\t\tVersionStartIncluding: \"1.0\",\n\t\t\t\tVersionEndIncluding:   \"2.0\",\n\t\t\t},\n\t\t\texpected: \">= 1.0, <= 2.0\",\n\t\t},\n\t\t{\n\t\t\tname: \"version range with start including and end excluding\",\n\t\t\tinput: affectedCPERange{\n\t\t\t\tVersionStartIncluding: \"1.0\",\n\t\t\t\tVersionEndExcluding:   \"2.0\",\n\t\t\t},\n\t\t\texpected: \">= 1.0, < 2.0\",\n\t\t},\n\t\t{\n\t\t\tname: \"version range with start excluding and end including\",\n\t\t\tinput: affectedCPERange{\n\t\t\t\tVersionStartExcluding: \"1.0\",\n\t\t\t\tVersionEndIncluding:   \"2.0\",\n\t\t\t},\n\t\t\texpected: \"> 1.0, <= 2.0\",\n\t\t},\n\t\t{\n\t\t\tname: \"version range with start and end excluding\",\n\t\t\tinput: affectedCPERange{\n\t\t\t\tVersionStartExcluding: \"1.0\",\n\t\t\t\tVersionEndExcluding:   \"2.0\",\n\t\t\t},\n\t\t\texpected: \"> 1.0, < 2.0\",\n\t\t},\n\t\t{\n\t\t\tname: \"version range with all bounds (prefer outer bounds)\",\n\t\t\tinput: affectedCPERange{\n\t\t\t\tVersionStartIncluding: \"1.0\",\n\t\t\t\tVersionStartExcluding: \"0.9\",\n\t\t\t\tVersionEndIncluding:   \"2.0\",\n\t\t\t\tVersionEndExcluding:   \"2.1\",\n\t\t\t},\n\t\t\texpected: \">= 1.0, < 2.1\",\n\t\t},\n\t\t{\n\t\t\tname: \"range constraints overrides exact version\",\n\t\t\tinput: affectedCPERange{\n\t\t\t\tExactVersion:          \"1.5\",\n\t\t\t\tExactUpdate:           \"p2\",\n\t\t\t\tVersionStartIncluding: \"1.0\",\n\t\t\t\tVersionEndExcluding:   \"2.0\",\n\t\t\t},\n\t\t\texpected: \">= 1.0, < 2.0\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tactual := tt.input.String()\n\n\t\t\tif diff := cmp.Diff(tt.expected, actual); diff != \"\" {\n\t\t\t\tt.Errorf(\"buildConstraints() mismatch (-want +got):\\n%s\", diff)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_newAffectedRange(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tmatch    nvd.CpeMatch\n\t\texpected affectedCPERange\n\t}{\n\t\t{\n\t\t\tname: \"basic range without fix info\",\n\t\t\tmatch: nvd.CpeMatch{\n\t\t\t\tVersionStartIncluding: stringPtr(\"1.0\"),\n\t\t\t\tVersionEndExcluding:   stringPtr(\"2.0\"),\n\t\t\t},\n\t\t\texpected: affectedCPERange{\n\t\t\t\tVersionStartIncluding: \"1.0\",\n\t\t\t\tVersionEndExcluding:   \"2.0\",\n\t\t\t\tFixInfo:               nil,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"range with fix info\",\n\t\t\tmatch: nvd.CpeMatch{\n\t\t\t\tVersionStartIncluding: stringPtr(\"1.0\"),\n\t\t\t\tVersionEndExcluding:   stringPtr(\"2.0\"),\n\t\t\t\tFix: &nvd.FixInfo{\n\t\t\t\t\tVersion: \"2.0\",\n\t\t\t\t\tDate:    \"2023-06-15\",\n\t\t\t\t\tKind:    \"advisory\",\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: affectedCPERange{\n\t\t\t\tVersionStartIncluding: \"1.0\",\n\t\t\t\tVersionEndExcluding:   \"2.0\",\n\t\t\t\tFixInfo: &nvd.FixInfo{\n\t\t\t\t\tVersion: \"2.0\",\n\t\t\t\t\tDate:    \"2023-06-15\",\n\t\t\t\t\tKind:    \"advisory\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tactual := newAffectedRange(tt.match)\n\t\t\tif diff := cmp.Diff(tt.expected, actual); diff != \"\" {\n\t\t\t\tt.Errorf(\"newAffectedRange() mismatch (-want +got):\\n%s\", diff)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc stringPtr(s string) *string {\n\treturn &s\n}\n"
  },
  {
    "path": "grype/db/v6/build/transformers/nvd/node.go",
    "content": "package nvd\n\nimport (\n\t\"fmt\"\n\t\"sort\"\n\t\"strings\"\n\n\t\"github.com/anchore/grype/grype/db/internal/provider/unmarshal/nvd\"\n\t\"github.com/anchore/grype/internal/log\"\n\t\"github.com/anchore/syft/syft/cpe\"\n)\n\ntype affectedPackageCandidate struct {\n\tVulnerableCPE cpe.Attributes\n\tPlatformCPEs  []cpe.Attributes\n\tRanges        affectedRangeSet\n}\n\nfunc allCandidates(cve string, configs []nvd.Configuration, cfg Config) ([]affectedPackageCandidate, error) {\n\tvar candidates []affectedPackageCandidate\n\n\tfor _, config := range configs {\n\t\tcs, err := processConfiguration(cve, config, cfg)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tcandidates = append(candidates, cs...)\n\t}\n\n\treturn deduplicateCandidates(candidates), nil\n}\n\n// processConfiguration processes a configuration recursively\nfunc processConfiguration(cve string, config nvd.Configuration, cfg Config) ([]affectedPackageCandidate, error) {\n\tvar opPtr = config.Operator\n\tvar op nvd.Operator\n\tif opPtr != nil {\n\t\top = *opPtr\n\t} else {\n\t\top = nvd.Or\n\t}\n\n\tif op == nvd.And {\n\t\treturn processANDNodes(cve, config.Nodes, cfg, 0)\n\t}\n\treturn processORNodes(cve, config.Nodes, cfg, 0)\n}\n\n// processANDNodes handles AND configurations\nfunc processANDNodes(cve string, nodes []nvd.Node, cfg Config, depth int) ([]affectedPackageCandidate, error) {\n\tdepth++\n\tif depth > 2 {\n\t\tlog.WithFields(\"depth\", depth, \"cve\", cve, \"operator\", \"and\").Warn(\"unexpected NVD node configuration depth\")\n\t}\n\tvar candidates []affectedPackageCandidate\n\n\t// find all vulnerable CPEs and all platform CPEs across all nodes\n\tvar allVulnerableCPEs []affectedPackageCandidate\n\tvar allPlatformCPEs []cpe.Attributes\n\n\tfor _, node := range nodes {\n\t\tswitch node.Operator {\n\t\tcase nvd.Or:\n\t\t\tvulnCPEs, err := extractVulnerableCPEs(node, cfg)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tallVulnerableCPEs = append(allVulnerableCPEs, vulnCPEs...)\n\t\t\tplatformCPEs, err := extractPlatformCPEs(node)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tallPlatformCPEs = append(allPlatformCPEs, platformCPEs...)\n\t\tcase nvd.And:\n\t\t\t// TODO: when we're processing AND'd nodes at this depth this tends to mean that all the given CPEs must\n\t\t\t// be present in the environment for the vulnerability to be applicable. This isn't something we can\n\t\t\t// express as a single affected package in grype today. We should consider how to handle this case in\n\t\t\t// the future.\n\t\t\tvar names []string\n\t\t\tfor _, match := range node.CpeMatch {\n\t\t\t\tshort := strings.ReplaceAll(strings.ReplaceAll(match.Criteria, \":*\", \"\"), \":-\", \"\")\n\t\t\t\tpostfix := \"\"\n\t\t\t\tif !match.Vulnerable {\n\t\t\t\t\tpostfix = \" (not vulnerable)\"\n\t\t\t\t}\n\t\t\t\tnames = append(names, fmt.Sprintf(\"%q%s\", short, postfix))\n\t\t\t}\n\t\t\tlog.WithFields(\"cve\", cve, \"criteria\", strings.Join(names, \" AND \")).Warnf(\"unsupported NVD node configuration (dropping criteria)\")\n\t\t}\n\t}\n\n\t// deduplicate CPEs\n\tuniqueVulnCPEs := make(map[string]affectedPackageCandidate)\n\tfor _, c := range allVulnerableCPEs {\n\t\tcKey := cpeKey(c.VulnerableCPE)\n\t\tif _, exists := uniqueVulnCPEs[cKey]; !exists {\n\t\t\tuniqueVulnCPEs[cKey] = c\n\t\t} else {\n\t\t\tuniqueVulnCPEs[cKey].Ranges.addRanges(c.Ranges.toSlice()...)\n\t\t}\n\t}\n\n\t// combine all unique vulnerable CPEs with their associated ranges\n\tfor _, vulnCPE := range uniqueVulnCPEs {\n\t\tif len(allPlatformCPEs) == 0 {\n\t\t\t// no platform constraints, app is vulnerable on all platforms\n\t\t\tcandidates = append(candidates, vulnCPE)\n\t\t} else {\n\t\t\t// associate this vulnerable CPE with all platform CPEs\n\t\t\tvulnCPE.PlatformCPEs = allPlatformCPEs\n\t\t\tcandidates = append(candidates, vulnCPE)\n\t\t}\n\t}\n\n\treturn candidates, nil\n}\n\n// processORNodes handles OR configurations\nfunc processORNodes(cve string, nodes []nvd.Node, cfg Config, depth int) ([]affectedPackageCandidate, error) {\n\tdepth++\n\tif depth > 2 {\n\t\tlog.WithFields(\"depth\", depth, \"cve\", cve, \"operator\", \"or\").Warnf(\"unexpected NVD node configuration depth\")\n\t}\n\tvar candidates []affectedPackageCandidate\n\n\tfor _, node := range nodes {\n\t\tswitch node.Operator {\n\t\tcase nvd.And:\n\t\t\tandCandidates, err := processANDNodes(cve, []nvd.Node{node}, cfg, depth)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tcandidates = append(candidates, andCandidates...)\n\t\tcase nvd.Or:\n\t\t\tvulnCPEs, err := extractVulnerableCPEs(node, cfg)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tcandidates = append(candidates, vulnCPEs...)\n\t\t}\n\t}\n\n\treturn candidates, nil\n}\n\nfunc deduplicateCandidates(candidates []affectedPackageCandidate) []affectedPackageCandidate {\n\tcandidateMap := make(map[string]*affectedPackageCandidate)\n\n\tfor _, candidate := range candidates {\n\t\tkey := cpeKey(candidate.VulnerableCPE)\n\n\t\texisting, exists := candidateMap[key]\n\t\tif !exists {\n\t\t\tnewCandidate := candidate\n\t\t\tcandidateMap[key] = &newCandidate\n\t\t\tcontinue\n\t\t}\n\n\t\t// merge platform CPEs...\n\t\tplatformMap := make(map[string]struct{})\n\t\tfor _, platform := range existing.PlatformCPEs {\n\t\t\tplatformKey := cpeKey(platform)\n\t\t\tplatformMap[platformKey] = struct{}{}\n\t\t}\n\n\t\tfor _, platform := range candidate.PlatformCPEs {\n\t\t\tplatformKey := cpeKey(platform)\n\t\t\tif _, ok := platformMap[platformKey]; !ok {\n\t\t\t\texisting.PlatformCPEs = append(existing.PlatformCPEs, platform)\n\t\t\t\tplatformMap[platformKey] = struct{}{}\n\t\t\t}\n\t\t}\n\n\t\t// merge ranges...\n\t\texisting.Ranges.addRanges(candidate.Ranges.toSlice()...)\n\t}\n\n\tvar result []affectedPackageCandidate\n\tfor _, candidate := range candidateMap {\n\t\tif len(candidate.Ranges) == 0 {\n\t\t\tcandidate.Ranges.addRanges(deriveRangesFromCPE(candidate.VulnerableCPE)...)\n\t\t}\n\t\tresult = append(result, *candidate)\n\t}\n\n\t// sort the slice for deterministic output\n\tsort.Slice(result, func(i, j int) bool {\n\t\treturn result[i].VulnerableCPE.String() < result[j].VulnerableCPE.String()\n\t})\n\n\treturn result\n}\n\nfunc deriveRangesFromCPE(attr cpe.Attributes) []affectedCPERange {\n\tif attr.Version == cpe.Any {\n\t\treturn nil\n\t}\n\n\tvar update string\n\tif attr.Update != \"-\" {\n\t\tupdate = attr.Update\n\t}\n\n\treturn []affectedCPERange{\n\t\t{\n\t\t\tExactVersion: attr.Version,\n\t\t\tExactUpdate:  update,\n\t\t},\n\t}\n}\n\n// extractVulnerableCPEs extracts CPES that are both within the CPE part configuration and are explicitly marked as vulnerable\nfunc extractVulnerableCPEs(node nvd.Node, cfg Config) ([]affectedPackageCandidate, error) {\n\tvar candidates []affectedPackageCandidate\n\n\tfor _, match := range node.CpeMatch {\n\t\tif !match.Vulnerable {\n\t\t\tcontinue\n\t\t}\n\n\t\tcpeAttr, err := cpe.NewAttributes(match.Criteria)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"unable to parse CPE '%s': %w\", match.Criteria, err)\n\t\t}\n\n\t\t// check if this CPE part is in our configured set of parts to process, if not then it should not be considered\n\t\t// as an affected package at all\n\t\tif !cfg.CPEParts.Has(cpeAttr.Part) {\n\t\t\tcontinue\n\t\t}\n\n\t\tcandidate := affectedPackageCandidate{\n\t\t\tVulnerableCPE: cpeAttr,\n\t\t}\n\n\t\tif match.VersionStartIncluding != nil || match.VersionStartExcluding != nil ||\n\t\t\tmatch.VersionEndIncluding != nil || match.VersionEndExcluding != nil {\n\t\t\tcandidate.Ranges = newAffectedRanges(newAffectedRange(match))\n\t\t} else {\n\t\t\t// no explicit version ranges in the match, check the CPE attributes for an exact version\n\t\t\tcandidate.Ranges = newAffectedRanges(deriveRangesFromCPE(cpeAttr)...)\n\t\t}\n\n\t\tcandidates = append(candidates, candidate)\n\t}\n\n\treturn candidates, nil\n}\n\n// extractPlatformCPEs extracts all platform CPEs from a node (explicitly non-vulnerable CPEs). Why not just\n// use the part indication (i.e. 'h' & 'o' are platform and 'a' is the vulnerable candidate)? Because you can\n// find cases where an application is the platform (e.g. kubernetes or openshift).\nfunc extractPlatformCPEs(node nvd.Node) ([]cpe.Attributes, error) {\n\tvar platformCPEs []cpe.Attributes\n\n\tfor _, match := range node.CpeMatch {\n\t\tcpeAttr, err := cpe.NewAttributes(match.Criteria)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"unable to parse CPE '%s': %w\", match.Criteria, err)\n\t\t}\n\n\t\tif !match.Vulnerable {\n\t\t\tplatformCPEs = append(platformCPEs, cpeAttr)\n\t\t}\n\t}\n\n\treturn platformCPEs, nil\n}\n\n// cpeKey generates a unique key for a CPE (everything except for the version and update)\nfunc cpeKey(cpe cpe.Attributes) string {\n\treturn fmt.Sprintf(\"%s|%s|%s|%s|%s|%s|%s|%s|%s\", cpe.Part, cpe.Vendor, cpe.Product, cpe.Edition, cpe.SWEdition, cpe.TargetSW, cpe.TargetHW, cpe.Other, cpe.Language)\n}\n"
  },
  {
    "path": "grype/db/v6/build/transformers/nvd/node_test.go",
    "content": "package nvd\n\nimport (\n\t\"testing\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/syft/syft/cpe\"\n)\n\nfunc TestDeduplicateCandidates(t *testing.T) {\n\taVendorProduct1 := cpe.Attributes{\n\t\tPart:    \"a\",\n\t\tVendor:  \"vendor1\",\n\t\tProduct: \"product1\",\n\t}\n\n\taVendorProduct2 := cpe.Attributes{\n\t\tPart:    \"a\",\n\t\tVendor:  \"vendor2\",\n\t\tProduct: \"product2\",\n\t}\n\n\tosProduct1 := cpe.Attributes{\n\t\tPart:    \"o\",\n\t\tVendor:  \"os1\",\n\t\tProduct: \"os1product\",\n\t}\n\n\tosProduct2 := cpe.Attributes{\n\n\t\tPart:    \"o\",\n\t\tVendor:  \"os2\",\n\t\tProduct: \"os2product\",\n\t}\n\n\ttests := []struct {\n\t\tname     string\n\t\tinput    []affectedPackageCandidate\n\t\texpected []affectedPackageCandidate\n\t}{\n\t\t{\n\t\t\tname:     \"empty input\",\n\t\t\tinput:    []affectedPackageCandidate{},\n\t\t\texpected: nil,\n\t\t},\n\t\t{\n\t\t\tname: \"go case\",\n\t\t\tinput: []affectedPackageCandidate{\n\t\t\t\t{\n\t\t\t\t\tVulnerableCPE: aVendorProduct1,\n\t\t\t\t\tRanges: newAffectedRanges(affectedCPERange{\n\t\t\t\t\t\tExactVersion: \"1.0\",\n\t\t\t\t\t}),\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: []affectedPackageCandidate{\n\t\t\t\t{\n\t\t\t\t\tVulnerableCPE: aVendorProduct1,\n\t\t\t\t\tRanges: newAffectedRanges(affectedCPERange{\n\t\t\t\t\t\tExactVersion: \"1.0\",\n\t\t\t\t\t}),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"deduplicate identical candidates\",\n\t\t\tinput: []affectedPackageCandidate{\n\t\t\t\t{\n\t\t\t\t\tVulnerableCPE: aVendorProduct1,\n\t\t\t\t\tRanges: newAffectedRanges(affectedCPERange{\n\t\t\t\t\t\tExactVersion: \"1.0\",\n\t\t\t\t\t}),\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tVulnerableCPE: aVendorProduct1,\n\t\t\t\t\tRanges: newAffectedRanges(affectedCPERange{\n\t\t\t\t\t\tExactVersion: \"1.0\",\n\t\t\t\t\t}),\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: []affectedPackageCandidate{\n\t\t\t\t{\n\t\t\t\t\tVulnerableCPE: aVendorProduct1,\n\t\t\t\t\tRanges: newAffectedRanges(affectedCPERange{\n\t\t\t\t\t\tExactVersion: \"1.0\",\n\t\t\t\t\t}),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"merge ranges for same CPE\",\n\t\t\tinput: []affectedPackageCandidate{\n\t\t\t\t{\n\t\t\t\t\tVulnerableCPE: aVendorProduct1,\n\t\t\t\t\tRanges: newAffectedRanges(affectedCPERange{\n\t\t\t\t\t\tExactVersion: \"1.0\",\n\t\t\t\t\t}),\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tVulnerableCPE: aVendorProduct1,\n\t\t\t\t\tRanges: newAffectedRanges(affectedCPERange{\n\t\t\t\t\t\tExactVersion: \"2.0\",\n\t\t\t\t\t}),\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: []affectedPackageCandidate{\n\t\t\t\t{\n\t\t\t\t\tVulnerableCPE: aVendorProduct1,\n\t\t\t\t\tRanges: newAffectedRanges(\n\t\t\t\t\t\taffectedCPERange{ExactVersion: \"1.0\"},\n\t\t\t\t\t\taffectedCPERange{ExactVersion: \"2.0\"},\n\t\t\t\t\t),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"merge platform CPEs for same vulnerable CPE\",\n\t\t\tinput: []affectedPackageCandidate{\n\t\t\t\t{\n\t\t\t\t\tVulnerableCPE: aVendorProduct1,\n\t\t\t\t\tPlatformCPEs: []cpe.Attributes{\n\t\t\t\t\t\tosProduct1,\n\t\t\t\t\t},\n\t\t\t\t\tRanges: newAffectedRanges(affectedCPERange{\n\t\t\t\t\t\tExactVersion: \"1.0\",\n\t\t\t\t\t}),\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tVulnerableCPE: aVendorProduct1,\n\t\t\t\t\tPlatformCPEs: []cpe.Attributes{\n\t\t\t\t\t\tosProduct2,\n\t\t\t\t\t},\n\t\t\t\t\tRanges: newAffectedRanges(affectedCPERange{\n\t\t\t\t\t\tExactVersion: \"1.0\",\n\t\t\t\t\t}),\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: []affectedPackageCandidate{\n\t\t\t\t{\n\t\t\t\t\tVulnerableCPE: aVendorProduct1,\n\t\t\t\t\tPlatformCPEs: []cpe.Attributes{\n\t\t\t\t\t\tosProduct1,\n\t\t\t\t\t\tosProduct2,\n\t\t\t\t\t},\n\t\t\t\t\tRanges: newAffectedRanges(affectedCPERange{\n\t\t\t\t\t\tExactVersion: \"1.0\",\n\t\t\t\t\t}),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"different CPEs not deduplicated\",\n\t\t\tinput: []affectedPackageCandidate{\n\t\t\t\t{\n\t\t\t\t\tVulnerableCPE: aVendorProduct1,\n\t\t\t\t\tRanges: newAffectedRanges(affectedCPERange{\n\t\t\t\t\t\tExactVersion: \"1.0\",\n\t\t\t\t\t}),\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tVulnerableCPE: aVendorProduct2,\n\t\t\t\t\tRanges: newAffectedRanges(affectedCPERange{\n\t\t\t\t\t\tExactVersion: \"2.0\",\n\t\t\t\t\t}),\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: []affectedPackageCandidate{\n\t\t\t\t{\n\t\t\t\t\tVulnerableCPE: aVendorProduct1,\n\t\t\t\t\tRanges: newAffectedRanges(affectedCPERange{\n\t\t\t\t\t\tExactVersion: \"1.0\",\n\t\t\t\t\t}),\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tVulnerableCPE: aVendorProduct2,\n\t\t\t\t\tRanges: newAffectedRanges(affectedCPERange{\n\t\t\t\t\t\tExactVersion: \"2.0\",\n\t\t\t\t\t}),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"deduplicate based on target software\",\n\t\t\tinput: []affectedPackageCandidate{\n\t\t\t\t{\n\t\t\t\t\tVulnerableCPE: cpe.Attributes{\n\t\t\t\t\t\tPart:     \"a\",\n\t\t\t\t\t\tVendor:   \"vendor\",\n\t\t\t\t\t\tProduct:  \"product\",\n\t\t\t\t\t\tTargetSW: \"target1\",\n\t\t\t\t\t},\n\t\t\t\t\tRanges: newAffectedRanges(affectedCPERange{\n\t\t\t\t\t\tExactVersion: \"1.0\",\n\t\t\t\t\t}),\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tVulnerableCPE: cpe.Attributes{\n\t\t\t\t\t\tPart:     \"a\",\n\t\t\t\t\t\tVendor:   \"vendor\",\n\t\t\t\t\t\tProduct:  \"product\",\n\t\t\t\t\t\tTargetSW: \"target2\",\n\t\t\t\t\t},\n\t\t\t\t\tRanges: newAffectedRanges(affectedCPERange{\n\t\t\t\t\t\tExactVersion: \"1.0\",\n\t\t\t\t\t}),\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: []affectedPackageCandidate{\n\t\t\t\t{\n\t\t\t\t\tVulnerableCPE: cpe.Attributes{\n\t\t\t\t\t\tPart:     \"a\",\n\t\t\t\t\t\tVendor:   \"vendor\",\n\t\t\t\t\t\tProduct:  \"product\",\n\t\t\t\t\t\tTargetSW: \"target1\",\n\t\t\t\t\t},\n\t\t\t\t\tRanges: newAffectedRanges(affectedCPERange{\n\t\t\t\t\t\tExactVersion: \"1.0\",\n\t\t\t\t\t}),\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tVulnerableCPE: cpe.Attributes{\n\t\t\t\t\t\tPart:     \"a\",\n\t\t\t\t\t\tVendor:   \"vendor\",\n\t\t\t\t\t\tProduct:  \"product\",\n\t\t\t\t\t\tTargetSW: \"target2\",\n\t\t\t\t\t},\n\t\t\t\t\tRanges: newAffectedRanges(affectedCPERange{\n\t\t\t\t\t\tExactVersion: \"1.0\",\n\t\t\t\t\t}),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"derive ranges when none specified\",\n\t\t\tinput: []affectedPackageCandidate{\n\t\t\t\t{\n\t\t\t\t\tVulnerableCPE: cpe.Attributes{\n\t\t\t\t\t\tPart:    \"a\",\n\t\t\t\t\t\tVendor:  \"vendor\",\n\t\t\t\t\t\tProduct: \"product\",\n\t\t\t\t\t\tVersion: \"3.0\",\n\t\t\t\t\t\tUpdate:  \"p2\",\n\t\t\t\t\t},\n\t\t\t\t\tRanges: newAffectedRanges(),\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: []affectedPackageCandidate{\n\t\t\t\t{\n\t\t\t\t\tVulnerableCPE: cpe.Attributes{\n\t\t\t\t\t\tPart:    \"a\",\n\t\t\t\t\t\tVendor:  \"vendor\",\n\t\t\t\t\t\tProduct: \"product\",\n\t\t\t\t\t\tVersion: \"3.0\",\n\t\t\t\t\t\tUpdate:  \"p2\",\n\t\t\t\t\t},\n\t\t\t\t\tRanges: newAffectedRanges(affectedCPERange{\n\t\t\t\t\t\tExactVersion: \"3.0\",\n\t\t\t\t\t\tExactUpdate:  \"p2\",\n\t\t\t\t\t}),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"derive ranges for one candidate but not others\",\n\t\t\tinput: []affectedPackageCandidate{\n\t\t\t\t{\n\t\t\t\t\tVulnerableCPE: cpe.Attributes{\n\t\t\t\t\t\tPart:    \"a\",\n\t\t\t\t\t\tVendor:  \"vendor\",\n\t\t\t\t\t\tProduct: \"product1\",\n\t\t\t\t\t\tVersion: \"3.0\",\n\t\t\t\t\t},\n\t\t\t\t\tRanges: newAffectedRanges(),\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tVulnerableCPE: cpe.Attributes{\n\t\t\t\t\t\tPart:    \"a\",\n\t\t\t\t\t\tVendor:  \"vendor\",\n\t\t\t\t\t\tProduct: \"product2\",\n\t\t\t\t\t},\n\t\t\t\t\tRanges: newAffectedRanges(affectedCPERange{\n\t\t\t\t\t\tExactVersion: \"1.0\",\n\t\t\t\t\t}),\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: []affectedPackageCandidate{\n\t\t\t\t{\n\t\t\t\t\tVulnerableCPE: cpe.Attributes{\n\t\t\t\t\t\tPart:    \"a\",\n\t\t\t\t\t\tVendor:  \"vendor\",\n\t\t\t\t\t\tProduct: \"product1\",\n\t\t\t\t\t\tVersion: \"3.0\",\n\t\t\t\t\t},\n\t\t\t\t\tRanges: newAffectedRanges(affectedCPERange{\n\t\t\t\t\t\tExactVersion: \"3.0\",\n\t\t\t\t\t}),\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tVulnerableCPE: cpe.Attributes{\n\t\t\t\t\t\tPart:    \"a\",\n\t\t\t\t\t\tVendor:  \"vendor\",\n\t\t\t\t\t\tProduct: \"product2\",\n\t\t\t\t\t},\n\t\t\t\t\tRanges: newAffectedRanges(affectedCPERange{\n\t\t\t\t\t\tExactVersion: \"1.0\",\n\t\t\t\t\t}),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"complex case with mixed input\",\n\t\t\tinput: []affectedPackageCandidate{\n\t\t\t\t{\n\t\t\t\t\tVulnerableCPE: cpe.Attributes{\n\t\t\t\t\t\tPart:      \"a\",\n\t\t\t\t\t\tVendor:    \"vendor\",\n\t\t\t\t\t\tProduct:   \"product\",\n\t\t\t\t\t\tVersion:   \"1.0\",\n\t\t\t\t\t\tSWEdition: \"enterprise\",\n\t\t\t\t\t},\n\t\t\t\t\tPlatformCPEs: []cpe.Attributes{\n\t\t\t\t\t\tosProduct1,\n\t\t\t\t\t},\n\t\t\t\t\tRanges: newAffectedRanges(affectedCPERange{\n\t\t\t\t\t\tExactVersion: \"1.0\",\n\t\t\t\t\t}),\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tVulnerableCPE: cpe.Attributes{\n\t\t\t\t\t\tPart:      \"a\",\n\t\t\t\t\t\tVendor:    \"vendor\",\n\t\t\t\t\t\tProduct:   \"product\",\n\t\t\t\t\t\tVersion:   \"1.0\",\n\t\t\t\t\t\tSWEdition: \"enterprise\",\n\t\t\t\t\t},\n\t\t\t\t\tPlatformCPEs: []cpe.Attributes{\n\t\t\t\t\t\tosProduct2,\n\t\t\t\t\t},\n\t\t\t\t\tRanges: newAffectedRanges(affectedCPERange{\n\t\t\t\t\t\tVersionStartIncluding: \"1.0\",\n\t\t\t\t\t\tVersionEndExcluding:   \"2.0\",\n\t\t\t\t\t}),\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tVulnerableCPE: cpe.Attributes{\n\t\t\t\t\t\tPart:      \"a\",\n\t\t\t\t\t\tVendor:    \"vendor\",\n\t\t\t\t\t\tProduct:   \"product\",\n\t\t\t\t\t\tVersion:   \"1.0\",\n\t\t\t\t\t\tSWEdition: \"community\",\n\t\t\t\t\t},\n\t\t\t\t\tRanges: newAffectedRanges(affectedCPERange{\n\t\t\t\t\t\tExactVersion: \"1.0\",\n\t\t\t\t\t}),\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: []affectedPackageCandidate{\n\t\t\t\t{\n\t\t\t\t\tVulnerableCPE: cpe.Attributes{\n\t\t\t\t\t\tPart:      \"a\",\n\t\t\t\t\t\tVendor:    \"vendor\",\n\t\t\t\t\t\tProduct:   \"product\",\n\t\t\t\t\t\tVersion:   \"1.0\",\n\t\t\t\t\t\tSWEdition: \"community\",\n\t\t\t\t\t},\n\t\t\t\t\tRanges: newAffectedRanges(affectedCPERange{\n\t\t\t\t\t\tExactVersion: \"1.0\",\n\t\t\t\t\t}),\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tVulnerableCPE: cpe.Attributes{\n\t\t\t\t\t\tPart:      \"a\",\n\t\t\t\t\t\tVendor:    \"vendor\",\n\t\t\t\t\t\tProduct:   \"product\",\n\t\t\t\t\t\tVersion:   \"1.0\",\n\t\t\t\t\t\tSWEdition: \"enterprise\",\n\t\t\t\t\t},\n\t\t\t\t\tPlatformCPEs: []cpe.Attributes{\n\t\t\t\t\t\tosProduct1,\n\t\t\t\t\t\tosProduct2,\n\t\t\t\t\t},\n\t\t\t\t\tRanges: newAffectedRanges(\n\t\t\t\t\t\taffectedCPERange{\n\t\t\t\t\t\t\tExactVersion: \"1.0\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\taffectedCPERange{\n\t\t\t\t\t\t\tVersionStartIncluding: \"1.0\",\n\t\t\t\t\t\t\tVersionEndExcluding:   \"2.0\",\n\t\t\t\t\t\t},\n\t\t\t\t\t),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tactual := deduplicateCandidates(tt.input)\n\n\t\t\tif diff := cmp.Diff(tt.expected, actual); diff != \"\" {\n\t\t\t\tt.Errorf(\"deduplicateCandidates() mismatch (-want +got):\\n%s\", diff)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDeduplicateCandidates_SensitiveToAllCPEFields(t *testing.T) {\n\tbase := cpe.Attributes{\n\t\tPart:      \"a\",\n\t\tVendor:    \"vendor\",\n\t\tProduct:   \"product\",\n\t\tVersion:   \"1.0\",\n\t\tUpdate:    \"update\",\n\t\tEdition:   \"edition\",\n\t\tSWEdition: \"sw-edition\",\n\t\tTargetSW:  \"target-sw\",\n\t\tTargetHW:  \"target-hw\",\n\t\tLanguage:  \"lang\",\n\t\tOther:     \"other\",\n\t}\n\n\t// note: we do not care about version and update fields for this part of the test...\n\tfor field, mutate := range map[string]func(cpe.Attributes) cpe.Attributes{\n\t\t\"Part\":      func(c cpe.Attributes) cpe.Attributes { c.Part = \"h\"; return c },\n\t\t\"Vendor\":    func(c cpe.Attributes) cpe.Attributes { c.Vendor = \"other-vendor\"; return c },\n\t\t\"Product\":   func(c cpe.Attributes) cpe.Attributes { c.Product = \"other-product\"; return c },\n\t\t\"Edition\":   func(c cpe.Attributes) cpe.Attributes { c.Edition = \"other-edition\"; return c },\n\t\t\"SWEdition\": func(c cpe.Attributes) cpe.Attributes { c.SWEdition = \"other-sw-edition\"; return c },\n\t\t\"TargetSW\":  func(c cpe.Attributes) cpe.Attributes { c.TargetSW = \"other-target-sw\"; return c },\n\t\t\"TargetHW\":  func(c cpe.Attributes) cpe.Attributes { c.TargetHW = \"other-target-hw\"; return c },\n\t\t\"Language\":  func(c cpe.Attributes) cpe.Attributes { c.Language = \"other-lang\"; return c },\n\t\t\"Other\":     func(c cpe.Attributes) cpe.Attributes { c.Other = \"other-other\"; return c },\n\t} {\n\t\tt.Run(\"field=\"+field, func(t *testing.T) {\n\t\t\ta := affectedPackageCandidate{VulnerableCPE: base, Ranges: newAffectedRanges()}\n\t\t\tb := affectedPackageCandidate{VulnerableCPE: mutate(base), Ranges: newAffectedRanges()}\n\t\t\tresult := deduplicateCandidates([]affectedPackageCandidate{a, b})\n\t\t\trequire.Len(t, result, 2, \"field %s should cause deduplication to treat entries as separate\", field)\n\t\t})\n\t}\n\n\t// now that all other fields have been tested, prove that we do not care about version and update fields...\n\tt.Run(\"Version and Update do not matter\", func(t *testing.T) {\n\t\tc1 := base\n\t\tc1.Version = \"1.0\"\n\t\tc1.Update = \"u1\"\n\n\t\tc2 := base\n\t\tc2.Version = \"2.0\"\n\t\tc2.Update = \"u2\"\n\n\t\ta := affectedPackageCandidate{VulnerableCPE: c1, Ranges: newAffectedRanges(affectedCPERange{ExactVersion: \"1.0\"})}\n\t\tb := affectedPackageCandidate{VulnerableCPE: c2, Ranges: newAffectedRanges(affectedCPERange{ExactVersion: \"2.0\"})}\n\n\t\tresult := deduplicateCandidates([]affectedPackageCandidate{a, b})\n\t\trequire.Len(t, result, 1)\n\t\trequire.Len(t, result[0].Ranges, 2)\n\t})\n}\n"
  },
  {
    "path": "grype/db/v6/build/transformers/nvd/testdata/CVE-2004-0377.json",
    "content": "{\n    \"cve\": {\n      \"id\": \"CVE-2004-0377\",\n      \"sourceIdentifier\": \"cve@mitre.org\",\n      \"published\": \"2004-05-04T04:00:00.000\",\n      \"lastModified\": \"2025-04-03T01:03:51.193\",\n      \"vulnStatus\": \"Deferred\",\n      \"cveTags\": [],\n      \"descriptions\": [\n        {\n          \"lang\": \"en\",\n          \"value\": \"Buffer overflow in the win32_stat function for (1) ActiveState's ActivePerl and (2) Larry Wall's Perl before 5.8.3 allows local or remote attackers to execute arbitrary commands via filenames that end in a backslash character.\"\n        },\n        {\n          \"lang\": \"es\",\n          \"value\": \"Desbordamiento de búfer en la función win32_stat de \\r\\n\\r\\nActivePerl de ActiveState, y \\r\\nPerl de Larry Wall anterior a 5.8.3\\r\\n\\r\\npermite a atacantes remotos ejecutar comandos arbitrarios mediante nombres de fichero que terminan en un carácter \\\"\\\" (barra invertida).\"\n        }\n      ],\n      \"metrics\": {},\n      \"weaknesses\": [\n        {\n          \"source\": \"nvd@nist.gov\",\n          \"type\": \"Primary\",\n          \"description\": [\n            {\n              \"lang\": \"en\",\n              \"value\": \"NVD-CWE-Other\"\n            }\n          ]\n        }\n      ],\n      \"configurations\": [\n        {\n          \"nodes\": [\n            {\n              \"operator\": \"OR\",\n              \"negate\": false,\n              \"cpeMatch\": [\n                {\n                  \"vulnerable\": true,\n                  \"criteria\": \"cpe:2.3:a:activestate:activeperl:*:*:*:*:*:*:*:*\",\n                  \"matchCriteriaId\": \"86BADAC0-8A7B-4348-A78C-BAAFD8A784FE\"\n                },\n                {\n                  \"vulnerable\": true,\n                  \"criteria\": \"cpe:2.3:a:larry_wall:perl:*:*:*:*:*:*:*:*\",\n                  \"versionEndIncluding\": \"5.8.3\",\n                  \"matchCriteriaId\": \"6851ACEC-141C-40B2-B6E1-CD52D979CE37\"\n                }\n              ]\n            }\n          ]\n        }\n      ],\n      \"references\": []\n    }\n  }"
  },
  {
    "path": "grype/db/v6/build/transformers/nvd/testdata/CVE-2008-3442.json",
    "content": "{\n  \"cve\": {\n    \"id\": \"CVE-2008-3442\",\n    \"sourceIdentifier\": \"cve@mitre.org\",\n    \"published\": \"2008-08-01T14:41:00.000\",\n    \"lastModified\": \"2008-09-05T21:43:05.500\",\n    \"vulnStatus\": \"Analyzed\",\n    \"descriptions\": [\n      {\n        \"lang\": \"en\",\n        \"value\": \"desc.\"\n      },\n      {\n        \"lang\": \"es\",\n        \"value\": \"WinZip anterior a 11.0 no verifica adecuadamente la autenticidad de las actualizaciones, lo cual permite a atacantes de tipo 'hombre en el medio' (man-in-the-middle) ejecutar código de su elección a través de la actualización de un Caballo de Troya, que se manifiesta en el grado de daño y el envenenamiento de la caché DNS.\\r\\n\\r\\n\"\n      }\n    ],\n    \"metrics\": {\n      \"cvssMetricV2\": [ ]\n    },\n    \"weaknesses\": [\n      {\n        \"source\": \"nvd@nist.gov\",\n        \"type\": \"Primary\",\n        \"description\": [\n          {\n            \"lang\": \"en\",\n            \"value\": \"CWE-94\"\n          }\n        ]\n      }\n    ],\n    \"configurations\": [\n      {\n        \"nodes\": [\n          {\n            \"operator\": \"OR\",\n            \"negate\": false,\n            \"cpeMatch\": [\n              {\n                \"vulnerable\": true,\n                \"criteria\": \"cpe:2.3:a:winzip:winzip:7.0:*:*:*:*:*:*:*\",\n                \"matchCriteriaId\": \"A2ACBE01-B77A-4D09-8FB3-D6365786C44F\"\n              },\n              {\n                \"vulnerable\": true,\n                \"criteria\": \"cpe:2.3:a:winzip:winzip:8.0:*:*:*:*:*:*:*\",\n                \"matchCriteriaId\": \"FDE7DCD6-90B3-4259-9BE6-B9F7A30A64AF\"\n              },\n              {\n                \"vulnerable\": true,\n                \"criteria\": \"cpe:2.3:a:winzip:winzip:8.1:*:*:*:*:*:*:*\",\n                \"matchCriteriaId\": \"4088C545-249E-47AD-8BF8-A6A2E5B2BF18\"\n              },\n              {\n                \"vulnerable\": true,\n                \"criteria\": \"cpe:2.3:a:winzip:winzip:8.1:*:sr1:*:*:*:*:*\",\n                \"matchCriteriaId\": \"FD308C7B-E9F6-4874-965D-E4271CF360DF\"\n              },\n              {\n                \"vulnerable\": true,\n                \"criteria\": \"cpe:2.3:a:winzip:winzip:9.0:*:*:*:*:*:*:*\",\n                \"matchCriteriaId\": \"523ADB29-C3D5-4C06-89B6-22B5FC68C240\"\n              },\n              {\n                \"vulnerable\": true,\n                \"criteria\": \"cpe:2.3:a:winzip:winzip:9.0:*:sr1:*:*:*:*:*\",\n                \"matchCriteriaId\": \"C79A7C70-F1CE-448B-B980-FB976609C48D\"\n              },\n              {\n                \"vulnerable\": true,\n                \"criteria\": \"cpe:2.3:a:winzip:winzip:10.0:*:*:*:*:*:*:*\",\n                \"matchCriteriaId\": \"B4FD09AC-2C56-4DB1-B00A-903103B453AD\"\n              }\n            ]\n          }\n        ]\n      }\n    ],\n    \"references\": [\n    ]\n  }\n}"
  },
  {
    "path": "grype/db/v6/build/transformers/nvd/testdata/CVE-2023-45283-platform-cpe-first.json",
    "content": "{\n  \"cve\": {\n    \"id\": \"CVE-2023-45283\",\n    \"sourceIdentifier\": \"security@golang.org\",\n    \"published\": \"2023-11-09T17:15:08.757\",\n    \"lastModified\": \"2023-12-14T10:15:07.947\",\n    \"vulnStatus\": \"Modified\",\n    \"cveTags\": [],\n    \"descriptions\": [\n      {\n        \"lang\": \"en\",\n        \"value\": \"The filepath package does not recognize paths with a \\\\??\\\\ prefix as special. On Windows, a path beginning with \\\\??\\\\ is a Root Local Device path equivalent to a path beginning with \\\\\\\\?\\\\. Paths with a \\\\??\\\\ prefix may be used to access arbitrary locations on the system. For example, the path \\\\??\\\\c:\\\\x is equivalent to the more common path c:\\\\x. Before fix, Clean could convert a rooted path such as \\\\a\\\\..\\\\??\\\\b into the root local device path \\\\??\\\\b. Clean will now convert this to .\\\\??\\\\b. Similarly, Join(\\\\, ??, b) could convert a seemingly innocent sequence of path elements into the root local device path \\\\??\\\\b. Join will now convert this to \\\\.\\\\??\\\\b. In addition, with fix, IsAbs now correctly reports paths beginning with \\\\??\\\\ as absolute, and VolumeName correctly reports the \\\\??\\\\ prefix as a volume name. UPDATE: Go 1.20.11 and Go 1.21.4 inadvertently changed the definition of the volume name in Windows paths starting with \\\\?, resulting in filepath.Clean(\\\\?\\\\c:) returning \\\\?\\\\c: rather than \\\\?\\\\c:\\\\ (among other effects). The previous behavior has been restored.\"\n      },\n      {\n        \"lang\": \"es\",\n        \"value\": \"El paquete filepath no reconoce las rutas con el prefijo \\\\??\\\\ como especiales. En Windows, una ruta que comienza con \\\\??\\\\ es una ruta de dispositivo local raíz equivalente a una ruta que comienza con \\\\\\\\?\\\\. Se pueden utilizar rutas con un prefijo \\\\??\\\\ para acceder a ubicaciones arbitrarias en el sistema. Por ejemplo, la ruta \\\\??\\\\c:\\\\x es equivalente a la ruta más común c:\\\\x. Antes de la solución, Clean podía convertir una ruta raíz como \\\\a\\\\..\\\\??\\\\b en la ruta raíz del dispositivo local \\\\??\\\\b. Clean ahora convertirá esto a .\\\\??\\\\b. De manera similar, Join(\\\\, ??, b) podría convertir una secuencia aparentemente inocente de elementos de ruta en la ruta del dispositivo local raíz \\\\??\\\\b. Unirse ahora convertirá esto a \\\\.\\\\??\\\\b. Además, con la solución, IsAbs ahora informa correctamente las rutas que comienzan con \\\\??\\\\ como absolutas, y VolumeName informa correctamente el prefijo \\\\??\\\\ como nombre de volumen.\"\n      }\n    ],\n    \"metrics\": {\n      \"cvssMetricV31\": [\n        {\n          \"source\": \"nvd@nist.gov\",\n          \"type\": \"Primary\",\n          \"cvssData\": {\n            \"version\": \"3.1\",\n            \"vectorString\": \"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N\",\n            \"attackVector\": \"NETWORK\",\n            \"attackComplexity\": \"LOW\",\n            \"privilegesRequired\": \"NONE\",\n            \"userInteraction\": \"NONE\",\n            \"scope\": \"UNCHANGED\",\n            \"confidentialityImpact\": \"HIGH\",\n            \"integrityImpact\": \"NONE\",\n            \"availabilityImpact\": \"NONE\",\n            \"baseScore\": 7.5,\n            \"baseSeverity\": \"HIGH\"\n          },\n          \"exploitabilityScore\": 3.9,\n          \"impactScore\": 3.6\n        }\n      ]\n    },\n    \"weaknesses\": [\n      {\n        \"source\": \"nvd@nist.gov\",\n        \"type\": \"Primary\",\n        \"description\": [\n          {\n            \"lang\": \"en\",\n            \"value\": \"CWE-22\"\n          }\n        ]\n      }\n    ],\n    \"configurations\": [\n      {\n        \"operator\": \"AND\",\n        \"nodes\": [\n          {\n            \"operator\": \"OR\",\n            \"negate\": false,\n            \"cpeMatch\": [\n              {\n                \"vulnerable\": false,\n                \"criteria\": \"cpe:2.3:o:microsoft:windows:-:*:*:*:*:*:*:*\",\n                \"matchCriteriaId\": \"A2572D17-1DE6-457B-99CC-64AFD54487EA\"\n              }\n            ]\n          },\n          {\n            \"operator\": \"OR\",\n            \"negate\": false,\n            \"cpeMatch\": [\n              {\n                \"vulnerable\": true,\n                \"criteria\": \"cpe:2.3:a:golang:go:*:*:*:*:*:*:*:*\",\n                \"versionEndExcluding\": \"1.20.11\",\n                \"matchCriteriaId\": \"C1E7C289-7484-4AA8-A96B-07D2E2933258\"\n              },\n              {\n                \"vulnerable\": true,\n                \"criteria\": \"cpe:2.3:a:golang:go:*:*:*:*:*:*:*:*\",\n                \"versionStartIncluding\": \"1.21.0-0\",\n                \"versionEndExcluding\": \"1.21.4\",\n                \"matchCriteriaId\": \"4E3FC16C-41B2-4900-901F-48BDA3DC9ED2\"\n              }\n            ]\n          }\n        ]\n      }\n    ],\n    \"references\": [\n      {\n        \"url\": \"http://www.openwall.com/lists/oss-security/2023/12/05/2\",\n        \"source\": \"security@golang.org\"\n      },\n      {\n        \"url\": \"https://go.dev/cl/540277\",\n        \"source\": \"security@golang.org\",\n        \"tags\": [\n          \"Issue Tracking\",\n          \"Vendor Advisory\"\n        ]\n      },\n      {\n        \"url\": \"https://go.dev/cl/541175\",\n        \"source\": \"security@golang.org\"\n      },\n      {\n        \"url\": \"https://go.dev/issue/63713\",\n        \"source\": \"security@golang.org\",\n        \"tags\": [\n          \"Issue Tracking\",\n          \"Vendor Advisory\"\n        ]\n      },\n      {\n        \"url\": \"https://go.dev/issue/64028\",\n        \"source\": \"security@golang.org\"\n      },\n      {\n        \"url\": \"https://groups.google.com/g/golang-announce/c/4tU8LZfBFkY\",\n        \"source\": \"security@golang.org\",\n        \"tags\": [\n          \"Issue Tracking\",\n          \"Mailing List\",\n          \"Vendor Advisory\"\n        ]\n      },\n      {\n        \"url\": \"https://groups.google.com/g/golang-dev/c/6ypN5EjibjM/m/KmLVYH_uAgAJ\",\n        \"source\": \"security@golang.org\"\n      },\n      {\n        \"url\": \"https://pkg.go.dev/vuln/GO-2023-2185\",\n        \"source\": \"security@golang.org\",\n        \"tags\": [\n          \"Issue Tracking\",\n          \"Vendor Advisory\"\n        ]\n      },\n      {\n        \"url\": \"https://security.netapp.com/advisory/ntap-20231214-0008/\",\n        \"source\": \"security@golang.org\"\n      }\n    ]\n  }\n}"
  },
  {
    "path": "grype/db/v6/build/transformers/nvd/testdata/CVE-2023-45283-platform-cpe-last.json",
    "content": "{\n  \"cve\": {\n    \"id\": \"CVE-2023-45283\",\n    \"sourceIdentifier\": \"security@golang.org\",\n    \"published\": \"2023-11-09T17:15:08.757\",\n    \"lastModified\": \"2023-12-14T10:15:07.947\",\n    \"vulnStatus\": \"Modified\",\n    \"descriptions\": [\n      {\n        \"lang\": \"en\",\n        \"value\": \"The filepath package does not recognize paths with a \\\\??\\\\ prefix as special. On Windows, a path beginning with \\\\??\\\\ is a Root Local Device path equivalent to a path beginning with \\\\\\\\?\\\\. Paths with a \\\\??\\\\ prefix may be used to access arbitrary locations on the system. For example, the path \\\\??\\\\c:\\\\x is equivalent to the more common path c:\\\\x. Before fix, Clean could convert a rooted path such as \\\\a\\\\..\\\\??\\\\b into the root local device path \\\\??\\\\b. Clean will now convert this to .\\\\??\\\\b. Similarly, Join(\\\\, ??, b) could convert a seemingly innocent sequence of path elements into the root local device path \\\\??\\\\b. Join will now convert this to \\\\.\\\\??\\\\b. In addition, with fix, IsAbs now correctly reports paths beginning with \\\\??\\\\ as absolute, and VolumeName correctly reports the \\\\??\\\\ prefix as a volume name. UPDATE: Go 1.20.11 and Go 1.21.4 inadvertently changed the definition of the volume name in Windows paths starting with \\\\?, resulting in filepath.Clean(\\\\?\\\\c:) returning \\\\?\\\\c: rather than \\\\?\\\\c:\\\\ (among other effects). The previous behavior has been restored.\"\n      },\n      {\n        \"lang\": \"es\",\n        \"value\": \"El paquete filepath no reconoce las rutas con el prefijo \\\\??\\\\ como especiales. En Windows, una ruta que comienza con \\\\??\\\\ es una ruta de dispositivo local raíz equivalente a una ruta que comienza con \\\\\\\\?\\\\. Se pueden utilizar rutas con un prefijo \\\\??\\\\ para acceder a ubicaciones arbitrarias en el sistema. Por ejemplo, la ruta \\\\??\\\\c:\\\\x es equivalente a la ruta más común c:\\\\x. Antes de la solución, Clean podía convertir una ruta raíz como \\\\a\\\\..\\\\??\\\\b en la ruta raíz del dispositivo local \\\\??\\\\b. Clean ahora convertirá esto a .\\\\??\\\\b. De manera similar, Join(\\\\, ??, b) podría convertir una secuencia aparentemente inocente de elementos de ruta en la ruta del dispositivo local raíz \\\\??\\\\b. Unirse ahora convertirá esto a \\\\.\\\\??\\\\b. Además, con la solución, IsAbs ahora informa correctamente las rutas que comienzan con \\\\??\\\\ como absolutas, y VolumeName informa correctamente el prefijo \\\\??\\\\ como nombre de volumen.\"\n      }\n    ],\n    \"metrics\": {\n      \"cvssMetricV31\": [\n        {\n          \"source\": \"nvd@nist.gov\",\n          \"type\": \"Primary\",\n          \"cvssData\": {\n            \"version\": \"3.1\",\n            \"vectorString\": \"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N\",\n            \"attackVector\": \"NETWORK\",\n            \"attackComplexity\": \"LOW\",\n            \"privilegesRequired\": \"NONE\",\n            \"userInteraction\": \"NONE\",\n            \"scope\": \"UNCHANGED\",\n            \"confidentialityImpact\": \"HIGH\",\n            \"integrityImpact\": \"NONE\",\n            \"availabilityImpact\": \"NONE\",\n            \"baseScore\": 7.5,\n            \"baseSeverity\": \"HIGH\"\n          },\n          \"exploitabilityScore\": 3.9,\n          \"impactScore\": 3.6\n        }\n      ]\n    },\n    \"weaknesses\": [\n      {\n        \"source\": \"nvd@nist.gov\",\n        \"type\": \"Primary\",\n        \"description\": [\n          {\n            \"lang\": \"en\",\n            \"value\": \"CWE-22\"\n          }\n        ]\n      }\n    ],\n    \"configurations\": [\n      {\n        \"operator\": \"AND\",\n        \"nodes\": [\n          {\n            \"operator\": \"OR\",\n            \"negate\": false,\n            \"cpeMatch\": [\n              {\n                \"vulnerable\": true,\n                \"criteria\": \"cpe:2.3:a:golang:go:*:*:*:*:*:*:*:*\",\n                \"versionEndExcluding\": \"1.20.11\",\n                \"matchCriteriaId\": \"C1E7C289-7484-4AA8-A96B-07D2E2933258\"\n              },\n              {\n                \"vulnerable\": true,\n                \"criteria\": \"cpe:2.3:a:golang:go:*:*:*:*:*:*:*:*\",\n                \"versionStartIncluding\": \"1.21.0-0\",\n                \"versionEndExcluding\": \"1.21.4\",\n                \"matchCriteriaId\": \"4E3FC16C-41B2-4900-901F-48BDA3DC9ED2\"\n              }\n            ]\n          },\n          {\n            \"operator\": \"OR\",\n            \"negate\": false,\n            \"cpeMatch\": [\n              {\n                \"vulnerable\": false,\n                \"criteria\": \"cpe:2.3:o:microsoft:windows:-:*:*:*:*:*:*:*\",\n                \"matchCriteriaId\": \"A2572D17-1DE6-457B-99CC-64AFD54487EA\"\n              }\n            ]\n          }\n        ]\n      }\n    ],\n    \"references\": [\n      {\n        \"url\": \"http://www.openwall.com/lists/oss-security/2023/12/05/2\",\n        \"source\": \"security@golang.org\"\n      },\n      {\n        \"url\": \"https://go.dev/cl/540277\",\n        \"source\": \"security@golang.org\",\n        \"tags\": [\n          \"Issue Tracking\",\n          \"Vendor Advisory\"\n        ]\n      },\n      {\n        \"url\": \"https://go.dev/cl/541175\",\n        \"source\": \"security@golang.org\"\n      },\n      {\n        \"url\": \"https://go.dev/issue/63713\",\n        \"source\": \"security@golang.org\",\n        \"tags\": [\n          \"Issue Tracking\",\n          \"Vendor Advisory\"\n        ]\n      },\n      {\n        \"url\": \"https://go.dev/issue/64028\",\n        \"source\": \"security@golang.org\"\n      },\n      {\n        \"url\": \"https://groups.google.com/g/golang-announce/c/4tU8LZfBFkY\",\n        \"source\": \"security@golang.org\",\n        \"tags\": [\n          \"Issue Tracking\",\n          \"Mailing List\",\n          \"Vendor Advisory\"\n        ]\n      },\n      {\n        \"url\": \"https://groups.google.com/g/golang-dev/c/6ypN5EjibjM/m/KmLVYH_uAgAJ\",\n        \"source\": \"security@golang.org\"\n      },\n      {\n        \"url\": \"https://pkg.go.dev/vuln/GO-2023-2185\",\n        \"source\": \"security@golang.org\",\n        \"tags\": [\n          \"Issue Tracking\",\n          \"Vendor Advisory\"\n        ]\n      },\n      {\n        \"url\": \"https://security.netapp.com/advisory/ntap-20231214-0008/\",\n        \"source\": \"security@golang.org\"\n      }\n    ]\n  }\n}"
  },
  {
    "path": "grype/db/v6/build/transformers/nvd/testdata/compound-pkg.json",
    "content": "{\n  \"cve\": {\n    \"id\": \"CVE-2018-10189\",\n    \"sourceIdentifier\": \"cve@mitre.org\",\n    \"published\": \"2018-04-17T20:29:00.410\",\n    \"lastModified\": \"2018-05-23T14:41:49.073\",\n    \"vulnStatus\": \"Analyzed\",\n    \"descriptions\": [\n      {\n        \"lang\": \"en\",\n        \"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.\"\n      },\n      {\n        \"lang\": \"es\",\n        \"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.\"\n      }\n    ],\n    \"metrics\": {\n      \"cvssMetricV30\": [\n        {\n          \"source\": \"nvd@nist.gov\",\n          \"type\": \"Primary\",\n          \"cvssData\": {\n            \"version\": \"3.0\",\n            \"vectorString\": \"CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N\",\n            \"attackVector\": \"NETWORK\",\n            \"attackComplexity\": \"LOW\",\n            \"privilegesRequired\": \"NONE\",\n            \"userInteraction\": \"NONE\",\n            \"scope\": \"UNCHANGED\",\n            \"confidentialityImpact\": \"HIGH\",\n            \"integrityImpact\": \"NONE\",\n            \"availabilityImpact\": \"NONE\",\n            \"baseScore\": 7.5,\n            \"baseSeverity\": \"HIGH\"\n          },\n          \"exploitabilityScore\": 3.9,\n          \"impactScore\": 3.6\n        }\n      ],\n      \"cvssMetricV2\": [\n        {\n          \"source\": \"nvd@nist.gov\",\n          \"type\": \"Primary\",\n          \"cvssData\": {\n            \"version\": \"2.0\",\n            \"vectorString\": \"AV:N/AC:L/Au:N/C:P/I:N/A:N\",\n            \"accessVector\": \"NETWORK\",\n            \"accessComplexity\": \"LOW\",\n            \"authentication\": \"NONE\",\n            \"confidentialityImpact\": \"PARTIAL\",\n            \"integrityImpact\": \"NONE\",\n            \"availabilityImpact\": \"NONE\",\n            \"baseScore\": 5.0\n          },\n          \"baseSeverity\": \"MEDIUM\",\n          \"exploitabilityScore\": 10.0,\n          \"impactScore\": 2.9,\n          \"acInsufInfo\": false,\n          \"obtainAllPrivilege\": false,\n          \"obtainUserPrivilege\": false,\n          \"obtainOtherPrivilege\": false,\n          \"userInteractionRequired\": false\n        }\n      ]\n    },\n    \"weaknesses\": [\n      {\n        \"source\": \"nvd@nist.gov\",\n        \"type\": \"Primary\",\n        \"description\": [\n          {\n            \"lang\": \"en\",\n            \"value\": \"CWE-200\"\n          }\n        ]\n      }\n    ],\n    \"configurations\": [\n      {\n        \"nodes\": [\n          {\n            \"operator\": \"OR\",\n            \"negate\": false,\n            \"cpeMatch\": [\n              {\n                \"vulnerable\": true,\n                \"criteria\": \"cpe:2.3:a:mautic:mautic:*:*:*:*:*:*:*:*\",\n                \"versionStartIncluding\": \"1.0.0\",\n                \"versionEndIncluding\": \"1.4.1\",\n                \"matchCriteriaId\": \"5779710D-099E-40EE-8DF3-55BD3179A50C\"\n              },\n              {\n                \"vulnerable\": true,\n                \"criteria\": \"cpe:2.3:a:mautic:mautic:*:*:*:*:*:*:*:*\",\n                \"versionStartIncluding\": \"2.0.0\",\n                \"versionEndExcluding\": \"2.13.0\",\n                \"matchCriteriaId\": \"4EFAEE48-4AEF-4F8C-95E0-6E8D848D900F\"\n              }\n            ]\n          }\n        ]\n      }\n    ],\n    \"references\": [\n      {\n        \"url\": \"https://github.com/mautic/mautic/releases/tag/2.13.0\",\n        \"source\": \"cve@mitre.org\",\n        \"tags\": [\n          \"Third Party Advisory\"\n        ]\n      }\n    ]\n  }\n}\n"
  },
  {
    "path": "grype/db/v6/build/transformers/nvd/testdata/cve-2020-10729.json",
    "content": "{\n  \"cve\": {\n    \"id\": \"CVE-2020-10729\",\n    \"sourceIdentifier\": \"secalert@redhat.com\",\n    \"published\": \"2021-05-27T19:15:07.880\",\n    \"lastModified\": \"2021-12-10T19:57:06.357\",\n    \"vulnStatus\": \"Analyzed\",\n    \"descriptions\": [\n      {\n        \"lang\": \"en\",\n        \"value\": \"A flaw was found in the use of insufficiently random values in Ansible. Two random password lookups of the same length generate the equal value as the template caching action for the same file since no re-evaluation happens. The highest threat from this vulnerability would be that all passwords are exposed at once for the file. This flaw affects Ansible Engine versions before 2.9.6.\"\n      },\n      {\n        \"lang\": \"es\",\n        \"value\": \"Se encontró un fallo en el uso de valores insuficientemente aleatorios en Ansible.&#xa0;Dos búsquedas de contraseñas aleatorias de la misma longitud generan el mismo valor que la acción de almacenamiento en caché de la plantilla para el mismo archivo, ya que no se realiza una reevaluación.&#xa0;La mayor amenaza de esta vulnerabilidad sería que todas las contraseñas estén expuestas a la vez para el archivo.&#xa0;Este fallo afecta a Ansible Engine versiones anteriores a 2.9.6\"\n      }\n    ],\n    \"metrics\": {\n      \"cvssMetricV31\": [\n        {\n          \"source\": \"nvd@nist.gov\",\n          \"type\": \"Primary\",\n          \"cvssData\": {\n            \"version\": \"3.1\",\n            \"vectorString\": \"CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N\",\n            \"attackVector\": \"LOCAL\",\n            \"attackComplexity\": \"LOW\",\n            \"privilegesRequired\": \"LOW\",\n            \"userInteraction\": \"NONE\",\n            \"scope\": \"UNCHANGED\",\n            \"confidentialityImpact\": \"HIGH\",\n            \"integrityImpact\": \"NONE\",\n            \"availabilityImpact\": \"NONE\",\n            \"baseScore\": 5.5,\n            \"baseSeverity\": \"MEDIUM\"\n          },\n          \"exploitabilityScore\": 1.8,\n          \"impactScore\": 3.6\n        }\n      ],\n      \"cvssMetricV2\": [\n        {\n          \"source\": \"nvd@nist.gov\",\n          \"type\": \"Primary\",\n          \"cvssData\": {\n            \"version\": \"2.0\",\n            \"vectorString\": \"AV:L/AC:L/Au:N/C:P/I:N/A:N\",\n            \"accessVector\": \"LOCAL\",\n            \"accessComplexity\": \"LOW\",\n            \"authentication\": \"NONE\",\n            \"confidentialityImpact\": \"PARTIAL\",\n            \"integrityImpact\": \"NONE\",\n            \"availabilityImpact\": \"NONE\",\n            \"baseScore\": 2.1\n          },\n          \"baseSeverity\": \"LOW\",\n          \"exploitabilityScore\": 3.9,\n          \"impactScore\": 2.9,\n          \"acInsufInfo\": false,\n          \"obtainAllPrivilege\": false,\n          \"obtainUserPrivilege\": false,\n          \"obtainOtherPrivilege\": false,\n          \"userInteractionRequired\": false\n        }\n      ]\n    },\n    \"weaknesses\": [\n      {\n        \"source\": \"nvd@nist.gov\",\n        \"type\": \"Primary\",\n        \"description\": [\n          {\n            \"lang\": \"en\",\n            \"value\": \"CWE-330\"\n          }\n        ]\n      },\n      {\n        \"source\": \"secalert@redhat.com\",\n        \"type\": \"Secondary\",\n        \"description\": [\n          {\n            \"lang\": \"en\",\n            \"value\": \"CWE-330\"\n          }\n        ]\n      }\n    ],\n    \"configurations\": [\n      {\n        \"operator\": \"AND\",\n        \"nodes\": [\n          {\n            \"operator\": \"OR\",\n            \"negate\": false,\n            \"cpeMatch\": [\n              {\n                \"vulnerable\": true,\n                \"criteria\": \"cpe:2.3:a:redhat:ansible_engine:*:*:*:*:*:*:*:*\",\n                \"versionEndExcluding\": \"2.9.6\",\n                \"matchCriteriaId\": \"EDFA8005-6FBE-4032-A499-608B7FA34F56\"\n              }\n            ]\n          },\n          {\n            \"operator\": \"OR\",\n            \"negate\": false,\n            \"cpeMatch\": [\n              {\n                \"vulnerable\": false,\n                \"criteria\": \"cpe:2.3:o:redhat:enterprise_linux:7.0:*:*:*:*:*:*:*\",\n                \"matchCriteriaId\": \"142AD0DD-4CF3-4D74-9442-459CE3347E3A\"\n              },\n              {\n                \"vulnerable\": false,\n                \"criteria\": \"cpe:2.3:o:redhat:enterprise_linux:8.0:*:*:*:*:*:*:*\",\n                \"matchCriteriaId\": \"F4CFF558-3C47-480D-A2F0-BABF26042943\"\n              }\n            ]\n          }\n        ]\n      },\n      {\n        \"nodes\": [\n          {\n            \"operator\": \"OR\",\n            \"negate\": false,\n            \"cpeMatch\": [\n              {\n                \"vulnerable\": true,\n                \"criteria\": \"cpe:2.3:o:debian:debian_linux:10.0:*:*:*:*:*:*:*\",\n                \"matchCriteriaId\": \"07B237A9-69A3-4A9C-9DA0-4E06BD37AE73\"\n              }\n            ]\n          }\n        ]\n      }\n    ],\n    \"references\": [\n      {\n        \"url\": \"https://bugzilla.redhat.com/show_bug.cgi?id=1831089\",\n        \"source\": \"secalert@redhat.com\",\n        \"tags\": [\n          \"Issue Tracking\",\n          \"Vendor Advisory\"\n        ]\n      },\n      {\n        \"url\": \"https://github.com/ansible/ansible/issues/34144\",\n        \"source\": \"secalert@redhat.com\",\n        \"tags\": [\n          \"Exploit\",\n          \"Issue Tracking\",\n          \"Third Party Advisory\"\n        ]\n      },\n      {\n        \"url\": \"https://www.debian.org/security/2021/dsa-4950\",\n        \"source\": \"secalert@redhat.com\",\n        \"tags\": [\n          \"Third Party Advisory\"\n        ]\n      }\n    ]\n  }\n}\n"
  },
  {
    "path": "grype/db/v6/build/transformers/nvd/testdata/cve-2021-1566.json",
    "content": "{\n  \"cve\": {\n    \"id\": \"CVE-2021-1566\",\n    \"sourceIdentifier\": \"psirt@cisco.com\",\n    \"published\": \"2021-06-16T18:15:08.710\",\n    \"lastModified\": \"2024-11-21T05:44:38.237\",\n    \"vulnStatus\": \"Modified\",\n    \"cveTags\": [],\n    \"descriptions\": [\n      {\n        \"lang\": \"en\",\n        \"value\": \"description.\"\n      },\n      {\n        \"lang\": \"es\",\n        \"value\": \"description\"\n      }\n    ],\n    \"metrics\": {\n      \"cvssMetricV31\": [\n        {\n          \"source\": \"psirt@cisco.com\",\n          \"type\": \"Secondary\",\n          \"cvssData\": {\n            \"version\": \"3.1\",\n            \"vectorString\": \"CVSS:3.1\\/AV:N\\/AC:H\\/PR:N\\/UI:N\\/S:U\\/C:H\\/I:H\\/A:N\",\n            \"baseScore\": 7.4,\n            \"baseSeverity\": \"HIGH\",\n            \"attackVector\": \"NETWORK\",\n            \"attackComplexity\": \"HIGH\",\n            \"privilegesRequired\": \"NONE\",\n            \"userInteraction\": \"NONE\",\n            \"scope\": \"UNCHANGED\",\n            \"confidentialityImpact\": \"HIGH\",\n            \"integrityImpact\": \"HIGH\",\n            \"availabilityImpact\": \"NONE\"\n          },\n          \"exploitabilityScore\": 2.2,\n          \"impactScore\": 5.2\n        },\n        {\n          \"source\": \"nvd@nist.gov\",\n          \"type\": \"Primary\",\n          \"cvssData\": {\n            \"version\": \"3.1\",\n            \"vectorString\": \"CVSS:3.1\\/AV:N\\/AC:H\\/PR:N\\/UI:N\\/S:U\\/C:H\\/I:H\\/A:N\",\n            \"baseScore\": 7.4,\n            \"baseSeverity\": \"HIGH\",\n            \"attackVector\": \"NETWORK\",\n            \"attackComplexity\": \"HIGH\",\n            \"privilegesRequired\": \"NONE\",\n            \"userInteraction\": \"NONE\",\n            \"scope\": \"UNCHANGED\",\n            \"confidentialityImpact\": \"HIGH\",\n            \"integrityImpact\": \"HIGH\",\n            \"availabilityImpact\": \"NONE\"\n          },\n          \"exploitabilityScore\": 2.2,\n          \"impactScore\": 5.2\n        }\n      ],\n      \"cvssMetricV2\": [\n        {\n          \"source\": \"nvd@nist.gov\",\n          \"type\": \"Primary\",\n          \"cvssData\": {\n            \"version\": \"2.0\",\n            \"vectorString\": \"AV:N\\/AC:M\\/Au:N\\/C:P\\/I:P\\/A:N\",\n            \"baseScore\": 5.8,\n            \"accessVector\": \"NETWORK\",\n            \"accessComplexity\": \"MEDIUM\",\n            \"authentication\": \"NONE\",\n            \"confidentialityImpact\": \"PARTIAL\",\n            \"integrityImpact\": \"PARTIAL\",\n            \"availabilityImpact\": \"NONE\"\n          },\n          \"baseSeverity\": \"MEDIUM\",\n          \"exploitabilityScore\": 8.6,\n          \"impactScore\": 4.9,\n          \"acInsufInfo\": false,\n          \"obtainAllPrivilege\": false,\n          \"obtainUserPrivilege\": false,\n          \"obtainOtherPrivilege\": false,\n          \"userInteractionRequired\": false\n        }\n      ]\n    },\n    \"weaknesses\": [\n      {\n        \"source\": \"psirt@cisco.com\",\n        \"type\": \"Secondary\",\n        \"description\": [{ \"lang\": \"en\", \"value\": \"CWE-296\" }]\n      },\n      {\n        \"source\": \"nvd@nist.gov\",\n        \"type\": \"Primary\",\n        \"description\": [{ \"lang\": \"en\", \"value\": \"CWE-295\" }]\n      }\n    ],\n    \"configurations\": [\n      {\n        \"nodes\": [\n          {\n            \"operator\": \"AND\",\n            \"negate\": false,\n            \"cpeMatch\": [\n              {\n                \"vulnerable\": true,\n                \"criteria\": \"cpe:2.3:a:cisco:email_security_appliance:-:*:*:*:*:*:*:*\",\n                \"matchCriteriaId\": \"678C2C6F-6D46-4BBE-A902-7AD031D8EBA8\"\n              },\n              {\n                \"vulnerable\": true,\n                \"criteria\": \"cpe:2.3:o:cisco:asyncos:*:*:*:*:*:*:*:*\",\n                \"versionEndExcluding\": \"12.5.3-035\",\n                \"matchCriteriaId\": \"6C3A8C94-CD5C-4309-8F1B-B151B3D091CC\"\n              }\n            ]\n          }\n        ]\n      },\n      {\n        \"nodes\": [\n          {\n            \"operator\": \"AND\",\n            \"negate\": false,\n            \"cpeMatch\": [\n              {\n                \"vulnerable\": true,\n                \"criteria\": \"cpe:2.3:a:cisco:email_security_appliance:-:*:*:*:*:*:*:*\",\n                \"matchCriteriaId\": \"678C2C6F-6D46-4BBE-A902-7AD031D8EBA8\"\n              },\n              {\n                \"vulnerable\": true,\n                \"criteria\": \"cpe:2.3:o:cisco:asyncos:*:*:*:*:*:*:*:*\",\n                \"versionStartIncluding\": \"13.0\",\n                \"versionEndExcluding\": \"13.0.0-030\",\n                \"matchCriteriaId\": \"BE1DE406-EA9E-40DD-B18B-C19DF63EC13B\"\n              }\n            ]\n          }\n        ]\n      },\n      {\n        \"nodes\": [\n          {\n            \"operator\": \"AND\",\n            \"negate\": false,\n            \"cpeMatch\": [\n              {\n                \"vulnerable\": true,\n                \"criteria\": \"cpe:2.3:a:cisco:email_security_appliance:-:*:*:*:*:*:*:*\",\n                \"matchCriteriaId\": \"678C2C6F-6D46-4BBE-A902-7AD031D8EBA8\"\n              },\n              {\n                \"vulnerable\": true,\n                \"criteria\": \"cpe:2.3:o:cisco:asyncos:*:*:*:*:*:*:*:*\",\n                \"versionStartIncluding\": \"13.5\",\n                \"versionEndExcluding\": \"13.5.3-010\",\n                \"matchCriteriaId\": \"39DEA2BD-4772-4F8D-9CD2-1BB377ECF64B\"\n              }\n            ]\n          }\n        ]\n      },\n      {\n        \"nodes\": [\n          {\n            \"operator\": \"AND\",\n            \"negate\": false,\n            \"cpeMatch\": [\n              {\n                \"vulnerable\": true,\n                \"criteria\": \"cpe:2.3:a:cisco:web_security_appliance:-:*:*:*:*:*:*:*\",\n                \"matchCriteriaId\": \"A7C2555C-7E97-475F-9EDC-027B51A40708\"\n              },\n              {\n                \"vulnerable\": true,\n                \"criteria\": \"cpe:2.3:o:cisco:asyncos:*:*:*:*:*:*:*:*\",\n                \"versionEndExcluding\": \"11.8.3-021\",\n                \"matchCriteriaId\": \"33FDC1BE-F1C3-4030-82CE-38D99DC30B5B\"\n              }\n            ]\n          }\n        ]\n      },\n      {\n        \"nodes\": [\n          {\n            \"operator\": \"AND\",\n            \"negate\": false,\n            \"cpeMatch\": [\n              {\n                \"vulnerable\": true,\n                \"criteria\": \"cpe:2.3:a:cisco:web_security_appliance:-:*:*:*:*:*:*:*\",\n                \"matchCriteriaId\": \"A7C2555C-7E97-475F-9EDC-027B51A40708\"\n              },\n              {\n                \"vulnerable\": true,\n                \"criteria\": \"cpe:2.3:o:cisco:asyncos:*:*:*:*:*:*:*:*\",\n                \"versionStartIncluding\": \"12.0.0\",\n                \"versionEndExcluding\": \"12.0.3-005\",\n                \"matchCriteriaId\": \"D1CC6572-4281-45E1-9B33-6993B45E6B4F\"\n              }\n            ]\n          }\n        ]\n      },\n      {\n        \"nodes\": [\n          {\n            \"operator\": \"AND\",\n            \"negate\": false,\n            \"cpeMatch\": [\n              {\n                \"vulnerable\": true,\n                \"criteria\": \"cpe:2.3:a:cisco:web_security_appliance:-:*:*:*:*:*:*:*\",\n                \"matchCriteriaId\": \"A7C2555C-7E97-475F-9EDC-027B51A40708\"\n              },\n              {\n                \"vulnerable\": true,\n                \"criteria\": \"cpe:2.3:o:cisco:asyncos:*:*:*:*:*:*:*:*\",\n                \"versionStartIncluding\": \"12.5.0\",\n                \"versionEndExcluding\": \"12.5.1-043\",\n                \"matchCriteriaId\": \"AA889DAF-1699-4A22-8A4C-D589F7BF10A8\"\n              }\n            ]\n          }\n        ]\n      }\n    ],\n    \"references\": [\n      {\n        \"url\": \"https:\\/\\/tools.cisco.com\\/security\\/center\\/content\\/CiscoSecurityAdvisory\\/cisco-sa-esa-wsa-cert-vali-n8L97RW\",\n        \"source\": \"psirt@cisco.com\",\n        \"tags\": [\"Vendor Advisory\"]\n      },\n      {\n        \"url\": \"https:\\/\\/tools.cisco.com\\/security\\/center\\/content\\/CiscoSecurityAdvisory\\/cisco-sa-esa-wsa-cert-vali-n8L97RW\",\n        \"source\": \"af854a3a-2127-422b-91ae-364da2661108\",\n        \"tags\": [\"Vendor Advisory\"]\n      }\n    ]\n  }\n}\n"
  },
  {
    "path": "grype/db/v6/build/transformers/nvd/testdata/cve-2022-0543.json",
    "content": "{\n  \"cve\": {\n    \"id\": \"CVE-2022-0543\",\n    \"sourceIdentifier\": \"security@debian.org\",\n    \"published\": \"2022-02-18T20:15:17.583\",\n    \"lastModified\": \"2023-09-29T15:55:24.533\",\n    \"vulnStatus\": \"Analyzed\",\n    \"cisaExploitAdd\": \"2022-03-28\",\n    \"cisaActionDue\": \"2022-04-18\",\n    \"cisaRequiredAction\": \"Apply updates per vendor instructions.\",\n    \"cisaVulnerabilityName\": \"Debian-specific Redis Server Lua Sandbox Escape Vulnerability\",\n    \"descriptions\": [\n      {\n        \"lang\": \"en\",\n        \"value\": \"It was discovered, that redis, a persistent key-value database, due to a packaging issue, is prone to a (Debian-specific) Lua sandbox escape, which could result in remote code execution.\"\n      },\n      {\n        \"lang\": \"es\",\n        \"value\": \"Se ha detectado que redis, una base de datos persistente de valores clave, debido a un problema de empaquetado, es propenso a un escape del sandbox de Lua (específico de Debian), que podría resultar en una ejecución de código remota\"\n      }\n    ],\n    \"metrics\": {\n      \"cvssMetricV31\": [\n        {\n          \"source\": \"nvd@nist.gov\",\n          \"type\": \"Primary\",\n          \"cvssData\": {\n            \"version\": \"3.1\",\n            \"vectorString\": \"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H\",\n            \"attackVector\": \"NETWORK\",\n            \"attackComplexity\": \"LOW\",\n            \"privilegesRequired\": \"NONE\",\n            \"userInteraction\": \"NONE\",\n            \"scope\": \"CHANGED\",\n            \"confidentialityImpact\": \"HIGH\",\n            \"integrityImpact\": \"HIGH\",\n            \"availabilityImpact\": \"HIGH\",\n            \"baseScore\": 10,\n            \"baseSeverity\": \"CRITICAL\"\n          },\n          \"exploitabilityScore\": 3.9,\n          \"impactScore\": 6\n        }\n      ],\n      \"cvssMetricV2\": [\n        {\n          \"source\": \"nvd@nist.gov\",\n          \"type\": \"Primary\",\n          \"cvssData\": {\n            \"version\": \"2.0\",\n            \"vectorString\": \"AV:N/AC:L/Au:N/C:C/I:C/A:C\",\n            \"accessVector\": \"NETWORK\",\n            \"accessComplexity\": \"LOW\",\n            \"authentication\": \"NONE\",\n            \"confidentialityImpact\": \"COMPLETE\",\n            \"integrityImpact\": \"COMPLETE\",\n            \"availabilityImpact\": \"COMPLETE\",\n            \"baseScore\": 10\n          },\n          \"baseSeverity\": \"HIGH\",\n          \"exploitabilityScore\": 10,\n          \"impactScore\": 10,\n          \"acInsufInfo\": false,\n          \"obtainAllPrivilege\": false,\n          \"obtainUserPrivilege\": false,\n          \"obtainOtherPrivilege\": false,\n          \"userInteractionRequired\": false\n        }\n      ]\n    },\n    \"weaknesses\": [\n      {\n        \"source\": \"nvd@nist.gov\",\n        \"type\": \"Primary\",\n        \"description\": [\n          {\n            \"lang\": \"en\",\n            \"value\": \"CWE-862\"\n          }\n        ]\n      }\n    ],\n    \"configurations\": [\n      {\n        \"operator\": \"AND\",\n        \"nodes\": [\n          {\n            \"operator\": \"OR\",\n            \"negate\": false,\n            \"cpeMatch\": [\n              {\n                \"vulnerable\": true,\n                \"criteria\": \"cpe:2.3:a:redis:redis:-:*:*:*:*:*:*:*\",\n                \"matchCriteriaId\": \"5EBE5E1C-C881-4A76-9E36-4FB7C48427E6\"\n              }\n            ]\n          },\n          {\n            \"operator\": \"OR\",\n            \"negate\": false,\n            \"cpeMatch\": [\n              {\n                \"vulnerable\": false,\n                \"criteria\": \"cpe:2.3:o:canonical:ubuntu_linux:20.04:*:*:*:lts:*:*:*\",\n                \"matchCriteriaId\": \"902B8056-9E37-443B-8905-8AA93E2447FB\"\n              },\n              {\n                \"vulnerable\": false,\n                \"criteria\": \"cpe:2.3:o:canonical:ubuntu_linux:21.10:*:*:*:-:*:*:*\",\n                \"matchCriteriaId\": \"3D94DA3B-FA74-4526-A0A0-A872684598C6\"\n              },\n              {\n                \"vulnerable\": false,\n                \"criteria\": \"cpe:2.3:o:debian:debian_linux:9.0:*:*:*:*:*:*:*\",\n                \"matchCriteriaId\": \"DEECE5FC-CACF-4496-A3E7-164736409252\"\n              },\n              {\n                \"vulnerable\": false,\n                \"criteria\": \"cpe:2.3:o:debian:debian_linux:10.0:*:*:*:*:*:*:*\",\n                \"matchCriteriaId\": \"07B237A9-69A3-4A9C-9DA0-4E06BD37AE73\"\n              },\n              {\n                \"vulnerable\": false,\n                \"criteria\": \"cpe:2.3:o:debian:debian_linux:11.0:*:*:*:*:*:*:*\",\n                \"matchCriteriaId\": \"FA6FEEC2-9F11-4643-8827-749718254FED\"\n              }\n            ]\n          }\n        ]\n      }\n    ],\n    \"references\": [\n      {\n        \"url\": \"http://packetstormsecurity.com/files/166885/Redis-Lua-Sandbox-Escape.html\",\n        \"source\": \"security@debian.org\",\n        \"tags\": [\n          \"Exploit\",\n          \"Third Party Advisory\",\n          \"VDB Entry\"\n        ]\n      },\n      {\n        \"url\": \"https://bugs.debian.org/1005787\",\n        \"source\": \"security@debian.org\",\n        \"tags\": [\n          \"Issue Tracking\",\n          \"Patch\",\n          \"Third Party Advisory\"\n        ]\n      },\n      {\n        \"url\": \"https://lists.debian.org/debian-security-announce/2022/msg00048.html\",\n        \"source\": \"security@debian.org\",\n        \"tags\": [\n          \"Mailing List\",\n          \"Third Party Advisory\"\n        ]\n      },\n      {\n        \"url\": \"https://security.netapp.com/advisory/ntap-20220331-0004/\",\n        \"source\": \"security@debian.org\",\n        \"tags\": [\n          \"Third Party Advisory\"\n        ]\n      },\n      {\n        \"url\": \"https://www.debian.org/security/2022/dsa-5081\",\n        \"source\": \"security@debian.org\",\n        \"tags\": [\n          \"Mailing List\",\n          \"Third Party Advisory\"\n        ]\n      },\n      {\n        \"url\": \"https://www.ubercomp.com/posts/2022-01-20_redis_on_debian_rce\",\n        \"source\": \"security@debian.org\",\n        \"tags\": [\n          \"Third Party Advisory\"\n        ]\n      }\n    ]\n  }\n}\n"
  },
  {
    "path": "grype/db/v6/build/transformers/nvd/testdata/cve-2024-26663-standalone-os.json",
    "content": "{\n  \"cve\": {\n    \"id\": \"CVE-2024-26663\",\n    \"sourceIdentifier\": \"416baaa9-dc9f-4396-8d5f-8c081fb06d67\",\n    \"published\": \"2024-04-02T07:15:43.287\",\n    \"lastModified\": \"2025-01-07T17:20:30.367\",\n    \"vulnStatus\": \"Analyzed\",\n    \"cveTags\": [],\n    \"descriptions\": [\n      {\n        \"lang\": \"en\",\n        \"value\": \"the description...\"\n      },\n      {\n        \"lang\": \"es\",\n        \"value\": \"el description...\"\n      }\n    ],\n    \"metrics\": {\n      \"cvssMetricV31\": [\n        {\n          \"source\": \"nvd@nist.gov\",\n          \"type\": \"Primary\",\n          \"cvssData\": {\n            \"version\": \"3.1\",\n            \"vectorString\": \"CVSS:3.1\\/AV:L\\/AC:L\\/PR:L\\/UI:N\\/S:U\\/C:N\\/I:N\\/A:H\",\n            \"baseScore\": 5.5,\n            \"baseSeverity\": \"MEDIUM\",\n            \"attackVector\": \"LOCAL\",\n            \"attackComplexity\": \"LOW\",\n            \"privilegesRequired\": \"LOW\",\n            \"userInteraction\": \"NONE\",\n            \"scope\": \"UNCHANGED\",\n            \"confidentialityImpact\": \"NONE\",\n            \"integrityImpact\": \"NONE\",\n            \"availabilityImpact\": \"HIGH\"\n          },\n          \"exploitabilityScore\": 1.8,\n          \"impactScore\": 3.6\n        }\n      ]\n    },\n    \"weaknesses\": [\n      {\n        \"source\": \"nvd@nist.gov\",\n        \"type\": \"Primary\",\n        \"description\": [{ \"lang\": \"en\", \"value\": \"CWE-476\" }]\n      }\n    ],\n    \"configurations\": [\n      {\n        \"nodes\": [\n          {\n            \"operator\": \"OR\",\n            \"negate\": false,\n            \"cpeMatch\": [\n              {\n                \"vulnerable\": true,\n                \"criteria\": \"cpe:2.3:o:linux:linux_kernel:*:*:*:*:*:*:*:*\",\n                \"versionStartIncluding\": \"4.9\",\n                \"versionEndExcluding\": \"4.19.307\",\n                \"matchCriteriaId\": \"A1A227E7-C02C-4FC4-84AA-230362C5E2C6\"\n              },\n              {\n                \"vulnerable\": true,\n                \"criteria\": \"cpe:2.3:o:linux:linux_kernel:*:*:*:*:*:*:*:*\",\n                \"versionStartIncluding\": \"6.7\",\n                \"versionEndExcluding\": \"6.7.5\",\n                \"matchCriteriaId\": \"01925741-2C95-47C1-A7EA-3DC2BB0012D3\"\n              },\n              {\n                \"vulnerable\": true,\n                \"criteria\": \"cpe:2.3:o:linux:linux_kernel:6.8:rc1:*:*:*:*:*:*\",\n                \"matchCriteriaId\": \"B9F4EA73-0894-400F-A490-3A397AB7A517\"\n              },\n              {\n                \"vulnerable\": true,\n                \"criteria\": \"cpe:2.3:o:linux:linux_kernel:6.8:rc2:*:*:*:*:*:*\",\n                \"matchCriteriaId\": \"056BD938-0A27-4569-B391-30578B309EE3\"\n              },\n              {\n                \"vulnerable\": true,\n                \"criteria\": \"cpe:2.3:o:linux:linux_kernel:6.8:rc3:*:*:*:*:*:*\",\n                \"matchCriteriaId\": \"F02056A5-B362-4370-9FF8-6F0BD384D520\"\n              }\n            ]\n          }\n        ]\n      },\n      {\n        \"nodes\": [\n          {\n            \"operator\": \"OR\",\n            \"negate\": false,\n            \"cpeMatch\": [\n              {\n                \"vulnerable\": true,\n                \"criteria\": \"cpe:2.3:o:debian:debian_linux:10.0:*:*:*:*:*:*:*\",\n                \"matchCriteriaId\": \"07B237A9-69A3-4A9C-9DA0-4E06BD37AE73\"\n              }\n            ]\n          }\n        ]\n      }\n    ],\n    \"references\": [\n      {\n        \"url\": \"https:\\/\\/git.kernel.org\\/stable\\/c\\/0cd331dfd6023640c9669d0592bc0fd491205f87\",\n        \"source\": \"416baaa9-dc9f-4396-8d5f-8c081fb06d67\",\n        \"tags\": [\"Patch\"]\n      }\n    ]\n  }\n}\n"
  },
  {
    "path": "grype/db/v6/build/transformers/nvd/testdata/fix-version.json",
    "content": "{\n  \"cve\": {\n    \"id\": \"CVE-2018-5487\",\n    \"sourceIdentifier\": \"security-alert@netapp.com\",\n    \"published\": \"2018-05-24T14:29:00.390\",\n    \"lastModified\": \"2018-07-05T13:52:30.627\",\n    \"vulnStatus\": \"Analyzed\",\n    \"descriptions\": [\n      {\n        \"lang\": \"en\",\n        \"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.\"\n      },\n      {\n        \"lang\": \"es\",\n        \"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.\"\n      }\n    ],\n    \"metrics\": {\n      \"cvssMetricV40\": [\n        {\n          \"source\": \"security@zabbix.com\",\n          \"type\": \"Secondary\",\n          \"cvssData\": {\n            \"version\": \"4.0\",\n            \"vectorString\": \"CVSS:4.0/AV:N/AC:H/AT:N/PR:N/UI:A/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X\",\n            \"baseScore\": 7.5,\n            \"baseSeverity\": \"HIGH\",\n            \"attackVector\": \"NETWORK\",\n            \"attackComplexity\": \"HIGH\",\n            \"attackRequirements\": \"NONE\",\n            \"privilegesRequired\": \"NONE\",\n            \"userInteraction\": \"ACTIVE\",\n            \"vulnConfidentialityImpact\": \"HIGH\",\n            \"vulnIntegrityImpact\": \"HIGH\",\n            \"vulnAvailabilityImpact\": \"HIGH\",\n            \"subConfidentialityImpact\": \"NONE\",\n            \"subIntegrityImpact\": \"NONE\",\n            \"subAvailabilityImpact\": \"NONE\",\n            \"exploitMaturity\": \"NOT_DEFINED\",\n            \"confidentialityRequirement\": \"NOT_DEFINED\",\n            \"integrityRequirement\": \"NOT_DEFINED\",\n            \"availabilityRequirement\": \"NOT_DEFINED\",\n            \"modifiedAttackVector\": \"NOT_DEFINED\",\n            \"modifiedAttackComplexity\": \"NOT_DEFINED\",\n            \"modifiedAttackRequirements\": \"NOT_DEFINED\",\n            \"modifiedPrivilegesRequired\": \"NOT_DEFINED\",\n            \"modifiedUserInteraction\": \"NOT_DEFINED\",\n            \"modifiedVulnConfidentialityImpact\": \"NOT_DEFINED\",\n            \"modifiedVulnIntegrityImpact\": \"NOT_DEFINED\",\n            \"modifiedVulnAvailabilityImpact\": \"NOT_DEFINED\",\n            \"modifiedSubConfidentialityImpact\": \"NOT_DEFINED\",\n            \"modifiedSubIntegrityImpact\": \"NOT_DEFINED\",\n            \"modifiedSubAvailabilityImpact\": \"NOT_DEFINED\",\n            \"Safety\": \"NOT_DEFINED\",\n            \"Automatable\": \"NOT_DEFINED\",\n            \"Recovery\": \"NOT_DEFINED\",\n            \"valueDensity\": \"NOT_DEFINED\",\n            \"vulnerabilityResponseEffort\": \"NOT_DEFINED\",\n            \"providerUrgency\": \"NOT_DEFINED\"\n          }\n        }\n      ],\n      \"cvssMetricV30\": [\n        {\n          \"source\": \"nvd@nist.gov\",\n          \"type\": \"Primary\",\n          \"cvssData\": {\n            \"version\": \"3.0\",\n            \"vectorString\": \"CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H\",\n            \"attackVector\": \"NETWORK\",\n            \"attackComplexity\": \"LOW\",\n            \"privilegesRequired\": \"NONE\",\n            \"userInteraction\": \"NONE\",\n            \"scope\": \"UNCHANGED\",\n            \"confidentialityImpact\": \"HIGH\",\n            \"integrityImpact\": \"HIGH\",\n            \"availabilityImpact\": \"HIGH\",\n            \"baseScore\": 9.8,\n            \"baseSeverity\": \"CRITICAL\"\n          },\n          \"exploitabilityScore\": 3.9,\n          \"impactScore\": 5.9\n        }\n      ],\n      \"cvssMetricV2\": [\n        {\n          \"source\": \"nvd@nist.gov\",\n          \"type\": \"Primary\",\n          \"cvssData\": {\n            \"version\": \"2.0\",\n            \"vectorString\": \"AV:N/AC:L/Au:N/C:P/I:P/A:P\",\n            \"accessVector\": \"NETWORK\",\n            \"accessComplexity\": \"LOW\",\n            \"authentication\": \"NONE\",\n            \"confidentialityImpact\": \"PARTIAL\",\n            \"integrityImpact\": \"PARTIAL\",\n            \"availabilityImpact\": \"PARTIAL\",\n            \"baseScore\": 7.5\n          },\n          \"baseSeverity\": \"HIGH\",\n          \"exploitabilityScore\": 10.0,\n          \"impactScore\": 6.4,\n          \"acInsufInfo\": true,\n          \"obtainAllPrivilege\": false,\n          \"obtainUserPrivilege\": false,\n          \"obtainOtherPrivilege\": false,\n          \"userInteractionRequired\": false\n        }\n      ]\n    },\n    \"weaknesses\": [\n      {\n        \"source\": \"nvd@nist.gov\",\n        \"type\": \"Primary\",\n        \"description\": [\n          {\n            \"lang\": \"en\",\n            \"value\": \"CWE-20\"\n          }\n        ]\n      }\n    ],\n    \"configurations\": [\n      {\n        \"operator\": \"AND\",\n        \"nodes\": [\n          {\n            \"operator\": \"OR\",\n            \"negate\": false,\n            \"cpeMatch\": [\n              {\n                \"vulnerable\": true,\n                \"criteria\": \"cpe:2.3:a:netapp:oncommand_unified_manager:*:*:*:*:*:*:*:*\",\n                \"versionStartIncluding\": \"7.2\",\n                \"versionEndExcluding\": \"7.3\",\n                \"matchCriteriaId\": \"A5949307-3E9B-441F-B008-81A0E0228DC0\",\n                \"fix\": {\n                  \"version\": \"7.3\",\n                  \"date\": \"2018-05-23\",\n                  \"kind\": \"advisory\"\n                }\n              }\n            ]\n          },\n          {\n            \"operator\": \"OR\",\n            \"negate\": false,\n            \"cpeMatch\": [\n              {\n                \"vulnerable\": false,\n                \"criteria\": \"cpe:2.3:o:linux:linux_kernel:-:*:*:*:*:*:*:*\",\n                \"matchCriteriaId\": \"703AF700-7A70-47E2-BC3A-7FD03B3CA9C1\"\n              }\n            ]\n          }\n        ]\n      }\n    ],\n    \"references\": [\n      {\n        \"url\": \"https://security.netapp.com/advisory/ntap-20180523-0001/\",\n        \"source\": \"security-alert@netapp.com\",\n        \"tags\": [\n          \"Patch\",\n          \"Vendor Advisory\"\n        ]\n      }\n    ]\n  }\n}\n"
  },
  {
    "path": "grype/db/v6/build/transformers/nvd/testdata/fix-wrong-version.json",
    "content": "{\n  \"cve\": {\n    \"id\": \"CVE-2018-5487\",\n    \"sourceIdentifier\": \"security-alert@netapp.com\",\n    \"published\": \"2018-05-24T14:29:00.390\",\n    \"lastModified\": \"2018-07-05T13:52:30.627\",\n    \"vulnStatus\": \"Analyzed\",\n    \"descriptions\": [\n      {\n        \"lang\": \"en\",\n        \"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.\"\n      },\n      {\n        \"lang\": \"es\",\n        \"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.\"\n      }\n    ],\n    \"metrics\": {\n      \"cvssMetricV40\": [\n        {\n          \"source\": \"security@zabbix.com\",\n          \"type\": \"Secondary\",\n          \"cvssData\": {\n            \"version\": \"4.0\",\n            \"vectorString\": \"CVSS:4.0/AV:N/AC:H/AT:N/PR:N/UI:A/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X\",\n            \"baseScore\": 7.5,\n            \"baseSeverity\": \"HIGH\",\n            \"attackVector\": \"NETWORK\",\n            \"attackComplexity\": \"HIGH\",\n            \"attackRequirements\": \"NONE\",\n            \"privilegesRequired\": \"NONE\",\n            \"userInteraction\": \"ACTIVE\",\n            \"vulnConfidentialityImpact\": \"HIGH\",\n            \"vulnIntegrityImpact\": \"HIGH\",\n            \"vulnAvailabilityImpact\": \"HIGH\",\n            \"subConfidentialityImpact\": \"NONE\",\n            \"subIntegrityImpact\": \"NONE\",\n            \"subAvailabilityImpact\": \"NONE\",\n            \"exploitMaturity\": \"NOT_DEFINED\",\n            \"confidentialityRequirement\": \"NOT_DEFINED\",\n            \"integrityRequirement\": \"NOT_DEFINED\",\n            \"availabilityRequirement\": \"NOT_DEFINED\",\n            \"modifiedAttackVector\": \"NOT_DEFINED\",\n            \"modifiedAttackComplexity\": \"NOT_DEFINED\",\n            \"modifiedAttackRequirements\": \"NOT_DEFINED\",\n            \"modifiedPrivilegesRequired\": \"NOT_DEFINED\",\n            \"modifiedUserInteraction\": \"NOT_DEFINED\",\n            \"modifiedVulnConfidentialityImpact\": \"NOT_DEFINED\",\n            \"modifiedVulnIntegrityImpact\": \"NOT_DEFINED\",\n            \"modifiedVulnAvailabilityImpact\": \"NOT_DEFINED\",\n            \"modifiedSubConfidentialityImpact\": \"NOT_DEFINED\",\n            \"modifiedSubIntegrityImpact\": \"NOT_DEFINED\",\n            \"modifiedSubAvailabilityImpact\": \"NOT_DEFINED\",\n            \"Safety\": \"NOT_DEFINED\",\n            \"Automatable\": \"NOT_DEFINED\",\n            \"Recovery\": \"NOT_DEFINED\",\n            \"valueDensity\": \"NOT_DEFINED\",\n            \"vulnerabilityResponseEffort\": \"NOT_DEFINED\",\n            \"providerUrgency\": \"NOT_DEFINED\"\n          }\n        }\n      ],\n      \"cvssMetricV30\": [\n        {\n          \"source\": \"nvd@nist.gov\",\n          \"type\": \"Primary\",\n          \"cvssData\": {\n            \"version\": \"3.0\",\n            \"vectorString\": \"CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H\",\n            \"attackVector\": \"NETWORK\",\n            \"attackComplexity\": \"LOW\",\n            \"privilegesRequired\": \"NONE\",\n            \"userInteraction\": \"NONE\",\n            \"scope\": \"UNCHANGED\",\n            \"confidentialityImpact\": \"HIGH\",\n            \"integrityImpact\": \"HIGH\",\n            \"availabilityImpact\": \"HIGH\",\n            \"baseScore\": 9.8,\n            \"baseSeverity\": \"CRITICAL\"\n          },\n          \"exploitabilityScore\": 3.9,\n          \"impactScore\": 5.9\n        }\n      ],\n      \"cvssMetricV2\": [\n        {\n          \"source\": \"nvd@nist.gov\",\n          \"type\": \"Primary\",\n          \"cvssData\": {\n            \"version\": \"2.0\",\n            \"vectorString\": \"AV:N/AC:L/Au:N/C:P/I:P/A:P\",\n            \"accessVector\": \"NETWORK\",\n            \"accessComplexity\": \"LOW\",\n            \"authentication\": \"NONE\",\n            \"confidentialityImpact\": \"PARTIAL\",\n            \"integrityImpact\": \"PARTIAL\",\n            \"availabilityImpact\": \"PARTIAL\",\n            \"baseScore\": 7.5\n          },\n          \"baseSeverity\": \"HIGH\",\n          \"exploitabilityScore\": 10.0,\n          \"impactScore\": 6.4,\n          \"acInsufInfo\": true,\n          \"obtainAllPrivilege\": false,\n          \"obtainUserPrivilege\": false,\n          \"obtainOtherPrivilege\": false,\n          \"userInteractionRequired\": false\n        }\n      ]\n    },\n    \"weaknesses\": [\n      {\n        \"source\": \"nvd@nist.gov\",\n        \"type\": \"Primary\",\n        \"description\": [\n          {\n            \"lang\": \"en\",\n            \"value\": \"CWE-20\"\n          }\n        ]\n      }\n    ],\n    \"configurations\": [\n      {\n        \"operator\": \"AND\",\n        \"nodes\": [\n          {\n            \"operator\": \"OR\",\n            \"negate\": false,\n            \"cpeMatch\": [\n              {\n                \"vulnerable\": true,\n                \"criteria\": \"cpe:2.3:a:netapp:oncommand_unified_manager:*:*:*:*:*:*:*:*\",\n                \"versionStartIncluding\": \"7.2\",\n                \"versionEndExcluding\": \"7.3\",\n                \"matchCriteriaId\": \"A5949307-3E9B-441F-B008-81A0E0228DC0\",\n                \"fix\": {\n                  \"version\": \"7.4\",\n                  \"date\": \"2018-05-23\",\n                  \"kind\": \"advisory\"\n                }\n              }\n            ]\n          },\n          {\n            \"operator\": \"OR\",\n            \"negate\": false,\n            \"cpeMatch\": [\n              {\n                \"vulnerable\": false,\n                \"criteria\": \"cpe:2.3:o:linux:linux_kernel:-:*:*:*:*:*:*:*\",\n                \"matchCriteriaId\": \"703AF700-7A70-47E2-BC3A-7FD03B3CA9C1\"\n              }\n            ]\n          }\n        ]\n      }\n    ],\n    \"references\": [\n      {\n        \"url\": \"https://security.netapp.com/advisory/ntap-20180523-0001/\",\n        \"source\": \"security-alert@netapp.com\",\n        \"tags\": [\n          \"Patch\",\n          \"Vendor Advisory\"\n        ]\n      }\n    ]\n  }\n}\n"
  },
  {
    "path": "grype/db/v6/build/transformers/nvd/testdata/invalid_cpe.json",
    "content": "{\n \"cve\": {\n  \"id\": \"CVE-2015-8978\",\n  \"sourceIdentifier\": \"cve@mitre.org\",\n  \"published\": \"2016-11-22T17:59:00.180\",\n  \"lastModified\": \"2016-11-28T19:50:59.600\",\n  \"vulnStatus\": \"Modified\",\n  \"descriptions\": [\n   {\n    \"lang\": \"en\",\n    \"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.\"\n   },\n   {\n    \"lang\": \"es\",\n    \"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.\"\n   }\n  ],\n  \"metrics\": {\n   \"cvssMetricV30\": [\n    {\n     \"source\": \"nvd@nist.gov\",\n     \"type\": \"Primary\",\n     \"cvssData\": {\n      \"version\": \"3.0\",\n      \"vectorString\": \"CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H\",\n      \"attackVector\": \"NETWORK\",\n      \"attackComplexity\": \"LOW\",\n      \"privilegesRequired\": \"NONE\",\n      \"userInteraction\": \"NONE\",\n      \"scope\": \"UNCHANGED\",\n      \"confidentialityImpact\": \"NONE\",\n      \"integrityImpact\": \"NONE\",\n      \"availabilityImpact\": \"HIGH\",\n      \"baseScore\": 7.5,\n      \"baseSeverity\": \"HIGH\"\n     },\n     \"exploitabilityScore\": 3.9,\n     \"impactScore\": 3.6\n    }\n   ],\n   \"cvssMetricV2\": [\n    {\n     \"source\": \"nvd@nist.gov\",\n     \"type\": \"Primary\",\n     \"cvssData\": {\n      \"version\": \"2.0\",\n      \"vectorString\": \"AV:N/AC:L/Au:N/C:N/I:N/A:P\",\n      \"accessVector\": \"NETWORK\",\n      \"accessComplexity\": \"LOW\",\n      \"authentication\": \"NONE\",\n      \"confidentialityImpact\": \"NONE\",\n      \"integrityImpact\": \"NONE\",\n      \"availabilityImpact\": \"PARTIAL\",\n      \"baseScore\": 5.0\n     },\n     \"baseSeverity\": \"MEDIUM\",\n     \"exploitabilityScore\": 10.0,\n     \"impactScore\": 2.9,\n     \"acInsufInfo\": false,\n     \"obtainAllPrivilege\": false,\n     \"obtainUserPrivilege\": false,\n     \"obtainOtherPrivilege\": false,\n     \"userInteractionRequired\": false\n    }\n   ]\n  },\n  \"weaknesses\": [\n   {\n    \"source\": \"nvd@nist.gov\",\n    \"type\": \"Primary\",\n    \"description\": [\n     {\n      \"lang\": \"en\",\n      \"value\": \"CWE-399\"\n     }\n    ]\n   }\n  ],\n  \"configurations\": [\n   {\n    \"nodes\": [\n     {\n      \"operator\": \"OR\",\n      \"negate\": false,\n      \"cpeMatch\": [\n       {\n        \"vulnerable\": true,\n        \"criteria\": \"cpe:2.3:a:soap::lite_project:soap::lite:*:*:*:*:*:perl:*:*\",\n        \"versionEndIncluding\": \"1.14\",\n        \"matchCriteriaId\": \"FB4DACB9-2E9E-4CBE-825F-FC0303D8CC86\"\n       }\n      ]\n     }\n    ]\n   }\n  ],\n  \"references\": [\n   {\n    \"url\": \"http://cpansearch.perl.org/src/PHRED/SOAP-Lite-1.20/Changes\",\n    \"source\": \"cve@mitre.org\",\n    \"tags\": [\n     \"Vendor Advisory\"\n    ]\n   },\n   {\n    \"url\": \"http://www.securityfocus.com/bid/94487\",\n    \"source\": \"cve@mitre.org\"\n   }\n  ]\n }\n}\n"
  },
  {
    "path": "grype/db/v6/build/transformers/nvd/testdata/jvm-packages.json",
    "content": "{\n  \"cve\": {\n    \"id\": \"CVE-2023-JVM-TEST\",\n    \"sourceIdentifier\": \"cve@mitre.org\",\n    \"published\": \"2024-01-17T00:15:51.677\",\n    \"lastModified\": \"2024-01-23T16:32:52.103\",\n    \"vulnStatus\": \"Analyzed\",\n    \"descriptions\": [\n      {\n        \"lang\": \"en\",\n        \"value\": \"Test vulnerability affecting JVM packages to demonstrate version format detection.\"\n      }\n    ],\n    \"metrics\": {\n      \"cvssMetricV31\": [\n        {\n          \"source\": \"nvd@nist.gov\",\n          \"type\": \"Primary\",\n          \"cvssData\": {\n            \"version\": \"3.1\",\n            \"vectorString\": \"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N\",\n            \"baseScore\": 6.1,\n            \"baseSeverity\": \"MEDIUM\",\n            \"attackVector\": \"NETWORK\",\n            \"attackComplexity\": \"LOW\",\n            \"privilegesRequired\": \"NONE\",\n            \"userInteraction\": \"REQUIRED\",\n            \"scope\": \"CHANGED\",\n            \"confidentialityImpact\": \"LOW\",\n            \"integrityImpact\": \"LOW\",\n            \"availabilityImpact\": \"NONE\"\n          },\n          \"exploitabilityScore\": 2.8,\n          \"impactScore\": 2.7\n        }\n      ]\n    },\n    \"weaknesses\": [\n      {\n        \"source\": \"nvd@nist.gov\",\n        \"type\": \"Primary\",\n        \"description\": [\n          {\n            \"lang\": \"en\",\n            \"value\": \"CWE-79\"\n          }\n        ]\n      }\n    ],\n    \"configurations\": [\n      {\n        \"nodes\": [\n          {\n            \"operator\": \"OR\",\n            \"negate\": false,\n            \"cpeMatch\": [\n              {\n                \"vulnerable\": true,\n                \"criteria\": \"cpe:2.3:a:oracle:jdk:8u401:*:*:*:*:*:*:*\",\n                \"matchCriteriaId\": \"oracle-jdk-8u401\"\n              },\n              {\n                \"vulnerable\": true,\n                \"criteria\": \"cpe:2.3:a:oracle:jdk:11.0.22:*:*:*:*:*:*:*\",\n                \"matchCriteriaId\": \"oracle-jdk-11.0.22\"\n              },\n              {\n                \"vulnerable\": true,\n                \"criteria\": \"cpe:2.3:a:eclipse:openjdk:17.0.10:*:*:*:*:*:*:*\",\n                \"matchCriteriaId\": \"eclipse-openjdk-17.0.10\"\n              },\n              {\n                \"vulnerable\": true,\n                \"criteria\": \"cpe:2.3:a:azul:zulu:21.0.2:*:*:*:*:*:*:*\",\n                \"matchCriteriaId\": \"azul-zulu-21.0.2\"\n              },\n              {\n                \"vulnerable\": true,\n                \"criteria\": \"cpe:2.3:a:adoptium:java:17.0.10:*:*:*:*:*:*:*\",\n                \"matchCriteriaId\": \"adoptium-java-17.0.10\"\n              }\n            ]\n          }\n        ]\n      }\n    ],\n    \"references\": [\n      {\n        \"url\": \"https://nvd.nist.gov/vuln/detail/CVE-2023-JVM-TEST\",\n        \"source\": \"nvd@nist.gov\"\n      },\n      {\n        \"url\": \"https://www.oracle.com/security-alerts/cpujan2024.html\",\n        \"source\": \"nvd@nist.gov\",\n        \"tags\": [\"Patch\", \"Vendor Advisory\"]\n      }\n    ]\n  }\n}"
  },
  {
    "path": "grype/db/v6/build/transformers/nvd/testdata/multiple-platforms-with-application-cpe.json",
    "content": "{\n  \"cve\": {\n    \"id\": \"CVE-2023-38733\",\n    \"sourceIdentifier\": \"psirt@us.ibm.com\",\n    \"published\": \"2023-08-22T22:15:08.460\",\n    \"lastModified\": \"2023-08-26T02:25:42.957\",\n    \"vulnStatus\": \"Analyzed\",\n    \"descriptions\": [\n      {\n        \"lang\": \"en\",\n        \"value\": \"\\nIBM Robotic Process Automation 21.0.0 through 21.0.7.1 and 23.0.0 through 23.0.1 server could allow an authenticated user to view sensitive information from installation logs.  IBM X-Force Id:  262293.\\n\\n\"\n      }\n    ],\n    \"metrics\": {\n      \"cvssMetricV31\": [\n        {\n          \"source\": \"nvd@nist.gov\",\n          \"type\": \"Primary\",\n          \"cvssData\": {\n            \"version\": \"3.1\",\n            \"vectorString\": \"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N\",\n            \"attackVector\": \"NETWORK\",\n            \"attackComplexity\": \"LOW\",\n            \"privilegesRequired\": \"LOW\",\n            \"userInteraction\": \"NONE\",\n            \"scope\": \"UNCHANGED\",\n            \"confidentialityImpact\": \"LOW\",\n            \"integrityImpact\": \"NONE\",\n            \"availabilityImpact\": \"NONE\",\n            \"baseScore\": 4.3,\n            \"baseSeverity\": \"MEDIUM\"\n          },\n          \"exploitabilityScore\": 2.8,\n          \"impactScore\": 1.4\n        },\n        {\n          \"source\": \"psirt@us.ibm.com\",\n          \"type\": \"Secondary\",\n          \"cvssData\": {\n            \"version\": \"3.1\",\n            \"vectorString\": \"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N\",\n            \"attackVector\": \"NETWORK\",\n            \"attackComplexity\": \"LOW\",\n            \"privilegesRequired\": \"LOW\",\n            \"userInteraction\": \"NONE\",\n            \"scope\": \"UNCHANGED\",\n            \"confidentialityImpact\": \"LOW\",\n            \"integrityImpact\": \"NONE\",\n            \"availabilityImpact\": \"NONE\",\n            \"baseScore\": 4.3,\n            \"baseSeverity\": \"MEDIUM\"\n          },\n          \"exploitabilityScore\": 2.8,\n          \"impactScore\": 1.4\n        }\n      ]\n    },\n    \"weaknesses\": [\n      {\n        \"source\": \"nvd@nist.gov\",\n        \"type\": \"Primary\",\n        \"description\": [\n          {\n            \"lang\": \"en\",\n            \"value\": \"CWE-532\"\n          }\n        ]\n      },\n      {\n        \"source\": \"psirt@us.ibm.com\",\n        \"type\": \"Secondary\",\n        \"description\": [\n          {\n            \"lang\": \"en\",\n            \"value\": \"CWE-532\"\n          }\n        ]\n      }\n    ],\n    \"configurations\": [\n      {\n        \"operator\": \"AND\",\n        \"nodes\": [\n          {\n            \"operator\": \"OR\",\n            \"negate\": false,\n            \"cpeMatch\": [\n              {\n                \"vulnerable\": true,\n                \"criteria\": \"cpe:2.3:a:ibm:robotic_process_automation:*:*:*:*:*:*:*:*\",\n                \"versionStartIncluding\": \"21.0.0\",\n                \"versionEndIncluding\": \"21.0.7.3\",\n                \"matchCriteriaId\": \"DDF503DD-23DC-4B22-8873-BE94BF0F1CD1\"\n              },\n              {\n                \"vulnerable\": true,\n                \"criteria\": \"cpe:2.3:a:ibm:robotic_process_automation:*:*:*:*:*:*:*:*\",\n                \"versionStartIncluding\": \"23.0.0\",\n                \"versionEndIncluding\": \"23.0.3\",\n                \"matchCriteriaId\": \"F513AA2B-F457-408B-8D5F-EBE657439000\"\n              }\n            ]\n          },\n          {\n            \"operator\": \"OR\",\n            \"negate\": false,\n            \"cpeMatch\": [\n              {\n                \"vulnerable\": false,\n                \"criteria\": \"cpe:2.3:a:redhat:openshift:-:*:*:*:*:*:*:*\",\n                \"matchCriteriaId\": \"F08E234C-BDCF-4B41-87B9-96BD5578CBBF\"\n              },\n              {\n                \"vulnerable\": false,\n                \"criteria\": \"cpe:2.3:o:microsoft:windows:-:*:*:*:*:*:*:*\",\n                \"matchCriteriaId\": \"A2572D17-1DE6-457B-99CC-64AFD54487EA\"\n              }\n            ]\n          }\n        ]\n      }\n    ],\n    \"references\": [\n      {\n        \"url\": \"https://exchange.xforce.ibmcloud.com/vulnerabilities/262293\",\n        \"source\": \"psirt@us.ibm.com\",\n        \"tags\": [\n          \"VDB Entry\",\n          \"Vendor Advisory\"\n        ]\n      },\n      {\n        \"url\": \"https://www.ibm.com/support/pages/node/7028223\",\n        \"source\": \"psirt@us.ibm.com\",\n        \"tags\": [\n          \"Patch\",\n          \"Vendor Advisory\"\n        ]\n      }\n    ]\n  }\n}\n"
  },
  {
    "path": "grype/db/v6/build/transformers/nvd/testdata/platform-cpe.json",
    "content": "{\n    \"cve\": {\n      \"id\": \"CVE-2022-26488\",\n      \"sourceIdentifier\": \"cve@mitre.org\",\n      \"published\": \"2022-03-10T17:47:45.383\",\n      \"lastModified\": \"2022-09-03T03:34:19.933\",\n      \"vulnStatus\": \"Analyzed\",\n      \"descriptions\": [\n        {\n          \"lang\": \"en\",\n          \"value\": \"In Python before 3.10.3 on Windows, local users can gain privileges because the search path is inadequately secured. The installer may allow a local attacker to add user-writable directories to the system search path. To exploit, an administrator must have installed Python for all users and enabled PATH entries. A non-administrative user can trigger a repair that incorrectly adds user-writable paths into PATH, enabling search-path hijacking of other users and system services. This affects Python (CPython) through 3.7.12, 3.8.x through 3.8.12, 3.9.x through 3.9.10, and 3.10.x through 3.10.2.\"\n        },\n        {\n          \"lang\": \"es\",\n          \"value\": \"En Python versiones anteriores a 3.10.3 en Windows, los usuarios locales pueden alcanzar privilegios porque la ruta de búsqueda no está asegurada apropiadamente. El instalador puede permitir a un atacante local añadir directorios escribibles por el usuario a la ruta de búsqueda del sistema. Para explotarla, un administrador debe haber instalado Python para todos los usuarios y habilitar las entradas PATH. Un usuario no administrador puede desencadenar una reparación que añada incorrectamente rutas escribibles por el usuario en el PATH, permitiendo el secuestro de la ruta de búsqueda de otros usuarios y servicios del sistema. Esto afecta a Python (CPython) versiones hasta 3.7.12, versiones 3.8.x hasta 3.8.12, versiones 3.9.x hasta 3.9.10, y versiones 3.10.x hasta 3.10.2\"\n        }\n      ],\n      \"metrics\": {\n        \"cvssMetricV31\": [\n          {\n            \"source\": \"nvd@nist.gov\",\n            \"type\": \"Primary\",\n            \"cvssData\": {\n              \"version\": \"3.1\",\n              \"vectorString\": \"CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H\",\n              \"attackVector\": \"LOCAL\",\n              \"attackComplexity\": \"HIGH\",\n              \"privilegesRequired\": \"LOW\",\n              \"userInteraction\": \"NONE\",\n              \"scope\": \"UNCHANGED\",\n              \"confidentialityImpact\": \"HIGH\",\n              \"integrityImpact\": \"HIGH\",\n              \"availabilityImpact\": \"HIGH\",\n              \"baseScore\": 7,\n              \"baseSeverity\": \"HIGH\"\n            },\n            \"exploitabilityScore\": 1,\n            \"impactScore\": 5.9\n          }\n        ],\n        \"cvssMetricV2\": [\n          {\n            \"source\": \"nvd@nist.gov\",\n            \"type\": \"Primary\",\n            \"cvssData\": {\n              \"version\": \"2.0\",\n              \"vectorString\": \"AV:L/AC:M/Au:N/C:P/I:P/A:P\",\n              \"accessVector\": \"LOCAL\",\n              \"accessComplexity\": \"MEDIUM\",\n              \"authentication\": \"NONE\",\n              \"confidentialityImpact\": \"PARTIAL\",\n              \"integrityImpact\": \"PARTIAL\",\n              \"availabilityImpact\": \"PARTIAL\",\n              \"baseScore\": 4.4\n            },\n            \"baseSeverity\": \"MEDIUM\",\n            \"exploitabilityScore\": 3.4,\n            \"impactScore\": 6.4,\n            \"acInsufInfo\": false,\n            \"obtainAllPrivilege\": false,\n            \"obtainUserPrivilege\": false,\n            \"obtainOtherPrivilege\": false,\n            \"userInteractionRequired\": false\n          }\n        ]\n      },\n      \"weaknesses\": [\n        {\n          \"source\": \"nvd@nist.gov\",\n          \"type\": \"Primary\",\n          \"description\": [\n            {\n              \"lang\": \"en\",\n              \"value\": \"CWE-426\"\n            }\n          ]\n        }\n      ],\n      \"configurations\": [\n        {\n          \"operator\": \"AND\",\n          \"nodes\": [\n            {\n              \"operator\": \"OR\",\n              \"negate\": false,\n              \"cpeMatch\": [\n                {\n                  \"vulnerable\": true,\n                  \"criteria\": \"cpe:2.3:a:python:python:*:*:*:*:*:*:*:*\",\n                  \"versionEndIncluding\": \"3.7.12\",\n                  \"matchCriteriaId\": \"1E05F88A-70C2-4DB6-9CCC-1D599AD26D4C\"\n                },\n                {\n                  \"vulnerable\": true,\n                  \"criteria\": \"cpe:2.3:a:python:python:*:*:*:*:*:*:*:*\",\n                  \"versionStartIncluding\": \"3.8.0\",\n                  \"versionEndIncluding\": \"3.8.12\",\n                  \"matchCriteriaId\": \"E80CA0FB-E708-4E92-BF36-7267F799FF8D\"\n                },\n                {\n                  \"vulnerable\": true,\n                  \"criteria\": \"cpe:2.3:a:python:python:*:*:*:*:*:*:*:*\",\n                  \"versionStartIncluding\": \"3.9.0\",\n                  \"versionEndIncluding\": \"3.9.10\",\n                  \"matchCriteriaId\": \"DD4B9F29-F505-4721-A630-C75103942F29\"\n                },\n                {\n                  \"vulnerable\": true,\n                  \"criteria\": \"cpe:2.3:a:python:python:*:*:*:*:*:*:*:*\",\n                  \"versionStartIncluding\": \"3.10.0\",\n                  \"versionEndIncluding\": \"3.10.2\",\n                  \"matchCriteriaId\": \"D5B55D1D-031C-4006-A368-BB66C2057916\"\n                },\n                {\n                  \"vulnerable\": true,\n                  \"criteria\": \"cpe:2.3:a:python:python:3.11.0:alpha1:*:*:*:*:*:*\",\n                  \"matchCriteriaId\": \"514A577E-5E60-40BA-ABD0-A8C5EB28BD90\"\n                },\n                {\n                  \"vulnerable\": true,\n                  \"criteria\": \"cpe:2.3:a:python:python:3.11.0:alpha2:*:*:*:*:*:*\",\n                  \"matchCriteriaId\": \"83B71795-9C81-4E5F-967C-C11808F24B05\"\n                },\n                {\n                  \"vulnerable\": true,\n                  \"criteria\": \"cpe:2.3:a:python:python:3.11.0:alpha3:*:*:*:*:*:*\",\n                  \"matchCriteriaId\": \"3F6F71F3-299E-4A4B-ADD1-EAD5A1D433E2\"\n                },\n                {\n                  \"vulnerable\": true,\n                  \"criteria\": \"cpe:2.3:a:python:python:3.11.0:alpha4:*:*:*:*:*:*\",\n                  \"matchCriteriaId\": \"09BBF4E9-EA54-41B5-948E-8E3D2660B7EF\"\n                },\n                {\n                  \"vulnerable\": true,\n                  \"criteria\": \"cpe:2.3:a:python:python:3.11.0:alpha4:*:*:*:*:*:*\",\n                  \"matchCriteriaId\": \"D9BBF4E9-EA54-41B5-948E-8E3D2660B7EF\"\n                },\n                {\n                  \"vulnerable\": true,\n                  \"criteria\": \"cpe:2.3:a:python:python:3.11.0:alpha5:*:*:*:*:*:*\",\n                  \"matchCriteriaId\": \"AEBFDCE7-81D4-4741-BB88-12C704515F5C\"\n                },\n                {\n                  \"vulnerable\": true,\n                  \"criteria\": \"cpe:2.3:a:python:python:3.11.0:alpha6:*:*:*:*:*:*\",\n                  \"matchCriteriaId\": \"156EB4C2-EFB7-4CEB-804D-93DB62992A63\"\n                }\n              ]\n            },\n            {\n              \"operator\": \"OR\",\n              \"negate\": false,\n              \"cpeMatch\": [\n                {\n                  \"vulnerable\": false,\n                  \"criteria\": \"cpe:2.3:o:microsoft:windows:-:*:*:*:*:*:*:*\",\n                  \"matchCriteriaId\": \"A2572D17-1DE6-457B-99CC-64AFD54487EA\"\n                }\n              ]\n            }\n          ]\n        },\n        {\n          \"operator\": \"AND\",\n          \"nodes\": [\n            {\n              \"operator\": \"OR\",\n              \"negate\": false,\n              \"cpeMatch\": [\n                {\n                  \"vulnerable\": true,\n                  \"criteria\": \"cpe:2.3:a:netapp:active_iq_unified_manager:-:*:*:*:*:windows:*:*\",\n                  \"matchCriteriaId\": \"B55E8D50-99B4-47EC-86F9-699B67D473CE\"\n                },\n                {\n                  \"vulnerable\": true,\n                  \"criteria\": \"cpe:2.3:a:netapp:ontap_select_deploy_administration_utility:-:*:*:*:*:*:*:*\",\n                  \"matchCriteriaId\": \"E7CF3019-975D-40BB-A8A4-894E62BD3797\"\n                }\n              ]\n            }\n          ]\n        }\n      ],\n      \"references\": [\n        {\n          \"url\": \"https://mail.python.org/archives/list/security-announce@python.org/thread/657Z4XULWZNIY5FRP3OWXHYKUSIH6DMN/\",\n          \"source\": \"cve@mitre.org\",\n          \"tags\": [\n            \"Patch\",\n            \"Vendor Advisory\"\n          ]\n        },\n        {\n          \"url\": \"https://security.netapp.com/advisory/ntap-20220419-0005/\",\n          \"source\": \"cve@mitre.org\",\n          \"tags\": [\n            \"Third Party Advisory\"\n          ]\n        }\n      ]\n    }\n  }\n"
  },
  {
    "path": "grype/db/v6/build/transformers/nvd/testdata/single-package-multi-distro.json",
    "content": "{\n  \"cve\": {\n    \"id\": \"CVE-2018-1000222\",\n    \"sourceIdentifier\": \"cve@mitre.org\",\n    \"published\": \"2018-08-20T20:29:01.347\",\n    \"lastModified\": \"2020-03-31T02:15:12.667\",\n    \"vulnStatus\": \"Modified\",\n    \"descriptions\": [\n      {\n        \"lang\": \"en\",\n        \"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.\"\n      },\n      {\n        \"lang\": \"es\",\n        \"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.\"\n      }\n    ],\n    \"metrics\": {\n      \"cvssMetricV30\": [\n        {\n          \"source\": \"nvd@nist.gov\",\n          \"type\": \"Primary\",\n          \"cvssData\": {\n            \"version\": \"3.0\",\n            \"vectorString\": \"CVSS:3.0/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H\",\n            \"attackVector\": \"NETWORK\",\n            \"attackComplexity\": \"LOW\",\n            \"privilegesRequired\": \"NONE\",\n            \"userInteraction\": \"REQUIRED\",\n            \"scope\": \"UNCHANGED\",\n            \"confidentialityImpact\": \"HIGH\",\n            \"integrityImpact\": \"HIGH\",\n            \"availabilityImpact\": \"HIGH\",\n            \"baseScore\": 8.8,\n            \"baseSeverity\": \"HIGH\"\n          },\n          \"exploitabilityScore\": 2.8,\n          \"impactScore\": 5.9\n        }\n      ],\n      \"cvssMetricV2\": [\n        {\n          \"source\": \"nvd@nist.gov\",\n          \"type\": \"Primary\",\n          \"cvssData\": {\n            \"version\": \"2.0\",\n            \"vectorString\": \"AV:N/AC:M/Au:N/C:P/I:P/A:P\",\n            \"accessVector\": \"NETWORK\",\n            \"accessComplexity\": \"MEDIUM\",\n            \"authentication\": \"NONE\",\n            \"confidentialityImpact\": \"PARTIAL\",\n            \"integrityImpact\": \"PARTIAL\",\n            \"availabilityImpact\": \"PARTIAL\",\n            \"baseScore\": 6.8\n          },\n          \"baseSeverity\": \"MEDIUM\",\n          \"exploitabilityScore\": 8.6,\n          \"impactScore\": 6.4,\n          \"acInsufInfo\": false,\n          \"obtainAllPrivilege\": false,\n          \"obtainUserPrivilege\": false,\n          \"obtainOtherPrivilege\": false,\n          \"userInteractionRequired\": true\n        }\n      ]\n    },\n    \"weaknesses\": [\n      {\n        \"source\": \"nvd@nist.gov\",\n        \"type\": \"Primary\",\n        \"description\": [\n          {\n            \"lang\": \"en\",\n            \"value\": \"CWE-415\"\n          }\n        ]\n      }\n    ],\n    \"configurations\": [\n      {\n        \"nodes\": [\n          {\n            \"operator\": \"OR\",\n            \"negate\": false,\n            \"cpeMatch\": [\n              {\n                \"vulnerable\": true,\n                \"criteria\": \"cpe:2.3:a:libgd:libgd:2.2.5:*:*:*:*:*:*:*\",\n                \"matchCriteriaId\": \"C257CC1C-BF6A-4125-AA61-9C2D09096084\"\n              }\n            ]\n          }\n        ]\n      },\n      {\n        \"nodes\": [\n          {\n            \"operator\": \"OR\",\n            \"negate\": false,\n            \"cpeMatch\": [\n              {\n                \"vulnerable\": true,\n                \"criteria\": \"cpe:2.3:o:canonical:ubuntu_linux:14.04:*:*:*:lts:*:*:*\",\n                \"matchCriteriaId\": \"B5A6F2F3-4894-4392-8296-3B8DD2679084\"\n              },\n              {\n                \"vulnerable\": true,\n                \"criteria\": \"cpe:2.3:o:canonical:ubuntu_linux:16.04:*:*:*:lts:*:*:*\",\n                \"matchCriteriaId\": \"F7016A2A-8365-4F1A-89A2-7A19F2BCAE5B\"\n              },\n              {\n                \"vulnerable\": true,\n                \"criteria\": \"cpe:2.3:o:canonical:ubuntu_linux:18.04:*:*:*:lts:*:*:*\",\n                \"matchCriteriaId\": \"23A7C53F-B80F-4E6A-AFA9-58EEA84BE11D\"\n              }\n            ]\n          }\n        ]\n      },\n      {\n        \"nodes\": [\n          {\n            \"operator\": \"OR\",\n            \"negate\": false,\n            \"cpeMatch\": [\n              {\n                \"vulnerable\": true,\n                \"criteria\": \"cpe:2.3:o:debian:debian_linux:8.0:*:*:*:*:*:*:*\",\n                \"matchCriteriaId\": \"C11E6FB0-C8C0-4527-9AA0-CB9B316F8F43\"\n              }\n            ]\n          }\n        ]\n      }\n    ],\n    \"references\": [\n      {\n        \"url\": \"https://github.com/libgd/libgd/issues/447\",\n        \"source\": \"cve@mitre.org\",\n        \"tags\": [\n          \"Issue Tracking\",\n          \"Third Party Advisory\"\n        ]\n      },\n      {\n        \"url\": \"https://lists.debian.org/debian-lts-announce/2019/01/msg00028.html\",\n        \"source\": \"cve@mitre.org\",\n        \"tags\": [\n          \"Mailing List\",\n          \"Third Party Advisory\"\n        ]\n      },\n      {\n        \"url\": \"https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/3CZ2QADQTKRHTGB2AHD7J4QQNDLBEMM6/\",\n        \"source\": \"cve@mitre.org\"\n      },\n      {\n        \"url\": \"https://security.gentoo.org/glsa/201903-18\",\n        \"source\": \"cve@mitre.org\",\n        \"tags\": [\n          \"Third Party Advisory\"\n        ]\n      },\n      {\n        \"url\": \"https://usn.ubuntu.com/3755-1/\",\n        \"source\": \"cve@mitre.org\",\n        \"tags\": [\n          \"Mitigation\",\n          \"Third Party Advisory\"\n        ]\n      }\n    ]\n  }\n}\n"
  },
  {
    "path": "grype/db/v6/build/transformers/nvd/testdata/version-range.json",
    "content": "{\n  \"cve\": {\n    \"id\": \"CVE-2018-5487\",\n    \"sourceIdentifier\": \"security-alert@netapp.com\",\n    \"published\": \"2018-05-24T14:29:00.390\",\n    \"lastModified\": \"2018-07-05T13:52:30.627\",\n    \"vulnStatus\": \"Analyzed\",\n    \"descriptions\": [\n      {\n        \"lang\": \"en\",\n        \"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.\"\n      },\n      {\n        \"lang\": \"es\",\n        \"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.\"\n      }\n    ],\n    \"metrics\": {\n      \"cvssMetricV40\": [\n        {\n          \"source\": \"security@zabbix.com\",\n          \"type\": \"Secondary\",\n          \"cvssData\": {\n            \"version\": \"4.0\",\n            \"vectorString\": \"CVSS:4.0/AV:N/AC:H/AT:N/PR:N/UI:A/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X\",\n            \"baseScore\": 7.5,\n            \"baseSeverity\": \"HIGH\",\n            \"attackVector\": \"NETWORK\",\n            \"attackComplexity\": \"HIGH\",\n            \"attackRequirements\": \"NONE\",\n            \"privilegesRequired\": \"NONE\",\n            \"userInteraction\": \"ACTIVE\",\n            \"vulnConfidentialityImpact\": \"HIGH\",\n            \"vulnIntegrityImpact\": \"HIGH\",\n            \"vulnAvailabilityImpact\": \"HIGH\",\n            \"subConfidentialityImpact\": \"NONE\",\n            \"subIntegrityImpact\": \"NONE\",\n            \"subAvailabilityImpact\": \"NONE\",\n            \"exploitMaturity\": \"NOT_DEFINED\",\n            \"confidentialityRequirement\": \"NOT_DEFINED\",\n            \"integrityRequirement\": \"NOT_DEFINED\",\n            \"availabilityRequirement\": \"NOT_DEFINED\",\n            \"modifiedAttackVector\": \"NOT_DEFINED\",\n            \"modifiedAttackComplexity\": \"NOT_DEFINED\",\n            \"modifiedAttackRequirements\": \"NOT_DEFINED\",\n            \"modifiedPrivilegesRequired\": \"NOT_DEFINED\",\n            \"modifiedUserInteraction\": \"NOT_DEFINED\",\n            \"modifiedVulnConfidentialityImpact\": \"NOT_DEFINED\",\n            \"modifiedVulnIntegrityImpact\": \"NOT_DEFINED\",\n            \"modifiedVulnAvailabilityImpact\": \"NOT_DEFINED\",\n            \"modifiedSubConfidentialityImpact\": \"NOT_DEFINED\",\n            \"modifiedSubIntegrityImpact\": \"NOT_DEFINED\",\n            \"modifiedSubAvailabilityImpact\": \"NOT_DEFINED\",\n            \"Safety\": \"NOT_DEFINED\",\n            \"Automatable\": \"NOT_DEFINED\",\n            \"Recovery\": \"NOT_DEFINED\",\n            \"valueDensity\": \"NOT_DEFINED\",\n            \"vulnerabilityResponseEffort\": \"NOT_DEFINED\",\n            \"providerUrgency\": \"NOT_DEFINED\"\n          }\n        }\n      ],\n      \"cvssMetricV30\": [\n        {\n          \"source\": \"nvd@nist.gov\",\n          \"type\": \"Primary\",\n          \"cvssData\": {\n            \"version\": \"3.0\",\n            \"vectorString\": \"CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H\",\n            \"attackVector\": \"NETWORK\",\n            \"attackComplexity\": \"LOW\",\n            \"privilegesRequired\": \"NONE\",\n            \"userInteraction\": \"NONE\",\n            \"scope\": \"UNCHANGED\",\n            \"confidentialityImpact\": \"HIGH\",\n            \"integrityImpact\": \"HIGH\",\n            \"availabilityImpact\": \"HIGH\",\n            \"baseScore\": 9.8,\n            \"baseSeverity\": \"CRITICAL\"\n          },\n          \"exploitabilityScore\": 3.9,\n          \"impactScore\": 5.9\n        }\n      ],\n      \"cvssMetricV2\": [\n        {\n          \"source\": \"nvd@nist.gov\",\n          \"type\": \"Primary\",\n          \"cvssData\": {\n            \"version\": \"2.0\",\n            \"vectorString\": \"AV:N/AC:L/Au:N/C:P/I:P/A:P\",\n            \"accessVector\": \"NETWORK\",\n            \"accessComplexity\": \"LOW\",\n            \"authentication\": \"NONE\",\n            \"confidentialityImpact\": \"PARTIAL\",\n            \"integrityImpact\": \"PARTIAL\",\n            \"availabilityImpact\": \"PARTIAL\",\n            \"baseScore\": 7.5\n          },\n          \"baseSeverity\": \"HIGH\",\n          \"exploitabilityScore\": 10.0,\n          \"impactScore\": 6.4,\n          \"acInsufInfo\": true,\n          \"obtainAllPrivilege\": false,\n          \"obtainUserPrivilege\": false,\n          \"obtainOtherPrivilege\": false,\n          \"userInteractionRequired\": false\n        }\n      ]\n    },\n    \"weaknesses\": [\n      {\n        \"source\": \"nvd@nist.gov\",\n        \"type\": \"Primary\",\n        \"description\": [\n          {\n            \"lang\": \"en\",\n            \"value\": \"CWE-20\"\n          }\n        ]\n      }\n    ],\n    \"configurations\": [\n      {\n        \"operator\": \"AND\",\n        \"nodes\": [\n          {\n            \"operator\": \"OR\",\n            \"negate\": false,\n            \"cpeMatch\": [\n              {\n                \"vulnerable\": true,\n                \"criteria\": \"cpe:2.3:a:netapp:oncommand_unified_manager:*:*:*:*:*:*:*:*\",\n                \"versionStartIncluding\": \"7.2\",\n                \"versionEndIncluding\": \"7.3\",\n                \"matchCriteriaId\": \"A5949307-3E9B-441F-B008-81A0E0228DC0\"\n              }\n            ]\n          },\n          {\n            \"operator\": \"OR\",\n            \"negate\": false,\n            \"cpeMatch\": [\n              {\n                \"vulnerable\": false,\n                \"criteria\": \"cpe:2.3:o:linux:linux_kernel:-:*:*:*:*:*:*:*\",\n                \"matchCriteriaId\": \"703AF700-7A70-47E2-BC3A-7FD03B3CA9C1\"\n              }\n            ]\n          }\n        ]\n      }\n    ],\n    \"references\": [\n      {\n        \"url\": \"https://security.netapp.com/advisory/ntap-20180523-0001/\",\n        \"source\": \"security-alert@netapp.com\",\n        \"tags\": [\n          \"Patch\",\n          \"Vendor Advisory\"\n        ]\n      }\n    ]\n  }\n}\n"
  },
  {
    "path": "grype/db/v6/build/transformers/nvd/transform.go",
    "content": "package nvd\n\nimport (\n\t\"regexp\"\n\t\"sort\"\n\t\"strings\"\n\n\t\"github.com/scylladb/go-set/strset\"\n\n\t\"github.com/anchore/grype/grype/db/data\"\n\t\"github.com/anchore/grype/grype/db/internal/provider/unmarshal\"\n\t\"github.com/anchore/grype/grype/db/internal/provider/unmarshal/nvd\"\n\t\"github.com/anchore/grype/grype/db/provider\"\n\tdb \"github.com/anchore/grype/grype/db/v6\"\n\t\"github.com/anchore/grype/grype/db/v6/build/transformers\"\n\t\"github.com/anchore/grype/grype/db/v6/build/transformers/internal\"\n\t\"github.com/anchore/grype/grype/pkg\"\n\t\"github.com/anchore/grype/internal/log\"\n\t\"github.com/anchore/syft/syft/cpe\"\n)\n\nvar cwePattern = regexp.MustCompile(`^CWE-\\d+$`)\n\ntype Config struct {\n\tCPEParts            *strset.Set\n\tInferNVDFixVersions bool\n}\n\nfunc defaultConfig() Config {\n\treturn Config{\n\t\tCPEParts:            strset.New(\"a\", \"h\", \"o\"),\n\t\tInferNVDFixVersions: true,\n\t}\n}\n\nfunc getVersionFormat(cpeProduct string) string {\n\tif pkg.HasJvmPackageName(cpeProduct) {\n\t\treturn \"jvm\"\n\t}\n\treturn \"\"\n}\n\nfunc Transformer(cfg Config) data.NVDTransformerV2 {\n\tif cfg == (Config{}) {\n\t\tcfg = defaultConfig()\n\t}\n\treturn func(vulnerability unmarshal.NVDVulnerability, state provider.State) ([]data.Entry, error) {\n\t\treturn transform(cfg, vulnerability, state)\n\t}\n}\n\nfunc transform(cfg Config, vulnerability unmarshal.NVDVulnerability, state provider.State) ([]data.Entry, error) {\n\tin := []any{\n\t\tdb.VulnerabilityHandle{\n\t\t\tName:          vulnerability.ID,\n\t\t\tProviderID:    state.Provider,\n\t\t\tProvider:      provider.Model(state),\n\t\t\tModifiedDate:  internal.ParseTime(vulnerability.LastModified),\n\t\t\tPublishedDate: internal.ParseTime(vulnerability.Published),\n\t\t\tStatus:        getVulnStatus(vulnerability),\n\t\t\tBlobValue: &db.VulnerabilityBlob{\n\t\t\t\tID:          vulnerability.ID,\n\t\t\t\tAssigners:   getAssigner(vulnerability),\n\t\t\t\tDescription: strings.TrimSpace(vulnerability.Description()),\n\t\t\t\tReferences:  getReferences(vulnerability),\n\t\t\t\tSeverities:  getSeverities(vulnerability),\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, a := range getAffected(cfg, vulnerability) {\n\t\tin = append(in, a)\n\t}\n\n\tfor _, cwe := range getCWEs(vulnerability) {\n\t\tin = append(in, cwe)\n\t}\n\n\treturn transformers.NewEntries(in...), nil\n}\n\nfunc getAssigner(vuln unmarshal.NVDVulnerability) []string {\n\tif vuln.SourceIdentifier == nil {\n\t\treturn nil\n\t}\n\n\tassigner := *vuln.SourceIdentifier\n\n\tif assigner == \"\" {\n\t\treturn nil\n\t}\n\n\treturn []string{assigner}\n}\n\nfunc getVulnStatus(vuln unmarshal.NVDVulnerability) db.VulnerabilityStatus {\n\tif vuln.VulnStatus == nil {\n\t\treturn db.UnknownVulnerabilityStatus\n\t}\n\n\t// TODO: there is no path for withdrawn?\n\n\t// based off of the NVD or CVE list status, set the current vulnerability record status\n\t// see https://nvd.nist.gov/vuln/vulnerability-status\n\ts := strings.TrimSpace(strings.ReplaceAll(strings.ToLower(*vuln.VulnStatus), \" \", \"\"))\n\tswitch s {\n\tcase \"reserved\", \"received\":\n\t\t// reserved (CVE list): A CVE Entry is marked as \"RESERVED\" when it has been reserved for use by a CVE Numbering Authority (CNA) or security\n\t\t//    researcher, but the details of it are not yet populated. A CVE Entry can change from the RESERVED state to being populated at any time\n\t\t//    based on a number of factors both internal and external to the CVE List.\n\t\t//\n\t\t// received (NVD): CVE has been recently published to the CVE List and has been received by the NVD.\n\t\t//\n\t\treturn db.UnknownVulnerabilityStatus\n\tcase \"awaitinganalysis\", \"undergoinganalysis\":\n\t\t// awaiting analysis (NVD): CVE has been marked for Analysis. Normally once in this state the CVE will be analyzed by NVD staff within 24 hours.\n\t\t//\n\t\t// undergoing analysis (NVD): CVE has been marked for Analysis. Normally once in this state the CVE will be analyzed by NVD staff within 24 hours.\n\t\t//\n\t\treturn db.VulnerabilityAnalyzing\n\tcase \"disputed\":\n\t\t// disputed (CVE list): When one party disagrees with another party's assertion that a particular issue in software is a vulnerability, a CVE Entry assigned\n\t\t//    to that issue may be designated as being \"DISPUTED\". In these cases, CVE is making no determination as to which party is correct. Instead, we make\n\t\t//    note of this dispute and try to offer any public references that will better inform those trying to understand the facts of the issue.\n\t\t//    When you see a CVE Entry that is \"DISPUTED\", we encourage you to research the issue through the references or by contacting the affected\n\t\t//    vendor or developer for more information.\n\t\t//\n\t\treturn db.VulnerabilityDisputed\n\tcase \"rejected\", \"reject\":\n\t\t// reject (CVE list): A CVE Entry listed as \"REJECT\" is a CVE Entry that is not accepted as a CVE Entry. The reason a CVE Entry is marked\n\t\t//    REJECT will most often be stated in the description of the CVE Entry. Possible examples include it being a duplicate CVE Entry, it being\n\t\t//    withdrawn by the original requester, it being assigned incorrectly, or some other administrative reason.\n\t\t//    As a rule, REJECT CVE Entries should be ignored.\n\t\t//\n\t\t// rejected (NVD): CVE has been marked as \"**REJECT**\" in the CVE List. These CVEs are stored in the NVD, but do not show up in search results.\n\t\treturn db.VulnerabilityRejected\n\tcase \"modified\", \"analyzed\", \"published\":\n\t\t// modified (NVD): CVE has been amended by a source (CVE Primary CNA or another CNA). Analysis data supplied by the NVD may be no longer be accurate due to these changes.\n\t\t//\n\t\t// analyzed (NVD): CVE has had analysis completed and all data associations made. Each Analysis has three sub-types, Initial, Modified and Reanalysis:\n\t\t//    Initial: Used to show the first time analysis was performed on a given CVE.\n\t\t//    Modified: Used to show that analysis was performed due to a modification the CVE’s information.\n\t\t//    Reanalysis: Used to show that new analysis occurred, but was not due to a modification from an external source.Analyzed CVEs do not show a banner on the vulnerability detail page.\n\t\t//\n\t\t// published (CVE list): The CVE Entry is populated with details. These are a CVE Description and reference link[s] regarding details of the CVE.\n\t\t//\n\t\treturn db.VulnerabilityActive\n\t}\n\n\treturn db.UnknownVulnerabilityStatus\n}\n\nfunc getAffected(cfg Config, vulnerability unmarshal.NVDVulnerability) []db.AffectedCPEHandle {\n\tcandidates, err := allCandidates(vulnerability.ID, vulnerability.Configurations, cfg)\n\tif err != nil {\n\t\tlog.WithFields(\"error\", err).Warn(\"failed to process affected NVD CPEs\")\n\t\treturn nil\n\t}\n\n\tvar affs []db.AffectedCPEHandle\n\tfor _, candidate := range candidates {\n\t\taffs = append(affs, affectedApplicationPackage(cfg, vulnerability, candidate)...)\n\t}\n\n\treturn affs\n}\n\nfunc getCWEs(vulnerability unmarshal.NVDVulnerability) []db.CWEHandle {\n\tvar cwes []db.CWEHandle\n\tfor _, w := range vulnerability.Weaknesses {\n\t\tfor _, d := range w.Description {\n\t\t\tif !isValidCWE(d.Value) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tcwes = append(cwes, db.CWEHandle{\n\t\t\t\tCVE:    vulnerability.ID,\n\t\t\t\tCWE:    d.Value,\n\t\t\t\tSource: w.Source,\n\t\t\t\tType:   w.Type,\n\t\t\t})\n\t\t}\n\t}\n\treturn cwes\n}\n\nfunc isValidCWE(cwe string) bool {\n\tswitch cwe {\n\tcase \"\":\n\t\treturn false\n\tcase \"NVD-CWE-noinfo\":\n\t\treturn false // explicitly skip these rather than fill the database with meaningless entries\n\tcase \"NVD-CWE-Other\":\n\t\treturn true\n\tdefault:\n\t\treturn cwePattern.MatchString(cwe)\n\t}\n}\n\nfunc encodeCPEs(cpes []cpe.Attributes) []string {\n\tvar results []string\n\tfor _, c := range cpes {\n\t\tresults = append(results, c.String())\n\t}\n\treturn results\n}\n\nfunc affectedApplicationPackage(cfg Config, vulnerability unmarshal.NVDVulnerability, p affectedPackageCandidate) []db.AffectedCPEHandle {\n\tvar affs []db.AffectedCPEHandle\n\n\tvar qualifiers *db.PackageQualifiers\n\tif len(p.PlatformCPEs) > 0 {\n\t\tqualifiers = &db.PackageQualifiers{\n\t\t\tPlatformCPEs: encodeCPEs(p.PlatformCPEs),\n\t\t}\n\t}\n\n\taffs = append(affs, db.AffectedCPEHandle{\n\t\tCPE: getCPEFromAttributes(p.VulnerableCPE),\n\t\tBlobValue: &db.PackageBlob{\n\t\t\tCVEs:       []string{vulnerability.ID},\n\t\t\tQualifiers: qualifiers,\n\t\t\tRanges:     getRanges(cfg, p.VulnerableCPE, p.Ranges.toSlice(), vulnerability.ID),\n\t\t},\n\t})\n\n\treturn affs\n}\n\nfunc getRanges(cfg Config, c cpe.Attributes, ras []affectedCPERange, vulnID string) []db.Range {\n\tvar ranges []db.Range\n\tfor _, ra := range ras {\n\t\tr := getRange(cfg, c, ra, vulnID)\n\t\tif r != nil {\n\t\t\tranges = append(ranges, *r)\n\t\t}\n\t}\n\n\treturn ranges\n}\n\nfunc getRange(cfg Config, c cpe.Attributes, ra affectedCPERange, vulnID string) *db.Range {\n\treturn &db.Range{\n\t\tVersion: db.Version{\n\t\t\tType:       getVersionFormat(c.Product),\n\t\t\tConstraint: ra.String(),\n\t\t},\n\t\tFix: getFix(cfg, c, ra, vulnID),\n\t}\n}\n\nfunc getFix(cfg Config, vulnCPE cpe.Attributes, ra affectedCPERange, vulnID string) *db.Fix {\n\tif !cfg.InferNVDFixVersions {\n\t\treturn nil\n\t}\n\n\tpossiblyFixed := strset.New()\n\tknownAffected := strset.New()\n\tunspecifiedSet := strset.New(\"*\", \"-\", \"*\")\n\n\tif ra.VersionEndExcluding != \"\" && !unspecifiedSet.Has(ra.VersionEndExcluding) {\n\t\tpossiblyFixed.Add(ra.VersionEndExcluding)\n\t}\n\n\tif ra.VersionStartIncluding != \"\" && !unspecifiedSet.Has(ra.VersionStartIncluding) {\n\t\tknownAffected.Add(ra.VersionStartIncluding)\n\t}\n\n\tif ra.VersionEndIncluding != \"\" && !unspecifiedSet.Has(ra.VersionEndIncluding) {\n\t\tknownAffected.Add(ra.VersionEndIncluding)\n\t}\n\n\tif !unspecifiedSet.Has(vulnCPE.Version) {\n\t\tknownAffected.Add(vulnCPE.Version)\n\t}\n\n\tpossiblyFixed.Remove(knownAffected.List()...)\n\n\tif possiblyFixed.Size() != 1 {\n\t\treturn nil\n\t}\n\n\tfixVersion := possiblyFixed.List()[0]\n\n\t// only include fix details if we have a date and kind that matches the inferred fix version\n\tvar detail *db.FixDetail\n\tif ra.FixInfo != nil {\n\t\tif fixVersion == ra.FixInfo.Version {\n\t\t\tdetail = &db.FixDetail{\n\t\t\t\tAvailable: &db.FixAvailability{\n\t\t\t\t\tDate: internal.ParseTime(ra.FixInfo.Date),\n\t\t\t\t\tKind: ra.FixInfo.Kind,\n\t\t\t\t},\n\t\t\t}\n\t\t} else {\n\t\t\tlog.WithFields(\"cpe\", vulnCPE, \"vuln\", vulnID, \"range\", ra, \"fix\", ra.FixInfo.Version).Debug(\"skipping fix detail because it does not match inferred fix version\")\n\t\t}\n\t}\n\treturn &db.Fix{\n\t\tVersion: fixVersion,\n\t\tState:   db.FixedStatus,\n\t\tDetail:  detail,\n\t}\n}\n\nfunc getCPEFromAttributes(atts cpe.Attributes) *db.Cpe {\n\treturn &db.Cpe{\n\t\tPart:            atts.Part,\n\t\tVendor:          atts.Vendor,\n\t\tProduct:         atts.Product,\n\t\tEdition:         atts.Edition,\n\t\tLanguage:        atts.Language,\n\t\tSoftwareEdition: atts.SWEdition,\n\t\tTargetHardware:  atts.TargetHW,\n\t\tTargetSoftware:  atts.TargetSW,\n\t\tOther:           atts.Other,\n\t}\n}\n\nfunc getSeverities(vuln unmarshal.NVDVulnerability) []db.Severity {\n\tsevs := nvd.CvssSummaries(vuln.CVSS()).Sorted()\n\tvar results []db.Severity\n\tfor _, sev := range sevs {\n\t\tpriority := 2\n\t\tif sev.Type == nvd.Primary {\n\t\t\tpriority = 1\n\t\t}\n\t\tresults = append(results, db.Severity{\n\t\t\tScheme: db.SeveritySchemeCVSS,\n\t\t\tValue: db.CVSSSeverity{\n\t\t\t\tVector:  sev.Vector,\n\t\t\t\tVersion: sev.Version,\n\t\t\t},\n\t\t\tSource: sev.Source,\n\t\t\tRank:   priority,\n\t\t})\n\t}\n\n\treturn results\n}\n\nfunc getReferences(vuln unmarshal.NVDVulnerability) []db.Reference {\n\treferences := []db.Reference{\n\t\t{\n\t\t\tURL: \"https://nvd.nist.gov/vuln/detail/\" + vuln.ID,\n\t\t},\n\t}\n\tfor _, reference := range vuln.References {\n\t\tif reference.URL == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\ttags := db.NormalizeReferenceTags(reference.Tags)\n\t\tsort.Strings(tags)\n\t\t// TODO there is other info we could be capturing too (source)\n\t\treferences = append(references, db.Reference{\n\t\t\tURL:  reference.URL,\n\t\t\tTags: tags,\n\t\t})\n\t}\n\n\treturn transformers.DeduplicateReferences(references)\n}\n"
  },
  {
    "path": "grype/db/v6/build/transformers/nvd/transform_test.go",
    "content": "package nvd\n\nimport (\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/google/go-cmp/cmp/cmpopts\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/grype/grype/db/internal/provider/unmarshal\"\n\t\"github.com/anchore/grype/grype/db/internal/provider/unmarshal/nvd\"\n\t\"github.com/anchore/grype/grype/db/provider\"\n\tdb \"github.com/anchore/grype/grype/db/v6\"\n\t\"github.com/anchore/grype/grype/db/v6/build/transformers\"\n)\n\nvar (\n\ttimeVal = time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC)\n\tlisting = provider.File{\n\t\tPath:      \"some\",\n\t\tDigest:    \"123456\",\n\t\tAlgorithm: \"sha256\",\n\t}\n)\n\nfunc inputProviderState(name string) provider.State {\n\treturn provider.State{\n\t\tProvider:  name,\n\t\tVersion:   12,\n\t\tProcessor: \"vunnel@1.2.3\",\n\t\tTimestamp: timeVal,\n\t\tListing:   &listing,\n\t}\n}\n\nfunc expectedProvider(name string) *db.Provider {\n\treturn &db.Provider{\n\t\tID:           name,\n\t\tVersion:      \"12\",\n\t\tProcessor:    \"vunnel@1.2.3\",\n\t\tDateCaptured: &timeVal,\n\t\tInputDigest:  \"sha256:123456\",\n\t}\n}\n\nfunc TestTransform(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tfixture  string\n\t\tconfig   Config\n\t\tprovider string\n\t\twant     []transformers.RelatedEntries\n\t}{\n\t\t{\n\t\t\tname:     \"basic version range\",\n\t\t\tfixture:  \"testdata/version-range.json\",\n\t\t\tprovider: \"nvd\",\n\t\t\tconfig:   defaultConfig(),\n\t\t\twant: []transformers.RelatedEntries{\n\t\t\t\t{\n\t\t\t\t\tVulnerabilityHandle: &db.VulnerabilityHandle{\n\t\t\t\t\t\tName:          \"CVE-2018-5487\",\n\t\t\t\t\t\tProviderID:    \"nvd\",\n\t\t\t\t\t\tProvider:      expectedProvider(\"nvd\"),\n\t\t\t\t\t\tModifiedDate:  timeRef(time.Date(2018, 7, 5, 13, 52, 30, 627000000, time.UTC)),\n\t\t\t\t\t\tPublishedDate: timeRef(time.Date(2018, 5, 24, 14, 29, 0, 390000000, time.UTC)),\n\t\t\t\t\t\tStatus:        db.VulnerabilityActive,\n\t\t\t\t\t\tBlobValue: &db.VulnerabilityBlob{\n\t\t\t\t\t\t\tID:          \"CVE-2018-5487\",\n\t\t\t\t\t\t\tAssigners:   []string{\"security-alert@netapp.com\"},\n\t\t\t\t\t\t\tDescription: \"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.\",\n\t\t\t\t\t\t\tReferences: []db.Reference{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tURL: \"https://nvd.nist.gov/vuln/detail/CVE-2018-5487\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tURL:  \"https://security.netapp.com/advisory/ntap-20180523-0001/\",\n\t\t\t\t\t\t\t\t\tTags: []string{\"patch\", \"vendor-advisory\"},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tSeverities: []db.Severity{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tScheme: db.SeveritySchemeCVSS,\n\t\t\t\t\t\t\t\t\tValue: db.CVSSSeverity{\n\t\t\t\t\t\t\t\t\t\tVector:  \"CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H\",\n\t\t\t\t\t\t\t\t\t\tVersion: \"3.0\",\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\tSource: \"nvd@nist.gov\",\n\t\t\t\t\t\t\t\t\tRank:   1,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tScheme: db.SeveritySchemeCVSS,\n\t\t\t\t\t\t\t\t\tValue: db.CVSSSeverity{\n\t\t\t\t\t\t\t\t\t\tVector:  \"AV:N/AC:L/Au:N/C:P/I:P/A:P\",\n\t\t\t\t\t\t\t\t\t\tVersion: \"2.0\",\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\tSource: \"nvd@nist.gov\",\n\t\t\t\t\t\t\t\t\tRank:   1,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tScheme: \"CVSS\",\n\t\t\t\t\t\t\t\t\tValue: db.CVSSSeverity{\n\t\t\t\t\t\t\t\t\t\tVector:  \"CVSS:4.0/AV:N/AC:H/AT:N/PR:N/UI:A/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X\",\n\t\t\t\t\t\t\t\t\t\tVersion: \"4.0\",\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\tSource: \"security@zabbix.com\",\n\t\t\t\t\t\t\t\t\tRank:   2,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tRelated: relatedEntries(\n\t\t\t\t\t\tdb.AffectedCPEHandle{\n\t\t\t\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\t\t\t\tCVEs: []string{\"CVE-2018-5487\"},\n\t\t\t\t\t\t\t\tQualifiers: &db.PackageQualifiers{\n\t\t\t\t\t\t\t\t\tPlatformCPEs: []string{\"cpe:2.3:o:linux:linux_kernel:-:*:*:*:*:*:*:*\"},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tVersion: db.Version{\n\t\t\t\t\t\t\t\t\t\t\tConstraint: \">= 7.2, <= 7.3\",\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tCPE: &db.Cpe{\n\t\t\t\t\t\t\t\tPart:    \"a\",\n\t\t\t\t\t\t\t\tVendor:  \"netapp\",\n\t\t\t\t\t\t\t\tProduct: \"oncommand_unified_manager\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tdb.CWEHandle{\n\t\t\t\t\t\t\tCVE:    \"CVE-2018-5487\",\n\t\t\t\t\t\t\tCWE:    \"CWE-20\",\n\t\t\t\t\t\t\tSource: \"nvd@nist.gov\",\n\t\t\t\t\t\t\tType:   \"Primary\",\n\t\t\t\t\t\t},\n\t\t\t\t\t),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:     \"with fix version information\",\n\t\t\tfixture:  \"testdata/fix-version.json\",\n\t\t\tprovider: \"nvd\",\n\t\t\tconfig:   defaultConfig(),\n\t\t\twant: []transformers.RelatedEntries{\n\t\t\t\t{\n\t\t\t\t\tVulnerabilityHandle: &db.VulnerabilityHandle{\n\t\t\t\t\t\tName:          \"CVE-2018-5487\",\n\t\t\t\t\t\tProviderID:    \"nvd\",\n\t\t\t\t\t\tProvider:      expectedProvider(\"nvd\"),\n\t\t\t\t\t\tModifiedDate:  timeRef(time.Date(2018, 7, 5, 13, 52, 30, 627000000, time.UTC)),\n\t\t\t\t\t\tPublishedDate: timeRef(time.Date(2018, 5, 24, 14, 29, 0, 390000000, time.UTC)),\n\t\t\t\t\t\tStatus:        db.VulnerabilityActive,\n\t\t\t\t\t\tBlobValue: &db.VulnerabilityBlob{\n\t\t\t\t\t\t\tID:          \"CVE-2018-5487\",\n\t\t\t\t\t\t\tAssigners:   []string{\"security-alert@netapp.com\"},\n\t\t\t\t\t\t\tDescription: \"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.\",\n\t\t\t\t\t\t\tReferences: []db.Reference{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tURL: \"https://nvd.nist.gov/vuln/detail/CVE-2018-5487\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tURL:  \"https://security.netapp.com/advisory/ntap-20180523-0001/\",\n\t\t\t\t\t\t\t\t\tTags: []string{\"patch\", \"vendor-advisory\"},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tSeverities: []db.Severity{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tScheme: db.SeveritySchemeCVSS,\n\t\t\t\t\t\t\t\t\tValue: db.CVSSSeverity{\n\t\t\t\t\t\t\t\t\t\tVector:  \"CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H\",\n\t\t\t\t\t\t\t\t\t\tVersion: \"3.0\",\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\tSource: \"nvd@nist.gov\",\n\t\t\t\t\t\t\t\t\tRank:   1,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tScheme: db.SeveritySchemeCVSS,\n\t\t\t\t\t\t\t\t\tValue: db.CVSSSeverity{\n\t\t\t\t\t\t\t\t\t\tVector:  \"AV:N/AC:L/Au:N/C:P/I:P/A:P\",\n\t\t\t\t\t\t\t\t\t\tVersion: \"2.0\",\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\tSource: \"nvd@nist.gov\",\n\t\t\t\t\t\t\t\t\tRank:   1,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tScheme: \"CVSS\",\n\t\t\t\t\t\t\t\t\tValue: db.CVSSSeverity{\n\t\t\t\t\t\t\t\t\t\tVector:  \"CVSS:4.0/AV:N/AC:H/AT:N/PR:N/UI:A/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X\",\n\t\t\t\t\t\t\t\t\t\tVersion: \"4.0\",\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\tSource: \"security@zabbix.com\",\n\t\t\t\t\t\t\t\t\tRank:   2,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tRelated: relatedEntries(\n\t\t\t\t\t\tdb.AffectedCPEHandle{\n\t\t\t\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\t\t\t\tCVEs: []string{\"CVE-2018-5487\"},\n\t\t\t\t\t\t\t\tQualifiers: &db.PackageQualifiers{\n\t\t\t\t\t\t\t\t\tPlatformCPEs: []string{\"cpe:2.3:o:linux:linux_kernel:-:*:*:*:*:*:*:*\"},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tVersion: db.Version{\n\t\t\t\t\t\t\t\t\t\t\tConstraint: \">= 7.2, < 7.3\",\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\tFix: &db.Fix{\n\t\t\t\t\t\t\t\t\t\t\tVersion: \"7.3\",\n\t\t\t\t\t\t\t\t\t\t\tState:   db.FixedStatus,\n\t\t\t\t\t\t\t\t\t\t\tDetail: &db.FixDetail{ // important! fix detail is associated to the record\n\t\t\t\t\t\t\t\t\t\t\t\tAvailable: &db.FixAvailability{\n\t\t\t\t\t\t\t\t\t\t\t\t\tDate: timeRef(time.Date(2018, 5, 23, 0, 0, 0, 0, time.UTC)),\n\t\t\t\t\t\t\t\t\t\t\t\t\tKind: \"advisory\",\n\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tCPE: &db.Cpe{\n\t\t\t\t\t\t\t\tPart:    \"a\",\n\t\t\t\t\t\t\t\tVendor:  \"netapp\",\n\t\t\t\t\t\t\t\tProduct: \"oncommand_unified_manager\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tdb.CWEHandle{\n\t\t\t\t\t\t\tCVE:    \"CVE-2018-5487\",\n\t\t\t\t\t\t\tCWE:    \"CWE-20\",\n\t\t\t\t\t\t\tSource: \"nvd@nist.gov\",\n\t\t\t\t\t\t\tType:   \"Primary\",\n\t\t\t\t\t\t},\n\t\t\t\t\t),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:     \"mismatched fix info\",\n\t\t\tfixture:  \"testdata/fix-wrong-version.json\",\n\t\t\tprovider: \"nvd\",\n\t\t\tconfig:   defaultConfig(),\n\t\t\twant: []transformers.RelatedEntries{\n\t\t\t\t{\n\t\t\t\t\tVulnerabilityHandle: &db.VulnerabilityHandle{\n\t\t\t\t\t\tName:          \"CVE-2018-5487\",\n\t\t\t\t\t\tProviderID:    \"nvd\",\n\t\t\t\t\t\tProvider:      expectedProvider(\"nvd\"),\n\t\t\t\t\t\tModifiedDate:  timeRef(time.Date(2018, 7, 5, 13, 52, 30, 627000000, time.UTC)),\n\t\t\t\t\t\tPublishedDate: timeRef(time.Date(2018, 5, 24, 14, 29, 0, 390000000, time.UTC)),\n\t\t\t\t\t\tStatus:        db.VulnerabilityActive,\n\t\t\t\t\t\tBlobValue: &db.VulnerabilityBlob{\n\t\t\t\t\t\t\tID:          \"CVE-2018-5487\",\n\t\t\t\t\t\t\tAssigners:   []string{\"security-alert@netapp.com\"},\n\t\t\t\t\t\t\tDescription: \"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.\",\n\t\t\t\t\t\t\tReferences: []db.Reference{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tURL: \"https://nvd.nist.gov/vuln/detail/CVE-2018-5487\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tURL:  \"https://security.netapp.com/advisory/ntap-20180523-0001/\",\n\t\t\t\t\t\t\t\t\tTags: []string{\"patch\", \"vendor-advisory\"},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tSeverities: []db.Severity{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tScheme: db.SeveritySchemeCVSS,\n\t\t\t\t\t\t\t\t\tValue: db.CVSSSeverity{\n\t\t\t\t\t\t\t\t\t\tVector:  \"CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H\",\n\t\t\t\t\t\t\t\t\t\tVersion: \"3.0\",\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\tSource: \"nvd@nist.gov\",\n\t\t\t\t\t\t\t\t\tRank:   1,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tScheme: db.SeveritySchemeCVSS,\n\t\t\t\t\t\t\t\t\tValue: db.CVSSSeverity{\n\t\t\t\t\t\t\t\t\t\tVector:  \"AV:N/AC:L/Au:N/C:P/I:P/A:P\",\n\t\t\t\t\t\t\t\t\t\tVersion: \"2.0\",\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\tSource: \"nvd@nist.gov\",\n\t\t\t\t\t\t\t\t\tRank:   1,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tScheme: \"CVSS\",\n\t\t\t\t\t\t\t\t\tValue: db.CVSSSeverity{\n\t\t\t\t\t\t\t\t\t\tVector:  \"CVSS:4.0/AV:N/AC:H/AT:N/PR:N/UI:A/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X\",\n\t\t\t\t\t\t\t\t\t\tVersion: \"4.0\",\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\tSource: \"security@zabbix.com\",\n\t\t\t\t\t\t\t\t\tRank:   2,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tRelated: relatedEntries(\n\t\t\t\t\t\tdb.AffectedCPEHandle{\n\t\t\t\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\t\t\t\tCVEs: []string{\"CVE-2018-5487\"},\n\t\t\t\t\t\t\t\tQualifiers: &db.PackageQualifiers{\n\t\t\t\t\t\t\t\t\tPlatformCPEs: []string{\"cpe:2.3:o:linux:linux_kernel:-:*:*:*:*:*:*:*\"},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tVersion: db.Version{\n\t\t\t\t\t\t\t\t\t\t\tConstraint: \">= 7.2, < 7.3\",\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\tFix: &db.Fix{\n\t\t\t\t\t\t\t\t\t\t\tVersion: \"7.3\",\n\t\t\t\t\t\t\t\t\t\t\tState:   db.FixedStatus,\n\t\t\t\t\t\t\t\t\t\t\tDetail:  nil, // important! though there is fix info on the record, the versions mismatch, thus the detail is not attached (there is a bug upstream)\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tCPE: &db.Cpe{\n\t\t\t\t\t\t\t\tPart:    \"a\",\n\t\t\t\t\t\t\t\tVendor:  \"netapp\",\n\t\t\t\t\t\t\t\tProduct: \"oncommand_unified_manager\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tdb.CWEHandle{\n\t\t\t\t\t\t\tCVE:    \"CVE-2018-5487\",\n\t\t\t\t\t\t\tCWE:    \"CWE-20\",\n\t\t\t\t\t\t\tSource: \"nvd@nist.gov\",\n\t\t\t\t\t\t\tType:   \"Primary\",\n\t\t\t\t\t\t},\n\t\t\t\t\t),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:     \"single package, multiple distros\",\n\t\t\tfixture:  \"testdata/single-package-multi-distro.json\",\n\t\t\tprovider: \"nvd\",\n\t\t\tconfig:   defaultConfig(),\n\t\t\twant: []transformers.RelatedEntries{\n\t\t\t\t{\n\t\t\t\t\tVulnerabilityHandle: &db.VulnerabilityHandle{\n\t\t\t\t\t\tName:          \"CVE-2018-1000222\",\n\t\t\t\t\t\tProviderID:    \"nvd\",\n\t\t\t\t\t\tProvider:      expectedProvider(\"nvd\"),\n\t\t\t\t\t\tModifiedDate:  timeRef(time.Date(2020, 3, 31, 2, 15, 12, 667000000, time.UTC)),\n\t\t\t\t\t\tPublishedDate: timeRef(time.Date(2018, 8, 20, 20, 29, 1, 347000000, time.UTC)),\n\t\t\t\t\t\tStatus:        db.VulnerabilityActive,\n\t\t\t\t\t\tBlobValue: &db.VulnerabilityBlob{\n\t\t\t\t\t\t\tID:          \"CVE-2018-1000222\",\n\t\t\t\t\t\t\tAssigners:   []string{\"cve@mitre.org\"},\n\t\t\t\t\t\t\tDescription: \"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.\",\n\t\t\t\t\t\t\tReferences: []db.Reference{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tURL: \"https://nvd.nist.gov/vuln/detail/CVE-2018-1000222\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tURL:  \"https://github.com/libgd/libgd/issues/447\",\n\t\t\t\t\t\t\t\t\tTags: []string{\"issue-tracking\", \"third-party-advisory\"},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tURL:  \"https://lists.debian.org/debian-lts-announce/2019/01/msg00028.html\",\n\t\t\t\t\t\t\t\t\tTags: []string{\"mailing-list\", \"third-party-advisory\"},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tURL: \"https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/3CZ2QADQTKRHTGB2AHD7J4QQNDLBEMM6/\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tURL:  \"https://security.gentoo.org/glsa/201903-18\",\n\t\t\t\t\t\t\t\t\tTags: []string{\"third-party-advisory\"},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tURL:  \"https://usn.ubuntu.com/3755-1/\",\n\t\t\t\t\t\t\t\t\tTags: []string{\"mitigation\", \"third-party-advisory\"},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tSeverities: []db.Severity{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tScheme: db.SeveritySchemeCVSS,\n\t\t\t\t\t\t\t\t\tValue: db.CVSSSeverity{\n\t\t\t\t\t\t\t\t\t\tVector:  \"CVSS:3.0/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H\",\n\t\t\t\t\t\t\t\t\t\tVersion: \"3.0\",\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\tSource: \"nvd@nist.gov\",\n\t\t\t\t\t\t\t\t\tRank:   1,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tScheme: db.SeveritySchemeCVSS,\n\t\t\t\t\t\t\t\t\tValue: db.CVSSSeverity{\n\t\t\t\t\t\t\t\t\t\tVector:  \"AV:N/AC:M/Au:N/C:P/I:P/A:P\",\n\t\t\t\t\t\t\t\t\t\tVersion: \"2.0\",\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\tSource: \"nvd@nist.gov\",\n\t\t\t\t\t\t\t\t\tRank:   1,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tRelated: relatedEntries(\n\t\t\t\t\t\t// the application package...\n\t\t\t\t\t\tdb.AffectedCPEHandle{\n\t\t\t\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\t\t\t\tCVEs: []string{\"CVE-2018-1000222\"},\n\t\t\t\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tVersion: db.Version{\n\t\t\t\t\t\t\t\t\t\t\tConstraint: \"= 2.2.5\",\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tCPE: &db.Cpe{\n\t\t\t\t\t\t\t\tPart:    \"a\",\n\t\t\t\t\t\t\t\tVendor:  \"libgd\",\n\t\t\t\t\t\t\t\tProduct: \"libgd\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\t// ubuntu OS ... (since the default config has all parts enabled, we should see this)\n\t\t\t\t\t\tdb.AffectedCPEHandle{\n\t\t\t\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\t\t\t\tCVEs: []string{\"CVE-2018-1000222\"},\n\t\t\t\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tVersion: db.Version{\n\t\t\t\t\t\t\t\t\t\t\tConstraint: \"= 14.04\",\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tVersion: db.Version{\n\t\t\t\t\t\t\t\t\t\t\tConstraint: \"= 16.04\",\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tVersion: db.Version{\n\t\t\t\t\t\t\t\t\t\t\tConstraint: \"= 18.04\",\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tCPE: &db.Cpe{\n\t\t\t\t\t\t\t\tPart:            \"o\",\n\t\t\t\t\t\t\t\tVendor:          \"canonical\",\n\t\t\t\t\t\t\t\tProduct:         \"ubuntu_linux\",\n\t\t\t\t\t\t\t\tSoftwareEdition: \"lts\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\t// debian OS ...  (since the default config has all parts enabled, we should see this)\n\t\t\t\t\t\tdb.AffectedCPEHandle{\n\t\t\t\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\t\t\t\tCVEs: []string{\"CVE-2018-1000222\"},\n\t\t\t\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tVersion: db.Version{\n\t\t\t\t\t\t\t\t\t\t\tConstraint: \"= 8.0\",\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tCPE: &db.Cpe{\n\t\t\t\t\t\t\t\tPart:    \"o\",\n\t\t\t\t\t\t\t\tVendor:  \"debian\",\n\t\t\t\t\t\t\t\tProduct: \"debian_linux\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tdb.CWEHandle{\n\t\t\t\t\t\t\tCVE:    \"CVE-2018-1000222\",\n\t\t\t\t\t\t\tCWE:    \"CWE-415\",\n\t\t\t\t\t\t\tSource: \"nvd@nist.gov\",\n\t\t\t\t\t\t\tType:   \"Primary\",\n\t\t\t\t\t\t},\n\t\t\t\t\t),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:     \"single package, multiple distros (application types only)\",\n\t\t\tfixture:  \"testdata/single-package-multi-distro.json\",\n\t\t\tprovider: \"nvd\",\n\t\t\tconfig: func() Config {\n\t\t\t\tc := defaultConfig()\n\t\t\t\tc.CPEParts.Remove(\"h\", \"o\") // important!\n\t\t\t\treturn c\n\t\t\t}(),\n\t\t\twant: []transformers.RelatedEntries{\n\t\t\t\t{\n\t\t\t\t\tVulnerabilityHandle: &db.VulnerabilityHandle{\n\t\t\t\t\t\tName:          \"CVE-2018-1000222\",\n\t\t\t\t\t\tProviderID:    \"nvd\",\n\t\t\t\t\t\tProvider:      expectedProvider(\"nvd\"),\n\t\t\t\t\t\tModifiedDate:  timeRef(time.Date(2020, 3, 31, 2, 15, 12, 667000000, time.UTC)),\n\t\t\t\t\t\tPublishedDate: timeRef(time.Date(2018, 8, 20, 20, 29, 1, 347000000, time.UTC)),\n\t\t\t\t\t\tStatus:        db.VulnerabilityActive,\n\t\t\t\t\t\tBlobValue: &db.VulnerabilityBlob{\n\t\t\t\t\t\t\tID:          \"CVE-2018-1000222\",\n\t\t\t\t\t\t\tAssigners:   []string{\"cve@mitre.org\"},\n\t\t\t\t\t\t\tDescription: \"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.\",\n\t\t\t\t\t\t\tReferences: []db.Reference{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tURL: \"https://nvd.nist.gov/vuln/detail/CVE-2018-1000222\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tURL:  \"https://github.com/libgd/libgd/issues/447\",\n\t\t\t\t\t\t\t\t\tTags: []string{\"issue-tracking\", \"third-party-advisory\"},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tURL:  \"https://lists.debian.org/debian-lts-announce/2019/01/msg00028.html\",\n\t\t\t\t\t\t\t\t\tTags: []string{\"mailing-list\", \"third-party-advisory\"},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tURL: \"https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/3CZ2QADQTKRHTGB2AHD7J4QQNDLBEMM6/\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tURL:  \"https://security.gentoo.org/glsa/201903-18\",\n\t\t\t\t\t\t\t\t\tTags: []string{\"third-party-advisory\"},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tURL:  \"https://usn.ubuntu.com/3755-1/\",\n\t\t\t\t\t\t\t\t\tTags: []string{\"mitigation\", \"third-party-advisory\"},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tSeverities: []db.Severity{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tScheme: db.SeveritySchemeCVSS,\n\t\t\t\t\t\t\t\t\tValue: db.CVSSSeverity{\n\t\t\t\t\t\t\t\t\t\tVector:  \"CVSS:3.0/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H\",\n\t\t\t\t\t\t\t\t\t\tVersion: \"3.0\",\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\tSource: \"nvd@nist.gov\",\n\t\t\t\t\t\t\t\t\tRank:   1,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tScheme: db.SeveritySchemeCVSS,\n\t\t\t\t\t\t\t\t\tValue: db.CVSSSeverity{\n\t\t\t\t\t\t\t\t\t\tVector:  \"AV:N/AC:M/Au:N/C:P/I:P/A:P\",\n\t\t\t\t\t\t\t\t\t\tVersion: \"2.0\",\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\tSource: \"nvd@nist.gov\",\n\t\t\t\t\t\t\t\t\tRank:   1,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tRelated: relatedEntries(\n\t\t\t\t\t\tdb.AffectedCPEHandle{\n\t\t\t\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\t\t\t\tCVEs: []string{\"CVE-2018-1000222\"},\n\t\t\t\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tVersion: db.Version{\n\t\t\t\t\t\t\t\t\t\t\tConstraint: \"= 2.2.5\",\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tCPE: &db.Cpe{\n\t\t\t\t\t\t\t\tPart:    \"a\",\n\t\t\t\t\t\t\t\tVendor:  \"libgd\",\n\t\t\t\t\t\t\t\tProduct: \"libgd\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tdb.CWEHandle{\n\t\t\t\t\t\t\tCVE:    \"CVE-2018-1000222\",\n\t\t\t\t\t\t\tCWE:    \"CWE-415\",\n\t\t\t\t\t\t\tSource: \"nvd@nist.gov\",\n\t\t\t\t\t\t\tType:   \"Primary\",\n\t\t\t\t\t\t},\n\t\t\t\t\t),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:     \"multiple packages, multiple distros\",\n\t\t\tfixture:  \"testdata/compound-pkg.json\",\n\t\t\tprovider: \"nvd\",\n\t\t\tconfig:   defaultConfig(),\n\t\t\twant: []transformers.RelatedEntries{\n\t\t\t\t{\n\t\t\t\t\tVulnerabilityHandle: &db.VulnerabilityHandle{\n\t\t\t\t\t\tName:          \"CVE-2018-10189\",\n\t\t\t\t\t\tProviderID:    \"nvd\",\n\t\t\t\t\t\tProvider:      expectedProvider(\"nvd\"),\n\t\t\t\t\t\tModifiedDate:  timeRef(time.Date(2018, 5, 23, 14, 41, 49, 73000000, time.UTC)),\n\t\t\t\t\t\tPublishedDate: timeRef(time.Date(2018, 4, 17, 20, 29, 0, 410000000, time.UTC)),\n\t\t\t\t\t\tStatus:        db.VulnerabilityActive,\n\t\t\t\t\t\tBlobValue: &db.VulnerabilityBlob{\n\t\t\t\t\t\t\tID:          \"CVE-2018-10189\",\n\t\t\t\t\t\t\tAssigners:   []string{\"cve@mitre.org\"},\n\t\t\t\t\t\t\tDescription: \"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.\",\n\t\t\t\t\t\t\tReferences: []db.Reference{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tURL: \"https://nvd.nist.gov/vuln/detail/CVE-2018-10189\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tURL:  \"https://github.com/mautic/mautic/releases/tag/2.13.0\",\n\t\t\t\t\t\t\t\t\tTags: []string{\"third-party-advisory\"},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tSeverities: []db.Severity{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tScheme: db.SeveritySchemeCVSS,\n\t\t\t\t\t\t\t\t\tValue: db.CVSSSeverity{\n\t\t\t\t\t\t\t\t\t\tVector:  \"CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N\",\n\t\t\t\t\t\t\t\t\t\tVersion: \"3.0\",\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\tSource: \"nvd@nist.gov\",\n\t\t\t\t\t\t\t\t\tRank:   1,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tScheme: db.SeveritySchemeCVSS,\n\t\t\t\t\t\t\t\t\tValue: db.CVSSSeverity{\n\t\t\t\t\t\t\t\t\t\tVector:  \"AV:N/AC:L/Au:N/C:P/I:N/A:N\",\n\t\t\t\t\t\t\t\t\t\tVersion: \"2.0\",\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\tSource: \"nvd@nist.gov\",\n\t\t\t\t\t\t\t\t\tRank:   1,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tRelated: relatedEntries(\n\t\t\t\t\t\tdb.AffectedCPEHandle{\n\t\t\t\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\t\t\t\tCVEs: []string{\"CVE-2018-10189\"},\n\t\t\t\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tVersion: db.Version{\n\t\t\t\t\t\t\t\t\t\t\tConstraint: \">= 1.0.0, <= 1.4.1\",\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t// since the top range operator is <= we cannot infer a fix\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tVersion: db.Version{\n\t\t\t\t\t\t\t\t\t\t\tConstraint: \">= 2.0.0, < 2.13.0\",\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\tFix: &db.Fix{\n\t\t\t\t\t\t\t\t\t\t\tVersion: \"2.13.0\",\n\t\t\t\t\t\t\t\t\t\t\tState:   db.FixedStatus,\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tCPE: &db.Cpe{\n\t\t\t\t\t\t\t\tPart:    \"a\",\n\t\t\t\t\t\t\t\tVendor:  \"mautic\",\n\t\t\t\t\t\t\t\tProduct: \"mautic\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tdb.CWEHandle{\n\t\t\t\t\t\t\tCVE:    \"CVE-2018-10189\",\n\t\t\t\t\t\t\tCWE:    \"CWE-200\",\n\t\t\t\t\t\t\tSource: \"nvd@nist.gov\",\n\t\t\t\t\t\t\tType:   \"Primary\",\n\t\t\t\t\t\t},\n\t\t\t\t\t),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:     \"invalid CPE\",\n\t\t\tfixture:  \"testdata/invalid_cpe.json\",\n\t\t\tprovider: \"nvd\",\n\t\t\tconfig:   defaultConfig(),\n\t\t\twant: []transformers.RelatedEntries{\n\t\t\t\t{\n\t\t\t\t\tVulnerabilityHandle: &db.VulnerabilityHandle{\n\t\t\t\t\t\tName:          \"CVE-2015-8978\",\n\t\t\t\t\t\tProviderID:    \"nvd\",\n\t\t\t\t\t\tProvider:      expectedProvider(\"nvd\"),\n\t\t\t\t\t\tModifiedDate:  timeRef(time.Date(2016, 11, 28, 19, 50, 59, 600000000, time.UTC)),\n\t\t\t\t\t\tPublishedDate: timeRef(time.Date(2016, 11, 22, 17, 59, 0, 180000000, time.UTC)),\n\t\t\t\t\t\tStatus:        db.VulnerabilityActive,\n\t\t\t\t\t\tBlobValue: &db.VulnerabilityBlob{\n\t\t\t\t\t\t\tID:          \"CVE-2015-8978\",\n\t\t\t\t\t\t\tAssigners:   []string{\"cve@mitre.org\"},\n\t\t\t\t\t\t\tDescription: \"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.\",\n\t\t\t\t\t\t\tReferences: []db.Reference{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tURL: \"https://nvd.nist.gov/vuln/detail/CVE-2015-8978\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tURL:  \"http://cpansearch.perl.org/src/PHRED/SOAP-Lite-1.20/Changes\",\n\t\t\t\t\t\t\t\t\tTags: []string{\"vendor-advisory\"},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tURL:  \"http://www.securityfocus.com/bid/94487\",\n\t\t\t\t\t\t\t\t\tTags: nil,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tSeverities: []db.Severity{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tScheme: db.SeveritySchemeCVSS,\n\t\t\t\t\t\t\t\t\tValue: db.CVSSSeverity{\n\t\t\t\t\t\t\t\t\t\tVector:  \"CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H\",\n\t\t\t\t\t\t\t\t\t\tVersion: \"3.0\",\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\tSource: \"nvd@nist.gov\",\n\t\t\t\t\t\t\t\t\tRank:   1,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tScheme: db.SeveritySchemeCVSS,\n\t\t\t\t\t\t\t\t\tValue: db.CVSSSeverity{\n\t\t\t\t\t\t\t\t\t\tVector:  \"AV:N/AC:L/Au:N/C:N/I:N/A:P\",\n\t\t\t\t\t\t\t\t\t\tVersion: \"2.0\",\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\tSource: \"nvd@nist.gov\",\n\t\t\t\t\t\t\t\t\tRank:   1,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tRelated: relatedEntries(\n\t\t\t\t\t\tdb.CWEHandle{\n\t\t\t\t\t\t\tCVE:    \"CVE-2015-8978\",\n\t\t\t\t\t\t\tCWE:    \"CWE-399\",\n\t\t\t\t\t\t\tSource: \"nvd@nist.gov\",\n\t\t\t\t\t\t\tType:   \"Primary\",\n\t\t\t\t\t\t},\n\t\t\t\t\t), // when we can't parse the CPE we should not add any affected CPE blobs (but we do add the vuln blob and CWE)\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:     \"basic platform CPE\",\n\t\t\tfixture:  \"testdata/platform-cpe.json\",\n\t\t\tprovider: \"nvd\",\n\t\t\tconfig:   defaultConfig(),\n\t\t\twant: []transformers.RelatedEntries{\n\t\t\t\t{\n\t\t\t\t\tVulnerabilityHandle: &db.VulnerabilityHandle{\n\t\t\t\t\t\tName:          \"CVE-2022-26488\",\n\t\t\t\t\t\tProviderID:    \"nvd\",\n\t\t\t\t\t\tProvider:      expectedProvider(\"nvd\"),\n\t\t\t\t\t\tModifiedDate:  timeRef(time.Date(2022, 9, 3, 3, 34, 19, 933000000, time.UTC)),\n\t\t\t\t\t\tPublishedDate: timeRef(time.Date(2022, 3, 10, 17, 47, 45, 383000000, time.UTC)),\n\t\t\t\t\t\tStatus:        db.VulnerabilityActive,\n\t\t\t\t\t\tBlobValue: &db.VulnerabilityBlob{\n\t\t\t\t\t\t\tID:          \"CVE-2022-26488\",\n\t\t\t\t\t\t\tAssigners:   []string{\"cve@mitre.org\"},\n\t\t\t\t\t\t\tDescription: \"In Python before 3.10.3 on Windows, local users can gain privileges because the search path is inadequately secured. The installer may allow a local attacker to add user-writable directories to the system search path. To exploit, an administrator must have installed Python for all users and enabled PATH entries. A non-administrative user can trigger a repair that incorrectly adds user-writable paths into PATH, enabling search-path hijacking of other users and system services. This affects Python (CPython) through 3.7.12, 3.8.x through 3.8.12, 3.9.x through 3.9.10, and 3.10.x through 3.10.2.\",\n\t\t\t\t\t\t\tReferences: []db.Reference{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tURL: \"https://nvd.nist.gov/vuln/detail/CVE-2022-26488\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tURL:  \"https://mail.python.org/archives/list/security-announce@python.org/thread/657Z4XULWZNIY5FRP3OWXHYKUSIH6DMN/\",\n\t\t\t\t\t\t\t\t\tTags: []string{\"patch\", \"vendor-advisory\"},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tURL:  \"https://security.netapp.com/advisory/ntap-20220419-0005/\",\n\t\t\t\t\t\t\t\t\tTags: []string{\"third-party-advisory\"},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tSeverities: []db.Severity{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tScheme: db.SeveritySchemeCVSS,\n\t\t\t\t\t\t\t\t\tValue: db.CVSSSeverity{\n\t\t\t\t\t\t\t\t\t\tVector:  \"CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H\",\n\t\t\t\t\t\t\t\t\t\tVersion: \"3.1\",\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\tSource: \"nvd@nist.gov\",\n\t\t\t\t\t\t\t\t\tRank:   1,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tScheme: db.SeveritySchemeCVSS,\n\t\t\t\t\t\t\t\t\tValue: db.CVSSSeverity{\n\t\t\t\t\t\t\t\t\t\tVector:  \"AV:L/AC:M/Au:N/C:P/I:P/A:P\",\n\t\t\t\t\t\t\t\t\t\tVersion: \"2.0\",\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\tSource: \"nvd@nist.gov\",\n\t\t\t\t\t\t\t\t\tRank:   1,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tRelated: relatedEntries(\n\t\t\t\t\t\tdb.AffectedCPEHandle{\n\t\t\t\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\t\t\t\tCVEs: []string{\"CVE-2022-26488\"},\n\t\t\t\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t// match all versions\n\t\t\t\t\t\t\t\t\t\tVersion: db.Version{Constraint: \"\"},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tCPE: &db.Cpe{\n\t\t\t\t\t\t\t\tPart:           \"a\",\n\t\t\t\t\t\t\t\tVendor:         \"netapp\",\n\t\t\t\t\t\t\t\tProduct:        \"active_iq_unified_manager\",\n\t\t\t\t\t\t\t\tTargetSoftware: \"windows\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tdb.AffectedCPEHandle{\n\t\t\t\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\t\t\t\tCVEs: []string{\"CVE-2022-26488\"},\n\t\t\t\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t// match all versions\n\t\t\t\t\t\t\t\t\t\tVersion: db.Version{Constraint: \"\"},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tCPE: &db.Cpe{\n\t\t\t\t\t\t\t\tPart:    \"a\",\n\t\t\t\t\t\t\t\tVendor:  \"netapp\",\n\t\t\t\t\t\t\t\tProduct: \"ontap_select_deploy_administration_utility\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tdb.AffectedCPEHandle{\n\t\t\t\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\t\t\t\tCVEs: []string{\"CVE-2022-26488\"},\n\t\t\t\t\t\t\t\tQualifiers: &db.PackageQualifiers{\n\t\t\t\t\t\t\t\t\tPlatformCPEs: []string{\"cpe:2.3:o:microsoft:windows:-:*:*:*:*:*:*:*\"}, // important!\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t\t\t\t{Version: db.Version{Constraint: \"<= 3.7.12\"}},\n\t\t\t\t\t\t\t\t\t{Version: db.Version{Constraint: \">= 3.10.0, <= 3.10.2\"}},\n\t\t\t\t\t\t\t\t\t{Version: db.Version{Constraint: \">= 3.8.0, <= 3.8.12\"}},\n\t\t\t\t\t\t\t\t\t{Version: db.Version{Constraint: \">= 3.9.0, <= 3.9.10\"}},\n\t\t\t\t\t\t\t\t\t{Version: db.Version{Constraint: \"= 3.11.0-alpha1\"}},\n\t\t\t\t\t\t\t\t\t{Version: db.Version{Constraint: \"= 3.11.0-alpha2\"}},\n\t\t\t\t\t\t\t\t\t{Version: db.Version{Constraint: \"= 3.11.0-alpha3\"}},\n\t\t\t\t\t\t\t\t\t{Version: db.Version{Constraint: \"= 3.11.0-alpha4\"}},\n\t\t\t\t\t\t\t\t\t{Version: db.Version{Constraint: \"= 3.11.0-alpha5\"}},\n\t\t\t\t\t\t\t\t\t{Version: db.Version{Constraint: \"= 3.11.0-alpha6\"}},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tCPE: &db.Cpe{\n\t\t\t\t\t\t\t\tPart:    \"a\",\n\t\t\t\t\t\t\t\tVendor:  \"python\",\n\t\t\t\t\t\t\t\tProduct: \"python\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tdb.CWEHandle{\n\t\t\t\t\t\t\tCVE:    \"CVE-2022-26488\",\n\t\t\t\t\t\t\tCWE:    \"CWE-426\",\n\t\t\t\t\t\t\tSource: \"nvd@nist.gov\",\n\t\t\t\t\t\t\tType:   \"Primary\",\n\t\t\t\t\t\t},\n\t\t\t\t\t),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:     \"multiple platform CPEs for single package\",\n\t\t\tfixture:  \"testdata/cve-2022-0543.json\",\n\t\t\tprovider: \"nvd\",\n\t\t\tconfig:   defaultConfig(),\n\t\t\twant: []transformers.RelatedEntries{\n\t\t\t\t{\n\t\t\t\t\tVulnerabilityHandle: &db.VulnerabilityHandle{\n\t\t\t\t\t\tName:          \"CVE-2022-0543\",\n\t\t\t\t\t\tProviderID:    \"nvd\",\n\t\t\t\t\t\tProvider:      expectedProvider(\"nvd\"),\n\t\t\t\t\t\tModifiedDate:  timeRef(time.Date(2023, 9, 29, 15, 55, 24, 533000000, time.UTC)),\n\t\t\t\t\t\tPublishedDate: timeRef(time.Date(2022, 2, 18, 20, 15, 17, 583000000, time.UTC)),\n\t\t\t\t\t\tStatus:        db.VulnerabilityActive,\n\t\t\t\t\t\tBlobValue: &db.VulnerabilityBlob{\n\t\t\t\t\t\t\tID:          \"CVE-2022-0543\",\n\t\t\t\t\t\t\tAssigners:   []string{\"security@debian.org\"},\n\t\t\t\t\t\t\tDescription: \"It was discovered, that redis, a persistent key-value database, due to a packaging issue, is prone to a (Debian-specific) Lua sandbox escape, which could result in remote code execution.\",\n\t\t\t\t\t\t\tReferences: []db.Reference{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tURL: \"https://nvd.nist.gov/vuln/detail/CVE-2022-0543\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tURL:  \"http://packetstormsecurity.com/files/166885/Redis-Lua-Sandbox-Escape.html\",\n\t\t\t\t\t\t\t\t\tTags: []string{\"exploit\", \"third-party-advisory\", \"vdb-entry\"},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tURL:  \"https://bugs.debian.org/1005787\",\n\t\t\t\t\t\t\t\t\tTags: []string{\"issue-tracking\", \"patch\", \"third-party-advisory\"},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tURL:  \"https://lists.debian.org/debian-security-announce/2022/msg00048.html\",\n\t\t\t\t\t\t\t\t\tTags: []string{\"mailing-list\", \"third-party-advisory\"},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tURL:  \"https://security.netapp.com/advisory/ntap-20220331-0004/\",\n\t\t\t\t\t\t\t\t\tTags: []string{\"third-party-advisory\"},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tURL:  \"https://www.debian.org/security/2022/dsa-5081\",\n\t\t\t\t\t\t\t\t\tTags: []string{\"mailing-list\", \"third-party-advisory\"},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tURL:  \"https://www.ubercomp.com/posts/2022-01-20_redis_on_debian_rce\",\n\t\t\t\t\t\t\t\t\tTags: []string{\"third-party-advisory\"},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tSeverities: []db.Severity{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tScheme: db.SeveritySchemeCVSS,\n\t\t\t\t\t\t\t\t\tValue: db.CVSSSeverity{\n\t\t\t\t\t\t\t\t\t\tVector:  \"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H\",\n\t\t\t\t\t\t\t\t\t\tVersion: \"3.1\",\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\tSource: \"nvd@nist.gov\",\n\t\t\t\t\t\t\t\t\tRank:   1,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tScheme: db.SeveritySchemeCVSS,\n\t\t\t\t\t\t\t\t\tValue: db.CVSSSeverity{\n\t\t\t\t\t\t\t\t\t\tVector:  \"AV:N/AC:L/Au:N/C:C/I:C/A:C\",\n\t\t\t\t\t\t\t\t\t\tVersion: \"2.0\",\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\tSource: \"nvd@nist.gov\",\n\t\t\t\t\t\t\t\t\tRank:   1,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tRelated: relatedEntries(\n\t\t\t\t\t\tdb.AffectedCPEHandle{\n\t\t\t\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\t\t\t\tCVEs: []string{\"CVE-2022-0543\"},\n\t\t\t\t\t\t\t\tQualifiers: &db.PackageQualifiers{\n\t\t\t\t\t\t\t\t\tPlatformCPEs: []string{\n\t\t\t\t\t\t\t\t\t\t\"cpe:2.3:o:canonical:ubuntu_linux:20.04:*:*:*:lts:*:*:*\",\n\t\t\t\t\t\t\t\t\t\t\"cpe:2.3:o:canonical:ubuntu_linux:21.10:*:*:*:-:*:*:*\",\n\t\t\t\t\t\t\t\t\t\t\"cpe:2.3:o:debian:debian_linux:9.0:*:*:*:*:*:*:*\",\n\t\t\t\t\t\t\t\t\t\t\"cpe:2.3:o:debian:debian_linux:10.0:*:*:*:*:*:*:*\",\n\t\t\t\t\t\t\t\t\t\t\"cpe:2.3:o:debian:debian_linux:11.0:*:*:*:*:*:*:*\",\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t// match all versions\n\t\t\t\t\t\t\t\t\t\tVersion: db.Version{Constraint: \"\"},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tCPE: &db.Cpe{\n\t\t\t\t\t\t\t\tPart:    \"a\",\n\t\t\t\t\t\t\t\tVendor:  \"redis\",\n\t\t\t\t\t\t\t\tProduct: \"redis\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tdb.CWEHandle{\n\t\t\t\t\t\t\tCVE:    \"CVE-2022-0543\",\n\t\t\t\t\t\t\tCWE:    \"CWE-862\",\n\t\t\t\t\t\t\tSource: \"nvd@nist.gov\",\n\t\t\t\t\t\t\tType:   \"Primary\",\n\t\t\t\t\t\t},\n\t\t\t\t\t),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:     \"multiple platform CPEs for single package + fix and OS match\",\n\t\t\tfixture:  \"testdata/cve-2020-10729.json\",\n\t\t\tprovider: \"nvd\",\n\t\t\tconfig:   defaultConfig(),\n\t\t\twant: []transformers.RelatedEntries{\n\t\t\t\t{\n\t\t\t\t\tVulnerabilityHandle: &db.VulnerabilityHandle{\n\t\t\t\t\t\tName:          \"CVE-2020-10729\",\n\t\t\t\t\t\tProviderID:    \"nvd\",\n\t\t\t\t\t\tProvider:      expectedProvider(\"nvd\"),\n\t\t\t\t\t\tModifiedDate:  timeRef(time.Date(2021, 12, 10, 19, 57, 6, 357000000, time.UTC)),\n\t\t\t\t\t\tPublishedDate: timeRef(time.Date(2021, 5, 27, 19, 15, 7, 880000000, time.UTC)),\n\t\t\t\t\t\tStatus:        db.VulnerabilityActive,\n\t\t\t\t\t\tBlobValue: &db.VulnerabilityBlob{\n\t\t\t\t\t\t\tID:          \"CVE-2020-10729\",\n\t\t\t\t\t\t\tAssigners:   []string{\"secalert@redhat.com\"},\n\t\t\t\t\t\t\tDescription: \"A flaw was found in the use of insufficiently random values in Ansible. Two random password lookups of the same length generate the equal value as the template caching action for the same file since no re-evaluation happens. The highest threat from this vulnerability would be that all passwords are exposed at once for the file. This flaw affects Ansible Engine versions before 2.9.6.\",\n\t\t\t\t\t\t\tReferences: []db.Reference{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tURL: \"https://nvd.nist.gov/vuln/detail/CVE-2020-10729\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tURL:  \"https://bugzilla.redhat.com/show_bug.cgi?id=1831089\",\n\t\t\t\t\t\t\t\t\tTags: []string{\"issue-tracking\", \"vendor-advisory\"},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tURL:  \"https://github.com/ansible/ansible/issues/34144\",\n\t\t\t\t\t\t\t\t\tTags: []string{\"exploit\", \"issue-tracking\", \"third-party-advisory\"},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tURL:  \"https://www.debian.org/security/2021/dsa-4950\",\n\t\t\t\t\t\t\t\t\tTags: []string{\"third-party-advisory\"},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tSeverities: []db.Severity{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tScheme: db.SeveritySchemeCVSS,\n\t\t\t\t\t\t\t\t\tValue: db.CVSSSeverity{\n\t\t\t\t\t\t\t\t\t\tVector:  \"CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N\",\n\t\t\t\t\t\t\t\t\t\tVersion: \"3.1\",\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\tSource: \"nvd@nist.gov\",\n\t\t\t\t\t\t\t\t\tRank:   1,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tScheme: db.SeveritySchemeCVSS,\n\t\t\t\t\t\t\t\t\tValue: db.CVSSSeverity{\n\t\t\t\t\t\t\t\t\t\tVector:  \"AV:L/AC:L/Au:N/C:P/I:N/A:N\",\n\t\t\t\t\t\t\t\t\t\tVersion: \"2.0\",\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\tSource: \"nvd@nist.gov\",\n\t\t\t\t\t\t\t\t\tRank:   1,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tRelated: relatedEntries(\n\t\t\t\t\t\tdb.AffectedCPEHandle{\n\t\t\t\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\t\t\t\tCVEs: []string{\"CVE-2020-10729\"},\n\t\t\t\t\t\t\t\tQualifiers: &db.PackageQualifiers{\n\t\t\t\t\t\t\t\t\tPlatformCPEs: []string{\n\t\t\t\t\t\t\t\t\t\t\"cpe:2.3:o:redhat:enterprise_linux:7.0:*:*:*:*:*:*:*\",\n\t\t\t\t\t\t\t\t\t\t\"cpe:2.3:o:redhat:enterprise_linux:8.0:*:*:*:*:*:*:*\",\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tVersion: db.Version{\n\t\t\t\t\t\t\t\t\t\t\tConstraint: \"< 2.9.6\",\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\tFix: &db.Fix{\n\t\t\t\t\t\t\t\t\t\t\tVersion: \"2.9.6\",\n\t\t\t\t\t\t\t\t\t\t\tState:   db.FixedStatus,\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tCPE: &db.Cpe{\n\t\t\t\t\t\t\t\tPart:    \"a\",\n\t\t\t\t\t\t\t\tVendor:  \"redhat\",\n\t\t\t\t\t\t\t\tProduct: \"ansible_engine\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tdb.AffectedCPEHandle{\n\t\t\t\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\t\t\t\tCVEs: []string{\"CVE-2020-10729\"},\n\t\t\t\t\t\t\t\t// note: no qualifiers !\n\t\t\t\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tVersion: db.Version{\n\t\t\t\t\t\t\t\t\t\t\tConstraint: \"= 10.0\",\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t// note: no fix!\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tCPE: &db.Cpe{\n\t\t\t\t\t\t\t\tPart:    \"o\",\n\t\t\t\t\t\t\t\tVendor:  \"debian\",\n\t\t\t\t\t\t\t\tProduct: \"debian_linux\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tdb.CWEHandle{\n\t\t\t\t\t\t\tCVE:    \"CVE-2020-10729\",\n\t\t\t\t\t\t\tCWE:    \"CWE-330\",\n\t\t\t\t\t\t\tSource: \"nvd@nist.gov\",\n\t\t\t\t\t\t\tType:   \"Primary\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tdb.CWEHandle{\n\t\t\t\t\t\t\tCVE:    \"CVE-2020-10729\",\n\t\t\t\t\t\t\tCWE:    \"CWE-330\",\n\t\t\t\t\t\t\tSource: \"secalert@redhat.com\",\n\t\t\t\t\t\t\tType:   \"Secondary\",\n\t\t\t\t\t\t},\n\t\t\t\t\t),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:     \"application type as platform CPE\",\n\t\t\tfixture:  \"testdata/multiple-platforms-with-application-cpe.json\",\n\t\t\tprovider: \"nvd\",\n\t\t\tconfig:   defaultConfig(),\n\t\t\twant: []transformers.RelatedEntries{\n\t\t\t\t{\n\t\t\t\t\tVulnerabilityHandle: &db.VulnerabilityHandle{\n\t\t\t\t\t\tName:          \"CVE-2023-38733\",\n\t\t\t\t\t\tProviderID:    \"nvd\",\n\t\t\t\t\t\tProvider:      expectedProvider(\"nvd\"),\n\t\t\t\t\t\tModifiedDate:  timeRef(time.Date(2023, 8, 26, 2, 25, 42, 957000000, time.UTC)),\n\t\t\t\t\t\tPublishedDate: timeRef(time.Date(2023, 8, 22, 22, 15, 8, 460000000, time.UTC)),\n\t\t\t\t\t\tStatus:        db.VulnerabilityActive,\n\t\t\t\t\t\tBlobValue: &db.VulnerabilityBlob{\n\t\t\t\t\t\t\tID:          \"CVE-2023-38733\",\n\t\t\t\t\t\t\tAssigners:   []string{\"psirt@us.ibm.com\"},\n\t\t\t\t\t\t\tDescription: \"IBM Robotic Process Automation 21.0.0 through 21.0.7.1 and 23.0.0 through 23.0.1 server could allow an authenticated user to view sensitive information from installation logs.  IBM X-Force Id:  262293.\",\n\t\t\t\t\t\t\tReferences: []db.Reference{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tURL: \"https://nvd.nist.gov/vuln/detail/CVE-2023-38733\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tURL:  \"https://exchange.xforce.ibmcloud.com/vulnerabilities/262293\",\n\t\t\t\t\t\t\t\t\tTags: []string{\"vdb-entry\", \"vendor-advisory\"},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tURL:  \"https://www.ibm.com/support/pages/node/7028223\",\n\t\t\t\t\t\t\t\t\tTags: []string{\"patch\", \"vendor-advisory\"},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tSeverities: []db.Severity{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tScheme: db.SeveritySchemeCVSS,\n\t\t\t\t\t\t\t\t\tValue: db.CVSSSeverity{\n\t\t\t\t\t\t\t\t\t\tVector:  \"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N\",\n\t\t\t\t\t\t\t\t\t\tVersion: \"3.1\",\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\tSource: \"nvd@nist.gov\",\n\t\t\t\t\t\t\t\t\tRank:   1,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tScheme: db.SeveritySchemeCVSS,\n\t\t\t\t\t\t\t\t\tValue: db.CVSSSeverity{\n\t\t\t\t\t\t\t\t\t\tVector:  \"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N\",\n\t\t\t\t\t\t\t\t\t\tVersion: \"3.1\",\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\tSource: \"psirt@us.ibm.com\",\n\t\t\t\t\t\t\t\t\tRank:   2,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tRelated: relatedEntries(\n\t\t\t\t\t\tdb.AffectedCPEHandle{\n\t\t\t\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\t\t\t\tCVEs: []string{\"CVE-2023-38733\"},\n\t\t\t\t\t\t\t\tQualifiers: &db.PackageQualifiers{\n\t\t\t\t\t\t\t\t\tPlatformCPEs: []string{\n\t\t\t\t\t\t\t\t\t\t\"cpe:2.3:a:redhat:openshift:-:*:*:*:*:*:*:*\",\n\t\t\t\t\t\t\t\t\t\t\"cpe:2.3:o:microsoft:windows:-:*:*:*:*:*:*:*\",\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tVersion: db.Version{\n\t\t\t\t\t\t\t\t\t\t\tConstraint: \">= 21.0.0, <= 21.0.7.3\",\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tVersion: db.Version{\n\t\t\t\t\t\t\t\t\t\t\tConstraint: \">= 23.0.0, <= 23.0.3\",\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tCPE: &db.Cpe{\n\t\t\t\t\t\t\t\tPart:    \"a\",\n\t\t\t\t\t\t\t\tVendor:  \"ibm\",\n\t\t\t\t\t\t\t\tProduct: \"robotic_process_automation\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tdb.CWEHandle{\n\t\t\t\t\t\t\tCVE:    \"CVE-2023-38733\",\n\t\t\t\t\t\t\tCWE:    \"CWE-532\",\n\t\t\t\t\t\t\tSource: \"nvd@nist.gov\",\n\t\t\t\t\t\t\tType:   \"Primary\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tdb.CWEHandle{\n\t\t\t\t\t\t\tCVE:    \"CVE-2023-38733\",\n\t\t\t\t\t\t\tCWE:    \"CWE-532\",\n\t\t\t\t\t\t\tSource: \"psirt@us.ibm.com\",\n\t\t\t\t\t\t\tType:   \"Secondary\",\n\t\t\t\t\t\t},\n\t\t\t\t\t),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:     \"can process entries when the platform CPE is first\",\n\t\t\tfixture:  \"testdata/CVE-2023-45283-platform-cpe-first.json\",\n\t\t\tprovider: \"nvd\",\n\t\t\tconfig:   defaultConfig(),\n\t\t\twant: []transformers.RelatedEntries{\n\t\t\t\t{\n\t\t\t\t\tVulnerabilityHandle: &db.VulnerabilityHandle{\n\t\t\t\t\t\tName:          \"CVE-2023-45283\",\n\t\t\t\t\t\tProviderID:    \"nvd\",\n\t\t\t\t\t\tProvider:      expectedProvider(\"nvd\"),\n\t\t\t\t\t\tModifiedDate:  timeRef(time.Date(2023, 12, 14, 10, 15, 7, 947000000, time.UTC)),\n\t\t\t\t\t\tPublishedDate: timeRef(time.Date(2023, 11, 9, 17, 15, 8, 757000000, time.UTC)),\n\t\t\t\t\t\tStatus:        db.VulnerabilityActive,\n\t\t\t\t\t\tBlobValue: &db.VulnerabilityBlob{\n\t\t\t\t\t\t\tID:          \"CVE-2023-45283\",\n\t\t\t\t\t\t\tAssigners:   []string{\"security@golang.org\"},\n\t\t\t\t\t\t\tDescription: \"The filepath package does not recognize paths with a \\\\??\\\\ prefix as special. On Windows, a path beginning with \\\\??\\\\ is a Root Local Device path equivalent to a path beginning with \\\\\\\\?\\\\. Paths with a \\\\??\\\\ prefix may be used to access arbitrary locations on the system. For example, the path \\\\??\\\\c:\\\\x is equivalent to the more common path c:\\\\x. Before fix, Clean could convert a rooted path such as \\\\a\\\\..\\\\??\\\\b into the root local device path \\\\??\\\\b. Clean will now convert this to .\\\\??\\\\b. Similarly, Join(\\\\, ??, b) could convert a seemingly innocent sequence of path elements into the root local device path \\\\??\\\\b. Join will now convert this to \\\\.\\\\??\\\\b. In addition, with fix, IsAbs now correctly reports paths beginning with \\\\??\\\\ as absolute, and VolumeName correctly reports the \\\\??\\\\ prefix as a volume name. UPDATE: Go 1.20.11 and Go 1.21.4 inadvertently changed the definition of the volume name in Windows paths starting with \\\\?, resulting in filepath.Clean(\\\\?\\\\c:) returning \\\\?\\\\c: rather than \\\\?\\\\c:\\\\ (among other effects). The previous behavior has been restored.\",\n\t\t\t\t\t\t\tReferences: []db.Reference{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tURL: \"https://nvd.nist.gov/vuln/detail/CVE-2023-45283\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tURL:  \"http://www.openwall.com/lists/oss-security/2023/12/05/2\",\n\t\t\t\t\t\t\t\t\tTags: nil,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tURL:  \"https://go.dev/cl/540277\",\n\t\t\t\t\t\t\t\t\tTags: []string{\"issue-tracking\", \"vendor-advisory\"},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tURL:  \"https://go.dev/cl/541175\",\n\t\t\t\t\t\t\t\t\tTags: nil,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tURL:  \"https://go.dev/issue/63713\",\n\t\t\t\t\t\t\t\t\tTags: []string{\"issue-tracking\", \"vendor-advisory\"},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tURL:  \"https://go.dev/issue/64028\",\n\t\t\t\t\t\t\t\t\tTags: nil,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tURL:  \"https://groups.google.com/g/golang-announce/c/4tU8LZfBFkY\",\n\t\t\t\t\t\t\t\t\tTags: []string{\"issue-tracking\", \"mailing-list\", \"vendor-advisory\"},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tURL:  \"https://groups.google.com/g/golang-dev/c/6ypN5EjibjM/m/KmLVYH_uAgAJ\",\n\t\t\t\t\t\t\t\t\tTags: nil,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tURL:  \"https://pkg.go.dev/vuln/GO-2023-2185\",\n\t\t\t\t\t\t\t\t\tTags: []string{\"issue-tracking\", \"vendor-advisory\"},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tURL:  \"https://security.netapp.com/advisory/ntap-20231214-0008/\",\n\t\t\t\t\t\t\t\t\tTags: nil,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tSeverities: []db.Severity{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tScheme: db.SeveritySchemeCVSS,\n\t\t\t\t\t\t\t\t\tValue: db.CVSSSeverity{\n\t\t\t\t\t\t\t\t\t\tVector:  \"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N\",\n\t\t\t\t\t\t\t\t\t\tVersion: \"3.1\",\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\tSource: \"nvd@nist.gov\",\n\t\t\t\t\t\t\t\t\tRank:   1,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tRelated: relatedEntries(\n\t\t\t\t\t\tdb.AffectedCPEHandle{\n\t\t\t\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\t\t\t\tCVEs: []string{\"CVE-2023-45283\"},\n\t\t\t\t\t\t\t\tQualifiers: &db.PackageQualifiers{\n\t\t\t\t\t\t\t\t\tPlatformCPEs: []string{\"cpe:2.3:o:microsoft:windows:-:*:*:*:*:*:*:*\"},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tVersion: db.Version{\n\t\t\t\t\t\t\t\t\t\t\tConstraint: \"< 1.20.11\",\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\tFix: &db.Fix{\n\t\t\t\t\t\t\t\t\t\t\tVersion: \"1.20.11\",\n\t\t\t\t\t\t\t\t\t\t\tState:   db.FixedStatus,\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tVersion: db.Version{\n\t\t\t\t\t\t\t\t\t\t\tConstraint: \">= 1.21.0-0, < 1.21.4\",\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\tFix: &db.Fix{\n\t\t\t\t\t\t\t\t\t\t\tVersion: \"1.21.4\",\n\t\t\t\t\t\t\t\t\t\t\tState:   db.FixedStatus,\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tCPE: &db.Cpe{\n\t\t\t\t\t\t\t\tPart:    \"a\",\n\t\t\t\t\t\t\t\tVendor:  \"golang\",\n\t\t\t\t\t\t\t\tProduct: \"go\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tdb.CWEHandle{\n\t\t\t\t\t\t\tCVE:    \"CVE-2023-45283\",\n\t\t\t\t\t\t\tCWE:    \"CWE-22\",\n\t\t\t\t\t\t\tSource: \"nvd@nist.gov\",\n\t\t\t\t\t\t\tType:   \"Primary\",\n\t\t\t\t\t\t},\n\t\t\t\t\t),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:     \"can process entries when the platform CPE is last\",\n\t\t\tfixture:  \"testdata/CVE-2023-45283-platform-cpe-last.json\",\n\t\t\tprovider: \"nvd\",\n\t\t\tconfig:   defaultConfig(),\n\t\t\twant: []transformers.RelatedEntries{\n\t\t\t\t{\n\t\t\t\t\tVulnerabilityHandle: &db.VulnerabilityHandle{\n\t\t\t\t\t\tName:          \"CVE-2023-45283\",\n\t\t\t\t\t\tProviderID:    \"nvd\",\n\t\t\t\t\t\tProvider:      expectedProvider(\"nvd\"),\n\t\t\t\t\t\tModifiedDate:  timeRef(time.Date(2023, 12, 14, 10, 15, 7, 947000000, time.UTC)),\n\t\t\t\t\t\tPublishedDate: timeRef(time.Date(2023, 11, 9, 17, 15, 8, 757000000, time.UTC)),\n\t\t\t\t\t\tStatus:        db.VulnerabilityActive,\n\t\t\t\t\t\tBlobValue: &db.VulnerabilityBlob{\n\t\t\t\t\t\t\tID:          \"CVE-2023-45283\",\n\t\t\t\t\t\t\tAssigners:   []string{\"security@golang.org\"},\n\t\t\t\t\t\t\tDescription: \"The filepath package does not recognize paths with a \\\\??\\\\ prefix as special. On Windows, a path beginning with \\\\??\\\\ is a Root Local Device path equivalent to a path beginning with \\\\\\\\?\\\\. Paths with a \\\\??\\\\ prefix may be used to access arbitrary locations on the system. For example, the path \\\\??\\\\c:\\\\x is equivalent to the more common path c:\\\\x. Before fix, Clean could convert a rooted path such as \\\\a\\\\..\\\\??\\\\b into the root local device path \\\\??\\\\b. Clean will now convert this to .\\\\??\\\\b. Similarly, Join(\\\\, ??, b) could convert a seemingly innocent sequence of path elements into the root local device path \\\\??\\\\b. Join will now convert this to \\\\.\\\\??\\\\b. In addition, with fix, IsAbs now correctly reports paths beginning with \\\\??\\\\ as absolute, and VolumeName correctly reports the \\\\??\\\\ prefix as a volume name. UPDATE: Go 1.20.11 and Go 1.21.4 inadvertently changed the definition of the volume name in Windows paths starting with \\\\?, resulting in filepath.Clean(\\\\?\\\\c:) returning \\\\?\\\\c: rather than \\\\?\\\\c:\\\\ (among other effects). The previous behavior has been restored.\",\n\t\t\t\t\t\t\tReferences: []db.Reference{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tURL: \"https://nvd.nist.gov/vuln/detail/CVE-2023-45283\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tURL:  \"http://www.openwall.com/lists/oss-security/2023/12/05/2\",\n\t\t\t\t\t\t\t\t\tTags: nil,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tURL:  \"https://go.dev/cl/540277\",\n\t\t\t\t\t\t\t\t\tTags: []string{\"issue-tracking\", \"vendor-advisory\"},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tURL:  \"https://go.dev/cl/541175\",\n\t\t\t\t\t\t\t\t\tTags: nil,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tURL:  \"https://go.dev/issue/63713\",\n\t\t\t\t\t\t\t\t\tTags: []string{\"issue-tracking\", \"vendor-advisory\"},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tURL:  \"https://go.dev/issue/64028\",\n\t\t\t\t\t\t\t\t\tTags: nil,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tURL:  \"https://groups.google.com/g/golang-announce/c/4tU8LZfBFkY\",\n\t\t\t\t\t\t\t\t\tTags: []string{\"issue-tracking\", \"mailing-list\", \"vendor-advisory\"},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tURL:  \"https://groups.google.com/g/golang-dev/c/6ypN5EjibjM/m/KmLVYH_uAgAJ\",\n\t\t\t\t\t\t\t\t\tTags: nil,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tURL:  \"https://pkg.go.dev/vuln/GO-2023-2185\",\n\t\t\t\t\t\t\t\t\tTags: []string{\"issue-tracking\", \"vendor-advisory\"},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tURL:  \"https://security.netapp.com/advisory/ntap-20231214-0008/\",\n\t\t\t\t\t\t\t\t\tTags: nil,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tSeverities: []db.Severity{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tScheme: db.SeveritySchemeCVSS,\n\t\t\t\t\t\t\t\t\tValue: db.CVSSSeverity{\n\t\t\t\t\t\t\t\t\t\tVector:  \"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N\",\n\t\t\t\t\t\t\t\t\t\tVersion: \"3.1\",\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\tSource: \"nvd@nist.gov\",\n\t\t\t\t\t\t\t\t\tRank:   1,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tRelated: relatedEntries(\n\t\t\t\t\t\tdb.AffectedCPEHandle{\n\t\t\t\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\t\t\t\tCVEs: []string{\"CVE-2023-45283\"},\n\t\t\t\t\t\t\t\tQualifiers: &db.PackageQualifiers{\n\t\t\t\t\t\t\t\t\tPlatformCPEs: []string{\"cpe:2.3:o:microsoft:windows:-:*:*:*:*:*:*:*\"},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tVersion: db.Version{\n\t\t\t\t\t\t\t\t\t\t\tConstraint: \"< 1.20.11\",\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\tFix: &db.Fix{\n\t\t\t\t\t\t\t\t\t\t\tVersion: \"1.20.11\",\n\t\t\t\t\t\t\t\t\t\t\tState:   db.FixedStatus,\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tVersion: db.Version{\n\t\t\t\t\t\t\t\t\t\t\tConstraint: \">= 1.21.0-0, < 1.21.4\",\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\tFix: &db.Fix{\n\t\t\t\t\t\t\t\t\t\t\tVersion: \"1.21.4\",\n\t\t\t\t\t\t\t\t\t\t\tState:   db.FixedStatus,\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tCPE: &db.Cpe{\n\t\t\t\t\t\t\t\tPart:    \"a\",\n\t\t\t\t\t\t\t\tVendor:  \"golang\",\n\t\t\t\t\t\t\t\tProduct: \"go\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tdb.CWEHandle{\n\t\t\t\t\t\t\tCVE:    \"CVE-2023-45283\",\n\t\t\t\t\t\t\tCWE:    \"CWE-22\",\n\t\t\t\t\t\t\tSource: \"nvd@nist.gov\",\n\t\t\t\t\t\t\tType:   \"Primary\",\n\t\t\t\t\t\t},\n\t\t\t\t\t),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"a simple list of OS matches\",\n\t\t\t// note: this was modified relative to the upstream data to account for additional interesting cases\n\t\t\tfixture:  \"testdata/cve-2024-26663-standalone-os.json\",\n\t\t\tprovider: \"nvd\",\n\t\t\tconfig:   defaultConfig(),\n\t\t\twant: []transformers.RelatedEntries{\n\t\t\t\t{\n\t\t\t\t\tVulnerabilityHandle: &db.VulnerabilityHandle{\n\t\t\t\t\t\tName:          \"CVE-2024-26663\",\n\t\t\t\t\t\tProviderID:    \"nvd\",\n\t\t\t\t\t\tProvider:      expectedProvider(\"nvd\"),\n\t\t\t\t\t\tModifiedDate:  timeRef(time.Date(2025, 1, 7, 17, 20, 30, 367000000, time.UTC)),\n\t\t\t\t\t\tPublishedDate: timeRef(time.Date(2024, 4, 2, 7, 15, 43, 287000000, time.UTC)),\n\t\t\t\t\t\tStatus:        db.VulnerabilityActive,\n\t\t\t\t\t\tBlobValue: &db.VulnerabilityBlob{\n\t\t\t\t\t\t\tID:          \"CVE-2024-26663\",\n\t\t\t\t\t\t\tAssigners:   []string{\"416baaa9-dc9f-4396-8d5f-8c081fb06d67\"},\n\t\t\t\t\t\t\tDescription: \"the description...\",\n\t\t\t\t\t\t\tReferences: []db.Reference{\n\t\t\t\t\t\t\t\t{URL: \"https://nvd.nist.gov/vuln/detail/CVE-2024-26663\"},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tURL:  \"https://git.kernel.org/stable/c/0cd331dfd6023640c9669d0592bc0fd491205f87\",\n\t\t\t\t\t\t\t\t\tTags: []string{\"patch\"},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tSeverities: []db.Severity{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tScheme: \"CVSS\",\n\t\t\t\t\t\t\t\t\tValue: db.CVSSSeverity{\n\t\t\t\t\t\t\t\t\t\tVector:  \"CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H\",\n\t\t\t\t\t\t\t\t\t\tVersion: \"3.1\",\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\tSource: \"nvd@nist.gov\",\n\t\t\t\t\t\t\t\t\tRank:   1,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tRelated: relatedEntries(\n\t\t\t\t\t\tdb.AffectedCPEHandle{\n\t\t\t\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\t\t\t\tCVEs: []string{\"CVE-2024-26663\"},\n\t\t\t\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tVersion: db.Version{\n\t\t\t\t\t\t\t\t\t\t\tConstraint: \"= 10.0\",\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tCPE: &db.Cpe{\n\t\t\t\t\t\t\t\tPart:    \"o\",\n\t\t\t\t\t\t\t\tVendor:  \"debian\",\n\t\t\t\t\t\t\t\tProduct: \"debian_linux\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tdb.AffectedCPEHandle{\n\t\t\t\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\t\t\t\tCVEs: []string{\"CVE-2024-26663\"},\n\t\t\t\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tVersion: db.Version{\n\t\t\t\t\t\t\t\t\t\t\tConstraint: \">= 4.9, < 4.19.307\",\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\tFix: &db.Fix{\n\t\t\t\t\t\t\t\t\t\t\tState:   db.FixedStatus,\n\t\t\t\t\t\t\t\t\t\t\tVersion: \"4.19.307\",\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tVersion: db.Version{\n\t\t\t\t\t\t\t\t\t\t\tConstraint: \">= 6.7, < 6.7.5\",\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\tFix: &db.Fix{\n\t\t\t\t\t\t\t\t\t\t\tState:   db.FixedStatus,\n\t\t\t\t\t\t\t\t\t\t\tVersion: \"6.7.5\",\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tVersion: db.Version{\n\t\t\t\t\t\t\t\t\t\t\tConstraint: \"= 6.8-rc1\",\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tVersion: db.Version{\n\t\t\t\t\t\t\t\t\t\t\tConstraint: \"= 6.8-rc2\",\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tVersion: db.Version{\n\t\t\t\t\t\t\t\t\t\t\tConstraint: \"= 6.8-rc3\",\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tCPE: &db.Cpe{\n\t\t\t\t\t\t\t\tPart:    \"o\",\n\t\t\t\t\t\t\t\tVendor:  \"linux\",\n\t\t\t\t\t\t\t\tProduct: \"linux_kernel\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tdb.CWEHandle{\n\t\t\t\t\t\t\tCVE:    \"CVE-2024-26663\",\n\t\t\t\t\t\t\tCWE:    \"CWE-476\",\n\t\t\t\t\t\t\tSource: \"nvd@nist.gov\",\n\t\t\t\t\t\t\tType:   \"Primary\",\n\t\t\t\t\t\t},\n\t\t\t\t\t),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:     \"drops nodes with unsupported topology\",\n\t\t\tfixture:  \"testdata/cve-2021-1566.json\",\n\t\t\tprovider: \"nvd\",\n\t\t\tconfig:   defaultConfig(),\n\t\t\twant: []transformers.RelatedEntries{\n\t\t\t\t{\n\t\t\t\t\tVulnerabilityHandle: &db.VulnerabilityHandle{\n\t\t\t\t\t\tName:          \"CVE-2021-1566\",\n\t\t\t\t\t\tProviderID:    \"nvd\",\n\t\t\t\t\t\tProvider:      expectedProvider(\"nvd\"),\n\t\t\t\t\t\tModifiedDate:  timeRef(time.Date(2024, 11, 21, 5, 44, 38, 237000000, time.UTC)),\n\t\t\t\t\t\tPublishedDate: timeRef(time.Date(2021, 6, 16, 18, 15, 8, 710000000, time.UTC)),\n\t\t\t\t\t\tStatus:        db.VulnerabilityActive,\n\t\t\t\t\t\tBlobValue: &db.VulnerabilityBlob{\n\t\t\t\t\t\t\tID:          \"CVE-2021-1566\",\n\t\t\t\t\t\t\tAssigners:   []string{\"psirt@cisco.com\"},\n\t\t\t\t\t\t\tDescription: \"description.\",\n\t\t\t\t\t\t\tReferences: []db.Reference{\n\t\t\t\t\t\t\t\t{URL: \"https://nvd.nist.gov/vuln/detail/CVE-2021-1566\"},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tURL:  \"https://tools.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-esa-wsa-cert-vali-n8L97RW\",\n\t\t\t\t\t\t\t\t\tTags: []string{\"vendor-advisory\"},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tSeverities: []db.Severity{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tScheme: \"CVSS\",\n\t\t\t\t\t\t\t\t\tValue: db.CVSSSeverity{\n\t\t\t\t\t\t\t\t\t\tVector:  \"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N\",\n\t\t\t\t\t\t\t\t\t\tVersion: \"3.1\",\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\tSource: \"nvd@nist.gov\",\n\t\t\t\t\t\t\t\t\tRank:   1,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tScheme: \"CVSS\",\n\t\t\t\t\t\t\t\t\tValue: db.CVSSSeverity{\n\t\t\t\t\t\t\t\t\t\tVector:  \"AV:N/AC:M/Au:N/C:P/I:P/A:N\",\n\t\t\t\t\t\t\t\t\t\tVersion: \"2.0\",\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\tSource: \"nvd@nist.gov\",\n\t\t\t\t\t\t\t\t\tRank:   1,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tScheme: \"CVSS\",\n\t\t\t\t\t\t\t\t\tValue: db.CVSSSeverity{\n\t\t\t\t\t\t\t\t\t\tVector:  \"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N\",\n\t\t\t\t\t\t\t\t\t\tVersion: \"3.1\",\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\tSource: \"psirt@cisco.com\",\n\t\t\t\t\t\t\t\t\tRank:   2,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tRelated: relatedEntries(\n\t\t\t\t\t\tdb.CWEHandle{\n\t\t\t\t\t\t\tCVE:    \"CVE-2021-1566\",\n\t\t\t\t\t\t\tCWE:    \"CWE-296\",\n\t\t\t\t\t\t\tSource: \"psirt@cisco.com\",\n\t\t\t\t\t\t\tType:   \"Secondary\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tdb.CWEHandle{\n\t\t\t\t\t\t\tCVE:    \"CVE-2021-1566\",\n\t\t\t\t\t\t\tCWE:    \"CWE-295\",\n\t\t\t\t\t\t\tSource: \"nvd@nist.gov\",\n\t\t\t\t\t\t\tType:   \"Primary\",\n\t\t\t\t\t\t},\n\t\t\t\t\t), // important! we dropped all of the node criteria since the topology is unsupported\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:     \"considers non-standard CPE fields\",\n\t\t\tfixture:  \"testdata/CVE-2008-3442.json\",\n\t\t\tprovider: \"nvd\",\n\t\t\tconfig:   defaultConfig(),\n\t\t\twant: []transformers.RelatedEntries{\n\t\t\t\t{\n\t\t\t\t\tVulnerabilityHandle: &db.VulnerabilityHandle{\n\t\t\t\t\t\tName:          \"CVE-2008-3442\",\n\t\t\t\t\t\tProviderID:    \"nvd\",\n\t\t\t\t\t\tProvider:      expectedProvider(\"nvd\"),\n\t\t\t\t\t\tModifiedDate:  timeRef(time.Date(2008, 9, 5, 21, 43, 5, 500000000, time.UTC)),\n\t\t\t\t\t\tPublishedDate: timeRef(time.Date(2008, 8, 1, 14, 41, 0, 0, time.UTC)),\n\t\t\t\t\t\tStatus:        db.VulnerabilityActive,\n\t\t\t\t\t\tBlobValue: &db.VulnerabilityBlob{\n\t\t\t\t\t\t\tID:          \"CVE-2008-3442\",\n\t\t\t\t\t\t\tAssigners:   []string{\"cve@mitre.org\"},\n\t\t\t\t\t\t\tDescription: \"desc.\",\n\t\t\t\t\t\t\tReferences:  []db.Reference{{URL: \"https://nvd.nist.gov/vuln/detail/CVE-2008-3442\"}},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tRelated: relatedEntries(\n\t\t\t\t\t\tdb.AffectedCPEHandle{\n\t\t\t\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\t\t\t\tCVEs: []string{\"CVE-2008-3442\"},\n\t\t\t\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tVersion: db.Version{\n\t\t\t\t\t\t\t\t\t\t\tConstraint: \"= 10.0\",\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tVersion: db.Version{\n\t\t\t\t\t\t\t\t\t\t\tConstraint: \"= 7.0\",\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tVersion: db.Version{\n\t\t\t\t\t\t\t\t\t\t\tConstraint: \"= 8.0\",\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tVersion: db.Version{\n\t\t\t\t\t\t\t\t\t\t\tConstraint: \"= 8.1\",\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tVersion: db.Version{\n\t\t\t\t\t\t\t\t\t\t\tConstraint: \"= 9.0\",\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tCPE: &db.Cpe{\n\t\t\t\t\t\t\t\tPart:    \"a\",\n\t\t\t\t\t\t\t\tVendor:  \"winzip\",\n\t\t\t\t\t\t\t\tProduct: \"winzip\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tdb.AffectedCPEHandle{\n\t\t\t\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\t\t\t\tCVEs: []string{\"CVE-2008-3442\"},\n\t\t\t\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tVersion: db.Version{\n\t\t\t\t\t\t\t\t\t\t\tConstraint: \"= 8.1\",\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tVersion: db.Version{\n\t\t\t\t\t\t\t\t\t\t\tConstraint: \"= 9.0\",\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tCPE: &db.Cpe{\n\t\t\t\t\t\t\t\tPart:    \"a\",\n\t\t\t\t\t\t\t\tVendor:  \"winzip\",\n\t\t\t\t\t\t\t\tProduct: \"winzip\",\n\t\t\t\t\t\t\t\tEdition: \"sr1\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tdb.CWEHandle{\n\t\t\t\t\t\t\tCVE:    \"CVE-2008-3442\",\n\t\t\t\t\t\t\tCWE:    \"CWE-94\",\n\t\t\t\t\t\t\tSource: \"nvd@nist.gov\",\n\t\t\t\t\t\t\tType:   \"Primary\",\n\t\t\t\t\t\t},\n\t\t\t\t\t),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:     \"https://github.com/anchore/grype/issues/2807#issuecomment-3101447594\",\n\t\t\tfixture:  \"testdata/CVE-2004-0377.json\",\n\t\t\tprovider: \"nvd\",\n\t\t\tconfig:   defaultConfig(),\n\t\t\twant: []transformers.RelatedEntries{\n\t\t\t\t{\n\t\t\t\t\tVulnerabilityHandle: &db.VulnerabilityHandle{\n\t\t\t\t\t\tName:          \"CVE-2004-0377\",\n\t\t\t\t\t\tProviderID:    \"nvd\",\n\t\t\t\t\t\tProvider:      expectedProvider(\"nvd\"),\n\t\t\t\t\t\tModifiedDate:  timeRef(time.Date(2025, 4, 3, 1, 3, 51, 193000000, time.UTC)),\n\t\t\t\t\t\tPublishedDate: timeRef(time.Date(2004, 5, 4, 4, 0, 0, 0, time.UTC)),\n\t\t\t\t\t\tStatus:        db.UnknownVulnerabilityStatus,\n\t\t\t\t\t\tBlobValue: &db.VulnerabilityBlob{\n\t\t\t\t\t\t\tID:          \"CVE-2004-0377\",\n\t\t\t\t\t\t\tAssigners:   []string{\"cve@mitre.org\"},\n\t\t\t\t\t\t\tDescription: \"Buffer overflow in the win32_stat function for (1) ActiveState's ActivePerl and (2) Larry Wall's Perl before 5.8.3 allows local or remote attackers to execute arbitrary commands via filenames that end in a backslash character.\",\n\t\t\t\t\t\t\tReferences: []db.Reference{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tURL: \"https://nvd.nist.gov/vuln/detail/CVE-2004-0377\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tRelated: relatedEntries(\n\t\t\t\t\t\tdb.AffectedCPEHandle{\n\t\t\t\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\t\t\t\tCVEs:   []string{\"CVE-2004-0377\"},\n\t\t\t\t\t\t\t\tRanges: nil,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tCPE: &db.Cpe{\n\t\t\t\t\t\t\t\tPart:    \"a\",\n\t\t\t\t\t\t\t\tVendor:  \"activestate\",\n\t\t\t\t\t\t\t\tProduct: \"activeperl\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tdb.AffectedCPEHandle{\n\t\t\t\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\t\t\t\tCVEs: []string{\"CVE-2004-0377\"},\n\t\t\t\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tVersion: db.Version{\n\t\t\t\t\t\t\t\t\t\t\tConstraint: \"<= 5.8.3\",\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tCPE: &db.Cpe{\n\t\t\t\t\t\t\t\tPart:    \"a\",\n\t\t\t\t\t\t\t\tVendor:  \"larry_wall\",\n\t\t\t\t\t\t\t\tProduct: \"perl\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tdb.CWEHandle{\n\t\t\t\t\t\t\tCVE:    \"CVE-2004-0377\",\n\t\t\t\t\t\t\tCWE:    \"NVD-CWE-Other\",\n\t\t\t\t\t\t\tSource: \"nvd@nist.gov\",\n\t\t\t\t\t\t\tType:   \"Primary\",\n\t\t\t\t\t\t},\n\t\t\t\t\t),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:     \"JVM packages version format detection\",\n\t\t\tfixture:  \"testdata/jvm-packages.json\",\n\t\t\tprovider: \"nvd\",\n\t\t\tconfig:   defaultConfig(),\n\t\t\twant: []transformers.RelatedEntries{\n\t\t\t\t{\n\t\t\t\t\tVulnerabilityHandle: &db.VulnerabilityHandle{\n\t\t\t\t\t\tName:          \"CVE-2023-JVM-TEST\",\n\t\t\t\t\t\tProviderID:    \"nvd\",\n\t\t\t\t\t\tProvider:      expectedProvider(\"nvd\"),\n\t\t\t\t\t\tModifiedDate:  timeRef(time.Date(2024, 1, 23, 16, 32, 52, 103000000, time.UTC)),\n\t\t\t\t\t\tPublishedDate: timeRef(time.Date(2024, 1, 17, 0, 15, 51, 677000000, time.UTC)),\n\t\t\t\t\t\tStatus:        db.VulnerabilityActive,\n\t\t\t\t\t\tBlobValue: &db.VulnerabilityBlob{\n\t\t\t\t\t\t\tID:          \"CVE-2023-JVM-TEST\",\n\t\t\t\t\t\t\tAssigners:   []string{\"cve@mitre.org\"},\n\t\t\t\t\t\t\tDescription: \"Test vulnerability affecting JVM packages to demonstrate version format detection.\",\n\t\t\t\t\t\t\tReferences: []db.Reference{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tURL: \"https://nvd.nist.gov/vuln/detail/CVE-2023-JVM-TEST\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tURL:  \"https://www.oracle.com/security-alerts/cpujan2024.html\",\n\t\t\t\t\t\t\t\t\tTags: []string{\"patch\", \"vendor-advisory\"},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tSeverities: []db.Severity{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tScheme: db.SeveritySchemeCVSS,\n\t\t\t\t\t\t\t\t\tValue: db.CVSSSeverity{\n\t\t\t\t\t\t\t\t\t\tVector:  \"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N\",\n\t\t\t\t\t\t\t\t\t\tVersion: \"3.1\",\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\tSource: \"nvd@nist.gov\",\n\t\t\t\t\t\t\t\t\tRank:   1,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tRelated: relatedEntries(\n\t\t\t\t\t\tdb.AffectedCPEHandle{\n\t\t\t\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\t\t\t\tCVEs: []string{\"CVE-2023-JVM-TEST\"},\n\t\t\t\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tVersion: db.Version{\n\t\t\t\t\t\t\t\t\t\t\tType:       \"jvm\",\n\t\t\t\t\t\t\t\t\t\t\tConstraint: \"= 17.0.10\",\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tCPE: &db.Cpe{\n\t\t\t\t\t\t\t\tPart:    \"a\",\n\t\t\t\t\t\t\t\tVendor:  \"adoptium\",\n\t\t\t\t\t\t\t\tProduct: \"java\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tdb.AffectedCPEHandle{\n\t\t\t\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\t\t\t\tCVEs: []string{\"CVE-2023-JVM-TEST\"},\n\t\t\t\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tVersion: db.Version{\n\t\t\t\t\t\t\t\t\t\t\tType:       \"jvm\",\n\t\t\t\t\t\t\t\t\t\t\tConstraint: \"= 21.0.2\",\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tCPE: &db.Cpe{\n\t\t\t\t\t\t\t\tPart:    \"a\",\n\t\t\t\t\t\t\t\tVendor:  \"azul\",\n\t\t\t\t\t\t\t\tProduct: \"zulu\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tdb.AffectedCPEHandle{\n\t\t\t\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\t\t\t\tCVEs: []string{\"CVE-2023-JVM-TEST\"},\n\t\t\t\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tVersion: db.Version{\n\t\t\t\t\t\t\t\t\t\t\tType:       \"jvm\",\n\t\t\t\t\t\t\t\t\t\t\tConstraint: \"= 17.0.10\",\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tCPE: &db.Cpe{\n\t\t\t\t\t\t\t\tPart:    \"a\",\n\t\t\t\t\t\t\t\tVendor:  \"eclipse\",\n\t\t\t\t\t\t\t\tProduct: \"openjdk\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tdb.AffectedCPEHandle{\n\t\t\t\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\t\t\t\tCVEs: []string{\"CVE-2023-JVM-TEST\"},\n\t\t\t\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tVersion: db.Version{\n\t\t\t\t\t\t\t\t\t\t\tType:       \"jvm\",\n\t\t\t\t\t\t\t\t\t\t\tConstraint: \"= 11.0.22\",\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tVersion: db.Version{\n\t\t\t\t\t\t\t\t\t\t\tType:       \"jvm\",\n\t\t\t\t\t\t\t\t\t\t\tConstraint: \"= 8u401\",\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tCPE: &db.Cpe{\n\t\t\t\t\t\t\t\tPart:    \"a\",\n\t\t\t\t\t\t\t\tVendor:  \"oracle\",\n\t\t\t\t\t\t\t\tProduct: \"jdk\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tdb.CWEHandle{\n\t\t\t\t\t\t\tCVE:    \"CVE-2023-JVM-TEST\",\n\t\t\t\t\t\t\tCWE:    \"CWE-79\",\n\t\t\t\t\t\t\tSource: \"nvd@nist.gov\",\n\t\t\t\t\t\t\tType:   \"Primary\",\n\t\t\t\t\t\t},\n\t\t\t\t\t),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tvulns := loadFixture(t, test.fixture)\n\n\t\t\tvar actual []transformers.RelatedEntries\n\t\t\tfor _, vuln := range vulns {\n\t\t\t\tif test.config == (Config{}) {\n\t\t\t\t\ttest.config = defaultConfig()\n\t\t\t\t}\n\t\t\t\tentries, err := Transformer(test.config)(vuln, inputProviderState(test.provider))\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tfor _, entry := range entries {\n\t\t\t\t\te, ok := entry.Data.(transformers.RelatedEntries)\n\t\t\t\t\trequire.True(t, ok)\n\t\t\t\t\tactual = append(actual, e)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif diff := cmp.Diff(test.want, actual); diff != \"\" {\n\t\t\t\tt.Errorf(\"data entries mismatch (-want +got):\\n%s\", diff)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc relatedEntries(items ...any) []any {\n\treturn items\n}\n\nfunc loadFixture(t *testing.T, fixturePath string) []unmarshal.NVDVulnerability {\n\tt.Helper()\n\n\tf, err := os.Open(fixturePath)\n\trequire.NoError(t, err)\n\tdefer func() {\n\t\trequire.NoError(t, f.Close())\n\t}()\n\n\tentries, err := unmarshal.NvdVulnerabilityEntries(f)\n\trequire.NoError(t, err)\n\n\tvar vulns []unmarshal.NVDVulnerability\n\tfor _, entry := range entries {\n\t\tvulns = append(vulns, entry.Cve)\n\t}\n\n\treturn vulns\n}\n\nfunc timeRef(ti time.Time) *time.Time {\n\treturn &ti\n}\n\nfunc TestIsValidCWE(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tcwe      string\n\t\texpected bool\n\t}{\n\t\t{\n\t\t\tname:     \"empty string\",\n\t\t\tcwe:      \"\",\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"NVD-CWE-noinfo\",\n\t\t\tcwe:      \"NVD-CWE-noinfo\",\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"NVD-CWE-Other\",\n\t\t\tcwe:      \"NVD-CWE-Other\",\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"valid CWE with single digit\",\n\t\t\tcwe:      \"CWE-1\",\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"valid CWE with multiple digits\",\n\t\t\tcwe:      \"CWE-123\",\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"valid CWE-79\",\n\t\t\tcwe:      \"CWE-79\",\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"valid CWE-89\",\n\t\t\tcwe:      \"CWE-89\",\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"invalid CWE without prefix\",\n\t\t\tcwe:      \"123\",\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"invalid CWE without number\",\n\t\t\tcwe:      \"CWE-\",\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"invalid CWE with letters\",\n\t\t\tcwe:      \"CWE-ABC\",\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"invalid lowercase cwe\",\n\t\t\tcwe:      \"cwe-123\",\n\t\t\texpected: false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot := isValidCWE(tt.cwe)\n\t\t\tif got != tt.expected {\n\t\t\t\tt.Errorf(\"isValidCWE(%q) = %v, want %v\", tt.cwe, got, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestGetReferences(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tvuln     unmarshal.NVDVulnerability\n\t\texpected []db.Reference\n\t}{\n\t\t{\n\t\t\tname: \"no upstream references - only canonical NVD URL\",\n\t\t\tvuln: unmarshal.NVDVulnerability{\n\t\t\t\tID:         \"CVE-2023-12345\",\n\t\t\t\tReferences: []nvd.Reference{},\n\t\t\t},\n\t\t\texpected: []db.Reference{\n\t\t\t\t{URL: \"https://nvd.nist.gov/vuln/detail/CVE-2023-12345\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"single unique reference\",\n\t\t\tvuln: unmarshal.NVDVulnerability{\n\t\t\t\tID: \"CVE-2023-12345\",\n\t\t\t\tReferences: []nvd.Reference{\n\t\t\t\t\t{URL: \"https://example.com/advisory\", Tags: []string{\"patch\", \"vendor-advisory\"}},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: []db.Reference{\n\t\t\t\t{URL: \"https://nvd.nist.gov/vuln/detail/CVE-2023-12345\"},\n\t\t\t\t{URL: \"https://example.com/advisory\", Tags: []string{\"patch\", \"vendor-advisory\"}},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"multiple unique references\",\n\t\t\tvuln: unmarshal.NVDVulnerability{\n\t\t\t\tID: \"CVE-2023-12345\",\n\t\t\t\tReferences: []nvd.Reference{\n\t\t\t\t\t{URL: \"https://example.com/advisory\", Tags: []string{\"patch\"}},\n\t\t\t\t\t{URL: \"https://github.com/project/issues/123\", Tags: []string{\"issue-tracking\"}},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: []db.Reference{\n\t\t\t\t{URL: \"https://nvd.nist.gov/vuln/detail/CVE-2023-12345\"},\n\t\t\t\t{URL: \"https://example.com/advisory\", Tags: []string{\"patch\"}},\n\t\t\t\t{URL: \"https://github.com/project/issues/123\", Tags: []string{\"issue-tracking\"}},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"exact duplicate references\",\n\t\t\tvuln: unmarshal.NVDVulnerability{\n\t\t\t\tID: \"CVE-2023-12345\",\n\t\t\t\tReferences: []nvd.Reference{\n\t\t\t\t\t{URL: \"https://example.com\", Tags: []string{\"patch\", \"vendor-advisory\"}},\n\t\t\t\t\t{URL: \"https://example.com\", Tags: []string{\"patch\", \"vendor-advisory\"}},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: []db.Reference{\n\t\t\t\t{URL: \"https://nvd.nist.gov/vuln/detail/CVE-2023-12345\"},\n\t\t\t\t{URL: \"https://example.com\", Tags: []string{\"patch\", \"vendor-advisory\"}},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"duplicate with tags in different order (congruent sets)\",\n\t\t\tvuln: unmarshal.NVDVulnerability{\n\t\t\t\tID: \"CVE-2023-12345\",\n\t\t\t\tReferences: []nvd.Reference{\n\t\t\t\t\t{URL: \"https://example.com\", Tags: []string{\"patch\", \"vendor-advisory\"}},\n\t\t\t\t\t{URL: \"https://example.com\", Tags: []string{\"vendor-advisory\", \"patch\"}},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: []db.Reference{\n\t\t\t\t{URL: \"https://nvd.nist.gov/vuln/detail/CVE-2023-12345\"},\n\t\t\t\t{URL: \"https://example.com\", Tags: []string{\"patch\", \"vendor-advisory\"}},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"same URL with different tags - keep both\",\n\t\t\tvuln: unmarshal.NVDVulnerability{\n\t\t\t\tID: \"CVE-2023-12345\",\n\t\t\t\tReferences: []nvd.Reference{\n\t\t\t\t\t{URL: \"https://example.com\", Tags: []string{\"patch\"}},\n\t\t\t\t\t{URL: \"https://example.com\", Tags: []string{\"vendor-advisory\"}},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: []db.Reference{\n\t\t\t\t{URL: \"https://nvd.nist.gov/vuln/detail/CVE-2023-12345\"},\n\t\t\t\t{URL: \"https://example.com\", Tags: []string{\"patch\"}},\n\t\t\t\t{URL: \"https://example.com\", Tags: []string{\"vendor-advisory\"}},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"same URL with and without tags - keep both\",\n\t\t\tvuln: unmarshal.NVDVulnerability{\n\t\t\t\tID: \"CVE-2023-12345\",\n\t\t\t\tReferences: []nvd.Reference{\n\t\t\t\t\t{URL: \"https://example.com\", Tags: []string{\"patch\"}},\n\t\t\t\t\t{URL: \"https://example.com\", Tags: nil},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: []db.Reference{\n\t\t\t\t{URL: \"https://nvd.nist.gov/vuln/detail/CVE-2023-12345\"},\n\t\t\t\t{URL: \"https://example.com\", Tags: []string{\"patch\"}},\n\t\t\t\t{URL: \"https://example.com\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"duplicate canonical NVD URL in upstream data\",\n\t\t\tvuln: unmarshal.NVDVulnerability{\n\t\t\t\tID: \"CVE-2023-12345\",\n\t\t\t\tReferences: []nvd.Reference{\n\t\t\t\t\t{URL: \"https://nvd.nist.gov/vuln/detail/CVE-2023-12345\", Tags: nil},\n\t\t\t\t\t{URL: \"https://example.com/advisory\", Tags: []string{\"vendor-advisory\"}},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: []db.Reference{\n\t\t\t\t{URL: \"https://nvd.nist.gov/vuln/detail/CVE-2023-12345\"},\n\t\t\t\t{URL: \"https://example.com/advisory\", Tags: []string{\"vendor-advisory\"}},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"empty URL in upstream data - should be filtered\",\n\t\t\tvuln: unmarshal.NVDVulnerability{\n\t\t\t\tID: \"CVE-2023-12345\",\n\t\t\t\tReferences: []nvd.Reference{\n\t\t\t\t\t{URL: \"\", Tags: []string{\"patch\"}},\n\t\t\t\t\t{URL: \"https://example.com\", Tags: []string{\"vendor-advisory\"}},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: []db.Reference{\n\t\t\t\t{URL: \"https://nvd.nist.gov/vuln/detail/CVE-2023-12345\"},\n\t\t\t\t{URL: \"https://example.com\", Tags: []string{\"vendor-advisory\"}},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"multiple duplicates among many references\",\n\t\t\tvuln: unmarshal.NVDVulnerability{\n\t\t\t\tID: \"CVE-2023-12345\",\n\t\t\t\tReferences: []nvd.Reference{\n\t\t\t\t\t{URL: \"https://example.com/1\", Tags: []string{\"patch\"}},\n\t\t\t\t\t{URL: \"https://example.com/2\", Tags: []string{\"vendor-advisory\"}},\n\t\t\t\t\t{URL: \"https://example.com/1\", Tags: []string{\"patch\"}},\n\t\t\t\t\t{URL: \"https://example.com/3\", Tags: nil},\n\t\t\t\t\t{URL: \"https://example.com/2\", Tags: []string{\"vendor-advisory\"}},\n\t\t\t\t\t{URL: \"https://example.com/3\", Tags: []string{}},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: []db.Reference{\n\t\t\t\t{URL: \"https://nvd.nist.gov/vuln/detail/CVE-2023-12345\"},\n\t\t\t\t{URL: \"https://example.com/1\", Tags: []string{\"patch\"}},\n\t\t\t\t{URL: \"https://example.com/2\", Tags: []string{\"vendor-advisory\"}},\n\t\t\t\t{URL: \"https://example.com/3\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"preserves order of first occurrence\",\n\t\t\tvuln: unmarshal.NVDVulnerability{\n\t\t\t\tID: \"CVE-2023-12345\",\n\t\t\t\tReferences: []nvd.Reference{\n\t\t\t\t\t{URL: \"https://example.com/1\", Tags: []string{\"patch\"}},\n\t\t\t\t\t{URL: \"https://example.com/2\", Tags: []string{\"advisory\"}},\n\t\t\t\t\t{URL: \"https://example.com/3\", Tags: []string{\"exploit\"}},\n\t\t\t\t\t{URL: \"https://example.com/1\", Tags: []string{\"patch\"}},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: []db.Reference{\n\t\t\t\t{URL: \"https://nvd.nist.gov/vuln/detail/CVE-2023-12345\"},\n\t\t\t\t{URL: \"https://example.com/1\", Tags: []string{\"patch\"}},\n\t\t\t\t{URL: \"https://example.com/2\", Tags: []string{\"advisory\"}},\n\t\t\t\t{URL: \"https://example.com/3\", Tags: []string{\"exploit\"}},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"complex real-world scenario with duplicates and tag reordering\",\n\t\t\tvuln: unmarshal.NVDVulnerability{\n\t\t\t\tID: \"CVE-2023-12345\",\n\t\t\t\tReferences: []nvd.Reference{\n\t\t\t\t\t{URL: \"https://nvd.nist.gov/vuln/detail/CVE-2023-12345\"},\n\t\t\t\t\t{URL: \"https://github.com/project/issues/123\", Tags: []string{\"issue-tracking\", \"third-party-advisory\"}},\n\t\t\t\t\t{URL: \"https://security.vendor.com/advisory\", Tags: []string{\"patch\", \"vendor-advisory\"}},\n\t\t\t\t\t{URL: \"https://github.com/project/issues/123\", Tags: []string{\"third-party-advisory\", \"issue-tracking\"}},\n\t\t\t\t\t{URL: \"https://lists.vendor.com/announce\", Tags: []string{\"mailing-list\"}},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: []db.Reference{\n\t\t\t\t{URL: \"https://nvd.nist.gov/vuln/detail/CVE-2023-12345\"},\n\t\t\t\t{URL: \"https://github.com/project/issues/123\", Tags: []string{\"issue-tracking\", \"third-party-advisory\"}},\n\t\t\t\t{URL: \"https://security.vendor.com/advisory\", Tags: []string{\"patch\", \"vendor-advisory\"}},\n\t\t\t\t{URL: \"https://lists.vendor.com/announce\", Tags: []string{\"mailing-list\"}},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"references with nil tags vs empty tags - treated as identical\",\n\t\t\tvuln: unmarshal.NVDVulnerability{\n\t\t\t\tID: \"CVE-2023-12345\",\n\t\t\t\tReferences: []nvd.Reference{\n\t\t\t\t\t{URL: \"https://example.com\", Tags: nil},\n\t\t\t\t\t{URL: \"https://example.com\", Tags: []string{}},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: []db.Reference{\n\t\t\t\t{URL: \"https://nvd.nist.gov/vuln/detail/CVE-2023-12345\"},\n\t\t\t\t{URL: \"https://example.com\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"duplicate after different tags on same URL - exposes logic bug\",\n\t\t\tvuln: unmarshal.NVDVulnerability{\n\t\t\t\tID: \"CVE-2023-12345\",\n\t\t\t\tReferences: []nvd.Reference{\n\t\t\t\t\t{URL: \"https://example.com\", Tags: []string{\"patch\"}},\n\t\t\t\t\t{URL: \"https://example.com\", Tags: []string{\"vendor-advisory\"}},\n\t\t\t\t\t{URL: \"https://example.com\", Tags: []string{\"patch\"}}, // duplicate of first\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: []db.Reference{\n\t\t\t\t{URL: \"https://nvd.nist.gov/vuln/detail/CVE-2023-12345\"},\n\t\t\t\t{URL: \"https://example.com\", Tags: []string{\"patch\"}},\n\t\t\t\t{URL: \"https://example.com\", Tags: []string{\"vendor-advisory\"}},\n\t\t\t\t// Should NOT have a duplicate \"patch\" entry here\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tactual := getReferences(tt.vuln)\n\n\t\t\tif diff := cmp.Diff(tt.expected, actual, cmpopts.EquateEmpty()); diff != \"\" {\n\t\t\t\tt.Errorf(\"getReferences() mismatch (-want +got):\\n%s\", diff)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/db/v6/build/transformers/openvex/transform.go",
    "content": "package openvex\n\nimport (\n\t\"fmt\"\n\t\"sort\"\n\n\tgovex \"github.com/openvex/go-vex/pkg/vex\"\n\n\t\"github.com/anchore/grype/grype/db/data\"\n\t\"github.com/anchore/grype/grype/db/internal/provider/unmarshal\"\n\t\"github.com/anchore/grype/grype/db/provider\"\n\tdb \"github.com/anchore/grype/grype/db/v6\"\n\t\"github.com/anchore/grype/grype/db/v6/build/transformers\"\n\t\"github.com/anchore/grype/grype/db/v6/build/transformers/internal\"\n\t\"github.com/anchore/grype/grype/version\"\n\t\"github.com/anchore/packageurl-go\"\n\tsyftPkg \"github.com/anchore/syft/syft/pkg\"\n)\n\nfunc AnnotatedTransform(wrapper unmarshal.AnnotatedOpenVEXVulnerability, state provider.State) ([]data.Entry, error) {\n\treturn transform(wrapper.Document, state, wrapper.Fixes)\n}\n\nfunc Transform(vulnerability unmarshal.OpenVEXVulnerability, state provider.State) ([]data.Entry, error) {\n\treturn transform(vulnerability, state, nil)\n}\n\nfunc transform(vulnerability unmarshal.OpenVEXVulnerability, state provider.State, fixes []unmarshal.AnnotatedOpenVEXFix) ([]data.Entry, error) {\n\tname := getName(&vulnerability)\n\tvulnHandle := db.VulnerabilityHandle{\n\t\tName:          name,\n\t\tStatus:        db.VulnerabilityActive,\n\t\tPublishedDate: vulnerability.Timestamp,\n\t\tModifiedDate:  vulnerability.LastUpdated,\n\t\tProviderID:    state.Provider,\n\t\tProvider:      provider.Model(state),\n\t\tBlobValue: &db.VulnerabilityBlob{\n\t\t\tID:          name,\n\t\t\tAssigners:   nil,\n\t\t\tDescription: vulnerability.Vulnerability.Description,\n\t\t\tReferences:  getReferences(&vulnerability),\n\t\t\tAliases:     getAliases(&vulnerability),\n\t\t},\n\t}\n\tpkgs, err := getPackageHandles(&vulnerability, fixes)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tin := []any{vulnHandle}\n\tin = append(in, pkgs...)\n\treturn transformers.NewEntries(in...), nil\n}\n\n// getPackageHandles for all products in this advisory\nfunc getPackageHandles(vuln *unmarshal.OpenVEXVulnerability, fixes []unmarshal.AnnotatedOpenVEXFix) ([]any, error) {\n\tif len(vuln.Products) == 0 {\n\t\treturn nil, nil\n\t}\n\n\tfixesByProduct := make(map[string][]unmarshal.AnnotatedOpenVEXFix)\n\tfor _, fix := range fixes {\n\t\tfixesByProduct[fix.Product] = append(fixesByProduct[fix.Product], fix)\n\t}\n\n\tvar aphs []db.AffectedPackageHandle\n\tvar uaphs []db.UnaffectedPackageHandle\n\tfor _, product := range vuln.Products {\n\t\taph, uph, err := getPackageHandle(&product, vuln, fixesByProduct[product.Identifiers[govex.PURL]])\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\taphs = append(aphs, aph...)\n\t\tuaphs = append(uaphs, uph...)\n\t}\n\n\tsort.Sort(internal.ByAffectedPackage(aphs))\n\tsort.Sort(internal.ByUnaffectedPackage(uaphs))\n\n\tvar all []any\n\tfor i := range aphs {\n\t\tall = append(all, aphs[i])\n\t}\n\tfor i := range uaphs {\n\t\tall = append(all, uaphs[i])\n\t}\n\n\treturn all, nil\n}\n\n// getPackageHandle for a single product\n//\n// OpenVEX defines product via:\n//\n//\tComponent {\n//\t  Identifiers: {\n//\t    PURLIdentifierType: pkg:type/name@version\n//\t  }\n//\t}\nfunc getPackageHandle(product *govex.Product, vuln *unmarshal.OpenVEXVulnerability, fixes []unmarshal.AnnotatedOpenVEXFix) (aphs []db.AffectedPackageHandle, uphs []db.UnaffectedPackageHandle, err error) {\n\tif product == nil || vuln == nil {\n\t\treturn nil, nil, fmt.Errorf(\"getAffectedPackage params cannot be nil\")\n\t}\n\tpurl, err := getPURL(product)\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"failed to parse purl %s: %w\", purl, err)\n\t}\n\n\tpkg := &db.Package{\n\t\tEcosystem: string(syftPkg.TypeFromPURL(purl.String())),\n\t\tName:      purl.Name,\n\t}\n\n\taliases := []string{getName(vuln)}\n\taliases = append(aliases, getAliases(vuln)...)\n\n\tswitch vuln.Status {\n\tcase govex.StatusAffected:\n\t\taphs = append(aphs, db.AffectedPackageHandle{\n\t\t\tPackage:   pkg,\n\t\t\tBlobValue: getPackageBlob(aliases, purl.Version, purl.Type, \"\", fixes),\n\t\t})\n\tcase govex.StatusNotAffected:\n\t\tuphs = append(uphs, db.UnaffectedPackageHandle{\n\t\t\tPackage:   pkg,\n\t\t\tBlobValue: getPackageBlob(aliases, purl.Version, purl.Type, db.NotAffectedFixStatus, fixes),\n\t\t})\n\tcase govex.StatusFixed:\n\t\tuphs = append(uphs, db.UnaffectedPackageHandle{\n\t\t\tPackage:   pkg,\n\t\t\tBlobValue: getPackageBlob(aliases, purl.Version, purl.Type, db.FixedStatus, fixes),\n\t\t})\n\tdefault:\n\t\terr = fmt.Errorf(\"invalid vuln states %s\", vuln.Status)\n\t}\n\treturn aphs, uphs, err\n}\n\n// getPURL from either ID field or identifiers\nfunc getPURL(product *govex.Product) (purl *packageurl.PackageURL, err error) {\n\tif p, ok := product.Identifiers[govex.PURL]; ok {\n\t\tpurl, err := packageurl.FromString(p)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to parse purl %s: %w\", p, err)\n\t\t}\n\t\treturn &purl, nil\n\t}\n\tif product.ID != \"\" {\n\t\tpurl, err := packageurl.FromString(product.ID)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn &purl, nil\n\t}\n\treturn nil, fmt.Errorf(\"invalid product: %v\", product)\n}\n\nfunc getAliases(vuln *unmarshal.OpenVEXVulnerability) []string {\n\tret := make([]string, 0, len(vuln.Vulnerability.Aliases))\n\tfor _, alias := range vuln.Vulnerability.Aliases {\n\t\tret = append(ret, string(alias))\n\t}\n\treturn ret\n}\n\nfunc getName(vuln *unmarshal.OpenVEXVulnerability) string {\n\treturn string(vuln.Vulnerability.Name)\n}\n\nfunc getReferences(vuln *unmarshal.OpenVEXVulnerability) []db.Reference {\n\trefs := []db.Reference{\n\t\t{\n\t\t\tURL: getName(vuln),\n\t\t},\n\t}\n\treturn refs\n}\n\nfunc getPackageBlob(aliases []string, ver string, ty string, fixState db.FixStatus, fixes []unmarshal.AnnotatedOpenVEXFix) *db.PackageBlob {\n\tvar fix *db.Fix\n\tif fixState != \"\" {\n\t\tfix = &db.Fix{\n\t\t\tState: fixState,\n\t\t}\n\n\t\tcanExpressFixVersion := ver != \"\" && fixState == db.FixedStatus\n\t\tif canExpressFixVersion {\n\t\t\t// only express a fix version if we have a version and the state is \"fixed\"\n\t\t\tfix.Version = ver\n\t\t}\n\n\t\tcanExpressFixDetail := len(fixes) > 0 && canExpressFixVersion\n\t\tvar detail *db.FixDetail\n\t\tif canExpressFixDetail {\n\t\t\ttime := internal.ParseTime(fixes[0].Available.Date)\n\t\t\tif time != nil && !time.IsZero() {\n\t\t\t\tdetail = &db.FixDetail{\n\t\t\t\t\tAvailable: &db.FixAvailability{\n\t\t\t\t\t\tDate: time,\n\t\t\t\t\t\tKind: fixes[0].Available.Kind,\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tfix.Detail = detail\n\t}\n\n\treturn &db.PackageBlob{\n\t\tCVEs: aliases,\n\t\tRanges: []db.Range{\n\t\t\t{\n\t\t\t\tVersion: db.Version{\n\t\t\t\t\tType:       version.ParseFormat(ty).String(),\n\t\t\t\t\tConstraint: fmt.Sprintf(\"= %s\", ver),\n\t\t\t\t},\n\t\t\t\tFix: fix,\n\t\t\t},\n\t\t},\n\t}\n}\n"
  },
  {
    "path": "grype/db/v6/build/transformers/openvex/transform_test.go",
    "content": "package openvex\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\tgovex \"github.com/openvex/go-vex/pkg/vex\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/grype/grype/db/internal/provider/unmarshal\"\n\t\"github.com/anchore/grype/grype/db/provider\"\n\tdb \"github.com/anchore/grype/grype/db/v6\"\n\t\"github.com/anchore/grype/grype/db/v6/build/transformers\"\n\t\"github.com/anchore/grype/grype/db/v6/build/transformers/internal\"\n\t\"github.com/anchore/grype/grype/version\"\n)\n\nvar timeVal = time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC)\nvar listing = provider.File{\n\tPath:      \"some\",\n\tDigest:    \"123456\",\n\tAlgorithm: \"sha256\",\n}\n\nfunc inputProviderState() provider.State {\n\treturn provider.State{\n\t\tProvider:  \"openvex\",\n\t\tVersion:   1,\n\t\tProcessor: \"vunnel@1.2.3\",\n\t\tTimestamp: timeVal,\n\t\tListing:   &listing,\n\t}\n}\n\nfunc TestOpenVEXTransform(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\tstate   provider.State\n\t\tvuln    unmarshal.OpenVEXVulnerability\n\t\twantErr bool\n\t\twant    transformers.RelatedEntries\n\t}{\n\t\t{\n\t\t\tname:  \"basic OpenVEX vulnerability with single product\",\n\t\t\tstate: inputProviderState(),\n\t\t\tvuln: govex.Statement{\n\t\t\t\tID: \"test-transform-1\",\n\t\t\t\tProducts: []govex.Product{\n\t\t\t\t\t{\n\t\t\t\t\t\tComponent: govex.Component{\n\t\t\t\t\t\t\tIdentifiers: map[govex.IdentifierType]string{\n\t\t\t\t\t\t\t\tgovex.PURL: \"pkg:pypi/urllib3@1.26.16\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tStatus: govex.StatusAffected,\n\t\t\t\tVulnerability: govex.Vulnerability{\n\t\t\t\t\tName:        \"cve-2023-43804\",\n\t\t\t\t\tDescription: \"urllib3 HTTP Request Smuggling vulnerability\",\n\t\t\t\t\tAliases:     []govex.VulnerabilityID{\"ghsa-v845-jxx5-vc9f\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: transformers.RelatedEntries{\n\t\t\t\tVulnerabilityHandle: &db.VulnerabilityHandle{\n\t\t\t\t\tName:       \"cve-2023-43804\",\n\t\t\t\t\tStatus:     db.VulnerabilityActive,\n\t\t\t\t\tProviderID: \"openvex\",\n\t\t\t\t\tProvider: &db.Provider{\n\t\t\t\t\t\tID:           \"openvex\",\n\t\t\t\t\t\tVersion:      \"1\",\n\t\t\t\t\t\tProcessor:    \"vunnel@1.2.3\",\n\t\t\t\t\t\tDateCaptured: &timeVal,\n\t\t\t\t\t\tInputDigest:  \"sha256:123456\",\n\t\t\t\t\t},\n\t\t\t\t\tBlobValue: &db.VulnerabilityBlob{\n\t\t\t\t\t\tID:          \"cve-2023-43804\",\n\t\t\t\t\t\tDescription: \"urllib3 HTTP Request Smuggling vulnerability\",\n\t\t\t\t\t\tReferences: []db.Reference{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tURL: \"cve-2023-43804\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tAliases: []string{\"ghsa-v845-jxx5-vc9f\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tRelated: []any{\n\t\t\t\t\tdb.AffectedPackageHandle{\n\t\t\t\t\t\tPackage: &db.Package{\n\t\t\t\t\t\t\t// convert pypi -> python\n\t\t\t\t\t\t\tEcosystem: \"python\",\n\t\t\t\t\t\t\tName:      \"urllib3\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\t\t\tCVEs: []string{\"cve-2023-43804\", \"ghsa-v845-jxx5-vc9f\"},\n\t\t\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tVersion: db.Version{\n\t\t\t\t\t\t\t\t\t\tType:       version.PythonFormat.String(),\n\t\t\t\t\t\t\t\t\t\tConstraint: \"= 1.26.16\",\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"OpenVEX vulnerability with multiple products\",\n\t\t\tstate: inputProviderState(),\n\t\t\tvuln: govex.Statement{\n\t\t\t\tID: \"test-transform-2\",\n\t\t\t\tProducts: []govex.Product{\n\t\t\t\t\t{\n\t\t\t\t\t\tComponent: govex.Component{\n\t\t\t\t\t\t\tIdentifiers: map[govex.IdentifierType]string{\n\t\t\t\t\t\t\t\tgovex.PURL: \"pkg:pypi/urllib3@1.26.16\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tComponent: govex.Component{\n\t\t\t\t\t\t\tIdentifiers: map[govex.IdentifierType]string{\n\t\t\t\t\t\t\t\tgovex.PURL: \"pkg:npm/express@4.18.2\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tStatus: govex.StatusNotAffected, // important! this means the versions will not be attributed to fixes\n\t\t\t\tVulnerability: govex.Vulnerability{\n\t\t\t\t\tName:        \"cve-2023-43804\",\n\t\t\t\t\tDescription: \"Test vulnerability affecting multiple packages\",\n\t\t\t\t\tAliases:     []govex.VulnerabilityID{\"ghsa-v845-jxx5-vc9f\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: transformers.RelatedEntries{\n\t\t\t\tVulnerabilityHandle: &db.VulnerabilityHandle{\n\t\t\t\t\tName:       \"cve-2023-43804\",\n\t\t\t\t\tStatus:     db.VulnerabilityActive,\n\t\t\t\t\tProviderID: \"openvex\",\n\t\t\t\t\tProvider: &db.Provider{\n\t\t\t\t\t\tID:           \"openvex\",\n\t\t\t\t\t\tVersion:      \"1\",\n\t\t\t\t\t\tProcessor:    \"vunnel@1.2.3\",\n\t\t\t\t\t\tDateCaptured: &timeVal,\n\t\t\t\t\t\tInputDigest:  \"sha256:123456\",\n\t\t\t\t\t},\n\t\t\t\t\tBlobValue: &db.VulnerabilityBlob{\n\t\t\t\t\t\tID:          \"cve-2023-43804\",\n\t\t\t\t\t\tDescription: \"Test vulnerability affecting multiple packages\",\n\t\t\t\t\t\tReferences: []db.Reference{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tURL: \"cve-2023-43804\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tAliases: []string{\"ghsa-v845-jxx5-vc9f\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tRelated: []any{\n\t\t\t\t\tdb.UnaffectedPackageHandle{\n\t\t\t\t\t\tPackage: &db.Package{\n\t\t\t\t\t\t\tEcosystem: \"npm\",\n\t\t\t\t\t\t\tName:      \"express\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\t\t\tCVEs: []string{\"cve-2023-43804\", \"ghsa-v845-jxx5-vc9f\"},\n\t\t\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tVersion: db.Version{\n\t\t\t\t\t\t\t\t\t\tType:       version.SemanticFormat.String(),\n\t\t\t\t\t\t\t\t\t\tConstraint: \"= 4.18.2\",\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\tFix: &db.Fix{\n\t\t\t\t\t\t\t\t\t\tState: db.NotAffectedFixStatus,\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tdb.UnaffectedPackageHandle{\n\t\t\t\t\t\tPackage: &db.Package{\n\t\t\t\t\t\t\t// convert pypi -> python\n\t\t\t\t\t\t\tEcosystem: \"python\",\n\t\t\t\t\t\t\tName:      \"urllib3\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\t\t\tCVEs: []string{\"cve-2023-43804\", \"ghsa-v845-jxx5-vc9f\"},\n\t\t\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tVersion: db.Version{\n\t\t\t\t\t\t\t\t\t\tType:       version.PythonFormat.String(),\n\t\t\t\t\t\t\t\t\t\tConstraint: \"= 1.26.16\",\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\tFix: &db.Fix{\n\t\t\t\t\t\t\t\t\t\tState: db.NotAffectedFixStatus,\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"OpenVEX vulnerability with no products\",\n\t\t\tstate: inputProviderState(),\n\t\t\tvuln: govex.Statement{\n\t\t\t\tID:       \"test-transform-3\",\n\t\t\t\tProducts: []govex.Product{},\n\t\t\t\tStatus:   govex.StatusNotAffected,\n\t\t\t\tVulnerability: govex.Vulnerability{\n\t\t\t\t\tName:        \"cve-2023-43804\",\n\t\t\t\t\tDescription: \"Test vulnerability with no products\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: transformers.RelatedEntries{\n\t\t\t\tVulnerabilityHandle: &db.VulnerabilityHandle{\n\t\t\t\t\tName:       \"cve-2023-43804\",\n\t\t\t\t\tStatus:     db.VulnerabilityActive,\n\t\t\t\t\tProviderID: \"openvex\",\n\t\t\t\t\tProvider: &db.Provider{\n\t\t\t\t\t\tID:           \"openvex\",\n\t\t\t\t\t\tVersion:      \"1\",\n\t\t\t\t\t\tProcessor:    \"vunnel@1.2.3\",\n\t\t\t\t\t\tDateCaptured: &timeVal,\n\t\t\t\t\t\tInputDigest:  \"sha256:123456\",\n\t\t\t\t\t},\n\t\t\t\t\tBlobValue: &db.VulnerabilityBlob{\n\t\t\t\t\t\tID:          \"cve-2023-43804\",\n\t\t\t\t\t\tDescription: \"Test vulnerability with no products\",\n\t\t\t\t\t\tReferences: []db.Reference{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tURL: \"cve-2023-43804\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tAliases: []string{},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"OpenVEX vulnerability with invalid product purl\",\n\t\t\tstate: inputProviderState(),\n\t\t\tvuln: govex.Statement{\n\t\t\t\tID: \"test-transform-4\",\n\t\t\t\tProducts: []govex.Product{\n\t\t\t\t\t{\n\t\t\t\t\t\tComponent: govex.Component{\n\t\t\t\t\t\t\tIdentifiers: map[govex.IdentifierType]string{\n\t\t\t\t\t\t\t\tgovex.PURL: \"invalid-purl\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tStatus: govex.StatusAffected,\n\t\t\t\tVulnerability: govex.Vulnerability{\n\t\t\t\t\tName:        \"cve-2023-43804\",\n\t\t\t\t\tDescription: \"Test vulnerability with invalid purl\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twantErr: true,\n\t\t},\n\t\t{\n\t\t\tname:  \"OpenVEX vulnerability with empty product\",\n\t\t\tstate: inputProviderState(),\n\t\t\tvuln: govex.Statement{\n\t\t\t\tID:       \"test-transform-4\",\n\t\t\t\tProducts: []govex.Product{{}},\n\t\t\t\tStatus:   govex.StatusAffected,\n\t\t\t\tVulnerability: govex.Vulnerability{\n\t\t\t\t\tName:        \"cve-2023-43804\",\n\t\t\t\t\tDescription: \"Test vulnerability with invalid purl\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twantErr: true,\n\t\t},\n\t}\n\n\tt.Parallel()\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := Transform(tt.vuln, tt.state)\n\t\t\tif tt.wantErr {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Len(t, got, 1, \"should return exactly one RelatedEntries\")\n\n\t\t\te, ok := got[0].Data.(transformers.RelatedEntries)\n\t\t\trequire.True(t, ok)\n\t\t\tif diff := cmp.Diff(tt.want, e); diff != \"\" {\n\t\t\t\tt.Errorf(\"data entries mismatch (-want +got):\\n%s\", diff)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_GetPackageHandles(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\tvuln    unmarshal.OpenVEXVulnerability\n\t\tfixes   []unmarshal.AnnotatedOpenVEXFix\n\t\twant    []any\n\t\twantErr bool\n\t}{\n\t\t{\n\t\t\tname: \"empty products returns nil\",\n\t\t\tvuln: govex.Statement{\n\t\t\t\tID:       \"test-vuln-1\",\n\t\t\t\tProducts: []govex.Product{},\n\t\t\t\tStatus:   govex.StatusAffected,\n\t\t\t},\n\t\t\twant:    nil,\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"single affected product\",\n\t\t\tvuln: govex.Statement{\n\t\t\t\tID: \"test-vuln-2\",\n\t\t\t\tProducts: []govex.Product{\n\t\t\t\t\t{\n\t\t\t\t\t\tComponent: govex.Component{\n\t\t\t\t\t\t\tIdentifiers: map[govex.IdentifierType]string{\n\t\t\t\t\t\t\t\tgovex.PURL: \"pkg:pypi/urllib3@1.26.16\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tStatus: govex.StatusAffected,\n\t\t\t\tVulnerability: govex.Vulnerability{\n\t\t\t\t\tName:    \"cve-2023-43804\",\n\t\t\t\t\tAliases: []govex.VulnerabilityID{\"ghsa-v845-jxx5-vc9f\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: []any{\n\t\t\t\tdb.AffectedPackageHandle{\n\t\t\t\t\tPackage: &db.Package{\n\t\t\t\t\t\t// converts pypi -> python\n\t\t\t\t\t\tEcosystem: \"python\",\n\t\t\t\t\t\tName:      \"urllib3\",\n\t\t\t\t\t},\n\t\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\t\tCVEs: []string{\"cve-2023-43804\", \"ghsa-v845-jxx5-vc9f\"},\n\t\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tVersion: db.Version{\n\t\t\t\t\t\t\t\t\tType:       version.PythonFormat.String(),\n\t\t\t\t\t\t\t\t\tConstraint: \"= 1.26.16\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"single product with fix\",\n\t\t\tvuln: govex.Statement{\n\t\t\t\tID: \"test-vuln-2\",\n\t\t\t\tProducts: []govex.Product{\n\t\t\t\t\t{\n\t\t\t\t\t\tComponent: govex.Component{\n\t\t\t\t\t\t\tIdentifiers: map[govex.IdentifierType]string{\n\t\t\t\t\t\t\t\tgovex.PURL: \"pkg:pypi/urllib3@1.26.16\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tStatus: govex.StatusFixed,\n\t\t\t\tVulnerability: govex.Vulnerability{\n\t\t\t\t\tName:    \"cve-2023-43804\",\n\t\t\t\t\tAliases: []govex.VulnerabilityID{\"ghsa-v845-jxx5-vc9f\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tfixes: []unmarshal.AnnotatedOpenVEXFix{\n\t\t\t\t{\n\t\t\t\t\tAvailable: unmarshal.AnnotatedOpenVEXFixAvailability{\n\t\t\t\t\t\tDate: \"2025-01-01\",\n\t\t\t\t\t\tKind: \"advisory\",\n\t\t\t\t\t},\n\t\t\t\t\tProduct: \"pkg:pypi/urllib3@1.26.16\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: []any{\n\t\t\t\tdb.UnaffectedPackageHandle{\n\t\t\t\t\tPackage: &db.Package{\n\t\t\t\t\t\t// converts pypi -> python\n\t\t\t\t\t\tEcosystem: \"python\",\n\t\t\t\t\t\tName:      \"urllib3\",\n\t\t\t\t\t},\n\t\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\t\tCVEs: []string{\"cve-2023-43804\", \"ghsa-v845-jxx5-vc9f\"},\n\t\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tVersion: db.Version{\n\t\t\t\t\t\t\t\t\tType:       version.PythonFormat.String(),\n\t\t\t\t\t\t\t\t\tConstraint: \"= 1.26.16\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tFix: &db.Fix{\n\t\t\t\t\t\t\t\t\tVersion: \"1.26.16\",\n\t\t\t\t\t\t\t\t\tState:   db.FixedStatus,\n\t\t\t\t\t\t\t\t\tDetail: &db.FixDetail{\n\t\t\t\t\t\t\t\t\t\tAvailable: &db.FixAvailability{\n\t\t\t\t\t\t\t\t\t\t\tDate: internal.ParseTime(\"2025-01-01\"),\n\t\t\t\t\t\t\t\t\t\t\tKind: \"advisory\",\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"don't include fix detail for a mismatched status\",\n\t\t\tvuln: govex.Statement{\n\t\t\t\tID: \"test-vuln-2\",\n\t\t\t\tProducts: []govex.Product{\n\t\t\t\t\t{\n\t\t\t\t\t\tComponent: govex.Component{\n\t\t\t\t\t\t\tIdentifiers: map[govex.IdentifierType]string{\n\t\t\t\t\t\t\t\tgovex.PURL: \"pkg:pypi/urllib3@1.26.16\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tStatus: govex.StatusNotAffected, // important! not-affected != fixed\n\t\t\t\tVulnerability: govex.Vulnerability{\n\t\t\t\t\tName:    \"cve-2023-43804\",\n\t\t\t\t\tAliases: []govex.VulnerabilityID{\"ghsa-v845-jxx5-vc9f\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tfixes: []unmarshal.AnnotatedOpenVEXFix{ // important! there is a fix, but the status is not \"fixed\", so this should never be associated with the package\n\t\t\t\t{\n\t\t\t\t\tAvailable: unmarshal.AnnotatedOpenVEXFixAvailability{\n\t\t\t\t\t\tDate: \"2025-01-01\",\n\t\t\t\t\t\tKind: \"advisory\",\n\t\t\t\t\t},\n\t\t\t\t\tProduct: \"pkg:pypi/urllib3@1.26.16\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: []any{\n\t\t\t\tdb.UnaffectedPackageHandle{\n\t\t\t\t\tPackage: &db.Package{\n\t\t\t\t\t\t// converts pypi -> python\n\t\t\t\t\t\tEcosystem: \"python\",\n\t\t\t\t\t\tName:      \"urllib3\",\n\t\t\t\t\t},\n\t\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\t\tCVEs: []string{\"cve-2023-43804\", \"ghsa-v845-jxx5-vc9f\"},\n\t\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tVersion: db.Version{\n\t\t\t\t\t\t\t\t\tType:       version.PythonFormat.String(),\n\t\t\t\t\t\t\t\t\tConstraint: \"= 1.26.16\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tFix: &db.Fix{\n\t\t\t\t\t\t\t\t\tVersion: \"\", // important! no version because the status is not \"fixed\"\n\t\t\t\t\t\t\t\t\tState:   db.NotAffectedFixStatus,\n\t\t\t\t\t\t\t\t\tDetail:  nil, // important! no detail because the status is not \"fixed\"\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"multiple products sorted alphabetically\",\n\t\t\tvuln: govex.Statement{\n\t\t\t\tID: \"test-vuln-3\",\n\t\t\t\tProducts: []govex.Product{\n\t\t\t\t\t{\n\t\t\t\t\t\tComponent: govex.Component{\n\t\t\t\t\t\t\tIdentifiers: map[govex.IdentifierType]string{\n\t\t\t\t\t\t\t\tgovex.PURL: \"pkg:pypi/urllib3@1.26.16\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tComponent: govex.Component{\n\t\t\t\t\t\t\tIdentifiers: map[govex.IdentifierType]string{\n\t\t\t\t\t\t\t\tgovex.PURL: \"pkg:npm/express@4.18.2\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tStatus: govex.StatusNotAffected,\n\t\t\t\tVulnerability: govex.Vulnerability{\n\t\t\t\t\tName:    \"cve-2023-43804\",\n\t\t\t\t\tAliases: []govex.VulnerabilityID{\"ghsa-v845-jxx5-vc9f\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: []any{\n\t\t\t\tdb.UnaffectedPackageHandle{\n\t\t\t\t\tPackage: &db.Package{\n\t\t\t\t\t\tEcosystem: \"npm\",\n\t\t\t\t\t\tName:      \"express\",\n\t\t\t\t\t},\n\t\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\t\tCVEs: []string{\"cve-2023-43804\", \"ghsa-v845-jxx5-vc9f\"},\n\t\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tVersion: db.Version{\n\t\t\t\t\t\t\t\t\tType:       version.SemanticFormat.String(),\n\t\t\t\t\t\t\t\t\tConstraint: \"= 4.18.2\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tFix: &db.Fix{\n\t\t\t\t\t\t\t\t\tState: db.NotAffectedFixStatus,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tdb.UnaffectedPackageHandle{\n\t\t\t\t\tPackage: &db.Package{\n\t\t\t\t\t\t// converts pypi -> python\n\t\t\t\t\t\tEcosystem: \"python\",\n\t\t\t\t\t\tName:      \"urllib3\",\n\t\t\t\t\t},\n\t\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\t\tCVEs: []string{\"cve-2023-43804\", \"ghsa-v845-jxx5-vc9f\"},\n\t\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tVersion: db.Version{\n\t\t\t\t\t\t\t\t\tType:       version.PythonFormat.String(),\n\t\t\t\t\t\t\t\t\tConstraint: \"= 1.26.16\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tFix: &db.Fix{\n\t\t\t\t\t\t\t\t\tState: db.NotAffectedFixStatus,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"fixed status product\",\n\t\t\tvuln: govex.Statement{\n\t\t\t\tID: \"test-vuln-4\",\n\t\t\t\tProducts: []govex.Product{\n\t\t\t\t\t{\n\t\t\t\t\t\tComponent: govex.Component{\n\t\t\t\t\t\t\tIdentifiers: map[govex.IdentifierType]string{\n\t\t\t\t\t\t\t\tgovex.PURL: \"pkg:pypi/urllib3@2.0.7\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tStatus: govex.StatusFixed,\n\t\t\t\tVulnerability: govex.Vulnerability{\n\t\t\t\t\tName:    \"cve-2023-43804\",\n\t\t\t\t\tAliases: []govex.VulnerabilityID{\"ghsa-v845-jxx5-vc9f\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: []any{\n\t\t\t\tdb.UnaffectedPackageHandle{\n\t\t\t\t\tPackage: &db.Package{\n\t\t\t\t\t\t// converts pypi -> python\n\t\t\t\t\t\tEcosystem: \"python\",\n\t\t\t\t\t\tName:      \"urllib3\",\n\t\t\t\t\t},\n\t\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\t\tCVEs: []string{\"cve-2023-43804\", \"ghsa-v845-jxx5-vc9f\"},\n\t\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tVersion: db.Version{\n\t\t\t\t\t\t\t\t\tType:       version.PythonFormat.String(),\n\t\t\t\t\t\t\t\t\tConstraint: \"= 2.0.7\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tFix: &db.Fix{\n\t\t\t\t\t\t\t\t\tVersion: \"2.0.7\",\n\t\t\t\t\t\t\t\t\tState:   db.FixedStatus,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"invalid purl returns error\",\n\t\t\tvuln: govex.Statement{\n\t\t\t\tID: \"test-vuln-5\",\n\t\t\t\tProducts: []govex.Product{\n\t\t\t\t\t{\n\t\t\t\t\t\tComponent: govex.Component{\n\t\t\t\t\t\t\tIdentifiers: map[govex.IdentifierType]string{\n\t\t\t\t\t\t\t\tgovex.PURL: \"invalid-purl\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tStatus: govex.StatusAffected,\n\t\t\t},\n\t\t\twant:    nil,\n\t\t\twantErr: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := getPackageHandles(&tt.vuln, tt.fixes)\n\t\t\tif tt.wantErr {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\trequire.NoError(t, err)\n\t\t\tif diff := cmp.Diff(tt.want, got); diff != \"\" {\n\t\t\t\tt.Errorf(\"GetPackages() mismatch (-want +got):\\n%s\", diff)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/db/v6/build/transformers/os/testdata/alpine-3.9.json",
    "content": "[\n  {\n    \"Vulnerability\": {\n      \"CVSS\": [],\n      \"Description\": \"\",\n      \"FixedIn\": [\n        {\n          \"Name\": \"xen\",\n          \"NamespaceName\": \"alpine:3.9\",\n          \"Version\": \"4.11.1-r0\",\n          \"VersionFormat\": \"apk\",\n          \"Available\": {\n            \"Date\": \"2018-12-01T09:15:30Z\",\n            \"Kind\": \"package\"\n          }\n        }\n      ],\n      \"Link\": \"http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-19967\",\n      \"Metadata\": {\n        \"NVD\": {\n          \"CVSSv2\": {\n            \"Score\": 4.9,\n            \"Vectors\": \"AV:L/AC:L/Au:N/C:N/I:N/A:C\"\n          }\n        }\n      },\n      \"Name\": \"CVE-2018-19967\",\n      \"NamespaceName\": \"alpine:3.9\",\n      \"Severity\": \"Medium\"\n    }\n  }\n]"
  },
  {
    "path": "grype/db/v6/build/transformers/os/testdata/amazon-multiple-kernel-advisories.json",
    "content": "[\n  {\n    \"Vulnerability\": {\n      \"Name\": \"ALAS-2021-1704\",\n      \"NamespaceName\": \"amzn:2\",\n      \"Description\": \"\",\n      \"Severity\": \"Medium\",\n      \"Metadata\": {\n        \"CVE\": [\n          {\n            \"Name\": \"CVE-2021-3653\"\n          },\n          {\n            \"Name\": \"CVE-2021-3656\"\n          },\n          {\n            \"Name\": \"CVE-2021-3732\"\n          }\n        ]\n      },\n      \"Link\": \"https://alas.aws.amazon.com/AL2/ALAS-2021-1704.html\",\n      \"FixedIn\": [\n        {\n          \"Name\": \"kernel-headers\",\n          \"NamespaceName\": \"amzn:2\",\n          \"VersionFormat\": \"rpm\",\n          \"Version\": \"4.14.246-187.474.amzn2\"\n        },\n        {\n          \"Name\": \"kernel\",\n          \"NamespaceName\": \"amzn:2\",\n          \"VersionFormat\": \"rpm\",\n          \"Version\": \"4.14.246-187.474.amzn2\"\n        }\n      ]\n    }\n  },\n  {\n    \"Vulnerability\": {\n      \"Name\": \"ALASKERNEL-5.4-2022-007\",\n      \"NamespaceName\": \"amzn:2\",\n      \"Description\": \"\",\n      \"Severity\": \"Medium\",\n      \"Metadata\": {\n        \"CVE\": [\n          {\n            \"Name\": \"CVE-2021-3753\"\n          },\n          {\n            \"Name\": \"CVE-2021-40490\"\n          }\n        ]\n      },\n      \"Link\": \"https://alas.aws.amazon.com/AL2/ALASKERNEL-5.4-2022-007.html\",\n      \"FixedIn\": [\n        {\n          \"Name\": \"kernel-headers\",\n          \"NamespaceName\": \"amzn:2\",\n          \"VersionFormat\": \"rpm\",\n          \"Version\": \"5.4.144-69.257.amzn2\"\n        },\n        {\n          \"Name\": \"kernel\",\n          \"NamespaceName\": \"amzn:2\",\n          \"VersionFormat\": \"rpm\",\n          \"Version\": \"5.4.144-69.257.amzn2\"\n        }\n      ]\n    }\n  },\n  {\n    \"Vulnerability\": {\n      \"Name\": \"ALASKERNEL-5.10-2022-005\",\n      \"NamespaceName\": \"amzn:2\",\n      \"Description\": \"\",\n      \"Severity\": \"Medium\",\n      \"Metadata\": {\n        \"CVE\": [\n          {\n            \"Name\": \"CVE-2021-3753\"\n          },\n          {\n            \"Name\": \"CVE-2021-40490\"\n          }\n        ]\n      },\n      \"Link\": \"https://alas.aws.amazon.com/AL2/ALASKERNEL-5.10-2022-005.html\",\n      \"FixedIn\": [\n        {\n          \"Name\": \"kernel-headers\",\n          \"NamespaceName\": \"amzn:2\",\n          \"VersionFormat\": \"rpm\",\n          \"Version\": \"5.10.62-55.141.amzn2\"\n        },\n        {\n          \"Name\": \"kernel\",\n          \"NamespaceName\": \"amzn:2\",\n          \"VersionFormat\": \"rpm\",\n          \"Version\": \"5.10.62-55.141.amzn2\"\n        }\n      ]\n    }\n  }\n]"
  },
  {
    "path": "grype/db/v6/build/transformers/os/testdata/amzn.json",
    "content": "[\n  {\n    \"Vulnerability\": {\n      \"Description\": \"\",\n      \"FixedIn\": [\n        {\n          \"Name\": \"389-ds-base\",\n          \"NamespaceName\": \"amzn:2\",\n          \"Version\": \"1.3.8.4-15.amzn2.0.1\",\n          \"VersionFormat\": \"rpm\"\n        },\n        {\n          \"Name\": \"389-ds-base-debuginfo\",\n          \"NamespaceName\": \"amzn:2\",\n          \"Version\": \"1.3.8.4-15.amzn2.0.1\",\n          \"VersionFormat\": \"rpm\"\n        },\n        {\n          \"Name\": \"389-ds-base-devel\",\n          \"NamespaceName\": \"amzn:2\",\n          \"Version\": \"1.3.8.4-15.amzn2.0.1\",\n          \"VersionFormat\": \"rpm\"\n        },\n        {\n          \"Name\": \"389-ds-base-libs\",\n          \"NamespaceName\": \"amzn:2\",\n          \"Version\": \"1.3.8.4-15.amzn2.0.1\",\n          \"VersionFormat\": \"rpm\"\n        },\n        {\n          \"Name\": \"389-ds-base-snmp\",\n          \"NamespaceName\": \"amzn:2\",\n          \"Version\": \"1.3.8.4-15.amzn2.0.1\",\n          \"VersionFormat\": \"rpm\"\n        }\n      ],\n      \"Link\": \"https://alas.aws.amazon.com/AL2/ALAS-2018-1106.html\",\n      \"Metadata\": {\n        \"CVE\": [\n          {\n            \"Name\": \"CVE-2018-14648\"\n          }\n        ]\n      },\n      \"Name\": \"ALAS-2018-1106\",\n      \"NamespaceName\": \"amzn:2\",\n      \"Severity\": \"Medium\"\n    }\n  }\n]"
  },
  {
    "path": "grype/db/v6/build/transformers/os/testdata/azure-linux-3.json",
    "content": "[\n  {\n    \"Vulnerability\": {\n      \"Name\": \"CVE-2023-29403\",\n      \"NamespaceName\": \"mariner:3.0\",\n      \"Description\": \"CVE-2023-29403 affecting package golang for versions less than 1.20.7-1. A patched version of the package is available.\",\n      \"Severity\": \"High\",\n      \"Link\": \"https://nvd.nist.gov/vuln/detail/CVE-2023-29403\",\n      \"CVSS\": [],\n      \"FixedIn\": [\n        {\n          \"Name\": \"golang\",\n          \"NamespaceName\": \"mariner:3.0\",\n          \"VersionFormat\": \"rpm\",\n          \"Version\": \"0:1.20.7-1.azl3\",\n          \"Module\": \"\",\n          \"VendorAdvisory\": {\n            \"NoAdvisory\": false,\n            \"AdvisorySummary\": []\n          }\n        }\n      ],\n      \"Metadata\": {}\n    }\n  }\n]\n"
  },
  {
    "path": "grype/db/v6/build/transformers/os/testdata/debian-8-multiple-entries-for-same-package.json",
    "content": "[\n  {\n    \"Vulnerability\": {\n      \"CVSS\": [],\n      \"Description\": \"\",\n      \"FixedIn\": [\n        {\n          \"Name\": \"rsyslog\",\n          \"NamespaceName\": \"debian:8\",\n          \"VendorAdvisory\": {\n            \"AdvisorySummary\": [],\n            \"NoAdvisory\": false\n          },\n          \"Version\": \"5.7.4-1\",\n          \"VersionFormat\": \"dpkg\"\n        }\n      ],\n      \"Link\": \"https://security-tracker.debian.org/tracker/CVE-2011-4623\",\n      \"Metadata\": {\n        \"NVD\": {\n          \"CVSSv2\": {\n            \"Score\": 2.1,\n            \"Vectors\": \"AV:L/AC:L/Au:N/C:N/I:N/A:P\"\n          }\n        }\n      },\n      \"Name\": \"CVE-2011-4623\",\n      \"NamespaceName\": \"debian:8\",\n      \"Severity\": \"Low\"\n    }\n  },\n  {\n    \"Vulnerability\": {\n      \"CVSS\": [],\n      \"Description\": \"\",\n      \"FixedIn\": [\n        {\n          \"Name\": \"rsyslog\",\n          \"NamespaceName\": \"debian:8\",\n          \"VendorAdvisory\": {\n            \"AdvisorySummary\": [],\n            \"NoAdvisory\": false\n          },\n          \"Version\": \"3.18.6-1\",\n          \"VersionFormat\": \"dpkg\"\n        }\n      ],\n      \"Link\": \"https://security-tracker.debian.org/tracker/CVE-2008-5618\",\n      \"Metadata\": {\n        \"NVD\": {\n          \"CVSSv2\": {\n            \"Score\": 5,\n            \"Vectors\": \"AV:N/AC:L/Au:N/C:N/I:N/A:P\"\n          }\n        }\n      },\n      \"Name\": \"CVE-2008-5618\",\n      \"NamespaceName\": \"debian:8\",\n      \"Severity\": \"Low\"\n    }\n  }\n]"
  },
  {
    "path": "grype/db/v6/build/transformers/os/testdata/debian-8.json",
    "content": "[\n  {\n    \"Vulnerability\": {\n      \"CVSS\": [],\n      \"Description\": \"\",\n      \"FixedIn\": [\n        {\n          \"Name\": \"asterisk\",\n          \"NamespaceName\": \"debian:8\",\n          \"VendorAdvisory\": {\n            \"AdvisorySummary\": [],\n            \"NoAdvisory\": false\n          },\n          \"Version\": \"1:1.6.2.0~rc3-1\",\n          \"VersionFormat\": \"dpkg\"\n        },\n        {\n          \"Name\": \"auth2db\",\n          \"NamespaceName\": \"debian:8\",\n          \"VendorAdvisory\": {\n            \"AdvisorySummary\": [],\n            \"NoAdvisory\": false\n          },\n          \"Version\": \"0.2.5-2+dfsg-1\",\n          \"VersionFormat\": \"dpkg\"\n        },\n        {\n          \"Name\": \"exaile\",\n          \"NamespaceName\": \"debian:8\",\n          \"VendorAdvisory\": {\n            \"AdvisorySummary\": [],\n            \"NoAdvisory\": false\n          },\n          \"Version\": \"0.2.14+debian-2.2\",\n          \"VersionFormat\": \"dpkg\"\n        },\n        {\n          \"Name\": \"wordpress\",\n          \"NamespaceName\": \"debian:8\",\n          \"VendorAdvisory\": {\n            \"AdvisorySummary\": [],\n            \"NoAdvisory\": false\n          },\n          \"Version\": \"\",\n          \"VersionFormat\": \"dpkg\"\n        }\n      ],\n      \"Link\": \"https://security-tracker.debian.org/tracker/CVE-2008-7220\",\n      \"Metadata\": {\n        \"NVD\": {\n          \"CVSSv2\": {\n            \"Score\": 7.5,\n            \"Vectors\": \"AV:N/AC:L/Au:N/C:P/I:P/A:P\"\n          }\n        }\n      },\n      \"Name\": \"CVE-2008-7220\",\n      \"NamespaceName\": \"debian:8\",\n      \"Severity\": \"High\"\n    }\n  }\n]"
  },
  {
    "path": "grype/db/v6/build/transformers/os/testdata/fedora-39.json",
    "content": "[\n  {\n    \"Vulnerability\": {\n      \"CVSS\": [],\n      \"Description\": \"Security update for glib2 to fix CVE-2024-34397\",\n      \"FixedIn\": [\n        {\n          \"Name\": \"glib2\",\n          \"NamespaceName\": \"fedora:39\",\n          \"VendorAdvisory\": {\n            \"AdvisorySummary\": [\n              {\n                \"ID\": \"FEDORA-2024-fd2569c4e9\",\n                \"Link\": \"https://bodhi.fedoraproject.org/updates/FEDORA-2024-fd2569c4e9\"\n              }\n            ],\n            \"NoAdvisory\": false\n          },\n          \"Version\": \"0:2.78.6-1.fc39\",\n          \"VersionFormat\": \"rpm\"\n        }\n      ],\n      \"Link\": \"https://bodhi.fedoraproject.org/updates/FEDORA-2024-fd2569c4e9\",\n      \"Metadata\": {\n        \"Issued\": \"2024-05-09T02:43:30Z\",\n        \"Updated\": \"2024-05-14T03:27:20Z\"\n      },\n      \"Name\": \"CVE-2024-34397\",\n      \"NamespaceName\": \"fedora:39\",\n      \"Severity\": \"High\"\n    }\n  }\n]\n"
  },
  {
    "path": "grype/db/v6/build/transformers/os/testdata/mariner-20.json",
    "content": "[\n  {\n    \"Vulnerability\": {\n      \"Name\": \"CVE-2021-37621\",\n      \"NamespaceName\": \"mariner:2.0\",\n      \"Description\": \"CVE-2021-37621 affecting package exiv2 for versions less than 0.27.5-1. An upgraded version of the package is available that resolves this issue.\",\n      \"Severity\": \"Medium\",\n      \"Link\": \"https://nvd.nist.gov/vuln/detail/CVE-2021-37621\",\n      \"CVSS\": [],\n      \"FixedIn\": [\n        {\n          \"Name\": \"exiv2\",\n          \"NamespaceName\": \"mariner:2.0\",\n          \"VersionFormat\": \"rpm\",\n          \"Version\": \"0:0.27.5-1.cm2\",\n          \"Module\": \"\",\n          \"VendorAdvisory\": {\n            \"NoAdvisory\": false,\n            \"AdvisorySummary\": []\n          }\n        }\n      ],\n      \"Metadata\": {}\n    }\n  }\n]\n"
  },
  {
    "path": "grype/db/v6/build/transformers/os/testdata/mariner-range.json",
    "content": "[\n  {\n      \"Vulnerability\": {\n        \"Name\": \"CVE-2023-29404\",\n        \"NamespaceName\": \"mariner:2.0\",\n        \"Description\": \"CVE-2023-29404 affecting package golang for versions less than 1.20.7-1. A patched version of the package is available.\",\n        \"Severity\": \"Critical\",\n        \"Link\": \"https://nvd.nist.gov/vuln/detail/CVE-2023-29404\",\n        \"CVSS\": [],\n        \"FixedIn\": [\n          {\n            \"Name\": \"golang\",\n            \"NamespaceName\": \"mariner:2.0\",\n            \"VersionFormat\": \"rpm\",\n            \"Version\": \"0:1.20.7-1.cm2\",\n            \"Module\": \"\",\n            \"VendorAdvisory\": {\n              \"NoAdvisory\": false,\n              \"AdvisorySummary\": []\n            },\n            \"VulnerableRange\": \"> 0:1.19.0.cm2, < 0:1.20.7-1.cm2\"\n          }\n        ],\n        \"Metadata\": {}\n      }\n    }\n]\n"
  },
  {
    "path": "grype/db/v6/build/transformers/os/testdata/ol-8-modules.json",
    "content": "[\n  {\n    \"Vulnerability\": {\n      \"CVSS\": [],\n      \"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.\",\n      \"FixedIn\": [\n        {\n          \"Module\": \"postgresql:10\",\n          \"Name\": \"postgresql\",\n          \"NamespaceName\": \"ol:8\",\n          \"Version\": \"0:10.14-1.module+el8.2.0+7801+be0fed80\",\n          \"VersionFormat\": \"rpm\"\n        },\n        {\n          \"Module\": \"postgresql:12\",\n          \"Name\": \"postgresql\",\n          \"NamespaceName\": \"ol:8\",\n          \"Version\": \"0:12.5-1.module+el8.3.0+9042+664538f4\",\n          \"VersionFormat\": \"rpm\"\n        },\n        {\n          \"Module\": \"postgresql:9.6\",\n          \"Name\": \"postgresql\",\n          \"NamespaceName\": \"ol:8\",\n          \"Version\": \"0:9.6.20-1.module+el8.3.0+8938+7f0e88b6\",\n          \"VersionFormat\": \"rpm\"\n        }\n      ],\n      \"Link\": \"https://access.redhat.com/security/cve/CVE-2020-14350\",\n      \"Metadata\": {},\n      \"Name\": \"CVE-2020-14350\",\n      \"NamespaceName\": \"ol:8\",\n      \"Severity\": \"Medium\"\n    }\n  }\n]"
  },
  {
    "path": "grype/db/v6/build/transformers/os/testdata/ol-8.json",
    "content": "[\n  {\n    \"Vulnerability\": {\n      \"CVSS\": [],\n      \"Description\": \"\",\n      \"FixedIn\": [\n        {\n          \"Name\": \"libexif\",\n          \"NamespaceName\": \"ol:8\",\n          \"Version\": \"0:0.6.21-17.el8_2\",\n          \"VersionFormat\": \"rpm\"\n        },\n        {\n          \"Name\": \"libexif-devel\",\n          \"NamespaceName\": \"ol:8\",\n          \"Version\": \"0:0.6.21-17.el8_2\",\n          \"VersionFormat\": \"rpm\"\n        },\n        {\n          \"Name\": \"libexif-dummy\",\n          \"NamespaceName\": \"ol:8\",\n          \"Version\": \"None\",\n          \"VersionFormat\": \"rpm\"\n        }\n      ],\n      \"Link\": \"http://linux.oracle.com/errata/ELSA-2020-2550.html\",\n      \"Metadata\": {\n        \"CVE\": [\n          {\n            \"Link\": \"http://linux.oracle.com/cve/CVE-2020-13112.html\",\n            \"Name\": \"CVE-2020-13112\"\n          }\n        ],\n        \"Issued\": \"2020-06-15\",\n        \"RefId\": \"ELSA-2020-2550\"\n      },\n      \"Name\": \"ELSA-2020-2550\",\n      \"NamespaceName\": \"ol:8\",\n      \"Severity\": \"Medium\"\n    }\n  }\n]"
  },
  {
    "path": "grype/db/v6/build/transformers/os/testdata/rhel-8-modules.json",
    "content": "[\n  {\n    \"Vulnerability\": {\n      \"CVSS\": [\n        {\n          \"base_metrics\": {\n            \"base_score\": 7.1,\n            \"base_severity\": \"High\",\n            \"exploitability_score\": 1.2,\n            \"impact_score\": 5.9\n          },\n          \"status\": \"verified\",\n          \"vector_string\": \"CVSS:3.1/AV:N/AC:H/PR:L/UI:R/S:U/C:H/I:H/A:H\",\n          \"version\": \"3.1\"\n        }\n      ],\n      \"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.\",\n      \"FixedIn\": [\n        {\n          \"Module\": \"postgresql:10\",\n          \"Name\": \"postgresql\",\n          \"NamespaceName\": \"rhel:8\",\n          \"VendorAdvisory\": {\n            \"AdvisorySummary\": [\n              {\n                \"ID\": \"RHSA-2020:3669\",\n                \"Link\": \"https://access.redhat.com/errata/RHSA-2020:3669\"\n              }\n            ],\n            \"NoAdvisory\": false\n          },\n          \"Version\": \"0:10.14-1.module+el8.2.0+7801+be0fed80\",\n          \"VersionFormat\": \"rpm\"\n        },\n        {\n          \"Module\": \"postgresql:12\",\n          \"Name\": \"postgresql\",\n          \"NamespaceName\": \"rhel:8\",\n          \"VendorAdvisory\": {\n            \"AdvisorySummary\": [\n              {\n                \"ID\": \"RHSA-2020:5620\",\n                \"Link\": \"https://access.redhat.com/errata/RHSA-2020:5620\"\n              }\n            ],\n            \"NoAdvisory\": false\n          },\n          \"Version\": \"0:12.5-1.module+el8.3.0+9042+664538f4\",\n          \"VersionFormat\": \"rpm\"\n        },\n        {\n          \"Module\": \"postgresql:9.6\",\n          \"Name\": \"postgresql\",\n          \"NamespaceName\": \"rhel:8\",\n          \"VendorAdvisory\": {\n            \"AdvisorySummary\": [\n              {\n                \"ID\": \"RHSA-2020:5619\",\n                \"Link\": \"https://access.redhat.com/errata/RHSA-2020:5619\"\n              }\n            ],\n            \"NoAdvisory\": false\n          },\n          \"Version\": \"0:9.6.20-1.module+el8.3.0+8938+7f0e88b6\",\n          \"VersionFormat\": \"rpm\"\n        }\n      ],\n      \"Link\": \"https://access.redhat.com/security/cve/CVE-2020-14350\",\n      \"Metadata\": {},\n      \"Name\": \"CVE-2020-14350\",\n      \"NamespaceName\": \"rhel:8\",\n      \"Severity\": \"Medium\"\n    }\n  }\n]"
  },
  {
    "path": "grype/db/v6/build/transformers/os/testdata/rhel-8.json",
    "content": "[\n  {\n    \"Vulnerability\": {\n      \"CVSS\": [\n        {\n          \"base_metrics\": {\n            \"base_score\": 8.8,\n            \"base_severity\": \"High\",\n            \"exploitability_score\": 2.8,\n            \"impact_score\": 5.9\n          },\n          \"status\": \"verified\",\n          \"vector_string\": \"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H\",\n          \"version\": \"3.1\"\n        }\n      ],\n      \"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.\",\n      \"FixedIn\": [\n        {\n          \"Name\": \"firefox\",\n          \"NamespaceName\": \"rhel:8\",\n          \"VendorAdvisory\": {\n            \"AdvisorySummary\": [\n              {\n                \"ID\": \"RHSA-2020:1341\",\n                \"Link\": \"https://access.redhat.com/errata/RHSA-2020:1341\"\n              }\n            ],\n            \"NoAdvisory\": false\n          },\n          \"Version\": \"0:68.6.1-1.el8_1\",\n          \"VersionFormat\": \"rpm\",\n          \"Available\": {\n            \"Date\": \"2020-04-08T14:30:15Z\",\n            \"Kind\": \"advisory\"\n          }\n        },\n        {\n          \"Name\": \"thunderbird\",\n          \"NamespaceName\": \"rhel:8\",\n          \"VendorAdvisory\": {\n            \"AdvisorySummary\": [\n              {\n                \"ID\": \"RHSA-2020:1495\",\n                \"Link\": \"https://access.redhat.com/errata/RHSA-2020:1495\"\n              }\n            ],\n            \"NoAdvisory\": false\n          },\n          \"Version\": \"0:68.7.0-1.el8_1\",\n          \"VersionFormat\": \"rpm\"\n        }\n      ],\n      \"Link\": \"https://access.redhat.com/security/cve/CVE-2020-6819\",\n      \"Metadata\": {},\n      \"Name\": \"CVE-2020-6819\",\n      \"NamespaceName\": \"rhel:8\",\n      \"Severity\": \"Critical\"\n    }\n  }\n]"
  },
  {
    "path": "grype/db/v6/build/transformers/os/transform.go",
    "content": "package os // nolint:revive\n\nimport (\n\t\"fmt\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/scylladb/go-set/strset\"\n\n\t\"github.com/anchore/grype/grype/db/data\"\n\t\"github.com/anchore/grype/grype/db/internal/codename\"\n\t\"github.com/anchore/grype/grype/db/internal/provider/unmarshal\"\n\t\"github.com/anchore/grype/grype/db/internal/versionutil\"\n\t\"github.com/anchore/grype/grype/db/provider\"\n\tdb \"github.com/anchore/grype/grype/db/v6\"\n\t\"github.com/anchore/grype/grype/db/v6/build/transformers\"\n\t\"github.com/anchore/grype/grype/db/v6/build/transformers/internal\"\n\t\"github.com/anchore/grype/grype/db/v6/name\"\n\t\"github.com/anchore/grype/grype/distro\"\n\t\"github.com/anchore/grype/internal/log\"\n\t\"github.com/anchore/syft/syft/pkg\"\n)\n\n// advisoryKey is an internal struct used for sorting and deduplicating advisories\n// that have both a link and ID from the vunnel results data\ntype advisoryKey struct {\n\tid   string\n\tlink string\n}\n\nfunc Transform(vulnerability unmarshal.OSVulnerability, state provider.State) ([]data.Entry, error) {\n\tin := []any{\n\t\tdb.VulnerabilityHandle{\n\t\t\tName:          vulnerability.Vulnerability.Name,\n\t\t\tProviderID:    state.Provider,\n\t\t\tProvider:      provider.Model(state),\n\t\t\tStatus:        db.VulnerabilityActive,\n\t\t\tModifiedDate:  internal.ParseTime(vulnerability.Vulnerability.Metadata.Updated),\n\t\t\tPublishedDate: internal.ParseTime(vulnerability.Vulnerability.Metadata.Issued),\n\t\t\tBlobValue: &db.VulnerabilityBlob{\n\t\t\t\tID:          vulnerability.Vulnerability.Name,\n\t\t\t\tAssigners:   nil,\n\t\t\t\tDescription: strings.TrimSpace(vulnerability.Vulnerability.Description),\n\t\t\t\tReferences:  getReferences(vulnerability),\n\t\t\t\tAliases:     getAliases(vulnerability),\n\t\t\t\tSeverities:  getSeverities(vulnerability),\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, a := range getAffectedPackages(vulnerability) {\n\t\tin = append(in, a)\n\t}\n\n\treturn transformers.NewEntries(in...), nil\n}\n\nfunc getAffectedPackages(vuln unmarshal.OSVulnerability) []db.AffectedPackageHandle {\n\tvar afs []db.AffectedPackageHandle\n\tgroups := groupFixedIns(vuln)\n\tfor group, fixedIns := range groups {\n\t\t// we only care about a single qualifier: rpm modules. The important thing to note about this is that\n\t\t// a package with no module vs a package with a module should be detectable in the DB.\n\t\tvar qualifiers *db.PackageQualifiers\n\t\tif group.format == \"rpm\" {\n\t\t\tmodule := \"\" // means the target package must have no module (where as nil means the module has no sway on matching)\n\t\t\tif group.hasModule {\n\t\t\t\tmodule = group.module\n\t\t\t}\n\t\t\tqualifiers = &db.PackageQualifiers{\n\t\t\t\tRpmModularity: &module,\n\t\t\t}\n\t\t}\n\n\t\taph := db.AffectedPackageHandle{\n\t\t\tOperatingSystem: getOperatingSystem(group.osName, group.id, group.osVersion, group.osChannel),\n\t\t\tPackage:         getPackage(group),\n\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\tCVEs:       getAliases(vuln),\n\t\t\t\tQualifiers: qualifiers,\n\t\t\t\tRanges:     nil,\n\t\t\t},\n\t\t}\n\n\t\tvar ranges []db.Range\n\t\tfor _, fixedInEntry := range fixedIns {\n\t\t\tranges = append(ranges, db.Range{\n\t\t\t\tVersion: db.Version{\n\t\t\t\t\tType:       fixedInEntry.VersionFormat,\n\t\t\t\t\tConstraint: enforceConstraint(fixedInEntry.Version, fixedInEntry.VulnerableRange, fixedInEntry.VersionFormat, vuln.Vulnerability.Name),\n\t\t\t\t},\n\t\t\t\tFix: getFix(fixedInEntry),\n\t\t\t})\n\t\t}\n\t\taph.BlobValue.Ranges = ranges\n\t\tafs = append(afs, aph)\n\t}\n\n\t// stable ordering\n\tsort.Sort(internal.ByAffectedPackage(afs))\n\n\treturn afs\n}\n\nfunc getFix(fixedInEntry unmarshal.OSFixedIn) *db.Fix {\n\tfixedInVersion := versionutil.CleanFixedInVersion(fixedInEntry.Version)\n\n\tfixState := db.NotFixedStatus\n\tif len(fixedInVersion) > 0 {\n\t\tfixState = db.FixedStatus\n\t} else if fixedInEntry.VendorAdvisory.NoAdvisory {\n\t\tfixState = db.WontFixStatus\n\t}\n\n\tvar advisoryOrder []advisoryKey\n\tadvisorySet := strset.New()\n\tfor _, a := range fixedInEntry.VendorAdvisory.AdvisorySummary {\n\t\tif a.Link != \"\" && !advisorySet.Has(a.Link) {\n\t\t\tadvisoryOrder = append(advisoryOrder, advisoryKey{id: a.ID, link: a.Link})\n\t\t\tadvisorySet.Add(a.Link)\n\t\t}\n\t}\n\n\tvar refs []db.Reference\n\tfor _, adv := range advisoryOrder {\n\t\trefs = append(refs, db.Reference{\n\t\t\tID:   adv.id,\n\t\t\tURL:  adv.link,\n\t\t\tTags: []string{db.AdvisoryReferenceTag},\n\t\t})\n\t}\n\n\tvar detail *db.FixDetail\n\tavailability := getFixAvailability(fixedInEntry)\n\tif len(refs) > 0 || availability != nil {\n\t\tdetail = &db.FixDetail{\n\t\t\tAvailable:  availability,\n\t\t\tReferences: refs,\n\t\t}\n\t}\n\n\treturn &db.Fix{\n\t\tVersion: fixedInVersion,\n\t\tState:   fixState,\n\t\tDetail:  detail,\n\t}\n}\n\nfunc getFixAvailability(fixedInEntry unmarshal.OSFixedIn) *db.FixAvailability {\n\tif fixedInEntry.Available.Date == \"\" {\n\t\treturn nil\n\t}\n\n\tt := internal.ParseTime(fixedInEntry.Available.Date)\n\tif t == nil {\n\t\tlog.WithFields(\"date\", fixedInEntry.Available.Date).Warn(\"unable to parse fix availability date\")\n\t\treturn nil\n\t}\n\n\treturn &db.FixAvailability{\n\t\tDate: t,\n\t\tKind: fixedInEntry.Available.Kind,\n\t}\n}\n\nfunc enforceConstraint(fixedVersion, vulnerableRange, format, vulnerabilityID string) string {\n\tif len(vulnerableRange) > 0 {\n\t\treturn vulnerableRange\n\t}\n\tfixedVersion = versionutil.CleanConstraint(fixedVersion)\n\tif len(fixedVersion) == 0 {\n\t\treturn \"\"\n\t}\n\tswitch strings.ToLower(format) {\n\tcase \"semver\":\n\t\treturn versionutil.EnforceSemVerConstraint(fixedVersion)\n\tdefault:\n\t\t// the passed constraint is a fixed version\n\t\treturn deriveConstraintFromFix(fixedVersion, vulnerabilityID)\n\t}\n}\n\nfunc deriveConstraintFromFix(fixVersion, vulnerabilityID string) string {\n\tconstraint := fmt.Sprintf(\"< %s\", fixVersion)\n\n\tif strings.HasPrefix(vulnerabilityID, \"ALASKERNEL-\") {\n\t\t// Amazon advisories of the form ALASKERNEL-5.4-2023-048 should be interpreted as only applying to\n\t\t// the 5.4.x kernel line since Amazon issue a separate advisory per affected line, thus the constraint\n\t\t// should be >= 5.4, < {fix version}.  In the future the vunnel schema for OS vulns should be enhanced\n\t\t// to emit actual constraints rather than fixed-in entries (tracked in https://github.com/anchore/vunnel/issues/266)\n\t\t// at which point this workaround in grype-db can be removed.\n\n\t\tcomponents := strings.Split(vulnerabilityID, \"-\")\n\n\t\tif len(components) == 4 {\n\t\t\tbase := components[1]\n\t\t\tconstraint = fmt.Sprintf(\">= %s, < %s\", base, fixVersion)\n\t\t}\n\t}\n\n\treturn constraint\n}\n\ntype groupIndex struct {\n\tname      string\n\tid        string\n\tosName    string\n\tosVersion string\n\tosChannel string\n\thasModule bool\n\tmodule    string\n\tformat    string\n}\n\nfunc groupFixedIns(vuln unmarshal.OSVulnerability) map[groupIndex][]unmarshal.OSFixedIn {\n\tgrouped := make(map[groupIndex][]unmarshal.OSFixedIn)\n\toi := getOSInfo(vuln.Vulnerability.NamespaceName)\n\n\tfor _, fixedIn := range vuln.Vulnerability.FixedIn {\n\t\tvar mod string\n\t\tif fixedIn.Module != nil {\n\t\t\tmod = *fixedIn.Module\n\t\t}\n\t\tg := groupIndex{\n\t\t\tname:      fixedIn.Name,\n\t\t\tid:        oi.id,\n\t\t\tosName:    oi.name,\n\t\t\tosVersion: oi.version,\n\t\t\tosChannel: oi.channel,\n\t\t\thasModule: fixedIn.Module != nil,\n\t\t\tmodule:    mod,\n\t\t\tformat:    fixedIn.VersionFormat,\n\t\t}\n\n\t\tgrouped[g] = append(grouped[g], fixedIn)\n\t}\n\treturn grouped\n}\n\nfunc getPackageType(osName string) pkg.Type {\n\tswitch osName {\n\tcase \"redhat\", \"amazonlinux\", \"oraclelinux\", \"sles\", \"mariner\", \"azurelinux\", \"photon\", \"fedora\", \"rocky\", \"rockylinux\", \"almalinux\", \"centos\":\n\t\treturn pkg.RpmPkg\n\tcase \"ubuntu\", \"debian\", \"echo\":\n\t\treturn pkg.DebPkg\n\tcase \"alpine\", \"chainguard\", \"wolfi\", \"minimos\", \"secureos\":\n\t\treturn pkg.ApkPkg\n\tcase \"windows\":\n\t\treturn pkg.KbPkg\n\t}\n\n\treturn \"\"\n}\n\nfunc getPackage(group groupIndex) *db.Package {\n\tt := getPackageType(group.osName)\n\treturn &db.Package{\n\t\tEcosystem: string(t),\n\t\tName:      name.Normalize(group.name, t),\n\t}\n}\n\ntype osInfo struct {\n\tname    string\n\tid      string\n\tversion string\n\tchannel string\n}\n\nfunc getOSInfo(group string) osInfo {\n\t// derived from enterprise feed groups, expected to be of the form {distro release ID}:{version}\n\tfeedGroupComponents := strings.Split(group, \":\")\n\n\tid := feedGroupComponents[0]\n\tversion := feedGroupComponents[1]\n\tchannel := \"\"\n\tif strings.Contains(feedGroupComponents[1], \"+\") {\n\t\tversionParts := strings.Split(feedGroupComponents[1], \"+\")\n\t\tchannel = versionParts[1]\n\t\tversion = versionParts[0]\n\t}\n\tif strings.ToLower(id) == \"mariner\" {\n\t\tverFields := strings.Split(version, \".\")\n\t\tmajorVersionStr := verFields[0]\n\t\tmajorVer, err := strconv.Atoi(majorVersionStr)\n\t\tif err == nil {\n\t\t\tif majorVer >= 3 {\n\t\t\t\tid = string(distro.Azure)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn osInfo{\n\t\tname:    normalizeOsName(id),\n\t\tid:      id,\n\t\tversion: version,\n\t\tchannel: channel,\n\t}\n}\n\nfunc normalizeOsName(id string) string {\n\td, ok := distro.IDMapping[id]\n\tif !ok {\n\t\tlog.WithFields(\"distro\", id).Warn(\"unknown distro name\")\n\n\t\treturn id\n\t}\n\n\treturn d.String()\n}\n\nfunc getOperatingSystem(osName, osID, osVersion, channel string) *db.OperatingSystem {\n\tif osName == \"\" || osVersion == \"\" {\n\t\treturn nil\n\t}\n\n\tversionFields := strings.Split(osVersion, \".\")\n\tvar majorVersion, minorVersion, labelVersion string\n\tmajorVersion = versionFields[0]\n\tif len(majorVersion) > 0 {\n\t\t// is the first field a number?\n\t\t_, err := strconv.Atoi(majorVersion[0:1])\n\t\tif err != nil {\n\t\t\tlabelVersion = majorVersion\n\t\t\tmajorVersion = \"\"\n\t\t} else if len(versionFields) > 1 {\n\t\t\tminorVersion = versionFields[1]\n\t\t}\n\t}\n\n\treturn &db.OperatingSystem{\n\t\tName:         osName,\n\t\tReleaseID:    osID,\n\t\tMajorVersion: majorVersion,\n\t\tMinorVersion: minorVersion,\n\t\tLabelVersion: labelVersion,\n\t\tChannel:      channel,\n\t\tCodename:     codename.LookupOS(osName, majorVersion, minorVersion),\n\t}\n}\n\nfunc getReferences(vuln unmarshal.OSVulnerability) []db.Reference {\n\tclean := strings.TrimSpace(vuln.Vulnerability.Link)\n\tif clean == \"\" {\n\t\treturn nil\n\t}\n\n\tvar linkOrder []string\n\tlinkSet := strset.New()\n\tif vuln.Vulnerability.Link != \"\" {\n\t\tlinkSet.Add(vuln.Vulnerability.Link)\n\t\tlinkOrder = append(linkOrder, vuln.Vulnerability.Link)\n\t}\n\tfor _, a := range vuln.Vulnerability.Metadata.CVE {\n\t\tif a.Link != \"\" && !linkSet.Has(a.Link) {\n\t\t\tlinkOrder = append(linkOrder, a.Link)\n\t\t}\n\t}\n\n\tvar refs []db.Reference\n\tfor _, l := range linkOrder {\n\t\trefs = append(refs,\n\t\t\tdb.Reference{\n\t\t\t\tURL: l,\n\t\t\t},\n\t\t)\n\t}\n\n\treturn refs\n}\n\nfunc getAliases(vuln unmarshal.OSVulnerability) []string {\n\tvar aliases []string\n\tfor _, cve := range vuln.Vulnerability.Metadata.CVE {\n\t\taliases = append(aliases,\n\t\t\tcve.Name,\n\t\t)\n\t}\n\treturn aliases\n}\n\nfunc getSeverities(vuln unmarshal.OSVulnerability) []db.Severity {\n\tvar severities []db.Severity\n\n\t// TODO: should we clean this here or not?\n\tif vuln.Vulnerability.Severity != \"\" && strings.ToLower(vuln.Vulnerability.Severity) != \"unknown\" {\n\t\tseverities = append(severities, db.Severity{\n\t\t\tScheme: db.SeveritySchemeCHMLN,\n\t\t\tValue:  strings.ToLower(vuln.Vulnerability.Severity),\n\t\t\tRank:   1, // TODO: enum this\n\t\t\t// TODO Source?\n\t\t})\n\t}\n\tfor _, vendorSeverity := range vuln.Vulnerability.CVSS {\n\t\tseverities = append(severities, db.Severity{\n\t\t\tScheme: db.SeveritySchemeCVSS,\n\t\t\tValue: db.CVSSSeverity{\n\t\t\t\tVector:  vendorSeverity.VectorString,\n\t\t\t\tVersion: vendorSeverity.Version,\n\t\t\t},\n\t\t\tRank: 2,\n\t\t\t// TODO: source?\n\t\t})\n\t}\n\n\treturn severities\n}\n"
  },
  {
    "path": "grype/db/v6/build/transformers/os/transform_test.go",
    "content": "package os\n\nimport (\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/grype/grype/db/internal/provider/unmarshal\"\n\t\"github.com/anchore/grype/grype/db/provider\"\n\tdb \"github.com/anchore/grype/grype/db/v6\"\n\t\"github.com/anchore/grype/grype/db/v6/build/transformers\"\n)\n\nvar timeVal = time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC)\nvar listing = provider.File{\n\tPath:      \"some\",\n\tDigest:    \"123456\",\n\tAlgorithm: \"sha256\",\n}\n\nfunc inputProviderState(name string) provider.State {\n\treturn provider.State{\n\t\tProvider:  name,\n\t\tVersion:   12,\n\t\tProcessor: \"vunnel@1.2.3\",\n\t\tTimestamp: timeVal,\n\t\tListing:   &listing,\n\t}\n}\n\nfunc expectedProvider(name string) *db.Provider {\n\treturn &db.Provider{\n\t\tID:           name,\n\t\tVersion:      \"12\",\n\t\tProcessor:    \"vunnel@1.2.3\",\n\t\tDateCaptured: &timeVal,\n\t\tInputDigest:  \"sha256:123456\",\n\t}\n}\n\nfunc TestTransform(t *testing.T) {\n\n\talpineOS := &db.OperatingSystem{\n\t\tName:         \"alpine\",\n\t\tReleaseID:    \"alpine\",\n\t\tMajorVersion: \"3\",\n\t\tMinorVersion: \"9\",\n\t}\n\n\tamazonOS := &db.OperatingSystem{\n\t\tName:         \"amazonlinux\",\n\t\tReleaseID:    \"amzn\",\n\t\tMajorVersion: \"2\",\n\t}\n\tazure3OS := &db.OperatingSystem{\n\t\tName:         \"azurelinux\",\n\t\tReleaseID:    \"azurelinux\",\n\t\tMajorVersion: \"3\",\n\t\tMinorVersion: \"0\", // TODO: is this right?\n\t}\n\tdebian8OS := &db.OperatingSystem{\n\t\tName:         \"debian\",\n\t\tReleaseID:    \"debian\",\n\t\tMajorVersion: \"8\",\n\t\tCodename:     \"jessie\",\n\t}\n\n\tmariner2OS := &db.OperatingSystem{\n\t\tName:         \"mariner\",\n\t\tReleaseID:    \"mariner\",\n\t\tMajorVersion: \"2\",\n\t\tMinorVersion: \"0\", // TODO: is this right?\n\t}\n\tol8OS := &db.OperatingSystem{\n\t\tName:         \"oraclelinux\",\n\t\tReleaseID:    \"ol\",\n\t\tMajorVersion: \"8\",\n\t}\n\trhel8OS := &db.OperatingSystem{\n\t\tName:         \"redhat\",\n\t\tReleaseID:    \"rhel\",\n\t\tMajorVersion: \"8\",\n\t}\n\tfedora39OS := &db.OperatingSystem{\n\t\tName:         \"fedora\",\n\t\tReleaseID:    \"fedora\",\n\t\tMajorVersion: \"39\",\n\t}\n\ttests := []struct {\n\t\tname     string\n\t\tprovider string\n\t\twant     []transformers.RelatedEntries\n\t}{\n\t\t{\n\t\t\tname:     \"testdata/alpine-3.9.json\",\n\t\t\tprovider: \"alpine\",\n\t\t\twant: []transformers.RelatedEntries{\n\t\t\t\t{\n\t\t\t\t\tVulnerabilityHandle: &db.VulnerabilityHandle{\n\t\t\t\t\t\tName:       \"CVE-2018-19967\",\n\t\t\t\t\t\tStatus:     \"active\",\n\t\t\t\t\t\tProviderID: \"alpine\",\n\t\t\t\t\t\tProvider:   expectedProvider(\"alpine\"),\n\t\t\t\t\t\tBlobValue: &db.VulnerabilityBlob{\n\t\t\t\t\t\t\tID: \"CVE-2018-19967\",\n\t\t\t\t\t\t\tReferences: []db.Reference{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tURL: \"http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-19967\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tSeverities: []db.Severity{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tScheme: db.SeveritySchemeCHMLN,\n\t\t\t\t\t\t\t\t\tValue:  \"medium\",\n\t\t\t\t\t\t\t\t\tRank:   1,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tRelated: affectedPkgSlice(\n\t\t\t\t\t\tdb.AffectedPackageHandle{\n\t\t\t\t\t\t\tOperatingSystem: alpineOS,\n\t\t\t\t\t\t\tPackage:         &db.Package{Ecosystem: \"apk\", Name: \"xen\"},\n\t\t\t\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tVersion: db.Version{Type: \"apk\", Constraint: \"< 4.11.1-r0\"},\n\t\t\t\t\t\t\t\t\t\tFix: &db.Fix{\n\t\t\t\t\t\t\t\t\t\t\tVersion: \"4.11.1-r0\",\n\t\t\t\t\t\t\t\t\t\t\tState:   db.FixedStatus,\n\t\t\t\t\t\t\t\t\t\t\tDetail: &db.FixDetail{\n\t\t\t\t\t\t\t\t\t\t\t\tAvailable: &db.FixAvailability{\n\t\t\t\t\t\t\t\t\t\t\t\t\tDate: timeRef(time.Date(2018, 12, 1, 9, 15, 30, 0, time.UTC)),\n\t\t\t\t\t\t\t\t\t\t\t\t\tKind: \"package\",\n\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:     \"testdata/amzn.json\",\n\t\t\tprovider: \"amazon\",\n\t\t\twant: []transformers.RelatedEntries{\n\t\t\t\t{\n\t\t\t\t\tVulnerabilityHandle: &db.VulnerabilityHandle{\n\t\t\t\t\t\tName:       \"ALAS-2018-1106\",\n\t\t\t\t\t\tProviderID: \"amazon\",\n\t\t\t\t\t\tProvider:   expectedProvider(\"amazon\"),\n\t\t\t\t\t\tStatus:     \"active\",\n\t\t\t\t\t\tBlobValue: &db.VulnerabilityBlob{\n\t\t\t\t\t\t\tID: \"ALAS-2018-1106\",\n\t\t\t\t\t\t\tReferences: []db.Reference{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tURL: \"https://alas.aws.amazon.com/AL2/ALAS-2018-1106.html\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tAliases: []string{\"CVE-2018-14648\"},\n\t\t\t\t\t\t\tSeverities: []db.Severity{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tScheme: db.SeveritySchemeCHMLN,\n\t\t\t\t\t\t\t\t\tValue:  \"medium\",\n\t\t\t\t\t\t\t\t\tRank:   1,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tRelated: affectedPkgSlice(\n\t\t\t\t\t\tdb.AffectedPackageHandle{\n\t\t\t\t\t\t\tOperatingSystem: amazonOS,\n\t\t\t\t\t\t\tPackage: &db.Package{\n\t\t\t\t\t\t\t\tName:      \"389-ds-base\",\n\t\t\t\t\t\t\t\tEcosystem: \"rpm\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\t\t\t\tCVEs:       []string{\"CVE-2018-14648\"},\n\t\t\t\t\t\t\t\tQualifiers: &db.PackageQualifiers{RpmModularity: strRef(\"\")},\n\t\t\t\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tVersion: db.Version{Type: \"rpm\", Constraint: \"< 1.3.8.4-15.amzn2.0.1\"},\n\t\t\t\t\t\t\t\t\t\tFix:     &db.Fix{Version: \"1.3.8.4-15.amzn2.0.1\", State: db.FixedStatus},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tdb.AffectedPackageHandle{\n\t\t\t\t\t\t\tOperatingSystem: amazonOS,\n\t\t\t\t\t\t\tPackage: &db.Package{\n\t\t\t\t\t\t\t\tName:      \"389-ds-base-debuginfo\",\n\t\t\t\t\t\t\t\tEcosystem: \"rpm\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\t\t\t\tCVEs:       []string{\"CVE-2018-14648\"},\n\t\t\t\t\t\t\t\tQualifiers: &db.PackageQualifiers{RpmModularity: strRef(\"\")},\n\t\t\t\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tVersion: db.Version{Type: \"rpm\", Constraint: \"< 1.3.8.4-15.amzn2.0.1\"},\n\t\t\t\t\t\t\t\t\t\tFix:     &db.Fix{Version: \"1.3.8.4-15.amzn2.0.1\", State: db.FixedStatus},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tdb.AffectedPackageHandle{\n\t\t\t\t\t\t\tOperatingSystem: amazonOS,\n\t\t\t\t\t\t\tPackage: &db.Package{\n\t\t\t\t\t\t\t\tName:      \"389-ds-base-devel\",\n\t\t\t\t\t\t\t\tEcosystem: \"rpm\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\t\t\t\tCVEs:       []string{\"CVE-2018-14648\"},\n\t\t\t\t\t\t\t\tQualifiers: &db.PackageQualifiers{RpmModularity: strRef(\"\")},\n\t\t\t\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tVersion: db.Version{Type: \"rpm\", Constraint: \"< 1.3.8.4-15.amzn2.0.1\"},\n\t\t\t\t\t\t\t\t\t\tFix:     &db.Fix{Version: \"1.3.8.4-15.amzn2.0.1\", State: db.FixedStatus},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tdb.AffectedPackageHandle{\n\t\t\t\t\t\t\tOperatingSystem: amazonOS,\n\t\t\t\t\t\t\tPackage: &db.Package{\n\t\t\t\t\t\t\t\tName:      \"389-ds-base-libs\",\n\t\t\t\t\t\t\t\tEcosystem: \"rpm\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\t\t\t\tCVEs:       []string{\"CVE-2018-14648\"},\n\t\t\t\t\t\t\t\tQualifiers: &db.PackageQualifiers{RpmModularity: strRef(\"\")},\n\t\t\t\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tVersion: db.Version{Type: \"rpm\", Constraint: \"< 1.3.8.4-15.amzn2.0.1\"},\n\t\t\t\t\t\t\t\t\t\tFix:     &db.Fix{Version: \"1.3.8.4-15.amzn2.0.1\", State: db.FixedStatus},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tdb.AffectedPackageHandle{\n\t\t\t\t\t\t\tOperatingSystem: amazonOS,\n\t\t\t\t\t\t\tPackage: &db.Package{\n\t\t\t\t\t\t\t\tName:      \"389-ds-base-snmp\",\n\t\t\t\t\t\t\t\tEcosystem: \"rpm\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\t\t\t\tCVEs:       []string{\"CVE-2018-14648\"},\n\t\t\t\t\t\t\t\tQualifiers: &db.PackageQualifiers{RpmModularity: strRef(\"\")},\n\t\t\t\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tVersion: db.Version{Type: \"rpm\", Constraint: \"< 1.3.8.4-15.amzn2.0.1\"},\n\t\t\t\t\t\t\t\t\t\tFix:     &db.Fix{Version: \"1.3.8.4-15.amzn2.0.1\", State: db.FixedStatus},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:     \"testdata/amazon-multiple-kernel-advisories.json\",\n\t\t\tprovider: \"amazon\",\n\t\t\twant: []transformers.RelatedEntries{\n\t\t\t\t{\n\t\t\t\t\tVulnerabilityHandle: &db.VulnerabilityHandle{\n\t\t\t\t\t\tName:       \"ALAS-2021-1704\",\n\t\t\t\t\t\tProviderID: \"amazon\",\n\t\t\t\t\t\tProvider:   expectedProvider(\"amazon\"),\n\t\t\t\t\t\tStatus:     \"active\",\n\t\t\t\t\t\tBlobValue: &db.VulnerabilityBlob{\n\t\t\t\t\t\t\tID: \"ALAS-2021-1704\",\n\n\t\t\t\t\t\t\tReferences: []db.Reference{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tURL: \"https://alas.aws.amazon.com/AL2/ALAS-2021-1704.html\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tAliases: []string{\"CVE-2021-3653\", \"CVE-2021-3656\", \"CVE-2021-3732\"},\n\t\t\t\t\t\t\tSeverities: []db.Severity{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tScheme: db.SeveritySchemeCHMLN,\n\t\t\t\t\t\t\t\t\tValue:  \"medium\",\n\t\t\t\t\t\t\t\t\tRank:   1,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tRelated: affectedPkgSlice(\n\t\t\t\t\t\tdb.AffectedPackageHandle{\n\t\t\t\t\t\t\tOperatingSystem: amazonOS,\n\t\t\t\t\t\t\tPackage:         &db.Package{Ecosystem: \"rpm\", Name: \"kernel\"},\n\t\t\t\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\t\t\t\tCVEs:       []string{\"CVE-2021-3653\", \"CVE-2021-3656\", \"CVE-2021-3732\"},\n\t\t\t\t\t\t\t\tQualifiers: &db.PackageQualifiers{RpmModularity: strRef(\"\")},\n\t\t\t\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tVersion: db.Version{Type: \"rpm\", Constraint: \"< 4.14.246-187.474.amzn2\"},\n\t\t\t\t\t\t\t\t\t\tFix:     &db.Fix{Version: \"4.14.246-187.474.amzn2\", State: db.FixedStatus},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tdb.AffectedPackageHandle{\n\t\t\t\t\t\t\tOperatingSystem: amazonOS,\n\t\t\t\t\t\t\tPackage:         &db.Package{Ecosystem: \"rpm\", Name: \"kernel-headers\"},\n\t\t\t\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\t\t\t\tCVEs:       []string{\"CVE-2021-3653\", \"CVE-2021-3656\", \"CVE-2021-3732\"},\n\t\t\t\t\t\t\t\tQualifiers: &db.PackageQualifiers{RpmModularity: strRef(\"\")},\n\t\t\t\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tVersion: db.Version{Type: \"rpm\", Constraint: \"< 4.14.246-187.474.amzn2\"},\n\t\t\t\t\t\t\t\t\t\tFix:     &db.Fix{Version: \"4.14.246-187.474.amzn2\", State: db.FixedStatus},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t),\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tVulnerabilityHandle: &db.VulnerabilityHandle{\n\t\t\t\t\t\tName:       \"ALASKERNEL-5.4-2022-007\",\n\t\t\t\t\t\tProviderID: \"amazon\",\n\t\t\t\t\t\tProvider:   expectedProvider(\"amazon\"),\n\t\t\t\t\t\tStatus:     \"active\",\n\t\t\t\t\t\tBlobValue: &db.VulnerabilityBlob{\n\t\t\t\t\t\t\tID: \"ALASKERNEL-5.4-2022-007\",\n\t\t\t\t\t\t\tReferences: []db.Reference{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tURL: \"https://alas.aws.amazon.com/AL2/ALASKERNEL-5.4-2022-007.html\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tAliases: []string{\"CVE-2021-3753\", \"CVE-2021-40490\"},\n\t\t\t\t\t\t\tSeverities: []db.Severity{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tScheme: db.SeveritySchemeCHMLN,\n\t\t\t\t\t\t\t\t\tValue:  \"medium\",\n\t\t\t\t\t\t\t\t\tRank:   1,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tRelated: affectedPkgSlice(\n\t\t\t\t\t\tdb.AffectedPackageHandle{\n\t\t\t\t\t\t\tOperatingSystem: amazonOS,\n\t\t\t\t\t\t\tPackage:         &db.Package{Ecosystem: \"rpm\", Name: \"kernel\"},\n\t\t\t\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\t\t\t\tCVEs:       []string{\"CVE-2021-3753\", \"CVE-2021-40490\"},\n\t\t\t\t\t\t\t\tQualifiers: &db.PackageQualifiers{RpmModularity: strRef(\"\")},\n\t\t\t\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tVersion: db.Version{Type: \"rpm\", Constraint: \">= 5.4, < 5.4.144-69.257.amzn2\"},\n\t\t\t\t\t\t\t\t\t\tFix:     &db.Fix{Version: \"5.4.144-69.257.amzn2\", State: db.FixedStatus},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tdb.AffectedPackageHandle{\n\t\t\t\t\t\t\tOperatingSystem: amazonOS,\n\t\t\t\t\t\t\tPackage:         &db.Package{Ecosystem: \"rpm\", Name: \"kernel-headers\"},\n\t\t\t\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\t\t\t\tCVEs:       []string{\"CVE-2021-3753\", \"CVE-2021-40490\"},\n\t\t\t\t\t\t\t\tQualifiers: &db.PackageQualifiers{RpmModularity: strRef(\"\")},\n\t\t\t\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tVersion: db.Version{Type: \"rpm\", Constraint: \">= 5.4, < 5.4.144-69.257.amzn2\"},\n\t\t\t\t\t\t\t\t\t\tFix:     &db.Fix{Version: \"5.4.144-69.257.amzn2\", State: db.FixedStatus},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t),\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tVulnerabilityHandle: &db.VulnerabilityHandle{\n\t\t\t\t\t\tName:       \"ALASKERNEL-5.10-2022-005\",\n\t\t\t\t\t\tProviderID: \"amazon\",\n\t\t\t\t\t\tProvider:   expectedProvider(\"amazon\"),\n\t\t\t\t\t\tStatus:     \"active\",\n\t\t\t\t\t\tBlobValue: &db.VulnerabilityBlob{\n\t\t\t\t\t\t\tID: \"ALASKERNEL-5.10-2022-005\",\n\t\t\t\t\t\t\tReferences: []db.Reference{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tURL: \"https://alas.aws.amazon.com/AL2/ALASKERNEL-5.10-2022-005.html\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tAliases: []string{\"CVE-2021-3753\", \"CVE-2021-40490\"},\n\t\t\t\t\t\t\tSeverities: []db.Severity{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tScheme: db.SeveritySchemeCHMLN,\n\t\t\t\t\t\t\t\t\tValue:  \"medium\",\n\t\t\t\t\t\t\t\t\tRank:   1,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tRelated: affectedPkgSlice(\n\t\t\t\t\t\tdb.AffectedPackageHandle{\n\t\t\t\t\t\t\tOperatingSystem: amazonOS,\n\t\t\t\t\t\t\tPackage:         &db.Package{Ecosystem: \"rpm\", Name: \"kernel\"},\n\t\t\t\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\t\t\t\tCVEs:       []string{\"CVE-2021-3753\", \"CVE-2021-40490\"},\n\t\t\t\t\t\t\t\tQualifiers: &db.PackageQualifiers{RpmModularity: strRef(\"\")},\n\t\t\t\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tVersion: db.Version{Type: \"rpm\", Constraint: \">= 5.10, < 5.10.62-55.141.amzn2\"},\n\t\t\t\t\t\t\t\t\t\tFix:     &db.Fix{Version: \"5.10.62-55.141.amzn2\", State: db.FixedStatus},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tdb.AffectedPackageHandle{\n\t\t\t\t\t\t\tOperatingSystem: amazonOS,\n\t\t\t\t\t\t\tPackage:         &db.Package{Ecosystem: \"rpm\", Name: \"kernel-headers\"},\n\t\t\t\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\t\t\t\tCVEs:       []string{\"CVE-2021-3753\", \"CVE-2021-40490\"},\n\t\t\t\t\t\t\t\tQualifiers: &db.PackageQualifiers{RpmModularity: strRef(\"\")},\n\t\t\t\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tVersion: db.Version{Type: \"rpm\", Constraint: \">= 5.10, < 5.10.62-55.141.amzn2\"},\n\t\t\t\t\t\t\t\t\t\tFix:     &db.Fix{Version: \"5.10.62-55.141.amzn2\", State: db.FixedStatus},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:     \"testdata/azure-linux-3.json\",\n\t\t\tprovider: \"mariner\",\n\t\t\twant: []transformers.RelatedEntries{\n\t\t\t\t{\n\t\t\t\t\tVulnerabilityHandle: &db.VulnerabilityHandle{\n\t\t\t\t\t\tName:       \"CVE-2023-29403\",\n\t\t\t\t\t\tProviderID: \"mariner\",\n\t\t\t\t\t\tProvider:   expectedProvider(\"mariner\"),\n\t\t\t\t\t\tStatus:     \"active\",\n\t\t\t\t\t\tBlobValue: &db.VulnerabilityBlob{\n\t\t\t\t\t\t\tID:          \"CVE-2023-29403\",\n\t\t\t\t\t\t\tDescription: \"CVE-2023-29403 affecting package golang for versions less than 1.20.7-1. A patched version of the package is available.\",\n\t\t\t\t\t\t\tReferences: []db.Reference{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tURL: \"https://nvd.nist.gov/vuln/detail/CVE-2023-29403\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tSeverities: []db.Severity{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tScheme: db.SeveritySchemeCHMLN,\n\t\t\t\t\t\t\t\t\tValue:  \"high\",\n\t\t\t\t\t\t\t\t\tRank:   1,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tRelated: affectedPkgSlice(\n\t\t\t\t\t\tdb.AffectedPackageHandle{\n\t\t\t\t\t\t\tOperatingSystem: azure3OS,\n\t\t\t\t\t\t\tPackage:         &db.Package{Ecosystem: \"rpm\", Name: \"golang\"},\n\t\t\t\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\t\t\t\tQualifiers: &db.PackageQualifiers{RpmModularity: strRef(\"\")},\n\t\t\t\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tVersion: db.Version{Type: \"rpm\", Constraint: \"< 0:1.20.7-1.azl3\"},\n\t\t\t\t\t\t\t\t\t\tFix:     &db.Fix{Version: \"0:1.20.7-1.azl3\", State: db.FixedStatus},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:     \"testdata/debian-8.json\",\n\t\t\tprovider: \"debian\",\n\t\t\twant: []transformers.RelatedEntries{\n\t\t\t\t{\n\t\t\t\t\tVulnerabilityHandle: &db.VulnerabilityHandle{\n\t\t\t\t\t\tName:       \"CVE-2008-7220\",\n\t\t\t\t\t\tProviderID: \"debian\",\n\t\t\t\t\t\tProvider:   expectedProvider(\"debian\"),\n\t\t\t\t\t\tStatus:     \"active\",\n\t\t\t\t\t\tBlobValue: &db.VulnerabilityBlob{\n\t\t\t\t\t\t\tID: \"CVE-2008-7220\",\n\t\t\t\t\t\t\tReferences: []db.Reference{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tURL: \"https://security-tracker.debian.org/tracker/CVE-2008-7220\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tSeverities: []db.Severity{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tScheme: db.SeveritySchemeCHMLN,\n\t\t\t\t\t\t\t\t\tValue:  \"high\",\n\t\t\t\t\t\t\t\t\tRank:   1,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tRelated: affectedPkgSlice(\n\t\t\t\t\t\tdb.AffectedPackageHandle{\n\t\t\t\t\t\t\tOperatingSystem: debian8OS,\n\t\t\t\t\t\t\tPackage:         &db.Package{Ecosystem: \"deb\", Name: \"asterisk\"},\n\t\t\t\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tVersion: db.Version{Type: \"dpkg\", Constraint: \"< 1:1.6.2.0~rc3-1\"},\n\t\t\t\t\t\t\t\t\t\tFix:     &db.Fix{Version: \"1:1.6.2.0~rc3-1\", State: db.FixedStatus},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tdb.AffectedPackageHandle{\n\t\t\t\t\t\t\tOperatingSystem: debian8OS,\n\t\t\t\t\t\t\tPackage:         &db.Package{Ecosystem: \"deb\", Name: \"auth2db\"},\n\t\t\t\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tVersion: db.Version{Type: \"dpkg\", Constraint: \"< 0.2.5-2+dfsg-1\"},\n\t\t\t\t\t\t\t\t\t\tFix:     &db.Fix{Version: \"0.2.5-2+dfsg-1\", State: db.FixedStatus},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tdb.AffectedPackageHandle{\n\t\t\t\t\t\t\tOperatingSystem: debian8OS,\n\t\t\t\t\t\t\tPackage:         &db.Package{Ecosystem: \"deb\", Name: \"exaile\"},\n\t\t\t\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tVersion: db.Version{Type: \"dpkg\", Constraint: \"< 0.2.14+debian-2.2\"},\n\t\t\t\t\t\t\t\t\t\tFix:     &db.Fix{Version: \"0.2.14+debian-2.2\", State: db.FixedStatus},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tdb.AffectedPackageHandle{\n\t\t\t\t\t\t\tOperatingSystem: debian8OS,\n\t\t\t\t\t\t\tPackage:         &db.Package{Ecosystem: \"deb\", Name: \"wordpress\"},\n\t\t\t\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tVersion: db.Version{Type: \"dpkg\", Constraint: \"\"},\n\t\t\t\t\t\t\t\t\t\tFix:     &db.Fix{Version: \"\", State: db.NotFixedStatus},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:     \"testdata/debian-8-multiple-entries-for-same-package.json\",\n\t\t\tprovider: \"debian\",\n\t\t\twant: []transformers.RelatedEntries{\n\t\t\t\t{\n\t\t\t\t\tVulnerabilityHandle: &db.VulnerabilityHandle{\n\t\t\t\t\t\tName:       \"CVE-2011-4623\",\n\t\t\t\t\t\tProviderID: \"debian\",\n\t\t\t\t\t\tProvider:   expectedProvider(\"debian\"),\n\t\t\t\t\t\tStatus:     \"active\",\n\t\t\t\t\t\tBlobValue: &db.VulnerabilityBlob{\n\t\t\t\t\t\t\tID: \"CVE-2011-4623\",\n\t\t\t\t\t\t\tReferences: []db.Reference{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tURL: \"https://security-tracker.debian.org/tracker/CVE-2011-4623\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tSeverities: []db.Severity{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tScheme: db.SeveritySchemeCHMLN,\n\t\t\t\t\t\t\t\t\tValue:  \"low\",\n\t\t\t\t\t\t\t\t\tRank:   1,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tRelated: affectedPkgSlice(\n\t\t\t\t\t\tdb.AffectedPackageHandle{\n\t\t\t\t\t\t\tOperatingSystem: debian8OS,\n\t\t\t\t\t\t\tPackage:         &db.Package{Ecosystem: \"deb\", Name: \"rsyslog\"},\n\t\t\t\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tVersion: db.Version{Type: \"dpkg\", Constraint: \"< 5.7.4-1\"},\n\t\t\t\t\t\t\t\t\t\tFix:     &db.Fix{Version: \"5.7.4-1\", State: db.FixedStatus},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t),\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tVulnerabilityHandle: &db.VulnerabilityHandle{\n\t\t\t\t\t\tName:       \"CVE-2008-5618\",\n\t\t\t\t\t\tProviderID: \"debian\",\n\t\t\t\t\t\tProvider:   expectedProvider(\"debian\"),\n\t\t\t\t\t\tStatus:     \"active\",\n\t\t\t\t\t\tBlobValue: &db.VulnerabilityBlob{\n\t\t\t\t\t\t\tID: \"CVE-2008-5618\",\n\t\t\t\t\t\t\tReferences: []db.Reference{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tURL: \"https://security-tracker.debian.org/tracker/CVE-2008-5618\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tSeverities: []db.Severity{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tScheme: db.SeveritySchemeCHMLN,\n\t\t\t\t\t\t\t\t\tValue:  \"low\",\n\t\t\t\t\t\t\t\t\tRank:   1,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tRelated: affectedPkgSlice(\n\t\t\t\t\t\tdb.AffectedPackageHandle{\n\t\t\t\t\t\t\tOperatingSystem: debian8OS,\n\t\t\t\t\t\t\tPackage:         &db.Package{Ecosystem: \"deb\", Name: \"rsyslog\"},\n\t\t\t\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tVersion: db.Version{Type: \"dpkg\", Constraint: \"< 3.18.6-1\"},\n\t\t\t\t\t\t\t\t\t\tFix:     &db.Fix{Version: \"3.18.6-1\", State: db.FixedStatus},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:     \"testdata/mariner-20.json\",\n\t\t\tprovider: \"mariner\",\n\t\t\twant: []transformers.RelatedEntries{\n\t\t\t\t{\n\t\t\t\t\tVulnerabilityHandle: &db.VulnerabilityHandle{\n\t\t\t\t\t\tName:       \"CVE-2021-37621\",\n\t\t\t\t\t\tProviderID: \"mariner\",\n\t\t\t\t\t\tProvider:   expectedProvider(\"mariner\"),\n\t\t\t\t\t\tStatus:     \"active\",\n\t\t\t\t\t\tBlobValue: &db.VulnerabilityBlob{\n\t\t\t\t\t\t\tID:          \"CVE-2021-37621\",\n\t\t\t\t\t\t\tDescription: \"CVE-2021-37621 affecting package exiv2 for versions less than 0.27.5-1. An upgraded version of the package is available that resolves this issue.\",\n\t\t\t\t\t\t\tReferences: []db.Reference{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tURL: \"https://nvd.nist.gov/vuln/detail/CVE-2021-37621\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tSeverities: []db.Severity{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tScheme: db.SeveritySchemeCHMLN,\n\t\t\t\t\t\t\t\t\tValue:  \"medium\",\n\t\t\t\t\t\t\t\t\tRank:   1,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\n\t\t\t\t\tRelated: affectedPkgSlice(\n\t\t\t\t\t\tdb.AffectedPackageHandle{\n\t\t\t\t\t\t\tOperatingSystem: mariner2OS,\n\t\t\t\t\t\t\tPackage:         &db.Package{Ecosystem: \"rpm\", Name: \"exiv2\"},\n\t\t\t\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\t\t\t\tQualifiers: &db.PackageQualifiers{RpmModularity: strRef(\"\")},\n\t\t\t\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tVersion: db.Version{Type: \"rpm\", Constraint: \"< 0:0.27.5-1.cm2\"},\n\t\t\t\t\t\t\t\t\t\tFix:     &db.Fix{Version: \"0:0.27.5-1.cm2\", State: db.FixedStatus},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\n\t\t{\n\t\t\tname:     \"testdata/mariner-range.json\",\n\t\t\tprovider: \"mariner\",\n\t\t\twant: []transformers.RelatedEntries{\n\t\t\t\t{\n\t\t\t\t\tVulnerabilityHandle: &db.VulnerabilityHandle{\n\t\t\t\t\t\tName:       \"CVE-2023-29404\",\n\t\t\t\t\t\tProviderID: \"mariner\",\n\t\t\t\t\t\tProvider:   expectedProvider(\"mariner\"),\n\t\t\t\t\t\tStatus:     \"active\",\n\t\t\t\t\t\tBlobValue: &db.VulnerabilityBlob{\n\t\t\t\t\t\t\tID:          \"CVE-2023-29404\",\n\t\t\t\t\t\t\tDescription: \"CVE-2023-29404 affecting package golang for versions less than 1.20.7-1. A patched version of the package is available.\",\n\t\t\t\t\t\t\tReferences: []db.Reference{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tURL: \"https://nvd.nist.gov/vuln/detail/CVE-2023-29404\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tSeverities: []db.Severity{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tScheme: db.SeveritySchemeCHMLN,\n\t\t\t\t\t\t\t\t\tValue:  \"critical\",\n\t\t\t\t\t\t\t\t\tRank:   1,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tRelated: affectedPkgSlice(\n\t\t\t\t\t\tdb.AffectedPackageHandle{\n\t\t\t\t\t\t\tOperatingSystem: mariner2OS,\n\t\t\t\t\t\t\tPackage:         &db.Package{Ecosystem: \"rpm\", Name: \"golang\"},\n\t\t\t\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\t\t\t\tQualifiers: &db.PackageQualifiers{RpmModularity: strRef(\"\")},\n\t\t\t\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tVersion: db.Version{Type: \"rpm\", Constraint: \"> 0:1.19.0.cm2, < 0:1.20.7-1.cm2\"},\n\t\t\t\t\t\t\t\t\t\tFix:     &db.Fix{Version: \"0:1.20.7-1.cm2\", State: db.FixedStatus},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:     \"testdata/ol-8.json\",\n\t\t\tprovider: \"oracle\",\n\t\t\twant: []transformers.RelatedEntries{\n\t\t\t\t{\n\t\t\t\t\tVulnerabilityHandle: &db.VulnerabilityHandle{\n\t\t\t\t\t\tName:          \"ELSA-2020-2550\",\n\t\t\t\t\t\tProviderID:    \"oracle\",\n\t\t\t\t\t\tProvider:      expectedProvider(\"oracle\"),\n\t\t\t\t\t\tStatus:        \"active\",\n\t\t\t\t\t\tPublishedDate: timeRef(time.Date(2020, 6, 15, 0, 0, 0, 0, time.UTC)),\n\t\t\t\t\t\tBlobValue: &db.VulnerabilityBlob{\n\t\t\t\t\t\t\tID:      \"ELSA-2020-2550\",\n\t\t\t\t\t\t\tAliases: []string{\"CVE-2020-13112\"},\n\t\t\t\t\t\t\tReferences: []db.Reference{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tURL: \"http://linux.oracle.com/errata/ELSA-2020-2550.html\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tURL: \"http://linux.oracle.com/cve/CVE-2020-13112.html\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tSeverities: []db.Severity{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tScheme: db.SeveritySchemeCHMLN,\n\t\t\t\t\t\t\t\t\tValue:  \"medium\",\n\t\t\t\t\t\t\t\t\tRank:   1,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tRelated: affectedPkgSlice(\n\t\t\t\t\t\tdb.AffectedPackageHandle{\n\t\t\t\t\t\t\tOperatingSystem: ol8OS,\n\t\t\t\t\t\t\tPackage:         &db.Package{Ecosystem: \"rpm\", Name: \"libexif\"},\n\t\t\t\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\t\t\t\tCVEs:       []string{\"CVE-2020-13112\"},\n\t\t\t\t\t\t\t\tQualifiers: &db.PackageQualifiers{RpmModularity: strRef(\"\")},\n\t\t\t\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tVersion: db.Version{Type: \"rpm\", Constraint: \"< 0:0.6.21-17.el8_2\"},\n\t\t\t\t\t\t\t\t\t\tFix:     &db.Fix{Version: \"0:0.6.21-17.el8_2\", State: db.FixedStatus},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tdb.AffectedPackageHandle{\n\t\t\t\t\t\t\tOperatingSystem: ol8OS,\n\t\t\t\t\t\t\tPackage:         &db.Package{Ecosystem: \"rpm\", Name: \"libexif-devel\"},\n\t\t\t\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\t\t\t\tCVEs:       []string{\"CVE-2020-13112\"},\n\t\t\t\t\t\t\t\tQualifiers: &db.PackageQualifiers{RpmModularity: strRef(\"\")},\n\t\t\t\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tVersion: db.Version{Type: \"rpm\", Constraint: \"< 0:0.6.21-17.el8_2\"},\n\t\t\t\t\t\t\t\t\t\tFix:     &db.Fix{Version: \"0:0.6.21-17.el8_2\", State: db.FixedStatus},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tdb.AffectedPackageHandle{\n\t\t\t\t\t\t\tOperatingSystem: ol8OS,\n\t\t\t\t\t\t\tPackage:         &db.Package{Ecosystem: \"rpm\", Name: \"libexif-dummy\"},\n\t\t\t\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\t\t\t\tCVEs:       []string{\"CVE-2020-13112\"},\n\t\t\t\t\t\t\t\tQualifiers: &db.PackageQualifiers{RpmModularity: strRef(\"\")},\n\t\t\t\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tVersion: db.Version{Type: \"rpm\", Constraint: \"\"},\n\t\t\t\t\t\t\t\t\t\tFix:     &db.Fix{State: db.NotFixedStatus},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:     \"testdata/ol-8-modules.json\",\n\t\t\tprovider: \"oracle\",\n\t\t\twant: []transformers.RelatedEntries{\n\t\t\t\t{\n\t\t\t\t\tVulnerabilityHandle: &db.VulnerabilityHandle{\n\t\t\t\t\t\tName:       \"CVE-2020-14350\",\n\t\t\t\t\t\tProviderID: \"oracle\",\n\t\t\t\t\t\tProvider:   expectedProvider(\"oracle\"),\n\t\t\t\t\t\tStatus:     \"active\",\n\t\t\t\t\t\tBlobValue: &db.VulnerabilityBlob{\n\t\t\t\t\t\t\tID:          \"CVE-2020-14350\",\n\t\t\t\t\t\t\tDescription: \"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.\",\n\t\t\t\t\t\t\tReferences: []db.Reference{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tURL: \"https://access.redhat.com/security/cve/CVE-2020-14350\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tSeverities: []db.Severity{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tScheme: db.SeveritySchemeCHMLN,\n\t\t\t\t\t\t\t\t\tValue:  \"medium\",\n\t\t\t\t\t\t\t\t\tRank:   1,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\n\t\t\t\t\tRelated: affectedPkgSlice(\n\t\t\t\t\t\tdb.AffectedPackageHandle{\n\t\t\t\t\t\t\tOperatingSystem: ol8OS,\n\t\t\t\t\t\t\tPackage:         &db.Package{Ecosystem: \"rpm\", Name: \"postgresql\"},\n\t\t\t\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\t\t\t\tQualifiers: &db.PackageQualifiers{\n\t\t\t\t\t\t\t\t\tRpmModularity: strRef(\"postgresql:10\"),\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tVersion: db.Version{\n\t\t\t\t\t\t\t\t\t\t\tType:       \"rpm\",\n\t\t\t\t\t\t\t\t\t\t\tConstraint: \"< 0:10.14-1.module+el8.2.0+7801+be0fed80\",\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\tFix: &db.Fix{\n\t\t\t\t\t\t\t\t\t\t\tVersion: \"0:10.14-1.module+el8.2.0+7801+be0fed80\",\n\t\t\t\t\t\t\t\t\t\t\tState:   db.FixedStatus,\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tdb.AffectedPackageHandle{\n\t\t\t\t\t\t\tOperatingSystem: ol8OS,\n\t\t\t\t\t\t\tPackage:         &db.Package{Ecosystem: \"rpm\", Name: \"postgresql\"},\n\t\t\t\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\t\t\t\tQualifiers: &db.PackageQualifiers{\n\t\t\t\t\t\t\t\t\tRpmModularity: strRef(\"postgresql:12\"),\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tVersion: db.Version{\n\t\t\t\t\t\t\t\t\t\t\tType:       \"rpm\",\n\t\t\t\t\t\t\t\t\t\t\tConstraint: \"< 0:12.5-1.module+el8.3.0+9042+664538f4\",\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\tFix: &db.Fix{\n\t\t\t\t\t\t\t\t\t\t\tVersion: \"0:12.5-1.module+el8.3.0+9042+664538f4\",\n\t\t\t\t\t\t\t\t\t\t\tState:   db.FixedStatus,\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tdb.AffectedPackageHandle{\n\t\t\t\t\t\t\tOperatingSystem: ol8OS,\n\t\t\t\t\t\t\tPackage:         &db.Package{Ecosystem: \"rpm\", Name: \"postgresql\"},\n\t\t\t\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\t\t\t\tQualifiers: &db.PackageQualifiers{\n\t\t\t\t\t\t\t\t\tRpmModularity: strRef(\"postgresql:9.6\"),\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tVersion: db.Version{\n\t\t\t\t\t\t\t\t\t\t\tType:       \"rpm\",\n\t\t\t\t\t\t\t\t\t\t\tConstraint: \"< 0:9.6.20-1.module+el8.3.0+8938+7f0e88b6\",\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\tFix: &db.Fix{\n\t\t\t\t\t\t\t\t\t\t\tVersion: \"0:9.6.20-1.module+el8.3.0+8938+7f0e88b6\",\n\t\t\t\t\t\t\t\t\t\t\tState:   db.FixedStatus,\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:     \"testdata/rhel-8.json\",\n\t\t\tprovider: \"redhat\",\n\t\t\twant: []transformers.RelatedEntries{\n\t\t\t\t{\n\t\t\t\t\tVulnerabilityHandle: &db.VulnerabilityHandle{\n\t\t\t\t\t\tName:       \"CVE-2020-6819\",\n\t\t\t\t\t\tProviderID: \"redhat\",\n\t\t\t\t\t\tProvider:   expectedProvider(\"redhat\"),\n\t\t\t\t\t\tStatus:     \"active\",\n\t\t\t\t\t\tBlobValue: &db.VulnerabilityBlob{\n\t\t\t\t\t\t\tID:          \"CVE-2020-6819\",\n\t\t\t\t\t\t\tDescription: \"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.\",\n\t\t\t\t\t\t\tReferences: []db.Reference{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tURL: \"https://access.redhat.com/security/cve/CVE-2020-6819\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tSeverities: []db.Severity{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tScheme: db.SeveritySchemeCHMLN,\n\t\t\t\t\t\t\t\t\tValue:  \"critical\",\n\t\t\t\t\t\t\t\t\tRank:   1,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tScheme: db.SeveritySchemeCVSS,\n\t\t\t\t\t\t\t\t\tValue: db.CVSSSeverity{\n\t\t\t\t\t\t\t\t\t\tVector:  \"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H\",\n\t\t\t\t\t\t\t\t\t\tVersion: \"3.1\",\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\tRank: 2,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tRelated: affectedPkgSlice(\n\t\t\t\t\t\tdb.AffectedPackageHandle{\n\t\t\t\t\t\t\tOperatingSystem: rhel8OS,\n\t\t\t\t\t\t\tPackage:         &db.Package{Ecosystem: \"rpm\", Name: \"firefox\"},\n\t\t\t\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\t\t\t\tQualifiers: &db.PackageQualifiers{RpmModularity: strRef(\"\")},\n\t\t\t\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tVersion: db.Version{\n\t\t\t\t\t\t\t\t\t\t\tType:       \"rpm\",\n\t\t\t\t\t\t\t\t\t\t\tConstraint: \"< 0:68.6.1-1.el8_1\",\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\tFix: &db.Fix{\n\t\t\t\t\t\t\t\t\t\t\tVersion: \"0:68.6.1-1.el8_1\",\n\t\t\t\t\t\t\t\t\t\t\tState:   db.FixedStatus,\n\t\t\t\t\t\t\t\t\t\t\tDetail: &db.FixDetail{\n\t\t\t\t\t\t\t\t\t\t\t\tAvailable: &db.FixAvailability{\n\t\t\t\t\t\t\t\t\t\t\t\t\tDate: timeRef(time.Date(2020, 4, 8, 14, 30, 15, 0, time.UTC)),\n\t\t\t\t\t\t\t\t\t\t\t\t\tKind: \"advisory\",\n\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\tReferences: []db.Reference{\n\t\t\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tID:   \"RHSA-2020:1341\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tURL:  \"https://access.redhat.com/errata/RHSA-2020:1341\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tTags: []string{db.AdvisoryReferenceTag},\n\t\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tdb.AffectedPackageHandle{\n\t\t\t\t\t\t\tOperatingSystem: rhel8OS,\n\t\t\t\t\t\t\tPackage:         &db.Package{Ecosystem: \"rpm\", Name: \"thunderbird\"},\n\t\t\t\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\t\t\t\tQualifiers: &db.PackageQualifiers{RpmModularity: strRef(\"\")},\n\t\t\t\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tVersion: db.Version{\n\t\t\t\t\t\t\t\t\t\t\tType:       \"rpm\",\n\t\t\t\t\t\t\t\t\t\t\tConstraint: \"< 0:68.7.0-1.el8_1\",\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\tFix: &db.Fix{\n\t\t\t\t\t\t\t\t\t\t\tVersion: \"0:68.7.0-1.el8_1\",\n\t\t\t\t\t\t\t\t\t\t\tState:   db.FixedStatus,\n\t\t\t\t\t\t\t\t\t\t\tDetail: &db.FixDetail{\n\t\t\t\t\t\t\t\t\t\t\t\tReferences: []db.Reference{\n\t\t\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tID:   \"RHSA-2020:1495\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tURL:  \"https://access.redhat.com/errata/RHSA-2020:1495\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tTags: []string{db.AdvisoryReferenceTag},\n\t\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:     \"testdata/rhel-8-modules.json\",\n\t\t\tprovider: \"redhat\",\n\t\t\twant: []transformers.RelatedEntries{\n\t\t\t\t{\n\t\t\t\t\tVulnerabilityHandle: &db.VulnerabilityHandle{\n\t\t\t\t\t\tName:       \"CVE-2020-14350\",\n\t\t\t\t\t\tProviderID: \"redhat\",\n\t\t\t\t\t\tProvider:   expectedProvider(\"redhat\"),\n\t\t\t\t\t\tStatus:     \"active\",\n\t\t\t\t\t\tBlobValue: &db.VulnerabilityBlob{\n\t\t\t\t\t\t\tID:          \"CVE-2020-14350\",\n\t\t\t\t\t\t\tDescription: \"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.\",\n\t\t\t\t\t\t\tReferences: []db.Reference{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tURL: \"https://access.redhat.com/security/cve/CVE-2020-14350\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tSeverities: []db.Severity{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tScheme: db.SeveritySchemeCHMLN,\n\t\t\t\t\t\t\t\t\tValue:  \"medium\",\n\t\t\t\t\t\t\t\t\tRank:   1,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tScheme: db.SeveritySchemeCVSS,\n\t\t\t\t\t\t\t\t\tValue: db.CVSSSeverity{\n\t\t\t\t\t\t\t\t\t\tVector:  \"CVSS:3.1/AV:N/AC:H/PR:L/UI:R/S:U/C:H/I:H/A:H\",\n\t\t\t\t\t\t\t\t\t\tVersion: \"3.1\",\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\tRank: 2,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tRelated: affectedPkgSlice(\n\t\t\t\t\t\tdb.AffectedPackageHandle{\n\t\t\t\t\t\t\tOperatingSystem: rhel8OS,\n\t\t\t\t\t\t\tPackage:         &db.Package{Ecosystem: \"rpm\", Name: \"postgresql\"},\n\t\t\t\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\t\t\t\tQualifiers: &db.PackageQualifiers{\n\t\t\t\t\t\t\t\t\tRpmModularity: strRef(\"postgresql:10\"),\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tVersion: db.Version{\n\t\t\t\t\t\t\t\t\t\t\tType:       \"rpm\",\n\t\t\t\t\t\t\t\t\t\t\tConstraint: \"< 0:10.14-1.module+el8.2.0+7801+be0fed80\",\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\tFix: &db.Fix{\n\t\t\t\t\t\t\t\t\t\t\tVersion: \"0:10.14-1.module+el8.2.0+7801+be0fed80\",\n\t\t\t\t\t\t\t\t\t\t\tState:   db.FixedStatus,\n\t\t\t\t\t\t\t\t\t\t\tDetail: &db.FixDetail{\n\t\t\t\t\t\t\t\t\t\t\t\tReferences: []db.Reference{\n\t\t\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tID:   \"RHSA-2020:3669\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tURL:  \"https://access.redhat.com/errata/RHSA-2020:3669\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tTags: []string{db.AdvisoryReferenceTag},\n\t\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tdb.AffectedPackageHandle{\n\t\t\t\t\t\t\tOperatingSystem: rhel8OS,\n\t\t\t\t\t\t\tPackage:         &db.Package{Ecosystem: \"rpm\", Name: \"postgresql\"},\n\t\t\t\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\t\t\t\tQualifiers: &db.PackageQualifiers{\n\t\t\t\t\t\t\t\t\tRpmModularity: strRef(\"postgresql:12\"),\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tVersion: db.Version{\n\t\t\t\t\t\t\t\t\t\t\tType:       \"rpm\",\n\t\t\t\t\t\t\t\t\t\t\tConstraint: \"< 0:12.5-1.module+el8.3.0+9042+664538f4\",\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\tFix: &db.Fix{\n\t\t\t\t\t\t\t\t\t\t\tVersion: \"0:12.5-1.module+el8.3.0+9042+664538f4\",\n\t\t\t\t\t\t\t\t\t\t\tState:   db.FixedStatus,\n\t\t\t\t\t\t\t\t\t\t\tDetail: &db.FixDetail{\n\t\t\t\t\t\t\t\t\t\t\t\tReferences: []db.Reference{\n\t\t\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tID:   \"RHSA-2020:5620\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tURL:  \"https://access.redhat.com/errata/RHSA-2020:5620\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tTags: []string{db.AdvisoryReferenceTag},\n\t\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tdb.AffectedPackageHandle{\n\t\t\t\t\t\t\tOperatingSystem: rhel8OS,\n\t\t\t\t\t\t\tPackage:         &db.Package{Ecosystem: \"rpm\", Name: \"postgresql\"},\n\t\t\t\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\t\t\t\tQualifiers: &db.PackageQualifiers{\n\t\t\t\t\t\t\t\t\tRpmModularity: strRef(\"postgresql:9.6\"),\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tVersion: db.Version{\n\t\t\t\t\t\t\t\t\t\t\tType:       \"rpm\",\n\t\t\t\t\t\t\t\t\t\t\tConstraint: \"< 0:9.6.20-1.module+el8.3.0+8938+7f0e88b6\",\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\tFix: &db.Fix{\n\t\t\t\t\t\t\t\t\t\t\tVersion: \"0:9.6.20-1.module+el8.3.0+8938+7f0e88b6\",\n\t\t\t\t\t\t\t\t\t\t\tState:   db.FixedStatus,\n\t\t\t\t\t\t\t\t\t\t\tDetail: &db.FixDetail{\n\t\t\t\t\t\t\t\t\t\t\t\tReferences: []db.Reference{\n\t\t\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tID:   \"RHSA-2020:5619\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tURL:  \"https://access.redhat.com/errata/RHSA-2020:5619\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tTags: []string{db.AdvisoryReferenceTag},\n\t\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:     \"testdata/fedora-39.json\",\n\t\t\tprovider: \"fedora\",\n\t\t\twant: []transformers.RelatedEntries{\n\t\t\t\t{\n\t\t\t\t\tVulnerabilityHandle: &db.VulnerabilityHandle{\n\t\t\t\t\t\tName:          \"CVE-2024-34397\",\n\t\t\t\t\t\tProviderID:    \"fedora\",\n\t\t\t\t\t\tProvider:      expectedProvider(\"fedora\"),\n\t\t\t\t\t\tStatus:        \"active\",\n\t\t\t\t\t\tPublishedDate: timeRef(time.Date(2024, 5, 9, 2, 43, 30, 0, time.UTC)),\n\t\t\t\t\t\tModifiedDate:  timeRef(time.Date(2024, 5, 14, 3, 27, 20, 0, time.UTC)),\n\t\t\t\t\t\tBlobValue: &db.VulnerabilityBlob{\n\t\t\t\t\t\t\tID:          \"CVE-2024-34397\",\n\t\t\t\t\t\t\tDescription: \"Security update for glib2 to fix CVE-2024-34397\",\n\t\t\t\t\t\t\tReferences: []db.Reference{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tURL: \"https://bodhi.fedoraproject.org/updates/FEDORA-2024-fd2569c4e9\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tSeverities: []db.Severity{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tScheme: db.SeveritySchemeCHMLN,\n\t\t\t\t\t\t\t\t\tValue:  \"high\",\n\t\t\t\t\t\t\t\t\tRank:   1,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tRelated: affectedPkgSlice(\n\t\t\t\t\t\tdb.AffectedPackageHandle{\n\t\t\t\t\t\t\tOperatingSystem: fedora39OS,\n\t\t\t\t\t\t\tPackage:         &db.Package{Ecosystem: \"rpm\", Name: \"glib2\"},\n\t\t\t\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\t\t\t\tQualifiers: &db.PackageQualifiers{RpmModularity: strRef(\"\")},\n\t\t\t\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tVersion: db.Version{\n\t\t\t\t\t\t\t\t\t\t\tType:       \"rpm\",\n\t\t\t\t\t\t\t\t\t\t\tConstraint: \"< 0:2.78.6-1.fc39\",\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\tFix: &db.Fix{\n\t\t\t\t\t\t\t\t\t\t\tVersion: \"0:2.78.6-1.fc39\",\n\t\t\t\t\t\t\t\t\t\t\tState:   db.FixedStatus,\n\t\t\t\t\t\t\t\t\t\t\tDetail: &db.FixDetail{\n\t\t\t\t\t\t\t\t\t\t\t\tReferences: []db.Reference{\n\t\t\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tID:   \"FEDORA-2024-fd2569c4e9\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tURL:  \"https://bodhi.fedoraproject.org/updates/FEDORA-2024-fd2569c4e9\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tTags: []string{db.AdvisoryReferenceTag},\n\t\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tvulns := loadFixture(t, test.name)\n\n\t\t\tvar actual []transformers.RelatedEntries\n\t\t\tfor _, vuln := range vulns {\n\t\t\t\tentries, err := Transform(vuln, inputProviderState(test.provider))\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tfor _, entry := range entries {\n\t\t\t\t\te, ok := entry.Data.(transformers.RelatedEntries)\n\t\t\t\t\trequire.True(t, ok)\n\t\t\t\t\tactual = append(actual, e)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif diff := cmp.Diff(test.want, actual); diff != \"\" {\n\t\t\t\tt.Errorf(\"data entries mismatch (-want +got):\\n%s\", diff)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestGetOperatingSystem(t *testing.T) {\n\ttests := []struct {\n\t\tname      string\n\t\tosName    string\n\t\tosID      string\n\t\tosVersion string\n\t\tchannel   string\n\t\texpected  *db.OperatingSystem\n\t}{\n\t\t{\n\t\t\tname:      \"works with given args\",\n\t\t\tosName:    \"alpine\",\n\t\t\tosID:      \"alpine\",\n\t\t\tosVersion: \"3.10\",\n\t\t\texpected: &db.OperatingSystem{\n\t\t\t\tName:         \"alpine\",\n\t\t\t\tReleaseID:    \"alpine\",\n\t\t\t\tMajorVersion: \"3\",\n\t\t\t\tMinorVersion: \"10\",\n\t\t\t\tLabelVersion: \"\",\n\t\t\t\tCodename:     \"\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:      \"does codename lookup (debian)\",\n\t\t\tosName:    \"debian\",\n\t\t\tosID:      \"debian\",\n\t\t\tosVersion: \"11\",\n\t\t\texpected: &db.OperatingSystem{\n\t\t\t\tName:         \"debian\",\n\t\t\t\tReleaseID:    \"debian\",\n\t\t\t\tMajorVersion: \"11\",\n\t\t\t\tMinorVersion: \"\",\n\t\t\t\tLabelVersion: \"\",\n\t\t\t\tCodename:     \"bullseye\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:      \"does codename lookup (ubuntu)\",\n\t\t\tosName:    \"ubuntu\",\n\t\t\tosID:      \"ubuntu\",\n\t\t\tosVersion: \"22.04\",\n\t\t\texpected: &db.OperatingSystem{\n\t\t\t\tName:         \"ubuntu\",\n\t\t\t\tReleaseID:    \"ubuntu\",\n\t\t\t\tMajorVersion: \"22\",\n\t\t\t\tMinorVersion: \"04\",\n\t\t\t\tLabelVersion: \"\",\n\t\t\t\tCodename:     \"jammy\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:      \"includes channel (rhel)\",\n\t\t\tosName:    \"redhat\",\n\t\t\tosID:      \"rhel\",\n\t\t\tosVersion: \"8.4\",\n\t\t\tchannel:   \"eus\",\n\t\t\texpected: &db.OperatingSystem{\n\t\t\t\tName:         \"redhat\",\n\t\t\t\tReleaseID:    \"rhel\",\n\t\t\t\tMajorVersion: \"8\",\n\t\t\t\tMinorVersion: \"4\",\n\t\t\t\tChannel:      \"eus\",\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := getOperatingSystem(tt.osName, tt.osID, tt.osVersion, tt.channel)\n\t\t\trequire.Equal(t, tt.expected, result)\n\t\t})\n\t}\n}\n\nfunc TestGetOSInfo(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tgroup    string\n\t\texpected osInfo\n\t}{\n\t\t{\n\t\t\tname:  \"alpine 3.10\",\n\t\t\tgroup: \"alpine:3.10\",\n\t\t\texpected: osInfo{\n\t\t\t\tname:    \"alpine\",\n\t\t\t\tid:      \"alpine\",\n\t\t\t\tversion: \"3.10\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"debian bullseye\",\n\t\t\tgroup: \"debian:11\",\n\t\t\texpected: osInfo{\n\t\t\t\tname:    \"debian\",\n\t\t\t\tid:      \"debian\",\n\t\t\t\tversion: \"11\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"mariner version 1\",\n\t\t\tgroup: \"mariner:1.0\",\n\t\t\texpected: osInfo{\n\t\t\t\tname:    \"mariner\",\n\t\t\t\tid:      \"mariner\",\n\t\t\t\tversion: \"1.0\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"mariner version 3 (azurelinux conversion)\",\n\t\t\tgroup: \"mariner:3.0\",\n\t\t\texpected: osInfo{\n\t\t\t\tname:    \"azurelinux\",\n\t\t\t\tid:      \"azurelinux\",\n\t\t\t\tversion: \"3.0\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"ubuntu focal\",\n\t\t\tgroup: \"ubuntu:20.04\",\n\t\t\texpected: osInfo{\n\t\t\t\tname:    \"ubuntu\",\n\t\t\t\tid:      \"ubuntu\",\n\t\t\t\tversion: \"20.04\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"oracle linux\",\n\t\t\tgroup: \"ol:8\",\n\t\t\texpected: osInfo{\n\t\t\t\tname:    \"oraclelinux\", // normalize name\n\t\t\t\tid:      \"ol\",          // keep original ID\n\t\t\t\tversion: \"8\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"redhat 8\",\n\t\t\tgroup: \"rhel:8\",\n\t\t\texpected: osInfo{\n\t\t\t\tname:    \"redhat\",\n\t\t\t\tid:      \"rhel\",\n\t\t\t\tversion: \"8\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"rhel + eus\",\n\t\t\tgroup: \"rhel:8+eus\",\n\t\t\texpected: osInfo{\n\t\t\t\tname:    \"redhat\",\n\t\t\t\tid:      \"rhel\",\n\t\t\t\tversion: \"8\",\n\t\t\t\tchannel: \"eus\",\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\toi := getOSInfo(tt.group)\n\t\t\tassert.Equal(t, tt.expected, oi, \"expected osInfo to match for group %s\", tt.group)\n\t\t})\n\t}\n}\n\nfunc TestGetFixAvailability(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tfixture  string\n\t\texpected map[string]*db.FixAvailability // keyed by package name for fixture-based testing\n\t}{\n\t\t{\n\t\t\tname:    \"alpine-3.9 with package availability\",\n\t\t\tfixture: \"testdata/alpine-3.9.json\",\n\t\t\texpected: map[string]*db.FixAvailability{\n\t\t\t\t\"xen\": {\n\t\t\t\t\tDate: timeRef(time.Date(2018, 12, 1, 9, 15, 30, 0, time.UTC)),\n\t\t\t\t\tKind: \"package\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:    \"rhel-8 with advisory availability\",\n\t\t\tfixture: \"testdata/rhel-8.json\",\n\t\t\texpected: map[string]*db.FixAvailability{\n\t\t\t\t\"firefox\": {\n\t\t\t\t\tDate: timeRef(time.Date(2020, 4, 8, 14, 30, 15, 0, time.UTC)),\n\t\t\t\t\tKind: \"advisory\",\n\t\t\t\t},\n\t\t\t\t\"thunderbird\": nil, // no availability data in fixture\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvulnerabilities := loadFixture(t, tt.fixture)\n\t\t\trequire.Len(t, vulnerabilities, 1, \"expected exactly one vulnerability\")\n\n\t\t\tfor _, fixedIn := range vulnerabilities[0].Vulnerability.FixedIn {\n\t\t\t\tresult := getFixAvailability(fixedIn)\n\t\t\t\texpected := tt.expected[fixedIn.Name]\n\n\t\t\t\tif expected == nil {\n\t\t\t\t\trequire.Nil(t, result, \"expected nil availability for %s\", fixedIn.Name)\n\t\t\t\t} else {\n\t\t\t\t\trequire.NotNil(t, result, \"expected non-nil availability for %s\", fixedIn.Name)\n\t\t\t\t\trequire.Equal(t, expected.Kind, result.Kind)\n\t\t\t\t\trequire.Equal(t, expected.Date, result.Date)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n\n\t// keep edge case test for scenarios not covered by fixtures\n\tt.Run(\"invalid date returns nil\", func(t *testing.T) {\n\t\tfixedIn := unmarshal.OSFixedIn{\n\t\t\tAvailable: struct {\n\t\t\t\tDate string `json:\"Date,omitempty\"`\n\t\t\t\tKind string `json:\"Kind,omitempty\"`\n\t\t\t}{\n\t\t\t\tDate: \"invalid-date\",\n\t\t\t\tKind: \"commit\",\n\t\t\t},\n\t\t}\n\t\tresult := getFixAvailability(fixedIn)\n\t\trequire.Nil(t, result)\n\t})\n}\n\nfunc TestGetFixWithDetail(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tfixedIn  unmarshal.OSFixedIn\n\t\texpected *db.Fix\n\t}{\n\t\t{\n\t\t\tname: \"fix with version and availability\",\n\t\t\tfixedIn: unmarshal.OSFixedIn{\n\t\t\t\tVersion: \"1.2.3\",\n\t\t\t\tAvailable: struct {\n\t\t\t\t\tDate string `json:\"Date,omitempty\"`\n\t\t\t\t\tKind string `json:\"Kind,omitempty\"`\n\t\t\t\t}{\n\t\t\t\t\tDate: \"2023-01-15T10:30:45Z\",\n\t\t\t\t\tKind: \"advisory\",\n\t\t\t\t},\n\t\t\t\tVendorAdvisory: struct {\n\t\t\t\t\tAdvisorySummary []struct {\n\t\t\t\t\t\tID   string `json:\"ID\"`\n\t\t\t\t\t\tLink string `json:\"Link\"`\n\t\t\t\t\t} `json:\"AdvisorySummary\"`\n\t\t\t\t\tNoAdvisory bool `json:\"NoAdvisory\"`\n\t\t\t\t}{\n\t\t\t\t\tAdvisorySummary: []struct {\n\t\t\t\t\t\tID   string `json:\"ID\"`\n\t\t\t\t\t\tLink string `json:\"Link\"`\n\t\t\t\t\t}{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tID:   \"RHSA-2023-001\",\n\t\t\t\t\t\t\tLink: \"https://access.redhat.com/errata/RHSA-2023-001\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: &db.Fix{\n\t\t\t\tVersion: \"1.2.3\",\n\t\t\t\tState:   db.FixedStatus,\n\t\t\t\tDetail: &db.FixDetail{\n\t\t\t\t\tAvailable: &db.FixAvailability{\n\t\t\t\t\t\tDate: timeRef(time.Date(2023, 1, 15, 10, 30, 45, 0, time.UTC)),\n\t\t\t\t\t\tKind: \"advisory\",\n\t\t\t\t\t},\n\t\t\t\t\tReferences: []db.Reference{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tID:   \"RHSA-2023-001\",\n\t\t\t\t\t\t\tURL:  \"https://access.redhat.com/errata/RHSA-2023-001\",\n\t\t\t\t\t\t\tTags: []string{db.AdvisoryReferenceTag},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"fix with version but no availability or references\",\n\t\t\tfixedIn: unmarshal.OSFixedIn{\n\t\t\t\tVersion: \"2.0.0\",\n\t\t\t\tAvailable: struct {\n\t\t\t\t\tDate string `json:\"Date,omitempty\"`\n\t\t\t\t\tKind string `json:\"Kind,omitempty\"`\n\t\t\t\t}{},\n\t\t\t},\n\t\t\texpected: &db.Fix{\n\t\t\t\tVersion: \"2.0.0\",\n\t\t\t\tState:   db.FixedStatus,\n\t\t\t\tDetail:  nil,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"no fix version with availability\",\n\t\t\tfixedIn: unmarshal.OSFixedIn{\n\t\t\t\tVersion: \"\",\n\t\t\t\tAvailable: struct {\n\t\t\t\t\tDate string `json:\"Date,omitempty\"`\n\t\t\t\t\tKind string `json:\"Kind,omitempty\"`\n\t\t\t\t}{\n\t\t\t\t\tDate: \"2023-01-15T10:30:45Z\",\n\t\t\t\t\tKind: \"release\",\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: &db.Fix{\n\t\t\t\tVersion: \"\",\n\t\t\t\tState:   db.NotFixedStatus,\n\t\t\t\tDetail: &db.FixDetail{\n\t\t\t\t\tAvailable: &db.FixAvailability{\n\t\t\t\t\t\tDate: timeRef(time.Date(2023, 1, 15, 10, 30, 45, 0, time.UTC)),\n\t\t\t\t\t\tKind: \"release\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"vendor advisory with no advisory flag set\",\n\t\t\tfixedIn: unmarshal.OSFixedIn{\n\t\t\t\tVersion: \"\",\n\t\t\t\tAvailable: struct {\n\t\t\t\t\tDate string `json:\"Date,omitempty\"`\n\t\t\t\t\tKind string `json:\"Kind,omitempty\"`\n\t\t\t\t}{},\n\t\t\t\tVendorAdvisory: struct {\n\t\t\t\t\tAdvisorySummary []struct {\n\t\t\t\t\t\tID   string `json:\"ID\"`\n\t\t\t\t\t\tLink string `json:\"Link\"`\n\t\t\t\t\t} `json:\"AdvisorySummary\"`\n\t\t\t\t\tNoAdvisory bool `json:\"NoAdvisory\"`\n\t\t\t\t}{\n\t\t\t\t\tNoAdvisory: true,\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: &db.Fix{\n\t\t\t\tVersion: \"\",\n\t\t\t\tState:   db.WontFixStatus,\n\t\t\t\tDetail:  nil,\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := getFix(tt.fixedIn)\n\n\t\t\tif d := cmp.Diff(tt.expected, result); d != \"\" {\n\t\t\t\tt.Fatalf(\"unexpected result: %s\", d)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestGetFixWithDetailFixtures(t *testing.T) {\n\t// additional fixture-based tests to complement the existing ad-hoc tests\n\ttests := []struct {\n\t\tname     string\n\t\tfixture  string\n\t\texpected map[string]*db.Fix // keyed by package name\n\t}{\n\t\t{\n\t\t\tname:    \"alpine-3.9 with availability\",\n\t\t\tfixture: \"testdata/alpine-3.9.json\",\n\t\t\texpected: map[string]*db.Fix{\n\t\t\t\t\"xen\": {\n\t\t\t\t\tVersion: \"4.11.1-r0\",\n\t\t\t\t\tState:   db.FixedStatus,\n\t\t\t\t\tDetail: &db.FixDetail{\n\t\t\t\t\t\tAvailable: &db.FixAvailability{\n\t\t\t\t\t\t\tDate: timeRef(time.Date(2018, 12, 1, 9, 15, 30, 0, time.UTC)),\n\t\t\t\t\t\t\tKind: \"package\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:    \"rhel-8 with availability and advisory references\",\n\t\t\tfixture: \"testdata/rhel-8.json\",\n\t\t\texpected: map[string]*db.Fix{\n\t\t\t\t\"firefox\": {\n\t\t\t\t\tVersion: \"0:68.6.1-1.el8_1\",\n\t\t\t\t\tState:   db.FixedStatus,\n\t\t\t\t\tDetail: &db.FixDetail{\n\t\t\t\t\t\tAvailable: &db.FixAvailability{\n\t\t\t\t\t\t\tDate: timeRef(time.Date(2020, 4, 8, 14, 30, 15, 0, time.UTC)),\n\t\t\t\t\t\t\tKind: \"advisory\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tReferences: []db.Reference{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tID:   \"RHSA-2020:1341\",\n\t\t\t\t\t\t\t\tURL:  \"https://access.redhat.com/errata/RHSA-2020:1341\",\n\t\t\t\t\t\t\t\tTags: []string{db.AdvisoryReferenceTag},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t\"thunderbird\": {\n\t\t\t\t\tVersion: \"0:68.7.0-1.el8_1\",\n\t\t\t\t\tState:   db.FixedStatus,\n\t\t\t\t\tDetail: &db.FixDetail{\n\t\t\t\t\t\tReferences: []db.Reference{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tID:   \"RHSA-2020:1495\",\n\t\t\t\t\t\t\t\tURL:  \"https://access.redhat.com/errata/RHSA-2020:1495\",\n\t\t\t\t\t\t\t\tTags: []string{db.AdvisoryReferenceTag},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvulnerabilities := loadFixture(t, tt.fixture)\n\t\t\trequire.Len(t, vulnerabilities, 1, \"expected exactly one vulnerability\")\n\n\t\t\tfor _, fixedIn := range vulnerabilities[0].Vulnerability.FixedIn {\n\t\t\t\tresult := getFix(fixedIn)\n\t\t\t\texpected := tt.expected[fixedIn.Name]\n\n\t\t\t\trequire.NotNil(t, expected, \"no expected result for package %s\", fixedIn.Name)\n\t\t\t\tif d := cmp.Diff(expected, result); d != \"\" {\n\t\t\t\t\tt.Fatalf(\"unexpected result for %s: %s\", fixedIn.Name, d)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc affectedPkgSlice(a ...db.AffectedPackageHandle) []any {\n\tvar r []any\n\tfor _, v := range a {\n\t\tr = append(r, v)\n\t}\n\treturn r\n}\n\nfunc loadFixture(t *testing.T, fixturePath string) []unmarshal.OSVulnerability {\n\tt.Helper()\n\n\tf, err := os.Open(fixturePath)\n\trequire.NoError(t, err)\n\tdefer func() {\n\t\trequire.NoError(t, f.Close())\n\t}()\n\n\tentries, err := unmarshal.OSVulnerabilityEntries(f)\n\trequire.NoError(t, err)\n\treturn entries\n}\n\nfunc timeRef(ti time.Time) *time.Time {\n\treturn &ti\n}\n\nfunc strRef(s string) *string {\n\treturn &s\n}\n"
  },
  {
    "path": "grype/db/v6/build/transformers/osv/testdata/ALSA-2025-7467.json",
    "content": "{\n  \"id\": \"ALSA-2025:7467\",\n  \"summary\": \"Moderate: skopeo security update\",\n  \"aliases\": [\n    \"CVE-2025-27144\"\n  ],\n  \"affected\": [\n    {\n      \"package\": {\n        \"ecosystem\": \"AlmaLinux:10\",\n        \"name\": \"skopeo\"\n      },\n      \"ranges\": [\n        {\n          \"type\": \"ECOSYSTEM\",\n          \"events\": [\n            {\n              \"introduced\": \"0\"\n            },\n            {\n              \"fixed\": \"2:1.18.1-1.el10_0\"\n            }\n          ]\n        }\n      ]\n    },\n    {\n      \"package\": {\n        \"ecosystem\": \"AlmaLinux:10\",\n        \"name\": \"skopeo-tests\"\n      },\n      \"ranges\": [\n        {\n          \"type\": \"ECOSYSTEM\",\n          \"events\": [\n            {\n              \"introduced\": \"0\"\n            },\n            {\n              \"fixed\": \"2:1.18.1-1.el10_0\"\n            }\n          ]\n        }\n      ]\n    }\n  ],\n  \"published\": \"2025-05-13T00:00:00Z\",\n  \"modified\": \"2025-07-02T12:50:06Z\",\n  \"details\": \"The skopeo command lets you inspect images from container image registries.\",\n  \"references\": [\n    {\n      \"url\": \"https://errata.almalinux.org/10/ALSA-2025-7467.html\",\n      \"type\": \"ADVISORY\"\n    }\n  ],\n  \"database_specific\": {\n    \"anchore\": {\n      \"record_type\": \"advisory\"\n    }\n  }\n}"
  },
  {
    "path": "grype/db/v6/build/transformers/osv/testdata/BIT-apache-2020-11984.json",
    "content": "{\n  \"schema_version\": \"1.5.0\",\n  \"id\": \"BIT-apache-2020-11984\",\n  \"details\": \"Apache HTTP server 2.4.32 to 2.4.44 mod_proxy_uwsgi info disclosure and possible RCE\",\n  \"aliases\": [\n    \"CVE-2020-11984\"\n  ],\n  \"affected\": [\n    {\n      \"package\": {\n        \"ecosystem\": \"Bitnami\",\n        \"name\": \"apache\",\n        \"purl\": \"pkg:bitnami/apache\"\n      },\n      \"severity\": [\n        {\n          \"type\": \"CVSS_V3\",\n          \"score\": \"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H\"\n        }\n      ],\n      \"ranges\": [\n        {\n          \"type\": \"SEMVER\",\n          \"events\": [\n            {\n              \"introduced\": \"2.4.32\"\n            },\n            {\n              \"last_affected\": \"2.4.43\"\n            }\n          ]\n        }\n      ]\n    }\n  ],\n  \"database_specific\": {\n    \"severity\": \"Critical\",\n    \"cpes\": [\n      \"cpe:2.3:a:apache:http_server:*:*:*:*:*:*:*:*\"\n    ]\n  },\n  \"references\": [\n    {\n      \"type\": \"WEB\",\n      \"url\": \"http://www.openwall.com/lists/oss-security/2020/08/08/1\"\n    },\n    {\n      \"type\": \"WEB\",\n      \"url\": \"http://www.openwall.com/lists/oss-security/2020/08/08/10\"\n    }\n  ],\n  \"published\": \"2024-03-06T10:57:57.770Z\",\n  \"modified\": \"2025-01-17T15:26:01.971Z\"\n}\n"
  },
  {
    "path": "grype/db/v6/build/transformers/osv/testdata/BIT-node-2020-8201.json",
    "content": "{\n  \"schema_version\": \"1.5.0\",\n  \"id\": \"BIT-node-2020-8201\",\n  \"details\": \"Node.js < 12.18.4 and < 14.11 can be exploited to perform HTTP desync attacks and deliver malicious payloads to unsuspecting users. The payloads can be crafted by an attacker to hijack user sessions, poison cookies, perform clickjacking, and a multitude of other attacks depending on the architecture of the underlying system. The attack was possible due to a bug in processing of carrier-return symbols in the HTTP header names.\",\n  \"aliases\": [\n    \"CVE-2020-8201\"\n  ],\n  \"affected\": [\n    {\n      \"package\": {\n        \"ecosystem\": \"Bitnami\",\n        \"name\": \"node\",\n        \"purl\": \"pkg:bitnami/node\"\n      },\n      \"severity\": [\n        {\n          \"type\": \"CVSS_V3\",\n          \"score\": \"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N\"\n        }\n      ],\n      \"ranges\": [\n        {\n          \"type\": \"SEMVER\",\n          \"events\": [\n            {\n              \"introduced\": \"12.0.0\"\n            },\n            {\n              \"fixed\": \"12.18.4\"\n            },\n            {\n              \"introduced\": \"14.0.0\"\n            },\n            {\n              \"fixed\": \"14.11.0\"\n            }\n          ],\n          \"database_specific\": {\n            \"anchore\": {\n              \"fixes\": [\n                {\n                  \"version\": \"12.18.4\",\n                  \"date\": \"2020-09-15\",\n                  \"kind\": \"first-observed\"\n                },\n                {\n                  \"version\": \"14.11.0\",\n                  \"date\": \"2020-09-15\",\n                  \"kind\": \"first-observed\"\n                }\n              ]\n            }\n          }\n        }\n      ]\n    }\n  ],\n  \"database_specific\": {\n    \"severity\": \"High\",\n    \"cpes\": [\n      \"cpe:2.3:a:nodejs:node.js:*:*:*:*:*:*:*:*\",\n      \"cpe:2.3:a:nodejs:node.js:*:*:*:*:lts:*:*:*\"\n    ]\n  },\n  \"references\": [\n    {\n      \"type\": \"WEB\",\n      \"url\": \"https://nodejs.org/en/blog/vulnerability/september-2020-security-releases/\"\n    },\n    {\n      \"type\": \"WEB\",\n      \"url\": \"https://security.gentoo.org/glsa/202101-07\"\n    }\n  ],\n  \"published\": \"2024-03-06T11:08:09.371Z\",\n  \"modified\": \"2024-03-06T11:25:28.861Z\"\n}\n"
  },
  {
    "path": "grype/db/v6/build/transformers/osv/transform.go",
    "content": "package osv\n\nimport (\n\t\"fmt\"\n\t\"regexp\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/google/osv-scanner/pkg/models\"\n\n\t\"github.com/anchore/grype/grype/db/data\"\n\t\"github.com/anchore/grype/grype/db/internal/codename\"\n\t\"github.com/anchore/grype/grype/db/internal/provider/unmarshal\"\n\t\"github.com/anchore/grype/grype/db/internal/versionutil\"\n\t\"github.com/anchore/grype/grype/db/provider\"\n\tdb \"github.com/anchore/grype/grype/db/v6\"\n\t\"github.com/anchore/grype/grype/db/v6/build/transformers\"\n\t\"github.com/anchore/grype/grype/db/v6/build/transformers/internal\"\n\t\"github.com/anchore/grype/grype/db/v6/name\"\n\t\"github.com/anchore/syft/syft/pkg\"\n)\n\nconst (\n\talmaLinux = \"almalinux\"\n)\n\nfunc Transform(vulnerability unmarshal.OSVVulnerability, state provider.State) ([]data.Entry, error) {\n\tseverities, err := getSeverities(vulnerability)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to obtain severities: %w\", err)\n\t}\n\n\tisAdvisory := isAdvisoryRecord(vulnerability)\n\taliases := vulnerability.Aliases\n\n\tif isAdvisory {\n\t\taliases = append(aliases, vulnerability.Related...)\n\t}\n\n\tin := []any{\n\t\tdb.VulnerabilityHandle{\n\t\t\tName:          vulnerability.ID,\n\t\t\tProviderID:    state.Provider,\n\t\t\tProvider:      provider.Model(state),\n\t\t\tStatus:        db.VulnerabilityActive,\n\t\t\tModifiedDate:  &vulnerability.Modified,\n\t\t\tPublishedDate: &vulnerability.Published,\n\t\t\tBlobValue: &db.VulnerabilityBlob{\n\t\t\t\tID:          vulnerability.ID,\n\t\t\t\tAssigners:   nil,\n\t\t\t\tDescription: vulnerability.Details,\n\t\t\t\tReferences:  getReferences(vulnerability),\n\t\t\t\tAliases:     aliases,\n\t\t\t\tSeverities:  severities,\n\t\t\t},\n\t\t},\n\t}\n\n\t// Check if this is an advisory record\n\tif isAdvisory {\n\t\t// For advisory records, emit unaffected packages\n\t\tfor _, u := range getUnaffectedPackages(vulnerability) {\n\t\t\tin = append(in, u)\n\t\t}\n\t} else {\n\t\t// For vulnerability records, emit affected packages\n\t\tfor _, a := range getAffectedPackages(vulnerability) {\n\t\t\tin = append(in, a)\n\t\t}\n\t}\n\n\treturn transformers.NewEntries(in...), nil\n}\n\nfunc getAffectedPackages(vuln unmarshal.OSVVulnerability) []db.AffectedPackageHandle {\n\tif len(vuln.Affected) == 0 {\n\t\treturn nil\n\t}\n\n\t// CPES might be in the database_specific information\n\tcpes, withCPE := vuln.DatabaseSpecific[\"cpes\"]\n\tif withCPE {\n\t\tif _, ok := cpes.([]string); !ok {\n\t\t\twithCPE = false\n\t\t}\n\t}\n\n\tvar aphs []db.AffectedPackageHandle\n\tfor _, affected := range vuln.Affected {\n\t\taph := db.AffectedPackageHandle{\n\t\t\tPackage:         getPackage(affected.Package),\n\t\t\tOperatingSystem: getOperatingSystemFromEcosystem(string(affected.Package.Ecosystem)),\n\t\t\tBlobValue:       &db.PackageBlob{CVEs: vuln.Aliases},\n\t\t}\n\n\t\t// Extract qualifiers (CPE and RPM modularity)\n\t\tqualifiers := getPackageQualifiers(affected, cpes, withCPE)\n\t\tif qualifiers != nil {\n\t\t\taph.BlobValue.Qualifiers = qualifiers\n\t\t}\n\n\t\tvar ranges []db.Range\n\t\tfor _, r := range affected.Ranges {\n\t\t\tranges = append(ranges, getGrypeRangesFromRange(r, string(affected.Package.Ecosystem))...)\n\t\t}\n\t\taph.BlobValue.Ranges = ranges\n\t\taphs = append(aphs, aph)\n\t}\n\n\t// stable ordering\n\tsort.Sort(internal.ByAffectedPackage(aphs))\n\n\treturn aphs\n}\n\n// getPackageQualifiers extracts package qualifiers from affected package data\n// including CPE information and RPM modularity\nfunc getPackageQualifiers(affected models.Affected, cpes any, withCPE bool) *db.PackageQualifiers {\n\tvar qualifiers *db.PackageQualifiers\n\n\t// Handle CPE qualifiers (existing logic)\n\tif withCPE {\n\t\tqualifiers = &db.PackageQualifiers{\n\t\t\tPlatformCPEs: cpes.([]string),\n\t\t}\n\t}\n\n\t// Extract RPM modularity from ecosystem_specific\n\trpmModularity := extractRpmModularity(affected)\n\tif rpmModularity != \"\" {\n\t\tif qualifiers == nil {\n\t\t\tqualifiers = &db.PackageQualifiers{}\n\t\t}\n\t\tqualifiers.RpmModularity = &rpmModularity\n\t}\n\n\treturn qualifiers\n}\n\n// extractRpmModularity extracts RPM modularity information from affected package ecosystem_specific\nfunc extractRpmModularity(affected models.Affected) string {\n\tif affected.EcosystemSpecific == nil {\n\t\treturn \"\"\n\t}\n\n\trpmModularity, ok := affected.EcosystemSpecific[\"rpm_modularity\"]\n\tif !ok {\n\t\treturn \"\"\n\t}\n\n\trpmModularityStr, ok := rpmModularity.(string)\n\tif !ok {\n\t\treturn \"\"\n\t}\n\n\treturn rpmModularityStr\n}\n\n// OSV supports flattered ranges, so both formats below are valid:\n// \"ranges\": [\n//\n//\t{\n//\t  \"type\": \"SEMVER\",\n//\t  \"events\": [\n//\t    {\n//\t      \"introduced\": \"12.0.0\"\n//\t    },\n//\t    {\n//\t      \"fixed\": \"12.18.4\"\n//\t    }\n//\t  ]\n//\t},\n//\t{\n//\t  \"type\": \"SEMVER\",\n//\t  \"events\": [\n//\t    {\n//\t      \"introduced\": \"14.0.0\"\n//\t    },\n//\t    {\n//\t      \"fixed\": \"14.11.0\"\n//\t    }\n//\t  ]\n//\t}\n//\n// ]\n// \"ranges\": [\n//\n//\t{\n//\t  \"type\": \"SEMVER\",\n//\t  \"events\": [\n//\t\t{\n//\t\t  \"introduced\": \"12.0.0\"\n//\t\t},\n//\t\t{\n//\t\t  \"fixed\": \"12.18.4\"\n//\t\t},\n//\t\t{\n//\t\t  \"introduced\": \"14.0.0\"\n//\t\t},\n//\t\t{\n//\t\t  \"fixed\": \"14.11.0\"\n//\t\t}\n//\t  ]\n//\t}\n//\n// ]\nfunc getGrypeRangesFromRange(r models.Range, ecosystem string) []db.Range { // nolint: gocognit,funlen\n\tvar ranges []db.Range\n\tif len(r.Events) == 0 {\n\t\treturn nil\n\t}\n\n\tvar constraint string\n\tupdateConstraint := func(c string) {\n\t\tif constraint == \"\" {\n\t\t\tconstraint = c\n\t\t} else {\n\t\t\tconstraint = versionutil.AndConstraints(constraint, c)\n\t\t}\n\t}\n\n\tfixByVersion := make(map[string]db.FixAvailability)\n\t// check r.DatabaseSpecific for \"anchore\" key which has\n\t// {\"fixes\": [{\n\t//   \"version\": \"v1.2.3\",\n\t//   \"date\": \"YYYY-MM-DD\",\n\t//   \"kind\": \"first-observed\",\n\t// }]}\n\n\tif dbSpecific, ok := r.DatabaseSpecific[\"anchore\"]; ok {\n\t\tif anchoreInfo, ok := dbSpecific.(map[string]any); ok {\n\t\t\tif fixes, ok := anchoreInfo[\"fixes\"]; ok {\n\t\t\t\tif fixList, ok := fixes.([]any); ok {\n\t\t\t\t\tfor _, fixEntry := range fixList {\n\t\t\t\t\t\tif fixMap, ok := fixEntry.(map[string]any); ok {\n\t\t\t\t\t\t\tversion, vOk := fixMap[\"version\"].(string)\n\t\t\t\t\t\t\tkind, kOk := fixMap[\"kind\"].(string)\n\t\t\t\t\t\t\tdate, dOk := fixMap[\"date\"].(string)\n\t\t\t\t\t\t\tif vOk && kOk && dOk {\n\t\t\t\t\t\t\t\tfixByVersion[version] = db.FixAvailability{\n\t\t\t\t\t\t\t\t\tDate: internal.ParseTime(date),\n\t\t\t\t\t\t\t\t\tKind: kind,\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\trangeType := normalizeRangeType(r.Type, ecosystem)\n\tfor _, e := range r.Events {\n\t\tswitch {\n\t\tcase e.Introduced != \"\" && e.Introduced != \"0\":\n\t\t\tconstraint = fmt.Sprintf(\">= %s\", e.Introduced)\n\t\tcase e.LastAffected != \"\":\n\t\t\tupdateConstraint(fmt.Sprintf(\"<= %s\", e.LastAffected))\n\t\t\t// We don't know the fix if last affected is set\n\t\t\tranges = append(ranges, db.Range{\n\t\t\t\tVersion: db.Version{\n\t\t\t\t\tType:       rangeType,\n\t\t\t\t\tConstraint: normalizeConstraint(constraint, rangeType),\n\t\t\t\t},\n\t\t\t})\n\t\t\t// Reset the constraint\n\t\t\tconstraint = \"\"\n\t\tcase e.Fixed != \"\":\n\t\t\tvar detail *db.FixDetail\n\t\t\tif f, ok := fixByVersion[e.Fixed]; ok {\n\t\t\t\tdetail = &db.FixDetail{\n\t\t\t\t\tAvailable: &f,\n\t\t\t\t}\n\t\t\t}\n\t\t\tupdateConstraint(fmt.Sprintf(\"< %s\", e.Fixed))\n\t\t\tranges = append(ranges, db.Range{\n\t\t\t\tFix: normalizeFix(e.Fixed, detail),\n\t\t\t\tVersion: db.Version{\n\t\t\t\t\tType:       rangeType,\n\t\t\t\t\tConstraint: normalizeConstraint(constraint, rangeType),\n\t\t\t\t},\n\t\t\t})\n\t\t\t// Reset the constraint\n\t\t\tconstraint = \"\"\n\t\t}\n\t}\n\n\t// Check if there's an event that \"introduced\" but never had a \"fixed\" or \"last affected\" event\n\tif constraint != \"\" {\n\t\tranges = append(ranges, db.Range{\n\t\t\tVersion: db.Version{\n\t\t\t\tType:       rangeType,\n\t\t\t\tConstraint: normalizeConstraint(constraint, rangeType),\n\t\t\t},\n\t\t})\n\t}\n\n\treturn ranges\n}\n\nfunc normalizeConstraint(constraint string, rangeType string) string {\n\tif rangeType == \"semver\" || rangeType == \"bitnami\" {\n\t\treturn versionutil.EnforceSemVerConstraint(constraint)\n\t}\n\treturn constraint\n}\n\nfunc normalizeFix(fix string, detail *db.FixDetail) *db.Fix {\n\tfixedInVersion := versionutil.CleanFixedInVersion(fix)\n\tfixState := db.NotFixedStatus\n\tif len(fixedInVersion) > 0 {\n\t\tfixState = db.FixedStatus\n\t}\n\n\treturn &db.Fix{\n\t\tState:   fixState,\n\t\tVersion: fixedInVersion,\n\t\tDetail:  detail,\n\t}\n}\n\nfunc normalizeRangeType(t models.RangeType, ecosystem string) string {\n\t// For Bitnami ecosystem, use \"bitnami\" format instead of \"semver\"\n\tif ecosystem == \"Bitnami\" && t == models.RangeSemVer {\n\t\treturn \"bitnami\"\n\t}\n\n\tswitch t {\n\tcase models.RangeSemVer, models.RangeEcosystem, models.RangeGit:\n\t\treturn strings.ToLower(string(t))\n\tdefault:\n\t\treturn \"unknown\"\n\t}\n}\n\nfunc getPackage(p models.Package) *db.Package {\n\t// Try to determine package type from ecosystem or PURL\n\tvar pkgType pkg.Type\n\tvar ecosystem string\n\n\tif p.Purl != \"\" {\n\t\tpkgType = pkg.TypeFromPURL(p.Purl)\n\t\tecosystem = string(p.Ecosystem)\n\t} else {\n\t\tpkgType = getPackageTypeFromEcosystem(string(p.Ecosystem))\n\t\t// If we found a package type from OS ecosystem, use it; otherwise use original ecosystem\n\t\tif pkgType != \"\" {\n\t\t\tecosystem = string(pkgType)\n\t\t} else {\n\t\t\tecosystem = string(p.Ecosystem)\n\t\t}\n\t}\n\n\treturn &db.Package{\n\t\tEcosystem: ecosystem,\n\t\tName:      name.Normalize(p.Name, pkgType),\n\t}\n}\n\n// getPackageTypeFromEcosystem determines package type from OSV ecosystem\n// Currently only supports AlmaLinux; other ecosystems use PURL-based detection\nfunc getPackageTypeFromEcosystem(ecosystem string) pkg.Type {\n\tif ecosystem == \"\" {\n\t\treturn \"\"\n\t}\n\n\t// Split ecosystem by colon to get OS name\n\tparts := strings.Split(ecosystem, \":\")\n\tosName := strings.ToLower(parts[0])\n\n\t// Only handle AlmaLinux\n\tif osName == almaLinux {\n\t\treturn pkg.RpmPkg\n\t}\n\n\t// For other ecosystems (like Bitnami, npm, pypi, etc.), return empty type\n\t// The package type will be determined from PURL if available\n\treturn \"\"\n}\n\nfunc getReferences(vuln unmarshal.OSVVulnerability) []db.Reference {\n\tvar refs []db.Reference\n\tfor _, ref := range vuln.References {\n\t\t// For advisory references, use the vulnerability ID as the advisory ID\n\t\t// This allows tools consuming the data to link back to the specific advisory\n\t\trefID := \"\"\n\t\tif ref.Type == models.ReferenceAdvisory && isAdvisoryRecord(vuln) {\n\t\t\trefID = vuln.ID\n\t\t}\n\n\t\trefs = append(refs,\n\t\t\tdb.Reference{\n\t\t\t\tID:   refID,\n\t\t\t\tURL:  ref.URL,\n\t\t\t\tTags: []string{string(ref.Type)},\n\t\t\t},\n\t\t)\n\t}\n\n\treturn refs\n}\n\n// extractCVSSInfo extracts the CVSS version and vector from the CVSS string\nfunc extractCVSSInfo(cvss string) (string, string, error) {\n\tre := regexp.MustCompile(`^CVSS:(\\d+\\.\\d+)/(.+)$`)\n\tmatches := re.FindStringSubmatch(cvss)\n\n\tif len(matches) != 3 {\n\t\treturn \"\", \"\", fmt.Errorf(\"invalid CVSS format\")\n\t}\n\n\treturn matches[1], matches[0], nil\n}\n\nfunc normalizeSeverity(severity models.Severity) (db.Severity, error) {\n\tswitch severity.Type {\n\tcase models.SeverityCVSSV2, models.SeverityCVSSV3, models.SeverityCVSSV4:\n\t\tversion, vector, err := extractCVSSInfo(severity.Score)\n\t\tif err != nil {\n\t\t\treturn db.Severity{}, err\n\t\t}\n\n\t\treturn db.Severity{\n\t\t\tScheme: db.SeveritySchemeCVSS,\n\t\t\tValue: db.CVSSSeverity{\n\t\t\t\tVector:  vector,\n\t\t\t\tVersion: version,\n\t\t\t},\n\t\t}, nil\n\tdefault:\n\t\treturn db.Severity{\n\t\t\tScheme: db.UnknownSeverityScheme,\n\t\t\tValue:  severity.Score,\n\t\t}, nil\n\t}\n}\n\nfunc getSeverities(vuln unmarshal.OSVVulnerability) ([]db.Severity, error) {\n\tvar severities []db.Severity\n\tfor _, sev := range vuln.Severity {\n\t\tseverity, err := normalizeSeverity(sev)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tseverities = append(severities, severity)\n\t}\n\n\tfor _, affected := range vuln.Affected {\n\t\tfor _, sev := range affected.Severity {\n\t\t\tseverity, err := normalizeSeverity(sev)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tseverities = append(severities, severity)\n\t\t}\n\t}\n\n\treturn severities, nil\n}\n\n// getOperatingSystemFromEcosystem extracts operating system information from OSV ecosystem field\n// Currently only supports AlmaLinux ecosystems\n// Example: \"AlmaLinux:8\" -> almalinux 8\nfunc getOperatingSystemFromEcosystem(ecosystem string) *db.OperatingSystem {\n\tif ecosystem == \"\" {\n\t\treturn nil\n\t}\n\n\t// Split ecosystem by colon to get components\n\tparts := strings.Split(ecosystem, \":\")\n\tif len(parts) < 2 {\n\t\treturn nil\n\t}\n\n\tosName := strings.ToLower(parts[0])\n\n\t// Only handle AlmaLinux\n\tif osName != almaLinux {\n\t\treturn nil\n\t}\n\n\tosVersion := parts[1]\n\n\t// Parse version into major/minor components\n\tversionFields := strings.Split(osVersion, \".\")\n\tvar majorVersion, minorVersion string\n\tif len(versionFields) > 0 {\n\t\tmajorVersion = versionFields[0]\n\t\t// Check if the first field is actually a number\n\t\tif _, err := strconv.Atoi(majorVersion[0:1]); err != nil {\n\t\t\t// If not numeric, treat the whole thing as a label version\n\t\t\treturn &db.OperatingSystem{\n\t\t\t\tName:         normalizeOSName(osName),\n\t\t\t\tLabelVersion: osVersion,\n\t\t\t\tCodename:     codename.LookupOS(normalizeOSName(osName), \"\", \"\"),\n\t\t\t}\n\t\t}\n\t\tif len(versionFields) > 1 {\n\t\t\tminorVersion = versionFields[1]\n\t\t}\n\t}\n\n\treturn &db.OperatingSystem{\n\t\tName:         normalizeOSName(osName),\n\t\tMajorVersion: majorVersion,\n\t\tMinorVersion: minorVersion,\n\t\tCodename:     codename.LookupOS(normalizeOSName(osName), majorVersion, minorVersion),\n\t}\n}\n\n// normalizeOSName normalizes operating system names for consistency\n// Currently only supports AlmaLinux\nfunc normalizeOSName(osName string) string {\n\tosName = strings.ToLower(osName)\n\n\t// Only handle AlmaLinux\n\tif osName == almaLinux {\n\t\treturn almaLinux\n\t}\n\n\treturn osName\n}\n\n// isAdvisoryRecord checks if the OSV record is marked as an advisory\nfunc isAdvisoryRecord(vuln unmarshal.OSVVulnerability) bool {\n\tif vuln.DatabaseSpecific == nil {\n\t\treturn false\n\t}\n\n\tanchoreData, ok := vuln.DatabaseSpecific[\"anchore\"]\n\tif !ok {\n\t\treturn false\n\t}\n\n\tanchoreMap, ok := anchoreData.(map[string]any)\n\tif !ok {\n\t\treturn false\n\t}\n\n\trecordType, ok := anchoreMap[\"record_type\"]\n\tif !ok {\n\t\treturn false\n\t}\n\n\trecordTypeStr, ok := recordType.(string)\n\tif !ok {\n\t\treturn false\n\t}\n\n\treturn recordTypeStr == \"advisory\"\n}\n\n// getUnaffectedPackages creates UnaffectedPackageHandle entries for advisory records\nfunc getUnaffectedPackages(vuln unmarshal.OSVVulnerability) []db.UnaffectedPackageHandle {\n\tif len(vuln.Affected) == 0 {\n\t\treturn nil\n\t}\n\n\tvar uphs []db.UnaffectedPackageHandle\n\tfor _, affected := range vuln.Affected {\n\t\tuph := db.UnaffectedPackageHandle{\n\t\t\tPackage:         getPackage(affected.Package),\n\t\t\tOperatingSystem: getOperatingSystemFromEcosystem(string(affected.Package.Ecosystem)),\n\t\t\tBlobValue:       getUnaffectedBlob(vuln.Aliases, affected.Ranges, affected),\n\t\t}\n\t\tuphs = append(uphs, uph)\n\t}\n\n\t// stable ordering\n\tsort.Sort(internal.ByUnaffectedPackage(uphs))\n\n\treturn uphs\n}\n\n// getUnaffectedBlob creates a package blob for unaffected packages (advisories)\n// For advisories, we need to invert the ranges to represent unaffected versions\nfunc getUnaffectedBlob(aliases []string, ranges []models.Range, affected models.Affected) *db.PackageBlob {\n\tvar grypeRanges []db.Range\n\tecosystem := string(affected.Package.Ecosystem)\n\tfor _, r := range ranges {\n\t\tgrypeRanges = append(grypeRanges, getGrypeUnaffectedRangesFromRange(r, ecosystem)...)\n\t}\n\n\t// Extract qualifiers including RPM modularity\n\tqualifiers := getPackageQualifiers(affected, nil, false)\n\n\treturn &db.PackageBlob{\n\t\tCVEs:       aliases,\n\t\tRanges:     grypeRanges,\n\t\tQualifiers: qualifiers,\n\t}\n}\n\n// getGrypeUnaffectedRangesFromRange converts OSV ranges to unaffected version ranges for unaffected packages\n// This inverts the logic: instead of \"< fix_version\" (affected), we create \">= fix_version\" (unaffected)\nfunc getGrypeUnaffectedRangesFromRange(r models.Range, ecosystem string) []db.Range {\n\tif len(r.Events) == 0 {\n\t\treturn nil\n\t}\n\n\tfixByVersion := extractFixAvailability(r)\n\trangeType := normalizeRangeType(r.Type, ecosystem)\n\n\treturn buildUnaffectedRangesFromEvents(r.Events, fixByVersion, rangeType)\n}\n\n// extractFixAvailability extracts fix availability information from DatabaseSpecific\nfunc extractFixAvailability(r models.Range) map[string]db.FixAvailability {\n\tfixByVersion := make(map[string]db.FixAvailability)\n\n\tdbSpecific, hasDBSpecific := r.DatabaseSpecific[\"anchore\"]\n\tif !hasDBSpecific {\n\t\treturn fixByVersion\n\t}\n\n\tanchoreInfo, isMap := dbSpecific.(map[string]any)\n\tif !isMap {\n\t\treturn fixByVersion\n\t}\n\n\tfixes, hasFixes := anchoreInfo[\"fixes\"]\n\tif !hasFixes {\n\t\treturn fixByVersion\n\t}\n\n\tfixList, isList := fixes.([]any)\n\tif !isList {\n\t\treturn fixByVersion\n\t}\n\n\tfor _, fixEntry := range fixList {\n\t\tparseSingleFixEntry(fixEntry, fixByVersion)\n\t}\n\n\treturn fixByVersion\n}\n\n// parseSingleFixEntry parses a single fix entry and adds it to the fixByVersion map\nfunc parseSingleFixEntry(fixEntry any, fixByVersion map[string]db.FixAvailability) {\n\tfixMap, isMap := fixEntry.(map[string]any)\n\tif !isMap {\n\t\treturn\n\t}\n\n\tversion, vOk := fixMap[\"version\"].(string)\n\tkind, kOk := fixMap[\"kind\"].(string)\n\tdate, dOk := fixMap[\"date\"].(string)\n\n\tif vOk && kOk && dOk {\n\t\tfixByVersion[version] = db.FixAvailability{\n\t\t\tDate: internal.ParseTime(date),\n\t\t\tKind: kind,\n\t\t}\n\t}\n}\n\n// buildUnaffectedRangesFromEvents processes events to create unaffected version ranges\nfunc buildUnaffectedRangesFromEvents(events []models.Event, fixByVersion map[string]db.FixAvailability, rangeType string) []db.Range {\n\tvar ranges []db.Range\n\n\tfor _, e := range events {\n\t\tif e.Fixed != \"\" {\n\t\t\tunaffectedRange := createUnaffectedRange(e.Fixed, fixByVersion, rangeType)\n\t\t\tranges = append(ranges, unaffectedRange)\n\t\t}\n\t}\n\n\treturn ranges\n}\n\n// createUnaffectedRange creates a single safe range for a fixed version\nfunc createUnaffectedRange(fixedVersion string, fixByVersion map[string]db.FixAvailability, rangeType string) db.Range {\n\tvar detail *db.FixDetail\n\tif f, ok := fixByVersion[fixedVersion]; ok {\n\t\tdetail = &db.FixDetail{\n\t\t\tAvailable: &f,\n\t\t}\n\t}\n\n\tconstraint := fmt.Sprintf(\">= %s\", fixedVersion)\n\treturn db.Range{\n\t\tFix: normalizeFix(fixedVersion, detail),\n\t\tVersion: db.Version{\n\t\t\tType:       rangeType,\n\t\t\tConstraint: normalizeConstraint(constraint, rangeType),\n\t\t},\n\t}\n}\n"
  },
  {
    "path": "grype/db/v6/build/transformers/osv/transform_test.go",
    "content": "package osv\n\nimport (\n\t\"os\"\n\t\"reflect\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/google/osv-scanner/pkg/models\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/grype/grype/db/internal/provider/unmarshal\"\n\t\"github.com/anchore/grype/grype/db/provider\"\n\tdb \"github.com/anchore/grype/grype/db/v6\"\n\t\"github.com/anchore/grype/grype/db/v6/build/transformers\"\n)\n\nvar timeVal = time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC)\nvar listing = provider.File{\n\tPath:      \"some\",\n\tDigest:    \"123456\",\n\tAlgorithm: \"sha256\",\n}\n\nfunc inputProviderState() provider.State {\n\treturn provider.State{\n\t\tProvider:  \"osv\",\n\t\tVersion:   12,\n\t\tProcessor: \"vunnel@1.2.3\",\n\t\tTimestamp: timeVal,\n\t\tListing:   &listing,\n\t}\n}\n\nfunc expectedProvider() *db.Provider {\n\treturn &db.Provider{\n\t\tID:           \"osv\",\n\t\tVersion:      \"12\",\n\t\tProcessor:    \"vunnel@1.2.3\",\n\t\tDateCaptured: &timeVal,\n\t\tInputDigest:  \"sha256:123456\",\n\t}\n}\n\nfunc timeRef(t time.Time) *time.Time {\n\treturn &t\n}\n\nfunc loadFixture(t *testing.T, fixturePath string) []unmarshal.OSVVulnerability {\n\tt.Helper()\n\n\tf, err := os.Open(fixturePath)\n\trequire.NoError(t, err)\n\tdefer func() {\n\t\trequire.NoError(t, f.Close())\n\t}()\n\n\tentries, err := unmarshal.OSVVulnerabilityEntries(f)\n\trequire.NoError(t, err)\n\treturn entries\n}\n\nfunc affectedPkgSlice(a ...db.AffectedPackageHandle) []any {\n\tvar r []any\n\tfor _, v := range a {\n\t\tr = append(r, v)\n\t}\n\treturn r\n}\n\nfunc unaffectedPkgSlice(u ...db.UnaffectedPackageHandle) []any {\n\tvar r []any\n\tfor _, v := range u {\n\t\tr = append(r, v)\n\t}\n\treturn r\n}\n\nfunc TestTransform(t *testing.T) {\n\ttests := []struct {\n\t\tname        string\n\t\tfixturePath string\n\t\twant        []transformers.RelatedEntries\n\t}{\n\t\t{\n\t\t\tname:        \"Apache 2020-11984\",\n\t\t\tfixturePath: \"testdata/BIT-apache-2020-11984.json\",\n\t\t\twant: []transformers.RelatedEntries{{\n\t\t\t\tVulnerabilityHandle: &db.VulnerabilityHandle{\n\t\t\t\t\tName:          \"BIT-apache-2020-11984\",\n\t\t\t\t\tStatus:        db.VulnerabilityActive,\n\t\t\t\t\tProviderID:    \"osv\",\n\t\t\t\t\tProvider:      expectedProvider(),\n\t\t\t\t\tModifiedDate:  timeRef(time.Date(2025, time.January, 17, 15, 26, 01, 971000000, time.UTC)),\n\t\t\t\t\tPublishedDate: timeRef(time.Date(2024, time.March, 6, 10, 57, 57, 770000000, time.UTC)),\n\t\t\t\t\tBlobValue: &db.VulnerabilityBlob{\n\t\t\t\t\t\tID:          \"BIT-apache-2020-11984\",\n\t\t\t\t\t\tDescription: \"Apache HTTP server 2.4.32 to 2.4.44 mod_proxy_uwsgi info disclosure and possible RCE\",\n\t\t\t\t\t\tReferences: []db.Reference{{\n\t\t\t\t\t\t\tURL:  \"http://www.openwall.com/lists/oss-security/2020/08/08/1\",\n\t\t\t\t\t\t\tTags: []string{\"WEB\"},\n\t\t\t\t\t\t}, {\n\t\t\t\t\t\t\tURL:  \"http://www.openwall.com/lists/oss-security/2020/08/08/10\",\n\t\t\t\t\t\t\tTags: []string{\"WEB\"},\n\t\t\t\t\t\t}},\n\t\t\t\t\t\tAliases: []string{\"CVE-2020-11984\"},\n\t\t\t\t\t\tSeverities: []db.Severity{{\n\t\t\t\t\t\t\tScheme: db.SeveritySchemeCVSS,\n\t\t\t\t\t\t\tValue: db.CVSSSeverity{\n\t\t\t\t\t\t\t\tVector:  \"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H\",\n\t\t\t\t\t\t\t\tVersion: \"3.1\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t}},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tRelated: affectedPkgSlice(\n\t\t\t\t\tdb.AffectedPackageHandle{\n\t\t\t\t\t\tPackage: &db.Package{\n\t\t\t\t\t\t\tName:      \"apache\",\n\t\t\t\t\t\t\tEcosystem: \"Bitnami\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\t\t\tCVEs: []string{\"CVE-2020-11984\"},\n\t\t\t\t\t\t\tRanges: []db.Range{{\n\t\t\t\t\t\t\t\tVersion: db.Version{\n\t\t\t\t\t\t\t\t\tType:       \"bitnami\",\n\t\t\t\t\t\t\t\t\tConstraint: \">=2.4.32,<=2.4.43\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t}},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t),\n\t\t\t}},\n\t\t},\n\t\t{\n\t\t\tname:        \"Node 2020-8201\",\n\t\t\tfixturePath: \"testdata/BIT-node-2020-8201.json\",\n\t\t\twant: []transformers.RelatedEntries{{\n\t\t\t\tVulnerabilityHandle: &db.VulnerabilityHandle{\n\t\t\t\t\tName:          \"BIT-node-2020-8201\",\n\t\t\t\t\tStatus:        db.VulnerabilityActive,\n\t\t\t\t\tProviderID:    \"osv\",\n\t\t\t\t\tProvider:      expectedProvider(),\n\t\t\t\t\tModifiedDate:  timeRef(time.Date(2024, time.March, 6, 11, 25, 28, 861000000, time.UTC)),\n\t\t\t\t\tPublishedDate: timeRef(time.Date(2024, time.March, 6, 11, 8, 9, 371000000, time.UTC)),\n\t\t\t\t\tBlobValue: &db.VulnerabilityBlob{\n\t\t\t\t\t\tID:          \"BIT-node-2020-8201\",\n\t\t\t\t\t\tDescription: \"Node.js < 12.18.4 and < 14.11 can be exploited to perform HTTP desync attacks and deliver malicious payloads to unsuspecting users. The payloads can be crafted by an attacker to hijack user sessions, poison cookies, perform clickjacking, and a multitude of other attacks depending on the architecture of the underlying system. The attack was possible due to a bug in processing of carrier-return symbols in the HTTP header names.\",\n\t\t\t\t\t\tReferences: []db.Reference{{\n\t\t\t\t\t\t\tURL:  \"https://nodejs.org/en/blog/vulnerability/september-2020-security-releases/\",\n\t\t\t\t\t\t\tTags: []string{\"WEB\"},\n\t\t\t\t\t\t}, {\n\t\t\t\t\t\t\tURL:  \"https://security.gentoo.org/glsa/202101-07\",\n\t\t\t\t\t\t\tTags: []string{\"WEB\"},\n\t\t\t\t\t\t}},\n\t\t\t\t\t\tAliases: []string{\"CVE-2020-8201\"},\n\t\t\t\t\t\tSeverities: []db.Severity{{\n\t\t\t\t\t\t\tScheme: db.SeveritySchemeCVSS,\n\t\t\t\t\t\t\tValue: db.CVSSSeverity{\n\t\t\t\t\t\t\t\tVector:  \"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N\",\n\t\t\t\t\t\t\t\tVersion: \"3.1\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t}},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tRelated: affectedPkgSlice(\n\t\t\t\t\tdb.AffectedPackageHandle{\n\t\t\t\t\t\tPackage: &db.Package{\n\t\t\t\t\t\t\tName:      \"node\",\n\t\t\t\t\t\t\tEcosystem: \"Bitnami\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\t\t\tCVEs: []string{\"CVE-2020-8201\"},\n\t\t\t\t\t\t\tRanges: []db.Range{{\n\t\t\t\t\t\t\t\tVersion: db.Version{\n\t\t\t\t\t\t\t\t\tType:       \"bitnami\",\n\t\t\t\t\t\t\t\t\tConstraint: \">=12.0.0,<12.18.4\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tFix: &db.Fix{\n\t\t\t\t\t\t\t\t\tVersion: \"12.18.4\",\n\t\t\t\t\t\t\t\t\tState:   db.FixedStatus,\n\t\t\t\t\t\t\t\t\tDetail: &db.FixDetail{\n\t\t\t\t\t\t\t\t\t\tAvailable: &db.FixAvailability{\n\t\t\t\t\t\t\t\t\t\t\tDate: timeRef(time.Date(2020, time.September, 15, 0, 0, 0, 0, time.UTC)),\n\t\t\t\t\t\t\t\t\t\t\tKind: \"first-observed\",\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t}, {\n\t\t\t\t\t\t\t\tVersion: db.Version{\n\t\t\t\t\t\t\t\t\tType:       \"bitnami\",\n\t\t\t\t\t\t\t\t\tConstraint: \">=14.0.0,<14.11.0\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tFix: &db.Fix{\n\t\t\t\t\t\t\t\t\tVersion: \"14.11.0\",\n\t\t\t\t\t\t\t\t\tState:   db.FixedStatus,\n\t\t\t\t\t\t\t\t\tDetail: &db.FixDetail{\n\t\t\t\t\t\t\t\t\t\tAvailable: &db.FixAvailability{\n\t\t\t\t\t\t\t\t\t\t\tDate: timeRef(time.Date(2020, time.September, 15, 0, 0, 0, 0, time.UTC)),\n\t\t\t\t\t\t\t\t\t\t\tKind: \"first-observed\",\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t}},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t),\n\t\t\t}},\n\t\t},\n\t\t{\n\t\t\tname:        \"AlmaLinux Advisory\",\n\t\t\tfixturePath: \"testdata/ALSA-2025-7467.json\",\n\t\t\twant: []transformers.RelatedEntries{{\n\t\t\t\tVulnerabilityHandle: &db.VulnerabilityHandle{\n\t\t\t\t\tName:          \"ALSA-2025:7467\",\n\t\t\t\t\tStatus:        db.VulnerabilityActive,\n\t\t\t\t\tProviderID:    \"osv\",\n\t\t\t\t\tProvider:      expectedProvider(),\n\t\t\t\t\tModifiedDate:  timeRef(time.Date(2025, time.July, 2, 12, 50, 6, 0, time.UTC)),\n\t\t\t\t\tPublishedDate: timeRef(time.Date(2025, time.May, 13, 0, 0, 0, 0, time.UTC)),\n\t\t\t\t\tBlobValue: &db.VulnerabilityBlob{\n\t\t\t\t\t\tID:          \"ALSA-2025:7467\",\n\t\t\t\t\t\tDescription: \"The skopeo command lets you inspect images from container image registries.\",\n\t\t\t\t\t\tReferences: []db.Reference{{\n\t\t\t\t\t\t\tID:   \"ALSA-2025:7467\",\n\t\t\t\t\t\t\tURL:  \"https://errata.almalinux.org/10/ALSA-2025-7467.html\",\n\t\t\t\t\t\t\tTags: []string{\"ADVISORY\"},\n\t\t\t\t\t\t}},\n\t\t\t\t\t\tAliases:    []string{\"CVE-2025-27144\"},\n\t\t\t\t\t\tSeverities: nil,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tRelated: unaffectedPkgSlice(\n\t\t\t\t\tdb.UnaffectedPackageHandle{\n\t\t\t\t\t\tPackage: &db.Package{\n\t\t\t\t\t\t\tName:      \"skopeo\",\n\t\t\t\t\t\t\tEcosystem: \"rpm\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tOperatingSystem: &db.OperatingSystem{\n\t\t\t\t\t\t\tName:         \"almalinux\",\n\t\t\t\t\t\t\tMajorVersion: \"10\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\t\t\tCVEs: []string{\"CVE-2025-27144\"},\n\t\t\t\t\t\t\tRanges: []db.Range{{\n\t\t\t\t\t\t\t\tVersion: db.Version{\n\t\t\t\t\t\t\t\t\tType:       \"ecosystem\",\n\t\t\t\t\t\t\t\t\tConstraint: \">= 2:1.18.1-1.el10_0\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tFix: &db.Fix{\n\t\t\t\t\t\t\t\t\tVersion: \"2:1.18.1-1.el10_0\",\n\t\t\t\t\t\t\t\t\tState:   db.FixedStatus,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t}},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tdb.UnaffectedPackageHandle{\n\t\t\t\t\t\tPackage: &db.Package{\n\t\t\t\t\t\t\tName:      \"skopeo-tests\",\n\t\t\t\t\t\t\tEcosystem: \"rpm\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tOperatingSystem: &db.OperatingSystem{\n\t\t\t\t\t\t\tName:         \"almalinux\",\n\t\t\t\t\t\t\tMajorVersion: \"10\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\t\t\tCVEs: []string{\"CVE-2025-27144\"},\n\t\t\t\t\t\t\tRanges: []db.Range{{\n\t\t\t\t\t\t\t\tVersion: db.Version{\n\t\t\t\t\t\t\t\t\tType:       \"ecosystem\",\n\t\t\t\t\t\t\t\t\tConstraint: \">= 2:1.18.1-1.el10_0\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tFix: &db.Fix{\n\t\t\t\t\t\t\t\t\tVersion: \"2:1.18.1-1.el10_0\",\n\t\t\t\t\t\t\t\t\tState:   db.FixedStatus,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t}},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t),\n\t\t\t}},\n\t\t},\n\t}\n\tt.Parallel()\n\tfor _, testToRun := range tests {\n\t\ttest := testToRun\n\t\tt.Run(test.name, func(tt *testing.T) {\n\t\t\ttt.Parallel()\n\t\t\tvulns := loadFixture(t, test.fixturePath)\n\t\t\tvar actual []transformers.RelatedEntries\n\t\t\tfor _, vuln := range vulns {\n\t\t\t\tentries, err := Transform(vuln, inputProviderState())\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tfor _, entry := range entries {\n\t\t\t\t\te, ok := entry.Data.(transformers.RelatedEntries)\n\t\t\t\t\trequire.True(t, ok)\n\t\t\t\t\tactual = append(actual, e)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif diff := cmp.Diff(test.want, actual); diff != \"\" {\n\t\t\t\tt.Errorf(\"data entries mismatch (-want +got):\\n%s\", diff)\n\t\t\t}\n\t\t})\n\t}\n}\nfunc Test_getGrypeRangesFromRange(t *testing.T) {\n\ttests := []struct {\n\t\tname      string\n\t\trnge      models.Range\n\t\tecosystem string\n\t\twant      []db.Range\n\t}{\n\t\t{\n\t\t\tname:      \"single range with 'fixed' status\",\n\t\t\tecosystem: \"npm\",\n\t\t\trnge: models.Range{\n\t\t\t\tType: models.RangeSemVer,\n\t\t\t\tEvents: []models.Event{{\n\t\t\t\t\tIntroduced: \"0.0.1\",\n\t\t\t\t}, {\n\t\t\t\t\tFixed: \"0.0.5\",\n\t\t\t\t}},\n\t\t\t},\n\t\t\twant: []db.Range{{\n\t\t\t\tVersion: db.Version{\n\t\t\t\t\tType:       \"semver\",\n\t\t\t\t\tConstraint: \">=0.0.1,<0.0.5\",\n\t\t\t\t},\n\t\t\t\tFix: &db.Fix{\n\t\t\t\t\tVersion: \"0.0.5\",\n\t\t\t\t\tState:   db.FixedStatus,\n\t\t\t\t},\n\t\t\t}},\n\t\t},\n\t\t{\n\t\t\tname:      \"single range with 'last affected' status\",\n\t\t\tecosystem: \"npm\",\n\t\t\trnge: models.Range{\n\t\t\t\tType: models.RangeSemVer,\n\t\t\t\tEvents: []models.Event{{\n\t\t\t\t\tIntroduced: \"0.0.1\",\n\t\t\t\t}, {\n\t\t\t\t\tLastAffected: \"0.0.5\",\n\t\t\t\t}},\n\t\t\t},\n\t\t\twant: []db.Range{{\n\t\t\t\tVersion: db.Version{\n\t\t\t\t\tType:       \"semver\",\n\t\t\t\t\tConstraint: \">=0.0.1,<=0.0.5\",\n\t\t\t\t},\n\t\t\t}},\n\t\t},\n\t\t{\n\t\t\tname:      \"single range with no 'fixed' or 'last affected' status\",\n\t\t\tecosystem: \"npm\",\n\t\t\trnge: models.Range{\n\t\t\t\tType: models.RangeSemVer,\n\t\t\t\tEvents: []models.Event{{\n\t\t\t\t\tIntroduced: \"0.0.1\",\n\t\t\t\t}},\n\t\t\t},\n\t\t\twant: []db.Range{{\n\t\t\t\tVersion: db.Version{\n\t\t\t\t\tType:       \"semver\",\n\t\t\t\t\tConstraint: \">=0.0.1\",\n\t\t\t\t},\n\t\t\t}},\n\t\t},\n\t\t{\n\t\t\tname:      \"single range introduced with '0'\",\n\t\t\tecosystem: \"npm\",\n\t\t\trnge: models.Range{\n\t\t\t\tType: models.RangeSemVer,\n\t\t\t\tEvents: []models.Event{{\n\t\t\t\t\tIntroduced: \"0\",\n\t\t\t\t}, {\n\t\t\t\t\tLastAffected: \"0.0.5\",\n\t\t\t\t}},\n\t\t\t},\n\t\t\twant: []db.Range{{\n\t\t\t\tVersion: db.Version{\n\t\t\t\t\tType:       \"semver\",\n\t\t\t\t\tConstraint: \"<=0.0.5\",\n\t\t\t\t},\n\t\t\t}},\n\t\t},\n\t\t{\n\t\t\tname:      \"multiple ranges\",\n\t\t\tecosystem: \"npm\",\n\t\t\trnge: models.Range{\n\t\t\t\tType: models.RangeSemVer,\n\t\t\t\tEvents: []models.Event{{\n\t\t\t\t\tIntroduced: \"0.0.1\",\n\t\t\t\t}, {\n\t\t\t\t\tFixed: \"0.0.5\",\n\t\t\t\t}, {\n\t\t\t\t\tIntroduced: \"1.0.1\",\n\t\t\t\t}, {\n\t\t\t\t\tFixed: \"1.0.5\",\n\t\t\t\t}},\n\t\t\t},\n\t\t\twant: []db.Range{{\n\t\t\t\tVersion: db.Version{\n\t\t\t\t\tType:       \"semver\",\n\t\t\t\t\tConstraint: \">=0.0.1,<0.0.5\",\n\t\t\t\t},\n\t\t\t\tFix: &db.Fix{\n\t\t\t\t\tVersion: \"0.0.5\",\n\t\t\t\t\tState:   db.FixedStatus,\n\t\t\t\t},\n\t\t\t}, {\n\t\t\t\tVersion: db.Version{\n\t\t\t\t\tType:       \"semver\",\n\t\t\t\t\tConstraint: \">=1.0.1,<1.0.5\",\n\t\t\t\t},\n\t\t\t\tFix: &db.Fix{\n\t\t\t\t\tVersion: \"1.0.5\",\n\t\t\t\t\tState:   db.FixedStatus,\n\t\t\t\t},\n\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:      \"single range with database-specific fix availability\",\n\t\t\tecosystem: \"npm\",\n\t\t\trnge: models.Range{\n\t\t\t\tType: models.RangeSemVer,\n\t\t\t\tEvents: []models.Event{{\n\t\t\t\t\tIntroduced: \"1.0.0\",\n\t\t\t\t}, {\n\t\t\t\t\tFixed: \"1.2.3\",\n\t\t\t\t}},\n\t\t\t\tDatabaseSpecific: map[string]interface{}{\n\t\t\t\t\t\"anchore\": map[string]interface{}{\n\t\t\t\t\t\t\"fixes\": []interface{}{\n\t\t\t\t\t\t\tmap[string]interface{}{\n\t\t\t\t\t\t\t\t\"version\": \"1.2.3\",\n\t\t\t\t\t\t\t\t\"date\":    \"2023-06-15\",\n\t\t\t\t\t\t\t\t\"kind\":    \"first-observed\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: []db.Range{{\n\t\t\t\tVersion: db.Version{\n\t\t\t\t\tType:       \"semver\",\n\t\t\t\t\tConstraint: \">=1.0.0,<1.2.3\",\n\t\t\t\t},\n\t\t\t\tFix: &db.Fix{\n\t\t\t\t\tVersion: \"1.2.3\",\n\t\t\t\t\tState:   db.FixedStatus,\n\t\t\t\t\tDetail: &db.FixDetail{\n\t\t\t\t\t\tAvailable: &db.FixAvailability{\n\t\t\t\t\t\t\tDate: timeRef(time.Date(2023, time.June, 15, 0, 0, 0, 0, time.UTC)),\n\t\t\t\t\t\t\tKind: \"first-observed\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}},\n\t\t},\n\t}\n\tt.Parallel()\n\tfor _, testToRun := range tests {\n\t\ttest := testToRun\n\t\tt.Run(test.name, func(tt *testing.T) {\n\t\t\ttt.Parallel()\n\t\t\tif got := getGrypeRangesFromRange(test.rnge, test.ecosystem); !reflect.DeepEqual(got, test.want) {\n\t\t\t\tt.Errorf(\"getGrypeRangesFromRange() = %v, want %v\", got, test.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_getPackage(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tpkg  models.Package\n\t\twant *db.Package\n\t}{\n\t\t{\n\t\t\tname: \"valid package\",\n\t\t\tpkg: models.Package{\n\t\t\t\tEcosystem: \"Bitnami\",\n\t\t\t\tName:      \"apache\",\n\t\t\t\tPurl:      \"pkg:bitnami/apache\",\n\t\t\t},\n\t\t\twant: &db.Package{\n\t\t\t\tName:      \"apache\",\n\t\t\t\tEcosystem: \"Bitnami\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"package with empty purl\",\n\t\t\tpkg: models.Package{\n\t\t\t\tEcosystem: \"Bitnami\",\n\t\t\t\tName:      \"apache\",\n\t\t\t\tPurl:      \"\",\n\t\t\t},\n\t\t\twant: &db.Package{\n\t\t\t\tName:      \"apache\",\n\t\t\t\tEcosystem: \"Bitnami\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"package with empty ecosystem\",\n\t\t\tpkg: models.Package{\n\t\t\t\tEcosystem: \"\",\n\t\t\t\tName:      \"apache\",\n\t\t\t\tPurl:      \"pkg:bitnami/apache\",\n\t\t\t},\n\t\t\twant: &db.Package{\n\t\t\t\tName:      \"apache\",\n\t\t\t\tEcosystem: \"\",\n\t\t\t},\n\t\t},\n\t}\n\tt.Parallel()\n\tfor _, testToRun := range tests {\n\t\ttest := testToRun\n\t\tt.Run(test.name, func(tt *testing.T) {\n\t\t\ttt.Parallel()\n\t\t\tgot := getPackage(test.pkg)\n\t\t\tif got.Name != test.want.Name {\n\t\t\t\tt.Errorf(\"getPackage() got name = %v, want %v\", got.Name, test.want.Name)\n\t\t\t}\n\t\t\tif got.Ecosystem != test.want.Ecosystem {\n\t\t\t\tt.Errorf(\"getPackage() got ecosystem = %v, want %v\", got.Ecosystem, test.want.Ecosystem)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_extractCVSSInfo(t *testing.T) {\n\ttests := []struct {\n\t\tname        string\n\t\tcvss        string\n\t\twantVersion string\n\t\twantVector  string\n\t\twantErr     bool\n\t}{\n\t\t{\n\t\t\tname:        \"valid cvss\",\n\t\t\tcvss:        \"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H\",\n\t\t\twantVersion: \"3.1\",\n\t\t\twantVector:  \"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H\",\n\t\t\twantErr:     false,\n\t\t},\n\t\t{\n\t\t\tname:        \"invalid cvss\",\n\t\t\tcvss:        \"foo:3.1/bar\",\n\t\t\twantVersion: \"\",\n\t\t\twantVector:  \"\",\n\t\t\twantErr:     true,\n\t\t},\n\t\t{\n\t\t\tname:        \"empty cvss\",\n\t\t\tcvss:        \"\",\n\t\t\twantVersion: \"\",\n\t\t\twantVector:  \"\",\n\t\t\twantErr:     true,\n\t\t},\n\t\t{\n\t\t\tname:        \"invalid cvss version\",\n\t\t\tcvss:        \"CVSS:foo/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H\",\n\t\t\twantVersion: \"\",\n\t\t\twantVector:  \"\",\n\t\t\twantErr:     true,\n\t\t},\n\t}\n\tt.Parallel()\n\tfor _, testToRun := range tests {\n\t\ttest := testToRun\n\t\tt.Run(test.name, func(tt *testing.T) {\n\t\t\ttt.Parallel()\n\t\t\tgotVersion, gotVector, err := extractCVSSInfo(test.cvss)\n\t\t\tif (err != nil) != test.wantErr {\n\t\t\t\tt.Errorf(\"extractCVSSInfo() error = %v, wantErr %v\", err, test.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif gotVersion != test.wantVersion {\n\t\t\t\tt.Errorf(\"extractCVSSInfo() got version = %v, want %v\", gotVersion, test.wantVersion)\n\t\t\t}\n\t\t\tif gotVector != test.wantVector {\n\t\t\t\tt.Errorf(\"extractCVSSInfo() got vector = %v, want %v\", gotVector, test.wantVector)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_extractRpmModularity(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\taffected models.Affected\n\t\twant     string\n\t}{\n\t\t{\n\t\t\tname: \"with rpm_modularity\",\n\t\t\taffected: models.Affected{\n\t\t\t\tEcosystemSpecific: map[string]interface{}{\n\t\t\t\t\t\"rpm_modularity\": \"mariadb:10.3\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: \"mariadb:10.3\",\n\t\t},\n\t\t{\n\t\t\tname: \"no ecosystem_specific\",\n\t\t\taffected: models.Affected{\n\t\t\t\tEcosystemSpecific: nil,\n\t\t\t},\n\t\t\twant: \"\",\n\t\t},\n\t\t{\n\t\t\tname: \"no rpm_modularity key\",\n\t\t\taffected: models.Affected{\n\t\t\t\tEcosystemSpecific: map[string]interface{}{\n\t\t\t\t\t\"other_key\": \"some_value\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: \"\",\n\t\t},\n\t\t{\n\t\t\tname: \"rpm_modularity not string\",\n\t\t\taffected: models.Affected{\n\t\t\t\tEcosystemSpecific: map[string]interface{}{\n\t\t\t\t\t\"rpm_modularity\": 123,\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: \"\",\n\t\t},\n\t\t{\n\t\t\tname: \"nodejs modularity\",\n\t\t\taffected: models.Affected{\n\t\t\t\tEcosystemSpecific: map[string]interface{}{\n\t\t\t\t\t\"rpm_modularity\": \"nodejs:16\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: \"nodejs:16\",\n\t\t},\n\t}\n\n\tfor _, testToRun := range tests {\n\t\ttest := testToRun\n\t\tt.Run(test.name, func(tt *testing.T) {\n\t\t\tgot := extractRpmModularity(test.affected)\n\t\t\tif got != test.want {\n\t\t\t\tt.Errorf(\"extractRpmModularity() = %v, want %v\", got, test.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_getPackageQualifiers(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\taffected models.Affected\n\t\tcpes     any\n\t\twithCPE  bool\n\t\twant     *db.PackageQualifiers\n\t}{\n\t\t{\n\t\t\tname: \"with rpm_modularity only\",\n\t\t\taffected: models.Affected{\n\t\t\t\tEcosystemSpecific: map[string]interface{}{\n\t\t\t\t\t\"rpm_modularity\": \"mariadb:10.3\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tcpes:    nil,\n\t\t\twithCPE: false,\n\t\t\twant: &db.PackageQualifiers{\n\t\t\t\tRpmModularity: stringRef(\"mariadb:10.3\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"with CPE only\",\n\t\t\taffected: models.Affected{\n\t\t\t\tEcosystemSpecific: nil,\n\t\t\t},\n\t\t\tcpes:    []string{\"cpe:2.3:a:vendor:product:*:*:*:*:*:*:*:*\"},\n\t\t\twithCPE: true,\n\t\t\twant: &db.PackageQualifiers{\n\t\t\t\tPlatformCPEs: []string{\"cpe:2.3:a:vendor:product:*:*:*:*:*:*:*:*\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"with both rpm_modularity and CPE\",\n\t\t\taffected: models.Affected{\n\t\t\t\tEcosystemSpecific: map[string]interface{}{\n\t\t\t\t\t\"rpm_modularity\": \"nodejs:16\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tcpes:    []string{\"cpe:2.3:a:nodejs:nodejs:*:*:*:*:*:*:*:*\"},\n\t\t\twithCPE: true,\n\t\t\twant: &db.PackageQualifiers{\n\t\t\t\tPlatformCPEs:  []string{\"cpe:2.3:a:nodejs:nodejs:*:*:*:*:*:*:*:*\"},\n\t\t\t\tRpmModularity: stringRef(\"nodejs:16\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"no qualifiers\",\n\t\t\taffected: models.Affected{\n\t\t\t\tEcosystemSpecific: nil,\n\t\t\t},\n\t\t\tcpes:    nil,\n\t\t\twithCPE: false,\n\t\t\twant:    nil,\n\t\t},\n\t}\n\n\tfor _, testToRun := range tests {\n\t\ttest := testToRun\n\t\tt.Run(test.name, func(tt *testing.T) {\n\t\t\tgot := getPackageQualifiers(test.affected, test.cpes, test.withCPE)\n\t\t\tif !reflect.DeepEqual(got, test.want) {\n\t\t\t\tt.Errorf(\"getPackageQualifiers() = %v, want %v\", got, test.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc stringRef(s string) *string {\n\treturn &s\n}\n"
  },
  {
    "path": "grype/db/v6/build/transformers/references.go",
    "content": "package transformers\n\nimport db \"github.com/anchore/grype/grype/db/v6\"\n\n// DeduplicateReferences removes duplicate references, where two references are considered\n// identical if they have the same URL and their normalized, sorted tags are equal\nfunc DeduplicateReferences(references []db.Reference) []db.Reference {\n\tvar result []db.Reference\n\tseenBefore := make(map[string][]db.Reference)\n\tfor _, ref := range references {\n\t\tif _, anySeenRefs := seenBefore[ref.URL]; !anySeenRefs {\n\t\t\tseenBefore[ref.URL] = []db.Reference{ref}\n\t\t\tresult = append(result, ref)\n\t\t\tcontinue\n\t\t}\n\t\talreadySeenRefs := seenBefore[ref.URL]\n\t\tisDuplicate := false\n\t\t// Check if this reference already exists for this URL\n\t\tfor _, already := range alreadySeenRefs {\n\t\t\tif refsAreEqual(already, ref) {\n\t\t\t\tisDuplicate = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif !isDuplicate {\n\t\t\tseenBefore[ref.URL] = append(seenBefore[ref.URL], ref)\n\t\t\tresult = append(result, ref)\n\t\t}\n\t}\n\n\treturn result\n}\n\nfunc refsAreEqual(a, b db.Reference) bool {\n\tif a.URL != b.URL {\n\t\treturn false\n\t}\n\n\tif len(a.Tags) != len(b.Tags) {\n\t\treturn false\n\t}\n\n\tfor i := range a.Tags {\n\t\tif a.Tags[i] != b.Tags[i] {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n"
  },
  {
    "path": "grype/db/v6/build/writer.go",
    "content": "package v6\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/anchore/go-logger\"\n\t\"github.com/anchore/grype/grype/db/data\"\n\t\"github.com/anchore/grype/grype/db/provider\"\n\tdb \"github.com/anchore/grype/grype/db/v6\"\n\t\"github.com/anchore/grype/grype/db/v6/build/transformers\"\n\t\"github.com/anchore/grype/internal/log\"\n)\n\nvar _ data.Writer = (*writer)(nil)\n\ntype writer struct {\n\tdbPath               string\n\tfailOnMissingFixDate bool\n\tstore                db.ReadWriter\n\tproviderCache        map[string]db.Provider\n\tstates               provider.States\n\tseverityCache        map[string]db.Severity\n\n\t// Two-tier batching: parent records (vulnerabilities + providers) and child records (related entries)\n\t// This maintains FK integrity while maximizing batch sizes\n\tparentBatchSize int\n\tchildBatchSize  int\n\tparentBuffer    []func() error\n\tchildBuffer     []func() error\n\tmu              sync.Mutex // Protect batch state\n\n\t// Metrics\n\ttotalParentBatches int\n\ttotalChildBatches  int\n}\n\ntype ProviderMetadata struct {\n\tProviders []Provider `json:\"providers\"`\n}\n\ntype Provider struct {\n\tName              string    `json:\"name\"`\n\tLastSuccessfulRun time.Time `json:\"lastSuccessfulRun\"`\n}\n\nfunc NewWriter(directory string, states provider.States, failOnMissingFixDate bool, batchSize int) (data.Writer, error) {\n\tcfg := db.Config{\n\t\tDBDirPath: directory,\n\t}\n\ts, err := db.NewWriter(cfg)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create store: %w\", err)\n\t}\n\n\tif err := s.SetDBMetadata(); err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to set DB ID: %w\", err)\n\t}\n\n\t// Use default if not configured\n\tif batchSize == 0 {\n\t\tbatchSize = 2000\n\t}\n\n\treturn &writer{\n\t\tdbPath:               cfg.DBFilePath(),\n\t\tfailOnMissingFixDate: failOnMissingFixDate,\n\t\tproviderCache:        make(map[string]db.Provider),\n\t\tstore:                s,\n\t\tstates:               states,\n\t\tseverityCache:        make(map[string]db.Severity),\n\t\tparentBatchSize:      batchSize,\n\t\tchildBatchSize:       batchSize,\n\t\tparentBuffer:         make([]func() error, 0, batchSize),\n\t\tchildBuffer:          make([]func() error, 0, batchSize),\n\t}, nil\n}\n\nfunc (w *writer) Write(entries ...data.Entry) error {\n\tfor _, entry := range entries {\n\t\tif entry.DBSchemaVersion != db.ModelVersion {\n\t\t\treturn fmt.Errorf(\"wrong schema version: want %+v got %+v\", db.ModelVersion, entry.DBSchemaVersion)\n\t\t}\n\n\t\tswitch row := entry.Data.(type) {\n\t\tcase transformers.RelatedEntries:\n\t\t\tif err := w.writeEntry(row); err != nil {\n\t\t\t\treturn fmt.Errorf(\"unable to write entry to store: %w\", err)\n\t\t\t}\n\t\tdefault:\n\t\t\treturn fmt.Errorf(\"data entry is not of type vulnerability, vulnerability metadata, or exclusion: %T\", row)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (w *writer) writeEntry(entry transformers.RelatedEntries) error {\n\tlog.WithFields(\"entry\", entry.String()).Trace(\"writing entry\")\n\n\tif entry.VulnerabilityHandle != nil {\n\t\tw.fillInMissingSeverity(entry.VulnerabilityHandle)\n\n\t\t// Add vulnerability to parent batch\n\t\t// CRITICAL: Use pointer directly (not copy) so ID assignment propagates to child operations\n\t\tvulnHandle := entry.VulnerabilityHandle\n\t\tif err := w.addToParentBatch(func() error {\n\t\t\treturn w.store.AddVulnerabilities(vulnHandle)\n\t\t}); err != nil {\n\t\t\treturn fmt.Errorf(\"unable to batch vulnerability write: %w\", err)\n\t\t}\n\t}\n\n\t// Handle providers for entries without vulnerabilities (EPSS, KEV, etc.)\n\t// AddVulnerabilities() only handles providers implicitly for vulnerability entries\n\tif entry.Provider != nil && entry.VulnerabilityHandle == nil {\n\t\tprovider := *entry.Provider\n\t\tif err := w.addToParentBatch(func() error {\n\t\t\treturn w.store.AddProvider(provider)\n\t\t}); err != nil {\n\t\t\treturn fmt.Errorf(\"unable to batch provider write: %w\", err)\n\t\t}\n\t}\n\n\t// Add all related entries to child batch\n\t// NOTE: No explicit flush here. Parent batch auto-flushes at threshold.\n\t// Child batch auto-flush will flush parent first to maintain FK integrity.\n\tfor i := range entry.Related {\n\t\tif err := w.writeRelatedEntry(entry.VulnerabilityHandle, entry.Related[i]); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (w *writer) writeRelatedEntry(vulnHandle *db.VulnerabilityHandle, related any) error {\n\tswitch row := related.(type) {\n\tcase db.AffectedPackageHandle:\n\t\treturn w.writeAffectedPackage(vulnHandle, row)\n\tcase db.AffectedCPEHandle:\n\t\treturn w.writeAffectedCPE(vulnHandle, row)\n\tcase db.KnownExploitedVulnerabilityHandle:\n\t\t// Add KEV to child batch - copy to avoid pointer reuse\n\t\tkevHandle := row\n\t\treturn w.addToChildBatch(func() error {\n\t\t\thandleCopy := kevHandle\n\t\t\treturn w.store.AddKnownExploitedVulnerabilities(&handleCopy)\n\t\t})\n\tcase db.UnaffectedPackageHandle:\n\t\treturn w.writeUnaffectedPackage(vulnHandle, row)\n\tcase db.UnaffectedCPEHandle:\n\t\treturn w.writeUnaffectedCPE(vulnHandle, row)\n\tcase db.EpssHandle:\n\t\t// Add EPSS to child batch - copy to avoid pointer reuse\n\t\tepssHandle := row\n\t\treturn w.addToChildBatch(func() error {\n\t\t\thandleCopy := epssHandle\n\t\t\treturn w.store.AddEpss(&handleCopy)\n\t\t})\n\tcase db.CWEHandle:\n\t\t// Add CWE to child batch - copy to avoid pointer reuse\n\t\tcweHandle := row\n\t\treturn w.addToChildBatch(func() error {\n\t\t\thandleCopy := cweHandle\n\t\t\treturn w.store.AddCWE(&handleCopy)\n\t\t})\n\tcase db.OperatingSystemEOLHandle:\n\t\t// Add OS EOL to child batch - copy to avoid pointer reuse\n\t\teolHandle := row\n\t\treturn w.addToChildBatch(func() error {\n\t\t\thandleCopy := eolHandle\n\t\t\treturn w.writeOperatingSystemEOL(handleCopy)\n\t\t})\n\tdefault:\n\t\treturn fmt.Errorf(\"data entry is not of type vulnerability, vulnerability metadata, or exclusion: %T\", row)\n\t}\n}\n\nfunc (w *writer) writeAffectedPackage(vulnHandle *db.VulnerabilityHandle, row db.AffectedPackageHandle) error {\n\tif w.failOnMissingFixDate {\n\t\tif err := ensureFixDates(&row); err != nil {\n\t\t\tfields := logger.Fields{\n\t\t\t\t\"pkg\": row.Package,\n\t\t\t}\n\t\t\tif vulnHandle != nil {\n\t\t\t\tfields[\"vulnerability\"] = vulnHandle.Name\n\t\t\t}\n\t\t\tif row.BlobValue != nil {\n\t\t\t\tfields[\"ranges\"] = row.BlobValue.String()\n\t\t\t}\n\t\t\tif row.OperatingSystem != nil {\n\t\t\t\tfields[\"os\"] = row.OperatingSystem\n\t\t\t}\n\t\t\tlog.WithFields(fields).Error(\"fix date validation failed\")\n\t\t\treturn fmt.Errorf(\"unable to validate fix dates: %w\", err)\n\t\t}\n\t}\n\n\t// Add affected package to child batch - defer VulnerabilityID assignment until flush\n\tpkgHandle := row\n\treturn w.addToChildBatch(func() error {\n\t\thandleCopy := pkgHandle\n\t\tif vulnHandle != nil {\n\t\t\thandleCopy.VulnerabilityID = vulnHandle.ID\n\t\t} else {\n\t\t\tlog.WithFields(\"package\", handleCopy.Package).Warn(\"affected package entry does not have a vulnerability ID\")\n\t\t}\n\t\treturn w.store.AddAffectedPackages(&handleCopy)\n\t})\n}\n\nfunc (w *writer) writeAffectedCPE(vulnHandle *db.VulnerabilityHandle, row db.AffectedCPEHandle) error {\n\t// Add affected CPE to child batch - defer VulnerabilityID assignment until flush\n\t// when the parent vulnerability has been written and ID is assigned\n\tcpeHandle := row\n\treturn w.addToChildBatch(func() error {\n\t\thandleCopy := cpeHandle\n\t\tif vulnHandle != nil {\n\t\t\thandleCopy.VulnerabilityID = vulnHandle.ID\n\t\t} else {\n\t\t\tlog.WithFields(\"cpe\", handleCopy.CPE).Warn(\"affected CPE entry does not have a vulnerability ID\")\n\t\t}\n\t\treturn w.store.AddAffectedCPEs(&handleCopy)\n\t})\n}\n\nfunc (w *writer) writeUnaffectedPackage(vulnHandle *db.VulnerabilityHandle, row db.UnaffectedPackageHandle) error {\n\t// Add unaffected package to child batch - defer VulnerabilityID assignment until flush\n\tpkgHandle := row\n\treturn w.addToChildBatch(func() error {\n\t\thandleCopy := pkgHandle\n\t\tif vulnHandle != nil {\n\t\t\thandleCopy.VulnerabilityID = vulnHandle.ID\n\t\t} else {\n\t\t\tlog.WithFields(\"package\", handleCopy.Package).Warn(\"unaffected package entry does not have a vulnerability ID\")\n\t\t}\n\t\treturn w.store.AddUnaffectedPackages(&handleCopy)\n\t})\n}\n\nfunc (w *writer) writeUnaffectedCPE(vulnHandle *db.VulnerabilityHandle, row db.UnaffectedCPEHandle) error {\n\t// Add unaffected CPE to child batch - defer VulnerabilityID assignment until flush\n\tcpeHandle := row\n\treturn w.addToChildBatch(func() error {\n\t\thandleCopy := cpeHandle\n\t\tif vulnHandle != nil {\n\t\t\thandleCopy.VulnerabilityID = vulnHandle.ID\n\t\t} else {\n\t\t\tlog.WithFields(\"cpe\", handleCopy.CPE).Warn(\"unaffected CPE entry does not have a vulnerability ID\")\n\t\t}\n\t\treturn w.store.AddUnaffectedCPEs(&handleCopy)\n\t})\n}\n\n// fillInMissingSeverity will add a severity entry to the vulnerability record if it is missing, empty, or \"unknown\".\n// The upstream NVD record is used to fill in these missing values. Note that the NVD provider is always guaranteed\n// to be processed first before other providers.\nfunc (w *writer) fillInMissingSeverity(handle *db.VulnerabilityHandle) {\n\tif handle == nil {\n\t\treturn\n\t}\n\n\tblob := handle.BlobValue\n\tif blob == nil {\n\t\treturn\n\t}\n\n\tid := strings.ToLower(blob.ID)\n\tisCVE := strings.HasPrefix(id, \"cve-\")\n\tif strings.ToLower(handle.ProviderID) == \"nvd\" && isCVE {\n\t\tif len(blob.Severities) > 0 {\n\t\t\tw.severityCache[id] = blob.Severities[0]\n\t\t}\n\t\treturn\n\t}\n\n\tif !isCVE {\n\t\treturn\n\t}\n\n\t// parse all string severities and remove all unknown values\n\tsevs := filterUnknownSeverities(blob.Severities)\n\n\ttopSevStr := \"none\"\n\tif len(sevs) > 0 {\n\t\tswitch v := sevs[0].Value.(type) {\n\t\tcase string:\n\t\t\ttopSevStr = v\n\t\tcase fmt.Stringer:\n\t\t\ttopSevStr = v.String()\n\t\tdefault:\n\t\t\ttopSevStr = fmt.Sprintf(\"%v\", sevs[0].Value)\n\t\t}\n\t}\n\n\tif len(sevs) > 0 {\n\t\treturn // already has a severity, don't normalize\n\t}\n\n\t// add the top NVD severity value\n\tnvdSev, ok := w.severityCache[id]\n\tif !ok {\n\t\tlog.WithFields(\"id\", blob.ID).Trace(\"unable to find NVD severity\")\n\t\treturn\n\t}\n\n\tlog.WithFields(\"id\", blob.ID, \"provider\", handle.Provider, \"sev-from\", topSevStr, \"sev-to\", nvdSev).Trace(\"overriding irrelevant severity with data from NVD record\")\n\tsevs = append([]db.Severity{nvdSev}, sevs...)\n\thandle.BlobValue.Severities = sevs\n}\n\n// addToParentBatch adds an operation to parent buffer and flushes when threshold reached\nfunc (w *writer) addToParentBatch(op func() error) error {\n\tw.mu.Lock()\n\tdefer w.mu.Unlock()\n\n\tw.parentBuffer = append(w.parentBuffer, op)\n\n\t// Flush parent batch when it reaches threshold to limit memory usage\n\tif len(w.parentBuffer) >= w.parentBatchSize {\n\t\treturn w.flushParentBatchLocked()\n\t}\n\treturn nil\n}\n\n// addToChildBatch adds an operation to child buffer and flushes when threshold reached\nfunc (w *writer) addToChildBatch(op func() error) error {\n\tw.mu.Lock()\n\tdefer w.mu.Unlock()\n\n\tw.childBuffer = append(w.childBuffer, op)\n\n\t// When child buffer is full, flush BOTH buffers (parents first for FK integrity)\n\tif len(w.childBuffer) >= w.childBatchSize {\n\t\t// Flush parents first to ensure IDs are assigned for children to reference\n\t\tif err := w.flushParentBatchLocked(); err != nil {\n\t\t\treturn err\n\t\t}\n\t\t// Then flush children\n\t\treturn w.flushChildBatchLocked()\n\t}\n\treturn nil\n}\n\n// flushParentBatch executes all pending parent operations in batches of parentBatchSize\nfunc (w *writer) flushParentBatch() error {\n\tw.mu.Lock()\n\tdefer w.mu.Unlock()\n\treturn w.flushParentBatchLocked()\n}\n\n// flushParentBatchLocked executes all pending parent operations (must be called with lock held)\nfunc (w *writer) flushParentBatchLocked() error {\n\tif len(w.parentBuffer) == 0 {\n\t\treturn nil\n\t}\n\n\tlog.WithFields(\"total_operations\", len(w.parentBuffer), \"batch_size\", w.parentBatchSize).Debug(\"flushing parent operations\")\n\n\t// Execute all accumulated operations\n\tfor j, op := range w.parentBuffer {\n\t\tif err := op(); err != nil {\n\t\t\treturn fmt.Errorf(\"parent operation %d failed: %w\", j, err)\n\t\t}\n\t}\n\n\tw.totalParentBatches++\n\tw.parentBuffer = w.parentBuffer[:0]\n\treturn nil\n}\n\n// flushChildBatch executes all pending child operations in batches of childBatchSize\nfunc (w *writer) flushChildBatch() error {\n\tw.mu.Lock()\n\tdefer w.mu.Unlock()\n\treturn w.flushChildBatchLocked()\n}\n\n// flushChildBatchLocked executes all pending child operations (must be called with lock held)\nfunc (w *writer) flushChildBatchLocked() error {\n\tif len(w.childBuffer) == 0 {\n\t\treturn nil\n\t}\n\n\tlog.WithFields(\"total_operations\", len(w.childBuffer), \"batch_size\", w.childBatchSize).Debug(\"flushing child operations\")\n\n\t// Execute all accumulated operations\n\tfor j, op := range w.childBuffer {\n\t\tif err := op(); err != nil {\n\t\t\treturn fmt.Errorf(\"child operation %d failed: %w\", j, err)\n\t\t}\n\t}\n\n\tw.totalChildBatches++\n\tw.childBuffer = w.childBuffer[:0]\n\treturn nil\n}\n\nfunc (w *writer) Close() error {\n\t// Flush any remaining batched operations (both parent and child)\n\tif err := w.flushParentBatch(); err != nil {\n\t\treturn fmt.Errorf(\"unable to flush parent batch: %w\", err)\n\t}\n\tif err := w.flushChildBatch(); err != nil {\n\t\treturn fmt.Errorf(\"unable to flush child batch: %w\", err)\n\t}\n\n\tif err := w.store.Close(); err != nil {\n\t\treturn fmt.Errorf(\"unable to close store: %w\", err)\n\t}\n\n\tlog.WithFields(\n\t\t\"path\", w.dbPath,\n\t\t\"parent_batches\", w.totalParentBatches,\n\t\t\"child_batches\", w.totalChildBatches,\n\t).Info(\"database created\")\n\n\treturn nil\n}\n\nfunc filterUnknownSeverities(sevs []db.Severity) []db.Severity {\n\tvar out []db.Severity\n\tfor _, s := range sevs {\n\t\tif isKnownSeverity(s) {\n\t\t\tout = append(out, s)\n\t\t}\n\t}\n\treturn out\n}\n\nfunc isKnownSeverity(s db.Severity) bool {\n\tswitch v := s.Value.(type) {\n\tcase string:\n\t\treturn v != \"\" && strings.ToLower(v) != \"unknown\"\n\tdefault:\n\t\treturn v != nil\n\t}\n}\n\nfunc ensureFixDates(row *db.AffectedPackageHandle) error {\n\tif row.BlobValue == nil {\n\t\treturn nil\n\t}\n\n\tfor _, r := range row.BlobValue.Ranges {\n\t\tif r.Fix == nil {\n\t\t\tcontinue\n\t\t}\n\t\tif !isFixVersion(r.Fix.Version) || r.Fix.State != db.FixedStatus {\n\t\t\tcontinue\n\t\t}\n\t\tif r.Fix.Detail == nil || r.Fix.Detail.Available == nil || r.Fix.Detail.Available.Date == nil {\n\t\t\treturn fmt.Errorf(\"missing fix date for version %q\", r.Fix.Version)\n\t\t}\n\t\tif r.Fix.Detail.Available.Date.IsZero() {\n\t\t\treturn fmt.Errorf(\"zero fix date for version %q\", r.Fix.Version)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc isFixVersion(v string) bool {\n\treturn v != \"\" && v != \"0\" && strings.ToLower(v) != \"none\"\n}\n\nfunc (w *writer) writeOperatingSystemEOL(row db.OperatingSystemEOLHandle) error {\n\tspec := db.OSSpecifier{\n\t\tName:         row.Name,\n\t\tMajorVersion: row.MajorVersion,\n\t\tMinorVersion: row.MinorVersion,\n\t\tLabelVersion: row.Codename,\n\t}\n\n\tupdated, err := w.store.UpdateOperatingSystemEOL(spec, row.EOLDate, row.EOASDate)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to update OS EOL data: %w\", err)\n\t}\n\n\tif updated == 0 {\n\t\tlog.WithFields(\"os\", row.String()).Trace(\"no OS record found to update with EOL data\")\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "grype/db/v6/build/writer_test.go",
    "content": "package v6\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/grype/grype/db/data\"\n\t\"github.com/anchore/grype/grype/db/provider\"\n\tdb \"github.com/anchore/grype/grype/db/v6\"\n\t\"github.com/anchore/grype/grype/db/v6/build/transformers\"\n)\n\nfunc TestFillInMissingSeverity(t *testing.T) {\n\ttests := []struct {\n\t\tname              string\n\t\thandle            *db.VulnerabilityHandle\n\t\tseverityCache     map[string]db.Severity\n\t\texpected          []db.Severity\n\t\texpectCacheUpdate bool\n\t}{\n\t\t{\n\t\t\tname:          \"nil handle\",\n\t\t\thandle:        nil,\n\t\t\tseverityCache: map[string]db.Severity{},\n\t\t\texpected:      nil,\n\t\t},\n\t\t{\n\t\t\tname: \"nil metadata\",\n\t\t\thandle: &db.VulnerabilityHandle{\n\t\t\t\tBlobValue: nil,\n\t\t\t},\n\t\t\tseverityCache: map[string]db.Severity{},\n\t\t\texpected:      nil,\n\t\t},\n\t\t{\n\t\t\tname: \"non-CVE ID\",\n\t\t\thandle: &db.VulnerabilityHandle{\n\t\t\t\tBlobValue: &db.VulnerabilityBlob{\n\t\t\t\t\tID: \"GHSA-123\",\n\t\t\t\t\tSeverities: []db.Severity{\n\t\t\t\t\t\t{Value: \"high\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tseverityCache: map[string]db.Severity{},\n\t\t\texpected:      []db.Severity{{Value: \"high\"}},\n\t\t},\n\t\t{\n\t\t\tname: \"NVD provider with CVE\",\n\t\t\thandle: &db.VulnerabilityHandle{\n\t\t\t\tProviderID: \"nvd\",\n\t\t\t\tBlobValue: &db.VulnerabilityBlob{\n\t\t\t\t\tID: \"CVE-2023-1234\",\n\t\t\t\t\tSeverities: []db.Severity{\n\t\t\t\t\t\t{Value: \"critical\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tseverityCache:     map[string]db.Severity{},\n\t\t\texpected:          []db.Severity{{Value: \"critical\"}},\n\t\t\texpectCacheUpdate: true,\n\t\t},\n\t\t{\n\t\t\tname: \"CVE with existing severities\",\n\t\t\thandle: &db.VulnerabilityHandle{\n\t\t\t\tProviderID: \"github\",\n\t\t\t\tBlobValue: &db.VulnerabilityBlob{\n\t\t\t\t\tID: \"CVE-2023-5678\",\n\t\t\t\t\tSeverities: []db.Severity{\n\t\t\t\t\t\t{Value: \"medium\"},\n\t\t\t\t\t\t{Value: \"high\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tseverityCache: map[string]db.Severity{\n\t\t\t\t\"cve-2023-5678\": {Value: \"critical\"},\n\t\t\t},\n\t\t\texpected: []db.Severity{\n\t\t\t\t{Value: \"medium\"},\n\t\t\t\t{Value: \"high\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"CVE with no severities, using cache\",\n\t\t\thandle: &db.VulnerabilityHandle{\n\t\t\t\tProviderID: \"github\",\n\t\t\t\tBlobValue: &db.VulnerabilityBlob{\n\t\t\t\t\tID:         \"CVE-2023-9012\",\n\t\t\t\t\tSeverities: []db.Severity{},\n\t\t\t\t},\n\t\t\t},\n\t\t\tseverityCache: map[string]db.Severity{\n\t\t\t\t\"cve-2023-9012\": {Value: \"high\"},\n\t\t\t},\n\t\t\texpected: []db.Severity{{Value: \"high\"}},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tw := &writer{\n\t\t\t\tseverityCache: tt.severityCache,\n\t\t\t}\n\n\t\t\tif tt.expectCacheUpdate {\n\t\t\t\t// assert expected ids are not in the cache\n\t\t\t\tif tt.handle != nil && tt.handle.BlobValue != nil {\n\t\t\t\t\tassert.NotContains(t, tt.severityCache, strings.ToLower(tt.handle.BlobValue.ID))\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tw.fillInMissingSeverity(tt.handle)\n\n\t\t\tif tt.handle == nil || tt.handle.BlobValue == nil {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif tt.expectCacheUpdate {\n\t\t\t\t// assert expected ids are not in the cache\n\t\t\t\tif tt.handle != nil && tt.handle.BlobValue != nil {\n\t\t\t\t\tid := strings.ToLower(tt.handle.BlobValue.ID)\n\t\t\t\t\tassert.Equal(t, tt.severityCache[id], w.severityCache[id])\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tassert.Equal(t, tt.expected, tt.handle.BlobValue.Severities)\n\t\t})\n\t}\n}\n\nfunc TestFilterUnknownSeverities(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    []db.Severity\n\t\texpected []db.Severity\n\t}{\n\t\t{\n\t\t\tname:     \"empty input\",\n\t\t\tinput:    []db.Severity{},\n\t\t\texpected: nil,\n\t\t},\n\t\t{\n\t\t\tname: \"all known severities\",\n\t\t\tinput: []db.Severity{\n\t\t\t\t{Value: \"critical\"},\n\t\t\t\t{Value: \"high\"},\n\t\t\t\t{Value: \"medium\"},\n\t\t\t},\n\t\t\texpected: []db.Severity{\n\t\t\t\t{Value: \"critical\"},\n\t\t\t\t{Value: \"high\"},\n\t\t\t\t{Value: \"medium\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"mix of known and unknown\",\n\t\t\tinput: []db.Severity{\n\t\t\t\t{Value: \"high\"},\n\t\t\t\t{Value: \"unknown\"},\n\t\t\t\t{Value: \"medium\"},\n\t\t\t\t{Value: \"\"},\n\t\t\t},\n\t\t\texpected: []db.Severity{\n\t\t\t\t{Value: \"high\"},\n\t\t\t\t{Value: \"medium\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"non-string values\",\n\t\t\tinput: []db.Severity{\n\t\t\t\t{Value: 5},\n\t\t\t\t{Value: nil},\n\t\t\t\t{Value: \"high\"},\n\t\t\t},\n\t\t\texpected: []db.Severity{\n\t\t\t\t{Value: 5},\n\t\t\t\t{Value: \"high\"},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := filterUnknownSeverities(tt.input)\n\t\t\tassert.Equal(t, tt.expected, result)\n\t\t})\n\t}\n}\n\nfunc TestIsKnownSeverity(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tseverity db.Severity\n\t\texpected bool\n\t}{\n\t\t{\n\t\t\tname:     \"empty string\",\n\t\t\tseverity: db.Severity{Value: \"\"},\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"unknown string\",\n\t\t\tseverity: db.Severity{Value: \"unknown\"},\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"case insensitive\",\n\t\t\tseverity: db.Severity{Value: \"UNKNOWN\"},\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"valid string severity\",\n\t\t\tseverity: db.Severity{Value: \"high\"},\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"nil value\",\n\t\t\tseverity: db.Severity{Value: nil},\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"numeric value\",\n\t\t\tseverity: db.Severity{Value: 7},\n\t\t\texpected: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := isKnownSeverity(tt.severity)\n\t\t\tassert.Equal(t, tt.expected, result)\n\t\t})\n\t}\n}\n\nfunc TestEnsureFixDates(t *testing.T) {\n\tvalidDate := time.Date(2023, 1, 15, 0, 0, 0, 0, time.UTC)\n\tzeroDate := time.Time{}\n\n\ttests := []struct {\n\t\tname    string\n\t\trow     *db.AffectedPackageHandle\n\t\twantErr require.ErrorAssertionFunc\n\t}{\n\t\t{\n\t\t\tname: \"nil BlobValue\",\n\t\t\trow: &db.AffectedPackageHandle{\n\t\t\t\tBlobValue: nil,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"empty ranges\",\n\t\t\trow: &db.AffectedPackageHandle{\n\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\tRanges: []db.Range{},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"range with nil Fix\",\n\t\t\trow: &db.AffectedPackageHandle{\n\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t{Fix: nil},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"range with empty Fix.Version\",\n\t\t\trow: &db.AffectedPackageHandle{\n\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t{Fix: &db.Fix{Version: \"\"}},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"range with Fix.Version '0' - skipped by isFixVersion\",\n\t\t\trow: &db.AffectedPackageHandle{\n\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tFix: &db.Fix{\n\t\t\t\t\t\t\t\tVersion: \"0\", // invalid version - validation skipped\n\t\t\t\t\t\t\t\tState:   db.FixedStatus,\n\t\t\t\t\t\t\t\tDetail:  nil, // no date but should not error\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"range with Fix.Version 'none' - skipped by isFixVersion\",\n\t\t\trow: &db.AffectedPackageHandle{\n\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tFix: &db.Fix{\n\t\t\t\t\t\t\t\tVersion: \"none\", // invalid version - validation skipped\n\t\t\t\t\t\t\t\tState:   db.FixedStatus,\n\t\t\t\t\t\t\t\tDetail:  nil, // no date but should not error\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"range with Fix.Version 'NONE' (case insensitive) - skipped by isFixVersion\",\n\t\t\trow: &db.AffectedPackageHandle{\n\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tFix: &db.Fix{\n\t\t\t\t\t\t\t\tVersion: \"NONE\", // invalid version - validation skipped\n\t\t\t\t\t\t\t\tState:   db.FixedStatus,\n\t\t\t\t\t\t\t\tDetail:  nil, // no date but should not error\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"range with Fix.State not FixedStatus\",\n\t\t\trow: &db.AffectedPackageHandle{\n\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tFix: &db.Fix{\n\t\t\t\t\t\t\t\tVersion: \"1.2.3\",\n\t\t\t\t\t\t\t\tState:   db.NotAffectedFixStatus,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"valid fix with proper date\",\n\t\t\trow: &db.AffectedPackageHandle{\n\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tFix: &db.Fix{\n\t\t\t\t\t\t\t\tVersion: \"1.2.3\",\n\t\t\t\t\t\t\t\tState:   db.FixedStatus,\n\t\t\t\t\t\t\t\tDetail: &db.FixDetail{\n\t\t\t\t\t\t\t\t\tAvailable: &db.FixAvailability{\n\t\t\t\t\t\t\t\t\t\tDate: &validDate,\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"valid version requires date validation\",\n\t\t\trow: &db.AffectedPackageHandle{\n\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tFix: &db.Fix{\n\t\t\t\t\t\t\t\tVersion: \"1.2.3\", // valid version - validation required\n\t\t\t\t\t\t\t\tState:   db.FixedStatus,\n\t\t\t\t\t\t\t\tDetail:  nil, // no date should cause error\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantErr: require.Error,\n\t\t},\n\t\t{\n\t\t\tname: \"multiple ranges with valid dates\",\n\t\t\trow: &db.AffectedPackageHandle{\n\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tFix: &db.Fix{\n\t\t\t\t\t\t\t\tVersion: \"1.2.3\",\n\t\t\t\t\t\t\t\tState:   db.FixedStatus,\n\t\t\t\t\t\t\t\tDetail: &db.FixDetail{\n\t\t\t\t\t\t\t\t\tAvailable: &db.FixAvailability{\n\t\t\t\t\t\t\t\t\t\tDate: &validDate,\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tFix: &db.Fix{\n\t\t\t\t\t\t\t\tVersion: \"2.0.0\",\n\t\t\t\t\t\t\t\tState:   db.FixedStatus,\n\t\t\t\t\t\t\t\tDetail: &db.FixDetail{\n\t\t\t\t\t\t\t\t\tAvailable: &db.FixAvailability{\n\t\t\t\t\t\t\t\t\t\tDate: &validDate,\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"mix of valid and nil Fix ranges\",\n\t\t\trow: &db.AffectedPackageHandle{\n\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t{Fix: nil},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tFix: &db.Fix{\n\t\t\t\t\t\t\t\tVersion: \"1.2.3\",\n\t\t\t\t\t\t\t\tState:   db.FixedStatus,\n\t\t\t\t\t\t\t\tDetail: &db.FixDetail{\n\t\t\t\t\t\t\t\t\tAvailable: &db.FixAvailability{\n\t\t\t\t\t\t\t\t\t\tDate: &validDate,\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{Fix: &db.Fix{Version: \"\"}},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"missing Fix.Detail with valid version\",\n\t\t\trow: &db.AffectedPackageHandle{\n\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tFix: &db.Fix{\n\t\t\t\t\t\t\t\tVersion: \"1.2.3\", // valid version triggers validation\n\t\t\t\t\t\t\t\tState:   db.FixedStatus,\n\t\t\t\t\t\t\t\tDetail:  nil,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantErr: require.Error,\n\t\t},\n\t\t{\n\t\t\tname: \"missing Fix.Detail.Available with valid version\",\n\t\t\trow: &db.AffectedPackageHandle{\n\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tFix: &db.Fix{\n\t\t\t\t\t\t\t\tVersion: \"2.0.0\", // valid version triggers validation\n\t\t\t\t\t\t\t\tState:   db.FixedStatus,\n\t\t\t\t\t\t\t\tDetail: &db.FixDetail{\n\t\t\t\t\t\t\t\t\tAvailable: nil,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantErr: require.Error,\n\t\t},\n\t\t{\n\t\t\tname: \"missing Fix.Detail.Available.Date with valid version\",\n\t\t\trow: &db.AffectedPackageHandle{\n\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tFix: &db.Fix{\n\t\t\t\t\t\t\t\tVersion: \"v1.0.0\", // valid version triggers validation\n\t\t\t\t\t\t\t\tState:   db.FixedStatus,\n\t\t\t\t\t\t\t\tDetail: &db.FixDetail{\n\t\t\t\t\t\t\t\t\tAvailable: &db.FixAvailability{\n\t\t\t\t\t\t\t\t\t\tDate: nil,\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantErr: require.Error,\n\t\t},\n\t\t{\n\t\t\tname: \"zero Fix.Detail.Available.Date with valid version\",\n\t\t\trow: &db.AffectedPackageHandle{\n\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tFix: &db.Fix{\n\t\t\t\t\t\t\t\tVersion: \"3.1.4\", // valid version triggers validation\n\t\t\t\t\t\t\t\tState:   db.FixedStatus,\n\t\t\t\t\t\t\t\tDetail: &db.FixDetail{\n\t\t\t\t\t\t\t\t\tAvailable: &db.FixAvailability{\n\t\t\t\t\t\t\t\t\t\tDate: &zeroDate,\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantErr: require.Error,\n\t\t},\n\t\t{\n\t\t\tname: \"multiple ranges with one missing date and valid versions\",\n\t\t\trow: &db.AffectedPackageHandle{\n\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tFix: &db.Fix{\n\t\t\t\t\t\t\t\tVersion: \"1.2.3\", // valid version triggers validation\n\t\t\t\t\t\t\t\tState:   db.FixedStatus,\n\t\t\t\t\t\t\t\tDetail: &db.FixDetail{\n\t\t\t\t\t\t\t\t\tAvailable: &db.FixAvailability{\n\t\t\t\t\t\t\t\t\t\tDate: &validDate,\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tFix: &db.Fix{\n\t\t\t\t\t\t\t\tVersion: \"2.0.0\", // valid version triggers validation\n\t\t\t\t\t\t\t\tState:   db.FixedStatus,\n\t\t\t\t\t\t\t\tDetail: &db.FixDetail{\n\t\t\t\t\t\t\t\t\tAvailable: &db.FixAvailability{\n\t\t\t\t\t\t\t\t\t\tDate: nil,\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantErr: require.Error,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif tt.wantErr == nil {\n\t\t\t\ttt.wantErr = require.NoError\n\t\t\t}\n\n\t\t\terr := ensureFixDates(tt.row)\n\t\t\ttt.wantErr(t, err)\n\t\t})\n\t}\n}\n\nfunc TestWrite_FailsOnMissingFixDate(t *testing.T) {\n\t// test proves that Write() method errors out when fix date validation is enabled\n\t// and a fix is marked as FixedStatus but lacks the required date information\n\tw := &writer{\n\t\tfailOnMissingFixDate: true,\n\t\tstore:                nil, // intentionally nil - we should error before reaching store operations\n\t\tseverityCache:        make(map[string]db.Severity),\n\t}\n\n\tvar vulnID db.ID = 123\n\n\tentry := data.Entry{\n\t\tDBSchemaVersion: db.ModelVersion,\n\t\tData: transformers.RelatedEntries{\n\t\t\tVulnerabilityHandle: nil, // no vulnerability handle to avoid store operations\n\t\t\tRelated: []any{\n\t\t\t\tdb.AffectedPackageHandle{\n\t\t\t\t\tVulnerabilityID: vulnID,\n\t\t\t\t\tPackage:         &db.Package{Name: \"test-package\"},\n\t\t\t\t\tBlobValue: &db.PackageBlob{\n\t\t\t\t\t\tRanges: []db.Range{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tFix: &db.Fix{\n\t\t\t\t\t\t\t\t\tVersion: \"1.2.3\", // valid version triggers validation\n\t\t\t\t\t\t\t\t\tState:   db.FixedStatus,\n\t\t\t\t\t\t\t\t\tDetail:  nil, // missing fix detail should cause error\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\terr := w.Write(entry)\n\trequire.Error(t, err)\n\trequire.Contains(t, err.Error(), \"unable to validate fix dates\")\n\trequire.Contains(t, err.Error(), \"missing fix date for version \\\"1.2.3\\\"\")\n}\n\nfunc TestIsFixVersion(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tversion  string\n\t\texpected bool\n\t}{\n\t\t{\n\t\t\tname:     \"empty string\",\n\t\t\tversion:  \"\",\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"zero version\",\n\t\t\tversion:  \"0\",\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"none lowercase\",\n\t\t\tversion:  \"none\",\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"none uppercase\",\n\t\t\tversion:  \"NONE\",\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"none mixed case\",\n\t\t\tversion:  \"None\",\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"valid semantic version\",\n\t\t\tversion:  \"1.2.3\",\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"valid version with prefix\",\n\t\t\tversion:  \"v1.2.3\",\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"valid version with patch level\",\n\t\t\tversion:  \"2.4.1-rc1\",\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"valid commit hash\",\n\t\t\tversion:  \"abc123def\",\n\t\t\texpected: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := isFixVersion(tt.version)\n\t\t\trequire.Equal(t, tt.expected, result)\n\t\t})\n\t}\n}\n\nfunc TestBatchedWritesEquivalence(t *testing.T) {\n\t// Test that batched writes produce identical database output to unbatched writes\n\t// This is the critical correctness test for the batching optimization\n\n\ttestCases := []struct {\n\t\tname       string\n\t\tbatchSize  int\n\t\tnumEntries int\n\t}{\n\t\t{\n\t\t\tname:       \"unbatched (batch_size=1)\",\n\t\t\tbatchSize:  1,\n\t\t\tnumEntries: 50,\n\t\t},\n\t\t{\n\t\t\tname:       \"small batch\",\n\t\t\tbatchSize:  10,\n\t\t\tnumEntries: 50,\n\t\t},\n\t\t{\n\t\t\tname:       \"large batch\",\n\t\t\tbatchSize:  2000,\n\t\t\tnumEntries: 50,\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\t// Create temp directory for database\n\t\t\ttmpDir := t.TempDir()\n\n\t\t\t// Create writer with specified batch size\n\t\t\tw, err := NewWriter(tmpDir, provider.States{}, false, tc.batchSize)\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// Write test entries\n\t\t\tentries := createTestEntries(tc.numEntries)\n\t\t\tfor _, entry := range entries {\n\t\t\t\terr := w.Write(entry)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\n\t\t\t// Close to flush all batches\n\t\t\terr = w.Close()\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// Verify database was created\n\t\t\tdbPath := filepath.Join(tmpDir, \"vulnerability.db\")\n\t\t\t_, err = os.Stat(dbPath)\n\t\t\trequire.NoError(t, err, \"database file should exist\")\n\n\t\t\t// Open and verify database contents\n\t\t\treader, err := db.NewReader(db.Config{DBDirPath: tmpDir})\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer reader.Close()\n\n\t\t\t// Basic validation: verify we can read data back\n\t\t\t// More detailed validation would require actual query methods\n\t\t\t// but this proves the database is valid and readable\n\t\t})\n\t}\n}\n\nfunc TestBatchAccumulation(t *testing.T) {\n\t// Test that operations accumulate in buffers before flushing\n\ttmpDir := t.TempDir()\n\n\tw, err := NewWriter(tmpDir, provider.States{}, false, 1000)\n\trequire.NoError(t, err)\n\n\twriterImpl := w.(*writer)\n\n\t// Write 50 entries (below batch threshold of 1000)\n\tentries := createTestEntries(50)\n\tfor _, entry := range entries {\n\t\terr := writerImpl.Write(entry)\n\t\trequire.NoError(t, err)\n\t}\n\n\t// Verify buffers contain accumulated operations (not flushed yet)\n\tassert.Greater(t, len(writerImpl.parentBuffer), 0, \"parent buffer should contain operations\")\n\tassert.Greater(t, len(writerImpl.childBuffer), 0, \"child buffer should contain operations\")\n\tassert.Equal(t, 0, writerImpl.totalParentBatches, \"should not have flushed yet\")\n\tassert.Equal(t, 0, writerImpl.totalChildBatches, \"should not have flushed yet\")\n\n\t// Close should flush everything\n\terr = writerImpl.Close()\n\trequire.NoError(t, err)\n\n\t// Verify buffers were flushed\n\tassert.Equal(t, 0, len(writerImpl.parentBuffer), \"parent buffer should be empty after close\")\n\tassert.Equal(t, 0, len(writerImpl.childBuffer), \"child buffer should be empty after close\")\n\tassert.Greater(t, writerImpl.totalParentBatches, 0, \"should have flushed parent batch\")\n\tassert.Greater(t, writerImpl.totalChildBatches, 0, \"should have flushed child batch\")\n}\n\nfunc TestBatchMetrics(t *testing.T) {\n\t// Test that batch counts accurately reflect number of flushes\n\ttmpDir := t.TempDir()\n\n\tbatchSize := 25\n\tnumEntries := 100\n\n\tw, err := NewWriter(tmpDir, provider.States{}, false, batchSize)\n\trequire.NoError(t, err)\n\n\twriterImpl := w.(*writer)\n\n\t// Write entries\n\tentries := createTestEntries(numEntries)\n\tfor _, entry := range entries {\n\t\terr := writerImpl.Write(entry)\n\t\trequire.NoError(t, err)\n\t}\n\n\terr = writerImpl.Close()\n\trequire.NoError(t, err)\n\n\t// Verify batch counts\n\t// With 100 entries, batchSize=25:\n\t// - Parent ops: 100 vulnerabilities / 25 = 4 batches\n\t// - Child ops: depends on children per entry, but should also batch\n\tassert.Greater(t, writerImpl.totalParentBatches, 0, \"should have parent batches\")\n\tassert.Greater(t, writerImpl.totalChildBatches, 0, \"should have child batches\")\n}\n\nfunc TestBatchSizeConfiguration(t *testing.T) {\n\t// Test that batch size defaults and configuration work correctly\n\ttmpDir := t.TempDir()\n\n\ttests := []struct {\n\t\tname         string\n\t\tinputSize    int\n\t\texpectedSize int\n\t}{\n\t\t{\n\t\t\tname:         \"default (0 -> 2000)\",\n\t\t\tinputSize:    0,\n\t\t\texpectedSize: 2000,\n\t\t},\n\t\t{\n\t\t\tname:         \"custom size\",\n\t\t\tinputSize:    500,\n\t\t\texpectedSize: 500,\n\t\t},\n\t\t{\n\t\t\tname:         \"unbatched mode\",\n\t\t\tinputSize:    1,\n\t\t\texpectedSize: 1,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tw, err := NewWriter(tmpDir, provider.States{}, false, tt.inputSize)\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer w.Close()\n\n\t\t\twriterImpl := w.(*writer)\n\t\t\tassert.Equal(t, tt.expectedSize, writerImpl.parentBatchSize)\n\t\t\tassert.Equal(t, tt.expectedSize, writerImpl.childBatchSize)\n\t\t})\n\t}\n}\n\n// createTestEntries creates test entries with unique identifiable content\nfunc createTestEntries(count int) []data.Entry {\n\tentries := make([]data.Entry, count)\n\n\tfor i := 0; i < count; i++ {\n\t\tentries[i] = data.Entry{\n\t\t\tDBSchemaVersion: db.ModelVersion,\n\t\t\tData: transformers.RelatedEntries{\n\t\t\t\tVulnerabilityHandle: &db.VulnerabilityHandle{\n\t\t\t\t\tName:       \"CVE-2023-TEST\",\n\t\t\t\t\tProviderID: \"test-provider\",\n\t\t\t\t\tProvider: &db.Provider{\n\t\t\t\t\t\tID:      \"test-provider\",\n\t\t\t\t\t\tVersion: \"1.0.0\",\n\t\t\t\t\t},\n\t\t\t\t\tBlobValue: &db.VulnerabilityBlob{\n\t\t\t\t\t\tID: \"CVE-2023-TEST\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tRelated: []any{\n\t\t\t\t\tdb.CWEHandle{\n\t\t\t\t\t\tCVE: \"CVE-2023-TEST\",\n\t\t\t\t\t\tCWE: \"CWE-79\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\t}\n\n\treturn entries\n}\n"
  },
  {
    "path": "grype/db/v6/cache.go",
    "content": "package v6\n\nimport (\n\t\"context\"\n\t\"sync\"\n)\n\nconst (\n\tcpesTableCacheKey             = \"cpes\"\n\tpackagesTableCacheKey         = \"packages\"\n\toperatingSystemsTableCacheKey = \"operating_systems\"\n\tvulnerabilitiesTableCacheKey  = \"vulnerabilities\"\n)\n\nconst cacheKey = contextKey(\"multiModelCache\")\n\ntype contextKey string\n\ntype cachable interface {\n\tcacheKey() string\n\ttableName() string\n}\n\ntype cacheIDManager interface {\n\trowID() ID\n\tsetRowID(ID)\n}\n\ntype cacheStringIDManager interface {\n\trowID() string\n\tsetRowID(string)\n}\n\nfunc withCacheContext(ctx context.Context, c *cache) context.Context {\n\treturn context.WithValue(ctx, cacheKey, c)\n}\n\nfunc cacheFromContext(ctx context.Context) (*cache, bool) {\n\tc, ok := ctx.Value(cacheKey).(*cache)\n\treturn c, ok\n}\n\ntype cache struct {\n\tmu      sync.RWMutex\n\tidKeys  map[string]map[string]ID\n\tstrKeys map[string]map[string]string\n}\n\nfunc newCache() *cache {\n\treturn &cache{\n\t\tidKeys:  make(map[string]map[string]ID),\n\t\tstrKeys: make(map[string]map[string]string),\n\t}\n}\n\nfunc (c *cache) getID(ca cachable) (ID, bool) {\n\tc.mu.RLock()\n\tdefer c.mu.RUnlock()\n\tif tableCache, exists := c.idKeys[ca.tableName()]; exists {\n\t\tid, found := tableCache[ca.cacheKey()]\n\t\treturn id, found\n\t}\n\treturn 0, false\n}\n\nfunc (c *cache) getString(ca cachable) (string, bool) {\n\tc.mu.RLock()\n\tdefer c.mu.RUnlock()\n\tif tableCache, exists := c.strKeys[ca.tableName()]; exists {\n\t\tid, found := tableCache[ca.cacheKey()]\n\t\treturn id, found\n\t}\n\treturn \"\", false\n}\n\nfunc (c *cache) set(ca cachable) {\n\tswitch cam := ca.(type) {\n\tcase cacheIDManager:\n\t\tc.setIDEntry(cam.rowID(), ca)\n\tcase cacheStringIDManager:\n\t\tc.setStringEntry(cam.rowID(), ca)\n\tdefault:\n\t\tpanic(\"unsupported cacheable type\")\n\t}\n}\n\nfunc (c *cache) setStringEntry(id string, ca cachable) {\n\ttable := ca.tableName()\n\tkey := ca.cacheKey()\n\n\tc.mu.Lock()\n\tdefer c.mu.Unlock()\n\n\tif _, exists := c.strKeys[table]; !exists {\n\t\tc.strKeys[table] = make(map[string]string)\n\t}\n\n\tc.strKeys[table][key] = id\n}\n\nfunc (c *cache) setIDEntry(id ID, ca cachable) {\n\ttable := ca.tableName()\n\tkey := ca.cacheKey()\n\n\tc.mu.Lock()\n\tdefer c.mu.Unlock()\n\n\tif _, exists := c.idKeys[table]; !exists {\n\t\tc.idKeys[table] = make(map[string]ID)\n\t}\n\n\tc.idKeys[table][key] = id\n}\n"
  },
  {
    "path": "grype/db/v6/cache_test.go",
    "content": "package v6\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\ntype mockCachableID struct {\n\tkey   string\n\ttable string\n\tid    ID\n}\n\nfunc (m *mockCachableID) cacheKey() string  { return m.key }\nfunc (m *mockCachableID) tableName() string { return m.table }\nfunc (m *mockCachableID) rowID() ID         { return m.id }\nfunc (m *mockCachableID) setRowID(id ID)    { m.id = id }\n\ntype mockCachableString struct {\n\tkey   string\n\ttable string\n\tid    string\n}\n\nfunc (m *mockCachableString) cacheKey() string   { return m.key }\nfunc (m *mockCachableString) tableName() string  { return m.table }\nfunc (m *mockCachableString) rowID() string      { return m.id }\nfunc (m *mockCachableString) setRowID(id string) { m.id = id }\n\nfunc newTestCachableString(key, table, id string) *mockCachableString {\n\treturn &mockCachableString{key: key, table: table, id: id}\n}\n\nfunc newTestCachableID(key, table string, id ID) *mockCachableID {\n\treturn &mockCachableID{key: key, table: table, id: id}\n}\n\nfunc TestCache_GetString_Found(t *testing.T) {\n\tc := newCache()\n\titem := newTestCachableString(\"test-key\", \"test-table\", \"test-id\")\n\n\tc.setStringEntry(\"test-id\", item)\n\n\tstr, found := c.getString(item)\n\trequire.True(t, found)\n\trequire.Equal(t, \"test-id\", str)\n}\n\nfunc TestCache_GetString_NotFound(t *testing.T) {\n\tc := newCache()\n\titem := newTestCachableString(\"missing-key\", \"test-table\", \"\")\n\n\tstr, found := c.getString(item)\n\trequire.False(t, found)\n\trequire.Empty(t, str)\n}\n\nfunc TestCache_Set_ID(t *testing.T) {\n\tc := newCache()\n\titem := newTestCachableID(\"test-key\", \"test-table\", 123)\n\n\tc.set(item)\n\n\tid, found := c.getID(item)\n\trequire.True(t, found)\n\trequire.Equal(t, ID(123), id)\n}\n\nfunc TestCache_Set_String(t *testing.T) {\n\tc := newCache()\n\titem := newTestCachableString(\"test-key\", \"test-table\", \"test-id\")\n\n\tc.set(item)\n\n\tstr, found := c.getString(item)\n\trequire.True(t, found)\n\trequire.Equal(t, \"test-id\", str)\n}\n\nfunc TestCache_Set_Panic(t *testing.T) {\n\tc := newCache()\n\tinvalidItem := struct{ cachable }{}\n\n\trequire.PanicsWithValue(t, \"unsupported cacheable type\", func() {\n\t\tc.set(invalidItem)\n\t})\n}\n\nfunc TestCache_SetStringEntry_New(t *testing.T) {\n\tc := newCache()\n\titem := newTestCachableString(\"test-key\", \"test-table\", \"\")\n\n\tc.setStringEntry(\"new-id\", item)\n\n\tstr, found := c.getString(item)\n\trequire.True(t, found)\n\trequire.Equal(t, \"new-id\", str)\n}\n\nfunc TestCache_SetStringEntry_Update(t *testing.T) {\n\tc := newCache()\n\titem := newTestCachableString(\"test-key\", \"test-table\", \"old-id\")\n\n\tc.setStringEntry(\"old-id\", item)\n\tc.setStringEntry(\"new-id\", item)\n\n\tstr, found := c.getString(item)\n\trequire.True(t, found)\n\trequire.Equal(t, \"new-id\", str)\n}\n\nfunc TestCache_SetIDEntry_New(t *testing.T) {\n\tc := newCache()\n\titem := newTestCachableID(\"test-key\", \"test-table\", 0)\n\n\tc.setIDEntry(123, item)\n\n\tid, found := c.getID(item)\n\trequire.True(t, found)\n\trequire.Equal(t, ID(123), id)\n}\n\nfunc TestCache_SetIDEntry_Update(t *testing.T) {\n\tc := newCache()\n\titem := newTestCachableID(\"test-key\", \"test-table\", 123)\n\n\tc.setIDEntry(123, item)\n\tc.setIDEntry(456, item)\n\n\tid, found := c.getID(item)\n\trequire.True(t, found)\n\trequire.Equal(t, ID(456), id)\n}\n\nfunc TestWithCacheContext(t *testing.T) {\n\tc := newCache()\n\tctx := withCacheContext(context.Background(), c)\n\n\tcache, ok := cacheFromContext(ctx)\n\trequire.True(t, ok)\n\trequire.Equal(t, c, cache)\n}\n\nfunc TestCacheFromContext_NotFound(t *testing.T) {\n\tcache, ok := cacheFromContext(context.Background())\n\trequire.False(t, ok)\n\trequire.Nil(t, cache)\n}\n"
  },
  {
    "path": "grype/db/v6/cpe_store.go",
    "content": "package v6\n\nimport (\n\t\"fmt\"\n\t\"time\"\n\n\t\"gorm.io/gorm\"\n\n\t\"github.com/anchore/go-logger\"\n\t\"github.com/anchore/grype/internal/log\"\n\t\"github.com/anchore/syft/syft/cpe\"\n)\n\ntype cpeHandleStore interface {\n\t*AffectedCPEHandle | *UnaffectedCPEHandle\n}\n\ntype cpeHandleAccessor interface {\n\tgetCPEHandle() *cpeHandle\n}\n\ntype GetCPEOptions struct {\n\tPreloadCPE            bool\n\tPreloadVulnerability  bool\n\tPreloadBlob           bool\n\tVulnerabilities       []VulnerabilitySpecifier\n\tAllowBroadCPEMatching bool\n\tLimit                 int\n}\n\ntype cpeStore struct {\n\tdb        *gorm.DB\n\tblobStore *blobStore\n}\n\nfunc newCPEStore(db *gorm.DB, bs *blobStore) *cpeStore {\n\treturn &cpeStore{\n\t\tdb:        db,\n\t\tblobStore: bs,\n\t}\n}\n\nfunc addCPEs[T cpeHandleStore](s *cpeStore, packages ...T) error {\n\tcacheInst, ok := cacheFromContext(s.db.Statement.Context)\n\tif !ok {\n\t\treturn fmt.Errorf(\"unable to fetch CPE cache from context\")\n\t}\n\n\tvar final []*Cpe\n\tbyCacheKey := make(map[string][]*Cpe)\n\n\tfor _, p := range packages {\n\t\tch := any(p).(cpeHandleAccessor).getCPEHandle()\n\n\t\tif ch.CPE != nil {\n\t\t\tkey := ch.CPE.cacheKey()\n\t\t\tif existingID, ok := cacheInst.getID(ch.CPE); ok {\n\t\t\t\t// seen in a previous transaction...\n\t\t\t\tch.CpeID = existingID\n\t\t\t} else if _, ok := byCacheKey[key]; !ok {\n\t\t\t\t// not seen within this transaction\n\t\t\t\tfinal = append(final, ch.CPE)\n\t\t\t}\n\t\t\tbyCacheKey[key] = append(byCacheKey[key], ch.CPE)\n\t\t}\n\t}\n\n\tif len(final) == 0 {\n\t\treturn nil\n\t}\n\n\tif err := s.db.Create(final).Error; err != nil {\n\t\treturn fmt.Errorf(\"unable to create CPE records: %w\", err)\n\t}\n\n\t// update the cache with the new records\n\tfor _, ref := range final {\n\t\tcacheInst.set(ref)\n\t}\n\n\t// update all references with the IDs from the cache\n\tfor _, refs := range byCacheKey {\n\t\tfor _, ref := range refs {\n\t\t\tid, ok := cacheInst.getID(ref)\n\t\t\tif ok {\n\t\t\t\tref.setRowID(id)\n\t\t\t}\n\t\t}\n\t}\n\n\t// update the parent objects with the FK ID\n\tfor _, p := range packages {\n\t\tch := any(p).(cpeHandleAccessor).getCPEHandle()\n\t\tif ch.CPE != nil {\n\t\t\tch.CpeID = ch.CPE.ID\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc addCPEHandles[T cpeHandleStore](s *cpeStore, packages ...T) error {\n\tif err := addCPEs(s, packages...); err != nil {\n\t\treturn fmt.Errorf(\"unable to add CPEs from CPE handles: %w\", err)\n\t}\n\n\tfor _, pkg := range packages {\n\t\tif err := s.blobStore.addBlobable(any(pkg).(blobable)); err != nil {\n\t\t\treturn fmt.Errorf(\"unable to add CPE handle blob: %w\", err)\n\t\t}\n\n\t\tif err := s.db.Omit(\"CPE\").Create(pkg).Error; err != nil {\n\t\t\treturn fmt.Errorf(\"unable to add CPE handles: %w\", err)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc getCPEHandles[T cpeHandleStore]( // nolint:funlen\n\ts *cpeStore,\n\tcpe *cpe.Attributes,\n\tconfig *GetCPEOptions,\n\ttableName string,\n) ([]T, error) {\n\tif config == nil {\n\t\tconfig = &GetCPEOptions{}\n\t}\n\n\tfields := make(logger.Fields)\n\tcount := 0\n\tif cpe == nil {\n\t\tfields[\"cpe\"] = \"any\"\n\t} else {\n\t\tfields[\"cpe\"] = cpe.String()\n\t}\n\tstart := time.Now()\n\tdefer func() {\n\t\tfields[\"duration\"] = time.Since(start)\n\t\tfields[\"records\"] = count\n\t\tlog.WithFields(fields).Trace(\"fetched CPE record\")\n\t}()\n\n\tquery := s.handleCPE(s.db.Table(tableName), cpe, config.AllowBroadCPEMatching, tableName)\n\n\tvar err error\n\tquery, err = s.handleVulnerabilityOptions(query, config.Vulnerabilities, tableName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tquery = s.handlePreload(query, *config)\n\n\tvar models []T\n\tvar results []T\n\n\tif err := query.FindInBatches(&results, batchSize, func(_ *gorm.DB, _ int) error {\n\t\tif config.PreloadBlob {\n\t\t\tvar blobs []blobable\n\t\t\tfor i := range results {\n\t\t\t\tblobs = append(blobs, any(results[i]).(blobable))\n\t\t\t}\n\t\t\tif err := s.blobStore.attachBlobValue(blobs...); err != nil {\n\t\t\t\treturn fmt.Errorf(\"unable to attach blobs: %w\", err)\n\t\t\t}\n\t\t}\n\n\t\tif config.PreloadVulnerability {\n\t\t\tvar vulns []blobable\n\t\t\tfor i := range results {\n\t\t\t\tch := any(results[i]).(cpeHandleAccessor).getCPEHandle()\n\t\t\t\tif ch.Vulnerability != nil {\n\t\t\t\t\tvulns = append(vulns, ch.Vulnerability)\n\t\t\t\t}\n\t\t\t}\n\t\t\tif err := s.blobStore.attachBlobValue(vulns...); err != nil {\n\t\t\t\treturn fmt.Errorf(\"unable to attach vulnerability blob: %w\", err)\n\t\t\t}\n\t\t}\n\n\t\tmodels = append(models, results...)\n\n\t\tcount += len(results)\n\n\t\tif config.Limit > 0 && len(models) >= config.Limit {\n\t\t\treturn ErrLimitReached\n\t\t}\n\n\t\treturn nil\n\t}).Error; err != nil {\n\t\treturn models, fmt.Errorf(\"unable to fetch CPE records: %w\", err)\n\t}\n\n\treturn models, nil\n}\n\nfunc (s *cpeStore) handleCPE(query *gorm.DB, c *cpe.Attributes, allowBroad bool, tableName string) *gorm.DB {\n\tif c == nil {\n\t\treturn query\n\t}\n\tquery = query.Joins(fmt.Sprintf(\"JOIN cpes ON cpes.id = %s.cpe_id\", tableName))\n\n\treturn handleCPEOptions(query, c, allowBroad)\n}\n\nfunc (s *cpeStore) handleVulnerabilityOptions(query *gorm.DB, configs []VulnerabilitySpecifier, tableName string) (*gorm.DB, error) {\n\tif len(configs) == 0 {\n\t\treturn query, nil\n\t}\n\n\tquery = query.Joins(fmt.Sprintf(\"JOIN vulnerability_handles ON %s.vulnerability_id = vulnerability_handles.id\", tableName))\n\n\treturn handleVulnerabilityOptions(s.db, query, configs...)\n}\n\nfunc (s *cpeStore) handlePreload(query *gorm.DB, config GetCPEOptions) *gorm.DB {\n\tvar limitArgs []interface{}\n\tif config.Limit > 0 {\n\t\tquery = query.Limit(config.Limit)\n\t\tlimitArgs = append(limitArgs, func(db *gorm.DB) *gorm.DB {\n\t\t\treturn db.Limit(config.Limit)\n\t\t})\n\t}\n\n\tif config.PreloadCPE {\n\t\tquery = query.Preload(\"CPE\", limitArgs...)\n\t}\n\n\tif config.PreloadVulnerability {\n\t\tquery = query.Preload(\"Vulnerability\", limitArgs...).Preload(\"Vulnerability.Provider\", limitArgs...)\n\t}\n\n\treturn query\n}\n"
  },
  {
    "path": "grype/db/v6/data.go",
    "content": "package v6\n\nimport (\n\t\"strings\"\n\n\t\"github.com/anchore/syft/syft/pkg\"\n)\n\n// TODO: in a future iteration these should be raised up more explicitly by the vunnel providers\nfunc KnownOperatingSystemSpecifierOverrides() []OperatingSystemSpecifierOverride {\n\tstrRef := func(s string) *string {\n\t\treturn &s\n\t}\n\treturn []OperatingSystemSpecifierOverride{\n\t\t// redhat clones or otherwise shared vulnerability data\n\t\t{Alias: \"centos\", ReplacementName: strRef(\"rhel\")},\n\t\t{Alias: \"rocky\", ReplacementName: strRef(\"rhel\")},\n\t\t{Alias: \"rockylinux\", ReplacementName: strRef(\"rhel\")}, // non-standard, but common (dockerhub uses \"rockylinux\")\n\t\t{Alias: \"alma\", ReplacementName: strRef(\"rhel\")},\n\t\t{Alias: \"almalinux\", ReplacementName: strRef(\"rhel\")}, // non-standard, but common (dockerhub uses \"almalinux\")\n\t\t{Alias: \"scientific\", ReplacementName: strRef(\"rhel\")},\n\t\t{Alias: \"sl\", ReplacementName: strRef(\"rhel\")}, // non-standard, but common (dockerhub uses \"sl\")\n\t\t{Alias: \"gentoo\", ReplacementName: strRef(\"rhel\")},\n\n\t\t// Alternaitve distros that should match against the debian vulnerability data\n\t\t{Alias: \"raspbian\", ReplacementName: strRef(\"debian\")},\n\n\t\t// to remain backwards compatible, we need to keep old clients from ignoring EUS data.\n\t\t// we do this by diverting any requests for a specific major.minor version of rhel to only\n\t\t// use the major version. But, this only applies to clients before v6.0.3 DB schema version.\n\t\t// Why 6.0.3? This is when OS channel was introduced, which grype-db will leverage, and add additional\n\t\t// rhel rows to the DB, all which have major.minor versions. This means that any old client (which wont\n\t\t// see the new channel column) will assume during OS resolution that there is major.minor vuln data\n\t\t// that should be used (which is incorrect).\n\t\t{Alias: \"rhel\", VersionPattern: `^\\d+\\.\\d+`, ReplacementMinorVersion: strRef(\"\"), ApplicableClientDBSchemas: \"< 6.0.3\"},\n\t\t// we pass in the distro.Type into the search specifier, not a raw release-id\n\t\t{Alias: \"redhat\", VersionPattern: `^\\d+\\.\\d+`, ReplacementMinorVersion: strRef(\"\"), ReplacementName: strRef(\"rhel\"), ApplicableClientDBSchemas: \"< 6.0.3\"},\n\n\t\t// alpine family\n\t\t{Alias: \"alpine\", VersionPattern: `.*_alpha.*`, ReplacementLabelVersion: strRef(\"edge\"), Rolling: true},\n\t\t{Alias: \"wolfi\", Rolling: true},\n\t\t{Alias: \"chainguard\", Rolling: true},\n\t\t{Alias: \"secureos\", Rolling: true},\n\n\t\t// others\n\t\t{Alias: \"archlinux\", Rolling: true},\n\t\t{Alias: \"minimos\", Rolling: true},\n\t\t{Alias: \"arch\", ReplacementName: strRef(\"archlinux\"), Rolling: true}, // os-release ID=arch, but namespace uses archlinux\n\t\t{Alias: \"oracle\", ReplacementName: strRef(\"ol\")},                     // non-standard, but common\n\t\t{Alias: \"oraclelinux\", ReplacementName: strRef(\"ol\")},                // non-standard, but common (dockerhub uses \"oraclelinux\")\n\t\t{Alias: \"amazon\", ReplacementName: strRef(\"amzn\")},                   // non-standard, but common\n\t\t{Alias: \"amazonlinux\", ReplacementName: strRef(\"amzn\")},              // non-standard, but common (dockerhub uses \"amazonlinux\")\n\t\t{Alias: \"echo\", Rolling: true},\n\t\t// TODO: forky is a placeholder for now, but should be updated to sid when the time comes\n\t\t// this needs to be automated, but isn't clear how to do so since you'll see things like this:\n\t\t//\n\t\t// ❯ docker run --rm debian:sid cat /etc/os-release | grep VERSION_CODENAME\n\t\t//   VERSION_CODENAME=forky\n\t\t// ❯ docker run --rm debian:testing cat /etc/os-release | grep VERSION_CODENAME\n\t\t//   VERSION_CODENAME=forky\n\t\t//\n\t\t// ❯ curl -s http://deb.debian.org/debian/dists/testing/Release | grep '^Codename:'\n\t\t//   Codename: forky\n\t\t// ❯ curl -s http://deb.debian.org/debian/dists/sid/Release | grep '^Codename:'\n\t\t//   Codename: sid\n\t\t//\n\t\t// depending where the team is during the development cycle you will see different behavior, making automating\n\t\t// this a little challenging.\n\t\t{Alias: \"debian\", Codename: \"forky\", Rolling: true, ReplacementLabelVersion: strRef(\"unstable\")}, // is currently sid, which is considered rolling\n\n\t\t// postmarketOS: map to correct underlying base alpine release version per https://wiki.postmarketos.org/wiki/Releases\n\t\t// NOTE: These are not the values as-is from the corresponding /etc/os-release files, these are the values after grype has parsed\n\t\t// the raw linux.Release objects from syft into grype Distro objects, so for instance the v prefix on the postmarketos VERSION_ID fields\n\t\t// is removed here.\n\n\t\t// edge is specified in the VERSION_ID field pf the /etc/os-release file for postmarketos, and there is no codename; however,\n\t\t// to be resilient handle both cases where edge may be parsed as the raw version or as the codename\n\t\t{Alias: \"postmarketos\", Version: \"edge\", ReplacementName: strRef(\"alpine\"), ReplacementLabelVersion: strRef(\"edge\"), Rolling: true},\n\t\t{Alias: \"postmarketos\", Codename: \"edge\", ReplacementName: strRef(\"alpine\"), ReplacementLabelVersion: strRef(\"edge\"), Rolling: true},\n\n\t\t{Alias: \"postmarketos\", Version: \"25.12\", ReplacementName: strRef(\"alpine\"), ReplacementMajorVersion: strRef(\"3\"), ReplacementMinorVersion: strRef(\"23\")},\n\t\t{Alias: \"postmarketos\", Version: \"25.06\", ReplacementName: strRef(\"alpine\"), ReplacementMajorVersion: strRef(\"3\"), ReplacementMinorVersion: strRef(\"22\")},\n\t\t{Alias: \"postmarketos\", Version: \"24.12\", ReplacementName: strRef(\"alpine\"), ReplacementMajorVersion: strRef(\"3\"), ReplacementMinorVersion: strRef(\"21\")},\n\t\t{Alias: \"postmarketos\", Version: \"24.06\", ReplacementName: strRef(\"alpine\"), ReplacementMajorVersion: strRef(\"3\"), ReplacementMinorVersion: strRef(\"20\")},\n\t\t{Alias: \"postmarketos\", Version: \"23.12\", ReplacementName: strRef(\"alpine\"), ReplacementMajorVersion: strRef(\"3\"), ReplacementMinorVersion: strRef(\"19\")},\n\t\t{Alias: \"postmarketos\", Version: \"23.06\", ReplacementName: strRef(\"alpine\"), ReplacementMajorVersion: strRef(\"3\"), ReplacementMinorVersion: strRef(\"18\")},\n\t\t{Alias: \"postmarketos\", Version: \"22.12\", ReplacementName: strRef(\"alpine\"), ReplacementMajorVersion: strRef(\"3\"), ReplacementMinorVersion: strRef(\"17\")},\n\t\t{Alias: \"postmarketos\", Version: \"22.06\", ReplacementName: strRef(\"alpine\"), ReplacementMajorVersion: strRef(\"3\"), ReplacementMinorVersion: strRef(\"16\")},\n\t\t{Alias: \"postmarketos\", Version: \"21.12\", ReplacementName: strRef(\"alpine\"), ReplacementMajorVersion: strRef(\"3\"), ReplacementMinorVersion: strRef(\"15\")},\n\t\t{Alias: \"postmarketos\", Version: \"21.06\", ReplacementName: strRef(\"alpine\"), ReplacementMajorVersion: strRef(\"3\"), ReplacementMinorVersion: strRef(\"14\")},\n\t\t{Alias: \"postmarketos\", Version: \"21.03\", ReplacementName: strRef(\"alpine\"), ReplacementMajorVersion: strRef(\"3\"), ReplacementMinorVersion: strRef(\"13\")},\n\t\t{Alias: \"postmarketos\", Version: \"20.05\", ReplacementName: strRef(\"alpine\"), ReplacementMajorVersion: strRef(\"3\"), ReplacementMinorVersion: strRef(\"12\")},\n\n\t\t// If no version is specified, map generally to alpine which has same behaviour as today where it matches against all possible\n\t\t// alpine releases, otherwise we will get no matches.\n\t\t// NOTE: We have to use a hack here with VersionPattern matching empty string because setting Version to \"\" with no other\n\t\t// primary key properties set breaks matching against the above release mappings\n\t\t{Alias: \"postmarketos\", VersionPattern: \"^$\", ReplacementName: strRef(\"alpine\")},\n\t}\n}\n\nfunc KnownPackageSpecifierOverrides() []PackageSpecifierOverride {\n\t// when matching packages, grype will always attempt to do so based off of the package type which means\n\t// that any request must be in terms of the package type (relative to syft).\n\n\tret := []PackageSpecifierOverride{\n\t\t// map all known language ecosystems to their respective syft package types\n\t\t{Ecosystem: pkg.Dart.String(), ReplacementEcosystem: ptr(string(pkg.DartPubPkg))},\n\t\t{Ecosystem: pkg.Dotnet.String(), ReplacementEcosystem: ptr(string(pkg.DotnetPkg))},\n\t\t{Ecosystem: pkg.Elixir.String(), ReplacementEcosystem: ptr(string(pkg.HexPkg))},\n\t\t{Ecosystem: pkg.Erlang.String(), ReplacementEcosystem: ptr(string(pkg.HexPkg))},      // Erlang packages use hex.pm, same as Elixir\n\t\t{Ecosystem: string(pkg.ErlangOTPPkg), ReplacementEcosystem: ptr(string(pkg.HexPkg))}, // remap erlang-otp to hex for GHSA matching\n\t\t{Ecosystem: pkg.Go.String(), ReplacementEcosystem: ptr(string(pkg.GoModulePkg))},\n\t\t{Ecosystem: pkg.Haskell.String(), ReplacementEcosystem: ptr(string(pkg.HackagePkg))},\n\t\t{Ecosystem: pkg.Java.String(), ReplacementEcosystem: ptr(string(pkg.JavaPkg))},\n\t\t{Ecosystem: pkg.JavaScript.String(), ReplacementEcosystem: ptr(string(pkg.NpmPkg))},\n\t\t{Ecosystem: pkg.Lua.String(), ReplacementEcosystem: ptr(string(pkg.LuaRocksPkg))},\n\t\t{Ecosystem: pkg.OCaml.String(), ReplacementEcosystem: ptr(string(pkg.OpamPkg))},\n\t\t{Ecosystem: pkg.PHP.String(), ReplacementEcosystem: ptr(string(pkg.PhpComposerPkg))},\n\t\t{Ecosystem: pkg.Python.String(), ReplacementEcosystem: ptr(string(pkg.PythonPkg))},\n\t\t{Ecosystem: pkg.R.String(), ReplacementEcosystem: ptr(string(pkg.Rpkg))},\n\t\t{Ecosystem: pkg.Ruby.String(), ReplacementEcosystem: ptr(string(pkg.GemPkg))},\n\t\t{Ecosystem: pkg.Rust.String(), ReplacementEcosystem: ptr(string(pkg.RustPkg))},\n\t\t{Ecosystem: pkg.Swift.String(), ReplacementEcosystem: ptr(string(pkg.SwiftPkg))},\n\t\t{Ecosystem: pkg.Swipl.String(), ReplacementEcosystem: ptr(string(pkg.SwiplPackPkg))},\n\n\t\t// jenkins plugins are a special case since they are always considered to be within the java ecosystem\n\t\t{Ecosystem: string(pkg.JenkinsPluginPkg), ReplacementEcosystem: ptr(string(pkg.JavaPkg))},\n\n\t\t// legacy cases\n\t\t{Ecosystem: \"pecl\", ReplacementEcosystem: ptr(string(pkg.PhpPeclPkg))},\n\t\t{Ecosystem: \"kb\", ReplacementEcosystem: ptr(string(pkg.KbPkg))},\n\t\t{Ecosystem: \"dpkg\", ReplacementEcosystem: ptr(string(pkg.DebPkg))},\n\t\t{Ecosystem: \"apkg\", ReplacementEcosystem: ptr(string(pkg.ApkPkg))},\n\t}\n\n\t// remap package URL types to syft package types\n\tfor _, t := range pkg.AllPkgs {\n\t\t// these types should never be mapped to\n\t\t// jenkins plugin: java-archive supersedes this\n\t\t// github action workflow: github-action supersedes this\n\t\tswitch t {\n\t\tcase pkg.JenkinsPluginPkg, pkg.GithubActionWorkflowPkg:\n\t\t\tcontinue\n\t\t}\n\n\t\tpurlType := t.PackageURLType()\n\t\tif purlType == \"\" || purlType == string(t) || strings.HasPrefix(purlType, \"generic\") {\n\t\t\tcontinue\n\t\t}\n\n\t\tret = append(ret, PackageSpecifierOverride{\n\t\t\tEcosystem:            purlType,\n\t\t\tReplacementEcosystem: ptr(string(t)),\n\t\t})\n\t}\n\treturn ret\n}\n\nfunc ptr[T any](v T) *T {\n\treturn &v\n}\n"
  },
  {
    "path": "grype/db/v6/db.go",
    "content": "package v6\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"path/filepath\"\n\n\t\"gorm.io/gorm\"\n\n\t\"github.com/anchore/grype/grype/db/internal/gormadapter\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n\t\"github.com/anchore/grype/internal/log\"\n)\n\nconst (\n\t// We follow SchemaVer semantics (see https://snowplow.io/blog/introducing-schemaver-for-semantic-versioning-of-schemas)\n\n\t// ModelVersion indicates how many breaking schema changes there have been (which will prevent interaction with any historical data)\n\t// note: this must ALWAYS be \"6\" in the context of this package.\n\tModelVersion = 6\n\n\t// Revision indicates how many changes have been introduced which **may** prevent interaction with some historical data\n\tRevision = 1\n\n\t// Addition indicates how many changes have been introduced that are compatible with all historical data\n\tAddition = 4\n\n\t// v6 model changelog:\n\t// 6.0.0: Initial version 🎉\n\t// 6.0.1: Add CISA KEV to VulnerabilityDecorator store\n\t// 6.0.2: Add EPSS to VulnerabilityDecorator store\n\t// 6.0.3: Add channel column to OperatingSystem model\n\t// 6.1.0: Add Fix availability information to AffectedPackageBlob.Range.Fix.Detail.\n\t//        Existing git commit and timestamp information was removed (as it was unused)\n\t// 6.1.1: Add UnaffectedCPE / UnaffectedPackage models and stores (remove \"Affected\" prefixes from existing blobs)\n\t// 6.1.2: Add CWEs\n\t// 6.1.3: Add ID field to Reference (for advisory IDs like RHSA-2023:5455)\n\t// 6.1.4: Add EOLDate and EOASDate fields to OperatingSystem model\n)\n\nconst (\n\tVulnerabilityDBFileName = \"vulnerability.db\"\n\n\t// batchSize affects how many records are fetched at a time from the DB. Note: when using preload, row entries\n\t// for related records may convey as parameters in a \"WHERE x in (...)\" which can lead to a large number of\n\t// parameters in the query -- if above 999 then this will result in an error for sqlite. For this reason we\n\t// try to keep this value well below 999.\n\tbatchSize = 300\n)\n\nvar ErrDBCapabilityNotSupported = fmt.Errorf(\"capability not supported by DB\")\n\ntype ReadWriter interface {\n\tReader\n\tWriter\n}\n\ntype Reader interface {\n\tDBMetadataStoreReader\n\tProviderStoreReader\n\tVulnerabilityStoreReader\n\tVulnerabilityDecoratorStoreReader\n\tOperatingSystemStoreReader\n\tAffectedPackageStoreReader\n\tUnaffectedPackageStoreReader\n\tAffectedCPEStoreReader\n\tUnaffectedCPEStoreReader\n\tio.Closer\n\tattachBlobValue(...blobable) error\n}\n\ntype Writer interface {\n\tDBMetadataStoreWriter\n\tProviderStoreWriter\n\tVulnerabilityStoreWriter\n\tVulnerabilityDecoratorStoreWriter\n\tOperatingSystemStoreWriter\n\tAffectedPackageStoreWriter\n\tUnaffectedPackageStoreWriter\n\tAffectedCPEStoreWriter\n\tUnaffectedCPEStoreWriter\n\tio.Closer\n}\n\ntype Curator interface {\n\tReader() (Reader, error)\n\tStatus() vulnerability.ProviderStatus\n\tDelete() error\n\tUpdate() (bool, error)\n\tImport(dbArchivePath string) error\n}\n\ntype Config struct {\n\tDBDirPath string\n\tDebug     bool\n}\n\nfunc (c Config) DBFilePath() string {\n\treturn filepath.Join(c.DBDirPath, VulnerabilityDBFileName)\n}\n\nfunc NewReader(cfg Config) (Reader, error) {\n\treturn newStore(cfg, false, false)\n}\n\nfunc NewWriter(cfg Config) (ReadWriter, error) {\n\treturn newStore(cfg, true, true)\n}\n\nfunc Hydrater() func(string) error {\n\treturn func(path string) error {\n\t\t// this will auto-migrate any models, creating and populating indexes as needed\n\t\t// we don't pass any data initialization here because the data is already in the db archive and we do not want\n\t\t// to affect the entries themselves, only indexes and schema.\n\t\ts, err := newStore(Config{DBDirPath: path}, false, true)\n\t\tif s != nil {\n\t\t\tlog.CloseAndLogError(s, path)\n\t\t}\n\t\treturn err\n\t}\n}\n\n// NewLowLevelDB creates a new empty DB for writing or opens an existing one for reading from the given path. This is\n// not recommended for typical interactions with the vulnerability DB, use NewReader and NewWriter instead.\nfunc NewLowLevelDB(dbFilePath string, empty, writable, debug bool) (*gorm.DB, error) {\n\topts := []gormadapter.Option{\n\t\tgormadapter.WithDebug(debug),\n\t}\n\n\tif empty && !writable {\n\t\treturn nil, fmt.Errorf(\"cannot open an empty database for reading only\")\n\t}\n\n\tif empty {\n\t\topts = append(opts,\n\t\t\tgormadapter.WithTruncate(true, Models(), InitialData()),\n\t\t)\n\t} else if writable {\n\t\topts = append(opts, gormadapter.WithWritable(true, Models()))\n\t}\n\n\tdbObj, err := gormadapter.Open(dbFilePath, opts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif empty {\n\t\t// speed up writes by persisting key-to-ID lookups when writing to the DB\n\t\tdbObj = dbObj.WithContext(withCacheContext(context.Background(), newCache()))\n\t}\n\n\treturn dbObj, err\n}\n"
  },
  {
    "path": "grype/db/v6/db_metadata_store.go",
    "content": "package v6\n\nimport (\n\t\"fmt\"\n\t\"time\"\n\n\t\"gorm.io/gorm\"\n\n\t\"github.com/anchore/grype/internal/log\"\n)\n\ntype DBMetadataStoreWriter interface {\n\tSetDBMetadata() error\n}\n\ntype DBMetadataStoreReader interface {\n\tGetDBMetadata() (*DBMetadata, error)\n}\n\ntype dbMetadataStore struct {\n\tdb *gorm.DB\n}\n\nfunc newDBMetadataStore(db *gorm.DB) *dbMetadataStore {\n\treturn &dbMetadataStore{\n\t\tdb: db,\n\t}\n}\n\nfunc (s *dbMetadataStore) GetDBMetadata() (*DBMetadata, error) {\n\tvar model DBMetadata\n\n\tresult := s.db.First(&model)\n\treturn &model, result.Error\n}\n\nfunc (s *dbMetadataStore) SetDBMetadata() error {\n\tlog.Trace(\"writing DB metadata\")\n\n\tif err := s.db.Where(\"true\").Delete(&DBMetadata{}).Error; err != nil {\n\t\treturn fmt.Errorf(\"failed to delete existing DB metadata record: %w\", err)\n\t}\n\n\t// note: it is important to round the time to the second to avoid issues with the database update check.\n\t// since we are comparing timestamps that are RFC3339 formatted, it's possible that milliseconds will\n\t// be rounded up, causing a slight difference in candidate timestamps vs current DB timestamps.\n\tts := time.Now().UTC().Round(time.Second)\n\n\tinstance := &DBMetadata{\n\t\tBuildTimestamp: &ts,\n\t\tModel:          ModelVersion,\n\t\tRevision:       Revision,\n\t\tAddition:       Addition,\n\t}\n\n\tif err := s.db.Create(instance).Error; err != nil {\n\t\treturn fmt.Errorf(\"failed to create DB metadata record: %w\", err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "grype/db/v6/db_metadata_store_test.go",
    "content": "package v6\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"gorm.io/gorm\"\n)\n\nfunc TestDbMetadataStore_empty(t *testing.T) {\n\tdb := setupTestStore(t).db\n\trequire.NoError(t, db.Where(\"true\").Delete(&DBMetadata{}).Error) // delete all existing records\n\ts := newDBMetadataStore(db)\n\n\t// attempt to fetch a non-existent record\n\tactualMetadata, err := s.GetDBMetadata()\n\trequire.ErrorIs(t, err, gorm.ErrRecordNotFound)\n\trequire.NotNil(t, actualMetadata)\n}\n\nfunc TestDbMetadataStore_oldDb(t *testing.T) {\n\tdb := setupTestStore(t).db\n\trequire.NoError(t, db.Where(\"true\").Model(DBMetadata{}).Update(\"Model\", \"5\").Error) // old database version\n\ts := newDBMetadataStore(db)\n\n\t// attempt to fetch a non-existent record\n\tactualMetadata, err := s.GetDBMetadata()\n\trequire.NoError(t, err)\n\trequire.NotNil(t, actualMetadata)\n}\n\nfunc TestDbMetadataStore(t *testing.T) {\n\ts := newDBMetadataStore(setupTestStore(t).db)\n\n\trequire.NoError(t, s.SetDBMetadata())\n\n\t// fetch the record\n\tactualMetadata, err := s.GetDBMetadata()\n\trequire.NoError(t, err)\n\trequire.NotNil(t, actualMetadata)\n\n\tassert.NotZero(t, *actualMetadata.BuildTimestamp) // a timestamp was set\n\tname, _ := actualMetadata.BuildTimestamp.Zone()\n\tassert.Equal(t, \"UTC\", name) // the timestamp is in UTC\n\n\tactualMetadata.BuildTimestamp = nil // value not under test\n\n\tassert.Equal(t, DBMetadata{\n\t\tBuildTimestamp: nil,\n\t\t// expect the correct version info\n\t\tModel:    ModelVersion,\n\t\tRevision: Revision,\n\t\tAddition: Addition,\n\t}, *actualMetadata)\n}\n\nfunc setupTestStore(t testing.TB, d ...string) *store {\n\tvar dir string\n\tswitch len(d) {\n\tcase 0:\n\t\tdir = t.TempDir()\n\tcase 1:\n\t\tdir = d[0]\n\tdefault:\n\t\tt.Fatal(\"too many arguments\")\n\n\t}\n\n\ts, err := newStore(Config{\n\t\tDBDirPath: dir,\n\t}, true, true)\n\trequire.NoError(t, err)\n\n\trequire.NoError(t, s.SetDBMetadata())\n\n\treturn s\n}\n\nfunc setupReadOnlyTestStore(t testing.TB, dir string) *store {\n\ts, err := newStore(Config{\n\t\tDBDirPath: dir,\n\t}, false, false)\n\trequire.NoError(t, err)\n\n\treturn s\n}\n"
  },
  {
    "path": "grype/db/v6/description.go",
    "content": "package v6\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"time\"\n\n\t\"github.com/anchore/grype/internal/log\"\n\t\"github.com/anchore/grype/internal/schemaver\"\n)\n\nvar ErrDBDoesNotExist = errors.New(\"database does not exist\")\n\ntype Description struct {\n\t// SchemaVersion is the version of the DB schema\n\tSchemaVersion schemaver.SchemaVer `json:\"schemaVersion,omitempty\"`\n\n\t// Built is the timestamp the database was built\n\tBuilt Time `json:\"built\"`\n}\n\ntype Time struct {\n\ttime.Time\n}\n\nfunc (t Time) MarshalJSON() ([]byte, error) {\n\treturn []byte(fmt.Sprintf(\"%q\", t.String())), nil\n}\n\nfunc (t *Time) UnmarshalJSON(data []byte) error {\n\tstr := string(data)\n\tif len(str) < 2 || str[0] != '\"' || str[len(str)-1] != '\"' {\n\t\treturn fmt.Errorf(\"invalid time format\")\n\t}\n\tstr = str[1 : len(str)-1]\n\n\tparsedTime, err := time.Parse(time.RFC3339, str)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tt.Time = parsedTime.In(time.UTC)\n\treturn nil\n}\n\nfunc (t Time) String() string {\n\treturn t.Time.UTC().Round(time.Second).Format(time.RFC3339)\n}\n\nfunc DescriptionFromMetadata(m *DBMetadata) *Description {\n\tif m == nil {\n\t\treturn nil\n\t}\n\treturn &Description{\n\t\tSchemaVersion: schemaver.New(m.Model, m.Revision, m.Addition),\n\t\tBuilt:         Time{Time: *m.BuildTimestamp},\n\t}\n}\n\nfunc ReadDescription(dbFilePath string) (*Description, error) {\n\t// check if exists\n\tif _, err := os.Stat(dbFilePath); err != nil {\n\t\tif errors.Is(err, os.ErrNotExist) {\n\t\t\treturn nil, ErrDBDoesNotExist\n\t\t}\n\t\treturn nil, fmt.Errorf(\"failed to access database file: %w\", err)\n\t}\n\n\t// access the DB to get the built time and schema version\n\tr, err := NewReader(Config{\n\t\tDBDirPath: filepath.Dir(dbFilePath),\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to read DB description: %w\", err)\n\t}\n\t// we need to ensure readers are closed, or we can see stale reads in new readers!\n\tdefer log.CloseAndLogError(r, dbFilePath)\n\n\tmeta, err := r.GetDBMetadata()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to read DB metadata: %w\", err)\n\t}\n\n\treturn &Description{\n\t\tSchemaVersion: schemaver.New(meta.Model, meta.Revision, meta.Addition),\n\t\tBuilt:         Time{Time: *meta.BuildTimestamp},\n\t}, nil\n}\n\nfunc (m Description) String() string {\n\treturn fmt.Sprintf(\"DB(version=%s built=%s)\", m.SchemaVersion, m.Built)\n}\n"
  },
  {
    "path": "grype/db/v6/description_test.go",
    "content": "package v6\n\nimport (\n\t\"encoding/json\"\n\t\"path/filepath\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/grype/internal/schemaver\"\n)\n\nfunc TestReadDescription(t *testing.T) {\n\ttempDir := t.TempDir()\n\n\ts, err := NewWriter(Config{DBDirPath: tempDir})\n\trequire.NoError(t, err)\n\trequire.NoError(t, s.SetDBMetadata())\n\texpected, err := s.GetDBMetadata()\n\trequire.NoError(t, err)\n\trequire.NoError(t, s.Close())\n\n\tdbFilePath := filepath.Join(tempDir, VulnerabilityDBFileName)\n\n\tdescription, err := ReadDescription(dbFilePath)\n\trequire.NoError(t, err)\n\trequire.NotNil(t, description)\n\n\tassert.Equal(t, Description{\n\t\tSchemaVersion: schemaver.New(expected.Model, expected.Revision, expected.Addition),\n\t\tBuilt:         Time{*expected.BuildTimestamp},\n\t}, *description)\n}\n\nfunc TestTime_JSONMarshalling(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\ttime     Time\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:     \"go case\",\n\t\t\ttime:     Time{time.Date(2023, 9, 26, 12, 0, 0, 0, time.UTC)},\n\t\t\texpected: `\"2023-09-26T12:00:00Z\"`,\n\t\t},\n\t\t{\n\t\t\tname:     \"convert to utc\",\n\t\t\ttime:     Time{time.Date(2023, 9, 26, 13, 0, 0, 0, time.FixedZone(\"UTC+1\", 3600))},\n\t\t\texpected: `\"2023-09-26T12:00:00Z\"`,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tjsonData, err := json.Marshal(tt.time)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, tt.expected, string(jsonData))\n\t\t})\n\t}\n}\n\nfunc TestTime_JSONUnmarshalling(t *testing.T) {\n\ttests := []struct {\n\t\tname         string\n\t\tjsonData     string\n\t\texpectedTime Time\n\t\texpectError  require.ErrorAssertionFunc\n\t}{\n\t\t{\n\t\t\tname:         \"use zulu offset\",\n\t\t\tjsonData:     `\"2023-09-26T12:00:00Z\"`,\n\t\t\texpectedTime: Time{time.Date(2023, 9, 26, 12, 0, 0, 0, time.UTC)},\n\t\t},\n\t\t{\n\t\t\tname:         \"use tz offset in another timezone\",\n\t\t\tjsonData:     `\"2023-09-26T14:00:00+02:00\"`,\n\t\t\texpectedTime: Time{time.Date(2023, 9, 26, 12, 0, 0, 0, time.UTC)},\n\t\t},\n\t\t{\n\t\t\tname:         \"use tz offset that is utc\",\n\t\t\tjsonData:     `\"2023-09-26T12:00:00+00:00\"`,\n\t\t\texpectedTime: Time{time.Date(2023, 9, 26, 12, 0, 0, 0, time.UTC)},\n\t\t},\n\t\t{\n\t\t\tname:        \"invalid format\",\n\t\t\tjsonData:    `\"invalid-time-format\"`,\n\t\t\texpectError: require.Error,\n\t\t},\n\t\t{\n\t\t\tname:        \"invalid json\",\n\t\t\tjsonData:    `invalid`,\n\t\t\texpectError: require.Error,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif tt.expectError == nil {\n\t\t\t\ttt.expectError = require.NoError\n\t\t\t}\n\t\t\tvar parsedTime Time\n\t\t\terr := json.Unmarshal([]byte(tt.jsonData), &parsedTime)\n\t\t\ttt.expectError(t, err)\n\t\t\tif err == nil {\n\t\t\t\tassert.Equal(t, tt.expectedTime.Time, parsedTime.Time)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/db/v6/distribution/client.go",
    "content": "package distribution\n\nimport (\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"path\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/hashicorp/go-cleanhttp\"\n\t\"github.com/spf13/afero\"\n\t\"github.com/wagoodman/go-progress\"\n\n\t\"github.com/anchore/clio\"\n\tv6 \"github.com/anchore/grype/grype/db/v6\"\n\t\"github.com/anchore/grype/internal/bus\"\n\t\"github.com/anchore/grype/internal/file\"\n\t\"github.com/anchore/grype/internal/log\"\n)\n\ntype Config struct {\n\tID clio.Identification\n\n\t// check/fetch parameters\n\tLatestURL string\n\tCACert    string\n\n\t// validations\n\tRequireUpdateCheck bool\n\n\t// timeouts\n\tCheckTimeout  time.Duration\n\tUpdateTimeout time.Duration\n}\n\ntype Client interface {\n\tLatest() (*LatestDocument, error)\n\tIsUpdateAvailable(current *v6.Description) (*Archive, error)\n\tResolveArchiveURL(archive Archive) (string, error)\n\tDownload(url, dest string, downloadProgress *progress.Manual) (string, error)\n}\n\ntype client struct {\n\tfs                afero.Fs\n\tdbDownloader      file.Getter\n\tlistingDownloader file.Getter\n\tconfig            Config\n}\n\nfunc DefaultConfig() Config {\n\treturn Config{\n\t\tLatestURL:          \"https://grype.anchore.io/databases\",\n\t\tRequireUpdateCheck: false,\n\t\tCheckTimeout:       30 * time.Second,\n\t\tUpdateTimeout:      300 * time.Second,\n\t}\n}\n\nfunc NewClient(cfg Config) (Client, error) {\n\tfs := afero.NewOsFs()\n\tlatestClient, err := defaultHTTPClient(fs, cfg.CACert, withClientTimeout(cfg.CheckTimeout), withUserAgent(cfg.ID))\n\tif err != nil {\n\t\treturn client{}, err\n\t}\n\n\tdbClient, err := defaultHTTPClient(fs, cfg.CACert, withClientTimeout(cfg.UpdateTimeout), withUserAgent(cfg.ID))\n\tif err != nil {\n\t\treturn client{}, err\n\t}\n\n\treturn client{\n\t\tfs:                fs,\n\t\tlistingDownloader: file.NewGetter(cfg.ID, latestClient),\n\t\tdbDownloader:      file.NewGetter(cfg.ID, dbClient),\n\t\tconfig:            cfg,\n\t}, nil\n}\n\n// IsUpdateAvailable indicates if there is a new update available as a boolean, and returns the latest db information\n// available for this schema.\nfunc (c client) IsUpdateAvailable(current *v6.Description) (*Archive, error) {\n\tlog.Debugf(\"checking for available database updates\")\n\n\tlatestDoc, err := c.Latest()\n\tif err != nil {\n\t\tif c.config.RequireUpdateCheck {\n\t\t\treturn nil, fmt.Errorf(\"check for vulnerability database update failed: %+v\", err)\n\t\t}\n\t\tlog.Warnf(\"unable to check for vulnerability database update\")\n\t\tlog.Debugf(\"check for vulnerability update failed: %+v\", err)\n\t}\n\n\tarchive, message := c.isUpdateAvailable(current, latestDoc)\n\n\tif message != \"\" {\n\t\tlog.Warn(message)\n\t\tbus.Notify(message)\n\t}\n\n\treturn archive, err\n}\n\nfunc (c client) isUpdateAvailable(current *v6.Description, candidate *LatestDocument) (*Archive, string) {\n\tif candidate == nil {\n\t\treturn nil, \"\"\n\t}\n\n\tvar message string\n\tswitch candidate.Status {\n\tcase StatusDeprecated:\n\t\tmessage = \"this version of grype will soon stop receiving vulnerability database updates, please update grype\"\n\tcase StatusEndOfLife:\n\t\tmessage = \"this version of grype is no longer receiving vulnerability database updates, please update grype\"\n\t}\n\n\t// compare created data to current db date\n\tif isSupersededBy(current, candidate.Description) {\n\t\tlog.Debugf(\"database update available: %s\", candidate.Description)\n\t\treturn &candidate.Archive, message\n\t}\n\n\tlog.Debugf(\"no database update available\")\n\treturn nil, message\n}\n\nfunc (c client) ResolveArchiveURL(archive Archive) (string, error) {\n\t// download the db to the temp dir\n\tu, err := url.Parse(c.latestURL())\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"unable to parse db URL %q: %w\", c.latestURL(), err)\n\t}\n\n\tu.Path = path.Join(path.Dir(u.Path), path.Clean(archive.Path))\n\n\t// from go-getter, adding a checksum as a query string will validate the payload after download\n\t// note: the checksum query parameter is not sent to the server\n\tquery := u.Query()\n\tif archive.Checksum != \"\" {\n\t\tquery.Add(\"checksum\", archive.Checksum)\n\t}\n\tu.RawQuery = query.Encode()\n\n\treturn u.String(), nil\n}\n\nfunc (c client) Download(archiveURL, dest string, downloadProgress *progress.Manual) (string, error) {\n\tdefer downloadProgress.SetCompleted()\n\n\tif err := os.MkdirAll(dest, 0700); err != nil {\n\t\treturn \"\", fmt.Errorf(\"unable to create db download root dir: %w\", err)\n\t}\n\n\t// note: as much as I'd like to use the afero FS abstraction here, the go-getter library does not support it\n\ttempDir, err := os.MkdirTemp(dest, \"grype-db-download\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"unable to create db client temp dir: %w\", err)\n\t}\n\n\t// go-getter will automatically extract all files within the archive to the temp dir\n\terr = c.dbDownloader.GetToDir(tempDir, archiveURL, downloadProgress)\n\tif err != nil {\n\t\tremoveAllOrLog(afero.NewOsFs(), tempDir)\n\t\treturn \"\", fmt.Errorf(\"unable to download db: %w\", err)\n\t}\n\n\treturn tempDir, nil\n}\n\n// Latest loads a LatestDocument from the configured URL.\nfunc (c client) Latest() (*LatestDocument, error) {\n\ttempFile, err := afero.TempFile(c.fs, \"\", \"grype-db-listing\")\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create listing temp file: %w\", err)\n\t}\n\tdefer func() {\n\t\tlog.CloseAndLogError(tempFile, tempFile.Name())\n\t\terr := c.fs.RemoveAll(tempFile.Name())\n\t\tif err != nil {\n\t\t\tlog.WithFields(\"error\", err, \"file\", tempFile.Name()).Errorf(\"failed to remove file\")\n\t\t}\n\t}()\n\n\terr = c.listingDownloader.GetFile(tempFile.Name(), c.latestURL())\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to download listing: %w\", err)\n\t}\n\n\treturn NewLatestFromFile(c.fs, tempFile.Name())\n}\n\nfunc (c client) latestURL() string {\n\tu := c.config.LatestURL\n\t// allow path to be specified directly to a json file, or the path without version information\n\tif !strings.HasSuffix(u, \".json\") {\n\t\tu = strings.TrimRight(u, \"/\")\n\t\tu = fmt.Sprintf(\"%s/v%d/%s\", u, v6.ModelVersion, LatestFileName)\n\t}\n\treturn u\n}\n\nfunc withClientTimeout(timeout time.Duration) func(*http.Client) {\n\treturn func(c *http.Client) {\n\t\tc.Timeout = timeout\n\t}\n}\n\nfunc withUserAgent(id clio.Identification) func(*http.Client) {\n\treturn func(c *http.Client) {\n\t\t*(c) = *newHTTPClientWithDefaultUserAgent(c.Transport, fmt.Sprintf(\"%s %s\", id.Name, id.Version))\n\t}\n}\n\nfunc defaultHTTPClient(fs afero.Fs, caCertPath string, postProcessor ...func(*http.Client)) (*http.Client, error) {\n\thttpClient := cleanhttp.DefaultClient()\n\thttpClient.Timeout = 30 * time.Second\n\tif caCertPath != \"\" {\n\t\trootCAs := x509.NewCertPool()\n\n\t\tpemBytes, err := afero.ReadFile(fs, caCertPath)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"unable to configure root CAs for curator: %w\", err)\n\t\t}\n\t\trootCAs.AppendCertsFromPEM(pemBytes)\n\n\t\thttpClient.Transport.(*http.Transport).TLSClientConfig = &tls.Config{\n\t\t\tMinVersion: tls.VersionTLS12,\n\t\t\tRootCAs:    rootCAs,\n\t\t}\n\t}\n\n\tfor _, pp := range postProcessor {\n\t\tpp(httpClient)\n\t}\n\n\treturn httpClient, nil\n}\n\nfunc removeAllOrLog(fs afero.Fs, dir string) {\n\tif err := fs.RemoveAll(dir); err != nil {\n\t\tlog.WithFields(\"error\", err).Warnf(\"failed to remove path %q\", dir)\n\t}\n}\n\nfunc isSupersededBy(current *v6.Description, candidate v6.Description) bool {\n\tif current == nil {\n\t\tlog.Debug(\"cannot find existing metadata, using update...\")\n\t\t// any valid update beats no database, use it!\n\t\treturn true\n\t}\n\n\tif !current.SchemaVersion.Valid() {\n\t\tlog.Error(\"existing database has no schema version, doing nothing...\")\n\t\treturn false\n\t}\n\n\tif !candidate.SchemaVersion.Valid() {\n\t\tlog.Error(\"update has no schema version, doing nothing...\")\n\t\treturn false\n\t}\n\n\tif candidate.SchemaVersion.Model != current.SchemaVersion.Model {\n\t\tlog.WithFields(\"want\", current.SchemaVersion.Model, \"received\", candidate.SchemaVersion.Model).Warn(\"update is for a different DB schema, skipping...\")\n\t\treturn false\n\t}\n\n\tif candidate.Built.After(current.Built.Time) {\n\t\td := candidate.Built.Sub(current.Built.Time).String()\n\t\tlog.WithFields(\"existing\", current.Built.String(), \"candidate\", candidate.Built.String(), \"delta\", d).Debug(\"existing database is older than candidate update, using update...\")\n\t\t// the listing is newer than the existing db, use it!\n\t\treturn true\n\t}\n\n\tlog.Debugf(\"existing database is already up to date\")\n\treturn false\n}\n\nfunc newHTTPClientWithDefaultUserAgent(baseTransport http.RoundTripper, userAgent string) *http.Client {\n\treturn &http.Client{\n\t\tTransport: roundTripperWithUserAgent{\n\t\t\ttransport: baseTransport,\n\t\t\tuserAgent: userAgent,\n\t\t},\n\t}\n}\n\ntype roundTripperWithUserAgent struct {\n\ttransport http.RoundTripper\n\tuserAgent string\n}\n\nfunc (r roundTripperWithUserAgent) RoundTrip(req *http.Request) (*http.Response, error) {\n\tclonedReq := req.Clone(req.Context())\n\n\tif clonedReq.Header.Get(\"User-Agent\") == \"\" {\n\t\tclonedReq.Header.Set(\"User-Agent\", r.userAgent)\n\t}\n\n\treturn r.transport.RoundTrip(clonedReq)\n}\n"
  },
  {
    "path": "grype/db/v6/distribution/client_test.go",
    "content": "package distribution\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"path/filepath\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/spf13/afero\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/mock\"\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/wagoodman/go-progress\"\n\n\tdb \"github.com/anchore/grype/grype/db/v6\"\n\t\"github.com/anchore/grype/internal/schemaver\"\n)\n\ntype mockGetter struct {\n\tmock.Mock\n}\n\nfunc (m *mockGetter) GetFile(dst, src string, manuals ...*progress.Manual) error {\n\targs := m.Called(dst, src, manuals)\n\treturn args.Error(0)\n}\n\nfunc (m *mockGetter) GetToDir(dst, src string, manuals ...*progress.Manual) error {\n\targs := m.Called(dst, src, manuals)\n\treturn args.Error(0)\n}\n\nfunc TestClient_Latest(t *testing.T) {\n\ttests := []struct {\n\t\tname           string\n\t\tlatestResponse []byte\n\t\tgetFileErr     error\n\t\texpectedDoc    *LatestDocument\n\t\texpectedErr    require.ErrorAssertionFunc\n\t}{\n\t\t{\n\t\t\tname: \"go case\",\n\t\t\tlatestResponse: func() []byte {\n\t\t\t\tdoc := LatestDocument{\n\t\t\t\t\tStatus: \"active\",\n\t\t\t\t\tArchive: Archive{\n\t\t\t\t\t\tDescription: db.Description{\n\t\t\t\t\t\t\tSchemaVersion: schemaver.New(1, 0, 0),\n\t\t\t\t\t\t\tBuilt:         db.Time{Time: time.Date(2023, 9, 26, 12, 0, 0, 0, time.UTC)},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tPath:     \"path/to/archive\",\n\t\t\t\t\t\tChecksum: \"checksum123\",\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\tdata, err := json.Marshal(doc)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\treturn data\n\t\t\t}(),\n\t\t\texpectedDoc: &LatestDocument{\n\t\t\t\tStatus: \"active\",\n\t\t\t\tArchive: Archive{\n\t\t\t\t\tDescription: db.Description{\n\t\t\t\t\t\tSchemaVersion: schemaver.New(1, 0, 0),\n\t\t\t\t\t\tBuilt:         db.Time{Time: time.Date(2023, 9, 26, 12, 0, 0, 0, time.UTC)},\n\t\t\t\t\t},\n\t\t\t\t\tPath:     \"path/to/archive\",\n\t\t\t\t\tChecksum: \"checksum123\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:        \"download error\",\n\t\t\tgetFileErr:  errors.New(\"failed to download file\"),\n\t\t\texpectedDoc: nil,\n\t\t\texpectedErr: func(t require.TestingT, err error, _ ...interface{}) {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\trequire.Contains(t, err.Error(), \"unable to download listing\")\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:           \"malformed JSON response\",\n\t\t\tlatestResponse: []byte(\"malformed json\"),\n\t\t\texpectedDoc:    nil,\n\t\t\texpectedErr: func(t require.TestingT, err error, _ ...interface{}) {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\trequire.Contains(t, err.Error(), \"invalid character 'm' looking for beginning of value\")\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif tt.expectedErr == nil {\n\t\t\t\ttt.expectedErr = require.NoError\n\t\t\t}\n\t\t\tmockFs := afero.NewMemMapFs()\n\n\t\t\tmg := new(mockGetter)\n\n\t\t\tmg.On(\"GetFile\", mock.Anything, \"http://localhost:8080/latest.json\", mock.Anything).Run(func(args mock.Arguments) {\n\t\t\t\tif tt.getFileErr != nil {\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tdst := args.String(0)\n\t\t\t\terr := afero.WriteFile(mockFs, dst, tt.latestResponse, 0644)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}).Return(tt.getFileErr)\n\n\t\t\tc, err := NewClient(Config{\n\t\t\t\tLatestURL: \"http://localhost:8080/latest.json\",\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\n\t\t\tcl := c.(client)\n\t\t\tcl.fs = mockFs\n\t\t\tcl.listingDownloader = mg\n\n\t\t\tdoc, err := cl.Latest()\n\t\t\ttt.expectedErr(t, err)\n\t\t\tif err != nil {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\trequire.Equal(t, tt.expectedDoc, doc)\n\t\t\tmg.AssertExpectations(t)\n\t\t})\n\t}\n}\n\nfunc TestClient_Download(t *testing.T) {\n\tdestDir := t.TempDir()\n\n\tsetup := func() (Client, *mockGetter) {\n\t\tmg := new(mockGetter)\n\n\t\tc, err := NewClient(Config{\n\t\t\tLatestURL: \"http://localhost:8080/latest.json\",\n\t\t})\n\t\trequire.NoError(t, err)\n\n\t\tcl := c.(client)\n\t\tcl.dbDownloader = mg\n\n\t\treturn cl, mg\n\t}\n\n\tt.Run(\"successful download\", func(t *testing.T) {\n\t\tc, mg := setup()\n\t\turl := \"http://localhost:8080/path/to/archive.tar.gz?checksum=checksum123\"\n\t\tmg.On(\"GetToDir\", mock.Anything, url, mock.Anything).Return(nil)\n\n\t\ttempDir, err := c.Download(url, destDir, &progress.Manual{})\n\t\trequire.NoError(t, err)\n\t\trequire.True(t, len(tempDir) > 0)\n\n\t\tmg.AssertExpectations(t)\n\t})\n\n\tt.Run(\"download error\", func(t *testing.T) {\n\t\tc, mg := setup()\n\t\turl := \"http://localhost:8080/path/to/archive.tar.gz?checksum=checksum123\"\n\t\tmg.On(\"GetToDir\", mock.Anything, url, mock.Anything).Return(errors.New(\"download failed\"))\n\n\t\ttempDir, err := c.Download(url, destDir, &progress.Manual{})\n\t\trequire.Error(t, err)\n\t\trequire.Empty(t, tempDir)\n\t\trequire.Contains(t, err.Error(), \"unable to download db\")\n\n\t\tmg.AssertExpectations(t)\n\t})\n\n\tt.Run(\"nested into dir that does not exist\", func(t *testing.T) {\n\t\tc, mg := setup()\n\t\turl := \"http://localhost:8080/path/to/archive.tar.gz?checksum=checksum123\"\n\t\tmg.On(\"GetToDir\", mock.Anything, url, mock.Anything).Return(nil)\n\n\t\tnestedPath := filepath.Join(destDir, \"nested\")\n\t\ttempDir, err := c.Download(url, nestedPath, &progress.Manual{})\n\t\trequire.NoError(t, err)\n\t\trequire.True(t, len(tempDir) > 0)\n\n\t\tmg.AssertExpectations(t)\n\t})\n}\n\nfunc TestClient_IsUpdateAvailable(t *testing.T) {\n\tcurrent := &db.Description{\n\t\tSchemaVersion: schemaver.New(1, 0, 0),\n\t\tBuilt:         db.Time{Time: time.Date(2023, 9, 26, 12, 0, 0, 0, time.UTC)},\n\t}\n\n\ttests := []struct {\n\t\tname      string\n\t\tcandidate *LatestDocument\n\t\tarchive   *Archive\n\t\tmessage   string\n\t}{\n\t\t{\n\t\t\tname: \"update available\",\n\t\t\tcandidate: &LatestDocument{\n\t\t\t\tStatus: StatusActive,\n\t\t\t\tArchive: Archive{\n\t\t\t\t\tDescription: db.Description{\n\t\t\t\t\t\tSchemaVersion: schemaver.New(1, 0, 0),\n\t\t\t\t\t\tBuilt:         db.Time{Time: time.Date(2023, 9, 27, 12, 0, 0, 0, time.UTC)},\n\t\t\t\t\t},\n\t\t\t\t\tPath:     \"path/to/archive.tar.gz\",\n\t\t\t\t\tChecksum: \"checksum123\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tarchive: &Archive{\n\t\t\t\tDescription: db.Description{\n\t\t\t\t\tSchemaVersion: schemaver.New(1, 0, 0),\n\t\t\t\t\tBuilt:         db.Time{Time: time.Date(2023, 9, 27, 12, 0, 0, 0, time.UTC)},\n\t\t\t\t},\n\t\t\t\tPath:     \"path/to/archive.tar.gz\",\n\t\t\t\tChecksum: \"checksum123\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"no update available\",\n\t\t\tcandidate: &LatestDocument{\n\t\t\t\tStatus: \"active\",\n\t\t\t\tArchive: Archive{\n\t\t\t\t\tDescription: db.Description{\n\t\t\t\t\t\tSchemaVersion: schemaver.New(1, 0, 0),\n\t\t\t\t\t\tBuilt:         db.Time{Time: time.Date(2023, 9, 26, 12, 0, 0, 0, time.UTC)},\n\t\t\t\t\t},\n\t\t\t\t\tPath:     \"path/to/archive.tar.gz\",\n\t\t\t\t\tChecksum: \"checksum123\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tarchive: nil,\n\t\t},\n\t\t{\n\t\t\tname:      \"no candidate available\",\n\t\t\tcandidate: nil,\n\t\t\tarchive:   nil,\n\t\t},\n\t\t{\n\t\t\tname: \"candidate deprecated\",\n\t\t\tcandidate: &LatestDocument{\n\t\t\t\tStatus: StatusDeprecated,\n\t\t\t\tArchive: Archive{\n\t\t\t\t\tDescription: db.Description{\n\t\t\t\t\t\tSchemaVersion: schemaver.New(1, 0, 0),\n\t\t\t\t\t\tBuilt:         db.Time{Time: time.Date(2023, 9, 27, 12, 0, 0, 0, time.UTC)},\n\t\t\t\t\t},\n\t\t\t\t\tPath:     \"path/to/archive.tar.gz\",\n\t\t\t\t\tChecksum: \"checksum123\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tarchive: &Archive{\n\t\t\t\tDescription: db.Description{\n\t\t\t\t\tSchemaVersion: schemaver.New(1, 0, 0),\n\t\t\t\t\tBuilt:         db.Time{Time: time.Date(2023, 9, 27, 12, 0, 0, 0, time.UTC)},\n\t\t\t\t},\n\t\t\t\tPath:     \"path/to/archive.tar.gz\",\n\t\t\t\tChecksum: \"checksum123\",\n\t\t\t},\n\t\t\tmessage: \"this version of grype will soon stop receiving vulnerability database updates, please update grype\",\n\t\t},\n\t\t{\n\t\t\tname: \"candidate end of life\",\n\t\t\tcandidate: &LatestDocument{\n\t\t\t\tStatus: StatusEndOfLife,\n\t\t\t\tArchive: Archive{\n\t\t\t\t\tDescription: db.Description{\n\t\t\t\t\t\tSchemaVersion: schemaver.New(1, 0, 0),\n\t\t\t\t\t\tBuilt:         db.Time{Time: time.Date(2023, 9, 27, 12, 0, 0, 0, time.UTC)},\n\t\t\t\t\t},\n\t\t\t\t\tPath:     \"path/to/archive.tar.gz\",\n\t\t\t\t\tChecksum: \"checksum123\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tarchive: &Archive{\n\t\t\t\tDescription: db.Description{\n\t\t\t\t\tSchemaVersion: schemaver.New(1, 0, 0),\n\t\t\t\t\tBuilt:         db.Time{Time: time.Date(2023, 9, 27, 12, 0, 0, 0, time.UTC)},\n\t\t\t\t},\n\t\t\t\tPath:     \"path/to/archive.tar.gz\",\n\t\t\t\tChecksum: \"checksum123\",\n\t\t\t},\n\t\t\tmessage: \"this version of grype is no longer receiving vulnerability database updates, please update grype\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tc, err := NewClient(Config{})\n\t\t\trequire.NoError(t, err)\n\n\t\t\tcl := c.(client)\n\n\t\t\tarchive, message := cl.isUpdateAvailable(current, tt.candidate)\n\t\t\tassert.Equal(t, tt.message, message)\n\t\t\tassert.Equal(t, tt.archive, archive)\n\t\t})\n\t}\n}\n\nfunc TestDatabaseDescription_IsSupersededBy(t *testing.T) {\n\tt1 := time.Date(2023, 9, 26, 12, 0, 0, 0, time.UTC)\n\tt2 := time.Date(2023, 9, 27, 12, 0, 0, 0, time.UTC)\n\n\tcurrentMetadata := db.Description{\n\t\tSchemaVersion: schemaver.New(1, 0, 0),\n\t\tBuilt:         db.Time{Time: t1},\n\t}\n\n\tnewerMetadata := db.Description{\n\t\tSchemaVersion: schemaver.New(1, 0, 0),\n\t\tBuilt:         db.Time{Time: t2},\n\t}\n\n\tolderMetadata := db.Description{\n\t\tSchemaVersion: schemaver.New(1, 0, 0),\n\t\tBuilt:         db.Time{Time: t1},\n\t}\n\n\tdifferentModelMetadata := db.Description{\n\t\tSchemaVersion: schemaver.New(2, 0, 0),\n\t\tBuilt:         db.Time{Time: t2},\n\t}\n\n\ttests := []struct {\n\t\tname     string\n\t\tcurrent  *db.Description\n\t\tother    db.Description\n\t\texpected bool\n\t}{\n\t\t{\n\t\t\tname:     \"no current metadata\",\n\t\t\tcurrent:  nil,\n\t\t\tother:    newerMetadata,\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"newer build\",\n\t\t\tcurrent:  &currentMetadata,\n\t\t\tother:    newerMetadata,\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"older build\",\n\t\t\tcurrent:  &currentMetadata,\n\t\t\tother:    olderMetadata,\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"different schema version\",\n\t\t\tcurrent:  &currentMetadata,\n\t\t\tother:    differentModelMetadata,\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"current metadata has no schema version\",\n\t\t\tcurrent:  &db.Description{Built: db.Time{Time: t1}},\n\t\t\tother:    newerMetadata,\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"update has no schema version\",\n\t\t\tcurrent:  &currentMetadata,\n\t\t\tother:    db.Description{Built: db.Time{Time: t2}},\n\t\t\texpected: false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := isSupersededBy(tt.current, tt.other)\n\t\t\trequire.Equal(t, tt.expected, result)\n\t\t})\n\t}\n}\n\nfunc Test_latestURL(t *testing.T) {\n\ttests := []struct {\n\t\turl      string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\turl:      \"https://grype.anchore.io/databases\",\n\t\t\texpected: \"https://grype.anchore.io/databases/v6/latest.json\",\n\t\t},\n\t\t{\n\t\t\turl:      \"https://grype.anchore.io/databases/\",\n\t\t\texpected: \"https://grype.anchore.io/databases/v6/latest.json\",\n\t\t},\n\t\t{\n\t\t\turl:      \"https://grype.anchore.io/databases/v6/latest.json\",\n\t\t\texpected: \"https://grype.anchore.io/databases/v6/latest.json\",\n\t\t},\n\t\t{\n\t\t\turl:      \"http://grype.anchore.io/databases/\",\n\t\t\texpected: \"http://grype.anchore.io/databases/v6/latest.json\",\n\t\t},\n\t\t{\n\t\t\turl:      \"https://example.com/file.json\",\n\t\t\texpected: \"https://example.com/file.json\",\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.url, func(t *testing.T) {\n\t\t\tc := client{\n\t\t\t\tconfig: Config{\n\t\t\t\t\tLatestURL: test.url,\n\t\t\t\t},\n\t\t\t}\n\t\t\tgot := c.latestURL()\n\t\t\trequire.Equal(t, test.expected, got)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/db/v6/distribution/latest.go",
    "content": "package distribution\n\nimport (\n\t\"crypto/sha256\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"path/filepath\"\n\t\"sort\"\n\t\"time\"\n\n\t\"github.com/spf13/afero\"\n\n\tdb \"github.com/anchore/grype/grype/db/v6\"\n\t\"github.com/anchore/grype/internal/file\"\n\t\"github.com/anchore/grype/internal/schemaver\"\n)\n\nconst LatestFileName = \"latest.json\"\n\ntype LatestDocument struct {\n\t// Status indicates if the database is actively being maintained and distributed\n\tStatus Status `json:\"status\"`\n\n\t// Archive is the most recent database that has been built and distributed, additionally annotated with provider-level information\n\tArchive `json:\",inline\"`\n}\n\ntype Archive struct {\n\t// Description contains details about the database contained within the distribution archive\n\tdb.Description `json:\",inline\"`\n\n\t// Path is the path to a DB archive relative to the listing file hosted location.\n\t// Note: this is NOT the absolute URL to download the database.\n\tPath string `json:\"path\"`\n\n\t// Checksum is the self describing digest of the database archive referenced in path\n\tChecksum string `json:\"checksum\"`\n}\n\nfunc NewLatestDocument(entries ...Archive) *LatestDocument {\n\tvar validEntries []Archive\n\tfor _, entry := range entries {\n\t\tif entry.SchemaVersion.Model == db.ModelVersion {\n\t\t\tvalidEntries = append(validEntries, entry)\n\t\t}\n\t}\n\n\tif len(validEntries) == 0 {\n\t\treturn nil\n\t}\n\n\t// sort from most recent to the least recent\n\tsort.SliceStable(validEntries, func(i, j int) bool {\n\t\treturn validEntries[i].Built.After(entries[j].Built.Time)\n\t})\n\n\treturn &LatestDocument{\n\t\tArchive: validEntries[0],\n\t\tStatus:  LifecycleStatus,\n\t}\n}\n\nfunc NewLatestFromReader(reader io.Reader) (*LatestDocument, error) {\n\tvar l LatestDocument\n\tif err := json.NewDecoder(reader).Decode(&l); err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to parse DB latest.json: %w\", err)\n\t}\n\n\tif l == (LatestDocument{}) {\n\t\treturn nil, nil\n\t}\n\n\treturn &l, nil\n}\n\nfunc NewLatestFromFile(fs afero.Fs, path string) (*LatestDocument, error) {\n\tfh, err := fs.Open(path)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to read listing file: %w\", err)\n\t}\n\tdefer fh.Close()\n\treturn NewLatestFromReader(fh)\n}\n\nfunc NewArchive(path string, t time.Time, model, revision, addition int) (*Archive, error) {\n\tchecksum, err := calculateArchiveDigest(path)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to calculate archive checksum: %w\", err)\n\t}\n\n\treturn &Archive{\n\t\tDescription: db.Description{\n\t\t\tSchemaVersion: schemaver.New(model, revision, addition),\n\t\t\tBuilt:         db.Time{Time: t},\n\t\t},\n\t\t// this is not the path on disk, this is the path relative to the latest.json file when hosted\n\t\tPath:     filepath.Base(path),\n\t\tChecksum: checksum,\n\t}, nil\n}\n\nfunc (l LatestDocument) Write(writer io.Writer) error {\n\tif l.SchemaVersion.Model == 0 {\n\t\treturn fmt.Errorf(\"missing schema version\")\n\t}\n\n\tif l.Status == \"\" {\n\t\tl.Status = LifecycleStatus\n\t}\n\n\tif l.Path == \"\" {\n\t\treturn fmt.Errorf(\"missing archive path\")\n\t}\n\n\tif l.Checksum == \"\" {\n\t\treturn fmt.Errorf(\"missing archive checksum\")\n\t}\n\n\tif l.Built.IsZero() {\n\t\treturn fmt.Errorf(\"missing built time\")\n\t}\n\n\tcontents, err := json.MarshalIndent(&l, \"\", \" \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to encode listing file: %w\", err)\n\t}\n\n\t_, err = writer.Write(contents)\n\treturn err\n}\n\nfunc calculateArchiveDigest(dbFilePath string) (string, error) {\n\tdigest, err := file.HashFile(afero.NewOsFs(), dbFilePath, sha256.New())\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to calculate checksum for DB archive file: %w\", err)\n\t}\n\treturn fmt.Sprintf(\"sha256:%s\", digest), nil\n}\n"
  },
  {
    "path": "grype/db/v6/distribution/latest_test.go",
    "content": "package distribution\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\tdb \"github.com/anchore/grype/grype/db/v6\"\n\t\"github.com/anchore/grype/internal/schemaver\"\n)\n\nfunc TestNewLatestDocument(t *testing.T) {\n\tt.Run(\"valid entries\", func(t *testing.T) {\n\t\tarchive1 := Archive{\n\t\t\tDescription: db.Description{\n\t\t\t\tSchemaVersion: schemaver.New(db.ModelVersion, db.Revision, db.Addition),\n\t\t\t\tBuilt:         db.Time{Time: time.Now()},\n\t\t\t},\n\t\t}\n\t\tarchive2 := Archive{\n\t\t\tDescription: db.Description{\n\t\t\t\tSchemaVersion: schemaver.New(db.ModelVersion, db.Revision, db.Addition),\n\t\t\t\tBuilt:         db.Time{Time: time.Now().Add(-1 * time.Hour)},\n\t\t\t},\n\t\t}\n\n\t\tlatestDoc := NewLatestDocument(archive1, archive2)\n\t\trequire.NotNil(t, latestDoc)\n\t\trequire.Equal(t, latestDoc.Archive, archive1) // most recent archive\n\t\trequire.Equal(t, latestDoc.SchemaVersion.Model, db.ModelVersion)\n\t})\n\n\tt.Run(\"filter entries\", func(t *testing.T) {\n\t\tarchive1 := Archive{\n\t\t\tDescription: db.Description{\n\t\t\t\tSchemaVersion: schemaver.New(5, db.Revision, db.Addition), // old!\n\t\t\t\tBuilt:         db.Time{Time: time.Now()},\n\t\t\t},\n\t\t}\n\t\tarchive2 := Archive{\n\t\t\tDescription: db.Description{\n\t\t\t\tSchemaVersion: schemaver.New(db.ModelVersion, db.Revision, db.Addition),\n\t\t\t\tBuilt:         db.Time{Time: time.Now().Add(-1 * time.Hour)},\n\t\t\t},\n\t\t}\n\n\t\tlatestDoc := NewLatestDocument(archive1, archive2)\n\t\trequire.NotNil(t, latestDoc)\n\t\trequire.Equal(t, latestDoc.Archive, archive2) // most recent archive with valid version\n\t\trequire.Equal(t, latestDoc.SchemaVersion.Model, db.ModelVersion)\n\t})\n\n\tt.Run(\"no entries\", func(t *testing.T) {\n\t\tlatestDoc := NewLatestDocument()\n\t\trequire.Nil(t, latestDoc)\n\t})\n}\n\nfunc TestNewLatestFromReader(t *testing.T) {\n\n\tt.Run(\"valid JSON\", func(t *testing.T) {\n\t\tlatestDoc := LatestDocument{\n\t\t\tArchive: Archive{\n\t\t\t\tDescription: db.Description{\n\t\t\t\t\tSchemaVersion: schemaver.New(db.ModelVersion, db.Revision, db.Addition),\n\t\t\t\t\tBuilt:         db.Time{Time: time.Now().Truncate(time.Second).UTC()},\n\t\t\t\t},\n\t\t\t},\n\t\t\tStatus: \"active\",\n\t\t}\n\n\t\tvar buf bytes.Buffer\n\t\trequire.NoError(t, json.NewEncoder(&buf).Encode(latestDoc))\n\n\t\tresult, err := NewLatestFromReader(&buf)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, latestDoc.SchemaVersion, result.SchemaVersion)\n\t\trequire.Equal(t, latestDoc.Archive.Description.Built.Time, result.Archive.Description.Built.Time)\n\t})\n\n\tt.Run(\"empty\", func(t *testing.T) {\n\t\temptyJSON := []byte(\"{}\")\n\t\tval, err := NewLatestFromReader(bytes.NewReader(emptyJSON))\n\t\trequire.NoError(t, err)\n\t\tassert.Nil(t, val)\n\t})\n\n\tt.Run(\"invalid JSON\", func(t *testing.T) {\n\t\tinvalidJSON := []byte(\"invalid json\")\n\t\tval, err := NewLatestFromReader(bytes.NewReader(invalidJSON))\n\t\trequire.Error(t, err)\n\t\trequire.Contains(t, err.Error(), \"unable to parse DB latest.json\")\n\t\tassert.Nil(t, val)\n\t})\n}\n\nfunc TestLatestDocument_Write(t *testing.T) {\n\n\terrContains := func(text string) require.ErrorAssertionFunc {\n\t\treturn func(t require.TestingT, err error, msgAndArgs ...interface{}) {\n\t\t\trequire.ErrorContains(t, err, text, msgAndArgs...)\n\t\t}\n\t}\n\n\tnow := db.Time{Time: time.Now().Truncate(time.Second).UTC()}\n\n\ttests := []struct {\n\t\tname          string\n\t\tlatestDoc     LatestDocument\n\t\texpectedError require.ErrorAssertionFunc\n\t}{\n\t\t{\n\t\t\tname: \"valid document\",\n\t\t\tlatestDoc: LatestDocument{\n\t\t\t\tArchive: Archive{\n\t\t\t\t\tDescription: db.Description{\n\t\t\t\t\t\tBuilt:         now,\n\t\t\t\t\t\tSchemaVersion: schemaver.New(db.ModelVersion, db.Revision, db.Addition),\n\t\t\t\t\t},\n\t\t\t\t\tPath:     \"valid/path/to/archive\",\n\t\t\t\t\tChecksum: \"sha256:validchecksum\",\n\t\t\t\t},\n\t\t\t\t// note: status not supplied, should assume to be active\n\t\t\t},\n\t\t\texpectedError: require.NoError,\n\t\t},\n\t\t{\n\t\t\tname: \"explicit status\",\n\t\t\tlatestDoc: LatestDocument{\n\t\t\t\tArchive: Archive{\n\t\t\t\t\tDescription: db.Description{\n\t\t\t\t\t\tBuilt:         now,\n\t\t\t\t\t\tSchemaVersion: schemaver.New(db.ModelVersion, db.Revision, db.Addition),\n\t\t\t\t\t},\n\t\t\t\t\tPath:     \"valid/path/to/archive\",\n\t\t\t\t\tChecksum: \"xxh64:validchecksum\",\n\t\t\t\t},\n\t\t\t\tStatus: StatusDeprecated,\n\t\t\t},\n\t\t\texpectedError: require.NoError,\n\t\t},\n\t\t{\n\t\t\tname: \"missing schema version\",\n\t\t\tlatestDoc: LatestDocument{\n\t\t\t\tArchive: Archive{\n\t\t\t\t\tDescription: db.Description{\n\t\t\t\t\t\tBuilt: now,\n\t\t\t\t\t},\n\t\t\t\t\tPath:     \"valid/path/to/archive\",\n\t\t\t\t\tChecksum: \"xxh64:validchecksum\",\n\t\t\t\t},\n\t\t\t\tStatus: \"active\",\n\t\t\t},\n\t\t\texpectedError: errContains(\"missing schema version\"),\n\t\t},\n\t\t{\n\t\t\tname: \"missing archive path\",\n\t\t\tlatestDoc: LatestDocument{\n\t\t\t\tArchive: Archive{\n\t\t\t\t\tDescription: db.Description{\n\t\t\t\t\t\tBuilt:         now,\n\t\t\t\t\t\tSchemaVersion: schemaver.New(db.ModelVersion, db.Revision, db.Addition),\n\t\t\t\t\t},\n\t\t\t\t\tPath:     \"\", // this!\n\t\t\t\t\tChecksum: \"xxh64:validchecksum\",\n\t\t\t\t},\n\t\t\t\tStatus: \"active\",\n\t\t\t},\n\t\t\texpectedError: errContains(\"missing archive path\"),\n\t\t},\n\t\t{\n\t\t\tname: \"missing archive checksum\",\n\t\t\tlatestDoc: LatestDocument{\n\t\t\t\tArchive: Archive{\n\t\t\t\t\tDescription: db.Description{\n\t\t\t\t\t\tBuilt:         now,\n\t\t\t\t\t\tSchemaVersion: schemaver.New(db.ModelVersion, db.Revision, db.Addition),\n\t\t\t\t\t},\n\t\t\t\t\tPath:     \"valid/path/to/archive\",\n\t\t\t\t\tChecksum: \"\", // this!\n\t\t\t\t},\n\t\t\t\tStatus: \"active\",\n\t\t\t},\n\t\t\texpectedError: errContains(\"missing archive checksum\"),\n\t\t},\n\t\t{\n\t\t\tname: \"missing built time\",\n\t\t\tlatestDoc: LatestDocument{\n\t\t\t\tArchive: Archive{\n\t\t\t\t\tDescription: db.Description{\n\t\t\t\t\t\tBuilt:         db.Time{}, // this!\n\t\t\t\t\t\tSchemaVersion: schemaver.New(db.ModelVersion, db.Revision, db.Addition),\n\t\t\t\t\t},\n\t\t\t\t\tPath:     \"valid/path/to/archive\",\n\t\t\t\t\tChecksum: \"xxh64:validchecksum\",\n\t\t\t\t},\n\t\t\t\tStatus: \"active\",\n\t\t\t},\n\t\t\texpectedError: errContains(\"missing built time\"),\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif tt.expectedError == nil {\n\t\t\t\ttt.expectedError = require.NoError\n\t\t\t}\n\t\t\tvar buf bytes.Buffer\n\t\t\terr := tt.latestDoc.Write(&buf)\n\t\t\ttt.expectedError(t, err)\n\t\t\tif err != nil {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tvar result LatestDocument\n\t\t\tassert.NoError(t, json.Unmarshal(buf.Bytes(), &result))\n\t\t\tassert.Equal(t, tt.latestDoc.SchemaVersion, result.SchemaVersion, \"schema version mismatch\")\n\t\t\tassert.Equal(t, tt.latestDoc.Archive.Checksum, result.Archive.Checksum, \"archive checksum mismatch\")\n\t\t\tassert.Equal(t, tt.latestDoc.Archive.Description.Built.Time, result.Archive.Description.Built.Time, \"built time mismatch\")\n\t\t\tassert.Equal(t, tt.latestDoc.Archive.Path, result.Archive.Path, \"path mismatch\")\n\t\t\tif tt.latestDoc.Status == \"\" {\n\t\t\t\tassert.Equal(t, StatusActive, result.Status, \"status mismatch\")\n\t\t\t} else {\n\t\t\t\tassert.Equal(t, tt.latestDoc.Status, result.Status, \"status mismatch\")\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewArchive(t *testing.T) {\n\ttests := []struct {\n\t\tname      string\n\t\tcontents  string\n\t\ttime      time.Time\n\t\tmodel     int\n\t\trevision  int\n\t\taddition  int\n\t\texpectErr require.ErrorAssertionFunc\n\t\texpected  *Archive\n\t}{\n\t\t{\n\t\t\tname:      \"valid input\",\n\t\t\tcontents:  \"test archive content\",\n\t\t\ttime:      time.Date(2023, 11, 24, 12, 0, 0, 0, time.UTC),\n\t\t\tmodel:     1,\n\t\t\trevision:  0,\n\t\t\taddition:  5,\n\t\t\texpectErr: require.NoError,\n\t\t\texpected: &Archive{\n\t\t\t\tDescription: db.Description{\n\t\t\t\t\tSchemaVersion: schemaver.New(1, 0, 5),\n\t\t\t\t\tBuilt:         db.Time{Time: time.Date(2023, 11, 24, 12, 0, 0, 0, time.UTC)},\n\t\t\t\t},\n\t\t\t\tPath:     \"archive.tar.gz\",\n\t\t\t\tChecksum: \"sha256:2a11c11d2c3803697c458a1f5f03c2b73235c101f93c88193cc8810003c40d87\",\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\td := t.TempDir()\n\t\t\ttempFile, err := os.Create(filepath.Join(d, tt.expected.Path))\n\t\t\trequire.NoError(t, err)\n\t\t\t_, err = tempFile.WriteString(tt.contents)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tarchive, err := NewArchive(tempFile.Name(), tt.time, tt.model, tt.revision, tt.addition)\n\t\t\ttt.expectErr(t, err)\n\t\t\tif err != nil {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif diff := cmp.Diff(tt.expected, archive); diff != \"\" {\n\t\t\t\tt.Errorf(\"unexpected archive (-want +got):\\n%s\", diff)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/db/v6/distribution/status.go",
    "content": "package distribution\n\ntype Status string\n\nconst LifecycleStatus = StatusActive\n\nconst (\n\t// StatusActive indicates the database is actively being maintained and distributed\n\tStatusActive Status = \"active\"\n\n\t// StatusDeprecated indicates the database is still being distributed but is approaching end of life. Upgrade grype to avoid future disruptions.\n\tStatusDeprecated Status = \"deprecated\"\n\n\t// StatusEndOfLife indicates the database is no longer being distributed. Users must build their own databases or upgrade grype.\n\tStatusEndOfLife Status = \"eol\"\n)\n"
  },
  {
    "path": "grype/db/v6/enumerations.go",
    "content": "package v6\n\nimport \"strings\"\n\n// VulnerabilityStatus is meant to convey the current point in the lifecycle for a vulnerability record.\n// This is roughly based on CVE status, NVD status, and vendor-specific status values (see https://nvd.nist.gov/vuln/vulnerability-status)\ntype VulnerabilityStatus string\n\nconst (\n\tUnknownVulnerabilityStatus VulnerabilityStatus = \"\"\n\n\t// VulnerabilityActive means that the information from the vulnerability record is actionable\n\tVulnerabilityActive VulnerabilityStatus = \"active\" // empty also means active\n\n\t// VulnerabilityAnalyzing means that the vulnerability record is being reviewed, it may or may not be actionable\n\tVulnerabilityAnalyzing VulnerabilityStatus = \"analyzing\"\n\n\t// VulnerabilityRejected means that data from the vulnerability record should not be acted upon\n\tVulnerabilityRejected VulnerabilityStatus = \"rejected\"\n\n\t// VulnerabilityDisputed means that the vulnerability record is in contention, it may or may not be actionable\n\tVulnerabilityDisputed VulnerabilityStatus = \"disputed\"\n)\n\n// SeverityScheme represents how to interpret the string value for a vulnerability severity\ntype SeverityScheme string\n\nconst (\n\tUnknownSeverityScheme SeverityScheme = \"\"\n\n\t// SeveritySchemeCVSS is the Common Vulnerability Scoring System severity scheme\n\tSeveritySchemeCVSS SeverityScheme = \"CVSS\"\n\n\t// SeveritySchemeHML is a string severity scheme (High, Medium, Low)\n\tSeveritySchemeHML SeverityScheme = \"HML\"\n\n\t// SeveritySchemeCHML is a string severity scheme (Critical, High, Medium, Low)\n\tSeveritySchemeCHML SeverityScheme = \"CHML\"\n\n\t// SeveritySchemeCHMLN is a string severity scheme (Critical, High, Medium, Low, Negligible)\n\tSeveritySchemeCHMLN SeverityScheme = \"CHMLN\"\n)\n\n// FixStatus conveys if the package is affected (or not) and the current availability (or not) of a fix\ntype FixStatus string\n\nconst (\n\tUnknownFixStatus FixStatus = \"\"\n\n\t// FixedStatus affirms the package is affected and a fix is available\n\tFixedStatus FixStatus = \"fixed\"\n\n\t// NotFixedStatus affirms the package is affected and a fix is not available\n\tNotFixedStatus FixStatus = \"not-fixed\"\n\n\t// WontFixStatus affirms the package is affected and a fix will not be provided\n\tWontFixStatus FixStatus = \"wont-fix\"\n\n\t// NotAffectedFixStatus affirms the package is not affected by the vulnerability\n\tNotAffectedFixStatus FixStatus = \"not-affected\"\n)\n\nconst (\n\t// AdvisoryReferenceTag is a tag that can be used to identify vulnerability advisory URL references\n\tAdvisoryReferenceTag = \"advisory\"\n)\n\nfunc ParseVulnerabilityStatus(s string) VulnerabilityStatus {\n\tswitch strings.TrimSpace(strings.ToLower(s)) {\n\tcase string(VulnerabilityActive), \"\":\n\t\treturn VulnerabilityActive\n\tcase string(VulnerabilityAnalyzing):\n\t\treturn VulnerabilityAnalyzing\n\tcase string(VulnerabilityRejected):\n\t\treturn VulnerabilityRejected\n\tcase string(VulnerabilityDisputed):\n\t\treturn VulnerabilityDisputed\n\tdefault:\n\t\treturn UnknownVulnerabilityStatus\n\t}\n}\n\nfunc ParseSeverityScheme(s string) SeverityScheme {\n\tswitch replaceAny(strings.TrimSpace(strings.ToLower(s)), \"\", \"-\", \"_\", \" \") {\n\tcase strings.ToLower(string(SeveritySchemeCVSS)):\n\t\treturn SeveritySchemeCVSS\n\tcase strings.ToLower(string(SeveritySchemeHML)):\n\t\treturn SeveritySchemeHML\n\tcase strings.ToLower(string(SeveritySchemeCHML)):\n\t\treturn SeveritySchemeCHML\n\tcase strings.ToLower(string(SeveritySchemeCHMLN)):\n\t\treturn SeveritySchemeCHMLN\n\tdefault:\n\t\treturn UnknownSeverityScheme\n\t}\n}\n\nfunc ParseFixStatus(s string) FixStatus {\n\tswitch replaceAny(strings.TrimSpace(strings.ToLower(s)), \"-\", \" \", \"_\") {\n\tcase string(FixedStatus):\n\t\treturn FixedStatus\n\tcase string(NotFixedStatus):\n\t\treturn NotFixedStatus\n\tcase string(WontFixStatus):\n\t\treturn WontFixStatus\n\tcase string(NotAffectedFixStatus):\n\t\treturn NotAffectedFixStatus\n\tdefault:\n\t\treturn UnknownFixStatus\n\t}\n}\n\nfunc NormalizeReferenceTags(tags []string) []string {\n\tvar normalized []string\n\tfor _, tag := range tags {\n\t\tnormalized = append(normalized, replaceAny(strings.ToLower(strings.TrimSpace(tag)), \"-\", \" \", \"_\"))\n\t}\n\treturn normalized\n}\n\nfunc replaceAny(input string, newStr string, searchFor ...string) string {\n\tfor _, s := range searchFor {\n\t\tinput = strings.ReplaceAll(input, s, newStr)\n\t}\n\treturn input\n}\n"
  },
  {
    "path": "grype/db/v6/enumerations_test.go",
    "content": "package v6\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestParseVulnerabilityStatus(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    string\n\t\texpected VulnerabilityStatus\n\t}{\n\t\t{\"Active status\", \"active\", VulnerabilityActive},\n\t\t{\"Analyzing status with whitespace\", \" analyzing \", VulnerabilityAnalyzing},\n\t\t{\"Rejected status in uppercase\", \"REJECTED\", VulnerabilityRejected},\n\t\t{\"Disputed status\", \"disputed\", VulnerabilityDisputed},\n\t\t{\"Unknown status\", \"unknown\", UnknownVulnerabilityStatus},\n\t\t{\"Empty string as active status\", \"\", VulnerabilityActive},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tassert.Equal(t, tt.expected, ParseVulnerabilityStatus(tt.input))\n\t\t})\n\t}\n}\n\nfunc TestParseSeverityScheme(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    string\n\t\texpected SeverityScheme\n\t}{\n\t\t{\"CVSS scheme\", \"Cvss\", SeveritySchemeCVSS},\n\t\t{\"HML scheme\", \"H-M-l\", SeveritySchemeHML},\n\t\t{\"CHML scheme\", \"ChmL\", SeveritySchemeCHML},\n\t\t{\"CHMLN scheme\", \"CHmLN\", SeveritySchemeCHMLN},\n\t\t{\"Unknown scheme\", \"unknown\", UnknownSeverityScheme},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tassert.Equal(t, tt.expected, ParseSeverityScheme(tt.input))\n\t\t})\n\t}\n}\n\nfunc TestParseFixStatus(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    string\n\t\texpected FixStatus\n\t}{\n\t\t{\"Fixed status\", \"fixed\", FixedStatus},\n\t\t{\"Not fixed status with hyphen\", \"not-fixed\", NotFixedStatus},\n\t\t{\"Wont fix status in uppercase with underscore\", \"WONT_FIX\", WontFixStatus},\n\t\t{\"Not affected status with whitespace\", \" not affected \", NotAffectedFixStatus},\n\t\t{\"Unknown status\", \"unknown\", UnknownFixStatus},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tassert.Equal(t, tt.expected, ParseFixStatus(tt.input))\n\t\t})\n\t}\n}\n\nfunc TestReplaceAny(t *testing.T) {\n\ttests := []struct {\n\t\tname      string\n\t\tinput     string\n\t\tnewStr    string\n\t\tsearchFor []string\n\t\texpected  string\n\t}{\n\t\t{\"go case\", \"really not_fixed-i'promise\", \"-\", []string{\"'\", \" \", \"_\"}, \"really-not-fixed-i-promise\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tassert.Equal(t, tt.expected, replaceAny(tt.input, tt.newStr, tt.searchFor...))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/db/v6/fillers.go",
    "content": "package v6\n\nimport (\n\t\"errors\"\n)\n\n// fillAffectedPackageHandles lazy loads all properties on the list of AffectedPackageHandles\nfunc fillAffectedPackageHandles(reader Reader, handles []*AffectedPackageHandle) error {\n\treturn errors.Join(\n\t\treader.attachBlobValue(toBlobables(handles)...),\n\t\tfillRefs(reader, handles, affectedPackageHandleOperatingSystemRef, operatingSystemID),\n\t\tfillRefs(reader, handles, affectedPackageHandlePackageRef, packageID),\n\t\tfillVulnerabilityHandles(reader, handles, affectedPackageHandleVulnerabilityHandleRef),\n\t)\n}\n\nfunc affectedPackageHandleOperatingSystemRef(t *AffectedPackageHandle) idRef[OperatingSystem] {\n\treturn idRef[OperatingSystem]{\n\t\tid:  t.OperatingSystemID,\n\t\tref: &t.OperatingSystem,\n\t}\n}\n\nfunc affectedPackageHandlePackageRef(t *AffectedPackageHandle) idRef[Package] {\n\treturn idRef[Package]{\n\t\tid:  &t.PackageID,\n\t\tref: &t.Package,\n\t}\n}\n\nfunc affectedPackageHandleVulnerabilityHandleRef(t *AffectedPackageHandle) idRef[VulnerabilityHandle] {\n\treturn idRef[VulnerabilityHandle]{\n\t\tid:  &t.VulnerabilityID,\n\t\tref: &t.Vulnerability,\n\t}\n}\n\n// fillAffectedCPEHandles lazy loads all properties on the list of AffectedCPEHandles\nfunc fillAffectedCPEHandles(reader Reader, handles []*AffectedCPEHandle) error {\n\treturn errors.Join(\n\t\treader.attachBlobValue(toBlobables(handles)...),\n\t\tfillRefs(reader, handles, affectedCPEHandleCpeRef, cpeHandleID),\n\t\tfillVulnerabilityHandles(reader, handles, affectedCPEHandleVulnerabilityHandleRef),\n\t)\n}\n\nfunc affectedCPEHandleCpeRef(t *AffectedCPEHandle) idRef[Cpe] {\n\treturn idRef[Cpe]{\n\t\tid:  &t.CpeID,\n\t\tref: &t.CPE,\n\t}\n}\n\nfunc affectedCPEHandleVulnerabilityHandleRef(t *AffectedCPEHandle) idRef[VulnerabilityHandle] {\n\treturn idRef[VulnerabilityHandle]{\n\t\tid:  &t.VulnerabilityID,\n\t\tref: &t.Vulnerability,\n\t}\n}\n\n// fillUnaffectedPackageHandles lazy loads all properties on the list of UnaffectedPackageHandles\nfunc fillUnaffectedPackageHandles(reader Reader, handles []*UnaffectedPackageHandle) error {\n\treturn errors.Join(\n\t\treader.attachBlobValue(toBlobables(handles)...),\n\t\tfillRefs(reader, handles, unaffectedPackageHandleOperatingSystemRef, operatingSystemID),\n\t\tfillRefs(reader, handles, unaffectedPackageHandlePackageRef, packageID),\n\t\tfillVulnerabilityHandles(reader, handles, unaffectedPackageHandleVulnerabilityHandleRef),\n\t)\n}\n\nfunc unaffectedPackageHandleOperatingSystemRef(t *UnaffectedPackageHandle) idRef[OperatingSystem] {\n\treturn idRef[OperatingSystem]{\n\t\tid:  t.OperatingSystemID,\n\t\tref: &t.OperatingSystem,\n\t}\n}\n\nfunc unaffectedPackageHandlePackageRef(t *UnaffectedPackageHandle) idRef[Package] {\n\treturn idRef[Package]{\n\t\tid:  &t.PackageID,\n\t\tref: &t.Package,\n\t}\n}\n\nfunc unaffectedPackageHandleVulnerabilityHandleRef(t *UnaffectedPackageHandle) idRef[VulnerabilityHandle] {\n\treturn idRef[VulnerabilityHandle]{\n\t\tid:  &t.VulnerabilityID,\n\t\tref: &t.Vulnerability,\n\t}\n}\n\n// fillUnaffectedCPEHandles lazy loads all properties on the list of UnaffectedCPEHandles\nfunc fillUnaffectedCPEHandles(reader Reader, handles []*UnaffectedCPEHandle) error {\n\treturn errors.Join(\n\t\treader.attachBlobValue(toBlobables(handles)...),\n\t\tfillRefs(reader, handles, unaffectedCPEHandleCpeRef, cpeHandleID),\n\t\tfillVulnerabilityHandles(reader, handles, unaffectedCPEHandleVulnerabilityHandleRef),\n\t)\n}\n\nfunc unaffectedCPEHandleCpeRef(t *UnaffectedCPEHandle) idRef[Cpe] {\n\treturn idRef[Cpe]{\n\t\tid:  &t.CpeID,\n\t\tref: &t.CPE,\n\t}\n}\n\nfunc unaffectedCPEHandleVulnerabilityHandleRef(t *UnaffectedCPEHandle) idRef[VulnerabilityHandle] {\n\treturn idRef[VulnerabilityHandle]{\n\t\tid:  &t.VulnerabilityID,\n\t\tref: &t.Vulnerability,\n\t}\n}\n\n// fillVulnerabilityHandles lazy loads vulnerability handle properties\nfunc fillVulnerabilityHandles[T any](reader Reader, handles []*T, vulnHandleRef refProvider[T, VulnerabilityHandle]) error {\n\t// fill vulnerabilities\n\tif err := fillRefs(reader, handles, vulnHandleRef, vulnerabilityHandleID); err != nil {\n\t\treturn err\n\t}\n\tvar providerRefs []ref[string, Provider]\n\tvulnHandles := make([]*VulnerabilityHandle, len(handles))\n\tfor i := range handles {\n\t\tvulnHandles[i] = *vulnHandleRef(handles[i]).ref\n\t\tproviderRefs = append(providerRefs, ref[string, Provider]{\n\t\t\tid:  &vulnHandles[i].ProviderID,\n\t\t\tref: &vulnHandles[i].Provider,\n\t\t})\n\t}\n\t// then get references to them to fill the properties\n\treturn errors.Join(\n\t\treader.attachBlobValue(toBlobables(vulnHandles)...),\n\t\treader.fillProviders(providerRefs),\n\t)\n}\n\nfunc vulnerabilityHandleID(h *VulnerabilityHandle) ID {\n\treturn h.ID\n}\n\nfunc cpeHandleID(h *Cpe) ID {\n\treturn h.ID\n}\n\nfunc operatingSystemID(h *OperatingSystem) ID {\n\treturn h.ID\n}\n\nfunc packageID(h *Package) ID {\n\treturn h.ID\n}\n\nfunc toBlobables[T blobable](handles []T) []blobable {\n\tout := make([]blobable, len(handles))\n\tfor i := range handles {\n\t\tout[i] = handles[i]\n\t}\n\treturn out\n}\n"
  },
  {
    "path": "grype/db/v6/import_metadata.go",
    "content": "package v6\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/OneOfOne/xxhash\"\n\t\"github.com/spf13/afero\"\n\n\t\"github.com/anchore/grype/internal/file\"\n\t\"github.com/anchore/grype/internal/schemaver\"\n)\n\nconst ImportMetadataFileName = \"import.json\"\n\ntype ImportMetadata struct {\n\tDigest        string `json:\"digest\"`\n\tSource        string `json:\"source,omitempty\"`\n\tClientVersion string `json:\"client_version\"`\n}\n\nfunc ReadImportMetadata(fs afero.Fs, dir string) (*ImportMetadata, error) {\n\tchecksumsFilePath := filepath.Join(dir, ImportMetadataFileName)\n\n\tif _, err := fs.Stat(checksumsFilePath); os.IsNotExist(err) {\n\t\treturn nil, fmt.Errorf(\"no import metadata file at: %v\", checksumsFilePath)\n\t}\n\n\tcontent, err := afero.ReadFile(fs, checksumsFilePath)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to read import metadata file: %w\", err)\n\t}\n\n\tif len(content) == 0 {\n\t\treturn nil, fmt.Errorf(\"no import metadata found at: %v\", checksumsFilePath)\n\t}\n\n\tvar doc ImportMetadata\n\tif err := json.Unmarshal(content, &doc); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to unmarshal import metadata: %w\", err)\n\t}\n\n\tif !strings.HasPrefix(doc.Digest, \"xxh64:\") {\n\t\treturn nil, fmt.Errorf(\"import metadata digest is not in the expected format\")\n\t}\n\n\treturn &doc, nil\n}\n\nfunc CalculateDBDigest(fs afero.Fs, dbFilePath string) (string, error) {\n\tdigest, err := file.HashFile(fs, dbFilePath, xxhash.New64())\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to digest DB file: %w\", err)\n\t}\n\treturn fmt.Sprintf(\"xxh64:%s\", digest), nil\n}\n\nfunc WriteImportMetadata(fs afero.Fs, dbDir, source string) (*ImportMetadata, error) {\n\tmetadataFilePath := filepath.Join(dbDir, ImportMetadataFileName)\n\tf, err := fs.OpenFile(metadataFilePath, os.O_TRUNC|os.O_WRONLY|os.O_CREATE, 0644)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create import metadata file: %w\", err)\n\t}\n\tdefer f.Close()\n\n\tchecksums, err := CalculateDBDigest(fs, filepath.Join(dbDir, VulnerabilityDBFileName))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to calculate checksum for DB file: %w\", err)\n\t}\n\n\treturn writeImportMetadata(f, checksums, source)\n}\n\nfunc writeImportMetadata(writer io.Writer, checksums, source string) (*ImportMetadata, error) {\n\tif checksums == \"\" {\n\t\treturn nil, fmt.Errorf(\"checksum is required\")\n\t}\n\n\tif !strings.HasPrefix(checksums, \"xxh64:\") {\n\t\treturn nil, fmt.Errorf(\"checksum missing algorithm prefix\")\n\t}\n\n\tenc := json.NewEncoder(writer)\n\tenc.SetIndent(\"\", \" \")\n\n\tdoc := ImportMetadata{\n\t\tDigest:        checksums,\n\t\tSource:        source,\n\t\tClientVersion: schemaver.New(ModelVersion, Revision, Addition).String(),\n\t}\n\n\treturn &doc, enc.Encode(doc)\n}\n"
  },
  {
    "path": "grype/db/v6/import_metadata_test.go",
    "content": "package v6\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/spf13/afero\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/grype/internal/schemaver\"\n)\n\nfunc TestReadImportMetadata(t *testing.T) {\n\ttests := []struct {\n\t\tname           string\n\t\tfileContent    string\n\t\temptyFile      bool\n\t\texpectedErr    string\n\t\texpectedResult *ImportMetadata\n\t}{\n\t\t{\n\t\t\tname:        \"file does not exist\",\n\t\t\tfileContent: \"\",\n\t\t\texpectedErr: \"no import metadata\",\n\t\t},\n\t\t{\n\t\t\tname:        \"empty file\",\n\t\t\temptyFile:   true,\n\t\t\texpectedErr: \"no import metadata\",\n\t\t},\n\t\t{\n\t\t\tname:        \"invalid json\",\n\t\t\tfileContent: \"invalid json\",\n\t\t\texpectedErr: \"failed to unmarshal import metadata\",\n\t\t},\n\t\t{\n\t\t\tname:        \"missing checksum prefix\",\n\t\t\tfileContent: `{\"digest\": \"invalid\", \"client_version\": \"1.0.0\"}`,\n\t\t\texpectedErr: \"import metadata digest is not in the expected format\",\n\t\t},\n\t\t{\n\t\t\tname:        \"valid metadata\",\n\t\t\tfileContent: `{\"digest\": \"xxh64:testdigest\", \"source\": \"http://localhost:1234/archive.tar.gz\", \"client_version\": \"1.0.0\"}`,\n\t\t\texpectedResult: &ImportMetadata{\n\t\t\t\tDigest:        \"xxh64:testdigest\",\n\t\t\t\tSource:        \"http://localhost:1234/archive.tar.gz\",\n\t\t\t\tClientVersion: \"1.0.0\",\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tdir := t.TempDir()\n\t\t\tfilePath := filepath.Join(dir, ImportMetadataFileName)\n\n\t\t\tif tt.fileContent != \"\" {\n\t\t\t\terr := os.WriteFile(filePath, []byte(tt.fileContent), 0644)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t} else if tt.emptyFile {\n\t\t\t\t_, err := os.Create(filePath)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\n\t\t\tresult, err := ReadImportMetadata(afero.NewOsFs(), dir)\n\n\t\t\tif tt.expectedErr != \"\" {\n\t\t\t\trequire.ErrorContains(t, err, tt.expectedErr)\n\t\t\t\trequire.Nil(t, result)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.Equal(t, tt.expectedResult, result)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestWriteImportMetadata(t *testing.T) {\n\tcases := []struct {\n\t\tname            string\n\t\tchecksum        string\n\t\texpectedVersion string\n\t\twantErr         require.ErrorAssertionFunc\n\t}{\n\t\t{\n\t\t\tname:            \"valid checksum\",\n\t\t\tchecksum:        \"xxh64:testdigest\",\n\t\t\texpectedVersion: schemaver.New(ModelVersion, Revision, Addition).String(),\n\t\t\twantErr:         require.NoError,\n\t\t},\n\t\t{\n\t\t\tname:     \"empty checksum\",\n\t\t\tchecksum: \"\",\n\t\t\twantErr:  require.Error,\n\t\t},\n\t\t{\n\t\t\tname:     \"missing prefix\",\n\t\t\tchecksum: \"testdigest\",\n\t\t\twantErr:  require.Error,\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tvar buf bytes.Buffer\n\t\t\tsrc := \"source!\"\n\t\t\tclaim, err := writeImportMetadata(&buf, tc.checksum, src)\n\t\t\ttc.wantErr(t, err)\n\n\t\t\tif err == nil {\n\t\t\t\tresult := buf.String()\n\n\t\t\t\tvar doc ImportMetadata\n\t\t\t\terr := json.Unmarshal([]byte(result), &doc)\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\tassert.Equal(t, tc.checksum, doc.Digest)\n\t\t\t\tassert.Equal(t, tc.checksum, claim.Digest)\n\t\t\t\tassert.Equal(t, tc.expectedVersion, doc.ClientVersion)\n\t\t\t\tassert.Equal(t, tc.expectedVersion, claim.ClientVersion)\n\t\t\t\tassert.Equal(t, src, doc.Source)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestCalculateDBDigest(t *testing.T) {\n\ttests := []struct {\n\t\tname           string\n\t\tfileContent    string\n\t\texpectedErr    string\n\t\texpectedDigest string\n\t}{\n\t\t{\n\t\t\tname:        \"file does not exist\",\n\t\t\tfileContent: \"\",\n\t\t\texpectedErr: \"failed to digest DB file\",\n\t\t},\n\t\t{\n\t\t\tname:           \"valid file\",\n\t\t\tfileContent:    \"testcontent\",\n\t\t\texpectedDigest: \"xxh64:d37ed71e4fee2ebd\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tdir := t.TempDir()\n\t\t\tfilePath := filepath.Join(dir, VulnerabilityDBFileName)\n\n\t\t\tif tt.fileContent != \"\" {\n\t\t\t\terr := os.WriteFile(filePath, []byte(tt.fileContent), 0644)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\n\t\t\tdigest, err := CalculateDBDigest(afero.NewOsFs(), filePath)\n\n\t\t\tif tt.expectedErr != \"\" {\n\t\t\t\trequire.ErrorContains(t, err, tt.expectedErr)\n\t\t\t\trequire.Empty(t, digest)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.Equal(t, tt.expectedDigest, digest)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/db/v6/installation/curator.go",
    "content": "package installation\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/adrg/xdg\"\n\t\"github.com/hako/durafmt\"\n\t\"github.com/mholt/archives\"\n\t\"github.com/spf13/afero\"\n\t\"github.com/wagoodman/go-partybus\"\n\t\"github.com/wagoodman/go-progress\"\n\n\t\"github.com/anchore/clio\"\n\tdb \"github.com/anchore/grype/grype/db/v6\"\n\t\"github.com/anchore/grype/grype/db/v6/distribution\"\n\t\"github.com/anchore/grype/grype/event\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n\t\"github.com/anchore/grype/internal/bus\"\n\t\"github.com/anchore/grype/internal/file\"\n\t\"github.com/anchore/grype/internal/log\"\n\t\"github.com/anchore/grype/internal/schemaver\"\n)\n\nconst lastUpdateCheckFileName = \"last_update_check\"\n\ntype monitor struct {\n\t*progress.AtomicStage\n\tdownloadProgress completionMonitor\n\timportProgress   completionMonitor\n\thydrateProgress  completionMonitor\n}\n\ntype Config struct {\n\tDBRootDir string\n\tDebug     bool\n\n\t// validations\n\tValidateAge             bool\n\tValidateChecksum        bool\n\tMaxAllowedBuiltAge      time.Duration\n\tUpdateCheckMaxFrequency time.Duration\n}\n\nfunc DefaultConfig(id clio.Identification) Config {\n\treturn Config{\n\t\tDBRootDir:               filepath.Join(xdg.CacheHome, id.Name, \"db\"),\n\t\tValidateAge:             true,\n\t\tValidateChecksum:        true,\n\t\tMaxAllowedBuiltAge:      time.Hour * 24 * 5, // 5 days\n\t\tUpdateCheckMaxFrequency: 2 * time.Hour,      // 2 hours\n\t}\n}\n\nfunc (c Config) DBFilePath() string {\n\treturn filepath.Join(c.DBDirectoryPath(), db.VulnerabilityDBFileName)\n}\n\nfunc (c Config) DBDirectoryPath() string {\n\treturn filepath.Join(c.DBRootDir, strconv.Itoa(db.ModelVersion))\n}\n\ntype curator struct {\n\tfs       afero.Fs\n\tclient   distribution.Client\n\tconfig   Config\n\thydrator func(string) error\n}\n\nfunc NewCurator(cfg Config, downloader distribution.Client) (db.Curator, error) {\n\treturn curator{\n\t\tfs:       afero.NewOsFs(),\n\t\tclient:   downloader,\n\t\tconfig:   cfg,\n\t\thydrator: db.Hydrater(),\n\t}, nil\n}\n\nfunc (c curator) Reader() (db.Reader, error) {\n\tstatus := c.Status()\n\tif err := status.Error; err != nil && errors.Is(err, db.ErrDBDoesNotExist) {\n\t\treturn nil, fmt.Errorf(\"vulnerability database error: %w. Try 'grype db update'\", err)\n\t}\n\n\ts, err := db.NewReader(\n\t\tdb.Config{\n\t\t\tDBDirPath: c.config.DBDirectoryPath(),\n\t\t\tDebug:     c.config.Debug,\n\t\t},\n\t)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tm, err := s.GetDBMetadata()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to get vulnerability store metadata: %w\", err)\n\t}\n\n\tvar currentDBSchemaVersion *schemaver.SchemaVer\n\tif m != nil {\n\t\tv := schemaver.New(m.Model, m.Revision, m.Addition)\n\t\tcurrentDBSchemaVersion = &v\n\t}\n\n\tdoRehydrate, err := isRehydrationNeeded(c.fs, c.config.DBDirectoryPath(), currentDBSchemaVersion, schemaver.New(db.ModelVersion, db.Revision, db.Addition))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif doRehydrate {\n\t\tif err = s.Close(); err != nil {\n\t\t\t// DB connection may be in an inconsistent state -- we cannot continue\n\t\t\treturn nil, fmt.Errorf(\"unable to close reader before rehydration: %w\", err)\n\t\t}\n\t\tmon := newMonitor()\n\n\t\tmon.Set(\"rehydrating DB\")\n\t\tlog.Info(\"rehydrating DB\")\n\n\t\t// we're not changing the source of the DB, so we just want to use any existing value.\n\t\t// if the source is empty/does not exist, it will be empty in the new metadata.\n\t\tvar source string\n\t\tim, err := db.ReadImportMetadata(c.fs, c.config.DBDirectoryPath())\n\t\tif err == nil && im != nil {\n\t\t\t// ignore errors, as this is just a best-effort to get the source\n\t\t\tsource = im.Source\n\t\t}\n\n\t\t// this is a condition where an old client imported a DB with additional capabilities than it can handle at hydration.\n\t\t// this could lead to missing indexes and degraded performance now that a newer client is running (that can handle these capabilities).\n\t\t// the only sensible thing to do is to rehydrate the existing DB to ensure indexes are up-to-date with the current client's capabilities.\n\t\tif err := c.hydrate(c.config.DBDirectoryPath(), source, mon); err != nil {\n\t\t\tlog.WithFields(\"error\", err).Warn(\"unable to rehydrate DB\")\n\t\t}\n\t\tmon.Set(\"rehydrated\")\n\t\tmon.SetCompleted()\n\n\t\ts, err = db.NewReader(\n\t\t\tdb.Config{\n\t\t\t\tDBDirPath: c.config.DBDirectoryPath(),\n\t\t\t\tDebug:     c.config.Debug,\n\t\t\t},\n\t\t)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"unable to create new reader after rehydration: %w\", err)\n\t\t}\n\t}\n\n\treturn s, nil\n}\n\nfunc (c curator) Status() vulnerability.ProviderStatus {\n\tdbFile := c.config.DBFilePath()\n\td, validateErr := db.ReadDescription(dbFile)\n\tif validateErr != nil {\n\t\treturn vulnerability.ProviderStatus{\n\t\t\tPath:  dbFile,\n\t\t\tError: validateErr,\n\t\t}\n\t}\n\tif d == nil {\n\t\treturn vulnerability.ProviderStatus{\n\t\t\tPath:  dbFile,\n\t\t\tError: fmt.Errorf(\"database not found at %q\", dbFile),\n\t\t}\n\t}\n\n\tvalidateErr = c.validateAge(d)\n\t_, checksumErr := c.validateIntegrity(d)\n\tif checksumErr != nil && c.config.ValidateChecksum {\n\t\tif validateErr != nil {\n\t\t\tvalidateErr = errors.Join(validateErr, checksumErr)\n\t\t} else {\n\t\t\tvalidateErr = checksumErr\n\t\t}\n\t}\n\n\tvar source string\n\tim, readErr := db.ReadImportMetadata(c.fs, c.config.DBDirectoryPath())\n\tif readErr == nil && im != nil {\n\t\t// only make a best-effort to get the source\n\t\tsource = im.Source\n\t}\n\n\treturn vulnerability.ProviderStatus{\n\t\tBuilt:         d.Built.Time,\n\t\tSchemaVersion: d.SchemaVersion.String(),\n\t\tFrom:          source,\n\t\tPath:          dbFile,\n\t\tError:         validateErr,\n\t}\n}\n\n// Delete removes the DB and metadata file for this specific schema.\nfunc (c curator) Delete() error {\n\treturn c.fs.RemoveAll(c.config.DBDirectoryPath())\n}\n\n// Update the existing DB, returning an indication if any action was taken.\nfunc (c curator) Update() (bool, error) {\n\tcurrent, err := db.ReadDescription(c.config.DBFilePath())\n\tif err != nil {\n\t\t// we should not warn if the DB does not exist, as this is a common first-run case... but other cases we\n\t\t// may care about, so warn in those cases.\n\t\tif !errors.Is(err, db.ErrDBDoesNotExist) {\n\t\t\tlog.WithFields(\"error\", err).Warn(\"unable to read current database metadata; continuing with update\")\n\t\t}\n\t\t// downstream any non-existent DB should always be replaced with any best-candidate found\n\t\tcurrent = nil\n\t} else {\n\t\terr = c.validateAge(current)\n\t\tif err != nil {\n\t\t\t// even if we are not allowed to check for an update, we should still attempt to update the DB if it is invalid\n\t\t\tlog.WithFields(\"error\", err).Warn(\"current database is invalid\")\n\t\t\tcurrent = nil\n\t\t}\n\t}\n\n\tif current != nil && !c.isUpdateCheckAllowed() {\n\t\t// we should not notify the user of an update check if the current configuration and state\n\t\t// indicates we are in a low-pass filter mode and the check frequency is too high.\n\t\t// this should appear to the user as if we never attempted to check for an update at all.\n\t\treturn false, nil\n\t}\n\n\tupdate, err := c.update(current)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tif update == nil {\n\t\treturn false, nil\n\t}\n\n\tif current != nil {\n\t\tlog.WithFields(\n\t\t\t\"from\", current.Built.String(),\n\t\t\t\"to\", update.Description.Built.String(),\n\t\t\t\"version\", update.Description.SchemaVersion,\n\t\t).Info(\"updated vulnerability DB\")\n\t\treturn true, nil\n\t}\n\n\tlog.WithFields(\n\t\t\"version\", update.Description.SchemaVersion,\n\t\t\"built\", update.Description.Built.String(),\n\t).Info(\"installed new vulnerability DB\")\n\treturn true, nil\n}\n\nfunc (c curator) isUpdateCheckAllowed() bool {\n\tif c.config.UpdateCheckMaxFrequency == 0 {\n\t\tlog.Trace(\"no max-frequency set for update check\")\n\t\treturn true\n\t}\n\n\telapsed, err := c.durationSinceUpdateCheck()\n\tif err != nil {\n\t\t// we had an IO error (or similar) trying to read or parse the file, we should not block the update check.\n\t\tlog.WithFields(\"error\", err).Trace(\"unable to determine if update check is allowed\")\n\t\treturn true\n\t}\n\tif elapsed == nil {\n\t\t// there was no last check (this is a first run case), we should not block the update check.\n\t\treturn true\n\t}\n\n\treturn *elapsed > c.config.UpdateCheckMaxFrequency\n}\n\nfunc (c curator) update(current *db.Description) (*distribution.Archive, error) {\n\tmon := newMonitor()\n\tdefer mon.SetCompleted()\n\tstartTime := time.Now()\n\n\tmon.Set(\"checking for update\")\n\tupdate, checkErr := c.client.IsUpdateAvailable(current)\n\tif checkErr != nil {\n\t\t// we want to continue even if we can't check for an update\n\t\tlog.Warnf(\"unable to check for vulnerability database update\")\n\t\tlog.WithFields(\"error\", checkErr).Debug(\"check for vulnerability update failed\")\n\t}\n\n\tif update == nil {\n\t\tif checkErr == nil {\n\t\t\t// there was no update (or any issue while checking for an update)\n\t\t\tc.setLastSuccessfulUpdateCheck()\n\t\t}\n\n\t\tmon.Set(\"no update available\")\n\t\treturn nil, checkErr\n\t}\n\n\tlog.Info(\"downloading new vulnerability DB\")\n\tmon.Set(\"downloading\")\n\turl, err := c.client.ResolveArchiveURL(*update)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to resolve vulnerability DB URL: %w\", err)\n\t}\n\n\t// Ensure parent of DBRootDir exists for the download client to create a temp dir within DBRootDir\n\t// This might be redundant if DBRootDir must already exist, but good for safety.\n\tif err := os.MkdirAll(c.config.DBRootDir, 0o700); err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create db root dir %s for download: %w\", c.config.DBRootDir, err)\n\t}\n\n\tdest, err := c.client.Download(url, c.config.DBRootDir, mon.downloadProgress.Manual)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to update vulnerability database: %w\", err)\n\t}\n\n\tlog.WithFields(\"url\", url, \"time\", time.Since(startTime)).Info(\"downloaded vulnerability DB\")\n\n\tmon.downloadProgress.SetCompleted()\n\tif err = c.activate(dest, url, mon); err != nil {\n\t\tlog.Warnf(\"Failed to activate downloaded database from %s, attempting cleanup of temporary download directory.\", dest)\n\t\tremoveAllOrLog(c.fs, dest)\n\t\treturn nil, fmt.Errorf(\"unable to activate new vulnerability database: %w\", err)\n\t}\n\n\tmon.Set(\"updated\")\n\n\t// only set the last successful update check if the update was successful\n\tc.setLastSuccessfulUpdateCheck()\n\n\treturn update, nil\n}\n\nfunc isRehydrationNeeded(fs afero.Fs, dirPath string, currentDBVersion *schemaver.SchemaVer, currentClientVersion schemaver.SchemaVer) (bool, error) {\n\tif currentDBVersion == nil {\n\t\t// there is no DB to rehydrate\n\t\treturn false, nil\n\t}\n\n\timportMetadata, err := db.ReadImportMetadata(fs, dirPath)\n\tif err != nil {\n\t\treturn false, fmt.Errorf(\"unable to read import metadata: %w\", err)\n\t}\n\n\tclientHydrationVersion, err := schemaver.Parse(importMetadata.ClientVersion)\n\tif err != nil {\n\t\treturn false, fmt.Errorf(\"unable to parse client version from import metadata: %w\", err)\n\t}\n\n\thydratedWithOldClient := clientHydrationVersion.LessThanOrEqualTo(*currentDBVersion)\n\thaveNewerClient := clientHydrationVersion.LessThan(currentClientVersion)\n\tdoRehydrate := hydratedWithOldClient && haveNewerClient\n\n\tmsg := \"DB rehydration not needed\"\n\tif doRehydrate {\n\t\tmsg = \"DB rehydration needed\"\n\t}\n\n\tlog.WithFields(\"clientHydrationVersion\", clientHydrationVersion, \"currentDBVersion\", currentDBVersion, \"currentClientVersion\", currentClientVersion).Trace(msg)\n\n\tif doRehydrate {\n\t\t// this is a condition where an old client imported a DB with additional capabilities than it can handle at hydration.\n\t\t// this could lead to missing indexes and degraded performance now that a newer client is running (that can handle these capabilities).\n\t\t// the only sensible thing to do is to rehydrate the existing DB to ensure indexes are up-to-date with the current client's capabilities.\n\t\treturn true, nil\n\t}\n\n\treturn false, nil\n}\n\nfunc (c curator) durationSinceUpdateCheck() (*time.Duration, error) {\n\t// open `$dbDir/last_update_check` file and read the timestamp and do now() - timestamp\n\n\tfilePath := filepath.Join(c.config.DBDirectoryPath(), lastUpdateCheckFileName)\n\n\tif _, err := c.fs.Stat(filePath); os.IsNotExist(err) {\n\t\tlog.Trace(\"first-run of DB update\")\n\t\treturn nil, nil\n\t}\n\n\tfh, err := c.fs.OpenFile(filePath, os.O_RDONLY, 0)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to read last update check timestamp: %w\", err)\n\t}\n\n\tdefer log.CloseAndLogError(fh, filePath)\n\n\t// read and parse rfc3339 timestamp\n\tvar lastCheckStr string\n\t_, err = fmt.Fscanf(fh, \"%s\", &lastCheckStr)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to read last update check timestamp: %w\", err)\n\t}\n\n\tlastCheck, err := time.Parse(time.RFC3339, lastCheckStr)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to parse last update check timestamp: %w\", err)\n\t}\n\n\tif lastCheck.IsZero() {\n\t\treturn nil, fmt.Errorf(\"empty update check timestamp\")\n\t}\n\n\telapsed := time.Since(lastCheck)\n\treturn &elapsed, nil\n}\n\nfunc (c curator) setLastSuccessfulUpdateCheck() {\n\t// note: we should always assume the DB dir actually exists, otherwise let this operation fail (since having a DB\n\t// is a prerequisite for a successful update).\n\n\tfilePath := filepath.Join(c.config.DBDirectoryPath(), lastUpdateCheckFileName)\n\tfh, err := c.fs.OpenFile(filePath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o644)\n\tif err != nil {\n\t\tlog.WithFields(\"error\", err).Trace(\"unable to write last update check timestamp\")\n\t\treturn\n\t}\n\n\tdefer log.CloseAndLogError(fh, filePath)\n\n\t_, _ = fmt.Fprintf(fh, \"%s\", time.Now().UTC().Format(time.RFC3339))\n}\n\n// Import takes a DB file path, archive file path, or URL and imports it into the final DB location.\nfunc (c curator) Import(reference string) error {\n\tmon := newMonitor()\n\tmon.Set(\"preparing\")\n\tdefer mon.SetCompleted()\n\n\tif err := os.MkdirAll(c.config.DBRootDir, 0o700); err != nil {\n\t\treturn fmt.Errorf(\"unable to create db root dir: %w\", err)\n\t}\n\n\tvar tempDir, url string\n\tif isURL(reference) {\n\t\tlog.Info(\"downloading new vulnerability DB\")\n\t\tmon.Set(\"downloading\")\n\t\tvar err error\n\n\t\ttempDir, err = c.client.Download(reference, c.config.DBRootDir, mon.downloadProgress.Manual)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"unable to update vulnerability database: %w\", err)\n\t\t}\n\n\t\turl = reference\n\t} else {\n\t\t// note: the temp directory is persisted upon download/validation/activation failure to allow for investigation\n\t\tvar err error\n\t\ttempDir, err = os.MkdirTemp(c.config.DBRootDir, fmt.Sprintf(\"tmp-v%v-import\", db.ModelVersion))\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"unable to create db import temp dir: %w\", err)\n\t\t}\n\n\t\turl = \"manual import\"\n\n\t\tif strings.HasSuffix(reference, \".db\") {\n\t\t\t// this is a raw DB file, copy it to the temp dir\n\t\t\tlog.Trace(\"copying DB\")\n\t\t\tif err := file.CopyFile(afero.NewOsFs(), reference, filepath.Join(tempDir, db.VulnerabilityDBFileName)); err != nil {\n\t\t\t\treturn fmt.Errorf(\"unable to copy DB file: %w\", err)\n\t\t\t}\n\t\t} else {\n\t\t\t// assume it is an archive\n\t\t\tlog.Info(\"unarchiving DB\")\n\t\t\terr := unarchive(reference, tempDir)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\n\tmon.downloadProgress.SetCompleted()\n\n\tif err := c.activate(tempDir, url, mon); err != nil {\n\t\tremoveAllOrLog(c.fs, tempDir)\n\t\treturn err\n\t}\n\n\tmon.Set(\"imported\")\n\n\treturn nil\n}\n\nvar urlPrefixPattern = regexp.MustCompile(\"^[a-zA-Z]+://\")\n\nfunc isURL(reference string) bool {\n\treturn urlPrefixPattern.MatchString(reference)\n}\n\n// activate swaps over the downloaded db to the application directory, calculates the checksum, and records the checksums to a file.\nfunc (c curator) activate(dbDirPath, url string, mon monitor) error {\n\tdefer mon.SetCompleted()\n\n\tstartTime := time.Now()\n\tif err := c.hydrate(dbDirPath, url, mon); err != nil {\n\t\treturn fmt.Errorf(\"failed to hydrate database: %w\", err)\n\t}\n\n\tlog.WithFields(\"time\", time.Since(startTime)).Trace(\"hydrated db\")\n\tstartTime = time.Now()\n\tdefer func() { log.WithFields(\"time\", time.Since(startTime)).Trace(\"replaced db\") }()\n\n\tmon.Set(\"activating\")\n\n\treturn c.replaceDB(dbDirPath)\n}\n\nfunc (c curator) hydrate(dbDirPath, from string, mon monitor) error {\n\tif c.hydrator != nil {\n\t\tmon.Set(\"hydrating\")\n\t\tif err := c.hydrator(dbDirPath); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tmon.hydrateProgress.SetCompleted()\n\n\tmon.Set(\"hashing\")\n\n\tdoc, err := db.WriteImportMetadata(c.fs, dbDirPath, from)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to write checksums file: %w\", err)\n\t}\n\n\tlog.WithFields(\"digest\", doc.Digest).Trace(\"captured DB digest\")\n\n\treturn nil\n}\n\n// replaceDB swaps over to using the given path.\nfunc (c curator) replaceDB(dbDirPath string) error {\n\tdbDir := c.config.DBDirectoryPath()\n\t_, err := c.fs.Stat(dbDir)\n\tif !os.IsNotExist(err) {\n\t\t// remove any previous databases\n\t\terr = c.Delete()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to purge existing database: %w\", err)\n\t\t}\n\t}\n\n\t// ensure parent db directory exists\n\tif err = c.fs.MkdirAll(filepath.Dir(dbDir), 0o700); err != nil {\n\t\treturn fmt.Errorf(\"unable to create db parent directory: %w\", err)\n\t}\n\n\t// activate the new db cache by moving the temp dir to final location\n\t// the rename should be safe because the temp dir is under GRYPE_DB_CACHE_DIR\n\t// and so on the same filesystem as the final location\n\terr = c.fs.Rename(dbDirPath, dbDir)\n\tif err != nil {\n\t\terr = fmt.Errorf(\"failed to move database directory to activate: %w\", err)\n\t}\n\tlog.WithFields(\"from\", dbDirPath, \"to\", dbDir, \"error\", err).Debug(\"moved database directory to activate\")\n\treturn err\n}\n\n// validateIntegrity checks that the disk checksum still matches the db payload\nfunc (c curator) validateIntegrity(description *db.Description) (string, error) {\n\tdbFilePath := c.config.DBFilePath()\n\n\t// check that the disk checksum still matches the db payload\n\tif description == nil {\n\t\treturn \"\", fmt.Errorf(\"database not found: %s\", dbFilePath)\n\t}\n\n\tif description.SchemaVersion.Model != db.ModelVersion {\n\t\treturn \"\", fmt.Errorf(\"unsupported database version: have=%d want=%d\", description.SchemaVersion.Model, db.ModelVersion)\n\t}\n\n\tif _, err := c.fs.Stat(dbFilePath); err != nil {\n\t\tif os.IsNotExist(err) {\n\t\t\treturn \"\", fmt.Errorf(\"database does not exist: %s\", dbFilePath)\n\t\t}\n\t\treturn \"\", fmt.Errorf(\"failed to access database file: %w\", err)\n\t}\n\n\timportMetadata, err := db.ReadImportMetadata(c.fs, filepath.Dir(dbFilePath))\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tvalid, actualHash, err := file.ValidateByHash(c.fs, dbFilePath, importMetadata.Digest)\n\tif err != nil {\n\t\treturn actualHash, err\n\t}\n\tif !valid {\n\t\treturn actualHash, fmt.Errorf(\"bad db checksum (%s): %q vs %q\", dbFilePath, importMetadata.Digest, actualHash)\n\t}\n\n\treturn actualHash, nil\n}\n\n// validateAge ensures the vulnerability database has not passed\n// the max allowed age, calculated from the time it was built until now.\nfunc (c curator) validateAge(m *db.Description) error {\n\tif m == nil {\n\t\treturn fmt.Errorf(\"no metadata to validate\")\n\t}\n\n\tif !c.config.ValidateAge {\n\t\treturn nil\n\t}\n\n\t// built time is defined in UTC,\n\t// we should compare it against UTC\n\tnow := time.Now().UTC()\n\n\tage := now.Sub(m.Built.Time)\n\tif age > c.config.MaxAllowedBuiltAge {\n\t\treturn fmt.Errorf(\"the vulnerability database was built %s ago (max allowed age is %s)\", durafmt.ParseShort(age), durafmt.ParseShort(c.config.MaxAllowedBuiltAge))\n\t}\n\n\treturn nil\n}\n\nfunc removeAllOrLog(fs afero.Fs, dir string) {\n\tif err := fs.RemoveAll(dir); err != nil {\n\t\tlog.WithFields(\"error\", err).Warnf(\"failed to remove path %q\", dir)\n\t}\n}\n\nfunc unarchive(source, destination string) error {\n\tsourceFile, err := os.Open(source)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer sourceFile.Close()\n\n\tformat, stream, err := archives.Identify(context.Background(), source, sourceFile)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\textractor, ok := format.(archives.Extractor)\n\tif !ok {\n\t\treturn fmt.Errorf(\"unable to extract DB file, format not supported: %s\", source)\n\t}\n\n\troot, err := os.OpenRoot(destination)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvisitor := func(_ context.Context, file archives.FileInfo) error {\n\t\tif file.IsDir() || file.LinkTarget != \"\" {\n\t\t\treturn nil\n\t\t}\n\n\t\tfileReader, err := file.Open()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tdefer fileReader.Close()\n\n\t\tfilename := filepath.Clean(file.NameInArchive)\n\n\t\toutputFile, err := root.Create(filename)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tdefer outputFile.Close()\n\n\t\t_, err = io.Copy(outputFile, fileReader)\n\n\t\treturn err\n\t}\n\n\treturn extractor.Extract(context.Background(), stream, visitor)\n}\n\nfunc newMonitor() monitor {\n\t// let consumers know of a monitorable event (download + import stages)\n\timportProgress := progress.NewManual(1)\n\tstage := progress.NewAtomicStage(\"\")\n\tdownloadProgress := progress.NewManual(1)\n\thydrateProgress := progress.NewManual(1)\n\taggregateProgress := progress.NewAggregator(progress.DefaultStrategy, downloadProgress, hydrateProgress, importProgress)\n\n\tbus.Publish(partybus.Event{\n\t\tType: event.UpdateVulnerabilityDatabase,\n\t\tValue: progress.StagedProgressable(&struct {\n\t\t\tprogress.Stager\n\t\t\tprogress.Progressable\n\t\t}{\n\t\t\tStager:       progress.Stager(stage),\n\t\t\tProgressable: progress.Progressable(aggregateProgress),\n\t\t}),\n\t})\n\n\treturn monitor{\n\t\tAtomicStage:      stage,\n\t\tdownloadProgress: completionMonitor{downloadProgress},\n\t\timportProgress:   completionMonitor{importProgress},\n\t\thydrateProgress:  completionMonitor{hydrateProgress},\n\t}\n}\n\nfunc (m monitor) SetCompleted() {\n\tm.downloadProgress.SetCompleted()\n\tm.importProgress.SetCompleted()\n\tm.hydrateProgress.SetCompleted()\n}\n\n// completionMonitor is a progressable that, when SetComplete() is called, will set the progress to the total size\ntype completionMonitor struct {\n\t*progress.Manual\n}\n\nfunc (m completionMonitor) SetCompleted() {\n\tm.Set(m.Size())\n\tm.Manual.SetCompleted()\n}\n"
  },
  {
    "path": "grype/db/v6/installation/curator_test.go",
    "content": "package installation\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/mholt/archives\"\n\t\"github.com/spf13/afero\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/mock\"\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/wagoodman/go-progress\"\n\n\t\"github.com/anchore/clio\"\n\tdb \"github.com/anchore/grype/grype/db/v6\"\n\t\"github.com/anchore/grype/grype/db/v6/distribution\"\n\t\"github.com/anchore/grype/internal/schemaver\"\n)\n\ntype mockClient struct {\n\tmock.Mock\n}\n\nfunc (m *mockClient) IsUpdateAvailable(current *db.Description) (*distribution.Archive, error) {\n\targs := m.Called(current)\n\n\terr := args.Error(1)\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn args.Get(0).(*distribution.Archive), nil\n}\n\nfunc (m *mockClient) ResolveArchiveURL(_ distribution.Archive) (string, error) {\n\treturn \"http://localhost/archive.tar.zst\", nil\n}\n\nfunc (m *mockClient) Download(url, dest string, downloadProgress *progress.Manual) (string, error) {\n\targs := m.Called(url, dest, downloadProgress)\n\treturn args.String(0), args.Error(1)\n}\n\nfunc (m *mockClient) Latest() (*distribution.LatestDocument, error) {\n\targs := m.Called()\n\treturn args.Get(0).(*distribution.LatestDocument), args.Error(1)\n}\n\nfunc newTestCurator(t *testing.T) curator {\n\ttempDir := t.TempDir()\n\tcfg := testConfig()\n\tcfg.DBRootDir = tempDir\n\n\tci, err := NewCurator(cfg, new(mockClient))\n\trequire.NoError(t, err)\n\n\tc := ci.(curator)\n\treturn c\n}\n\ntype setupConfig struct {\n\tworkingUpdate bool\n}\n\ntype setupOption func(*setupConfig)\n\nfunc withWorkingUpdateIntegrations() setupOption {\n\treturn func(c *setupConfig) {\n\t\tc.workingUpdate = true\n\t}\n}\n\nfunc setupCuratorForUpdate(t *testing.T, opts ...setupOption) curator {\n\tcfg := setupConfig{}\n\n\tfor _, o := range opts {\n\t\to(&cfg)\n\t}\n\n\tc := newTestCurator(t)\n\n\tdbDir := c.config.DBDirectoryPath()\n\tstageConfig := Config{DBRootDir: filepath.Join(c.config.DBRootDir, \"staged\")}\n\tstageDir := stageConfig.DBDirectoryPath()\n\n\t// populate metadata into the downloaded dir\n\toldDescription := db.Description{\n\t\tSchemaVersion: schemaver.New(db.ModelVersion, db.Revision, db.Addition),\n\t\tBuilt:         db.Time{Time: time.Now().Add(-48 * time.Hour)},\n\t}\n\twriteTestDB(t, c.fs, dbDir)\n\n\tnewDescription := oldDescription\n\tnewDescription.Built = db.Time{Time: time.Now()}\n\n\twriteTestDB(t, c.fs, stageDir)\n\n\twriteTestDescriptionToDB(t, dbDir, oldDescription)\n\twriteTestDescriptionToDB(t, stageDir, newDescription)\n\n\tif cfg.workingUpdate {\n\t\tmc := c.client.(*mockClient)\n\n\t\t// ensure the update \"works\"\n\t\tmc.On(\"IsUpdateAvailable\", mock.Anything).Return(&distribution.Archive{}, nil)\n\t\tmc.On(\"Download\", mock.Anything, mock.Anything, mock.Anything).Return(stageDir, nil)\n\t}\n\n\treturn c\n}\n\nfunc writeTestDescriptionToDB(t *testing.T, dir string, desc db.Description) string {\n\tc := db.Config{DBDirPath: dir}\n\td, err := db.NewLowLevelDB(c.DBFilePath(), false, true, true)\n\trequire.NoError(t, err)\n\n\tif err := d.Where(\"true\").Delete(&db.DBMetadata{}).Error; err != nil {\n\t\tt.Fatalf(\"failed to delete existing DB metadata record: %v\", err)\n\t}\n\n\trequire.NotEmpty(t, desc.SchemaVersion.Model)\n\trequire.NotEmpty(t, desc.SchemaVersion.String())\n\n\tts := time.Now().UTC()\n\tinstance := &db.DBMetadata{\n\t\tBuildTimestamp: &ts,\n\t\tModel:          desc.SchemaVersion.Model,\n\t\tRevision:       desc.SchemaVersion.Revision,\n\t\tAddition:       desc.SchemaVersion.Revision,\n\t}\n\n\trequire.NoError(t, d.Create(instance).Error)\n\n\trequire.NoError(t, d.Exec(\"VACUUM\").Error)\n\n\tdigest, err := db.CalculateDBDigest(afero.NewOsFs(), c.DBFilePath())\n\trequire.NoError(t, err)\n\n\twriteTestImportMetadata(t, afero.NewOsFs(), dir, digest)\n\n\treturn digest\n}\n\nfunc writeTestImportMetadata(t *testing.T, fs afero.Fs, dir string, checksums string) {\n\twriteTestImportMetadataWithCustomVersion(t, fs, dir, checksums, schemaver.New(db.ModelVersion, db.Revision, db.Addition).String())\n}\n\nfunc writeTestImportMetadataWithCustomVersion(t *testing.T, fs afero.Fs, dir string, checksums string, ver string) {\n\trequire.NoError(t, fs.MkdirAll(dir, 0755))\n\n\tmetadataFilePath := filepath.Join(dir, db.ImportMetadataFileName)\n\n\twriter, err := afero.NewOsFs().Create(metadataFilePath)\n\trequire.NoError(t, err)\n\tdefer func() { _ = writer.Close() }()\n\tenc := json.NewEncoder(writer)\n\tenc.SetIndent(\"\", \" \")\n\n\tdoc := db.ImportMetadata{\n\t\tDigest:        checksums,\n\t\tClientVersion: ver,\n\t}\n\n\trequire.NoError(t, enc.Encode(doc))\n}\n\nfunc writeTestDB(t *testing.T, fs afero.Fs, dir string) string {\n\trequire.NoError(t, fs.MkdirAll(dir, 0755))\n\n\trw, err := db.NewWriter(db.Config{\n\t\tDBDirPath: dir,\n\t})\n\trequire.NoError(t, err)\n\n\trequire.NoError(t, rw.SetDBMetadata())\n\trequire.NoError(t, rw.Close())\n\n\tdoc, err := db.WriteImportMetadata(fs, dir, \"source\")\n\trequire.NoError(t, err)\n\trequire.NotNil(t, doc)\n\n\treturn doc.Digest\n}\n\nfunc TestCurator_Update(t *testing.T) {\n\n\tt.Run(\"happy path: successful update\", func(t *testing.T) {\n\t\tc := setupCuratorForUpdate(t, withWorkingUpdateIntegrations())\n\t\tmc := c.client.(*mockClient)\n\t\t// nop hydrator, assert error if NOT called\n\t\thydrateCalled := false\n\t\tc.hydrator = func(string) error {\n\t\t\thydrateCalled = true\n\t\t\treturn nil\n\t\t}\n\n\t\tupdated, err := c.Update()\n\n\t\trequire.NoError(t, err)\n\t\trequire.True(t, updated)\n\t\trequire.FileExists(t, filepath.Join(c.config.DBDirectoryPath(), lastUpdateCheckFileName))\n\n\t\tmc.AssertExpectations(t)\n\t\tassert.True(t, hydrateCalled, \"expected hydrator to be called\")\n\t})\n\n\tt.Run(\"error checking for updates\", func(t *testing.T) {\n\t\tc := setupCuratorForUpdate(t)\n\t\tmc := c.client.(*mockClient)\n\n\t\tmc.On(\"IsUpdateAvailable\", mock.Anything).Return(nil, errors.New(\"check failed\"))\n\n\t\tupdated, err := c.Update()\n\n\t\trequire.Error(t, err)\n\t\trequire.False(t, updated)\n\t\trequire.NoFileExists(t, filepath.Join(c.config.DBDirectoryPath(), lastUpdateCheckFileName))\n\n\t\tmc.AssertExpectations(t)\n\t})\n\n\tt.Run(\"error during download\", func(t *testing.T) {\n\t\tc := setupCuratorForUpdate(t)\n\t\tmc := c.client.(*mockClient)\n\n\t\tmc.On(\"IsUpdateAvailable\", mock.Anything).Return(&distribution.Archive{}, nil)\n\t\tmc.On(\"Download\", mock.Anything, mock.Anything, mock.Anything).Return(\"\", errors.New(\"download failed\"))\n\n\t\tupdated, err := c.Update()\n\n\t\trequire.ErrorContains(t, err, \"download failed\")\n\t\trequire.False(t, updated)\n\t\trequire.NoFileExists(t, filepath.Join(c.config.DBDirectoryPath(), lastUpdateCheckFileName))\n\n\t\tmc.AssertExpectations(t)\n\t})\n\n\tt.Run(\"error during activation: cannot move dir\", func(t *testing.T) {\n\t\tc := setupCuratorForUpdate(t, withWorkingUpdateIntegrations())\n\t\tmc := c.client.(*mockClient)\n\t\t// nop hydrator\n\t\tc.hydrator = nil\n\n\t\t// simulate not being able to move the staged dir to the db dir\n\t\tc.fs = afero.NewReadOnlyFs(c.fs)\n\n\t\tupdated, err := c.Update()\n\n\t\trequire.ErrorContains(t, err, \"operation not permitted\")\n\t\trequire.False(t, updated)\n\t\trequire.NoFileExists(t, filepath.Join(c.config.DBDirectoryPath(), lastUpdateCheckFileName))\n\n\t\tmc.AssertExpectations(t)\n\t})\n}\n\nfunc TestCurator_IsUpdateCheckAllowed(t *testing.T) {\n\n\tnewCurator := func(t *testing.T) curator {\n\t\ttempDir := t.TempDir()\n\n\t\tcfg := testConfig()\n\t\tcfg.UpdateCheckMaxFrequency = 10 * time.Minute\n\t\tcfg.DBRootDir = tempDir\n\n\t\tci, err := NewCurator(cfg, nil)\n\t\trequire.NoError(t, err)\n\n\t\tc := ci.(curator)\n\t\treturn c\n\t}\n\n\twriteLastCheckContents := func(t *testing.T, cfg Config, contents string) {\n\t\trequire.NoError(t, os.MkdirAll(cfg.DBDirectoryPath(), 0755))\n\t\tp := filepath.Join(cfg.DBDirectoryPath(), lastUpdateCheckFileName)\n\t\terr := os.WriteFile(p, []byte(contents), 0644)\n\t\trequire.NoError(t, err)\n\t}\n\n\twriteLastCheckTime := func(t *testing.T, cfg Config, lastCheckTime time.Time) {\n\t\twriteLastCheckContents(t, cfg, lastCheckTime.Format(time.RFC3339))\n\t}\n\n\tt.Run(\"first run check (no last check file)\", func(t *testing.T) {\n\t\tc := newCurator(t)\n\t\trequire.True(t, c.isUpdateCheckAllowed())\n\t})\n\n\tt.Run(\"check not allowed due to frequency\", func(t *testing.T) {\n\t\tc := newCurator(t)\n\t\twriteLastCheckTime(t, c.config, time.Now().Add(-5*time.Minute))\n\n\t\trequire.False(t, c.isUpdateCheckAllowed())\n\t})\n\n\tt.Run(\"check allowed after the frequency period\", func(t *testing.T) {\n\t\tc := newCurator(t)\n\t\twriteLastCheckTime(t, c.config, time.Now().Add(-20*time.Minute))\n\n\t\trequire.True(t, c.isUpdateCheckAllowed())\n\t})\n\n\tt.Run(\"error reading last check file\", func(t *testing.T) {\n\t\tc := newCurator(t)\n\n\t\t// simulate a situation where the last check file exists but is corrupted\n\t\twriteLastCheckContents(t, c.config, \"invalid timestamp\")\n\n\t\tallowed := c.isUpdateCheckAllowed()\n\t\trequire.True(t, allowed) // should return true since an error is encountered\n\t})\n\n}\n\nfunc TestCurator_DurationSinceUpdateCheck(t *testing.T) {\n\tnewCurator := func(t *testing.T) curator {\n\t\ttempDir := t.TempDir()\n\n\t\tcfg := testConfig()\n\t\tcfg.DBRootDir = tempDir\n\n\t\tci, err := NewCurator(cfg, nil)\n\t\trequire.NoError(t, err)\n\n\t\tc := ci.(curator)\n\t\treturn c\n\t}\n\n\twriteLastCheckContents := func(t *testing.T, cfg Config, contents string) {\n\t\trequire.NoError(t, os.MkdirAll(cfg.DBDirectoryPath(), 0755))\n\t\tp := filepath.Join(cfg.DBDirectoryPath(), lastUpdateCheckFileName)\n\t\terr := os.WriteFile(p, []byte(contents), 0644)\n\t\trequire.NoError(t, err)\n\t}\n\n\tt.Run(\"no last check file\", func(t *testing.T) {\n\t\tc := newCurator(t)\n\t\telapsed, err := c.durationSinceUpdateCheck()\n\t\trequire.NoError(t, err)\n\t\trequire.Nil(t, elapsed) // should be nil since no file exists\n\t})\n\n\tt.Run(\"valid last check file\", func(t *testing.T) {\n\t\tc := newCurator(t)\n\t\twriteLastCheckContents(t, c.config, time.Now().Add(-5*time.Minute).Format(time.RFC3339))\n\n\t\telapsed, err := c.durationSinceUpdateCheck()\n\t\trequire.NoError(t, err)\n\t\trequire.NotNil(t, elapsed)\n\t\trequire.True(t, *elapsed >= 5*time.Minute) // should be at least 5 minutes\n\t})\n\n\tt.Run(\"malformed last check file\", func(t *testing.T) {\n\t\tc := newCurator(t)\n\t\twriteLastCheckContents(t, c.config, \"invalid timestamp\")\n\n\t\t_, err := c.durationSinceUpdateCheck()\n\t\trequire.Error(t, err)\n\t\trequire.Contains(t, err.Error(), \"unable to parse last update check timestamp\")\n\t})\n}\n\nfunc TestCurator_SetLastSuccessfulUpdateCheck(t *testing.T) {\n\tnewCurator := func(t *testing.T) curator {\n\t\ttempDir := t.TempDir()\n\n\t\tcfg := testConfig()\n\t\tcfg.DBRootDir = tempDir\n\n\t\tci, err := NewCurator(cfg, nil)\n\t\trequire.NoError(t, err)\n\n\t\tc := ci.(curator)\n\n\t\trequire.NoError(t, c.fs.MkdirAll(c.config.DBDirectoryPath(), 0755))\n\n\t\treturn c\n\t}\n\n\tt.Run(\"set last successful update check\", func(t *testing.T) {\n\t\tc := newCurator(t)\n\n\t\tc.setLastSuccessfulUpdateCheck()\n\n\t\tdata, err := afero.ReadFile(c.fs, filepath.Join(c.config.DBDirectoryPath(), lastUpdateCheckFileName))\n\t\trequire.NoError(t, err)\n\n\t\tlastCheckTime, err := time.Parse(time.RFC3339, string(data))\n\t\trequire.NoError(t, err)\n\t\trequire.WithinDuration(t, time.Now().UTC(), lastCheckTime, time.Second)\n\t})\n\n\tt.Run(\"error writing last successful update check\", func(t *testing.T) {\n\t\tc := newCurator(t)\n\n\t\t// make the file system read-only to simulate a write error\n\t\treadonlyFs := afero.NewReadOnlyFs(c.fs)\n\t\tc.fs = readonlyFs\n\n\t\tc.setLastSuccessfulUpdateCheck()\n\n\t\trequire.NoFileExists(t, filepath.Join(c.config.DBDirectoryPath(), lastUpdateCheckFileName))\n\t})\n\n\tt.Run(\"ensure last successful update check file is created\", func(t *testing.T) {\n\t\tc := newCurator(t)\n\n\t\tc.setLastSuccessfulUpdateCheck()\n\n\t\trequire.FileExists(t, filepath.Join(c.config.DBDirectoryPath(), lastUpdateCheckFileName))\n\t})\n}\n\nfunc TestCurator_validateAge(t *testing.T) {\n\tnewCurator := func(t *testing.T) curator {\n\t\ttempDir := t.TempDir()\n\t\tcfg := testConfig()\n\t\tcfg.DBRootDir = tempDir\n\t\tcfg.MaxAllowedBuiltAge = 48 * time.Hour // set max age to 48 hours\n\n\t\tci, err := NewCurator(cfg, new(mockClient))\n\t\trequire.NoError(t, err)\n\n\t\treturn ci.(curator)\n\t}\n\n\thoursAgo := func(h int) db.Time {\n\t\treturn db.Time{Time: time.Now().UTC().Add(-time.Duration(h) * time.Hour)}\n\t}\n\n\ttests := []struct {\n\t\tname         string\n\t\tdescription  *db.Description\n\t\twantErr      require.ErrorAssertionFunc\n\t\tmodifyConfig func(*Config)\n\t}{\n\t\t{\n\t\t\tname: \"valid metadata within age limit\",\n\t\t\tdescription: &db.Description{\n\t\t\t\tBuilt: hoursAgo(24),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"stale metadata exactly at age limit\",\n\t\t\tdescription: &db.Description{\n\t\t\t\tBuilt: hoursAgo(48),\n\t\t\t},\n\t\t\twantErr: func(t require.TestingT, err error, msgAndArgs ...interface{}) {\n\t\t\t\trequire.ErrorContains(t, err, \"the vulnerability database was built\")\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"stale metadata\",\n\t\t\tdescription: &db.Description{\n\t\t\t\tBuilt: hoursAgo(50),\n\t\t\t},\n\t\t\twantErr: func(t require.TestingT, err error, msgAndArgs ...interface{}) {\n\t\t\t\trequire.ErrorContains(t, err, \"the vulnerability database was built\")\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:        \"no metadata\",\n\t\t\tdescription: nil,\n\t\t\twantErr: func(t require.TestingT, err error, msgAndArgs ...interface{}) {\n\t\t\t\trequire.ErrorContains(t, err, \"no metadata to validate\")\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"age validation disabled\",\n\t\t\tdescription: &db.Description{\n\t\t\t\tBuilt: hoursAgo(50),\n\t\t\t},\n\t\t\tmodifyConfig: func(cfg *Config) {\n\t\t\t\tcfg.ValidateAge = false\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif tt.wantErr == nil {\n\t\t\t\ttt.wantErr = require.NoError\n\t\t\t}\n\n\t\t\tc := newCurator(t)\n\n\t\t\tif tt.modifyConfig != nil {\n\t\t\t\ttt.modifyConfig(&c.config)\n\t\t\t}\n\n\t\t\terr := c.validateAge(tt.description)\n\t\t\ttt.wantErr(t, err)\n\t\t})\n\t}\n}\n\nfunc TestCurator_validateIntegrity(t *testing.T) {\n\tnewCurator := func(t *testing.T) (curator, *db.Description) {\n\t\ttempDir := t.TempDir()\n\t\tcfg := testConfig()\n\t\tcfg.DBRootDir = tempDir\n\n\t\trequire.NoError(t, os.MkdirAll(cfg.DBDirectoryPath(), 0755))\n\n\t\tsw := setupTestDB(t, cfg.DBDirectoryPath())\n\t\trequire.NoError(t, sw.SetDBMetadata())\n\t\trequire.NoError(t, sw.Close())\n\t\ts := setupReadOnlyTestDB(t, cfg.DBDirectoryPath())\n\n\t\t// assume that we already have a valid checksum file\n\t\tdigest, err := db.CalculateDBDigest(afero.NewOsFs(), cfg.DBFilePath())\n\t\trequire.NoError(t, err)\n\n\t\twriteTestImportMetadata(t, afero.NewOsFs(), cfg.DBDirectoryPath(), digest)\n\n\t\tci, err := NewCurator(cfg, new(mockClient))\n\t\trequire.NoError(t, err)\n\n\t\tm, err := s.GetDBMetadata()\n\t\trequire.NoError(t, err)\n\n\t\treturn ci.(curator), db.DescriptionFromMetadata(m)\n\t}\n\n\tt.Run(\"valid metadata with correct checksum\", func(t *testing.T) {\n\t\tc, d := newCurator(t)\n\n\t\tdigest, err := c.validateIntegrity(d)\n\t\trequire.NoError(t, err)\n\t\trequire.NotEmpty(t, digest)\n\t})\n\n\tt.Run(\"db does not exist\", func(t *testing.T) {\n\t\tc, d := newCurator(t)\n\n\t\trequire.NoError(t, os.Remove(c.config.DBFilePath()))\n\n\t\t_, err := c.validateIntegrity(d)\n\t\trequire.ErrorContains(t, err, \"database does not exist\")\n\t})\n\n\tt.Run(\"import metadata file does not exist\", func(t *testing.T) {\n\t\tc, d := newCurator(t)\n\t\tdbDir := c.config.DBDirectoryPath()\n\t\trequire.NoError(t, os.Remove(filepath.Join(dbDir, db.ImportMetadataFileName)))\n\t\t_, err := c.validateIntegrity(d)\n\t\trequire.ErrorContains(t, err, \"no import metadata\")\n\t})\n\n\tt.Run(\"invalid checksum\", func(t *testing.T) {\n\t\tc, d := newCurator(t)\n\t\tdbDir := c.config.DBDirectoryPath()\n\n\t\twriteTestImportMetadata(t, c.fs, dbDir, \"xxh64:invalidchecksum\")\n\n\t\t_, err := c.validateIntegrity(d)\n\t\trequire.ErrorContains(t, err, \"bad db checksum\")\n\t})\n\n\tt.Run(\"unsupported database version\", func(t *testing.T) {\n\t\tc, d := newCurator(t)\n\n\t\td.SchemaVersion = schemaver.New(db.ModelVersion-1, 0, 0)\n\n\t\t_, err := c.validateIntegrity(d)\n\t\trequire.ErrorContains(t, err, \"unsupported database version\")\n\t})\n}\n\nfunc TestReplaceDB(t *testing.T) {\n\tcases := []struct {\n\t\tname     string\n\t\tconfig   Config\n\t\texpected map[string]string // expected file name to content mapping in the DB dir\n\t\tinit     func(t *testing.T, dir string, dbDir string) afero.Fs\n\t\twantErr  require.ErrorAssertionFunc\n\t\tverify   func(t *testing.T, fs afero.Fs, config Config, expected map[string]string)\n\t}{\n\t\t{\n\t\t\tname: \"replace non-existent DB\",\n\t\t\tconfig: Config{\n\t\t\t\tDBRootDir: \"/test\",\n\t\t\t},\n\t\t\texpected: map[string]string{\n\t\t\t\t\"file.txt\": \"new content\",\n\t\t\t},\n\t\t\tinit: func(t *testing.T, dir string, dbDir string) afero.Fs {\n\t\t\t\tfs := afero.NewBasePathFs(afero.NewOsFs(), t.TempDir())\n\t\t\t\trequire.NoError(t, fs.MkdirAll(dir, 0700))\n\t\t\t\trequire.NoError(t, afero.WriteFile(fs, filepath.Join(dir, \"file.txt\"), []byte(\"new content\"), 0644))\n\t\t\t\treturn fs\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"replace existing DB\",\n\t\t\tconfig: Config{\n\t\t\t\tDBRootDir: \"/test\",\n\t\t\t},\n\t\t\texpected: map[string]string{\n\t\t\t\t\"new_file.txt\": \"new content\",\n\t\t\t},\n\t\t\tinit: func(t *testing.T, dir string, dbDir string) afero.Fs {\n\t\t\t\tfs := afero.NewBasePathFs(afero.NewOsFs(), t.TempDir())\n\t\t\t\trequire.NoError(t, fs.MkdirAll(dbDir, 0700))\n\t\t\t\trequire.NoError(t, afero.WriteFile(fs, filepath.Join(dbDir, \"old_file.txt\"), []byte(\"old content\"), 0644))\n\t\t\t\trequire.NoError(t, fs.MkdirAll(dir, 0700))\n\t\t\t\trequire.NoError(t, afero.WriteFile(fs, filepath.Join(dir, \"new_file.txt\"), []byte(\"new content\"), 0644))\n\t\t\t\treturn fs\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"non-existent parent dir creation\",\n\t\t\tconfig: Config{\n\t\t\t\tDBRootDir: \"/dir/does/not/exist/db3\",\n\t\t\t},\n\t\t\texpected: map[string]string{\n\t\t\t\t\"file.txt\": \"new content\",\n\t\t\t},\n\t\t\tinit: func(t *testing.T, dir string, dbDir string) afero.Fs {\n\t\t\t\tfs := afero.NewBasePathFs(afero.NewOsFs(), t.TempDir())\n\t\t\t\trequire.NoError(t, fs.MkdirAll(dir, 0700))\n\t\t\t\trequire.NoError(t, afero.WriteFile(fs, filepath.Join(dir, \"file.txt\"), []byte(\"new content\"), 0644))\n\t\t\t\treturn fs\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"error during rename\",\n\t\t\tconfig: Config{\n\t\t\t\tDBRootDir: \"/test\",\n\t\t\t},\n\t\t\texpected: nil, // no files expected since operation fails\n\t\t\tinit: func(t *testing.T, dir string, dbDir string) afero.Fs {\n\t\t\t\tfs := afero.NewBasePathFs(afero.NewOsFs(), t.TempDir())\n\t\t\t\trequire.NoError(t, fs.MkdirAll(dir, 0700))\n\t\t\t\trequire.NoError(t, afero.WriteFile(fs, filepath.Join(dir, \"file.txt\"), []byte(\"content\"), 0644))\n\t\t\t\treturn afero.NewReadOnlyFs(fs)\n\t\t\t},\n\t\t\twantErr: require.Error,\n\t\t\tverify: func(t *testing.T, fs afero.Fs, config Config, expected map[string]string) {\n\t\t\t\t_, err := fs.Stat(config.DBDirectoryPath())\n\t\t\t\trequire.Error(t, err)\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tif tc.wantErr == nil {\n\t\t\t\ttc.wantErr = require.NoError\n\t\t\t}\n\t\t\tdbDir := tc.config.DBDirectoryPath()\n\t\t\tcandidateDir := \"/temp/db\"\n\t\t\tfs := tc.init(t, candidateDir, dbDir)\n\n\t\t\tc := curator{\n\t\t\t\tfs:     fs,\n\t\t\t\tconfig: tc.config,\n\t\t\t}\n\n\t\t\terr := c.replaceDB(candidateDir)\n\t\t\ttc.wantErr(t, err)\n\t\t\tif tc.verify != nil {\n\t\t\t\ttc.verify(t, fs, tc.config, tc.expected)\n\t\t\t}\n\t\t\tif err != nil {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tfor fileName, expectedContent := range tc.expected {\n\t\t\t\tfilePath := filepath.Join(tc.config.DBDirectoryPath(), fileName)\n\t\t\t\tactualContent, err := afero.ReadFile(fs, filePath)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.Equal(t, expectedContent, string(actualContent))\n\t\t\t}\n\t\t})\n\t}\n}\nfunc Test_isRehydrationNeeded(t *testing.T) {\n\ttests := []struct {\n\t\tname               string\n\t\tcurrentDBVersion   schemaver.SchemaVer\n\t\thydrationClientVer schemaver.SchemaVer\n\t\tcurrentClientVer   schemaver.SchemaVer\n\t\texpectedResult     bool\n\t\texpectedErr        string\n\t}{\n\t\t{\n\t\t\tname:             \"no database exists\",\n\t\t\tcurrentDBVersion: schemaver.SchemaVer{},\n\t\t\tcurrentClientVer: schemaver.New(6, 2, 0),\n\t\t\texpectedResult:   false,\n\t\t},\n\t\t{\n\t\t\tname:             \"no import metadata exists\",\n\t\t\tcurrentDBVersion: schemaver.New(6, 0, 0),\n\t\t\tcurrentClientVer: schemaver.New(6, 2, 0),\n\t\t\texpectedErr:      \"unable to read import metadata\",\n\t\t\texpectedResult:   false,\n\t\t},\n\t\t{\n\t\t\tname:               \"invalid client version in metadata\",\n\t\t\tcurrentDBVersion:   schemaver.New(6, 0, 0),\n\t\t\thydrationClientVer: schemaver.SchemaVer{-19, 0, 0},\n\t\t\tcurrentClientVer:   schemaver.New(6, 2, 0),\n\t\t\texpectedResult:     false,\n\t\t\texpectedErr:        \"unable to parse client version from import metadata\",\n\t\t},\n\t\t{\n\t\t\tname:               \"rehydration needed\",\n\t\t\tcurrentDBVersion:   schemaver.New(6, 0, 1),\n\t\t\thydrationClientVer: schemaver.New(6, 0, 0),\n\t\t\tcurrentClientVer:   schemaver.New(6, 0, 2),\n\t\t\texpectedResult:     true,\n\t\t},\n\t\t{\n\t\t\tname:               \"no rehydration needed - client version equals current client version\",\n\t\t\tcurrentDBVersion:   schemaver.New(6, 0, 0),\n\t\t\thydrationClientVer: schemaver.New(6, 2, 0),\n\t\t\tcurrentClientVer:   schemaver.New(6, 2, 0),\n\t\t\texpectedResult:     false,\n\t\t},\n\t\t{\n\t\t\tname:               \"no rehydration needed - client version greater than current client version\",\n\t\t\tcurrentDBVersion:   schemaver.New(6, 0, 0),\n\t\t\thydrationClientVer: schemaver.New(6, 3, 0),\n\t\t\tcurrentClientVer:   schemaver.New(6, 2, 0),\n\t\t\texpectedResult:     false,\n\t\t},\n\t\t{\n\t\t\t// there are cases where new features will result in new columns, thus an old client downloading and hydrating\n\t\t\t// a DB should function, however, when the new client is downloaded it should trigger at least a rehydration\n\t\t\t// of the existing DB (in cases where the new DB is not available for download yet).\n\t\t\tname:               \"rehydration needed - we have a new client version, with an old DB version\",\n\t\t\tcurrentDBVersion:   schemaver.New(6, 0, 2),\n\t\t\thydrationClientVer: schemaver.New(6, 0, 2),\n\t\t\tcurrentClientVer:   schemaver.New(6, 0, 3),\n\t\t\texpectedResult:     true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tfs := afero.NewOsFs()\n\t\t\ttestDir := t.TempDir()\n\n\t\t\tif tt.hydrationClientVer.Model != 0 {\n\t\t\t\twriteTestImportMetadataWithCustomVersion(t, fs, testDir, \"xxh64:something\", tt.hydrationClientVer.String())\n\t\t\t}\n\n\t\t\tvar dbVersion *schemaver.SchemaVer\n\t\t\tif tt.currentDBVersion.Model != 0 {\n\t\t\t\tdbVersion = &tt.currentDBVersion\n\t\t\t}\n\n\t\t\tresult, err := isRehydrationNeeded(fs, testDir, dbVersion, tt.currentClientVer)\n\n\t\t\tif tt.expectedErr != \"\" {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\tassert.Contains(t, err.Error(), tt.expectedErr)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tassert.Equal(t, tt.expectedResult, result)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestCurator_Update_UsesDBRootDirForDownloadTempBase(t *testing.T) {\n\tc := newTestCurator(t) // This sets up c.fs as afero.NewOsFs() rooted in t.TempDir()\n\tmc := c.client.(*mockClient)\n\n\t// This is the path that the mocked Download method will return.\n\t// It simulates a temporary directory created by the download client within DBRootDir.\n\texpectedDownloadedContentPath := filepath.Join(c.config.DBRootDir, \"temp-downloaded-db-content-123\")\n\n\t// Pre-create this directory and make it look like a valid DB source for the hydrator and replaceDB.\n\trequire.NoError(t, c.fs.MkdirAll(expectedDownloadedContentPath, 0755))\n\t// Write minimal valid DB metadata so that hydration/activation can proceed far enough.\n\t// Using existing helpers to create a semblance of a DB.\n\twriteTestDB(t, c.fs, expectedDownloadedContentPath) // This creates a basic DB file and import metadata.\n\n\t// Mock client responses\n\tmc.On(\"IsUpdateAvailable\", mock.Anything).Return(&distribution.Archive{}, nil)\n\t// CRUCIAL ASSERTION:\n\t// Verify that Download is called with c.config.DBRootDir as its second argument (baseDirForTemp).\n\t// It will return the expectedDownloadedContentPath, simulating successful download and extraction.\n\tmc.On(\"Download\", mock.Anything, c.config.DBRootDir, mock.Anything).Return(expectedDownloadedContentPath, nil)\n\n\thydrateCalled := false\n\tc.hydrator = func(path string) error {\n\t\t// Ensure hydrator is called with the path returned by Download\n\t\tassert.Equal(t, expectedDownloadedContentPath, path, \"hydrator called with incorrect path\")\n\t\thydrateCalled = true\n\t\treturn nil // Simulate successful hydration\n\t}\n\n\t// Call Update to trigger the download and activation sequence\n\tupdated, err := c.Update()\n\n\t// Assertions\n\trequire.NoError(t, err, \"Update should succeed\")\n\trequire.True(t, updated, \"Update should report true\")\n\tmc.AssertExpectations(t) // Verifies that Download was called with the expected arguments\n\tassert.True(t, hydrateCalled, \"expected hydrator to be called\")\n\n\t// Check if the DB was \"activated\" (i.e., renamed)\n\tfinalDBPath := c.config.DBDirectoryPath()\n\t_, err = c.fs.Stat(finalDBPath)\n\trequire.NoError(t, err, \"final DB directory should exist after successful update\")\n\t// And the temporary downloaded content path should no longer exist as it was renamed\n\t_, err = c.fs.Stat(expectedDownloadedContentPath)\n\trequire.True(t, os.IsNotExist(err), \"temporary download path should not exist after rename\")\n}\n\nfunc TestCurator_Update_CleansUpDownloadDirOnActivationFailure(t *testing.T) {\n\tc := newTestCurator(t) // Sets up c.fs as afero.NewOsFs() rooted in t.TempDir()\n\tmc := c.client.(*mockClient)\n\n\t// This is the path that the mocked Download method will return.\n\t// This directory should be cleaned up if activation fails.\n\tdownloadedContentPath := filepath.Join(c.config.DBRootDir, \"temp-download-to-be-cleaned-up\")\n\n\t// Simulate the download client successfully creating this directory.\n\trequire.NoError(t, c.fs.MkdirAll(downloadedContentPath, 0755))\n\t// Optionally, put a dummy file inside to make the cleanup more tangible.\n\trequire.NoError(t, afero.WriteFile(c.fs, filepath.Join(downloadedContentPath, \"dummy_file.txt\"), []byte(\"test data\"), 0644))\n\n\t// Mock client responses\n\tmc.On(\"IsUpdateAvailable\", mock.Anything).Return(&distribution.Archive{}, nil)\n\t// Download is called with DBRootDir as base, and returns the path to the (simulated) downloaded content.\n\tmc.On(\"Download\", mock.Anything, c.config.DBRootDir, mock.Anything).Return(downloadedContentPath, nil)\n\n\t// Configure the hydrator to fail, which will cause c.activate() to fail.\n\texpectedHydrationError := \"simulated hydration failure\"\n\tc.hydrator = func(path string) error {\n\t\tassert.Equal(t, downloadedContentPath, path, \"hydrator called with incorrect path\")\n\t\treturn errors.New(expectedHydrationError)\n\t}\n\n\t// Call Update, expecting it to fail during activation.\n\tupdated, err := c.Update()\n\n\t// Assertions\n\trequire.Error(t, err, \"Update should fail due to activation error\")\n\trequire.Contains(t, err.Error(), expectedHydrationError, \"Error message should reflect hydration failure\")\n\trequire.False(t, updated, \"Update should report false on failure\")\n\tmc.AssertExpectations(t) // Verifies Download was called as expected.\n\n\t// CRUCIAL ASSERTION:\n\t// Verify that the temporary download directory was cleaned up.\n\t_, statErr := c.fs.Stat(downloadedContentPath)\n\trequire.True(t, os.IsNotExist(statErr), \"expected temporary download directory to be cleaned up after activation failure\")\n}\n\n// Test for the Import path (URL case) - very similar to the Update tests\nfunc TestCurator_Import_URL_UsesDBRootDirForDownloadTempBaseAndCleansUp(t *testing.T) {\n\tt.Run(\"successful import from URL\", func(t *testing.T) {\n\t\tc := newTestCurator(t)\n\t\tmc := c.client.(*mockClient)\n\n\t\timportURL := \"http://localhost/some/db.tar.gz\"\n\t\texpectedDownloadedContentPath := filepath.Join(c.config.DBRootDir, \"temp-imported-db-content-url\")\n\n\t\trequire.NoError(t, c.fs.MkdirAll(expectedDownloadedContentPath, 0755))\n\t\twriteTestDB(t, c.fs, expectedDownloadedContentPath)\n\n\t\tmc.On(\"Download\", importURL, c.config.DBRootDir, mock.Anything).Return(expectedDownloadedContentPath, nil)\n\n\t\thydrateCalled := false\n\t\tc.hydrator = func(path string) error {\n\t\t\tassert.Equal(t, expectedDownloadedContentPath, path)\n\t\t\thydrateCalled = true\n\t\t\treturn nil\n\t\t}\n\n\t\terr := c.Import(importURL)\n\n\t\trequire.NoError(t, err)\n\t\tmc.AssertExpectations(t)\n\t\tassert.True(t, hydrateCalled)\n\t\t_, err = c.fs.Stat(c.config.DBDirectoryPath())\n\t\trequire.NoError(t, err, \"final DB directory should exist\")\n\t\t_, err = c.fs.Stat(expectedDownloadedContentPath)\n\t\trequire.True(t, os.IsNotExist(err), \"temp import path should not exist after rename\")\n\t})\n\n\tt.Run(\"import from URL fails activation\", func(t *testing.T) {\n\t\tc := newTestCurator(t)\n\t\tmc := c.client.(*mockClient)\n\n\t\timportURL := \"http://localhost/some/other/db.tar.gz\"\n\t\tdownloadedContentPath := filepath.Join(c.config.DBRootDir, \"temp-imported-to-cleanup-url\")\n\n\t\trequire.NoError(t, c.fs.MkdirAll(downloadedContentPath, 0755))\n\t\trequire.NoError(t, afero.WriteFile(c.fs, filepath.Join(downloadedContentPath, \"dummy.txt\"), []byte(\"test\"), 0644))\n\n\t\tmc.On(\"Download\", importURL, c.config.DBRootDir, mock.Anything).Return(downloadedContentPath, nil)\n\n\t\texpectedHydrationError := \"simulated hydration failure for import\"\n\t\tc.hydrator = func(path string) error {\n\t\t\treturn errors.New(expectedHydrationError)\n\t\t}\n\n\t\terr := c.Import(importURL)\n\n\t\trequire.Error(t, err)\n\t\trequire.Contains(t, err.Error(), expectedHydrationError)\n\t\tmc.AssertExpectations(t)\n\n\t\t_, statErr := c.fs.Stat(downloadedContentPath)\n\t\trequire.True(t, os.IsNotExist(statErr), \"expected temp import directory to be cleaned up\")\n\t})\n}\n\nfunc Test_unarchive(t *testing.T) {\n\ttestFile := filepath.Join(t.TempDir(), \"vulnerability.db\")\n\tf, err := os.Create(testFile)\n\trequire.NoError(t, err)\n\tf.Close()\n\n\tfiles, err := archives.FilesFromDisk(t.Context(), nil, map[string]string{\n\t\ttestFile: \"\",\n\t})\n\trequire.NoError(t, err)\n\n\tsource := filepath.Join(t.TempDir(), \"archive.tar.zst\")\n\tout, err := os.Create(source)\n\trequire.NoError(t, err)\n\n\tformat := archives.CompressedArchive{\n\t\tCompression: archives.Zstd{},\n\t\tArchival:    archives.Tar{},\n\t}\n\terr = format.Archive(t.Context(), out, files)\n\trequire.NoError(t, err)\n\n\tdestination := t.TempDir()\n\terr = unarchive(source, destination)\n\trequire.NoError(t, err)\n\n\texpectFile := filepath.Join(destination, \"vulnerability.db\")\n\trequire.FileExists(t, expectFile)\n}\n\nfunc setupTestDB(t *testing.T, dbDir string) db.ReadWriter {\n\ts, err := db.NewWriter(db.Config{\n\t\tDBDirPath: dbDir,\n\t})\n\trequire.NoError(t, err)\n\n\treturn s\n}\n\nfunc setupReadOnlyTestDB(t *testing.T, dbDir string) db.Reader {\n\ts, err := db.NewReader(db.Config{\n\t\tDBDirPath: dbDir,\n\t})\n\trequire.NoError(t, err)\n\n\treturn s\n}\n\nfunc testConfig() Config {\n\treturn DefaultConfig(clio.Identification{\n\t\tName: \"grype-test\",\n\t})\n}\n"
  },
  {
    "path": "grype/db/v6/log_dropped.go",
    "content": "package v6\n\nimport (\n\t\"github.com/anchore/go-logger\"\n\t\"github.com/anchore/grype/internal/log\"\n)\n\n// logDroppedVulnerability is a hook called when vulnerabilities are dropped from consideration in a vulnerability Provider,\n// this offers a convenient location to set a breakpoint\n//\n//go:noinline\nfunc logDroppedVulnerability(vuln string, reason any, fields logger.Fields) {\n\tfields[\"reason\"] = reason\n\tfields[\"vulnerability\"] = vuln\n\n\tlog.WithFields(fields).Trace(\"dropped vulnerability\")\n}\n"
  },
  {
    "path": "grype/db/v6/models.go",
    "content": "package v6\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/OneOfOne/xxhash\"\n\t\"gorm.io/gorm\"\n\n\t\"github.com/anchore/grype/internal/log\"\n\t\"github.com/anchore/grype/internal/schemaver\"\n)\n\nvar (\n\t// ensure that the generic packageHandleStore will function when type asserting\n\t_ blobable              = (*packageHandle)(nil)\n\t_ blobable              = (*AffectedPackageHandle)(nil)\n\t_ blobable              = (*UnaffectedPackageHandle)(nil)\n\t_ packageHandleAccessor = (*AffectedPackageHandle)(nil)\n\t_ packageHandleAccessor = (*UnaffectedPackageHandle)(nil)\n\n\t// ensure that the generic cpeHandleStore will function when type asserting\n\t_ blobable          = (*cpeHandle)(nil)\n\t_ blobable          = (*AffectedCPEHandle)(nil)\n\t_ blobable          = (*UnaffectedCPEHandle)(nil)\n\t_ cpeHandleAccessor = (*AffectedCPEHandle)(nil)\n\t_ cpeHandleAccessor = (*UnaffectedCPEHandle)(nil)\n)\n\nfunc Models() []any {\n\treturn []any{\n\t\t// core data store\n\t\t&Blob{},\n\n\t\t// non-domain info\n\t\t&DBMetadata{},\n\n\t\t// data source info\n\t\t&Provider{},\n\n\t\t// vulnerability related search tables\n\t\t&VulnerabilityHandle{},\n\t\t&VulnerabilityAlias{},\n\n\t\t// package related search tables\n\t\t&AffectedPackageHandle{},   // join on package, operating system\n\t\t&UnaffectedPackageHandle{}, // join on package, operating system\n\t\t&OperatingSystem{},\n\t\t&OperatingSystemSpecifierOverride{},\n\t\t&Package{},\n\t\t&PackageSpecifierOverride{},\n\n\t\t// CPE related search tables\n\t\t&AffectedCPEHandle{},   // join on CPE\n\t\t&UnaffectedCPEHandle{}, // join on CPE\n\t\t&Cpe{},\n\n\t\t// decorations to vulnerability records\n\t\t&KnownExploitedVulnerabilityHandle{},\n\t\t&EpssHandle{},\n\t\t&EpssMetadata{},\n\t\t&CWEHandle{},\n\t}\n}\n\ntype ID int64\n\n// core data store //////////////////////////////////////////////////////\n\ntype Blob struct {\n\tID    ID     `gorm:\"column:id;primaryKey\"`\n\tValue string `gorm:\"column:value;not null\"`\n}\n\nfunc (b Blob) computeDigest() string {\n\th := xxhash.New64()\n\tif _, err := h.Write([]byte(b.Value)); err != nil {\n\t\tlog.Errorf(\"unable to hash blob: %v\", err)\n\t\tpanic(err)\n\t}\n\treturn fmt.Sprintf(\"xxh64:%x\", h.Sum(nil))\n}\n\n// non-domain info //////////////////////////////////////////////////////\n\ntype DBMetadata struct {\n\tBuildTimestamp *time.Time `gorm:\"column:build_timestamp;not null\"`\n\tModel          int        `gorm:\"column:model;not null\"`\n\tRevision       int        `gorm:\"column:revision;not null\"`\n\tAddition       int        `gorm:\"column:addition;not null\"`\n}\n\nfunc newSchemaVerFromDBMetadata(m DBMetadata) schemaver.SchemaVer {\n\treturn schemaver.New(m.Model, m.Revision, m.Addition)\n}\n\n// data source info //////////////////////////////////////////////////////\n\n// Provider is the upstream data processor (usually Vunnel) that is responsible for vulnerability records. Each provider\n// should be scoped to a specific vulnerability dataset, for instance, the \"ubuntu\" provider for all records from\n// Canonicals' Ubuntu Security Notices (for all Ubuntu distro versions).\ntype Provider struct {\n\t// ID of the Vunnel provider (or sub processor responsible for data records from a single specific source, e.g. \"ubuntu\")\n\tID string `gorm:\"column:id;primaryKey\"`\n\n\t// Version of the Vunnel provider (or sub processor equivalent)\n\tVersion string `gorm:\"column:version\"`\n\n\t// Processor is the name of the application that processed the data (e.g. \"vunnel\")\n\tProcessor string `gorm:\"column:processor\"`\n\n\t// DateCaptured is the timestamp which the upstream data was pulled and processed\n\tDateCaptured *time.Time `gorm:\"column:date_captured\"`\n\n\t// InputDigest is a self describing hash (e.g. sha256:123... not 123...) of all data used by the provider to generate the vulnerability records\n\tInputDigest string `gorm:\"column:input_digest\"`\n}\n\nfunc (p *Provider) String() string {\n\tif p == nil {\n\t\treturn \"\"\n\t}\n\tdate := \"?\"\n\tif p.DateCaptured != nil {\n\t\tdate = p.DateCaptured.UTC().Format(time.RFC3339)\n\t}\n\treturn fmt.Sprintf(\"%s@v%s from %s using %q at %s\", p.ID, p.Version, p.Processor, p.InputDigest, date)\n}\n\nfunc (p *Provider) cacheKey() string {\n\treturn strings.ToLower(p.String())\n}\n\nfunc (p *Provider) tableName() string {\n\treturn cpesTableCacheKey\n}\n\nfunc (p *Provider) rowID() string {\n\treturn p.ID\n}\n\nfunc (p *Provider) setRowID(i string) {\n\tp.ID = i\n}\n\nfunc (p *Provider) BeforeCreate(tx *gorm.DB) (err error) {\n\tif cacheInst, ok := cacheFromContext(tx.Statement.Context); ok {\n\t\tif existingID, ok := cacheInst.getString(p); ok {\n\t\t\tp.setRowID(existingID)\n\t\t}\n\t\treturn nil\n\t}\n\treturn fmt.Errorf(\"provider creation is not supported\")\n}\n\nfunc (p *Provider) AfterCreate(tx *gorm.DB) (err error) {\n\tif cacheInst, ok := cacheFromContext(tx.Statement.Context); ok {\n\t\tcacheInst.set(p)\n\t}\n\treturn nil\n}\n\n// vulnerability related search tables //////////////////////////////////////////////////////\n\n// VulnerabilityHandle represents the pointer to the core advisory record for a single known vulnerability from a specific provider.\n// indexes: idx_vuln_provider_id: this is used --by-cve to find all vulnerabilities from the NVD provider\ntype VulnerabilityHandle struct {\n\tID ID `gorm:\"column:id;primaryKey\"`\n\n\t// Name is the unique name for the vulnerability (same as the decoded VulnerabilityBlob.ID)\n\tName string `gorm:\"column:name;not null;index,collate:NOCASE;index:idx_vuln_provider_id,collate:NOCASE\"`\n\n\t// Status conveys the actionability of the current record (one of \"active\", \"analyzing\", \"rejected\", \"disputed\")\n\tStatus VulnerabilityStatus `gorm:\"column:status;not null;index,collate:NOCASE\"`\n\n\t// PublishedDate is the date the vulnerability record was first published\n\tPublishedDate *time.Time `gorm:\"column:published_date;index\"`\n\n\t// ModifiedDate is the date the vulnerability record was last modified\n\tModifiedDate *time.Time `gorm:\"column:modified_date;index\"`\n\n\t// WithdrawnDate is the date the vulnerability record was withdrawn\n\tWithdrawnDate *time.Time `gorm:\"column:withdrawn_date;index\"`\n\n\tProviderID string    `gorm:\"column:provider_id;not null;index;index:idx_vuln_provider_id,collate:NOCASE\"`\n\tProvider   *Provider `gorm:\"foreignKey:ProviderID\"`\n\n\tBlobID    ID                 `gorm:\"column:blob_id;index,unique\"`\n\tBlobValue *VulnerabilityBlob `gorm:\"-\"`\n}\n\nfunc (v VulnerabilityHandle) String() string {\n\treturn fmt.Sprintf(\"%s/%s\", v.Provider, v.Name)\n}\n\nfunc (v VulnerabilityHandle) getBlobValue() any {\n\tif v.BlobValue == nil {\n\t\treturn nil // must return untyped nil or getBlobValue() == nil will always be false\n\t}\n\treturn v.BlobValue\n}\n\nfunc (v *VulnerabilityHandle) setBlobID(id ID) {\n\tv.BlobID = id\n}\n\nfunc (v VulnerabilityHandle) getBlobID() ID {\n\treturn v.BlobID\n}\n\nfunc (v *VulnerabilityHandle) setBlob(rawBlobValue []byte) error {\n\tvar blobValue VulnerabilityBlob\n\tif err := json.Unmarshal(rawBlobValue, &blobValue); err != nil {\n\t\treturn fmt.Errorf(\"unable to unmarshal vulnerability blob value: %w\", err)\n\t}\n\n\tv.BlobValue = &blobValue\n\treturn nil\n}\n\nfunc (v *VulnerabilityHandle) cacheKey() string {\n\tprovider := \"none\"\n\tif v.Provider != nil {\n\t\tprovider = v.Provider.ID\n\t}\n\treturn strings.ToLower(fmt.Sprintf(\"%s from %s with %d\", v.Name, provider, v.BlobID))\n}\n\nfunc (v *VulnerabilityHandle) rowID() ID {\n\treturn v.ID\n}\n\nfunc (v *VulnerabilityHandle) tableName() string {\n\treturn vulnerabilitiesTableCacheKey\n}\n\nfunc (v *VulnerabilityHandle) setRowID(i ID) {\n\tv.ID = i\n}\n\nfunc (v *VulnerabilityHandle) BeforeCreate(tx *gorm.DB) (err error) {\n\tif cacheInst, ok := cacheFromContext(tx.Statement.Context); ok {\n\t\tif existing, ok := cacheInst.getID(v); ok {\n\t\t\tv.setRowID(existing)\n\t\t}\n\n\t\treturn nil\n\t}\n\n\treturn fmt.Errorf(\"vulnerability creation is not supported\")\n}\n\nfunc (v *VulnerabilityHandle) AfterCreate(tx *gorm.DB) (err error) {\n\tif cacheInst, ok := cacheFromContext(tx.Statement.Context); ok {\n\t\tcacheInst.set(v)\n\t}\n\treturn nil\n}\n\ntype VulnerabilityAlias struct {\n\t// Name is the unique name for the vulnerability\n\tName string `gorm:\"column:name;primaryKey;index,collate:NOCASE\"`\n\n\t// Alias is an alternative name for the vulnerability that must be upstream from the Name (e.g if name is \"RHSA-1234\" then the upstream could be \"CVE-1234-5678\", but not the other way around)\n\tAlias string `gorm:\"column:alias;primaryKey;index,collate:NOCASE;not null\"`\n}\n\n// package related search tables //////////////////////////////////////////////////////\n\n// packageHandle represents a single package affected or unaffected by the specified vulnerability.\n// This is a shared struct used by both AffectedPackageHandle and UnaffectedPackageHandle. This is not a table itself.\ntype packageHandle struct {\n\tID              ID                   `gorm:\"column:id;primaryKey\"`\n\tVulnerabilityID ID                   `gorm:\"column:vulnerability_id;index;not null\"`\n\tVulnerability   *VulnerabilityHandle `gorm:\"foreignKey:VulnerabilityID\"`\n\n\tOperatingSystemID *ID              `gorm:\"column:operating_system_id;index\"`\n\tOperatingSystem   *OperatingSystem `gorm:\"foreignKey:OperatingSystemID\"`\n\n\tPackageID ID       `gorm:\"column:package_id;index\"`\n\tPackage   *Package `gorm:\"foreignKey:PackageID\"`\n\n\tBlobID    ID           `gorm:\"column:blob_id\"`\n\tBlobValue *PackageBlob `gorm:\"-\"`\n}\n\nfunc (ph packageHandle) vulnerability() string {\n\tif ph.Vulnerability != nil {\n\t\treturn ph.Vulnerability.Name\n\t}\n\tif ph.BlobValue != nil {\n\t\tif len(ph.BlobValue.CVEs) > 0 {\n\t\t\treturn ph.BlobValue.CVEs[0]\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc (ph packageHandle) String() string {\n\tvar fields []string\n\n\tif ph.BlobValue != nil {\n\t\tv := ph.BlobValue.String()\n\t\tif v != \"\" {\n\t\t\tfields = append(fields, v)\n\t\t}\n\t}\n\tif ph.OperatingSystem != nil {\n\t\tfields = append(fields, fmt.Sprintf(\"os=%q\", ph.OperatingSystem.String()))\n\t} else {\n\t\tfields = append(fields, fmt.Sprintf(\"os=%d\", ph.OperatingSystemID))\n\t}\n\n\tif ph.Package != nil {\n\t\tfields = append(fields, fmt.Sprintf(\"pkg=%q\", ph.Package.String()))\n\t} else {\n\t\tfields = append(fields, fmt.Sprintf(\"pkg=%d\", ph.PackageID))\n\t}\n\n\tif ph.Vulnerability != nil {\n\t\tfields = append(fields, fmt.Sprintf(\"vuln=%q\", ph.Vulnerability.String()))\n\t} else {\n\t\tfields = append(fields, fmt.Sprintf(\"vuln=%d\", ph.VulnerabilityID))\n\t}\n\n\treturn fmt.Sprintf(\"package(%s)\", strings.Join(fields, \", \"))\n}\n\nfunc (ph packageHandle) getBlobValue() any {\n\tif ph.BlobValue == nil {\n\t\treturn nil // must return untyped nil or getBlobValue() == nil will always be false\n\t}\n\treturn ph.BlobValue\n}\n\nfunc (ph *packageHandle) setBlobID(id ID) {\n\tph.BlobID = id\n}\n\nfunc (ph packageHandle) getBlobID() ID {\n\treturn ph.BlobID\n}\n\nfunc (ph *packageHandle) setBlob(rawBlobValue []byte) error {\n\tvar blobValue PackageBlob\n\tif err := json.Unmarshal(rawBlobValue, &blobValue); err != nil {\n\t\treturn fmt.Errorf(\"unable to unmarshal affected package blob value: %w\", err)\n\t}\n\n\tph.BlobValue = &blobValue\n\treturn nil\n}\n\n// AffectedPackageHandle represents a single package affected by the specified vulnerability.\n//\n// A package here is a name within a known ecosystem, such as \"python\" or \"golang\". It is important to note that this\n// table relates vulnerabilities to resolved packages. There are cases when we have package identifiers but are not\n// resolved to packages; for example, when we have a CPE but not a clear understanding of the package ecosystem and\n// authoritative name (which might or might not be the product name in the CPE), in which case AffectedCPEHandle\n// should be used.\ntype AffectedPackageHandle packageHandle\n\nfunc (ph *AffectedPackageHandle) getPackageHandle() *packageHandle {\n\treturn (*packageHandle)(ph)\n}\n\nfunc (ph AffectedPackageHandle) vulnerability() string { // nolint:unused // when implementing filter functions in the future this will be needed\n\treturn (packageHandle)(ph).vulnerability()\n}\n\nfunc (ph AffectedPackageHandle) String() string {\n\treturn (packageHandle)(ph).String()\n}\n\nfunc (ph AffectedPackageHandle) getBlobValue() any {\n\treturn (packageHandle)(ph).getBlobValue()\n}\n\nfunc (ph *AffectedPackageHandle) setBlobID(id ID) {\n\t(*packageHandle)(ph).setBlobID(id)\n}\n\nfunc (ph AffectedPackageHandle) getBlobID() ID {\n\treturn (packageHandle)(ph).getBlobID()\n}\n\nfunc (ph *AffectedPackageHandle) setBlob(rawBlobValue []byte) error {\n\treturn (*packageHandle)(ph).setBlob(rawBlobValue)\n}\n\n// UnaffectedPackageHandle represents a single package that is explicitly NOT affected by the specified vulnerability.\ntype UnaffectedPackageHandle packageHandle\n\nfunc (ph *UnaffectedPackageHandle) getPackageHandle() *packageHandle {\n\treturn (*packageHandle)(ph)\n}\n\nfunc (ph UnaffectedPackageHandle) vulnerability() string { // nolint:unused // when implementing filter functions in the future this will be needed\n\treturn (packageHandle)(ph).vulnerability()\n}\n\nfunc (ph UnaffectedPackageHandle) String() string {\n\treturn (packageHandle)(ph).String()\n}\n\nfunc (ph UnaffectedPackageHandle) getBlobValue() any {\n\treturn (packageHandle)(ph).getBlobValue()\n}\n\nfunc (ph *UnaffectedPackageHandle) setBlobID(id ID) {\n\t(*packageHandle)(ph).setBlobID(id)\n}\n\nfunc (ph UnaffectedPackageHandle) getBlobID() ID {\n\treturn (packageHandle)(ph).getBlobID()\n}\n\nfunc (ph *UnaffectedPackageHandle) setBlob(rawBlobValue []byte) error {\n\treturn (*packageHandle)(ph).setBlob(rawBlobValue)\n}\n\n// Package represents a package name within a known ecosystem, such as \"python\" or \"golang\".\ntype Package struct {\n\tID ID `gorm:\"column:id;primaryKey\"`\n\n\t// Ecosystem is the tooling and language ecosystem that the package is released within\n\tEcosystem string `gorm:\"column:ecosystem;index:idx_package,unique,collate:NOCASE\"`\n\n\t// Name is the name of the package within the ecosystem\n\tName string `gorm:\"column:name;index:idx_package,unique,collate:NOCASE;index:idx_package_name,collate:NOCASE\"`\n\n\t// CPEs is the list of Common Platform Enumeration (CPE) identifiers that represent this package\n\tCPEs []Cpe `gorm:\"many2many:package_cpes;\"`\n}\n\nfunc (p Package) String() string {\n\tvar cpes []string\n\tfor _, cpe := range p.CPEs {\n\t\tcpes = append(cpes, cpe.String())\n\t}\n\tif p.Ecosystem != \"\" && p.Name != \"\" {\n\t\tbase := fmt.Sprintf(\"%s/%s\", p.Ecosystem, p.Name)\n\t\tif len(cpes) == 0 {\n\t\t\treturn base\n\t\t}\n\n\t\treturn fmt.Sprintf(\"%s (%s)\", base, strings.Join(cpes, \", \"))\n\t}\n\n\treturn strings.Join(cpes, \", \")\n}\n\nfunc (p Package) cacheKey() string {\n\tif p.Ecosystem == \"\" && p.Name == \"\" {\n\t\treturn \"\"\n\t}\n\t// we're intentionally not including anything about CPEs here, since there is potentially a merge operation for\n\t// packages with CPEs we cannot reason about packages with CPEs in the cache, they must always pass through.\n\treturn strings.ToLower(fmt.Sprintf(\"%s/%s\", p.Ecosystem, p.Name))\n}\n\nfunc (p Package) rowID() ID {\n\treturn p.ID\n}\n\nfunc (p *Package) tableName() string {\n\treturn packagesTableCacheKey\n}\n\nfunc (p *Package) setRowID(i ID) {\n\tp.ID = i\n}\n\nfunc (p *Package) BeforeCreate(tx *gorm.DB) (err error) { // nolint:gocognit\n\tcacheInst, ok := cacheFromContext(tx.Statement.Context)\n\tif !ok {\n\t\treturn fmt.Errorf(\"cache not found in context\")\n\t}\n\n\tvar existingPackage Package\n\terr = tx.Preload(\"CPEs\").Where(\"ecosystem = ? collate nocase AND name = ? collate nocase\", p.Ecosystem, p.Name).First(&existingPackage).Error\n\tif err == nil {\n\t\t// package exists; merge CPEs\n\t\tfor _, newCPE := range p.CPEs {\n\t\t\tvar existingCPE Cpe\n\n\t\t\tif existingID, ok := cacheInst.getID(&newCPE); ok {\n\t\t\t\tif err := tx.Where(\"id = ?\", existingID).First(&existingCPE).Error; err != nil {\n\t\t\t\t\tif !errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to find CPE by ID %d: %w\", existingID, err)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif existingCPE.ID != 0 {\n\t\t\t\t// if the record already exists, then we should use the existing record\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// if the CPE does not exist, proceed with creating it\n\t\t\texistingPackage.CPEs = append(existingPackage.CPEs, newCPE)\n\n\t\t\tif err := tx.Create(&newCPE).Error; err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to create CPE %v for package %v: %w\", newCPE, existingPackage, err)\n\t\t\t}\n\t\t}\n\t\t// use the existing package instead of creating a new one\n\t\t*p = existingPackage\n\t\treturn nil\n\t}\n\treturn nil\n}\n\nfunc (p *Package) AfterCreate(tx *gorm.DB) (err error) {\n\tif cacheInst, ok := cacheFromContext(tx.Statement.Context); ok {\n\t\tcacheInst.set(p)\n\t\tfor _, cpe := range p.CPEs {\n\t\t\tcacheInst.set(&cpe)\n\t\t}\n\t}\n\treturn nil\n}\n\n// PackageSpecifierOverride is a table that allows for overriding fields on v6.PackageSpecifier instances when searching for specific Packages.\ntype PackageSpecifierOverride struct {\n\tEcosystem string `gorm:\"column:ecosystem;primaryKey;index:pkg_ecosystem_idx,collate:NOCASE\"`\n\n\t// below are the fields that should be used as replacement for fields in the Packages table\n\n\tReplacementEcosystem *string `gorm:\"column:replacement_ecosystem;primaryKey\"`\n}\n\n// OperatingSystem represents a specific release of an operating system. The resolution of the version is\n// relative to the available data by the vulnerability data provider, so though there may be major.minor.patch OS\n// releases, there may only be data available for major.minor.\ntype OperatingSystem struct {\n\tID ID `gorm:\"column:id;primaryKey\"`\n\n\t// Name is the operating system family name (e.g. \"debian\")\n\tName      string `gorm:\"column:name;index:os_idx,unique;index,collate:NOCASE\"`\n\tReleaseID string `gorm:\"column:release_id;index:os_idx,unique;index,collate:NOCASE\"`\n\n\t// MajorVersion is the major version of a specific release (e.g. \"10\" for debian 10)\n\tMajorVersion string `gorm:\"column:major_version;index:os_idx,unique;index\"`\n\n\t// MinorVersion is the minor version of a specific release (e.g. \"1\" for debian 10.1)\n\tMinorVersion string `gorm:\"column:minor_version;index:os_idx,unique;index\"`\n\n\t// LabelVersion is an optional non-codename string representation of the version (e.g. \"unstable\" or for debian:sid)\n\tLabelVersion string `gorm:\"column:label_version;index:os_idx,unique;index,collate:NOCASE\"`\n\n\t// Codename is the codename of a specific release (e.g. \"buster\" for debian 10)\n\tCodename string `gorm:\"column:codename;index,collate:NOCASE\"`\n\n\t// Channel is a string used to distinguish between fix and vulnerability data for the same OS release.\n\t// such as RHEL-9.4+EUS vs RHEL-9\n\tChannel string `gorm:\"column:channel;index:os_idx,unique;index,collate:NOCASE\"`\n\n\t// EOLDate is when this OS release reaches end-of-life (no more security updates)\n\tEOLDate *time.Time `gorm:\"column:eol_date;index\"`\n\n\t// EOASDate is when this OS release reaches end-of-active-support (reduced support, before full EOL)\n\tEOASDate *time.Time `gorm:\"column:eoas_date\"`\n}\n\nfunc (o *OperatingSystem) VersionNumber() string {\n\tif o == nil {\n\t\treturn \"\"\n\t}\n\tif o.MinorVersion != \"\" {\n\t\treturn fmt.Sprintf(\"%s.%s\", o.MajorVersion, o.MinorVersion)\n\t}\n\treturn o.MajorVersion\n}\n\nfunc (o *OperatingSystem) Version() string {\n\tif o == nil {\n\t\treturn \"\"\n\t}\n\n\tif o.LabelVersion != \"\" {\n\t\treturn o.LabelVersion\n\t}\n\n\tvar suffix string\n\tif o.Channel != \"\" {\n\t\tsuffix = fmt.Sprintf(\"+%s\", o.Channel)\n\t}\n\n\tif o.MajorVersion != \"\" {\n\t\tif o.MinorVersion != \"\" {\n\t\t\treturn fmt.Sprintf(\"%s.%s%s\", o.MajorVersion, o.MinorVersion, suffix)\n\t\t}\n\t\treturn o.MajorVersion + suffix\n\t}\n\n\treturn o.Codename\n}\n\nfunc (o OperatingSystem) String() string {\n\treturn fmt.Sprintf(\"%s@%s\", o.Name, o.Version())\n}\n\nfunc (o OperatingSystem) cacheKey() string {\n\treturn strings.ToLower(o.String())\n}\n\nfunc (o OperatingSystem) rowID() ID {\n\treturn o.ID\n}\n\nfunc (o *OperatingSystem) tableName() string {\n\treturn operatingSystemsTableCacheKey\n}\n\nfunc (o *OperatingSystem) setRowID(i ID) {\n\to.ID = i\n}\n\nfunc (o *OperatingSystem) clean() {\n\to.MajorVersion = trimZeroes(o.MajorVersion)\n\to.MinorVersion = trimZeroes(o.MinorVersion)\n}\n\nfunc (o *OperatingSystem) BeforeCreate(tx *gorm.DB) (err error) {\n\to.clean()\n\n\tif cacheInst, ok := cacheFromContext(tx.Statement.Context); ok {\n\t\tif existing, ok := cacheInst.getID(o); ok {\n\t\t\to.setRowID(existing)\n\t\t}\n\t\treturn nil\n\t}\n\n\treturn fmt.Errorf(\"OS creation is not supported\")\n}\n\nfunc (o *OperatingSystem) AfterCreate(tx *gorm.DB) (err error) {\n\tif cacheInst, ok := cacheFromContext(tx.Statement.Context); ok {\n\t\tcacheInst.set(o)\n\t}\n\treturn nil\n}\n\n// OperatingSystemSpecifierOverride is a table that allows for overriding fields on v6.OSSpecifier instances when searching for specific OperatingSystems.\ntype OperatingSystemSpecifierOverride struct {\n\t// Alias is an alternative name/ID for the operating system.\n\tAlias string `gorm:\"column:alias;primaryKey;index:os_alias_idx,collate:NOCASE\"`\n\n\t// Version is the matching version as found in the VERSION_ID field if the /etc/os-release file\n\tVersion string `gorm:\"column:version;primaryKey\"`\n\n\t// VersionPattern is a regex pattern to match against the VERSION_ID field if the /etc/os-release file\n\tVersionPattern string `gorm:\"column:version_pattern;primaryKey\"`\n\n\t// Codename is the matching codename as found in the VERSION_CODENAME field if the /etc/os-release file\n\tCodename string `gorm:\"column:codename;collate:NOCASE\"`\n\n\t// Channel is a string used to distinguish between fix and vulnerability data for the same OS release (e.g. RHEL mainline vs EUS).\n\tChannel string `gorm:\"column:channel;collate:NOCASE\"`\n\n\t// below are the fields that should be used as replacement for fields in the OperatingSystem table\n\n\tReplacementName         *string `gorm:\"column:replacement;primaryKey\"`\n\tReplacementMajorVersion *string `gorm:\"column:replacement_major_version;primaryKey\"`\n\tReplacementMinorVersion *string `gorm:\"column:replacement_minor_version;primaryKey\"`\n\tReplacementLabelVersion *string `gorm:\"column:replacement_label_version;primaryKey\"`\n\tReplacementChannel      *string `gorm:\"column:replacement_channel;primaryKey\"`\n\tRolling                 bool    `gorm:\"column:rolling;primaryKey\"`\n\n\t// ApplicableClientDBSchemas is a constraint on the database version that this override can be applied to (relative to the client library being used to access the DB).\n\tApplicableClientDBSchemas string `gorm:\"column:applicable_client_db_schemas\"`\n}\n\nfunc (os *OperatingSystemSpecifierOverride) BeforeCreate(_ *gorm.DB) (err error) {\n\tif os.Version != \"\" && os.VersionPattern != \"\" {\n\t\treturn fmt.Errorf(\"cannot have both version and version_pattern set\")\n\t}\n\n\treturn nil\n}\n\n// CPE related search tables //////////////////////////////////////////////////////\n\n// AffectedCPEHandle represents a single CPE affected by the specified vulnerability.\n// This is a shared struct used by both AffectedCPEHandle and UnaffectedCPEHandle. This is not a table itself.\ntype cpeHandle struct {\n\tID              ID                   `gorm:\"column:id;primaryKey\"`\n\tVulnerabilityID ID                   `gorm:\"column:vulnerability_id;not null\"`\n\tVulnerability   *VulnerabilityHandle `gorm:\"foreignKey:VulnerabilityID\"`\n\n\tCpeID ID   `gorm:\"column:cpe_id;index\"`\n\tCPE   *Cpe `gorm:\"foreignKey:CpeID\"`\n\n\tBlobID    ID           `gorm:\"column:blob_id\"`\n\tBlobValue *PackageBlob `gorm:\"-\"`\n}\n\nfunc (ch cpeHandle) vulnerability() string {\n\tif ch.Vulnerability != nil {\n\t\treturn ch.Vulnerability.Name\n\t}\n\tif ch.BlobValue != nil {\n\t\tif len(ch.BlobValue.CVEs) > 0 {\n\t\t\treturn ch.BlobValue.CVEs[0]\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc (ch cpeHandle) String() string {\n\tvar fields []string\n\n\tif ch.BlobValue != nil {\n\t\tv := ch.BlobValue.String()\n\t\tif v != \"\" {\n\t\t\tfields = append(fields, v)\n\t\t}\n\t}\n\n\tif ch.CPE != nil {\n\t\tfields = append(fields, fmt.Sprintf(\"cpe=%q\", ch.CPE.String()))\n\t} else {\n\t\tfields = append(fields, fmt.Sprintf(\"cpe=%d\", ch.CpeID))\n\t}\n\n\tif ch.Vulnerability != nil {\n\t\tfields = append(fields, fmt.Sprintf(\"vuln=%q\", ch.Vulnerability.String()))\n\t} else {\n\t\tfields = append(fields, fmt.Sprintf(\"vuln=%d\", ch.VulnerabilityID))\n\t}\n\n\treturn fmt.Sprintf(\"cpe(%s)\", strings.Join(fields, \", \"))\n}\n\nfunc (ch cpeHandle) getBlobID() ID {\n\treturn ch.BlobID\n}\n\nfunc (ch cpeHandle) getBlobValue() any {\n\tif ch.BlobValue == nil {\n\t\treturn nil // must return untyped nil or getBlobValue() == nil will always be false\n\t}\n\treturn ch.BlobValue\n}\n\nfunc (ch *cpeHandle) setBlobID(id ID) {\n\tch.BlobID = id\n}\n\nfunc (ch *cpeHandle) setBlob(rawBlobValue []byte) error {\n\tvar blobValue PackageBlob\n\tif err := json.Unmarshal(rawBlobValue, &blobValue); err != nil {\n\t\treturn fmt.Errorf(\"unable to unmarshal affected cpe blob value: %w\", err)\n\t}\n\n\tch.BlobValue = &blobValue\n\treturn nil\n}\n\n// AffectedCPEHandle represents a single CPE affected by the specified vulnerability.\n//\n// Note the CPEs in this table must NOT be resolvable to Packages (use AffectedPackageHandle for that). This table is\n// used when the CPE is known, but we do not have a clear understanding of the package ecosystem or authoritative\n// name, so we can still find vulnerabilities by these identifiers but not assert they are related to an entry in\n// the AffectedPackages table.\ntype AffectedCPEHandle cpeHandle\n\nfunc (ch *AffectedCPEHandle) getCPEHandle() *cpeHandle {\n\treturn (*cpeHandle)(ch)\n}\n\nfunc (ch AffectedCPEHandle) vulnerability() string {\n\treturn (cpeHandle)(ch).vulnerability()\n}\n\nfunc (ch AffectedCPEHandle) String() string {\n\treturn (cpeHandle)(ch).String()\n}\n\nfunc (ch AffectedCPEHandle) getBlobID() ID {\n\treturn (cpeHandle)(ch).getBlobID()\n}\n\nfunc (ch AffectedCPEHandle) getBlobValue() any {\n\treturn (cpeHandle)(ch).getBlobValue()\n}\n\nfunc (ch *AffectedCPEHandle) setBlobID(id ID) {\n\t(*cpeHandle)(ch).setBlobID(id)\n}\n\nfunc (ch *AffectedCPEHandle) setBlob(rawBlobValue []byte) error {\n\treturn (*cpeHandle)(ch).setBlob(rawBlobValue)\n}\n\ntype UnaffectedCPEHandle cpeHandle\n\nfunc (ch *UnaffectedCPEHandle) getCPEHandle() *cpeHandle {\n\treturn (*cpeHandle)(ch)\n}\n\nfunc (ch UnaffectedCPEHandle) vulnerability() string { // nolint:unused // when implementing filter functions in the future this will be needed\n\treturn (cpeHandle)(ch).vulnerability()\n}\n\nfunc (ch UnaffectedCPEHandle) String() string {\n\treturn (cpeHandle)(ch).String()\n}\n\nfunc (ch UnaffectedCPEHandle) getBlobID() ID {\n\treturn (cpeHandle)(ch).getBlobID()\n}\n\nfunc (ch UnaffectedCPEHandle) getBlobValue() any {\n\treturn (cpeHandle)(ch).getBlobValue()\n}\n\nfunc (ch *UnaffectedCPEHandle) setBlobID(id ID) {\n\t(*cpeHandle)(ch).setBlobID(id)\n}\n\nfunc (ch *UnaffectedCPEHandle) setBlob(rawBlobValue []byte) error {\n\treturn (*cpeHandle)(ch).setBlob(rawBlobValue)\n}\n\ntype Cpe struct {\n\t// TODO: what about different CPE versions?\n\tID ID `gorm:\"primaryKey\"`\n\n\tPart            string `gorm:\"column:part;not null;index:idx_cpe,unique,collate:NOCASE\"`\n\tVendor          string `gorm:\"column:vendor;index:idx_cpe,unique,collate:NOCASE;index:idx_cpe_vendor,collate:NOCASE\"`\n\tProduct         string `gorm:\"column:product;not null;index:idx_cpe,unique,collate:NOCASE;index:idx_cpe_product,collate:NOCASE\"`\n\tEdition         string `gorm:\"column:edition;index:idx_cpe,unique,collate:NOCASE\"`\n\tLanguage        string `gorm:\"column:language;index:idx_cpe,unique,collate:NOCASE\"`\n\tSoftwareEdition string `gorm:\"column:software_edition;index:idx_cpe,unique,collate:NOCASE\"`\n\tTargetHardware  string `gorm:\"column:target_hardware;index:idx_cpe,unique,collate:NOCASE\"`\n\tTargetSoftware  string `gorm:\"column:target_software;index:idx_cpe,unique,collate:NOCASE\"`\n\tOther           string `gorm:\"column:other;index:idx_cpe,unique,collate:NOCASE\"`\n\n\tPackages []Package `gorm:\"many2many:package_cpes;\"`\n}\n\nfunc (c Cpe) String() string {\n\tparts := []string{\"cpe:2.3\", c.Part, c.Vendor, c.Product, \"*\", \"*\", c.Edition, c.Language, c.SoftwareEdition, c.TargetSoftware, c.TargetHardware, c.Other}\n\tfor i, part := range parts {\n\t\tif part == \"\" {\n\t\t\tparts[i] = \"*\"\n\t\t}\n\t}\n\treturn strings.Join(parts, \":\")\n}\n\nfunc (c *Cpe) cacheKey() string {\n\treturn strings.ToLower(c.String())\n}\n\nfunc (c *Cpe) tableName() string {\n\treturn cpesTableCacheKey\n}\n\nfunc (c *Cpe) rowID() ID {\n\treturn c.ID\n}\n\nfunc (c *Cpe) setRowID(i ID) {\n\tc.ID = i\n}\n\nfunc (c *Cpe) BeforeCreate(tx *gorm.DB) (err error) {\n\tcacheInst, ok := cacheFromContext(tx.Statement.Context)\n\tif !ok {\n\t\treturn fmt.Errorf(\"CPE creation is not supported\")\n\t}\n\tif existingID, ok := cacheInst.getID(c); ok {\n\t\tvar existing Cpe\n\t\tresult := tx.Where(\"id = ?\", existingID).First(&existing)\n\t\tif result.Error == nil {\n\t\t\t// if the record already exists, then we should use the existing record\n\t\t\t*c = existing\n\t\t}\n\n\t\tc.setRowID(existingID)\n\t}\n\treturn nil\n}\n\nfunc (c *Cpe) AfterCreate(tx *gorm.DB) (err error) {\n\tif cacheInst, ok := cacheFromContext(tx.Statement.Context); ok {\n\t\tcacheInst.set(c)\n\t}\n\treturn nil\n}\n\n// PackageCpe join table for the many-to-many relationship\ntype PackageCpe struct {\n\tPackageID ID `gorm:\"primaryKey;column:package_id\"`\n\tCpeID     ID `gorm:\"primaryKey;column:cpe_id\"`\n}\n\nfunc (PackageCpe) TableName() string {\n\t// note: this value is referenced in multiple struct tags and must not be changed or removed\n\t// without this override the table name would be both model names in alphabetical order: cpes_packages\n\treturn \"package_cpes\"\n}\n\ntype KnownExploitedVulnerabilityHandle struct {\n\tID int64 `gorm:\"primaryKey\"`\n\n\tCve string `gorm:\"column:cve;not null;index:kev_cve_idx,collate:NOCASE\"`\n\n\tBlobID    ID                               `gorm:\"column:blob_id\"`\n\tBlobValue *KnownExploitedVulnerabilityBlob `gorm:\"-\"`\n}\n\nfunc (v KnownExploitedVulnerabilityHandle) getBlobValue() any {\n\tif v.BlobValue == nil {\n\t\treturn nil // must return untyped nil or getBlobValue() == nil will always be false\n\t}\n\treturn v.BlobValue\n}\n\nfunc (v *KnownExploitedVulnerabilityHandle) setBlobID(id ID) {\n\tv.BlobID = id\n}\n\nfunc (v KnownExploitedVulnerabilityHandle) getBlobID() ID {\n\treturn v.BlobID\n}\n\nfunc (v *KnownExploitedVulnerabilityHandle) setBlob(rawBlobValue []byte) error {\n\tvar blobValue KnownExploitedVulnerabilityBlob\n\tif err := json.Unmarshal(rawBlobValue, &blobValue); err != nil {\n\t\treturn fmt.Errorf(\"unable to unmarshal KEV blob value: %w\", err)\n\t}\n\n\tv.BlobValue = &blobValue\n\treturn nil\n}\n\ntype EpssMetadata struct {\n\tDate time.Time `gorm:\"column:date;not null\"`\n}\n\ntype EpssHandle struct {\n\tID int64 `gorm:\"primaryKey\"`\n\n\tCve        string    `gorm:\"column:cve;not null;index:epss_cve_idx,collate:NOCASE\"`\n\tEpss       float64   `gorm:\"column:epss;not null\"`\n\tPercentile float64   `gorm:\"column:percentile;not null\"`\n\tDate       time.Time `gorm:\"-\"` // note we do not store the date in this table since it is expected to be the same for all records, that is what EpssMetadata is for\n}\n\ntype CWEHandle struct {\n\tID     int64  `gorm:\"primaryKey\"`\n\tCVE    string `gorm:\"column:cve;not null;index:cwes_cve_idx,collate:NOCASE\"`\n\tCWE    string `gorm:\"column:cwe;not null;\"`\n\tSource string `gorm:\"column:source;\"`\n\tType   string `gorm:\"column:type;\"`\n}\n\nfunc (c CWEHandle) String() string {\n\treturn fmt.Sprintf(\"CWE(%s: %s, source=%s, type=%s)\", c.CVE, c.CWE, c.Source, c.Type)\n}\n\n// OperatingSystemEOLHandle carries end-of-life data for an operating system.\n// This is not a GORM model - it's used to update existing OperatingSystem records.\ntype OperatingSystemEOLHandle struct {\n\tName         string     // distro name (e.g., \"debian\", \"ubuntu\")\n\tMajorVersion string     // major version (e.g., \"12\")\n\tMinorVersion string     // minor version (e.g., \"04\" for ubuntu)\n\tCodename     string     // optional codename\n\tEOLDate      *time.Time // end-of-life date\n\tEOASDate     *time.Time // end-of-active-support date\n}\n\nfunc (o OperatingSystemEOLHandle) String() string {\n\teol := \"nil\"\n\tif o.EOLDate != nil {\n\t\teol = o.EOLDate.Format(\"2006-01-02\")\n\t}\n\treturn fmt.Sprintf(\"OSEol(%s %s.%s, eol=%s)\", o.Name, o.MajorVersion, o.MinorVersion, eol)\n}\n"
  },
  {
    "path": "grype/db/v6/models_test.go",
    "content": "package v6\n\nimport (\n\t\"testing\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestOperatingSystemAlias_VersionMutualExclusivity(t *testing.T) {\n\tdb := setupTestStore(t).db\n\n\tmsg := \"cannot have both version and version_pattern set\"\n\n\ttests := []struct {\n\t\tname   string\n\t\tinput  *OperatingSystemSpecifierOverride\n\t\terrMsg string\n\t}{\n\t\t{\n\t\t\tname: \"version and version_pattern are mutually exclusive\",\n\t\t\tinput: &OperatingSystemSpecifierOverride{\n\t\t\t\tAlias:          \"ubuntu\",\n\t\t\t\tVersion:        \"20.04\",\n\t\t\t\tVersionPattern: \"20.*\",\n\t\t\t},\n\t\t\terrMsg: msg,\n\t\t},\n\t\t{\n\t\t\tname: \"only version is set\",\n\t\t\tinput: &OperatingSystemSpecifierOverride{\n\t\t\t\tAlias:   \"ubuntu\",\n\t\t\t\tVersion: \"20.04\",\n\t\t\t},\n\t\t\terrMsg: \"\",\n\t\t},\n\t\t{\n\t\t\tname: \"only version_pattern is set\",\n\t\t\tinput: &OperatingSystemSpecifierOverride{\n\t\t\t\tAlias:          \"ubuntu\",\n\t\t\t\tVersionPattern: \"20.*\",\n\t\t\t},\n\t\t\terrMsg: \"\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := db.Create(tt.input).Error\n\t\t\tif tt.errMsg == \"\" {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t} else {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\tassert.Contains(t, err.Error(), tt.errMsg)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestOperatingSystem_VersionNumber(t *testing.T) {\n\ttests := []struct {\n\t\tname           string\n\t\tos             *OperatingSystem\n\t\texpectedResult string\n\t}{\n\t\t{\n\t\t\tname:           \"nil OS\",\n\t\t\tos:             nil,\n\t\t\texpectedResult: \"\",\n\t\t},\n\t\t{\n\t\t\tname:           \"major and minor versions\",\n\t\t\tos:             &OperatingSystem{MajorVersion: \"10\", MinorVersion: \"1\"},\n\t\t\texpectedResult: \"10.1\",\n\t\t},\n\t\t{\n\t\t\tname:           \"major version only\",\n\t\t\tos:             &OperatingSystem{MajorVersion: \"10\"},\n\t\t\texpectedResult: \"10\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tassert.Equal(t, tt.expectedResult, tt.os.VersionNumber())\n\t\t})\n\t}\n}\n\nfunc TestOperatingSystem_Version(t *testing.T) {\n\ttests := []struct {\n\t\tname           string\n\t\tos             *OperatingSystem\n\t\texpectedResult string\n\t}{\n\t\t{\n\t\t\tname:           \"nil OS\",\n\t\t\tos:             nil,\n\t\t\texpectedResult: \"\",\n\t\t},\n\t\t{\n\t\t\tname:           \"label version\",\n\t\t\tos:             &OperatingSystem{LabelVersion: \"unstable\"},\n\t\t\texpectedResult: \"unstable\",\n\t\t},\n\t\t{\n\t\t\tname:           \"major and minor versions\",\n\t\t\tos:             &OperatingSystem{MajorVersion: \"10\", MinorVersion: \"1\"},\n\t\t\texpectedResult: \"10.1\",\n\t\t},\n\t\t{\n\t\t\tname:           \"major version only\",\n\t\t\tos:             &OperatingSystem{MajorVersion: \"10\"},\n\t\t\texpectedResult: \"10\",\n\t\t},\n\t\t{\n\t\t\tname:           \"codename\",\n\t\t\tos:             &OperatingSystem{Codename: \"buster\"},\n\t\t\texpectedResult: \"buster\",\n\t\t},\n\t\t{\n\t\t\tname:           \"with channel\",\n\t\t\tos:             &OperatingSystem{MajorVersion: \"10\", MinorVersion: \"1\", Channel: \"stable\"},\n\t\t\texpectedResult: \"10.1+stable\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tassert.Equal(t, tt.expectedResult, tt.os.Version())\n\t\t})\n\t}\n}\n\nfunc TestOperatingSystem_clean(t *testing.T) {\n\n\ttests := []struct {\n\t\tname  string\n\t\tinput OperatingSystem\n\t\twant  OperatingSystem\n\t}{\n\t\t{\n\t\t\tname: \"trim 0s\",\n\t\t\tinput: OperatingSystem{\n\t\t\t\tName:         \"Ubuntu\",\n\t\t\t\tMajorVersion: \"20\",\n\t\t\t\tMinorVersion: \"04\",\n\t\t\t},\n\t\t\twant: OperatingSystem{\n\t\t\t\tName:         \"Ubuntu\",\n\t\t\t\tMajorVersion: \"20\",\n\t\t\t\tMinorVersion: \"4\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"preserve 0 value\",\n\t\t\tinput: OperatingSystem{\n\t\t\t\tName:         \"Redhat\",\n\t\t\t\tMajorVersion: \"9\",\n\t\t\t\tMinorVersion: \"0\",\n\t\t\t},\n\t\t\twant: OperatingSystem{\n\t\t\t\tName:         \"Redhat\",\n\t\t\t\tMajorVersion: \"9\",\n\t\t\t\tMinorVersion: \"0\", // important! ...9 != 9.0 since 9 includes multiple minor versions\n\t\t\t},\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\to := tt.input\n\t\t\to.clean()\n\t\t\tif d := cmp.Diff(tt.want, o); d != \"\" {\n\t\t\t\tt.Errorf(\"OperatingSystem.clean() mismatch (-want +got):\\n%s\", d)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/db/v6/name/java.go",
    "content": "package name\n\nimport (\n\t\"fmt\"\n\n\tgrypePkg \"github.com/anchore/grype/grype/pkg\"\n\t\"github.com/anchore/grype/internal/log\"\n\t\"github.com/anchore/grype/internal/stringutil\"\n\t\"github.com/anchore/packageurl-go\"\n)\n\ntype JavaResolver struct {\n}\n\nfunc (r *JavaResolver) Normalize(name string) string {\n\treturn name\n}\n\nfunc (r *JavaResolver) Names(p grypePkg.Package) []string {\n\tnames := stringutil.NewStringSet()\n\n\t// The current default for the Java ecosystem is to use a Maven-like identifier of the form\n\t// \"<group-name>:<artifact-name>\"\n\tif metadata, ok := p.Metadata.(grypePkg.JavaMetadata); ok {\n\t\tif metadata.PomGroupID != \"\" {\n\t\t\tif metadata.PomArtifactID != \"\" {\n\t\t\t\tnames.Add(r.Normalize(fmt.Sprintf(\"%s:%s\", metadata.PomGroupID, metadata.PomArtifactID)))\n\t\t\t}\n\t\t\tif metadata.ManifestName != \"\" {\n\t\t\t\tnames.Add(r.Normalize(fmt.Sprintf(\"%s:%s\", metadata.PomGroupID, metadata.ManifestName)))\n\t\t\t}\n\t\t}\n\t}\n\n\tif p.PURL != \"\" {\n\t\tpurl, err := packageurl.FromString(p.PURL)\n\t\tif err != nil {\n\t\t\tlog.Warnf(\"unable to resolve java package identifier from purl=%q: %+v\", p.PURL, err)\n\t\t} else {\n\t\t\tnames.Add(r.Normalize(fmt.Sprintf(\"%s:%s\", purl.Namespace, purl.Name)))\n\t\t}\n\t}\n\n\treturn names.ToSlice()\n}\n"
  },
  {
    "path": "grype/db/v6/name/java_test.go",
    "content": "package name\n\nimport (\n\t\"testing\"\n\n\t\"github.com/google/uuid\"\n\t\"github.com/stretchr/testify/assert\"\n\n\tgrypePkg \"github.com/anchore/grype/grype/pkg\"\n)\n\nfunc TestJavaResolver_Normalize(t *testing.T) {\n\ttests := []struct {\n\t\tname       string\n\t\tnormalized string\n\t}{\n\t\t{\n\t\t\tname: \"PyYAML\",\n\t\t\t// note we are not lowercasing since the DB is case-insensitive for name columns\n\t\t\tnormalized: \"PyYAML\",\n\t\t},\n\t\t{\n\t\t\tname:       \"oslo.concurrency\",\n\t\t\tnormalized: \"oslo.concurrency\",\n\t\t},\n\t\t{\n\t\t\tname:       \"\",\n\t\t\tnormalized: \"\",\n\t\t},\n\t\t{\n\t\t\tname:       \"test---1\",\n\t\t\tnormalized: \"test---1\",\n\t\t},\n\t\t{\n\t\t\tname:       \"AbCd.-__.--.-___.__.--1234____----....XyZZZ\",\n\t\t\tnormalized: \"AbCd.-__.--.-___.__.--1234____----....XyZZZ\",\n\t\t},\n\t}\n\n\tresolver := JavaResolver{}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tresolvedNames := resolver.Normalize(test.name)\n\t\t\tassert.Equal(t, resolvedNames, test.normalized)\n\t\t})\n\t}\n}\n\nfunc TestJavaResolver_Names(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tpkg      grypePkg.Package\n\t\tresolved []string\n\t}{\n\t\t{\n\t\t\tname: \"both artifact and manifest 1\",\n\t\t\tpkg: grypePkg.Package{\n\t\t\t\tName:     \"ABCD\",\n\t\t\t\tVersion:  \"1.2.3.4\",\n\t\t\t\tLanguage: \"java\",\n\t\t\t\tMetadata: grypePkg.JavaMetadata{\n\t\t\t\t\tVirtualPath:   \"virtual-path-info\",\n\t\t\t\t\tPomArtifactID: \"pom-ARTIFACT-ID-info\",\n\t\t\t\t\tPomGroupID:    \"pom-group-ID-info\",\n\t\t\t\t\tManifestName:  \"main-section-name-info\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tresolved: []string{\"pom-group-ID-info:pom-ARTIFACT-ID-info\", \"pom-group-ID-info:main-section-name-info\"},\n\t\t},\n\t\t{\n\t\t\tname: \"both artifact and manifest 2\",\n\t\t\tpkg: grypePkg.Package{\n\t\t\t\tID:   grypePkg.ID(uuid.NewString()),\n\t\t\t\tName: \"a-name\",\n\t\t\t\tMetadata: grypePkg.JavaMetadata{\n\t\t\t\t\tVirtualPath:   \"v-path\",\n\t\t\t\t\tPomArtifactID: \"art-id\",\n\t\t\t\t\tPomGroupID:    \"g-id\",\n\t\t\t\t\tManifestName:  \"man-name\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tresolved: []string{\n\t\t\t\t\"g-id:art-id\",\n\t\t\t\t\"g-id:man-name\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"no group id\",\n\t\t\tpkg: grypePkg.Package{\n\t\t\t\tID:   grypePkg.ID(uuid.NewString()),\n\t\t\t\tName: \"a-name\",\n\t\t\t\tMetadata: grypePkg.JavaMetadata{\n\t\t\t\t\tVirtualPath:   \"v-path\",\n\t\t\t\t\tPomArtifactID: \"art-id\",\n\t\t\t\t\tManifestName:  \"man-name\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tresolved: []string{},\n\t\t},\n\t\t{\n\t\t\tname: \"only manifest\",\n\t\t\tpkg: grypePkg.Package{\n\t\t\t\tID:   grypePkg.ID(uuid.NewString()),\n\t\t\t\tName: \"a-name\",\n\t\t\t\tMetadata: grypePkg.JavaMetadata{\n\t\t\t\t\tVirtualPath:  \"v-path\",\n\t\t\t\t\tPomGroupID:   \"g-id\",\n\t\t\t\t\tManifestName: \"man-name\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tresolved: []string{\n\t\t\t\t\"g-id:man-name\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"only artifact\",\n\t\t\tpkg: grypePkg.Package{\n\t\t\t\tID:   grypePkg.ID(uuid.NewString()),\n\t\t\t\tName: \"a-name\",\n\t\t\t\tMetadata: grypePkg.JavaMetadata{\n\t\t\t\t\tVirtualPath:   \"v-path\",\n\t\t\t\t\tPomArtifactID: \"art-id\",\n\t\t\t\t\tPomGroupID:    \"g-id\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tresolved: []string{\n\t\t\t\t\"g-id:art-id\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"no artifact or manifest\",\n\t\t\tpkg: grypePkg.Package{\n\t\t\t\tID:   grypePkg.ID(uuid.NewString()),\n\t\t\t\tName: \"a-name\",\n\t\t\t\tMetadata: grypePkg.JavaMetadata{\n\t\t\t\t\tVirtualPath: \"v-path\",\n\t\t\t\t\tPomGroupID:  \"g-id\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tresolved: []string{},\n\t\t},\n\t\t{\n\t\t\tname: \"with valid purl\",\n\t\t\tpkg: grypePkg.Package{\n\t\t\t\tID:   grypePkg.ID(uuid.NewString()),\n\t\t\t\tName: \"a-name\",\n\t\t\t\tPURL: \"pkg:maven/org.anchore/b-name@0.2\",\n\t\t\t},\n\t\t\tresolved: []string{\"org.anchore:b-name\"},\n\t\t},\n\t\t{\n\t\t\tname: \"ignore invalid pURLs\",\n\t\t\tpkg: grypePkg.Package{\n\t\t\t\tID:   grypePkg.ID(uuid.NewString()),\n\t\t\t\tName: \"a-name\",\n\t\t\t\tPURL: \"pkg:BAD/\",\n\t\t\t\tMetadata: grypePkg.JavaMetadata{\n\t\t\t\t\tVirtualPath:   \"v-path\",\n\t\t\t\t\tPomArtifactID: \"art-id\",\n\t\t\t\t\tPomGroupID:    \"g-id\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tresolved: []string{\n\t\t\t\t\"g-id:art-id\",\n\t\t\t},\n\t\t},\n\t}\n\n\tresolver := JavaResolver{}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tresolvedNames := resolver.Names(test.pkg)\n\t\t\tassert.ElementsMatch(t, resolvedNames, test.resolved)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/db/v6/name/python.go",
    "content": "package name\n\nimport (\n\t\"regexp\"\n\n\tgrypePkg \"github.com/anchore/grype/grype/pkg\"\n)\n\ntype PythonResolver struct {\n}\n\nfunc (r *PythonResolver) Normalize(name string) string {\n\t// Canonical naming of packages within python is defined by PEP 503 at\n\t// https://peps.python.org/pep-0503/#normalized-names, and this code is derived from\n\t// the official python implementation of canonical naming at\n\t// https://packaging.pypa.io/en/latest/_modules/packaging/utils.html#canonicalize_name\n\n\treturn regexp.MustCompile(`[-_.]+`).ReplaceAllString(name, \"-\")\n}\n\nfunc (r *PythonResolver) Names(p grypePkg.Package) []string {\n\t// Canonical naming of packages within python is defined by PEP 503 at\n\t// https://peps.python.org/pep-0503/#normalized-names, and this code is derived from\n\t// the official python implementation of canonical naming at\n\t// https://packaging.pypa.io/en/latest/_modules/packaging/utils.html#canonicalize_name\n\n\treturn []string{r.Normalize(p.Name)}\n}\n"
  },
  {
    "path": "grype/db/v6/name/python_test.go",
    "content": "package name\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestPythonResolver_Normalize(t *testing.T) {\n\ttests := []struct {\n\t\tname       string\n\t\tnormalized string\n\t}{\n\t\t{\n\t\t\tname: \"PyYAML\",\n\t\t\t// note we are not lowercasing since the DB is case-insensitive for name columns\n\t\t\tnormalized: \"PyYAML\",\n\t\t},\n\t\t{\n\t\t\tname:       \"oslo.concurrency\",\n\t\t\tnormalized: \"oslo-concurrency\",\n\t\t},\n\t\t{\n\t\t\tname:       \"\",\n\t\t\tnormalized: \"\",\n\t\t},\n\t\t{\n\t\t\tname:       \"test---1\",\n\t\t\tnormalized: \"test-1\",\n\t\t},\n\t\t{\n\t\t\tname:       \"AbCd.-__.--.-___.__.--1234____----....XyZZZ\",\n\t\t\tnormalized: \"AbCd-1234-XyZZZ\",\n\t\t},\n\t}\n\n\tresolver := PythonResolver{}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tresolvedNames := resolver.Normalize(test.name)\n\t\t\tassert.Equal(t, resolvedNames, test.normalized)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/db/v6/name/resolver.go",
    "content": "package name\n\nimport (\n\tgrypePkg \"github.com/anchore/grype/grype/pkg\"\n\tsyftPkg \"github.com/anchore/syft/syft/pkg\"\n)\n\ntype Resolver interface {\n\tNormalize(string) string\n\tNames(p grypePkg.Package) []string\n}\n\nfunc FromType(t syftPkg.Type) Resolver {\n\tswitch t {\n\tcase syftPkg.PythonPkg:\n\t\treturn &PythonResolver{}\n\tcase syftPkg.JavaPkg, syftPkg.JenkinsPluginPkg:\n\t\treturn &JavaResolver{}\n\t}\n\n\treturn nil\n}\n\nfunc PackageNames(p grypePkg.Package) []string {\n\tnames := []string{p.Name}\n\tr := FromType(p.Type)\n\tif r == nil {\n\t\treturn names\n\t}\n\n\tparts := r.Names(p)\n\tif len(parts) > 0 {\n\t\tnames = parts\n\t}\n\treturn names\n}\n\nfunc Normalize(name string, pkgType syftPkg.Type) string {\n\tr := FromType(pkgType)\n\tif r != nil {\n\t\treturn r.Normalize(name)\n\t}\n\treturn name\n}\n"
  },
  {
    "path": "grype/db/v6/operating_system_store.go",
    "content": "package v6\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"regexp\"\n\t\"strings\"\n\t\"time\"\n\n\t\"gorm.io/gorm\"\n\n\t\"github.com/anchore/grype/grype/version\"\n\t\"github.com/anchore/grype/internal/log\"\n)\n\ntype OSSpecifiers []*OSSpecifier\n\n// OSSpecifier is a struct that represents a distro in a way that can be used to query the affected package store.\ntype OSSpecifier struct {\n\t// Name of the distro as identified by the ID field in /etc/os-release (or similar normalized name, e.g. \"oracle\" instead of \"ol\")\n\tName string\n\n\t// MajorVersion is the first field in the VERSION_ID field in /etc/os-release (e.g. 7 in \"7.0.1406\")\n\tMajorVersion string\n\n\t// MinorVersion is the second field in the VERSION_ID field in /etc/os-release (e.g. 0 in \"7.0.1406\")\n\tMinorVersion string\n\n\t// RemainingVersion is anything after the minor version in the VERSION_ID field in /etc/os-release (e.g. 1406 in \"7.0.1406\")\n\tRemainingVersion string\n\n\t// LabelVersion is a string that represents a floating version (e.g. \"edge\" or \"unstable\") or is the CODENAME field in /etc/os-release (e.g. \"wheezy\" for debian 7)\n\tLabelVersion string\n\n\t// Channel is a string that represents a different feed for fix and vulnerability data (e.g. \"eus\" for RHEL)\n\tChannel string\n\n\t// DisableAliasing prevents OS aliasing when true (used for exact distro matching)\n\tDisableAliasing bool\n\n\t// DisableFallback prevents fallback to less specific version matching when true.\n\t// When set, only exact version matches are returned (no major-only fallback).\n\t// Used for EOL lookups where we don't want e.g. Alpine 3.24 to match Alpine 3.12.\n\tDisableFallback bool\n}\n\nfunc (d *OSSpecifier) clean() {\n\td.MajorVersion = trimZeroes(d.MajorVersion)\n\td.MinorVersion = trimZeroes(d.MinorVersion)\n}\n\nfunc (d *OSSpecifier) String() string {\n\tif d == nil {\n\t\treturn anyOS\n\t}\n\n\tif *d == *NoOSSpecified {\n\t\treturn \"none\"\n\t}\n\n\tvar ver string\n\tif d.MajorVersion != \"\" {\n\t\tver = d.version()\n\t} else {\n\t\tver = d.LabelVersion\n\t}\n\n\tdistroDisplayName := d.Name\n\tif ver != \"\" {\n\t\tdistroDisplayName += \"@\" + ver\n\t}\n\tif ver == d.MajorVersion && d.LabelVersion != \"\" {\n\t\tdistroDisplayName += \" (\" + d.LabelVersion + \")\"\n\t}\n\n\treturn distroDisplayName\n}\n\nfunc (d OSSpecifier) version() string {\n\tif d.MajorVersion != \"\" {\n\t\tif d.MinorVersion != \"\" {\n\t\t\tif d.RemainingVersion != \"\" {\n\t\t\t\treturn d.MajorVersion + \".\" + d.MinorVersion + \".\" + d.RemainingVersion\n\t\t\t}\n\t\t\treturn d.MajorVersion + \".\" + d.MinorVersion\n\t\t}\n\t\treturn d.MajorVersion\n\t}\n\n\treturn d.LabelVersion\n}\n\nfunc (d OSSpecifiers) String() string {\n\tif d.IsAny() {\n\t\treturn anyOS\n\t}\n\tvar parts []string\n\tfor _, v := range d {\n\t\tparts = append(parts, v.String())\n\t}\n\treturn strings.Join(parts, \", \")\n}\n\nfunc (d OSSpecifiers) IsAny() bool {\n\tif len(d) == 0 {\n\t\treturn true\n\t}\n\tif len(d) == 1 && d[0] == AnyOSSpecified {\n\t\treturn true\n\t}\n\treturn false\n}\n\nfunc (d OSSpecifier) matchesVersionPattern(pattern string) bool {\n\t// check if version or version label matches the given regex\n\tr, err := regexp.Compile(pattern)\n\tif err != nil {\n\t\tlog.Tracef(\"failed to compile distro specifier regex pattern %q: %v\", pattern, err)\n\t\treturn false\n\t}\n\n\tif r.MatchString(d.version()) {\n\t\treturn true\n\t}\n\n\tif d.LabelVersion != \"\" {\n\t\treturn r.MatchString(d.LabelVersion)\n\t}\n\treturn false\n}\n\ntype OperatingSystemStoreReader interface {\n\tGetOperatingSystems(OSSpecifier) ([]OperatingSystem, error)\n}\n\ntype OperatingSystemStoreWriter interface {\n\t// UpdateOperatingSystemEOL updates the EOL and EOAS dates for an operating system\n\t// matching the given specifier. Returns the number of records updated.\n\tUpdateOperatingSystemEOL(spec OSSpecifier, eolDate, eoasDate *time.Time) (int64, error)\n}\n\ntype operatingSystemStore struct {\n\tdb            *gorm.DB\n\tblobStore     *blobStore\n\tclientVersion *version.Version\n}\n\nfunc newOperatingSystemStore(db *gorm.DB, bs *blobStore) *operatingSystemStore {\n\treturn &operatingSystemStore{\n\t\tdb:            db,\n\t\tblobStore:     bs,\n\t\tclientVersion: version.New(fmt.Sprintf(\"%d.%d.%d\", ModelVersion, Revision, Addition), version.SemanticFormat),\n\t}\n}\n\nfunc (s *operatingSystemStore) addOsFromPackages(packages ...*packageHandle) error { // nolint:dupl\n\tcacheInst, ok := cacheFromContext(s.db.Statement.Context)\n\tif !ok {\n\t\treturn fmt.Errorf(\"unable to fetch OS cache from context\")\n\t}\n\n\tvar final []*OperatingSystem\n\tbyCacheKey := make(map[string][]*OperatingSystem)\n\tfor _, p := range packages {\n\t\tif p.OperatingSystem != nil {\n\t\t\tp.OperatingSystem.clean()\n\t\t\tkey := p.OperatingSystem.cacheKey()\n\t\t\tif existingID, ok := cacheInst.getID(p.OperatingSystem); ok {\n\t\t\t\t// seen in a previous transaction...\n\t\t\t\tp.OperatingSystemID = &existingID\n\t\t\t} else if _, ok := byCacheKey[key]; !ok {\n\t\t\t\t// not seen within this transaction\n\t\t\t\tfinal = append(final, p.OperatingSystem)\n\t\t\t}\n\t\t\tbyCacheKey[key] = append(byCacheKey[key], p.OperatingSystem)\n\t\t}\n\t}\n\n\tif len(final) == 0 {\n\t\treturn nil\n\t}\n\n\tif err := s.db.Create(final).Error; err != nil {\n\t\treturn fmt.Errorf(\"unable to create OS records: %w\", err)\n\t}\n\n\t// update the cache with the new records\n\tfor _, ref := range final {\n\t\tcacheInst.set(ref)\n\t}\n\n\t// update all references with the IDs from the cache\n\tfor _, refs := range byCacheKey {\n\t\tfor _, ref := range refs {\n\t\t\tid, ok := cacheInst.getID(ref)\n\t\t\tif ok {\n\t\t\t\tref.setRowID(id)\n\t\t\t}\n\t\t}\n\t}\n\n\t// update the parent objects with the FK ID\n\tfor _, p := range packages {\n\t\tif p.OperatingSystem != nil {\n\t\t\tp.OperatingSystemID = &p.OperatingSystem.ID\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (s *operatingSystemStore) GetOperatingSystems(d OSSpecifier) ([]OperatingSystem, error) {\n\tif d.Name == \"\" && d.LabelVersion == \"\" {\n\t\treturn nil, ErrMissingOSIdentification\n\t}\n\n\t// search for aliases for the given distro; we intentionally map some OSs to other OSs in terms of\n\t// vulnerability (e.g. `centos` is an alias for `rhel`). If an alias is found always use that alias in\n\t// searches (there will never be anything in the DB for aliased distros).\n\t// Skip aliasing if explicitly disabled (used for exact distro matching).\n\tif !d.DisableAliasing {\n\t\tif err := s.applyOSAlias(&d); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\td.clean()\n\n\t// handle non-version fields\n\tquery := s.prepareQuery(d)\n\n\t// handle version-like fields\n\treturn s.searchForOSExactVersions(query, d)\n}\n\nfunc (s *operatingSystemStore) applyOSAlias(d *OSSpecifier) error {\n\tif d.Name == \"\" {\n\t\treturn nil\n\t}\n\n\tvar aliases []OperatingSystemSpecifierOverride\n\terr := s.db.Where(\"alias = ? collate nocase\", d.Name).Find(&aliases).Error\n\tif err != nil {\n\t\tif !errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t\treturn fmt.Errorf(\"failed to resolve alias for distro %q: %w\", d.Name, err)\n\t\t}\n\t\treturn nil\n\t}\n\n\treturn applyOSSpecifierOverrides(d, aliases, s.clientVersion)\n}\n\nfunc applyOSSpecifierOverrides(d *OSSpecifier, overrides []OperatingSystemSpecifierOverride, clientVersion *version.Version) error {\n\tfor _, o := range overrides {\n\t\tcanUse, err := canUseOverride(o, clientVersion)\n\t\tif err != nil {\n\t\t\tlog.Tracef(\"failed to check if override %q is applicable for client version %s: %v\", o.Alias, clientVersion, err)\n\t\t\t// we cannot check if we can use this override, so we assume it is applicable\n\t\t} else if !canUse {\n\t\t\t// this override is not applicable for the current client version\n\t\t\tcontinue\n\t\t}\n\n\t\tif o.Codename != \"\" && o.Codename != d.LabelVersion {\n\t\t\tcontinue\n\t\t}\n\n\t\tif o.Version != \"\" && o.Version != d.version() {\n\t\t\tcontinue\n\t\t}\n\n\t\tif o.VersionPattern != \"\" && !d.matchesVersionPattern(o.VersionPattern) {\n\t\t\tcontinue\n\t\t}\n\n\t\t// first match wins, we do not apply any further overrides\n\t\tapplyOverride(d, o)\n\t\tbreak\n\t}\n\n\treturn nil\n}\n\nfunc applyOverride(d *OSSpecifier, override OperatingSystemSpecifierOverride) bool {\n\tvar applied bool\n\tif override.ReplacementName != nil {\n\t\td.Name = *override.ReplacementName\n\t\tapplied = true\n\t}\n\n\tif override.Rolling {\n\t\td.MajorVersion = \"\"\n\t\td.MinorVersion = \"\"\n\t\tapplied = true\n\t}\n\n\tif override.ReplacementMajorVersion != nil {\n\t\td.MajorVersion = *override.ReplacementMajorVersion\n\t\tapplied = true\n\t}\n\n\tif override.ReplacementMinorVersion != nil {\n\t\td.MinorVersion = *override.ReplacementMinorVersion\n\t\tapplied = true\n\t}\n\n\tif override.ReplacementLabelVersion != nil {\n\t\td.LabelVersion = *override.ReplacementLabelVersion\n\t\tapplied = true\n\t}\n\n\tif override.ReplacementChannel != nil {\n\t\td.Channel = *override.ReplacementChannel\n\t\tapplied = true\n\t}\n\treturn applied\n}\n\nfunc canUseOverride(override OperatingSystemSpecifierOverride, clientVersion *version.Version) (bool, error) {\n\tif override.ApplicableClientDBSchemas == \"\" || clientVersion == nil {\n\t\treturn true, nil\n\t}\n\tc, err := version.GetConstraint(override.ApplicableClientDBSchemas, version.SemanticFormat)\n\tif err != nil {\n\t\treturn true, fmt.Errorf(\"unable to parse version constraint: %w\", err)\n\t}\n\tok, err := c.Satisfied(clientVersion)\n\tif err != nil {\n\t\treturn true, fmt.Errorf(\"unable to check if client constraint: %w\", err)\n\t}\n\tif !ok {\n\t\t// explicitly told that this override does not apply to this client version\n\t\treturn false, nil\n\t}\n\n\treturn true, nil\n}\n\nfunc (s *operatingSystemStore) prepareQuery(d OSSpecifier) *gorm.DB {\n\tquery := s.db.Model(&OperatingSystem{})\n\n\tif d.Name != \"\" {\n\t\tquery = query.Where(\"name = ? collate nocase OR release_id = ? collate nocase\", d.Name, d.Name)\n\t}\n\n\tif d.LabelVersion != \"\" {\n\t\tquery = query.Where(\"codename = ? collate nocase OR label_version = ? collate nocase\", d.LabelVersion, d.LabelVersion)\n\t}\n\n\tif d.Channel != \"\" {\n\t\tquery = query.Where(\"channel = ? collate nocase\", d.Channel)\n\t} else {\n\t\t// we specifically want to match vanilla...\n\t\tquery = query.Where(\"channel IS NULL OR channel = ''\")\n\t}\n\treturn query\n}\n\nfunc (s *operatingSystemStore) searchForOSExactVersions(query *gorm.DB, d OSSpecifier) ([]OperatingSystem, error) {\n\tvar allOs []OperatingSystem\n\n\thandleQuery := func(q *gorm.DB, desc string) ([]OperatingSystem, error) {\n\t\terr := q.Find(&allOs).Error\n\t\tif err == nil {\n\t\t\treturn allOs, nil\n\t\t}\n\t\tif !errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t\treturn nil, fmt.Errorf(\"failed to query distro by %s: %w\", desc, err)\n\t\t}\n\t\treturn nil, nil\n\t}\n\n\tif d.MajorVersion == \"\" && d.MinorVersion == \"\" {\n\t\treturn handleQuery(query, \"name and codename only\")\n\t}\n\n\t// search by the most specific criteria first, then fallback\n\tvar result []OperatingSystem\n\tvar err error\n\tif d.MajorVersion != \"\" {\n\t\tif d.MinorVersion != \"\" {\n\t\t\t// non-empty major and minor versions\n\t\t\tspecificQuery := query.Session(&gorm.Session{}).Where(\"major_version = ? AND minor_version = ?\", d.MajorVersion, d.MinorVersion)\n\t\t\tresult, err = handleQuery(specificQuery, \"major and minor versions\")\n\t\t\tif err != nil || len(result) > 0 {\n\t\t\t\treturn result, err\n\t\t\t}\n\t\t} else {\n\t\t\t// empty minor version - exact match for major-only distros (e.g., Debian 8, 9, 10...)\n\t\t\tmajorExclusiveQuery := query.Session(&gorm.Session{}).Where(\"major_version = ? AND minor_version = ?\", d.MajorVersion, \"\")\n\t\t\tresult, err = handleQuery(majorExclusiveQuery, \"major version with empty minor\")\n\t\t\tif err != nil || len(result) > 0 {\n\t\t\t\treturn result, err\n\t\t\t}\n\t\t}\n\n\t\t// when fallback is disabled, don't try less specific version matches\n\t\tif d.DisableFallback {\n\t\t\treturn nil, nil\n\t\t}\n\n\t\t// fallback to major version only, requiring the minor version to be blank. Note: it is important that we don't\n\t\t// match on any record with the given major version, we must only match on records that are intentionally empty\n\t\t// minor version. For instance, the DB may have rhel 8.1, 8.2, 8.3, 8.4, etc. We don't want to arbitrarily match\n\t\t// on one of these or match even the latest version, as even that may yield incorrect vulnerability matching\n\t\t// results. We are only intending to allow matches for when the vulnerability data is only specified at the major version level.\n\t\tmajorExclusiveQuery := query.Session(&gorm.Session{}).Where(\"major_version = ? AND minor_version = ?\", d.MajorVersion, \"\")\n\t\tresult, err = handleQuery(majorExclusiveQuery, \"exclusively major version\")\n\t\tif err != nil || len(result) > 0 {\n\t\t\treturn result, err\n\t\t}\n\n\t\t// fallback to major version for any minor version\n\t\tmajorQuery := query.Session(&gorm.Session{}).Where(\"major_version = ?\", d.MajorVersion)\n\t\tresult, err = handleQuery(majorQuery, \"major version with any minor version\")\n\t\tif err != nil || len(result) > 0 {\n\t\t\treturn result, err\n\t\t}\n\t}\n\n\treturn allOs, nil\n}\n\n// UpdateOperatingSystemEOL updates the EOL and EOAS dates for operating systems\n// matching the given specifier. Returns the number of records updated.\nfunc (s *operatingSystemStore) UpdateOperatingSystemEOL(spec OSSpecifier, eolDate, eoasDate *time.Time) (int64, error) {\n\tspec.clean()\n\n\t// Build the query to find matching OS records\n\tquery := s.db.Model(&OperatingSystem{})\n\n\tif spec.Name != \"\" {\n\t\tquery = query.Where(\"name = ? collate nocase\", spec.Name)\n\t}\n\n\tif spec.MajorVersion != \"\" {\n\t\tquery = query.Where(\"major_version = ?\", spec.MajorVersion)\n\t}\n\n\tif spec.MinorVersion != \"\" {\n\t\tquery = query.Where(\"minor_version = ?\", spec.MinorVersion)\n\t}\n\n\tif spec.LabelVersion != \"\" {\n\t\tquery = query.Where(\"codename = ? collate nocase OR label_version = ? collate nocase\", spec.LabelVersion, spec.LabelVersion)\n\t}\n\n\t// Update the EOL fields\n\tupdates := map[string]interface{}{\n\t\t\"eol_date\":  eolDate,\n\t\t\"eoas_date\": eoasDate,\n\t}\n\n\tresult := query.Updates(updates)\n\tif result.Error != nil {\n\t\treturn 0, fmt.Errorf(\"failed to update OS EOL data: %w\", result.Error)\n\t}\n\n\treturn result.RowsAffected, nil\n}\n\nfunc trimZeroes(s string) string {\n\t// trim leading zeros from the version components\n\tif s == \"\" {\n\t\treturn s\n\t}\n\tif s[0] == '0' {\n\t\ts = strings.TrimLeft(s, \"0\")\n\t}\n\tif s == \"\" {\n\t\t// we've not only trimmed leading zeros, but also the entire string\n\t\t// we should preserve the zero value for the version\n\t\treturn \"0\"\n\t}\n\treturn s\n}\n"
  },
  {
    "path": "grype/db/v6/operating_system_store_test.go",
    "content": "package v6\n\nimport (\n\t\"testing\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/google/go-cmp/cmp/cmpopts\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/grype/grype/version\"\n)\n\nfunc TestOperatingSystemStore_ResolveOperatingSystem(t *testing.T) {\n\t// we always preload the OS aliases into the DB when staging for writing\n\tdb := setupTestStore(t).db\n\tbs := newBlobStore(db)\n\toss := newOperatingSystemStore(db, bs)\n\n\tubuntu2004 := &OperatingSystem{Name: \"ubuntu\", ReleaseID: \"ubuntu\", MajorVersion: \"20\", MinorVersion: \"04\", LabelVersion: \"focal\"}\n\tubuntu2010 := &OperatingSystem{Name: \"ubuntu\", MajorVersion: \"20\", MinorVersion: \"10\", LabelVersion: \"groovy\"}\n\trhel8 := &OperatingSystem{Name: \"rhel\", ReleaseID: \"rhel\", MajorVersion: \"8\"}\n\trhel81 := &OperatingSystem{Name: \"rhel\", ReleaseID: \"rhel\", MajorVersion: \"8\", MinorVersion: \"1\"}\n\tdebian10 := &OperatingSystem{Name: \"debian\", ReleaseID: \"debian\", MajorVersion: \"10\"}\n\tdebian13 := &OperatingSystem{Name: \"debian\", ReleaseID: \"debian\", MajorVersion: \"13\", Codename: \"trixie\"}\n\techo := &OperatingSystem{Name: \"echo\", ReleaseID: \"echo\", MajorVersion: \"1\"}\n\talpine318 := &OperatingSystem{Name: \"alpine\", ReleaseID: \"alpine\", MajorVersion: \"3\", MinorVersion: \"18\"}\n\talpineEdge := &OperatingSystem{Name: \"alpine\", ReleaseID: \"alpine\", LabelVersion: \"edge\"}\n\tdebianUnstable := &OperatingSystem{Name: \"debian\", ReleaseID: \"debian\", LabelVersion: \"unstable\"}\n\tdebian7 := &OperatingSystem{Name: \"debian\", ReleaseID: \"debian\", MajorVersion: \"7\", LabelVersion: \"wheezy\"}\n\twolfi := &OperatingSystem{Name: \"wolfi\", ReleaseID: \"wolfi\", MajorVersion: \"20230201\"}\n\tarch := &OperatingSystem{Name: \"archlinux\", ReleaseID: \"arch\", MajorVersion: \"20241110\", MinorVersion: \"0\"}\n\toracle5 := &OperatingSystem{Name: \"oracle\", ReleaseID: \"ol\", MajorVersion: \"5\"}\n\toracle6 := &OperatingSystem{Name: \"oracle\", ReleaseID: \"ol\", MajorVersion: \"6\"}\n\tamazon2 := &OperatingSystem{Name: \"amazon\", ReleaseID: \"amzn\", MajorVersion: \"2\"}\n\tminimos := &OperatingSystem{Name: \"minimos\", ReleaseID: \"minimos\", MajorVersion: \"20241031\"}\n\trocky8 := &OperatingSystem{Name: \"rocky\", ReleaseID: \"rocky\", MajorVersion: \"8\"}        // should not be matched\n\talma8 := &OperatingSystem{Name: \"almalinux\", ReleaseID: \"almalinux\", MajorVersion: \"8\"} // should not be matched\n\n\toperatingSystems := []*OperatingSystem{\n\t\tubuntu2004,\n\t\tubuntu2010,\n\t\trhel8,\n\t\trhel81,\n\t\tdebian10,\n\t\tdebian13,\n\t\talpine318,\n\t\talpineEdge,\n\t\tdebianUnstable,\n\t\tdebian7,\n\t\twolfi,\n\t\tarch,\n\t\toracle5,\n\t\toracle6,\n\t\tamazon2,\n\t\tminimos,\n\t\trocky8,\n\t\talma8,\n\t\techo,\n\t}\n\trequire.NoError(t, db.Create(&operatingSystems).Error)\n\n\ttests := []struct {\n\t\tname      string\n\t\tos        OSSpecifier\n\t\texpected  []OperatingSystem\n\t\texpectErr require.ErrorAssertionFunc\n\t}{\n\t\t{\n\t\t\tname: \"specific distro with major and minor version\",\n\t\t\tos: OSSpecifier{\n\t\t\t\tName:         \"ubuntu\",\n\t\t\t\tMajorVersion: \"20\",\n\t\t\t\tMinorVersion: \"04\",\n\t\t\t},\n\t\t\texpected: []OperatingSystem{*ubuntu2004},\n\t\t},\n\t\t{\n\t\t\tname: \"specific distro with major and minor version (missing left padding)\",\n\t\t\tos: OSSpecifier{\n\t\t\t\tName:         \"ubuntu\",\n\t\t\t\tMajorVersion: \"20\",\n\t\t\t\tMinorVersion: \"4\",\n\t\t\t},\n\t\t\texpected: []OperatingSystem{*ubuntu2004},\n\t\t},\n\t\t{\n\t\t\tname: \"alias resolution with major version\",\n\t\t\tos: OSSpecifier{\n\t\t\t\tName:         \"centos\",\n\t\t\t\tMajorVersion: \"8\",\n\t\t\t},\n\t\t\texpected: []OperatingSystem{*rhel8},\n\t\t},\n\t\t{\n\t\t\tname: \"alias resolution with major and minor version\",\n\t\t\tos: OSSpecifier{\n\t\t\t\tName:         \"centos\",\n\t\t\t\tMajorVersion: \"8\",\n\t\t\t\tMinorVersion: \"1\",\n\t\t\t},\n\t\t\texpected: []OperatingSystem{*rhel81},\n\t\t},\n\t\t{\n\t\t\tname: \"distro with major version only\",\n\t\t\tos: OSSpecifier{\n\t\t\t\tName:         \"debian\",\n\t\t\t\tMajorVersion: \"10\",\n\t\t\t},\n\t\t\texpected: []OperatingSystem{*debian10},\n\t\t},\n\t\t{\n\t\t\tname: \"codename resolution\",\n\t\t\tos: OSSpecifier{\n\t\t\t\tName:         \"ubuntu\",\n\t\t\t\tLabelVersion: \"focal\",\n\t\t\t},\n\t\t\texpected: []OperatingSystem{*ubuntu2004},\n\t\t},\n\t\t{\n\t\t\tname: \"codename and version info\",\n\t\t\tos: OSSpecifier{\n\t\t\t\tName:         \"ubuntu\",\n\t\t\t\tMajorVersion: \"20\",\n\t\t\t\tMinorVersion: \"04\",\n\t\t\t\tLabelVersion: \"focal\",\n\t\t\t},\n\t\t\texpected: []OperatingSystem{*ubuntu2004},\n\t\t},\n\t\t{\n\t\t\tname: \"conflicting codename and version info\",\n\t\t\tos: OSSpecifier{\n\t\t\t\tName:         \"ubuntu\",\n\t\t\t\tMajorVersion: \"20\",\n\t\t\t\tMinorVersion: \"04\",\n\t\t\t\tLabelVersion: \"fake\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"alpine edge version\",\n\t\t\tos: OSSpecifier{\n\t\t\t\tName:         \"alpine\",\n\t\t\t\tMajorVersion: \"3\",\n\t\t\t\tMinorVersion: \"21\",\n\t\t\t\tLabelVersion: \"3.21.0_alpha20240807\",\n\t\t\t},\n\t\t\texpected: []OperatingSystem{*alpineEdge},\n\t\t},\n\t\t{\n\t\t\tname: \"arch rolling variant\",\n\t\t\tos: OSSpecifier{\n\t\t\t\tName: \"arch\",\n\t\t\t},\n\t\t\texpected: []OperatingSystem{*arch},\n\t\t},\n\t\t{\n\t\t\tname: \"wolfi rolling variant\",\n\t\t\tos: OSSpecifier{\n\t\t\t\tName:         \"wolfi\",\n\t\t\t\tMajorVersion: \"20221018\",\n\t\t\t},\n\t\t\texpected: []OperatingSystem{*wolfi},\n\t\t},\n\t\t{\n\t\t\tname: \"debian by codename for rolling alias\",\n\t\t\tos: OSSpecifier{\n\t\t\t\tName:         \"debian\",\n\t\t\t\tMajorVersion: \"13\",\n\t\t\t\tLabelVersion: \"trixie\",\n\t\t\t},\n\t\t\texpected: []OperatingSystem{*debian13},\n\t\t},\n\t\t{\n\t\t\tname: \"debian by codename for rolling alias\",\n\t\t\tos: OSSpecifier{\n\t\t\t\tName:         \"debian\",\n\t\t\t\tMajorVersion: \"14\",\n\t\t\t\tLabelVersion: \"forky\",\n\t\t\t},\n\t\t\texpected: []OperatingSystem{*debianUnstable},\n\t\t},\n\t\t{\n\t\t\tname: \"debian by codename\",\n\t\t\tos: OSSpecifier{\n\t\t\t\tName:         \"debian\",\n\t\t\t\tLabelVersion: \"wheezy\",\n\t\t\t},\n\t\t\texpected: []OperatingSystem{*debian7},\n\t\t},\n\t\t{\n\t\t\tname: \"debian by major version\",\n\t\t\tos: OSSpecifier{\n\t\t\t\tName:         \"debian\",\n\t\t\t\tMajorVersion: \"7\",\n\t\t\t},\n\t\t\texpected: []OperatingSystem{*debian7},\n\t\t},\n\t\t{\n\t\t\tname: \"debian by major.minor version\",\n\t\t\tos: OSSpecifier{\n\t\t\t\tName:         \"debian\",\n\t\t\t\tMajorVersion: \"7\",\n\t\t\t\tMinorVersion: \"2\",\n\t\t\t},\n\t\t\texpected: []OperatingSystem{*debian7},\n\t\t},\n\t\t{\n\t\t\tname: \"alpine with major and minor version\",\n\t\t\tos: OSSpecifier{\n\t\t\t\tName:         \"alpine\",\n\t\t\t\tMajorVersion: \"3\",\n\t\t\t\tMinorVersion: \"18\",\n\t\t\t},\n\t\t\texpected: []OperatingSystem{*alpine318},\n\t\t},\n\t\t{\n\t\t\tname: \"lookup by release ID (not name)\",\n\t\t\tos: OSSpecifier{\n\t\t\t\tName:         \"ol\",\n\t\t\t\tMajorVersion: \"5\",\n\t\t\t},\n\t\t\texpected: []OperatingSystem{*oracle5},\n\t\t},\n\t\t{\n\t\t\tname: \"lookup by non-standard name (oraclelinux)\",\n\t\t\tos: OSSpecifier{\n\t\t\t\tName:         \"oraclelinux\", // based on the grype distro names\n\t\t\t\tMajorVersion: \"5\",\n\t\t\t},\n\t\t\texpected: []OperatingSystem{*oracle5},\n\t\t},\n\t\t{\n\t\t\tname: \"lookup by non-standard name (amazonlinux)\",\n\t\t\tos: OSSpecifier{\n\t\t\t\tName:         \"amazonlinux\", // based on the grype distro names\n\t\t\t\tMajorVersion: \"2\",\n\t\t\t},\n\t\t\texpected: []OperatingSystem{*amazon2},\n\t\t},\n\t\t{\n\t\t\tname: \"lookup by non-standard name (oracle)\",\n\t\t\tos: OSSpecifier{\n\t\t\t\tName:         \"oracle\",\n\t\t\t\tMajorVersion: \"5\",\n\t\t\t},\n\t\t\texpected: []OperatingSystem{*oracle5},\n\t\t},\n\t\t{\n\t\t\tname: \"lookup by non-standard name (amazon)\",\n\t\t\tos: OSSpecifier{\n\t\t\t\tName:         \"amazon\",\n\t\t\t\tMajorVersion: \"2\",\n\t\t\t},\n\t\t\texpected: []OperatingSystem{*amazon2},\n\t\t},\n\t\t{\n\t\t\tname: \"lookup by non-standard name (rocky)\",\n\t\t\tos: OSSpecifier{\n\t\t\t\tName:         \"rocky\",\n\t\t\t\tMajorVersion: \"8\",\n\t\t\t},\n\t\t\texpected: []OperatingSystem{*rhel8},\n\t\t},\n\t\t{\n\t\t\tname: \"lookup by non-standard name (rockylinux)\",\n\t\t\tos: OSSpecifier{\n\t\t\t\tName:         \"rockylinux\",\n\t\t\t\tMajorVersion: \"8\",\n\t\t\t},\n\t\t\texpected: []OperatingSystem{*rhel8},\n\t\t},\n\t\t{\n\t\t\tname: \"lookup by non-standard name (alma)\",\n\t\t\tos: OSSpecifier{\n\t\t\t\tName:         \"alma\",\n\t\t\t\tMajorVersion: \"8\",\n\t\t\t},\n\t\t\texpected: []OperatingSystem{*rhel8},\n\t\t},\n\t\t{\n\t\t\tname: \"lookup by non-standard name (almalinux)\",\n\t\t\tos: OSSpecifier{\n\t\t\t\tName:         \"almalinux\",\n\t\t\t\tMajorVersion: \"8\",\n\t\t\t},\n\t\t\texpected: []OperatingSystem{*rhel8},\n\t\t},\n\t\t{\n\t\t\tname: \"echo rolling variant\",\n\t\t\tos: OSSpecifier{\n\t\t\t\tName:         \"echo\",\n\t\t\t\tMajorVersion: \"1\",\n\t\t\t},\n\t\t\texpected: []OperatingSystem{*echo},\n\t\t},\n\t\t{\n\t\t\tname: \"missing distro name\",\n\t\t\tos: OSSpecifier{\n\t\t\t\tMajorVersion: \"8\",\n\t\t\t},\n\t\t\texpectErr: expectErrIs(t, ErrMissingOSIdentification),\n\t\t},\n\t\t{\n\t\t\tname: \"nonexistent distro\",\n\t\t\tos: OSSpecifier{\n\t\t\t\tName:         \"madeup\",\n\t\t\t\tMajorVersion: \"99\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"minimos rolling variant\",\n\t\t\tos: OSSpecifier{\n\t\t\t\tName: \"minimos\",\n\t\t\t},\n\t\t\texpected: []OperatingSystem{*minimos},\n\t\t},\n\t\t{\n\t\t\tname: \"nonexistent minor version falls back to major by default\",\n\t\t\tos: OSSpecifier{\n\t\t\t\tName:         \"alpine\",\n\t\t\t\tMajorVersion: \"3\",\n\t\t\t\tMinorVersion: \"99\", // doesn't exist, should fall back to 3.18\n\t\t\t},\n\t\t\texpected: []OperatingSystem{*alpine318},\n\t\t},\n\t\t{\n\t\t\tname: \"nonexistent minor version with DisableFallback returns nothing\",\n\t\t\tos: OSSpecifier{\n\t\t\t\tName:            \"alpine\",\n\t\t\t\tMajorVersion:    \"3\",\n\t\t\t\tMinorVersion:    \"99\", // doesn't exist\n\t\t\t\tDisableFallback: true, // should NOT fall back\n\t\t\t},\n\t\t\texpected: nil,\n\t\t},\n\t\t{\n\t\t\tname: \"major-only distro with DisableFallback finds exact match (Debian EOL lookup)\",\n\t\t\tos: OSSpecifier{\n\t\t\t\tName:            \"debian\",\n\t\t\t\tMajorVersion:    \"10\",\n\t\t\t\tMinorVersion:    \"\", // Debian uses major-only versions\n\t\t\t\tDisableFallback: true,\n\t\t\t},\n\t\t\texpected: []OperatingSystem{*debian10},\n\t\t},\n\t\t{\n\t\t\tname: \"RHEL major-only lookup finds major-only record (vuln matching)\",\n\t\t\tos: OSSpecifier{\n\t\t\t\tName:         \"rhel\",\n\t\t\t\tMajorVersion: \"8\",\n\t\t\t\tMinorVersion: \"\", // empty minor should find rhel8 directly\n\t\t\t},\n\t\t\texpected: []OperatingSystem{*rhel8},\n\t\t},\n\t\t{\n\t\t\tname: \"RHEL nonexistent minor falls back to major-only record (vuln matching)\",\n\t\t\tos: OSSpecifier{\n\t\t\t\tName:         \"rhel\",\n\t\t\t\tMajorVersion: \"8\",\n\t\t\t\tMinorVersion: \"5\", // 8.5 doesn't exist, should fall back to 8\n\t\t\t},\n\t\t\texpected: []OperatingSystem{*rhel8},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif tt.expectErr == nil {\n\t\t\t\ttt.expectErr = require.NoError\n\t\t\t}\n\t\t\tresult, err := oss.GetOperatingSystems(tt.os)\n\t\t\ttt.expectErr(t, err)\n\t\t\tif err != nil {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif diff := cmp.Diff(tt.expected, result, cmpopts.EquateEmpty()); diff != \"\" {\n\t\t\t\tt.Errorf(\"unexpected result (-want +got):\\n%s\", diff)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestOSSpecifier_String(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tos       *OSSpecifier\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:     \"nil distro\",\n\t\t\tos:       AnyOSSpecified,\n\t\t\texpected: \"any\",\n\t\t},\n\t\t{\n\t\t\tname:     \"no distro specified\",\n\t\t\tos:       NoOSSpecified,\n\t\t\texpected: \"none\",\n\t\t},\n\t\t{\n\t\t\tname: \"only name specified\",\n\t\t\tos: &OSSpecifier{\n\t\t\t\tName: \"ubuntu\",\n\t\t\t},\n\t\t\texpected: \"ubuntu\",\n\t\t},\n\t\t{\n\t\t\tname: \"name and major version specified\",\n\t\t\tos: &OSSpecifier{\n\t\t\t\tName:         \"ubuntu\",\n\t\t\t\tMajorVersion: \"20\",\n\t\t\t},\n\t\t\texpected: \"ubuntu@20\",\n\t\t},\n\t\t{\n\t\t\tname: \"name, major, and minor version specified\",\n\t\t\tos: &OSSpecifier{\n\t\t\t\tName:         \"ubuntu\",\n\t\t\t\tMajorVersion: \"20\",\n\t\t\t\tMinorVersion: \"04\",\n\t\t\t},\n\t\t\texpected: \"ubuntu@20.04\",\n\t\t},\n\t\t{\n\t\t\tname: \"name, major version, and codename specified\",\n\t\t\tos: &OSSpecifier{\n\t\t\t\tName:         \"ubuntu\",\n\t\t\t\tMajorVersion: \"20\",\n\t\t\t\tLabelVersion: \"focal\",\n\t\t\t},\n\t\t\texpected: \"ubuntu@20 (focal)\",\n\t\t},\n\t\t{\n\t\t\tname: \"name and codename specified\",\n\t\t\tos: &OSSpecifier{\n\t\t\t\tName:         \"ubuntu\",\n\t\t\t\tLabelVersion: \"focal\",\n\t\t\t},\n\t\t\texpected: \"ubuntu@focal\",\n\t\t},\n\t\t{\n\t\t\tname: \"name, major version, minor version, and codename specified\",\n\t\t\tos: &OSSpecifier{\n\t\t\t\tName:         \"ubuntu\",\n\t\t\t\tMajorVersion: \"20\",\n\t\t\t\tMinorVersion: \"04\",\n\t\t\t\tLabelVersion: \"focal\",\n\t\t\t},\n\t\t\texpected: \"ubuntu@20.04\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := tt.os.String()\n\t\t\trequire.Equal(t, tt.expected, result)\n\t\t})\n\t}\n}\n\nfunc TestTrimZeroes(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:     \"empty string\",\n\t\t\tinput:    \"\",\n\t\t\texpected: \"\",\n\t\t},\n\t\t{\n\t\t\tname:     \"single zero\",\n\t\t\tinput:    \"0\",\n\t\t\texpected: \"0\",\n\t\t},\n\t\t{\n\t\t\tname:     \"multiple zeros only\",\n\t\t\tinput:    \"000\",\n\t\t\texpected: \"0\",\n\t\t},\n\t\t{\n\t\t\tname:     \"single non-zero digit\",\n\t\t\tinput:    \"5\",\n\t\t\texpected: \"5\",\n\t\t},\n\t\t{\n\t\t\tname:     \"no leading zeros\",\n\t\t\tinput:    \"123\",\n\t\t\texpected: \"123\",\n\t\t},\n\t\t{\n\t\t\tname:     \"single leading zero\",\n\t\t\tinput:    \"0123\",\n\t\t\texpected: \"123\",\n\t\t},\n\t\t{\n\t\t\tname:     \"multiple leading zeros\",\n\t\t\tinput:    \"000123\",\n\t\t\texpected: \"123\",\n\t\t},\n\t\t{\n\t\t\tname:     \"leading zeros with trailing zeros\",\n\t\t\tinput:    \"001230\",\n\t\t\texpected: \"1230\",\n\t\t},\n\t\t{\n\t\t\tname:     \"string starting with non-zero\",\n\t\t\tinput:    \"1000\",\n\t\t\texpected: \"1000\",\n\t\t},\n\t\t{\n\t\t\tname:     \"mixed digits with leading zeros\",\n\t\t\tinput:    \"00042\",\n\t\t\texpected: \"42\",\n\t\t},\n\t\t{\n\t\t\tname:     \"very long leading zeros\",\n\t\t\tinput:    \"00000000001\",\n\t\t\texpected: \"1\",\n\t\t},\n\t\t{\n\t\t\tname:     \"alphanumeric with leading zero\",\n\t\t\tinput:    \"0abc\",\n\t\t\texpected: \"abc\",\n\t\t},\n\t\t{\n\t\t\tname:     \"special characters with leading zeros\",\n\t\t\tinput:    \"00.123\",\n\t\t\texpected: \".123\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := trimZeroes(tt.input)\n\t\t\tassert.Equal(t, tt.expected, result)\n\t\t})\n\t}\n}\n\nfunc TestOSSpecifier_clean(t *testing.T) {\n\ttests := []struct {\n\t\tname  string\n\t\tinput OSSpecifier\n\t\twant  OSSpecifier\n\t}{\n\t\t{\n\t\t\tname: \"trim 0s\",\n\t\t\tinput: OSSpecifier{\n\t\t\t\tName:         \"Ubuntu\",\n\t\t\t\tMajorVersion: \"20\",\n\t\t\t\tMinorVersion: \"04\",\n\t\t\t},\n\t\t\twant: OSSpecifier{\n\t\t\t\tName:         \"Ubuntu\",\n\t\t\t\tMajorVersion: \"20\",\n\t\t\t\tMinorVersion: \"4\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"preserve 0 value\",\n\t\t\tinput: OSSpecifier{\n\t\t\t\tName:         \"Redhat\",\n\t\t\t\tMajorVersion: \"9\",\n\t\t\t\tMinorVersion: \"0\",\n\t\t\t},\n\t\t\twant: OSSpecifier{\n\t\t\t\tName:         \"Redhat\",\n\t\t\t\tMajorVersion: \"9\",\n\t\t\t\tMinorVersion: \"0\", // important! ...9 != 9.0 since 9 includes multiple minor versions\n\t\t\t},\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\to := tt.input\n\t\t\to.clean()\n\t\t\tif d := cmp.Diff(tt.want, o); d != \"\" {\n\t\t\t\tt.Errorf(\"OSSpecifier.clean() mismatch (-want +got):\\n%s\", d)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestApplyOverride(t *testing.T) {\n\ttests := []struct {\n\t\tname        string\n\t\tosSpecifier OSSpecifier\n\t\toverride    OperatingSystemSpecifierOverride\n\t\texpected    OSSpecifier\n\t\twantApplied bool\n\t}{\n\t\t{\n\t\t\tname: \"replace name\",\n\t\t\tosSpecifier: OSSpecifier{\n\t\t\t\tName:         \"centos\",\n\t\t\t\tMajorVersion: \"8\",\n\t\t\t\tMinorVersion: \"1\",\n\t\t\t},\n\t\t\toverride: OperatingSystemSpecifierOverride{\n\t\t\t\tReplacementName: strPtr(\"rhel\"),\n\t\t\t},\n\t\t\texpected: OSSpecifier{\n\t\t\t\tName:         \"rhel\",\n\t\t\t\tMajorVersion: \"8\",\n\t\t\t\tMinorVersion: \"1\",\n\t\t\t},\n\t\t\twantApplied: true,\n\t\t},\n\t\t{\n\t\t\tname: \"replace major version\",\n\t\t\tosSpecifier: OSSpecifier{\n\t\t\t\tName:         \"rhel\",\n\t\t\t\tMajorVersion: \"8\",\n\t\t\t\tMinorVersion: \"1\",\n\t\t\t},\n\t\t\toverride: OperatingSystemSpecifierOverride{\n\t\t\t\tReplacementMajorVersion: strPtr(\"9\"),\n\t\t\t},\n\t\t\texpected: OSSpecifier{\n\t\t\t\tName:         \"rhel\",\n\t\t\t\tMajorVersion: \"9\",\n\t\t\t\tMinorVersion: \"1\",\n\t\t\t},\n\t\t\twantApplied: true,\n\t\t},\n\t\t{\n\t\t\tname: \"replace minor version\",\n\t\t\tosSpecifier: OSSpecifier{\n\t\t\t\tName:         \"ubuntu\",\n\t\t\t\tMajorVersion: \"20\",\n\t\t\t\tMinorVersion: \"04\",\n\t\t\t},\n\t\t\toverride: OperatingSystemSpecifierOverride{\n\t\t\t\tReplacementMinorVersion: strPtr(\"10\"),\n\t\t\t},\n\t\t\texpected: OSSpecifier{\n\t\t\t\tName:         \"ubuntu\",\n\t\t\t\tMajorVersion: \"20\",\n\t\t\t\tMinorVersion: \"10\",\n\t\t\t},\n\t\t\twantApplied: true,\n\t\t},\n\t\t{\n\t\t\tname: \"replace label version\",\n\t\t\tosSpecifier: OSSpecifier{\n\t\t\t\tName:         \"ubuntu\",\n\t\t\t\tMajorVersion: \"20\",\n\t\t\t\tMinorVersion: \"04\",\n\t\t\t\tLabelVersion: \"focal\",\n\t\t\t},\n\t\t\toverride: OperatingSystemSpecifierOverride{\n\t\t\t\tReplacementLabelVersion: strPtr(\"jammy\"),\n\t\t\t},\n\t\t\texpected: OSSpecifier{\n\t\t\t\tName:         \"ubuntu\",\n\t\t\t\tMajorVersion: \"20\",\n\t\t\t\tMinorVersion: \"04\",\n\t\t\t\tLabelVersion: \"jammy\",\n\t\t\t},\n\t\t\twantApplied: true,\n\t\t},\n\t\t{\n\t\t\tname: \"replace channel\",\n\t\t\tosSpecifier: OSSpecifier{\n\t\t\t\tName:         \"rhel\",\n\t\t\t\tMajorVersion: \"9\",\n\t\t\t\tMinorVersion: \"1\",\n\t\t\t\tChannel:      \"eeus\",\n\t\t\t},\n\t\t\toverride: OperatingSystemSpecifierOverride{\n\t\t\t\tReplacementChannel: strPtr(\"eus\"),\n\t\t\t},\n\t\t\texpected: OSSpecifier{\n\t\t\t\tName:         \"rhel\",\n\t\t\t\tMajorVersion: \"9\",\n\t\t\t\tMinorVersion: \"1\",\n\t\t\t\tChannel:      \"eus\",\n\t\t\t},\n\t\t\twantApplied: true,\n\t\t},\n\t\t{\n\t\t\tname: \"rolling flag clears versions\",\n\t\t\tosSpecifier: OSSpecifier{\n\t\t\t\tName:         \"arch\",\n\t\t\t\tMajorVersion: \"2024\",\n\t\t\t\tMinorVersion: \"01\",\n\t\t\t\tLabelVersion: \"rolling\",\n\t\t\t},\n\t\t\toverride: OperatingSystemSpecifierOverride{\n\t\t\t\tRolling: true,\n\t\t\t},\n\t\t\texpected: OSSpecifier{\n\t\t\t\tName:         \"arch\",\n\t\t\t\tMajorVersion: \"\",\n\t\t\t\tMinorVersion: \"\",\n\t\t\t\tLabelVersion: \"rolling\",\n\t\t\t},\n\t\t\twantApplied: true,\n\t\t},\n\t\t{\n\t\t\tname: \"comprehensive override - all fields\",\n\t\t\tosSpecifier: OSSpecifier{\n\t\t\t\tName:         \"centos\",\n\t\t\t\tMajorVersion: \"7\",\n\t\t\t\tMinorVersion: \"5\",\n\t\t\t\tLabelVersion: \"core\",\n\t\t\t},\n\t\t\toverride: OperatingSystemSpecifierOverride{\n\t\t\t\tReplacementName:         strPtr(\"rhel\"),\n\t\t\t\tReplacementMajorVersion: strPtr(\"7\"),\n\t\t\t\tReplacementMinorVersion: strPtr(\"9\"),\n\t\t\t\tReplacementLabelVersion: strPtr(\"server\"),\n\t\t\t},\n\t\t\texpected: OSSpecifier{\n\t\t\t\tName:         \"rhel\",\n\t\t\t\tMajorVersion: \"7\",\n\t\t\t\tMinorVersion: \"9\",\n\t\t\t\tLabelVersion: \"server\",\n\t\t\t},\n\t\t\twantApplied: true,\n\t\t},\n\t\t{\n\t\t\tname: \"no replacement fields - no changes\",\n\t\t\tosSpecifier: OSSpecifier{\n\t\t\t\tName:         \"ubuntu\",\n\t\t\t\tMajorVersion: \"20\",\n\t\t\t\tMinorVersion: \"04\",\n\t\t\t\tLabelVersion: \"focal\",\n\t\t\t},\n\t\t\toverride: OperatingSystemSpecifierOverride{\n\t\t\t\tAlias: \"ubuntu\",\n\t\t\t},\n\t\t\texpected: OSSpecifier{\n\t\t\t\tName:         \"ubuntu\",\n\t\t\t\tMajorVersion: \"20\",\n\t\t\t\tMinorVersion: \"04\",\n\t\t\t\tLabelVersion: \"focal\",\n\t\t\t},\n\t\t\twantApplied: false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\t// make a copy to avoid modifying the original\n\t\t\td := tt.osSpecifier\n\t\t\tapplied := applyOverride(&d, tt.override)\n\n\t\t\trequire.Equal(t, tt.wantApplied, applied)\n\n\t\t\tif diff := cmp.Diff(tt.expected, d); diff != \"\" {\n\t\t\t\tt.Errorf(\"unexpected result (-want +got):\\n%s\", diff)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestApplyOSSpecifierOverrides(t *testing.T) {\n\ttests := []struct {\n\t\tname          string\n\t\tosSpecifier   OSSpecifier\n\t\taliases       []OperatingSystemSpecifierOverride\n\t\tclientVersion *version.Version\n\t\twantErr       require.ErrorAssertionFunc\n\t\texpected      OSSpecifier\n\t}{\n\t\t{\n\t\t\tname: \"no aliases - no change\",\n\t\t\tosSpecifier: OSSpecifier{\n\t\t\t\tName:         \"ubuntu\",\n\t\t\t\tMajorVersion: \"20\",\n\t\t\t\tMinorVersion: \"04\",\n\t\t\t\tLabelVersion: \"focal\",\n\t\t\t},\n\t\t\taliases: []OperatingSystemSpecifierOverride{},\n\t\t\texpected: OSSpecifier{\n\t\t\t\tName:         \"ubuntu\",\n\t\t\t\tMajorVersion: \"20\",\n\t\t\t\tMinorVersion: \"04\",\n\t\t\t\tLabelVersion: \"focal\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"multiple overrides - first match wins\",\n\t\t\tosSpecifier: OSSpecifier{\n\t\t\t\tName:         \"centos\",\n\t\t\t\tMajorVersion: \"8\",\n\t\t\t\tMinorVersion: \"1\",\n\t\t\t},\n\t\t\taliases: []OperatingSystemSpecifierOverride{\n\t\t\t\t{\n\t\t\t\t\tAlias:           \"centos\",\n\t\t\t\t\tReplacementName: strPtr(\"rhel\"),\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tAlias:           \"centos\",\n\t\t\t\t\tReplacementName: strPtr(\"fedora\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: OSSpecifier{\n\t\t\t\tName:         \"rhel\", // overridden\n\t\t\t\tMajorVersion: \"8\",\n\t\t\t\tMinorVersion: \"1\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"codename mismatch - no override\",\n\t\t\tosSpecifier: OSSpecifier{\n\t\t\t\tName:         \"ubuntu\",\n\t\t\t\tMajorVersion: \"20\",\n\t\t\t\tMinorVersion: \"04\",\n\t\t\t\tLabelVersion: \"focal\",\n\t\t\t},\n\t\t\taliases: []OperatingSystemSpecifierOverride{\n\t\t\t\t{\n\t\t\t\t\tAlias:           \"ubuntu\",\n\t\t\t\t\tCodename:        \"jammy\",\n\t\t\t\t\tReplacementName: strPtr(\"ubuntu-lts\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: OSSpecifier{\n\t\t\t\tName:         \"ubuntu\",\n\t\t\t\tMajorVersion: \"20\",\n\t\t\t\tMinorVersion: \"04\",\n\t\t\t\tLabelVersion: \"focal\", // not overridden\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"version mismatch - no override\",\n\t\t\tosSpecifier: OSSpecifier{\n\t\t\t\tName:         \"debian\",\n\t\t\t\tMajorVersion: \"10\",\n\t\t\t\tMinorVersion: \"5\",\n\t\t\t},\n\t\t\taliases: []OperatingSystemSpecifierOverride{\n\t\t\t\t{\n\t\t\t\t\tAlias:           \"debian\",\n\t\t\t\t\tVersion:         \"11.0\",\n\t\t\t\t\tReplacementName: strPtr(\"debian-bullseye\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: OSSpecifier{\n\t\t\t\tName:         \"debian\",\n\t\t\t\tMajorVersion: \"10\", // not overridden\n\t\t\t\tMinorVersion: \"5\",  // not overridden\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"version pattern mismatch - no override\",\n\t\t\tosSpecifier: OSSpecifier{\n\t\t\t\tName:         \"alpine\",\n\t\t\t\tMajorVersion: \"2\",\n\t\t\t\tMinorVersion: \"18\",\n\t\t\t},\n\t\t\taliases: []OperatingSystemSpecifierOverride{\n\t\t\t\t{\n\t\t\t\t\tAlias:           \"alpine\",\n\t\t\t\t\tVersionPattern:  \"^3\\\\.[0-9]+$\",\n\t\t\t\t\tReplacementName: strPtr(\"alpine-stable\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: OSSpecifier{\n\t\t\t\tName:         \"alpine\", // not overridden\n\t\t\t\tMajorVersion: \"2\",\n\t\t\t\tMinorVersion: \"18\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"client version constraint satisfied\",\n\t\t\tosSpecifier: OSSpecifier{\n\t\t\t\tName:         \"ubuntu\",\n\t\t\t\tMajorVersion: \"20\",\n\t\t\t\tMinorVersion: \"04\",\n\t\t\t},\n\t\t\taliases: []OperatingSystemSpecifierOverride{\n\t\t\t\t{\n\t\t\t\t\tAlias:                     \"ubuntu\",\n\t\t\t\t\tApplicableClientDBSchemas: \">=1.0.0\",\n\t\t\t\t\tReplacementName:           strPtr(\"ubuntu-new\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\tclientVersion: version.New(\"1.2.0\", version.SemanticFormat), // matches the constraint, thus allowed to override\n\t\t\texpected: OSSpecifier{\n\t\t\t\tName:         \"ubuntu-new\", // overridden\n\t\t\t\tMajorVersion: \"20\",\n\t\t\t\tMinorVersion: \"04\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"client version constraint not satisfied\",\n\t\t\tosSpecifier: OSSpecifier{\n\t\t\t\tName:         \"ubuntu\",\n\t\t\t\tMajorVersion: \"20\",\n\t\t\t\tMinorVersion: \"04\",\n\t\t\t},\n\t\t\taliases: []OperatingSystemSpecifierOverride{\n\t\t\t\t{\n\t\t\t\t\tAlias:                     \"ubuntu\",\n\t\t\t\t\tApplicableClientDBSchemas: \">=2.0.0\", // does not match the client version, thus no override\n\t\t\t\t\tReplacementName:           strPtr(\"ubuntu-new\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\tclientVersion: version.New(\"1.2.0\", version.SemanticFormat),\n\t\t\texpected: OSSpecifier{\n\t\t\t\tName:         \"ubuntu\", // not overridden\n\t\t\t\tMajorVersion: \"20\",\n\t\t\t\tMinorVersion: \"04\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"invalid client version constraint - honor the override\",\n\t\t\tosSpecifier: OSSpecifier{\n\t\t\t\tName:         \"ubuntu\",\n\t\t\t\tMajorVersion: \"20\",\n\t\t\t\tMinorVersion: \"04\",\n\t\t\t},\n\t\t\taliases: []OperatingSystemSpecifierOverride{\n\t\t\t\t{\n\t\t\t\t\tAlias:                     \"ubuntu\",\n\t\t\t\t\tApplicableClientDBSchemas: \"invalid-constraint\", // oops!\n\t\t\t\t\tReplacementName:           strPtr(\"ubuntu-new\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\tclientVersion: version.New(\"1.2.0\", version.SemanticFormat),\n\t\t\texpected: OSSpecifier{\n\t\t\t\tName:         \"ubuntu-new\", // overridden\n\t\t\t\tMajorVersion: \"20\",\n\t\t\t\tMinorVersion: \"04\",\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif tt.wantErr == nil {\n\t\t\t\ttt.wantErr = require.NoError\n\t\t\t}\n\n\t\t\t// make a copy to avoid modifying the original\n\t\t\td := tt.osSpecifier\n\t\t\terr := applyOSSpecifierOverrides(&d, tt.aliases, tt.clientVersion)\n\t\t\ttt.wantErr(t, err)\n\n\t\t\tif err != nil {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif diff := cmp.Diff(tt.expected, d); diff != \"\" {\n\t\t\t\tt.Errorf(\"unexpected result (-want +got):\\n%s\", diff)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc strPtr(s string) *string {\n\treturn &s\n}\n"
  },
  {
    "path": "grype/db/v6/package_store.go",
    "content": "package v6\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"strings\"\n\t\"time\"\n\n\t\"golang.org/x/exp/maps\"\n\t\"gorm.io/gorm\"\n\t\"gorm.io/gorm/clause\"\n\n\t\"github.com/anchore/grype/internal/log\"\n\t\"github.com/anchore/syft/syft/cpe\"\n)\n\nconst (\n\tanyPkg = \"any\"\n\tanyOS  = \"any\"\n)\n\nvar NoOSSpecified = &OSSpecifier{}\nvar AnyOSSpecified *OSSpecifier\nvar AnyPackageSpecified *PackageSpecifier\nvar ErrMissingOSIdentification = errors.New(\"missing OS name or codename\")\nvar ErrOSNotPresent = errors.New(\"OS not present\")\nvar ErrLimitReached = errors.New(\"query limit reached\")\n\ntype GetPackageOptions struct {\n\tPreloadOS             bool\n\tPreloadPackage        bool\n\tPreloadPackageCPEs    bool\n\tPreloadVulnerability  bool\n\tPreloadBlob           bool\n\tOSs                   OSSpecifiers\n\tVulnerabilities       VulnerabilitySpecifiers\n\tAllowBroadCPEMatching bool\n\tLimit                 int\n}\n\ntype PackageSpecifiers []*PackageSpecifier\n\ntype PackageSpecifier struct {\n\tName      string\n\tEcosystem string\n\tCPE       *cpe.Attributes\n}\n\nfunc (p *PackageSpecifier) String() string {\n\tif p == nil {\n\t\treturn anyPkg\n\t}\n\n\tvar args []string\n\tif p.Name != \"\" {\n\t\targs = append(args, fmt.Sprintf(\"name=%s\", p.Name))\n\t}\n\n\tif p.Ecosystem != \"\" {\n\t\targs = append(args, fmt.Sprintf(\"ecosystem=%s\", p.Ecosystem))\n\t}\n\n\tif p.CPE != nil {\n\t\targs = append(args, fmt.Sprintf(\"cpe=%s\", p.CPE.String()))\n\t}\n\n\tif len(args) > 0 {\n\t\treturn fmt.Sprintf(\"package(%s)\", strings.Join(args, \", \"))\n\t}\n\n\treturn anyPkg\n}\n\nfunc (p PackageSpecifiers) String() string {\n\tif len(p) == 0 {\n\t\treturn anyPkg\n\t}\n\n\tvar parts []string\n\tfor _, v := range p {\n\t\tparts = append(parts, v.String())\n\t}\n\treturn strings.Join(parts, \", \")\n}\n\ntype packageHandleStore interface {\n\t*AffectedPackageHandle | *UnaffectedPackageHandle\n}\n\ntype packageHandleAccessor interface {\n\tgetPackageHandle() *packageHandle\n}\n\ntype packageStore struct {\n\tdb        *gorm.DB\n\tblobStore *blobStore\n\tosStore   *operatingSystemStore\n}\n\nfunc newPackageStore(db *gorm.DB, bs *blobStore, oss *operatingSystemStore) *packageStore {\n\treturn &packageStore{\n\t\tdb:        db,\n\t\tblobStore: bs,\n\t\tosStore:   oss,\n\t}\n}\n\nfunc addPackages[T packageHandleStore](s *packageStore, packages ...T) (bool, error) {\n\tcacheInst, ok := cacheFromContext(s.db.Statement.Context)\n\tif !ok {\n\t\treturn false, fmt.Errorf(\"unable to fetch package cache from context\")\n\t}\n\n\tvar final []*Package\n\tvar hasCPEs bool\n\tbyCacheKey := make(map[string][]*Package)\n\n\tfor _, p := range packages {\n\t\t// convert to packageHandle to access fields\n\t\tph := any(p).(packageHandleAccessor).getPackageHandle()\n\n\t\tif ph.Package != nil {\n\t\t\tif len(ph.Package.CPEs) > 0 {\n\t\t\t\t// never use the cache if there are CPEs involved\n\t\t\t\tfinal = append(final, ph.Package)\n\t\t\t\thasCPEs = true\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tkey := ph.Package.cacheKey()\n\t\t\tif existingID, ok := cacheInst.getID(ph.Package); ok {\n\t\t\t\t// seen in a previous transaction...\n\t\t\t\tph.PackageID = existingID\n\t\t\t} else if _, ok := byCacheKey[key]; !ok {\n\t\t\t\t// not seen within this transaction\n\t\t\t\tfinal = append(final, ph.Package)\n\t\t\t}\n\t\t\tbyCacheKey[key] = append(byCacheKey[key], ph.Package)\n\t\t}\n\t}\n\n\tif len(final) == 0 {\n\t\treturn false, nil\n\t}\n\n\t// since there is risk of needing to write through packages with conflicting CPEs we cannot write these in batches,\n\t// and since the before hooks reason about previous entries within this loop (potentially) we must ensure that\n\t// these are written in different transactions.\n\tfor _, p := range final {\n\t\tif err := s.db.Clauses(clause.OnConflict{DoNothing: true}).Create(p).Error; err != nil {\n\t\t\treturn false, fmt.Errorf(\"unable to create package records: %w\", err)\n\t\t}\n\t}\n\n\t// update the cache with the new records\n\tfor _, r := range final {\n\t\tcacheInst.set(r)\n\t}\n\n\t// update all references with the IDs from the cache\n\tfor _, refs := range byCacheKey {\n\t\tfor _, r := range refs {\n\t\t\tid, ok := cacheInst.getID(r)\n\t\t\tif ok {\n\t\t\t\tr.setRowID(id)\n\t\t\t}\n\t\t}\n\t}\n\n\t// update the parent objects with the FK ID\n\tfor _, p := range packages {\n\t\tph := any(p).(packageHandleAccessor).getPackageHandle()\n\t\tif ph.Package != nil {\n\t\t\tph.PackageID = ph.Package.ID\n\t\t}\n\t}\n\treturn hasCPEs, nil\n}\n\nfunc addPackagesWithOS[T packageHandleStore](s *packageStore, packages ...T) error {\n\tfor _, p := range packages {\n\t\tph := any(p).(packageHandleAccessor).getPackageHandle()\n\t\tif err := s.osStore.addOsFromPackages(ph); err != nil {\n\t\t\treturn fmt.Errorf(\"unable to add package OS: %w\", err)\n\t\t}\n\t}\n\n\thasCpes, err := addPackages(s, packages...)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to add packages: %w\", err)\n\t}\n\n\tomit := []string{\"OperatingSystem\"}\n\tif !hasCpes {\n\t\tomit = append(omit, \"Package\")\n\t}\n\n\tfor _, v := range packages {\n\t\tif err := s.blobStore.addBlobable(any(v).(blobable)); err != nil {\n\t\t\treturn fmt.Errorf(\"unable to add blob: %w\", err)\n\t\t}\n\n\t\tif err := s.db.Omit(omit...).Create(v).Error; err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc getPackages[T packageHandleStore]( //nolint:funlen\n\ts *packageStore,\n\tpkg *PackageSpecifier,\n\tconfig *GetPackageOptions,\n\ttableName string,\n) ([]T, error) {\n\tif config == nil {\n\t\tconfig = &GetPackageOptions{}\n\t}\n\n\tstart := time.Now()\n\tcount := 0\n\tdefer func() {\n\t\tlog.\n\t\t\tWithFields(\n\t\t\t\t\"pkg\", pkg.String(),\n\t\t\t\t\"distro\", config.OSs,\n\t\t\t\t\"vulns\", config.Vulnerabilities,\n\t\t\t\t\"duration\", time.Since(start),\n\t\t\t\t\"records\", count,\n\t\t\t).\n\t\t\tTrace(\"fetched package record\")\n\t}()\n\n\tquery := s.handlePackage(s.db.Table(tableName), pkg, config.AllowBroadCPEMatching)\n\n\tvar err error\n\tquery, err = s.handleVulnerabilityOptions(query, config.Vulnerabilities, tableName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tquery, err = s.handleOSOptions(query, config.OSs, tableName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tquery = s.handlePreload(query, *config)\n\n\tvar models []T\n\tvar results []T\n\n\tif err := query.FindInBatches(&results, batchSize, func(_ *gorm.DB, _ int) error {\n\t\tif config.PreloadBlob {\n\t\t\tvar blobs []blobable\n\t\t\tfor _, r := range results {\n\t\t\t\tblobs = append(blobs, any(r).(blobable))\n\t\t\t}\n\t\t\tif err := s.blobStore.attachBlobValue(blobs...); err != nil {\n\t\t\t\treturn fmt.Errorf(\"unable to attach package blobs: %w\", err)\n\t\t\t}\n\t\t}\n\n\t\tif config.PreloadVulnerability {\n\t\t\tvar vulns []blobable\n\t\t\tfor _, r := range results {\n\t\t\t\tph := any(r).(packageHandleAccessor).getPackageHandle()\n\t\t\t\tif ph.Vulnerability != nil {\n\t\t\t\t\tvulns = append(vulns, ph.Vulnerability)\n\t\t\t\t}\n\t\t\t}\n\t\t\tif err := s.blobStore.attachBlobValue(vulns...); err != nil {\n\t\t\t\treturn fmt.Errorf(\"unable to attach vulnerability blob: %w\", err)\n\t\t\t}\n\t\t}\n\n\t\tmodels = append(models, results...)\n\n\t\tcount += len(results)\n\n\t\tif config.Limit > 0 && len(models) >= config.Limit {\n\t\t\treturn ErrLimitReached\n\t\t}\n\n\t\treturn nil\n\t}).Error; err != nil {\n\t\treturn models, fmt.Errorf(\"unable to fetch package records: %w\", err)\n\t}\n\n\treturn models, nil\n}\n\nfunc (s *packageStore) handlePackage(query *gorm.DB, p *PackageSpecifier, allowBroad bool) *gorm.DB {\n\tif p == nil {\n\t\treturn query\n\t}\n\n\tif err := s.applyPackageAlias(p); err != nil {\n\t\tlog.Errorf(\"failed to apply package alias: %v\", err)\n\t}\n\n\t// Get table name from the query\n\ttableName := query.Statement.Table\n\tquery = query.Joins(fmt.Sprintf(\"JOIN packages ON %s.package_id = packages.id\", tableName))\n\n\tif p.Name != \"\" {\n\t\tquery = query.Where(\"packages.name = ? collate nocase\", p.Name)\n\t}\n\tif p.Ecosystem != \"\" {\n\t\tquery = query.Where(\"packages.ecosystem = ? collate nocase\", p.Ecosystem)\n\t}\n\n\tif p.CPE != nil {\n\t\tquery = query.Joins(\"JOIN package_cpes ON packages.id = package_cpes.package_id\")\n\t\tquery = query.Joins(\"JOIN cpes ON package_cpes.cpe_id = cpes.id\")\n\t\tquery = handleCPEOptions(query, p.CPE, allowBroad)\n\t}\n\n\treturn query\n}\n\nfunc (s *packageStore) handleVulnerabilityOptions(query *gorm.DB, configs []VulnerabilitySpecifier, tableName string) (*gorm.DB, error) {\n\tif len(configs) == 0 {\n\t\treturn query, nil\n\t}\n\tquery = query.Joins(fmt.Sprintf(\"JOIN vulnerability_handles ON %s.vulnerability_id = vulnerability_handles.id\", tableName))\n\n\treturn handleVulnerabilityOptions(s.db, query, configs...)\n}\n\nfunc (s *packageStore) handleOSOptions(query *gorm.DB, configs []*OSSpecifier, tableName string) (*gorm.DB, error) {\n\tids := map[int64]struct{}{}\n\n\tif len(configs) == 0 {\n\t\tconfigs = append(configs, AnyOSSpecified)\n\t}\n\n\tvar hasAny, hasNone, hasSpecific bool\n\t// process OS specs...\n\tfor _, config := range configs {\n\t\tswitch {\n\t\tcase hasOSSpecified(config):\n\t\t\tcurResolved, err := s.osStore.GetOperatingSystems(*config)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"unable to resolve operating system: %w\", err)\n\t\t\t}\n\n\t\t\thasSpecific = true\n\t\t\tfor _, d := range curResolved {\n\t\t\t\tids[int64(d.ID)] = struct{}{}\n\t\t\t}\n\t\tcase config == AnyOSSpecified:\n\t\t\thasAny = true\n\t\tcase *config == *NoOSSpecified:\n\t\t\thasNone = true\n\t\t}\n\t}\n\n\tif (hasAny || hasNone) && hasSpecific {\n\t\treturn nil, fmt.Errorf(\"cannot mix specific OS with 'any' or 'none' OS specifiers\")\n\t}\n\n\tswitch {\n\tcase hasAny:\n\t\treturn query, nil\n\tcase hasNone:\n\t\treturn query.Where(\"operating_system_id IS NULL\"), nil\n\t}\n\n\t// we were told to filter by specific OSes but found no matching OSes...\n\tif len(ids) == 0 {\n\t\treturn nil, ErrOSNotPresent\n\t}\n\n\tquery = query.Where(fmt.Sprintf(\"%s.operating_system_id IN ?\", tableName), maps.Keys(ids))\n\n\treturn query, nil\n}\n\n// Keep the original helper methods unchanged\nfunc (s *packageStore) applyPackageAlias(d *PackageSpecifier) error {\n\tif d.Ecosystem == \"\" {\n\t\treturn nil\n\t}\n\n\t// only ecosystem replacement is supported today\n\tvar aliases []PackageSpecifierOverride\n\terr := s.db.Where(\"ecosystem = ? collate nocase\", d.Ecosystem).Find(&aliases).Error\n\tif err != nil {\n\t\tif !errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t\treturn fmt.Errorf(\"failed to resolve alias for distro %q: %w\", d.Name, err)\n\t\t}\n\t\treturn nil\n\t}\n\n\tvar alias *PackageSpecifierOverride\n\n\tfor _, a := range aliases {\n\t\tif a.Ecosystem == \"\" {\n\t\t\tcontinue\n\t\t}\n\n\t\talias = &a\n\t\tbreak\n\t}\n\n\tif alias == nil {\n\t\treturn nil\n\t}\n\n\tif alias.ReplacementEcosystem != nil {\n\t\td.Ecosystem = *alias.ReplacementEcosystem\n\t}\n\n\treturn nil\n}\n\nfunc (s *packageStore) handlePreload(query *gorm.DB, config GetPackageOptions) *gorm.DB {\n\tvar limitArgs []interface{}\n\tif config.Limit > 0 {\n\t\tquery = query.Limit(config.Limit)\n\t\tlimitArgs = append(limitArgs, func(db *gorm.DB) *gorm.DB {\n\t\t\treturn db.Limit(config.Limit)\n\t\t})\n\t}\n\n\tif config.PreloadPackage {\n\t\tquery = query.Preload(\"Package\", limitArgs...)\n\n\t\tif config.PreloadPackageCPEs {\n\t\t\tquery = query.Preload(\"Package.CPEs\", limitArgs...)\n\t\t}\n\t}\n\n\tif config.PreloadVulnerability {\n\t\tquery = query.Preload(\"Vulnerability\", limitArgs...).Preload(\"Vulnerability.Provider\", limitArgs...)\n\t}\n\n\tif config.PreloadOS {\n\t\tquery = query.Preload(\"OperatingSystem\", limitArgs...)\n\t}\n\n\treturn query\n}\n\nfunc handleCPEOptions(query *gorm.DB, c *cpe.Attributes, allowBroad bool) *gorm.DB {\n\tquery = queryCPEAttributeScope(query, c.Part, \"cpes.part\", allowBroad)\n\tquery = queryCPEAttributeScope(query, c.Vendor, \"cpes.vendor\", allowBroad)\n\tquery = queryCPEAttributeScope(query, c.Product, \"cpes.product\", allowBroad)\n\tquery = queryCPEAttributeScope(query, c.Edition, \"cpes.edition\", allowBroad)\n\tquery = queryCPEAttributeScope(query, c.Language, \"cpes.language\", allowBroad)\n\tquery = queryCPEAttributeScope(query, c.SWEdition, \"cpes.software_edition\", allowBroad)\n\tquery = queryCPEAttributeScope(query, c.TargetSW, \"cpes.target_software\", allowBroad)\n\tquery = queryCPEAttributeScope(query, c.TargetHW, \"cpes.target_hardware\", allowBroad)\n\tquery = queryCPEAttributeScope(query, c.Other, \"cpes.other\", allowBroad)\n\treturn query\n}\n\nfunc queryCPEAttributeScope(query *gorm.DB, value string, dbColumn string, allowBroad bool) *gorm.DB {\n\tif value == cpe.Any {\n\t\treturn query\n\t}\n\tif allowBroad {\n\t\t// this allows for a package that specifies a CPE like\n\t\t//\n\t\t//   'cpe:2.3:a:cloudflare:octorpki:1.4.1:*:*:*:*:golang:*:*'\n\t\t//\n\t\t// to be able to positively match with a package CPE that claims to match \"any\" target software.\n\t\t//\n\t\t//   'cpe:2.3:a:cloudflare:octorpki:1.4.1:*:*:*:*:*:*:*'\n\t\t//\n\t\t// practically speaking, how would a vulnerability provider know that the package is vulnerable for all\n\t\t// target software values (against the universe of packaging) -- this isn't practical.\n\t\treturn query.Where(fmt.Sprintf(\"%s = ? collate nocase or %s = ? collate nocase\", dbColumn, dbColumn), value, cpe.Any)\n\t}\n\t// this is the most practical use case, where the package CPE with specified values must match the vulnerability\n\t// CPE exactly (only for specified fields)\n\treturn query.Where(fmt.Sprintf(\"%s = ? collate nocase\", dbColumn), value)\n}\n\nfunc hasOSSpecified(d *OSSpecifier) bool {\n\tif d == AnyOSSpecified {\n\t\treturn false\n\t}\n\n\tif *d == *NoOSSpecified {\n\t\treturn false\n\t}\n\treturn true\n}\n"
  },
  {
    "path": "grype/db/v6/provider_store.go",
    "content": "package v6\n\nimport (\n\t\"fmt\"\n\t\"sort\"\n\n\t\"gorm.io/gorm\"\n\n\t\"github.com/anchore/grype/internal/log\"\n)\n\ntype ProviderStoreReader interface {\n\tGetProvider(name string) (*Provider, error)\n\tAllProviders() ([]Provider, error)\n\tfillProviders(handles []ref[string, Provider]) error\n}\n\ntype ProviderStoreWriter interface {\n\tAddProvider(p Provider) error\n}\n\ntype providerStore struct {\n\tdb *gorm.DB\n}\n\nfunc newProviderStore(db *gorm.DB) *providerStore {\n\treturn &providerStore{\n\t\tdb: db,\n\t}\n}\n\nfunc (s *providerStore) AddProvider(p Provider) error {\n\tresult := s.db.FirstOrCreate(&p)\n\tif result.Error != nil {\n\t\treturn fmt.Errorf(\"failed to create provider record: %w\", result.Error)\n\t}\n\n\treturn nil\n}\n\nfunc (s *providerStore) GetProvider(name string) (*Provider, error) {\n\tlog.WithFields(\"name\", name).Trace(\"fetching provider record\")\n\n\tvar provider Provider\n\tresult := s.db.Where(\"id = ?\", name).First(&provider)\n\tif result.Error != nil {\n\t\treturn nil, fmt.Errorf(\"failed to fetch provider (name=%q): %w\", name, result.Error)\n\t}\n\n\treturn &provider, nil\n}\n\nfunc (s *providerStore) AllProviders() ([]Provider, error) {\n\tlog.Trace(\"fetching all provider records\")\n\n\tvar providers []Provider\n\tresult := s.db.Find(&providers)\n\tif result.Error != nil {\n\t\treturn nil, fmt.Errorf(\"failed to fetch all providers: %w\", result.Error)\n\t}\n\n\tsort.Slice(providers, func(i, j int) bool {\n\t\treturn providers[i].ID < providers[j].ID\n\t})\n\n\treturn providers, nil\n}\n\nfunc (s *providerStore) fillProviders(handles []ref[string, Provider]) error {\n\tproviders, err := s.AllProviders()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tproviderMap := make(map[string]*Provider)\n\tfor _, provider := range providers {\n\t\tproviderMap[provider.ID] = &provider\n\t}\n\n\tfor _, handle := range handles {\n\t\tif handle.id == nil {\n\t\t\tcontinue\n\t\t}\n\t\t*handle.ref = providerMap[*handle.id]\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "grype/db/v6/provider_store_test.go",
    "content": "package v6\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestProviderStore(t *testing.T) {\n\tnow := time.Date(2021, 1, 1, 2, 3, 4, 5, time.UTC)\n\ttests := []struct {\n\t\tname      string\n\t\tproviders []Provider\n\t\twantErr   require.ErrorAssertionFunc\n\t}{\n\t\t{\n\t\t\tname: \"add new provider\",\n\t\t\tproviders: []Provider{\n\t\t\t\t{\n\t\t\t\t\tID:           \"ubuntu\",\n\t\t\t\t\tVersion:      \"1.0\",\n\t\t\t\t\tProcessor:    \"vunnel\",\n\t\t\t\t\tDateCaptured: &now,\n\t\t\t\t\tInputDigest:  \"sha256:abcd1234\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tdb := setupTestStore(t).db\n\t\t\ts := newProviderStore(db)\n\t\t\tif tt.wantErr == nil {\n\t\t\t\ttt.wantErr = require.NoError\n\t\t\t}\n\t\t\tfor i := range tt.providers {\n\t\t\t\tp := tt.providers[i]\n\t\t\t\t// note: we always write providers via the vulnerability handle (there is no store adder)\n\t\t\t\tvuln := VulnerabilityHandle{\n\t\t\t\t\tName:     \"CVE-1234-5678\",\n\t\t\t\t\tProvider: &p,\n\t\t\t\t}\n\t\t\t\tisLast := i == len(tt.providers)-1\n\t\t\t\terr := db.Create(&vuln).Error\n\t\t\t\tif !isLast {\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\ttt.wantErr(t, err)\n\t\t\t\tif err != nil {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tprovider, err := s.GetProvider(p.ID)\n\t\t\t\ttt.wantErr(t, err)\n\t\t\t\tif err != nil {\n\t\t\t\t\tassert.Nil(t, provider)\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, provider)\n\t\t\t\tif d := cmp.Diff(p, *provider); d != \"\" {\n\t\t\t\t\tt.Errorf(\"unexpected provider (-want +got): %s\", d)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestProviderStore_GetProvider(t *testing.T) {\n\ts := newProviderStore(setupTestStore(t).db)\n\tp, err := s.GetProvider(\"fake\")\n\trequire.Error(t, err)\n\tassert.Nil(t, p)\n}\n"
  },
  {
    "path": "grype/db/v6/refs.go",
    "content": "package v6\n\nimport (\n\t\"slices\"\n\n\t\"gorm.io/gorm\"\n)\n\ntype ref[ID, T any] struct {\n\tid  *ID\n\tref **T\n}\n\ntype idRef[T any] ref[ID, T]\n\ntype refProvider[T, R any] func(*T) idRef[R]\n\ntype idProvider[T any] func(*T) ID\n\nfunc fillRefs[T, R any](reader Reader, handles []*T, getRef refProvider[T, R], refID idProvider[R]) error {\n\tif len(handles) == 0 {\n\t\treturn nil\n\t}\n\n\t// collect all ref locations and IDs\n\tvar refs []idRef[R]\n\tvar ids []ID\n\tfor i := range handles {\n\t\tref := getRef(handles[i])\n\t\tif ref.id == nil {\n\t\t\tcontinue\n\t\t}\n\t\trefs = append(refs, ref)\n\t\tid := *ref.id\n\t\tif slices.Contains(ids, id) {\n\t\t\tcontinue\n\t\t}\n\t\tids = append(ids, id)\n\t}\n\n\t// load a map with all id -> ref results\n\tvar values []R\n\ttx := reader.(lowLevelReader).GetDB().Where(\"id IN (?)\", ids)\n\terr := tx.Find(&values).Error\n\tif err != nil {\n\t\treturn err\n\t}\n\trefsByID := map[ID]*R{}\n\tfor i := range values {\n\t\tv := &values[i]\n\t\tid := refID(v)\n\t\trefsByID[id] = v\n\t}\n\n\t// assign matching refs back to the object graph\n\tfor _, ref := range refs {\n\t\tif ref.id == nil {\n\t\t\tcontinue\n\t\t}\n\t\tincomingRef := refsByID[*ref.id]\n\t\t*ref.ref = incomingRef\n\t}\n\n\treturn nil\n}\n\n// ptrs returns a slice of pointers to each element in the provided slice\nfunc ptrs[T any](values []T) []*T {\n\tif len(values) == 0 {\n\t\treturn nil\n\t}\n\tout := make([]*T, len(values))\n\tfor i := range values {\n\t\tout[i] = &values[i]\n\t}\n\treturn out\n}\n\ntype lowLevelReader interface {\n\tGetDB() *gorm.DB\n}\n"
  },
  {
    "path": "grype/db/v6/schema/main.go",
    "content": "package main\n\nimport (\n\t\"bytes\"\n\t\"database/sql\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"go/ast\"\n\t\"io\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"reflect\"\n\t\"sort\"\n\t\"strings\"\n\n\t\"github.com/invopop/jsonschema\"\n\t\"golang.org/x/tools/go/packages\"\n\n\tv6 \"github.com/anchore/grype/grype/db/v6\"\n)\n\nfunc main() {\n\t// The schema version is derived from the database version\n\tversion := fmt.Sprintf(\"%d.%d.%d\", v6.ModelVersion, v6.Revision, v6.Addition)\n\n\tpkgPatterns := []string{\"..\"}\n\tcomments := parseCommentsFromPackages(pkgPatterns)\n\tfmt.Printf(\"Extracted field comments from %d structs\\n\", len(comments))\n\n\t// Generate SQL schema\n\tif err := generateSQLSchema(version); err != nil {\n\t\tfmt.Printf(\"Failed to generate SQL schema: %v\\n\", err)\n\t\tos.Exit(1)\n\t}\n\n\t// Generate unified blob JSON schema\n\terr := generateBlobSchema(version, comments)\n\tif err != nil {\n\t\tfmt.Printf(\"Failed to generate blob JSON schema: %v\\n\", err)\n\t\tos.Exit(1)\n\t}\n}\n\nfunc generateSQLSchema(version string) error {\n\t// Create an in-memory database with all models\n\tdb, err := v6.NewLowLevelDB(\"\", true, true, false)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create database: %w\", err)\n\t}\n\n\tsqlDB, err := db.DB()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get underlying database: %w\", err)\n\t}\n\tdefer sqlDB.Close()\n\n\tvar schema strings.Builder\n\tschema.WriteString(\"-- Generated by grype/db/v6/schema\\n\")\n\tschema.WriteString(\"-- DO NOT EDIT: This file is auto-generated. Run 'task generate-db-schema' to update.\\n\")\n\tfmt.Fprintf(&schema, \"-- Schema version: %s\\n\\n\", version)\n\n\t// Query for all tables and their CREATE statements\n\tcreateStatements, err := querySchemaSorted(sqlDB, \"table\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfor _, stmt := range createStatements {\n\t\t// Normalize the CREATE TABLE statement to ensure deterministic output\n\t\tnormalized := normalizeCreateTable(stmt)\n\t\tschema.WriteString(normalized)\n\t\tschema.WriteString(\";\\n\\n\")\n\t}\n\n\t// Get all indexes\n\tindexStatements, err := querySchemaSorted(sqlDB, \"index\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif len(indexStatements) > 0 {\n\t\tschema.WriteString(\"-- Indexes\\n\")\n\t\tfor _, stmt := range indexStatements {\n\t\t\tschema.WriteString(stmt)\n\t\t\tschema.WriteString(\";\\n\\n\")\n\t\t}\n\t}\n\n\treturn writeFile(schema.String(), \"db/sql\", version, \".sql\")\n}\n\nfunc normalizeCreateTable(stmt string) string {\n\t// Sort CONSTRAINT clauses within CREATE TABLE statements for deterministic output\n\t// Foreign keys can appear in non-deterministic order from SQLite\n\n\t// Find the constraints section\n\tif !strings.Contains(stmt, \"CONSTRAINT\") {\n\t\treturn stmt\n\t}\n\n\t// Split by CONSTRAINT keyword\n\tparts := strings.Split(stmt, \"CONSTRAINT\")\n\tif len(parts) <= 1 {\n\t\treturn stmt\n\t}\n\n\t// First part contains everything before constraints\n\tprefix := parts[0]\n\n\t// Collect all constraints\n\tvar constraints []string\n\tfor i := 1; i < len(parts); i++ {\n\t\tconstraints = append(constraints, \"CONSTRAINT\"+parts[i])\n\t}\n\n\t// Sort constraints\n\tsort.Strings(constraints)\n\n\t// Rebuild: need to handle the last column/field before constraints\n\t// The prefix ends with a comma, and each constraint except the last should have a comma\n\tresult := strings.TrimRight(prefix, \",\")\n\tfor _, constraint := range constraints {\n\t\tresult += \",\"\n\t\t// Remove trailing comma or closing paren from constraint\n\t\tconstraint = strings.TrimRight(constraint, \",)\")\n\t\tresult += constraint\n\t}\n\tresult += \")\"\n\n\treturn result\n}\n\nfunc querySchemaSorted(db *sql.DB, objectType string) ([]string, error) {\n\t// Use a placeholder '?' to prevent SQL injection warnings (gosec G201)\n\tquery := `\n\t\tSELECT sql\n\t\tFROM sqlite_master\n\t\tWHERE type = ?\n\t\tAND name NOT LIKE 'sqlite_%%'\n\t`\n\n\tif objectType == \"index\" {\n\t\tquery += \" AND sql IS NOT NULL\"\n\t}\n\n\tquery += \" ORDER BY name\"\n\n\t// Pass the variable as a parameter to the query function\n\trows, err := db.Query(query, objectType)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to query schema for type %s: %w\", objectType, err)\n\t}\n\tdefer rows.Close()\n\n\tvar statements []string\n\tfor rows.Next() {\n\t\tvar sql string\n\t\tif err := rows.Scan(&sql); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to scan schema: %w\", err)\n\t\t}\n\t\tstatements = append(statements, sql)\n\t}\n\n\tif err := rows.Err(); err != nil {\n\t\treturn nil, fmt.Errorf(\"error iterating schema: %w\", err)\n\t}\n\n\t// Sort for deterministic output\n\tsort.Strings(statements)\n\treturn statements, nil\n}\n\nfunc generateBlobSchema(version string, comments map[string]map[string]string) error {\n\t// Create a unified schema that includes all blob types\n\tschema := buildUnifiedBlobSchema(version, comments)\n\tencoded := encode(schema)\n\treturn writeFile(string(encoded), \"db/blob/json\", version, \".json\")\n}\n\nfunc buildUnifiedBlobSchema(version string, comments map[string]map[string]string) *jsonschema.Schema {\n\treflector := &jsonschema.Reflector{\n\t\tAllowAdditionalProperties: true,\n\t\tNamer: func(r reflect.Type) string {\n\t\t\treturn strings.TrimPrefix(r.Name(), \"JSON\")\n\t\t},\n\t}\n\n\t// Reflect all three blob types to get their definitions\n\tvulnBlobSchema := reflector.ReflectFromType(reflect.TypeOf(v6.VulnerabilityBlob{}))\n\tpackageBlobSchema := reflector.ReflectFromType(reflect.TypeOf(v6.PackageBlob{}))\n\tkevBlobSchema := reflector.ReflectFromType(reflect.TypeOf(v6.KnownExploitedVulnerabilityBlob{}))\n\n\t// Create the unified schema with oneOf\n\tunifiedSchema := &jsonschema.Schema{\n\t\tVersion:     jsonschema.Version,\n\t\tID:          jsonschema.ID(fmt.Sprintf(\"anchore.io/schema/grype/db/blob/json/%s\", version)),\n\t\tDescription: \"Unified schema for all blob types stored in the Grype v6 database\",\n\t\tOneOf: []*jsonschema.Schema{\n\t\t\t{Ref: \"#/$defs/VulnerabilityBlob\"},\n\t\t\t{Ref: \"#/$defs/PackageBlob\"},\n\t\t\t{Ref: \"#/$defs/KnownExploitedVulnerabilityBlob\"},\n\t\t},\n\t\tDefinitions: make(map[string]*jsonschema.Schema),\n\t}\n\n\t// Merge all definitions from the three schemas\n\tmergeDefinitions(unifiedSchema.Definitions, vulnBlobSchema.Definitions)\n\tmergeDefinitions(unifiedSchema.Definitions, packageBlobSchema.Definitions)\n\tmergeDefinitions(unifiedSchema.Definitions, kevBlobSchema.Definitions)\n\n\t// Apply comments to the definitions\n\tapplyComments(unifiedSchema.Definitions, comments)\n\n\treturn unifiedSchema\n}\n\nfunc mergeDefinitions(target, source map[string]*jsonschema.Schema) {\n\tfor k, v := range source {\n\t\ttarget[k] = v\n\t}\n}\n\nfunc applyComments(definitions map[string]*jsonschema.Schema, comments map[string]map[string]string) {\n\tfor structName, fields := range comments {\n\t\tif structSchema, exists := definitions[structName]; exists {\n\t\t\tif structSchema.Definitions == nil {\n\t\t\t\tstructSchema.Definitions = make(map[string]*jsonschema.Schema)\n\t\t\t}\n\t\t\tfor fieldName, comment := range fields {\n\t\t\t\tif fieldName == \"\" {\n\t\t\t\t\t// struct-level comment\n\t\t\t\t\tstructSchema.Description = comment\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\t// field level comment\n\t\t\t\tif comment == \"\" {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif _, exists := structSchema.Properties.Get(fieldName); exists {\n\t\t\t\t\tfieldSchema, exists := structSchema.Definitions[fieldName]\n\t\t\t\t\tif exists {\n\t\t\t\t\t\tfieldSchema.Description = comment\n\t\t\t\t\t} else {\n\t\t\t\t\t\tfieldSchema = &jsonschema.Schema{\n\t\t\t\t\t\t\tDescription: comment,\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tstructSchema.Definitions[fieldName] = fieldSchema\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc encode(schema *jsonschema.Schema) []byte {\n\tnewSchemaBuffer := new(bytes.Buffer)\n\tenc := json.NewEncoder(newSchemaBuffer)\n\t// prevent > and < from being escaped in the payload\n\tenc.SetEscapeHTML(false)\n\tenc.SetIndent(\"\", \"  \")\n\terr := enc.Encode(&schema)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn newSchemaBuffer.Bytes()\n}\n\nfunc writeFile(content, component, version, extension string) error {\n\tparent := filepath.Join(repoRoot(), \"schema\", \"grype\", component)\n\tschemaPath := filepath.Join(parent, fmt.Sprintf(\"schema-%s%s\", version, extension))\n\tlatestSchemaPath := filepath.Join(parent, fmt.Sprintf(\"schema-latest%s\", extension))\n\n\t// Create parent directory if it doesn't exist\n\tif err := os.MkdirAll(parent, 0o755); err != nil {\n\t\treturn fmt.Errorf(\"unable to create schema directory: %w\", err)\n\t}\n\n\tif _, err := os.Stat(schemaPath); !os.IsNotExist(err) {\n\t\t// check if the schema is the same...\n\t\texistingFh, err := os.Open(schemaPath)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tdefer existingFh.Close()\n\n\t\texistingBytes, err := io.ReadAll(existingFh)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif string(existingBytes) == content {\n\t\t\t// the generated schema is the same, bail with no error :)\n\t\t\tfmt.Printf(\"No change to the existing %q schema!\\n\", component)\n\t\t\treturn nil\n\t\t}\n\n\t\t// the generated schema is different, bail with error :(\n\t\tfmt.Printf(\"Cowardly refusing to overwrite existing %q schema (%s)!\\n\", component, schemaPath)\n\t\tfmt.Printf(\"The schema has changed but the version has not been incremented.\\n\")\n\t\tfmt.Printf(\"See grype/db/v6/db.go to increment the ModelVersion, Revision, or Addition constants.\\n\")\n\t\treturn fmt.Errorf(\"refusing to overwrite existing schema\")\n\t}\n\n\tfh, err := os.Create(schemaPath)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer fh.Close()\n\n\tif _, err = fh.WriteString(content); err != nil {\n\t\treturn err\n\t}\n\n\tlatestFile, err := os.Create(latestSchemaPath)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer latestFile.Close()\n\n\tif _, err = latestFile.WriteString(content); err != nil {\n\t\treturn err\n\t}\n\n\tfmt.Printf(\"Wrote new %q schema to %q\\n\", component, schemaPath)\n\treturn nil\n}\n\n// parseCommentsFromPackages scans multiple packages and collects field comments for structs.\nfunc parseCommentsFromPackages(pkgPatterns []string) map[string]map[string]string {\n\tcommentMap := make(map[string]map[string]string)\n\n\tcfg := &packages.Config{\n\t\tMode: packages.NeedFiles | packages.NeedSyntax | packages.NeedDeps | packages.NeedImports,\n\t}\n\tpkgs, err := packages.Load(cfg, pkgPatterns...)\n\tif err != nil {\n\t\tpanic(fmt.Errorf(\"failed to load packages: %w\", err))\n\t}\n\n\tfor _, pkg := range pkgs {\n\t\tfor _, file := range pkg.Syntax {\n\t\t\tfileComments := parseFileComments(file)\n\t\t\tfor structName, fields := range fileComments {\n\t\t\t\tif _, exists := commentMap[structName]; !exists {\n\t\t\t\t\tcommentMap[structName] = fields\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn commentMap\n}\n\n// parseFileComments extracts comments for structs and their fields in a single file.\nfunc parseFileComments(node *ast.File) map[string]map[string]string {\n\tcommentMap := make(map[string]map[string]string)\n\n\tast.Inspect(node, func(n ast.Node) bool {\n\t\tts, ok := n.(*ast.TypeSpec)\n\t\tif !ok {\n\t\t\treturn true\n\t\t}\n\t\tst, ok := ts.Type.(*ast.StructType)\n\t\tif !ok {\n\t\t\treturn true\n\t\t}\n\n\t\tstructName := ts.Name.Name\n\t\tfieldComments := make(map[string]string)\n\n\t\t// extract struct-level comment\n\t\tif ts.Doc != nil {\n\t\t\tstructComment := strings.TrimSpace(ts.Doc.Text())\n\t\t\tif !strings.Contains(structComment, \"TODO:\") {\n\t\t\t\tfieldComments[\"\"] = cleanComment(structComment)\n\t\t\t}\n\t\t}\n\n\t\t// extract field-level comments\n\t\tfor _, field := range st.Fields.List {\n\t\t\tif len(field.Names) == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tfieldName := field.Names[0].Name\n\t\t\tjsonTag := getJSONTag(field)\n\n\t\t\tif field.Doc != nil {\n\t\t\t\tcomment := strings.TrimSpace(field.Doc.Text())\n\t\t\t\tif strings.Contains(comment, \"TODO:\") {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif jsonTag != \"\" {\n\t\t\t\t\tfieldComments[jsonTag] = cleanComment(comment)\n\t\t\t\t} else {\n\t\t\t\t\tfieldComments[fieldName] = cleanComment(comment)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif len(fieldComments) > 0 {\n\t\t\tcommentMap[structName] = fieldComments\n\t\t}\n\t\treturn true\n\t})\n\n\treturn commentMap\n}\n\nfunc cleanComment(comment string) string {\n\t// remove the first word, since that is the field name (if following go-doc patterns)\n\tsplit := strings.SplitN(comment, \" \", 2)\n\tif len(split) > 1 {\n\t\tcomment = split[1]\n\t}\n\n\treturn strings.TrimSpace(strings.ReplaceAll(comment, \"\\\"\", \"'\"))\n}\n\nfunc getJSONTag(field *ast.Field) string {\n\tif field.Tag != nil {\n\t\ttagValue := strings.Trim(field.Tag.Value, \"`\")\n\t\tstructTag := reflect.StructTag(tagValue)\n\t\tif jsonTag, ok := structTag.Lookup(\"json\"); ok {\n\t\t\tjsonParts := strings.Split(jsonTag, \",\")\n\t\t\treturn strings.TrimSpace(jsonParts[0])\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc repoRoot() string {\n\troot, err := exec.Command(\"git\", \"rev-parse\", \"--show-toplevel\").Output()\n\tif err != nil {\n\t\tpanic(fmt.Errorf(\"unable to find repo root dir: %+v\", err))\n\t}\n\tabsRepoRoot, err := filepath.Abs(strings.TrimSpace(string(root)))\n\tif err != nil {\n\t\tpanic(fmt.Errorf(\"unable to get abs path to repo root: %w\", err))\n\t}\n\treturn absRepoRoot\n}\n"
  },
  {
    "path": "grype/db/v6/search_query.go",
    "content": "package v6\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/anchore/grype/grype/db/v6/name\"\n\t\"github.com/anchore/grype/grype/search\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n\t\"github.com/anchore/syft/syft/cpe\"\n\tsyftPkg \"github.com/anchore/syft/syft/pkg\"\n)\n\n// searchQuery holds the parsed criteria and search parameters\ntype searchQuery struct {\n\tpkgSpec        *PackageSpecifier\n\tcpeSpec        *cpe.Attributes\n\tosSpecs        OSSpecifiers\n\tvulnSpecs      VulnerabilitySpecifiers\n\tpkgType        syftPkg.Type\n\tversionMatcher search.VersionConstraintMatcher\n\tunaffectedOnly bool\n}\n\nfunc newSearchQuery(criteriaSet []vulnerability.Criteria) (*searchQuery, []vulnerability.Criteria, error) {\n\tbuilder := newSearchQueryBuilder()\n\n\tif err := builder.ApplyCriteria(criteriaSet); err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\treturn builder.Build()\n}\n\n// searchQueryBuilder provides a structured way to build searchQuery objects\n// from vulnerability criteria, replacing the large switch statement with focused handler methods.\ntype searchQueryBuilder struct {\n\tquery             *searchQuery\n\tremainingCriteria []vulnerability.Criteria\n}\n\n// newSearchQueryBuilder creates a new searchQueryBuilder with an empty query\nfunc newSearchQueryBuilder() *searchQueryBuilder {\n\treturn &searchQueryBuilder{\n\t\tquery:             &searchQuery{},\n\t\tremainingCriteria: make([]vulnerability.Criteria, 0),\n\t}\n}\n\n// ApplyCriteria processes all criteria using type-switch dispatch to individual handlers\nfunc (b *searchQueryBuilder) ApplyCriteria(criteriaSet []vulnerability.Criteria) error {\n\tfor _, c := range criteriaSet {\n\t\tapplied := false\n\n\t\tswitch c := c.(type) {\n\t\tcase *search.PackageNameCriteria:\n\t\t\tb.handlePackageName(c)\n\t\t\tapplied = true\n\t\tcase *search.UnaffectedCriteria:\n\t\t\tb.handleUnaffected(c)\n\t\t\tapplied = true\n\t\tcase *search.EcosystemCriteria:\n\t\t\tb.handleEcosystem(c)\n\t\t\tapplied = true\n\t\tcase *search.IDCriteria:\n\t\t\tb.handleID(c)\n\t\t\tapplied = true\n\t\tcase *search.CPECriteria:\n\t\t\tif err := b.handleCPE(c); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tapplied = true\n\t\tcase *search.DistroCriteria:\n\t\t\tb.handleDistro(c)\n\t\t\tapplied = true\n\t\t}\n\n\t\tif !applied {\n\t\t\tb.remainingCriteria = append(b.remainingCriteria, c)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (b *searchQueryBuilder) handlePackageName(c *search.PackageNameCriteria) {\n\tif b.query.pkgSpec == nil {\n\t\tb.query.pkgSpec = &PackageSpecifier{}\n\t}\n\tb.query.pkgSpec.Name = c.PackageName\n}\n\nfunc (b *searchQueryBuilder) handleUnaffected(_ *search.UnaffectedCriteria) {\n\tb.query.unaffectedOnly = true\n}\n\nfunc (b *searchQueryBuilder) handleEcosystem(c *search.EcosystemCriteria) {\n\tif b.query.pkgSpec == nil {\n\t\tb.query.pkgSpec = &PackageSpecifier{}\n\t}\n\n\t// the v6 store normalizes ecosystems around the syft package type, so that field is preferred\n\tswitch {\n\tcase c.PackageType != \"\" && c.PackageType != syftPkg.UnknownPkg:\n\t\t// prefer to match by a non-blank, known package type\n\t\tb.query.pkgType = c.PackageType\n\t\tb.query.pkgSpec.Ecosystem = string(c.PackageType)\n\tcase c.Language != \"\":\n\t\t// if there's no known package type, but there is a non-blank language try that\n\t\tb.query.pkgSpec.Ecosystem = string(c.Language)\n\tcase c.PackageType == syftPkg.UnknownPkg:\n\t\t// if language is blank, and package type is explicitly \"UnknownPkg\" and not just blank, use that\n\t\tb.query.pkgType = c.PackageType\n\t\tb.query.pkgSpec.Ecosystem = string(c.PackageType)\n\t}\n}\n\nfunc (b *searchQueryBuilder) handleID(c *search.IDCriteria) {\n\tb.query.vulnSpecs = append(b.query.vulnSpecs, VulnerabilitySpecifier{\n\t\tName: c.ID,\n\t})\n}\n\nfunc (b *searchQueryBuilder) handleCPE(c *search.CPECriteria) error {\n\tif b.query.cpeSpec == nil {\n\t\tb.query.cpeSpec = &cpe.Attributes{}\n\t}\n\t*b.query.cpeSpec = c.CPE.Attributes\n\n\tif b.query.cpeSpec.Product == cpe.Any {\n\t\treturn fmt.Errorf(\"must specify product to search by CPE; got: %s\", c.CPE.Attributes.BindToFmtString())\n\t}\n\n\tif b.query.pkgSpec == nil {\n\t\tb.query.pkgSpec = &PackageSpecifier{}\n\t}\n\tb.query.pkgSpec.CPE = &c.CPE.Attributes\n\n\treturn nil\n}\n\nfunc (b *searchQueryBuilder) handleDistro(c *search.DistroCriteria) {\n\tfor _, d := range c.Distros {\n\t\tvar foundChannels int\n\t\tfor _, channel := range d.Channels {\n\t\t\tif channel == \"\" {\n\t\t\t\t// if the channel is empty, we should not add it to the OS specifier\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tfoundChannels++\n\t\t\tb.query.osSpecs = append(b.query.osSpecs, &OSSpecifier{\n\t\t\t\tName:             d.Name(),\n\t\t\t\tMajorVersion:     d.MajorVersion(),\n\t\t\t\tMinorVersion:     d.MinorVersion(),\n\t\t\t\tRemainingVersion: d.RemainingVersion(),\n\t\t\t\tLabelVersion:     d.LabelVersion(),\n\t\t\t\tChannel:          channel,\n\t\t\t\tDisableAliasing:  c.Exact,\n\t\t\t})\n\t\t}\n\t\tif foundChannels == 0 {\n\t\t\tb.query.osSpecs = append(b.query.osSpecs, &OSSpecifier{\n\t\t\t\tName:             d.Name(),\n\t\t\t\tMajorVersion:     d.MajorVersion(),\n\t\t\t\tMinorVersion:     d.MinorVersion(),\n\t\t\t\tRemainingVersion: d.RemainingVersion(),\n\t\t\t\tLabelVersion:     d.LabelVersion(),\n\t\t\t\tDisableAliasing:  c.Exact,\n\t\t\t})\n\t\t}\n\t}\n}\n\n// setDefaultOS sets default OS if none specified\nfunc (b *searchQueryBuilder) setDefaultOS() {\n\tif len(b.query.osSpecs) == 0 {\n\t\t// we don't want to search across all distros, instead if the user did not specify a distro we should assume that\n\t\t// they want to search across affected packages not associated with any distro.\n\t\tb.query.osSpecs = append(b.query.osSpecs, NoOSSpecified)\n\t}\n}\n\n// normalizePackageName normalizes package name if needed\nfunc (b *searchQueryBuilder) normalizePackageName() {\n\tif b.query.pkgType != \"\" && b.query.pkgSpec != nil && b.query.pkgSpec.Name != \"\" {\n\t\tb.query.pkgSpec.Name = name.Normalize(b.query.pkgSpec.Name, b.query.pkgType)\n\t}\n}\n\n// extractVersionMatcher extracts version constraints from remaining criteria\nfunc (b *searchQueryBuilder) extractVersionMatcher() {\n\tvar remaining []vulnerability.Criteria\n\tvar matcher search.VersionConstraintMatcher\n\n\tfor _, c := range b.remainingCriteria {\n\t\tif nextMatcher, ok := c.(search.VersionConstraintMatcher); ok {\n\t\t\tif matcher == nil {\n\t\t\t\tmatcher = nextMatcher\n\t\t\t} else {\n\t\t\t\tmatcher = search.MultiConstraintMatcher(matcher, nextMatcher)\n\t\t\t}\n\t\t} else {\n\t\t\tremaining = append(remaining, c)\n\t\t}\n\t}\n\n\tb.query.versionMatcher = matcher\n\tb.remainingCriteria = remaining\n}\n\n// Build returns the final query and remaining criteria\nfunc (b *searchQueryBuilder) Build() (*searchQuery, []vulnerability.Criteria, error) {\n\tb.setDefaultOS()\n\tb.normalizePackageName()\n\tb.extractVersionMatcher()\n\n\treturn b.query, b.remainingCriteria, nil\n}\n"
  },
  {
    "path": "grype/db/v6/search_query_test.go",
    "content": "package v6\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/grype/grype/distro\"\n\t\"github.com/anchore/grype/grype/search\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n\t\"github.com/anchore/syft/syft/cpe\"\n\tsyftPkg \"github.com/anchore/syft/syft/pkg\"\n)\n\nfunc TestNewSearchCriteria(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tcriteria []vulnerability.Criteria\n\t\tvalidate func(t *testing.T, input *searchQuery)\n\t}{\n\t\t{\n\t\t\tname: \"package name criteria sets correct fields\",\n\t\t\tcriteria: []vulnerability.Criteria{\n\t\t\t\tsearch.ByPackageName(\"test-package\"),\n\t\t\t},\n\t\t\tvalidate: func(t *testing.T, input *searchQuery) {\n\t\t\t\trequire.NotNil(t, input.pkgSpec)\n\t\t\t\trequire.Equal(t, \"test-package\", input.pkgSpec.Name)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"unaffected criteria sets flag\",\n\t\t\tcriteria: []vulnerability.Criteria{\n\t\t\t\tsearch.ForUnaffected(),\n\t\t\t},\n\t\t\tvalidate: func(t *testing.T, input *searchQuery) {\n\t\t\t\trequire.True(t, input.unaffectedOnly)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"ecosystem criteria sets package type and ecosystem\",\n\t\t\tcriteria: []vulnerability.Criteria{\n\t\t\t\tsearch.ByEcosystem(syftPkg.Java, syftPkg.JavaPkg),\n\t\t\t},\n\t\t\tvalidate: func(t *testing.T, input *searchQuery) {\n\t\t\t\trequire.NotNil(t, input.pkgSpec)\n\t\t\t\trequire.Equal(t, syftPkg.JavaPkg, input.pkgType)\n\t\t\t\trequire.Equal(t, \"java-archive\", input.pkgSpec.Ecosystem)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"ID criteria adds vulnerability spec\",\n\t\t\tcriteria: []vulnerability.Criteria{\n\t\t\t\tsearch.ByID(\"CVE-2021-1234\"),\n\t\t\t},\n\t\t\tvalidate: func(t *testing.T, input *searchQuery) {\n\t\t\t\trequire.Len(t, input.vulnSpecs, 1)\n\t\t\t\trequire.Equal(t, \"CVE-2021-1234\", input.vulnSpecs[0].Name)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"distro criteria sets OS specs\",\n\t\t\tcriteria: []vulnerability.Criteria{\n\t\t\t\tsearch.ByDistro(*distro.New(distro.Ubuntu, \"20.04\", \"\")),\n\t\t\t},\n\t\t\tvalidate: func(t *testing.T, input *searchQuery) {\n\t\t\t\trequire.Len(t, input.osSpecs, 1)\n\t\t\t\trequire.Equal(t, \"ubuntu\", input.osSpecs[0].Name)\n\t\t\t\trequire.Equal(t, \"20\", input.osSpecs[0].MajorVersion)\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tquery, _, err := newSearchQuery(test.criteria)\n\t\t\trequire.NoError(t, err)\n\t\t\ttest.validate(t, query)\n\t\t})\n\t}\n}\n\nfunc TestQueryBuilder_ApplyCriteria(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tcriteria []vulnerability.Criteria\n\t\tvalidate func(t *testing.T, builder *searchQueryBuilder)\n\t}{\n\t\t{\n\t\t\tname: \"package name criteria\",\n\t\t\tcriteria: []vulnerability.Criteria{\n\t\t\t\tsearch.ByPackageName(\"test-package\"),\n\t\t\t},\n\t\t\tvalidate: func(t *testing.T, builder *searchQueryBuilder) {\n\t\t\t\trequire.NotNil(t, builder.query.pkgSpec)\n\t\t\t\trequire.Equal(t, \"test-package\", builder.query.pkgSpec.Name)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"unaffected criteria\",\n\t\t\tcriteria: []vulnerability.Criteria{\n\t\t\t\tsearch.ForUnaffected(),\n\t\t\t},\n\t\t\tvalidate: func(t *testing.T, builder *searchQueryBuilder) {\n\t\t\t\trequire.True(t, builder.query.unaffectedOnly)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"ecosystem criteria with package type\",\n\t\t\tcriteria: []vulnerability.Criteria{\n\t\t\t\tsearch.ByEcosystem(syftPkg.Java, syftPkg.JavaPkg),\n\t\t\t},\n\t\t\tvalidate: func(t *testing.T, builder *searchQueryBuilder) {\n\t\t\t\trequire.NotNil(t, builder.query.pkgSpec)\n\t\t\t\trequire.Equal(t, syftPkg.JavaPkg, builder.query.pkgType)\n\t\t\t\trequire.Equal(t, \"java-archive\", builder.query.pkgSpec.Ecosystem)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"ID criteria\",\n\t\t\tcriteria: []vulnerability.Criteria{\n\t\t\t\tsearch.ByID(\"CVE-2021-1234\"),\n\t\t\t},\n\t\t\tvalidate: func(t *testing.T, builder *searchQueryBuilder) {\n\t\t\t\trequire.Len(t, builder.query.vulnSpecs, 1)\n\t\t\t\trequire.Equal(t, \"CVE-2021-1234\", builder.query.vulnSpecs[0].Name)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"CPE criteria\",\n\t\t\tcriteria: []vulnerability.Criteria{\n\t\t\t\tsearch.ByCPE(cpe.Must(\"cpe:2.3:a:apache:tomcat:9.0.0:*:*:*:*:*:*:*\", \"\")),\n\t\t\t},\n\t\t\tvalidate: func(t *testing.T, builder *searchQueryBuilder) {\n\t\t\t\trequire.NotNil(t, builder.query.cpeSpec)\n\t\t\t\trequire.Equal(t, \"apache\", builder.query.cpeSpec.Vendor)\n\t\t\t\trequire.Equal(t, \"tomcat\", builder.query.cpeSpec.Product)\n\t\t\t\trequire.NotNil(t, builder.query.pkgSpec)\n\t\t\t\trequire.NotNil(t, builder.query.pkgSpec.CPE)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"distro criteria\",\n\t\t\tcriteria: []vulnerability.Criteria{\n\t\t\t\tsearch.ByDistro(*distro.New(distro.Ubuntu, \"20.04\", \"\")),\n\t\t\t},\n\t\t\tvalidate: func(t *testing.T, builder *searchQueryBuilder) {\n\t\t\t\trequire.Len(t, builder.query.osSpecs, 1)\n\t\t\t\trequire.Equal(t, \"ubuntu\", builder.query.osSpecs[0].Name)\n\t\t\t\trequire.Equal(t, \"20\", builder.query.osSpecs[0].MajorVersion)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"multiple criteria\",\n\t\t\tcriteria: []vulnerability.Criteria{\n\t\t\t\tsearch.ByPackageName(\"test-package\"),\n\t\t\t\tsearch.ForUnaffected(),\n\t\t\t\tsearch.ByID(\"CVE-2021-1234\"),\n\t\t\t},\n\t\t\tvalidate: func(t *testing.T, builder *searchQueryBuilder) {\n\t\t\t\trequire.NotNil(t, builder.query.pkgSpec)\n\t\t\t\trequire.Equal(t, \"test-package\", builder.query.pkgSpec.Name)\n\t\t\t\trequire.True(t, builder.query.unaffectedOnly)\n\t\t\t\trequire.Len(t, builder.query.vulnSpecs, 1)\n\t\t\t\trequire.Equal(t, \"CVE-2021-1234\", builder.query.vulnSpecs[0].Name)\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tbuilder := newSearchQueryBuilder()\n\t\t\terr := builder.ApplyCriteria(tt.criteria)\n\t\t\trequire.NoError(t, err)\n\t\t\ttt.validate(t, builder)\n\t\t})\n\t}\n}\n\nfunc TestQueryBuilder_CPEErrorHandling(t *testing.T) {\n\tbuilder := newSearchQueryBuilder()\n\n\t// create a CPE without a product (which should cause an error)\n\tinvalidCPE := cpe.CPE{\n\t\tAttributes: cpe.Attributes{\n\t\t\tPart:   \"a\",\n\t\t\tVendor: \"vendor\",\n\t\t\t// no product specified\n\t\t},\n\t}\n\tcriteria := []vulnerability.Criteria{\n\t\tsearch.ByCPE(invalidCPE),\n\t}\n\n\terr := builder.ApplyCriteria(criteria)\n\trequire.Error(t, err)\n\trequire.Contains(t, err.Error(), \"must specify product to search by CPE\")\n}\n\nfunc TestQueryBuilder_PostProcess(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tsetup    func(*searchQueryBuilder)\n\t\tvalidate func(t *testing.T, builder *searchQueryBuilder)\n\t}{\n\t\t{\n\t\t\tname: \"sets default OS when none specified\",\n\t\t\tsetup: func(builder *searchQueryBuilder) {\n\t\t\t\t// no OS specs set\n\t\t\t},\n\t\t\tvalidate: func(t *testing.T, builder *searchQueryBuilder) {\n\t\t\t\trequire.Len(t, builder.query.osSpecs, 1)\n\t\t\t\trequire.Equal(t, NoOSSpecified, builder.query.osSpecs[0])\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"does not override existing OS specs\",\n\t\t\tsetup: func(builder *searchQueryBuilder) {\n\t\t\t\tbuilder.query.osSpecs = append(builder.query.osSpecs, &OSSpecifier{\n\t\t\t\t\tName:         \"ubuntu\",\n\t\t\t\t\tMajorVersion: \"20\",\n\t\t\t\t})\n\t\t\t},\n\t\t\tvalidate: func(t *testing.T, builder *searchQueryBuilder) {\n\t\t\t\trequire.Len(t, builder.query.osSpecs, 1)\n\t\t\t\trequire.Equal(t, \"ubuntu\", builder.query.osSpecs[0].Name)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"normalizes package name when pkgType and pkgSpec are set\",\n\t\t\tsetup: func(builder *searchQueryBuilder) {\n\t\t\t\tbuilder.query.pkgType = syftPkg.GemPkg\n\t\t\t\tbuilder.query.pkgSpec = &PackageSpecifier{\n\t\t\t\t\tName: \"Test_Package\",\n\t\t\t\t}\n\t\t\t},\n\t\t\tvalidate: func(t *testing.T, builder *searchQueryBuilder) {\n\t\t\t\t// verify that normalization was attempted (actual result may vary by package type)\n\t\t\t\trequire.NotEmpty(t, builder.query.pkgSpec.Name)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"preserves remaining criteria that aren't processed\",\n\t\t\tsetup: func(builder *searchQueryBuilder) {\n\t\t\t\t// add some criteria that should remain\n\t\t\t\tbuilder.remainingCriteria = []vulnerability.Criteria{\n\t\t\t\t\tsearch.ByFunc(func(vulnerability.Vulnerability) (bool, string, error) {\n\t\t\t\t\t\treturn true, \"\", nil\n\t\t\t\t\t}),\n\t\t\t\t}\n\t\t\t},\n\t\t\tvalidate: func(t *testing.T, builder *searchQueryBuilder) {\n\t\t\t\trequire.Len(t, builder.remainingCriteria, 1, \"func criteria should remain unprocessed\")\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tbuilder := newSearchQueryBuilder()\n\t\t\ttt.setup(builder)\n\n\t\t\t_, _, err := builder.Build()\n\t\t\trequire.NoError(t, err)\n\n\t\t\ttt.validate(t, builder)\n\t\t})\n\t}\n}\n\nfunc TestQueryBuilder_Build(t *testing.T) {\n\tbuilder := newSearchQueryBuilder()\n\n\t// add some test data\n\tbuilder.query.unaffectedOnly = true\n\tbuilder.remainingCriteria = []vulnerability.Criteria{\n\t\tsearch.ByPackageName(\"some-remaining-criteria\"),\n\t}\n\n\tquery, remaining, err := builder.Build()\n\trequire.NoError(t, err)\n\trequire.NotNil(t, query)\n\trequire.True(t, query.unaffectedOnly)\n\trequire.Len(t, remaining, 1)\n}\n\nfunc TestQueryBuilder_ExactDistroCriteria(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tcriteria []vulnerability.Criteria\n\t\tvalidate func(t *testing.T, query *searchQuery, remaining []vulnerability.Criteria)\n\t}{\n\t\t{\n\t\t\tname: \"exact distro criteria should be handled with DisableAliasing set\",\n\t\t\tcriteria: []vulnerability.Criteria{\n\t\t\t\tsearch.ByExactDistro(*distro.New(distro.AlmaLinux, \"8\", \"\")),\n\t\t\t},\n\t\t\tvalidate: func(t *testing.T, query *searchQuery, remaining []vulnerability.Criteria) {\n\t\t\t\trequire.Len(t, query.osSpecs, 1)\n\t\t\t\trequire.Equal(t, \"almalinux\", query.osSpecs[0].Name)\n\t\t\t\trequire.Equal(t, \"8\", query.osSpecs[0].MajorVersion)\n\t\t\t\trequire.True(t, query.osSpecs[0].DisableAliasing, \"ExactDistroCriteria should set DisableAliasing=true\")\n\t\t\t\trequire.Empty(t, remaining, \"ExactDistroCriteria should be handled, not left in remaining\")\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"exact distro criteria should not be left in remaining criteria\",\n\t\t\tcriteria: []vulnerability.Criteria{\n\t\t\t\tsearch.ByPackageName(\"mariadb\"),\n\t\t\t\tsearch.ByExactDistro(*distro.New(distro.AlmaLinux, \"8\", \"\")),\n\t\t\t\tsearch.ForUnaffected(),\n\t\t\t},\n\t\t\tvalidate: func(t *testing.T, query *searchQuery, remaining []vulnerability.Criteria) {\n\t\t\t\trequire.NotNil(t, query.pkgSpec)\n\t\t\t\trequire.Equal(t, \"mariadb\", query.pkgSpec.Name)\n\t\t\t\trequire.Len(t, query.osSpecs, 1)\n\t\t\t\trequire.Equal(t, \"almalinux\", query.osSpecs[0].Name)\n\t\t\t\trequire.True(t, query.osSpecs[0].DisableAliasing, \"ExactDistroCriteria should set DisableAliasing=true\")\n\t\t\t\trequire.True(t, query.unaffectedOnly)\n\t\t\t\trequire.Empty(t, remaining, \"ExactDistroCriteria should be handled, not left in remaining\")\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"regular distro criteria should not set DisableAliasing\",\n\t\t\tcriteria: []vulnerability.Criteria{\n\t\t\t\tsearch.ByDistro(*distro.New(distro.AlmaLinux, \"8\", \"\")),\n\t\t\t},\n\t\t\tvalidate: func(t *testing.T, query *searchQuery, remaining []vulnerability.Criteria) {\n\t\t\t\trequire.Len(t, query.osSpecs, 1)\n\t\t\t\trequire.Equal(t, \"almalinux\", query.osSpecs[0].Name)\n\t\t\t\trequire.Equal(t, \"8\", query.osSpecs[0].MajorVersion)\n\t\t\t\trequire.False(t, query.osSpecs[0].DisableAliasing, \"Regular DistroCriteria should keep DisableAliasing=false\")\n\t\t\t\trequire.Empty(t, remaining)\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tquery, remaining, err := newSearchQuery(test.criteria)\n\t\t\trequire.NoError(t, err)\n\t\t\ttest.validate(t, query, remaining)\n\t\t})\n\t}\n}\n\nfunc TestQueryBuilder_IntegrationWithRealCriteria(t *testing.T) {\n\t// test the full flow that mimics parseCriteria behavior\n\tcriteria := []vulnerability.Criteria{\n\t\tsearch.ByPackageName(\"log4j\"),\n\t\tsearch.ByEcosystem(syftPkg.Java, syftPkg.JavaPkg),\n\t\tsearch.ByDistro(*distro.New(distro.Ubuntu, \"20.04\", \"\")),\n\t\tsearch.ByID(\"CVE-2021-44228\"),\n\t\tsearch.ForUnaffected(),\n\t\tsearch.ByFunc(func(vulnerability.Vulnerability) (bool, string, error) {\n\t\t\treturn true, \"\", nil\n\t\t}),\n\t}\n\n\tbuilder := newSearchQueryBuilder()\n\n\terr := builder.ApplyCriteria(criteria)\n\trequire.NoError(t, err)\n\n\tquery, remaining, err := builder.Build()\n\trequire.NoError(t, err)\n\n\t// validate the built query\n\trequire.NotNil(t, query.pkgSpec)\n\trequire.Equal(t, \"log4j\", query.pkgSpec.Name)\n\trequire.Equal(t, syftPkg.JavaPkg, query.pkgType)\n\trequire.Len(t, query.osSpecs, 1)\n\trequire.Equal(t, \"ubuntu\", query.osSpecs[0].Name)\n\trequire.Len(t, query.vulnSpecs, 1)\n\trequire.Equal(t, \"CVE-2021-44228\", query.vulnSpecs[0].Name)\n\trequire.True(t, query.unaffectedOnly)\n\n\t// func criteria should remain unprocessed\n\trequire.Len(t, remaining, 1)\n}\n"
  },
  {
    "path": "grype/db/v6/severity.go",
    "content": "package v6\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/anchore/grype/grype/vulnerability\"\n\t\"github.com/anchore/grype/internal/cvss\"\n\t\"github.com/anchore/grype/internal/log\"\n)\n\nfunc extractSeverities(vuln *VulnerabilityHandle) (vulnerability.Severity, []vulnerability.Cvss, error) {\n\tif vuln.BlobValue == nil {\n\t\treturn vulnerability.UnknownSeverity, nil, nil\n\t}\n\tsev := vulnerability.UnknownSeverity\n\tif len(vuln.BlobValue.Severities) > 0 {\n\t\tvar err error\n\t\t// grype DB v6+ will order the set of severities by rank, so we can just take the first one\n\t\tsev, err = extractSeverity(vuln.BlobValue.Severities[0].Value)\n\t\tif err != nil {\n\t\t\treturn vulnerability.UnknownSeverity, nil, fmt.Errorf(\"unable to extract severity: %w\", err)\n\t\t}\n\t}\n\treturn sev, toCvss(vuln.BlobValue.Severities...), nil\n}\n\nfunc extractSeverity(severity any) (vulnerability.Severity, error) {\n\tswitch sev := severity.(type) {\n\tcase string:\n\t\treturn vulnerability.ParseSeverity(sev), nil\n\tcase CVSSSeverity:\n\t\tmetrics, err := cvss.ParseMetricsFromVector(sev.Vector)\n\t\tif err != nil {\n\t\t\treturn vulnerability.UnknownSeverity, fmt.Errorf(\"unable to parse CVSS vector: %w\", err)\n\t\t}\n\t\tif metrics == nil {\n\t\t\treturn vulnerability.UnknownSeverity, nil\n\t\t}\n\t\treturn interpretCVSS(metrics.BaseScore, sev.Version), nil\n\tdefault:\n\t\treturn vulnerability.UnknownSeverity, nil\n\t}\n}\n\nfunc interpretCVSS(score float64, version string) vulnerability.Severity {\n\tswitch version {\n\tcase \"2.0\":\n\t\treturn interpretCVSSv2(score)\n\tcase \"3.0\", \"3.1\", \"4.0\":\n\t\treturn interpretCVSSv3Plus(score)\n\tdefault:\n\t\treturn vulnerability.UnknownSeverity\n\t}\n}\n\nfunc interpretCVSSv2(score float64) vulnerability.Severity {\n\tif score < 0 {\n\t\treturn vulnerability.UnknownSeverity\n\t}\n\tif score == 0 {\n\t\treturn vulnerability.NegligibleSeverity\n\t}\n\tif score < 4.0 {\n\t\treturn vulnerability.LowSeverity\n\t}\n\tif score < 7.0 {\n\t\treturn vulnerability.MediumSeverity\n\t}\n\tif score <= 10.0 {\n\t\treturn vulnerability.HighSeverity\n\t}\n\treturn vulnerability.UnknownSeverity\n}\n\nfunc interpretCVSSv3Plus(score float64) vulnerability.Severity {\n\tif score < 0 {\n\t\treturn vulnerability.UnknownSeverity\n\t}\n\tif score == 0 {\n\t\treturn vulnerability.NegligibleSeverity\n\t}\n\tif score < 4.0 {\n\t\treturn vulnerability.LowSeverity\n\t}\n\tif score < 7.0 {\n\t\treturn vulnerability.MediumSeverity\n\t}\n\tif score < 9.0 {\n\t\treturn vulnerability.HighSeverity\n\t}\n\tif score <= 10.0 {\n\t\treturn vulnerability.CriticalSeverity\n\t}\n\treturn vulnerability.UnknownSeverity\n}\n\nfunc toCvss(severities ...Severity) []vulnerability.Cvss {\n\t//nolint:prealloc\n\tvar out []vulnerability.Cvss\n\tfor _, sev := range severities {\n\t\tswitch sev.Scheme {\n\t\tcase SeveritySchemeCVSS:\n\t\tdefault:\n\t\t\t// not a CVSS score\n\t\t\tcontinue\n\t\t}\n\t\tcvssSev, ok := sev.Value.(CVSSSeverity)\n\t\tif !ok {\n\t\t\t// not a CVSS score\n\t\t\tcontinue\n\t\t}\n\t\tvar usedMetrics vulnerability.CvssMetrics\n\t\t// though the DB has the base score, we parse the vector for all metrics\n\t\tmetrics, err := cvss.ParseMetricsFromVector(cvssSev.Vector)\n\t\tif err != nil {\n\t\t\tlog.WithFields(\"vector\", cvssSev.Vector, \"error\", err).Warn(\"unable to parse CVSS vector\")\n\t\t\tcontinue\n\t\t}\n\t\tif metrics != nil {\n\t\t\tusedMetrics = *metrics\n\t\t}\n\n\t\tout = append(out, vulnerability.Cvss{\n\t\t\tSource:  sev.Source,\n\t\t\tType:    legacyCVSSType(sev.Rank),\n\t\t\tVersion: cvssSev.Version,\n\t\t\tVector:  cvssSev.Vector,\n\t\t\tMetrics: usedMetrics,\n\t\t})\n\t}\n\treturn out\n}\n\nfunc legacyCVSSType(rank int) string {\n\tif rank == 1 {\n\t\treturn \"Primary\"\n\t}\n\treturn \"Secondary\"\n}\n"
  },
  {
    "path": "grype/db/v6/severity_test.go",
    "content": "package v6\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/grype/grype/vulnerability\"\n)\n\nfunc TestExtractSeverity(t *testing.T) {\n\ttests := []struct {\n\t\tname        string\n\t\tinput       any\n\t\texpected    vulnerability.Severity\n\t\texpectedErr require.ErrorAssertionFunc\n\t}{\n\t\t{\n\t\t\tname:        \"string low severity\",\n\t\t\tinput:       \"low\",\n\t\t\texpected:    vulnerability.LowSeverity,\n\t\t\texpectedErr: require.NoError,\n\t\t},\n\t\t{\n\t\t\tname:        \"string high severity\",\n\t\t\tinput:       \"high\",\n\t\t\texpected:    vulnerability.HighSeverity,\n\t\t\texpectedErr: require.NoError,\n\t\t},\n\t\t{\n\t\t\tname:        \"string critical severity\",\n\t\t\tinput:       \"critical\",\n\t\t\texpected:    vulnerability.CriticalSeverity,\n\t\t\texpectedErr: require.NoError,\n\t\t},\n\t\t{\n\t\t\tname:        \"string unknown severity\",\n\t\t\tinput:       \"invalid\",\n\t\t\texpected:    vulnerability.UnknownSeverity,\n\t\t\texpectedErr: require.NoError,\n\t\t},\n\t\t{\n\t\t\tname: \"CVSS v2 low severity\",\n\t\t\tinput: CVSSSeverity{\n\t\t\t\tVersion: \"2.0\",\n\t\t\t\tVector:  \"AV:L/AC:L/Au:N/C:N/I:P/A:N\",\n\t\t\t},\n\t\t\texpected:    vulnerability.LowSeverity,\n\t\t\texpectedErr: require.NoError,\n\t\t},\n\t\t{\n\t\t\tname: \"CVSS v2 medium severity\",\n\t\t\tinput: CVSSSeverity{\n\t\t\t\tVersion: \"2.0\",\n\t\t\t\tVector:  \"AV:N/AC:L/Au:N/C:P/I:P/A:N\",\n\t\t\t},\n\t\t\texpected:    vulnerability.MediumSeverity,\n\t\t\texpectedErr: require.NoError,\n\t\t},\n\t\t{\n\t\t\tname: \"CVSS v2 high severity\",\n\t\t\tinput: CVSSSeverity{\n\t\t\t\tVersion: \"2.0\",\n\t\t\t\tVector:  \"AV:N/AC:L/Au:N/C:P/I:P/A:P\",\n\t\t\t},\n\t\t\texpected:    vulnerability.HighSeverity,\n\t\t\texpectedErr: require.NoError,\n\t\t},\n\t\t{\n\t\t\tname: \"CVSS v3 negligible severity\",\n\t\t\tinput: CVSSSeverity{\n\t\t\t\tVersion: \"3.1\",\n\t\t\t\tVector:  \"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:N\",\n\t\t\t},\n\t\t\texpected:    vulnerability.NegligibleSeverity,\n\t\t\texpectedErr: require.NoError,\n\t\t},\n\t\t{\n\t\t\tname: \"CVSS v3 critical severity\",\n\t\t\tinput: CVSSSeverity{\n\t\t\t\tVersion: \"3.1\",\n\t\t\t\tVector:  \"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H\",\n\t\t\t},\n\t\t\texpected:    vulnerability.CriticalSeverity,\n\t\t\texpectedErr: require.NoError,\n\t\t},\n\t\t{\n\t\t\tname: \"CVSS v4 critical severity\",\n\t\t\tinput: CVSSSeverity{\n\t\t\t\tVersion: \"4.0\",\n\t\t\t\tVector:  \"CVSS:4.0/AV:N/AC:H/AT:P/PR:L/UI:N/VC:N/VI:H/VA:L/SC:L/SI:H/SA:L/MAC:L/MAT:P/MPR:N/S:N/R:A/RE:L/U:Clear\",\n\t\t\t},\n\t\t\texpected:    vulnerability.CriticalSeverity,\n\t\t\texpectedErr: require.NoError,\n\t\t},\n\t\t{\n\t\t\tname: \"invalid CVSS vector\",\n\t\t\tinput: CVSSSeverity{\n\t\t\t\tVersion: \"3.1\",\n\t\t\t\tVector:  \"INVALID\",\n\t\t\t},\n\t\t\texpected:    vulnerability.UnknownSeverity,\n\t\t\texpectedErr: require.Error,\n\t\t},\n\t\t{\n\t\t\tname:        \"invalid type\",\n\t\t\tinput:       123,\n\t\t\texpected:    vulnerability.UnknownSeverity,\n\t\t\texpectedErr: require.NoError,\n\t\t},\n\t\t{\n\t\t\tname:        \"nil input\",\n\t\t\tinput:       nil,\n\t\t\texpected:    vulnerability.UnknownSeverity,\n\t\t\texpectedErr: require.NoError,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult, err := extractSeverity(tt.input)\n\t\t\ttt.expectedErr(t, err)\n\t\t\tassert.Equal(t, tt.expected, result)\n\t\t})\n\t}\n}\n\nfunc TestExtractSeverities(t *testing.T) {\n\ttests := []struct {\n\t\tname          string\n\t\tinput         *VulnerabilityHandle\n\t\texpectedSev   vulnerability.Severity\n\t\texpectedCVSS  []vulnerability.Cvss\n\t\texpectedError require.ErrorAssertionFunc\n\t}{\n\t\t{\n\t\t\tname:          \"nil blob\",\n\t\t\tinput:         &VulnerabilityHandle{BlobValue: nil},\n\t\t\texpectedSev:   vulnerability.UnknownSeverity,\n\t\t\texpectedCVSS:  nil,\n\t\t\texpectedError: require.NoError,\n\t\t},\n\t\t{\n\t\t\tname: \"empty severities\",\n\t\t\tinput: &VulnerabilityHandle{\n\t\t\t\tBlobValue: &VulnerabilityBlob{\n\t\t\t\t\tSeverities: []Severity{},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedSev:   vulnerability.UnknownSeverity,\n\t\t\texpectedCVSS:  nil,\n\t\t\texpectedError: require.NoError,\n\t\t},\n\t\t{\n\t\t\tname: \"valid primary CVSS severity\",\n\t\t\tinput: &VulnerabilityHandle{\n\t\t\t\tBlobValue: &VulnerabilityBlob{\n\t\t\t\t\tSeverities: []Severity{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tScheme: SeveritySchemeCVSS,\n\t\t\t\t\t\t\tSource: \"NVD\",\n\t\t\t\t\t\t\tValue: CVSSSeverity{\n\t\t\t\t\t\t\t\tVersion: \"3.1\",\n\t\t\t\t\t\t\t\tVector:  \"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tRank: 1,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedSev: vulnerability.CriticalSeverity,\n\t\t\texpectedCVSS: []vulnerability.Cvss{\n\t\t\t\t{\n\t\t\t\t\tSource:  \"NVD\",\n\t\t\t\t\tType:    \"Primary\",\n\t\t\t\t\tVersion: \"3.1\",\n\t\t\t\t\tVector:  \"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H\",\n\t\t\t\t\tMetrics: vulnerability.CvssMetrics{\n\t\t\t\t\t\tBaseScore:           9.8,\n\t\t\t\t\t\tExploitabilityScore: ptr(3.9),\n\t\t\t\t\t\tImpactScore:         ptr(5.9),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedError: require.NoError,\n\t\t},\n\t\t{\n\t\t\tname: \"valid secondary CVSS severity\",\n\t\t\tinput: &VulnerabilityHandle{\n\t\t\t\tBlobValue: &VulnerabilityBlob{\n\t\t\t\t\tSeverities: []Severity{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tScheme: SeveritySchemeCVSS,\n\t\t\t\t\t\t\tSource: \"NVD\",\n\t\t\t\t\t\t\tValue: CVSSSeverity{\n\t\t\t\t\t\t\t\tVersion: \"3.1\",\n\t\t\t\t\t\t\t\tVector:  \"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tRank: 2,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedSev: vulnerability.CriticalSeverity,\n\t\t\texpectedCVSS: []vulnerability.Cvss{\n\t\t\t\t{\n\t\t\t\t\tSource:  \"NVD\",\n\t\t\t\t\tType:    \"Secondary\",\n\t\t\t\t\tVersion: \"3.1\",\n\t\t\t\t\tVector:  \"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H\",\n\t\t\t\t\tMetrics: vulnerability.CvssMetrics{\n\t\t\t\t\t\tBaseScore:           9.8,\n\t\t\t\t\t\tExploitabilityScore: ptr(3.9),\n\t\t\t\t\t\tImpactScore:         ptr(5.9),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedError: require.NoError,\n\t\t},\n\t\t{\n\t\t\tname: \"valid CVSS severity with unknown rank (default to secondary)\",\n\t\t\tinput: &VulnerabilityHandle{\n\t\t\t\tBlobValue: &VulnerabilityBlob{\n\t\t\t\t\tSeverities: []Severity{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tScheme: SeveritySchemeCVSS,\n\t\t\t\t\t\t\tSource: \"NVD\",\n\t\t\t\t\t\t\tValue: CVSSSeverity{\n\t\t\t\t\t\t\t\tVersion: \"3.1\",\n\t\t\t\t\t\t\t\tVector:  \"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tRank: 3,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedSev: vulnerability.CriticalSeverity,\n\t\t\texpectedCVSS: []vulnerability.Cvss{\n\t\t\t\t{\n\t\t\t\t\tSource:  \"NVD\",\n\t\t\t\t\tType:    \"Secondary\",\n\t\t\t\t\tVersion: \"3.1\",\n\t\t\t\t\tVector:  \"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H\",\n\t\t\t\t\tMetrics: vulnerability.CvssMetrics{\n\t\t\t\t\t\tBaseScore:           9.8,\n\t\t\t\t\t\tExploitabilityScore: ptr(3.9),\n\t\t\t\t\t\tImpactScore:         ptr(5.9),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedError: require.NoError,\n\t\t},\n\t\t{\n\t\t\tname: \"invalid CVSS vector\",\n\t\t\tinput: &VulnerabilityHandle{\n\t\t\t\tBlobValue: &VulnerabilityBlob{\n\t\t\t\t\tSeverities: []Severity{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tScheme: SeveritySchemeCVSS,\n\t\t\t\t\t\t\tValue: CVSSSeverity{\n\t\t\t\t\t\t\t\tVersion: \"3.1\",\n\t\t\t\t\t\t\t\tVector:  \"INVALID\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedSev:   vulnerability.UnknownSeverity,\n\t\t\texpectedCVSS:  nil,\n\t\t\texpectedError: require.Error,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif tt.expectedError == nil {\n\t\t\t\ttt.expectedError = require.NoError\n\t\t\t}\n\t\t\tsev, cvss, err := extractSeverities(tt.input)\n\t\t\ttt.expectedError(t, err)\n\t\t\tassert.Equal(t, tt.expectedSev, sev)\n\t\t\tassert.Equal(t, tt.expectedCVSS, cvss)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/db/v6/store.go",
    "content": "package v6\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"gorm.io/gorm\"\n\n\t\"github.com/anchore/grype/internal/log\"\n)\n\ntype store struct {\n\t*dbMetadataStore\n\t*providerStore\n\t*vulnerabilityStore\n\t*operatingSystemStore\n\t*affectedPackageStore\n\t*unaffectedPackageStore\n\t*affectedCPEStore\n\t*unaffectedCPEStore\n\t*vulnerabilityDecoratorStore\n\tblobStore *blobStore\n\tdb        *gorm.DB\n\tconfig    Config\n\tempty     bool\n\twritable  bool\n}\n\nfunc (s *store) GetDB() *gorm.DB {\n\treturn s.db\n}\n\nfunc (s *store) attachBlobValue(values ...blobable) error {\n\treturn s.blobStore.attachBlobValue(values...)\n}\n\nfunc InitialData() []any {\n\tvar data []any\n\tos := KnownOperatingSystemSpecifierOverrides()\n\tfor i := range os {\n\t\tdata = append(data, &os[i])\n\t}\n\n\tp := KnownPackageSpecifierOverrides()\n\tfor i := range p {\n\t\tdata = append(data, &p[i])\n\t}\n\treturn data\n}\n\nfunc newStore(cfg Config, empty, writable bool) (*store, error) {\n\tvar path string\n\tif cfg.DBDirPath != \"\" {\n\t\tpath = cfg.DBFilePath()\n\t}\n\n\tdb, err := NewLowLevelDB(path, empty, writable, cfg.Debug)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to open db: %w\", err)\n\t}\n\n\tmetadataStore := newDBMetadataStore(db)\n\n\tif empty {\n\t\tif err := metadataStore.SetDBMetadata(); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to set db metadata: %w\", err)\n\t\t}\n\t}\n\n\tmeta, err := metadataStore.GetDBMetadata()\n\tif err != nil || meta == nil || meta.Model != ModelVersion {\n\t\t// db.Close must be called, or we will get stale reads\n\t\td, _ := db.DB()\n\t\tif d != nil {\n\t\t\t_ = d.Close()\n\t\t}\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"not a v%d database: %w\", ModelVersion, err)\n\t\t}\n\t\treturn nil, fmt.Errorf(\"not a v%d database\", ModelVersion)\n\t}\n\n\tdbVersion := newSchemaVerFromDBMetadata(*meta)\n\n\tbs := newBlobStore(db)\n\n\tosStore := newOperatingSystemStore(db, bs)\n\n\treturn &store{\n\t\tdbMetadataStore:             metadataStore,\n\t\tproviderStore:               newProviderStore(db),\n\t\tvulnerabilityStore:          newVulnerabilityStore(db, bs),\n\t\toperatingSystemStore:        osStore,\n\t\taffectedPackageStore:        newAffectedPackageStore(db, bs, osStore),\n\t\tunaffectedPackageStore:      newUnaffectedPackageStore(db, bs, osStore),\n\t\taffectedCPEStore:            newAffectedCPEStore(db, bs),\n\t\tvulnerabilityDecoratorStore: newVulnerabilityDecoratorStore(db, bs, dbVersion),\n\t\tunaffectedCPEStore:          newUnaffectedCPEStore(db, bs),\n\t\tblobStore:                   bs,\n\t\tdb:                          db,\n\t\tconfig:                      cfg,\n\t\tempty:                       empty,\n\t\twritable:                    writable,\n\t}, nil\n}\n\n// Close closes the store and finalizes the blobs when the DB is open for writing. If open for reading, only closes the connection to the DB.\nfunc (s *store) Close() error {\n\tif !s.writable || !s.empty {\n\t\td, err := s.db.DB()\n\t\tif err == nil {\n\t\t\treturn d.Close()\n\t\t}\n\t\t// if not empty, this writable execution created indexes\n\t\treturn nil\n\t}\n\tlog.Debug(\"closing store\")\n\n\t// drop all indexes, which saves a lot of space distribution-wise (these get re-created on running gorm auto-migrate)\n\tif err := dropAllIndexes(s.db); err != nil {\n\t\treturn err\n\t}\n\n\t// compact the DB size\n\tlog.Debug(\"vacuuming database\")\n\tif err := s.db.Exec(\"VACUUM\").Error; err != nil {\n\t\treturn fmt.Errorf(\"failed to vacuum: %w\", err)\n\t}\n\n\t// since we are using riskier statements to optimize write speeds, do a last integrity check\n\tlog.Debug(\"running integrity check\")\n\tif err := s.db.Exec(\"PRAGMA integrity_check\").Error; err != nil {\n\t\treturn fmt.Errorf(\"integrity check failed: %w\", err)\n\t}\n\n\td, err := s.db.DB()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn d.Close()\n}\n\nfunc dropAllIndexes(db *gorm.DB) error {\n\ttables, err := db.Migrator().GetTables()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get tables: %w\", err)\n\t}\n\n\tlog.WithFields(\"tables\", len(tables)).Debug(\"discovering indexes\")\n\n\tfor _, table := range tables {\n\t\tindexes, err := db.Migrator().GetIndexes(table)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to get indexes for table %s: %w\", table, err)\n\t\t}\n\n\t\tlog.WithFields(\"table\", table, \"indexes\", len(indexes)).Trace(\"dropping indexes\")\n\t\tfor _, index := range indexes {\n\t\t\t// skip auto-generated UNIQUE or PRIMARY KEY indexes (sqlite will not allow you to drop these without more major surgery)\n\t\t\tif strings.HasPrefix(index.Name(), \"sqlite_autoindex\") {\n\t\t\t\tlog.WithFields(\"table\", table, \"index\", index.Name()).Trace(\"skip dropping autoindex\")\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tlog.WithFields(\"table\", table, \"index\", index.Name()).Trace(\"dropping index\")\n\t\t\tif err := db.Migrator().DropIndex(table, index.Name()); err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to drop index %s on table %s: %w\", index, table, err)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "grype/db/v6/store_test.go",
    "content": "package v6\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"gorm.io/gorm\"\n)\n\nfunc TestStoreClose(t *testing.T) {\n\n\tt.Run(\"readonly mode does nothing\", func(t *testing.T) {\n\t\tdir := t.TempDir()\n\t\ts := setupTestStore(t, dir)\n\t\ts.empty = false\n\t\ts.writable = false\n\n\t\terr := s.Close()\n\t\trequire.NoError(t, err)\n\n\t\t// ensure the connection is no longer open\n\t\tvar indexes []string\n\t\ts.db.Raw(`SELECT name FROM sqlite_master WHERE type = 'index' AND name NOT LIKE 'sqlite_autoindex%'`).Scan(&indexes)\n\t\tassert.Empty(t, indexes)\n\n\t\t// get a new connection (readonly)\n\t\ts = setupReadOnlyTestStore(t, dir)\n\n\t\t// ensure we have our indexes\n\t\tindexes = nil\n\t\ts.db.Raw(`SELECT name FROM sqlite_master WHERE type = 'index' AND name NOT LIKE 'sqlite_autoindex%'`).Scan(&indexes)\n\t\tassert.NotEmpty(t, indexes)\n\n\t})\n\n\tt.Run(\"successful close in writable mode\", func(t *testing.T) {\n\t\tdir := t.TempDir()\n\t\ts := setupTestStore(t, dir)\n\n\t\t// ensure we have indexes to start with\n\t\tvar indexes []string\n\t\ts.db.Raw(`SELECT name FROM sqlite_master WHERE type = 'index' AND name NOT LIKE 'sqlite_autoindex%'`).Scan(&indexes)\n\t\tassert.NotEmpty(t, indexes)\n\n\t\terr := s.Close()\n\t\trequire.NoError(t, err)\n\n\t\t// get a new connection (readonly)\n\t\ts = setupReadOnlyTestStore(t, dir)\n\n\t\t// ensure all of our indexes were dropped\n\t\tindexes = nil\n\t\ts.db.Raw(`SELECT name FROM sqlite_master WHERE type = 'index' AND name NOT LIKE 'sqlite_autoindex%'`).Scan(&indexes)\n\t\tassert.Empty(t, indexes)\n\t})\n}\n\nfunc Test_oldDbV5(t *testing.T) {\n\ts := setupTestStore(t)\n\trequire.NoError(t, s.db.Where(\"true\").Delete(&DBMetadata{}).Error) // delete all existing records\n\trequire.NoError(t, s.Close())\n\ts, err := newStore(s.config, false, true)\n\trequire.Nil(t, s)\n\trequire.ErrorIs(t, err, gorm.ErrRecordNotFound)\n\trequire.ErrorContains(t, err, fmt.Sprintf(\"not a v%d database\", ModelVersion))\n}\n\nfunc Test_oldDbWithMetadata(t *testing.T) {\n\ts := setupTestStore(t)\n\trequire.NoError(t, s.db.Where(\"true\").Model(DBMetadata{}).Update(\"Model\", \"5\").Error) // old database version\n\trequire.NoError(t, s.Close())\n\ts, err := newStore(s.config, false, true)\n\trequire.Nil(t, s)\n\trequire.NotErrorIs(t, err, gorm.ErrRecordNotFound)\n\trequire.ErrorContains(t, err, fmt.Sprintf(\"not a v%d database\", ModelVersion))\n}\n"
  },
  {
    "path": "grype/db/v6/unaffected_cpe_store.go",
    "content": "package v6\n\nimport (\n\t\"gorm.io/gorm\"\n\n\t\"github.com/anchore/syft/syft/cpe\"\n)\n\ntype UnaffectedCPEStoreWriter interface {\n\tAddUnaffectedCPEs(packages ...*UnaffectedCPEHandle) error\n}\n\ntype UnaffectedCPEStoreReader interface {\n\tGetUnaffectedCPEs(cpe *cpe.Attributes, config *GetCPEOptions) ([]UnaffectedCPEHandle, error)\n}\n\ntype unaffectedCPEStore struct {\n\tdb        *gorm.DB\n\tblobStore *blobStore\n\tcpeStore  *cpeStore\n}\n\nfunc newUnaffectedCPEStore(db *gorm.DB, bs *blobStore) *unaffectedCPEStore {\n\treturn &unaffectedCPEStore{\n\t\tdb:        db,\n\t\tblobStore: bs,\n\t\tcpeStore:  newCPEStore(db, bs),\n\t}\n}\n\nfunc (s *unaffectedCPEStore) AddUnaffectedCPEs(packages ...*UnaffectedCPEHandle) error {\n\treturn addCPEHandles(s.cpeStore, packages...)\n}\n\nfunc (s *unaffectedCPEStore) GetUnaffectedCPEs(cpe *cpe.Attributes, config *GetCPEOptions) ([]UnaffectedCPEHandle, error) {\n\tresults, err := getCPEHandles[*UnaffectedCPEHandle](\n\t\ts.cpeStore,\n\t\tcpe,\n\t\tconfig,\n\t\t\"unaffected_cpe_handles\",\n\t)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tmodels := make([]UnaffectedCPEHandle, len(results))\n\tfor i, r := range results {\n\t\tmodels[i] = *r\n\t}\n\treturn models, nil\n}\n"
  },
  {
    "path": "grype/db/v6/unaffected_cpe_store_test.go",
    "content": "package v6\n\nimport (\n\t\"testing\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestUnaffectedCPEStore_AddUnaffectedCPEs(t *testing.T) {\n\tdb := setupTestStore(t).db\n\tbw := newBlobStore(db)\n\ts := newUnaffectedCPEStore(db, bw)\n\n\tcpe1 := &UnaffectedCPEHandle{\n\t\tVulnerability: &VulnerabilityHandle{ // vuln id = 1\n\t\t\tProvider: &Provider{\n\t\t\t\tID: \"nvd\",\n\t\t\t},\n\t\t\tName: \"CVE-2023-5678\",\n\t\t},\n\t\tCPE: &Cpe{\n\t\t\tPart:    \"a\",\n\t\t\tVendor:  \"vendor-1\",\n\t\t\tProduct: \"product-1\",\n\t\t\tEdition: \"edition-1\",\n\t\t},\n\t\tBlobValue: &PackageBlob{\n\t\t\tCVEs: []string{\"CVE-2023-5678\"},\n\t\t},\n\t}\n\n\tcpe2 := testUnaffectedCPEHandle() // vuln id = 2\n\n\terr := s.AddUnaffectedCPEs(cpe1, cpe2)\n\trequire.NoError(t, err)\n\n\tvar result1 UnaffectedCPEHandle\n\terr = db.Where(\"cpe_id = ?\", 1).First(&result1).Error\n\trequire.NoError(t, err)\n\tassert.Equal(t, cpe1.VulnerabilityID, result1.VulnerabilityID)\n\tassert.Equal(t, cpe1.ID, result1.ID)\n\tassert.Equal(t, cpe1.BlobID, result1.BlobID)\n\tassert.Nil(t, result1.BlobValue) // since we're not preloading any fields on the fetch\n\n\tvar result2 UnaffectedCPEHandle\n\terr = db.Where(\"cpe_id = ?\", 2).First(&result2).Error\n\trequire.NoError(t, err)\n\tassert.Equal(t, cpe2.VulnerabilityID, result2.VulnerabilityID)\n\tassert.Equal(t, cpe2.ID, result2.ID)\n\tassert.Equal(t, cpe2.BlobID, result2.BlobID)\n\tassert.Nil(t, result2.BlobValue) // since we're not preloading any fields on the fetch\n}\n\nfunc TestUnaffectedCPEStore_GetCPEs(t *testing.T) {\n\tdb := setupTestStore(t).db\n\tbw := newBlobStore(db)\n\ts := newUnaffectedCPEStore(db, bw)\n\n\tc := testUnaffectedCPEHandle()\n\terr := s.AddUnaffectedCPEs(c)\n\trequire.NoError(t, err)\n\n\tresults, err := s.GetUnaffectedCPEs(cpeFromProduct(c.CPE.Product), nil)\n\trequire.NoError(t, err)\n\n\texpected := []UnaffectedCPEHandle{*c}\n\trequire.Len(t, results, len(expected))\n\tresult := results[0]\n\tassert.Equal(t, c.CpeID, result.CpeID)\n\tassert.Equal(t, c.ID, result.ID)\n\tassert.Equal(t, c.BlobID, result.BlobID)\n\trequire.Nil(t, result.BlobValue) // since we're not preloading any fields on the fetch\n\n\t// fetch again with blob & cpe preloaded\n\tresults, err = s.GetUnaffectedCPEs(cpeFromProduct(c.CPE.Product), &GetCPEOptions{PreloadCPE: true, PreloadBlob: true, PreloadVulnerability: true})\n\trequire.NoError(t, err)\n\trequire.Len(t, results, len(expected))\n\tresult = results[0]\n\tassert.NotNil(t, result.BlobValue)\n\tif d := cmp.Diff(*c, result); d != \"\" {\n\t\tt.Errorf(\"unexpected result (-want +got):\\n%s\", d)\n\t}\n}\n\nfunc TestUnaffectedCPEStore_GetExact(t *testing.T) {\n\tdb := setupTestStore(t).db\n\tbw := newBlobStore(db)\n\ts := newUnaffectedCPEStore(db, bw)\n\n\tc := testUnaffectedCPEHandle()\n\terr := s.AddUnaffectedCPEs(c)\n\trequire.NoError(t, err)\n\n\t// we want to search by all fields to ensure that all are accounted for in the query (since there are string fields referenced in the where clauses)\n\tresults, err := s.GetUnaffectedCPEs(toCPE(c.CPE), nil)\n\trequire.NoError(t, err)\n\n\texpected := []UnaffectedCPEHandle{*c}\n\trequire.Len(t, results, len(expected))\n\tresult := results[0]\n\tassert.Equal(t, c.CpeID, result.CpeID)\n\n}\n\nfunc TestUnaffectedCPEStore_Get_CaseInsensitive(t *testing.T) {\n\tdb := setupTestStore(t).db\n\tbw := newBlobStore(db)\n\ts := newUnaffectedCPEStore(db, bw)\n\n\tc := testUnaffectedCPEHandle()\n\terr := s.AddUnaffectedCPEs(c)\n\trequire.NoError(t, err)\n\n\t// we want to search by all fields to ensure that all are accounted for in the query (since there are string fields referenced in the where clauses)\n\tresults, err := s.GetUnaffectedCPEs(toCPE(&Cpe{\n\t\tPart:            \"Application\",      // capitalized\n\t\tVendor:          \"Vendor\",           // capitalized\n\t\tProduct:         \"Product\",          // capitalized\n\t\tEdition:         \"Edition\",          // capitalized\n\t\tLanguage:        \"Language\",         // capitalized\n\t\tSoftwareEdition: \"Software_edition\", // capitalized\n\t\tTargetHardware:  \"Target_hardware\",  // capitalized\n\t\tTargetSoftware:  \"Target_software\",  // capitalized\n\t\tOther:           \"Other\",            // capitalized\n\t}), nil)\n\trequire.NoError(t, err)\n\n\texpected := []UnaffectedCPEHandle{*c}\n\trequire.Len(t, results, len(expected))\n\tresult := results[0]\n\tassert.Equal(t, c.CpeID, result.CpeID)\n}\n\nfunc TestUnaffectedCPEStore_PreventDuplicateCPEs(t *testing.T) {\n\tdb := setupTestStore(t).db\n\tbw := newBlobStore(db)\n\ts := newUnaffectedCPEStore(db, bw)\n\n\tcpe1 := &UnaffectedCPEHandle{\n\t\tVulnerability: &VulnerabilityHandle{ // vuln id = 1\n\t\t\tName: \"CVE-2023-5678\",\n\t\t\tProvider: &Provider{\n\t\t\t\tID: \"nvd\",\n\t\t\t},\n\t\t},\n\t\tCPE: &Cpe{ // ID = 1\n\t\t\tPart:    \"a\",\n\t\t\tVendor:  \"vendor-1\",\n\t\t\tProduct: \"product-1\",\n\t\t\tEdition: \"edition-1\",\n\t\t},\n\t\tBlobValue: &PackageBlob{\n\t\t\tCVEs: []string{\"CVE-2023-5678\"},\n\t\t},\n\t}\n\n\terr := s.AddUnaffectedCPEs(cpe1)\n\trequire.NoError(t, err)\n\n\t// attempt to add a duplicate CPE with the same values\n\tduplicateCPE := &UnaffectedCPEHandle{\n\t\tVulnerability: &VulnerabilityHandle{ // vuln id = 2, different VulnerabilityID for testing...\n\t\t\tName: \"CVE-2024-1234\",\n\t\t\tProvider: &Provider{\n\t\t\t\tID: \"nvd\",\n\t\t\t},\n\t\t},\n\t\tCpeID: 2, // for testing explicitly set to 2, but this is unrealistic\n\t\tCPE: &Cpe{\n\t\t\tID:      2,           // different, again, unrealistic but useful for testing\n\t\t\tPart:    \"a\",         // same\n\t\t\tVendor:  \"vendor-1\",  // same\n\t\t\tProduct: \"product-1\", // same\n\t\t\tEdition: \"edition-1\", // same\n\t\t},\n\t\tBlobValue: &PackageBlob{\n\t\t\tCVEs: []string{\"CVE-2024-1234\"},\n\t\t},\n\t}\n\n\terr = s.AddUnaffectedCPEs(duplicateCPE)\n\trequire.NoError(t, err)\n\n\trequire.Equal(t, cpe1.CpeID, duplicateCPE.CpeID, \"expected the CPE DB ID to be the same\")\n\n\tvar existingCPEs []Cpe\n\terr = db.Find(&existingCPEs).Error\n\trequire.NoError(t, err)\n\trequire.Len(t, existingCPEs, 1, \"expected only one CPE to exist\")\n\n\tactualHandles, err := s.GetUnaffectedCPEs(cpeFromProduct(cpe1.CPE.Product), &GetCPEOptions{\n\t\tPreloadCPE:           true,\n\t\tPreloadBlob:          true,\n\t\tPreloadVulnerability: true,\n\t})\n\trequire.NoError(t, err)\n\n\t// the CPEs should be the same, and the store should reconcile the IDs\n\tduplicateCPE.CpeID = cpe1.CpeID\n\tduplicateCPE.CPE.ID = cpe1.CPE.ID\n\n\texpected := []UnaffectedCPEHandle{*cpe1, *duplicateCPE}\n\trequire.Len(t, actualHandles, len(expected), \"expected both handles to be stored\")\n\tif d := cmp.Diff(expected, actualHandles); d != \"\" {\n\t\tt.Errorf(\"unexpected result (-want +got):\\n%s\", d)\n\t}\n}\n\nfunc testUnaffectedCPEHandle() *UnaffectedCPEHandle {\n\treturn &UnaffectedCPEHandle{\n\t\tVulnerability: &VulnerabilityHandle{\n\t\t\tName: \"CVE-2024-4321\",\n\t\t\tProvider: &Provider{\n\t\t\t\tID: \"nvd\",\n\t\t\t},\n\t\t},\n\t\tCPE: &Cpe{\n\t\t\tPart:            \"application\",\n\t\t\tVendor:          \"vendor\",\n\t\t\tProduct:         \"product\",\n\t\t\tEdition:         \"edition\",\n\t\t\tLanguage:        \"language\",\n\t\t\tSoftwareEdition: \"software_edition\",\n\t\t\tTargetHardware:  \"target_hardware\",\n\t\t\tTargetSoftware:  \"target_software\",\n\t\t\tOther:           \"other\",\n\t\t},\n\t\tBlobValue: &PackageBlob{\n\t\t\tCVEs: []string{\"CVE-2024-4321\"},\n\t\t},\n\t}\n}\n"
  },
  {
    "path": "grype/db/v6/unaffected_package_store.go",
    "content": "package v6\n\nimport \"gorm.io/gorm\"\n\ntype UnaffectedPackageStoreWriter interface {\n\tAddUnaffectedPackages(packages ...*UnaffectedPackageHandle) error\n}\n\ntype UnaffectedPackageStoreReader interface {\n\tGetUnaffectedPackages(pkg *PackageSpecifier, config *GetPackageOptions) ([]UnaffectedPackageHandle, error)\n}\ntype unaffectedPackageStore struct {\n\tdb       *gorm.DB\n\tosStore  *operatingSystemStore\n\tpkgStore *packageStore\n}\n\nfunc newUnaffectedPackageStore(db *gorm.DB, bs *blobStore, oss *operatingSystemStore) *unaffectedPackageStore {\n\treturn &unaffectedPackageStore{\n\t\tdb:       db,\n\t\tosStore:  oss,\n\t\tpkgStore: newPackageStore(db, bs, oss),\n\t}\n}\n\nfunc (s *unaffectedPackageStore) AddUnaffectedPackages(packages ...*UnaffectedPackageHandle) error {\n\treturn addPackagesWithOS(s.pkgStore, packages...)\n}\n\nfunc (s *unaffectedPackageStore) GetUnaffectedPackages(pkg *PackageSpecifier, config *GetPackageOptions) ([]UnaffectedPackageHandle, error) {\n\tresults, err := getPackages[*UnaffectedPackageHandle](\n\t\ts.pkgStore,\n\t\tpkg,\n\t\tconfig,\n\t\t\"unaffected_package_handles\",\n\t)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tmodels := make([]UnaffectedPackageHandle, len(results))\n\tfor i, r := range results {\n\t\tmodels[i] = *r\n\t}\n\treturn models, nil\n}\n"
  },
  {
    "path": "grype/db/v6/unaffected_package_store_test.go",
    "content": "package v6\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/google/go-cmp/cmp/cmpopts\"\n\t\"github.com/scylladb/go-set/strset\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/syft/syft/cpe\"\n)\n\ntype unaffectedPackageHandlePreloadConfig struct {\n\tname                 string\n\tPreloadOS            bool\n\tPreloadPackage       bool\n\tPreloadBlob          bool\n\tPreloadVulnerability bool\n\tprepExpectations     func(*testing.T, []UnaffectedPackageHandle) []UnaffectedPackageHandle\n}\n\nfunc defaultUnaffectedPackageHandlePreloadCases() []unaffectedPackageHandlePreloadConfig {\n\treturn []unaffectedPackageHandlePreloadConfig{\n\t\t{\n\t\t\tname:                 \"preload-all\",\n\t\t\tPreloadOS:            true,\n\t\t\tPreloadPackage:       true,\n\t\t\tPreloadBlob:          true,\n\t\t\tPreloadVulnerability: true,\n\t\t\tprepExpectations: func(t *testing.T, in []UnaffectedPackageHandle) []UnaffectedPackageHandle {\n\t\t\t\tfor _, a := range in {\n\t\t\t\t\tif a.OperatingSystemID != nil {\n\t\t\t\t\t\trequire.NotNil(t, a.OperatingSystem)\n\t\t\t\t\t}\n\t\t\t\t\trequire.NotNil(t, a.Package)\n\t\t\t\t\trequire.NotNil(t, a.BlobValue)\n\t\t\t\t\trequire.NotNil(t, a.Vulnerability)\n\t\t\t\t}\n\t\t\t\treturn in\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"preload-none\",\n\t\t\tprepExpectations: func(t *testing.T, in []UnaffectedPackageHandle) []UnaffectedPackageHandle {\n\t\t\t\tvar out []UnaffectedPackageHandle\n\t\t\t\tfor _, a := range in {\n\t\t\t\t\tif a.OperatingSystem == nil && a.BlobValue == nil && a.Package == nil && a.Vulnerability == nil {\n\t\t\t\t\t\tt.Skip(\"preload already matches expectation\")\n\t\t\t\t\t}\n\t\t\t\t\ta.OperatingSystem = nil\n\t\t\t\t\ta.Package = nil\n\t\t\t\t\ta.BlobValue = nil\n\t\t\t\t\ta.Vulnerability = nil\n\t\t\t\t\tout = append(out, a)\n\t\t\t\t}\n\t\t\t\treturn out\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:      \"preload-os-only\",\n\t\t\tPreloadOS: true,\n\t\t\tprepExpectations: func(t *testing.T, in []UnaffectedPackageHandle) []UnaffectedPackageHandle {\n\t\t\t\tvar out []UnaffectedPackageHandle\n\t\t\t\tfor _, a := range in {\n\t\t\t\t\tif a.OperatingSystemID != nil {\n\t\t\t\t\t\trequire.NotNil(t, a.OperatingSystem)\n\t\t\t\t\t}\n\t\t\t\t\tif a.Package == nil && a.BlobValue == nil && a.Vulnerability == nil {\n\t\t\t\t\t\tt.Skip(\"preload already matches expectation\")\n\t\t\t\t\t}\n\t\t\t\t\ta.Package = nil\n\t\t\t\t\ta.BlobValue = nil\n\t\t\t\t\ta.Vulnerability = nil\n\t\t\t\t\tout = append(out, a)\n\t\t\t\t}\n\t\t\t\treturn out\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:           \"preload-package-only\",\n\t\t\tPreloadPackage: true,\n\t\t\tprepExpectations: func(t *testing.T, in []UnaffectedPackageHandle) []UnaffectedPackageHandle {\n\t\t\t\tvar out []UnaffectedPackageHandle\n\t\t\t\tfor _, a := range in {\n\t\t\t\t\trequire.NotNil(t, a.Package)\n\t\t\t\t\tif a.OperatingSystem == nil && a.BlobValue == nil && a.Vulnerability == nil {\n\t\t\t\t\t\tt.Skip(\"preload already matches expectation\")\n\t\t\t\t\t}\n\t\t\t\t\ta.OperatingSystem = nil\n\t\t\t\t\ta.BlobValue = nil\n\t\t\t\t\ta.Vulnerability = nil\n\t\t\t\t\tout = append(out, a)\n\t\t\t\t}\n\t\t\t\treturn out\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:        \"preload-blob-only\",\n\t\t\tPreloadBlob: true,\n\t\t\tprepExpectations: func(t *testing.T, in []UnaffectedPackageHandle) []UnaffectedPackageHandle {\n\t\t\t\tvar out []UnaffectedPackageHandle\n\t\t\t\tfor _, a := range in {\n\t\t\t\t\tif a.OperatingSystem == nil && a.Package == nil && a.Vulnerability == nil {\n\t\t\t\t\t\tt.Skip(\"preload already matches expectation\")\n\t\t\t\t\t}\n\t\t\t\t\ta.OperatingSystem = nil\n\t\t\t\t\ta.Package = nil\n\t\t\t\t\ta.Vulnerability = nil\n\t\t\t\t\tout = append(out, a)\n\t\t\t\t}\n\t\t\t\treturn out\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:                 \"preload-vulnerability-only\",\n\t\t\tPreloadVulnerability: true,\n\t\t\tprepExpectations: func(t *testing.T, in []UnaffectedPackageHandle) []UnaffectedPackageHandle {\n\t\t\t\tvar out []UnaffectedPackageHandle\n\t\t\t\tfor _, a := range in {\n\t\t\t\t\tif a.OperatingSystem == nil && a.Package == nil && a.BlobValue == nil {\n\t\t\t\t\t\tt.Skip(\"preload already matches expectation\")\n\t\t\t\t\t}\n\t\t\t\t\ta.OperatingSystem = nil\n\t\t\t\t\ta.Package = nil\n\t\t\t\t\ta.BlobValue = nil\n\t\t\t\t\tout = append(out, a)\n\t\t\t\t}\n\t\t\t\treturn out\n\t\t\t},\n\t\t},\n\t}\n}\n\nfunc TestUnaffectedPackageStore_AddUnaffectedPackages(t *testing.T) {\n\tsetupUnaffectedPackageStore := func(t *testing.T) *unaffectedPackageStore {\n\t\tdb := setupTestStore(t).db\n\t\tbs := newBlobStore(db)\n\t\treturn newUnaffectedPackageStore(db, bs, newOperatingSystemStore(db, bs))\n\t}\n\n\tsetupTestStoreWithPackages := func(t *testing.T) (*UnaffectedPackageHandle, *UnaffectedPackageHandle, *unaffectedPackageStore) {\n\t\tpkg1 := &UnaffectedPackageHandle{\n\t\t\tVulnerability: &VulnerabilityHandle{\n\t\t\t\tName: \"CVE-2023-1234\",\n\t\t\t\tProvider: &Provider{\n\t\t\t\t\tID: \"provider1\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tPackage: &Package{Name: \"pkg1\", Ecosystem: \"type1\"},\n\t\t\tBlobValue: &PackageBlob{\n\t\t\t\tCVEs: []string{\"CVE-2023-1234\"},\n\t\t\t},\n\t\t}\n\n\t\tpkg2 := testDistro1UnaffectedPackage2Handle()\n\n\t\treturn pkg1, pkg2, setupUnaffectedPackageStore(t)\n\t}\n\n\tt.Run(\"no preloading\", func(t *testing.T) {\n\t\tpkg1, pkg2, s := setupTestStoreWithPackages(t)\n\n\t\terr := s.AddUnaffectedPackages(pkg1, pkg2)\n\t\trequire.NoError(t, err)\n\n\t\tvar result1 UnaffectedPackageHandle\n\t\terr = s.db.Where(\"package_id = ?\", pkg1.PackageID).First(&result1).Error\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, pkg1.PackageID, result1.PackageID)\n\t\tassert.Equal(t, pkg1.BlobID, result1.BlobID)\n\t\trequire.Nil(t, result1.BlobValue) // no preloading on fetch\n\n\t\tvar result2 UnaffectedPackageHandle\n\t\terr = s.db.Where(\"package_id = ?\", pkg2.PackageID).First(&result2).Error\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, pkg2.PackageID, result2.PackageID)\n\t\tassert.Equal(t, pkg2.BlobID, result2.BlobID)\n\t\trequire.Nil(t, result2.BlobValue)\n\t})\n\n\tt.Run(\"preloading\", func(t *testing.T) {\n\t\tpkg1, pkg2, s := setupTestStoreWithPackages(t)\n\n\t\terr := s.AddUnaffectedPackages(pkg1, pkg2)\n\t\trequire.NoError(t, err)\n\n\t\toptions := &GetPackageOptions{\n\t\t\tPreloadOS:      true,\n\t\t\tPreloadPackage: true,\n\t\t\tPreloadBlob:    true,\n\t\t}\n\n\t\tresults, err := s.GetUnaffectedPackages(pkgFromName(pkg1.Package.Name), options)\n\t\trequire.NoError(t, err)\n\t\trequire.Len(t, results, 1)\n\n\t\tresult := results[0]\n\t\trequire.NotNil(t, result.Package)\n\t\trequire.NotNil(t, result.BlobValue)\n\t\tassert.Nil(t, result.OperatingSystem) // pkg1 has no OS\n\t})\n\n\tt.Run(\"preload CPEs\", func(t *testing.T) {\n\t\tpkg1, _, s := setupTestStoreWithPackages(t)\n\n\t\tc := Cpe{\n\t\t\tPart:    \"a\",\n\t\t\tVendor:  \"vendor1\",\n\t\t\tProduct: \"product1\",\n\t\t}\n\t\tpkg1.Package.CPEs = []Cpe{c}\n\n\t\terr := s.AddUnaffectedPackages(pkg1)\n\t\trequire.NoError(t, err)\n\n\t\toptions := &GetPackageOptions{\n\t\t\tPreloadPackage:     true,\n\t\t\tPreloadPackageCPEs: true,\n\t\t}\n\n\t\tresults, err := s.GetUnaffectedPackages(pkgFromName(pkg1.Package.Name), options)\n\t\trequire.NoError(t, err)\n\t\trequire.Len(t, results, 1)\n\n\t\tresult := results[0]\n\t\trequire.NotNil(t, result.Package)\n\n\t\t// the IDs should have been set, and there is only one, so we know the correct values\n\t\tc.ID = 1\n\n\t\tif d := cmp.Diff([]Cpe{c}, result.Package.CPEs); d != \"\" {\n\t\t\tt.Errorf(\"unexpected result (-want +got):\\n%s\", d)\n\t\t}\n\t})\n\n\tt.Run(\"Package deduplication\", func(t *testing.T) {\n\t\tpkg1 := &UnaffectedPackageHandle{\n\t\t\tVulnerability: &VulnerabilityHandle{\n\t\t\t\tName: \"CVE-2023-1234\",\n\t\t\t\tProvider: &Provider{\n\t\t\t\t\tID: \"provider1\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tPackage: &Package{Name: \"pkg1\", Ecosystem: \"type1\"},\n\t\t\tBlobValue: &PackageBlob{\n\t\t\t\tCVEs: []string{\"CVE-2023-1234\"},\n\t\t\t},\n\t\t}\n\n\t\tpkg2 := &UnaffectedPackageHandle{\n\t\t\tVulnerability: &VulnerabilityHandle{\n\t\t\t\tName: \"CVE-2023-1234\",\n\t\t\t\tProvider: &Provider{\n\t\t\t\t\tID: \"provider1\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tPackage: &Package{Name: \"pkg1\", Ecosystem: \"type1\"}, // same!\n\t\t\tBlobValue: &PackageBlob{\n\t\t\t\tCVEs: []string{\"CVE-2023-56789\"},\n\t\t\t},\n\t\t}\n\n\t\ts := setupUnaffectedPackageStore(t)\n\t\terr := s.AddUnaffectedPackages(pkg1, pkg2)\n\t\trequire.NoError(t, err)\n\n\t\tvar pkgs []Package\n\t\terr = s.db.Find(&pkgs).Error\n\t\trequire.NoError(t, err)\n\n\t\texpected := []Package{\n\t\t\t*pkg1.Package,\n\t\t}\n\n\t\tif d := cmp.Diff(expected, pkgs); d != \"\" {\n\t\t\tt.Errorf(\"unexpected result (-want +got):\\n%s\", d)\n\t\t}\n\t})\n\n\tt.Run(\"same package with multiple CPEs\", func(t *testing.T) {\n\t\tcpe1 := Cpe{\n\t\t\tPart:    \"a\",\n\t\t\tVendor:  \"vendor1\",\n\t\t\tProduct: \"product1\",\n\t\t}\n\n\t\tcpe2 := Cpe{\n\t\t\tPart:    \"a\",\n\t\t\tVendor:  \"vendor2\",\n\t\t\tProduct: \"product2\",\n\t\t}\n\n\t\tpkg1 := &UnaffectedPackageHandle{\n\t\t\tVulnerability: &VulnerabilityHandle{\n\t\t\t\tName: \"CVE-2023-1234\",\n\t\t\t\tProvider: &Provider{\n\t\t\t\t\tID: \"provider1\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tPackage: &Package{Name: \"pkg1\", Ecosystem: \"type1\", CPEs: []Cpe{cpe1}},\n\t\t\tBlobValue: &PackageBlob{\n\t\t\t\tCVEs: []string{\"CVE-2023-1234\"},\n\t\t\t},\n\t\t}\n\n\t\tpkg2 := &UnaffectedPackageHandle{\n\t\t\tVulnerability: &VulnerabilityHandle{\n\t\t\t\tName: \"CVE-2023-56789\",\n\t\t\t\tProvider: &Provider{\n\t\t\t\t\tID: \"provider1\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tPackage: &Package{Name: \"pkg1\", Ecosystem: \"type1\", CPEs: []Cpe{cpe1, cpe2}}, // duplicate CPE + additional CPE\n\t\t\tBlobValue: &PackageBlob{\n\t\t\t\tCVEs: []string{\"CVE-2023-56789\"},\n\t\t\t},\n\t\t}\n\n\t\ts := setupUnaffectedPackageStore(t)\n\t\terr := s.AddUnaffectedPackages(pkg1, pkg2)\n\t\trequire.NoError(t, err)\n\n\t\tvar pkgs []Package\n\t\terr = s.db.Preload(\"CPEs\").Find(&pkgs).Error\n\t\trequire.NoError(t, err)\n\n\t\texpPkg := *pkg1.Package\n\t\texpPkg.ID = 1\n\t\tcpe1.ID = 1\n\t\tcpe2.ID = 2\n\t\texpPkg.CPEs = []Cpe{cpe1, cpe2}\n\n\t\texpected := []Package{\n\t\t\texpPkg,\n\t\t}\n\n\t\tif d := cmp.Diff(expected, pkgs); d != \"\" {\n\t\t\tt.Errorf(\"unexpected result (-want +got):\\n%s\", d)\n\t\t}\n\n\t\texpectedCPEs := []Cpe{cpe1, cpe2}\n\t\tvar cpeResults []Cpe\n\t\terr = s.db.Find(&cpeResults).Error\n\t\trequire.NoError(t, err)\n\t\tif d := cmp.Diff(expectedCPEs, cpeResults); d != \"\" {\n\t\t\tt.Errorf(\"unexpected result (-want +got):\\n%s\", d)\n\t\t}\n\n\t})\n\n\tt.Run(\"allow same CPE to belong to multiple packages\", func(t *testing.T) {\n\t\tcpe1 := Cpe{\n\t\t\tPart:    \"a\",\n\t\t\tVendor:  \"vendor1\",\n\t\t\tProduct: \"product1\",\n\t\t}\n\n\t\tcpe2 := Cpe{\n\t\t\tPart:    \"a\",\n\t\t\tVendor:  \"vendor2\",\n\t\t\tProduct: \"product2\",\n\t\t}\n\n\t\tpkg1 := &UnaffectedPackageHandle{\n\t\t\tVulnerability: &VulnerabilityHandle{\n\t\t\t\tName: \"CVE-2023-1234\",\n\t\t\t\tProvider: &Provider{\n\t\t\t\t\tID: \"provider1\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tPackage: &Package{Name: \"pkg1\", Ecosystem: \"type1\", CPEs: []Cpe{cpe1}},\n\t\t\tBlobValue: &PackageBlob{\n\t\t\t\tCVEs: []string{\"CVE-2023-1234\"},\n\t\t\t},\n\t\t}\n\n\t\tpkg2 := &UnaffectedPackageHandle{\n\t\t\tVulnerability: &VulnerabilityHandle{\n\t\t\t\tName: \"CVE-2023-56789\",\n\t\t\t\tProvider: &Provider{\n\t\t\t\t\tID: \"provider1\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tPackage: &Package{Name: \"pkg2\", Ecosystem: \"type1\", CPEs: []Cpe{cpe1, cpe2}}, // overlapping CPEs for different packages\n\t\t\tBlobValue: &PackageBlob{\n\t\t\t\tCVEs: []string{\"CVE-2023-56789\"},\n\t\t\t},\n\t\t}\n\n\t\ts := setupUnaffectedPackageStore(t)\n\t\terr := s.AddUnaffectedPackages(pkg1, pkg2)\n\t\trequire.NoError(t, err)\n\n\t\tvar pkgs []Package\n\t\terr = s.db.Preload(\"CPEs\").Find(&pkgs).Error\n\t\trequire.NoError(t, err)\n\n\t\tcpe1.ID = 1\n\t\tcpe2.ID = 2\n\n\t\texpPkg1 := *pkg1.Package\n\t\texpPkg1.ID = 1\n\t\texpPkg1.CPEs = []Cpe{cpe1}\n\n\t\texpPkg2 := *pkg2.Package\n\t\texpPkg2.ID = 2\n\t\texpPkg2.CPEs = []Cpe{cpe1, cpe2}\n\n\t\texpected := []Package{\n\t\t\texpPkg1,\n\t\t\texpPkg2,\n\t\t}\n\n\t\tif d := cmp.Diff(expected, pkgs); d != \"\" {\n\t\t\tt.Errorf(\"unexpected result (-want +got):\\n%s\", d)\n\t\t}\n\n\t\texpectedCPEs := []Cpe{cpe1, cpe2}\n\t\tvar cpeResults []Cpe\n\t\terr = s.db.Find(&cpeResults).Error\n\t\trequire.NoError(t, err)\n\t\tif d := cmp.Diff(expectedCPEs, cpeResults); d != \"\" {\n\t\t\tt.Errorf(\"unexpected result (-want +got):\\n%s\", d)\n\t\t}\n\t})\n}\n\nfunc TestUnaffectedPackageStore_GetUnaffectedPackages_ByCPE(t *testing.T) {\n\tdb := setupTestStore(t).db\n\tbs := newBlobStore(db)\n\toss := newOperatingSystemStore(db, bs)\n\ts := newUnaffectedPackageStore(db, bs, oss)\n\n\tcpe1 := Cpe{Part: \"a\", Vendor: \"vendor1\", Product: \"product1\"}\n\tcpe2 := Cpe{Part: \"a\", Vendor: \"vendor2\", Product: \"product2\"}\n\tcpe3 := Cpe{Part: \"a\", Vendor: \"vendor2\", Product: \"product2\", TargetSoftware: \"target1\"}\n\tpkg1 := &UnaffectedPackageHandle{\n\t\tVulnerability: &VulnerabilityHandle{\n\t\t\tName: \"CVE-2023-1234\",\n\t\t\tProvider: &Provider{\n\t\t\t\tID: \"provider1\",\n\t\t\t},\n\t\t},\n\t\tPackage: &Package{Name: \"pkg1\", Ecosystem: \"type1\", CPEs: []Cpe{cpe1}},\n\t\tBlobValue: &PackageBlob{\n\t\t\tCVEs: []string{\"CVE-2023-1234\"},\n\t\t},\n\t}\n\tpkg2 := &UnaffectedPackageHandle{\n\t\tVulnerability: &VulnerabilityHandle{\n\t\t\tName: \"CVE-2023-5678\",\n\t\t\tProvider: &Provider{\n\t\t\t\tID: \"provider1\",\n\t\t\t},\n\t\t},\n\t\tPackage: &Package{Name: \"pkg2\", Ecosystem: \"type2\", CPEs: []Cpe{cpe2}},\n\t\tBlobValue: &PackageBlob{\n\t\t\tCVEs: []string{\"CVE-2023-5678\"},\n\t\t},\n\t}\n\n\tpkg3 := &UnaffectedPackageHandle{\n\t\tVulnerability: &VulnerabilityHandle{\n\t\t\tName: \"CVE-2023-5678\",\n\t\t\tProvider: &Provider{\n\t\t\t\tID: \"provider1\",\n\t\t\t},\n\t\t},\n\t\tPackage: &Package{Name: \"pkg3\", Ecosystem: \"type2\", CPEs: []Cpe{cpe3}},\n\t\tBlobValue: &PackageBlob{\n\t\t\tCVEs: []string{\"CVE-2023-5678\"},\n\t\t},\n\t}\n\n\terr := s.AddUnaffectedPackages(pkg1, pkg2, pkg3)\n\trequire.NoError(t, err)\n\n\ttests := []struct {\n\t\tname     string\n\t\tcpe      cpe.Attributes\n\t\toptions  *GetPackageOptions\n\t\texpected []UnaffectedPackageHandle\n\t\twantErr  require.ErrorAssertionFunc\n\t}{\n\t\t{\n\t\t\tname: \"full match CPE\",\n\t\t\tcpe: cpe.Attributes{\n\t\t\t\tPart:    \"a\",\n\t\t\t\tVendor:  \"vendor1\",\n\t\t\t\tProduct: \"product1\",\n\t\t\t},\n\t\t\toptions: &GetPackageOptions{\n\t\t\t\tPreloadPackageCPEs:   true,\n\t\t\t\tPreloadPackage:       true,\n\t\t\t\tPreloadBlob:          true,\n\t\t\t\tPreloadVulnerability: true,\n\t\t\t},\n\t\t\texpected: []UnaffectedPackageHandle{*pkg1},\n\t\t},\n\t\t{\n\t\t\tname: \"partial match CPE\",\n\t\t\tcpe: cpe.Attributes{\n\t\t\t\tPart:   \"a\",\n\t\t\t\tVendor: \"vendor2\",\n\t\t\t},\n\t\t\toptions: &GetPackageOptions{\n\t\t\t\tPreloadPackageCPEs:   true,\n\t\t\t\tPreloadPackage:       true,\n\t\t\t\tPreloadBlob:          true,\n\t\t\t\tPreloadVulnerability: true,\n\t\t\t},\n\t\t\texpected: []UnaffectedPackageHandle{*pkg2, *pkg3},\n\t\t},\n\t\t{\n\t\t\tname: \"match on any TSW when specific one provided when broad matching enabled\",\n\t\t\tcpe: cpe.Attributes{\n\t\t\t\tPart:     \"a\",\n\t\t\t\tVendor:   \"vendor2\",\n\t\t\t\tTargetSW: \"target1\",\n\t\t\t},\n\t\t\toptions: &GetPackageOptions{\n\t\t\t\tPreloadPackageCPEs:    true,\n\t\t\t\tPreloadPackage:        true,\n\t\t\t\tPreloadBlob:           true,\n\t\t\t\tPreloadVulnerability:  true,\n\t\t\t\tAllowBroadCPEMatching: true,\n\t\t\t},\n\t\t\texpected: []UnaffectedPackageHandle{*pkg2, *pkg3},\n\t\t},\n\t\t{\n\t\t\tname: \"do NOT match on any TSW when specific one provided when broad matching disabled\",\n\t\t\tcpe: cpe.Attributes{\n\t\t\t\tPart:     \"a\",\n\t\t\t\tVendor:   \"vendor2\",\n\t\t\t\tTargetSW: \"target1\",\n\t\t\t},\n\t\t\toptions: &GetPackageOptions{\n\t\t\t\tPreloadPackageCPEs:    true,\n\t\t\t\tPreloadPackage:        true,\n\t\t\t\tPreloadBlob:           true,\n\t\t\t\tPreloadVulnerability:  true,\n\t\t\t\tAllowBroadCPEMatching: false,\n\t\t\t},\n\t\t\texpected: []UnaffectedPackageHandle{*pkg3},\n\t\t},\n\t\t{\n\t\t\tname: \"missing attributes\",\n\t\t\tcpe: cpe.Attributes{\n\t\t\t\tPart: \"a\",\n\t\t\t},\n\t\t\toptions: &GetPackageOptions{\n\t\t\t\tPreloadPackageCPEs:   true,\n\t\t\t\tPreloadPackage:       true,\n\t\t\t\tPreloadBlob:          true,\n\t\t\t\tPreloadVulnerability: true,\n\t\t\t},\n\t\t\texpected: []UnaffectedPackageHandle{*pkg1, *pkg2, *pkg3},\n\t\t},\n\t\t{\n\t\t\tname: \"no matches\",\n\t\t\tcpe: cpe.Attributes{\n\t\t\t\tPart:    \"a\",\n\t\t\t\tVendor:  \"unknown_vendor\",\n\t\t\t\tProduct: \"unknown_product\",\n\t\t\t},\n\t\t\toptions: &GetPackageOptions{\n\t\t\t\tPreloadPackageCPEs:   true,\n\t\t\t\tPreloadPackage:       true,\n\t\t\t\tPreloadBlob:          true,\n\t\t\t\tPreloadVulnerability: true,\n\t\t\t},\n\t\t\texpected: nil,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif tt.wantErr == nil {\n\t\t\t\ttt.wantErr = require.NoError\n\t\t\t}\n\n\t\t\tresult, err := s.GetUnaffectedPackages(&PackageSpecifier{CPE: &tt.cpe}, tt.options)\n\t\t\ttt.wantErr(t, err)\n\t\t\tif err != nil {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif d := cmp.Diff(tt.expected, result, cmpopts.EquateEmpty()); d != \"\" {\n\t\t\t\tt.Errorf(\"unexpected result: %s\", d)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestUnaffectedPackageStore_GetUnaffectedPackages_CaseInsensitive(t *testing.T) {\n\tdb := setupTestStore(t).db\n\tbs := newBlobStore(db)\n\toss := newOperatingSystemStore(db, bs)\n\ts := newUnaffectedPackageStore(db, bs, oss)\n\n\tcpe1 := Cpe{Part: \"a\", Vendor: \"Vendor1\", Product: \"Product1\"} // capitalized\n\tpkg1 := &UnaffectedPackageHandle{\n\t\tVulnerability: &VulnerabilityHandle{\n\t\t\tName: \"CVE-2023-1234\",\n\t\t\tProvider: &Provider{\n\t\t\t\tID: \"provider1\",\n\t\t\t},\n\t\t},\n\t\tOperatingSystem: &OperatingSystem{\n\t\t\tName:         \"Ubuntu\", // capitalized\n\t\t\tReleaseID:    \"zubuntu\",\n\t\t\tMajorVersion: \"20\",\n\t\t\tMinorVersion: \"04\", // leading 0\n\t\t\tCodename:     \"focal\",\n\t\t},\n\t\tPackage: &Package{Name: \"Pkg1\", Ecosystem: \"Type1\", CPEs: []Cpe{cpe1}}, // capitalized\n\t\tBlobValue: &PackageBlob{\n\t\t\tCVEs: []string{\"CVE-2023-1234\"},\n\t\t},\n\t}\n\n\tpkg2 := &UnaffectedPackageHandle{ // this should never register as a match\n\t\tVulnerability: &VulnerabilityHandle{\n\t\t\tName: \"CVE-2222-2222\",\n\t\t\tProvider: &Provider{\n\t\t\t\tID: \"provider2\",\n\t\t\t},\n\t\t},\n\t\tOperatingSystem: &OperatingSystem{\n\t\t\tName:         \"ubuntu\",\n\t\t\tReleaseID:    \"ubuntu\",\n\t\t\tMajorVersion: \"20\",\n\t\t\tMinorVersion: \"10\",\n\t\t},\n\t\tPackage: &Package{Name: \"pkg2\", Ecosystem: \"type2\"},\n\t\tBlobValue: &PackageBlob{\n\t\t\tCVEs: []string{\"CVE-2222-2222\"},\n\t\t},\n\t}\n\n\terr := s.AddUnaffectedPackages(pkg1, pkg2)\n\trequire.NoError(t, err)\n\n\ttests := []struct {\n\t\tname     string\n\t\tpkgSpec  *PackageSpecifier\n\t\toptions  *GetPackageOptions\n\t\texpected int\n\t}{\n\t\t{\n\t\t\tname:     \"sanity check: search miss\",\n\t\t\tpkgSpec:  pkgFromName(\"does not exist\"),\n\t\t\texpected: 0,\n\t\t},\n\t\t{\n\t\t\tname:     \"get by name\",\n\t\t\tpkgSpec:  pkgFromName(\"pKG1\"),\n\t\t\texpected: 1,\n\t\t},\n\t\t{\n\t\t\tname: \"get by CPE\",\n\t\t\tpkgSpec: &PackageSpecifier{\n\t\t\t\tCPE: &cpe.Attributes{Part: \"a\", Vendor: \"veNDor1\", Product: \"pRODuct1\"},\n\t\t\t},\n\t\t\texpected: 1,\n\t\t},\n\t\t{\n\t\t\tname: \"get by ecosystem\",\n\t\t\tpkgSpec: &PackageSpecifier{\n\t\t\t\tEcosystem: \"tYPE1\",\n\t\t\t},\n\t\t\texpected: 1,\n\t\t},\n\t\t{\n\t\t\tname: \"get by OS name and version (leading 0)\",\n\t\t\toptions: &GetPackageOptions{\n\t\t\t\tOSs: []*OSSpecifier{{\n\t\t\t\t\tName:         \"uBUNtu\",\n\t\t\t\t\tMajorVersion: \"20\",\n\t\t\t\t\tMinorVersion: \"04\",\n\t\t\t\t}},\n\t\t\t},\n\t\t\texpected: 1,\n\t\t},\n\t\t{\n\t\t\tname: \"get by OS name and version\",\n\t\t\toptions: &GetPackageOptions{\n\t\t\t\tOSs: []*OSSpecifier{{\n\t\t\t\t\tName:         \"uBUNtu\",\n\t\t\t\t\tMajorVersion: \"20\",\n\t\t\t\t\tMinorVersion: \"4\",\n\t\t\t\t}},\n\t\t\t},\n\t\t\texpected: 1,\n\t\t},\n\t\t{\n\t\t\tname: \"get by OS release\",\n\t\t\toptions: &GetPackageOptions{\n\t\t\t\tOSs: []*OSSpecifier{{\n\t\t\t\t\tName: \"zUBuntu\",\n\t\t\t\t}},\n\t\t\t},\n\t\t\texpected: 1,\n\t\t},\n\t\t{\n\t\t\tname: \"get by OS codename\",\n\t\t\toptions: &GetPackageOptions{\n\t\t\t\tOSs: []*OSSpecifier{{\n\t\t\t\t\tLabelVersion: \"fOCAL\",\n\t\t\t\t}},\n\t\t\t},\n\t\t\texpected: 1,\n\t\t},\n\t\t{\n\t\t\tname: \"get by vuln ID\",\n\t\t\toptions: &GetPackageOptions{\n\t\t\t\tVulnerabilities: []VulnerabilitySpecifier{{Name: \"cVe-2023-1234\"}},\n\t\t\t},\n\t\t\texpected: 1,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult, err := s.GetUnaffectedPackages(tt.pkgSpec, tt.options)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Len(t, result, tt.expected)\n\t\t\tif tt.expected > 0 {\n\t\t\t\tassert.Equal(t, pkg1.PackageID, result[0].PackageID)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestUnaffectedPackageStore_GetUnaffectedPackages_MultipleVulnerabilitySpecs(t *testing.T) {\n\tdb := setupTestStore(t).db\n\tbs := newBlobStore(db)\n\toss := newOperatingSystemStore(db, bs)\n\ts := newUnaffectedPackageStore(db, bs, oss)\n\n\tcpe1 := Cpe{Part: \"a\", Vendor: \"vendor1\", Product: \"product1\"}\n\tcpe2 := Cpe{Part: \"a\", Vendor: \"vendor2\", Product: \"product2\"}\n\tpkg1 := &UnaffectedPackageHandle{\n\t\tVulnerability: &VulnerabilityHandle{\n\t\t\tName: \"CVE-2023-1234\",\n\t\t\tProvider: &Provider{\n\t\t\t\tID: \"provider1\",\n\t\t\t},\n\t\t},\n\t\tPackage: &Package{Name: \"pkg1\", Ecosystem: \"type1\", CPEs: []Cpe{cpe1}},\n\t\tBlobValue: &PackageBlob{\n\t\t\tCVEs: []string{\"CVE-2023-1234\"},\n\t\t},\n\t}\n\tpkg2 := &UnaffectedPackageHandle{\n\t\tVulnerability: &VulnerabilityHandle{\n\t\t\tName: \"CVE-2023-5678\",\n\t\t\tProvider: &Provider{\n\t\t\t\tID: \"provider1\",\n\t\t\t},\n\t\t},\n\t\tPackage: &Package{Name: \"pkg2\", Ecosystem: \"type2\", CPEs: []Cpe{cpe2}},\n\t\tBlobValue: &PackageBlob{\n\t\t\tCVEs: []string{\"CVE-2023-5678\"},\n\t\t},\n\t}\n\n\terr := s.AddUnaffectedPackages(pkg1, pkg2)\n\trequire.NoError(t, err)\n\n\tresult, err := s.GetUnaffectedPackages(nil, &GetPackageOptions{\n\t\tPreloadVulnerability: true,\n\t\tVulnerabilities: []VulnerabilitySpecifier{\n\t\t\t{Name: \"CVE-2023-1234\"},\n\t\t\t{Name: \"CVE-2023-5678\"},\n\t\t},\n\t})\n\trequire.NoError(t, err)\n\n\tactualVulns := strset.New()\n\tfor _, r := range result {\n\t\tactualVulns.Add(r.Vulnerability.Name)\n\t}\n\n\texpectedVulns := strset.New(\"CVE-2023-1234\", \"CVE-2023-5678\")\n\n\tassert.ElementsMatch(t, expectedVulns.List(), actualVulns.List())\n\n}\n\nfunc TestUnaffectedPackageStore_GetUnaffectedPackages(t *testing.T) {\n\tdb := setupTestStore(t).db\n\tbs := newBlobStore(db)\n\toss := newOperatingSystemStore(db, bs)\n\ts := newUnaffectedPackageStore(db, bs, oss)\n\n\tpkg2d1 := testDistro1UnaffectedPackage2Handle()\n\tpkg2 := testNonDistroUnaffectedPackage2Handle()\n\tpkg2d2 := testDistro2UnaffectedPackage2Handle()\n\terr := s.AddUnaffectedPackages(pkg2d1, pkg2, pkg2d2)\n\trequire.NoError(t, err)\n\n\ttests := []struct {\n\t\tname     string\n\t\tpkg      *PackageSpecifier\n\t\toptions  *GetPackageOptions\n\t\texpected []UnaffectedPackageHandle\n\t\twantErr  require.ErrorAssertionFunc\n\t}{\n\t\t{\n\t\t\tname: \"specific distro\",\n\t\t\tpkg:  pkgFromName(pkg2d1.Package.Name),\n\t\t\toptions: &GetPackageOptions{\n\t\t\t\tOSs: []*OSSpecifier{{\n\t\t\t\t\tName:         \"ubuntu\",\n\t\t\t\t\tMajorVersion: \"20\",\n\t\t\t\t\tMinorVersion: \"04\",\n\t\t\t\t}},\n\t\t\t},\n\t\t\texpected: []UnaffectedPackageHandle{*pkg2d1},\n\t\t},\n\t\t{\n\t\t\tname: \"distro major version only\",\n\t\t\tpkg:  pkgFromName(pkg2d1.Package.Name),\n\t\t\toptions: &GetPackageOptions{\n\t\t\t\tOSs: []*OSSpecifier{{\n\t\t\t\t\tName:         \"ubuntu\",\n\t\t\t\t\tMajorVersion: \"20\",\n\t\t\t\t}},\n\t\t\t},\n\t\t\texpected: []UnaffectedPackageHandle{*pkg2d1, *pkg2d2},\n\t\t},\n\t\t{\n\t\t\tname: \"distro codename\",\n\t\t\tpkg:  pkgFromName(pkg2d1.Package.Name),\n\t\t\toptions: &GetPackageOptions{\n\t\t\t\tOSs: []*OSSpecifier{{\n\t\t\t\t\tName:         \"ubuntu\",\n\t\t\t\t\tLabelVersion: \"groovy\",\n\t\t\t\t}},\n\t\t\t},\n\t\t\texpected: []UnaffectedPackageHandle{*pkg2d2},\n\t\t},\n\t\t{\n\t\t\tname: \"no distro\",\n\t\t\tpkg:  pkgFromName(pkg2.Package.Name),\n\t\t\toptions: &GetPackageOptions{\n\t\t\t\tOSs: []*OSSpecifier{NoOSSpecified},\n\t\t\t},\n\t\t\texpected: []UnaffectedPackageHandle{*pkg2},\n\t\t},\n\t\t{\n\t\t\tname: \"any distro\",\n\t\t\tpkg:  pkgFromName(pkg2d1.Package.Name),\n\t\t\toptions: &GetPackageOptions{\n\t\t\t\tOSs: []*OSSpecifier{AnyOSSpecified},\n\t\t\t},\n\t\t\texpected: []UnaffectedPackageHandle{*pkg2d1, *pkg2, *pkg2d2},\n\t\t},\n\t\t{\n\t\t\tname:     \"package type\",\n\t\t\tpkg:      &PackageSpecifier{Name: pkg2.Package.Name, Ecosystem: \"type2\"},\n\t\t\texpected: []UnaffectedPackageHandle{*pkg2},\n\t\t},\n\t\t{\n\t\t\tname: \"specific CVE\",\n\t\t\tpkg:  pkgFromName(pkg2d1.Package.Name),\n\t\t\toptions: &GetPackageOptions{\n\t\t\t\tVulnerabilities: []VulnerabilitySpecifier{{\n\t\t\t\t\tName: \"CVE-2023-1234\",\n\t\t\t\t}},\n\t\t\t},\n\t\t\texpected: []UnaffectedPackageHandle{*pkg2d1},\n\t\t},\n\t\t{\n\t\t\tname: \"any CVE published after a date\",\n\t\t\tpkg:  pkgFromName(pkg2d1.Package.Name),\n\t\t\toptions: &GetPackageOptions{\n\t\t\t\tVulnerabilities: []VulnerabilitySpecifier{{\n\t\t\t\t\tPublishedAfter: func() *time.Time {\n\t\t\t\t\t\tnow := time.Date(2020, 1, 1, 1, 1, 1, 0, time.UTC)\n\t\t\t\t\t\treturn &now\n\t\t\t\t\t}(),\n\t\t\t\t}},\n\t\t\t},\n\t\t\texpected: []UnaffectedPackageHandle{*pkg2d1, *pkg2d2},\n\t\t},\n\t\t{\n\t\t\tname: \"any CVE modified after a date\",\n\t\t\tpkg:  pkgFromName(pkg2d1.Package.Name),\n\t\t\toptions: &GetPackageOptions{\n\t\t\t\tVulnerabilities: []VulnerabilitySpecifier{{\n\t\t\t\t\tModifiedAfter: func() *time.Time {\n\t\t\t\t\t\tnow := time.Date(2023, 1, 1, 3, 4, 5, 0, time.UTC).Add(time.Hour * 2)\n\t\t\t\t\t\treturn &now\n\t\t\t\t\t}(),\n\t\t\t\t}},\n\t\t\t},\n\t\t\texpected: []UnaffectedPackageHandle{*pkg2d1},\n\t\t},\n\t\t{\n\t\t\tname: \"any rejected CVE\",\n\t\t\tpkg:  pkgFromName(pkg2d1.Package.Name),\n\t\t\toptions: &GetPackageOptions{\n\t\t\t\tVulnerabilities: []VulnerabilitySpecifier{{\n\t\t\t\t\tStatus: VulnerabilityRejected,\n\t\t\t\t}},\n\t\t\t},\n\t\t\texpected: []UnaffectedPackageHandle{*pkg2d1},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif tt.wantErr == nil {\n\t\t\t\ttt.wantErr = require.NoError\n\t\t\t}\n\t\t\tfor _, pc := range defaultUnaffectedPackageHandlePreloadCases() {\n\t\t\t\tt.Run(pc.name, func(t *testing.T) {\n\t\t\t\t\topts := tt.options\n\t\t\t\t\tif opts == nil {\n\t\t\t\t\t\topts = &GetPackageOptions{}\n\t\t\t\t\t}\n\t\t\t\t\topts.PreloadOS = pc.PreloadOS\n\t\t\t\t\topts.PreloadPackage = pc.PreloadPackage\n\t\t\t\t\topts.PreloadBlob = pc.PreloadBlob\n\t\t\t\t\topts.PreloadVulnerability = pc.PreloadVulnerability\n\t\t\t\t\texpected := tt.expected\n\t\t\t\t\tif pc.prepExpectations != nil {\n\t\t\t\t\t\texpected = pc.prepExpectations(t, expected)\n\t\t\t\t\t}\n\t\t\t\t\tresult, err := s.GetUnaffectedPackages(tt.pkg, opts)\n\t\t\t\t\ttt.wantErr(t, err)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tif d := cmp.Diff(expected, result); d != \"\" {\n\t\t\t\t\t\tt.Errorf(\"unexpected result: %s\", d)\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestUnaffectedPackageStore_ApplyPackageAlias(t *testing.T) {\n\tdb := setupTestStore(t).db\n\tbs := newBlobStore(db)\n\toss := newOperatingSystemStore(db, bs)\n\ts := newUnaffectedPackageStore(db, bs, oss)\n\n\ttests := []struct {\n\t\tname     string\n\t\tinput    *PackageSpecifier\n\t\texpected string\n\t}{\n\t\t// positive cases\n\t\t{name: \"alias cocoapods\", input: &PackageSpecifier{Ecosystem: \"cocoapods\"}, expected: \"pod\"},\n\t\t{name: \"alias pub\", input: &PackageSpecifier{Ecosystem: \"pub\"}, expected: \"dart-pub\"},\n\t\t{name: \"alias otp\", input: &PackageSpecifier{Ecosystem: \"otp\"}, expected: \"erlang-otp\"},\n\t\t{name: \"alias github\", input: &PackageSpecifier{Ecosystem: \"github\"}, expected: \"github-action\"},\n\t\t{name: \"alias golang\", input: &PackageSpecifier{Ecosystem: \"golang\"}, expected: \"go-module\"},\n\t\t{name: \"alias maven\", input: &PackageSpecifier{Ecosystem: \"maven\"}, expected: \"java-archive\"},\n\t\t{name: \"alias composer\", input: &PackageSpecifier{Ecosystem: \"composer\"}, expected: \"php-composer\"},\n\t\t{name: \"alias pecl\", input: &PackageSpecifier{Ecosystem: \"pecl\"}, expected: \"php-pecl\"},\n\t\t{name: \"alias pypi\", input: &PackageSpecifier{Ecosystem: \"pypi\"}, expected: \"python\"},\n\t\t{name: \"alias cran\", input: &PackageSpecifier{Ecosystem: \"cran\"}, expected: \"R-package\"},\n\t\t{name: \"alias luarocks\", input: &PackageSpecifier{Ecosystem: \"luarocks\"}, expected: \"lua-rocks\"},\n\t\t{name: \"alias cargo\", input: &PackageSpecifier{Ecosystem: \"cargo\"}, expected: \"rust-crate\"},\n\n\t\t// negative cases\n\t\t{name: \"generic type\", input: &PackageSpecifier{Ecosystem: \"generic/linux-kernel\"}, expected: \"generic/linux-kernel\"},\n\t\t{name: \"empty ecosystem\", input: &PackageSpecifier{Ecosystem: \"\"}, expected: \"\"},\n\t\t{name: \"matching type\", input: &PackageSpecifier{Ecosystem: \"python\"}, expected: \"python\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := s.pkgStore.applyPackageAlias(tt.input)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, tt.expected, tt.input.Ecosystem)\n\t\t})\n\t}\n}\n\nfunc testDistro1UnaffectedPackage2Handle() *UnaffectedPackageHandle {\n\tnow := time.Date(2023, 1, 1, 3, 4, 5, 0, time.UTC)\n\tlater := now.Add(time.Hour * 200)\n\treturn &UnaffectedPackageHandle{\n\t\tPackage: &Package{\n\t\t\tName:      \"pkg2\",\n\t\t\tEcosystem: \"type2d\",\n\t\t},\n\t\tVulnerability: &VulnerabilityHandle{\n\t\t\tName:          \"CVE-2023-1234\",\n\t\t\tStatus:        VulnerabilityRejected,\n\t\t\tPublishedDate: &now,\n\t\t\tModifiedDate:  &later,\n\t\t\tProvider: &Provider{\n\t\t\t\tID: \"ubuntu\",\n\t\t\t},\n\t\t},\n\t\tOperatingSystem: &OperatingSystem{\n\t\t\tName:         \"ubuntu\",\n\t\t\tMajorVersion: \"20\",\n\t\t\tMinorVersion: \"04\",\n\t\t\tLabelVersion: \"focal\",\n\t\t},\n\t\tBlobValue: &PackageBlob{\n\t\t\tCVEs: []string{\"CVE-2023-1234\"},\n\t\t},\n\t}\n}\n\nfunc testDistro2UnaffectedPackage2Handle() *UnaffectedPackageHandle {\n\tnow := time.Date(2020, 1, 1, 3, 4, 5, 0, time.UTC)\n\tlater := now.Add(time.Hour * 200)\n\treturn &UnaffectedPackageHandle{\n\t\tPackage: &Package{\n\t\t\tName:      \"pkg2\",\n\t\t\tEcosystem: \"type2d\",\n\t\t},\n\t\tVulnerability: &VulnerabilityHandle{\n\t\t\tName:          \"CVE-2023-4567\",\n\t\t\tPublishedDate: &now,\n\t\t\tModifiedDate:  &later,\n\t\t\tProvider: &Provider{\n\t\t\t\tID: \"ubuntu\",\n\t\t\t},\n\t\t},\n\t\tOperatingSystem: &OperatingSystem{\n\t\t\tName:         \"ubuntu\",\n\t\t\tMajorVersion: \"20\",\n\t\t\tMinorVersion: \"10\",\n\t\t\tLabelVersion: \"groovy\",\n\t\t},\n\t\tBlobValue: &PackageBlob{\n\t\t\tCVEs: []string{\"CVE-2023-4567\"},\n\t\t},\n\t}\n}\n\nfunc testNonDistroUnaffectedPackage2Handle() *UnaffectedPackageHandle {\n\tnow := time.Date(2005, 1, 1, 3, 4, 5, 0, time.UTC)\n\tlater := now.Add(time.Hour * 200)\n\treturn &UnaffectedPackageHandle{\n\t\tPackage: &Package{\n\t\t\tName:      \"pkg2\",\n\t\t\tEcosystem: \"type2\",\n\t\t},\n\t\tVulnerability: &VulnerabilityHandle{\n\t\t\tName:          \"CVE-2023-4567\",\n\t\t\tPublishedDate: &now,\n\t\t\tModifiedDate:  &later,\n\t\t\tProvider: &Provider{\n\t\t\t\tID: \"wolfi\",\n\t\t\t},\n\t\t},\n\t\tBlobValue: &PackageBlob{\n\t\t\tCVEs: []string{\"CVE-2023-4567\"},\n\t\t},\n\t}\n}\n"
  },
  {
    "path": "grype/db/v6/vulnerability.go",
    "content": "package v6\n\nimport (\n\t\"fmt\"\n\t\"sort\"\n\t\"strings\"\n\n\t\"github.com/scylladb/go-set/strset\"\n\n\t\"github.com/anchore/grype/grype/pkg/qualifier\"\n\t\"github.com/anchore/grype/grype/pkg/qualifier/platformcpe\"\n\t\"github.com/anchore/grype/grype/pkg/qualifier/rpmmodularity\"\n\t\"github.com/anchore/grype/grype/version\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n\t\"github.com/anchore/grype/internal/log\"\n\t\"github.com/anchore/syft/syft/cpe\"\n\t\"github.com/anchore/syft/syft/pkg\"\n)\n\nconst (\n\tnvdProvider    = \"nvd\"\n\tgithubProvider = \"github\"\n\tv5NvdNamespace = \"nvd:cpe\"\n)\n\nfunc newVulnerabilityFromAffectedPackageHandle(affected AffectedPackageHandle, affectedRanges []Range) (*vulnerability.Vulnerability, error) {\n\tpackageName := \"\"\n\tif affected.Package != nil {\n\t\tpackageName = affected.Package.Name\n\t}\n\n\tif affected.Vulnerability == nil || affected.Vulnerability.BlobValue == nil || affected.BlobValue == nil {\n\t\treturn nil, fmt.Errorf(\"nil data when attempting to create vulnerability from AffectedPackageHandle\")\n\t}\n\n\treturn newVulnerabilityFromParts(packageName, affected.Vulnerability, affected.BlobValue, affectedRanges, &affected, nil)\n}\n\nfunc newVulnerabilityFromAffectedCPEHandle(affected AffectedCPEHandle, affectedRanges []Range) (*vulnerability.Vulnerability, error) {\n\tif affected.Vulnerability == nil || affected.Vulnerability.BlobValue == nil || affected.BlobValue == nil {\n\t\treturn nil, fmt.Errorf(\"nil data when attempting to create vulnerability from AffectedCPEHandle\")\n\t}\n\treturn newVulnerabilityFromParts(affected.CPE.Product, affected.Vulnerability, affected.BlobValue, affectedRanges, nil, &affected)\n}\n\nfunc newVulnerabilityFromUnaffectedPackageHandle(unaffected UnaffectedPackageHandle, unaffectedRanges []Range) (*vulnerability.Vulnerability, error) {\n\tpackageName := \"\"\n\tif unaffected.Package != nil {\n\t\tpackageName = unaffected.Package.Name\n\t}\n\n\tif unaffected.Vulnerability == nil || unaffected.Vulnerability.BlobValue == nil || unaffected.BlobValue == nil {\n\t\treturn nil, fmt.Errorf(\"nil data when attempting to create vulnerability from UnaffectedPackageHandle\")\n\t}\n\n\tvuln, err := newVulnerabilityFromParts(packageName, unaffected.Vulnerability, unaffected.BlobValue, unaffectedRanges, (*AffectedPackageHandle)(&unaffected), nil)\n\tif vuln != nil {\n\t\tvuln.Unaffected = true\n\t}\n\treturn vuln, err\n}\n\nfunc newVulnerabilityFromUnaffectedCPEHandle(unaffected UnaffectedCPEHandle, unaffectedRanges []Range) (*vulnerability.Vulnerability, error) {\n\tif unaffected.Vulnerability == nil || unaffected.Vulnerability.BlobValue == nil || unaffected.BlobValue == nil {\n\t\treturn nil, fmt.Errorf(\"nil data when attempting to create vulnerability from UnaffectedCPEHandle\")\n\t}\n\tvuln, err := newVulnerabilityFromParts(unaffected.CPE.Product, unaffected.Vulnerability, unaffected.BlobValue, unaffectedRanges, nil, (*AffectedCPEHandle)(&unaffected))\n\tif vuln != nil {\n\t\tvuln.Unaffected = true\n\t}\n\treturn vuln, err\n}\n\nfunc newVulnerabilityFromParts(packageName string, vuln *VulnerabilityHandle, pkgBlob *PackageBlob, ranges []Range, affectedPackageHandle *AffectedPackageHandle, affectedCpeHandle *AffectedCPEHandle) (*vulnerability.Vulnerability, error) {\n\tif vuln.BlobValue == nil {\n\t\treturn nil, fmt.Errorf(\"vuln has no blob value: %+v\", vuln)\n\t}\n\n\tconstraint, err := getVersionConstraint(ranges)\n\tif err != nil {\n\t\treturn nil, nil\n\t}\n\n\tvar language string\n\tif affectedPackageHandle != nil && affectedPackageHandle.Package != nil {\n\t\tlanguage = affectedPackageHandle.Package.Ecosystem\n\t}\n\n\tv5namespace := MimicV5Namespace(vuln, affectedPackageHandle)\n\treturn &vulnerability.Vulnerability{\n\t\tReference: vulnerability.Reference{\n\t\t\tID:        vuln.Name,\n\t\t\tNamespace: v5namespace,\n\t\t\tInternal:  vuln, // just hold a reference to the vulnHandle for later use\n\t\t},\n\t\tPackageName:            packageName,\n\t\tPackageQualifiers:      getPackageQualifiers(pkgBlob),\n\t\tConstraint:             constraint,\n\t\tCPEs:                   toCPEs(affectedPackageHandle, affectedCpeHandle),\n\t\tRelatedVulnerabilities: getRelatedVulnerabilities(vuln, pkgBlob, language),\n\t\tFix:                    toFix(ranges),\n\t\tAdvisories:             toAdvisories(ranges),\n\t\tStatus:                 string(vuln.Status),\n\t}, nil\n}\n\nfunc getVersionConstraint(affectedRanges []Range) (version.Constraint, error) {\n\tvar constraints []string\n\ttypes := strset.New()\n\tfor _, r := range affectedRanges {\n\t\tif r.Version.Constraint != \"\" {\n\t\t\tif r.Version.Type != \"\" {\n\t\t\t\ttypes.Add(r.Version.Type)\n\t\t\t}\n\n\t\t\tconstraints = append(constraints, r.Version.Constraint)\n\t\t}\n\t}\n\n\tif types.Size() > 1 {\n\t\tlog.WithFields(\"types\", types.List()).Debug(\"multiple version formats found for a single vulnerability\")\n\t}\n\n\tvar ty string\n\tif types.Size() >= 1 {\n\t\ttypeStrs := types.List()\n\t\tsort.Strings(typeStrs)\n\t\tty = typeStrs[0]\n\t}\n\n\tversionFormat := version.ParseFormat(ty)\n\tconstraint, err := version.GetConstraint(strings.Join(constraints, \",\"), versionFormat)\n\tif err != nil {\n\t\tlog.WithFields(\"error\", err, \"constraint\", constraints).Debug(\"unable to parse constraint\")\n\t\treturn nil, err\n\t}\n\treturn constraint, nil\n}\n\n// getRelatedVulnerabilities returns a list of related vulnerabilities based on the vulnerability ID, aliases, and CVEs from the affected package (if available).\nfunc getRelatedVulnerabilities(vuln *VulnerabilityHandle, affected *PackageBlob, language string) []vulnerability.Reference {\n\tvar relatedVulnerabilities []vulnerability.Reference\n\tidsToProcess := append([]string{vuln.Name}, vuln.BlobValue.Aliases...)\n\n\tif affected != nil {\n\t\tidsToProcess = append(idsToProcess, affected.CVEs...)\n\t}\n\n\tencountered := strset.New()\n\tfor _, id := range idsToProcess {\n\t\tif encountered.Has(id) {\n\t\t\tcontinue\n\t\t}\n\n\t\tlowerID := strings.ToLower(id)\n\n\t\tswitch {\n\t\tcase strings.HasPrefix(lowerID, \"cve-\"):\n\t\t\tif vuln.ProviderID == nvdProvider && strings.EqualFold(vuln.Name, id) {\n\t\t\t\t// the original vuln is an NVD CVE, so we don't need to add a self-reference\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\trelatedVulnerabilities = append(relatedVulnerabilities,\n\t\t\t\tvulnerability.Reference{\n\t\t\t\t\tID:        id,\n\t\t\t\t\tNamespace: v5NvdNamespace,\n\t\t\t\t},\n\t\t\t)\n\t\tcase strings.HasPrefix(lowerID, \"ghsa-\"):\n\t\t\tif vuln.ProviderID == githubProvider && strings.EqualFold(vuln.Name, id) {\n\t\t\t\t// the original vuln is a GitHub GHSA, so we don't need to add a self-reference\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\trelatedVulnerabilities = append(relatedVulnerabilities,\n\t\t\t\tvulnerability.Reference{\n\t\t\t\t\tID:        id,\n\t\t\t\t\tNamespace: mimicV5GithubNamespace(githubProvider, language),\n\t\t\t\t},\n\t\t\t)\n\t\t}\n\n\t\tencountered.Add(id)\n\t}\n\n\treturn relatedVulnerabilities\n}\n\nfunc getPackageQualifiers(affected *PackageBlob) []qualifier.Qualifier {\n\tif affected != nil {\n\t\treturn toPackageQualifiers(affected.Qualifiers)\n\t}\n\n\treturn nil\n}\n\n// MimicV5Namespace returns the namespace for a given affected package based on what schema v5 did.\n//\n//nolint:funlen\nfunc MimicV5Namespace(vuln *VulnerabilityHandle, affected *AffectedPackageHandle) string {\n\tif affected == nil || affected.Package == nil { // for CPE matches\n\t\treturn fmt.Sprintf(\"%s:cpe\", vuln.Provider.ID)\n\t}\n\n\tif affected.OperatingSystem != nil {\n\t\t// distro family fixes\n\t\tfamily := affected.OperatingSystem.Name\n\t\tver := affected.OperatingSystem.Version()\n\t\tswitch affected.OperatingSystem.Name {\n\t\tcase \"amazon\":\n\t\t\tfamily = \"amazonlinux\"\n\t\tcase \"mariner\", \"azurelinux\":\n\t\t\tfields := strings.Split(ver, \".\")\n\t\t\tmajor := fields[0]\n\t\t\tswitch len(fields) {\n\t\t\tcase 1:\n\t\t\t\tver = fmt.Sprintf(\"%s.0\", major)\n\t\t\tdefault:\n\t\t\t\tver = fmt.Sprintf(\"%s.%s\", major, fields[1])\n\t\t\t}\n\t\t\tswitch major {\n\t\t\tcase \"1\", \"2\":\n\t\t\t\tfamily = \"mariner\"\n\t\t\tdefault:\n\t\t\t\tfamily = \"azurelinux\"\n\t\t\t}\n\t\tcase \"ubuntu\":\n\t\t\tif strings.Count(ver, \".\") == 1 {\n\t\t\t\t// convert 20.4 to 20.04\n\t\t\t\tfields := strings.Split(ver, \".\")\n\t\t\t\tmajor, minor := fields[0], fields[1]\n\t\t\t\tif len(minor) == 1 {\n\t\t\t\t\tver = fmt.Sprintf(\"%s.0%s\", major, minor)\n\t\t\t\t}\n\t\t\t}\n\t\tcase \"oracle\":\n\t\t\tfamily = \"oraclelinux\"\n\t\t}\n\n\t\t// provider fixes\n\t\tpr := vuln.Provider.ID\n\t\tif pr == \"rhel\" {\n\t\t\tpr = \"redhat\"\n\t\t}\n\n\t\t// version fixes\n\t\tswitch vuln.Provider.ID {\n\t\tcase \"rhel\", \"oracle\":\n\t\t\t// ensure we only keep the major version\n\t\t\tver = strings.Split(ver, \".\")[0]\n\t\t}\n\n\t\treturn fmt.Sprintf(\"%s:distro:%s:%s\", pr, family, ver)\n\t}\n\n\tif affected.Package != nil {\n\t\tlanguage := affected.Package.Ecosystem\n\t\tswitch strings.ToLower(language) {\n\t\tcase \"msrc\", string(pkg.KbPkg): // msrc packages were previously modelled as distro\n\t\t\treturn fmt.Sprintf(\"%s:distro:windows:%s\", vuln.Provider.ID, affected.Package.Name)\n\t\tcase string(pkg.BitnamiPkg): // bitnami packages were previously modelled as distro\n\t\t\treturn \"bitnami\"\n\t\tcase \"\": // CPE\n\t\t\treturn fmt.Sprintf(\"%s:cpe\", vuln.Provider.ID)\n\t\t}\n\t\treturn mimicV5GithubNamespace(vuln.Provider.ID, affected.Package.Ecosystem)\n\t}\n\n\t// this shouldn't happen and is not a valid v5 namespace, but some information is better than none\n\treturn vuln.Provider.ID\n}\n\nfunc mimicV5GithubNamespace(provider, language string) string {\n\t// normalize from purl type, github ecosystem types, and vunnel mappings\n\tswitch strings.ToLower(language) {\n\tcase \"golang\", string(pkg.GoModulePkg):\n\t\tlanguage = \"go\"\n\tcase \"composer\", string(pkg.PhpComposerPkg):\n\t\tlanguage = \"php\"\n\tcase \"cargo\", string(pkg.RustPkg):\n\t\tlanguage = \"rust\"\n\tcase \"pub\", string(pkg.DartPubPkg):\n\t\tlanguage = \"dart\"\n\tcase \"nuget\", string(pkg.DotnetPkg):\n\t\tlanguage = \"dotnet\"\n\tcase \"maven\", string(pkg.JavaPkg), string(pkg.JenkinsPluginPkg):\n\t\tlanguage = \"java\"\n\tcase \"swifturl\", string(pkg.SwiplPackPkg), string(pkg.SwiftPkg):\n\t\tlanguage = \"swift\"\n\tcase \"node\", string(pkg.NpmPkg):\n\t\tlanguage = \"javascript\"\n\tcase \"pypi\", \"pip\", string(pkg.PythonPkg):\n\t\tlanguage = \"python\"\n\tcase \"rubygems\", string(pkg.GemPkg):\n\t\tlanguage = \"ruby\"\n\t}\n\treturn fmt.Sprintf(\"%s:language:%s\", provider, language)\n}\n\nfunc toPackageQualifiers(qualifiers *PackageQualifiers) []qualifier.Qualifier {\n\tif qualifiers == nil {\n\t\treturn nil\n\t}\n\tvar out []qualifier.Qualifier\n\tfor _, c := range qualifiers.PlatformCPEs {\n\t\tout = append(out, platformcpe.New(c))\n\t}\n\tif qualifiers.RpmModularity != nil {\n\t\tout = append(out, rpmmodularity.New(*qualifiers.RpmModularity))\n\t}\n\treturn out\n}\n\nfunc toFix(affectedRanges []Range) vulnerability.Fix {\n\tvar state vulnerability.FixState\n\tvar versions []string\n\tvar availables []vulnerability.FixAvailable\n\tfor _, r := range affectedRanges {\n\t\tif r.Fix == nil {\n\t\t\tcontinue\n\t\t}\n\t\tswitch r.Fix.State {\n\t\tcase FixedStatus:\n\t\t\tstate = vulnerability.FixStateFixed\n\t\t\tversions = append(versions, r.Fix.Version)\n\t\t\tif r.Fix.Detail != nil && r.Fix.Detail.Available != nil {\n\t\t\t\ta := r.Fix.Detail.Available\n\t\t\t\tif a.Date != nil {\n\t\t\t\t\tavailables = append(availables, vulnerability.FixAvailable{\n\t\t\t\t\t\tVersion: r.Fix.Version,\n\t\t\t\t\t\tDate:    *a.Date,\n\t\t\t\t\t\tKind:    a.Kind,\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t}\n\t\tcase NotAffectedFixStatus:\n\t\t\t// TODO: not handled yet\n\t\tcase WontFixStatus:\n\t\t\tif state != vulnerability.FixStateFixed {\n\t\t\t\tstate = vulnerability.FixStateWontFix\n\t\t\t}\n\t\tcase NotFixedStatus:\n\t\t\tif state != vulnerability.FixStateFixed {\n\t\t\t\tstate = vulnerability.FixStateNotFixed\n\t\t\t}\n\t\t}\n\t}\n\tif len(versions) == 0 && state == \"\" {\n\t\treturn vulnerability.Fix{}\n\t}\n\treturn vulnerability.Fix{\n\t\tVersions:  versions,\n\t\tState:     state,\n\t\tAvailable: availables,\n\t}\n}\n\nfunc toAdvisories(affectedRanges []Range) []vulnerability.Advisory {\n\tvar advisories []vulnerability.Advisory\n\tfor _, r := range affectedRanges {\n\t\tif r.Fix == nil || r.Fix.Detail == nil {\n\t\t\tcontinue\n\t\t}\n\t\tfor _, urlRef := range r.Fix.Detail.References {\n\t\t\tif urlRef.URL == \"\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tadvisories = append(advisories, vulnerability.Advisory{\n\t\t\t\tID:   urlRef.ID,\n\t\t\t\tLink: urlRef.URL,\n\t\t\t})\n\t\t}\n\t}\n\n\treturn advisories\n}\n\nfunc toCPEs(affectedPackageHandle *AffectedPackageHandle, affectedCPEHandle *AffectedCPEHandle) []cpe.CPE {\n\tvar out []cpe.CPE\n\tvar cpes []Cpe\n\tif affectedPackageHandle != nil {\n\t\tcpes = affectedPackageHandle.Package.CPEs\n\t}\n\tif affectedCPEHandle != nil && affectedCPEHandle.CPE != nil {\n\t\tcpes = append(cpes, *affectedCPEHandle.CPE)\n\t}\n\tfor _, c := range cpes {\n\t\tout = append(out, cpe.CPE{\n\t\t\tAttributes: cpe.Attributes{\n\t\t\t\tPart:      c.Part,\n\t\t\t\tVendor:    c.Vendor,\n\t\t\t\tProduct:   c.Product,\n\t\t\t\tVersion:   cpe.Any,\n\t\t\t\tUpdate:    cpe.Any,\n\t\t\t\tEdition:   c.Edition,\n\t\t\t\tSWEdition: c.SoftwareEdition,\n\t\t\t\tTargetSW:  c.TargetSoftware,\n\t\t\t\tTargetHW:  c.TargetHardware,\n\t\t\t\tOther:     c.Other,\n\t\t\t\tLanguage:  c.Language,\n\t\t\t},\n\t\t\tSource: \"\",\n\t\t})\n\t}\n\treturn out\n}\n"
  },
  {
    "path": "grype/db/v6/vulnerability_decorator_store.go",
    "content": "package v6\n\nimport (\n\t\"fmt\"\n\t\"time\"\n\n\t\"gorm.io/gorm\"\n\n\t\"github.com/anchore/go-logger\"\n\t\"github.com/anchore/grype/internal/log\"\n\t\"github.com/anchore/grype/internal/schemaver\"\n)\n\ntype VulnerabilityDecoratorStoreWriter interface {\n\tAddKnownExploitedVulnerabilities(...*KnownExploitedVulnerabilityHandle) error\n\tAddEpss(...*EpssHandle) error\n\tAddCWE(...*CWEHandle) error\n}\n\ntype VulnerabilityDecoratorStoreReader interface {\n\tGetKnownExploitedVulnerabilities(cve string) ([]KnownExploitedVulnerabilityHandle, error)\n\tGetEpss(cve string) ([]EpssHandle, error)\n\tGetCWEs(cve string) ([]CWEHandle, error)\n}\n\ntype vulnerabilityDecoratorStore struct {\n\tdb        *gorm.DB\n\tblobStore *blobStore\n\tepssDate  *time.Time\n\tvulnerabilityDecoratorCapabilities\n}\n\ntype vulnerabilityDecoratorCapabilities struct {\n\tkevEnabled  bool\n\tepssEnabled bool\n\tcweEnabled  bool\n}\n\nfunc newVulnerabilityDecoratorStore(db *gorm.DB, bs *blobStore, dbVersion schemaver.SchemaVer) *vulnerabilityDecoratorStore {\n\tminSupportedKEVClientVersion := schemaver.New(6, 0, 1)\n\tminSupportedEPSSClientVersion := schemaver.New(6, 0, 2)\n\tminSupportedCWEClientVersion := schemaver.New(6, 1, 2)\n\treturn &vulnerabilityDecoratorStore{\n\t\tdb:        db,\n\t\tblobStore: bs,\n\t\tvulnerabilityDecoratorCapabilities: vulnerabilityDecoratorCapabilities{\n\t\t\tkevEnabled:  dbVersion.GreaterOrEqualTo(minSupportedKEVClientVersion),\n\t\t\tepssEnabled: dbVersion.GreaterOrEqualTo(minSupportedEPSSClientVersion),\n\t\t\tcweEnabled:  dbVersion.GreaterOrEqualTo(minSupportedCWEClientVersion),\n\t\t},\n\t}\n}\n\nfunc (s *vulnerabilityDecoratorStore) AddEpss(epss ...*EpssHandle) error {\n\tif !s.epssEnabled {\n\t\t// when populating a new DB any capability issues found should result in halting\n\t\treturn ErrDBCapabilityNotSupported\n\t}\n\n\tfor i := range epss {\n\t\te := epss[i]\n\n\t\tif err := s.db.Create(e).Error; err != nil {\n\t\t\treturn fmt.Errorf(\"unable to create EPSS: %w\", err)\n\t\t}\n\n\t\tif err := s.setEPSSMetadata(e.Date); err != nil {\n\t\t\treturn fmt.Errorf(\"unable to set EPSS metadata: %w\", err)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (s *vulnerabilityDecoratorStore) AddCWE(cwe ...*CWEHandle) error {\n\tif !s.cweEnabled {\n\t\t// when populating a new DB any capability issues found should result in halting\n\t\treturn ErrDBCapabilityNotSupported\n\t}\n\n\tfor i := range cwe {\n\t\tc := cwe[i]\n\n\t\tif err := s.db.Create(c).Error; err != nil {\n\t\t\treturn fmt.Errorf(\"unable to create CWE: %w\", err)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (s *vulnerabilityDecoratorStore) setEPSSMetadata(date time.Time) error {\n\tif !s.epssEnabled {\n\t\t// when populating a new DB any capability issues found should result in halting\n\t\treturn ErrDBCapabilityNotSupported\n\t}\n\n\tif s.epssDate != nil {\n\t\tif s.epssDate.Equal(date) {\n\t\t\treturn nil\n\t\t}\n\t\treturn fmt.Errorf(\"observed multiple EPSS dates: current=%q new=%q\", s.epssDate.String(), date.String())\n\t}\n\n\tlog.Trace(\"writing EPSS metadata\")\n\n\tif err := s.db.Where(\"true\").Delete(&EpssMetadata{}).Error; err != nil {\n\t\treturn fmt.Errorf(\"failed to delete existing EPSS metadata record: %w\", err)\n\t}\n\n\tinstance := &EpssMetadata{\n\t\tDate: date,\n\t}\n\n\tif err := s.db.Create(instance).Error; err != nil {\n\t\treturn fmt.Errorf(\"failed to create EPSS metadata record: %w\", err)\n\t}\n\n\ts.epssDate = &date\n\treturn nil\n}\n\nfunc (s *vulnerabilityDecoratorStore) getEPSSMetadata() (*EpssMetadata, error) {\n\tlog.Trace(\"fetching EPSS metadata\")\n\n\tvar model EpssMetadata\n\n\tresult := s.db.First(&model)\n\treturn &model, result.Error\n}\n\nfunc (s *vulnerabilityDecoratorStore) GetEpss(cve string) ([]EpssHandle, error) {\n\tif !s.epssEnabled {\n\t\t// capability incompatibilities should gracefully degrade, returning no data or errors\n\t\treturn nil, nil\n\t}\n\n\tfields := logger.Fields{\n\t\t\"cve\": cve,\n\t}\n\tstart := time.Now()\n\tvar count int\n\tdefer func() {\n\t\tfields[\"duration\"] = time.Since(start)\n\t\tfields[\"records\"] = count\n\t\tlog.WithFields(fields).Trace(\"fetched EPSS records\")\n\t}()\n\n\tvar models []EpssHandle\n\tvar results []*EpssHandle\n\n\tif s.epssDate == nil {\n\t\t// fetch and cache the EPSS metadata\n\t\tmetadata, err := s.getEPSSMetadata()\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"unable to fetch EPSS metadata: %w\", err)\n\t\t}\n\t\ts.epssDate = &metadata.Date\n\t}\n\n\tif err := s.db.Where(\"cve = ? collate nocase\", cve).FindInBatches(&results, batchSize, func(_ *gorm.DB, _ int) error {\n\t\tfor _, r := range results {\n\t\t\tr.Date = *s.epssDate\n\t\t\tmodels = append(models, *r)\n\t\t}\n\n\t\tcount += len(results)\n\n\t\treturn nil\n\t}).Error; err != nil {\n\t\treturn models, fmt.Errorf(\"unable to fetch EPSS records: %w\", err)\n\t}\n\n\treturn models, nil\n}\n\nfunc (s *vulnerabilityDecoratorStore) GetCWEs(cve string) ([]CWEHandle, error) {\n\tif !s.cweEnabled {\n\t\t// capability incompatibilities should gracefully degrade, returning no data or errors\n\t\treturn nil, nil\n\t}\n\n\tfields := logger.Fields{\n\t\t\"cve\": cve,\n\t}\n\tstart := time.Now()\n\tvar count int\n\tdefer func() {\n\t\tfields[\"duration\"] = time.Since(start)\n\t\tfields[\"records\"] = count\n\t\tlog.WithFields(fields).Trace(\"fetched CWE records\")\n\t}()\n\n\tvar models []CWEHandle\n\tvar results []*CWEHandle\n\n\tif err := s.db.Where(\"cve = ? collate nocase\", cve).FindInBatches(&results, batchSize, func(_ *gorm.DB, _ int) error {\n\t\tfor _, r := range results {\n\t\t\tmodels = append(models, *r)\n\t\t}\n\n\t\tcount += len(results)\n\n\t\treturn nil\n\t}).Error; err != nil {\n\t\treturn models, fmt.Errorf(\"unable to fetch CWE records: %w\", err)\n\t}\n\n\treturn models, nil\n}\n\nfunc (s *vulnerabilityDecoratorStore) AddKnownExploitedVulnerabilities(kevs ...*KnownExploitedVulnerabilityHandle) error {\n\tif !s.kevEnabled {\n\t\t// when populating a new DB any capability issues found should result in halting\n\t\treturn ErrDBCapabilityNotSupported\n\t}\n\n\tfor i := range kevs {\n\t\tk := kevs[i]\n\t\t// this adds the blob value to the DB and sets the ID on the kev handle\n\t\tif err := s.blobStore.addBlobable(k); err != nil {\n\t\t\treturn fmt.Errorf(\"unable to add KEV blob: %w\", err)\n\t\t}\n\n\t\tif err := s.db.Create(k).Error; err != nil {\n\t\t\treturn fmt.Errorf(\"unable to create known exploited vulnerability: %w\", err)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (s *vulnerabilityDecoratorStore) GetKnownExploitedVulnerabilities(cve string) ([]KnownExploitedVulnerabilityHandle, error) {\n\tif !s.kevEnabled {\n\t\t// capability incompatibilities should gracefully degrade, returning no data or errors\n\t\treturn nil, nil\n\t}\n\n\tfields := logger.Fields{\n\t\t\"cve\": cve,\n\t}\n\tstart := time.Now()\n\tvar count int\n\tdefer func() {\n\t\tfields[\"duration\"] = time.Since(start)\n\t\tfields[\"records\"] = count\n\t\tlog.WithFields(fields).Trace(\"fetched KEV records\")\n\t}()\n\n\tvar models []KnownExploitedVulnerabilityHandle\n\tvar results []*KnownExploitedVulnerabilityHandle\n\n\tif err := s.db.Where(\"cve = ? collate nocase\", cve).FindInBatches(&results, batchSize, func(_ *gorm.DB, _ int) error {\n\t\tvar blobs []blobable\n\t\tfor _, r := range results {\n\t\t\tblobs = append(blobs, r)\n\t\t}\n\t\tif err := s.blobStore.attachBlobValue(blobs...); err != nil {\n\t\t\treturn fmt.Errorf(\"unable to attach KEV blobs: %w\", err)\n\t\t}\n\n\t\tfor _, r := range results {\n\t\t\tmodels = append(models, *r)\n\t\t}\n\n\t\tcount += len(results)\n\n\t\treturn nil\n\t}).Error; err != nil {\n\t\treturn models, fmt.Errorf(\"unable to fetch KEV records: %w\", err)\n\t}\n\n\treturn models, nil\n}\n"
  },
  {
    "path": "grype/db/v6/vulnerability_decorator_store_test.go",
    "content": "package v6\n\nimport (\n\t\"reflect\"\n\t\"slices\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/grype/internal/schemaver\"\n)\n\nfunc TestVulnerabilityDecoratorStore(t *testing.T) {\n\ttests := []struct {\n\t\tname        string\n\t\tkevEnabled  bool\n\t\tsetupStore  func(*vulnerabilityDecoratorStore) error\n\t\tinput       []*KnownExploitedVulnerabilityHandle\n\t\texpectError require.ErrorAssertionFunc\n\t}{\n\t\t{\n\t\t\tname:       \"happy path - single KEV\",\n\t\t\tkevEnabled: true,\n\t\t\tinput: []*KnownExploitedVulnerabilityHandle{\n\t\t\t\t{\n\t\t\t\t\tCve: \"CVE-2023-1234\",\n\t\t\t\t\tBlobValue: &KnownExploitedVulnerabilityBlob{\n\t\t\t\t\t\tCve:           \"CVE-2023-1234\",\n\t\t\t\t\t\tVendorProject: \"Test Vendor\",\n\t\t\t\t\t\tProduct:       \"Test Product\",\n\t\t\t\t\t\tDateAdded:     timeRef(time.Now()),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:       \"happy path - multiple KEVs\",\n\t\t\tkevEnabled: true,\n\t\t\tinput: []*KnownExploitedVulnerabilityHandle{\n\t\t\t\t{\n\t\t\t\t\tCve: \"CVE-2023-1234\",\n\t\t\t\t\tBlobValue: &KnownExploitedVulnerabilityBlob{\n\t\t\t\t\t\tCve:           \"CVE-2023-1234\",\n\t\t\t\t\t\tVendorProject: \"Vendor 1\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tCve: \"CVE-2023-5678\",\n\t\t\t\t\tBlobValue: &KnownExploitedVulnerabilityBlob{\n\t\t\t\t\t\tCve:           \"CVE-2023-5678\",\n\t\t\t\t\t\tVendorProject: \"Vendor 2\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:        \"error - KEV disabled\",\n\t\t\tkevEnabled:  false,\n\t\t\tinput:       []*KnownExploitedVulnerabilityHandle{{Cve: \"CVE-2023-1234\"}},\n\t\t\texpectError: require.Error,\n\t\t},\n\t\t{\n\t\t\tname:       \"duplicate CVEs (unexpected but allowed)\",\n\t\t\tkevEnabled: true,\n\t\t\tinput: []*KnownExploitedVulnerabilityHandle{\n\t\t\t\t{\n\t\t\t\t\tCve: \"CVE-2023-1234\",\n\t\t\t\t\tBlobValue: &KnownExploitedVulnerabilityBlob{\n\t\t\t\t\t\tCve:            \"CVE-2023-1234\",\n\t\t\t\t\t\tRequiredAction: \"1\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tCve: \"CVE-2023-1234\",\n\t\t\t\t\tBlobValue: &KnownExploitedVulnerabilityBlob{\n\t\t\t\t\t\tCve:            \"CVE-2023-1234\",\n\t\t\t\t\t\tRequiredAction: \"2\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif tt.expectError == nil {\n\t\t\t\ttt.expectError = require.NoError\n\t\t\t}\n\n\t\t\tdb := setupTestStore(t).db\n\t\t\tbs := newBlobStore(db)\n\n\t\t\ts := &vulnerabilityDecoratorStore{\n\t\t\t\tdb:        db,\n\t\t\t\tblobStore: bs,\n\t\t\t\tvulnerabilityDecoratorCapabilities: vulnerabilityDecoratorCapabilities{\n\t\t\t\t\tkevEnabled: tt.kevEnabled,\n\t\t\t\t},\n\t\t\t}\n\n\t\t\tif tt.setupStore != nil {\n\t\t\t\trequire.NoError(t, tt.setupStore(s))\n\t\t\t}\n\n\t\t\terr := s.AddKnownExploitedVulnerabilities(tt.input...)\n\t\t\ttt.expectError(t, err)\n\t\t\tif err != nil {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tvar cves []string\n\t\t\tfor _, kev := range tt.input {\n\t\t\t\tif !slices.Contains(cves, kev.Cve) {\n\t\t\t\t\tcves = append(cves, kev.Cve)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar actual []*KnownExploitedVulnerabilityHandle\n\t\t\tfor _, cve := range cves {\n\t\t\t\tintermediate, err := s.GetKnownExploitedVulnerabilities(cve)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tfor i := range intermediate {\n\t\t\t\t\tactual = append(actual, &intermediate[i])\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tfor _, a := range actual {\n\t\t\t\tassert.NotZero(t, a.ID)\n\t\t\t\tassert.NotZero(t, a.BlobID)\n\t\t\t}\n\n\t\t\tif d := cmp.Diff(tt.input, actual); d != \"\" {\n\t\t\t\tt.Errorf(\"unexpected known exploited vulnerabilities (-expected, +actual): %s\", d)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestVulnerabilityDecoratorStore_AddKnownExploitedVulnerabilities_VersionCompatibility(t *testing.T) {\n\ttests := []struct {\n\t\tname          string\n\t\tdbVersion     schemaver.SchemaVer\n\t\tinput         []*KnownExploitedVulnerabilityHandle\n\t\texpectEnabled bool\n\t\texpectError   require.ErrorAssertionFunc\n\t\texpectedCount int\n\t}{\n\t\t{\n\t\t\tname:      \"supported db version\",\n\t\t\tdbVersion: schemaver.New(6, 0, 1),\n\t\t\tinput: []*KnownExploitedVulnerabilityHandle{\n\t\t\t\t{\n\t\t\t\t\tCve: \"CVE-2023-1234\",\n\t\t\t\t\tBlobValue: &KnownExploitedVulnerabilityBlob{\n\t\t\t\t\t\tCve:           \"CVE-2023-1234\",\n\t\t\t\t\t\tVendorProject: \"Test Vendor\",\n\t\t\t\t\t\tDateAdded:     timeRef(time.Now()),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectEnabled: true,\n\t\t\texpectError:   require.NoError,\n\t\t\texpectedCount: 1,\n\t\t},\n\t\t{\n\t\t\tname:      \"unsupported db version\",\n\t\t\tdbVersion: schemaver.New(6, 0, 0),\n\t\t\tinput: []*KnownExploitedVulnerabilityHandle{\n\t\t\t\t{\n\t\t\t\t\tCve: \"CVE-2023-1234\",\n\t\t\t\t\tBlobValue: &KnownExploitedVulnerabilityBlob{\n\t\t\t\t\t\tCve: \"CVE-2023-1234\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectEnabled: false,\n\t\t\texpectError:   require.Error,\n\t\t\texpectedCount: 0,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif tt.expectError == nil {\n\t\t\t\ttt.expectError = require.NoError\n\t\t\t}\n\n\t\t\tdb := setupTestStore(t).db\n\t\t\tbs := newBlobStore(db)\n\n\t\t\ts := newVulnerabilityDecoratorStore(db, bs, tt.dbVersion)\n\t\t\tassert.Equal(t, tt.expectEnabled, s.kevEnabled)\n\n\t\t\terr := s.AddKnownExploitedVulnerabilities(tt.input...)\n\t\t\ttt.expectError(t, err)\n\t\t\tif err != nil {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tresults, err := s.GetKnownExploitedVulnerabilities(tt.input[0].Cve)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Len(t, results, tt.expectedCount)\n\t\t})\n\t}\n}\n\nfunc TestVulnerabilityDecoratorStore_GetKnownExploitedVulnerabilities_VersionCompatibility(t *testing.T) {\n\ttests := []struct {\n\t\tname          string\n\t\tdbVersion     schemaver.SchemaVer\n\t\tinput         []*KnownExploitedVulnerabilityHandle\n\t\texpectEnabled bool\n\t\texpectError   require.ErrorAssertionFunc\n\t\texpectedCount int\n\t}{\n\t\t{\n\t\t\tname:      \"supported db version\",\n\t\t\tdbVersion: schemaver.New(6, 0, 1),\n\t\t\tinput: []*KnownExploitedVulnerabilityHandle{\n\t\t\t\t{\n\t\t\t\t\tCve: \"CVE-2023-1234\",\n\t\t\t\t\tBlobValue: &KnownExploitedVulnerabilityBlob{\n\t\t\t\t\t\tCve: \"CVE-2023-1234\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectEnabled: true,\n\t\t\texpectError:   require.NoError,\n\t\t\texpectedCount: 1,\n\t\t},\n\t\t{\n\t\t\tname:      \"unsupported db version\",\n\t\t\tdbVersion: schemaver.New(6, 0, 0),\n\t\t\tinput: []*KnownExploitedVulnerabilityHandle{\n\t\t\t\t{\n\t\t\t\t\tCve: \"CVE-2023-1234\",\n\t\t\t\t\tBlobValue: &KnownExploitedVulnerabilityBlob{\n\t\t\t\t\t\tCve: \"CVE-2023-1234\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectEnabled: false,\n\t\t\texpectError:   require.NoError,\n\t\t\texpectedCount: 0,\n\t\t},\n\t\t{\n\t\t\tname:      \"future db version\",\n\t\t\tdbVersion: schemaver.New(6, 1, 0),\n\t\t\tinput: []*KnownExploitedVulnerabilityHandle{\n\t\t\t\t{\n\t\t\t\t\tCve: \"CVE-2023-1234\",\n\t\t\t\t\tBlobValue: &KnownExploitedVulnerabilityBlob{\n\t\t\t\t\t\tCve: \"CVE-2023-1234\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectEnabled: true,\n\t\t\texpectError:   require.NoError,\n\t\t\texpectedCount: 1,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif tt.expectError == nil {\n\t\t\t\ttt.expectError = require.NoError\n\t\t\t}\n\n\t\t\tdb := setupTestStore(t).db\n\t\t\tbs := newBlobStore(db)\n\n\t\t\ts := newVulnerabilityDecoratorStore(db, bs, tt.dbVersion)\n\t\t\tassert.Equal(t, tt.expectEnabled, s.kevEnabled)\n\n\t\t\t// this is just to get around not being able to write entries...\n\t\t\tsupportedStore := newVulnerabilityDecoratorStore(db, bs, schemaver.New(6, 0, 1))\n\t\t\terr := supportedStore.AddKnownExploitedVulnerabilities(tt.input...)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tresults, err := s.GetKnownExploitedVulnerabilities(tt.input[0].Cve)\n\t\t\ttt.expectError(t, err)\n\t\t\tassert.Len(t, results, tt.expectedCount)\n\n\t\t\tif tt.expectedCount > 0 {\n\t\t\t\tfor _, result := range results {\n\t\t\t\t\tassert.NotNil(t, result.BlobValue)\n\t\t\t\t\tassert.Equal(t, tt.input[0].Cve, result.BlobValue.Cve)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestVulnerabilityDecoratorCapabilities_AllCapabilitiesCovered(t *testing.T) {\n\ttests := []struct {\n\t\tcapability          string\n\t\tminSupportedVersion schemaver.SchemaVer\n\t\tfieldName           string\n\t}{\n\t\t{\n\t\t\tcapability:          \"KEV\",\n\t\t\tminSupportedVersion: schemaver.New(6, 0, 1),\n\t\t\tfieldName:           \"kevEnabled\",\n\t\t},\n\t\t{\n\t\t\tcapability:          \"EPSS\",\n\t\t\tminSupportedVersion: schemaver.New(6, 0, 2),\n\t\t\tfieldName:           \"epssEnabled\",\n\t\t},\n\t\t{\n\t\t\tcapability:          \"CWE\",\n\t\t\tminSupportedVersion: schemaver.New(6, 1, 2),\n\t\t\tfieldName:           \"cweEnabled\",\n\t\t},\n\t}\n\n\t// Verify all fields in vulnerabilityDecoratorCapabilities are covered\n\tcapabilitiesType := reflect.TypeOf(vulnerabilityDecoratorCapabilities{})\n\tfieldCount := capabilitiesType.NumField()\n\n\tif fieldCount != len(tests) {\n\t\tt.Errorf(\"vulnerabilityDecoratorCapabilities has %d fields but only %d test cases. All fields must be covered.\", fieldCount, len(tests))\n\t}\n\n\tcoveredFields := make(map[string]bool)\n\tfor _, tt := range tests {\n\t\tcoveredFields[tt.fieldName] = true\n\t}\n\n\tfor i := 0; i < fieldCount; i++ {\n\t\tfield := capabilitiesType.Field(i)\n\t\tif !coveredFields[field.Name] {\n\t\t\tt.Errorf(\"Field %q in vulnerabilityDecoratorCapabilities is not covered by any test case\", field.Name)\n\t\t}\n\t}\n\n\t// Test each capability at lower, equal, and higher versions\n\tfor _, tt := range tests {\n\t\tt.Run(tt.capability+\"_version_checks\", func(t *testing.T) {\n\t\t\tdb := setupTestStore(t).db\n\t\t\tbs := newBlobStore(db)\n\n\t\t\t// Test 1: Lower version - capability should be OFF\n\t\t\tlowerVersion := schemaver.New(\n\t\t\t\ttt.minSupportedVersion.Model,\n\t\t\t\ttt.minSupportedVersion.Revision,\n\t\t\t\ttt.minSupportedVersion.Addition-1,\n\t\t\t)\n\t\t\tt.Run(\"lower_version\", func(t *testing.T) {\n\t\t\t\tstore := newVulnerabilityDecoratorStore(db, bs, lowerVersion)\n\t\t\t\tcapValue := reflect.ValueOf(store.vulnerabilityDecoratorCapabilities).FieldByName(tt.fieldName)\n\t\t\t\tassert.False(t, capValue.Bool(), \"capability %s should be disabled for version %s (below %s)\", tt.capability, lowerVersion, tt.minSupportedVersion)\n\t\t\t})\n\n\t\t\t// Test 2: Equal version - capability should be ON\n\t\t\tt.Run(\"equal_version\", func(t *testing.T) {\n\t\t\t\tstore := newVulnerabilityDecoratorStore(db, bs, tt.minSupportedVersion)\n\t\t\t\tcapValue := reflect.ValueOf(store.vulnerabilityDecoratorCapabilities).FieldByName(tt.fieldName)\n\t\t\t\tassert.True(t, capValue.Bool(), \"capability %s should be enabled for version %s\", tt.capability, tt.minSupportedVersion)\n\t\t\t})\n\n\t\t\t// Test 3: Higher version - capability should be ON\n\t\t\thigherVersion := schemaver.New(\n\t\t\t\ttt.minSupportedVersion.Model,\n\t\t\t\ttt.minSupportedVersion.Revision+1,\n\t\t\t\t0,\n\t\t\t)\n\t\t\tt.Run(\"higher_version\", func(t *testing.T) {\n\t\t\t\tstore := newVulnerabilityDecoratorStore(db, bs, higherVersion)\n\t\t\t\tcapValue := reflect.ValueOf(store.vulnerabilityDecoratorCapabilities).FieldByName(tt.fieldName)\n\t\t\t\tassert.True(t, capValue.Bool(), \"capability %s should be enabled for version %s (above %s)\", tt.capability, higherVersion, tt.minSupportedVersion)\n\t\t\t})\n\t\t})\n\t}\n}\n\nfunc timeRef(t time.Time) *time.Time {\n\treturn &t\n}\n"
  },
  {
    "path": "grype/db/v6/vulnerability_provider.go",
    "content": "package v6\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/hashicorp/go-multierror\"\n\t\"github.com/iancoleman/strcase\"\n\t\"github.com/scylladb/go-set/strset\"\n\n\t\"github.com/anchore/go-logger\"\n\t\"github.com/anchore/grype/grype/db/v6/name\"\n\t\"github.com/anchore/grype/grype/distro\"\n\t\"github.com/anchore/grype/grype/pkg\"\n\t\"github.com/anchore/grype/grype/search\"\n\t\"github.com/anchore/grype/grype/version\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n\t\"github.com/anchore/grype/internal/log\"\n\t\"github.com/anchore/syft/syft/cpe\"\n)\n\nvar (\n\t_ vulnerability.Provider              = (*vulnerabilityProvider)(nil)\n\t_ vulnerability.StoreMetadataProvider = (*vulnerabilityProvider)(nil)\n\t_ vulnerability.EOLChecker            = (*vulnerabilityProvider)(nil)\n)\n\nfunc NewVulnerabilityProvider(rdr Reader) vulnerability.Provider {\n\treturn &vulnerabilityProvider{\n\t\treader: rdr,\n\t}\n}\n\ntype vulnerabilityProvider struct {\n\treader Reader\n}\n\n// Deprecated: vulnerability.Vulnerability objects now have metadata included\nfunc (vp vulnerabilityProvider) VulnerabilityMetadata(ref vulnerability.Reference) (*vulnerability.Metadata, error) {\n\tvuln, ok := ref.Internal.(*VulnerabilityHandle)\n\tif !ok {\n\t\tvar err error\n\t\tvuln, err = vp.fetchVulnerability(ref)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tif vuln == nil {\n\t\tlog.WithFields(\"id\", ref.ID, \"namespace\", ref.Namespace).Debug(\"unable to find vulnerability for given reference\")\n\t\treturn &vulnerability.Metadata{\n\t\t\tID:         ref.ID,\n\t\t\tDataSource: strings.Split(ref.Namespace, \":\")[0],\n\t\t\tNamespace:  ref.Namespace,\n\t\t\tSeverity:   toSeverityString(vulnerability.UnknownSeverity),\n\t\t}, nil\n\t}\n\n\treturn vp.getVulnerabilityMetadata(vuln, ref.Namespace)\n}\n\nfunc (vp vulnerabilityProvider) getVulnerabilityMetadata(vuln *VulnerabilityHandle, namespace string) (*vulnerability.Metadata, error) {\n\tcves := getCVEs(vuln)\n\n\tkevs, err := vp.fetchKnownExploited(cves)\n\tif err != nil {\n\t\tlog.WithFields(\"id\", vuln.Name, \"vulnerability\", vuln.String(), \"error\", err).Debug(\"unable to fetch known exploited from vulnerability\")\n\t}\n\n\tepss, err := vp.fetchEpss(cves)\n\tif err != nil {\n\t\tlog.WithFields(\"id\", vuln.Name, \"vulnerability\", vuln.String(), \"error\", err).Debug(\"unable to fetch epss from vulnerability\")\n\t}\n\n\tcwes, err := vp.fetchCWE(cves)\n\tif err != nil {\n\t\tlog.WithFields(\"id\", vuln.Name, \"vulnerability\", vuln.String(), \"error\", err).Debug(\"unable to fetch cwes from vulnerability\")\n\t}\n\n\treturn newVulnerabilityMetadata(vuln, namespace, kevs, epss, cwes)\n}\n\nfunc (vp vulnerabilityProvider) fetchCWE(cves []string) ([]vulnerability.CWE, error) {\n\tvar out []vulnerability.CWE\n\tvar errs error\n\tfor _, cve := range cves {\n\t\tentries, err := vp.reader.GetCWEs(cve)\n\t\tif err != nil {\n\t\t\terrs = multierror.Append(errs, err)\n\t\t\tcontinue\n\t\t}\n\t\tfor _, entry := range entries {\n\t\t\tout = append(out, vulnerability.CWE{\n\t\t\t\tCVE:    entry.CVE,\n\t\t\t\tCWE:    entry.CWE,\n\t\t\t\tSource: entry.Source,\n\t\t\t\tType:   entry.Type,\n\t\t\t})\n\t\t}\n\t}\n\treturn out, errs\n}\n\nfunc newVulnerabilityMetadata(vuln *VulnerabilityHandle, namespace string, kevs []vulnerability.KnownExploited, epss []vulnerability.EPSS, cwes []vulnerability.CWE) (*vulnerability.Metadata, error) {\n\tif vuln == nil {\n\t\treturn nil, nil\n\t}\n\n\tsev, cvss, err := extractSeverities(vuln)\n\tif err != nil {\n\t\tlog.WithFields(\"id\", vuln.Name, \"vulnerability\", vuln.String()).Debug(\"unable to extract severity from vulnerability\")\n\t}\n\n\treturn &vulnerability.Metadata{\n\t\tID:             vuln.Name,\n\t\tDataSource:     firstReferenceURL(vuln),\n\t\tNamespace:      namespace,\n\t\tSeverity:       toSeverityString(sev),\n\t\tURLs:           lastReferenceURLs(vuln),\n\t\tDescription:    vuln.BlobValue.Description,\n\t\tCvss:           cvss,\n\t\tKnownExploited: kevs,\n\t\tEPSS:           epss,\n\t\tCWEs:           cwes,\n\t}, nil\n}\n\nfunc (vp vulnerabilityProvider) DataProvenance() (map[string]vulnerability.DataProvenance, error) {\n\tproviders, err := vp.reader.AllProviders()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdps := make(map[string]vulnerability.DataProvenance)\n\n\tfor _, p := range providers {\n\t\tvar date time.Time\n\t\tif p.DateCaptured != nil {\n\t\t\tdate = *p.DateCaptured\n\t\t}\n\t\tdps[p.ID] = vulnerability.DataProvenance{\n\t\t\tDateCaptured: date,\n\t\t\tInputDigest:  p.InputDigest,\n\t\t}\n\t}\n\treturn dps, nil\n}\n\nfunc (vp vulnerabilityProvider) fetchVulnerability(ref vulnerability.Reference) (*VulnerabilityHandle, error) {\n\tprovider := strings.Split(ref.Namespace, \":\")[0]\n\tvulns, err := vp.reader.GetVulnerabilities(&VulnerabilitySpecifier{Name: ref.ID, Providers: []string{provider}}, &GetVulnerabilityOptions{Preload: true})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif len(vulns) > 0 {\n\t\treturn &vulns[0], nil\n\t}\n\treturn nil, nil\n}\n\nfunc (vp vulnerabilityProvider) fetchKnownExploited(cves []string) ([]vulnerability.KnownExploited, error) {\n\tvar out []vulnerability.KnownExploited\n\tvar errs error\n\tfor _, cve := range cves {\n\t\tkevs, err := vp.reader.GetKnownExploitedVulnerabilities(cve)\n\t\tif err != nil {\n\t\t\terrs = multierror.Append(errs, err)\n\t\t\tcontinue\n\t\t}\n\t\tfor _, kev := range kevs {\n\t\t\tout = append(out, vulnerability.KnownExploited{\n\t\t\t\tCVE:                        kev.Cve,\n\t\t\t\tVendorProject:              kev.BlobValue.VendorProject,\n\t\t\t\tProduct:                    kev.BlobValue.Product,\n\t\t\t\tDateAdded:                  kev.BlobValue.DateAdded,\n\t\t\t\tRequiredAction:             kev.BlobValue.RequiredAction,\n\t\t\t\tDueDate:                    kev.BlobValue.DueDate,\n\t\t\t\tKnownRansomwareCampaignUse: kev.BlobValue.KnownRansomwareCampaignUse,\n\t\t\t\tNotes:                      kev.BlobValue.Notes,\n\t\t\t\tURLs:                       kev.BlobValue.URLs,\n\t\t\t\tCWEs:                       kev.BlobValue.CWEs,\n\t\t\t})\n\t\t}\n\t}\n\treturn out, errs\n}\n\nfunc (vp vulnerabilityProvider) fetchEpss(cves []string) ([]vulnerability.EPSS, error) {\n\tvar out []vulnerability.EPSS\n\tvar errs error\n\tfor _, cve := range cves {\n\t\tentries, err := vp.reader.GetEpss(cve)\n\t\tif err != nil {\n\t\t\terrs = multierror.Append(errs, err)\n\t\t\tcontinue\n\t\t}\n\t\tfor _, entry := range entries {\n\t\t\tout = append(out, vulnerability.EPSS{\n\t\t\t\tCVE:        entry.Cve,\n\t\t\t\tEPSS:       entry.Epss,\n\t\t\t\tPercentile: entry.Percentile,\n\t\t\t\tDate:       entry.Date,\n\t\t\t})\n\t\t}\n\t}\n\treturn out, errs\n}\n\nfunc (vp vulnerabilityProvider) PackageSearchNames(p pkg.Package) []string {\n\treturn name.PackageNames(p)\n}\n\nfunc (vp vulnerabilityProvider) Close() error {\n\treturn vp.reader.(io.Closer).Close()\n}\n\nfunc (vp vulnerabilityProvider) FindVulnerabilities(criteria ...vulnerability.Criteria) ([]vulnerability.Vulnerability, error) {\n\tif err := search.ValidateCriteria(criteria); err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar out []vulnerability.Vulnerability\n\tfor _, criteriaSet := range search.CriteriaIterator(criteria) {\n\t\t// parse criteria into a search query object\n\t\tquery, remainingCriteria, err := newSearchQuery(criteriaSet)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\t// fetch and process packages\n\t\tpkgVulns, err := vp.fetchAndProcessPackages(query)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\t// fetch and process CPEs\n\t\tcpeVulns, err := vp.fetchAndProcessCPEs(query)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\t// combine vulnerabilities\n\t\tvar vulns []vulnerability.Vulnerability\n\t\tvulns = append(vulns, pkgVulns...)\n\t\tvulns = append(vulns, cpeVulns...)\n\n\t\t// apply remaining filters\n\t\tvulns, err = vp.filterVulnerabilities(vulns, remainingCriteria...)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tout = append(out, vulns...)\n\t}\n\n\treturn out, nil\n}\n\n// fetchAndProcessPackages fetches packages and returns vulnerabilities directly\nfunc (vp vulnerabilityProvider) fetchAndProcessPackages(query *searchQuery) ([]vulnerability.Vulnerability, error) {\n\t// only fetch if we have package specifications or vulnerability specifications\n\tif query.pkgSpec == nil && len(query.vulnSpecs) == 0 {\n\t\treturn nil, nil\n\t}\n\n\tif query.unaffectedOnly {\n\t\treturn vp.fetchAndProcessUnaffectedPackages(query)\n\t}\n\treturn vp.fetchAndProcessAffectedPackages(query)\n}\n\n// fetchAndProcessAffectedPackages fetches affected packages and returns vulnerabilities\nfunc (vp vulnerabilityProvider) fetchAndProcessAffectedPackages(query *searchQuery) ([]vulnerability.Vulnerability, error) {\n\tvar vulns []vulnerability.Vulnerability\n\tmetadataCache := make(map[string]*vulnerability.Metadata)\n\n\taffectedPackages, err := vp.reader.GetAffectedPackages(query.pkgSpec, &GetPackageOptions{\n\t\tOSs:             query.osSpecs,\n\t\tVulnerabilities: query.vulnSpecs,\n\t\tPreloadBlob:     true,\n\t})\n\tif err != nil {\n\t\tif err = vp.handleOSError(err, query.osSpecs); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\t// if handleOSError returned nil, it means ErrOSNotPresent was handled\n\t\treturn vulns, nil\n\t}\n\n\taffectedPackages = filterAffectedPackageVersions(query.versionMatcher, affectedPackages)\n\n\tif err = fillAffectedPackageHandles(vp.reader, ptrs(affectedPackages)); err != nil {\n\t\treturn nil, err\n\t}\n\n\tvulns, err = vp.processAffectedPackageHandles(vulns, affectedPackages, metadataCache)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn vulns, nil\n}\n\n// fetchAndProcessUnaffectedPackages fetches unaffected packages and returns vulnerabilities\nfunc (vp vulnerabilityProvider) fetchAndProcessUnaffectedPackages(query *searchQuery) ([]vulnerability.Vulnerability, error) {\n\tvar vulns []vulnerability.Vulnerability\n\tmetadataCache := make(map[string]*vulnerability.Metadata)\n\n\tunaffectedPackages, err := vp.reader.GetUnaffectedPackages(query.pkgSpec, &GetPackageOptions{\n\t\tOSs:             query.osSpecs,\n\t\tVulnerabilities: query.vulnSpecs,\n\t\tPreloadBlob:     true,\n\t})\n\tif err != nil {\n\t\tif err = vp.handleOSError(err, query.osSpecs); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\t// if handleOSError returned nil, it means ErrOSNotPresent was handled\n\t\treturn vulns, nil\n\t}\n\n\tunaffectedPackages = filterUnaffectedPackageVersions(query.versionMatcher, unaffectedPackages)\n\n\tif err = fillUnaffectedPackageHandles(vp.reader, ptrs(unaffectedPackages)); err != nil {\n\t\treturn nil, err\n\t}\n\n\tvulns, err = vp.processUnaffectedPackageHandles(vulns, unaffectedPackages, metadataCache)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn vulns, nil\n}\n\n// handleOSError handles the common pattern of checking for ErrOSNotPresent\nfunc (vp vulnerabilityProvider) handleOSError(err error, osSpecs OSSpecifiers) error {\n\tif err == nil {\n\t\treturn nil\n\t}\n\tif errors.Is(err, ErrOSNotPresent) {\n\t\tlog.WithFields(\"os\", osSpecs).Debug(\"no OS found in the DB for the given criteria\")\n\t\treturn nil\n\t}\n\treturn err\n}\n\n// GetOperatingSystemEOL returns the EOL and EOAS dates for the given distro.\n// Uses exact matching (no aliasing) since each distro has its own EOL dates\n// (e.g., CentOS EOL != RHEL EOL even though CentOS aliases to RHEL for vulns).\nfunc (vp vulnerabilityProvider) GetOperatingSystemEOL(d *distro.Distro) (eolDate, eoasDate *time.Time, err error) {\n\tif d == nil {\n\t\treturn nil, nil, nil\n\t}\n\n\tspec := OSSpecifier{\n\t\tName:             d.Name(),\n\t\tMajorVersion:     d.MajorVersion(),\n\t\tMinorVersion:     d.MinorVersion(),\n\t\tRemainingVersion: d.RemainingVersion(),\n\t\tLabelVersion:     d.LabelVersion(),\n\t\tDisableAliasing:  true, // EOL lookups must use exact distro match\n\t\tDisableFallback:  true, // don't fall back to major-only matching (e.g. Alpine 3.24 shouldn't match EOL'd 3.12)\n\t}\n\n\tresults, err := vp.reader.GetOperatingSystems(spec)\n\tif err != nil {\n\t\tif errors.Is(err, ErrMissingOSIdentification) {\n\t\t\treturn nil, nil, nil\n\t\t}\n\t\treturn nil, nil, err\n\t}\n\n\tif len(results) == 0 {\n\t\treturn nil, nil, nil\n\t}\n\n\tif len(results) > 1 {\n\t\tlog.WithFields(\"distro\", d.String(), \"matches\", len(results)).Debug(\"multiple OS records matched for EOL lookup, using first match\")\n\t}\n\n\t// Return EOL data from the first matching OS record\n\tos := results[0]\n\treturn os.EOLDate, os.EOASDate, nil\n}\n\n// fetchAndProcessCPEs fetches CPEs and returns vulnerabilities directly\nfunc (vp vulnerabilityProvider) fetchAndProcessCPEs(query *searchQuery) ([]vulnerability.Vulnerability, error) {\n\tvar vulns []vulnerability.Vulnerability\n\tmetadataCache := make(map[string]*vulnerability.Metadata)\n\n\t// only fetch if we have CPE specifications\n\tif query.cpeSpec == nil {\n\t\treturn vulns, nil\n\t}\n\n\tif query.unaffectedOnly {\n\t\tunaffectedCPEs, err := vp.reader.GetUnaffectedCPEs(query.cpeSpec, &GetCPEOptions{\n\t\t\tVulnerabilities: query.vulnSpecs,\n\t\t\tPreloadBlob:     true,\n\t\t})\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tunaffectedCPEs = filterUnaffectedCPEVersions(query.versionMatcher, unaffectedCPEs, query.cpeSpec)\n\n\t\tif err = fillUnaffectedCPEHandles(vp.reader, ptrs(unaffectedCPEs)); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tvulns, err = vp.processUnaffectedCPEHandles(vulns, unaffectedCPEs, metadataCache)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t} else {\n\t\taffectedCPEs, err := vp.reader.GetAffectedCPEs(query.cpeSpec, &GetCPEOptions{\n\t\t\tVulnerabilities: query.vulnSpecs,\n\t\t\tPreloadBlob:     true,\n\t\t})\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\taffectedCPEs = filterAffectedCPEVersions(query.versionMatcher, affectedCPEs, query.cpeSpec)\n\n\t\tif err = fillAffectedCPEHandles(vp.reader, ptrs(affectedCPEs)); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tvulns, err = vp.processAffectedCPEHandles(vulns, affectedCPEs, metadataCache)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn vulns, nil\n}\n\nfunc (vp vulnerabilityProvider) filterVulnerabilities(vulns []vulnerability.Vulnerability, criteria ...vulnerability.Criteria) ([]vulnerability.Vulnerability, error) {\n\tisMatch := func(v vulnerability.Vulnerability) (bool, error) {\n\t\tfor _, c := range criteria {\n\t\t\tif _, ok := c.(search.VersionConstraintMatcher); ok {\n\t\t\t\tcontinue // already run\n\t\t\t}\n\t\t\tmatches, reason, err := c.MatchesVulnerability(v)\n\t\t\tif !matches || err != nil {\n\t\t\t\tfields := logger.Fields{\n\t\t\t\t\t\"vulnerability\": v,\n\t\t\t\t}\n\t\t\t\tif err != nil {\n\t\t\t\t\tfields[\"error\"] = err\n\t\t\t\t}\n\n\t\t\t\tlogDroppedVulnerability(v.ID, reason, fields)\n\t\t\t\treturn false, err\n\t\t\t}\n\t\t}\n\t\treturn true, nil\n\t}\n\tfor i := 0; i < len(vulns); i++ {\n\t\tmatches, err := isMatch(vulns[i])\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif !matches {\n\t\t\tvulns = append(vulns[0:i], vulns[i+1:]...)\n\t\t\ti--\n\t\t}\n\t}\n\treturn vulns, nil\n}\n\n// processAffectedPackageHandles processes affected package handles and adds them to the vulnerabilities list\nfunc (vp vulnerabilityProvider) processAffectedPackageHandles(out []vulnerability.Vulnerability, handles []AffectedPackageHandle, metadataCache map[string]*vulnerability.Metadata) ([]vulnerability.Vulnerability, error) {\n\tfor _, ph := range handles {\n\t\tif ph.BlobValue == nil {\n\t\t\tlog.Debugf(\"unable to find blobValue for %+v\", ph)\n\t\t\tcontinue\n\t\t}\n\t\tv, err := newVulnerabilityFromAffectedPackageHandle(ph, ph.BlobValue.Ranges)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif v == nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tmeta, err := vp.getCachedMetadata(ph.Vulnerability, v.Namespace, metadataCache)\n\t\tif err != nil {\n\t\t\tlog.WithFields(\"error\", err, \"vulnerability\", v.String()).Debug(\"unable to fetch metadata for vulnerability\")\n\t\t} else {\n\t\t\tv.Metadata = meta\n\t\t}\n\n\t\tout = append(out, *v)\n\t}\n\treturn out, nil\n}\n\n// processAffectedCPEHandles processes affected CPE handles and adds them to the vulnerabilities list\nfunc (vp vulnerabilityProvider) processAffectedCPEHandles(out []vulnerability.Vulnerability, handles []AffectedCPEHandle, metadataCache map[string]*vulnerability.Metadata) ([]vulnerability.Vulnerability, error) {\n\tfor _, ch := range handles {\n\t\tif ch.BlobValue == nil {\n\t\t\tlog.Debugf(\"unable to find blobValue for %+v\", ch)\n\t\t\tcontinue\n\t\t}\n\t\tv, err := newVulnerabilityFromAffectedCPEHandle(ch, ch.BlobValue.Ranges)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif v == nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tmeta, err := vp.getCachedMetadata(ch.Vulnerability, v.Namespace, metadataCache)\n\t\tif err != nil {\n\t\t\tlog.WithFields(\"error\", err, \"vulnerability\", v.String()).Debug(\"unable to fetch metadata for vulnerability\")\n\t\t} else {\n\t\t\tv.Metadata = meta\n\t\t}\n\n\t\tout = append(out, *v)\n\t}\n\treturn out, nil\n}\n\n// processUnaffectedPackageHandles processes unaffected package handles and adds them to the vulnerabilities list\nfunc (vp vulnerabilityProvider) processUnaffectedPackageHandles(out []vulnerability.Vulnerability, handles []UnaffectedPackageHandle, metadataCache map[string]*vulnerability.Metadata) ([]vulnerability.Vulnerability, error) {\n\tfor _, ph := range handles {\n\t\tif ph.BlobValue == nil {\n\t\t\tlog.Debugf(\"unable to find blobValue for %+v\", ph)\n\t\t\tcontinue\n\t\t}\n\t\tv, err := newVulnerabilityFromUnaffectedPackageHandle(ph, ph.BlobValue.Ranges)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif v == nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tmeta, err := vp.getCachedMetadata(ph.Vulnerability, v.Namespace, metadataCache)\n\t\tif err != nil {\n\t\t\tlog.WithFields(\"error\", err, \"vulnerability\", v.String()).Debug(\"unable to fetch metadata for vulnerability\")\n\t\t} else {\n\t\t\tv.Metadata = meta\n\t\t}\n\n\t\tout = append(out, *v)\n\t}\n\treturn out, nil\n}\n\n// processUnaffectedCPEHandles processes unaffected CPE handles and adds them to the vulnerabilities list\nfunc (vp vulnerabilityProvider) processUnaffectedCPEHandles(out []vulnerability.Vulnerability, handles []UnaffectedCPEHandle, metadataCache map[string]*vulnerability.Metadata) ([]vulnerability.Vulnerability, error) {\n\tfor _, ch := range handles {\n\t\tif ch.BlobValue == nil {\n\t\t\tlog.Debugf(\"unable to find blobValue for %+v\", ch)\n\t\t\tcontinue\n\t\t}\n\t\tv, err := newVulnerabilityFromUnaffectedCPEHandle(ch, ch.BlobValue.Ranges)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif v == nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tmeta, err := vp.getCachedMetadata(ch.Vulnerability, v.Namespace, metadataCache)\n\t\tif err != nil {\n\t\t\tlog.WithFields(\"error\", err, \"vulnerability\", v.String()).Debug(\"unable to fetch metadata for vulnerability\")\n\t\t} else {\n\t\t\tv.Metadata = meta\n\t\t}\n\n\t\tout = append(out, *v)\n\t}\n\treturn out, nil\n}\n\n// getCachedMetadata retrieves metadata from cache or fetches it if not cached\nfunc (vp vulnerabilityProvider) getCachedMetadata(vuln *VulnerabilityHandle, namespace string, metadataCache map[string]*vulnerability.Metadata) (*vulnerability.Metadata, error) {\n\tif vuln == nil {\n\t\treturn nil, nil\n\t}\n\n\tif metadata, ok := metadataCache[vuln.Name]; ok {\n\t\treturn metadata, nil\n\t}\n\n\tmetadata, err := vp.getVulnerabilityMetadata(vuln, namespace)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tmetadataCache[vuln.Name] = metadata\n\treturn metadata, nil\n}\n\nfunc filterAffectedPackageVersions(constraintMatcher search.VersionConstraintMatcher, packages []AffectedPackageHandle) []AffectedPackageHandle {\n\t// no constraint matcher, just return all packages\n\tif constraintMatcher == nil {\n\t\treturn packages\n\t}\n\tvar out []AffectedPackageHandle\n\tfor packageIdx := 0; packageIdx < len(packages); packageIdx++ {\n\t\thandle := packages[packageIdx]\n\t\tvuln := handle.vulnerability()\n\t\tallDropped, unmatchedConstraints := filterAffectedPackageRanges(constraintMatcher, handle.BlobValue)\n\t\tif !allDropped {\n\t\t\tout = append(out, handle)\n\t\t\tcontinue // keep this handle\n\t\t}\n\n\t\treason := fmt.Sprintf(\"not within vulnerability version constraints: %q\", strings.Join(unmatchedConstraints, \", \"))\n\t\tf := make(logger.Fields)\n\t\tif handle.Package != nil {\n\t\t\tf[\"package\"] = handle.Package.String()\n\t\t} else {\n\t\t\tf[\"affectedPackage\"] = handle\n\t\t}\n\n\t\tlogDroppedVulnerability(vuln, reason, f)\n\t}\n\treturn out\n}\n\nfunc filterUnaffectedPackageVersions(constraintMatcher search.VersionConstraintMatcher, packages []UnaffectedPackageHandle) []UnaffectedPackageHandle {\n\t// no constraint matcher, just return all packages\n\tif constraintMatcher == nil {\n\t\treturn packages\n\t}\n\tvar out []UnaffectedPackageHandle\n\tfor packageIdx := 0; packageIdx < len(packages); packageIdx++ {\n\t\thandle := packages[packageIdx]\n\t\tvuln := handle.vulnerability()\n\t\tpkgHandle := handle.getPackageHandle()\n\t\tallDropped, unmatchedConstraints := filterAffectedPackageRanges(constraintMatcher, pkgHandle.BlobValue)\n\t\tif !allDropped {\n\t\t\tout = append(out, handle)\n\t\t\tcontinue // keep this handle\n\t\t}\n\n\t\treason := fmt.Sprintf(\"not within vulnerability version constraints: %q\", strings.Join(unmatchedConstraints, \", \"))\n\t\tf := make(logger.Fields)\n\t\tif pkgHandle.Package != nil {\n\t\t\tf[\"package\"] = pkgHandle.Package.String()\n\t\t} else {\n\t\t\tf[\"unaffectedPackage\"] = handle\n\t\t}\n\n\t\tlogDroppedVulnerability(vuln, reason, f)\n\t}\n\treturn out\n}\n\nfunc filterAffectedCPEVersions(constraintMatcher search.VersionConstraintMatcher, handles []AffectedCPEHandle, cpeSpec *cpe.Attributes) []AffectedCPEHandle {\n\t// no constraint matcher, just return all packages\n\tif constraintMatcher == nil {\n\t\treturn handles\n\t}\n\tvar out []AffectedCPEHandle\n\tfor i := range handles {\n\t\thandle := handles[i]\n\t\tvuln := handle.vulnerability()\n\t\tallDropped, unmatchedConstraints := filterAffectedPackageRanges(constraintMatcher, handle.BlobValue)\n\t\tif !allDropped {\n\t\t\tout = append(out, handle)\n\t\t\tcontinue // keep this handle\n\t\t}\n\n\t\treason := fmt.Sprintf(\"not within vulnerability version constraints: %q\", strings.Join(unmatchedConstraints, \", \"))\n\t\tlogDroppedVulnerability(vuln, reason, logger.Fields{\n\t\t\t\"cpe\": cpeSpec.String(),\n\t\t})\n\t}\n\treturn out\n}\n\nfunc filterUnaffectedCPEVersions(constraintMatcher search.VersionConstraintMatcher, handles []UnaffectedCPEHandle, cpeSpec *cpe.Attributes) []UnaffectedCPEHandle {\n\t// no constraint matcher, just return all packages\n\tif constraintMatcher == nil {\n\t\treturn handles\n\t}\n\tvar out []UnaffectedCPEHandle\n\tfor i := range handles {\n\t\thandle := handles[i]\n\t\tvuln := handle.vulnerability()\n\t\tallDropped, unmatchedConstraints := filterAffectedPackageRanges(constraintMatcher, handle.BlobValue)\n\t\tif !allDropped {\n\t\t\tout = append(out, handle)\n\t\t\tcontinue // keep this handle\n\t\t}\n\n\t\treason := fmt.Sprintf(\"not within vulnerability version constraints: %q\", strings.Join(unmatchedConstraints, \", \"))\n\t\tlogDroppedVulnerability(vuln, reason, logger.Fields{\n\t\t\t\"cpe\": cpeSpec.String(),\n\t\t})\n\t}\n\treturn out\n}\n\n// filterAffectedPackageRanges returns true if all ranges removed\nfunc filterAffectedPackageRanges(matcher search.VersionConstraintMatcher, b *PackageBlob) (bool, []string) {\n\tif len(b.Ranges) == 0 {\n\t\t// no ranges means that we're implicitly vulnerable to all versions\n\t\treturn false, nil\n\t}\n\tvar unmatchedConstraints []string\n\tfor _, r := range b.Ranges {\n\t\tv := r.Version\n\t\tformat := version.ParseFormat(v.Type)\n\t\tconstraint, err := version.GetConstraint(v.Constraint, format)\n\t\tif err != nil || constraint == nil {\n\t\t\tlog.WithFields(\"error\", err, \"constraint\", v.Constraint, \"format\", v.Type).Debug(\"unable to parse constraint\")\n\t\t\tcontinue\n\t\t}\n\t\tmatches, err := matcher.MatchesConstraint(constraint)\n\t\tif err != nil {\n\t\t\tlog.WithFields(\"error\", err, \"constraint\", v.Constraint, \"format\", v.Type).Debug(\"match constraint error\")\n\t\t}\n\t\tif matches {\n\t\t\tcontinue\n\t\t}\n\t\tunmatchedConstraints = append(unmatchedConstraints, v.Constraint)\n\t}\n\treturn len(b.Ranges) == len(unmatchedConstraints), unmatchedConstraints\n}\n\nfunc toSeverityString(sev vulnerability.Severity) string {\n\treturn strcase.ToCamel(sev.String())\n}\n\n// returns the first reference url to populate the DataSource\nfunc firstReferenceURL(vuln *VulnerabilityHandle) string {\n\tfor _, v := range vuln.BlobValue.References {\n\t\treturn v.URL\n\t}\n\treturn \"\"\n}\n\n// skip the first reference URL and return the remainder to populate the URLs\nfunc lastReferenceURLs(vuln *VulnerabilityHandle) []string {\n\tvar out []string\n\tfor i, v := range vuln.BlobValue.References {\n\t\tif i == 0 {\n\t\t\tcontinue\n\t\t}\n\t\tout = append(out, v.URL)\n\t}\n\treturn out\n}\n\nfunc getCVEs(vuln *VulnerabilityHandle) []string {\n\tvar cves []string\n\tset := strset.New()\n\n\taddCVE := func(id string) {\n\t\tlower := strings.ToLower(id)\n\t\tif strings.HasPrefix(lower, \"cve-\") {\n\t\t\tif !set.Has(lower) {\n\t\t\t\tcves = append(cves, id)\n\t\t\t\tset.Add(lower)\n\t\t\t}\n\t\t}\n\t}\n\n\tif vuln == nil {\n\t\treturn cves\n\t}\n\n\taddCVE(vuln.Name)\n\n\tif vuln.BlobValue == nil {\n\t\treturn cves\n\t}\n\n\taddCVE(vuln.BlobValue.ID)\n\n\tfor _, alias := range vuln.BlobValue.Aliases {\n\t\taddCVE(alias)\n\t}\n\n\treturn cves\n}\n"
  },
  {
    "path": "grype/db/v6/vulnerability_provider_mocks_test.go",
    "content": "package v6\n\nimport (\n\t\"encoding/hex\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/require\"\n\n\tv5 \"github.com/anchore/grype/grype/db/v5\"\n\t\"github.com/anchore/grype/grype/db/v5/namespace\"\n\tdistroNs \"github.com/anchore/grype/grype/db/v5/namespace/distro\"\n\t\"github.com/anchore/grype/grype/db/v5/namespace/language\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n\t\"github.com/anchore/grype/internal/log\"\n\t\"github.com/anchore/syft/syft/cpe\"\n)\n\nfunc testVulnerabilityProvider(t *testing.T) vulnerability.Provider {\n\tt.Helper()\n\ttmp := t.TempDir()\n\tw, err := NewWriter(Config{\n\t\tDBDirPath: tmp,\n\t})\n\tdefer log.CloseAndLogError(w, tmp)\n\trequire.NoError(t, err)\n\n\taDayAgo := time.Now().Add(-1 * 24 * time.Hour)\n\taWeekAgo := time.Now().Add(-7 * 24 * time.Hour)\n\ttwoWeeksAgo := time.Now().Add(-14 * 24 * time.Hour)\n\n\tdebianProvider := &Provider{\n\t\tID:           \"debian\",\n\t\tVersion:      \"1\",\n\t\tProcessor:    \"debian-processor\",\n\t\tDateCaptured: &aDayAgo,\n\t\tInputDigest:  hex.EncodeToString([]byte(\"debian\")),\n\t}\n\n\tnvdProvider := &Provider{\n\t\tID:           \"nvd\",\n\t\tVersion:      \"1\",\n\t\tProcessor:    \"nvd-processor\",\n\t\tDateCaptured: &aDayAgo,\n\t\tInputDigest:  hex.EncodeToString([]byte(\"nvd\")),\n\t}\n\n\tv5vulns := []v5.Vulnerability{\n\t\t// neutron\n\t\t{\n\t\t\tPackageName:       \"neutron\",\n\t\t\tNamespace:         \"debian:distro:debian:8\",\n\t\t\tVersionConstraint: \"< 2014.1.3-6\",\n\t\t\tID:                \"CVE-2014-fake-1\",\n\t\t\tVersionFormat:     \"deb\",\n\t\t},\n\t\t{\n\t\t\tPackageName:       \"neutron\",\n\t\t\tNamespace:         \"debian:distro:debian:8\",\n\t\t\tVersionConstraint: \"< 2013.0.2-1\",\n\t\t\tID:                \"CVE-2013-fake-2\",\n\t\t\tVersionFormat:     \"deb\",\n\t\t},\n\t\t// poison the well! this is not a valid entry, but we want the matching process to survive and find other good results...\n\t\t{\n\t\t\tPackageName:       \"neutron\",\n\t\t\tNamespace:         \"debian:distro:debian:8\",\n\t\t\tVersionConstraint: \"< 70.3.0-rc0\", // intentionally bad value\n\t\t\tID:                \"CVE-2014-fake-3\",\n\t\t\tVersionFormat:     \"apk\",\n\t\t},\n\n\t\t// activerecord\n\t\t{\n\t\t\tPackageName:       \"activerecord\",\n\t\t\tNamespace:         \"nvd:cpe\",\n\t\t\tVersionConstraint: \"< 3.7.6\",\n\t\t\tID:                \"CVE-2014-fake-3\",\n\t\t\tVersionFormat:     \"unknown\",\n\t\t\tCPEs: []string{\n\t\t\t\t\"cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tPackageName:       \"activerecord\",\n\t\t\tNamespace:         \"nvd:cpe\",\n\t\t\tVersionConstraint: \"< 3.7.4\",\n\t\t\tID:                \"CVE-2014-fake-4\",\n\t\t\tVersionFormat:     \"unknown\",\n\t\t\tCPEs: []string{\n\t\t\t\t\"cpe:2.3:*:activerecord:activerecord:*:*:something:*:*:ruby:*:*\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tPackageName:       \"activerecord\",\n\t\t\tNamespace:         \"nvd:cpe\",\n\t\t\tVersionConstraint: \"= 4.0.1\",\n\t\t\tID:                \"CVE-2014-fake-5\",\n\t\t\tVersionFormat:     \"unknown\",\n\t\t\tCPEs: []string{\n\t\t\t\t\"cpe:2.3:*:couldntgetthisrightcouldyou:activerecord:4.0.1:*:*:*:*:*:*:*\", // shouldn't match on this\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tPackageName:       \"activerecord\",\n\t\t\tNamespace:         \"nvd:cpe\",\n\t\t\tVersionConstraint: \"< 98SP3\",\n\t\t\tID:                \"CVE-2014-fake-6\",\n\t\t\tVersionFormat:     \"unknown\",\n\t\t\tCPEs: []string{\n\t\t\t\t\"cpe:2.3:*:awesome:awesome:*:*:*:*:*:*:*:*\", // shouldn't match on this\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tPackageName:       \"Newtonsoft.Json\",\n\t\t\tNamespace:         \"github:language:dotnet\",\n\t\t\tID:                \"GHSA-5crp-9r3c-p9vr\",\n\t\t\tVersionFormat:     \"unknown\",\n\t\t\tVersionConstraint: \"<13.0.1\",\n\t\t},\n\t\t// poison the well! this is not a valid entry, but we want the matching process to survive and find other good results...\n\t\t{\n\t\t\tPackageName:       \"activerecord\",\n\t\t\tNamespace:         \"nvd:cpe\",\n\t\t\tVersionConstraint: \"< 70.3.0-rc0\", // intentionally bad value\n\t\t\tID:                \"CVE-2014-fake-7\",\n\t\t\tVersionFormat:     \"apk\",\n\t\t\tCPEs: []string{\n\t\t\t\t\"cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*\",\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, v := range v5vulns {\n\t\tvar os *OperatingSystem\n\t\tprov := nvdProvider\n\n\t\tswitch v.Namespace {\n\t\tcase \"nvd:cpe\":\n\t\tcase \"debian:distro:debian:8\":\n\t\t\tprov = debianProvider\n\t\t\tos = &OperatingSystem{\n\t\t\t\tName:         \"debian\",\n\t\t\t\tMajorVersion: \"8\",\n\t\t\t}\n\t\t}\n\n\t\tvuln := &VulnerabilityHandle{\n\t\t\tID:            0,\n\t\t\tName:          v.ID,\n\t\t\tStatus:        \"\",\n\t\t\tPublishedDate: &twoWeeksAgo,\n\t\t\tModifiedDate:  &aWeekAgo,\n\t\t\tWithdrawnDate: nil,\n\t\t\tProviderID:    prov.ID,\n\t\t\tProvider:      prov,\n\t\t\tBlobID:        0,\n\t\t\tBlobValue: &VulnerabilityBlob{\n\t\t\t\tID:          v.ID,\n\t\t\t\tAssigners:   []string{v.ID + \"-assigner-1\", v.ID + \"-assigner-2\"},\n\t\t\t\tDescription: v.ID + \"-description\",\n\t\t\t\tReferences: []Reference{\n\t\t\t\t\t{\n\t\t\t\t\t\tURL:  \"http://somewhere/\" + v.ID,\n\t\t\t\t\t\tTags: []string{v.ID + \"-tag-1\", v.ID + \"-tag-2\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t//Aliases: []string{\"GHSA-\" + v.ID},\n\t\t\t\tSeverities: []Severity{\n\t\t\t\t\t{\n\t\t\t\t\t\tScheme: SeveritySchemeCVSS,\n\t\t\t\t\t\tValue:  \"high\",\n\t\t\t\t\t\tSource: \"\",\n\t\t\t\t\t\tRank:   0,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\n\t\terr = w.AddVulnerabilities(vuln)\n\t\trequire.NoError(t, err)\n\n\t\tvar cpes []Cpe\n\t\tfor _, c := range v.CPEs {\n\t\t\tcp, err := cpe.New(c, \"\")\n\t\t\trequire.NoError(t, err)\n\t\t\tcpes = append(cpes, Cpe{\n\t\t\t\tPart:            cp.Attributes.Part,\n\t\t\t\tVendor:          cp.Attributes.Vendor,\n\t\t\t\tProduct:         cp.Attributes.Product,\n\t\t\t\tEdition:         cp.Attributes.Edition,\n\t\t\t\tLanguage:        cp.Attributes.Language,\n\t\t\t\tSoftwareEdition: cp.Attributes.SWEdition,\n\t\t\t\tTargetHardware:  cp.Attributes.TargetHW,\n\t\t\t\tTargetSoftware:  cp.Attributes.TargetSW,\n\t\t\t\tOther:           cp.Attributes.Other,\n\t\t\t})\n\t\t}\n\n\t\tpackageType := \"\"\n\n\t\tns, err := namespace.FromString(v.Namespace)\n\t\trequire.NoError(t, err)\n\n\t\td, _ := ns.(*distroNs.Namespace)\n\t\tif d != nil {\n\t\t\tpackageType = string(d.DistroType())\n\t\t}\n\t\tlang, _ := ns.(*language.Namespace)\n\t\tif lang != nil {\n\t\t\tpackageType = string(lang.Language())\n\t\t}\n\n\t\tpkg := &Package{\n\t\t\tID:        0,\n\t\t\tEcosystem: packageType,\n\t\t\tName:      v.PackageName,\n\t\t\t//CPEs: cpes,\n\t\t}\n\n\t\tap := &AffectedPackageHandle{\n\t\t\tID:                0,\n\t\t\tVulnerabilityID:   0,\n\t\t\tVulnerability:     vuln,\n\t\t\tOperatingSystemID: nil,\n\t\t\tOperatingSystem:   os,\n\t\t\tPackageID:         0,\n\t\t\tPackage:           pkg,\n\t\t\tBlobID:            0,\n\t\t\tBlobValue: &PackageBlob{\n\t\t\t\tCVEs:       nil,\n\t\t\t\tQualifiers: nil,\n\t\t\t\tRanges: []Range{\n\t\t\t\t\t{\n\t\t\t\t\t\tFix: nil,\n\t\t\t\t\t\tVersion: Version{\n\t\t\t\t\t\t\tType:       v.VersionFormat,\n\t\t\t\t\t\t\tConstraint: v.VersionConstraint,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\n\t\terr = w.AddAffectedPackages(ap)\n\t\trequire.NoError(t, err)\n\n\t\tfor _, c := range cpes {\n\t\t\tac := &AffectedCPEHandle{\n\t\t\t\tVulnerability: vuln,\n\t\t\t\tCPE:           &c,\n\t\t\t\tBlobValue: &PackageBlob{\n\t\t\t\t\tRanges: []Range{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tVersion: Version{\n\t\t\t\t\t\t\t\tType:       v.VersionFormat,\n\t\t\t\t\t\t\t\tConstraint: v.VersionConstraint,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\n\t\t\terr = w.AddAffectedCPEs(ac)\n\t\t\trequire.NoError(t, err)\n\t\t}\n\t}\n\n\t// add unaffected packages for testing unaffected stores\n\tunaffectedVuln := &VulnerabilityHandle{\n\t\tID:            0,\n\t\tName:          \"CVE-2024-unaffected-test\",\n\t\tStatus:        \"\",\n\t\tPublishedDate: &twoWeeksAgo,\n\t\tModifiedDate:  &aWeekAgo,\n\t\tWithdrawnDate: nil,\n\t\tProviderID:    nvdProvider.ID,\n\t\tProvider:      nvdProvider,\n\t\tBlobID:        0,\n\t\tBlobValue: &VulnerabilityBlob{\n\t\t\tID:          \"CVE-2024-unaffected-test\",\n\t\t\tAssigners:   []string{\"CVE-2024-unaffected-test-assigner-1\"},\n\t\t\tDescription: \"CVE-2024-unaffected-test-description\",\n\t\t\tReferences: []Reference{\n\t\t\t\t{\n\t\t\t\t\tURL:  \"http://somewhere/CVE-2024-unaffected-test\",\n\t\t\t\t\tTags: []string{\"CVE-2024-unaffected-test-tag-1\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tSeverities: []Severity{\n\t\t\t\t{\n\t\t\t\t\tScheme: SeveritySchemeCVSS,\n\t\t\t\t\tValue:  \"medium\",\n\t\t\t\t\tSource: \"\",\n\t\t\t\t\tRank:   0,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\terr = w.AddVulnerabilities(unaffectedVuln)\n\trequire.NoError(t, err)\n\n\t// add unaffected package: test-unaffected-package\n\ttestUnaffectedPkg := &Package{\n\t\tID:        0,\n\t\tEcosystem: \"deb\",\n\t\tName:      \"test-unaffected-package\",\n\t}\n\n\ttestUnaffectedPackageHandle := &UnaffectedPackageHandle{\n\t\tID:                0,\n\t\tVulnerabilityID:   0,\n\t\tVulnerability:     unaffectedVuln,\n\t\tOperatingSystemID: nil,\n\t\tOperatingSystem:   nil,\n\t\tPackageID:         0,\n\t\tPackage:           testUnaffectedPkg,\n\t\tBlobID:            0,\n\t\tBlobValue: &PackageBlob{\n\t\t\tCVEs:       nil,\n\t\t\tQualifiers: nil,\n\t\t\tRanges: []Range{\n\t\t\t\t{\n\t\t\t\t\tFix: nil,\n\t\t\t\t\tVersion: Version{\n\t\t\t\t\t\tType:       \"deb\",\n\t\t\t\t\t\tConstraint: \"< 1.0.0\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\terr = w.AddUnaffectedPackages(testUnaffectedPackageHandle)\n\trequire.NoError(t, err)\n\n\t// add unaffected CPE\n\ttestUnaffectedCPE, err := cpe.New(\"cpe:2.3:a:test:unaffected:*:*:*:*:*:*:*:*\", \"\")\n\trequire.NoError(t, err)\n\n\ttestUnaffectedCPEModel := Cpe{\n\t\tPart:            testUnaffectedCPE.Attributes.Part,\n\t\tVendor:          testUnaffectedCPE.Attributes.Vendor,\n\t\tProduct:         testUnaffectedCPE.Attributes.Product,\n\t\tEdition:         testUnaffectedCPE.Attributes.Edition,\n\t\tLanguage:        testUnaffectedCPE.Attributes.Language,\n\t\tSoftwareEdition: testUnaffectedCPE.Attributes.SWEdition,\n\t\tTargetHardware:  testUnaffectedCPE.Attributes.TargetHW,\n\t\tTargetSoftware:  testUnaffectedCPE.Attributes.TargetSW,\n\t\tOther:           testUnaffectedCPE.Attributes.Other,\n\t}\n\n\ttestUnaffectedCPEHandle := &UnaffectedCPEHandle{\n\t\tID:              0,\n\t\tVulnerabilityID: 0,\n\t\tVulnerability:   unaffectedVuln,\n\t\tCpeID:           0,\n\t\tCPE:             &testUnaffectedCPEModel,\n\t\tBlobID:          0,\n\t\tBlobValue: &PackageBlob{\n\t\t\tCVEs:       nil,\n\t\t\tQualifiers: nil,\n\t\t\tRanges: []Range{\n\t\t\t\t{\n\t\t\t\t\tFix: nil,\n\t\t\t\t\tVersion: Version{\n\t\t\t\t\t\tType:       \"unknown\",\n\t\t\t\t\t\tConstraint: \"< 1.0.0\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\terr = w.AddUnaffectedCPEs(testUnaffectedCPEHandle)\n\trequire.NoError(t, err)\n\n\t// add unaffected neutron package for distro test\n\tneutronUnaffectedPkg := &Package{\n\t\tID:        0,\n\t\tEcosystem: \"deb\",\n\t\tName:      \"neutron\",\n\t}\n\n\tneutronUnaffectedHandle := &UnaffectedPackageHandle{\n\t\tID:                0,\n\t\tVulnerabilityID:   0,\n\t\tVulnerability:     unaffectedVuln,\n\t\tOperatingSystemID: nil,\n\t\tOperatingSystem: &OperatingSystem{\n\t\t\tName:         \"debian\",\n\t\t\tMajorVersion: \"8\",\n\t\t},\n\t\tPackageID: 0,\n\t\tPackage:   neutronUnaffectedPkg,\n\t\tBlobID:    0,\n\t\tBlobValue: &PackageBlob{\n\t\t\tCVEs:       nil,\n\t\t\tQualifiers: nil,\n\t\t\tRanges: []Range{\n\t\t\t\t{\n\t\t\t\t\tFix: nil,\n\t\t\t\t\tVersion: Version{\n\t\t\t\t\t\tType:       \"deb\",\n\t\t\t\t\t\tConstraint: \">= 2015.0.0\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\terr = w.AddUnaffectedPackages(neutronUnaffectedHandle)\n\trequire.NoError(t, err)\n\n\treturn NewVulnerabilityProvider(setupReadOnlyTestStore(t, tmp))\n}\n"
  },
  {
    "path": "grype/db/v6/vulnerability_provider_test.go",
    "content": "package v6\n\nimport (\n\t\"errors\"\n\t\"testing\"\n\t\"unicode\"\n\t\"unicode/utf8\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/google/go-cmp/cmp/cmpopts\"\n\t\"github.com/google/uuid\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/grype/grype/distro\"\n\t\"github.com/anchore/grype/grype/pkg\"\n\t\"github.com/anchore/grype/grype/pkg/qualifier\"\n\t\"github.com/anchore/grype/grype/search\"\n\t\"github.com/anchore/grype/grype/version\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n\t\"github.com/anchore/syft/syft/cpe\"\n\tsyftPkg \"github.com/anchore/syft/syft/pkg\"\n)\n\nfunc Test_FindVulnerabilitiesByDistro(t *testing.T) {\n\tprovider := testVulnerabilityProvider(t)\n\n\td := distro.New(distro.Debian, \"8\", \"\")\n\n\tp := pkg.Package{\n\t\tID:      pkg.ID(uuid.NewString()),\n\t\tName:    \"neutron\",\n\t\tVersion: \"1.0.0\",\n\t\tType:    syftPkg.DebPkg,\n\t}\n\n\tactual, err := provider.FindVulnerabilities(search.ByDistro(*d), search.ByPackageName(p.Name), search.ByVersion(*version.New(p.Version, pkg.VersionFormat(p))))\n\trequire.NoError(t, err)\n\n\texpected := []vulnerability.Vulnerability{\n\t\t{\n\t\t\tPackageName: \"neutron\",\n\t\t\tConstraint:  version.MustGetConstraint(\"< 2014.1.3-6\", version.DebFormat),\n\t\t\tReference: vulnerability.Reference{\n\t\t\t\tID:        \"CVE-2014-fake-1\",\n\t\t\t\tNamespace: \"debian:distro:debian:8\",\n\t\t\t},\n\t\t\tPackageQualifiers: []qualifier.Qualifier{},\n\t\t\tCPEs:              nil,\n\t\t\tAdvisories:        []vulnerability.Advisory{},\n\t\t\tMetadata: &vulnerability.Metadata{\n\t\t\t\tID:          \"CVE-2014-fake-1\",\n\t\t\t\tDataSource:  \"http://somewhere/CVE-2014-fake-1\",\n\t\t\t\tNamespace:   \"debian:distro:debian:8\",\n\t\t\t\tSeverity:    \"High\",\n\t\t\t\tURLs:        nil,\n\t\t\t\tDescription: \"CVE-2014-fake-1-description\",\n\t\t\t},\n\t\t\tRelatedVulnerabilities: []vulnerability.Reference{{ID: \"CVE-2014-fake-1\", Namespace: \"nvd:cpe\"}},\n\t\t},\n\t\t{\n\t\t\tPackageName: \"neutron\",\n\t\t\tConstraint:  version.MustGetConstraint(\"< 2013.0.2-1\", version.DebFormat),\n\t\t\tReference: vulnerability.Reference{\n\t\t\t\tID:        \"CVE-2013-fake-2\",\n\t\t\t\tNamespace: \"debian:distro:debian:8\",\n\t\t\t},\n\t\t\tPackageQualifiers: []qualifier.Qualifier{},\n\t\t\tCPEs:              nil,\n\t\t\tAdvisories:        []vulnerability.Advisory{},\n\t\t\tMetadata: &vulnerability.Metadata{\n\t\t\t\tID:          \"CVE-2013-fake-2\",\n\t\t\t\tDataSource:  \"http://somewhere/CVE-2013-fake-2\",\n\t\t\t\tNamespace:   \"debian:distro:debian:8\",\n\t\t\t\tSeverity:    \"High\",\n\t\t\t\tURLs:        nil,\n\t\t\t\tDescription: \"CVE-2013-fake-2-description\",\n\t\t\t},\n\t\t\tRelatedVulnerabilities: []vulnerability.Reference{{ID: \"CVE-2013-fake-2\", Namespace: \"nvd:cpe\"}},\n\t\t},\n\t}\n\n\trequire.Len(t, actual, len(expected))\n\n\tfor idx, vuln := range actual {\n\t\tif d := cmp.Diff(expected[idx], vuln, cmpOpts()...); d != \"\" {\n\t\t\tt.Errorf(\"diff: %+v\", d)\n\t\t}\n\t}\n}\n\nfunc Test_FindVulnerabilitiesByEmptyDistro(t *testing.T) {\n\tprovider := testVulnerabilityProvider(t)\n\n\tp := pkg.Package{\n\t\tID:   pkg.ID(uuid.NewString()),\n\t\tName: \"neutron\",\n\t}\n\n\tvulnerabilities, err := provider.FindVulnerabilities(search.ByDistro(distro.Distro{}), search.ByPackageName(p.Name))\n\n\trequire.Empty(t, vulnerabilities)\n\trequire.NoError(t, err)\n}\n\nfunc Test_FindVulnerabilitiesByCPE(t *testing.T) {\n\n\ttests := []struct {\n\t\tname     string\n\t\tcpe      cpe.CPE\n\t\texpected []vulnerability.Vulnerability\n\t\terr      bool\n\t}{\n\t\t{\n\t\t\tname: \"match from name and target SW\",\n\t\t\tcpe:  cpe.Must(\"cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:ruby:*:*\", \"\"),\n\t\t\texpected: []vulnerability.Vulnerability{\n\t\t\t\t{\n\t\t\t\t\tPackageName: \"activerecord\",\n\t\t\t\t\tConstraint:  version.MustGetConstraint(\"< 3.7.4\", version.UnknownFormat),\n\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\tID:        \"CVE-2014-fake-4\",\n\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t},\n\t\t\t\t\tCPEs: []cpe.CPE{\n\t\t\t\t\t\tcpe.Must(\"cpe:2.3:*:activerecord:activerecord:*:*:something:*:*:ruby:*:*\", \"\"),\n\t\t\t\t\t},\n\t\t\t\t\tPackageQualifiers: []qualifier.Qualifier{},\n\t\t\t\t\tAdvisories:        []vulnerability.Advisory{},\n\t\t\t\t\tMetadata: &vulnerability.Metadata{\n\t\t\t\t\t\tID:          \"CVE-2014-fake-4\",\n\t\t\t\t\t\tDataSource:  \"http://somewhere/CVE-2014-fake-4\",\n\t\t\t\t\t\tNamespace:   \"nvd:cpe\",\n\t\t\t\t\t\tSeverity:    \"High\",\n\t\t\t\t\t\tURLs:        nil,\n\t\t\t\t\t\tDescription: \"CVE-2014-fake-4-description\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"match with normalization\",\n\t\t\tcpe:  cpe.Must(\"cpe:2.3:*:ActiVERecord:ACTiveRecord:*:*:*:*:*:ruby:*:*\", \"\"),\n\t\t\texpected: []vulnerability.Vulnerability{\n\t\t\t\t{\n\t\t\t\t\tPackageName: \"activerecord\",\n\t\t\t\t\tConstraint:  version.MustGetConstraint(\"< 3.7.4\", version.UnknownFormat),\n\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\tID:        \"CVE-2014-fake-4\",\n\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t},\n\t\t\t\t\tCPEs: []cpe.CPE{\n\t\t\t\t\t\tcpe.Must(\"cpe:2.3:*:activerecord:activerecord:*:*:something:*:*:ruby:*:*\", \"\"),\n\t\t\t\t\t},\n\t\t\t\t\tPackageQualifiers: []qualifier.Qualifier{},\n\t\t\t\t\tAdvisories:        []vulnerability.Advisory{},\n\t\t\t\t\tMetadata: &vulnerability.Metadata{\n\t\t\t\t\t\tID:          \"CVE-2014-fake-4\",\n\t\t\t\t\t\tDataSource:  \"http://somewhere/CVE-2014-fake-4\",\n\t\t\t\t\t\tNamespace:   \"nvd:cpe\",\n\t\t\t\t\t\tSeverity:    \"High\",\n\t\t\t\t\t\tURLs:        nil,\n\t\t\t\t\t\tDescription: \"CVE-2014-fake-4-description\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"match from vendor & name\",\n\t\t\tcpe:  cpe.Must(\"cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:*:*:*\", \"\"),\n\t\t\texpected: []vulnerability.Vulnerability{\n\t\t\t\t{\n\t\t\t\t\tPackageName: \"activerecord\",\n\t\t\t\t\tConstraint:  version.MustGetConstraint(\"< 3.7.6\", version.UnknownFormat),\n\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\tID:        \"CVE-2014-fake-3\",\n\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t},\n\t\t\t\t\tCPEs: []cpe.CPE{\n\t\t\t\t\t\tcpe.Must(\"cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*\", \"\"),\n\t\t\t\t\t},\n\t\t\t\t\tPackageQualifiers: []qualifier.Qualifier{},\n\t\t\t\t\tAdvisories:        []vulnerability.Advisory{},\n\t\t\t\t\tMetadata: &vulnerability.Metadata{\n\t\t\t\t\t\tID:          \"CVE-2014-fake-3\",\n\t\t\t\t\t\tDataSource:  \"http://somewhere/CVE-2014-fake-3\",\n\t\t\t\t\t\tNamespace:   \"nvd:cpe\",\n\t\t\t\t\t\tSeverity:    \"High\",\n\t\t\t\t\t\tURLs:        nil,\n\t\t\t\t\t\tDescription: \"CVE-2014-fake-3-description\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tPackageName: \"activerecord\",\n\t\t\t\t\tConstraint:  version.MustGetConstraint(\"< 3.7.4\", version.UnknownFormat),\n\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\tID:        \"CVE-2014-fake-4\",\n\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t},\n\t\t\t\t\tCPEs: []cpe.CPE{\n\t\t\t\t\t\tcpe.Must(\"cpe:2.3:*:activerecord:activerecord:*:*:something:*:*:ruby:*:*\", \"\"),\n\t\t\t\t\t},\n\t\t\t\t\tPackageQualifiers: []qualifier.Qualifier{},\n\t\t\t\t\tAdvisories:        []vulnerability.Advisory{},\n\t\t\t\t\tMetadata: &vulnerability.Metadata{\n\t\t\t\t\t\tID:          \"CVE-2014-fake-4\",\n\t\t\t\t\t\tDataSource:  \"http://somewhere/CVE-2014-fake-4\",\n\t\t\t\t\t\tNamespace:   \"nvd:cpe\",\n\t\t\t\t\t\tSeverity:    \"High\",\n\t\t\t\t\t\tURLs:        nil,\n\t\t\t\t\t\tDescription: \"CVE-2014-fake-4-description\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tPackageName: \"activerecord\",\n\t\t\t\t\tConstraint:  version.MustGetConstraint(\"< 70.3.0-rc0\", version.ApkFormat),\n\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\tID:        \"CVE-2014-fake-7\",\n\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t},\n\t\t\t\t\tCPEs: []cpe.CPE{\n\t\t\t\t\t\tcpe.Must(\"cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*\", \"\"),\n\t\t\t\t\t},\n\t\t\t\t\tPackageQualifiers: []qualifier.Qualifier{},\n\t\t\t\t\tAdvisories:        []vulnerability.Advisory{},\n\t\t\t\t\tMetadata: &vulnerability.Metadata{\n\t\t\t\t\t\tID:          \"CVE-2014-fake-7\",\n\t\t\t\t\t\tDataSource:  \"http://somewhere/CVE-2014-fake-7\",\n\t\t\t\t\t\tNamespace:   \"nvd:cpe\",\n\t\t\t\t\t\tSeverity:    \"High\",\n\t\t\t\t\t\tURLs:        nil,\n\t\t\t\t\t\tDescription: \"CVE-2014-fake-7-description\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"allow query with only product\",\n\t\t\tcpe:  cpe.Must(\"cpe:2.3:a:*:product:*:*:*:*:*:*:*:*\", \"\"),\n\t\t},\n\t\t{\n\t\t\tname: \"do not allow query without product\",\n\t\t\tcpe: cpe.CPE{\n\t\t\t\tAttributes: cpe.Attributes{\n\t\t\t\t\tPart:   \"a\",\n\t\t\t\t\tVendor: \"v\",\n\t\t\t\t},\n\t\t\t},\n\t\t\terr: true,\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tprovider := testVulnerabilityProvider(t)\n\n\t\t\tactual, err := provider.FindVulnerabilities(search.ByCPE(test.cpe))\n\t\t\tif err != nil && !test.err {\n\t\t\t\tt.Fatalf(\"expected no err, got: %+v\", err)\n\t\t\t} else if err == nil && test.err {\n\t\t\t\tt.Fatalf(\"expected an err, gots\" +\n\t\t\t\t\t\" none\")\n\t\t\t}\n\n\t\t\trequire.Len(t, actual, len(test.expected))\n\n\t\t\tfor idx, vuln := range actual {\n\t\t\t\tif d := cmp.Diff(test.expected[idx], vuln, cmpOpts()...); d != \"\" {\n\t\t\t\t\tt.Errorf(\"diff: %+v\", d)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n\n}\n\nfunc Test_FindVulnerabilitiesByID(t *testing.T) {\n\tprovider := testVulnerabilityProvider(t)\n\n\td := distro.New(distro.Debian, \"8\", \"\")\n\n\t// with distro\n\tactual, err := provider.FindVulnerabilities(search.ByDistro(*d), search.ByID(\"CVE-2014-fake-1\"))\n\trequire.NoError(t, err)\n\n\texpected := []vulnerability.Vulnerability{\n\t\t{\n\t\t\tReference: vulnerability.Reference{\n\t\t\t\tID:        \"CVE-2014-fake-1\",\n\t\t\t\tNamespace: \"debian:distro:debian:8\",\n\t\t\t},\n\t\t\tPackageName:       \"neutron\",\n\t\t\tConstraint:        version.MustGetConstraint(\"< 2014.1.3-6\", version.DebFormat),\n\t\t\tPackageQualifiers: []qualifier.Qualifier{},\n\t\t\tCPEs:              nil,\n\t\t\tAdvisories:        []vulnerability.Advisory{},\n\t\t\tMetadata: &vulnerability.Metadata{\n\t\t\t\tID:          \"CVE-2014-fake-1\",\n\t\t\t\tDataSource:  \"http://somewhere/CVE-2014-fake-1\",\n\t\t\t\tNamespace:   \"debian:distro:debian:8\",\n\t\t\t\tSeverity:    \"High\",\n\t\t\t\tURLs:        nil,\n\t\t\t\tDescription: \"CVE-2014-fake-1-description\",\n\t\t\t},\n\t\t\tRelatedVulnerabilities: []vulnerability.Reference{{ID: \"CVE-2014-fake-1\", Namespace: \"nvd:cpe\"}},\n\t\t},\n\t}\n\n\trequire.Len(t, actual, len(expected))\n\n\tfor idx, vuln := range actual {\n\t\tif d := cmp.Diff(expected[idx], vuln, cmpOpts()...); d != \"\" {\n\t\t\tt.Errorf(\"diff: %+v\", d)\n\t\t}\n\t}\n\n\t// without distro\n\tactual, err = provider.FindVulnerabilities(search.ByID(\"CVE-2014-fake-1\"))\n\trequire.NoError(t, err)\n\n\tfor idx, vuln := range actual {\n\t\tif d := cmp.Diff(expected[idx], vuln, cmpOpts()...); d != \"\" {\n\t\t\tt.Errorf(\"diff: %+v\", d)\n\t\t}\n\t}\n}\n\nfunc Test_FindVulnerabilitiesByEcosystem_UnknownPackageType(t *testing.T) {\n\ttests := []struct {\n\t\tname        string\n\t\tpackageName string\n\t\tpackageType syftPkg.Type\n\t\tlanguage    syftPkg.Language\n\t\texpectedIDs []string\n\t}{\n\t\t{\n\t\t\tname:        \"known package type\",\n\t\t\tpackageName: \"Newtonsoft.Json\",\n\t\t\tpackageType: syftPkg.DotnetPkg,\n\t\t\tlanguage:    syftPkg.Java, // deliberately wrong to prove we're using package type\n\t\t\texpectedIDs: []string{\"GHSA-5crp-9r3c-p9vr\"},\n\t\t},\n\t\t{\n\t\t\tname:        \"unknown package type, known language\",\n\t\t\tpackageName: \"Newtonsoft.Json\",\n\t\t\tpackageType: syftPkg.UnknownPkg,\n\t\t\tlanguage:    syftPkg.Dotnet,\n\t\t\texpectedIDs: []string{\"GHSA-5crp-9r3c-p9vr\"},\n\t\t},\n\t\t{\n\t\t\tname:        \"unknown package type, unknown language\",\n\t\t\tpackageName: \"Newtonsoft.Json\",\n\t\t\tpackageType: syftPkg.UnknownPkg,\n\t\t\tlanguage:    syftPkg.UnknownLanguage,\n\t\t\t// The vuln GHSA-5crp-9r3c-p9vr is specifically associated\n\t\t\t// with the dotnet ecosystem, so it should not be returned here.\n\t\t\t// In a real search for UnknownPkg + UnknownLanguage, there should\n\t\t\t// be a separate search.ByCPE run that _does_ return it.\n\t\t\texpectedIDs: []string{},\n\t\t},\n\t}\n\tprovider := testVulnerabilityProvider(t)\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tactual, err := provider.FindVulnerabilities(\n\t\t\t\tsearch.ByEcosystem(test.language, test.packageType),\n\t\t\t\tsearch.ByPackageName(test.packageName),\n\t\t\t)\n\t\t\trequire.NoError(t, err)\n\t\t\tactualIDs := make([]string, len(actual))\n\t\t\tfor idx, vuln := range actual {\n\t\t\t\tactualIDs[idx] = vuln.ID\n\t\t\t}\n\t\t\tif d := cmp.Diff(test.expectedIDs, actualIDs); d != \"\" {\n\t\t\t\tt.Errorf(\"diff: %+v\", d)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_DataSource(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tvuln     VulnerabilityHandle\n\t\texpected vulnerability.Metadata\n\t}{\n\t\t{\n\t\t\tname: \"no reference urls\",\n\t\t\tvuln: VulnerabilityHandle{\n\t\t\t\tBlobValue: &VulnerabilityBlob{\n\t\t\t\t\tReferences: nil,\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: vulnerability.Metadata{\n\t\t\t\tDataSource: \"\",\n\t\t\t\tURLs:       nil,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"one reference url\",\n\t\t\tvuln: VulnerabilityHandle{\n\t\t\t\tBlobValue: &VulnerabilityBlob{\n\t\t\t\t\tReferences: []Reference{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tURL: \"url1\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: vulnerability.Metadata{\n\t\t\t\tDataSource: \"url1\",\n\t\t\t\tURLs:       nil,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"two reference urls\",\n\t\t\tvuln: VulnerabilityHandle{\n\t\t\t\tBlobValue: &VulnerabilityBlob{\n\t\t\t\t\tReferences: []Reference{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tURL: \"url1\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tURL: \"url2\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: vulnerability.Metadata{\n\t\t\t\tDataSource: \"url1\",\n\t\t\t\tURLs:       []string{\"url2\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"many reference urls\",\n\t\t\tvuln: VulnerabilityHandle{\n\t\t\t\tBlobValue: &VulnerabilityBlob{\n\t\t\t\t\tReferences: []Reference{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tURL: \"url4\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tURL: \"url3\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tURL: \"url2\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tURL: \"url1\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: vulnerability.Metadata{\n\t\t\t\tDataSource: \"url4\",\n\t\t\t\tURLs:       []string{\"url3\", \"url2\", \"url1\"},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := newVulnerabilityMetadata(&tt.vuln, \"\", nil, nil, nil)\n\t\t\tgot.Severity = \"\"\n\t\t\trequire.NoError(t, err)\n\t\t\tif diff := cmp.Diff(&tt.expected, got, cmpOpts()...); diff != \"\" {\n\t\t\t\tt.Fatal(diff)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_filterAffectedPackageRanges(t *testing.T) {\n\ttests := []struct {\n\t\tname                     string\n\t\tranges                   []Range\n\t\tmatchesConstraint        func(constraint version.Constraint) (bool, error)\n\t\texpectedAllRangesRemoved bool\n\t\texpectedUnmatchedStrings []string\n\t}{\n\t\t{\n\t\t\tname:                     \"no ranges\",\n\t\t\tranges:                   nil,\n\t\t\texpectedAllRangesRemoved: false, // important! we assume that a vulnerability with no ranges is always vulnerable\n\t\t\texpectedUnmatchedStrings: nil,\n\t\t},\n\t\t{\n\t\t\tname: \"has ranges within constraint\",\n\t\t\tranges: []Range{\n\t\t\t\t{\n\t\t\t\t\tVersion: Version{\n\t\t\t\t\t\tType:       \"rpm\",\n\t\t\t\t\t\tConstraint: \"< 1.0.0\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tVersion: Version{\n\t\t\t\t\t\tType:       \"rpm\",\n\t\t\t\t\t\tConstraint: \"< 2.0.0\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tmatchesConstraint: func(constraint version.Constraint) (bool, error) {\n\t\t\t\treturn true, nil\n\t\t\t},\n\t\t\texpectedAllRangesRemoved: false,\n\t\t\texpectedUnmatchedStrings: nil,\n\t\t},\n\t\t{\n\t\t\tname: \"has ranges outside constraint\",\n\t\t\tranges: []Range{\n\t\t\t\t{\n\t\t\t\t\tVersion: Version{\n\t\t\t\t\t\tType:       \"rpm\",\n\t\t\t\t\t\tConstraint: \"< 1.0.0\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tVersion: Version{\n\t\t\t\t\t\tType:       \"rpm\",\n\t\t\t\t\t\tConstraint: \"< 2.0.0\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tmatchesConstraint: func(constraint version.Constraint) (bool, error) {\n\t\t\t\treturn false, nil\n\t\t\t},\n\t\t\texpectedAllRangesRemoved: true,\n\t\t\texpectedUnmatchedStrings: []string{\"< 1.0.0\", \"< 2.0.0\"},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tmockMatcher := &mockVersionConstraintMatcher{\n\t\t\t\tmatchesConstraintFunc: tt.matchesConstraint,\n\t\t\t}\n\n\t\t\tblob := &PackageBlob{\n\t\t\t\tRanges: tt.ranges,\n\t\t\t}\n\n\t\t\tallRangesRemoved, unmatchedConstraints := filterAffectedPackageRanges(mockMatcher, blob)\n\n\t\t\trequire.Equal(t, tt.expectedAllRangesRemoved, allRangesRemoved)\n\t\t\trequire.Equal(t, tt.expectedUnmatchedStrings, unmatchedConstraints)\n\t\t})\n\t}\n}\n\ntype mockVersionConstraintMatcher struct {\n\tmatchesConstraintFunc func(constraint version.Constraint) (bool, error)\n}\n\nfunc (m *mockVersionConstraintMatcher) MatchesConstraint(constraint version.Constraint) (bool, error) {\n\tif m.matchesConstraintFunc != nil {\n\t\treturn m.matchesConstraintFunc(constraint)\n\t}\n\treturn false, nil\n}\n\nfunc Test_FindVulnerabilitiesByUnaffectedCriteria(t *testing.T) {\n\tprovider := testVulnerabilityProvider(t)\n\n\ttests := []struct {\n\t\tname        string\n\t\tcriteria    []vulnerability.Criteria\n\t\texpected    []vulnerability.Vulnerability\n\t\tdescription string\n\t}{\n\t\t{\n\t\t\tname: \"search for unaffected packages\",\n\t\t\tcriteria: []vulnerability.Criteria{\n\t\t\t\tsearch.ForUnaffected(),\n\t\t\t\tsearch.ByPackageName(\"test-unaffected-package\"),\n\t\t\t},\n\t\t\texpected: []vulnerability.Vulnerability{\n\t\t\t\t{\n\t\t\t\t\tPackageName: \"test-unaffected-package\",\n\t\t\t\t\tConstraint:  version.MustGetConstraint(\"< 1.0.0\", version.DebFormat),\n\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\tID:        \"CVE-2024-unaffected-test\",\n\t\t\t\t\t\tNamespace: \"nvd:language:deb\",\n\t\t\t\t\t},\n\t\t\t\t\tPackageQualifiers:      []qualifier.Qualifier{},\n\t\t\t\t\tCPEs:                   nil,\n\t\t\t\t\tAdvisories:             []vulnerability.Advisory{},\n\t\t\t\t\tRelatedVulnerabilities: nil,\n\t\t\t\t\tMetadata: &vulnerability.Metadata{\n\t\t\t\t\t\tID:          \"CVE-2024-unaffected-test\",\n\t\t\t\t\t\tDataSource:  \"http://somewhere/CVE-2024-unaffected-test\",\n\t\t\t\t\t\tNamespace:   \"nvd:language:deb\",\n\t\t\t\t\t\tSeverity:    \"Medium\",\n\t\t\t\t\t\tURLs:        nil,\n\t\t\t\t\t\tDescription: \"CVE-2024-unaffected-test-description\",\n\t\t\t\t\t},\n\t\t\t\t\tUnaffected: true,\n\t\t\t\t},\n\t\t\t},\n\t\t\tdescription: \"should use unaffected package store\",\n\t\t},\n\t\t{\n\t\t\tname: \"search for unaffected CPEs\",\n\t\t\tcriteria: []vulnerability.Criteria{\n\t\t\t\tsearch.ForUnaffected(),\n\t\t\t\tsearch.ByCPE(cpe.Must(\"cpe:2.3:a:test:unaffected:*:*:*:*:*:*:*:*\", \"\")),\n\t\t\t},\n\t\t\texpected: []vulnerability.Vulnerability{\n\t\t\t\t{\n\t\t\t\t\tPackageName: \"unaffected\",\n\t\t\t\t\tConstraint:  version.MustGetConstraint(\"< 1.0.0\", version.UnknownFormat),\n\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\tID:        \"CVE-2024-unaffected-test\",\n\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t},\n\t\t\t\t\tPackageQualifiers:      []qualifier.Qualifier{},\n\t\t\t\t\tCPEs:                   []cpe.CPE{cpe.Must(\"cpe:2.3:a:test:unaffected:*:*:*:*:*:*:*:*\", \"\")},\n\t\t\t\t\tAdvisories:             []vulnerability.Advisory{},\n\t\t\t\t\tRelatedVulnerabilities: nil,\n\t\t\t\t\tMetadata: &vulnerability.Metadata{\n\t\t\t\t\t\tID:          \"CVE-2024-unaffected-test\",\n\t\t\t\t\t\tDataSource:  \"http://somewhere/CVE-2024-unaffected-test\",\n\t\t\t\t\t\tNamespace:   \"nvd:cpe\",\n\t\t\t\t\t\tSeverity:    \"Medium\",\n\t\t\t\t\t\tURLs:        nil,\n\t\t\t\t\t\tDescription: \"CVE-2024-unaffected-test-description\",\n\t\t\t\t\t},\n\t\t\t\t\tUnaffected: true,\n\t\t\t\t},\n\t\t\t},\n\t\t\tdescription: \"should use unaffected CPE store\",\n\t\t},\n\t\t{\n\t\t\tname: \"search with unaffected criteria and distro\",\n\t\t\tcriteria: []vulnerability.Criteria{\n\t\t\t\tsearch.ForUnaffected(),\n\t\t\t\tsearch.ByDistro(*distro.New(distro.Debian, \"8\", \"\")),\n\t\t\t\tsearch.ByPackageName(\"neutron\"),\n\t\t\t},\n\t\t\texpected: []vulnerability.Vulnerability{\n\t\t\t\t{\n\t\t\t\t\tPackageName: \"neutron\",\n\t\t\t\t\tConstraint:  version.MustGetConstraint(\">= 2015.0.0\", version.DebFormat),\n\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\tID:        \"CVE-2024-unaffected-test\",\n\t\t\t\t\t\tNamespace: \"nvd:distro:debian:8\",\n\t\t\t\t\t},\n\t\t\t\t\tPackageQualifiers:      []qualifier.Qualifier{},\n\t\t\t\t\tCPEs:                   nil,\n\t\t\t\t\tAdvisories:             []vulnerability.Advisory{},\n\t\t\t\t\tRelatedVulnerabilities: nil,\n\t\t\t\t\tMetadata: &vulnerability.Metadata{\n\t\t\t\t\t\tID:          \"CVE-2024-unaffected-test\",\n\t\t\t\t\t\tDataSource:  \"http://somewhere/CVE-2024-unaffected-test\",\n\t\t\t\t\t\tNamespace:   \"nvd:distro:debian:8\",\n\t\t\t\t\t\tSeverity:    \"Medium\",\n\t\t\t\t\t\tURLs:        nil,\n\t\t\t\t\t\tDescription: \"CVE-2024-unaffected-test-description\",\n\t\t\t\t\t},\n\t\t\t\t\tUnaffected: true,\n\t\t\t\t},\n\t\t\t},\n\t\t\tdescription: \"should combine unaffected and distro criteria\",\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tactual, err := provider.FindVulnerabilities(test.criteria...)\n\t\t\trequire.NoError(t, err, test.description)\n\t\t\trequire.Len(t, actual, len(test.expected), test.description)\n\n\t\t\tfor idx, vuln := range actual {\n\t\t\t\tif d := cmp.Diff(test.expected[idx], vuln, cmpOpts()...); d != \"\" {\n\t\t\t\t\tt.Errorf(\"diff: %+v\", d)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_FindVulnerabilitiesErrorHandling(t *testing.T) {\n\tprovider := testVulnerabilityProvider(t)\n\n\ttests := []struct {\n\t\tname        string\n\t\tcriteria    []vulnerability.Criteria\n\t\texpectError bool\n\t\tdescription string\n\t}{\n\t\t{\n\t\t\tname: \"CPE without product should error\",\n\t\t\tcriteria: []vulnerability.Criteria{\n\t\t\t\tsearch.ByCPE(cpe.CPE{\n\t\t\t\t\tAttributes: cpe.Attributes{\n\t\t\t\t\t\tPart:   \"a\",\n\t\t\t\t\t\tVendor: \"vendor\",\n\t\t\t\t\t\t// Product is missing - should cause error\n\t\t\t\t\t},\n\t\t\t\t}),\n\t\t\t},\n\t\t\texpectError: true,\n\t\t\tdescription: \"CPE searches require a product\",\n\t\t},\n\t\t{\n\t\t\tname:        \"empty criteria should work\",\n\t\t\tcriteria:    []vulnerability.Criteria{},\n\t\t\texpectError: false,\n\t\t\tdescription: \"empty criteria should be handled gracefully\",\n\t\t},\n\t\t{\n\t\t\tname: \"unaffected with CPE without product should error\",\n\t\t\tcriteria: []vulnerability.Criteria{\n\t\t\t\tsearch.ForUnaffected(),\n\t\t\t\tsearch.ByCPE(cpe.CPE{\n\t\t\t\t\tAttributes: cpe.Attributes{\n\t\t\t\t\t\tPart:   \"a\",\n\t\t\t\t\t\tVendor: \"vendor\",\n\t\t\t\t\t},\n\t\t\t\t}),\n\t\t\t},\n\t\t\texpectError: true,\n\t\t\tdescription: \"unaffected CPE searches also require a product\",\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\t_, err := provider.FindVulnerabilities(test.criteria...)\n\t\t\tif test.expectError {\n\t\t\t\trequire.Error(t, err, test.description)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err, test.description)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_FindVulnerabilitiesMixedCriteria(t *testing.T) {\n\tprovider := testVulnerabilityProvider(t)\n\n\t// test complex criteria combinations\n\td := distro.New(distro.Debian, \"8\", \"\")\n\n\ttests := []struct {\n\t\tname        string\n\t\tcriteria    []vulnerability.Criteria\n\t\texpectedLen int\n\t\tdescription string\n\t}{\n\t\t{\n\t\t\tname: \"package name with version constraint\",\n\t\t\tcriteria: []vulnerability.Criteria{\n\t\t\t\tsearch.ByDistro(*d),\n\t\t\t\tsearch.ByPackageName(\"neutron\"),\n\t\t\t\tsearch.ByVersion(*version.New(\"1.0.0\", version.DebFormat)),\n\t\t\t},\n\t\t\texpectedLen: 2, // based on existing test data\n\t\t\tdescription: \"should find vulnerabilities matching version constraints\",\n\t\t},\n\t\t{\n\t\t\tname: \"ID search with distro\",\n\t\t\tcriteria: []vulnerability.Criteria{\n\t\t\t\tsearch.ByDistro(*d),\n\t\t\t\tsearch.ByID(\"CVE-2014-fake-1\"),\n\t\t\t},\n\t\t\texpectedLen: 1,\n\t\t\tdescription: \"should find specific vulnerability by ID and distro\",\n\t\t},\n\t\t{\n\t\t\tname: \"ecosystem search with package name\",\n\t\t\tcriteria: []vulnerability.Criteria{\n\t\t\t\tsearch.ByEcosystem(syftPkg.Dotnet, syftPkg.DotnetPkg),\n\t\t\t\tsearch.ByPackageName(\"Newtonsoft.Json\"),\n\t\t\t},\n\t\t\texpectedLen: 1,\n\t\t\tdescription: \"should find vulnerabilities by ecosystem\",\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tactual, err := provider.FindVulnerabilities(test.criteria...)\n\t\t\trequire.NoError(t, err, test.description)\n\t\t\trequire.Len(t, actual, test.expectedLen, test.description)\n\t\t})\n\t}\n}\n\nfunc Test_FindVulnerabilitiesNormalization(t *testing.T) {\n\tprovider := testVulnerabilityProvider(t)\n\n\ttests := []struct {\n\t\tname            string\n\t\tpackageName     string\n\t\tpackageType     syftPkg.Type\n\t\texpectedResults int\n\t\tdescription     string\n\t}{\n\t\t{\n\t\t\tname:            \"package name normalization with known type\",\n\t\t\tpackageName:     \"Newtonsoft.Json\", // mixed case\n\t\t\tpackageType:     syftPkg.DotnetPkg,\n\t\t\texpectedResults: 1,\n\t\t\tdescription:     \"package names should be normalized based on ecosystem\",\n\t\t},\n\t\t{\n\t\t\tname:            \"package name with unknown type\",\n\t\t\tpackageName:     \"SomePackage\",\n\t\t\tpackageType:     syftPkg.UnknownPkg,\n\t\t\texpectedResults: 0, // likely no matches\n\t\t\tdescription:     \"unknown package types should still be handled\",\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tactual, err := provider.FindVulnerabilities(\n\t\t\t\tsearch.ByEcosystem(syftPkg.UnknownLanguage, test.packageType),\n\t\t\t\tsearch.ByPackageName(test.packageName),\n\t\t\t)\n\t\t\trequire.NoError(t, err, test.description)\n\t\t\trequire.Len(t, actual, test.expectedResults, test.description)\n\t\t})\n\t}\n}\n\nfunc Test_HandleOSError(t *testing.T) {\n\tprovider := testVulnerabilityProvider(t).(*vulnerabilityProvider)\n\n\tosSpecs := OSSpecifiers{&OSSpecifier{Name: \"debian\", MajorVersion: \"8\"}}\n\n\ttests := []struct {\n\t\tname        string\n\t\tinputError  error\n\t\texpectError bool\n\t\tdescription string\n\t}{\n\t\t{\n\t\t\tname:        \"nil error returns nil\",\n\t\t\tinputError:  nil,\n\t\t\texpectError: false,\n\t\t\tdescription: \"nil errors should pass through as nil\",\n\t\t},\n\t\t{\n\t\t\tname:        \"ErrOSNotPresent returns nil\",\n\t\t\tinputError:  ErrOSNotPresent,\n\t\t\texpectError: false,\n\t\t\tdescription: \"ErrOSNotPresent should be handled and return nil\",\n\t\t},\n\t\t{\n\t\t\tname:        \"other errors return unchanged\",\n\t\t\tinputError:  errors.New(\"some other error\"),\n\t\t\texpectError: true,\n\t\t\tdescription: \"other errors should be returned unchanged\",\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\terr := provider.handleOSError(test.inputError, osSpecs)\n\t\t\tif test.expectError {\n\t\t\t\trequire.Error(t, err, test.description)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err, test.description)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_FetchAndProcessPackagesEdgeCases(t *testing.T) {\n\tprovider := testVulnerabilityProvider(t).(*vulnerabilityProvider)\n\n\ttests := []struct {\n\t\tname        string\n\t\tctx         *searchQuery\n\t\tdescription string\n\t}{\n\t\t{\n\t\t\tname: \"context with no specs should return empty results\",\n\t\t\tctx: &searchQuery{\n\t\t\t\t// no pkgSpec, no vulnSpecs\n\t\t\t\tosSpecs:        OSSpecifiers{NoOSSpecified},\n\t\t\t\tunaffectedOnly: false,\n\t\t\t},\n\t\t\tdescription: \"should handle context with no package or vulnerability specs\",\n\t\t},\n\t\t{\n\t\t\tname: \"unaffected context with no specs should return empty results\",\n\t\t\tctx: &searchQuery{\n\t\t\t\t// no pkgSpec, no vulnSpecs\n\t\t\t\tosSpecs:        OSSpecifiers{NoOSSpecified},\n\t\t\t\tunaffectedOnly: true,\n\t\t\t},\n\t\t\tdescription: \"should handle unaffected context with no package or vulnerability specs\",\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tvulns, err := provider.fetchAndProcessPackages(test.ctx)\n\t\t\trequire.NoError(t, err, test.description)\n\t\t\trequire.Empty(t, vulns, \"should return empty vulnerabilities\")\n\t\t})\n\t}\n}\n\nfunc Test_FetchAndProcessCPEsEdgeCases(t *testing.T) {\n\tprovider := testVulnerabilityProvider(t).(*vulnerabilityProvider)\n\n\ttests := []struct {\n\t\tname        string\n\t\tctx         *searchQuery\n\t\tdescription string\n\t}{\n\t\t{\n\t\t\tname: \"context with no CPE spec should return empty results\",\n\t\t\tctx: &searchQuery{\n\t\t\t\t// no cpeSpec\n\t\t\t\tosSpecs:        OSSpecifiers{NoOSSpecified},\n\t\t\t\tunaffectedOnly: false,\n\t\t\t},\n\t\t\tdescription: \"should handle context with no CPE spec\",\n\t\t},\n\t\t{\n\t\t\tname: \"unaffected context with no CPE spec should return empty results\",\n\t\t\tctx: &searchQuery{\n\t\t\t\t// no cpeSpec\n\t\t\t\tosSpecs:        OSSpecifiers{NoOSSpecified},\n\t\t\t\tunaffectedOnly: true,\n\t\t\t},\n\t\t\tdescription: \"should handle unaffected context with no CPE spec\",\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tvulns, err := provider.fetchAndProcessCPEs(test.ctx)\n\t\t\trequire.NoError(t, err, test.description)\n\t\t\trequire.Empty(t, vulns, \"should return empty vulnerabilities\")\n\t\t})\n\t}\n}\n\nfunc cmpOpts() []cmp.Option {\n\treturn []cmp.Option{\n\t\t// globally ignore unexported -- these are unexported structs we cannot reference here to use cmpopts.IgnoreUnexported\n\t\tcmp.FilterPath(func(p cmp.Path) bool {\n\t\t\tsf, ok := p.Index(-1).(cmp.StructField)\n\t\t\tif !ok {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tr, _ := utf8.DecodeRuneInString(sf.Name())\n\t\t\treturn !unicode.IsUpper(r)\n\t\t}, cmp.Ignore()),\n\t\tcmpopts.EquateEmpty(),\n\t\tcmpopts.IgnoreFields(vulnerability.Reference{}, \"Internal\"),\n\t}\n}\n"
  },
  {
    "path": "grype/db/v6/vulnerability_store.go",
    "content": "package v6\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/scylladb/go-set/strset\"\n\t\"gorm.io/gorm\"\n\n\t\"github.com/anchore/go-logger\"\n\t\"github.com/anchore/grype/internal/log\"\n)\n\nconst anyVulnerability = \"any\"\n\ntype VulnerabilityStoreWriter interface {\n\tAddVulnerabilities(vulns ...*VulnerabilityHandle) error\n}\n\ntype VulnerabilityStoreReader interface {\n\tGetVulnerabilities(vuln *VulnerabilitySpecifier, config *GetVulnerabilityOptions) ([]VulnerabilityHandle, error)\n}\n\ntype GetVulnerabilityOptions struct {\n\tPreload bool\n\tLimit   int\n}\n\ntype VulnerabilitySpecifiers []VulnerabilitySpecifier\n\ntype VulnerabilitySpecifier struct {\n\t// Name of the vulnerability (e.g. CVE-2020-1234)\n\tName string\n\n\t// ID is the DB ID of the vulnerability\n\tID ID\n\n\t// Status is the status of the vulnerability (e.g. \"active\", \"rejected\", etc.)\n\tStatus VulnerabilityStatus\n\n\t// PublishedAfter is a filter to only return vulnerabilities published after the given time\n\tPublishedAfter *time.Time\n\n\t// ModifiedAfter is a filter to only return vulnerabilities modified after the given time\n\tModifiedAfter *time.Time\n\n\t// IncludeAliases for the given name or ID in results\n\tIncludeAliases bool\n\n\t// Providers\n\tProviders []string\n}\n\nfunc (v *VulnerabilitySpecifier) String() string {\n\tvar parts []string\n\tif v.Name != \"\" {\n\t\tparts = append(parts, fmt.Sprintf(\"name=%s\", v.Name))\n\t}\n\n\tif v.ID != 0 {\n\t\tparts = append(parts, fmt.Sprintf(\"id=%d\", v.ID))\n\t}\n\n\tif v.Status != \"\" {\n\t\tparts = append(parts, fmt.Sprintf(\"status=%s\", v.Status))\n\t}\n\n\tif v.PublishedAfter != nil {\n\t\tparts = append(parts, fmt.Sprintf(\"publishedAfter=%s\", v.PublishedAfter.String()))\n\t}\n\n\tif v.ModifiedAfter != nil {\n\t\tparts = append(parts, fmt.Sprintf(\"modifiedAfter=%s\", v.ModifiedAfter.String()))\n\t}\n\n\tif v.IncludeAliases {\n\t\tparts = append(parts, \"includeAliases=true\")\n\t}\n\n\tif len(v.Providers) > 0 {\n\t\tparts = append(parts, fmt.Sprintf(\"providers=%s\", strings.Join(v.Providers, \",\")))\n\t}\n\n\tif len(parts) == 0 {\n\t\treturn anyVulnerability\n\t}\n\n\treturn fmt.Sprintf(\"vulnerability(%s)\", strings.Join(parts, \", \"))\n}\n\nfunc (s VulnerabilitySpecifiers) String() string {\n\tif len(s) == 0 {\n\t\treturn anyVulnerability\n\t}\n\tvar parts []string\n\tfor _, v := range s {\n\t\tparts = append(parts, v.String())\n\t}\n\treturn strings.Join(parts, \", \")\n}\n\nfunc DefaultGetVulnerabilityOptions() *GetVulnerabilityOptions {\n\treturn &GetVulnerabilityOptions{\n\t\tPreload: false,\n\t}\n}\n\ntype vulnerabilityStore struct {\n\tdb        *gorm.DB\n\tblobStore *blobStore\n}\n\nfunc newVulnerabilityStore(db *gorm.DB, bs *blobStore) *vulnerabilityStore {\n\treturn &vulnerabilityStore{\n\t\tdb:        db,\n\t\tblobStore: bs,\n\t}\n}\n\nfunc (s *vulnerabilityStore) AddVulnerabilities(vulnerabilities ...*VulnerabilityHandle) error {\n\tif err := s.addProviders(s.db, vulnerabilities...); err != nil {\n\t\treturn fmt.Errorf(\"unable to add providers: %w\", err)\n\t}\n\tfor i := range vulnerabilities {\n\t\tv := vulnerabilities[i]\n\t\t// this adds the blob value to the DB and sets the ID on the vulnerability handle\n\t\tif err := s.blobStore.addBlobable(v); err != nil {\n\t\t\treturn fmt.Errorf(\"unable to add affected blob: %w\", err)\n\t\t}\n\n\t\tif v.PublishedDate != nil && v.ModifiedDate == nil {\n\t\t\t// the data here should be consistent, and we are norming around initial publication counts as a modification date.\n\t\t\t// this allows for easily refining queries based on both publication date and modification date without needing\n\t\t\t// to worry about this edge case.\n\t\t\tv.ModifiedDate = v.PublishedDate\n\t\t}\n\n\t\tif v.BlobValue != nil {\n\t\t\taliases := strset.New(v.BlobValue.Aliases...)\n\t\t\taliases.Remove(v.Name)\n\t\t\tvar aliasModels []VulnerabilityAlias\n\t\t\tfor _, alias := range aliases.List() {\n\t\t\t\taliasModels = append(aliasModels, VulnerabilityAlias{\n\t\t\t\t\tName:  v.Name,\n\t\t\t\t\tAlias: alias,\n\t\t\t\t})\n\t\t\t}\n\t\t\tfor _, aliasModel := range aliasModels {\n\t\t\t\tif err := s.db.FirstOrCreate(&aliasModel).Error; err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif err := createRecordsWithCache(s.db, v); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (s *vulnerabilityStore) addProviders(tx *gorm.DB, vulnerabilities ...*VulnerabilityHandle) error { // nolint:dupl\n\tcacheInst, ok := cacheFromContext(tx.Statement.Context)\n\tif !ok {\n\t\treturn fmt.Errorf(\"unable to fetch provider cache from context\")\n\t}\n\n\tvar final []*Provider\n\tbyCacheKey := make(map[string][]*Provider)\n\tfor _, v := range vulnerabilities {\n\t\tif v.Provider != nil {\n\t\t\tkey := v.Provider.cacheKey()\n\t\t\tif existingID, ok := cacheInst.getString(v.Provider); ok {\n\t\t\t\t// seen in a previous transaction...\n\t\t\t\tv.ProviderID = existingID\n\t\t\t} else if _, ok := byCacheKey[key]; !ok {\n\t\t\t\t// not seen within this transaction\n\t\t\t\tfinal = append(final, v.Provider)\n\t\t\t}\n\t\t\tbyCacheKey[key] = append(byCacheKey[key], v.Provider)\n\t\t}\n\t}\n\n\tif len(final) == 0 {\n\t\treturn nil\n\t}\n\n\tif err := tx.Create(final).Error; err != nil {\n\t\treturn fmt.Errorf(\"unable to create provider records: %w\", err)\n\t}\n\n\t// update the cache with the new records\n\tfor _, ref := range final {\n\t\tcacheInst.set(ref)\n\t}\n\n\t// update all references with the IDs from the cache\n\tfor _, refs := range byCacheKey {\n\t\tfor _, ref := range refs {\n\t\t\tid, ok := cacheInst.getString(ref)\n\t\t\tif ok {\n\t\t\t\tref.setRowID(id)\n\t\t\t}\n\t\t}\n\t}\n\n\t// update the parent objects with the FK ID\n\tfor _, p := range vulnerabilities {\n\t\tif p.Provider != nil {\n\t\t\tp.ProviderID = p.Provider.ID\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc createRecordsWithCache(tx *gorm.DB, items ...*VulnerabilityHandle) error {\n\t// look for existing records from the cache, and only create new records\n\tcacheInst, ok := cacheFromContext(tx.Statement.Context)\n\tif !ok {\n\t\treturn fmt.Errorf(\"cache not found in context\")\n\t}\n\n\t// store all entries by their cache key (throw away duplicates)\n\tskippedRecordsByCacheKey := map[string][]*VulnerabilityHandle{}\n\tusedKeys := strset.New()\n\tvar finalWrites []*VulnerabilityHandle\n\tfor i := range items {\n\t\tp := items[i]\n\t\tkey := p.cacheKey()\n\n\t\tif usedKeys.Has(key) {\n\t\t\tskippedRecordsByCacheKey[key] = append(skippedRecordsByCacheKey[key], p)\n\t\t\tcontinue\n\t\t}\n\n\t\tif _, ok := skippedRecordsByCacheKey[key]; ok {\n\t\t\tskippedRecordsByCacheKey[key] = append(skippedRecordsByCacheKey[key], p)\n\t\t\tcontinue\n\t\t}\n\t\tif _, ok := cacheInst.getID(p); ok {\n\t\t\tskippedRecordsByCacheKey[key] = append(skippedRecordsByCacheKey[key], p)\n\t\t\tcontinue\n\t\t}\n\n\t\tfinalWrites = append(finalWrites, p)\n\t\tusedKeys.Add(key)\n\t}\n\n\tfor i := range finalWrites {\n\t\tif err := tx.Omit(\"Provider\").Create(finalWrites[i]).Error; err != nil {\n\t\t\treturn fmt.Errorf(\"unable to create record %#v: %w\", finalWrites[i], err)\n\t\t}\n\t}\n\n\t// ensure we're always updating the cache with the latest data + the records with any new IDs\n\tfor i := range finalWrites {\n\t\tcacheInst.set(finalWrites[i])\n\t}\n\n\tfor _, batch := range skippedRecordsByCacheKey {\n\t\tfor i := range batch {\n\t\t\tid, ok := cacheInst.getID(batch[i])\n\t\t\tif !ok {\n\t\t\t\treturn fmt.Errorf(\"unable to find ID: %#v\", batch[i])\n\t\t\t}\n\t\t\tbatch[i].setRowID(id)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (s *vulnerabilityStore) GetVulnerabilities(vuln *VulnerabilitySpecifier, config *GetVulnerabilityOptions) ([]VulnerabilityHandle, error) {\n\tif config == nil {\n\t\tconfig = DefaultGetVulnerabilityOptions()\n\t}\n\tfields := logger.Fields{\n\t\t\"vuln\":    vuln,\n\t\t\"preload\": config.Preload,\n\t}\n\tstart := time.Now()\n\tvar count int\n\tdefer func() {\n\t\tfields[\"duration\"] = time.Since(start)\n\t\tfields[\"records\"] = count\n\t\tlog.WithFields(fields).Trace(\"fetched vulnerability records\")\n\t}()\n\n\tvar err error\n\tquery := s.db\n\tif vuln != nil {\n\t\tquery, err = handleVulnerabilityOptions(s.db, query, *vuln)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tquery = s.handlePreload(query, *config)\n\n\tvar models []VulnerabilityHandle\n\n\tvar results []*VulnerabilityHandle\n\tif err := query.FindInBatches(&results, batchSize, func(_ *gorm.DB, _ int) error {\n\t\tif config.Preload {\n\t\t\tvar blobs []blobable\n\t\t\tfor _, r := range results {\n\t\t\t\tblobs = append(blobs, r)\n\t\t\t}\n\t\t\tif err := s.blobStore.attachBlobValue(blobs...); err != nil {\n\t\t\t\treturn fmt.Errorf(\"unable to attach vulnerability blobs: %w\", err)\n\t\t\t}\n\t\t}\n\n\t\tfor _, r := range results {\n\t\t\tmodels = append(models, *r)\n\t\t}\n\n\t\tcount += len(results)\n\n\t\tif config.Limit > 0 && len(models) >= config.Limit {\n\t\t\treturn ErrLimitReached\n\t\t}\n\n\t\treturn nil\n\t}).Error; err != nil {\n\t\treturn models, fmt.Errorf(\"unable to fetch vulnerability records: %w\", err)\n\t}\n\n\treturn models, err\n}\n\nfunc (s *vulnerabilityStore) handlePreload(query *gorm.DB, config GetVulnerabilityOptions) *gorm.DB {\n\tvar limitArgs []interface{}\n\tif config.Limit > 0 {\n\t\tquery = query.Limit(config.Limit)\n\t\tlimitArgs = append(limitArgs, func(db *gorm.DB) *gorm.DB {\n\t\t\treturn db.Limit(config.Limit)\n\t\t})\n\t}\n\tif config.Preload {\n\t\tquery = query.Preload(\"Provider\", limitArgs...)\n\t}\n\treturn query\n}\n\nfunc handleVulnerabilityOptions(base, parentQuery *gorm.DB, configs ...VulnerabilitySpecifier) (*gorm.DB, error) {\n\tif len(configs) == 0 {\n\t\treturn parentQuery, nil\n\t}\n\n\torConditions := base.Model(&VulnerabilityHandle{})\n\tvar includeAliasJoin bool\n\tfor _, config := range configs {\n\t\tquery := base.Model(&VulnerabilityHandle{})\n\t\tif config.Name != \"\" {\n\t\t\tif config.IncludeAliases {\n\t\t\t\tincludeAliasJoin = true\n\t\t\t\tquery = query.Where(\"vulnerability_handles.name = ? collate nocase OR vulnerability_aliases.alias = ?  collate nocase\", config.Name, config.Name)\n\t\t\t} else {\n\t\t\t\tquery = query.Where(\"vulnerability_handles.name = ?  collate nocase\", config.Name)\n\t\t\t}\n\t\t}\n\n\t\tif config.ID != 0 {\n\t\t\tquery = query.Where(\"vulnerability_handles.id = ?\", config.ID)\n\t\t}\n\n\t\tif config.PublishedAfter != nil {\n\t\t\tquery = query.Where(\"vulnerability_handles.published_date > ?\", *config.PublishedAfter)\n\t\t}\n\n\t\tif config.ModifiedAfter != nil {\n\t\t\tquery = query.Where(\"vulnerability_handles.modified_date > ?\", *config.ModifiedAfter)\n\t\t}\n\n\t\tif config.Status != \"\" {\n\t\t\tquery = query.Where(\"vulnerability_handles.status = ?\", config.Status)\n\t\t}\n\n\t\tif len(config.Providers) > 0 {\n\t\t\tquery = query.Where(\"vulnerability_handles.provider_id IN ?\", config.Providers)\n\t\t}\n\n\t\torConditions = orConditions.Or(query)\n\t}\n\n\tif includeAliasJoin {\n\t\tparentQuery = parentQuery.Joins(\"LEFT JOIN vulnerability_aliases ON vulnerability_aliases.name = vulnerability_handles.name collate nocase\")\n\t}\n\n\treturn parentQuery.Where(orConditions), nil\n}\n"
  },
  {
    "path": "grype/db/v6/vulnerability_store_test.go",
    "content": "package v6\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/google/go-cmp/cmp/cmpopts\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestVulnerabilityStore_AddVulnerabilities(t *testing.T) {\n\tdb := setupTestStore(t).db\n\tbw := newBlobStore(db)\n\ts := newVulnerabilityStore(db, bw)\n\n\tvuln1 := VulnerabilityHandle{\n\t\tName: \"CVE-1234-5678\",\n\t\tBlobValue: &VulnerabilityBlob{\n\t\t\tID: \"CVE-1234-5678\",\n\t\t},\n\t\tProvider: &Provider{\n\t\t\tID: \"provider!\",\n\t\t},\n\t}\n\n\tvuln2 := testVulnerabilityHandle()\n\n\terr := s.AddVulnerabilities(&vuln1, &vuln2)\n\trequire.NoError(t, err)\n\n\tvar result1 VulnerabilityHandle\n\terr = db.Where(\"name = ?\", \"CVE-1234-5678\").First(&result1).Error\n\trequire.NoError(t, err)\n\tassert.Equal(t, vuln1.Name, result1.Name)\n\tassert.Equal(t, vuln1.ID, result1.ID)\n\tassert.Equal(t, vuln1.BlobID, result1.BlobID)\n\tassert.Nil(t, result1.BlobValue) // since we're not preloading any fields on the fetch\n\tassert.Nil(t, result1.Provider)  // since we're not preloading any fields on the fetch\n\n\tvar result2 VulnerabilityHandle\n\terr = db.Where(\"name = ?\", \"CVE-8765-4321\").First(&result2).Error\n\trequire.NoError(t, err)\n\tassert.Equal(t, vuln2.Name, result2.Name)\n\tassert.Equal(t, vuln2.ID, result2.ID)\n\tassert.Equal(t, vuln2.BlobID, result2.BlobID)\n\tassert.Nil(t, result2.BlobValue) // since we're not preloading any fields on the fetch\n\tassert.Nil(t, result1.Provider)  // since we're not preloading any fields on the fetch\n}\n\nfunc TestVulnerabilityStore_NoDuplicateVulnerabilities(t *testing.T) {\n\tdb := setupTestStore(t).db\n\tbw := newBlobStore(db)\n\ts := newVulnerabilityStore(db, bw)\n\n\tvuln := VulnerabilityHandle{\n\t\tName: \"CVE-1234-5678\",\n\t\tBlobValue: &VulnerabilityBlob{\n\t\t\tID: \"CVE-1234-5678\",\n\t\t},\n\t\tProvider: &Provider{\n\t\t\tID: \"provider!\",\n\t\t},\n\t}\n\n\terr := s.AddVulnerabilities(&vuln)\n\trequire.NoError(t, err)\n\n\terr = s.AddVulnerabilities(&vuln)\n\trequire.NoError(t, err)\n\n\tvar results []VulnerabilityHandle\n\terr = db.Where(\"name = ?\", \"CVE-1234-5678\").Preload(\"Provider\").Find(&results).Error\n\trequire.NoError(t, err)\n\trequire.Len(t, results, 1, \"expected exactly one vulnerability handle to be added\")\n\n\tresult := results[0]\n\tassert.NotEmpty(t, result.ProviderID)\n\tassert.NotEmpty(t, result.BlobID)\n\tif d := cmp.Diff(vuln, result, cmpopts.IgnoreFields(VulnerabilityHandle{}, \"BlobValue\")); d != \"\" {\n\t\tt.Errorf(\"unexpected result (-want +got):\\n%s\", d)\n\t}\n}\n\nfunc TestVulnerabilityStore_AddVulnerabilities_missingModifiedDate(t *testing.T) {\n\tdb := setupTestStore(t).db\n\tbw := newBlobStore(db)\n\ts := newVulnerabilityStore(db, bw)\n\n\tnow := time.Now()\n\tvuln := &VulnerabilityHandle{\n\t\tName:          \"CVE-1234-5678\",\n\t\tPublishedDate: &now, // have publication date without modification date\n\t\tProvider: &Provider{\n\t\t\tID: \"provider!\",\n\t\t},\n\t}\n\n\terr := s.AddVulnerabilities(vuln)\n\trequire.NoError(t, err)\n\n\t// patched!\n\tassert.NotNil(t, vuln.ModifiedDate)\n}\n\nfunc TestVulnerabilityStore_AddVulnerabilities_Aliases(t *testing.T) {\n\tdb := setupTestStore(t).db\n\tbw := newBlobStore(db)\n\ts := newVulnerabilityStore(db, bw)\n\n\tvuln := &VulnerabilityHandle{\n\t\tName: \"CVE-1234-5678\",\n\t\tBlobValue: &VulnerabilityBlob{\n\t\t\tID:      \"CVE-1234-5678\",\n\t\t\tAliases: []string{\"ALIAS-1\", \"ALIAS-2\", \"CVE-1234-5678\"},\n\t\t},\n\t\tProvider: &Provider{\n\t\t\tID: \"provider!\",\n\t\t},\n\t}\n\n\terr := s.AddVulnerabilities(vuln)\n\trequire.NoError(t, err)\n\n\tvar aliases []VulnerabilityAlias\n\terr = db.Where(\"name = ?\", \"CVE-1234-5678\").Find(&aliases).Error\n\trequire.NoError(t, err)\n\n\texpectedAliases := []VulnerabilityAlias{\n\t\t{Name: \"CVE-1234-5678\", Alias: \"ALIAS-1\"},\n\t\t{Name: \"CVE-1234-5678\", Alias: \"ALIAS-2\"},\n\t}\n\tassert.Len(t, aliases, len(expectedAliases))\n\n\tfor _, expected := range expectedAliases {\n\t\tassert.Contains(t, aliases, expected)\n\t}\n\n\tuniqueAliases := make(map[string]struct{})\n\tfor _, alias := range aliases {\n\t\tkey := alias.Name + \":\" + alias.Alias\n\t\t_, exists := uniqueAliases[key]\n\t\tassert.False(t, exists, \"duplicate alias found\")\n\t\tuniqueAliases[key] = struct{}{}\n\t}\n}\n\nfunc TestVulnerabilityStore_GetVulnerability_ByID(t *testing.T) {\n\tdb := setupTestStore(t).db\n\tbw := newBlobStore(db)\n\ts := newVulnerabilityStore(db, bw)\n\n\tvuln := testVulnerabilityHandle()\n\terr := s.AddVulnerabilities(&vuln)\n\trequire.NoError(t, err)\n\n\tresults, err := s.GetVulnerabilities(&VulnerabilitySpecifier{ID: vuln.ID}, nil) // don't preload by default\n\trequire.NoError(t, err)\n\trequire.Len(t, results, 1)\n\tresult := results[0]\n\n\tif d := cmp.Diff(vuln, result, cmpopts.IgnoreFields(VulnerabilityHandle{}, \"Provider\", \"BlobValue\")); d != \"\" {\n\t\tt.Errorf(\"unexpected result (-want +got):\\n%s\", d)\n\t}\n\tassert.Nil(t, result.BlobValue) // since we're not preloading any fields on the fetch\n\tassert.Nil(t, result.Provider)  // since we're not preloading any fields on the fetch\n\n\tresults, err = s.GetVulnerabilities(&VulnerabilitySpecifier{ID: vuln.ID}, &GetVulnerabilityOptions{Preload: true})\n\trequire.NoError(t, err)\n\trequire.Len(t, results, 1)\n\tresult = results[0]\n\n\tassert.NotNil(t, result.BlobValue)\n\tassert.NotNil(t, result.Provider)\n\tif d := cmp.Diff(vuln, result); d != \"\" {\n\t\tt.Errorf(\"unexpected result (-want +got):\\n%s\", d)\n\t}\n}\n\nfunc TestVulnerabilityStore_GetVulnerabilities_ByName(t *testing.T) {\n\tdb := setupTestStore(t).db\n\tbw := newBlobStore(db)\n\ts := newVulnerabilityStore(db, bw)\n\n\tvuln1 := testVulnerabilityHandle()\n\tname := vuln1.Name\n\tvuln2 := VulnerabilityHandle{Name: name, BlobID: 2, Provider: vuln1.Provider, BlobValue: &VulnerabilityBlob{\n\t\tID: name,\n\t}}\n\terr := s.AddVulnerabilities(&vuln1, &vuln2)\n\trequire.NoError(t, err)\n\n\texpected := []VulnerabilityHandle{vuln1, vuln2}\n\n\tresults, err := s.GetVulnerabilities(&VulnerabilitySpecifier{Name: name}, nil) // don't preload by default\n\trequire.NoError(t, err)\n\trequire.Len(t, results, 2)\n\tfor i, result := range results {\n\t\tassert.Equal(t, expected[i].Name, result.Name)\n\t\tassert.Equal(t, expected[i].ID, result.ID)\n\t\tassert.Equal(t, expected[i].BlobID, result.BlobID)\n\t\tassert.Nil(t, result.BlobValue) // since we're not preloading any fields on the fetch\n\t\tassert.Nil(t, result.Provider)  // since we're not preloading any fields on the fetch\n\t}\n\n\tresults, err = s.GetVulnerabilities(&VulnerabilitySpecifier{Name: name}, &GetVulnerabilityOptions{Preload: true})\n\trequire.NoError(t, err)\n\trequire.Len(t, results, 2)\n\n\tfor i, result := range results {\n\t\tif d := cmp.Diff(expected[i], result); d != \"\" {\n\t\t\tt.Errorf(\"unexpected result (-want +got):\\n%s\", d)\n\t\t}\n\t}\n}\n\nfunc TestVulnerabilityStore_GetVulnerabilities_Aliases(t *testing.T) {\n\tdb := setupTestStore(t).db\n\tbw := newBlobStore(db)\n\ts := newVulnerabilityStore(db, bw)\n\n\tvuln1 := &VulnerabilityHandle{\n\t\tName: \"CVE-1234-5678\",\n\t\tBlobValue: &VulnerabilityBlob{\n\t\t\tID:      \"CVE-1234-5678\",\n\t\t\tAliases: []string{\"ALIAS-1\", \"ALIAS-2\"},\n\t\t},\n\t\tProvider: &Provider{\n\t\t\tID: \"provider!\",\n\t\t},\n\t}\n\n\tvuln2 := &VulnerabilityHandle{\n\t\tName: \"ALIAS-1\",\n\t\tBlobValue: &VulnerabilityBlob{\n\t\t\tID: \"ALIAS-1\",\n\t\t},\n\t\tProvider: &Provider{\n\t\t\tID: \"provider2!\",\n\t\t},\n\t}\n\n\terr := s.AddVulnerabilities(vuln1, vuln2)\n\trequire.NoError(t, err)\n\n\tt.Run(\"include aliases\", func(t *testing.T) {\n\t\tspecifierWithAliases := &VulnerabilitySpecifier{\n\t\t\tName:           \"ALIAS-1\",\n\t\t\tIncludeAliases: true,\n\t\t}\n\n\t\tresults, err := s.GetVulnerabilities(specifierWithAliases, nil)\n\t\trequire.NoError(t, err)\n\t\trequire.Len(t, results, 2)\n\t\tassert.ElementsMatch(t, []string{\"CVE-1234-5678\", \"ALIAS-1\"}, []string{results[0].Name, results[1].Name})\n\t})\n\n\tt.Run(\"dont include aliases\", func(t *testing.T) {\n\t\tspecifierWithoutAliases := &VulnerabilitySpecifier{\n\t\t\tName:           \"ALIAS-1\",\n\t\t\tIncludeAliases: false,\n\t\t}\n\n\t\tresults, err := s.GetVulnerabilities(specifierWithoutAliases, nil)\n\t\trequire.NoError(t, err)\n\t\trequire.Len(t, results, 1)\n\t\tassert.Equal(t, \"ALIAS-1\", results[0].Name)\n\t})\n\n\tt.Run(\"direct match without aliases\", func(t *testing.T) {\n\t\tspecifierDirectMatch := &VulnerabilitySpecifier{\n\t\t\tName:           \"CVE-1234-5678\",\n\t\t\tIncludeAliases: false,\n\t\t}\n\n\t\tresults, err := s.GetVulnerabilities(specifierDirectMatch, nil)\n\t\trequire.NoError(t, err)\n\t\trequire.Len(t, results, 1)\n\t\tassert.Equal(t, \"CVE-1234-5678\", results[0].Name)\n\t})\n}\n\nfunc testVulnerabilityHandle() VulnerabilityHandle {\n\tnow := time.Now()\n\n\treturn VulnerabilityHandle{\n\t\tName:          \"CVE-8765-4321\",\n\t\tStatus:        \"status!\",\n\t\tPublishedDate: &now,\n\t\tModifiedDate:  &now,\n\t\tWithdrawnDate: &now,\n\t\tProvider: &Provider{\n\t\t\tID: \"provider!\",\n\t\t},\n\t\tBlobValue: &VulnerabilityBlob{\n\t\t\tID:          \"CVE-8765-4321\",\n\t\t\tAssigners:   []string{\"assigner!\"},\n\t\t\tDescription: \"description!\",\n\t\t\tReferences: []Reference{\n\t\t\t\t{\n\t\t\t\t\tURL:  \"url!\",\n\t\t\t\t\tTags: []string{\"tag!\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tAliases: []string{\"alias!\"},\n\t\t\tSeverities: []Severity{\n\t\t\t\t{\n\t\t\t\t\tScheme: \"scheme!\",\n\t\t\t\t\tValue:  \"value!\",\n\t\t\t\t\tSource: \"source!\",\n\t\t\t\t\tRank:   10,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tScheme: SeveritySchemeCVSS,\n\t\t\t\t\tValue: CVSSSeverity{\n\t\t\t\t\t\tVector:  \"CVSS:4.0/AV:L/AC:H/AT:P/PR:N/UI:P/VC:L/VI:H/VA:N/SC:N/SI:L/SA:N\",\n\t\t\t\t\t\tVersion: \"4.0\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n}\n\nfunc TestVulnerabilityStore_GetVulnerabilities_ByProviders(t *testing.T) {\n\tdb := setupTestStore(t).db\n\tbw := newBlobStore(db)\n\ts := newVulnerabilityStore(db, bw)\n\n\tprovider1 := &Provider{ID: \"provider1\"}\n\tprovider2 := &Provider{ID: \"provider2\"}\n\n\tvuln1 := VulnerabilityHandle{Name: \"CVE-1234-5678\", BlobID: 1, Provider: provider1}\n\tvuln2 := VulnerabilityHandle{Name: \"CVE-2345-6789\", BlobID: 2, Provider: provider2}\n\n\terr := s.AddVulnerabilities(&vuln1, &vuln2)\n\trequire.NoError(t, err)\n\n\tresults, err := s.GetVulnerabilities(&VulnerabilitySpecifier{Providers: []string{\"provider1\"}}, nil)\n\trequire.NoError(t, err)\n\trequire.Len(t, results, 1)\n\tassert.Equal(t, vuln1.Name, results[0].Name)\n\tassert.Equal(t, vuln1.Provider.ID, results[0].ProviderID)\n\n\tresults, err = s.GetVulnerabilities(&VulnerabilitySpecifier{Providers: []string{\"provider1\", \"provider2\"}}, nil)\n\trequire.NoError(t, err)\n\trequire.Len(t, results, 2)\n\tassert.ElementsMatch(t, []string{vuln1.Name, vuln2.Name}, []string{results[0].Name, results[1].Name})\n}\n\nfunc TestVulnerabilityStore_GetVulnerabilities_FilterByMultipleFactors(t *testing.T) {\n\tdb := setupTestStore(t).db\n\tbw := newBlobStore(db)\n\ts := newVulnerabilityStore(db, bw)\n\n\tnow := time.Now()\n\toneDayAgo := now.Add(-24 * time.Hour)\n\thalfDayAgo := now.Add(-12 * time.Hour)\n\ttenDaysAgo := now.Add(-240 * time.Hour)\n\n\tprovider1 := &Provider{ID: \"provider1\"}\n\tprovider2 := &Provider{ID: \"provider2\"}\n\n\tvuln1 := VulnerabilityHandle{\n\t\tName:          \"CVE-1234-5678\",\n\t\tBlobID:        1,\n\t\tProvider:      provider1,\n\t\tPublishedDate: &halfDayAgo,\n\t}\n\n\tvuln2 := VulnerabilityHandle{\n\t\tName:          \"CVE-2345-6789\",\n\t\tBlobID:        2,\n\t\tProvider:      provider2, // filtered out due to provider\n\t\tPublishedDate: &now,\n\t}\n\n\tvuln3 := VulnerabilityHandle{\n\t\tName:          \"CVE-1234-5678\",\n\t\tBlobID:        3,\n\t\tProvider:      provider1,\n\t\tPublishedDate: &tenDaysAgo, // filtered out due to date\n\t}\n\n\terr := s.AddVulnerabilities(&vuln1, &vuln2, &vuln3)\n\trequire.NoError(t, err)\n\n\tresults, err := s.GetVulnerabilities(&VulnerabilitySpecifier{\n\t\tProviders:      []string{\"provider1\"}, // filter by provider...\n\t\tPublishedAfter: &oneDayAgo,            // filter by date published...\n\t}, nil)\n\trequire.NoError(t, err)\n\trequire.Len(t, results, 1)\n\tassert.Equal(t, vuln1.Name, results[0].Name)\n}\n"
  },
  {
    "path": "grype/db/v6/vulnerability_test.go",
    "content": "package v6\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\t\"unicode\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/grype/grype/vulnerability\"\n)\n\nfunc TestV5Namespace(t *testing.T) {\n\t// provider input should be derived from the Providers table:\n\t// +------------+---------+---------------+----------------------------------+------------------------+\n\t// | id         | version | processor     | date_captured                    | input_digest           |\n\t// +------------+---------+---------------+----------------------------------+------------------------+\n\t// | nvd        | 2       | vunnel@0.29.0 | 2025-01-08 01:32:55.179881+00:00 | xxh64:0a160d2b53dd0208 |\n\t// | alpine     | 1       | vunnel@0.29.0 | 2025-01-08 01:31:28.824872+00:00 | xxh64:30c5b7b8efa0c087 |\n\t// | amazon     | 1       | vunnel@0.29.0 | 2025-01-08 01:31:28.837469+00:00 | xxh64:7d90b3fa66b183bc |\n\t// | chainguard | 1       | vunnel@0.29.0 | 2025-01-08 01:31:26.969865+00:00 | xxh64:25a82fa97ac9e077 |\n\t// | debian     | 1       | vunnel@0.29.0 | 2025-01-08 01:31:50.718966+00:00 | xxh64:4b1834b9e4e68987 |\n\t// | github     | 1       | vunnel@0.29.0 | 2025-01-08 01:31:27.450124+00:00 | xxh64:a3ee6b48d37a0124 |\n\t// | mariner    | 1       | vunnel@0.29.0 | 2025-01-08 01:32:35.005761+00:00 | xxh64:cb4f5861a1fda0af |\n\t// | oracle     | 1       | vunnel@0.29.0 | 2025-01-08 01:32:33.696274+00:00 | xxh64:72c0a15731e96ab3 |\n\t// | rhel       | 1       | vunnel@0.29.0 | 2025-01-08 01:32:32.192345+00:00 | xxh64:abf5d2fd5a26c194 |\n\t// | sles       | 1       | vunnel@0.29.0 | 2025-01-08 01:32:42.988937+00:00 | xxh64:8f558f8f28a04489 |\n\t// | ubuntu     | 3       | vunnel@0.29.0 | 2025-01-08 01:33:25.795537+00:00 | xxh64:97ef8421c0093620 |\n\t// | wolfi      | 1       | vunnel@0.29.0 | 2025-01-08 01:32:58.571417+00:00 | xxh64:f294f3474d35b1a9 |\n\t// +------------+---------+---------------+----------------------------------+------------------------+\n\n\t// the expected results should mimic what is found as v5 namespace values:\n\t// +--------------------------------------+\n\t// | namespace                            |\n\t// +--------------------------------------+\n\t// | nvd:cpe                              |\n\t// | github:language:javascript           |\n\t// | ubuntu:distro:ubuntu:14.04           |\n\t// | ubuntu:distro:ubuntu:16.04           |\n\t// | ubuntu:distro:ubuntu:18.04           |\n\t// | ubuntu:distro:ubuntu:20.04           |\n\t// | ubuntu:distro:ubuntu:22.04           |\n\t// | ubuntu:distro:ubuntu:22.10           |\n\t// | ubuntu:distro:ubuntu:23.04           |\n\t// | ubuntu:distro:ubuntu:23.10           |\n\t// | ubuntu:distro:ubuntu:24.10           |\n\t// | debian:distro:debian:8               |\n\t// | debian:distro:debian:9               |\n\t// | ubuntu:distro:ubuntu:12.04           |\n\t// | ubuntu:distro:ubuntu:15.04           |\n\t// | sles:distro:sles:15                  |\n\t// | sles:distro:sles:15.1                |\n\t// | sles:distro:sles:15.2                |\n\t// | sles:distro:sles:15.3                |\n\t// | sles:distro:sles:15.4                |\n\t// | sles:distro:sles:15.5                |\n\t// | sles:distro:sles:15.6                |\n\t// | amazon:distro:amazonlinux:2          |\n\t// | debian:distro:debian:10              |\n\t// | debian:distro:debian:11              |\n\t// | debian:distro:debian:12              |\n\t// | debian:distro:debian:unstable        |\n\t// | oracle:distro:oraclelinux:6          |\n\t// | oracle:distro:oraclelinux:7          |\n\t// | oracle:distro:oraclelinux:8          |\n\t// | oracle:distro:oraclelinux:9          |\n\t// | redhat:distro:redhat:6               |\n\t// | redhat:distro:redhat:7               |\n\t// | redhat:distro:redhat:8               |\n\t// | redhat:distro:redhat:9               |\n\t// | ubuntu:distro:ubuntu:12.10           |\n\t// | ubuntu:distro:ubuntu:13.04           |\n\t// | ubuntu:distro:ubuntu:14.10           |\n\t// | ubuntu:distro:ubuntu:15.10           |\n\t// | ubuntu:distro:ubuntu:16.10           |\n\t// | ubuntu:distro:ubuntu:17.04           |\n\t// | ubuntu:distro:ubuntu:17.10           |\n\t// | ubuntu:distro:ubuntu:18.10           |\n\t// | ubuntu:distro:ubuntu:19.04           |\n\t// | ubuntu:distro:ubuntu:19.10           |\n\t// | ubuntu:distro:ubuntu:20.10           |\n\t// | ubuntu:distro:ubuntu:21.04           |\n\t// | ubuntu:distro:ubuntu:21.10           |\n\t// | ubuntu:distro:ubuntu:24.04           |\n\t// | github:language:php                  |\n\t// | debian:distro:debian:13              |\n\t// | debian:distro:debian:7               |\n\t// | redhat:distro:redhat:5               |\n\t// | sles:distro:sles:11.1                |\n\t// | sles:distro:sles:11.3                |\n\t// | sles:distro:sles:11.4                |\n\t// | sles:distro:sles:11.2                |\n\t// | sles:distro:sles:12                  |\n\t// | sles:distro:sles:12.1                |\n\t// | sles:distro:sles:12.2                |\n\t// | sles:distro:sles:12.3                |\n\t// | sles:distro:sles:12.4                |\n\t// | sles:distro:sles:12.5                |\n\t// | chainguard:distro:chainguard:rolling |\n\t// | wolfi:distro:wolfi:rolling           |\n\t// | minimos:distro:minimos:rolling       |\n\t// | github:language:go                   |\n\t// | alpine:distro:alpine:3.20            |\n\t// | alpine:distro:alpine:3.21            |\n\t// | alpine:distro:alpine:edge            |\n\t// | github:language:rust                 |\n\t// | github:language:python               |\n\t// | sles:distro:sles:11                  |\n\t// | oracle:distro:oraclelinux:5          |\n\t// | github:language:ruby                 |\n\t// | github:language:dotnet               |\n\t// | alpine:distro:alpine:3.12            |\n\t// | alpine:distro:alpine:3.13            |\n\t// | alpine:distro:alpine:3.14            |\n\t// | alpine:distro:alpine:3.15            |\n\t// | alpine:distro:alpine:3.16            |\n\t// | alpine:distro:alpine:3.17            |\n\t// | alpine:distro:alpine:3.18            |\n\t// | alpine:distro:alpine:3.19            |\n\t// | mariner:distro:mariner:2.0           |\n\t// | github:language:java                 |\n\t// | github:language:dart                 |\n\t// | amazon:distro:amazonlinux:2023       |\n\t// | alpine:distro:alpine:3.10            |\n\t// | alpine:distro:alpine:3.11            |\n\t// | alpine:distro:alpine:3.4             |\n\t// | alpine:distro:alpine:3.5             |\n\t// | alpine:distro:alpine:3.7             |\n\t// | alpine:distro:alpine:3.8             |\n\t// | alpine:distro:alpine:3.9             |\n\t// | mariner:distro:azurelinux:3.0        |\n\t// | mariner:distro:mariner:1.0           |\n\t// | alpine:distro:alpine:3.3             |\n\t// | alpine:distro:alpine:3.6             |\n\t// | amazon:distro:amazonlinux:2022       |\n\t// | alpine:distro:alpine:3.2             |\n\t// | github:language:swift                |\n\t// +--------------------------------------+\n\n\ttype testCase struct {\n\t\tname        string\n\t\tprovider    string // from Providers.id\n\t\tecosystem   string // only used when provider non-os provider\n\t\tpackageName string // only used for msrc\n\t\tosName      string // only used for OS-based providers\n\t\tosVersion   string // only used for OS-based providers\n\t\texpected    string //\n\t}\n\n\ttests := []testCase{\n\t\t// NVD\n\t\t{\n\t\t\tname:     \"nvd provider\",\n\t\t\tprovider: \"nvd\",\n\t\t\texpected: \"nvd:cpe\",\n\t\t},\n\n\t\t// GitHub ecosystem tests\n\t\t{\n\t\t\tname:      \"github golang direct\",\n\t\t\tprovider:  \"github\",\n\t\t\tecosystem: \"golang\",\n\t\t\texpected:  \"github:language:go\",\n\t\t},\n\t\t{\n\t\t\tname:      \"github go-module ecosystem\",\n\t\t\tprovider:  \"github\",\n\t\t\tecosystem: \"go-module\",\n\t\t\texpected:  \"github:language:go\",\n\t\t},\n\t\t{\n\t\t\tname:      \"github composer ecosystem\",\n\t\t\tprovider:  \"github\",\n\t\t\tecosystem: \"composer\",\n\t\t\texpected:  \"github:language:php\",\n\t\t},\n\t\t{\n\t\t\tname:      \"github php-composer ecosystem\",\n\t\t\tprovider:  \"github\",\n\t\t\tecosystem: \"php-composer\",\n\t\t\texpected:  \"github:language:php\",\n\t\t},\n\t\t{\n\t\t\tname:      \"github cargo ecosystem\",\n\t\t\tprovider:  \"github\",\n\t\t\tecosystem: \"cargo\",\n\t\t\texpected:  \"github:language:rust\",\n\t\t},\n\t\t{\n\t\t\tname:      \"github rust-crate ecosystem\",\n\t\t\tprovider:  \"github\",\n\t\t\tecosystem: \"rust-crate\",\n\t\t\texpected:  \"github:language:rust\",\n\t\t},\n\t\t{\n\t\t\tname:      \"github pub ecosystem\",\n\t\t\tprovider:  \"github\",\n\t\t\tecosystem: \"pub\",\n\t\t\texpected:  \"github:language:dart\",\n\t\t},\n\t\t{\n\t\t\tname:      \"github dart-pub ecosystem\",\n\t\t\tprovider:  \"github\",\n\t\t\tecosystem: \"dart-pub\",\n\t\t\texpected:  \"github:language:dart\",\n\t\t},\n\t\t{\n\t\t\tname:      \"github nuget ecosystem\",\n\t\t\tprovider:  \"github\",\n\t\t\tecosystem: \"nuget\",\n\t\t\texpected:  \"github:language:dotnet\",\n\t\t},\n\t\t{\n\t\t\tname:      \"github maven ecosystem\",\n\t\t\tprovider:  \"github\",\n\t\t\tecosystem: \"maven\",\n\t\t\texpected:  \"github:language:java\",\n\t\t},\n\t\t{\n\t\t\tname:      \"github java ecosystem\",\n\t\t\tprovider:  \"github\",\n\t\t\tecosystem: \"java\",\n\t\t\texpected:  \"github:language:java\",\n\t\t},\n\t\t{\n\t\t\tname:      \"syft pkg type java-archive\",\n\t\t\tprovider:  \"github\",\n\t\t\tecosystem: \"java-archive\",\n\t\t\texpected:  \"github:language:java\",\n\t\t},\n\t\t{\n\t\t\tname:      \"github swifturl ecosystem\",\n\t\t\tprovider:  \"github\",\n\t\t\tecosystem: \"swifturl\",\n\t\t\texpected:  \"github:language:swift\",\n\t\t},\n\t\t{\n\t\t\tname:      \"github npm ecosystem\",\n\t\t\tprovider:  \"github\",\n\t\t\tecosystem: \"npm\",\n\t\t\texpected:  \"github:language:javascript\",\n\t\t},\n\t\t{\n\t\t\tname:      \"github node ecosystem\",\n\t\t\tprovider:  \"github\",\n\t\t\tecosystem: \"node\",\n\t\t\texpected:  \"github:language:javascript\",\n\t\t},\n\t\t{\n\t\t\tname:      \"github pypi ecosystem\",\n\t\t\tprovider:  \"github\",\n\t\t\tecosystem: \"pypi\",\n\t\t\texpected:  \"github:language:python\",\n\t\t},\n\t\t{\n\t\t\tname:      \"github pip ecosystem\",\n\t\t\tprovider:  \"github\",\n\t\t\tecosystem: \"pip\",\n\t\t\texpected:  \"github:language:python\",\n\t\t},\n\t\t{\n\t\t\tname:      \"github rubygems ecosystem\",\n\t\t\tprovider:  \"github\",\n\t\t\tecosystem: \"rubygems\",\n\t\t\texpected:  \"github:language:ruby\",\n\t\t},\n\t\t{\n\t\t\tname:      \"github gem ecosystem\",\n\t\t\tprovider:  \"github\",\n\t\t\tecosystem: \"gem\",\n\t\t\texpected:  \"github:language:ruby\",\n\t\t},\n\n\t\t// OS Distribution tests\n\t\t{\n\t\t\tname:      \"ubuntu distribution\",\n\t\t\tprovider:  \"ubuntu\",\n\t\t\tosName:    \"ubuntu\",\n\t\t\tosVersion: \"22.04\",\n\t\t\texpected:  \"ubuntu:distro:ubuntu:22.04\",\n\t\t},\n\t\t{\n\t\t\tname:      \"ubuntu distribution (trimmed 0s)\",\n\t\t\tprovider:  \"ubuntu\",\n\t\t\tosName:    \"ubuntu\",\n\t\t\tosVersion: \"22.4\",\n\t\t\texpected:  \"ubuntu:distro:ubuntu:22.04\",\n\t\t},\n\t\t{\n\t\t\tname:      \"redhat distribution\",\n\t\t\tprovider:  \"rhel\",\n\t\t\tosName:    \"redhat\",\n\t\t\tosVersion: \"8\",\n\t\t\texpected:  \"redhat:distro:redhat:8\",\n\t\t},\n\t\t{\n\t\t\tname:      \"debian distribution\",\n\t\t\tprovider:  \"debian\",\n\t\t\tosName:    \"debian\",\n\t\t\tosVersion: \"11\",\n\t\t\texpected:  \"debian:distro:debian:11\",\n\t\t},\n\t\t{\n\t\t\tname:      \"sles distribution\",\n\t\t\tprovider:  \"sles\",\n\t\t\tosName:    \"sles\",\n\t\t\tosVersion: \"15.5\",\n\t\t\texpected:  \"sles:distro:sles:15.5\",\n\t\t},\n\t\t{\n\t\t\tname:      \"alpine distribution\",\n\t\t\tprovider:  \"alpine\",\n\t\t\tosName:    \"alpine\",\n\t\t\tosVersion: \"3.18\",\n\t\t\texpected:  \"alpine:distro:alpine:3.18\",\n\t\t},\n\t\t{\n\t\t\tname:      \"chainguard distribution\",\n\t\t\tprovider:  \"chainguard\",\n\t\t\tosName:    \"chainguard\",\n\t\t\tosVersion: \"rolling\",\n\t\t\texpected:  \"chainguard:distro:chainguard:rolling\",\n\t\t},\n\t\t{\n\t\t\tname:      \"wolfi distribution\",\n\t\t\tprovider:  \"wolfi\",\n\t\t\tosName:    \"wolfi\",\n\t\t\tosVersion: \"rolling\",\n\t\t\texpected:  \"wolfi:distro:wolfi:rolling\",\n\t\t},\n\t\t{\n\t\t\tname:      \"minimos distribution\",\n\t\t\tprovider:  \"minimos\",\n\t\t\tosName:    \"minimos\",\n\t\t\tosVersion: \"rolling\",\n\t\t\texpected:  \"minimos:distro:minimos:rolling\",\n\t\t},\n\t\t{\n\t\t\tname:      \"amazon linux distribution\",\n\t\t\tprovider:  \"amazon\",\n\t\t\tosName:    \"amazon\",\n\t\t\tosVersion: \"2023\",\n\t\t\texpected:  \"amazon:distro:amazonlinux:2023\",\n\t\t},\n\t\t{\n\t\t\tname:      \"mariner regular version\",\n\t\t\tprovider:  \"mariner\",\n\t\t\tosName:    \"mariner\",\n\t\t\tosVersion: \"2.0\",\n\t\t\texpected:  \"mariner:distro:mariner:2.0\",\n\t\t},\n\t\t{\n\t\t\tname:      \"mariner regular version (not exact match)\",\n\t\t\tprovider:  \"mariner\",\n\t\t\tosName:    \"mariner\",\n\t\t\tosVersion: \"2.1\",\n\t\t\texpected:  \"mariner:distro:mariner:2.1\",\n\t\t},\n\t\t{\n\t\t\tname:      \"mariner regular version (auto fill minor version)\",\n\t\t\tprovider:  \"mariner\",\n\t\t\tosName:    \"mariner\",\n\t\t\tosVersion: \"1\",\n\t\t\texpected:  \"mariner:distro:mariner:1.0\",\n\t\t},\n\t\t{\n\t\t\tname:      \"mariner azure version\",\n\t\t\tprovider:  \"mariner\",\n\t\t\tosName:    \"mariner\",\n\t\t\tosVersion: \"3.0\",\n\t\t\texpected:  \"mariner:distro:azurelinux:3.0\",\n\t\t},\n\t\t{\n\t\t\tname:      \"mariner azure version (missing version fields)\",\n\t\t\tprovider:  \"mariner\",\n\t\t\tosName:    \"mariner\",\n\t\t\tosVersion: \"3\",\n\t\t\texpected:  \"mariner:distro:azurelinux:3.0\",\n\t\t},\n\t\t{\n\t\t\tname:      \"azurelinux version (extra version fields)\",\n\t\t\tprovider:  \"mariner\",\n\t\t\tosName:    \"azurelinux\",\n\t\t\tosVersion: \"3.0.20240727\",\n\t\t\texpected:  \"mariner:distro:azurelinux:3.0\",\n\t\t},\n\t\t{\n\t\t\tname:      \"azurelinux version\",\n\t\t\tprovider:  \"mariner\",\n\t\t\tosName:    \"azurelinux\",\n\t\t\tosVersion: \"3.0\",\n\t\t\texpected:  \"mariner:distro:azurelinux:3.0\",\n\t\t},\n\t\t{\n\t\t\tname:      \"azurelinux version (missing version fields)\",\n\t\t\tprovider:  \"mariner\",\n\t\t\tosName:    \"azurelinux\",\n\t\t\tosVersion: \"3\",\n\t\t\texpected:  \"mariner:distro:azurelinux:3.0\",\n\t\t},\n\t\t{\n\t\t\tname:      \"mariner azure version (extra version fields)\",\n\t\t\tprovider:  \"mariner\",\n\t\t\tosName:    \"mariner\",\n\t\t\tosVersion: \"3.0.20240727\",\n\t\t\texpected:  \"mariner:distro:azurelinux:3.0\",\n\t\t},\n\t\t{\n\t\t\tname:      \"oracle linux distribution\",\n\t\t\tprovider:  \"oracle\",\n\t\t\tosName:    \"oracle\",\n\t\t\tosVersion: \"8\",\n\t\t\texpected:  \"oracle:distro:oraclelinux:8\",\n\t\t},\n\t\t{\n\t\t\tname:      \"echo distribution\",\n\t\t\tprovider:  \"echo\",\n\t\t\tosName:    \"echo\",\n\t\t\tosVersion: \"rolling\",\n\t\t\texpected:  \"echo:distro:echo:rolling\",\n\t\t},\n\t\t{\n\t\t\tname:      \"minimos distribution\",\n\t\t\tprovider:  \"minimos\",\n\t\t\tosName:    \"minimos\",\n\t\t\tosVersion: \"rolling\",\n\t\t\texpected:  \"minimos:distro:minimos:rolling\",\n\t\t},\n\n\t\t// Version truncation tests\n\t\t{\n\t\t\tname:      \"rhel with minor version\",\n\t\t\tprovider:  \"rhel\",\n\t\t\tosName:    \"redhat\",\n\t\t\tosVersion: \"8.6\",\n\t\t\texpected:  \"redhat:distro:redhat:8\",\n\t\t},\n\t\t{\n\t\t\tname:      \"rhel with patch version\",\n\t\t\tprovider:  \"rhel\",\n\t\t\tosName:    \"redhat\",\n\t\t\tosVersion: \"9.2.1\",\n\t\t\texpected:  \"redhat:distro:redhat:9\",\n\t\t},\n\t\t{\n\t\t\tname:      \"oracle with minor version\",\n\t\t\tprovider:  \"oracle\",\n\t\t\tosName:    \"oracle\",\n\t\t\tosVersion: \"8.7\",\n\t\t\texpected:  \"oracle:distro:oraclelinux:8\",\n\t\t},\n\t\t{\n\t\t\tname:      \"oracle with patch version\",\n\t\t\tprovider:  \"oracle\",\n\t\t\tosName:    \"oracle\",\n\t\t\tosVersion: \"9.3.1\",\n\t\t\texpected:  \"oracle:distro:oraclelinux:9\",\n\t\t},\n\t\t// msrc is modeled as a distro for v5 but is just a package in v6\n\t\t{\n\t\t\tname:        \"microsoft msrc-kb\",\n\t\t\tprovider:    \"msrc\",\n\t\t\tecosystem:   \"msrc-kb\",\n\t\t\tpackageName: \"10012\",\n\t\t\texpected:    \"msrc:distro:windows:10012\",\n\t\t},\n\n\t\t// new provider existing ecosystem\n\t\t{\n\t\t\tname:      \"grizzly go-module\",\n\t\t\tprovider:  \"grizzly\",\n\t\t\tecosystem: \"go-module\",\n\t\t\texpected:  \"grizzly:language:go\",\n\t\t},\n\n\t\t// new provider new ecosystem\n\t\t{\n\t\t\tname:      \"armadillo pizza\",\n\t\t\tprovider:  \"armadillo\",\n\t\t\tecosystem: \"pizza\",\n\t\t\texpected:  \"armadillo:language:pizza\",\n\t\t},\n\n\t\t// new OS\n\t\t{\n\t\t\tname:      \"gothmog\",\n\t\t\tprovider:  \"gothmog\",\n\t\t\tosName:    \"gothmoglinux\",\n\t\t\tosVersion: \"zzzzzz11123\",\n\t\t\texpected:  \"gothmog:distro:gothmoglinux:zzzzzz11123\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvuln := &VulnerabilityHandle{\n\t\t\t\tProvider: &Provider{\n\t\t\t\t\tID: tt.provider,\n\t\t\t\t},\n\t\t\t}\n\n\t\t\tpkg := &AffectedPackageHandle{}\n\n\t\t\tif tt.osName != \"\" {\n\t\t\t\tmajor, minor, _ := majorMinorPatch(tt.osVersion)\n\t\t\t\tvar label string\n\t\t\t\tif major == \"\" {\n\t\t\t\t\tlabel = tt.osVersion\n\t\t\t\t}\n\t\t\t\tpkg.OperatingSystem = &OperatingSystem{\n\t\t\t\t\tName:         tt.osName,\n\t\t\t\t\tMajorVersion: major,\n\t\t\t\t\tMinorVersion: minor,\n\t\t\t\t\tLabelVersion: label,\n\t\t\t\t}\n\t\t\t\tpkg.Package = &Package{\n\t\t\t\t\tName:      \"os-package\",\n\t\t\t\t\tEcosystem: \"os-ecosystem\",\n\t\t\t\t}\n\t\t\t} else if tt.ecosystem != \"\" {\n\t\t\t\tpkg.Package = &Package{\n\t\t\t\t\tEcosystem: tt.ecosystem,\n\t\t\t\t\tName:      tt.packageName,\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tresult := MimicV5Namespace(vuln, pkg)\n\t\t\tassert.Equal(t, tt.expected, result)\n\t\t})\n\t}\n}\n\nfunc Test_getRelatedVulnerabilities(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tvuln     VulnerabilityHandle\n\t\taffected PackageBlob\n\t\tlanguage string\n\t\texpected []vulnerability.Reference\n\t}{\n\t\t{\n\t\t\tname: \"GHSA with related CVEs\",\n\t\t\tvuln: VulnerabilityHandle{\n\t\t\t\tName:       \"GHSA-1234\",\n\t\t\t\tProviderID: githubProvider,\n\t\t\t\tBlobValue: &VulnerabilityBlob{\n\t\t\t\t\tAliases: []string{\"CVE-2024-1\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\taffected: PackageBlob{\n\t\t\t\tCVEs: []string{\"CVE-2024-2\", \"CVE-2024-3\"},\n\t\t\t},\n\t\t\tlanguage: \"python\",\n\t\t\texpected: []vulnerability.Reference{\n\t\t\t\t{ID: \"CVE-2024-1\", Namespace: v5NvdNamespace},\n\t\t\t\t{ID: \"CVE-2024-2\", Namespace: v5NvdNamespace},\n\t\t\t\t{ID: \"CVE-2024-3\", Namespace: v5NvdNamespace},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"CGA with related GHSA\",\n\t\t\tvuln: VulnerabilityHandle{\n\t\t\t\tName:       \"CGA-1234\",\n\t\t\t\tProviderID: githubProvider,\n\t\t\t\tBlobValue: &VulnerabilityBlob{\n\t\t\t\t\tAliases: []string{\"GHSA-1234\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\taffected: PackageBlob{\n\t\t\t\tCVEs: []string{\"CVE-2024-2\", \"CVE-2024-3\"},\n\t\t\t},\n\t\t\tlanguage: \"python\",\n\t\t\texpected: []vulnerability.Reference{\n\t\t\t\t{ID: \"GHSA-1234\", Namespace: \"github:language:python\"},\n\t\t\t\t{ID: \"CVE-2024-2\", Namespace: v5NvdNamespace},\n\t\t\t\t{ID: \"CVE-2024-3\", Namespace: v5NvdNamespace},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"CVE with related CVEs\",\n\t\t\tvuln: VulnerabilityHandle{\n\t\t\t\tName:       \"CVE-2024-1234\",\n\t\t\t\tProviderID: \"rhel\",\n\t\t\t\tBlobValue: &VulnerabilityBlob{\n\t\t\t\t\tAliases: []string{\"CVE-2024-1\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\taffected: PackageBlob{\n\t\t\t\tCVEs: []string{\"CVE-2024-2\", \"CVE-2024-3\"},\n\t\t\t},\n\t\t\texpected: []vulnerability.Reference{\n\t\t\t\t{ID: \"CVE-2024-1234\", Namespace: v5NvdNamespace},\n\t\t\t\t{ID: \"CVE-2024-1\", Namespace: v5NvdNamespace},\n\t\t\t\t{ID: \"CVE-2024-2\", Namespace: v5NvdNamespace},\n\t\t\t\t{ID: \"CVE-2024-3\", Namespace: v5NvdNamespace},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"nvd CVE skips related CVEs to self\",\n\t\t\tvuln: VulnerabilityHandle{\n\t\t\t\tName:       \"CVE-2024-1234\",\n\t\t\t\tProviderID: nvdProvider,\n\t\t\t\tBlobValue: &VulnerabilityBlob{\n\t\t\t\t\tAliases: []string{\"CVE-2024-1\", \"CVE-2024-1234\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\taffected: PackageBlob{\n\t\t\t\tCVEs: []string{\"CVE-2024-2\", \"CVE-2024-1234\"},\n\t\t\t},\n\t\t\texpected: []vulnerability.Reference{\n\t\t\t\t{ID: \"CVE-2024-1\", Namespace: v5NvdNamespace},\n\t\t\t\t{ID: \"CVE-2024-2\", Namespace: v5NvdNamespace},\n\t\t\t}, // does not include \"CVE-2024-1234\"\n\t\t},\n\t\t{\n\t\t\tname: \"non-nvd CVE with related nvd CVEs to self\",\n\t\t\tvuln: VulnerabilityHandle{\n\t\t\t\tName:       \"CVE-2024-1234\",\n\t\t\t\tProviderID: \"rhel\",\n\t\t\t\tBlobValue: &VulnerabilityBlob{\n\t\t\t\t\tAliases: []string{\"CVE-2024-1\", \"CVE-2024-1234\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\taffected: PackageBlob{\n\t\t\t\tCVEs: []string{\"CVE-2024-2\", \"CVE-2024-1234\"},\n\t\t\t},\n\t\t\texpected: []vulnerability.Reference{\n\t\t\t\t{ID: \"CVE-2024-1\", Namespace: v5NvdNamespace},\n\t\t\t\t{ID: \"CVE-2024-2\", Namespace: v5NvdNamespace},\n\t\t\t\t{ID: \"CVE-2024-1234\", Namespace: v5NvdNamespace},\n\t\t\t}, // does include \"CVE-2024-1234\"\n\t\t},\n\t\t{\n\t\t\tname: \"non-nvd CVE always relates back to NVD\",\n\t\t\tvuln: VulnerabilityHandle{\n\t\t\t\tName:       \"CVE-2024-1234\",\n\t\t\t\tProviderID: \"rhel\",\n\t\t\t\tBlobValue: &VulnerabilityBlob{\n\t\t\t\t\tAliases: []string{},\n\t\t\t\t},\n\t\t\t},\n\t\t\taffected: PackageBlob{\n\t\t\t\tCVEs: []string{},\n\t\t\t},\n\t\t\texpected: []vulnerability.Reference{\n\t\t\t\t{ID: \"CVE-2024-1234\", Namespace: v5NvdNamespace},\n\t\t\t}, // does include \"CVE-2024-1234\"\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot := getRelatedVulnerabilities(&tt.vuln, &tt.affected, tt.language)\n\t\t\trequire.ElementsMatch(t, tt.expected, got)\n\t\t})\n\t}\n}\n\nfunc TestToFix(t *testing.T) {\n\tfixedDate := time.Date(2024, 1, 15, 10, 0, 0, 0, time.UTC)\n\tanotherDate := time.Date(2024, 2, 1, 12, 0, 0, 0, time.UTC)\n\n\ttests := []struct {\n\t\tname           string\n\t\taffectedRanges []Range\n\t\texpectedFix    vulnerability.Fix\n\t}{\n\t\t{\n\t\t\tname:           \"empty affected ranges\",\n\t\t\taffectedRanges: []Range{},\n\t\t\texpectedFix:    vulnerability.Fix{},\n\t\t},\n\t\t{\n\t\t\tname: \"all ranges have nil fix\",\n\t\t\taffectedRanges: []Range{\n\t\t\t\t{Fix: nil},\n\t\t\t\t{Fix: nil},\n\t\t\t},\n\t\t\texpectedFix: vulnerability.Fix{},\n\t\t},\n\t\t{\n\t\t\tname: \"single fixed version without available info\",\n\t\t\taffectedRanges: []Range{\n\t\t\t\t{\n\t\t\t\t\tFix: &Fix{\n\t\t\t\t\t\tVersion: \"1.2.3\",\n\t\t\t\t\t\tState:   FixedStatus,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedFix: vulnerability.Fix{\n\t\t\t\tVersions:  []string{\"1.2.3\"},\n\t\t\t\tState:     vulnerability.FixStateFixed,\n\t\t\t\tAvailable: nil,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"single fixed version with complete available info\",\n\t\t\taffectedRanges: []Range{\n\t\t\t\t{\n\t\t\t\t\tFix: &Fix{\n\t\t\t\t\t\tVersion: \"1.2.3\",\n\t\t\t\t\t\tState:   FixedStatus,\n\t\t\t\t\t\tDetail: &FixDetail{\n\t\t\t\t\t\t\tAvailable: &FixAvailability{\n\t\t\t\t\t\t\t\tDate: &fixedDate,\n\t\t\t\t\t\t\t\tKind: \"first-observed\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedFix: vulnerability.Fix{\n\t\t\t\tVersions: []string{\"1.2.3\"},\n\t\t\t\tState:    vulnerability.FixStateFixed,\n\t\t\t\tAvailable: []vulnerability.FixAvailable{\n\t\t\t\t\t{\n\t\t\t\t\t\tVersion: \"1.2.3\",\n\t\t\t\t\t\tDate:    fixedDate,\n\t\t\t\t\t\tKind:    \"first-observed\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"fix detail is nil - no available info\",\n\t\t\taffectedRanges: []Range{\n\t\t\t\t{\n\t\t\t\t\tFix: &Fix{\n\t\t\t\t\t\tVersion: \"1.2.3\",\n\t\t\t\t\t\tState:   FixedStatus,\n\t\t\t\t\t\tDetail:  nil,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedFix: vulnerability.Fix{\n\t\t\t\tVersions:  []string{\"1.2.3\"},\n\t\t\t\tState:     vulnerability.FixStateFixed,\n\t\t\t\tAvailable: nil,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"fix detail available is nil - no available info\",\n\t\t\taffectedRanges: []Range{\n\t\t\t\t{\n\t\t\t\t\tFix: &Fix{\n\t\t\t\t\t\tVersion: \"1.2.3\",\n\t\t\t\t\t\tState:   FixedStatus,\n\t\t\t\t\t\tDetail: &FixDetail{\n\t\t\t\t\t\t\tAvailable: nil,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedFix: vulnerability.Fix{\n\t\t\t\tVersions:  []string{\"1.2.3\"},\n\t\t\t\tState:     vulnerability.FixStateFixed,\n\t\t\t\tAvailable: nil,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"fix detail available date is nil - no available info\",\n\t\t\taffectedRanges: []Range{\n\t\t\t\t{\n\t\t\t\t\tFix: &Fix{\n\t\t\t\t\t\tVersion: \"1.2.3\",\n\t\t\t\t\t\tState:   FixedStatus,\n\t\t\t\t\t\tDetail: &FixDetail{\n\t\t\t\t\t\t\tAvailable: &FixAvailability{\n\t\t\t\t\t\t\t\tDate: nil,\n\t\t\t\t\t\t\t\tKind: \"first-observed\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedFix: vulnerability.Fix{\n\t\t\t\tVersions:  []string{\"1.2.3\"},\n\t\t\t\tState:     vulnerability.FixStateFixed,\n\t\t\t\tAvailable: nil,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"multiple fixed versions with mixed available info\",\n\t\t\taffectedRanges: []Range{\n\t\t\t\t{\n\t\t\t\t\tFix: &Fix{\n\t\t\t\t\t\tVersion: \"1.2.3\",\n\t\t\t\t\t\tState:   FixedStatus,\n\t\t\t\t\t\tDetail: &FixDetail{\n\t\t\t\t\t\t\tAvailable: &FixAvailability{\n\t\t\t\t\t\t\t\tDate: &fixedDate,\n\t\t\t\t\t\t\t\tKind: \"first-observed\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tFix: &Fix{\n\t\t\t\t\t\tVersion: \"1.3.0\",\n\t\t\t\t\t\tState:   FixedStatus,\n\t\t\t\t\t\tDetail: &FixDetail{\n\t\t\t\t\t\t\tAvailable: &FixAvailability{\n\t\t\t\t\t\t\t\tDate: nil, // this should not create an available entry\n\t\t\t\t\t\t\t\tKind: \"first-observed\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tFix: &Fix{\n\t\t\t\t\t\tVersion: \"1.4.0\",\n\t\t\t\t\t\tState:   FixedStatus,\n\t\t\t\t\t\tDetail: &FixDetail{\n\t\t\t\t\t\t\tAvailable: &FixAvailability{\n\t\t\t\t\t\t\t\tDate: &anotherDate,\n\t\t\t\t\t\t\t\tKind: \"first-observed\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedFix: vulnerability.Fix{\n\t\t\t\tVersions: []string{\"1.2.3\", \"1.3.0\", \"1.4.0\"},\n\t\t\t\tState:    vulnerability.FixStateFixed,\n\t\t\t\tAvailable: []vulnerability.FixAvailable{\n\t\t\t\t\t{\n\t\t\t\t\t\tVersion: \"1.2.3\",\n\t\t\t\t\t\tDate:    fixedDate,\n\t\t\t\t\t\tKind:    \"first-observed\",\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tVersion: \"1.4.0\",\n\t\t\t\t\t\tDate:    anotherDate,\n\t\t\t\t\t\tKind:    \"first-observed\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"wont fix status only\",\n\t\t\taffectedRanges: []Range{\n\t\t\t\t{\n\t\t\t\t\tFix: &Fix{\n\t\t\t\t\t\tVersion: \"1.2.3\",\n\t\t\t\t\t\tState:   WontFixStatus,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedFix: vulnerability.Fix{\n\t\t\t\tVersions:  nil,\n\t\t\t\tState:     vulnerability.FixStateWontFix,\n\t\t\t\tAvailable: nil,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"not fixed status only\",\n\t\t\taffectedRanges: []Range{\n\t\t\t\t{\n\t\t\t\t\tFix: &Fix{\n\t\t\t\t\t\tVersion: \"1.2.3\",\n\t\t\t\t\t\tState:   NotFixedStatus,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedFix: vulnerability.Fix{\n\t\t\t\tVersions:  nil,\n\t\t\t\tState:     vulnerability.FixStateNotFixed,\n\t\t\t\tAvailable: nil,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"not affected status - not handled yet\",\n\t\t\taffectedRanges: []Range{\n\t\t\t\t{\n\t\t\t\t\tFix: &Fix{\n\t\t\t\t\t\tVersion: \"1.2.3\",\n\t\t\t\t\t\tState:   NotAffectedFixStatus,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedFix: vulnerability.Fix{},\n\t\t},\n\t\t{\n\t\t\tname: \"fixed status overrides wont fix\",\n\t\t\taffectedRanges: []Range{\n\t\t\t\t{\n\t\t\t\t\tFix: &Fix{\n\t\t\t\t\t\tVersion: \"1.2.3\",\n\t\t\t\t\t\tState:   WontFixStatus,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tFix: &Fix{\n\t\t\t\t\t\tVersion: \"1.3.0\",\n\t\t\t\t\t\tState:   FixedStatus,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedFix: vulnerability.Fix{\n\t\t\t\tVersions:  []string{\"1.3.0\"},\n\t\t\t\tState:     vulnerability.FixStateFixed,\n\t\t\t\tAvailable: nil,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"fixed status overrides not fixed\",\n\t\t\taffectedRanges: []Range{\n\t\t\t\t{\n\t\t\t\t\tFix: &Fix{\n\t\t\t\t\t\tVersion: \"1.2.3\",\n\t\t\t\t\t\tState:   NotFixedStatus,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tFix: &Fix{\n\t\t\t\t\t\tVersion: \"1.3.0\",\n\t\t\t\t\t\tState:   FixedStatus,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedFix: vulnerability.Fix{\n\t\t\t\tVersions:  []string{\"1.3.0\"},\n\t\t\t\tState:     vulnerability.FixStateFixed,\n\t\t\t\tAvailable: nil,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"wont fix overrides not fixed\",\n\t\t\taffectedRanges: []Range{\n\t\t\t\t{\n\t\t\t\t\tFix: &Fix{\n\t\t\t\t\t\tVersion: \"1.2.3\",\n\t\t\t\t\t\tState:   NotFixedStatus,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tFix: &Fix{\n\t\t\t\t\t\tVersion: \"1.3.0\",\n\t\t\t\t\t\tState:   WontFixStatus,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedFix: vulnerability.Fix{\n\t\t\t\tVersions:  nil,\n\t\t\t\tState:     vulnerability.FixStateWontFix,\n\t\t\t\tAvailable: nil,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"mix of nil fixes and various states\",\n\t\t\taffectedRanges: []Range{\n\t\t\t\t{Fix: nil},\n\t\t\t\t{\n\t\t\t\t\tFix: &Fix{\n\t\t\t\t\t\tVersion: \"1.2.3\",\n\t\t\t\t\t\tState:   WontFixStatus,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{Fix: nil},\n\t\t\t\t{\n\t\t\t\t\tFix: &Fix{\n\t\t\t\t\t\tVersion: \"1.3.0\",\n\t\t\t\t\t\tState:   FixedStatus,\n\t\t\t\t\t\tDetail: &FixDetail{\n\t\t\t\t\t\t\tAvailable: &FixAvailability{\n\t\t\t\t\t\t\t\tDate: &fixedDate,\n\t\t\t\t\t\t\t\tKind: \"first-observed\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedFix: vulnerability.Fix{\n\t\t\t\tVersions: []string{\"1.3.0\"},\n\t\t\t\tState:    vulnerability.FixStateFixed,\n\t\t\t\tAvailable: []vulnerability.FixAvailable{\n\t\t\t\t\t{\n\t\t\t\t\t\tVersion: \"1.3.0\",\n\t\t\t\t\t\tDate:    fixedDate,\n\t\t\t\t\t\tKind:    \"first-observed\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"available with empty kind field\",\n\t\t\taffectedRanges: []Range{\n\t\t\t\t{\n\t\t\t\t\tFix: &Fix{\n\t\t\t\t\t\tVersion: \"1.2.3\",\n\t\t\t\t\t\tState:   FixedStatus,\n\t\t\t\t\t\tDetail: &FixDetail{\n\t\t\t\t\t\t\tAvailable: &FixAvailability{\n\t\t\t\t\t\t\t\tDate: &fixedDate,\n\t\t\t\t\t\t\t\tKind: \"\", // empty kind should still work\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedFix: vulnerability.Fix{\n\t\t\t\tVersions: []string{\"1.2.3\"},\n\t\t\t\tState:    vulnerability.FixStateFixed,\n\t\t\t\tAvailable: []vulnerability.FixAvailable{\n\t\t\t\t\t{\n\t\t\t\t\t\tVersion: \"1.2.3\",\n\t\t\t\t\t\tDate:    fixedDate,\n\t\t\t\t\t\tKind:    \"\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot := toFix(tt.affectedRanges)\n\t\t\trequire.Equal(t, tt.expectedFix, got)\n\t\t})\n\t}\n}\n\nfunc majorMinorPatch(ver string) (string, string, string) {\n\tif !unicode.IsDigit(rune(ver[0])) {\n\t\treturn \"\", \"\", \"\"\n\t}\n\tparts := strings.Split(ver, \".\")\n\tif len(parts) == 0 {\n\t\treturn \"\", \"\", \"\"\n\t}\n\tif len(parts) == 1 {\n\t\treturn parts[0], \"\", \"\"\n\t}\n\tif len(parts) == 2 {\n\t\treturn parts[0], parts[1], \"\"\n\t}\n\treturn parts[0], parts[1], parts[2]\n}\n"
  },
  {
    "path": "grype/deprecated.go",
    "content": "package grype\n\nimport (\n\t\"github.com/anchore/grype/grype/match\"\n\t\"github.com/anchore/grype/grype/matcher\"\n\t\"github.com/anchore/grype/grype/pkg\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n\t\"github.com/anchore/grype/internal/log\"\n\t\"github.com/anchore/stereoscope/pkg/image\"\n\t\"github.com/anchore/syft/syft\"\n\t\"github.com/anchore/syft/syft/source\"\n)\n\n// TODO: deprecated, will remove before v1.0.0\nfunc FindVulnerabilities(store vulnerability.Provider, userImageStr string, scopeOpt source.Scope, registryOptions *image.RegistryOptions) (match.Matches, pkg.Context, []pkg.Package, error) {\n\tproviderConfig := pkg.ProviderConfig{\n\t\tSyftProviderConfig: pkg.SyftProviderConfig{\n\t\t\tRegistryOptions: registryOptions,\n\t\t\tSBOMOptions:     syft.DefaultCreateSBOMConfig(),\n\t\t},\n\t}\n\tproviderConfig.SBOMOptions.Search.Scope = scopeOpt\n\n\tpackages, context, _, err := pkg.Provide(userImageStr, providerConfig)\n\tif err != nil {\n\t\treturn match.Matches{}, pkg.Context{}, nil, err\n\t}\n\n\tmatchers := matcher.NewDefaultMatchers(matcher.Config{})\n\n\treturn FindVulnerabilitiesForPackage(store, matchers, packages), context, packages, nil\n}\n\n// TODO: deprecated, will remove before v1.0.0\nfunc FindVulnerabilitiesForPackage(store vulnerability.Provider, matchers []match.Matcher, packages []pkg.Package) match.Matches {\n\texclusionProvider, _ := store.(match.ExclusionProvider) // TODO v5 is an exclusion provider, but v6 is not\n\trunner := VulnerabilityMatcher{\n\t\tVulnerabilityProvider: store,\n\t\tExclusionProvider:     exclusionProvider,\n\t\tMatchers:              matchers,\n\t\tNormalizeByCVE:        false,\n\t}\n\n\tactualResults, _, err := runner.FindMatches(packages, pkg.Context{})\n\tif err != nil || actualResults == nil {\n\t\tlog.WithFields(\"error\", err).Error(\"unable to find vulnerabilities\")\n\t\treturn match.NewMatches()\n\t}\n\treturn *actualResults\n}\n"
  },
  {
    "path": "grype/distro/distro.go",
    "content": "package distro\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/anchore/grype/grype/version\"\n\t\"github.com/anchore/grype/internal/log\"\n\t\"github.com/anchore/grype/internal/stringutil\"\n\t\"github.com/anchore/syft/syft/linux\"\n)\n\n// Distro represents a Linux Distribution.\ntype Distro struct {\n\tType     Type\n\tVersion  string   // major.minor.patch\n\tCodename string   // in lieu of a version e.g. \"fossa\" instead of \"20.04\"\n\tChannels []string // distinguish between different feeds for fix and vulnerability data, e.g. \"eus\" for RHEL\n\tIDLike   []string\n\n\t// fields populated in the constructor\n\n\tmajor     string\n\tminor     string\n\tremaining string\n}\n\n// New creates a new Distro object populated with the given values.\nfunc New(t Type, version, label string, idLikes ...string) *Distro {\n\tmajor, minor, remaining, versionWithoutSuffix, channels := parseVersion(version)\n\n\tfor i := range idLikes {\n\t\ttyp, ok := IDMapping[strings.TrimSpace(idLikes[i])]\n\t\tif ok {\n\t\t\tidLikes[i] = typ.String()\n\t\t}\n\t}\n\n\treturn &Distro{\n\t\tType:     t,\n\t\tVersion:  versionWithoutSuffix,\n\t\tCodename: label,\n\t\tIDLike:   idLikes,\n\t\tChannels: channels,\n\n\t\tmajor:     major,\n\t\tminor:     minor,\n\t\tremaining: remaining,\n\t}\n}\n\nfunc parseVersion(version string) (major, minor, remaining, versionWithoutSuffix string, channels []string) {\n\tif version == \"\" {\n\t\treturn \"\", \"\", \"\", \"\", nil\n\t}\n\n\tversionWithoutSuffix = version\n\tvar channelStr string\n\tif strings.Contains(version, \"+\") {\n\t\tvParts := strings.SplitN(version, \"+\", 2)\n\t\tversion = vParts[0]\n\t\tversionWithoutSuffix = version\n\t\tchannelStr = vParts[1]\n\t}\n\n\tversion = strings.TrimPrefix(version, \"v\")\n\n\t// if starts with a digit, then assume it's a version and extract the major, minor, and remaining versions\n\tif version[0] >= '0' && version[0] <= '9' {\n\t\t// extract the major, minor, and remaining versions\n\t\tparts := strings.Split(version, \".\")\n\t\tif len(parts) > 0 {\n\t\t\tmajor = parts[0]\n\t\t\tif len(parts) > 1 {\n\t\t\t\tminor = parts[1]\n\t\t\t}\n\t\t\tif len(parts) > 2 {\n\t\t\t\tremaining = strings.Join(parts[2:], \".\")\n\t\t\t}\n\t\t}\n\t}\n\n\treturn major, minor, remaining, versionWithoutSuffix, stringutil.SplitOnAny(strings.TrimSpace(channelStr), \",\", \"+\")\n}\n\n// ParseDistroString parses a user-provided distro string in the format \"name<separator>version\"\n// where separator can be \"-\", \":\", or \"@\". It handles the special case of opensuse-leap\n// which contains a hyphen in its distro ID. Returns the distro name and version parts.\nfunc ParseDistroString(s string) (name, version string) {\n\tif s == \"\" {\n\t\treturn \"\", \"\"\n\t}\n\n\ts = strings.TrimSpace(s)\n\n\t// Special handling for opensuse-leap which has a hyphen in its ID\n\t// Check if it starts with \"opensuse-leap\" and handle accordingly\n\tconst opensuseLeap = \"opensuse-leap\"\n\tif strings.HasPrefix(strings.ToLower(s), opensuseLeap) {\n\t\t// Check if there's a separator after \"opensuse-leap\"\n\t\tremaining := s[len(opensuseLeap):]\n\t\tif len(remaining) == 0 {\n\t\t\treturn opensuseLeap, \"\"\n\t\t}\n\t\t// If the next character is a separator, split there\n\t\tif remaining[0] == '-' || remaining[0] == ':' || remaining[0] == '@' {\n\t\t\treturn opensuseLeap, strings.TrimSpace(remaining[1:])\n\t\t}\n\t\t// Otherwise, treat the whole thing as the name\n\t\treturn s, \"\"\n\t}\n\n\t// Find the first occurrence of any separator\n\tseparators := []string{\"-\", \":\", \"@\"}\n\tminIdx := len(s)\n\tfoundSep := \"\"\n\n\tfor _, sep := range separators {\n\t\tif idx := strings.Index(s, sep); idx != -1 && idx < minIdx {\n\t\t\tminIdx = idx\n\t\t\tfoundSep = sep\n\t\t}\n\t}\n\n\tif foundSep == \"\" {\n\t\treturn s, \"\"\n\t}\n\n\treturn strings.TrimSpace(s[:minIdx]), strings.TrimSpace(s[minIdx+len(foundSep):])\n}\n\n// NewFromNameVersion creates a new Distro object derived from the provided name and version\nfunc NewFromNameVersion(name, version string) *Distro {\n\tvar codename string\n\n\t// if there are no digits in the version, it is likely a codename\n\tif !strings.ContainsAny(version, \"0123456789\") {\n\t\tcodename = version\n\t\tversion = \"\"\n\t}\n\n\ttyp := IDMapping[name]\n\tif typ == \"\" {\n\t\ttyp = Type(name)\n\t}\n\n\treturn New(typ, version, codename, string(typ))\n}\n\n// FromRelease attempts to get a distro from the linux release, only logging any errors\nfunc FromRelease(linuxRelease *linux.Release, channels []FixChannel) *Distro {\n\tif linuxRelease == nil {\n\t\treturn nil\n\t}\n\td, err := NewFromRelease(*linuxRelease, channels)\n\tif err != nil {\n\t\tlog.WithFields(\"error\", err).Warn(\"unable to create distro from linux distribution\")\n\t}\n\treturn d\n}\n\n// NewFromRelease creates a new Distro object derived from a syft linux.Release object.\nfunc NewFromRelease(release linux.Release, channels []FixChannel) (*Distro, error) {\n\tt := TypeFromRelease(release)\n\tif t == \"\" {\n\t\treturn nil, fmt.Errorf(\"unable to determine distro type\")\n\t}\n\n\tvar (\n\t\tselectedVersion    string\n\t\tselectedVersionObj *version.Version\n\t)\n\n\tfor _, ver := range []string{release.VersionID, release.Version} {\n\t\tif ver == \"\" {\n\t\t\tcontinue\n\t\t}\n\n\t\tselectedVersionObj = version.New(ver, version.SemanticFormat)\n\n\t\tif selectedVersionObj.Validate() == nil {\n\t\t\tselectedVersion = ver\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif selectedVersion == \"\" {\n\t\tselectedVersion = release.VersionID\n\t}\n\n\td := New(t, selectedVersion, release.VersionCodename, release.IDLike...)\n\td.Channels = applyChannels(release, selectedVersionObj, d.Channels, channels)\n\n\treturn d, nil\n}\n\nfunc (d Distro) Name() string {\n\treturn string(d.Type)\n}\n\n// MajorVersion returns the major version value from the pseudo-semantically versioned distro version value.\nfunc (d Distro) MajorVersion() string {\n\treturn d.major\n}\n\n// MinorVersion returns the minor version value from the pseudo-semantically versioned distro version value.\nfunc (d Distro) MinorVersion() string {\n\treturn d.minor\n}\n\nfunc (d Distro) RemainingVersion() string {\n\treturn d.remaining\n}\n\n// String returns a human-friendly representation of the Linux distribution.\nfunc (d Distro) String() string {\n\treturn fmt.Sprintf(\"%s %s\", d.ID(), d.VersionString())\n}\n\nfunc (d Distro) ID() string {\n\treturn typeToIDMapping[d.Type]\n}\n\nfunc (d Distro) VersionString() string {\n\tversionStr := \"\"\n\tif d.Version != \"\" {\n\t\tversionStr = d.Version\n\t} else if d.Codename != \"\" {\n\t\tversionStr = d.Codename\n\t}\n\n\tchannels := nonEmptyStrings(d.Channels...)\n\n\tif len(channels) > 0 {\n\t\tversionStr += \"+\" + strings.Join(channels, \",\")\n\t}\n\n\treturn versionStr\n}\n\nfunc (d Distro) LabelVersion() string {\n\tif d.Codename != \"\" {\n\t\treturn d.Codename\n\t}\n\n\tif d.major == \"\" && d.minor == \"\" && d.remaining == \"\" {\n\t\treturn d.Version\n\t}\n\n\treturn \"\"\n}\n\nfunc nonEmptyStrings(ss ...string) (res []string) {\n\tfor _, s := range ss {\n\t\tif s != \"\" {\n\t\t\tres = append(res, s)\n\t\t}\n\t}\n\treturn res\n}\n"
  },
  {
    "path": "grype/distro/distro_test.go",
    "content": "package distro\n\nimport (\n\t\"testing\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/google/go-cmp/cmp/cmpopts\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/grype/internal/stringutil\"\n\t\"github.com/anchore/syft/syft/linux\"\n\t\"github.com/anchore/syft/syft/source\"\n\t\"github.com/anchore/syft/syft/source/directorysource\"\n)\n\nfunc testFixChannels() []FixChannel {\n\treturn DefaultFixChannels()\n}\n\nfunc Test_NewDistroFromRelease(t *testing.T) {\n\ttests := []struct {\n\t\tname      string\n\t\trelease   linux.Release\n\t\tchannels  []FixChannel\n\t\texpected  *Distro\n\t\tminor     string\n\t\tmajor     string\n\t\texpectErr require.ErrorAssertionFunc\n\t}{\n\t\t{\n\t\t\tname: \"go case: derive version from version-id\",\n\t\t\trelease: linux.Release{\n\t\t\t\tID:        \"centos\",\n\t\t\t\tVersionID: \"8\",\n\t\t\t\tVersion:   \"7\",\n\t\t\t\tIDLike:    []string{\"rhel\"},\n\t\t\t},\n\t\t\tchannels: testFixChannels(),\n\t\t\texpected: &Distro{\n\t\t\t\tType:    CentOS,\n\t\t\t\tVersion: \"8\",\n\t\t\t\tIDLike:  []string{\"redhat\"},\n\t\t\t},\n\t\t\tmajor: \"8\",\n\t\t\tminor: \"\",\n\t\t},\n\t\t{\n\t\t\tname: \"fallback to release name when release id is missing\",\n\t\t\trelease: linux.Release{\n\t\t\t\tName:      \"windows\",\n\t\t\t\tVersionID: \"8\",\n\t\t\t},\n\t\t\tchannels: testFixChannels(),\n\t\t\texpected: &Distro{\n\t\t\t\tType:    Windows,\n\t\t\t\tVersion: \"8\",\n\t\t\t},\n\t\t\tmajor: \"8\",\n\t\t\tminor: \"\",\n\t\t},\n\t\t{\n\t\t\tname: \"fallback to version when version-id missing\",\n\t\t\trelease: linux.Release{\n\t\t\t\tID:      \"centos\",\n\t\t\t\tVersion: \"8\",\n\t\t\t},\n\t\t\tchannels: testFixChannels(),\n\t\t\texpected: &Distro{\n\t\t\t\tType:    CentOS,\n\t\t\t\tVersion: \"8\",\n\t\t\t},\n\t\t\tmajor: \"8\",\n\t\t\tminor: \"\",\n\t\t},\n\t\t{\n\t\t\t// this enables matching on multiple OS versions at once\n\t\t\tname: \"missing version or label version is allowed\",\n\t\t\trelease: linux.Release{\n\t\t\t\tID: \"centos\",\n\t\t\t},\n\t\t\tchannels: testFixChannels(),\n\t\t\texpected: &Distro{\n\t\t\t\tType: CentOS,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"bogus distro type results in error\",\n\t\t\trelease: linux.Release{\n\t\t\t\tID:        \"bogosity\",\n\t\t\t\tVersionID: \"8\",\n\t\t\t},\n\t\t\tchannels:  testFixChannels(),\n\t\t\texpectErr: require.Error,\n\t\t},\n\t\t{\n\t\t\t// syft -o json debian:testing | jq .distro\n\t\t\tname: \"unstable debian\",\n\t\t\trelease: linux.Release{\n\t\t\t\tID:              \"debian\",\n\t\t\t\tVersionID:       \"\",\n\t\t\t\tVersion:         \"\",\n\t\t\t\tPrettyName:      \"Debian GNU/Linux trixie/sid\",\n\t\t\t\tVersionCodename: \"trixie\",\n\t\t\t\tName:            \"Debian GNU/Linux\",\n\t\t\t},\n\t\t\tchannels: testFixChannels(),\n\t\t\texpected: &Distro{\n\t\t\t\tType:     Debian,\n\t\t\t\tCodename: \"trixie\",\n\t\t\t},\n\t\t\tmajor: \"\",\n\t\t\tminor: \"\",\n\t\t},\n\t\t{\n\t\t\tname: \"azure linux 3\",\n\t\t\trelease: linux.Release{\n\t\t\t\tID:        \"azurelinux\",\n\t\t\t\tVersion:   \"3.0.20240417\",\n\t\t\t\tVersionID: \"3.0\",\n\t\t\t},\n\t\t\tchannels: testFixChannels(),\n\t\t\texpected: &Distro{\n\t\t\t\tType:    Azure,\n\t\t\t\tVersion: \"3.0\",\n\t\t\t},\n\t\t\tmajor: \"3\",\n\t\t\tminor: \"0\",\n\t\t},\n\t\t{\n\t\t\tname: \"eus hint ignored when configured to never apply\",\n\t\t\trelease: linux.Release{\n\t\t\t\tID:              \"rhel\",\n\t\t\t\tVersion:         \"9.4\",\n\t\t\t\tExtendedSupport: true,\n\t\t\t},\n\t\t\tchannels: testFixChannels(),\n\t\t\texpected: &Distro{\n\t\t\t\tType:     RedHat,\n\t\t\t\tVersion:  \"9.4\",\n\t\t\t\tChannels: names(\"eus\"),\n\t\t\t},\n\t\t\tmajor: \"9\",\n\t\t\tminor: \"4\",\n\t\t},\n\t\t{\n\t\t\tname: \"eus hinted at as attribute\",\n\t\t\trelease: linux.Release{\n\t\t\t\tID:              \"rhel\",\n\t\t\t\tVersion:         \"9.4\",\n\t\t\t\tExtendedSupport: true,\n\t\t\t},\n\t\t\tchannels: []FixChannel{\n\t\t\t\t{\n\t\t\t\t\tName:  \"eus\",\n\t\t\t\t\tIDs:   []string{\"rhel\"},\n\t\t\t\t\tApply: ChannelConditionallyEnabled,\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: &Distro{\n\t\t\t\tType:     RedHat,\n\t\t\t\tVersion:  \"9.4\",\n\t\t\t\tChannels: names(\"eus\"),\n\t\t\t},\n\t\t\tmajor: \"9\",\n\t\t\tminor: \"4\",\n\t\t},\n\t\t{\n\t\t\tname: \"eus embedded in the version\",\n\t\t\trelease: linux.Release{\n\t\t\t\tID:      \"rhel\",\n\t\t\t\tVersion: \"9.4+eus\",\n\t\t\t},\n\t\t\tchannels: []FixChannel{\n\t\t\t\t{\n\t\t\t\t\tName:  \"eus\",\n\t\t\t\t\tIDs:   []string{\"rhel\"},\n\t\t\t\t\tApply: ChannelConditionallyEnabled,\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: &Distro{\n\t\t\t\tType:     RedHat,\n\t\t\t\tVersion:  \"9.4\",\n\t\t\t\tChannels: names(\"eus\"),\n\t\t\t},\n\t\t\tmajor: \"9\",\n\t\t\tminor: \"4\",\n\t\t},\n\t\t{\n\t\t\tname: \"eus hinted at as attribute (always apply)\",\n\t\t\trelease: linux.Release{\n\t\t\t\tID:              \"rhel\",\n\t\t\t\tVersion:         \"9.4\",\n\t\t\t\tExtendedSupport: true,\n\t\t\t},\n\t\t\tchannels: []FixChannel{\n\t\t\t\t{\n\t\t\t\t\tName:  \"eus\",\n\t\t\t\t\tIDs:   []string{\"rhel\"},\n\t\t\t\t\tApply: ChannelAlwaysEnabled, // important!\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: &Distro{\n\t\t\t\tType:     RedHat,\n\t\t\t\tVersion:  \"9.4\",\n\t\t\t\tChannels: names(\"eus\"),\n\t\t\t},\n\t\t\tmajor: \"9\",\n\t\t\tminor: \"4\",\n\t\t},\n\t\t{\n\t\t\tname: \"eus embedded in the version (always apply)\",\n\t\t\trelease: linux.Release{\n\t\t\t\tID:      \"rhel\",\n\t\t\t\tVersion: \"9.4+eus\",\n\t\t\t},\n\t\t\tchannels: []FixChannel{\n\t\t\t\t{\n\t\t\t\t\tName:  \"eus\",\n\t\t\t\t\tIDs:   []string{\"rhel\"},\n\t\t\t\t\tApply: ChannelAlwaysEnabled, // important!\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: &Distro{\n\t\t\t\tType:     RedHat,\n\t\t\t\tVersion:  \"9.4\",\n\t\t\t\tChannels: names(\"eus\"),\n\t\t\t},\n\t\t\tmajor: \"9\",\n\t\t\tminor: \"4\",\n\t\t},\n\t\t{\n\t\t\tname: \"eus hinted at as attribute (never apply)\",\n\t\t\trelease: linux.Release{\n\t\t\t\tID:              \"rhel\",\n\t\t\t\tVersion:         \"9.4\",\n\t\t\t\tExtendedSupport: true,\n\t\t\t},\n\t\t\tchannels: []FixChannel{\n\t\t\t\t{\n\t\t\t\t\tName:  \"eus\",\n\t\t\t\t\tIDs:   []string{\"rhel\"},\n\t\t\t\t\tApply: ChannelNeverEnabled, // important!\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: &Distro{\n\t\t\t\tType:    RedHat,\n\t\t\t\tVersion: \"9.4\",\n\t\t\t},\n\t\t\tmajor: \"9\",\n\t\t\tminor: \"4\",\n\t\t},\n\t\t{\n\t\t\tname: \"eus embedded in the version (never apply)\",\n\t\t\trelease: linux.Release{\n\t\t\t\tID:      \"rhel\",\n\t\t\t\tVersion: \"9.4+eus\",\n\t\t\t},\n\t\t\tchannels: []FixChannel{\n\t\t\t\t{\n\t\t\t\t\tName:  \"eus\",\n\t\t\t\t\tIDs:   []string{\"rhel\"},\n\t\t\t\t\tApply: ChannelNeverEnabled, // important!\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: &Distro{\n\t\t\t\tType:    RedHat,\n\t\t\t\tVersion: \"9.4\",\n\t\t\t},\n\t\t\tmajor: \"9\",\n\t\t\tminor: \"4\",\n\t\t},\n\t\t{\n\t\t\tname: \"v versionID prefix postmarketos\",\n\t\t\trelease: linux.Release{\n\t\t\t\tID:        \"postmarketos\",\n\t\t\t\tVersionID: \"v24.06\",\n\t\t\t},\n\t\t\texpected: &Distro{\n\t\t\t\tType:    PostmarketOS,\n\t\t\t\tVersion: \"v24.06\",\n\t\t\t},\n\t\t\tmajor: \"24\",\n\t\t\tminor: \"06\",\n\t\t},\n\t\t{\n\t\t\tname: \"edge as versionID prefix postmarketos\",\n\t\t\trelease: linux.Release{\n\t\t\t\tID:        \"postmarketos\",\n\t\t\t\tVersionID: \"edge\",\n\t\t\t},\n\t\t\texpected: &Distro{\n\t\t\t\tType:    PostmarketOS,\n\t\t\t\tVersion: \"edge\",\n\t\t\t},\n\t\t\tmajor: \"\",\n\t\t\tminor: \"\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif tt.expectErr == nil {\n\t\t\t\ttt.expectErr = require.NoError\n\t\t\t}\n\n\t\t\tdistro, err := NewFromRelease(tt.release, tt.channels)\n\t\t\ttt.expectErr(t, err)\n\t\t\tif err != nil {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif d := cmp.Diff(tt.expected, distro, cmpopts.IgnoreUnexported(Distro{})); d != \"\" {\n\t\t\t\tt.Errorf(\"unexpected result: %s\", d)\n\t\t\t}\n\t\t\tassert.Equal(t, tt.major, distro.MajorVersion(), \"unexpected major version\")\n\t\t\tassert.Equal(t, tt.minor, distro.MinorVersion(), \"unexpected minor version\")\n\t\t})\n\t}\n\n}\n\nfunc Test_NewDistroFromRelease_Coverage(t *testing.T) {\n\tobservedDistros := stringutil.NewStringSet()\n\tdefinedDistros := stringutil.NewStringSet()\n\n\tfor _, distroType := range All {\n\t\tdefinedDistros.Add(string(distroType))\n\t}\n\n\t// Somewhat cheating with Windows. There is no support for detecting/parsing a Windows OS, so it is not\n\t// possible to comply with this test unless it is added manually to the \"observed distros\"\n\tdefinedDistros.Remove(string(Windows))\n\n\ttests := []struct {\n\t\tName         string\n\t\tType         Type\n\t\tVersion      string\n\t\tLabelVersion string\n\t}{\n\t\t{\n\t\t\tName:    \"testdata/os/alpine\",\n\t\t\tType:    Alpine,\n\t\t\tVersion: \"3.11.6\",\n\t\t},\n\t\t{\n\t\t\tName:    \"testdata/os/alpine-edge\",\n\t\t\tType:    Alpine,\n\t\t\tVersion: \"3.22.0_alpha20250108\",\n\t\t},\n\t\t{\n\t\t\tName:    \"testdata/os/amazon\",\n\t\t\tType:    AmazonLinux,\n\t\t\tVersion: \"2\",\n\t\t},\n\t\t{\n\t\t\tName:    \"testdata/os/busybox\",\n\t\t\tType:    Busybox,\n\t\t\tVersion: \"1.31.1\",\n\t\t},\n\t\t{\n\t\t\tName:    \"testdata/os/centos\",\n\t\t\tType:    CentOS,\n\t\t\tVersion: \"8\",\n\t\t},\n\t\t{\n\t\t\tName:    \"testdata/os/debian\",\n\t\t\tType:    Debian,\n\t\t\tVersion: \"8\",\n\t\t},\n\t\t{\n\t\t\tName:         \"testdata/os/debian-sid\",\n\t\t\tType:         Debian,\n\t\t\tLabelVersion: \"trixie\",\n\t\t},\n\t\t{\n\t\t\tName:    \"testdata/os/fedora\",\n\t\t\tType:    Fedora,\n\t\t\tVersion: \"31\",\n\t\t},\n\t\t{\n\t\t\tName:    \"testdata/os/redhat\",\n\t\t\tType:    RedHat,\n\t\t\tVersion: \"7.3\",\n\t\t},\n\t\t{\n\t\t\tName:         \"testdata/os/ubuntu\",\n\t\t\tType:         Ubuntu,\n\t\t\tVersion:      \"20.04\",\n\t\t\tLabelVersion: \"focal\",\n\t\t},\n\t\t{\n\t\t\tName:    \"testdata/os/oraclelinux\",\n\t\t\tType:    OracleLinux,\n\t\t\tVersion: \"8.3\",\n\t\t},\n\t\t{\n\t\t\tName:    \"testdata/os/custom\",\n\t\t\tType:    Scientific,\n\t\t\tVersion: \"8\",\n\t\t},\n\t\t{\n\t\t\tName:    \"testdata/os/opensuse-leap\",\n\t\t\tType:    OpenSuseLeap,\n\t\t\tVersion: \"15.2\",\n\t\t},\n\t\t{\n\t\t\tName:    \"testdata/os/sles\",\n\t\t\tType:    SLES,\n\t\t\tVersion: \"15.2\",\n\t\t},\n\t\t{\n\t\t\tName:    \"testdata/os/photon\",\n\t\t\tType:    Photon,\n\t\t\tVersion: \"2.0\",\n\t\t},\n\t\t{\n\t\t\tName: \"testdata/os/arch\",\n\t\t\tType: ArchLinux,\n\t\t},\n\t\t{\n\t\t\tName:    \"testdata/partial-fields/missing-id\",\n\t\t\tType:    Debian,\n\t\t\tVersion: \"8\",\n\t\t},\n\t\t{\n\t\t\tName:    \"testdata/partial-fields/unknown-id\",\n\t\t\tType:    Debian,\n\t\t\tVersion: \"8\",\n\t\t},\n\t\t{\n\t\t\tName:    \"testdata/os/centos6\",\n\t\t\tType:    CentOS,\n\t\t\tVersion: \"6\",\n\t\t},\n\t\t{\n\t\t\tName:    \"testdata/os/centos5\",\n\t\t\tType:    CentOS,\n\t\t\tVersion: \"5.7\",\n\t\t},\n\t\t{\n\t\t\tName:    \"testdata/os/mariner\",\n\t\t\tType:    Mariner,\n\t\t\tVersion: \"1.0\",\n\t\t},\n\t\t{\n\t\t\tName:    \"testdata/os/azurelinux\",\n\t\t\tType:    Azure,\n\t\t\tVersion: \"3.0\",\n\t\t},\n\t\t{\n\t\t\tName:    \"testdata/os/rockylinux\",\n\t\t\tType:    RockyLinux,\n\t\t\tVersion: \"8.4\",\n\t\t},\n\t\t{\n\t\t\tName:    \"testdata/os/almalinux\",\n\t\t\tType:    AlmaLinux,\n\t\t\tVersion: \"8.4\",\n\t\t},\n\t\t{\n\t\t\tName:    \"testdata/os/echo\",\n\t\t\tType:    Echo,\n\t\t\tVersion: \"1\",\n\t\t},\n\t\t{\n\t\t\tName: \"testdata/os/gentoo\",\n\t\t\tType: Gentoo,\n\t\t},\n\t\t{\n\t\t\tName:    \"testdata/os/wolfi\",\n\t\t\tType:    Wolfi,\n\t\t\tVersion: \"20220914\",\n\t\t},\n\t\t{\n\t\t\tName:    \"testdata/os/chainguard\",\n\t\t\tType:    Chainguard,\n\t\t\tVersion: \"20230214\",\n\t\t},\n\t\t{\n\t\t\tName:    \"testdata/os/minimos\",\n\t\t\tType:    MinimOS,\n\t\t\tVersion: \"20241031\",\n\t\t},\n\t\t{\n\t\t\tName:    \"testdata/os/raspbian\",\n\t\t\tType:    Raspbian,\n\t\t\tVersion: \"9\",\n\t\t},\n\t\t{\n\t\t\tName:    \"testdata/os/scientific\",\n\t\t\tType:    Scientific,\n\t\t\tVersion: \"7.5\",\n\t\t},\n\t\t{\n\t\t\tName:    \"testdata/os/scientific6\",\n\t\t\tType:    Scientific,\n\t\t\tVersion: \"6.10\",\n\t\t},\n\t\t{\n\t\t\tName:    \"testdata/os/secureos\",\n\t\t\tType:    SecureOS,\n\t\t\tVersion: \"2025.09.09\",\n\t\t},\n\t\t{\n\t\t\tName:    \"testdata/os/postmarketos\",\n\t\t\tType:    PostmarketOS,\n\t\t\tVersion: \"v25.06\",\n\t\t},\n\t\t{\n\t\t\tName:         \"testdata/os/postmarketos-edge\",\n\t\t\tType:         PostmarketOS,\n\t\t\tVersion:      \"edge\",\n\t\t\tLabelVersion: \"edge\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.Name, func(t *testing.T) {\n\t\t\ts, err := directorysource.NewFromPath(tt.Name)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tresolver, err := s.FileResolver(source.SquashedScope)\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// make certain syft and pick up on the raw information we need\n\t\t\trelease := linux.IdentifyRelease(resolver)\n\t\t\trequire.NotNil(t, release, \"empty linux release info\")\n\n\t\t\t// craft a new distro from the syft raw info\n\t\t\td, err := NewFromRelease(*release, testFixChannels())\n\t\t\trequire.NoError(t, err)\n\n\t\t\tobservedDistros.Add(d.Type.String())\n\n\t\t\tassert.Equal(t, tt.Type, d.Type, \"unexpected distro type\")\n\t\t\tassert.Equal(t, tt.LabelVersion, d.LabelVersion(), \"unexpected label version\")\n\t\t\tassert.Equal(t, tt.Version, d.Version, \"unexpected version\")\n\t\t})\n\t}\n\n\t// ensure that test cases stay in sync with the distros that can be identified\n\tif len(observedDistros) < len(definedDistros) {\n\t\tfor _, d := range definedDistros.ToSlice() {\n\t\t\tt.Logf(\"   defined: %s\", d)\n\t\t}\n\t\tfor _, d := range observedDistros.ToSlice() {\n\t\t\tt.Logf(\"   observed: %s\", d)\n\t\t}\n\t\tt.Errorf(\"distro coverage incomplete (defined=%d, coverage=%d)\", len(definedDistros), len(observedDistros))\n\t}\n}\n\nfunc TestDistro_FullVersion(t *testing.T) {\n\n\ttests := []struct {\n\t\tversion  string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tversion:  \"8\",\n\t\t\texpected: \"8\",\n\t\t},\n\t\t{\n\t\t\tversion:  \"18.04\",\n\t\t\texpected: \"18.04\",\n\t\t},\n\t\t{\n\t\t\tversion:  \"0\",\n\t\t\texpected: \"0\",\n\t\t},\n\t\t{\n\t\t\tversion:  \"18.1.2\",\n\t\t\texpected: \"18.1.2\",\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.version, func(t *testing.T) {\n\t\t\td, err := NewFromRelease(linux.Release{\n\t\t\t\tID:      \"centos\",\n\t\t\t\tVersion: test.version,\n\t\t\t}, testFixChannels())\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, test.expected, d.Version)\n\t\t})\n\t}\n\n}\n\nfunc TestDistro_MajorVersion(t *testing.T) {\n\n\ttests := []struct {\n\t\tversion  string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tversion:  \"8\",\n\t\t\texpected: \"8\",\n\t\t},\n\t\t{\n\t\t\tversion:  \"18.04\",\n\t\t\texpected: \"18\",\n\t\t},\n\t\t{\n\t\t\tversion:  \"0\",\n\t\t\texpected: \"0\",\n\t\t},\n\t\t{\n\t\t\tversion:  \"18.1.2\",\n\t\t\texpected: \"18\",\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.version, func(t *testing.T) {\n\t\t\td, err := NewFromRelease(linux.Release{\n\t\t\t\tID:      \"centos\",\n\t\t\t\tVersion: test.version,\n\t\t\t}, testFixChannels())\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, test.expected, d.MajorVersion())\n\n\t\t})\n\t}\n\n}\n\nfunc names(ns ...string) []string {\n\treturn ns\n}\n\nfunc TestParseDistroString(t *testing.T) {\n\ttests := []struct {\n\t\tname            string\n\t\tinput           string\n\t\texpectedName    string\n\t\texpectedVersion string\n\t}{\n\t\t{\n\t\t\tname:            \"hyphen separator\",\n\t\t\tinput:           \"debian-11\",\n\t\t\texpectedName:    \"debian\",\n\t\t\texpectedVersion: \"11\",\n\t\t},\n\t\t{\n\t\t\tname:            \"colon separator\",\n\t\t\tinput:           \"debian:11\",\n\t\t\texpectedName:    \"debian\",\n\t\t\texpectedVersion: \"11\",\n\t\t},\n\t\t{\n\t\t\tname:            \"at separator\",\n\t\t\tinput:           \"debian@11\",\n\t\t\texpectedName:    \"debian\",\n\t\t\texpectedVersion: \"11\",\n\t\t},\n\t\t{\n\t\t\tname:            \"no separator\",\n\t\t\tinput:           \"debian\",\n\t\t\texpectedName:    \"debian\",\n\t\t\texpectedVersion: \"\",\n\t\t},\n\t\t{\n\t\t\tname:            \"with major.minor version\",\n\t\t\tinput:           \"ubuntu-20.04\",\n\t\t\texpectedName:    \"ubuntu\",\n\t\t\texpectedVersion: \"20.04\",\n\t\t},\n\t\t{\n\t\t\tname:            \"with codename\",\n\t\t\tinput:           \"ubuntu@focal\",\n\t\t\texpectedName:    \"ubuntu\",\n\t\t\texpectedVersion: \"focal\",\n\t\t},\n\t\t{\n\t\t\tname:            \"with channels\",\n\t\t\tinput:           \"rhel:9.4+eus\",\n\t\t\texpectedName:    \"rhel\",\n\t\t\texpectedVersion: \"9.4+eus\",\n\t\t},\n\t\t{\n\t\t\tname:            \"opensuse-leap with hyphen separator\",\n\t\t\tinput:           \"opensuse-leap-15.2\",\n\t\t\texpectedName:    \"opensuse-leap\",\n\t\t\texpectedVersion: \"15.2\",\n\t\t},\n\t\t{\n\t\t\tname:            \"opensuse-leap with colon separator\",\n\t\t\tinput:           \"opensuse-leap:15.2\",\n\t\t\texpectedName:    \"opensuse-leap\",\n\t\t\texpectedVersion: \"15.2\",\n\t\t},\n\t\t{\n\t\t\tname:            \"opensuse-leap with at separator\",\n\t\t\tinput:           \"opensuse-leap@15.2\",\n\t\t\texpectedName:    \"opensuse-leap\",\n\t\t\texpectedVersion: \"15.2\",\n\t\t},\n\t\t{\n\t\t\tname:            \"opensuse-leap without version\",\n\t\t\tinput:           \"opensuse-leap\",\n\t\t\texpectedName:    \"opensuse-leap\",\n\t\t\texpectedVersion: \"\",\n\t\t},\n\t\t{\n\t\t\tname:            \"opensuse-leap with mixed case\",\n\t\t\tinput:           \"OpenSUSE-Leap-15.2\",\n\t\t\texpectedName:    \"opensuse-leap\",\n\t\t\texpectedVersion: \"15.2\",\n\t\t},\n\t\t{\n\t\t\tname:            \"empty string\",\n\t\t\tinput:           \"\",\n\t\t\texpectedName:    \"\",\n\t\t\texpectedVersion: \"\",\n\t\t},\n\t\t{\n\t\t\tname:            \"with whitespace\",\n\t\t\tinput:           \"  debian : 11  \",\n\t\t\texpectedName:    \"debian\",\n\t\t\texpectedVersion: \"11\",\n\t\t},\n\t\t{\n\t\t\tname:            \"multiple separators uses first\",\n\t\t\tinput:           \"debian-11:test\",\n\t\t\texpectedName:    \"debian\",\n\t\t\texpectedVersion: \"11:test\",\n\t\t},\n\t\t{\n\t\t\tname:            \"rhel with hyphen\",\n\t\t\tinput:           \"rhel-8\",\n\t\t\texpectedName:    \"rhel\",\n\t\t\texpectedVersion: \"8\",\n\t\t},\n\t\t{\n\t\t\tname:            \"centos with colon\",\n\t\t\tinput:           \"centos:7\",\n\t\t\texpectedName:    \"centos\",\n\t\t\texpectedVersion: \"7\",\n\t\t},\n\t\t{\n\t\t\tname:            \"alpine with at\",\n\t\t\tinput:           \"alpine@3.11\",\n\t\t\texpectedName:    \"alpine\",\n\t\t\texpectedVersion: \"3.11\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tname, version := ParseDistroString(tt.input)\n\t\t\tassert.Equal(t, tt.expectedName, name, \"unexpected name\")\n\t\t\tassert.Equal(t, tt.expectedVersion, version, \"unexpected version\")\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/distro/fix_channel.go",
    "content": "package distro\n\nimport (\n\t\"strings\"\n\n\t\"github.com/scylladb/go-set/strset\"\n\n\t\"github.com/anchore/grype/grype/version\"\n\t\"github.com/anchore/grype/internal/log\"\n\t\"github.com/anchore/syft/syft/linux\"\n)\n\ntype FixChannelEnabled string\n\nconst (\n\t// ChannelNeverEnabled means that the channel should never be applied to the distro\n\tChannelNeverEnabled FixChannelEnabled = \"never\"\n\n\t// ChannelAlwaysEnabled means that the channel should always be applied to the distro\n\tChannelAlwaysEnabled FixChannelEnabled = \"always\"\n\n\t// ChannelConditionallyEnabled means that the channel should conditionally be applied to the distro if there is SBOM material that indicates the channel was configured at build time\n\tChannelConditionallyEnabled FixChannelEnabled = \"auto\"\n)\n\n// FixChannel represents a subscription or repository where package fixes and updates are provided for a Linux distribution\ntype FixChannel struct {\n\t// Name is the name of the channel, e.g. \"eus\" for RHEL\n\tName string\n\n\t// IDs is a list of distro release IDs that this channel applies to, e.g. \"rhel\" for RHEL (this is relative to the /etc/os-release ID field)\n\tIDs []string\n\n\t// Apply indicates how the channel should be applied to the distro\n\tApply FixChannelEnabled\n\n\t// Versions is a version constraint that indicates which versions of the distro this channel applies to (e.g. \">= 8.0\" for RHEL 8 and above)\n\tVersions version.Constraint\n}\n\ntype FixChannels []FixChannel\n\nfunc DefaultFixChannels() FixChannels {\n\treturn []FixChannel{\n\t\t{\n\t\t\tName:     \"eus\",\n\t\t\tIDs:      []string{\"rhel\"},\n\t\t\tApply:    ChannelConditionallyEnabled,\n\t\t\tVersions: version.MustGetConstraint(\">= 8.0\", version.SemanticFormat),\n\t\t},\n\t}\n}\n\nfunc (f FixChannels) Apply(enable FixChannelEnabled) FixChannels {\n\tfor i := range f {\n\t\tf[i].Apply = enable\n\t}\n\treturn f\n}\n\nfunc (f FixChannels) Get(name string) *FixChannel {\n\tfor i := range f {\n\t\tif strings.EqualFold(f[i].Name, name) {\n\t\t\treturn &f[i]\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc applyChannels(release linux.Release, ver *version.Version, existingChannels []string, channels []FixChannel) []string {\n\texistingChannelSet := strset.New(existingChannels...)\n\tvar result []string\n\n\taddResult := func(channel string, extendedSupport bool, pref FixChannelEnabled) {\n\t\tres := applyChannel(channel, extendedSupport, pref)\n\t\tif res != \"\" {\n\t\t\tresult = append(result, res)\n\t\t}\n\t}\n\n\tfor _, channel := range channels {\n\t\tvar found bool\n\t\tfor _, channelID := range channel.IDs {\n\t\t\tif release.ID == channelID {\n\t\t\t\tfound = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif !found {\n\t\t\tcontinue\n\t\t}\n\n\t\t// we will either get a direct indication as a flag, or as a result of the channel being applied to the distro already\n\t\textendedSupport := release.ExtendedSupport || existingChannelSet.Has(channel.Name)\n\n\t\tif ver == nil && release.VersionCodename != \"\" {\n\t\t\t// TODO: there is not a good way to do this without a DB call, so for now we will assume the channel applies\n\t\t\tlog.Debugf(\"using channel %q for distro %q with codename %q\", channel.Name, release.ID, release.VersionCodename)\n\n\t\t\taddResult(channel.Name, extendedSupport, channel.Apply)\n\t\t\tcontinue\n\t\t}\n\n\t\tif channel.Versions != nil && ver != nil {\n\t\t\tisApplicable, err := channel.Versions.Satisfied(ver)\n\t\t\tif err != nil {\n\t\t\t\tlog.WithFields(\"error\", err, \"constraint\", channel.Versions).Debugf(\"unable to determine if channel %q is applicable for distro %q with version %q\", channel.Name, release.ID, ver)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif isApplicable {\n\t\t\t\tlog.Debugf(\"using channel %q for distro %q with version %q\", channel.Name, release.ID, ver)\n\t\t\t\taddResult(channel.Name, extendedSupport, channel.Apply)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\t\tlog.Debugf(\"using channel %q for distro %q\", channel.Name, release.ID)\n\t\taddResult(channel.Name, extendedSupport, channel.Apply)\n\t}\n\treturn result\n}\n\nfunc applyChannel(channel string, hintsExtendedSupport bool, pref FixChannelEnabled) string {\n\tswitch pref {\n\tcase ChannelNeverEnabled:\n\t\treturn \"\"\n\tcase ChannelAlwaysEnabled:\n\t\treturn channel\n\tcase ChannelConditionallyEnabled:\n\t\tif hintsExtendedSupport {\n\t\t\treturn channel\n\t\t}\n\t}\n\treturn \"\"\n}\n"
  },
  {
    "path": "grype/distro/fix_channel_test.go",
    "content": "package distro\n\nimport (\n\t\"testing\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\n\t\"github.com/anchore/grype/grype/version\"\n)\n\nfunc TestDefaultFixChannels(t *testing.T) {\n\tchannels := DefaultFixChannels()\n\n\t// this seems like a silly test, however, it is critical to ensure that the default channels have EUS with expected values\n\texpected := FixChannels{\n\t\t{\n\t\t\tName:     \"eus\",\n\t\t\tIDs:      []string{\"rhel\"},\n\t\t\tApply:    ChannelConditionallyEnabled,\n\t\t\tVersions: version.MustGetConstraint(\">= 8.0\", version.SemanticFormat),\n\t\t},\n\t}\n\n\tif diff := cmp.Diff(expected, channels); diff != \"\" {\n\t\tt.Errorf(\"DefaultFixChannels() mismatch (-want +got):\\n%s\", diff)\n\t}\n}\n\nfunc TestFixChannels_Apply(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tchannels FixChannels\n\t\tenable   FixChannelEnabled\n\t\twant     FixChannels\n\t}{\n\t\t{\n\t\t\tname: \"apply always enabled to single channel\",\n\t\t\tchannels: FixChannels{\n\t\t\t\t{\n\t\t\t\t\tName:  \"eus\",\n\t\t\t\t\tApply: ChannelConditionallyEnabled,\n\t\t\t\t},\n\t\t\t},\n\t\t\tenable: ChannelAlwaysEnabled,\n\t\t\twant: FixChannels{\n\t\t\t\t{\n\t\t\t\t\tName:  \"eus\",\n\t\t\t\t\tApply: ChannelAlwaysEnabled,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"apply never enabled to multiple channels\",\n\t\t\tchannels: FixChannels{\n\t\t\t\t{\n\t\t\t\t\tName:  \"eus\",\n\t\t\t\t\tApply: ChannelConditionallyEnabled,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tName:  \"main\",\n\t\t\t\t\tApply: ChannelAlwaysEnabled,\n\t\t\t\t},\n\t\t\t},\n\t\t\tenable: ChannelNeverEnabled,\n\t\t\twant: FixChannels{\n\t\t\t\t{\n\t\t\t\t\tName:  \"eus\",\n\t\t\t\t\tApply: ChannelNeverEnabled,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tName:  \"main\",\n\t\t\t\t\tApply: ChannelNeverEnabled,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:     \"apply to empty channels\",\n\t\t\tchannels: FixChannels{},\n\t\t\tenable:   ChannelAlwaysEnabled,\n\t\t\twant:     FixChannels{},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot := tt.channels.Apply(tt.enable)\n\t\t\tif diff := cmp.Diff(tt.want, got); diff != \"\" {\n\t\t\t\tt.Errorf(\"FixChannels.Apply() mismatch (-want +got):\\n%s\", diff)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestFixChannels_Get(t *testing.T) {\n\tchannels := FixChannels{\n\t\t{\n\t\t\tName:  \"eus\",\n\t\t\tIDs:   []string{\"rhel\"},\n\t\t\tApply: ChannelConditionallyEnabled,\n\t\t},\n\t\t{\n\t\t\tName:  \"main\",\n\t\t\tIDs:   []string{\"debian\", \"ubuntu\"},\n\t\t\tApply: ChannelAlwaysEnabled,\n\t\t},\n\t}\n\n\ttests := []struct {\n\t\tname        string\n\t\tchannelName string\n\t\twant        *FixChannel\n\t}{\n\t\t{\n\t\t\tname:        \"find existing channel by exact name\",\n\t\t\tchannelName: \"eus\",\n\t\t\twant: &FixChannel{\n\t\t\t\tName:  \"eus\",\n\t\t\t\tIDs:   []string{\"rhel\"},\n\t\t\t\tApply: ChannelConditionallyEnabled,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:        \"find existing channel by case insensitive name\",\n\t\t\tchannelName: \"EUS\",\n\t\t\twant: &FixChannel{\n\t\t\t\tName:  \"eus\",\n\t\t\t\tIDs:   []string{\"rhel\"},\n\t\t\t\tApply: ChannelConditionallyEnabled,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:        \"find existing channel by mixed case name\",\n\t\t\tchannelName: \"Main\",\n\t\t\twant: &FixChannel{\n\t\t\t\tName:  \"main\",\n\t\t\t\tIDs:   []string{\"debian\", \"ubuntu\"},\n\t\t\t\tApply: ChannelAlwaysEnabled,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:        \"channel not found\",\n\t\t\tchannelName: \"nonexistent\",\n\t\t\twant:        nil,\n\t\t},\n\t\t{\n\t\t\tname:        \"empty channel name\",\n\t\t\tchannelName: \"\",\n\t\t\twant:        nil,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot := channels.Get(tt.channelName)\n\t\t\tif diff := cmp.Diff(tt.want, got); diff != \"\" {\n\t\t\t\tt.Errorf(\"FixChannels.Get() mismatch (-want +got):\\n%s\", diff)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/distro/testdata/bad-id",
    "content": "NAME=\"Red Hat Enterprise Linux\"\nVERSION=\"8.1 (Ootpa)\"\nID_LIKE=\"fedora\"\nPLATFORM_ID=\"platform:el8\"\nPRETTY_NAME=\"Red Hat Enterprise Linux 8.1 (Ootpa)\"\nANSI_COLOR=\"0;31\"\nCPE_NAME=\"cpe:/o:redhat:enterprise_linux:8.1:GA\"\nHOME_URL=\"https://www.redhat.com/\"\nBUG_REPORT_URL=\"https://bugzilla.redhat.com/\"\n\nREDHAT_BUGZILLA_PRODUCT=\"Red Hat Enterprise Linux 8\"\nREDHAT_BUGZILLA_PRODUCT_VERSION=8.1\nREDHAT_SUPPORT_PRODUCT=\"Red Hat Enterprise Linux\"\nREDHAT_SUPPORT_PRODUCT_VERSION=\"8.1\"\n\n"
  },
  {
    "path": "grype/distro/testdata/bad-redhat-release",
    "content": "CentOS release 5 (Final)"
  },
  {
    "path": "grype/distro/testdata/bad-system-release-cpe",
    "content": "cpe:/o:centos:6:GA"
  },
  {
    "path": "grype/distro/testdata/centos-8",
    "content": "NAME=\"CentOS Linux\"\nVERSION=\"8 (Core)\"\nID=\"centos\"\nID_LIKE=\"rhel fedora\"\nVERSION_ID=\"8\"\nPLATFORM_ID=\"platform:el8\"\nPRETTY_NAME=\"CentOS Linux 8 (Core)\"\nANSI_COLOR=\"0;31\"\nCPE_NAME=\"cpe:/o:centos:centos:8\"\nHOME_URL=\"https://www.centos.org/\"\nBUG_REPORT_URL=\"https://bugs.centos.org/\"\n\nCENTOS_MANTISBT_PROJECT=\"CentOS-8\"\nCENTOS_MANTISBT_PROJECT_VERSION=\"8\"\nREDHAT_SUPPORT_PRODUCT=\"centos\"\nREDHAT_SUPPORT_PRODUCT_VERSION=\"8\"\n\n"
  },
  {
    "path": "grype/distro/testdata/debian-8",
    "content": "PRETTY_NAME=\"Debian GNU/Linux 8 (jessie)\"\nNAME=\"Debian GNU/Linux\"\nVERSION_ID=\"8\"\nVERSION=\"8 (jessie)\"\nID=debian\nHOME_URL=\"http://www.debian.org/\"\nSUPPORT_URL=\"http://www.debian.org/support\"\nBUG_REPORT_URL=\"https://bugs.debian.org/\"\n"
  },
  {
    "path": "grype/distro/testdata/os/almalinux/etc/os-release",
    "content": "NAME=\"AlmaLinux\"\nVERSION=\"8.4 (Electric Cheetah)\"\nID=\"almalinux\"\nID_LIKE=\"rhel centos fedora\"\nVERSION_ID=\"8.4\"\nPLATFORM_ID=\"platform:el8\"\nPRETTY_NAME=\"AlmaLinux 8.4 (Electric Cheetah)\"\nANSI_COLOR=\"0;34\"\nCPE_NAME=\"cpe:/o:almalinux:almalinux:8.4:GA\"\nHOME_URL=\"https://almalinux.org/\"\nDOCUMENTATION_URL=\"https://wiki.almalinux.org/\"\nBUG_REPORT_URL=\"https://bugs.almalinux.org/\"\n\nALMALINUX_MANTISBT_PROJECT=\"AlmaLinux-8\"\nALMALINUX_MANTISBT_PROJECT_VERSION=\"8.4\""
  },
  {
    "path": "grype/distro/testdata/os/alpine/etc/os-release",
    "content": "NAME=\"Alpine Linux\"\nID=alpine\nVERSION_ID=3.11.6\nPRETTY_NAME=\"Alpine Linux v3.11\"\nHOME_URL=\"https://alpinelinux.org/\"\nBUG_REPORT_URL=\"https://bugs.alpinelinux.org/\"\n"
  },
  {
    "path": "grype/distro/testdata/os/alpine-edge/etc/os-release",
    "content": "NAME=\"Alpine Linux\"\nID=alpine\nVERSION_ID=3.22.0_alpha20250108\nPRETTY_NAME=\"Alpine Linux edge\"\nHOME_URL=\"https://alpinelinux.org/\"\nBUG_REPORT_URL=\"https://gitlab.alpinelinux.org/alpine/aports/-/issues\""
  },
  {
    "path": "grype/distro/testdata/os/amazon/etc/os-release",
    "content": "NAME=\"Amazon Linux\"\nVERSION=\"2\"\nID=\"amzn\"\nID_LIKE=\"centos rhel fedora\"\nVERSION_ID=\"2\"\nPRETTY_NAME=\"Amazon Linux 2\"\nANSI_COLOR=\"0;33\"\nCPE_NAME=\"cpe:2.3:o:amazon:amazon_linux:2\"\nHOME_URL=\"https://amazonlinux.com/\"\n"
  },
  {
    "path": "grype/distro/testdata/os/arch/etc/os-release",
    "content": "NAME=\"Arch Linux\"\nPRETTY_NAME=\"Arch Linux\"\nID=arch\nBUILD_ID=rolling\nANSI_COLOR=\"38;2;23;147;209\"\nHOME_URL=\"https://www.archlinux.org/\"\nDOCUMENTATION_URL=\"https://wiki.archlinux.org/\"\nSUPPORT_URL=\"https://bbs.archlinux.org/\"\nBUG_REPORT_URL=\"https://bugs.archlinux.org/\"\nLOGO=archlinux\n"
  },
  {
    "path": "grype/distro/testdata/os/azurelinux/etc/os-release",
    "content": "NAME=\"Microsoft Azure Linux\"\nVERSION=\"3.0.20240417\"\nID=azurelinux\nVERSION_ID=\"3.0\"\nPRETTY_NAME=\"Microsoft Azure Linux 3.0\"\nANSI_COLOR=\"1;34\"\nHOME_URL=\"https://aka.ms/azurelinux\"\nBUG_REPORT_URL=\"https://aka.ms/azurelinux\"\nSUPPORT_URL=\"https://aka.ms/azurelinux\"\n"
  },
  {
    "path": "grype/distro/testdata/os/busybox/bin/busybox",
    "content": "junk...BusyBox v1.31.1more junk"
  },
  {
    "path": "grype/distro/testdata/os/centos/usr/lib/os-release",
    "content": "NAME=\"CentOS Linux\"\nVERSION=\"8 (Core)\"\nID=\"centos\"\nID_LIKE=\"rhel fedora\"\nVERSION_ID=\"8\"\nPLATFORM_ID=\"platform:el8\"\nPRETTY_NAME=\"CentOS Linux 8 (Core)\"\nANSI_COLOR=\"0;31\"\nCPE_NAME=\"cpe:/o:centos:centos:8\"\nHOME_URL=\"https://www.centos.org/\"\nBUG_REPORT_URL=\"https://bugs.centos.org/\"\n\nCENTOS_MANTISBT_PROJECT=\"CentOS-8\"\nCENTOS_MANTISBT_PROJECT_VERSION=\"8\"\nREDHAT_SUPPORT_PRODUCT=\"centos\"\nREDHAT_SUPPORT_PRODUCT_VERSION=\"8\"\n"
  },
  {
    "path": "grype/distro/testdata/os/centos5/etc/redhat-release",
    "content": "CentOS release 5.7 (Final)"
  },
  {
    "path": "grype/distro/testdata/os/centos6/etc/system-release-cpe",
    "content": "cpe:/o:centos:linux:6:GA"
  },
  {
    "path": "grype/distro/testdata/os/chainguard/etc/os-release",
    "content": "ID=chainguard\nNAME=\"Chainguard\"\nPRETTY_NAME=\"Chainguard\"\nVERSION_ID=\"20230214\"\nHOME_URL=\"https://chainguard.dev/\"\n"
  },
  {
    "path": "grype/distro/testdata/os/custom/etc/os-release",
    "content": "NAME=\"Scientific Linux\"\nVERSION=\"16 (Core)\"\nID=\"scientific\"\nID_LIKE=\"rhel fedora\"\nVERSION_ID=\"8\"\nPLATFORM_ID=\"platform:el8\"\nPRETTY_NAME=\"CentOS Linux 8 (Core)\"\nANSI_COLOR=\"0;31\"\nCPE_NAME=\"cpe:/o:centos:centos:8\"\nHOME_URL=\"https://www.centos.org/\"\nBUG_REPORT_URL=\"https://bugs.centos.org/\"\n\nCENTOS_MANTISBT_PROJECT=\"CentOS-8\"\nCENTOS_MANTISBT_PROJECT_VERSION=\"8\"\nREDHAT_SUPPORT_PRODUCT=\"centos\"\nREDHAT_SUPPORT_PRODUCT_VERSION=\"8\"\n\n"
  },
  {
    "path": "grype/distro/testdata/os/debian/usr/lib/os-release",
    "content": "PRETTY_NAME=\"Debian GNU/Linux 8 (jessie)\"\nNAME=\"Debian GNU/Linux\"\nVERSION_ID=\"8\"\nVERSION=\"8 (jessie)\"\nID=debian\nHOME_URL=\"http://www.debian.org/\"\nSUPPORT_URL=\"http://www.debian.org/support\"\nBUG_REPORT_URL=\"https://bugs.debian.org/\"\n"
  },
  {
    "path": "grype/distro/testdata/os/debian-sid/usr/lib/os-release",
    "content": "PRETTY_NAME=\"Debian GNU/Linux trixie/sid\"\nNAME=\"Debian GNU/Linux\"\nVERSION_CODENAME=trixie\nID=debian\nHOME_URL=\"https://www.debian.org/\"\nSUPPORT_URL=\"https://www.debian.org/support\"\nBUG_REPORT_URL=\"https://bugs.debian.org/\"\n"
  },
  {
    "path": "grype/distro/testdata/os/echo/etc/os-release",
    "content": "NAME=\"Echo Linux\"\nPRETTY_NAME=\"Echo Linux\"\nID=\"echo\"\nID_LIKE=\"debian\"\nVERSION_ID=\"1\"\nHOME_URL=\"https://echohq.com/\""
  },
  {
    "path": "grype/distro/testdata/os/empty/etc/os-release",
    "content": ""
  },
  {
    "path": "grype/distro/testdata/os/fedora/usr/lib/os-release",
    "content": "NAME=Fedora\nVERSION=\"31 (Container Image)\"\nID=fedora\nVERSION_ID=31\nVERSION_CODENAME=\"\"\nPLATFORM_ID=\"platform:f31\"\nPRETTY_NAME=\"Fedora 31 (Container Image)\"\nANSI_COLOR=\"0;34\"\nLOGO=fedora-logo-icon\nCPE_NAME=\"cpe:/o:fedoraproject:fedora:31\"\nHOME_URL=\"https://fedoraproject.org/\"\nDOCUMENTATION_URL=\"https://docs.fedoraproject.org/en-US/fedora/f31/system-administrators-guide/\"\nSUPPORT_URL=\"https://fedoraproject.org/wiki/Communicating_and_getting_help\"\nBUG_REPORT_URL=\"https://bugzilla.redhat.com/\"\nREDHAT_BUGZILLA_PRODUCT=\"Fedora\"\nREDHAT_BUGZILLA_PRODUCT_VERSION=31\nREDHAT_SUPPORT_PRODUCT=\"Fedora\"\nREDHAT_SUPPORT_PRODUCT_VERSION=31\nPRIVACY_POLICY_URL=\"https://fedoraproject.org/wiki/Legal:PrivacyPolicy\"\nVARIANT=\"Container Image\"\nVARIANT_ID=container\n"
  },
  {
    "path": "grype/distro/testdata/os/gentoo/etc/os-release",
    "content": "NAME=Gentoo\nID=gentoo\nPRETTY_NAME=\"Gentoo/Linux\"\nANSI_COLOR=\"1;32\"\nHOME_URL=\"https://www.gentoo.org/\"\nSUPPORT_URL=\"https://www.gentoo.org/support/\"\nBUG_REPORT_URL=\"https://bugs.gentoo.org/\"\n"
  },
  {
    "path": "grype/distro/testdata/os/mariner/etc/os-release",
    "content": "NAME=\"Common Base Linux Mariner\"\nVERSION=\"1.0.20210901\"\nID=mariner\nVERSION_ID=1.0\nPRETTY_NAME=\"CBL-Mariner/Linux\"\nANSI_COLOR=\"1;34\"\nHOME_URL=\"https://aka.ms/cbl-mariner\"\nBUG_REPORT_URL=\"https://aka.ms/cbl-mariner\"\nSUPPORT_URL=\"https://aka.ms/cbl-mariner\""
  },
  {
    "path": "grype/distro/testdata/os/minimos/etc/os-release",
    "content": "ID=minimos\nNAME=\"MinimOS\"\nPRETTY_NAME=\"MinimOS\"\nVERSION_ID=\"20241031\"\nHOME_URL=\"https://minimus.io\"\n"
  },
  {
    "path": "grype/distro/testdata/os/opensuse-leap/etc/os-release",
    "content": "NAME=\"openSUSE Leap\"\nVERSION=\"15.2\"\nID=\"opensuse-leap\"\nID_LIKE=\"suse opensuse\"\nVERSION_ID=\"15.2\"\nPRETTY_NAME=\"openSUSE Leap 15.2\"\nANSI_COLOR=\"0;32\"\nCPE_NAME=\"cpe:/o:opensuse:leap:15.2\"\nBUG_REPORT_URL=\"https://bugs.opensuse.org\"\nHOME_URL=\"https://www.opensuse.org/\"\n"
  },
  {
    "path": "grype/distro/testdata/os/oraclelinux/etc/os-release",
    "content": "NAME=\"Oracle Linux Server\"\nVERSION=\"8.3\"\nID=\"ol\"\nID_LIKE=\"fedora\"\nVARIANT=\"Server\"\nVARIANT_ID=\"server\"\nVERSION_ID=\"8.3\"\nPLATFORM_ID=\"platform:el8\"\nPRETTY_NAME=\"Oracle Linux Server 8.3\"\nANSI_COLOR=\"0;31\"\nCPE_NAME=\"cpe:/o:oracle:linux:8:3:server\"\nHOME_URL=\"https://linux.oracle.com/\"\nBUG_REPORT_URL=\"https://bugzilla.oracle.com/\"\n\nORACLE_BUGZILLA_PRODUCT=\"Oracle Linux 8\"\nORACLE_BUGZILLA_PRODUCT_VERSION=8.3\nORACLE_SUPPORT_PRODUCT=\"Oracle Linux\"\nORACLE_SUPPORT_PRODUCT_VERSION=8.3"
  },
  {
    "path": "grype/distro/testdata/os/photon/etc/os-release",
    "content": "NAME=\"VMware Photon OS\"\nVERSION=\"2.0\"\nID=photon\nVERSION_ID=2.0\nPRETTY_NAME=\"VMware Photon OS/Linux\"\nANSI_COLOR=\"1;34\"\nHOME_URL=\"https://vmware.github.io/photon/\"\nBUG_REPORT_URL=\"https://github.com/vmware/photon/issues\" \n"
  },
  {
    "path": "grype/distro/testdata/os/postmarketos/etc/os-release",
    "content": "PRETTY_NAME=\"postmarketOS v25.06\"\nNAME=\"postmarketOS\"\nVERSION_ID=\"v25.06\"\nVERSION=\"v25.06\"\nID=\"postmarketos\"\nID_LIKE=\"alpine\"\nHOME_URL=\"https://www.postmarketos.org/\"\nSUPPORT_URL=\"https://gitlab.postmarketos.org/postmarketOS\"\nBUG_REPORT_URL=\"https://gitlab.postmarketos.org/postmarketOS/pmaports/issues\"\nLOGO=\"postmarketos-logo\"\nANSI_COLOR=\"0;32\""
  },
  {
    "path": "grype/distro/testdata/os/postmarketos-edge/etc/os-release",
    "content": "PRETTY_NAME=\"postmarketOS edge\"\nNAME=\"postmarketOS\"\nVERSION_ID=\"edge\"\nVERSION=\"edge\"\nID=\"postmarketos\"\nID_LIKE=\"alpine\"\nHOME_URL=\"https://www.postmarketos.org/\"\nSUPPORT_URL=\"https://gitlab.postmarketos.org/postmarketOS\"\nBUG_REPORT_URL=\"https://gitlab.postmarketos.org/postmarketOS/pmaports/issues\"\nLOGO=\"postmarketos-logo\"\nANSI_COLOR=\"0;32\""
  },
  {
    "path": "grype/distro/testdata/os/raspbian/etc/os-release",
    "content": "PRETTY_NAME=\"Raspbian GNU/Linux 9 (stretch)\"\nNAME=\"Raspbian GNU/Linux\"\nVERSION_ID=\"9\"\nVERSION=\"9 (stretch)\"\nID=raspbian\nID_LIKE=debian\nHOME_URL=\"http://www.raspbian.org/\"\nSUPPORT_URL=\"http://www.raspbian.org/RaspbianForums\"\nBUG_REPORT_URL=\"http://www.raspbian.org/RaspbianBugs\"\n"
  },
  {
    "path": "grype/distro/testdata/os/redhat/usr/lib/os-release",
    "content": "NAME=\"Red Hat Enterprise Linux Server\"\nVERSION=\"7.3 (Maipo)\"\nID=\"rhel\"\nID_LIKE=\"fedora\"\nVERSION_ID=\"7.3\"\nPRETTY_NAME=\"Red Hat Enterprise Linux Server 7.3 (Maipo)\"\nANSI_COLOR=\"0;31\"\nCPE_NAME=\"cpe:/o:redhat:enterprise_linux:7.3:GA:server\"\nHOME_URL=\"https://www.redhat.com/\"\nBUG_REPORT_URL=\"https://bugzilla.redhat.com/\"\n\nREDHAT_BUGZILLA_PRODUCT=\"Red Hat Enterprise Linux 7\"\nREDHAT_BUGZILLA_PRODUCT_VERSION=7.3\nREDHAT_SUPPORT_PRODUCT=\"Red Hat Enterprise Linux\"\nREDHAT_SUPPORT_PRODUCT_VERSION=\"7.3\""
  },
  {
    "path": "grype/distro/testdata/os/rockylinux/etc/os-release",
    "content": "NAME=\"Rocky Linux\"\nVERSION=\"8.4 (Green Obsidian)\"\nID=\"rocky\"\nID_LIKE=\"rhel fedora\"\nVERSION_ID=\"8.4\"\nPLATFORM_ID=\"platform:el8\"\nPRETTY_NAME=\"Rocky Linux 8.4 (Green Obsidian)\"\nANSI_COLOR=\"0;32\"\nCPE_NAME=\"cpe:/o:rocky:rocky:8.4:GA\"\nHOME_URL=\"https://rockylinux.org/\"\nBUG_REPORT_URL=\"https://bugs.rockylinux.org/\"\nROCKY_SUPPORT_PRODUCT=\"Rocky Linux\"\nROCKY_SUPPORT_PRODUCT_VERSION=\"8\""
  },
  {
    "path": "grype/distro/testdata/os/scientific/etc/os-release",
    "content": "NAME=\"Scientific Linux\"\nVERSION=\"7.5 (Nitrogen)\"\nID=\"scientific\"\nID_LIKE=\"rhel centos fedora\"\nVERSION_ID=\"7.5\"\nPRETTY_NAME=\"Scientific Linux 7.5 (Nitrogen)\"\nANSI_COLOR=\"0;31\"\nCPE_NAME=\"cpe:/o:scientificlinux:scientificlinux:7.5:GA\"\nHOME_URL=\"http://www.scientificlinux.org//\"\nBUG_REPORT_URL=\"mailto:scientific-linux-devel@listserv.fnal.gov\"\n\nREDHAT_BUGZILLA_PRODUCT=\"Scientific Linux 7\"\nREDHAT_BUGZILLA_PRODUCT_VERSION=7.5\nREDHAT_SUPPORT_PRODUCT=\"Scientific Linux\"\nREDHAT_SUPPORT_PRODUCT_VERSION=\"7.5\""
  },
  {
    "path": "grype/distro/testdata/os/scientific6/etc/redhat-release",
    "content": "Scientific Linux release 6.10 (Carbon)"
  },
  {
    "path": "grype/distro/testdata/os/secureos/etc/os-release",
    "content": "ID=secureos\nNAME=\"SecureOS\"\nPRETTY_NAME=\"SecureOS (SecureBuild)\"\nVERSION_ID=\"2025.09.09\"\nHOME_URL=\"https://securebuild.com/\"\n"
  },
  {
    "path": "grype/distro/testdata/os/sles/etc/os-release",
    "content": "NAME=\"SLES\"\nVERSION=\"15-SP2\"\nVERSION_ID=\"15.2\"\nPRETTY_NAME=\"SUSE Linux Enterprise Server 15 SP2\"\nID=\"sles\"\nID_LIKE=\"suse\"\nANSI_COLOR=\"0;32\"\nCPE_NAME=\"cpe:/o:suse:sles:15:sp2\"\nDOCUMENTATION_URL=\"https://documentation.suse.com/\""
  },
  {
    "path": "grype/distro/testdata/os/ubuntu/etc/os-release",
    "content": "NAME=\"Ubuntu\"\nVERSION=\"20.04 LTS (Focal Fossa)\"\nID=ubuntu\nID_LIKE=debian\nPRETTY_NAME=\"Ubuntu 20.04 LTS\"\nVERSION_ID=\"20.04\"\nHOME_URL=\"https://www.ubuntu.com/\"\nSUPPORT_URL=\"https://help.ubuntu.com/\"\nBUG_REPORT_URL=\"https://bugs.launchpad.net/ubuntu/\"\nPRIVACY_POLICY_URL=\"https://www.ubuntu.com/legal/terms-and-policies/privacy-policy\"\nVERSION_CODENAME=focal\nUBUNTU_CODENAME=focal\n"
  },
  {
    "path": "grype/distro/testdata/os/wolfi/etc/os-release",
    "content": "ID=wolfi\nNAME=\"Wolfi\"\nPRETTY_NAME=\"Wolfi\"\nVERSION_ID=\"20220914\"\nHOME_URL=\"https://wolfi.dev\"\n"
  },
  {
    "path": "grype/distro/testdata/partial-fields/missing-id/usr/lib/os-release",
    "content": "NAME=\"Debian GNU/Linux\"\nVERSION_ID=\"8\"\nID_LIKE=debian\n"
  },
  {
    "path": "grype/distro/testdata/partial-fields/missing-version/usr/lib/os-release",
    "content": "NAME=\"Debian GNU/Linux\"\nID_LIKE=debian\n"
  },
  {
    "path": "grype/distro/testdata/partial-fields/unknown-id/usr/lib/os-release",
    "content": "NAME=\"Debian GNU/Linux\"\nVERSION_ID=\"8\"\nID=my-awesome-distro\nID_LIKE=debian\n"
  },
  {
    "path": "grype/distro/testdata/rhel-8",
    "content": "NAME=\"Red Hat Enterprise Linux\"\nVERSION=\"8.1 (Ootpa)\"\nID=\"rhel\"\nID_LIKE=\"fedora\"\nVERSION_ID=\"8.1\"\nPLATFORM_ID=\"platform:el8\"\nPRETTY_NAME=\"Red Hat Enterprise Linux 8.1 (Ootpa)\"\nANSI_COLOR=\"0;31\"\nCPE_NAME=\"cpe:/o:redhat:enterprise_linux:8.1:GA\"\nHOME_URL=\"https://www.redhat.com/\"\nBUG_REPORT_URL=\"https://bugzilla.redhat.com/\"\n\nREDHAT_BUGZILLA_PRODUCT=\"Red Hat Enterprise Linux 8\"\nREDHAT_BUGZILLA_PRODUCT_VERSION=8.1\nREDHAT_SUPPORT_PRODUCT=\"Red Hat Enterprise Linux\"\nREDHAT_SUPPORT_PRODUCT_VERSION=\"8.1\"\n"
  },
  {
    "path": "grype/distro/testdata/ubuntu-20.04",
    "content": "NAME=\"Ubuntu\"\nVERSION=\"20.04 LTS (Focal Fossa)\"\nID=ubuntu\nID_LIKE=debian\nPRETTY_NAME=\"Ubuntu 20.04 LTS\"\nVERSION_ID=\"20.04\"\nHOME_URL=\"https://www.ubuntu.com/\"\nSUPPORT_URL=\"https://help.ubuntu.com/\"\nBUG_REPORT_URL=\"https://bugs.launchpad.net/ubuntu/\"\nPRIVACY_POLICY_URL=\"https://www.ubuntu.com/legal/terms-and-policies/privacy-policy\"\nVERSION_CODENAME=focal\nUBUNTU_CODENAME=focal\n"
  },
  {
    "path": "grype/distro/testdata/unprintable",
    "content": "PRETTY_NAME=\"Debian GNU/Linux 8 (jessie)\"\r\nNAME=\"Debian GNU/Linux\"\r\nVERSION_ID=\"8\"\r\nVERSION=\"8 (jessie)\"\r\nID=debian\r\nHOME_URL=\"http://www.debian.org/\"\r\nSUPPORT_URL=\"http://www.debian.org/support\"\r\nBUG_REPORT_URL=\"https://bugs.debian.org/\"\r\n"
  },
  {
    "path": "grype/distro/type.go",
    "content": "package distro\n\nimport (\n\t\"github.com/anchore/syft/syft/linux\"\n)\n\n// Type represents the different Linux distribution options\ntype Type string\n\nconst (\n\t// represents the set of supported Linux Distributions\n\n\tDebian       Type = \"debian\"\n\tUbuntu       Type = \"ubuntu\"\n\tRedHat       Type = \"redhat\"\n\tCentOS       Type = \"centos\"\n\tFedora       Type = \"fedora\"\n\tAlpine       Type = \"alpine\"\n\tBusybox      Type = \"busybox\"\n\tAmazonLinux  Type = \"amazonlinux\"\n\tOracleLinux  Type = \"oraclelinux\"\n\tArchLinux    Type = \"archlinux\"\n\tOpenSuseLeap Type = \"opensuseleap\"\n\tSLES         Type = \"sles\"\n\tPhoton       Type = \"photon\"\n\tEcho         Type = \"echo\"\n\tWindows      Type = \"windows\"\n\tMariner      Type = \"mariner\"\n\tAzure        Type = \"azurelinux\"\n\tRockyLinux   Type = \"rockylinux\"\n\tAlmaLinux    Type = \"almalinux\"\n\tGentoo       Type = \"gentoo\"\n\tWolfi        Type = \"wolfi\"\n\tChainguard   Type = \"chainguard\"\n\tMinimOS      Type = \"minimos\"\n\tRaspbian     Type = \"raspbian\"\n\tScientific   Type = \"scientific\"\n\tSecureOS     Type = \"secureos\"\n\tPostmarketOS Type = \"postmarketos\"\n)\n\n// All contains all Linux distribution options\nvar All = []Type{\n\tDebian,\n\tUbuntu,\n\tRedHat,\n\tCentOS,\n\tFedora,\n\tAlpine,\n\tBusybox,\n\tAmazonLinux,\n\tOracleLinux,\n\tArchLinux,\n\tOpenSuseLeap,\n\tSLES,\n\tPhoton,\n\tEcho,\n\tWindows,\n\tMariner,\n\tAzure,\n\tRockyLinux,\n\tAlmaLinux,\n\tGentoo,\n\tWolfi,\n\tChainguard,\n\tMinimOS,\n\tRaspbian,\n\tScientific,\n\tSecureOS,\n\tPostmarketOS,\n}\n\n// IDMapping maps a distro ID from the /etc/os-release (e.g. like \"ubuntu\") to a Distro type.\nvar IDMapping = map[string]Type{\n\t\"debian\":        Debian,\n\t\"ubuntu\":        Ubuntu,\n\t\"rhel\":          RedHat,\n\t\"centos\":        CentOS,\n\t\"fedora\":        Fedora,\n\t\"alpine\":        Alpine,\n\t\"busybox\":       Busybox,\n\t\"amzn\":          AmazonLinux,\n\t\"ol\":            OracleLinux,\n\t\"arch\":          ArchLinux,\n\t\"opensuse-leap\": OpenSuseLeap,\n\t\"sles\":          SLES,\n\t\"photon\":        Photon,\n\t\"echo\":          Echo,\n\t\"mariner\":       Mariner,\n\t\"azurelinux\":    Azure,\n\t\"rocky\":         RockyLinux,\n\t\"almalinux\":     AlmaLinux,\n\t\"gentoo\":        Gentoo,\n\t\"wolfi\":         Wolfi,\n\t\"chainguard\":    Chainguard,\n\t\"minimos\":       MinimOS,\n\t\"raspbian\":      Raspbian,\n\t\"scientific\":    Scientific,\n\t\"secureos\":      SecureOS,\n\t\"postmarketos\":  PostmarketOS,\n}\n\n// aliasTypes maps common aliases to their corresponding Type.\nvar aliasTypes = map[string]Type{\n\t\"Alpine Linux\":     Alpine, // needed for CPE matching (see #2039)\n\t\"windows\":          Windows,\n\t\"scientific linux\": Scientific, // Scientific linux prior to v7 didn't have an os-release file and syft raises up \"scientific linux\" as the release id as parsed from /etc/redhat-release\n}\n\nvar typeToIDMapping = map[Type]string{}\n\nfunc init() {\n\tfor id, t := range IDMapping {\n\t\tif _, ok := typeToIDMapping[t]; ok {\n\t\t\tpanic(\"duplicate Type found for ID: \" + id + \" with Type: \" + string(t))\n\t\t}\n\t\ttypeToIDMapping[t] = id\n\t}\n}\n\nfunc TypeFromRelease(release linux.Release) Type {\n\t// first try the release ID\n\tif t, ok := IDMapping[release.ID]; ok {\n\t\treturn t\n\t}\n\n\tif t, ok := aliasTypes[release.ID]; ok {\n\t\treturn t\n\t}\n\n\t// use ID_LIKE as a backup\n\tfor _, l := range release.IDLike {\n\t\tif t, ok := IDMapping[l]; ok {\n\t\t\treturn t\n\t\t}\n\t\tif t, ok := aliasTypes[l]; ok {\n\t\t\treturn t\n\t\t}\n\t}\n\n\t// then try the release name as a fallback\n\tif t, ok := IDMapping[release.Name]; ok {\n\t\treturn t\n\t}\n\n\tif t, ok := aliasTypes[release.Name]; ok {\n\t\treturn t\n\t}\n\n\treturn \"\"\n}\n\n// String returns the string representation of the given Linux distribution.\nfunc (t Type) String() string {\n\treturn string(t)\n}\n"
  },
  {
    "path": "grype/distro/type_test.go",
    "content": "package distro\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\n\t\"github.com/anchore/syft/syft/linux\"\n)\n\nfunc TestTypeFromRelease(t *testing.T) {\n\n\ttests := []struct {\n\t\tname    string\n\t\trelease linux.Release\n\t\twant    Type\n\t}{\n\t\t{\n\t\t\tname: \"direct ID mapping\",\n\t\t\trelease: linux.Release{\n\t\t\t\tID: \"ubuntu\",\n\t\t\t},\n\t\t\twant: Ubuntu,\n\t\t},\n\t\t{\n\t\t\tname: \"direct ID mapping rhel\",\n\t\t\trelease: linux.Release{\n\t\t\t\tID: \"rhel\",\n\t\t\t},\n\t\t\twant: RedHat,\n\t\t},\n\t\t{\n\t\t\tname: \"alias mapping\",\n\t\t\trelease: linux.Release{\n\t\t\t\tID: \"Alpine Linux\",\n\t\t\t},\n\t\t\twant: Alpine,\n\t\t},\n\t\t{\n\t\t\tname: \"alias mapping windows\",\n\t\t\trelease: linux.Release{\n\t\t\t\tID: \"windows\",\n\t\t\t},\n\t\t\twant: Windows,\n\t\t},\n\t\t{\n\t\t\tname: \"ID_LIKE mapping\",\n\t\t\trelease: linux.Release{\n\t\t\t\tID:     \"unknown-distro\",\n\t\t\t\tIDLike: []string{\"debian\", \"ubuntu\"},\n\t\t\t},\n\t\t\twant: Debian,\n\t\t},\n\t\t{\n\t\t\tname: \"ID_LIKE alias mapping\",\n\t\t\trelease: linux.Release{\n\t\t\t\tID:     \"custom-alpine\",\n\t\t\t\tIDLike: []string{\"Alpine Linux\"},\n\t\t\t},\n\t\t\twant: Alpine,\n\t\t},\n\t\t{\n\t\t\tname: \"fallback to name ID mapping\",\n\t\t\trelease: linux.Release{\n\t\t\t\tID:   \"unrecognized\",\n\t\t\t\tName: \"fedora\",\n\t\t\t},\n\t\t\twant: Fedora,\n\t\t},\n\t\t{\n\t\t\tname: \"fallback to name alias mapping\",\n\t\t\trelease: linux.Release{\n\t\t\t\tID:   \"unrecognized\",\n\t\t\t\tName: \"windows\",\n\t\t\t},\n\t\t\twant: Windows,\n\t\t},\n\t\t{\n\t\t\tname: \"empty result when no matches\",\n\t\t\trelease: linux.Release{\n\t\t\t\tID:   \"totally-unknown\",\n\t\t\t\tName: \"also-unknown\",\n\t\t\t},\n\t\t\twant: \"\",\n\t\t},\n\t\t{\n\t\t\tname: \"prefer ID over ID_LIKE\",\n\t\t\trelease: linux.Release{\n\t\t\t\tID:     \"ubuntu\",\n\t\t\t\tIDLike: []string{\"debian\"},\n\t\t\t},\n\t\t\twant: Ubuntu,\n\t\t},\n\t\t{\n\t\t\tname: \"prefer ID over name\",\n\t\t\trelease: linux.Release{\n\t\t\t\tID:   \"ubuntu\",\n\t\t\t\tName: \"fedora\",\n\t\t\t},\n\t\t\twant: Ubuntu,\n\t\t},\n\t\t{\n\t\t\tname: \"prefer ID_LIKE over name\",\n\t\t\trelease: linux.Release{\n\t\t\t\tID:     \"unknown\",\n\t\t\t\tIDLike: []string{\"centos\"},\n\t\t\t\tName:   \"fedora\",\n\t\t\t},\n\t\t\twant: CentOS,\n\t\t},\n\t\t{\n\t\t\tname: \"multiple ID_LIKE entries use first match\",\n\t\t\trelease: linux.Release{\n\t\t\t\tID:     \"unknown\",\n\t\t\t\tIDLike: []string{\"nonexistent\", \"alpine\", \"debian\"},\n\t\t\t},\n\t\t\twant: Alpine,\n\t\t},\n\t\t{\n\t\t\tname: \"Scientific Linux 6\",\n\t\t\trelease: linux.Release{\n\t\t\t\tName:      \"Scientific Linux\",\n\t\t\t\tID:        \"scientific linux\",\n\t\t\t\tIDLike:    []string{\"scientific linux\"},\n\t\t\t\tVersion:   \"6.10 Carbon\",\n\t\t\t\tVersionID: \"6.10\",\n\t\t\t},\n\t\t\twant: Scientific,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot := TypeFromRelease(tt.release)\n\t\t\tassert.Equal(t, tt.want, got)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/event/event.go",
    "content": "package event\n\nimport (\n\t\"github.com/wagoodman/go-partybus\"\n)\n\nconst (\n\ttypePrefix    = \"grype\"\n\tcliTypePrefix = typePrefix + \"-cli\"\n\n\t// Events from the grype library\n\n\tUpdateVulnerabilityDatabase  partybus.EventType = typePrefix + \"-update-vulnerability-database\"\n\tVulnerabilityScanningStarted partybus.EventType = typePrefix + \"-vulnerability-scanning-started\"\n\tDatabaseDiffingStarted       partybus.EventType = typePrefix + \"-database-diffing-started\"\n\n\t// Events exclusively for the CLI\n\n\t// CLIAppUpdateAvailable is a partybus event that occurs when an application update is available\n\tCLIAppUpdateAvailable partybus.EventType = cliTypePrefix + \"-app-update-available\"\n\n\t// CLIReport is a partybus event that occurs when an analysis result is ready for final presentation to stdout\n\tCLIReport partybus.EventType = cliTypePrefix + \"-report\"\n\n\t// CLINotification is a partybus event that occurs when auxiliary information is ready for presentation to stderr\n\tCLINotification partybus.EventType = cliTypePrefix + \"-notification\"\n)\n"
  },
  {
    "path": "grype/event/monitor/db_diff.go",
    "content": "package monitor\n\nimport \"github.com/wagoodman/go-progress\"\n\ntype DBDiff struct {\n\tStager                progress.Stager\n\tStageProgress         progress.Progressable\n\tDifferencesDiscovered progress.Monitorable\n}\n"
  },
  {
    "path": "grype/event/monitor/matching.go",
    "content": "package monitor\n\nimport (\n\t\"github.com/wagoodman/go-progress\"\n\n\t\"github.com/anchore/grype/grype/vulnerability\"\n)\n\ntype Matching struct {\n\tPackagesProcessed progress.Progressable\n\tMatchesDiscovered progress.Monitorable\n\tFixed             progress.Monitorable\n\tIgnored           progress.Monitorable\n\tDropped           progress.Monitorable\n\tBySeverity        map[vulnerability.Severity]progress.Monitorable\n}\n"
  },
  {
    "path": "grype/event/parsers/parsers.go",
    "content": "package parsers\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/wagoodman/go-partybus\"\n\t\"github.com/wagoodman/go-progress\"\n\n\t\"github.com/anchore/grype/grype/event\"\n\t\"github.com/anchore/grype/grype/event/monitor\"\n)\n\ntype ErrBadPayload struct {\n\tType  partybus.EventType\n\tField string\n\tValue interface{}\n}\n\nfunc (e *ErrBadPayload) Error() string {\n\treturn fmt.Sprintf(\"event='%s' has bad event payload field='%v': '%+v'\", string(e.Type), e.Field, e.Value)\n}\n\nfunc newPayloadErr(t partybus.EventType, field string, value interface{}) error {\n\treturn &ErrBadPayload{\n\t\tType:  t,\n\t\tField: field,\n\t\tValue: value,\n\t}\n}\n\nfunc checkEventType(actual, expected partybus.EventType) error {\n\tif actual != expected {\n\t\treturn newPayloadErr(expected, \"Type\", actual)\n\t}\n\treturn nil\n}\n\nfunc ParseUpdateVulnerabilityDatabase(e partybus.Event) (progress.StagedProgressable, error) {\n\tif err := checkEventType(e.Type, event.UpdateVulnerabilityDatabase); err != nil {\n\t\treturn nil, err\n\t}\n\n\tprog, ok := e.Value.(progress.StagedProgressable)\n\tif !ok {\n\t\treturn nil, newPayloadErr(e.Type, \"Value\", e.Value)\n\t}\n\n\treturn prog, nil\n}\n\nfunc ParseVulnerabilityScanningStarted(e partybus.Event) (*monitor.Matching, error) {\n\tif err := checkEventType(e.Type, event.VulnerabilityScanningStarted); err != nil {\n\t\treturn nil, err\n\t}\n\n\tmon, ok := e.Value.(monitor.Matching)\n\tif !ok {\n\t\treturn nil, newPayloadErr(e.Type, \"Value\", e.Value)\n\t}\n\n\treturn &mon, nil\n}\n\nfunc ParseDatabaseDiffingStarted(e partybus.Event) (*monitor.DBDiff, error) {\n\tif err := checkEventType(e.Type, event.DatabaseDiffingStarted); err != nil {\n\t\treturn nil, err\n\t}\n\n\tmon, ok := e.Value.(monitor.DBDiff)\n\tif !ok {\n\t\treturn nil, newPayloadErr(e.Type, \"Value\", e.Value)\n\t}\n\n\treturn &mon, nil\n}\n\ntype UpdateCheck struct {\n\tNew     string\n\tCurrent string\n}\n\nfunc ParseCLIAppUpdateAvailable(e partybus.Event) (*UpdateCheck, error) {\n\tif err := checkEventType(e.Type, event.CLIAppUpdateAvailable); err != nil {\n\t\treturn nil, err\n\t}\n\n\tupdateCheck, ok := e.Value.(UpdateCheck)\n\tif !ok {\n\t\treturn nil, newPayloadErr(e.Type, \"Value\", e.Value)\n\t}\n\n\treturn &updateCheck, nil\n}\n\nfunc ParseCLIReport(e partybus.Event) (string, string, error) {\n\tif err := checkEventType(e.Type, event.CLIReport); err != nil {\n\t\treturn \"\", \"\", err\n\t}\n\n\tcontext, ok := e.Source.(string)\n\tif !ok {\n\t\t// this is optional\n\t\tcontext = \"\"\n\t}\n\n\treport, ok := e.Value.(string)\n\tif !ok {\n\t\treturn \"\", \"\", newPayloadErr(e.Type, \"Value\", e.Value)\n\t}\n\n\treturn context, report, nil\n}\n\nfunc ParseCLINotification(e partybus.Event) (string, string, error) {\n\tif err := checkEventType(e.Type, event.CLINotification); err != nil {\n\t\treturn \"\", \"\", err\n\t}\n\n\tcontext, ok := e.Source.(string)\n\tif !ok {\n\t\t// this is optional\n\t\tcontext = \"\"\n\t}\n\n\tnotification, ok := e.Value.(string)\n\tif !ok {\n\t\treturn \"\", \"\", newPayloadErr(e.Type, \"Value\", e.Value)\n\t}\n\n\treturn context, notification, nil\n}\n"
  },
  {
    "path": "grype/grypeerr/errors.go",
    "content": "package grypeerr\n\nvar (\n\t// ErrAboveSeverityThreshold indicates when a vulnerability severity is discovered that is equal\n\t// or above the given --fail-on severity value.\n\tErrAboveSeverityThreshold = NewExpectedErr(\"discovered vulnerabilities at or above the severity threshold\")\n\n\t// ErrDBUpgradeAvailable indicates that a DB upgrade is available.\n\tErrDBUpgradeAvailable = NewExpectedErr(\"db upgrade available\")\n)\n"
  },
  {
    "path": "grype/grypeerr/expected_error.go",
    "content": "package grypeerr\n\nimport (\n\t\"fmt\"\n)\n\n// ExpectedErr represents a class of expected errors that grype may produce.\ntype ExpectedErr struct {\n\tErr error\n}\n\n// New generates a new ExpectedErr.\nfunc NewExpectedErr(msgFormat string, args ...interface{}) ExpectedErr {\n\treturn ExpectedErr{\n\t\tErr: fmt.Errorf(msgFormat, args...),\n\t}\n}\n\n// Error returns a string representing the underlying error condition.\nfunc (e ExpectedErr) Error() string {\n\treturn e.Err.Error()\n}\n"
  },
  {
    "path": "grype/internal/generate.go",
    "content": "package internal\n\n//go:generate go run ./packagemetadata/generate/main.go\n"
  },
  {
    "path": "grype/internal/packagemetadata/discover_type_names.go",
    "content": "package packagemetadata\n\nimport (\n\t\"fmt\"\n\t\"go/ast\"\n\t\"go/parser\"\n\t\"go/token\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"sort\"\n\t\"strings\"\n\t\"unicode\"\n\n\t\"github.com/scylladb/go-set/strset\"\n)\n\nvar metadataExceptions = strset.New(\n\t\"FileMetadata\",\n\t\"SBOMFileMetadata\",\n\t\"PURLLiteralMetadata\",\n\t\"CPELiteralMetadata\",\n)\n\nfunc DiscoverTypeNames() ([]string, error) {\n\troot, err := RepoRoot()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tfiles, err := filepath.Glob(filepath.Join(root, \"grype/pkg/*.go\"))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn findMetadataDefinitionNames(files...)\n}\n\nfunc RepoRoot() (string, error) {\n\troot, err := exec.Command(\"git\", \"rev-parse\", \"--show-toplevel\").Output()\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"unable to find repo root dir: %+v\", err)\n\t}\n\tabsRepoRoot, err := filepath.Abs(strings.TrimSpace(string(root)))\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"unable to get abs path to repo root: %w\", err)\n\t}\n\treturn absRepoRoot, nil\n}\n\nfunc findMetadataDefinitionNames(paths ...string) ([]string, error) {\n\tnames := strset.New()\n\tusedNames := strset.New()\n\tfor _, path := range paths {\n\t\tmetadataDefinitions, usedTypeNames, err := findMetadataDefinitionNamesInFile(path)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\t// useful for debugging...\n\t\t// fmt.Println(path)\n\t\t// fmt.Println(\"Defs:\", metadataDefinitions)\n\t\t// fmt.Println(\"Used Types:\", usedTypeNames)\n\t\t// fmt.Println()\n\n\t\tnames.Add(metadataDefinitions...)\n\t\tusedNames.Add(usedTypeNames...)\n\t}\n\n\t// any definition that is used within another struct should not be considered a top-level metadata definition\n\tnames.Remove(usedNames.List()...)\n\n\tstrNames := names.List()\n\tsort.Strings(strNames)\n\n\t// note: 3 is a point-in-time gut check. This number could be updated if new metadata definitions are added, but is not required.\n\t// it is really intended to catch any major issues with the generation process that would generate, say, 0 definitions.\n\tif len(strNames) < 3 {\n\t\treturn nil, fmt.Errorf(\"not enough metadata definitions found: discovered %d \", len(strNames))\n\t}\n\n\treturn strNames, nil\n}\n\nfunc findMetadataDefinitionNamesInFile(path string) ([]string, []string, error) {\n\t// set up the parser\n\tfs := token.NewFileSet()\n\tf, err := parser.ParseFile(fs, path, nil, parser.ParseComments)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\tvar metadataDefinitions []string\n\tvar usedTypeNames []string\n\tfor _, decl := range f.Decls {\n\t\t// check if the declaration is a type declaration\n\t\tspec, ok := decl.(*ast.GenDecl)\n\t\tif !ok || spec.Tok != token.TYPE {\n\t\t\tcontinue\n\t\t}\n\n\t\t// loop over all types declared in the type declaration\n\t\tfor _, typ := range spec.Specs {\n\t\t\t// check if the type is a struct type\n\t\t\tspec, ok := typ.(*ast.TypeSpec)\n\t\t\tif !ok || spec.Type == nil {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tstructType, ok := spec.Type.(*ast.StructType)\n\t\t\tif !ok {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// check if the struct type ends with \"Metadata\"\n\t\t\tname := spec.Name.String()\n\n\t\t\t// only look for exported types that end with \"Metadata\"\n\t\t\tif isMetadataTypeCandidate(name) {\n\t\t\t\t// print the full declaration of the struct type\n\t\t\t\tmetadataDefinitions = append(metadataDefinitions, name)\n\t\t\t\tusedTypeNames = append(usedTypeNames, typeNamesUsedInStruct(structType)...)\n\t\t\t}\n\t\t}\n\t}\n\treturn metadataDefinitions, usedTypeNames, nil\n}\n\nfunc typeNamesUsedInStruct(structType *ast.StructType) []string {\n\t// recursively find all type names used in the struct type\n\tvar names []string\n\tfor i := range structType.Fields.List {\n\t\t// capture names of all of the types (not field names)\n\t\tast.Inspect(structType.Fields.List[i].Type, func(n ast.Node) bool {\n\t\t\tident, ok := n.(*ast.Ident)\n\t\t\tif !ok {\n\t\t\t\treturn true\n\t\t\t}\n\n\t\t\t// add the type name to the list\n\t\t\tnames = append(names, ident.Name)\n\n\t\t\t// continue inspecting\n\t\t\treturn true\n\t\t})\n\t}\n\n\treturn names\n}\n\nfunc isMetadataTypeCandidate(name string) bool {\n\treturn len(name) > 0 &&\n\t\tstrings.HasSuffix(name, \"Metadata\") &&\n\t\tunicode.IsUpper(rune(name[0])) && // must be exported\n\t\t!metadataExceptions.Has(name)\n}\n"
  },
  {
    "path": "grype/internal/packagemetadata/generate/main.go",
    "content": "package main\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/dave/jennifer/jen\"\n\n\t\"github.com/anchore/grype/grype/internal/packagemetadata\"\n)\n\n// This program is invoked from grype/internal and generates packagemetadata/generated.go\n\nconst (\n\tpkgImport = \"github.com/anchore/grype/grype/pkg\"\n\tpath      = \"packagemetadata/generated.go\"\n)\n\nfunc main() {\n\ttypeNames, err := packagemetadata.DiscoverTypeNames()\n\tif err != nil {\n\t\tpanic(fmt.Errorf(\"unable to get all metadata type names: %w\", err))\n\t}\n\n\tfmt.Printf(\"updating package metadata type list with %+v types\\n\", len(typeNames))\n\n\tf := jen.NewFile(\"packagemetadata\")\n\tf.HeaderComment(\"DO NOT EDIT: generated by grype/internal/packagemetadata/generate/main.go\")\n\tf.ImportName(pkgImport, \"pkg\")\n\tf.Comment(\"AllTypes returns a list of all pkg metadata types that grype supports (that are represented in the pkg.Package.Metadata field).\")\n\n\tf.Func().Id(\"AllTypes\").Params().Index().Any().BlockFunc(func(g *jen.Group) {\n\t\tg.ReturnFunc(func(g *jen.Group) {\n\t\t\tg.Index().Any().ValuesFunc(func(g *jen.Group) {\n\t\t\t\tfor _, typeName := range typeNames {\n\t\t\t\t\tg.Qual(pkgImport, typeName).Values()\n\t\t\t\t}\n\t\t\t})\n\t\t})\n\t})\n\n\trendered := fmt.Sprintf(\"%#v\", f)\n\n\tfh, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)\n\tif err != nil {\n\t\tpanic(fmt.Errorf(\"unable to open file: %w\", err))\n\t}\n\t_, err = fh.WriteString(rendered)\n\tif err != nil {\n\t\tpanic(fmt.Errorf(\"unable to write file: %w\", err))\n\t}\n\tif err := fh.Close(); err != nil {\n\t\tpanic(fmt.Errorf(\"unable to close file: %w\", err))\n\t}\n}\n"
  },
  {
    "path": "grype/internal/packagemetadata/generated.go",
    "content": "// DO NOT EDIT: generated by grype/internal/packagemetadata/generate/main.go\n\npackage packagemetadata\n\nimport \"github.com/anchore/grype/grype/pkg\"\n\n// AllTypes returns a list of all pkg metadata types that grype supports (that are represented in the pkg.Package.Metadata field).\nfunc AllTypes() []any {\n\treturn []any{pkg.ApkMetadata{}, pkg.GolangBinMetadata{}, pkg.GolangModMetadata{}, pkg.GolangSourceMetadata{}, pkg.JavaMetadata{}, pkg.JavaVMInstallationMetadata{}, pkg.RpmMetadata{}}\n}\n"
  },
  {
    "path": "grype/internal/packagemetadata/names.go",
    "content": "package packagemetadata\n\nimport (\n\t\"reflect\"\n\t\"sort\"\n\t\"strings\"\n\n\t\"github.com/anchore/grype/grype/pkg\"\n)\n\n// jsonNameFromType is a map of all known package metadata types to their current JSON name and all previously known aliases.\n// TODO: in the future the metadata type names should match how it is used in syft. However, since the data shapes are\n// not the same it may be important to select different names. This design decision has been deferred, for now\n// the same metadata types that have been used in the past should be used here.\nvar jsonNameFromType = map[reflect.Type][]string{\n\treflect.TypeOf(pkg.ApkMetadata{}):                nameList(\"ApkMetadata\"),\n\treflect.TypeOf(pkg.GolangBinMetadata{}):          nameList(\"GolangBinMetadata\"),\n\treflect.TypeOf(pkg.GolangModMetadata{}):          nameList(\"GolangModMetadata\"),\n\treflect.TypeOf(pkg.GolangSourceMetadata{}):       nameList(\"GolangSourceMetadata\"),\n\treflect.TypeOf(pkg.JavaMetadata{}):               nameList(\"JavaMetadata\"),\n\treflect.TypeOf(pkg.RpmMetadata{}):                nameList(\"RpmMetadata\"),\n\treflect.TypeOf(pkg.JavaVMInstallationMetadata{}): nameList(\"JavaVMInstallationMetadata\"),\n}\n\n//nolint:unparam\nfunc nameList(id string, others ...string) []string {\n\tnames := []string{id}\n\tfor _, o := range others {\n\t\tnames = append(names, expandLegacyNameVariants(o)...)\n\t}\n\treturn names\n}\n\nfunc expandLegacyNameVariants(name string) []string {\n\tcandidates := []string{name}\n\tif strings.HasSuffix(name, \"MetadataType\") {\n\t\tcandidates = append(candidates, strings.TrimSuffix(name, \"Type\"))\n\t} else if strings.HasSuffix(name, \"Metadata\") {\n\t\tcandidates = append(candidates, name+\"Type\")\n\t}\n\treturn candidates\n}\n\nfunc AllTypeNames() []string {\n\tnames := make([]string, 0)\n\tfor _, t := range AllTypes() {\n\t\tnames = append(names, reflect.TypeOf(t).Name())\n\t}\n\treturn names\n}\n\nfunc JSONName(metadata any) string {\n\tif vs, exists := jsonNameFromType[reflect.TypeOf(metadata)]; exists {\n\t\treturn vs[0]\n\t}\n\treturn \"\"\n}\n\nfunc ReflectTypeFromJSONName(name string) reflect.Type {\n\tname = strings.ToLower(name)\n\tfor _, t := range sortedTypes(jsonNameFromType) {\n\t\tvs := jsonNameFromType[t]\n\t\tfor _, v := range vs {\n\t\t\tif strings.ToLower(v) == name {\n\t\t\t\treturn t\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc sortedTypes(typeNameMapping map[reflect.Type][]string) []reflect.Type {\n\ttypes := make([]reflect.Type, 0)\n\tfor t := range typeNameMapping {\n\t\ttypes = append(types, t)\n\t}\n\n\t// sort the types by their first JSON name\n\tsort.Slice(types, func(i, j int) bool {\n\t\treturn typeNameMapping[types[i]][0] < typeNameMapping[types[j]][0]\n\t})\n\n\treturn types\n}\n"
  },
  {
    "path": "grype/internal/packagemetadata/names_test.go",
    "content": "package packagemetadata\n\nimport (\n\t\"reflect\"\n\t\"testing\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/grype/grype/pkg\"\n)\n\nfunc TestAllNames(t *testing.T) {\n\t// note: this is a form of completion testing relative to the current code base.\n\n\texpected, err := DiscoverTypeNames()\n\trequire.NoError(t, err)\n\n\tactual := AllTypeNames()\n\n\t// ensure that the codebase (from ast analysis) reflects the latest code generated state\n\tif !assert.ElementsMatch(t, expected, actual) {\n\t\tt.Errorf(\"metadata types not fully represented: \\n%s\", cmp.Diff(expected, actual))\n\t\tt.Log(\"did you add a new pkg.*Metadata type without updating the JSON schema?\")\n\t\tt.Log(\"if so, you need to update the schema version and regenerate the JSON schema (make generate-json-schema)\")\n\t}\n\n\tfor _, ty := range AllTypes() {\n\t\tassert.NotEmpty(t, JSONName(ty), \"metadata type %q does not have a JSON name\", ty)\n\t}\n}\n\nfunc TestReflectTypeFromJSONName(t *testing.T) {\n\n\ttests := []struct {\n\t\tname       string\n\t\tlookup     string\n\t\twantRecord reflect.Type\n\t}{\n\t\t{\n\t\t\tname:       \"GolangBinMetadata lookup\",\n\t\t\tlookup:     \"GolangBinMetadata\",\n\t\t\twantRecord: reflect.TypeOf(pkg.GolangBinMetadata{}),\n\t\t},\n\t\t{\n\t\t\tname:       \"GolangModMetadata lookup\",\n\t\t\tlookup:     \"GolangModMetadata\",\n\t\t\twantRecord: reflect.TypeOf(pkg.GolangModMetadata{}),\n\t\t},\n\t\t{\n\t\t\tname:       \"JavaMetadata lookup\",\n\t\t\tlookup:     \"JavaMetadata\",\n\t\t\twantRecord: reflect.TypeOf(pkg.JavaMetadata{}),\n\t\t},\n\t\t{\n\t\t\tname:       \"RpmMetadata lookup\",\n\t\t\tlookup:     \"RpmMetadata\",\n\t\t\twantRecord: reflect.TypeOf(pkg.RpmMetadata{}),\n\t\t},\n\t\t{\n\t\t\tname:       \"JavaVMInstallationMetadata lookup\",\n\t\t\tlookup:     \"JavaVMInstallationMetadata\",\n\t\t\twantRecord: reflect.TypeOf(pkg.JavaVMInstallationMetadata{}),\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot := ReflectTypeFromJSONName(tt.lookup)\n\t\t\tassert.Equal(t, tt.wantRecord.Name(), got.Name())\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/lib.go",
    "content": "package grype\n\nimport (\n\t\"github.com/wagoodman/go-partybus\"\n\n\t\"github.com/anchore/go-logger\"\n\t\"github.com/anchore/grype/internal/bus\"\n\t\"github.com/anchore/grype/internal/log\"\n)\n\nfunc SetLogger(l logger.Logger) {\n\tlog.Set(l)\n}\n\nfunc SetBus(b *partybus.Bus) {\n\tbus.Set(b)\n}\n"
  },
  {
    "path": "grype/load_vulnerability_db.go",
    "content": "package grype\n\nimport (\n\t\"fmt\"\n\n\tv6 \"github.com/anchore/grype/grype/db/v6\"\n\tv6dist \"github.com/anchore/grype/grype/db/v6/distribution\"\n\tv6inst \"github.com/anchore/grype/grype/db/v6/installation\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n\t\"github.com/anchore/grype/internal/log\"\n)\n\nfunc LoadVulnerabilityDB(distCfg v6dist.Config, installCfg v6inst.Config, update bool) (vulnerability.Provider, *vulnerability.ProviderStatus, error) {\n\tclient, err := v6dist.NewClient(distCfg)\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"unable to create distribution client: %w\", err)\n\t}\n\tc, err := v6inst.NewCurator(installCfg, client)\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"unable to create curator: %w\", err)\n\t}\n\n\tif update {\n\t\tupdated, err := c.Update()\n\t\tif err != nil {\n\t\t\tif distCfg.RequireUpdateCheck {\n\t\t\t\treturn nil, nil, fmt.Errorf(\"unable to update db: %w\", err)\n\t\t\t}\n\t\t\tlog.WithFields(\"error\", err).Warn(\"error updating db\")\n\t\t}\n\t\tif !updated {\n\t\t\tlog.Debug(\"no db update found\")\n\t\t}\n\t} else {\n\t\tlog.Debug(\"skipping db update\")\n\t}\n\n\ts := c.Status()\n\tif s.Error != nil {\n\t\treturn nil, nil, s.Error\n\t}\n\n\trdr, err := c.Reader()\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"unable to create db reader: %w\", err)\n\t}\n\n\treturn v6.NewVulnerabilityProvider(rdr), &s, nil\n}\n"
  },
  {
    "path": "grype/load_vulnerability_db_bench_test.go",
    "content": "package grype\n\nimport (\n\t\"math\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/anchore/grype/grype/db/v6/distribution\"\n\t\"github.com/anchore/grype/grype/db/v6/installation\"\n)\n\n// this benchmark was added to measure the performance\n// of LoadVulnerabilityDB, specifically regarding hash validation.\n// https://github.com/anchore/grype/issues/1502\nfunc BenchmarkLoadVulnerabilityDB(b *testing.B) {\n\tfor range b.N {\n\t\t_, _, err := LoadVulnerabilityDB(distribution.Config{\n\t\t\tLatestURL: distribution.DefaultConfig().LatestURL,\n\t\t}, installation.Config{\n\t\t\tDBRootDir:               filepath.Join(\".tmp\", \"grype-db\"),\n\t\t\tValidateAge:             false,\n\t\t\tValidateChecksum:        true,\n\t\t\tMaxAllowedBuiltAge:      math.MaxInt32,\n\t\t\tUpdateCheckMaxFrequency: math.MaxInt32,\n\t\t}, true)\n\t\tif err != nil {\n\t\t\tb.Fatal(err)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "grype/match/details.go",
    "content": "package match\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/gohugoio/hashstructure\"\n)\n\ntype Details []Detail\n\ntype Detail struct {\n\tType       Type        // The kind of match made (an exact match, fuzzy match, indirect vs direct, etc).\n\tSearchedBy interface{} // The specific attributes that were used to search (other than package name and version) --this indicates \"how\" the match was made.\n\tFound      interface{} // The specific attributes on the vulnerability object that were matched with --this indicates \"what\" was matched on / within.\n\tMatcher    MatcherType // The matcher object that discovered the match.\n\tConfidence float64     // The certainty of the match as a ratio (currently unused, reserved for future use).\n}\n\n// String is the string representation of select match fields.\nfunc (m Detail) String() string {\n\treturn fmt.Sprintf(\"Detail(searchedBy=%q found=%q matcher=%q)\", m.SearchedBy, m.Found, m.Matcher)\n}\n\nfunc (m Details) Matchers() (tys []MatcherType) {\n\tif len(m) == 0 {\n\t\treturn nil\n\t}\n\tfor _, d := range m {\n\t\ttys = append(tys, d.Matcher)\n\t}\n\treturn tys\n}\n\nfunc (m Details) Types() (tys []Type) {\n\tif len(m) == 0 {\n\t\treturn nil\n\t}\n\tfor _, d := range m {\n\t\ttys = append(tys, d.Type)\n\t}\n\treturn tys\n}\n\nfunc (m Detail) ID() string {\n\tf, err := hashstructure.Hash(&m, &hashstructure.HashOptions{\n\t\tZeroNil:      true,\n\t\tSlicesAsSets: true,\n\t})\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\n\treturn fmt.Sprintf(\"%x\", f)\n}\n\nfunc (m Details) Len() int {\n\treturn len(m)\n}\n\nfunc (m Details) Less(i, j int) bool {\n\ta := m[i]\n\tb := m[j]\n\n\tif a.Type != b.Type {\n\t\t// exact-direct-match < exact-indirect-match < cpe-match\n\n\t\tat := typeOrder[a.Type]\n\t\tbt := typeOrder[b.Type]\n\t\tif at == 0 {\n\t\t\treturn false\n\t\t} else if bt == 0 {\n\t\t\treturn true\n\t\t}\n\t\treturn at < bt\n\t}\n\n\t// sort by confidence\n\tif a.Confidence != b.Confidence {\n\t\t// flipped comparison since we want higher confidence to be first\n\t\treturn a.Confidence > b.Confidence\n\t}\n\n\t// if the types are the same, then sort by the ID (costly, but deterministic)\n\treturn strings.Compare(a.ID(), b.ID()) < 0\n}\n\nfunc (m Details) Swap(i, j int) {\n\tm[i], m[j] = m[j], m[i]\n}\n"
  },
  {
    "path": "grype/match/details_test.go",
    "content": "package match\n\nimport (\n\t\"sort\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestDetails_Sorting(t *testing.T) {\n\n\tdetailExactDirectHigh := Detail{\n\t\tType:       ExactDirectMatch,\n\t\tConfidence: 0.9,\n\t\tSearchedBy: \"attribute1\",\n\t\tFound:      \"value1\",\n\t\tMatcher:    \"matcher1\",\n\t}\n\tdetailExactDirectLow := Detail{\n\t\tType:       ExactDirectMatch,\n\t\tConfidence: 0.5,\n\t\tSearchedBy: \"attribute1\",\n\t\tFound:      \"value1\",\n\t\tMatcher:    \"matcher1\",\n\t}\n\tdetailExactIndirect := Detail{\n\t\tType:       ExactIndirectMatch,\n\t\tConfidence: 0.7,\n\t\tSearchedBy: \"attribute2\",\n\t\tFound:      \"value2\",\n\t\tMatcher:    \"matcher2\",\n\t}\n\tdetailCPEMatch := Detail{\n\t\tType:       CPEMatch,\n\t\tConfidence: 0.8,\n\t\tSearchedBy: \"attribute3\",\n\t\tFound:      \"value3\",\n\t\tMatcher:    \"matcher3\",\n\t}\n\n\ttests := []struct {\n\t\tname     string\n\t\tdetails  Details\n\t\texpected Details\n\t}{\n\t\t{\n\t\t\tname: \"sorts by type first, then by confidence\",\n\t\t\tdetails: Details{\n\t\t\t\tdetailCPEMatch,\n\t\t\t\tdetailExactDirectHigh,\n\t\t\t\tdetailExactIndirect,\n\t\t\t\tdetailExactDirectLow,\n\t\t\t},\n\t\t\texpected: Details{\n\t\t\t\tdetailExactDirectHigh,\n\t\t\t\tdetailExactDirectLow,\n\t\t\t\tdetailExactIndirect,\n\t\t\t\tdetailCPEMatch,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"sorts by confidence within the same type\",\n\t\t\tdetails: Details{\n\t\t\t\tdetailExactDirectLow,\n\t\t\t\tdetailExactDirectHigh,\n\t\t\t},\n\t\t\texpected: Details{\n\t\t\t\tdetailExactDirectHigh,\n\t\t\t\tdetailExactDirectLow,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"sorts by ID when type and confidence are the same\",\n\t\t\tdetails: Details{\n\t\t\t\t// clone of detailExactDirectLow with slight difference to enforce ID sorting\n\t\t\t\t{\n\t\t\t\t\tType:       ExactDirectMatch,\n\t\t\t\t\tConfidence: 0.5,\n\t\t\t\t\tSearchedBy: \"attribute2\",\n\t\t\t\t\tFound:      \"value2\",\n\t\t\t\t\tMatcher:    \"matcher2\",\n\t\t\t\t},\n\t\t\t\tdetailExactDirectLow,\n\t\t\t},\n\t\t\texpected: Details{\n\t\t\t\tdetailExactDirectLow,\n\t\t\t\t{\n\t\t\t\t\tType:       ExactDirectMatch,\n\t\t\t\t\tConfidence: 0.5,\n\t\t\t\t\tSearchedBy: \"attribute2\",\n\t\t\t\t\tFound:      \"value2\",\n\t\t\t\t\tMatcher:    \"matcher2\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tsort.Sort(tt.details)\n\t\t\trequire.Equal(t, tt.expected, tt.details)\n\t\t})\n\t}\n}\n\nfunc TestHasExclusivelyAnyMatchTypes(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tdetails  Details\n\t\ttypes    []Type\n\t\texpected bool\n\t}{\n\t\t{\n\t\t\tname:     \"all types allowed\",\n\t\t\tdetails:  Details{{Type: \"A\"}, {Type: \"B\"}},\n\t\t\ttypes:    []Type{\"A\", \"B\"},\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"mixed types with disallowed\",\n\t\t\tdetails:  Details{{Type: \"A\"}, {Type: \"B\"}, {Type: \"C\"}},\n\t\t\ttypes:    []Type{\"A\", \"B\"},\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"single allowed type\",\n\t\t\tdetails:  Details{{Type: \"A\"}},\n\t\t\ttypes:    []Type{\"A\"},\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"empty details\",\n\t\t\tdetails:  Details{},\n\t\t\ttypes:    []Type{\"A\"},\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"empty types list\",\n\t\t\tdetails:  Details{{Type: \"A\"}},\n\t\t\ttypes:    []Type{},\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"no match with disallowed type\",\n\t\t\tdetails:  Details{{Type: \"C\"}},\n\t\t\ttypes:    []Type{\"A\", \"B\"},\n\t\t\texpected: false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := hasExclusivelyAnyMatchTypes(tt.details, tt.types...)\n\t\t\tassert.Equal(t, tt.expected, result)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/match/explicit_ignores.go",
    "content": "package match\n\nimport (\n\t\"github.com/anchore/grype/internal/log\"\n)\n\nvar explicitIgnoreRules []IgnoreRule\n\nfunc init() {\n\ttype ignoreValues struct {\n\t\ttyp             string\n\t\tvulnerabilities []string\n\t\tpackages        []string\n\t}\n\n\tvar explicitIgnores = []ignoreValues{\n\t\t// Based on https://github.com/anchore/grype/issues/552, which includes a reference to the\n\t\t// https://github.com/mergebase/log4j-samples collection, we want to filter these explicitly:\n\t\t{\n\t\t\ttyp:             \"java-archive\",\n\t\t\tvulnerabilities: []string{\"CVE-2021-44228\", \"CVE-2021-45046\", \"GHSA-jfh8-c2jp-5v3q\", \"GHSA-7rjr-3q55-vv33\", \"CVE-2020-9493\", \"CVE-2022-23307\", \"CVE-2023-26464\"},\n\t\t\tpackages:        []string{\"log4j-api\", \"log4j-slf4j-impl\", \"log4j-to-slf4j\", \"log4j-1.2-api\", \"log4j-detector\", \"log4j-over-slf4j\", \"slf4j-log4j12\"},\n\t\t},\n\t\t// Based on https://github.com/anchore/grype/issues/558:\n\t\t{\n\t\t\ttyp:             \"go-module\",\n\t\t\tvulnerabilities: []string{\"CVE-2015-5237\", \"CVE-2021-22570\"},\n\t\t\tpackages:        []string{\"google.golang.org/protobuf\"},\n\t\t},\n\t\t// Affects Squiz Matrix, not in any way related to the matrix ruby gem\n\t\t{\n\t\t\ttyp:             \"gem\",\n\t\t\tvulnerabilities: []string{\"CVE-2017-14196\", \"CVE-2017-14197\", \"CVE-2017-14198\", \"CVE-2019-19373\", \"CVE-2019-19374\"},\n\t\t\tpackages:        []string{\"matrix\"},\n\t\t},\n\t\t// Affects the DeleGate proxy server, not in any way related to the delegate ruby gem\n\t\t{\n\t\t\ttyp:             \"gem\",\n\t\t\tvulnerabilities: []string{\"CVE-1999-1338\", \"CVE-2001-1202\", \"CVE-2002-1781\", \"CVE-2004-0789\", \"CVE-2004-2003\", \"CVE-2005-0036\", \"CVE-2005-0861\", \"CVE-2006-2072\", \"CVE-2015-7556\"},\n\t\t\tpackages:        []string{\"delegate\"},\n\t\t},\n\t\t// Affects the Observer autodiscovery PHP/MySQL/SNMP/CDP based network management system, not in any way related to the observer ruby gem\n\t\t{\n\t\t\ttyp:             \"gem\",\n\t\t\tvulnerabilities: []string{\"CVE-2008-4318\"},\n\t\t\tpackages:        []string{\"observer\"},\n\t\t},\n\t\t// Affects the WeeChat logger plugin, not in any way related to the logger ruby gem\n\t\t{\n\t\t\ttyp:             \"gem\",\n\t\t\tvulnerabilities: []string{\"CVE-2017-14727\"},\n\t\t\tpackages:        []string{\"logger\"},\n\t\t},\n\t\t// https://github.com/anchore/grype/issues/2412#issuecomment-2663656195\n\t\t{\n\t\t\ttyp:             \"deb\",\n\t\t\tvulnerabilities: []string{\"CVE-2023-45853\"},\n\t\t\tpackages:        []string{\"zlib1g\", \"zlib\"},\n\t\t},\n\t}\n\n\tfor _, ignore := range explicitIgnores {\n\t\tfor _, vulnerability := range ignore.vulnerabilities {\n\t\t\tfor _, packageName := range ignore.packages {\n\t\t\t\texplicitIgnoreRules = append(explicitIgnoreRules, IgnoreRule{\n\t\t\t\t\tVulnerability: vulnerability,\n\t\t\t\t\tPackage: IgnoreRulePackage{\n\t\t\t\t\t\tName: packageName,\n\t\t\t\t\t\tType: ignore.typ,\n\t\t\t\t\t},\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\t}\n}\n\n// ApplyExplicitIgnoreRules Filters out matches meeting the criteria defined above and those within the grype database\nfunc ApplyExplicitIgnoreRules(provider ExclusionProvider, matches Matches) (Matches, []IgnoredMatch) {\n\tvar ignoreRules []IgnoreRule\n\tignoreRules = append(ignoreRules, explicitIgnoreRules...)\n\n\tif provider != nil {\n\t\tfor _, m := range matches.Sorted() {\n\t\t\tr, err := provider.IgnoreRules(m.Vulnerability.ID)\n\n\t\t\tif err != nil {\n\t\t\t\tlog.Warnf(\"unable to get ignore rules for vuln id=%s\", m.Vulnerability.ID)\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tignoreRules = append(ignoreRules, r...)\n\t\t}\n\t}\n\n\treturn ApplyIgnoreRules(matches, ignoreRules)\n}\n"
  },
  {
    "path": "grype/match/explicit_ignores_test.go",
    "content": "package match\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\n\t\"github.com/anchore/grype/grype/pkg\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n\tsyftPkg \"github.com/anchore/syft/syft/pkg\"\n)\n\ntype mockExclusionProvider struct {\n\tdata map[string][]IgnoreRule\n}\n\nfunc newMockExclusionProvider() *mockExclusionProvider {\n\td := mockExclusionProvider{\n\t\tdata: make(map[string][]IgnoreRule),\n\t}\n\td.stub()\n\treturn &d\n}\n\nfunc (d *mockExclusionProvider) stub() {\n}\n\nfunc (d *mockExclusionProvider) IgnoreRules(vulnerabilityID string) ([]IgnoreRule, error) {\n\treturn d.data[vulnerabilityID], nil\n}\n\nfunc Test_ApplyExplicitIgnoreRules(t *testing.T) {\n\ttype cvePkg struct {\n\t\tcve string\n\t\tpkg string\n\t}\n\ttests := []struct {\n\t\tname     string\n\t\ttyp      syftPkg.Type\n\t\tmatches  []cvePkg\n\t\texpected []string\n\t\tignored  []string\n\t}{\n\t\t// some explicit log4j-related data:\n\t\t// \"CVE-2021-44228\", \"CVE-2021-45046\", \"GHSA-jfh8-c2jp-5v3q\", \"GHSA-7rjr-3q55-vv33\",\n\t\t// \"log4j-api\", \"log4j-slf4j-impl\", \"log4j-to-slf4j\", \"log4j-1.2-api\",\n\t\t{\n\t\t\tname: \"keeps non-matching packages\",\n\t\t\ttyp:  \"java-archive\",\n\t\t\tmatches: []cvePkg{\n\t\t\t\t{\"CVE-2021-44228\", \"log4j-core\"},\n\t\t\t\t{\"CVE-2021-43452\", \"foo-tool\"},\n\t\t\t},\n\t\t\texpected: []string{\"log4j-core\", \"foo-tool\"},\n\t\t},\n\t\t{\n\t\t\tname: \"keeps non-matching CVEs\",\n\t\t\ttyp:  \"java-archive\",\n\t\t\tmatches: []cvePkg{\n\t\t\t\t{\"CVE-2021-428\", \"log4j-api\"},\n\t\t\t\t{\"CVE-2021-43452\", \"foo-tool\"},\n\t\t\t},\n\t\t\texpected: []string{\"log4j-api\", \"foo-tool\"},\n\t\t},\n\t\t{\n\t\t\tname: \"filters only matching CVE and package\",\n\t\t\ttyp:  \"java-archive\",\n\t\t\tmatches: []cvePkg{\n\t\t\t\t{\"CVE-2021-44228\", \"log4j-api\"},\n\t\t\t\t{\"CVE-2021-44228\", \"log4j-core\"},\n\t\t\t},\n\t\t\texpected: []string{\"log4j-core\"},\n\t\t\tignored:  []string{\"log4j-api\"},\n\t\t},\n\t\t{\n\t\t\tname: \"filters all matching CVEs and packages\",\n\t\t\ttyp:  \"java-archive\",\n\t\t\tmatches: []cvePkg{\n\t\t\t\t{\"GHSA-jfh8-c2jp-5v3q\", \"log4j-api\"},\n\t\t\t\t{\"GHSA-jfh8-c2jp-5v3q\", \"log4j-slf4j-impl\"},\n\t\t\t},\n\t\t\texpected: []string{},\n\t\t\tignored:  []string{\"log4j-api\", \"log4j-slf4j-impl\"},\n\t\t},\n\t\t{\n\t\t\tname: \"filters invalid CVEs for protobuf Go module\",\n\t\t\ttyp:  \"go-module\",\n\t\t\tmatches: []cvePkg{\n\t\t\t\t{\"CVE-2015-5237\", \"google.golang.org/protobuf\"},\n\t\t\t\t{\"CVE-2021-22570\", \"google.golang.org/protobuf\"},\n\t\t\t},\n\t\t\texpected: []string{},\n\t\t\tignored:  []string{\"google.golang.org/protobuf\", \"google.golang.org/protobuf\"},\n\t\t},\n\t\t{\n\t\t\tname: \"keeps valid CVEs for protobuf Go module\",\n\t\t\ttyp:  \"go-module\",\n\t\t\tmatches: []cvePkg{\n\t\t\t\t{\"CVE-1998-99999\", \"google.golang.org/protobuf\"},\n\t\t\t},\n\t\t\texpected: []string{\"google.golang.org/protobuf\"},\n\t\t},\n\t}\n\n\tp := newMockExclusionProvider()\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tmatches := NewMatches()\n\n\t\t\tfor _, cp := range test.matches {\n\t\t\t\tmatches.Add(Match{\n\n\t\t\t\t\tPackage: pkg.Package{\n\t\t\t\t\t\tID:   pkg.ID(cp.pkg),\n\t\t\t\t\t\tName: cp.pkg,\n\t\t\t\t\t\tType: test.typ,\n\t\t\t\t\t},\n\t\t\t\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\t\t\t\tReference: vulnerability.Reference{ID: cp.cve},\n\t\t\t\t\t},\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tfiltered, ignores := ApplyExplicitIgnoreRules(p, matches)\n\n\t\t\tvar found []string\n\t\t\tfor match := range filtered.Enumerate() {\n\t\t\t\tfound = append(found, match.Package.Name)\n\n\t\t\t}\n\t\t\tassert.ElementsMatch(t, test.expected, found)\n\n\t\t\tif len(test.ignored) > 0 {\n\t\t\t\tvar ignored []string\n\t\t\t\tfor _, i := range ignores {\n\t\t\t\t\tignored = append(ignored, i.Package.Name)\n\t\t\t\t}\n\t\t\t\tassert.ElementsMatch(t, test.ignored, ignored)\n\t\t\t} else {\n\t\t\t\tassert.Empty(t, ignores)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/match/fingerprint.go",
    "content": "package match\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/gohugoio/hashstructure\"\n\n\t\"github.com/anchore/grype/grype/pkg\"\n)\n\ntype Fingerprint struct {\n\tcoreFingerprint\n\tvulnerabilityFixes string\n}\n\ntype coreFingerprint struct {\n\tvulnerabilityID        string\n\tvulnerabilityNamespace string\n\tpackageID              pkg.ID // note: this encodes package name, version, type, location\n}\n\nfunc (m Fingerprint) String() string {\n\treturn fmt.Sprintf(\"Fingerprint(vuln=%q namespace=%q fixes=%q package=%q)\", m.vulnerabilityID, m.vulnerabilityNamespace, m.vulnerabilityFixes, m.packageID)\n}\n\nfunc (m Fingerprint) ID() string {\n\tf, err := hashstructure.Hash(&m, &hashstructure.HashOptions{\n\t\tZeroNil:      true,\n\t\tSlicesAsSets: true,\n\t})\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\n\treturn fmt.Sprintf(\"%x\", f)\n}\n"
  },
  {
    "path": "grype/match/ignore.go",
    "content": "package match\n\nimport (\n\t\"regexp\"\n\n\t\"github.com/bmatcuk/doublestar/v2\"\n\n\t\"github.com/anchore/grype/grype/vulnerability\"\n\t\"github.com/anchore/grype/internal/log\"\n)\n\n// IgnoreFilter implementations are used to filter matches, returning all applicable IgnoreRule(s) that applied,\n// these could include an IgnoreRule with only a Reason value filled in for synthetically generated rules\ntype IgnoreFilter interface {\n\tIgnoreMatch(match Match) []IgnoreRule\n}\n\n// An IgnoredMatch is a vulnerability Match that has been ignored because one or more IgnoreRules applied to the match.\ntype IgnoredMatch struct {\n\tMatch\n\n\t// AppliedIgnoreRules are the rules that were applied to the match that caused Grype to ignore it.\n\tAppliedIgnoreRules []IgnoreRule\n}\n\n// An IgnoreRule specifies criteria for a vulnerability match to meet in order\n// to be ignored. Not all criteria (fields) need to be specified, but all\n// specified criteria must be met by the vulnerability match in order for the\n// rule to apply.\ntype IgnoreRule struct {\n\tVulnerability    string            `yaml:\"vulnerability\" json:\"vulnerability\" mapstructure:\"vulnerability\"`\n\tIncludeAliases   bool              `yaml:\"include-aliases\" json:\"include-aliases\" mapstructure:\"include-aliases\"`\n\tReason           string            `yaml:\"reason\" json:\"reason\" mapstructure:\"reason\"`\n\tNamespace        string            `yaml:\"namespace\" json:\"namespace\" mapstructure:\"namespace\"`\n\tFixState         string            `yaml:\"fix-state\" json:\"fix-state\" mapstructure:\"fix-state\"`\n\tPackage          IgnoreRulePackage `yaml:\"package\" json:\"package\" mapstructure:\"package\"`\n\tVexStatus        string            `yaml:\"vex-status\" json:\"vex-status\" mapstructure:\"vex-status\"`\n\tVexJustification string            `yaml:\"vex-justification\" json:\"vex-justification\" mapstructure:\"vex-justification\"`\n\tMatchType        Type              `yaml:\"match-type\" json:\"match-type\" mapstructure:\"match-type\"`\n}\n\n// IgnoreRulePackage describes the Package-specific fields that comprise the IgnoreRule.\ntype IgnoreRulePackage struct {\n\tName         string `yaml:\"name\" json:\"name\" mapstructure:\"name\"`\n\tVersion      string `yaml:\"version\" json:\"version\" mapstructure:\"version\"`\n\tLanguage     string `yaml:\"language\" json:\"language\" mapstructure:\"language\"`\n\tType         string `yaml:\"type\" json:\"type\" mapstructure:\"type\"`\n\tLocation     string `yaml:\"location\" json:\"location\" mapstructure:\"location\"`\n\tUpstreamName string `yaml:\"upstream-name\" json:\"upstream-name\" mapstructure:\"upstream-name\"`\n}\n\n// ApplyIgnoreRules iterates through the provided matches and, for each match,\n// determines if the match should be ignored, by evaluating if any of the\n// provided IgnoreRules apply to the match. If any rules apply to the match, all\n// applicable rules are attached to the Match to form an IgnoredMatch.\n// ApplyIgnoreRules returns two collections: the matches that are not being\n// ignored, and the matches that are being ignored.\nfunc ApplyIgnoreRules(matches Matches, rules []IgnoreRule) (Matches, []IgnoredMatch) {\n\tmatched, ignored := ApplyIgnoreFilters(matches.Sorted(), rules...)\n\treturn NewMatches(matched...), ignored\n}\n\n// ApplyIgnoreFilters applies all the IgnoreFilter(s) to the provided set of matches,\n// splitting the results into a set of matched matches and ignored matches\nfunc ApplyIgnoreFilters[T IgnoreFilter](matches []Match, filters ...T) ([]Match, []IgnoredMatch) {\n\tvar out []Match\n\tvar ignoredMatches []IgnoredMatch\n\n\tfor _, match := range matches {\n\t\tvar applicableRules []IgnoreRule\n\n\t\tfor _, filter := range filters {\n\t\t\tapplicableRules = append(applicableRules, filter.IgnoreMatch(match)...)\n\t\t}\n\n\t\tif len(applicableRules) > 0 {\n\t\t\tignoredMatches = append(ignoredMatches, IgnoredMatch{\n\t\t\t\tMatch:              match,\n\t\t\t\tAppliedIgnoreRules: applicableRules,\n\t\t\t})\n\n\t\t\tcontinue\n\t\t}\n\n\t\tout = append(out, match)\n\t}\n\n\treturn out, ignoredMatches\n}\n\nfunc (r IgnoreRule) IgnoreMatch(match Match) []IgnoreRule {\n\t// VEX rules are handled by the vex processor\n\tif r.VexStatus != \"\" {\n\t\treturn nil\n\t}\n\n\tignoreConditions := getIgnoreConditionsForRule(r)\n\tif len(ignoreConditions) == 0 {\n\t\t// this rule specifies no criteria, so it doesn't apply to the Match\n\t\treturn nil\n\t}\n\n\tfor _, condition := range ignoreConditions {\n\t\tif !condition(match) {\n\t\t\t// as soon as one rule criterion doesn't apply, we know this rule doesn't apply to the Match\n\t\t\treturn nil\n\t\t}\n\t}\n\n\t// all criteria specified in the rule apply to this Match\n\treturn []IgnoreRule{r}\n}\n\n// HasConditions returns true if the ignore rule has conditions\n// that can cause a match to be ignored\nfunc (r IgnoreRule) HasConditions() bool {\n\treturn len(getIgnoreConditionsForRule(r)) == 0\n}\n\n// ignoreFilters implements match.IgnoreFilter on a slice of objects that implement the same interface\ntype ignoreFilters[T IgnoreFilter] []T\n\nfunc (r ignoreFilters[T]) IgnoreMatch(match Match) []IgnoreRule {\n\tfor _, rule := range r {\n\t\tignores := rule.IgnoreMatch(match)\n\t\tif len(ignores) > 0 {\n\t\t\treturn ignores\n\t\t}\n\t}\n\treturn nil\n}\n\nvar _ IgnoreFilter = (*ignoreFilters[IgnoreRule])(nil)\n\n// An ignoreCondition is a function that returns a boolean indicating whether\n// the given Match should be ignored.\ntype ignoreCondition func(match Match) bool\n\nfunc getIgnoreConditionsForRule(rule IgnoreRule) []ignoreCondition {\n\tvar ignoreConditions []ignoreCondition\n\n\tif v := rule.Vulnerability; v != \"\" {\n\t\tignoreConditions = append(ignoreConditions, ifVulnerabilityApplies(v, rule.IncludeAliases))\n\t}\n\n\tif ns := rule.Namespace; ns != \"\" {\n\t\tignoreConditions = append(ignoreConditions, ifNamespaceApplies(ns))\n\t}\n\n\tif n := rule.Package.Name; n != \"\" {\n\t\tignoreConditions = append(ignoreConditions, ifPackageNameApplies(n))\n\t}\n\n\tif v := rule.Package.Version; v != \"\" {\n\t\tignoreConditions = append(ignoreConditions, ifPackageVersionApplies(v))\n\t}\n\n\tif l := rule.Package.Language; l != \"\" {\n\t\tignoreConditions = append(ignoreConditions, ifPackageLanguageApplies(l))\n\t}\n\n\tif t := rule.Package.Type; t != \"\" {\n\t\tignoreConditions = append(ignoreConditions, ifPackageTypeApplies(t))\n\t}\n\n\tif l := rule.Package.Location; l != \"\" {\n\t\tignoreConditions = append(ignoreConditions, ifPackageLocationApplies(l))\n\t}\n\n\tif fs := rule.FixState; fs != \"\" {\n\t\tignoreConditions = append(ignoreConditions, ifFixStateApplies(fs))\n\t}\n\n\tif upstreamName := rule.Package.UpstreamName; upstreamName != \"\" {\n\t\tignoreConditions = append(ignoreConditions, ifUpstreamPackageNameApplies(upstreamName))\n\t}\n\n\tif matchType := rule.MatchType; matchType != \"\" {\n\t\tignoreConditions = append(ignoreConditions, ifMatchTypeApplies(matchType))\n\t}\n\treturn ignoreConditions\n}\n\nfunc ifFixStateApplies(fs string) ignoreCondition {\n\treturn func(match Match) bool {\n\t\tif fs == string(vulnerability.FixStateUnknown) &&\n\t\t\tmatch.Vulnerability.Fix.State == \"\" { // no fix state specified is effectively \"unknown\"\n\t\t\treturn true\n\t\t}\n\t\treturn fs == string(match.Vulnerability.Fix.State)\n\t}\n}\n\nfunc ifVulnerabilityApplies(vulnerability string, includeAliases bool) ignoreCondition {\n\treturn func(match Match) bool {\n\t\tif vulnerability == match.Vulnerability.ID {\n\t\t\treturn true\n\t\t}\n\t\tif includeAliases {\n\t\t\tfor _, related := range match.Vulnerability.RelatedVulnerabilities {\n\t\t\t\tif vulnerability == related.ID {\n\t\t\t\t\treturn true\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn false\n\t}\n}\n\nfunc ifNamespaceApplies(namespace string) ignoreCondition {\n\treturn func(match Match) bool {\n\t\treturn namespace == match.Vulnerability.Namespace\n\t}\n}\n\nfunc packageNameRegex(packageName string) (*regexp.Regexp, error) {\n\tpattern := packageName\n\tif packageName[0] != '$' || packageName[len(packageName)-1] != '^' {\n\t\tpattern = \"^\" + packageName + \"$\"\n\t}\n\treturn regexp.Compile(pattern)\n}\n\nfunc ifPackageNameApplies(name string) ignoreCondition {\n\t// with enough ignore rules, we could end up needlessly creating a lot of regexes, which is not ideal.\n\t// instead lets detect if the input string is a regex or not, and if it is, then compile it...\n\t// otherwise, we can just do a simple string comparison\n\tif isLikelyARegex(name) {\n\t\tpattern, err := packageNameRegex(name)\n\t\tif err != nil || pattern == nil {\n\t\t\treturn func(Match) bool { return false }\n\t\t}\n\n\t\treturn func(match Match) bool {\n\t\t\treturn pattern.MatchString(match.Package.Name)\n\t\t}\n\t}\n\treturn func(match Match) bool {\n\t\treturn name == match.Package.Name\n\t}\n}\n\nfunc ifPackageVersionApplies(version string) ignoreCondition {\n\t// TODO I think we will might need to add the metadata compare logic here\n\treturn func(match Match) bool {\n\t\treturn version == match.Package.Version\n\t}\n}\n\nfunc ifPackageLanguageApplies(language string) ignoreCondition {\n\treturn func(match Match) bool {\n\t\treturn language == string(match.Package.Language)\n\t}\n}\n\nfunc ifPackageTypeApplies(t string) ignoreCondition {\n\treturn func(match Match) bool {\n\t\treturn t == string(match.Package.Type)\n\t}\n}\n\nfunc ifPackageLocationApplies(location string) ignoreCondition {\n\treturn func(match Match) bool {\n\t\treturn ruleLocationAppliesToMatch(location, match)\n\t}\n}\n\nfunc ifUpstreamPackageNameApplies(name string) ignoreCondition {\n\t// with enough ignore rules, we could end up needlessly creating a lot of regexes, which is not ideal.\n\t// instead lets detect if the input string is a regex or not, and if it is, then compile it...\n\t// otherwise, we can just do a simple string comparison\n\tif isLikelyARegex(name) {\n\t\tpattern, err := packageNameRegex(name)\n\t\tif err != nil {\n\t\t\tlog.WithFields(\"name\", name, \"error\", err).Debug(\"unable to parse name expression\")\n\t\t\treturn func(Match) bool { return false }\n\t\t}\n\t\treturn func(match Match) bool {\n\t\t\tfor _, upstream := range match.Package.Upstreams {\n\t\t\t\tif pattern.MatchString(upstream.Name) {\n\t\t\t\t\treturn true\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn false\n\t\t}\n\t}\n\treturn func(match Match) bool {\n\t\tfor _, upstream := range match.Package.Upstreams {\n\t\t\tif name == upstream.Name {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t\treturn false\n\t}\n}\n\n// isRegexPattern is a compiled regex that matches common regex characters. We intentionally leave out\n// the '.' character, as it is a common character in package names and versions, and we do not want to\n// treat it as a regex unless there is other evidence that it is a regex.\nvar isRegexPattern = regexp.MustCompile(`[\\^\\$\\*\\+\\?\\[\\]\\(\\)\\{\\}\\|\\\\]|\\\\[dDwWsSnrtfv]`)\n\nfunc isLikelyARegex(s string) bool {\n\treturn isRegexPattern.MatchString(s)\n}\n\nfunc ifMatchTypeApplies(matchType Type) ignoreCondition {\n\treturn func(match Match) bool {\n\t\tfor _, mType := range match.Details.Types() {\n\t\t\tif mType == matchType {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t\treturn false\n\t}\n}\n\nfunc ruleLocationAppliesToMatch(location string, match Match) bool {\n\tfor _, packageLocation := range match.Package.Locations.ToSlice() {\n\t\tif ruleLocationAppliesToPath(location, packageLocation.RealPath) {\n\t\t\treturn true\n\t\t}\n\n\t\tif ruleLocationAppliesToPath(location, packageLocation.AccessPath) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\nfunc ruleLocationAppliesToPath(location, path string) bool {\n\tdoesMatch, err := doublestar.Match(location, path)\n\tif err != nil {\n\t\treturn false\n\t}\n\n\treturn doesMatch\n}\n"
  },
  {
    "path": "grype/match/ignore_test.go",
    "content": "package match\n\nimport (\n\t\"testing\"\n\n\t\"github.com/google/uuid\"\n\t\"github.com/stretchr/testify/assert\"\n\n\t\"github.com/anchore/grype/grype/pkg\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n\t\"github.com/anchore/syft/syft/file\"\n\tsyftPkg \"github.com/anchore/syft/syft/pkg\"\n)\n\nvar (\n\tallMatches = []Match{\n\t\t{\n\t\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\tID:        \"CVE-123\",\n\t\t\t\t\tNamespace: \"debian-vulns\",\n\t\t\t\t},\n\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\tState: vulnerability.FixStateFixed,\n\t\t\t\t},\n\t\t\t},\n\t\t\tPackage: pkg.Package{\n\t\t\t\tID:        pkg.ID(uuid.NewString()),\n\t\t\t\tName:      \"dive\",\n\t\t\t\tVersion:   \"0.5.2\",\n\t\t\t\tType:      \"deb\",\n\t\t\t\tLocations: file.NewLocationSet(file.NewLocation(\"/path/that/has/dive\")),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\tID:        \"CVE-456\",\n\t\t\t\t\tNamespace: \"ruby-vulns\",\n\t\t\t\t},\n\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\tState: vulnerability.FixStateNotFixed,\n\t\t\t\t},\n\t\t\t\tRelatedVulnerabilities: []vulnerability.Reference{\n\t\t\t\t\t{\n\t\t\t\t\t\tID: \"CVE-123\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tPackage: pkg.Package{\n\t\t\t\tID:       pkg.ID(uuid.NewString()),\n\t\t\t\tName:     \"reach\",\n\t\t\t\tVersion:  \"100.0.50\",\n\t\t\t\tLanguage: syftPkg.Ruby,\n\t\t\t\tType:     syftPkg.GemPkg,\n\t\t\t\tLocations: file.NewLocationSet(file.NewVirtualLocation(\"/real/path/with/reach\",\n\t\t\t\t\t\"/virtual/path/that/has/reach\")),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\tID:        \"CVE-457\",\n\t\t\t\t\tNamespace: \"ruby-vulns\",\n\t\t\t\t},\n\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\tState: vulnerability.FixStateWontFix,\n\t\t\t\t},\n\t\t\t},\n\t\t\tPackage: pkg.Package{\n\t\t\t\tID:       pkg.ID(uuid.NewString()),\n\t\t\t\tName:     \"beach\",\n\t\t\t\tVersion:  \"100.0.51\",\n\t\t\t\tLanguage: syftPkg.Ruby,\n\t\t\t\tType:     syftPkg.GemPkg,\n\t\t\t\tLocations: file.NewLocationSet(file.NewVirtualLocation(\"/real/path/with/beach\",\n\t\t\t\t\t\"/virtual/path/that/has/beach\")),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\tID:        \"CVE-458\",\n\t\t\t\t\tNamespace: \"ruby-vulns\",\n\t\t\t\t},\n\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\tState: vulnerability.FixStateUnknown,\n\t\t\t\t},\n\t\t\t},\n\t\t\tPackage: pkg.Package{\n\t\t\t\tID:       pkg.ID(uuid.NewString()),\n\t\t\t\tName:     \"speach\",\n\t\t\t\tVersion:  \"100.0.52\",\n\t\t\t\tLanguage: syftPkg.Ruby,\n\t\t\t\tType:     syftPkg.GemPkg,\n\t\t\t\tLocations: file.NewLocationSet(file.NewVirtualLocation(\"/real/path/with/speach\",\n\t\t\t\t\t\"/virtual/path/that/has/speach\")),\n\t\t\t},\n\t\t},\n\t}\n\n\t// For testing the match-type rules\n\tmatchTypesMatches = []Match{\n\t\t// Direct match, not like a normal kernel header match\n\t\t{\n\t\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\tID:        \"CVE-1\",\n\t\t\t\t\tNamespace: \"fake-redhat-vulns\",\n\t\t\t\t},\n\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\tState: vulnerability.FixStateUnknown,\n\t\t\t\t},\n\t\t\t},\n\t\t\tPackage: pkg.Package{\n\t\t\t\tID:      pkg.ID(uuid.NewString()),\n\t\t\t\tName:    \"kernel-headers1\",\n\t\t\t\tVersion: \"5.1.0\",\n\t\t\t\tType:    syftPkg.RpmPkg,\n\t\t\t\tUpstreams: []pkg.UpstreamPackage{\n\t\t\t\t\t{Name: \"kernel2\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tDetails: []Detail{\n\t\t\t\t{\n\t\t\t\t\tType: ExactDirectMatch,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\tID:        \"CVE-2\",\n\t\t\t\t\tNamespace: \"fake-deb-vulns\",\n\t\t\t\t},\n\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\tState: vulnerability.FixStateUnknown,\n\t\t\t\t},\n\t\t\t},\n\t\t\tPackage: pkg.Package{\n\t\t\t\tID:      pkg.ID(uuid.NewString()),\n\t\t\t\tName:    \"kernel-headers2\",\n\t\t\t\tVersion: \"5.1.0\",\n\t\t\t\tType:    syftPkg.DebPkg,\n\t\t\t\tUpstreams: []pkg.UpstreamPackage{\n\t\t\t\t\t{Name: \"kernel2\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tDetails: []Detail{\n\t\t\t\t{\n\t\t\t\t\tType: ExactIndirectMatch,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\tID:        \"CVE-1\",\n\t\t\t\t\tNamespace: \"npm-vulns\",\n\t\t\t\t},\n\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\tState: vulnerability.FixStateUnknown,\n\t\t\t\t},\n\t\t\t},\n\t\t\tPackage: pkg.Package{\n\t\t\t\tID:      pkg.ID(uuid.NewString()),\n\t\t\t\tName:    \"npm1\",\n\t\t\t\tVersion: \"5.1.0\",\n\t\t\t\tType:    syftPkg.NpmPkg,\n\t\t\t},\n\t\t\tDetails: []Detail{\n\t\t\t\t{\n\t\t\t\t\tType: CPEMatch,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\t// For testing the match-type and upstream ignore rules\n\tkernelHeadersMatches = []Match{\n\t\t// RPM-like match similar to what we see from RedHat\n\t\t{\n\t\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\tID:        \"CVE-2\",\n\t\t\t\t\tNamespace: \"fake-redhat-vulns\",\n\t\t\t\t},\n\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\tState: vulnerability.FixStateUnknown,\n\t\t\t\t},\n\t\t\t},\n\t\t\tPackage: pkg.Package{\n\t\t\t\tID:      pkg.ID(uuid.NewString()),\n\t\t\t\tName:    \"kernel-headers\",\n\t\t\t\tVersion: \"5.1.0\",\n\t\t\t\tType:    syftPkg.RpmPkg,\n\t\t\t\tUpstreams: []pkg.UpstreamPackage{\n\t\t\t\t\t{Name: \"kernel\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tDetails: []Detail{\n\t\t\t\t{\n\t\t\t\t\tType: ExactIndirectMatch,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t// debian-like match, showing the kernel header package name w/embedded version\n\t\t{\n\t\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\tID:        \"CVE-2\",\n\t\t\t\t\tNamespace: \"fake-debian-vulns\",\n\t\t\t\t},\n\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\tState: vulnerability.FixStateUnknown,\n\t\t\t\t},\n\t\t\t},\n\t\t\tPackage: pkg.Package{\n\t\t\t\tID:      pkg.ID(uuid.NewString()),\n\t\t\t\tName:    \"linux-headers-5.2.0\",\n\t\t\t\tVersion: \"5.2.1\",\n\t\t\t\tType:    syftPkg.DebPkg,\n\t\t\t\tUpstreams: []pkg.UpstreamPackage{\n\t\t\t\t\t{Name: \"linux\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tDetails: []Detail{\n\t\t\t\t{\n\t\t\t\t\tType: ExactIndirectMatch,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t// linux-like match, similar to what we see from debian\\ubuntu\n\t\t{\n\t\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\tID:        \"CVE-3\",\n\t\t\t\t\tNamespace: \"fake-linux-vulns\",\n\t\t\t\t},\n\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\tState: vulnerability.FixStateUnknown,\n\t\t\t\t},\n\t\t\t},\n\t\t\tPackage: pkg.Package{\n\t\t\t\tID:      pkg.ID(uuid.NewString()),\n\t\t\t\tName:    \"linux-azure-headers-generic\",\n\t\t\t\tVersion: \"5.2.1\",\n\t\t\t\tType:    syftPkg.DebPkg,\n\t\t\t\tUpstreams: []pkg.UpstreamPackage{\n\t\t\t\t\t{Name: \"linux-azure\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tDetails: []Detail{\n\t\t\t\t{\n\t\t\t\t\tType: ExactIndirectMatch,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\t// For testing the match-type and upstream ignore rules\n\tpackageTypeMatches = []Match{\n\t\t{\n\t\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\tID:        \"CVE-2\",\n\t\t\t\t\tNamespace: \"fake-redhat-vulns\",\n\t\t\t\t},\n\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\tState: vulnerability.FixStateUnknown,\n\t\t\t\t},\n\t\t\t},\n\t\t\tPackage: pkg.Package{\n\t\t\t\tID:      pkg.ID(uuid.NewString()),\n\t\t\t\tName:    \"kernel-headers\",\n\t\t\t\tVersion: \"5.1.0\",\n\t\t\t\tType:    syftPkg.RpmPkg,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\tID:        \"CVE-2\",\n\t\t\t\t\tNamespace: \"fake-debian-vulns\",\n\t\t\t\t},\n\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\tState: vulnerability.FixStateUnknown,\n\t\t\t\t},\n\t\t\t},\n\t\t\tPackage: pkg.Package{\n\t\t\t\tID:      pkg.ID(uuid.NewString()),\n\t\t\t\tName:    \"linux-headers-5.2.0\",\n\t\t\t\tVersion: \"5.2.1\",\n\t\t\t\tType:    syftPkg.DebPkg,\n\t\t\t},\n\t\t},\n\t}\n)\n\nfunc TestApplyIgnoreRules(t *testing.T) {\n\tcases := []struct {\n\t\tname                     string\n\t\tallMatches               []Match\n\t\tignoreRules              []IgnoreRule\n\t\texpectedRemainingMatches []Match\n\t\texpectedIgnoredMatches   []IgnoredMatch\n\t}{\n\t\t{\n\t\t\tname:                     \"no ignore rules\",\n\t\t\tallMatches:               allMatches,\n\t\t\tignoreRules:              nil,\n\t\t\texpectedRemainingMatches: allMatches,\n\t\t\texpectedIgnoredMatches:   nil,\n\t\t},\n\t\t{\n\t\t\tname:       \"no applicable ignore rules\",\n\t\t\tallMatches: allMatches,\n\t\t\tignoreRules: []IgnoreRule{\n\t\t\t\t{\n\t\t\t\t\tVulnerability: \"CVE-789\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tPackage: IgnoreRulePackage{\n\t\t\t\t\t\tName:    \"bashful\",\n\t\t\t\t\t\tVersion: \"5\",\n\t\t\t\t\t\tType:    \"npm\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tPackage: IgnoreRulePackage{\n\t\t\t\t\t\tName:    \"reach\",\n\t\t\t\t\t\tVersion: \"3000\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedRemainingMatches: allMatches,\n\t\t\texpectedIgnoredMatches:   nil,\n\t\t},\n\t\t{\n\t\t\tname:       \"ignore all matches\",\n\t\t\tallMatches: allMatches,\n\t\t\tignoreRules: []IgnoreRule{\n\t\t\t\t{\n\t\t\t\t\tVulnerability: \"CVE-123\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tPackage: IgnoreRulePackage{\n\t\t\t\t\t\tLocation: \"/virtual/path/that/has/reach\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedRemainingMatches: []Match{\n\t\t\t\tallMatches[2],\n\t\t\t\tallMatches[3],\n\t\t\t},\n\t\t\texpectedIgnoredMatches: []IgnoredMatch{\n\t\t\t\t{\n\t\t\t\t\tMatch: allMatches[0],\n\t\t\t\t\tAppliedIgnoreRules: []IgnoreRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tVulnerability: \"CVE-123\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tMatch: allMatches[1],\n\t\t\t\t\tAppliedIgnoreRules: []IgnoreRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tPackage: IgnoreRulePackage{\n\t\t\t\t\t\t\t\tLocation: \"/virtual/path/that/has/reach\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:       \"ignore related matches\",\n\t\t\tallMatches: allMatches,\n\t\t\tignoreRules: []IgnoreRule{\n\t\t\t\t{\n\t\t\t\t\tVulnerability:  \"CVE-123\",\n\t\t\t\t\tIncludeAliases: true,\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedRemainingMatches: []Match{\n\t\t\t\tallMatches[2],\n\t\t\t\tallMatches[3],\n\t\t\t},\n\t\t\texpectedIgnoredMatches: []IgnoredMatch{\n\t\t\t\t{\n\t\t\t\t\tMatch: allMatches[0],\n\t\t\t\t\tAppliedIgnoreRules: []IgnoreRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tVulnerability:  \"CVE-123\",\n\t\t\t\t\t\t\tIncludeAliases: true,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tMatch: allMatches[1],\n\t\t\t\t\tAppliedIgnoreRules: []IgnoreRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tVulnerability:  \"CVE-123\",\n\t\t\t\t\t\t\tIncludeAliases: true,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:       \"ignore subset of matches\",\n\t\t\tallMatches: allMatches,\n\t\t\tignoreRules: []IgnoreRule{\n\t\t\t\t{\n\t\t\t\t\tVulnerability: \"CVE-456\",\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedRemainingMatches: []Match{\n\t\t\t\tallMatches[0],\n\t\t\t\tallMatches[2],\n\t\t\t\tallMatches[3],\n\t\t\t},\n\t\t\texpectedIgnoredMatches: []IgnoredMatch{\n\t\t\t\t{\n\t\t\t\t\tMatch: allMatches[1],\n\t\t\t\t\tAppliedIgnoreRules: []IgnoreRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tVulnerability: \"CVE-456\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:       \"ignore matches without fix\",\n\t\t\tallMatches: allMatches,\n\t\t\tignoreRules: []IgnoreRule{\n\t\t\t\t{FixState: string(vulnerability.FixStateNotFixed)},\n\t\t\t\t{FixState: string(vulnerability.FixStateWontFix)},\n\t\t\t\t{FixState: string(vulnerability.FixStateUnknown)},\n\t\t\t},\n\t\t\texpectedRemainingMatches: []Match{\n\t\t\t\tallMatches[0],\n\t\t\t},\n\t\t\texpectedIgnoredMatches: []IgnoredMatch{\n\t\t\t\t{\n\t\t\t\t\tMatch: allMatches[1],\n\t\t\t\t\tAppliedIgnoreRules: []IgnoreRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tFixState: \"not-fixed\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tMatch: allMatches[2],\n\t\t\t\t\tAppliedIgnoreRules: []IgnoreRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tFixState: \"wont-fix\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tMatch: allMatches[3],\n\t\t\t\t\tAppliedIgnoreRules: []IgnoreRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tFixState: \"unknown\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:       \"ignore matches on namespace\",\n\t\t\tallMatches: allMatches,\n\t\t\tignoreRules: []IgnoreRule{\n\t\t\t\t{Namespace: \"ruby-vulns\"},\n\t\t\t},\n\t\t\texpectedRemainingMatches: []Match{\n\t\t\t\tallMatches[0],\n\t\t\t},\n\t\t\texpectedIgnoredMatches: []IgnoredMatch{\n\t\t\t\t{\n\t\t\t\t\tMatch: allMatches[1],\n\t\t\t\t\tAppliedIgnoreRules: []IgnoreRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tNamespace: \"ruby-vulns\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tMatch: allMatches[2],\n\t\t\t\t\tAppliedIgnoreRules: []IgnoreRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tNamespace: \"ruby-vulns\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tMatch: allMatches[3],\n\t\t\t\t\tAppliedIgnoreRules: []IgnoreRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tNamespace: \"ruby-vulns\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:       \"ignore matches on language\",\n\t\t\tallMatches: allMatches,\n\t\t\tignoreRules: []IgnoreRule{\n\t\t\t\t{\n\t\t\t\t\tPackage: IgnoreRulePackage{\n\t\t\t\t\t\tLanguage: string(syftPkg.Ruby),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedRemainingMatches: []Match{\n\t\t\t\tallMatches[0],\n\t\t\t},\n\t\t\texpectedIgnoredMatches: []IgnoredMatch{\n\t\t\t\t{\n\t\t\t\t\tMatch: allMatches[1],\n\t\t\t\t\tAppliedIgnoreRules: []IgnoreRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tPackage: IgnoreRulePackage{\n\t\t\t\t\t\t\t\tLanguage: string(syftPkg.Ruby),\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tMatch: allMatches[2],\n\t\t\t\t\tAppliedIgnoreRules: []IgnoreRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tPackage: IgnoreRulePackage{\n\t\t\t\t\t\t\t\tLanguage: string(syftPkg.Ruby),\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tMatch: allMatches[3],\n\t\t\t\t\tAppliedIgnoreRules: []IgnoreRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tPackage: IgnoreRulePackage{\n\t\t\t\t\t\t\t\tLanguage: string(syftPkg.Ruby),\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:       \"ignore matches on indirect match-type\",\n\t\t\tallMatches: matchTypesMatches,\n\t\t\tignoreRules: []IgnoreRule{\n\t\t\t\t{\n\t\t\t\t\tMatchType: ExactIndirectMatch,\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedRemainingMatches: []Match{\n\t\t\t\tmatchTypesMatches[0], matchTypesMatches[2],\n\t\t\t},\n\t\t\texpectedIgnoredMatches: []IgnoredMatch{\n\t\t\t\t{\n\t\t\t\t\tMatch: matchTypesMatches[1],\n\t\t\t\t\tAppliedIgnoreRules: []IgnoreRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tMatchType: ExactIndirectMatch,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:       \"ignore matches on cpe match-type\",\n\t\t\tallMatches: matchTypesMatches,\n\t\t\tignoreRules: []IgnoreRule{\n\t\t\t\t{\n\t\t\t\t\tMatchType: CPEMatch,\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedRemainingMatches: []Match{\n\t\t\t\tmatchTypesMatches[0], matchTypesMatches[1],\n\t\t\t},\n\t\t\texpectedIgnoredMatches: []IgnoredMatch{\n\t\t\t\t{\n\t\t\t\t\tMatch: matchTypesMatches[2],\n\t\t\t\t\tAppliedIgnoreRules: []IgnoreRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tMatchType: CPEMatch,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:       \"ignore matches on upstream name\",\n\t\t\tallMatches: kernelHeadersMatches,\n\t\t\tignoreRules: []IgnoreRule{\n\t\t\t\t{\n\t\t\t\t\tPackage: IgnoreRulePackage{\n\t\t\t\t\t\tUpstreamName: \"kernel\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tPackage: IgnoreRulePackage{\n\t\t\t\t\t\tUpstreamName: \"linux-.*\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedRemainingMatches: []Match{\n\t\t\t\tkernelHeadersMatches[1],\n\t\t\t},\n\t\t\texpectedIgnoredMatches: []IgnoredMatch{\n\t\t\t\t{\n\t\t\t\t\tMatch: kernelHeadersMatches[0],\n\t\t\t\t\tAppliedIgnoreRules: []IgnoreRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tPackage: IgnoreRulePackage{\n\t\t\t\t\t\t\t\tUpstreamName: \"kernel\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tMatch: kernelHeadersMatches[2],\n\t\t\t\t\tAppliedIgnoreRules: []IgnoreRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tPackage: IgnoreRulePackage{\n\t\t\t\t\t\t\t\tUpstreamName: \"linux-.*\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:       \"ignore matches on package type\",\n\t\t\tallMatches: packageTypeMatches,\n\t\t\tignoreRules: []IgnoreRule{\n\t\t\t\t{\n\t\t\t\t\tPackage: IgnoreRulePackage{\n\t\t\t\t\t\tType: string(syftPkg.RpmPkg),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedRemainingMatches: []Match{\n\t\t\t\tpackageTypeMatches[1],\n\t\t\t},\n\t\t\texpectedIgnoredMatches: []IgnoredMatch{\n\t\t\t\t{\n\t\t\t\t\tMatch: packageTypeMatches[0],\n\t\t\t\t\tAppliedIgnoreRules: []IgnoreRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tPackage: IgnoreRulePackage{\n\t\t\t\t\t\t\t\tType: string(syftPkg.RpmPkg),\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:       \"ignore matches rpms for kernel-headers with kernel upstream\",\n\t\t\tallMatches: kernelHeadersMatches,\n\t\t\tignoreRules: []IgnoreRule{\n\t\t\t\t{\n\t\t\t\t\tPackage: IgnoreRulePackage{\n\t\t\t\t\t\tName:         \"kernel-headers\",\n\t\t\t\t\t\tUpstreamName: \"kernel\",\n\t\t\t\t\t\tType:         string(syftPkg.RpmPkg),\n\t\t\t\t\t},\n\t\t\t\t\tMatchType: ExactIndirectMatch,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tPackage: IgnoreRulePackage{\n\t\t\t\t\t\tName:         \"linux-.*-headers-.*\",\n\t\t\t\t\t\tUpstreamName: \"linux.*\",\n\t\t\t\t\t\tType:         string(syftPkg.DebPkg),\n\t\t\t\t\t},\n\t\t\t\t\tMatchType: ExactIndirectMatch,\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedRemainingMatches: []Match{\n\t\t\t\tkernelHeadersMatches[1],\n\t\t\t},\n\t\t\texpectedIgnoredMatches: []IgnoredMatch{\n\t\t\t\t{\n\t\t\t\t\tMatch: kernelHeadersMatches[0],\n\t\t\t\t\tAppliedIgnoreRules: []IgnoreRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tPackage: IgnoreRulePackage{\n\t\t\t\t\t\t\t\tName:         \"kernel-headers\",\n\t\t\t\t\t\t\t\tUpstreamName: \"kernel\",\n\t\t\t\t\t\t\t\tType:         string(syftPkg.RpmPkg),\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tMatchType: ExactIndirectMatch,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tMatch: kernelHeadersMatches[2],\n\t\t\t\t\tAppliedIgnoreRules: []IgnoreRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tPackage: IgnoreRulePackage{\n\t\t\t\t\t\t\t\tName:         \"linux-.*-headers-.*\",\n\t\t\t\t\t\t\t\tUpstreamName: \"linux.*\",\n\t\t\t\t\t\t\t\tType:         string(syftPkg.DebPkg),\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tMatchType: ExactIndirectMatch,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:       \"ignore on name regex\",\n\t\t\tallMatches: kernelHeadersMatches,\n\t\t\tignoreRules: []IgnoreRule{\n\t\t\t\t{\n\t\t\t\t\tPackage: IgnoreRulePackage{\n\t\t\t\t\t\tName: \"kernel-headers.*\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedRemainingMatches: []Match{\n\t\t\t\tkernelHeadersMatches[1],\n\t\t\t\tkernelHeadersMatches[2],\n\t\t\t},\n\t\t\texpectedIgnoredMatches: []IgnoredMatch{\n\t\t\t\t{\n\t\t\t\t\tMatch: kernelHeadersMatches[0],\n\t\t\t\t\tAppliedIgnoreRules: []IgnoreRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tPackage: IgnoreRulePackage{\n\t\t\t\t\t\t\t\tName: \"kernel-headers.*\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:       \"ignore on name regex, no matches\",\n\t\t\tallMatches: kernelHeadersMatches,\n\t\t\tignoreRules: []IgnoreRule{\n\t\t\t\t{\n\t\t\t\t\tPackage: IgnoreRulePackage{\n\t\t\t\t\t\tName: \"foo.*\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedRemainingMatches: kernelHeadersMatches,\n\t\t\texpectedIgnoredMatches:   nil,\n\t\t},\n\t\t{\n\t\t\tname:       \"ignore on name regex, line termination verification\",\n\t\t\tallMatches: kernelHeadersMatches,\n\t\t\tignoreRules: []IgnoreRule{\n\t\t\t\t{\n\t\t\t\t\tPackage: IgnoreRulePackage{\n\t\t\t\t\t\tName: \"^kernel-header$\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedRemainingMatches: kernelHeadersMatches,\n\t\t\texpectedIgnoredMatches:   nil,\n\t\t},\n\t\t{\n\t\t\tname:       \"ignore on name regex, line termination test match\",\n\t\t\tallMatches: kernelHeadersMatches,\n\t\t\tignoreRules: []IgnoreRule{\n\t\t\t\t{\n\t\t\t\t\tPackage: IgnoreRulePackage{\n\t\t\t\t\t\tName: \"^kernel-headers$\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedRemainingMatches: []Match{\n\t\t\t\tkernelHeadersMatches[1],\n\t\t\t\tkernelHeadersMatches[2],\n\t\t\t},\n\t\t\texpectedIgnoredMatches: []IgnoredMatch{\n\t\t\t\t{\n\t\t\t\t\tMatch: kernelHeadersMatches[0],\n\t\t\t\t\tAppliedIgnoreRules: []IgnoreRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tPackage: IgnoreRulePackage{\n\t\t\t\t\t\t\t\tName: \"^kernel-headers$\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, testCase := range cases {\n\t\tt.Run(testCase.name, func(t *testing.T) {\n\t\t\tactualRemainingMatches, actualIgnoredMatches := ApplyIgnoreRules(sliceToMatches(testCase.allMatches), testCase.ignoreRules)\n\n\t\t\tassertMatchOrder(t, testCase.expectedRemainingMatches, actualRemainingMatches.Sorted())\n\t\t\tassertIgnoredMatchOrder(t, testCase.expectedIgnoredMatches, actualIgnoredMatches)\n\n\t\t})\n\t}\n}\n\nfunc sliceToMatches(s []Match) Matches {\n\tmatches := NewMatches()\n\tmatches.Add(s...)\n\treturn matches\n}\n\nvar (\n\texampleMatch = Match{\n\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\tReference: vulnerability.Reference{ID: \"CVE-2000-1234\"},\n\t\t},\n\t\tPackage: pkg.Package{\n\t\t\tID:      pkg.ID(uuid.NewString()),\n\t\t\tName:    \"a-pkg\",\n\t\t\tVersion: \"1.0\",\n\t\t\tLocations: file.NewLocationSet(\n\t\t\t\tfile.NewLocation(\"/some/path\"),\n\t\t\t\tfile.NewVirtualLocation(\"/some/path\", \"/some/virtual/path\"),\n\t\t\t),\n\t\t\tType: \"rpm\",\n\t\t},\n\t}\n)\n\nfunc TestIsRegex(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    string\n\t\texpected bool\n\t}{\n\t\t// simple strings that should NOT be detected as regex\n\t\t{\n\t\t\tname:     \"simple string\",\n\t\t\tinput:    \"hello\",\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"alphanumeric with dashes\",\n\t\t\tinput:    \"kernel-headers\",\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"alphanumeric with underscores\",\n\t\t\tinput:    \"my_package_name\",\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"version numbers\",\n\t\t\tinput:    \"1.2.3\",\n\t\t\texpected: false, // dots are no longer considered regex metacharacters\n\t\t},\n\t\t{\n\t\t\tname:     \"empty string\",\n\t\t\tinput:    \"\",\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"spaces only\",\n\t\t\tinput:    \"   \",\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"numbers only\",\n\t\t\tinput:    \"12345\",\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"letters and numbers\",\n\t\t\tinput:    \"abc123\",\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"with slashes\",\n\t\t\tinput:    \"path/to/file\",\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"with colons\",\n\t\t\tinput:    \"namespace:package\",\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"with at symbol\",\n\t\t\tinput:    \"user@domain.com\",\n\t\t\texpected: false, // dots are no longer considered regex metacharacters\n\t\t},\n\n\t\t// strings with regex metacharacters that SHOULD be detected as regex\n\t\t{\n\t\t\tname:     \"caret at start\",\n\t\t\tinput:    \"^start\",\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"dollar at end\",\n\t\t\tinput:    \"end$\",\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"asterisk wildcard\",\n\t\t\tinput:    \"test*\",\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"plus quantifier\",\n\t\t\tinput:    \"test+\",\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"question mark\",\n\t\t\tinput:    \"test?\",\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"dot wildcard\",\n\t\t\tinput:    \"test.\",\n\t\t\texpected: false, // dots are no longer considered regex metacharacters\n\t\t},\n\t\t{\n\t\t\tname:     \"square brackets\",\n\t\t\tinput:    \"test[abc]\",\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"parentheses grouping\",\n\t\t\tinput:    \"(test)\",\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"curly braces quantifier\",\n\t\t\tinput:    \"test{1,3}\",\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"pipe alternation\",\n\t\t\tinput:    \"test|other\",\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"backslash escape\",\n\t\t\tinput:    \"test\\\\\",\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"multiple metacharacters\",\n\t\t\tinput:    \"^test.*$\",\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"complex regex pattern\",\n\t\t\tinput:    \"kernel-headers.*\",\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"anchored regex\",\n\t\t\tinput:    \"^kernel-headers$\",\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"character class\",\n\t\t\tinput:    \"test[0-9]\",\n\t\t\texpected: true,\n\t\t},\n\n\t\t// escaped character classes\n\t\t{\n\t\t\tname:     \"escaped digit\",\n\t\t\tinput:    \"\\\\d\",\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"escaped non-digit\",\n\t\t\tinput:    \"\\\\D\",\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"escaped word character\",\n\t\t\tinput:    \"\\\\w\",\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"escaped non-word character\",\n\t\t\tinput:    \"\\\\W\",\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"escaped whitespace\",\n\t\t\tinput:    \"\\\\s\",\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"escaped non-whitespace\",\n\t\t\tinput:    \"\\\\S\",\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"escaped newline\",\n\t\t\tinput:    \"\\\\n\",\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"escaped carriage return\",\n\t\t\tinput:    \"\\\\r\",\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"escaped tab\",\n\t\t\tinput:    \"\\\\t\",\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"escaped form feed\",\n\t\t\tinput:    \"\\\\f\",\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"escaped vertical tab\",\n\t\t\tinput:    \"\\\\v\",\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"escaped character classes in longer string\",\n\t\t\tinput:    \"prefix\\\\dpostfix\",\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"multiple escaped classes\",\n\t\t\tinput:    \"\\\\w+\\\\s*\\\\d+\",\n\t\t\texpected: true,\n\t\t},\n\n\t\t// edge cases\n\t\t{\n\t\t\tname:     \"single backslash\",\n\t\t\tinput:    \"\\\\\",\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"single caret\",\n\t\t\tinput:    \"^\",\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"single dollar\",\n\t\t\tinput:    \"$\",\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"single dot\",\n\t\t\tinput:    \".\",\n\t\t\texpected: false, // dots are no longer considered regex metacharacters\n\t\t},\n\t\t{\n\t\t\tname:     \"backslash followed by regular character\",\n\t\t\tinput:    \"\\\\a\",\n\t\t\texpected: true, // backslash is still a metacharacter\n\t\t},\n\t\t{\n\t\t\tname:     \"backslash at end\",\n\t\t\tinput:    \"test\\\\\",\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"mixed metacharacters and escaped classes\",\n\t\t\tinput:    \"^\\\\w+\\\\.\\\\d{2,}$\",\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"real world package patterns\",\n\t\t\tinput:    \"linux-.*\",\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"real world upstream patterns\",\n\t\t\tinput:    \"linux.*\",\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"real world header patterns\",\n\t\t\tinput:    \"linux-.*-headers-.*\",\n\t\t\texpected: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot := isLikelyARegex(tt.input)\n\t\t\tassert.Equal(t, tt.expected, got)\n\t\t})\n\t}\n}\n\nfunc TestShouldIgnore(t *testing.T) {\n\tcases := []struct {\n\t\tname     string\n\t\tmatch    Match\n\t\trule     IgnoreRule\n\t\texpected bool\n\t}{\n\t\t{\n\t\t\tname:     \"empty rule\",\n\t\t\tmatch:    exampleMatch,\n\t\t\trule:     IgnoreRule{},\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:  \"rule applies via vulnerability ID\",\n\t\t\tmatch: exampleMatch,\n\t\t\trule: IgnoreRule{\n\t\t\t\tVulnerability: exampleMatch.Vulnerability.ID,\n\t\t\t},\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:  \"rule applies via package name\",\n\t\t\tmatch: exampleMatch,\n\t\t\trule: IgnoreRule{\n\t\t\t\tPackage: IgnoreRulePackage{\n\t\t\t\t\tName: exampleMatch.Package.Name,\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:  \"rule applies via package version\",\n\t\t\tmatch: exampleMatch,\n\t\t\trule: IgnoreRule{\n\t\t\t\tPackage: IgnoreRulePackage{\n\t\t\t\t\tVersion: exampleMatch.Package.Version,\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:  \"rule applies via package type\",\n\t\t\tmatch: exampleMatch,\n\t\t\trule: IgnoreRule{\n\t\t\t\tPackage: IgnoreRulePackage{\n\t\t\t\t\tType: string(exampleMatch.Package.Type),\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:  \"rule applies via package location real path\",\n\t\t\tmatch: exampleMatch,\n\t\t\trule: IgnoreRule{\n\t\t\t\tPackage: IgnoreRulePackage{\n\t\t\t\t\tLocation: exampleMatch.Package.Locations.ToSlice()[0].RealPath,\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:  \"rule applies via package location virtual path\",\n\t\t\tmatch: exampleMatch,\n\t\t\trule: IgnoreRule{\n\t\t\t\tPackage: IgnoreRulePackage{\n\t\t\t\t\tLocation: exampleMatch.Package.Locations.ToSlice()[1].AccessPath,\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:  \"rule applies via package location glob\",\n\t\t\tmatch: exampleMatch,\n\t\t\trule: IgnoreRule{\n\t\t\t\tPackage: IgnoreRulePackage{\n\t\t\t\t\tLocation: \"/some/**\",\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:  \"rule applies via multiple fields\",\n\t\t\tmatch: exampleMatch,\n\t\t\trule: IgnoreRule{\n\t\t\t\tVulnerability: exampleMatch.Vulnerability.ID,\n\t\t\t\tPackage: IgnoreRulePackage{\n\t\t\t\t\tType: string(exampleMatch.Package.Type),\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:  \"rule doesn't apply despite some fields matching\",\n\t\t\tmatch: exampleMatch,\n\t\t\trule: IgnoreRule{\n\t\t\t\tVulnerability: exampleMatch.Vulnerability.ID,\n\t\t\t\tPackage: IgnoreRulePackage{\n\t\t\t\t\tName:    \"not-the-right-package\",\n\t\t\t\t\tVersion: exampleMatch.Package.Version,\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: false,\n\t\t},\n\t}\n\n\tfor _, testCase := range cases {\n\t\tt.Run(testCase.name, func(t *testing.T) {\n\t\t\tactual := len(testCase.rule.IgnoreMatch(testCase.match)) > 0\n\t\t\tassert.Equal(t, testCase.expected, actual)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/match/match.go",
    "content": "package match\n\nimport (\n\t\"fmt\"\n\t\"sort\"\n\t\"strings\"\n\n\t\"github.com/scylladb/go-set/strset\"\n\n\t\"github.com/anchore/grype/grype/pkg\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n\t\"github.com/anchore/syft/syft/cpe\"\n)\n\nvar ErrCannotMerge = fmt.Errorf(\"unable to merge vulnerability matches\")\n\n// Match represents a finding in the vulnerability matching process, pairing a single package and a single vulnerability object.\ntype Match struct {\n\tVulnerability vulnerability.Vulnerability // The vulnerability details of the match.\n\tPackage       pkg.Package                 // The package used to search for a match.\n\tDetails       Details                     // all the ways this particular match was made.\n}\n\n// String is the string representation of select match fields.\nfunc (m Match) String() string {\n\treturn fmt.Sprintf(\"Match(pkg=%s vuln=%q types=%q)\", m.Package, m.Vulnerability.String(), m.Details.Types())\n}\n\nfunc (m Match) Fingerprint() Fingerprint {\n\treturn Fingerprint{\n\t\tcoreFingerprint: coreFingerprint{\n\t\t\tvulnerabilityID:        m.Vulnerability.ID,\n\t\t\tvulnerabilityNamespace: m.Vulnerability.Namespace,\n\t\t\tpackageID:              m.Package.ID,\n\t\t},\n\t\tvulnerabilityFixes: strings.Join(m.Vulnerability.Fix.Versions, \",\"),\n\t}\n}\n\nfunc (m *Match) Merge(other Match) error {\n\tif other.Fingerprint() != m.Fingerprint() {\n\t\treturn ErrCannotMerge\n\t}\n\n\t// there are cases related vulnerabilities are synthetic, for example when\n\t// orienting results by CVE. we need to keep track of these\n\trelated := strset.New()\n\tfor _, r := range m.Vulnerability.RelatedVulnerabilities {\n\t\trelated.Add(referenceID(r))\n\t}\n\tfor _, r := range other.Vulnerability.RelatedVulnerabilities {\n\t\tif related.Has(referenceID(r)) {\n\t\t\tcontinue\n\t\t}\n\t\tm.Vulnerability.RelatedVulnerabilities = append(m.Vulnerability.RelatedVulnerabilities, r)\n\t}\n\n\t// for stable output\n\tsort.Slice(m.Vulnerability.RelatedVulnerabilities, func(i, j int) bool {\n\t\ta := m.Vulnerability.RelatedVulnerabilities[i]\n\t\tb := m.Vulnerability.RelatedVulnerabilities[j]\n\t\treturn strings.Compare(referenceID(a), referenceID(b)) < 0\n\t})\n\n\t// also keep details from the other match that are unique\n\tdetailIDs := strset.New()\n\tfor _, d := range m.Details {\n\t\tdetailIDs.Add(d.ID())\n\t}\n\tfor _, d := range other.Details {\n\t\tif detailIDs.Has(d.ID()) {\n\t\t\tcontinue\n\t\t}\n\t\tm.Details = append(m.Details, d)\n\t}\n\n\t// for stable output\n\tsort.Sort(m.Details)\n\n\t// retain all unique CPEs for consistent output\n\tm.Vulnerability.CPEs = cpe.Merge(m.Vulnerability.CPEs, other.Vulnerability.CPEs)\n\tif m.Vulnerability.CPEs == nil {\n\t\t// ensure we always have a non-nil slice\n\t\tm.Vulnerability.CPEs = []cpe.CPE{}\n\t}\n\n\treturn nil\n}\n\n// referenceID returns an \"ID\" string for a vulnerability.Reference\nfunc referenceID(r vulnerability.Reference) string {\n\treturn fmt.Sprintf(\"%s:%s\", r.Namespace, r.ID)\n}\n"
  },
  {
    "path": "grype/match/match_test.go",
    "content": "package match\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/grype/grype/pkg\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n\t\"github.com/anchore/syft/syft/cpe\"\n)\n\nfunc TestMatch_Merge(t *testing.T) {\n\ttests := []struct {\n\t\tname        string\n\t\tm1          Match\n\t\tm2          Match\n\t\texpectedErr error\n\t\texpected    Match\n\t}{\n\t\t{\n\t\t\tname: \"error on fingerprint mismatch\",\n\t\t\tm1: Match{\n\t\t\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\tID:        \"CVE-2023-0001\",\n\t\t\t\t\t\tNamespace: \"namespace1\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tPackage: pkg.Package{\n\t\t\t\t\tID: \"pkg1\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tm2: Match{\n\t\t\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\tID:        \"CVE-2023-0002\",\n\t\t\t\t\t\tNamespace: \"namespace2\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tPackage: pkg.Package{\n\t\t\t\t\tID: \"pkg2\",\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedErr: ErrCannotMerge,\n\t\t},\n\t\t{\n\t\t\tname: \"merge with unique values\",\n\t\t\tm1: Match{\n\t\t\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\tID:        \"CVE-2023-0001\",\n\t\t\t\t\t\tNamespace: \"namespace\",\n\t\t\t\t\t},\n\t\t\t\t\tRelatedVulnerabilities: []vulnerability.Reference{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tNamespace: \"ns1\",\n\t\t\t\t\t\t\tID:        \"ID1\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tCPEs: []cpe.CPE{\n\t\t\t\t\t\tcpe.Must(\"cpe:2.3:a:example:example:1.0:*:*:*:*:*:*:*\", cpe.DeclaredSource),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tPackage: pkg.Package{\n\t\t\t\t\tID: \"pkg1\",\n\t\t\t\t},\n\t\t\t\tDetails: Details{\n\t\t\t\t\t{\n\t\t\t\t\t\tType:       ExactDirectMatch,\n\t\t\t\t\t\tSearchedBy: \"attr1\",\n\t\t\t\t\t\tFound:      \"value1\",\n\t\t\t\t\t\tMatcher:    \"matcher1\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tm2: Match{\n\t\t\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\tID:        \"CVE-2023-0001\",\n\t\t\t\t\t\tNamespace: \"namespace\",\n\t\t\t\t\t},\n\t\t\t\t\tRelatedVulnerabilities: []vulnerability.Reference{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tNamespace: \"ns2\",\n\t\t\t\t\t\t\tID:        \"ID2\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tCPEs: []cpe.CPE{\n\t\t\t\t\t\tcpe.Must(\"cpe:2.3:a:example:example:1.1:*:*:*:*:*:*:*\", cpe.DeclaredSource),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tPackage: pkg.Package{\n\t\t\t\t\tID: \"pkg1\",\n\t\t\t\t},\n\t\t\t\tDetails: Details{\n\t\t\t\t\t{\n\t\t\t\t\t\tType:       ExactIndirectMatch,\n\t\t\t\t\t\tSearchedBy: \"attr2\",\n\t\t\t\t\t\tFound:      \"value2\",\n\t\t\t\t\t\tMatcher:    \"matcher2\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedErr: nil,\n\t\t\texpected: Match{\n\t\t\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\tID:        \"CVE-2023-0001\",\n\t\t\t\t\t\tNamespace: \"namespace\",\n\t\t\t\t\t},\n\t\t\t\t\tRelatedVulnerabilities: []vulnerability.Reference{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tNamespace: \"ns1\",\n\t\t\t\t\t\t\tID:        \"ID1\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tNamespace: \"ns2\",\n\t\t\t\t\t\t\tID:        \"ID2\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tCPEs: []cpe.CPE{\n\t\t\t\t\t\tcpe.Must(\"cpe:2.3:a:example:example:1.0:*:*:*:*:*:*:*\", cpe.DeclaredSource),\n\t\t\t\t\t\tcpe.Must(\"cpe:2.3:a:example:example:1.1:*:*:*:*:*:*:*\", cpe.DeclaredSource),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tPackage: pkg.Package{\n\t\t\t\t\tID: \"pkg1\",\n\t\t\t\t},\n\t\t\t\tDetails: Details{\n\t\t\t\t\t{\n\t\t\t\t\t\tType:       ExactDirectMatch,\n\t\t\t\t\t\tSearchedBy: \"attr1\",\n\t\t\t\t\t\tFound:      \"value1\",\n\t\t\t\t\t\tMatcher:    \"matcher1\",\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tType:       ExactIndirectMatch,\n\t\t\t\t\t\tSearchedBy: \"attr2\",\n\t\t\t\t\t\tFound:      \"value2\",\n\t\t\t\t\t\tMatcher:    \"matcher2\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"merges with duplicate values\",\n\t\t\tm1: Match{\n\t\t\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\tID:        \"CVE-2023-0001\",\n\t\t\t\t\t\tNamespace: \"namespace\",\n\t\t\t\t\t},\n\t\t\t\t\tRelatedVulnerabilities: []vulnerability.Reference{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tNamespace: \"ns1\",\n\t\t\t\t\t\t\tID:        \"ID1\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tCPEs: []cpe.CPE{\n\t\t\t\t\t\tcpe.Must(\"cpe:2.3:a:example:example:1.0:*:*:*:*:*:*:*\", cpe.DeclaredSource),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tPackage: pkg.Package{\n\t\t\t\t\tID: \"pkg1\",\n\t\t\t\t},\n\t\t\t\tDetails: Details{\n\t\t\t\t\t{\n\t\t\t\t\t\tType:       ExactDirectMatch,\n\t\t\t\t\t\tSearchedBy: \"attr1\",\n\t\t\t\t\t\tFound:      \"value1\",\n\t\t\t\t\t\tMatcher:    \"matcher1\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tm2: Match{\n\t\t\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\tID:        \"CVE-2023-0001\",\n\t\t\t\t\t\tNamespace: \"namespace\",\n\t\t\t\t\t},\n\t\t\t\t\tRelatedVulnerabilities: []vulnerability.Reference{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tNamespace: \"ns1\",\n\t\t\t\t\t\t\tID:        \"ID1\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tCPEs: []cpe.CPE{\n\t\t\t\t\t\tcpe.Must(\"cpe:2.3:a:example:example:1.0:*:*:*:*:*:*:*\", cpe.DeclaredSource),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tPackage: pkg.Package{\n\t\t\t\t\tID: \"pkg1\",\n\t\t\t\t},\n\t\t\t\tDetails: Details{\n\t\t\t\t\t{\n\t\t\t\t\t\tType:       ExactDirectMatch,\n\t\t\t\t\t\tSearchedBy: \"attr1\",\n\t\t\t\t\t\tFound:      \"value1\",\n\t\t\t\t\t\tMatcher:    \"matcher1\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedErr: nil,\n\t\t\texpected: Match{\n\t\t\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\tID:        \"CVE-2023-0001\",\n\t\t\t\t\t\tNamespace: \"namespace\",\n\t\t\t\t\t},\n\t\t\t\t\tRelatedVulnerabilities: []vulnerability.Reference{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tNamespace: \"ns1\",\n\t\t\t\t\t\t\tID:        \"ID1\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tCPEs: []cpe.CPE{\n\t\t\t\t\t\tcpe.Must(\"cpe:2.3:a:example:example:1.0:*:*:*:*:*:*:*\", cpe.DeclaredSource),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tPackage: pkg.Package{\n\t\t\t\t\tID: \"pkg1\",\n\t\t\t\t},\n\t\t\t\tDetails: Details{\n\t\t\t\t\t{\n\t\t\t\t\t\tType:       ExactDirectMatch,\n\t\t\t\t\t\tSearchedBy: \"attr1\",\n\t\t\t\t\t\tFound:      \"value1\",\n\t\t\t\t\t\tMatcher:    \"matcher1\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := tt.m1.Merge(tt.m2)\n\t\t\tif tt.expectedErr != nil {\n\t\t\t\trequire.ErrorIs(t, err, tt.expectedErr)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.Equal(t, tt.expected.Vulnerability.RelatedVulnerabilities, tt.m1.Vulnerability.RelatedVulnerabilities)\n\t\t\t\trequire.Equal(t, tt.expected.Details, tt.m1.Details)\n\t\t\t\trequire.Equal(t, tt.expected.Vulnerability.CPEs, tt.m1.Vulnerability.CPEs)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/match/matcher.go",
    "content": "package match\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\n\t\"github.com/anchore/grype/grype/pkg\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n\tsyftPkg \"github.com/anchore/syft/syft/pkg\"\n)\n\n// Matcher is the interface to implement to provide top-level package-to-match\ntype Matcher interface {\n\tPackageTypes() []syftPkg.Type\n\n\tType() MatcherType\n\n\t// Match is called for every package found, returning any matches and an optional Ignorer which will be applied\n\t// after all matches are found\n\tMatch(vp vulnerability.Provider, p pkg.Package) ([]Match, []IgnoreFilter, error)\n}\n\n// fatalError can be returned from a Matcher to indicate the matching process should stop.\n// When fatalError(s) are encountered by the top-level matching process, these will be returned as errors to the caller.\ntype fatalError struct {\n\tmatcher MatcherType\n\tinner   error\n}\n\n// NewFatalError creates a new fatalError wrapping the given error\nfunc NewFatalError(matcher MatcherType, e error) error {\n\treturn fatalError{matcher: matcher, inner: e}\n}\n\n// Error implements the error interface for fatalError.\nfunc (f fatalError) Error() string {\n\treturn fmt.Sprintf(\"%s encountered a fatal error: %v\", f.matcher, f.inner)\n}\n\n// IsFatalError returns true if err includes a fatalError\nfunc IsFatalError(err error) bool {\n\tvar fe fatalError\n\treturn err != nil && errors.As(err, &fe)\n}\n"
  },
  {
    "path": "grype/match/matcher_type.go",
    "content": "package match\n\nconst (\n\tUnknownMatcherType MatcherType = \"UnknownMatcherType\"\n\tStockMatcher       MatcherType = \"stock-matcher\"\n\tApkMatcher         MatcherType = \"apk-matcher\"\n\tRubyGemMatcher     MatcherType = \"ruby-gem-matcher\"\n\tDpkgMatcher        MatcherType = \"dpkg-matcher\"\n\tRpmMatcher         MatcherType = \"rpm-matcher\"\n\tJavaMatcher        MatcherType = \"java-matcher\"\n\tPythonMatcher      MatcherType = \"python-matcher\"\n\tDotnetMatcher      MatcherType = \"dotnet-matcher\"\n\tJavascriptMatcher  MatcherType = \"javascript-matcher\"\n\tMsrcMatcher        MatcherType = \"msrc-matcher\"\n\tPortageMatcher     MatcherType = \"portage-matcher\"\n\tGoModuleMatcher    MatcherType = \"go-module-matcher\"\n\tOpenVexMatcher     MatcherType = \"openvex-matcher\"\n\tCsafVexMatcher     MatcherType = \"csafvex-matcher\"\n\tRustMatcher        MatcherType = \"rust-matcher\"\n\tBitnamiMatcher     MatcherType = \"bitnami-matcher\"\n\tPacmanMatcher      MatcherType = \"pacman-matcher\"\n\tHexMatcher         MatcherType = \"hex-matcher\"\n)\n\nvar AllMatcherTypes = []MatcherType{\n\tApkMatcher,\n\tRubyGemMatcher,\n\tDpkgMatcher,\n\tRpmMatcher,\n\tJavaMatcher,\n\tPythonMatcher,\n\tDotnetMatcher,\n\tJavascriptMatcher,\n\tMsrcMatcher,\n\tPortageMatcher,\n\tGoModuleMatcher,\n\tOpenVexMatcher,\n\tCsafVexMatcher,\n\tRustMatcher,\n\tBitnamiMatcher,\n\tPacmanMatcher,\n\tHexMatcher,\n}\n\ntype MatcherType string\n\nfunc (t MatcherType) String() string {\n\treturn string(t)\n}\n"
  },
  {
    "path": "grype/match/matches.go",
    "content": "package match\n\nimport (\n\t\"sort\"\n\n\t\"github.com/scylladb/go-set/strset\"\n\n\t\"github.com/anchore/grype/grype/pkg\"\n\t\"github.com/anchore/grype/internal/log\"\n)\n\ntype Matches struct {\n\tbyFingerprint     map[Fingerprint]Match\n\tbyCoreFingerprint map[coreFingerprint]map[Fingerprint]struct{}\n\tbyPackage         map[pkg.ID]map[Fingerprint]struct{}\n}\n\nfunc NewMatches(matches ...Match) Matches {\n\tm := newMatches()\n\tm.Add(matches...)\n\treturn m\n}\n\nfunc newMatches() Matches {\n\treturn Matches{\n\t\tbyFingerprint:     make(map[Fingerprint]Match),\n\t\tbyCoreFingerprint: make(map[coreFingerprint]map[Fingerprint]struct{}),\n\t\tbyPackage:         make(map[pkg.ID]map[Fingerprint]struct{}),\n\t}\n}\n\n// GetByPkgID returns a slice of potential matches from an ID\nfunc (r *Matches) GetByPkgID(id pkg.ID) (matches []Match) {\n\tfor fingerprint := range r.byPackage[id] {\n\t\tmatches = append(matches, r.byFingerprint[fingerprint])\n\t}\n\treturn matches\n}\n\n// AllByPkgID returns a map of all matches organized by package ID\nfunc (r *Matches) AllByPkgID() map[pkg.ID][]Match {\n\tmatches := make(map[pkg.ID][]Match)\n\tfor id, fingerprints := range r.byPackage {\n\t\tfor fingerprint := range fingerprints {\n\t\t\tmatches[id] = append(matches[id], r.byFingerprint[fingerprint])\n\t\t}\n\t}\n\treturn matches\n}\n\nfunc (r *Matches) Merge(other Matches) {\n\tfor _, fingerprints := range other.byPackage {\n\t\tfor fingerprint := range fingerprints {\n\t\t\tr.Add(other.byFingerprint[fingerprint])\n\t\t}\n\t}\n}\n\nfunc (r *Matches) Diff(other Matches) *Matches {\n\tdiff := newMatches()\n\tfor fingerprint := range r.byFingerprint {\n\t\tif _, exists := other.byFingerprint[fingerprint]; !exists {\n\t\t\tdiff.Add(r.byFingerprint[fingerprint])\n\t\t}\n\t}\n\treturn &diff\n}\n\nfunc (r *Matches) Add(matches ...Match) {\n\tfor _, newMatch := range matches {\n\t\tnewFp := newMatch.Fingerprint()\n\n\t\t// add or merge the new match with an existing match\n\t\tr.addOrMerge(newMatch, newFp)\n\n\t\t// track common elements (core fingerprint + package index)\n\n\t\tif _, exists := r.byCoreFingerprint[newFp.coreFingerprint]; !exists {\n\t\t\tr.byCoreFingerprint[newFp.coreFingerprint] = make(map[Fingerprint]struct{})\n\t\t}\n\n\t\tr.byCoreFingerprint[newFp.coreFingerprint][newFp] = struct{}{}\n\n\t\tif _, exists := r.byPackage[newMatch.Package.ID]; !exists {\n\t\t\tr.byPackage[newMatch.Package.ID] = make(map[Fingerprint]struct{})\n\t\t}\n\t\tr.byPackage[newMatch.Package.ID][newFp] = struct{}{}\n\t}\n}\n\nfunc (r *Matches) addOrMerge(newMatch Match, newFp Fingerprint) {\n\t// a) if there is an exact fingerprint match, then merge with that\n\t// b) otherwise, look for core fingerprint matches (looser rules)\n\t//   we prefer direct matches to indirect matches:\n\t//    1. if the new match is a direct match and there is an indirect match, replace the indirect match with the direct match\n\t//    2. if the new match is an indirect match and there is a direct match, merge with the existing direct match\n\t// c) this is a new match\n\n\tif existingMatch, exists := r.byFingerprint[newFp]; exists {\n\t\t// case A\n\t\tif err := existingMatch.Merge(newMatch); err != nil {\n\t\t\tlog.WithFields(\"original\", existingMatch.String(), \"new\", newMatch.String(), \"error\", err).Warn(\"unable to merge matches\")\n\t\t\t// at least capture the additional details\n\t\t\texistingMatch.Details = append(existingMatch.Details, newMatch.Details...)\n\t\t}\n\n\t\tr.byFingerprint[newFp] = existingMatch\n\t} else if existingFingerprints, exists := r.byCoreFingerprint[newFp.coreFingerprint]; exists {\n\t\t// case B\n\t\tif !r.mergeCoreMatches(newMatch, newFp, existingFingerprints) {\n\t\t\t// case C (we should not drop this match if we were unable to merge it)\n\t\t\tr.byFingerprint[newFp] = newMatch\n\t\t}\n\t} else {\n\t\t// case C\n\t\tr.byFingerprint[newFp] = newMatch\n\t}\n}\n\nfunc (r *Matches) mergeCoreMatches(newMatch Match, newFp Fingerprint, existingFingerprints map[Fingerprint]struct{}) bool {\n\tfor existingFp := range existingFingerprints {\n\t\texistingMatch := r.byFingerprint[existingFp]\n\n\t\tshouldSupersede := hasMatchType(newMatch.Details, ExactDirectMatch) && hasExclusivelyAnyMatchTypes(existingMatch.Details, ExactIndirectMatch)\n\t\tif shouldSupersede {\n\t\t\t// case B1\n\t\t\tif replaced := r.replace(newMatch, existingFp, newFp, existingMatch.Details...); !replaced {\n\t\t\t\tlog.WithFields(\"original\", existingMatch.String(), \"new\", newMatch.String()).Trace(\"unable to replace match\")\n\t\t\t\t// at least capture the new details\n\t\t\t\texistingMatch.Details = append(existingMatch.Details, newMatch.Details...)\n\t\t\t} else {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\n\t\t// case B2\n\t\tif err := existingMatch.Merge(newMatch); err != nil {\n\t\t\tlog.WithFields(\"original\", existingMatch.String(), \"new\", newMatch.String(), \"error\", err).Trace(\"unable to merge matches\")\n\t\t\t// at least capture the new details\n\t\t\texistingMatch.Details = append(existingMatch.Details, newMatch.Details...)\n\t\t} else {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc (r *Matches) replace(m Match, ogFp, newFp Fingerprint, extraDetails ...Detail) bool {\n\tif ogFp.coreFingerprint != newFp.coreFingerprint {\n\t\treturn false\n\t}\n\n\t// update indexes\n\tfor pkgID, fingerprints := range r.byPackage {\n\t\tif _, exists := fingerprints[ogFp]; exists {\n\t\t\tdelete(fingerprints, ogFp)\n\t\t\tfingerprints[newFp] = struct{}{}\n\t\t\tr.byPackage[pkgID] = fingerprints\n\t\t}\n\t}\n\n\t// update the match\n\tdelete(r.byFingerprint, ogFp)\n\tm.Details = append(m.Details, extraDetails...)\n\tsort.Sort(m.Details)\n\tr.byFingerprint[newFp] = m\n\treturn true\n}\n\nfunc (r *Matches) Enumerate() <-chan Match {\n\tchannel := make(chan Match)\n\tgo func() {\n\t\tdefer close(channel)\n\t\tfor _, match := range r.byFingerprint {\n\t\t\tchannel <- match\n\t\t}\n\t}()\n\treturn channel\n}\n\nfunc (r *Matches) Sorted() []Match {\n\tmatches := make([]Match, 0)\n\tfor m := range r.Enumerate() {\n\t\tmatches = append(matches, m)\n\t}\n\n\tsort.Sort(ByElements(matches))\n\n\treturn matches\n}\n\n// Count returns the total number of matches in a result\nfunc (r *Matches) Count() int {\n\treturn len(r.byFingerprint)\n}\n\nfunc hasMatchType(details Details, ty Type) bool {\n\tfor _, d := range details {\n\t\tif d.Type == ty {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc hasExclusivelyAnyMatchTypes(details Details, tys ...Type) bool {\n\tallowed := strset.New()\n\tfor _, ty := range tys {\n\t\tallowed.Add(string(ty))\n\t}\n\tvar found bool\n\tfor _, d := range details {\n\t\tif allowed.Has(string(d.Type)) {\n\t\t\tfound = true\n\t\t} else {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn found\n}\n"
  },
  {
    "path": "grype/match/matches_test.go",
    "content": "package match\n\nimport (\n\t\"testing\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/google/go-cmp/cmp/cmpopts\"\n\t\"github.com/google/uuid\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/grype/grype/pkg\"\n\t\"github.com/anchore/grype/grype/version\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n\t\"github.com/anchore/syft/syft/file\"\n\tsyftPkg \"github.com/anchore/syft/syft/pkg\"\n)\n\nfunc TestMatchesSortMixedDimensions(t *testing.T) {\n\tfirst := Match{\n\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\tReference: vulnerability.Reference{\n\t\t\t\tID: \"CVE-2020-0010\",\n\t\t\t},\n\t\t},\n\t\tPackage: pkg.Package{\n\t\t\tID:      pkg.ID(uuid.NewString()),\n\t\t\tName:    \"package-b\",\n\t\t\tVersion: \"1.0.0\",\n\t\t\tType:    syftPkg.RpmPkg,\n\t\t},\n\t}\n\tsecond := Match{\n\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\tReference: vulnerability.Reference{\n\t\t\t\tID: \"CVE-2020-0020\",\n\t\t\t},\n\t\t},\n\t\tPackage: pkg.Package{\n\t\t\tID:      pkg.ID(uuid.NewString()),\n\t\t\tName:    \"package-a\",\n\t\t\tVersion: \"1.0.0\",\n\t\t\tType:    syftPkg.NpmPkg,\n\t\t},\n\t}\n\tthird := Match{\n\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\tReference: vulnerability.Reference{\n\t\t\t\tID: \"CVE-2020-0020\",\n\t\t\t},\n\t\t},\n\t\tPackage: pkg.Package{\n\t\t\tID:      pkg.ID(uuid.NewString()),\n\t\t\tName:    \"package-a\",\n\t\t\tVersion: \"2.0.0\",\n\t\t\tType:    syftPkg.RpmPkg,\n\t\t},\n\t}\n\tfourth := Match{\n\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\tReference: vulnerability.Reference{\n\t\t\t\tID: \"CVE-2020-0020\",\n\t\t\t},\n\t\t},\n\t\tPackage: pkg.Package{\n\t\t\tID:      pkg.ID(uuid.NewString()),\n\t\t\tName:    \"package-c\",\n\t\t\tVersion: \"3.0.0\",\n\t\t\tType:    syftPkg.ApkPkg,\n\t\t},\n\t}\n\tfifth := Match{\n\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\tReference: vulnerability.Reference{\n\t\t\t\tID: \"CVE-2020-0020\",\n\t\t\t},\n\t\t},\n\t\tPackage: pkg.Package{\n\t\t\tID:      pkg.ID(uuid.NewString()),\n\t\t\tName:    \"package-d\",\n\t\t\tVersion: \"2.0.0\",\n\t\t\tType:    syftPkg.RpmPkg,\n\t\t},\n\t}\n\tsixth := Match{\n\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\tReference: vulnerability.Reference{\n\t\t\t\tID: \"CVE-2020-0020\",\n\t\t\t},\n\t\t\tFix: vulnerability.Fix{\n\t\t\t\tVersions: []string{\"2.0.0\", \"1.0.0\"},\n\t\t\t},\n\t\t},\n\t\tPackage: pkg.Package{\n\t\t\tID:      pkg.ID(uuid.NewString()),\n\t\t\tName:    \"package-d\",\n\t\t\tVersion: \"2.0.0\",\n\t\t\tType:    syftPkg.RpmPkg,\n\t\t},\n\t}\n\tseventh := Match{\n\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\tReference: vulnerability.Reference{\n\t\t\t\tID: \"CVE-2020-0020\",\n\t\t\t},\n\t\t\tFix: vulnerability.Fix{\n\t\t\t\tVersions: []string{\"2.0.1\"},\n\t\t\t},\n\t\t},\n\t\tPackage: pkg.Package{\n\t\t\tID:      pkg.ID(uuid.NewString()),\n\t\t\tName:    \"package-d\",\n\t\t\tVersion: \"2.0.0\",\n\t\t\tType:    syftPkg.RpmPkg,\n\t\t},\n\t}\n\teighth := Match{\n\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\tReference: vulnerability.Reference{\n\t\t\t\tID: \"CVE-2020-0020\",\n\t\t\t},\n\t\t\tFix: vulnerability.Fix{\n\t\t\t\tVersions: []string{\"3.0.0\"},\n\t\t\t},\n\t\t},\n\t\tPackage: pkg.Package{\n\t\t\tID:        pkg.ID(uuid.NewString()),\n\t\t\tName:      \"package-d\",\n\t\t\tVersion:   \"2.0.0\",\n\t\t\tType:      syftPkg.RpmPkg,\n\t\t\tLocations: file.NewLocationSet(file.NewLocation(\"/some/first-path\")),\n\t\t},\n\t}\n\tninth := Match{\n\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\tReference: vulnerability.Reference{\n\t\t\t\tID: \"CVE-2020-0020\",\n\t\t\t},\n\t\t\tFix: vulnerability.Fix{\n\t\t\t\tVersions: []string{\"3.0.0\"},\n\t\t\t},\n\t\t},\n\t\tPackage: pkg.Package{\n\t\t\tID:        pkg.ID(uuid.NewString()),\n\t\t\tName:      \"package-d\",\n\t\t\tVersion:   \"2.0.0\",\n\t\t\tType:      syftPkg.RpmPkg,\n\t\t\tLocations: file.NewLocationSet(file.NewLocation(\"/some/other-path\")),\n\t\t},\n\t}\n\n\tinput := []Match{\n\t\t// shuffle vulnerability id, package name, package version, and package type\n\t\tninth, fifth, eighth, third, seventh, first, sixth, second, fourth,\n\t}\n\tmatches := NewMatches(input...)\n\n\tassertMatchOrder(t, []Match{first, second, third, fourth, fifth, sixth, seventh, eighth, ninth}, matches.Sorted())\n\n}\n\nfunc TestMatchesSortByVulnerability(t *testing.T) {\n\tfirst := Match{\n\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\tReference: vulnerability.Reference{\n\t\t\t\tID: \"CVE-2020-0010\",\n\t\t\t},\n\t\t},\n\t\tPackage: pkg.Package{\n\t\t\tID:      pkg.ID(uuid.NewString()),\n\t\t\tName:    \"package-b\",\n\t\t\tVersion: \"1.0.0\",\n\t\t\tType:    syftPkg.RpmPkg,\n\t\t},\n\t}\n\tsecond := Match{\n\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\tReference: vulnerability.Reference{\n\t\t\t\tID: \"CVE-2020-0020\",\n\t\t\t},\n\t\t},\n\t\tPackage: pkg.Package{\n\t\t\tID:      pkg.ID(uuid.NewString()),\n\t\t\tName:    \"package-b\",\n\t\t\tVersion: \"1.0.0\",\n\t\t\tType:    syftPkg.RpmPkg,\n\t\t},\n\t}\n\n\tinput := []Match{\n\t\tsecond, first,\n\t}\n\tmatches := NewMatches(input...)\n\n\tassertMatchOrder(t, []Match{first, second}, matches.Sorted())\n\n}\n\nfunc TestMatches_AllByPkgID(t *testing.T) {\n\tfirst := Match{\n\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\tReference: vulnerability.Reference{\n\t\t\t\tID: \"CVE-2020-0010\",\n\t\t\t},\n\t\t},\n\t\tPackage: pkg.Package{\n\t\t\tID:      pkg.ID(uuid.NewString()),\n\t\t\tName:    \"package-b\",\n\t\t\tVersion: \"1.0.0\",\n\t\t\tType:    syftPkg.RpmPkg,\n\t\t},\n\t}\n\tsecond := Match{\n\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\tReference: vulnerability.Reference{\n\t\t\t\tID: \"CVE-2020-0010\",\n\t\t\t},\n\t\t},\n\t\tPackage: pkg.Package{\n\t\t\tID:      pkg.ID(uuid.NewString()),\n\t\t\tName:    \"package-c\",\n\t\t\tVersion: \"1.0.0\",\n\t\t\tType:    syftPkg.RpmPkg,\n\t\t},\n\t}\n\n\tinput := []Match{\n\t\tsecond, first,\n\t}\n\tmatches := NewMatches(input...)\n\n\texpected := map[pkg.ID][]Match{\n\t\tfirst.Package.ID: {\n\t\t\tfirst,\n\t\t},\n\t\tsecond.Package.ID: {\n\t\t\tsecond,\n\t\t},\n\t}\n\n\tassert.Equal(t, expected, matches.AllByPkgID())\n\n}\n\nfunc TestMatchesSortByPackage(t *testing.T) {\n\tfirst := Match{\n\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\tReference: vulnerability.Reference{\n\t\t\t\tID: \"CVE-2020-0010\",\n\t\t\t},\n\t\t},\n\t\tPackage: pkg.Package{\n\t\t\tID:      pkg.ID(uuid.NewString()),\n\t\t\tName:    \"package-b\",\n\t\t\tVersion: \"1.0.0\",\n\t\t\tType:    syftPkg.RpmPkg,\n\t\t},\n\t}\n\tsecond := Match{\n\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\tReference: vulnerability.Reference{\n\t\t\t\tID: \"CVE-2020-0010\",\n\t\t\t},\n\t\t},\n\t\tPackage: pkg.Package{\n\t\t\tID:      pkg.ID(uuid.NewString()),\n\t\t\tName:    \"package-c\",\n\t\t\tVersion: \"1.0.0\",\n\t\t\tType:    syftPkg.RpmPkg,\n\t\t},\n\t}\n\n\tinput := []Match{\n\t\tsecond, first,\n\t}\n\tmatches := NewMatches(input...)\n\n\tassertMatchOrder(t, []Match{first, second}, matches.Sorted())\n\n}\n\nfunc TestMatchesSortByPackageVersion(t *testing.T) {\n\tfirst := Match{\n\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\tReference: vulnerability.Reference{\n\t\t\t\tID: \"CVE-2020-0010\",\n\t\t\t},\n\t\t},\n\t\tPackage: pkg.Package{\n\t\t\tID:      pkg.ID(uuid.NewString()),\n\t\t\tName:    \"package-b\",\n\t\t\tVersion: \"1.0.0\",\n\t\t\tType:    syftPkg.RpmPkg,\n\t\t},\n\t}\n\tsecond := Match{\n\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\tReference: vulnerability.Reference{\n\t\t\t\tID: \"CVE-2020-0010\",\n\t\t\t},\n\t\t},\n\t\tPackage: pkg.Package{\n\t\t\tID:      pkg.ID(uuid.NewString()),\n\t\t\tName:    \"package-b\",\n\t\t\tVersion: \"2.0.0\",\n\t\t\tType:    syftPkg.RpmPkg,\n\t\t},\n\t}\n\n\tinput := []Match{\n\t\tsecond, first,\n\t}\n\tmatches := NewMatches(input...)\n\n\tassertMatchOrder(t, []Match{first, second}, matches.Sorted())\n\n}\n\nfunc TestMatchesSortByPackageType(t *testing.T) {\n\tfirst := Match{\n\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\tReference: vulnerability.Reference{\n\t\t\t\tID: \"CVE-2020-0010\",\n\t\t\t},\n\t\t},\n\t\tPackage: pkg.Package{\n\t\t\tID:      pkg.ID(uuid.NewString()),\n\t\t\tName:    \"package-b\",\n\t\t\tVersion: \"1.0.0\",\n\t\t\tType:    syftPkg.ApkPkg,\n\t\t},\n\t}\n\tsecond := Match{\n\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\tReference: vulnerability.Reference{\n\t\t\t\tID: \"CVE-2020-0010\",\n\t\t\t},\n\t\t},\n\t\tPackage: pkg.Package{\n\t\t\tID:      pkg.ID(uuid.NewString()),\n\t\t\tName:    \"package-b\",\n\t\t\tVersion: \"1.0.0\",\n\t\t\tType:    syftPkg.RpmPkg,\n\t\t},\n\t}\n\n\tinput := []Match{\n\t\tsecond, first,\n\t}\n\tmatches := NewMatches(input...)\n\n\tassertMatchOrder(t, []Match{first, second}, matches.Sorted())\n\n}\n\nfunc assertMatchOrder(t *testing.T, expected, actual []Match) {\n\n\tvar expectedStr []string\n\tfor _, e := range expected {\n\t\texpectedStr = append(expectedStr, e.Package.Name)\n\t}\n\n\tvar actualStr []string\n\tfor _, a := range actual {\n\t\tactualStr = append(actualStr, a.Package.Name)\n\t}\n\n\t// makes this easier on the eyes to sanity check...\n\trequire.Equal(t, expectedStr, actualStr)\n\n\t// make certain the fields are what you'd expect\n\tassert.Equal(t, expected, actual)\n}\n\nfunc assertIgnoredMatchOrder(t *testing.T, expected, actual []IgnoredMatch) {\n\n\tvar expectedStr []string\n\tfor _, e := range expected {\n\t\texpectedStr = append(expectedStr, e.Package.Name)\n\t}\n\n\tvar actualStr []string\n\tfor _, a := range actual {\n\t\tactualStr = append(actualStr, a.Package.Name)\n\t}\n\n\t// makes this easier on the eyes to sanity check...\n\trequire.Equal(t, expectedStr, actualStr)\n\n\t// make certain the fields are what you'd expect\n\tassert.Equal(t, expected, actual)\n}\n\nfunc TestMatches_Diff(t *testing.T) {\n\ta := Match{\n\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\tReference: vulnerability.Reference{\n\t\t\t\tID:        \"vuln-a\",\n\t\t\t\tNamespace: \"name-a\",\n\t\t\t},\n\t\t},\n\t\tPackage: pkg.Package{\n\t\t\tID: \"package-a\",\n\t\t},\n\t}\n\n\tb := Match{\n\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\tReference: vulnerability.Reference{\n\t\t\t\tID:        \"vuln-b\",\n\t\t\t\tNamespace: \"name-b\",\n\t\t\t},\n\t\t},\n\t\tPackage: pkg.Package{\n\t\t\tID: \"package-b\",\n\t\t},\n\t}\n\n\tc := Match{\n\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\tReference: vulnerability.Reference{\n\t\t\t\tID:        \"vuln-c\",\n\t\t\t\tNamespace: \"name-c\",\n\t\t\t},\n\t\t},\n\t\tPackage: pkg.Package{\n\t\t\tID: \"package-c\",\n\t\t},\n\t}\n\n\ttests := []struct {\n\t\tname    string\n\t\tsubject Matches\n\t\tother   Matches\n\t\twant    Matches\n\t}{\n\t\t{\n\t\t\tname:    \"no diff\",\n\t\t\tsubject: NewMatches(a, b, c),\n\t\t\tother:   NewMatches(a, b, c),\n\t\t\twant:    newMatches(),\n\t\t},\n\t\t{\n\t\t\tname:    \"extra items in subject\",\n\t\t\tsubject: NewMatches(a, b, c),\n\t\t\tother:   NewMatches(a, b),\n\t\t\twant:    NewMatches(c),\n\t\t},\n\t\t{\n\t\t\t// this demonstrates that this is not meant to implement a symmetric diff\n\t\t\tname:    \"extra items in other (results in no diff)\",\n\t\t\tsubject: NewMatches(a, b),\n\t\t\tother:   NewMatches(a, b, c),\n\t\t\twant:    NewMatches(),\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tassert.Equalf(t, &tt.want, tt.subject.Diff(tt.other), \"Diff(%v)\", tt.other)\n\t\t})\n\t}\n}\n\nfunc TestMatches_Add_Merge(t *testing.T) {\n\tcommonVuln := \"CVE-2023-0001\"\n\tcommonNamespace := \"namespace1\"\n\tcommonVulnerability := vulnerability.Vulnerability{\n\t\tReference: vulnerability.Reference{\n\t\t\tID:        commonVuln,\n\t\t\tNamespace: commonNamespace,\n\t\t},\n\t\tConstraint: func() version.Constraint {\n\t\t\tc, err := version.GetConstraint(\"< 1.0.0\", version.SemanticFormat)\n\t\t\trequire.NoError(t, err)\n\t\t\treturn c\n\t\t}(),\n\t\tFix: vulnerability.Fix{\n\t\t\tVersions: []string{\"1.0.0\"},\n\t\t},\n\t}\n\n\tcommonDirectDetail := Detail{\n\t\tType:       ExactDirectMatch,\n\t\tSearchedBy: \"attr1\",\n\t\tFound:      \"value1\",\n\t\tMatcher:    \"matcher1\",\n\t}\n\n\tmatchPkg1Direct := Match{\n\t\tVulnerability: commonVulnerability,\n\t\tPackage: pkg.Package{\n\t\t\tID: \"pkg1\",\n\t\t},\n\t\tDetails: Details{\n\t\t\tcommonDirectDetail,\n\t\t},\n\t}\n\n\tmatchPkg2Indirect := Match{\n\t\tVulnerability: commonVulnerability,\n\t\tPackage: pkg.Package{\n\t\t\tID: \"pkg2\",\n\t\t},\n\t\tDetails: Details{\n\t\t\t{\n\t\t\t\tType:       ExactIndirectMatch,\n\t\t\t\tSearchedBy: \"attr2\",\n\t\t\t\tFound:      \"value2\",\n\t\t\t\tMatcher:    \"matcher2\",\n\t\t\t},\n\t\t},\n\t}\n\n\ttests := []struct {\n\t\tname            string\n\t\tmatches         []Match\n\t\texpectedMatches map[string][]Match\n\t}{\n\t\t{\n\t\t\tname:    \"adds new match without merging\",\n\t\t\tmatches: []Match{matchPkg1Direct, matchPkg2Indirect},\n\t\t\texpectedMatches: map[string][]Match{\n\t\t\t\t\"pkg1\": {\n\t\t\t\t\tmatchPkg1Direct,\n\t\t\t\t},\n\t\t\t\t\"pkg2\": {\n\t\t\t\t\tmatchPkg2Indirect,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"merges matches with identical fingerprints\",\n\t\t\tmatches: []Match{\n\t\t\t\tmatchPkg1Direct,\n\t\t\t\t{\n\t\t\t\t\tVulnerability: matchPkg1Direct.Vulnerability,\n\t\t\t\t\tPackage:       matchPkg1Direct.Package,\n\t\t\t\t\tDetails: Details{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tType:       ExactIndirectMatch, // different!\n\t\t\t\t\t\t\tSearchedBy: \"attr2\",            // different!\n\t\t\t\t\t\t\tFound:      \"value2\",           // different!\n\t\t\t\t\t\t\tMatcher:    \"matcher2\",         // different!\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedMatches: map[string][]Match{\n\t\t\t\t\"pkg1\": {\n\t\t\t\t\t{\n\t\t\t\t\t\tVulnerability: commonVulnerability,\n\t\t\t\t\t\tPackage:       matchPkg1Direct.Package,\n\t\t\t\t\t\tDetails: Details{\n\t\t\t\t\t\t\tcommonDirectDetail,\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tType:       ExactIndirectMatch,\n\t\t\t\t\t\t\t\tSearchedBy: \"attr2\",\n\t\t\t\t\t\t\t\tFound:      \"value2\",\n\t\t\t\t\t\t\t\tMatcher:    \"matcher2\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"merges matches with different fingerprints but semantically the same\",\n\t\t\tmatches: []Match{\n\t\t\t\t{\n\t\t\t\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\t\tID:        commonVuln,\n\t\t\t\t\t\t\tNamespace: commonNamespace,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tConstraint: func() version.Constraint { // different!\n\t\t\t\t\t\t\tc, err := version.GetConstraint(\"< 3.2.12\", version.SemanticFormat)\n\t\t\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\t\t\treturn c\n\t\t\t\t\t\t}(),\n\t\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\t\tVersions: []string{\"3.2.12\"}, // different!\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tPackage: matchPkg1Direct.Package,\n\t\t\t\t\tDetails: Details{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tType:       ExactIndirectMatch, // different!\n\t\t\t\t\t\t\tSearchedBy: \"attr1\",\n\t\t\t\t\t\t\tFound:      \"value1\",\n\t\t\t\t\t\t\tMatcher:    \"matcher1\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tmatchPkg1Direct,\n\t\t\t},\n\t\t\texpectedMatches: map[string][]Match{\n\t\t\t\t\"pkg1\": {\n\t\t\t\t\t{\n\t\t\t\t\t\tVulnerability: commonVulnerability,\n\t\t\t\t\t\tPackage:       matchPkg1Direct.Package,\n\t\t\t\t\t\tDetails: Details{\n\t\t\t\t\t\t\tcommonDirectDetail, // sorts to first (direct should be prioritized over indirect)\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tType:       ExactIndirectMatch, // different!\n\t\t\t\t\t\t\t\tSearchedBy: \"attr1\",\n\t\t\t\t\t\t\t\tFound:      \"value1\",\n\t\t\t\t\t\t\t\tMatcher:    \"matcher1\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"does not merge matches with different fingerprints but semantically the same when matched by CPE\",\n\t\t\tmatches: []Match{\n\t\t\t\t{\n\t\t\t\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\t\tID:        commonVuln,\n\t\t\t\t\t\t\tNamespace: commonNamespace,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tConstraint: func() version.Constraint { // different!\n\t\t\t\t\t\t\tc, err := version.GetConstraint(\"< 3.2.12\", version.SemanticFormat)\n\t\t\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\t\t\treturn c\n\t\t\t\t\t\t}(),\n\t\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\t\tVersions: []string{\"3.2.12\"}, // different!\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tPackage: matchPkg1Direct.Package,\n\t\t\t\t\tDetails: Details{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tType:       CPEMatch, // different!\n\t\t\t\t\t\t\tSearchedBy: \"attr1\",\n\t\t\t\t\t\t\tFound:      \"value1\",\n\t\t\t\t\t\t\tMatcher:    \"matcher1\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tmatchPkg1Direct,\n\t\t\t},\n\t\t\texpectedMatches: map[string][]Match{\n\t\t\t\t\"pkg1\": {\n\t\t\t\t\t{\n\t\t\t\t\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\t\t\tID:        commonVuln,\n\t\t\t\t\t\t\t\tNamespace: commonNamespace,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tConstraint: func() version.Constraint { // different!\n\t\t\t\t\t\t\t\tc, err := version.GetConstraint(\"< 3.2.12\", version.SemanticFormat)\n\t\t\t\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\t\t\t\treturn c\n\t\t\t\t\t\t\t}(),\n\t\t\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\t\t\tVersions: []string{\"3.2.12\"}, // different!\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tPackage: matchPkg1Direct.Package,\n\t\t\t\t\t\tDetails: Details{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tType:       CPEMatch, // different!\n\t\t\t\t\t\t\t\tSearchedBy: \"attr1\",\n\t\t\t\t\t\t\t\tFound:      \"value1\",\n\t\t\t\t\t\t\t\tMatcher:    \"matcher1\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tmatchPkg1Direct,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tcmpOpts := []cmp.Option{\n\t\tcmpopts.IgnoreUnexported(vulnerability.Vulnerability{}, pkg.Package{}, file.Location{}, file.LocationSet{}),\n\t\tcmpopts.IgnoreFields(vulnerability.Vulnerability{}, \"Constraint\"),\n\t\tcmpopts.EquateEmpty(),\n\t\tcmpopts.SortSlices(func(a, b Match) bool {\n\t\t\treturn ByElements([]Match{a, b}).Less(0, 1)\n\t\t}),\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tactual := NewMatches(tt.matches...)\n\n\t\t\trequire.NotEmpty(t, tt.expectedMatches)\n\n\t\t\tfor pkgId, expected := range tt.expectedMatches {\n\t\t\t\tstoredMatches := actual.GetByPkgID(pkg.ID(pkgId))\n\n\t\t\t\tif d := cmp.Diff(expected, storedMatches, cmpOpts...); d != \"\" {\n\t\t\t\t\tt.Errorf(\"unexpected matches for %q (-want, +got): %s\", pkgId, d)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tassert.Len(t, actual.byPackage, len(tt.expectedMatches))\n\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/match/provider.go",
    "content": "package match\n\ntype ExclusionProvider interface {\n\tIgnoreRules(vulnerabilityID string) ([]IgnoreRule, error)\n}\n"
  },
  {
    "path": "grype/match/results.go",
    "content": "package match\n\nimport (\n\t\"fmt\"\n\t\"sort\"\n\n\t\"github.com/scylladb/go-set/strset\"\n)\n\ntype CPEParameters struct {\n\tNamespace string           `json:\"namespace\"`\n\tCPEs      []string         `json:\"cpes\"`\n\tPackage   PackageParameter `json:\"package\"`\n}\n\ntype PackageParameter struct {\n\tName    string `json:\"name\"`\n\tVersion string `json:\"version\"`\n}\n\nfunc (i *CPEParameters) Merge(other CPEParameters) error {\n\tif i.Namespace != other.Namespace {\n\t\treturn fmt.Errorf(\"namespaces do not match\")\n\t}\n\n\texistingCPEs := strset.New(i.CPEs...)\n\tnewCPEs := strset.New(other.CPEs...)\n\tmergedCPEs := strset.Union(existingCPEs, newCPEs).List()\n\tsort.Strings(mergedCPEs)\n\ti.CPEs = mergedCPEs\n\treturn nil\n}\n\ntype CPEResult struct {\n\tVulnerabilityID   string   `json:\"vulnerabilityID\"`\n\tVersionConstraint string   `json:\"versionConstraint\"`\n\tCPEs              []string `json:\"cpes\"`\n}\n\nfunc (h CPEResult) Equals(other CPEResult) bool {\n\tif h.VersionConstraint != other.VersionConstraint {\n\t\treturn false\n\t}\n\n\tif len(h.CPEs) != len(other.CPEs) {\n\t\treturn false\n\t}\n\n\tfor i := range h.CPEs {\n\t\tif h.CPEs[i] != other.CPEs[i] {\n\t\t\treturn false\n\t\t}\n\t}\n\n\treturn true\n}\n\ntype DistroParameters struct {\n\tDistro    DistroIdentification `json:\"distro\"`\n\tPackage   PackageParameter     `json:\"package\"`\n\tNamespace string               `json:\"namespace\"`\n}\n\ntype DistroIdentification struct {\n\tType    string `json:\"type\"`\n\tVersion string `json:\"version\"`\n}\n\nfunc (d *DistroParameters) Merge(other DistroParameters) error {\n\tif d.Namespace != other.Namespace {\n\t\treturn fmt.Errorf(\"namespaces do not match\")\n\t}\n\tif d.Distro.Type != other.Distro.Type {\n\t\treturn fmt.Errorf(\"distro types do not match\")\n\t}\n\tif d.Distro.Version != other.Distro.Version {\n\t\treturn fmt.Errorf(\"distro versions do not match\")\n\t}\n\tif d.Package.Name != other.Package.Name {\n\t\treturn fmt.Errorf(\"package names do not match\")\n\t}\n\tif d.Package.Version != other.Package.Version {\n\t\treturn fmt.Errorf(\"package versions do not match\")\n\t}\n\treturn nil\n}\n\ntype DistroResult struct {\n\tVulnerabilityID   string `json:\"vulnerabilityID\"`\n\tVersionConstraint string `json:\"versionConstraint\"`\n}\n\nfunc (d DistroResult) Equals(other DistroResult) bool {\n\treturn d.VulnerabilityID == other.VulnerabilityID &&\n\t\td.VersionConstraint == other.VersionConstraint\n}\n\ntype EcosystemParameters struct {\n\tLanguage  string           `json:\"language\"`\n\tNamespace string           `json:\"namespace\"`\n\tPackage   PackageParameter `json:\"package\"`\n}\n\nfunc (e *EcosystemParameters) Merge(other EcosystemParameters) error {\n\tif e.Namespace != other.Namespace {\n\t\treturn fmt.Errorf(\"namespaces do not match\")\n\t}\n\tif e.Language != other.Language {\n\t\treturn fmt.Errorf(\"languages do not match\")\n\t}\n\tif e.Package.Name != other.Package.Name {\n\t\treturn fmt.Errorf(\"package names do not match\")\n\t}\n\tif e.Package.Version != other.Package.Version {\n\t\treturn fmt.Errorf(\"package versions do not match\")\n\t}\n\treturn nil\n}\n\ntype EcosystemResult struct {\n\tVulnerabilityID   string `json:\"vulnerabilityID\"`\n\tVersionConstraint string `json:\"versionConstraint\"`\n}\n"
  },
  {
    "path": "grype/match/sort.go",
    "content": "package match\n\nimport (\n\t\"sort\"\n\t\"strings\"\n)\n\nvar _ sort.Interface = (*ByElements)(nil)\n\ntype ByElements []Match\n\n// Len is the number of elements in the collection.\nfunc (m ByElements) Len() int {\n\treturn len(m)\n}\n\n// Less reports whether the element with index i should sort before the element with index j.\nfunc (m ByElements) Less(i, j int) bool {\n\tif m[i].Vulnerability.ID == m[j].Vulnerability.ID {\n\t\tif m[i].Package.Name == m[j].Package.Name {\n\t\t\tif m[i].Package.Version == m[j].Package.Version {\n\t\t\t\tif m[i].Package.Type == m[j].Package.Type {\n\t\t\t\t\t// this is an approximate ordering, but is not accurate in terms of semver and other version formats\n\t\t\t\t\t// but stability is what is important here, not the accuracy of the sort.\n\t\t\t\t\tfixVersions1 := m[i].Vulnerability.Fix.Versions\n\t\t\t\t\tfixVersions2 := m[j].Vulnerability.Fix.Versions\n\t\t\t\t\tsort.Strings(fixVersions1)\n\t\t\t\t\tsort.Strings(fixVersions2)\n\t\t\t\t\tfixStr1 := strings.Join(fixVersions1, \",\")\n\t\t\t\t\tfixStr2 := strings.Join(fixVersions2, \",\")\n\n\t\t\t\t\tif fixStr1 == fixStr2 {\n\t\t\t\t\t\tloc1 := m[i].Package.Locations.ToSlice()\n\t\t\t\t\t\tloc2 := m[j].Package.Locations.ToSlice()\n\t\t\t\t\t\tvar locStr1 string\n\t\t\t\t\t\tfor _, location := range loc1 {\n\t\t\t\t\t\t\tlocStr1 += location.RealPath\n\t\t\t\t\t\t}\n\t\t\t\t\t\tvar locStr2 string\n\t\t\t\t\t\tfor _, location := range loc2 {\n\t\t\t\t\t\t\tlocStr2 += location.RealPath\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\treturn locStr1 < locStr2\n\t\t\t\t\t}\n\t\t\t\t\treturn fixStr1 < fixStr2\n\t\t\t\t}\n\t\t\t\treturn m[i].Package.Type < m[j].Package.Type\n\t\t\t}\n\t\t\treturn m[i].Package.Version < m[j].Package.Version\n\t\t}\n\t\treturn m[i].Package.Name < m[j].Package.Name\n\t}\n\treturn m[i].Vulnerability.ID < m[j].Vulnerability.ID\n}\n\n// Swap swaps the elements with indexes i and j.\nfunc (m ByElements) Swap(i, j int) {\n\tm[i], m[j] = m[j], m[i]\n}\n"
  },
  {
    "path": "grype/match/type.go",
    "content": "package match\n\nimport (\n\t\"github.com/anchore/grype/grype/pkg\"\n)\n\nconst (\n\tExactDirectMatch   Type = \"exact-direct-match\"\n\tExactIndirectMatch Type = \"exact-indirect-match\"\n\tCPEMatch           Type = \"cpe-match\"\n)\n\nvar typeOrder = map[Type]int{\n\tExactDirectMatch:   1,\n\tExactIndirectMatch: 2,\n\tCPEMatch:           3,\n}\n\ntype Type string\n\nfunc (t Type) String() string {\n\treturn string(t)\n}\n\nfunc ConvertToIndirectMatches(matches []Match, p pkg.Package) {\n\tfor idx := range matches {\n\t\tfor dIdx := range matches[idx].Details {\n\t\t\t// only override the match details to \"indirect\" if the match details are explicitly indicate a \"direct\" match\n\t\t\tif matches[idx].Details[dIdx].Type == ExactDirectMatch {\n\t\t\t\tmatches[idx].Details[dIdx].Type = ExactIndirectMatch\n\t\t\t}\n\t\t}\n\t\t// we always override the package to the direct package\n\t\tmatches[idx].Package = p\n\t}\n}\n"
  },
  {
    "path": "grype/matcher/apk/matcher.go",
    "content": "package apk\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\n\t\"github.com/anchore/grype/grype/match\"\n\t\"github.com/anchore/grype/grype/matcher/internal\"\n\t\"github.com/anchore/grype/grype/pkg\"\n\t\"github.com/anchore/grype/grype/search\"\n\t\"github.com/anchore/grype/grype/version\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n\t\"github.com/anchore/grype/internal/log\"\n\tsyftPkg \"github.com/anchore/syft/syft/pkg\"\n)\n\nvar (\n\tnakVersionString = version.MustGetConstraint(\"< 0\", version.ApkFormat).String()\n\n\t// nakConstraint checks the exact version string for being an APK version with \"< 0\"\n\tnakConstraint = search.ByConstraintFunc(func(c version.Constraint) (bool, error) {\n\t\treturn c.String() == nakVersionString, nil\n\t})\n)\n\ntype Matcher struct{}\n\nfunc (m *Matcher) PackageTypes() []syftPkg.Type {\n\treturn []syftPkg.Type{syftPkg.ApkPkg}\n}\n\nfunc (m *Matcher) Type() match.MatcherType {\n\treturn match.ApkMatcher\n}\n\nfunc (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoreFilter, error) {\n\tvar matches []match.Match\n\tvar ignoreFilters []match.IgnoreFilter\n\n\t// direct matches with package itself (+ distro-fixed ignore rules when metadata implements FileOwner)\n\tdirectMatches, directIgnores, err := m.findMatchesForPackage(store, p, nil)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\tmatches = append(matches, directMatches...)\n\tignoreFilters = append(ignoreFilters, directIgnores...)\n\n\t// indirect matches, via package's origin package\n\tindirectMatches, indirectIgnores, err := m.findMatchesForOriginPackage(store, p)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\tmatches = append(matches, indirectMatches...)\n\tignoreFilters = append(ignoreFilters, indirectIgnores...)\n\n\t// APK sources are also able to NAK vulnerabilities, so we want to return these as explicit ignores in order\n\t// to allow rules later to use these to ignore \"the same\" vulnerability found in \"the same\" locations\n\tnaks, err := m.findNaksForPackage(store, p)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\tignoreFilters = append(ignoreFilters, naks...)\n\n\treturn matches, ignoreFilters, nil\n}\n\n//nolint:funlen,gocognit\nfunc (m *Matcher) cpeMatchesWithoutSecDBFixes(provider vulnerability.Provider, p pkg.Package) ([]match.Match, error) {\n\t// find CPE-indexed vulnerability matches specific to the given package name and version\n\tcpeMatches, err := internal.MatchPackageByCPEs(provider, p, m.Type())\n\tif err != nil {\n\t\tlog.WithFields(\"package\", p.Name, \"error\", err).Debug(\"failed to find CPE matches for package\")\n\t}\n\tif p.Distro == nil {\n\t\treturn cpeMatches, nil\n\t}\n\n\tcpeMatchesByID := matchesByID(cpeMatches)\n\n\t// remove cpe matches where there is an entry in the secDB for the particular package-vulnerability pairing, and the\n\t// installed package version is >= the fixed in version for the secDB record.\n\tsecDBVulnerabilities, err := provider.FindVulnerabilities(\n\t\tsearch.ByPackageName(p.Name),\n\t\tsearch.ByDistro(*p.Distro))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfor _, upstreamPkg := range pkg.UpstreamPackages(p) {\n\t\tsecDBVulnerabilitiesForUpstream, err := provider.FindVulnerabilities(\n\t\t\tsearch.ByPackageName(upstreamPkg.Name),\n\t\t\tsearch.ByDistro(*upstreamPkg.Distro))\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tsecDBVulnerabilities = append(secDBVulnerabilities, secDBVulnerabilitiesForUpstream...)\n\t}\n\n\tsecDBVulnerabilitiesByID := vulnerabilitiesByID(secDBVulnerabilities)\n\n\tverObj := version.New(p.Version, pkg.VersionFormat(p))\n\n\tvar finalCpeMatches []match.Match\n\ncveLoop:\n\tfor id, cpeMatchesForID := range cpeMatchesByID {\n\t\t// check to see if there is a secdb entry for this ID (CVE)\n\t\tsecDBVulnerabilitiesForID, exists := secDBVulnerabilitiesByID[id]\n\t\tif !exists {\n\t\t\t// does not exist in secdb, so the CPE record(s) should be added to the final results\n\n\t\t\t// remove fixed-in versions, since NVD doesn't know when Alpine will fix things\n\t\t\tfor _, nvdOnlyMatch := range cpeMatchesForID {\n\t\t\t\tif len(nvdOnlyMatch.Vulnerability.Fix.Versions) > 0 {\n\t\t\t\t\tnvdOnlyMatch.Vulnerability.Fix = vulnerability.Fix{\n\t\t\t\t\t\tState: vulnerability.FixStateUnknown,\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tfinalCpeMatches = append(finalCpeMatches, nvdOnlyMatch)\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\t// there is a secdb entry...\n\t\tfor _, vuln := range secDBVulnerabilitiesForID {\n\t\t\t// ...is there a fixed in entry? (should always be yes)\n\t\t\tif len(vuln.Fix.Versions) == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// ...is the current package vulnerable?\n\t\t\tvulnerable, err := vuln.Constraint.Satisfied(verObj)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tif vulnerable {\n\t\t\t\t// if there is at least one vulnerable entry, then all CPE record(s) should be added to the final results\n\t\t\t\tfinalCpeMatches = append(finalCpeMatches, cpeMatchesForID...)\n\t\t\t\tcontinue cveLoop\n\t\t\t}\n\t\t}\n\t}\n\treturn finalCpeMatches, nil\n}\n\nfunc deduplicateMatches(secDBMatches, cpeMatches []match.Match) (matches []match.Match) {\n\t// add additional unique matches from CPE source that is unique from the SecDB matches\n\tsecDBMatchesByID := matchesByID(secDBMatches)\n\tcpeMatchesByID := matchesByID(cpeMatches)\n\tfor id, cpeMatchesForID := range cpeMatchesByID {\n\t\t// by this point all matches have been verified to be vulnerable within the given package version relative to the vulnerability source.\n\t\t// now we will add unique CPE candidates that were not found in secdb.\n\t\tif _, exists := secDBMatchesByID[id]; !exists {\n\t\t\t// add the new CPE-based record (e.g. NVD) since it was not found in secDB\n\t\t\tmatches = append(matches, cpeMatchesForID...)\n\t\t}\n\t}\n\treturn matches\n}\n\nfunc matchesByID(matches []match.Match) map[string][]match.Match {\n\tvar results = make(map[string][]match.Match)\n\tfor _, secDBMatch := range matches {\n\t\tresults[secDBMatch.Vulnerability.ID] = append(results[secDBMatch.Vulnerability.ID], secDBMatch)\n\t}\n\treturn results\n}\n\nfunc vulnerabilitiesByID(vulns []vulnerability.Vulnerability) map[string][]vulnerability.Vulnerability {\n\tvar results = make(map[string][]vulnerability.Vulnerability)\n\tfor _, vuln := range vulns {\n\t\tresults[vuln.ID] = append(results[vuln.ID], vuln)\n\t}\n\n\treturn results\n}\n\nfunc (m *Matcher) findMatchesForPackage(store vulnerability.Provider, p pkg.Package, catalogPkg *pkg.Package) ([]match.Match, []match.IgnoreFilter, error) {\n\t// find SecDB matches for the given package name and version\n\t// APK doesn't use epochs, so pass nil for the config\n\tsecDBMatches, secDBIgnores, err := internal.MatchPackageByDistroWithOwnedFiles(store, p, catalogPkg, m.Type(), nil)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\t// TODO: are there other errors that we should handle here that causes this to short circuit\n\tcpeMatches, err := m.cpeMatchesWithoutSecDBFixes(store, p)\n\tif err != nil && !errors.Is(err, internal.ErrEmptyCPEMatch) {\n\t\treturn nil, nil, err\n\t}\n\n\tvar matches []match.Match\n\n\t// keep all secdb matches, as this is an authoritative source\n\tmatches = append(matches, secDBMatches...)\n\n\t// keep only unique CPE matches\n\tmatches = append(matches, deduplicateMatches(secDBMatches, cpeMatches)...)\n\n\treturn matches, secDBIgnores, nil\n}\n\nfunc (m *Matcher) findMatchesForOriginPackage(store vulnerability.Provider, catalogPkg pkg.Package) ([]match.Match, []match.IgnoreFilter, error) {\n\tvar matches []match.Match\n\tvar ignores []match.IgnoreFilter\n\n\tfor _, indirectPackage := range pkg.UpstreamPackages(catalogPkg) {\n\t\tindirectMatches, indirectIgnores, err := m.findMatchesForPackage(store, indirectPackage, &catalogPkg)\n\t\tif err != nil {\n\t\t\treturn nil, nil, fmt.Errorf(\"failed to find vulnerabilities for apk upstream source package: %w\", err)\n\t\t}\n\t\tmatches = append(matches, indirectMatches...)\n\t\tignores = append(ignores, indirectIgnores...)\n\t}\n\n\t// we want to make certain that we are tracking the match based on the package from the SBOM (not the indirect package)\n\t// however, we also want to keep the indirect package around for future reference\n\tmatch.ConvertToIndirectMatches(matches, catalogPkg)\n\n\treturn matches, ignores, nil\n}\n\n// ownedFilesFromMetadata returns the files owned by a package if its metadata implements pkg.FileOwner.\nfunc ownedFilesFromMetadata(p pkg.Package) []string {\n\tif fo, ok := p.Metadata.(pkg.FileOwner); ok {\n\t\treturn fo.OwnedFiles()\n\t}\n\treturn nil\n}\n\n// NAK entries are those reported as explicitly not vulnerable by the upstream provider,\n// for example this entry is present in the v5 database:\n// 312891,CVE-2020-7224,openvpn,alpine:distro:alpine:3.10,,< 0,apk,,\"[{\"\"id\"\":\"\"CVE-2020-7224\"\",\"\"namespace\"\":\"\"nvd:cpe\"\"}]\",\"[\"\"0\"\"]\",fixed,\n// which indicates, for the alpine:3.10 distro, package openvpn is not vulnerable to CVE-2020-7224\n// we want to report these NAK entries as match.IgnoredMatch, to allow for later processing to create ignore rules\n// based on packages which overlap by location, such as a python binary found in addition to the python APK entry --\n// we want to NAK this vulnerability for BOTH packages\nfunc (m *Matcher) findNaksForPackage(provider vulnerability.Provider, p pkg.Package) ([]match.IgnoreFilter, error) {\n\tif p.Distro == nil {\n\t\treturn nil, nil\n\t}\n\n\t// get all the direct naks\n\tnaks, err := provider.FindVulnerabilities(\n\t\tsearch.ByDistro(*p.Distro),\n\t\tsearch.ByPackageName(p.Name),\n\t\tnakConstraint,\n\t)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// append all the upstream naks\n\tfor _, upstreamPkg := range pkg.UpstreamPackages(p) {\n\t\tupstreamNaks, err := provider.FindVulnerabilities(\n\t\t\tsearch.ByDistro(*upstreamPkg.Distro),\n\t\t\tsearch.ByPackageName(upstreamPkg.Name),\n\t\t\tnakConstraint,\n\t\t)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tnaks = append(naks, upstreamNaks...)\n\t}\n\n\tpaths := ownedFilesFromMetadata(p)\n\tif len(paths) == 0 {\n\t\treturn nil, nil\n\t}\n\n\tvar ignores []match.IgnoreFilter\n\tfor _, nak := range naks {\n\t\tfor _, path := range paths {\n\t\t\tignores = append(ignores,\n\t\t\t\tmatch.IgnoreRule{\n\t\t\t\t\tVulnerability:  nak.ID,\n\t\t\t\t\tIncludeAliases: true,\n\t\t\t\t\tReason:         \"Explicit APK NAK\",\n\t\t\t\t\tPackage: match.IgnoreRulePackage{\n\t\t\t\t\t\tLocation: path,\n\t\t\t\t\t},\n\t\t\t\t})\n\t\t}\n\t}\n\n\treturn ignores, nil\n}\n"
  },
  {
    "path": "grype/matcher/apk/matcher_test.go",
    "content": "package apk\n\nimport (\n\t\"testing\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/google/go-cmp/cmp/cmpopts\"\n\t\"github.com/google/uuid\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/grype/grype/distro\"\n\t\"github.com/anchore/grype/grype/match\"\n\t\"github.com/anchore/grype/grype/pkg\"\n\t\"github.com/anchore/grype/grype/version\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n\t\"github.com/anchore/grype/grype/vulnerability/mock\"\n\t\"github.com/anchore/syft/syft/cpe\"\n\tsyftPkg \"github.com/anchore/syft/syft/pkg\"\n)\n\nfunc TestSecDBOnlyMatch(t *testing.T) {\n\tsecDbVuln := vulnerability.Vulnerability{\n\t\tReference: vulnerability.Reference{\n\t\t\t// ID doesn't match - this is the key for comparison in the matcher\n\t\t\tID:        \"CVE-2020-2\",\n\t\t\tNamespace: \"secdb:distro:alpine:3.12\",\n\t\t},\n\t\tPackageName: \"libvncserver\",\n\t\tConstraint:  version.MustGetConstraint(\"<= 0.9.11\", version.ApkFormat),\n\t}\n\n\tvp := mock.VulnerabilityProvider(secDbVuln)\n\n\tm := Matcher{}\n\td := distro.New(distro.Alpine, \"3.12.0\", \"\")\n\n\tp := pkg.Package{\n\t\tID:      pkg.ID(uuid.NewString()),\n\t\tName:    \"libvncserver\",\n\t\tVersion: \"0.9.9\",\n\t\tType:    syftPkg.ApkPkg,\n\t\tDistro:  d,\n\t\tCPEs: []cpe.CPE{\n\t\t\tcpe.Must(\"cpe:2.3:a:*:libvncserver:0.9.9:*:*:*:*:*:*:*\", \"\"),\n\t\t},\n\t}\n\n\texpected := []match.Match{\n\t\t{\n\n\t\t\tVulnerability: secDbVuln,\n\t\t\tPackage:       p,\n\t\t\tDetails: []match.Detail{\n\t\t\t\t{\n\t\t\t\t\tType:       match.ExactDirectMatch,\n\t\t\t\t\tConfidence: 1.0,\n\t\t\t\t\tSearchedBy: match.DistroParameters{\n\t\t\t\t\t\tDistro: match.DistroIdentification{\n\t\t\t\t\t\t\tType:    d.Type.String(),\n\t\t\t\t\t\t\tVersion: d.Version,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tPackage: match.PackageParameter{\n\t\t\t\t\t\t\tName:    \"libvncserver\",\n\t\t\t\t\t\t\tVersion: \"0.9.9\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tNamespace: \"secdb:distro:alpine:3.12\",\n\t\t\t\t\t},\n\t\t\t\t\tFound: match.DistroResult{\n\t\t\t\t\t\tVulnerabilityID:   \"CVE-2020-2\",\n\t\t\t\t\t\tVersionConstraint: secDbVuln.Constraint.String(),\n\t\t\t\t\t},\n\t\t\t\t\tMatcher: match.ApkMatcher,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tactual, _, err := m.Match(vp, p)\n\tassert.NoError(t, err)\n\n\tassertMatches(t, expected, actual)\n}\n\nfunc TestBothSecdbAndNvdMatches(t *testing.T) {\n\t// NVD and Alpine's secDB both have the same CVE ID for the package\n\tnvdVuln := vulnerability.Vulnerability{\n\t\tReference: vulnerability.Reference{\n\t\t\tID:        \"CVE-2020-1\",\n\t\t\tNamespace: \"nvd:cpe\",\n\t\t},\n\t\tPackageName: \"libvncserver\",\n\t\tConstraint:  version.MustGetConstraint(\"<= 0.9.11\", version.UnknownFormat),\n\t\tCPEs: []cpe.CPE{\n\t\t\tcpe.Must(`cpe:2.3:a:lib_vnc_project-\\(server\\):libvncserver:*:*:*:*:*:*:*:*`, \"\"),\n\t\t},\n\t}\n\n\tsecDbVuln := vulnerability.Vulnerability{\n\t\tReference: vulnerability.Reference{\n\t\t\t// ID *does* match - this is the key for comparison in the matcher\n\t\t\tID:        \"CVE-2020-1\",\n\t\t\tNamespace: \"secdb:distro:alpine:3.12\",\n\t\t},\n\t\tPackageName: \"libvncserver\",\n\t\tConstraint:  version.MustGetConstraint(\"<= 0.9.11\", version.ApkFormat),\n\t}\n\n\tvp := mock.VulnerabilityProvider(nvdVuln, secDbVuln)\n\n\tm := Matcher{}\n\td := distro.New(distro.Alpine, \"3.12.0\", \"\")\n\n\tp := pkg.Package{\n\t\tID:      pkg.ID(uuid.NewString()),\n\t\tName:    \"libvncserver\",\n\t\tVersion: \"0.9.9\",\n\t\tType:    syftPkg.ApkPkg,\n\t\tDistro:  d,\n\t\tCPEs: []cpe.CPE{\n\t\t\tcpe.Must(\"cpe:2.3:a:*:libvncserver:0.9.9:*:*:*:*:*:*:*\", \"\"),\n\t\t},\n\t}\n\n\texpected := []match.Match{\n\t\t{\n\t\t\t// ensure the SECDB record is preferred over the NVD record\n\t\t\tVulnerability: secDbVuln,\n\t\t\tPackage:       p,\n\t\t\tDetails: []match.Detail{\n\t\t\t\t{\n\t\t\t\t\tType:       match.ExactDirectMatch,\n\t\t\t\t\tConfidence: 1.0,\n\t\t\t\t\tSearchedBy: match.DistroParameters{\n\t\t\t\t\t\tDistro: match.DistroIdentification{\n\t\t\t\t\t\t\tType:    d.Type.String(),\n\t\t\t\t\t\t\tVersion: d.Version,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tPackage: match.PackageParameter{\n\t\t\t\t\t\t\tName:    \"libvncserver\",\n\t\t\t\t\t\t\tVersion: \"0.9.9\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tNamespace: \"secdb:distro:alpine:3.12\",\n\t\t\t\t\t},\n\t\t\t\t\tFound: match.DistroResult{\n\t\t\t\t\t\tVulnerabilityID:   \"CVE-2020-1\",\n\t\t\t\t\t\tVersionConstraint: secDbVuln.Constraint.String(),\n\t\t\t\t\t},\n\t\t\t\t\tMatcher: match.ApkMatcher,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tactual, _, err := m.Match(vp, p)\n\tassert.NoError(t, err)\n\n\tassertMatches(t, expected, actual)\n}\n\nfunc TestBothSecdbAndNvdMatches_DifferentFixInfo(t *testing.T) {\n\t// NVD and Alpine's secDB both have the same CVE ID for the package\n\tnvdVuln := vulnerability.Vulnerability{\n\t\tReference: vulnerability.Reference{\n\t\t\tID:        \"CVE-2020-1\",\n\t\t\tNamespace: \"nvd:cpe\",\n\t\t},\n\t\tPackageName: \"libvncserver\",\n\t\tConstraint:  version.MustGetConstraint(\"< 1.0.0\", version.UnknownFormat),\n\t\tCPEs: []cpe.CPE{\n\t\t\tcpe.Must(`cpe:2.3:a:lib_vnc_project-\\(server\\):libvncserver:*:*:*:*:*:*:*:*`, \"\"),\n\t\t},\n\t\tFix: vulnerability.Fix{\n\t\t\tVersions: []string{\"1.0.0\"},\n\t\t\tState:    vulnerability.FixStateFixed,\n\t\t},\n\t}\n\n\tsecDbVuln := vulnerability.Vulnerability{\n\t\tReference: vulnerability.Reference{\n\t\t\t// ID *does* match - this is the key for comparison in the matcher\n\t\t\tID:        \"CVE-2020-1\",\n\t\t\tNamespace: \"secdb:distro:alpine:3.12\",\n\t\t},\n\t\tPackageName: \"libvncserver\",\n\t\tConstraint:  version.MustGetConstraint(\"< 0.9.12\", version.ApkFormat),\n\t\t// SecDB indicates Alpine have backported a fix to v0.9...\n\t\tFix: vulnerability.Fix{\n\t\t\tVersions: []string{\"0.9.12\"},\n\t\t\tState:    vulnerability.FixStateFixed,\n\t\t},\n\t}\n\tvp := mock.VulnerabilityProvider(nvdVuln, secDbVuln)\n\tm := Matcher{}\n\td := distro.New(distro.Alpine, \"3.12.0\", \"\")\n\n\tp := pkg.Package{\n\t\tID:      pkg.ID(uuid.NewString()),\n\t\tName:    \"libvncserver\",\n\t\tVersion: \"0.9.9\",\n\t\tType:    syftPkg.ApkPkg,\n\t\tDistro:  d,\n\t\tCPEs: []cpe.CPE{\n\t\t\tcpe.Must(\"cpe:2.3:a:*:libvncserver:0.9.9:*:*:*:*:*:*:*\", \"\"),\n\t\t},\n\t}\n\n\texpected := []match.Match{\n\t\t{\n\t\t\t// ensure the SECDB record is preferred over the NVD record\n\t\t\tVulnerability: secDbVuln,\n\t\t\tPackage:       p,\n\t\t\tDetails: []match.Detail{\n\t\t\t\t{\n\t\t\t\t\tType:       match.ExactDirectMatch,\n\t\t\t\t\tConfidence: 1.0,\n\t\t\t\t\tSearchedBy: match.DistroParameters{\n\t\t\t\t\t\tDistro: match.DistroIdentification{\n\t\t\t\t\t\t\tType:    d.Type.String(),\n\t\t\t\t\t\t\tVersion: d.Version,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tPackage: match.PackageParameter{\n\t\t\t\t\t\t\tName:    \"libvncserver\",\n\t\t\t\t\t\t\tVersion: \"0.9.9\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tNamespace: \"secdb:distro:alpine:3.12\",\n\t\t\t\t\t},\n\t\t\t\t\tFound: match.DistroResult{\n\t\t\t\t\t\tVulnerabilityID:   \"CVE-2020-1\",\n\t\t\t\t\t\tVersionConstraint: secDbVuln.Constraint.String(),\n\t\t\t\t\t},\n\t\t\t\t\tMatcher: match.ApkMatcher,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tactual, _, err := m.Match(vp, p)\n\tassert.NoError(t, err)\n\n\tassertMatches(t, expected, actual)\n}\n\nfunc TestBothSecdbAndNvdMatches_DifferentPackageName(t *testing.T) {\n\t// NVD and Alpine's secDB both have the same CVE ID for the package\n\tnvdVuln := vulnerability.Vulnerability{\n\t\tReference: vulnerability.Reference{\n\t\t\tID:        \"CVE-2020-1\",\n\t\t\tNamespace: \"nvd:cpe\",\n\t\t},\n\t\tPackageName: \"libvncserver\",\n\t\tConstraint:  version.MustGetConstraint(\"<= 0.9.11\", version.UnknownFormat),\n\t\t// Note: the product name is NOT the same as the target package name\n\t\tCPEs: []cpe.CPE{\n\t\t\tcpe.Must(\"cpe:2.3:a:lib_vnc_project-(server):libvncumbrellaproject:*:*:*:*:*:*:*:*\", \"\"),\n\t\t},\n\t}\n\n\tsecDbVuln := vulnerability.Vulnerability{\n\t\tReference: vulnerability.Reference{\n\t\t\t// ID *does* match - this is the key for comparison in the matcher\n\t\t\tID:        \"CVE-2020-1\",\n\t\t\tNamespace: \"secdb:distro:alpine:3.12\",\n\t\t},\n\t\tPackageName: \"libvncserver\",\n\t\tConstraint:  version.MustGetConstraint(\"<= 0.9.11\", version.ApkFormat),\n\t}\n\n\tvp := mock.VulnerabilityProvider(nvdVuln, secDbVuln)\n\n\tm := Matcher{}\n\td := distro.New(distro.Alpine, \"3.12.0\", \"\")\n\n\tp := pkg.Package{\n\t\tID:      pkg.ID(uuid.NewString()),\n\t\tName:    \"libvncserver\",\n\t\tVersion: \"0.9.9\",\n\t\tType:    syftPkg.ApkPkg,\n\t\tDistro:  d,\n\t\tCPEs: []cpe.CPE{\n\t\t\t// Note: the product name is NOT the same as the package name\n\t\t\tcpe.Must(\"cpe:2.3:a:*:libvncumbrellaproject:0.9.9:*:*:*:*:*:*:*\", \"\"),\n\t\t},\n\t}\n\n\texpected := []match.Match{\n\t\t{\n\t\t\t// ensure the SECDB record is preferred over the NVD record\n\t\t\tVulnerability: secDbVuln,\n\t\t\tPackage:       p,\n\t\t\tDetails: []match.Detail{\n\t\t\t\t{\n\t\t\t\t\tType:       match.ExactDirectMatch,\n\t\t\t\t\tConfidence: 1.0,\n\t\t\t\t\tSearchedBy: match.DistroParameters{\n\t\t\t\t\t\tDistro: match.DistroIdentification{\n\t\t\t\t\t\t\tType:    d.Type.String(),\n\t\t\t\t\t\t\tVersion: d.Version,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tPackage: match.PackageParameter{\n\t\t\t\t\t\t\tName:    \"libvncserver\",\n\t\t\t\t\t\t\tVersion: \"0.9.9\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tNamespace: \"secdb:distro:alpine:3.12\",\n\t\t\t\t\t},\n\t\t\t\t\tFound: match.DistroResult{\n\t\t\t\t\t\tVulnerabilityID:   \"CVE-2020-1\",\n\t\t\t\t\t\tVersionConstraint: secDbVuln.Constraint.String(),\n\t\t\t\t\t},\n\t\t\t\t\tMatcher: match.ApkMatcher,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tactual, _, err := m.Match(vp, p)\n\tassert.NoError(t, err)\n\n\tassertMatches(t, expected, actual)\n}\n\nfunc TestNvdOnlyMatches(t *testing.T) {\n\tnvdVuln := vulnerability.Vulnerability{\n\t\tReference: vulnerability.Reference{\n\t\t\tID:        \"CVE-2020-1\",\n\t\t\tNamespace: \"nvd:cpe\",\n\t\t},\n\t\tPackageName: \"libvncserver\",\n\t\tConstraint:  version.MustGetConstraint(\"<= 0.9.11\", version.UnknownFormat),\n\t\tCPEs: []cpe.CPE{\n\t\t\tcpe.Must(`cpe:2.3:a:lib_vnc_project-\\(server\\):lib/vncserver:*:*:*:*:*:*:*:*`, \"\"),\n\t\t},\n\t}\n\tvp := mock.VulnerabilityProvider(nvdVuln)\n\n\tm := Matcher{}\n\td := distro.New(distro.Alpine, \"3.12.0\", \"\")\n\n\tp := pkg.Package{\n\t\tID:      pkg.ID(uuid.NewString()),\n\t\tName:    \"libvncserver\",\n\t\tVersion: \"0.9.9\",\n\t\tType:    syftPkg.ApkPkg,\n\t\tDistro:  d,\n\t\tCPEs: []cpe.CPE{\n\t\t\tcpe.Must(\"cpe:2.3:a:*:lib/vncserver:0.9.9:*:*:*:*:*:*:*\", \"\"),\n\t\t},\n\t}\n\n\texpected := []match.Match{\n\t\t{\n\n\t\t\tVulnerability: nvdVuln,\n\t\t\tPackage:       p,\n\t\t\tDetails: []match.Detail{\n\t\t\t\t{\n\t\t\t\t\tType:       match.CPEMatch,\n\t\t\t\t\tConfidence: 0.9,\n\t\t\t\t\tSearchedBy: match.CPEParameters{\n\t\t\t\t\t\tCPEs:      []string{\"cpe:2.3:a:*:lib\\\\/vncserver:0.9.9:*:*:*:*:*:*:*\"},\n\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\tPackage: match.PackageParameter{\n\t\t\t\t\t\t\tName:    \"libvncserver\",\n\t\t\t\t\t\t\tVersion: \"0.9.9\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tFound: match.CPEResult{\n\t\t\t\t\t\t// use .String() for proper escaping\n\t\t\t\t\t\tCPEs:              []string{nvdVuln.CPEs[0].Attributes.String()},\n\t\t\t\t\t\tVersionConstraint: nvdVuln.Constraint.String(),\n\t\t\t\t\t\tVulnerabilityID:   \"CVE-2020-1\",\n\t\t\t\t\t},\n\t\t\t\t\tMatcher: match.ApkMatcher,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tactual, _, err := m.Match(vp, p)\n\tassert.NoError(t, err)\n\n\tassertMatches(t, expected, actual)\n}\n\nfunc TestNvdOnlyMatches_FixInNvd(t *testing.T) {\n\tnvdVuln := vulnerability.Vulnerability{\n\t\tReference: vulnerability.Reference{\n\t\t\tID:        \"CVE-2020-1\",\n\t\t\tNamespace: \"nvd:cpe\",\n\t\t},\n\t\tPackageName: \"libvncserver\",\n\t\tConstraint:  version.MustGetConstraint(\"< 0.9.11\", version.UnknownFormat),\n\t\tCPEs: []cpe.CPE{\n\t\t\tcpe.Must(`cpe:2.3:a:lib_vnc_project-\\(server\\):libvncserver:*:*:*:*:*:*:*:*`, \"\"),\n\t\t},\n\t\tFix: vulnerability.Fix{\n\t\t\tVersions: []string{\"0.9.12\"},\n\t\t\tState:    vulnerability.FixStateFixed,\n\t\t},\n\t}\n\tvp := mock.VulnerabilityProvider(nvdVuln)\n\n\tm := Matcher{}\n\td := distro.New(distro.Alpine, \"3.12.0\", \"\")\n\n\tp := pkg.Package{\n\t\tID:      pkg.ID(uuid.NewString()),\n\t\tName:    \"libvncserver\",\n\t\tVersion: \"0.9.9\",\n\t\tType:    syftPkg.ApkPkg,\n\t\tDistro:  d,\n\t\tCPEs: []cpe.CPE{\n\t\t\tcpe.Must(\"cpe:2.3:a:*:libvncserver:0.9.9:*:*:*:*:*:*:*\", \"\"),\n\t\t},\n\t}\n\n\tvulnFound := nvdVuln\n\t// Important: for alpine matcher, fix version can come from secDB but _not_ from\n\t// NVD data.\n\tvulnFound.Fix = vulnerability.Fix{State: vulnerability.FixStateUnknown}\n\n\texpected := []match.Match{\n\t\t{\n\t\t\tVulnerability: vulnFound,\n\t\t\tPackage:       p,\n\t\t\tDetails: []match.Detail{\n\t\t\t\t{\n\t\t\t\t\tType:       match.CPEMatch,\n\t\t\t\t\tConfidence: 0.9,\n\t\t\t\t\tSearchedBy: match.CPEParameters{\n\t\t\t\t\t\tCPEs:      []string{\"cpe:2.3:a:*:libvncserver:0.9.9:*:*:*:*:*:*:*\"},\n\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\tPackage: match.PackageParameter{\n\t\t\t\t\t\t\tName:    \"libvncserver\",\n\t\t\t\t\t\t\tVersion: \"0.9.9\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tFound: match.CPEResult{\n\t\t\t\t\t\tCPEs:              []string{vulnFound.CPEs[0].Attributes.String()},\n\t\t\t\t\t\tVersionConstraint: vulnFound.Constraint.String(),\n\t\t\t\t\t\tVulnerabilityID:   \"CVE-2020-1\",\n\t\t\t\t\t},\n\t\t\t\t\tMatcher: match.ApkMatcher,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tactual, _, err := m.Match(vp, p)\n\tassert.NoError(t, err)\n\n\tassertMatches(t, expected, actual)\n}\n\nfunc TestNvdMatchesProperVersionFiltering(t *testing.T) {\n\tnvdVulnMatch := vulnerability.Vulnerability{\n\t\tReference: vulnerability.Reference{\n\t\t\tID:        \"CVE-2020-1\",\n\t\t\tNamespace: \"nvd:cpe\",\n\t\t},\n\t\tPackageName: \"libvncserver\",\n\t\tConstraint:  version.MustGetConstraint(\"<= 0.9.11\", version.UnknownFormat),\n\t\tCPEs: []cpe.CPE{\n\t\t\tcpe.Must(`cpe:2.3:a:lib_vnc_project-\\(server\\):libvncserver:*:*:*:*:*:*:*:*`, \"\"),\n\t\t},\n\t}\n\tnvdVulnNoMatch := vulnerability.Vulnerability{\n\t\tReference: vulnerability.Reference{\n\t\t\tID:        \"CVE-2020-2\",\n\t\t\tNamespace: \"nvd:cpe\",\n\t\t},\n\t\tPackageName: \"libvncserver\",\n\t\tConstraint:  version.MustGetConstraint(\"< 0.9.11\", version.UnknownFormat),\n\t\tCPEs: []cpe.CPE{\n\t\t\tcpe.Must(`cpe:2.3:a:lib_vnc_project-\\(server\\):libvncserver:*:*:*:*:*:*:*:*`, \"\"),\n\t\t},\n\t}\n\tvp := mock.VulnerabilityProvider(nvdVulnMatch, nvdVulnNoMatch)\n\n\tm := Matcher{}\n\td := distro.New(distro.Alpine, \"3.12.0\", \"\")\n\n\tp := pkg.Package{\n\t\tID:      pkg.ID(uuid.NewString()),\n\t\tName:    \"libvncserver\",\n\t\tVersion: \"0.9.11-r10\",\n\t\tType:    syftPkg.ApkPkg,\n\t\tDistro:  d,\n\t\tCPEs: []cpe.CPE{\n\t\t\tcpe.Must(\"cpe:2.3:a:*:libvncserver:0.9.11:*:*:*:*:*:*:*\", \"\"),\n\t\t},\n\t}\n\n\texpected := []match.Match{\n\t\t{\n\t\t\tVulnerability: nvdVulnMatch,\n\t\t\tPackage:       p,\n\t\t\tDetails: []match.Detail{\n\t\t\t\t{\n\t\t\t\t\tType:       match.CPEMatch,\n\t\t\t\t\tConfidence: 0.9,\n\t\t\t\t\tSearchedBy: match.CPEParameters{\n\t\t\t\t\t\tCPEs:      []string{\"cpe:2.3:a:*:libvncserver:0.9.11:*:*:*:*:*:*:*\"},\n\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\tPackage: match.PackageParameter{\n\t\t\t\t\t\t\tName:    \"libvncserver\",\n\t\t\t\t\t\t\tVersion: \"0.9.11-r10\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tFound: match.CPEResult{\n\t\t\t\t\t\tCPEs:              []string{nvdVulnMatch.CPEs[0].Attributes.String()},\n\t\t\t\t\t\tVersionConstraint: nvdVulnMatch.Constraint.String(),\n\t\t\t\t\t\tVulnerabilityID:   \"CVE-2020-1\",\n\t\t\t\t\t},\n\t\t\t\t\tMatcher: match.ApkMatcher,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tactual, _, err := m.Match(vp, p)\n\tassert.NoError(t, err)\n\n\tassertMatches(t, expected, actual)\n}\n\nfunc TestNvdMatchesWithSecDBFix(t *testing.T) {\n\tnvdVuln := vulnerability.Vulnerability{\n\t\tReference: vulnerability.Reference{\n\t\t\tID:        \"CVE-2020-1\",\n\t\t\tNamespace: \"nvd:cpe\",\n\t\t},\n\t\tPackageName: \"libvncserver\",\n\t\tConstraint:  version.MustGetConstraint(\"> 0.9.0, < 0.10.0\", version.UnknownFormat), // note: this is not normal NVD configuration, but has the desired effect of a \"wide net\" for vulnerable indication\n\t\tCPEs: []cpe.CPE{\n\t\t\tcpe.Must(`cpe:2.3:a:lib_vnc_project-\\(server\\):libvncserver:*:*:*:*:*:*:*:*`, \"\"),\n\t\t},\n\t}\n\n\tsecDbVuln := vulnerability.Vulnerability{\n\t\tReference: vulnerability.Reference{\n\t\t\tID:        \"CVE-2020-1\",\n\t\t\tNamespace: \"secdb:distro:alpine:3.12\",\n\t\t},\n\t\tPackageName: \"libvncserver\",\n\t\tConstraint:  version.MustGetConstraint(\"< 0.9.11\", version.ApkFormat), // note: this does NOT include 0.9.11, so NVD and SecDB mismatch here... secDB should trump in this case\n\t}\n\n\tvp := mock.VulnerabilityProvider(nvdVuln, secDbVuln)\n\n\tm := Matcher{}\n\td := distro.New(distro.Alpine, \"3.12.0\", \"\")\n\n\tp := pkg.Package{\n\t\tID:      pkg.ID(uuid.NewString()),\n\t\tName:    \"libvncserver\",\n\t\tVersion: \"0.9.11\",\n\t\tType:    syftPkg.ApkPkg,\n\t\tDistro:  d,\n\t\tCPEs: []cpe.CPE{\n\t\t\tcpe.Must(\"cpe:2.3:a:*:libvncserver:0.9.9:*:*:*:*:*:*:*\", \"\"),\n\t\t},\n\t}\n\n\tvar expected []match.Match\n\n\tactual, _, err := m.Match(vp, p)\n\tassert.NoError(t, err)\n\n\tassertMatches(t, expected, actual)\n}\n\nfunc TestNvdMatchesNoConstraintWithSecDBFix(t *testing.T) {\n\tnvdVuln := vulnerability.Vulnerability{\n\t\tReference: vulnerability.Reference{\n\t\t\tID:        \"CVE-2020-1\",\n\t\t\tNamespace: \"nvd:cpe\",\n\t\t},\n\t\tPackageName: \"libvncserver\",\n\t\tConstraint:  version.MustGetConstraint(\"\", version.UnknownFormat), // note: empty value indicates that all versions are vulnerable\n\t\tCPEs: []cpe.CPE{\n\t\t\tcpe.Must(`cpe:2.3:a:lib_vnc_project-\\(server\\):libvncserver:*:*:*:*:*:*:*:*`, \"\"),\n\t\t},\n\t}\n\n\tsecDbVuln := vulnerability.Vulnerability{\n\t\tReference: vulnerability.Reference{\n\t\t\tID:        \"CVE-2020-1\",\n\t\t\tNamespace: \"secdb:distro:alpine:3.12\",\n\t\t},\n\t\tPackageName: \"libvncserver\",\n\t\tConstraint:  version.MustGetConstraint(\"< 0.9.11\", version.ApkFormat),\n\t}\n\n\tvp := mock.VulnerabilityProvider(nvdVuln, secDbVuln)\n\n\tm := Matcher{}\n\td := distro.New(distro.Alpine, \"3.12.0\", \"\")\n\n\tp := pkg.Package{\n\t\tID:      pkg.ID(uuid.NewString()),\n\t\tName:    \"libvncserver\",\n\t\tVersion: \"0.9.11\",\n\t\tType:    syftPkg.ApkPkg,\n\t\tDistro:  d,\n\t\tCPEs: []cpe.CPE{\n\t\t\tcpe.Must(\"cpe:2.3:a:*:libvncserver:0.9.9:*:*:*:*:*:*:*\", \"\"),\n\t\t},\n\t}\n\n\tvar expected []match.Match\n\n\tactual, _, err := m.Match(vp, p)\n\tassert.NoError(t, err)\n\n\tassertMatches(t, expected, actual)\n}\n\nfunc TestNVDMatchCanceledByOriginPackageInSecDB(t *testing.T) {\n\tnvdVuln := vulnerability.Vulnerability{\n\t\tReference: vulnerability.Reference{\n\t\t\tID:        \"CVE-2015-3211\",\n\t\t\tNamespace: \"nvd:cpe\",\n\t\t},\n\t\tPackageName: \"php-fpm\",\n\t\tConstraint:  version.MustGetConstraint(\"\", version.UnknownFormat),\n\t\tCPEs: []cpe.CPE{\n\t\t\tcpe.Must(\"cpe:2.3:a:php-fpm:php-fpm:-:*:*:*:*:*:*:*\", \"\"),\n\t\t},\n\t}\n\tsecDBVuln := vulnerability.Vulnerability{\n\t\tReference: vulnerability.Reference{\n\t\t\tID:        \"CVE-2015-3211\",\n\t\t\tNamespace: \"wolfi:distro:wolfi:rolling\",\n\t\t},\n\t\tPackageName: \"php-8.3\",\n\t\tConstraint:  version.MustGetConstraint(\"< 0\", version.ApkFormat),\n\t}\n\tvp := mock.VulnerabilityProvider(nvdVuln, secDBVuln)\n\n\tm := Matcher{}\n\td := distro.New(distro.Wolfi, \"\", \"\")\n\n\tp := pkg.Package{\n\t\tID:      pkg.ID(uuid.NewString()),\n\t\tName:    \"php-8.3-fpm\", // the package will not match anything\n\t\tVersion: \"8.3.11-r0\",\n\t\tType:    syftPkg.ApkPkg,\n\t\tDistro:  d,\n\t\tCPEs: []cpe.CPE{\n\t\t\tcpe.Must(\"cpe:2.3:a:php-fpm:php-fpm:8.3.11-r0:*:*:*:*:*:*:*\", \"\"),\n\t\t},\n\t\tUpstreams: []pkg.UpstreamPackage{\n\t\t\t{\n\t\t\t\tName:    \"php-8.3\", // this upstream should match\n\t\t\t\tVersion: \"8.3.11-r0\",\n\t\t\t},\n\t\t},\n\t}\n\n\tvar expected []match.Match\n\n\tactual, _, err := m.Match(vp, p)\n\tassert.NoError(t, err)\n\n\tassertMatches(t, expected, actual)\n}\n\nfunc TestDistroMatchBySourceIndirection(t *testing.T) {\n\n\tsecDbVuln := vulnerability.Vulnerability{\n\t\tReference: vulnerability.Reference{\n\t\t\t// ID doesn't match - this is the key for comparison in the matcher\n\t\t\tID:        \"CVE-2020-2\",\n\t\t\tNamespace: \"secdb:distro:alpine:3.12\",\n\t\t},\n\t\tPackageName: \"musl\",\n\t\tConstraint:  version.MustGetConstraint(\"<= 1.3.3-r0\", version.ApkFormat),\n\t}\n\tvp := mock.VulnerabilityProvider(secDbVuln)\n\n\tm := Matcher{}\n\td := distro.New(distro.Alpine, \"3.12.0\", \"\")\n\n\tp := pkg.Package{\n\t\tID:      pkg.ID(uuid.NewString()),\n\t\tName:    \"musl-utils\",\n\t\tVersion: \"1.3.2-r0\",\n\t\tType:    syftPkg.ApkPkg,\n\t\tDistro:  d,\n\t\tUpstreams: []pkg.UpstreamPackage{\n\t\t\t{\n\t\t\t\tName: \"musl\",\n\t\t\t},\n\t\t},\n\t\tCPEs: []cpe.CPE{\n\t\t\tcpe.Must(\"cpe:2.3:a:musl-utils:musl-utils:*:*:*:*:*:*:*:*\", cpe.GeneratedSource),\n\t\t},\n\t}\n\n\texpected := []match.Match{\n\t\t{\n\n\t\t\tVulnerability: secDbVuln,\n\t\t\tPackage:       p,\n\t\t\tDetails: []match.Detail{\n\t\t\t\t{\n\t\t\t\t\tType:       match.ExactIndirectMatch,\n\t\t\t\t\tConfidence: 1.0,\n\t\t\t\t\tSearchedBy: match.DistroParameters{\n\t\t\t\t\t\tDistro: match.DistroIdentification{\n\t\t\t\t\t\t\tType:    d.Type.String(),\n\t\t\t\t\t\t\tVersion: d.Version,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tPackage: match.PackageParameter{\n\t\t\t\t\t\t\tName:    \"musl\",\n\t\t\t\t\t\t\tVersion: p.Version,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tNamespace: \"secdb:distro:alpine:3.12\",\n\t\t\t\t\t},\n\t\t\t\t\tFound: match.DistroResult{\n\t\t\t\t\t\tVulnerabilityID:   \"CVE-2020-2\",\n\t\t\t\t\t\tVersionConstraint: secDbVuln.Constraint.String(),\n\t\t\t\t\t},\n\t\t\t\t\tMatcher: match.ApkMatcher,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tactual, _, err := m.Match(vp, p)\n\tassert.NoError(t, err)\n\n\tassertMatches(t, expected, actual)\n}\n\nfunc TestSecDBMatchesStillCountedWithCpeErrors(t *testing.T) {\n\t// this should match the test package\n\t// the test package will have no CPE causing an error,\n\t// but the error should not cause the secDB matches to fail\n\tsecDbVuln := vulnerability.Vulnerability{\n\t\tReference: vulnerability.Reference{\n\t\t\tID:        \"CVE-2020-2\",\n\t\t\tNamespace: \"secdb:distro:alpine:3.12\",\n\t\t},\n\t\tPackageName: \"musl\",\n\t\tConstraint:  version.MustGetConstraint(\"<= 1.3.3-r0\", version.ApkFormat),\n\t}\n\n\tvp := mock.VulnerabilityProvider(secDbVuln)\n\n\tm := Matcher{}\n\td := distro.New(distro.Alpine, \"3.12.0\", \"\")\n\n\tp := pkg.Package{\n\t\tID:      pkg.ID(uuid.NewString()),\n\t\tName:    \"musl-utils\",\n\t\tVersion: \"1.3.2-r0\",\n\t\tType:    syftPkg.ApkPkg,\n\t\tDistro:  d,\n\t\tUpstreams: []pkg.UpstreamPackage{\n\t\t\t{\n\t\t\t\tName: \"musl\",\n\t\t\t},\n\t\t},\n\t\tCPEs: []cpe.CPE{},\n\t}\n\n\texpected := []match.Match{\n\t\t{\n\n\t\t\tVulnerability: secDbVuln,\n\t\t\tPackage:       p,\n\t\t\tDetails: []match.Detail{\n\t\t\t\t{\n\t\t\t\t\tType:       match.ExactIndirectMatch,\n\t\t\t\t\tConfidence: 1.0,\n\t\t\t\t\tSearchedBy: match.DistroParameters{\n\t\t\t\t\t\tDistro: match.DistroIdentification{\n\t\t\t\t\t\t\tType:    d.Type.String(),\n\t\t\t\t\t\t\tVersion: d.Version,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tPackage: match.PackageParameter{\n\t\t\t\t\t\t\tName:    \"musl\",\n\t\t\t\t\t\t\tVersion: p.Version,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tNamespace: \"secdb:distro:alpine:3.12\",\n\t\t\t\t\t},\n\t\t\t\t\tFound: match.DistroResult{\n\t\t\t\t\t\tVulnerabilityID:   \"CVE-2020-2\",\n\t\t\t\t\t\tVersionConstraint: secDbVuln.Constraint.String(),\n\t\t\t\t\t},\n\t\t\t\t\tMatcher: match.ApkMatcher,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tactual, _, err := m.Match(vp, p)\n\tassert.NoError(t, err)\n\n\tassertMatches(t, expected, actual)\n}\n\nfunc TestNVDMatchBySourceIndirection(t *testing.T) {\n\tnvdVuln := vulnerability.Vulnerability{\n\t\tReference: vulnerability.Reference{\n\t\t\tID:        \"CVE-2020-1\",\n\t\t\tNamespace: \"nvd:cpe\",\n\t\t},\n\t\tPackageName: \"musl\",\n\t\tConstraint:  version.MustGetConstraint(\"<= 1.3.3-r0\", version.UnknownFormat),\n\t\tCPEs: []cpe.CPE{\n\t\t\tcpe.Must(\"cpe:2.3:a:musl:musl:*:*:*:*:*:*:*:*\", \"\"),\n\t\t},\n\t}\n\tvp := mock.VulnerabilityProvider(nvdVuln)\n\n\tm := Matcher{}\n\td := distro.New(distro.Alpine, \"3.12.0\", \"\")\n\n\tp := pkg.Package{\n\t\tID:      pkg.ID(uuid.NewString()),\n\t\tName:    \"musl-utils\",\n\t\tVersion: \"1.3.2-r0\",\n\t\tType:    syftPkg.ApkPkg,\n\t\tDistro:  d,\n\t\tCPEs: []cpe.CPE{\n\t\t\tcpe.Must(\"cpe:2.3:a:musl-utils:musl-utils:*:*:*:*:*:*:*:*\", \"\"),\n\t\t\tcpe.Must(\"cpe:2.3:a:musl-utils:musl-utils:*:*:*:*:*:*:*:*\", \"\"),\n\t\t},\n\t\tUpstreams: []pkg.UpstreamPackage{\n\t\t\t{\n\t\t\t\tName: \"musl\",\n\t\t\t},\n\t\t},\n\t}\n\n\texpected := []match.Match{\n\t\t{\n\t\t\tVulnerability: nvdVuln,\n\t\t\tPackage:       p,\n\t\t\tDetails: []match.Detail{\n\t\t\t\t{\n\t\t\t\t\tType:       match.CPEMatch,\n\t\t\t\t\tConfidence: 0.9,\n\t\t\t\t\tSearchedBy: match.CPEParameters{\n\t\t\t\t\t\tCPEs:      []string{\"cpe:2.3:a:musl:musl:1.3.2-r0:*:*:*:*:*:*:*\"},\n\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\tPackage: match.PackageParameter{\n\t\t\t\t\t\t\tName:    \"musl\",\n\t\t\t\t\t\t\tVersion: \"1.3.2-r0\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tFound: match.CPEResult{\n\t\t\t\t\t\tCPEs:              []string{nvdVuln.CPEs[0].Attributes.String()},\n\t\t\t\t\t\tVersionConstraint: nvdVuln.Constraint.String(),\n\t\t\t\t\t\tVulnerabilityID:   \"CVE-2020-1\",\n\t\t\t\t\t},\n\t\t\t\t\tMatcher: match.ApkMatcher,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tactual, _, err := m.Match(vp, p)\n\tassert.NoError(t, err)\n\n\tassertMatches(t, expected, actual)\n}\n\nfunc assertMatches(t *testing.T, expected, actual []match.Match) {\n\tt.Helper()\n\tvar opts = []cmp.Option{\n\t\tcmpopts.IgnoreFields(vulnerability.Vulnerability{}, \"Constraint\"),\n\t\tcmpopts.IgnoreFields(pkg.Package{}, \"Locations\"),\n\t\tcmpopts.IgnoreUnexported(distro.Distro{}),\n\t}\n\n\tif diff := cmp.Diff(expected, actual, opts...); diff != \"\" {\n\t\tt.Errorf(\"mismatch (-want +got):\\n%s\", diff)\n\t}\n}\n\nfunc Test_nakConstraint(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\tinput   vulnerability.Vulnerability\n\t\twantErr require.ErrorAssertionFunc\n\t\tmatches bool\n\t}{\n\t\t{\n\t\t\tname: \"matches apk\",\n\t\t\tinput: vulnerability.Vulnerability{\n\t\t\t\tConstraint: version.MustGetConstraint(\"< 0\", version.ApkFormat),\n\t\t\t},\n\t\t\tmatches: true,\n\t\t},\n\t\t{\n\t\t\tname: \"not match due to type\",\n\t\t\tinput: vulnerability.Vulnerability{\n\t\t\t\tConstraint: version.MustGetConstraint(\"< 0\", version.SemanticFormat),\n\t\t\t},\n\t\t\tmatches: false,\n\t\t},\n\t\t{\n\t\t\tname: \"not match\",\n\t\t\tinput: vulnerability.Vulnerability{\n\t\t\t\tConstraint: version.MustGetConstraint(\"< 2.0\", version.SemanticFormat),\n\t\t\t},\n\t\t\tmatches: false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif tt.wantErr == nil {\n\t\t\t\ttt.wantErr = require.NoError\n\t\t\t}\n\n\t\t\tmatches, _, err := nakConstraint.MatchesVulnerability(tt.input)\n\t\t\ttt.wantErr(t, err)\n\t\t\trequire.Equal(t, tt.matches, matches)\n\t\t})\n\t}\n}\n\nfunc Test_nakIgnoreRules(t *testing.T) {\n\tcases := []struct {\n\t\tname                    string\n\t\tpkgs                    []pkg.Package\n\t\tvulns                   []vulnerability.Vulnerability\n\t\texpectedLocationIgnores map[string][]string\n\t\terrAssertion            assert.ErrorAssertionFunc\n\t}{\n\t\t{\n\t\t\tname: \"false positive in wolfi package adds index entry\",\n\t\t\tpkgs: []pkg.Package{\n\t\t\t\t{\n\t\t\t\t\tName:   \"foo\",\n\t\t\t\t\tDistro: &distro.Distro{Type: distro.Wolfi},\n\t\t\t\t\tMetadata: pkg.ApkMetadata{Files: []pkg.ApkFileRecord{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tPath: \"/bin/foo-binary\",\n\t\t\t\t\t\t},\n\t\t\t\t\t}},\n\t\t\t\t},\n\t\t\t},\n\t\t\tvulns: []vulnerability.Vulnerability{\n\t\t\t\t{\n\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\tID:        \"GHSA-2014-fake-3\",\n\t\t\t\t\t\tNamespace: \"wolfi:distro:wolfi:rolling\",\n\t\t\t\t\t},\n\t\t\t\t\tPackageName: \"foo\",\n\t\t\t\t\tConstraint:  version.MustGetConstraint(\"< 0\", version.ApkFormat),\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedLocationIgnores: map[string][]string{\n\t\t\t\t\"/bin/foo-binary\": {\"GHSA-2014-fake-3\"},\n\t\t\t},\n\t\t\terrAssertion: assert.NoError,\n\t\t},\n\t\t{\n\t\t\tname: \"false positive in wolfi subpackage adds index entry\",\n\t\t\tpkgs: []pkg.Package{\n\t\t\t\t{\n\t\t\t\t\tName:   \"subpackage-foo\",\n\t\t\t\t\tDistro: &distro.Distro{Type: distro.Wolfi},\n\t\t\t\t\tMetadata: pkg.ApkMetadata{Files: []pkg.ApkFileRecord{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tPath: \"/bin/foo-subpackage-binary\",\n\t\t\t\t\t\t},\n\t\t\t\t\t}},\n\t\t\t\t\tUpstreams: []pkg.UpstreamPackage{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName: \"origin-foo\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tvulns: []vulnerability.Vulnerability{\n\t\t\t\t{\n\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\tID:        \"GHSA-2014-fake-3\",\n\t\t\t\t\t\tNamespace: \"wolfi:distro:wolfi:rolling\",\n\t\t\t\t\t},\n\t\t\t\t\tPackageName: \"origin-foo\",\n\t\t\t\t\tConstraint:  version.MustGetConstraint(\"< 0\", version.ApkFormat),\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedLocationIgnores: map[string][]string{\n\t\t\t\t\"/bin/foo-subpackage-binary\": {\"GHSA-2014-fake-3\"},\n\t\t\t},\n\t\t\terrAssertion: assert.NoError,\n\t\t},\n\t\t{\n\t\t\tname: \"fixed vuln (not a false positive) in wolfi package\",\n\t\t\tpkgs: []pkg.Package{\n\t\t\t\t{\n\t\t\t\t\tName:   \"foo\",\n\t\t\t\t\tDistro: &distro.Distro{Type: distro.Wolfi},\n\t\t\t\t\tMetadata: pkg.ApkMetadata{Files: []pkg.ApkFileRecord{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tPath: \"/bin/foo-binary\",\n\t\t\t\t\t\t},\n\t\t\t\t\t}},\n\t\t\t\t},\n\t\t\t},\n\t\t\tvulns: []vulnerability.Vulnerability{\n\t\t\t\t{\n\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\tID:        \"GHSA-2014-fake-3\",\n\t\t\t\t\t\tNamespace: \"wolfi:distro:wolfi:rolling\",\n\t\t\t\t\t},\n\t\t\t\t\tPackageName: \"foo\",\n\t\t\t\t\tConstraint:  version.MustGetConstraint(\"< 1.2.3-r4\", version.ApkFormat),\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedLocationIgnores: map[string][]string{},\n\t\t\terrAssertion:            assert.NoError,\n\t\t},\n\t\t{\n\t\t\tname: \"no vuln data for wolfi package\",\n\t\t\tpkgs: []pkg.Package{\n\t\t\t\t{\n\t\t\t\t\tName:   \"foo\",\n\t\t\t\t\tDistro: &distro.Distro{Type: distro.Wolfi},\n\t\t\t\t\tMetadata: pkg.ApkMetadata{Files: []pkg.ApkFileRecord{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tPath: \"/bin/foo-binary\",\n\t\t\t\t\t\t},\n\t\t\t\t\t}},\n\t\t\t\t},\n\t\t\t},\n\t\t\tvulns:                   []vulnerability.Vulnerability{},\n\t\t\texpectedLocationIgnores: map[string][]string{},\n\t\t\terrAssertion:            assert.NoError,\n\t\t},\n\t\t{\n\t\t\tname: \"no files listed for a wolfi package\",\n\t\t\tpkgs: []pkg.Package{\n\t\t\t\t{\n\t\t\t\t\tName:     \"foo\",\n\t\t\t\t\tDistro:   &distro.Distro{Type: distro.Wolfi},\n\t\t\t\t\tMetadata: pkg.ApkMetadata{Files: nil},\n\t\t\t\t},\n\t\t\t},\n\t\t\tvulns: []vulnerability.Vulnerability{\n\t\t\t\t{\n\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\tID:        \"GHSA-2014-fake-3\",\n\t\t\t\t\t\tNamespace: \"wolfi:distro:wolfi:rolling\",\n\t\t\t\t\t},\n\t\t\t\t\tPackageName: \"foo\",\n\t\t\t\t\tConstraint:  version.MustGetConstraint(\"< 0\", version.ApkFormat),\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedLocationIgnores: map[string][]string{},\n\t\t\terrAssertion:            assert.NoError,\n\t\t},\n\t}\n\n\tfor _, tt := range cases {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\t// create mock vulnerability provider\n\t\t\tvp := mock.VulnerabilityProvider(tt.vulns...)\n\t\t\tapkMatcher := &Matcher{}\n\n\t\t\tvar allMatches []match.Match\n\t\t\tvar allIgnores []match.IgnoreFilter\n\t\t\tfor _, p := range tt.pkgs {\n\t\t\t\tmatches, ignores, err := apkMatcher.Match(vp, p)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tallMatches = append(allMatches, matches...)\n\t\t\t\tallIgnores = append(allIgnores, ignores...)\n\t\t\t}\n\n\t\t\tactualResult := map[string][]string{}\n\t\t\tfor _, ignore := range allIgnores {\n\t\t\t\trule, ok := ignore.(match.IgnoreRule)\n\t\t\t\tif !ok {\n\t\t\t\t\trequire.Fail(t, \"expected ignore to be of type IgnoreRule\")\n\t\t\t\t}\n\t\t\t\tif rule.Package.Location == \"\" {\n\t\t\t\t\trequire.Fail(t, \"expected package location to be set in ignore rule\")\n\t\t\t\t}\n\t\t\t\tactualResult[rule.Package.Location] = append(actualResult[rule.Package.Location], rule.Vulnerability)\n\t\t\t}\n\t\t\trequire.Equal(t, tt.expectedLocationIgnores, actualResult)\n\t\t})\n\t}\n}\n\nfunc TestMatcherApk_DistroFixedIgnoreRules(t *testing.T) {\n\tapkNamespace := \"secdb:distro:wolfi:rolling\"\n\n\tapkFiles := pkg.ApkMetadata{Files: []pkg.ApkFileRecord{\n\t\t{Path: \"/usr/bin/kyverno\"},\n\t\t{Path: \"/usr/lib/kyverno/config\"},\n\t}}\n\n\ttests := []struct {\n\t\tname                  string\n\t\tp                     pkg.Package\n\t\tvulnerabilities       []vulnerability.Vulnerability\n\t\texpectedIgnoreVulnIDs []string\n\t\texpectedMatchIDs      []string\n\t}{\n\t\t{\n\t\t\tname: \"package already at fixed version - should produce location-scoped ignore rules but no matches\",\n\t\t\tp: pkg.Package{\n\t\t\t\tID:       pkg.ID(uuid.NewString()),\n\t\t\t\tName:     \"kyverno\",\n\t\t\t\tVersion:  \"1.15.3-r0\",\n\t\t\t\tType:     syftPkg.ApkPkg,\n\t\t\t\tDistro:   distro.New(distro.Wolfi, \"\", \"\"),\n\t\t\t\tMetadata: apkFiles,\n\t\t\t},\n\t\t\tvulnerabilities: []vulnerability.Vulnerability{\n\t\t\t\t{\n\t\t\t\t\tPackageName: \"kyverno\",\n\t\t\t\t\tConstraint:  version.MustGetConstraint(\"< 1.15.3-r0\", version.ApkFormat),\n\t\t\t\t\tReference:   vulnerability.Reference{ID: \"CVE-2026-22039\", Namespace: apkNamespace},\n\t\t\t\t},\n\t\t\t},\n\t\t\t// one rule per owned path\n\t\t\texpectedIgnoreVulnIDs: []string{\"CVE-2026-22039\", \"CVE-2026-22039\"},\n\t\t\texpectedMatchIDs:      nil,\n\t\t},\n\t\t{\n\t\t\tname: \"package still vulnerable - should produce matches but no ignore rules\",\n\t\t\tp: pkg.Package{\n\t\t\t\tID:       pkg.ID(uuid.NewString()),\n\t\t\t\tName:     \"kyverno\",\n\t\t\t\tVersion:  \"1.14.5-r0\",\n\t\t\t\tType:     syftPkg.ApkPkg,\n\t\t\t\tDistro:   distro.New(distro.Wolfi, \"\", \"\"),\n\t\t\t\tMetadata: apkFiles,\n\t\t\t},\n\t\t\tvulnerabilities: []vulnerability.Vulnerability{\n\t\t\t\t{\n\t\t\t\t\tPackageName: \"kyverno\",\n\t\t\t\t\tConstraint:  version.MustGetConstraint(\"< 1.15.3-r0\", version.ApkFormat),\n\t\t\t\t\tReference:   vulnerability.Reference{ID: \"CVE-2026-22039\", Namespace: apkNamespace},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedIgnoreVulnIDs: nil,\n\t\t\texpectedMatchIDs:      []string{\"CVE-2026-22039\"},\n\t\t},\n\t\t{\n\t\t\tname: \"no distro data for the package - no ignore rules (search miss allows GHSA to stand)\",\n\t\t\tp: pkg.Package{\n\t\t\t\tID:       pkg.ID(uuid.NewString()),\n\t\t\t\tName:     \"something-obscure\",\n\t\t\t\tVersion:  \"1.0.0-r0\",\n\t\t\t\tType:     syftPkg.ApkPkg,\n\t\t\t\tDistro:   distro.New(distro.Wolfi, \"\", \"\"),\n\t\t\t\tMetadata: apkFiles,\n\t\t\t},\n\t\t\tvulnerabilities: []vulnerability.Vulnerability{\n\t\t\t\t{\n\t\t\t\t\tPackageName: \"kyverno\",\n\t\t\t\t\tConstraint:  version.MustGetConstraint(\"< 1.15.3-r0\", version.ApkFormat),\n\t\t\t\t\tReference:   vulnerability.Reference{ID: \"CVE-2026-22039\", Namespace: apkNamespace},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedIgnoreVulnIDs: nil,\n\t\t\texpectedMatchIDs:      nil,\n\t\t},\n\t\t{\n\t\t\tname: \"upstream package is fixed - should produce location-scoped ignore rules\",\n\t\t\tp: pkg.Package{\n\t\t\t\tID:       pkg.ID(uuid.NewString()),\n\t\t\t\tName:     \"kyverno-cli\",\n\t\t\t\tVersion:  \"1.15.3-r0\",\n\t\t\t\tType:     syftPkg.ApkPkg,\n\t\t\t\tDistro:   distro.New(distro.Wolfi, \"\", \"\"),\n\t\t\t\tMetadata: apkFiles,\n\t\t\t\tUpstreams: []pkg.UpstreamPackage{\n\t\t\t\t\t{\n\t\t\t\t\t\tName:    \"kyverno\",\n\t\t\t\t\t\tVersion: \"1.15.3-r0\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tvulnerabilities: []vulnerability.Vulnerability{\n\t\t\t\t{\n\t\t\t\t\tPackageName: \"kyverno\",\n\t\t\t\t\tConstraint:  version.MustGetConstraint(\"< 1.15.3-r0\", version.ApkFormat),\n\t\t\t\t\tReference:   vulnerability.Reference{ID: \"CVE-2026-22039\", Namespace: apkNamespace},\n\t\t\t\t},\n\t\t\t},\n\t\t\t// one rule per owned path\n\t\t\texpectedIgnoreVulnIDs: []string{\"CVE-2026-22039\", \"CVE-2026-22039\"},\n\t\t\texpectedMatchIDs:      nil,\n\t\t},\n\t\t{\n\t\t\tname: \"fixed CVE with related GHSA - ignore rules include both IDs at all paths\",\n\t\t\tp: pkg.Package{\n\t\t\t\tID:       pkg.ID(uuid.NewString()),\n\t\t\t\tName:     \"kyverno\",\n\t\t\t\tVersion:  \"1.15.3-r0\",\n\t\t\t\tType:     syftPkg.ApkPkg,\n\t\t\t\tDistro:   distro.New(distro.Wolfi, \"\", \"\"),\n\t\t\t\tMetadata: apkFiles,\n\t\t\t},\n\t\t\tvulnerabilities: []vulnerability.Vulnerability{\n\t\t\t\t{\n\t\t\t\t\tPackageName: \"kyverno\",\n\t\t\t\t\tConstraint:  version.MustGetConstraint(\"< 1.15.3-r0\", version.ApkFormat),\n\t\t\t\t\tReference:   vulnerability.Reference{ID: \"CVE-2026-22039\", Namespace: apkNamespace},\n\t\t\t\t\tRelatedVulnerabilities: []vulnerability.Reference{\n\t\t\t\t\t\t{ID: \"GHSA-8p9x-46gm-qfx2\", Namespace: \"github:language:go\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\t// 2 IDs × 2 paths = 4 rules\n\t\t\texpectedIgnoreVulnIDs: []string{\"CVE-2026-22039\", \"CVE-2026-22039\", \"GHSA-8p9x-46gm-qfx2\", \"GHSA-8p9x-46gm-qfx2\"},\n\t\t\texpectedMatchIDs:      nil,\n\t\t},\n\t\t{\n\t\t\tname: \"no APK metadata (no file list) - should NOT produce ignore rules even if fixed\",\n\t\t\tp: pkg.Package{\n\t\t\t\tID:      pkg.ID(uuid.NewString()),\n\t\t\t\tName:    \"kyverno\",\n\t\t\t\tVersion: \"1.15.3-r0\",\n\t\t\t\tType:    syftPkg.ApkPkg,\n\t\t\t\tDistro:  distro.New(distro.Wolfi, \"\", \"\"),\n\t\t\t\t// no Metadata\n\t\t\t},\n\t\t\tvulnerabilities: []vulnerability.Vulnerability{\n\t\t\t\t{\n\t\t\t\t\tPackageName: \"kyverno\",\n\t\t\t\t\tConstraint:  version.MustGetConstraint(\"< 1.15.3-r0\", version.ApkFormat),\n\t\t\t\t\tReference:   vulnerability.Reference{ID: \"CVE-2026-22039\", Namespace: apkNamespace},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedIgnoreVulnIDs: nil,\n\t\t\texpectedMatchIDs:      nil,\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tmatcher := Matcher{}\n\n\t\t\tstore := mock.VulnerabilityProvider(test.vulnerabilities...)\n\t\t\tmatches, ignoreFilters, err := matcher.Match(store, test.p)\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// verify matches\n\t\t\tvar gotMatchIDs []string\n\t\t\tfor _, m := range matches {\n\t\t\t\tgotMatchIDs = append(gotMatchIDs, m.Vulnerability.ID)\n\t\t\t}\n\t\t\tif test.expectedMatchIDs == nil {\n\t\t\t\tassert.Empty(t, gotMatchIDs, \"expected no matches\")\n\t\t\t} else {\n\t\t\t\tassert.ElementsMatch(t, test.expectedMatchIDs, gotMatchIDs, \"unexpected match IDs\")\n\t\t\t}\n\n\t\t\t// verify ignore rules - filter to only DistroPackageFixed rules (not NAK rules)\n\t\t\tvar gotIgnoreIDs []string\n\t\t\tfor _, filter := range ignoreFilters {\n\t\t\t\trule, ok := filter.(match.IgnoreRule)\n\t\t\t\trequire.True(t, ok, \"expected IgnoreRule type\")\n\t\t\t\tif rule.Reason != \"DistroPackageFixed\" {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tgotIgnoreIDs = append(gotIgnoreIDs, rule.Vulnerability)\n\t\t\t\tassert.True(t, rule.IncludeAliases, \"expected IncludeAliases to be true\")\n\t\t\t\tassert.NotEmpty(t, rule.Package.Location, \"expected location to be set on DistroPackageFixed rule\")\n\t\t\t}\n\t\t\tif test.expectedIgnoreVulnIDs == nil {\n\t\t\t\tassert.Empty(t, gotIgnoreIDs, \"expected no ignore rules\")\n\t\t\t} else {\n\t\t\t\tassert.ElementsMatch(t, test.expectedIgnoreVulnIDs, gotIgnoreIDs, \"unexpected ignore rule vulnerability IDs\")\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/matcher/bitnami/matcher.go",
    "content": "package bitnami\n\nimport (\n\t\"github.com/anchore/grype/grype/match\"\n\t\"github.com/anchore/grype/grype/matcher/internal\"\n\t\"github.com/anchore/grype/grype/pkg\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n\tsyftPkg \"github.com/anchore/syft/syft/pkg\"\n)\n\ntype Matcher struct{}\n\nfunc (m *Matcher) PackageTypes() []syftPkg.Type {\n\treturn []syftPkg.Type{syftPkg.BitnamiPkg}\n}\n\nfunc (m *Matcher) Type() match.MatcherType {\n\treturn match.BitnamiMatcher\n}\n\nfunc (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoreFilter, error) {\n\t// Bitnami packages' metadata are built from the package URL which contains\n\t// info such as the package name, version, revision, distro or architecture.\n\t// ref: https://github.com/anchore/syft/blob/main/syft/pkg/bitnami.go#L3-L13\n\t// ref: https://github.com/anchore/syft/blob/main/syft/pkg/cataloger/bitnami/package.go#L18-L45\n\treturn internal.MatchPackageByEcosystemPackageName(store, p, p.Name, m.Type())\n}\n"
  },
  {
    "path": "grype/matcher/dotnet/matcher.go",
    "content": "package dotnet\n\nimport (\n\t\"github.com/anchore/grype/grype/match\"\n\t\"github.com/anchore/grype/grype/matcher/internal\"\n\t\"github.com/anchore/grype/grype/pkg\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n\tsyftPkg \"github.com/anchore/syft/syft/pkg\"\n)\n\ntype Matcher struct {\n\tcfg MatcherConfig\n}\n\ntype MatcherConfig struct {\n\tUseCPEs bool\n}\n\nfunc NewDotnetMatcher(cfg MatcherConfig) *Matcher {\n\treturn &Matcher{\n\t\tcfg: cfg,\n\t}\n}\n\nfunc (m *Matcher) PackageTypes() []syftPkg.Type {\n\treturn []syftPkg.Type{syftPkg.DotnetPkg}\n}\n\nfunc (m *Matcher) Type() match.MatcherType {\n\treturn match.DotnetMatcher\n}\n\nfunc (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoreFilter, error) {\n\treturn internal.MatchPackageByEcosystemAndCPEs(store, p, m.Type(), m.cfg.UseCPEs)\n}\n"
  },
  {
    "path": "grype/matcher/dpkg/matcher.go",
    "content": "package dpkg\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\n\t\"github.com/anchore/grype/grype/match\"\n\t\"github.com/anchore/grype/grype/matcher/internal\"\n\t\"github.com/anchore/grype/grype/pkg\"\n\t\"github.com/anchore/grype/grype/version\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n\t\"github.com/anchore/grype/internal/log\"\n\tsyftPkg \"github.com/anchore/syft/syft/pkg\"\n)\n\ntype Matcher struct {\n\tcfg MatcherConfig\n}\n\ntype MatcherConfig struct {\n\tMissingEpochStrategy version.MissingEpochStrategy\n\tUseCPEsForEOL        bool\n}\n\nfunc NewDpkgMatcher(cfg MatcherConfig) *Matcher {\n\treturn &Matcher{\n\t\tcfg: cfg,\n\t}\n}\n\nfunc (m *Matcher) PackageTypes() []syftPkg.Type {\n\treturn []syftPkg.Type{syftPkg.DebPkg}\n}\n\nfunc (m *Matcher) Type() match.MatcherType {\n\treturn match.DpkgMatcher\n}\n\nfunc (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoreFilter, error) {\n\tmatches := make([]match.Match, 0)\n\n\tsourceMatches, err := m.matchUpstreamPackages(store, p)\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"failed to match by source indirection: %w\", err)\n\t}\n\tmatches = append(matches, sourceMatches...)\n\n\tversionConfig := version.ComparisonConfig{\n\t\tMissingEpochStrategy: m.cfg.MissingEpochStrategy,\n\t}\n\texactMatches, _, err := internal.MatchPackageByDistro(store, p, nil, m.Type(), &versionConfig)\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"failed to match by exact package name: %w\", err)\n\t}\n\tmatches = append(matches, exactMatches...)\n\n\t// if configured, also search by CPEs for packages from EOL distros\n\tif m.cfg.UseCPEsForEOL && internal.IsDistroEOL(store, p.Distro) {\n\t\tlog.WithFields(\"package\", p.Name, \"distro\", p.Distro).Debug(\"distro is EOL, searching by CPEs\")\n\t\tcpeMatches, err := internal.MatchPackageByCPEs(store, p, m.Type())\n\t\tswitch {\n\t\tcase errors.Is(err, internal.ErrEmptyCPEMatch):\n\t\t\tlog.WithFields(\"package\", p.Name).Debug(\"package has no CPEs for EOL fallback matching\")\n\t\tcase err != nil:\n\t\t\tlog.WithFields(\"package\", p.Name, \"error\", err).Debug(\"failed to match by CPEs for EOL distro\")\n\t\tdefault:\n\t\t\tmatches = append(matches, cpeMatches...)\n\t\t}\n\t}\n\n\treturn matches, nil, nil\n}\n\nfunc (m *Matcher) matchUpstreamPackages(store vulnerability.Provider, p pkg.Package) ([]match.Match, error) {\n\tvar matches []match.Match\n\n\tversionConfig := version.ComparisonConfig{\n\t\tMissingEpochStrategy: m.cfg.MissingEpochStrategy,\n\t}\n\tfor _, indirectPackage := range pkg.UpstreamPackages(p) {\n\t\tindirectMatches, _, err := internal.MatchPackageByDistro(store, indirectPackage, &p, m.Type(), &versionConfig)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to find vulnerabilities for dpkg upstream source package: %w\", err)\n\t\t}\n\t\tmatches = append(matches, indirectMatches...)\n\t}\n\n\t// we want to make certain that we are tracking the match based on the package from the SBOM (not the indirect package)\n\t// however, we also want to keep the indirect package around for future reference\n\tmatch.ConvertToIndirectMatches(matches, p)\n\n\treturn matches, nil\n}\n"
  },
  {
    "path": "grype/matcher/dpkg/matcher_mocks_test.go",
    "content": "package dpkg\n\nimport (\n\t\"time\"\n\n\t\"github.com/anchore/grype/grype/distro\"\n\t\"github.com/anchore/grype/grype/version\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n\t\"github.com/anchore/grype/grype/vulnerability/mock\"\n\tsyftCpe \"github.com/anchore/syft/syft/cpe\"\n)\n\nfunc newMockProvider() vulnerability.Provider {\n\treturn mock.VulnerabilityProvider([]vulnerability.Vulnerability{\n\t\t{\n\t\t\tPackageName: \"neutron\",\n\t\t\tReference:   vulnerability.Reference{ID: \"CVE-2014-fake-1\", Namespace: \"secdb:distro:debian:8\"},\n\t\t\tConstraint:  version.MustGetConstraint(\"< 2014.1.3-6\", version.DebFormat),\n\t\t},\n\t\t// expected...\n\t\t{\n\t\t\tPackageName: \"neutron-devel\",\n\t\t\tConstraint:  version.MustGetConstraint(\"< 2014.1.4-5\", version.DebFormat),\n\t\t\tReference:   vulnerability.Reference{ID: \"CVE-2014-fake-2\", Namespace: \"secdb:distro:debian:8\"},\n\t\t},\n\t\t{\n\t\t\tPackageName: \"neutron-devel\",\n\t\t\tConstraint:  version.MustGetConstraint(\"< 2015.0.0-1\", version.DebFormat),\n\t\t\tReference:   vulnerability.Reference{ID: \"CVE-2013-fake-3\", Namespace: \"secdb:distro:debian:8\"},\n\t\t},\n\t\t// unexpected...\n\t\t{\n\t\t\tPackageName: \"neutron-devel\",\n\t\t\tConstraint:  version.MustGetConstraint(\"< 2014.0.4-1\", version.DebFormat),\n\t\t\tReference:   vulnerability.Reference{ID: \"CVE-2013-fake-BAD\", Namespace: \"secdb:distro:debian:8\"},\n\t\t},\n\t}...)\n}\n\n// mockEOLProvider wraps mock.VulnerabilityProvider and adds EOLChecker support for testing\ntype mockEOLProvider struct {\n\tvulnerability.Provider\n\teolDate *time.Time\n}\n\nfunc (m *mockEOLProvider) GetOperatingSystemEOL(d *distro.Distro) (eolDate, eoasDate *time.Time, err error) {\n\treturn m.eolDate, nil, nil\n}\n\nfunc newMockEOLProvider(eolDate *time.Time) *mockEOLProvider {\n\t// include CPE vulnerability for testing CPE fallback\n\treturn &mockEOLProvider{\n\t\tProvider: mock.VulnerabilityProvider([]vulnerability.Vulnerability{\n\t\t\t// distro-based vulnerability\n\t\t\t{\n\t\t\t\tPackageName: \"openssl\",\n\t\t\t\tReference:   vulnerability.Reference{ID: \"CVE-2014-distro-1\", Namespace: \"secdb:distro:debian:8\"},\n\t\t\t\tConstraint:  version.MustGetConstraint(\"< 1.0.2\", version.DebFormat),\n\t\t\t},\n\t\t\t// CPE-based vulnerability\n\t\t\t{\n\t\t\t\tPackageName: \"openssl\",\n\t\t\t\tReference:   vulnerability.Reference{ID: \"CVE-2014-cpe-1\", Namespace: \"nvd:cpe\"},\n\t\t\t\tConstraint:  version.MustGetConstraint(\"< 1.0.2\", version.UnknownFormat),\n\t\t\t\tCPEs: []syftCpe.CPE{\n\t\t\t\t\tsyftCpe.Must(\"cpe:2.3:a:openssl:openssl:*:*:*:*:*:*:*:*\", \"\"),\n\t\t\t\t},\n\t\t\t},\n\t\t}...),\n\t\teolDate: eolDate,\n\t}\n}\n"
  },
  {
    "path": "grype/matcher/dpkg/matcher_test.go",
    "content": "package dpkg\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/google/uuid\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/grype/grype/distro\"\n\t\"github.com/anchore/grype/grype/match\"\n\t\"github.com/anchore/grype/grype/pkg\"\n\t\"github.com/anchore/grype/internal/stringutil\"\n\tsyftCpe \"github.com/anchore/syft/syft/cpe\"\n\tsyftPkg \"github.com/anchore/syft/syft/pkg\"\n)\n\nfunc TestMatcherDpkg_matchBySourceIndirection(t *testing.T) {\n\tmatcher := Matcher{}\n\n\td := distro.New(distro.Debian, \"8\", \"\")\n\n\tp := pkg.Package{\n\t\tID:      pkg.ID(uuid.NewString()),\n\t\tName:    \"neutron\",\n\t\tVersion: \"2014.1.3-6\",\n\t\tType:    syftPkg.DebPkg,\n\t\tDistro:  d,\n\t\tUpstreams: []pkg.UpstreamPackage{\n\t\t\t{\n\t\t\t\tName: \"neutron-devel\",\n\t\t\t},\n\t\t},\n\t}\n\n\tvp := newMockProvider()\n\tactual, err := matcher.matchUpstreamPackages(vp, p)\n\tassert.NoError(t, err, \"unexpected err from matchUpstreamPackages\", err)\n\n\tassert.Len(t, actual, 2, \"unexpected indirect matches count\")\n\n\tfoundCVEs := stringutil.NewStringSet()\n\tfor _, a := range actual {\n\t\tfoundCVEs.Add(a.Vulnerability.ID)\n\n\t\trequire.NotEmpty(t, a.Details)\n\t\tfor _, d := range a.Details {\n\t\t\tassert.Equal(t, match.ExactIndirectMatch, d.Type, \"indirect match not indicated\")\n\t\t}\n\t\tassert.Equal(t, p.Name, a.Package.Name, \"failed to capture original package name\")\n\t\tfor _, detail := range a.Details {\n\t\t\tassert.Equal(t, matcher.Type(), detail.Matcher, \"failed to capture matcher type\")\n\t\t}\n\t}\n\n\tfor _, id := range []string{\"CVE-2014-fake-2\", \"CVE-2013-fake-3\"} {\n\t\tif !foundCVEs.Contains(id) {\n\t\t\tt.Errorf(\"missing discovered CVE: %s\", id)\n\t\t}\n\t}\n\tif t.Failed() {\n\t\tt.Logf(\"discovered CVES: %+v\", foundCVEs)\n\t}\n}\n\nfunc TestMatcherDpkg_CPEFallbackWhenEOL(t *testing.T) {\n\tpastEOL := time.Now().AddDate(-1, 0, 0)  // 1 year ago\n\tfutureEOL := time.Now().AddDate(1, 0, 0) // 1 year from now\n\n\td := distro.New(distro.Debian, \"8\", \"\")\n\n\t// package with CPEs for CPE-based matching\n\tp := pkg.Package{\n\t\tID:      pkg.ID(uuid.NewString()),\n\t\tName:    \"openssl\",\n\t\tVersion: \"1.0.1\",\n\t\tType:    syftPkg.DebPkg,\n\t\tDistro:  d,\n\t\tCPEs: []syftCpe.CPE{\n\t\t\tsyftCpe.Must(\"cpe:2.3:a:openssl:openssl:1.0.1:*:*:*:*:*:*:*\", \"\"),\n\t\t},\n\t}\n\n\ttests := []struct {\n\t\tname             string\n\t\tuseCPEsForEOL    bool\n\t\teolDate          *time.Time\n\t\texpectCPEMatches bool\n\t}{\n\t\t{\n\t\t\tname:             \"CPE fallback enabled and distro is EOL - should include CPE matches\",\n\t\t\tuseCPEsForEOL:    true,\n\t\t\teolDate:          &pastEOL,\n\t\t\texpectCPEMatches: true,\n\t\t},\n\t\t{\n\t\t\tname:             \"CPE fallback enabled but distro not EOL - should not include CPE matches\",\n\t\t\tuseCPEsForEOL:    true,\n\t\t\teolDate:          &futureEOL,\n\t\t\texpectCPEMatches: false,\n\t\t},\n\t\t{\n\t\t\tname:             \"CPE fallback disabled and distro is EOL - should not include CPE matches\",\n\t\t\tuseCPEsForEOL:    false,\n\t\t\teolDate:          &pastEOL,\n\t\t\texpectCPEMatches: false,\n\t\t},\n\t\t{\n\t\t\tname:             \"CPE fallback disabled and distro not EOL - should not include CPE matches\",\n\t\t\tuseCPEsForEOL:    false,\n\t\t\teolDate:          &futureEOL,\n\t\t\texpectCPEMatches: false,\n\t\t},\n\t\t{\n\t\t\tname:             \"CPE fallback enabled but no EOL data - should not include CPE matches\",\n\t\t\tuseCPEsForEOL:    true,\n\t\t\teolDate:          nil,\n\t\t\texpectCPEMatches: false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tmatcher := NewDpkgMatcher(MatcherConfig{\n\t\t\t\tUseCPEsForEOL: tt.useCPEsForEOL,\n\t\t\t})\n\n\t\t\tvp := newMockEOLProvider(tt.eolDate)\n\t\t\tmatches, _, err := matcher.Match(vp, p)\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// check if any CPE matches were found\n\t\t\thasCPEMatch := false\n\t\t\tfor _, m := range matches {\n\t\t\t\tfor _, detail := range m.Details {\n\t\t\t\t\tif detail.Type == match.CPEMatch {\n\t\t\t\t\t\thasCPEMatch = true\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif tt.expectCPEMatches {\n\t\t\t\tassert.True(t, hasCPEMatch, \"expected CPE matches for EOL distro\")\n\t\t\t} else {\n\t\t\t\tassert.False(t, hasCPEMatch, \"did not expect CPE matches\")\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/matcher/golang/matcher.go",
    "content": "package golang\n\nimport (\n\t\"strings\"\n\n\t\"github.com/anchore/grype/grype/match\"\n\t\"github.com/anchore/grype/grype/matcher/internal\"\n\t\"github.com/anchore/grype/grype/pkg\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n\tsyftPkg \"github.com/anchore/syft/syft/pkg\"\n)\n\ntype Matcher struct {\n\tcfg MatcherConfig\n}\n\ntype MatcherConfig struct {\n\tUseCPEs                                bool\n\tAlwaysUseCPEForStdlib                  bool\n\tAllowMainModulePseudoVersionComparison bool\n}\n\nfunc NewGolangMatcher(cfg MatcherConfig) *Matcher {\n\treturn &Matcher{\n\t\tcfg: cfg,\n\t}\n}\n\nfunc (m *Matcher) PackageTypes() []syftPkg.Type {\n\treturn []syftPkg.Type{syftPkg.GoModulePkg}\n}\n\nfunc (m *Matcher) Type() match.MatcherType {\n\treturn match.GoModuleMatcher\n}\n\nfunc (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoreFilter, error) {\n\tmatches := make([]match.Match, 0)\n\n\tmainModule := \"\"\n\tif m, ok := p.Metadata.(pkg.GolangBinMetadata); ok {\n\t\tmainModule = m.MainModule\n\t}\n\n\t// Golang currently does not have a standard way of incorporating the main\n\t// module's version into the compiled binary:\n\t// https://github.com/golang/go/issues/50603.\n\t//\n\t// Syft has some fallback mechanisms to come up with a more sane version value\n\t// depending on the scenario. But if none of these apply, the Go-set value of\n\t// \"(devel)\" is used, which is altogether unhelpful for vulnerability matching.\n\tvar isNotCorrected bool\n\tif m.cfg.AllowMainModulePseudoVersionComparison {\n\t\tisNotCorrected = strings.HasPrefix(p.Version, \"(devel)\")\n\t} else {\n\t\t// when AllowPseudoVersionComparison is false\n\t\tisNotCorrected = strings.HasPrefix(p.Version, \"v0.0.0-\") || strings.HasPrefix(p.Version, \"(devel)\")\n\t}\n\tif p.Name == mainModule && isNotCorrected {\n\t\treturn matches, nil, nil\n\t}\n\n\treturn internal.MatchPackageByEcosystemAndCPEs(store, p, m.Type(), searchByCPE(p.Name, m.cfg))\n}\n\nfunc searchByCPE(name string, cfg MatcherConfig) bool {\n\tif cfg.UseCPEs {\n\t\treturn true\n\t}\n\n\treturn cfg.AlwaysUseCPEForStdlib && (name == \"stdlib\")\n}\n"
  },
  {
    "path": "grype/matcher/golang/matcher_test.go",
    "content": "package golang\n\nimport (\n\t\"testing\"\n\n\t\"github.com/google/uuid\"\n\t\"github.com/scylladb/go-set/strset\"\n\t\"github.com/stretchr/testify/assert\"\n\n\t\"github.com/anchore/grype/grype/pkg\"\n\t\"github.com/anchore/grype/grype/version\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n\t\"github.com/anchore/grype/grype/vulnerability/mock\"\n\t\"github.com/anchore/syft/syft/cpe\"\n\tsyftPkg \"github.com/anchore/syft/syft/pkg\"\n)\n\nfunc TestMatcher_DropMainPackageGivenVersionInfo(t *testing.T) {\n\ttests := []struct {\n\t\tname                         string\n\t\tsubjectWithoutMainModule     pkg.Package\n\t\tmainModuleData               pkg.GolangBinMetadata\n\t\tallowPsuedoVersionComparison bool\n\t\texpectedMatchCount           int\n\t}{\n\t\t{\n\t\t\tname: \"main module with version is matched when pseudo version comparison is allowed\",\n\t\t\tsubjectWithoutMainModule: pkg.Package{\n\t\t\t\tID:       pkg.ID(uuid.NewString()),\n\t\t\t\tName:     \"istio.io/istio\",\n\t\t\t\tVersion:  \"v0.0.0-20220606222826-f59ce19ec6b6\",\n\t\t\t\tType:     syftPkg.GoModulePkg,\n\t\t\t\tLanguage: syftPkg.Go,\n\t\t\t\tMetadata: pkg.GolangBinMetadata{},\n\t\t\t},\n\t\t\tmainModuleData: pkg.GolangBinMetadata{\n\t\t\t\tMainModule: \"istio.io/istio\",\n\t\t\t},\n\t\t\tallowPsuedoVersionComparison: true,\n\t\t\texpectedMatchCount:           1,\n\t\t},\n\t\t{\n\t\t\tname: \"main module with version is NOT matched when pseudo version comparison is disabled\",\n\t\t\tsubjectWithoutMainModule: pkg.Package{\n\t\t\t\tID:       pkg.ID(uuid.NewString()),\n\t\t\t\tName:     \"istio.io/istio\",\n\t\t\t\tVersion:  \"v0.0.0-20220606222826-f59ce19ec6b6\",\n\t\t\t\tType:     syftPkg.GoModulePkg,\n\t\t\t\tLanguage: syftPkg.Go,\n\t\t\t\tMetadata: pkg.GolangBinMetadata{},\n\t\t\t},\n\t\t\tmainModuleData: pkg.GolangBinMetadata{\n\t\t\t\tMainModule: \"istio.io/istio\",\n\t\t\t},\n\t\t\tallowPsuedoVersionComparison: false,\n\t\t\texpectedMatchCount:           0,\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tmainModuleMetadata := test.mainModuleData\n\t\t\tsubjectWithoutMainModule := test.subjectWithoutMainModule\n\n\t\t\tsubjectWithMainModule := subjectWithoutMainModule\n\t\t\tsubjectWithMainModule.Metadata = mainModuleMetadata\n\n\t\t\tsubjectWithMainModuleAsDevel := subjectWithMainModule\n\t\t\tsubjectWithMainModuleAsDevel.Version = \"(devel)\"\n\n\t\t\tmatcher := NewGolangMatcher(MatcherConfig{\n\t\t\t\tAllowMainModulePseudoVersionComparison: test.allowPsuedoVersionComparison,\n\t\t\t})\n\t\t\tstore := newMockProvider()\n\n\t\t\tpreTest, _, _ := matcher.Match(store, subjectWithoutMainModule)\n\t\t\tassert.Len(t, preTest, 1, \"should have matched the package when there is not a main module\")\n\n\t\t\tactual, _, _ := matcher.Match(store, subjectWithMainModule)\n\t\t\tassert.Len(t, actual, test.expectedMatchCount, \"should match the main module depending on config (i.e. 1 match)\")\n\n\t\t\tactual, _, _ = matcher.Match(store, subjectWithMainModuleAsDevel)\n\t\t\tassert.Len(t, actual, 0, \"unexpected match count; should never match main module (devel)\")\n\t\t})\n\t}\n}\n\nfunc TestMatcher_SearchForStdlib(t *testing.T) {\n\n\t// values derived from:\n\t//   $ go version -m $(which grype)\n\t//  /opt/homebrew/bin/grype: go1.21.1\n\n\tsubject := pkg.Package{\n\t\tID:       pkg.ID(uuid.NewString()),\n\t\tName:     \"stdlib\",\n\t\tVersion:  \"go1.18.3\",\n\t\tType:     syftPkg.GoModulePkg,\n\t\tLanguage: syftPkg.Go,\n\t\tCPEs: []cpe.CPE{\n\t\t\tcpe.Must(\"cpe:2.3:a:golang:go:1.18.3:-:*:*:*:*:*:*\", \"\"),\n\t\t},\n\t\tMetadata: pkg.GolangBinMetadata{},\n\t}\n\n\tcases := []struct {\n\t\tname         string\n\t\tcfg          MatcherConfig\n\t\tsubject      pkg.Package\n\t\texpectedCVEs []string\n\t}{\n\t\t// positive\n\t\t{\n\t\t\tname: \"cpe enables, no override enabled\",\n\t\t\tcfg: MatcherConfig{\n\t\t\t\tUseCPEs:               true,\n\t\t\t\tAlwaysUseCPEForStdlib: false,\n\t\t\t},\n\t\t\tsubject: subject,\n\t\t\texpectedCVEs: []string{\n\t\t\t\t\"CVE-2022-27664\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"stdlib search, cpe enables, no override enabled\",\n\t\t\tcfg: MatcherConfig{\n\t\t\t\tUseCPEs:               true,\n\t\t\t\tAlwaysUseCPEForStdlib: true,\n\t\t\t},\n\t\t\tsubject: subject,\n\t\t\texpectedCVEs: []string{\n\t\t\t\t\"CVE-2022-27664\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"stdlib search, cpe enables, no override enabled\",\n\t\t\tcfg: MatcherConfig{\n\t\t\t\tUseCPEs:               false,\n\t\t\t\tAlwaysUseCPEForStdlib: true,\n\t\t\t},\n\t\t\tsubject: subject,\n\t\t\texpectedCVEs: []string{\n\t\t\t\t\"CVE-2022-27664\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"go package search should be found by cpe\",\n\t\t\tcfg: MatcherConfig{\n\t\t\t\tUseCPEs:               true,\n\t\t\t\tAlwaysUseCPEForStdlib: true,\n\t\t\t},\n\t\t\tsubject: func() pkg.Package { p := subject; p.Name = \"go\"; return p }(),\n\t\t\texpectedCVEs: []string{\n\t\t\t\t\"CVE-2022-27664\",\n\t\t\t}},\n\t\t// negative\n\t\t{\n\t\t\tname: \"stdlib search, cpe suppressed, no override enabled\",\n\t\t\tcfg: MatcherConfig{\n\t\t\t\tUseCPEs:               false,\n\t\t\t\tAlwaysUseCPEForStdlib: false,\n\t\t\t},\n\t\t\tsubject:      subject,\n\t\t\texpectedCVEs: nil,\n\t\t},\n\t\t{\n\t\t\tname: \"go package search should not be an exception (only the stdlib)\",\n\t\t\tcfg: MatcherConfig{\n\t\t\t\tUseCPEs:               false,\n\t\t\t\tAlwaysUseCPEForStdlib: true,\n\t\t\t},\n\t\t\tsubject:      func() pkg.Package { p := subject; p.Name = \"go\"; return p }(),\n\t\t\texpectedCVEs: nil,\n\t\t},\n\t}\n\n\tstore := newMockProvider()\n\n\tfor _, c := range cases {\n\t\tt.Run(c.name, func(t *testing.T) {\n\t\t\tmatcher := NewGolangMatcher(c.cfg)\n\n\t\t\tactual, _, _ := matcher.Match(store, c.subject)\n\t\t\tactualCVEs := strset.New()\n\t\t\tfor _, m := range actual {\n\t\t\t\tactualCVEs.Add(m.Vulnerability.ID)\n\t\t\t}\n\n\t\t\texpectedCVEs := strset.New(c.expectedCVEs...)\n\n\t\t\tassert.ElementsMatch(t, expectedCVEs.List(), actualCVEs.List())\n\n\t\t})\n\t}\n}\n\nfunc newMockProvider() vulnerability.Provider {\n\treturn mock.VulnerabilityProvider([]vulnerability.Vulnerability{\n\t\t// for TestMatcher_DropMainPackageIfNoVersion\n\t\t{\n\t\t\tPackageName: \"istio.io/istio\",\n\t\t\tConstraint:  version.MustGetConstraint(\"< 5.0.7\", version.UnknownFormat),\n\t\t\tReference:   vulnerability.Reference{ID: \"CVE-2013-fake-BAD\", Namespace: \"github:language:\" + syftPkg.Go.String()},\n\t\t},\n\t\t{\n\t\t\tCPEs:       []cpe.CPE{cpe.Must(\"cpe:2.3:a:golang:go:1.18.3:-:*:*:*:*:*:*\", \"test\")},\n\t\t\tConstraint: version.MustGetConstraint(\"< 1.18.6 || = 1.19.0\", version.UnknownFormat),\n\t\t\tReference:  vulnerability.Reference{ID: \"CVE-2022-27664\", Namespace: \"nvd:cpe\"},\n\t\t},\n\t}...)\n}\n"
  },
  {
    "path": "grype/matcher/hex/matcher.go",
    "content": "package hex\n\nimport (\n\t\"github.com/anchore/grype/grype/match\"\n\t\"github.com/anchore/grype/grype/matcher/internal\"\n\t\"github.com/anchore/grype/grype/pkg\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n\tsyftPkg \"github.com/anchore/syft/syft/pkg\"\n)\n\ntype Matcher struct {\n\tcfg MatcherConfig\n}\n\ntype MatcherConfig struct {\n\tUseCPEs bool\n}\n\nfunc NewHexMatcher(cfg MatcherConfig) *Matcher {\n\treturn &Matcher{\n\t\tcfg: cfg,\n\t}\n}\n\nfunc (m *Matcher) PackageTypes() []syftPkg.Type {\n\treturn []syftPkg.Type{syftPkg.HexPkg, syftPkg.ErlangOTPPkg}\n}\n\nfunc (m *Matcher) Type() match.MatcherType {\n\treturn match.HexMatcher\n}\n\nfunc (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoreFilter, error) {\n\treturn internal.MatchPackageByEcosystemAndCPEs(store, p, m.Type(), m.cfg.UseCPEs)\n}\n"
  },
  {
    "path": "grype/matcher/internal/common.go",
    "content": "package internal\n\nimport (\n\t\"errors\"\n\n\t\"github.com/anchore/grype/grype/match\"\n\t\"github.com/anchore/grype/grype/pkg\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n\t\"github.com/anchore/grype/internal/log\"\n)\n\nfunc MatchPackageByEcosystemAndCPEs(store vulnerability.Provider, p pkg.Package, matcher match.MatcherType, includeCPEs bool) ([]match.Match, []match.IgnoreFilter, error) {\n\tvar matches []match.Match\n\tvar ignored []match.IgnoreFilter\n\n\tfor _, name := range store.PackageSearchNames(p) {\n\t\tnameMatches, nameIgnores, err := MatchPackageByEcosystemPackageNameAndCPEs(store, p, name, matcher, includeCPEs)\n\t\tif err != nil {\n\t\t\treturn nil, nil, err\n\t\t}\n\t\tmatches = append(matches, nameMatches...)\n\t\tignored = append(ignored, nameIgnores...)\n\t}\n\n\treturn matches, ignored, nil\n}\n\nfunc MatchPackageByEcosystemPackageNameAndCPEs(store vulnerability.Provider, p pkg.Package, packageName string, matcher match.MatcherType, includeCPEs bool) ([]match.Match, []match.IgnoreFilter, error) {\n\tmatches, ignored, err := MatchPackageByEcosystemPackageName(store, p, packageName, matcher)\n\tif err != nil {\n\t\tlog.Debugf(\"could not match by package ecosystem (package=%+v): %v\", p, err)\n\t}\n\tif includeCPEs {\n\t\tcpeMatches, err := MatchPackageByCPEs(store, p, matcher)\n\t\tif errors.Is(err, ErrEmptyCPEMatch) {\n\t\t\tlog.Debugf(\"attempted CPE search on %s, which has no CPEs. Consider re-running with --add-cpes-if-none\", p.Name)\n\t\t} else if err != nil {\n\t\t\tlog.Debugf(\"could not match by package CPE (package=%+v): %v\", p, err)\n\t\t}\n\t\tmatches = append(matches, cpeMatches...)\n\t}\n\treturn matches, ignored, nil\n}\n"
  },
  {
    "path": "grype/matcher/internal/cpe.go",
    "content": "package internal\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"sort\"\n\t\"strings\"\n\n\t\"github.com/facebookincubator/nvdtools/wfn\"\n\n\t\"github.com/anchore/grype/grype/match\"\n\t\"github.com/anchore/grype/grype/pkg\"\n\t\"github.com/anchore/grype/grype/search\"\n\t\"github.com/anchore/grype/grype/version\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n\t\"github.com/anchore/grype/internal/log\"\n\t\"github.com/anchore/syft/syft/cpe\"\n\tsyftPkg \"github.com/anchore/syft/syft/pkg\"\n)\n\nfunc alpineCPEComparableVersion(version string) string {\n\t// clean the alpine package version so that it compares correctly with the CPE version comparison logic\n\t// alpine versions are suffixed with -r{buildindex}; however, if left intact CPE comparison logic will\n\t// incorrectly treat these as a pre-release.  In actuality, we just want to treat 1.2.3-r21 as equivalent to\n\t// 1.2.3 for purposes of CPE-based matching since the alpine fix should filter out any cases where a later\n\t// build fixes something that was vulnerable in 1.2.3\n\tcomponents := strings.Split(version, \"-r\")\n\tcpeComparableVersion := version\n\n\tif len(components) == 2 {\n\t\tcpeComparableVersion = components[0]\n\t}\n\n\treturn cpeComparableVersion\n}\n\nvar ErrEmptyCPEMatch = errors.New(\"attempted CPE match against package with no CPEs\")\n\n// MatchPackageByCPEs retrieves all vulnerabilities that match any of the provided package's CPEs\nfunc MatchPackageByCPEs(provider vulnerability.Provider, p pkg.Package, upstreamMatcher match.MatcherType) ([]match.Match, error) {\n\t// we attempt to merge match details within the same matcher when searching by CPEs, in this way there are fewer duplicated match\n\t// objects (and fewer duplicated match details).\n\n\t// Warn the user if they are matching by CPE, but there are no CPEs available.\n\tif len(p.CPEs) == 0 {\n\t\treturn nil, ErrEmptyCPEMatch\n\t}\n\n\tmatchesByFingerprint := make(map[match.Fingerprint]match.Match)\n\tfor _, c := range p.CPEs {\n\t\t// prefer the CPE version, but if npt specified use the package version\n\t\tsearchVersion := c.Attributes.Version\n\n\t\tif p.Type == syftPkg.ApkPkg {\n\t\t\tsearchVersion = alpineCPEComparableVersion(searchVersion)\n\t\t}\n\n\t\tif searchVersion == wfn.NA || searchVersion == wfn.Any || isUnknownVersion(searchVersion) {\n\t\t\tsearchVersion = p.Version\n\t\t}\n\n\t\tif isUnknownVersion(searchVersion) {\n\t\t\tlog.WithFields(\"package\", p.Name).Trace(\"skipping package with unknown version\")\n\t\t\tcontinue\n\t\t}\n\n\t\t// we should always show the exact CPE we searched by, not just what's in the component analysis (since we\n\t\t// may alter the version based on above processing)\n\t\tc.Attributes.Version = searchVersion\n\n\t\tformat := pkg.VersionFormat(p)\n\n\t\tif format == version.JVMFormat {\n\t\t\tsearchVersion = transformJvmVersion(searchVersion, c.Attributes.Update)\n\t\t}\n\n\t\tvar verObj *version.Version\n\t\tvar err error\n\t\tif searchVersion != \"\" {\n\t\t\tverObj = version.New(searchVersion, format)\n\t\t}\n\n\t\t// find all vulnerability records in the DB for the given CPE (not including version comparisons)\n\t\tvulns, err := provider.FindVulnerabilities(\n\t\t\tsearch.ByCPE(c),\n\t\t\tOnlyVulnerableTargets(p),\n\t\t\tOnlyQualifiedPackages(p),\n\t\t\tOnlyVulnerableVersions(verObj),\n\t\t\tOnlyNonWithdrawnVulnerabilities(),\n\t\t)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"matcher failed to fetch by CPE pkg=%q: %w\", p.Name, err)\n\t\t}\n\n\t\t// for each vulnerability record found, check the version constraint. If the constraint is satisfied\n\t\t// relative to the current version information from the CPE (or the package) then the given package\n\t\t// is vulnerable.\n\t\tfor _, vuln := range vulns {\n\t\t\taddNewMatch(matchesByFingerprint, vuln, p, verObj, upstreamMatcher, c)\n\t\t}\n\t}\n\n\treturn toMatches(matchesByFingerprint), nil\n}\n\nfunc transformJvmVersion(searchVersion, updateCpeField string) string {\n\t// we should take into consideration the CPE update field for JVM packages\n\tif strings.HasPrefix(searchVersion, \"1.\") && !strings.Contains(searchVersion, \"_\") && updateCpeField != wfn.NA && updateCpeField != wfn.Any {\n\t\tsearchVersion = fmt.Sprintf(\"%s_%s\", searchVersion, strings.TrimPrefix(updateCpeField, \"update\"))\n\t}\n\treturn searchVersion\n}\n\nfunc addNewMatch(matchesByFingerprint map[match.Fingerprint]match.Match, vuln vulnerability.Vulnerability, p pkg.Package, searchVersion *version.Version, upstreamMatcher match.MatcherType, searchedByCPE cpe.CPE) {\n\tcandidateMatch := match.Match{\n\n\t\tVulnerability: vuln,\n\t\tPackage:       p,\n\t}\n\n\tif existingMatch, exists := matchesByFingerprint[candidateMatch.Fingerprint()]; exists {\n\t\tcandidateMatch = existingMatch\n\t}\n\n\tcandidateMatch.Details = addMatchDetails(candidateMatch.Details,\n\t\tCPEMatchDetails(upstreamMatcher, vuln, searchedByCPE, p, searchVersion),\n\t)\n\n\tmatchesByFingerprint[candidateMatch.Fingerprint()] = candidateMatch\n}\n\nfunc CPEMatchDetails(matcherType match.MatcherType, vuln vulnerability.Vulnerability, searchedByCPE cpe.CPE, p pkg.Package, searchVersion *version.Version) match.Detail {\n\treturn match.Detail{\n\t\tType:       match.CPEMatch,\n\t\tConfidence: 0.9, // TODO: this is hard coded for now\n\t\tMatcher:    matcherType,\n\t\tSearchedBy: match.CPEParameters{\n\t\t\tNamespace: vuln.Namespace,\n\t\t\tCPEs: []string{\n\t\t\t\t// use .String() for proper escaping\n\t\t\t\tsearchedByCPE.Attributes.String(),\n\t\t\t},\n\t\t\tPackage: match.PackageParameter{\n\t\t\t\tName:    p.Name,\n\t\t\t\tVersion: p.Version,\n\t\t\t},\n\t\t},\n\t\tFound: match.CPEResult{\n\t\t\tVulnerabilityID:   vuln.ID,\n\t\t\tVersionConstraint: vuln.Constraint.String(),\n\t\t\tCPEs:              cpesToString(filterCPEsByVersion(searchVersion, vuln.CPEs)),\n\t\t},\n\t}\n}\n\nfunc addMatchDetails(existingDetails []match.Detail, newDetails match.Detail) []match.Detail {\n\tnewFound, ok := newDetails.Found.(match.CPEResult)\n\tif !ok {\n\t\treturn existingDetails\n\t}\n\n\tnewSearchedBy, ok := newDetails.SearchedBy.(match.CPEParameters)\n\tif !ok {\n\t\treturn existingDetails\n\t}\n\tfor idx, detail := range existingDetails {\n\t\tfound, ok := detail.Found.(match.CPEResult)\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\n\t\tsearchedBy, ok := detail.SearchedBy.(match.CPEParameters)\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\n\t\tif !found.Equals(newFound) {\n\t\t\tcontinue\n\t\t}\n\n\t\terr := searchedBy.Merge(newSearchedBy)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\texistingDetails[idx].SearchedBy = searchedBy\n\t\treturn existingDetails\n\t}\n\n\t// could not merge with another entry, append to the end\n\texistingDetails = append(existingDetails, newDetails)\n\treturn existingDetails\n}\n\nfunc filterCPEsByVersion(pkgVersion *version.Version, allCPEs []cpe.CPE) (matchedCPEs []cpe.CPE) {\n\tif pkgVersion == nil {\n\t\t// all CPEs are valid in the case when a version is not specified\n\t\treturn allCPEs\n\t}\n\tfor _, c := range allCPEs {\n\t\tif c.Attributes.Version == wfn.Any || c.Attributes.Version == wfn.NA {\n\t\t\tmatchedCPEs = append(matchedCPEs, c)\n\t\t\tcontinue\n\t\t}\n\n\t\tver := c.Attributes.Version\n\n\t\tif pkgVersion.Format == version.JVMFormat {\n\t\t\tif c.Attributes.Update != wfn.Any && c.Attributes.Update != wfn.NA {\n\t\t\t\tver = transformJvmVersion(ver, c.Attributes.Update)\n\t\t\t}\n\t\t}\n\n\t\tconstraint, err := version.GetConstraint(ver, pkgVersion.Format)\n\t\tif err != nil {\n\t\t\t// if we can't get a version constraint, don't filter out the CPE\n\t\t\tmatchedCPEs = append(matchedCPEs, c)\n\t\t\tcontinue\n\t\t}\n\n\t\tsatisfied, err := constraint.Satisfied(pkgVersion)\n\t\tif err != nil || satisfied {\n\t\t\t// if we can't check for version satisfaction, don't filter out the CPE\n\t\t\tmatchedCPEs = append(matchedCPEs, c)\n\t\t\tcontinue\n\t\t}\n\t}\n\treturn matchedCPEs\n}\n\nfunc toMatches(matchesByFingerprint map[match.Fingerprint]match.Match) (matches []match.Match) {\n\tfor _, m := range matchesByFingerprint {\n\t\tmatches = append(matches, m)\n\t}\n\tsort.Sort(match.ByElements(matches))\n\treturn matches\n}\n\n// cpesToString receives one or more CPEs and stringifies them\nfunc cpesToString(cpes []cpe.CPE) []string {\n\tvar strs = make([]string, len(cpes))\n\tfor idx, c := range cpes {\n\t\t// use .String() for proper escaping\n\t\tstrs[idx] = c.Attributes.String()\n\t}\n\tsort.Strings(strs)\n\treturn strs\n}\n"
  },
  {
    "path": "grype/matcher/internal/cpe_test.go",
    "content": "package internal\n\nimport (\n\t\"errors\"\n\t\"testing\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/google/uuid\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/grype/grype/match\"\n\t\"github.com/anchore/grype/grype/pkg\"\n\t\"github.com/anchore/grype/grype/version\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n\t\"github.com/anchore/grype/grype/vulnerability/mock\"\n\t\"github.com/anchore/syft/syft/cpe\"\n\tsyftPkg \"github.com/anchore/syft/syft/pkg\"\n)\n\nfunc newCPETestStore() vulnerability.Provider {\n\treturn mock.VulnerabilityProvider([]vulnerability.Vulnerability{\n\t\t{\n\t\t\tReference: vulnerability.Reference{\n\t\t\t\tID:        \"CVE-2017-fake-1\",\n\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t},\n\t\t\tPackageName: \"activerecord\",\n\t\t\tConstraint:  version.MustGetConstraint(\"< 3.7.6\", version.GemFormat),\n\t\t\tCPEs:        []cpe.CPE{cpe.Must(\"cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*\", \"\")},\n\t\t},\n\t\t{\n\t\t\tReference: vulnerability.Reference{\n\t\t\t\tID:        \"CVE-2017-fake-2\",\n\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t},\n\t\t\tPackageName: \"activerecord\",\n\t\t\tConstraint:  version.MustGetConstraint(\"< 3.7.4\", version.GemFormat),\n\t\t\tCPEs:        []cpe.CPE{cpe.Must(\"cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:ruby:*:*\", \"\")},\n\t\t},\n\t\t{\n\t\t\tReference: vulnerability.Reference{\n\t\t\t\tID:        \"CVE-2017-fake-3\",\n\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t},\n\t\t\tPackageName: \"activerecord\",\n\t\t\tConstraint:  version.MustGetConstraint(\"= 4.0.1\", version.GemFormat),\n\t\t\tCPEs:        []cpe.CPE{cpe.Must(\"cpe:2.3:*:activerecord:activerecord:4.0.1:*:*:*:*:*:*:*\", \"\")},\n\t\t},\n\t\t{\n\t\t\tReference: vulnerability.Reference{\n\t\t\t\tID:        \"CVE-2017-fake-4\",\n\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t},\n\t\t\tPackageName: \"awesome\",\n\t\t\tConstraint:  version.MustGetConstraint(\"< 98SP3\", version.UnknownFormat),\n\t\t\tCPEs: []cpe.CPE{\n\t\t\t\tcpe.Must(\"cpe:2.3:*:awesome:awesome:*:*:*:*:*:*:*:*\", \"\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tReference: vulnerability.Reference{\n\t\t\t\tID:        \"CVE-2017-fake-5\",\n\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t},\n\t\t\tPackageName: \"multiple\",\n\t\t\tConstraint:  version.MustGetConstraint(\"< 4.0\", version.UnknownFormat),\n\t\t\tCPEs: []cpe.CPE{\n\t\t\t\tcpe.Must(\"cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*\", \"\"),\n\t\t\t\tcpe.Must(\"cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*\", \"\"),\n\t\t\t\tcpe.Must(\"cpe:2.3:*:multiple:multiple:2.0:*:*:*:*:*:*:*\", \"\"),\n\t\t\t\tcpe.Must(\"cpe:2.3:*:multiple:multiple:3.0:*:*:*:*:*:*:*\", \"\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tReference: vulnerability.Reference{\n\t\t\t\tID:        \"CVE-2017-fake-6\",\n\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t},\n\t\t\tPackageName: \"funfun\",\n\t\t\tConstraint:  version.MustGetConstraint(\"= 5.2.1\", version.UnknownFormat),\n\t\t\tCPEs: []cpe.CPE{\n\t\t\t\tcpe.Must(\"cpe:2.3:*:funfun:funfun:5.2.1:*:*:*:*:python:*:*\", \"\"),\n\t\t\t\tcpe.Must(\"cpe:2.3:*:funfun:funfun:*:*:*:*:*:python:*:*\", \"\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tReference: vulnerability.Reference{\n\t\t\t\tID:        \"CVE-2017-fake-7\",\n\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t},\n\t\t\tPackageName: \"sw\",\n\t\t\tConstraint:  version.MustGetConstraint(\"< 1.0\", version.UnknownFormat),\n\t\t\tCPEs:        []cpe.CPE{cpe.Must(\"cpe:2.3:*:sw:sw:*:*:*:*:*:puppet:*:*\", \"\")},\n\t\t},\n\t\t{\n\t\t\tReference: vulnerability.Reference{\n\t\t\t\tID:        \"CVE-2021-23369\",\n\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t},\n\t\t\tPackageName: \"handlebars\",\n\t\t\tConstraint:  version.MustGetConstraint(\"< 4.7.7\", version.UnknownFormat),\n\t\t\tCPEs:        []cpe.CPE{cpe.Must(\"cpe:2.3:a:handlebarsjs:handlebars:*:*:*:*:*:node.js:*:*\", \"\")},\n\t\t},\n\t}...)\n}\n\nfunc TestFindMatchesByPackageCPE(t *testing.T) {\n\tmatcher := match.RubyGemMatcher\n\ttests := []struct {\n\t\tname     string\n\t\tp        pkg.Package\n\t\texpected []match.Match\n\t\twantErr  require.ErrorAssertionFunc\n\t}{\n\t\t{\n\t\t\tname: \"match from range\",\n\t\t\tp: pkg.Package{\n\t\t\t\tCPEs: []cpe.CPE{\n\t\t\t\t\tcpe.Must(\"cpe:2.3:*:activerecord:activerecord:3.7.5:rando1:*:ra:*:ruby:*:*\", \"\"),\n\t\t\t\t\tcpe.Must(\"cpe:2.3:*:activerecord:activerecord:3.7.5:rando4:*:re:*:rails:*:*\", \"\"),\n\t\t\t\t},\n\t\t\t\tName:     \"activerecord\",\n\t\t\t\tVersion:  \"3.7.5\",\n\t\t\t\tLanguage: syftPkg.Ruby,\n\t\t\t\tType:     syftPkg.GemPkg,\n\t\t\t},\n\t\t\texpected: []match.Match{\n\t\t\t\t{\n\n\t\t\t\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\t\t\t\tReference: vulnerability.Reference{ID: \"CVE-2017-fake-1\"},\n\t\t\t\t\t},\n\t\t\t\t\tPackage: pkg.Package{\n\t\t\t\t\t\tCPEs: []cpe.CPE{\n\t\t\t\t\t\t\tcpe.Must(\"cpe:2.3:*:activerecord:activerecord:3.7.5:rando1:*:ra:*:ruby:*:*\", \"\"),\n\t\t\t\t\t\t\tcpe.Must(\"cpe:2.3:*:activerecord:activerecord:3.7.5:rando4:*:re:*:rails:*:*\", \"\"),\n\t\t\t\t\t\t},\n\t\t\t\t\t\tName:     \"activerecord\",\n\t\t\t\t\t\tVersion:  \"3.7.5\",\n\t\t\t\t\t\tLanguage: syftPkg.Ruby,\n\t\t\t\t\t\tType:     syftPkg.GemPkg,\n\t\t\t\t\t},\n\t\t\t\t\tDetails: []match.Detail{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tType:       match.CPEMatch,\n\t\t\t\t\t\t\tConfidence: 0.9,\n\t\t\t\t\t\t\tSearchedBy: match.CPEParameters{\n\t\t\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\t\t\tCPEs:      []string{\"cpe:2.3:*:activerecord:activerecord:3.7.5:rando4:*:re:*:rails:*:*\"},\n\t\t\t\t\t\t\t\tPackage: match.PackageParameter{\n\t\t\t\t\t\t\t\t\tName:    \"activerecord\",\n\t\t\t\t\t\t\t\t\tVersion: \"3.7.5\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tFound: match.CPEResult{\n\t\t\t\t\t\t\t\tCPEs:              []string{\"cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*\"},\n\t\t\t\t\t\t\t\tVersionConstraint: \"< 3.7.6 (gem)\",\n\t\t\t\t\t\t\t\tVulnerabilityID:   \"CVE-2017-fake-1\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tMatcher: matcher,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"fallback to package version\",\n\t\t\tp: pkg.Package{\n\t\t\t\tCPEs: []cpe.CPE{\n\t\t\t\t\tcpe.Must(\"cpe:2.3:*:activerecord:activerecord:unknown:rando1:*:ra:*:ruby:*:*\", \"\"),\n\t\t\t\t\tcpe.Must(\"cpe:2.3:*:activerecord:activerecord:unknown:rando4:*:re:*:rails:*:*\", \"\"),\n\t\t\t\t},\n\t\t\t\tName:     \"activerecord\",\n\t\t\t\tVersion:  \"3.7.5\",\n\t\t\t\tLanguage: syftPkg.Ruby,\n\t\t\t\tType:     syftPkg.GemPkg,\n\t\t\t},\n\t\t\texpected: []match.Match{\n\t\t\t\t{\n\n\t\t\t\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\t\t\t\tReference: vulnerability.Reference{ID: \"CVE-2017-fake-1\"},\n\t\t\t\t\t},\n\t\t\t\t\tPackage: pkg.Package{\n\t\t\t\t\t\tCPEs: []cpe.CPE{\n\t\t\t\t\t\t\tcpe.Must(\"cpe:2.3:*:activerecord:activerecord:unknown:rando1:*:ra:*:ruby:*:*\", \"\"),\n\t\t\t\t\t\t\tcpe.Must(\"cpe:2.3:*:activerecord:activerecord:unknown:rando4:*:re:*:rails:*:*\", \"\"),\n\t\t\t\t\t\t},\n\t\t\t\t\t\tName:     \"activerecord\",\n\t\t\t\t\t\tVersion:  \"3.7.5\",\n\t\t\t\t\t\tLanguage: syftPkg.Ruby,\n\t\t\t\t\t\tType:     syftPkg.GemPkg,\n\t\t\t\t\t},\n\t\t\t\t\tDetails: []match.Detail{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tType:       match.CPEMatch,\n\t\t\t\t\t\t\tConfidence: 0.9,\n\t\t\t\t\t\t\tSearchedBy: match.CPEParameters{\n\t\t\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\t\t\tCPEs:      []string{\"cpe:2.3:*:activerecord:activerecord:3.7.5:rando4:*:re:*:rails:*:*\"},\n\t\t\t\t\t\t\t\tPackage: match.PackageParameter{\n\t\t\t\t\t\t\t\t\tName:    \"activerecord\",\n\t\t\t\t\t\t\t\t\tVersion: \"3.7.5\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tFound: match.CPEResult{\n\t\t\t\t\t\t\t\tCPEs:              []string{\"cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*\"},\n\t\t\t\t\t\t\t\tVersionConstraint: \"< 3.7.6 (gem)\",\n\t\t\t\t\t\t\t\tVulnerabilityID:   \"CVE-2017-fake-1\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tMatcher: matcher,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"return all possible matches when missing version\",\n\t\t\tp: pkg.Package{\n\t\t\t\tCPEs: []cpe.CPE{\n\t\t\t\t\tcpe.Must(\"cpe:2.3:*:activerecord:activerecord:*:rando1:*:ra:*:ruby:*:*\", \"\"),\n\t\t\t\t\tcpe.Must(\"cpe:2.3:*:activerecord:activerecord:*:rando4:*:re:*:rails:*:*\", \"\"),\n\t\t\t\t},\n\t\t\t\tName:     \"activerecord\",\n\t\t\t\tVersion:  \"\",\n\t\t\t\tLanguage: syftPkg.Ruby,\n\t\t\t\tType:     syftPkg.GemPkg,\n\t\t\t},\n\t\t\texpected: []match.Match{\n\t\t\t\t{\n\n\t\t\t\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\t\t\t\tReference: vulnerability.Reference{ID: \"CVE-2017-fake-1\"},\n\t\t\t\t\t},\n\t\t\t\t\tPackage: pkg.Package{\n\t\t\t\t\t\tCPEs: []cpe.CPE{\n\t\t\t\t\t\t\tcpe.Must(\"cpe:2.3:*:activerecord:activerecord:*:rando1:*:ra:*:ruby:*:*\", \"\"),\n\t\t\t\t\t\t\tcpe.Must(\"cpe:2.3:*:activerecord:activerecord:*:rando4:*:re:*:rails:*:*\", \"\"),\n\t\t\t\t\t\t},\n\t\t\t\t\t\tName:     \"activerecord\",\n\t\t\t\t\t\tVersion:  \"\", // important!\n\t\t\t\t\t\tLanguage: syftPkg.Ruby,\n\t\t\t\t\t\tType:     syftPkg.GemPkg,\n\t\t\t\t\t},\n\n\t\t\t\t\tDetails: []match.Detail{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tType:       match.CPEMatch,\n\t\t\t\t\t\t\tConfidence: 0.9,\n\t\t\t\t\t\t\tSearchedBy: match.CPEParameters{\n\t\t\t\t\t\t\t\tCPEs: []string{\n\t\t\t\t\t\t\t\t\t\"cpe:2.3:*:activerecord:activerecord:*:rando4:*:re:*:rails:*:*\", //important!\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\t\t\tPackage: match.PackageParameter{\n\t\t\t\t\t\t\t\t\tName:    \"activerecord\",\n\t\t\t\t\t\t\t\t\tVersion: \"\", // important!\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tFound: match.CPEResult{\n\t\t\t\t\t\t\t\tCPEs:              []string{\"cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*\"},\n\t\t\t\t\t\t\t\tVersionConstraint: \"< 3.7.6 (gem)\",\n\t\t\t\t\t\t\t\tVulnerabilityID:   \"CVE-2017-fake-1\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tMatcher: matcher,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\n\t\t\t\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\t\t\t\tReference: vulnerability.Reference{ID: \"CVE-2017-fake-2\"},\n\t\t\t\t\t},\n\t\t\t\t\tPackage: pkg.Package{\n\t\t\t\t\t\tCPEs: []cpe.CPE{\n\t\t\t\t\t\t\tcpe.Must(\"cpe:2.3:*:activerecord:activerecord:*:rando1:*:ra:*:ruby:*:*\", \"\"),\n\t\t\t\t\t\t\tcpe.Must(\"cpe:2.3:*:activerecord:activerecord:*:rando4:*:re:*:rails:*:*\", \"\"),\n\t\t\t\t\t\t},\n\t\t\t\t\t\tName:     \"activerecord\",\n\t\t\t\t\t\tVersion:  \"\", // important!\n\t\t\t\t\t\tLanguage: syftPkg.Ruby,\n\t\t\t\t\t\tType:     syftPkg.GemPkg,\n\t\t\t\t\t},\n\n\t\t\t\t\tDetails: []match.Detail{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tType:       match.CPEMatch,\n\t\t\t\t\t\t\tConfidence: 0.9,\n\t\t\t\t\t\t\tSearchedBy: match.CPEParameters{\n\t\t\t\t\t\t\t\tCPEs:      []string{\"cpe:2.3:*:activerecord:activerecord:*:rando1:*:ra:*:ruby:*:*\"}, //important!\n\t\t\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\t\t\tPackage: match.PackageParameter{\n\t\t\t\t\t\t\t\t\tName:    \"activerecord\",\n\t\t\t\t\t\t\t\t\tVersion: \"\", // important!\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tFound: match.CPEResult{\n\t\t\t\t\t\t\t\tCPEs:              []string{\"cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:ruby:*:*\"},\n\t\t\t\t\t\t\t\tVersionConstraint: \"< 3.7.4 (gem)\",\n\t\t\t\t\t\t\t\tVulnerabilityID:   \"CVE-2017-fake-2\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tMatcher: matcher,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\n\t\t\t\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\t\t\t\tReference: vulnerability.Reference{ID: \"CVE-2017-fake-3\"},\n\t\t\t\t\t},\n\t\t\t\t\tPackage: pkg.Package{\n\t\t\t\t\t\tCPEs: []cpe.CPE{\n\t\t\t\t\t\t\tcpe.Must(\"cpe:2.3:*:activerecord:activerecord:*:rando1:*:ra:*:ruby:*:*\", \"\"),\n\t\t\t\t\t\t\tcpe.Must(\"cpe:2.3:*:activerecord:activerecord:*:rando4:*:re:*:rails:*:*\", \"\"),\n\t\t\t\t\t\t},\n\t\t\t\t\t\tName:     \"activerecord\",\n\t\t\t\t\t\tVersion:  \"\", // important!\n\t\t\t\t\t\tLanguage: syftPkg.Ruby,\n\t\t\t\t\t\tType:     syftPkg.GemPkg,\n\t\t\t\t\t},\n\t\t\t\t\tDetails: []match.Detail{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tType:       match.CPEMatch,\n\t\t\t\t\t\t\tConfidence: 0.9,\n\t\t\t\t\t\t\tSearchedBy: match.CPEParameters{\n\t\t\t\t\t\t\t\tCPEs: []string{\n\t\t\t\t\t\t\t\t\t\"cpe:2.3:*:activerecord:activerecord:*:rando1:*:ra:*:ruby:*:*\",  //important!\n\t\t\t\t\t\t\t\t\t\"cpe:2.3:*:activerecord:activerecord:*:rando4:*:re:*:rails:*:*\", //important!\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\t\t\tPackage: match.PackageParameter{\n\t\t\t\t\t\t\t\t\tName:    \"activerecord\",\n\t\t\t\t\t\t\t\t\tVersion: \"\", // important!\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tFound: match.CPEResult{\n\t\t\t\t\t\t\t\tCPEs:              []string{\"cpe:2.3:*:activerecord:activerecord:4.0.1:*:*:*:*:*:*:*\"},\n\t\t\t\t\t\t\t\tVersionConstraint: \"= 4.0.1 (gem)\",\n\t\t\t\t\t\t\t\tVulnerabilityID:   \"CVE-2017-fake-3\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tMatcher: matcher,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"suppress matching when version is unknown\",\n\t\t\tp: pkg.Package{\n\t\t\t\tCPEs: []cpe.CPE{\n\t\t\t\t\tcpe.Must(\"cpe:2.3:*:activerecord:activerecord:*:rando1:*:ra:*:ruby:*:*\", \"\"),\n\t\t\t\t\tcpe.Must(\"cpe:2.3:*:activerecord:activerecord:*:rando4:*:re:*:rails:*:*\", \"\"),\n\t\t\t\t},\n\t\t\t\tName:     \"activerecord\",\n\t\t\t\tVersion:  \"unknown\",\n\t\t\t\tLanguage: syftPkg.Ruby,\n\t\t\t\tType:     syftPkg.GemPkg,\n\t\t\t},\n\t\t\texpected: []match.Match{},\n\t\t},\n\t\t{\n\t\t\tname: \"multiple matches\",\n\t\t\tp: pkg.Package{\n\t\t\t\tCPEs: []cpe.CPE{\n\t\t\t\t\tcpe.Must(\"cpe:2.3:*:activerecord:activerecord:3.7.3:rando1:*:ra:*:ruby:*:*\", \"\"),\n\t\t\t\t\tcpe.Must(\"cpe:2.3:*:activerecord:activerecord:3.7.3:rando4:*:re:*:rails:*:*\", \"\"),\n\t\t\t\t},\n\t\t\t\tName:     \"activerecord\",\n\t\t\t\tVersion:  \"3.7.3\",\n\t\t\t\tLanguage: syftPkg.Ruby,\n\t\t\t\tType:     syftPkg.GemPkg,\n\t\t\t},\n\t\t\texpected: []match.Match{\n\t\t\t\t{\n\n\t\t\t\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\t\t\t\tReference: vulnerability.Reference{ID: \"CVE-2017-fake-1\"},\n\t\t\t\t\t},\n\t\t\t\t\tPackage: pkg.Package{\n\t\t\t\t\t\tCPEs: []cpe.CPE{\n\t\t\t\t\t\t\tcpe.Must(\"cpe:2.3:*:activerecord:activerecord:3.7.3:rando1:*:ra:*:ruby:*:*\", \"\"),\n\t\t\t\t\t\t\tcpe.Must(\"cpe:2.3:*:activerecord:activerecord:3.7.3:rando4:*:re:*:rails:*:*\", \"\"),\n\t\t\t\t\t\t},\n\t\t\t\t\t\tName:     \"activerecord\",\n\t\t\t\t\t\tVersion:  \"3.7.3\",\n\t\t\t\t\t\tLanguage: syftPkg.Ruby,\n\t\t\t\t\t\tType:     syftPkg.GemPkg,\n\t\t\t\t\t},\n\n\t\t\t\t\tDetails: []match.Detail{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tType:       match.CPEMatch,\n\t\t\t\t\t\t\tConfidence: 0.9,\n\t\t\t\t\t\t\tSearchedBy: match.CPEParameters{\n\t\t\t\t\t\t\t\tCPEs: []string{\n\t\t\t\t\t\t\t\t\t\"cpe:2.3:*:activerecord:activerecord:3.7.3:rando4:*:re:*:rails:*:*\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\t\t\tPackage: match.PackageParameter{\n\t\t\t\t\t\t\t\t\tName:    \"activerecord\",\n\t\t\t\t\t\t\t\t\tVersion: \"3.7.3\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tFound: match.CPEResult{\n\t\t\t\t\t\t\t\tCPEs:              []string{\"cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*\"},\n\t\t\t\t\t\t\t\tVersionConstraint: \"< 3.7.6 (gem)\",\n\t\t\t\t\t\t\t\tVulnerabilityID:   \"CVE-2017-fake-1\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tMatcher: matcher,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\n\t\t\t\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\t\t\t\tReference: vulnerability.Reference{ID: \"CVE-2017-fake-2\"},\n\t\t\t\t\t},\n\t\t\t\t\tPackage: pkg.Package{\n\t\t\t\t\t\tCPEs: []cpe.CPE{\n\t\t\t\t\t\t\tcpe.Must(\"cpe:2.3:*:activerecord:activerecord:3.7.3:rando1:*:ra:*:ruby:*:*\", \"\"),\n\t\t\t\t\t\t\tcpe.Must(\"cpe:2.3:*:activerecord:activerecord:3.7.3:rando4:*:re:*:rails:*:*\", \"\"),\n\t\t\t\t\t\t},\n\t\t\t\t\t\tName:     \"activerecord\",\n\t\t\t\t\t\tVersion:  \"3.7.3\",\n\t\t\t\t\t\tLanguage: syftPkg.Ruby,\n\t\t\t\t\t\tType:     syftPkg.GemPkg,\n\t\t\t\t\t},\n\n\t\t\t\t\tDetails: []match.Detail{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tType:       match.CPEMatch,\n\t\t\t\t\t\t\tConfidence: 0.9,\n\t\t\t\t\t\t\tSearchedBy: match.CPEParameters{\n\t\t\t\t\t\t\t\tCPEs:      []string{\"cpe:2.3:*:activerecord:activerecord:3.7.3:rando1:*:ra:*:ruby:*:*\"},\n\t\t\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\t\t\tPackage: match.PackageParameter{\n\t\t\t\t\t\t\t\t\tName:    \"activerecord\",\n\t\t\t\t\t\t\t\t\tVersion: \"3.7.3\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tFound: match.CPEResult{\n\t\t\t\t\t\t\t\tCPEs:              []string{\"cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:ruby:*:*\"},\n\t\t\t\t\t\t\t\tVersionConstraint: \"< 3.7.4 (gem)\",\n\t\t\t\t\t\t\t\tVulnerabilityID:   \"CVE-2017-fake-2\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tMatcher: matcher,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"exact match\",\n\t\t\tp: pkg.Package{\n\t\t\t\tCPEs: []cpe.CPE{\n\t\t\t\t\tcpe.Must(\"cpe:2.3:*:*:activerecord:4.0.1:*:*:*:*:*:*:*\", \"\"),\n\t\t\t\t},\n\t\t\t\tName:     \"activerecord\",\n\t\t\t\tVersion:  \"4.0.1\",\n\t\t\t\tLanguage: syftPkg.Ruby,\n\t\t\t\tType:     syftPkg.GemPkg,\n\t\t\t},\n\t\t\texpected: []match.Match{\n\t\t\t\t{\n\n\t\t\t\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\t\t\t\tReference: vulnerability.Reference{ID: \"CVE-2017-fake-3\"},\n\t\t\t\t\t},\n\t\t\t\t\tPackage: pkg.Package{\n\t\t\t\t\t\tCPEs: []cpe.CPE{\n\t\t\t\t\t\t\tcpe.Must(\"cpe:2.3:*:*:activerecord:4.0.1:*:*:*:*:*:*:*\", \"\"),\n\t\t\t\t\t\t},\n\t\t\t\t\t\tName:     \"activerecord\",\n\t\t\t\t\t\tVersion:  \"4.0.1\",\n\t\t\t\t\t\tLanguage: syftPkg.Ruby,\n\t\t\t\t\t\tType:     syftPkg.GemPkg,\n\t\t\t\t\t},\n\t\t\t\t\tDetails: []match.Detail{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tType:       match.CPEMatch,\n\t\t\t\t\t\t\tConfidence: 0.9,\n\t\t\t\t\t\t\tSearchedBy: match.CPEParameters{\n\t\t\t\t\t\t\t\tCPEs:      []string{\"cpe:2.3:*:*:activerecord:4.0.1:*:*:*:*:*:*:*\"},\n\t\t\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\t\t\tPackage: match.PackageParameter{\n\t\t\t\t\t\t\t\t\tName:    \"activerecord\",\n\t\t\t\t\t\t\t\t\tVersion: \"4.0.1\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tFound: match.CPEResult{\n\t\t\t\t\t\t\t\tCPEs:              []string{\"cpe:2.3:*:activerecord:activerecord:4.0.1:*:*:*:*:*:*:*\"},\n\t\t\t\t\t\t\t\tVersionConstraint: \"= 4.0.1 (gem)\",\n\t\t\t\t\t\t\t\tVulnerabilityID:   \"CVE-2017-fake-3\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tMatcher: matcher,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"no match\",\n\t\t\tp: pkg.Package{\n\t\t\t\tID:       pkg.ID(uuid.NewString()),\n\t\t\t\tName:     \"couldntgetthisrightcouldyou\",\n\t\t\t\tVersion:  \"4.0.1\",\n\t\t\t\tLanguage: syftPkg.Ruby,\n\t\t\t\tType:     syftPkg.GemPkg,\n\t\t\t\tCPEs: []cpe.CPE{\n\t\t\t\t\tcpe.Must(\"cpe:2.3:a:no_match:no_match:0.9.9:*:*:*:*:*:*:*\", cpe.GeneratedSource),\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: []match.Match{},\n\t\t},\n\t\t{\n\t\t\tname: \"fuzzy version match\",\n\t\t\tp: pkg.Package{\n\t\t\t\tCPEs: []cpe.CPE{\n\t\t\t\t\tcpe.Must(\"cpe:2.3:*:awesome:awesome:98SE1:rando1:*:ra:*:dunno:*:*\", \"\"),\n\t\t\t\t},\n\t\t\t\tName:    \"awesome\",\n\t\t\t\tVersion: \"98SE1\",\n\t\t\t},\n\t\t\texpected: []match.Match{\n\t\t\t\t{\n\n\t\t\t\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\t\t\t\tReference: vulnerability.Reference{ID: \"CVE-2017-fake-4\"},\n\t\t\t\t\t},\n\t\t\t\t\tPackage: pkg.Package{\n\t\t\t\t\t\tCPEs: []cpe.CPE{\n\t\t\t\t\t\t\tcpe.Must(\"cpe:2.3:*:awesome:awesome:98SE1:rando1:*:ra:*:dunno:*:*\", \"\"),\n\t\t\t\t\t\t},\n\t\t\t\t\t\tName:    \"awesome\",\n\t\t\t\t\t\tVersion: \"98SE1\",\n\t\t\t\t\t},\n\n\t\t\t\t\tDetails: []match.Detail{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tType:       match.CPEMatch,\n\t\t\t\t\t\t\tConfidence: 0.9,\n\t\t\t\t\t\t\tSearchedBy: match.CPEParameters{\n\t\t\t\t\t\t\t\tCPEs:      []string{\"cpe:2.3:*:awesome:awesome:98SE1:rando1:*:ra:*:dunno:*:*\"},\n\t\t\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\t\t\tPackage: match.PackageParameter{\n\t\t\t\t\t\t\t\t\tName:    \"awesome\",\n\t\t\t\t\t\t\t\t\tVersion: \"98SE1\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tFound: match.CPEResult{\n\t\t\t\t\t\t\t\tCPEs:              []string{\"cpe:2.3:*:awesome:awesome:*:*:*:*:*:*:*:*\"},\n\t\t\t\t\t\t\t\tVersionConstraint: \"< 98SP3 (unknown)\",\n\t\t\t\t\t\t\t\tVulnerabilityID:   \"CVE-2017-fake-4\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tMatcher: matcher,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"multiple matched CPEs\",\n\t\t\tp: pkg.Package{\n\t\t\t\tCPEs: []cpe.CPE{\n\t\t\t\t\tcpe.Must(\"cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*\", \"\"),\n\t\t\t\t},\n\t\t\t\tName:     \"multiple\",\n\t\t\t\tVersion:  \"1.0\",\n\t\t\t\tLanguage: syftPkg.Ruby,\n\t\t\t\tType:     syftPkg.GemPkg,\n\t\t\t},\n\t\t\texpected: []match.Match{\n\t\t\t\t{\n\n\t\t\t\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\t\t\t\tReference: vulnerability.Reference{ID: \"CVE-2017-fake-5\"},\n\t\t\t\t\t},\n\t\t\t\t\tPackage: pkg.Package{\n\t\t\t\t\t\tCPEs: []cpe.CPE{\n\t\t\t\t\t\t\tcpe.Must(\"cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*\", \"\"),\n\t\t\t\t\t\t},\n\t\t\t\t\t\tName:     \"multiple\",\n\t\t\t\t\t\tVersion:  \"1.0\",\n\t\t\t\t\t\tLanguage: syftPkg.Ruby,\n\t\t\t\t\t\tType:     syftPkg.GemPkg,\n\t\t\t\t\t},\n\n\t\t\t\t\tDetails: []match.Detail{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tType:       match.CPEMatch,\n\t\t\t\t\t\t\tConfidence: 0.9,\n\t\t\t\t\t\t\tSearchedBy: match.CPEParameters{\n\t\t\t\t\t\t\t\tCPEs:      []string{\"cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*\"},\n\t\t\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\t\t\tPackage: match.PackageParameter{\n\t\t\t\t\t\t\t\t\tName:    \"multiple\",\n\t\t\t\t\t\t\t\t\tVersion: \"1.0\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tFound: match.CPEResult{\n\t\t\t\t\t\t\t\tCPEs: []string{\n\t\t\t\t\t\t\t\t\t\"cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*\",\n\t\t\t\t\t\t\t\t\t\"cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tVersionConstraint: \"< 4.0 (unknown)\",\n\t\t\t\t\t\t\t\tVulnerabilityID:   \"CVE-2017-fake-5\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tMatcher: matcher,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"filtered out match due to target_sw mismatch\",\n\t\t\tp: pkg.Package{\n\t\t\t\tCPEs: []cpe.CPE{\n\t\t\t\t\tcpe.Must(\"cpe:2.3:*:funfun:funfun:*:*:*:*:*:*:*:*\", \"\"),\n\t\t\t\t},\n\t\t\t\tName:     \"funfun\",\n\t\t\t\tVersion:  \"5.2.1\",\n\t\t\t\tLanguage: syftPkg.Rust, // this is identified as a rust package\n\t\t\t\tType:     syftPkg.RustPkg,\n\t\t\t},\n\t\t\texpected: []match.Match{},\n\t\t},\n\t\t{\n\t\t\tname: \"target_sw mismatch with unsupported target_sw\",\n\t\t\tp: pkg.Package{\n\t\t\t\tCPEs: []cpe.CPE{\n\t\t\t\t\tcpe.Must(\"cpe:2.3:*:sw:sw:*:*:*:*:*:*:*:*\", \"\"),\n\t\t\t\t},\n\t\t\t\tName:     \"sw\",\n\t\t\t\tVersion:  \"0.1\",\n\t\t\t\tLanguage: syftPkg.Erlang,\n\t\t\t\tType:     syftPkg.HexPkg,\n\t\t\t},\n\t\t\texpected: []match.Match{\n\t\t\t\t{\n\t\t\t\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\t\t\t\tReference: vulnerability.Reference{ID: \"CVE-2017-fake-7\"},\n\t\t\t\t\t},\n\t\t\t\t\tPackage: pkg.Package{\n\t\t\t\t\t\tCPEs: []cpe.CPE{\n\t\t\t\t\t\t\tcpe.Must(\"cpe:2.3:*:sw:sw:*:*:*:*:*:*:*:*\", \"\"),\n\t\t\t\t\t\t},\n\t\t\t\t\t\tName:     \"sw\",\n\t\t\t\t\t\tVersion:  \"0.1\",\n\t\t\t\t\t\tLanguage: syftPkg.Erlang,\n\t\t\t\t\t\tType:     syftPkg.HexPkg,\n\t\t\t\t\t},\n\t\t\t\t\tDetails: []match.Detail{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tType:       match.CPEMatch,\n\t\t\t\t\t\t\tConfidence: 0.9,\n\t\t\t\t\t\t\tSearchedBy: match.CPEParameters{\n\t\t\t\t\t\t\t\tCPEs:      []string{\"cpe:2.3:*:sw:sw:0.1:*:*:*:*:*:*:*\"},\n\t\t\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\t\t\tPackage: match.PackageParameter{\n\t\t\t\t\t\t\t\t\tName:    \"sw\",\n\t\t\t\t\t\t\t\t\tVersion: \"0.1\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tFound: match.CPEResult{\n\t\t\t\t\t\t\t\tCPEs: []string{\n\t\t\t\t\t\t\t\t\t\"cpe:2.3:*:sw:sw:*:*:*:*:*:puppet:*:*\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tVersionConstraint: \"< 1.0 (unknown)\",\n\t\t\t\t\t\t\t\tVulnerabilityID:   \"CVE-2017-fake-7\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tMatcher: matcher,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"match included even though multiple cpes are mismatch\",\n\t\t\tp: pkg.Package{\n\t\t\t\tCPEs: []cpe.CPE{\n\t\t\t\t\tcpe.Must(\"cpe:2.3:*:funfun:funfun:*:*:*:*:*:rust:*:*\", \"\"),\n\t\t\t\t\tcpe.Must(\"cpe:2.3:*:funfun:funfun:*:*:*:*:*:rails:*:*\", \"\"),\n\t\t\t\t\tcpe.Must(\"cpe:2.3:*:funfun:funfun:*:*:*:*:*:ruby:*:*\", \"\"),\n\t\t\t\t\tcpe.Must(\"cpe:2.3:*:funfun:funfun:*:*:*:*:*:python:*:*\", \"\"),\n\t\t\t\t},\n\t\t\t\tName:     \"funfun\",\n\t\t\t\tVersion:  \"5.2.1\",\n\t\t\t\tLanguage: syftPkg.Python,\n\t\t\t\tType:     syftPkg.PythonPkg,\n\t\t\t},\n\t\t\texpected: []match.Match{\n\t\t\t\t{\n\t\t\t\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\t\t\t\tReference: vulnerability.Reference{ID: \"CVE-2017-fake-6\"},\n\t\t\t\t\t},\n\t\t\t\t\tPackage: pkg.Package{\n\t\t\t\t\t\tCPEs: []cpe.CPE{\n\t\t\t\t\t\t\tcpe.Must(\"cpe:2.3:*:funfun:funfun:*:*:*:*:*:rust:*:*\", \"\"),\n\t\t\t\t\t\t\tcpe.Must(\"cpe:2.3:*:funfun:funfun:*:*:*:*:*:rails:*:*\", \"\"),\n\t\t\t\t\t\t\tcpe.Must(\"cpe:2.3:*:funfun:funfun:*:*:*:*:*:ruby:*:*\", \"\"),\n\t\t\t\t\t\t\tcpe.Must(\"cpe:2.3:*:funfun:funfun:*:*:*:*:*:python:*:*\", \"\"),\n\t\t\t\t\t\t},\n\t\t\t\t\t\tName:     \"funfun\",\n\t\t\t\t\t\tVersion:  \"5.2.1\",\n\t\t\t\t\t\tLanguage: syftPkg.Python,\n\t\t\t\t\t\tType:     syftPkg.PythonPkg,\n\t\t\t\t\t},\n\t\t\t\t\tDetails: []match.Detail{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tType:       match.CPEMatch,\n\t\t\t\t\t\t\tConfidence: 0.9,\n\t\t\t\t\t\t\tSearchedBy: match.CPEParameters{\n\t\t\t\t\t\t\t\tCPEs:      []string{\"cpe:2.3:*:funfun:funfun:5.2.1:*:*:*:*:python:*:*\"},\n\t\t\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\t\t\tPackage: match.PackageParameter{\n\t\t\t\t\t\t\t\t\tName:    \"funfun\",\n\t\t\t\t\t\t\t\t\tVersion: \"5.2.1\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tFound: match.CPEResult{\n\t\t\t\t\t\t\t\tCPEs: []string{\n\t\t\t\t\t\t\t\t\t\"cpe:2.3:*:funfun:funfun:*:*:*:*:*:python:*:*\",\n\t\t\t\t\t\t\t\t\t\"cpe:2.3:*:funfun:funfun:5.2.1:*:*:*:*:python:*:*\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tVersionConstraint: \"= 5.2.1 (unknown)\",\n\t\t\t\t\t\t\t\tVulnerabilityID:   \"CVE-2017-fake-6\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tMatcher: matcher,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Ensure target_sw mismatch does not apply to java packages\",\n\t\t\tp: pkg.Package{\n\t\t\t\tCPEs: []cpe.CPE{\n\t\t\t\t\tcpe.Must(\"cpe:2.3:a:handlebarsjs:handlebars:*:*:*:*:*:*:*:*\", \"\"),\n\t\t\t\t},\n\t\t\t\tName:     \"handlebars\",\n\t\t\t\tVersion:  \"0.1\",\n\t\t\t\tLanguage: syftPkg.Java,\n\t\t\t\tType:     syftPkg.JavaPkg,\n\t\t\t},\n\t\t\texpected: []match.Match{\n\t\t\t\t{\n\t\t\t\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\t\t\t\tReference: vulnerability.Reference{ID: \"CVE-2021-23369\"},\n\t\t\t\t\t},\n\t\t\t\t\tPackage: pkg.Package{\n\t\t\t\t\t\tCPEs: []cpe.CPE{\n\t\t\t\t\t\t\tcpe.Must(\"cpe:2.3:a:handlebarsjs:handlebars:*:*:*:*:*:*:*:*\", \"\"),\n\t\t\t\t\t\t},\n\t\t\t\t\t\tName:     \"handlebars\",\n\t\t\t\t\t\tVersion:  \"0.1\",\n\t\t\t\t\t\tLanguage: syftPkg.Java,\n\t\t\t\t\t\tType:     syftPkg.JavaPkg,\n\t\t\t\t\t},\n\t\t\t\t\tDetails: []match.Detail{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tType:       match.CPEMatch,\n\t\t\t\t\t\t\tConfidence: 0.9,\n\t\t\t\t\t\t\tSearchedBy: match.CPEParameters{\n\t\t\t\t\t\t\t\tCPEs:      []string{\"cpe:2.3:a:handlebarsjs:handlebars:0.1:*:*:*:*:*:*:*\"},\n\t\t\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\t\t\tPackage: match.PackageParameter{\n\t\t\t\t\t\t\t\t\tName:    \"handlebars\",\n\t\t\t\t\t\t\t\t\tVersion: \"0.1\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tFound: match.CPEResult{\n\t\t\t\t\t\t\t\tCPEs: []string{\n\t\t\t\t\t\t\t\t\t\"cpe:2.3:a:handlebarsjs:handlebars:*:*:*:*:*:node.js:*:*\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tVersionConstraint: \"< 4.7.7 (unknown)\",\n\t\t\t\t\t\t\t\tVulnerabilityID:   \"CVE-2021-23369\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tMatcher: matcher,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Ensure target_sw mismatch does not apply to java jenkins plugins packages\",\n\t\t\tp: pkg.Package{\n\t\t\t\tCPEs: []cpe.CPE{\n\t\t\t\t\tcpe.Must(\"cpe:2.3:a:handlebarsjs:handlebars:*:*:*:*:*:*:*:*\", \"\"),\n\t\t\t\t},\n\t\t\t\tName:     \"handlebars\",\n\t\t\t\tVersion:  \"0.1\",\n\t\t\t\tLanguage: syftPkg.Java,\n\t\t\t\tType:     syftPkg.JenkinsPluginPkg,\n\t\t\t},\n\t\t\texpected: []match.Match{\n\t\t\t\t{\n\t\t\t\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\t\t\t\tReference: vulnerability.Reference{ID: \"CVE-2021-23369\"},\n\t\t\t\t\t},\n\t\t\t\t\tPackage: pkg.Package{\n\t\t\t\t\t\tCPEs: []cpe.CPE{\n\t\t\t\t\t\t\tcpe.Must(\"cpe:2.3:a:handlebarsjs:handlebars:*:*:*:*:*:*:*:*\", \"\"),\n\t\t\t\t\t\t},\n\t\t\t\t\t\tName:     \"handlebars\",\n\t\t\t\t\t\tVersion:  \"0.1\",\n\t\t\t\t\t\tLanguage: syftPkg.Java,\n\t\t\t\t\t\tType:     syftPkg.JenkinsPluginPkg,\n\t\t\t\t\t},\n\t\t\t\t\tDetails: []match.Detail{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tType:       match.CPEMatch,\n\t\t\t\t\t\t\tConfidence: 0.9,\n\t\t\t\t\t\t\tSearchedBy: match.CPEParameters{\n\t\t\t\t\t\t\t\tCPEs:      []string{\"cpe:2.3:a:handlebarsjs:handlebars:0.1:*:*:*:*:*:*:*\"},\n\t\t\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\t\t\tPackage: match.PackageParameter{\n\t\t\t\t\t\t\t\t\tName:    \"handlebars\",\n\t\t\t\t\t\t\t\t\tVersion: \"0.1\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tFound: match.CPEResult{\n\t\t\t\t\t\t\t\tCPEs: []string{\n\t\t\t\t\t\t\t\t\t\"cpe:2.3:a:handlebarsjs:handlebars:*:*:*:*:*:node.js:*:*\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tVersionConstraint: \"< 4.7.7 (unknown)\",\n\t\t\t\t\t\t\t\tVulnerabilityID:   \"CVE-2021-23369\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tMatcher: matcher,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Ensure target_sw mismatch does not apply to binary packages\",\n\t\t\tp: pkg.Package{\n\t\t\t\tCPEs: []cpe.CPE{\n\t\t\t\t\tcpe.Must(\"cpe:2.3:a:handlebarsjs:handlebars:*:*:*:*:*:*:*:*\", \"\"),\n\t\t\t\t},\n\t\t\t\tName:     \"handlebars\",\n\t\t\t\tVersion:  \"0.1\",\n\t\t\t\tLanguage: syftPkg.UnknownLanguage,\n\t\t\t\tType:     syftPkg.BinaryPkg,\n\t\t\t},\n\t\t\texpected: []match.Match{\n\t\t\t\t{\n\t\t\t\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\t\t\t\tReference: vulnerability.Reference{ID: \"CVE-2021-23369\"},\n\t\t\t\t\t},\n\t\t\t\t\tPackage: pkg.Package{\n\t\t\t\t\t\tCPEs: []cpe.CPE{\n\t\t\t\t\t\t\tcpe.Must(\"cpe:2.3:a:handlebarsjs:handlebars:*:*:*:*:*:*:*:*\", \"\"),\n\t\t\t\t\t\t},\n\t\t\t\t\t\tName:     \"handlebars\",\n\t\t\t\t\t\tVersion:  \"0.1\",\n\t\t\t\t\t\tLanguage: syftPkg.UnknownLanguage,\n\t\t\t\t\t\tType:     syftPkg.BinaryPkg,\n\t\t\t\t\t},\n\t\t\t\t\tDetails: []match.Detail{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tType:       match.CPEMatch,\n\t\t\t\t\t\t\tConfidence: 0.9,\n\t\t\t\t\t\t\tSearchedBy: match.CPEParameters{\n\t\t\t\t\t\t\t\tCPEs:      []string{\"cpe:2.3:a:handlebarsjs:handlebars:0.1:*:*:*:*:*:*:*\"},\n\t\t\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\t\t\tPackage: match.PackageParameter{\n\t\t\t\t\t\t\t\t\tName:    \"handlebars\",\n\t\t\t\t\t\t\t\t\tVersion: \"0.1\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tFound: match.CPEResult{\n\t\t\t\t\t\t\t\tCPEs: []string{\n\t\t\t\t\t\t\t\t\t\"cpe:2.3:a:handlebarsjs:handlebars:*:*:*:*:*:node.js:*:*\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tVersionConstraint: \"< 4.7.7 (unknown)\",\n\t\t\t\t\t\t\t\tVulnerabilityID:   \"CVE-2021-23369\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tMatcher: matcher,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Ensure target_sw mismatch does not apply to unknown packages\",\n\t\t\tp: pkg.Package{\n\t\t\t\tCPEs: []cpe.CPE{\n\t\t\t\t\tcpe.Must(\"cpe:2.3:a:handlebarsjs:handlebars:*:*:*:*:*:*:*:*\", \"\"),\n\t\t\t\t},\n\t\t\t\tName:     \"handlebars\",\n\t\t\t\tVersion:  \"0.1\",\n\t\t\t\tLanguage: syftPkg.UnknownLanguage,\n\t\t\t\tType:     syftPkg.UnknownPkg,\n\t\t\t},\n\t\t\texpected: []match.Match{\n\t\t\t\t{\n\t\t\t\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\t\t\t\tReference: vulnerability.Reference{ID: \"CVE-2021-23369\"},\n\t\t\t\t\t},\n\t\t\t\t\tPackage: pkg.Package{\n\t\t\t\t\t\tCPEs: []cpe.CPE{\n\t\t\t\t\t\t\tcpe.Must(\"cpe:2.3:a:handlebarsjs:handlebars:*:*:*:*:*:*:*:*\", \"\"),\n\t\t\t\t\t\t},\n\t\t\t\t\t\tName:     \"handlebars\",\n\t\t\t\t\t\tVersion:  \"0.1\",\n\t\t\t\t\t\tLanguage: syftPkg.UnknownLanguage,\n\t\t\t\t\t\tType:     syftPkg.UnknownPkg,\n\t\t\t\t\t},\n\t\t\t\t\tDetails: []match.Detail{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tType:       match.CPEMatch,\n\t\t\t\t\t\t\tConfidence: 0.9,\n\t\t\t\t\t\t\tSearchedBy: match.CPEParameters{\n\t\t\t\t\t\t\t\tCPEs:      []string{\"cpe:2.3:a:handlebarsjs:handlebars:0.1:*:*:*:*:*:*:*\"},\n\t\t\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\t\t\tPackage: match.PackageParameter{\n\t\t\t\t\t\t\t\t\tName:    \"handlebars\",\n\t\t\t\t\t\t\t\t\tVersion: \"0.1\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tFound: match.CPEResult{\n\t\t\t\t\t\t\t\tCPEs: []string{\n\t\t\t\t\t\t\t\t\t\"cpe:2.3:a:handlebarsjs:handlebars:*:*:*:*:*:node.js:*:*\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tVersionConstraint: \"< 4.7.7 (unknown)\",\n\t\t\t\t\t\t\t\tVulnerabilityID:   \"CVE-2021-23369\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tMatcher: matcher,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"package without CPEs returns error\",\n\t\t\tp: pkg.Package{\n\t\t\t\tName: \"some-package\",\n\t\t\t},\n\t\t\texpected: nil,\n\t\t\twantErr: func(t require.TestingT, err error, i ...interface{}) {\n\t\t\t\tif !errors.Is(err, ErrEmptyCPEMatch) {\n\t\t\t\t\tt.Errorf(\"expected %v but got %v\", ErrEmptyCPEMatch, err)\n\t\t\t\t\tt.FailNow()\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Ensure match is kept for target software that matches the syft package language type\",\n\t\t\tp: pkg.Package{\n\t\t\t\tCPEs: []cpe.CPE{\n\t\t\t\t\tcpe.Must(\"cpe:2.3:a:handlebarsjs:handlebars:*:*:*:*:*:*:*:*\", \"\"),\n\t\t\t\t},\n\t\t\t\tName:     \"handlebars\",\n\t\t\t\tVersion:  \"0.1\",\n\t\t\t\tLanguage: syftPkg.JavaScript,\n\t\t\t\tType:     syftPkg.NpmPkg,\n\t\t\t},\n\t\t\texpected: []match.Match{\n\t\t\t\t{\n\t\t\t\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\t\t\t\tReference: vulnerability.Reference{ID: \"CVE-2021-23369\"},\n\t\t\t\t\t},\n\t\t\t\t\tPackage: pkg.Package{\n\t\t\t\t\t\tCPEs: []cpe.CPE{\n\t\t\t\t\t\t\tcpe.Must(\"cpe:2.3:a:handlebarsjs:handlebars:*:*:*:*:*:*:*:*\", \"\"),\n\t\t\t\t\t\t},\n\t\t\t\t\t\tName:     \"handlebars\",\n\t\t\t\t\t\tVersion:  \"0.1\",\n\t\t\t\t\t\tLanguage: syftPkg.JavaScript,\n\t\t\t\t\t\tType:     syftPkg.NpmPkg,\n\t\t\t\t\t},\n\t\t\t\t\tDetails: []match.Detail{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tType:       match.CPEMatch,\n\t\t\t\t\t\t\tConfidence: 0.9,\n\t\t\t\t\t\t\tSearchedBy: match.CPEParameters{\n\t\t\t\t\t\t\t\tCPEs:      []string{\"cpe:2.3:a:handlebarsjs:handlebars:0.1:*:*:*:*:*:*:*\"},\n\t\t\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\t\t\tPackage: match.PackageParameter{\n\t\t\t\t\t\t\t\t\tName:    \"handlebars\",\n\t\t\t\t\t\t\t\t\tVersion: \"0.1\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tFound: match.CPEResult{\n\t\t\t\t\t\t\t\tCPEs: []string{\n\t\t\t\t\t\t\t\t\t\"cpe:2.3:a:handlebarsjs:handlebars:*:*:*:*:*:node.js:*:*\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tVersionConstraint: \"< 4.7.7 (unknown)\",\n\t\t\t\t\t\t\t\tVulnerabilityID:   \"CVE-2021-23369\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tMatcher: matcher,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tactual, err := MatchPackageByCPEs(newCPETestStore(), test.p, matcher)\n\t\t\tif test.wantErr == nil {\n\t\t\t\ttest.wantErr = require.NoError\n\t\t\t}\n\t\t\ttest.wantErr(t, err)\n\t\t\tassertMatchesUsingIDsForVulnerabilities(t, test.expected, actual)\n\t\t\tfor idx, e := range test.expected {\n\t\t\t\tif idx < len(actual) {\n\t\t\t\t\tif d := cmp.Diff(e.Details, actual[idx].Details); d != \"\" {\n\t\t\t\t\t\tt.Errorf(\"unexpected match details (-want +got):\\n%s\", d)\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tt.Errorf(\"expected match details (-want +got)\\n%+v:\\n\", e.Details)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestFilterCPEsByVersion(t *testing.T) {\n\ttests := []struct {\n\t\tname              string\n\t\tversion           string\n\t\tvulnerabilityCPEs []string\n\t\texpected          []string\n\t}{\n\t\t{\n\t\t\tname:    \"filter out by simple version\",\n\t\t\tversion: \"1.0\",\n\t\t\tvulnerabilityCPEs: []string{\n\t\t\t\t\"cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*\",\n\t\t\t\t\"cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*\",\n\t\t\t\t\"cpe:2.3:*:multiple:multiple:2.0:*:*:*:*:*:*:*\",\n\t\t\t},\n\t\t\texpected: []string{\n\t\t\t\t\"cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*\",\n\t\t\t\t\"cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:    \"do not filter on empty version\",\n\t\t\tversion: \"\", // important!\n\t\t\tvulnerabilityCPEs: []string{\n\t\t\t\t\"cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*\",\n\t\t\t\t\"cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*\",\n\t\t\t\t\"cpe:2.3:*:multiple:multiple:2.0:*:*:*:*:*:*:*\",\n\t\t\t},\n\t\t\texpected: []string{\n\t\t\t\t\"cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*\",\n\t\t\t\t\"cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*\",\n\t\t\t\t\"cpe:2.3:*:multiple:multiple:2.0:*:*:*:*:*:*:*\",\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\t// format strings to CPE objects...\n\t\t\tvulnerabilityCPEs := make([]cpe.CPE, len(test.vulnerabilityCPEs))\n\t\t\tfor idx, c := range test.vulnerabilityCPEs {\n\t\t\t\tvulnerabilityCPEs[idx] = cpe.Must(c, \"\")\n\t\t\t}\n\n\t\t\tvar versionObj *version.Version\n\t\t\tif test.version != \"\" {\n\t\t\t\tversionObj = version.New(test.version, version.UnknownFormat)\n\t\t\t}\n\n\t\t\t// run the test subject...\n\t\t\tactual := filterCPEsByVersion(versionObj, vulnerabilityCPEs)\n\n\t\t\t// format CPE objects to string...\n\t\t\tactualStrs := make([]string, len(actual))\n\t\t\tfor idx, a := range actual {\n\t\t\t\t// use .String() for proper escaping\n\t\t\t\tactualStrs[idx] = a.Attributes.String()\n\t\t\t}\n\n\t\t\tassert.ElementsMatch(t, test.expected, actualStrs)\n\t\t})\n\t}\n}\n\nfunc TestAddMatchDetails(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\texisting []match.Detail\n\t\tnew      match.Detail\n\t\texpected []match.Detail\n\t}{\n\t\t{\n\t\t\tname: \"append new entry -- found not equal\",\n\t\t\texisting: []match.Detail{\n\t\t\t\t{\n\t\t\t\t\tSearchedBy: match.CPEParameters{\n\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\tCPEs: []string{\n\t\t\t\t\t\t\t\"cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tFound: match.CPEResult{\n\t\t\t\t\t\tVersionConstraint: \"< 2.0 (unknown)\",\n\t\t\t\t\t\tCPEs: []string{\n\t\t\t\t\t\t\t\"cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tnew: match.Detail{\n\t\t\t\tSearchedBy: match.CPEParameters{\n\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\tCPEs: []string{\n\t\t\t\t\t\t\"totally-different-search\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tFound: match.CPEResult{\n\t\t\t\t\tVersionConstraint: \"< 2.0 (unknown)\",\n\t\t\t\t\tCPEs: []string{\n\t\t\t\t\t\t\"totally-different-match\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: []match.Detail{\n\t\t\t\t{\n\t\t\t\t\tSearchedBy: match.CPEParameters{\n\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\tCPEs: []string{\n\t\t\t\t\t\t\t\"cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tFound: match.CPEResult{\n\t\t\t\t\t\tVersionConstraint: \"< 2.0 (unknown)\",\n\t\t\t\t\t\tCPEs: []string{\n\t\t\t\t\t\t\t\"cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tSearchedBy: match.CPEParameters{\n\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\tCPEs: []string{\n\t\t\t\t\t\t\t\"totally-different-search\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tFound: match.CPEResult{\n\t\t\t\t\t\tVersionConstraint: \"< 2.0 (unknown)\",\n\t\t\t\t\t\tCPEs: []string{\n\t\t\t\t\t\t\t\"totally-different-match\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"append new entry -- searchedBy merge fails\",\n\t\t\texisting: []match.Detail{\n\t\t\t\t{\n\t\t\t\t\tSearchedBy: match.CPEParameters{\n\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\tCPEs: []string{\n\t\t\t\t\t\t\t\"cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tFound: match.CPEResult{\n\t\t\t\t\t\tVersionConstraint: \"< 2.0 (unknown)\",\n\t\t\t\t\t\tCPEs: []string{\n\t\t\t\t\t\t\t\"cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tnew: match.Detail{\n\t\t\t\tSearchedBy: match.CPEParameters{\n\t\t\t\t\tNamespace: \"totally-different\",\n\t\t\t\t\tCPEs: []string{\n\t\t\t\t\t\t\"cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tFound: match.CPEResult{\n\t\t\t\t\tVersionConstraint: \"< 2.0 (unknown)\",\n\t\t\t\t\tCPEs: []string{\n\t\t\t\t\t\t\"cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: []match.Detail{\n\t\t\t\t{\n\t\t\t\t\tSearchedBy: match.CPEParameters{\n\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\tCPEs: []string{\n\t\t\t\t\t\t\t\"cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tFound: match.CPEResult{\n\t\t\t\t\t\tVersionConstraint: \"< 2.0 (unknown)\",\n\t\t\t\t\t\tCPEs: []string{\n\t\t\t\t\t\t\t\"cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tSearchedBy: match.CPEParameters{\n\t\t\t\t\t\tNamespace: \"totally-different\",\n\t\t\t\t\t\tCPEs: []string{\n\t\t\t\t\t\t\t\"cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tFound: match.CPEResult{\n\t\t\t\t\t\tVersionConstraint: \"< 2.0 (unknown)\",\n\t\t\t\t\t\tCPEs: []string{\n\t\t\t\t\t\t\t\"cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"merge with exiting entry\",\n\t\t\texisting: []match.Detail{\n\t\t\t\t{\n\t\t\t\t\tSearchedBy: match.CPEParameters{\n\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\tCPEs: []string{\n\t\t\t\t\t\t\t\"cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tFound: match.CPEResult{\n\t\t\t\t\t\tVersionConstraint: \"< 2.0 (unknown)\",\n\t\t\t\t\t\tCPEs: []string{\n\t\t\t\t\t\t\t\"cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tnew: match.Detail{\n\t\t\t\tSearchedBy: match.CPEParameters{\n\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\tCPEs: []string{\n\t\t\t\t\t\t\"totally-different-search\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tFound: match.CPEResult{\n\t\t\t\t\tVersionConstraint: \"< 2.0 (unknown)\",\n\t\t\t\t\tCPEs: []string{\n\t\t\t\t\t\t\"cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: []match.Detail{\n\t\t\t\t{\n\t\t\t\t\tSearchedBy: match.CPEParameters{\n\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\tCPEs: []string{\n\t\t\t\t\t\t\t\"cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*\",\n\t\t\t\t\t\t\t\"totally-different-search\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tFound: match.CPEResult{\n\t\t\t\t\t\tVersionConstraint: \"< 2.0 (unknown)\",\n\t\t\t\t\t\tCPEs: []string{\n\t\t\t\t\t\t\t\"cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"no addition - bad new searchedBy type\",\n\t\t\texisting: []match.Detail{\n\t\t\t\t{\n\t\t\t\t\tSearchedBy: match.CPEParameters{\n\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\tCPEs: []string{\n\t\t\t\t\t\t\t\"cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tFound: match.CPEResult{\n\t\t\t\t\t\tVersionConstraint: \"< 2.0 (unknown)\",\n\t\t\t\t\t\tCPEs: []string{\n\t\t\t\t\t\t\t\"cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tnew: match.Detail{\n\t\t\t\tSearchedBy: \"something else!\",\n\t\t\t\tFound: match.CPEResult{\n\t\t\t\t\tVersionConstraint: \"< 2.0 (unknown)\",\n\t\t\t\t\tCPEs: []string{\n\t\t\t\t\t\t\"cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: []match.Detail{\n\t\t\t\t{\n\t\t\t\t\tSearchedBy: match.CPEParameters{\n\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\tCPEs: []string{\n\t\t\t\t\t\t\t\"cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tFound: match.CPEResult{\n\t\t\t\t\t\tVersionConstraint: \"< 2.0 (unknown)\",\n\t\t\t\t\t\tCPEs: []string{\n\t\t\t\t\t\t\t\"cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"no addition - bad new found type\",\n\t\t\texisting: []match.Detail{\n\t\t\t\t{\n\t\t\t\t\tSearchedBy: match.CPEParameters{\n\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\tCPEs: []string{\n\t\t\t\t\t\t\t\"cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tFound: match.CPEResult{\n\t\t\t\t\t\tVersionConstraint: \"< 2.0 (unknown)\",\n\t\t\t\t\t\tCPEs: []string{\n\t\t\t\t\t\t\t\"cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tnew: match.Detail{\n\t\t\t\tSearchedBy: match.CPEParameters{\n\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\tCPEs: []string{\n\t\t\t\t\t\t\"cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tFound: \"something-else!\",\n\t\t\t},\n\t\t\texpected: []match.Detail{\n\t\t\t\t{\n\t\t\t\t\tSearchedBy: match.CPEParameters{\n\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\tCPEs: []string{\n\t\t\t\t\t\t\t\"cpe:2.3:*:multiple:multiple:1.0:*:*:*:*:*:*:*\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tFound: match.CPEResult{\n\t\t\t\t\t\tVersionConstraint: \"< 2.0 (unknown)\",\n\t\t\t\t\t\tCPEs: []string{\n\t\t\t\t\t\t\t\"cpe:2.3:*:multiple:multiple:*:*:*:*:*:*:*:*\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tassert.Equal(t, test.expected, addMatchDetails(test.existing, test.new))\n\t\t})\n\t}\n}\n\nfunc TestCPESearchHit_Equals(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tcurrent  match.CPEResult\n\t\tother    match.CPEResult\n\t\texpected bool\n\t}{\n\t\t{\n\t\t\tname: \"different version constraint\",\n\t\t\tcurrent: match.CPEResult{\n\t\t\t\tVersionConstraint: \"current-constraint\",\n\t\t\t\tCPEs: []string{\n\t\t\t\t\t\"a-cpe\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tother: match.CPEResult{\n\t\t\t\tVersionConstraint: \"different-constraint\",\n\t\t\t\tCPEs: []string{\n\t\t\t\t\t\"a-cpe\",\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname: \"different number of CPEs\",\n\t\t\tcurrent: match.CPEResult{\n\t\t\t\tVersionConstraint: \"current-constraint\",\n\t\t\t\tCPEs: []string{\n\t\t\t\t\t\"a-cpe\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tother: match.CPEResult{\n\t\t\t\tVersionConstraint: \"current-constraint\",\n\t\t\t\tCPEs: []string{\n\t\t\t\t\t\"a-cpe\",\n\t\t\t\t\t\"b-cpe\",\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname: \"different CPE value\",\n\t\t\tcurrent: match.CPEResult{\n\t\t\t\tVersionConstraint: \"current-constraint\",\n\t\t\t\tCPEs: []string{\n\t\t\t\t\t\"a-cpe\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tother: match.CPEResult{\n\t\t\t\tVersionConstraint: \"current-constraint\",\n\t\t\t\tCPEs: []string{\n\t\t\t\t\t\"b-cpe\",\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname: \"matches\",\n\t\t\tcurrent: match.CPEResult{\n\t\t\t\tVersionConstraint: \"current-constraint\",\n\t\t\t\tCPEs: []string{\n\t\t\t\t\t\"a-cpe\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tother: match.CPEResult{\n\t\t\t\tVersionConstraint: \"current-constraint\",\n\t\t\t\tCPEs: []string{\n\t\t\t\t\t\"a-cpe\",\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: true,\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tassert.Equal(t, test.expected, test.current.Equals(test.other))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/matcher/internal/distro.go",
    "content": "package internal\n\nimport (\n\t\"fmt\"\n\t\"slices\"\n\t\"strings\"\n\n\t\"github.com/anchore/grype/grype/match\"\n\t\"github.com/anchore/grype/grype/matcher/internal/result\"\n\t\"github.com/anchore/grype/grype/pkg\"\n\t\"github.com/anchore/grype/grype/search\"\n\t\"github.com/anchore/grype/grype/version\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n\t\"github.com/anchore/grype/internal/log\"\n)\n\nfunc MatchPackageByDistro(provider vulnerability.Provider, searchPkg pkg.Package, catalogPkg *pkg.Package, upstreamMatcher match.MatcherType, cfg *version.ComparisonConfig) ([]match.Match, []match.IgnoreFilter, error) {\n\tif searchPkg.Distro == nil {\n\t\treturn nil, nil, nil\n\t}\n\n\tif isUnknownVersion(searchPkg.Version) {\n\t\tlog.WithFields(\"package\", searchPkg.Name).Trace(\"skipping package with unknown version\")\n\t\treturn nil, nil, nil\n\t}\n\n\tvar matches []match.Match\n\n\t// Create version with config embedded if provided\n\tvar pkgVersion *version.Version\n\tif cfg != nil {\n\t\tpkgVersion = version.NewWithConfig(searchPkg.Version, pkg.VersionFormat(searchPkg), *cfg)\n\t} else {\n\t\tpkgVersion = version.New(searchPkg.Version, pkg.VersionFormat(searchPkg))\n\t}\n\n\tversionCriteria := OnlyVulnerableVersions(pkgVersion)\n\n\tvulns, err := provider.FindVulnerabilities(\n\t\tsearch.ByPackageName(searchPkg.Name),\n\t\tsearch.ByDistro(*searchPkg.Distro),\n\t\tOnlyQualifiedPackages(searchPkg),\n\t\tversionCriteria,\n\t)\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"matcher failed to fetch distro=%q pkg=%q: %w\", searchPkg.Distro, searchPkg.Name, err)\n\t}\n\n\tfor _, vuln := range vulns {\n\t\tmatches = append(matches, match.Match{\n\t\t\tVulnerability: vuln,\n\t\t\tPackage:       matchPackage(searchPkg, catalogPkg),\n\t\t\tDetails:       distroMatchDetails(upstreamMatcher, searchPkg, catalogPkg, vuln),\n\t\t})\n\t}\n\treturn matches, nil, err\n}\n\n// MatchPackageByDistroWithOwnedFiles searches for all vulnerabilities the distro knows about for a\n// package in a single query, then partitions the results in memory into vulnerable matches and\n// location-scoped ignore rules for fixed vulnerabilities. The ignore rules are scoped to files\n// owned by the package so they only suppress findings for co-located packages.\n//\n// Owned files are discovered by checking whether the package metadata (on either catalogPkg or\n// searchPkg) implements [pkg.FileOwner]. When no owned files are available, this falls back to\n// [MatchPackageByDistro] (version-filtered query, no ignore rules) to avoid over-fetching.\nfunc MatchPackageByDistroWithOwnedFiles(provider vulnerability.Provider, searchPkg pkg.Package, catalogPkg *pkg.Package, upstreamMatcher match.MatcherType, cfg *version.ComparisonConfig) ([]match.Match, []match.IgnoreFilter, error) {\n\t// Use the SBOM package (not the synthetic upstream) for file ownership — the upstream\n\t// package won't carry file metadata.\n\townedFiles := ownedFilesFor(matchPackage(searchPkg, catalogPkg))\n\tif len(ownedFiles) == 0 {\n\t\treturn MatchPackageByDistro(provider, searchPkg, catalogPkg, upstreamMatcher, cfg)\n\t}\n\n\tif searchPkg.Distro == nil {\n\t\treturn nil, nil, nil\n\t}\n\n\tif isUnknownVersion(searchPkg.Version) {\n\t\tlog.WithFields(\"package\", searchPkg.Name).Trace(\"skipping package with unknown version\")\n\t\treturn nil, nil, nil\n\t}\n\n\t// Create version with config embedded if provided\n\tvar pkgVersion *version.Version\n\tif cfg != nil {\n\t\tpkgVersion = version.NewWithConfig(searchPkg.Version, pkg.VersionFormat(searchPkg), *cfg)\n\t} else {\n\t\tpkgVersion = version.New(searchPkg.Version, pkg.VersionFormat(searchPkg))\n\t}\n\n\tversionCriteria := OnlyVulnerableVersions(pkgVersion)\n\n\t// Fetch all vulnerabilities the distro knows about for this package (1 query, no version filter).\n\trp := result.NewProvider(provider, matchPackage(searchPkg, catalogPkg), upstreamMatcher)\n\n\tallVulns, err := rp.FindResults(\n\t\tsearch.ByPackageName(searchPkg.Name),\n\t\tsearch.ByDistro(*searchPkg.Distro),\n\t\tOnlyQualifiedPackages(searchPkg),\n\t)\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"matcher failed to fetch distro=%q pkg=%q: %w\", searchPkg.Distro, searchPkg.Name, err)\n\t}\n\n\t// Split in memory: vulnerable vs. fixed.\n\tvulnerable := allVulns.Filter(versionCriteria)\n\tfixed := allVulns.Remove(vulnerable)\n\n\t// The superset query omits version criteria, so match details are missing the searched-by\n\t// version. Patch it in from the search package before converting to matches.\n\tpatchDetailVersion(vulnerable, searchPkg.Version)\n\n\tmatches := vulnerable.ToMatches()\n\tignores := distroFixedIgnoreRules(fixed, ownedFiles)\n\n\treturn matches, ignores, nil\n}\n\n// distroFixedIgnoreRules builds location-scoped ignore rules for vulnerabilities that the distro has\n// assessed as fixed. Each vulnerability ID (including aliases) gets one rule per owned path.\nfunc distroFixedIgnoreRules(fixed result.Set, ownedFiles []string) []match.IgnoreFilter {\n\tvar ignores []match.IgnoreFilter\n\tfor _, results := range fixed {\n\t\tfor _, r := range results {\n\t\t\tfor _, v := range r.Vulnerabilities {\n\t\t\t\tids := collectVulnerabilityIDs(v)\n\t\t\t\tfor _, id := range ids {\n\t\t\t\t\tfor _, path := range ownedFiles {\n\t\t\t\t\t\tignores = append(ignores, match.IgnoreRule{\n\t\t\t\t\t\t\tVulnerability:  id,\n\t\t\t\t\t\t\tIncludeAliases: true,\n\t\t\t\t\t\t\tReason:         \"DistroPackageFixed\",\n\t\t\t\t\t\t\tPackage: match.IgnoreRulePackage{\n\t\t\t\t\t\t\t\tLocation: path,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t})\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn ignores\n}\n\nfunc matchPackage(searchPkg pkg.Package, catalogPkg *pkg.Package) pkg.Package {\n\tif catalogPkg != nil {\n\t\treturn *catalogPkg\n\t}\n\treturn searchPkg\n}\n\nfunc distroMatchDetails(upstreamMatcher match.MatcherType, searchPkg pkg.Package, catalogPkg *pkg.Package, vuln vulnerability.Vulnerability) []match.Detail {\n\tty := match.ExactIndirectMatch\n\tif catalogPkg == nil {\n\t\tty = match.ExactDirectMatch\n\t}\n\n\treturn []match.Detail{\n\t\t{\n\t\t\tType:    ty,\n\t\t\tMatcher: upstreamMatcher,\n\t\t\tSearchedBy: match.DistroParameters{\n\t\t\t\tDistro: match.DistroIdentification{\n\t\t\t\t\tType:    searchPkg.Distro.Type.String(),\n\t\t\t\t\tVersion: searchPkg.Distro.Version,\n\t\t\t\t},\n\t\t\t\tPackage: match.PackageParameter{\n\t\t\t\t\tName:    searchPkg.Name,\n\t\t\t\t\tVersion: searchPkg.Version,\n\t\t\t\t},\n\t\t\t\tNamespace: vuln.Namespace,\n\t\t\t},\n\t\t\tFound: match.DistroResult{\n\t\t\t\tVulnerabilityID:   vuln.ID,\n\t\t\t\tVersionConstraint: vuln.Constraint.String(),\n\t\t\t},\n\t\t\tConfidence: 1.0, // TODO: this is hard coded for now\n\t\t},\n\t}\n}\n\n// collectVulnerabilityIDs returns the primary ID plus all related/alias IDs for a vulnerability.\nfunc collectVulnerabilityIDs(v vulnerability.Vulnerability) []string {\n\tids := []string{v.ID}\n\tfor _, related := range v.RelatedVulnerabilities {\n\t\tif !slices.Contains(ids, related.ID) {\n\t\t\tids = append(ids, related.ID)\n\t\t}\n\t}\n\treturn ids\n}\n\nfunc isUnknownVersion(v string) bool {\n\treturn strings.ToLower(v) == \"unknown\"\n}\n\n// patchDetailVersion fills in the searched-by package version on match details that are missing it.\n// This is needed when results come from a superset query (no version criteria), since\n// result.Provider only populates the version from VersionCriteria in the query.\nfunc patchDetailVersion(s result.Set, version string) {\n\tfor _, results := range s {\n\t\tfor i := range results {\n\t\t\tfor j := range results[i].Details {\n\t\t\t\td := &results[i].Details[j]\n\t\t\t\tswitch sb := d.SearchedBy.(type) {\n\t\t\t\tcase match.DistroParameters:\n\t\t\t\t\tif sb.Package.Version == \"\" {\n\t\t\t\t\t\tsb.Package.Version = version\n\t\t\t\t\t\td.SearchedBy = sb\n\t\t\t\t\t}\n\t\t\t\tcase match.EcosystemParameters:\n\t\t\t\t\tif sb.Package.Version == \"\" {\n\t\t\t\t\t\tsb.Package.Version = version\n\t\t\t\t\t\td.SearchedBy = sb\n\t\t\t\t\t}\n\t\t\t\tcase match.CPEParameters:\n\t\t\t\t\tif sb.Package.Version == \"\" {\n\t\t\t\t\t\tsb.Package.Version = version\n\t\t\t\t\t\td.SearchedBy = sb\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\n// ownedFilesFor returns the files owned by the package if its metadata implements [pkg.FileOwner].\nfunc ownedFilesFor(p pkg.Package) []string {\n\tif fo, ok := p.Metadata.(pkg.FileOwner); ok {\n\t\treturn fo.OwnedFiles()\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "grype/matcher/internal/distro_test.go",
    "content": "package internal\n\nimport (\n\t\"testing\"\n\n\t\"github.com/google/uuid\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/grype/grype/distro\"\n\t\"github.com/anchore/grype/grype/match\"\n\t\"github.com/anchore/grype/grype/pkg\"\n\t\"github.com/anchore/grype/grype/version\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n\t\"github.com/anchore/grype/grype/vulnerability/mock\"\n\tsyftPkg \"github.com/anchore/syft/syft/pkg\"\n)\n\nfunc newMockProviderByDistro() vulnerability.Provider {\n\treturn mock.VulnerabilityProvider([]vulnerability.Vulnerability{\n\t\t{\n\t\t\t// direct...\n\t\t\tPackageName: \"neutron\",\n\t\t\tConstraint:  version.MustGetConstraint(\"< 2014.1.5-6\", version.DebFormat),\n\t\t\tReference: vulnerability.Reference{\n\t\t\t\tID:        \"CVE-2014-fake-1\",\n\t\t\t\tNamespace: \"secdb:distro:debian:8\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tPackageName: \"sles_test_package\",\n\t\t\tConstraint:  version.MustGetConstraint(\"< 2014.1.5-6\", version.RpmFormat),\n\t\t\tReference: vulnerability.Reference{\n\t\t\t\tID:        \"CVE-2014-fake-4\",\n\t\t\t\tNamespace: \"secdb:distro:sles:12.5\",\n\t\t\t},\n\t\t},\n\t}...)\n}\n\nfunc TestFindMatchesByPackageDistro(t *testing.T) {\n\tp := pkg.Package{\n\t\tID:      pkg.ID(uuid.NewString()),\n\t\tName:    \"neutron\",\n\t\tVersion: \"2014.1.3-6\",\n\t\tType:    syftPkg.DebPkg,\n\t\tUpstreams: []pkg.UpstreamPackage{\n\t\t\t{\n\t\t\t\tName: \"neutron-devel\",\n\t\t\t},\n\t\t},\n\t}\n\n\td := distro.New(distro.Debian, \"8\", \"\")\n\tp.Distro = d\n\n\texpected := []match.Match{\n\t\t{\n\n\t\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\tID: \"CVE-2014-fake-1\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tPackage: p,\n\t\t\tDetails: []match.Detail{\n\t\t\t\t{\n\t\t\t\t\tType:       match.ExactDirectMatch,\n\t\t\t\t\tConfidence: 1,\n\t\t\t\t\tSearchedBy: match.DistroParameters{\n\t\t\t\t\t\tDistro: match.DistroIdentification{\n\t\t\t\t\t\t\tType:    \"debian\",\n\t\t\t\t\t\t\tVersion: \"8\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tPackage: match.PackageParameter{\n\t\t\t\t\t\t\tName:    \"neutron\",\n\t\t\t\t\t\t\tVersion: \"2014.1.3-6\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tNamespace: \"secdb:distro:debian:8\",\n\t\t\t\t\t},\n\t\t\t\t\tFound: match.DistroResult{\n\t\t\t\t\t\tVersionConstraint: \"< 2014.1.5-6 (deb)\",\n\t\t\t\t\t\tVulnerabilityID:   \"CVE-2014-fake-1\",\n\t\t\t\t\t},\n\t\t\t\t\tMatcher: match.PythonMatcher,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tstore := newMockProviderByDistro()\n\tactual, ignored, err := MatchPackageByDistro(store, p, nil, match.PythonMatcher, nil)\n\trequire.NoError(t, err)\n\trequire.Empty(t, ignored)\n\tassertMatchesUsingIDsForVulnerabilities(t, expected, actual)\n\n\t// prove we do not search for unknown versions\n\tp.Version = \"unknown\"\n\tactual, ignored, err = MatchPackageByDistro(store, p, nil, match.PythonMatcher, nil)\n\trequire.NoError(t, err)\n\trequire.Empty(t, ignored)\n\tassert.Empty(t, actual)\n}\n\nfunc TestMatchPackageByDistroWithIgnoreRules(t *testing.T) {\n\townedFiles := pkg.ApkMetadata{Files: []pkg.ApkFileRecord{\n\t\t{Path: \"/usr/lib/python3/dist-packages/requests\"},\n\t\t{Path: \"/usr/bin/python3\"},\n\t}}\n\n\ttests := []struct {\n\t\tname                  string\n\t\tpkg                   pkg.Package\n\t\tvulnerabilities       []vulnerability.Vulnerability\n\t\texpectedIgnoreVulnIDs []string\n\t\texpectedMatchIDs      []string\n\t\texpectNoIgnoreRules   bool\n\t}{\n\t\t{\n\t\t\tname: \"package version is already fixed - should produce ignore rules scoped to paths\",\n\t\t\tpkg: pkg.Package{\n\t\t\t\tID:       pkg.ID(uuid.NewString()),\n\t\t\t\tName:     \"python3-requests\",\n\t\t\t\tVersion:  \"2.25.1-14.el8\",\n\t\t\t\tType:     syftPkg.RpmPkg,\n\t\t\t\tDistro:   distro.New(distro.RedHat, \"8\", \"\"),\n\t\t\t\tMetadata: ownedFiles,\n\t\t\t},\n\t\t\tvulnerabilities: []vulnerability.Vulnerability{\n\t\t\t\t{\n\t\t\t\t\tPackageName: \"python3-requests\",\n\t\t\t\t\tConstraint:  version.MustGetConstraint(\"< 2.25.1-14.el8\", version.RpmFormat),\n\t\t\t\t\tReference:   vulnerability.Reference{ID: \"CVE-2023-backported\", Namespace: \"secdb:distro:redhat:8\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\t// one rule per (vulnID, path) pair\n\t\t\texpectedIgnoreVulnIDs: []string{\"CVE-2023-backported\", \"CVE-2023-backported\"},\n\t\t},\n\t\t{\n\t\t\tname: \"package version is still vulnerable - should NOT produce ignore rules\",\n\t\t\tpkg: pkg.Package{\n\t\t\t\tID:       pkg.ID(uuid.NewString()),\n\t\t\t\tName:     \"python3-requests\",\n\t\t\t\tVersion:  \"2.25.1-10.el8\",\n\t\t\t\tType:     syftPkg.RpmPkg,\n\t\t\t\tDistro:   distro.New(distro.RedHat, \"8\", \"\"),\n\t\t\t\tMetadata: ownedFiles,\n\t\t\t},\n\t\t\tvulnerabilities: []vulnerability.Vulnerability{\n\t\t\t\t{\n\t\t\t\t\tPackageName: \"python3-requests\",\n\t\t\t\t\tConstraint:  version.MustGetConstraint(\"< 2.25.1-14.el8\", version.RpmFormat),\n\t\t\t\t\tReference:   vulnerability.Reference{ID: \"CVE-2023-backported\", Namespace: \"secdb:distro:redhat:8\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedMatchIDs:    []string{\"CVE-2023-backported\"},\n\t\t\texpectNoIgnoreRules: true,\n\t\t},\n\t\t{\n\t\t\tname: \"distro has no data about the package - should NOT produce ignore rules (search miss)\",\n\t\t\tpkg: pkg.Package{\n\t\t\t\tID:       pkg.ID(uuid.NewString()),\n\t\t\t\tName:     \"python3-something-obscure\",\n\t\t\t\tVersion:  \"1.0.0-1.el8\",\n\t\t\t\tType:     syftPkg.RpmPkg,\n\t\t\t\tDistro:   distro.New(distro.RedHat, \"8\", \"\"),\n\t\t\t\tMetadata: ownedFiles,\n\t\t\t},\n\t\t\tvulnerabilities: []vulnerability.Vulnerability{\n\t\t\t\t// no vulnerabilities for this package in the distro feed\n\t\t\t\t{\n\t\t\t\t\tPackageName: \"other-package\",\n\t\t\t\t\tConstraint:  version.MustGetConstraint(\"< 2.0.0\", version.RpmFormat),\n\t\t\t\t\tReference:   vulnerability.Reference{ID: \"CVE-2023-other\", Namespace: \"secdb:distro:redhat:8\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectNoIgnoreRules: true,\n\t\t},\n\t\t{\n\t\t\tname: \"mix of fixed and still-vulnerable CVEs - should only produce ignore rules for fixed ones\",\n\t\t\tpkg: pkg.Package{\n\t\t\t\tID:       pkg.ID(uuid.NewString()),\n\t\t\t\tName:     \"python3-requests\",\n\t\t\t\tVersion:  \"2.25.1-14.el8\",\n\t\t\t\tType:     syftPkg.RpmPkg,\n\t\t\t\tDistro:   distro.New(distro.RedHat, \"8\", \"\"),\n\t\t\t\tMetadata: ownedFiles,\n\t\t\t},\n\t\t\tvulnerabilities: []vulnerability.Vulnerability{\n\t\t\t\t{\n\t\t\t\t\t// fixed: package version 2.25.1-14.el8 >= fix version\n\t\t\t\t\tPackageName: \"python3-requests\",\n\t\t\t\t\tConstraint:  version.MustGetConstraint(\"< 2.25.1-14.el8\", version.RpmFormat),\n\t\t\t\t\tReference:   vulnerability.Reference{ID: \"CVE-2023-already-fixed\", Namespace: \"secdb:distro:redhat:8\"},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t// still vulnerable: package version 2.25.1-14.el8 < 2.25.1-20.el8\n\t\t\t\t\tPackageName: \"python3-requests\",\n\t\t\t\t\tConstraint:  version.MustGetConstraint(\"< 2.25.1-20.el8\", version.RpmFormat),\n\t\t\t\t\tReference:   vulnerability.Reference{ID: \"CVE-2023-still-vulnerable\", Namespace: \"secdb:distro:redhat:8\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedMatchIDs: []string{\"CVE-2023-still-vulnerable\"},\n\t\t\t// one rule per path for the fixed CVE only\n\t\t\texpectedIgnoreVulnIDs: []string{\"CVE-2023-already-fixed\", \"CVE-2023-already-fixed\"},\n\t\t},\n\t\t{\n\t\t\tname: \"fixed CVE with related vulnerabilities - should produce ignore rules for all IDs at all paths\",\n\t\t\tpkg: pkg.Package{\n\t\t\t\tID:       pkg.ID(uuid.NewString()),\n\t\t\t\tName:     \"python3-requests\",\n\t\t\t\tVersion:  \"2.25.1-14.el8\",\n\t\t\t\tType:     syftPkg.RpmPkg,\n\t\t\t\tDistro:   distro.New(distro.RedHat, \"8\", \"\"),\n\t\t\t\tMetadata: ownedFiles,\n\t\t\t},\n\t\t\tvulnerabilities: []vulnerability.Vulnerability{\n\t\t\t\t{\n\t\t\t\t\tPackageName: \"python3-requests\",\n\t\t\t\t\tConstraint:  version.MustGetConstraint(\"< 2.25.1-14.el8\", version.RpmFormat),\n\t\t\t\t\tReference:   vulnerability.Reference{ID: \"CVE-2023-backported\", Namespace: \"secdb:distro:redhat:8\"},\n\t\t\t\t\tRelatedVulnerabilities: []vulnerability.Reference{\n\t\t\t\t\t\t{ID: \"GHSA-xxxx-yyyy-zzzz\", Namespace: \"github:language:python\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\t// both IDs × 2 paths = 4 rules\n\t\t\texpectedIgnoreVulnIDs: []string{\"CVE-2023-backported\", \"CVE-2023-backported\", \"GHSA-xxxx-yyyy-zzzz\", \"GHSA-xxxx-yyyy-zzzz\"},\n\t\t},\n\t\t{\n\t\t\tname: \"no distro on package - should NOT produce ignore rules\",\n\t\t\tpkg: pkg.Package{\n\t\t\t\tID:       pkg.ID(uuid.NewString()),\n\t\t\t\tName:     \"python3-requests\",\n\t\t\t\tVersion:  \"2.25.1-14.el8\",\n\t\t\t\tType:     syftPkg.RpmPkg,\n\t\t\t\tDistro:   nil,\n\t\t\t\tMetadata: ownedFiles,\n\t\t\t},\n\t\t\tvulnerabilities: []vulnerability.Vulnerability{\n\t\t\t\t{\n\t\t\t\t\tPackageName: \"python3-requests\",\n\t\t\t\t\tConstraint:  version.MustGetConstraint(\"< 2.25.1-14.el8\", version.RpmFormat),\n\t\t\t\t\tReference:   vulnerability.Reference{ID: \"CVE-2023-backported\", Namespace: \"secdb:distro:redhat:8\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectNoIgnoreRules: true,\n\t\t},\n\t\t{\n\t\t\tname: \"unknown version - should NOT produce ignore rules\",\n\t\t\tpkg: pkg.Package{\n\t\t\t\tID:       pkg.ID(uuid.NewString()),\n\t\t\t\tName:     \"python3-requests\",\n\t\t\t\tVersion:  \"unknown\",\n\t\t\t\tType:     syftPkg.RpmPkg,\n\t\t\t\tDistro:   distro.New(distro.RedHat, \"8\", \"\"),\n\t\t\t\tMetadata: ownedFiles,\n\t\t\t},\n\t\t\tvulnerabilities: []vulnerability.Vulnerability{\n\t\t\t\t{\n\t\t\t\t\tPackageName: \"python3-requests\",\n\t\t\t\t\tConstraint:  version.MustGetConstraint(\"< 2.25.1-14.el8\", version.RpmFormat),\n\t\t\t\t\tReference:   vulnerability.Reference{ID: \"CVE-2023-backported\", Namespace: \"secdb:distro:redhat:8\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectNoIgnoreRules: true,\n\t\t},\n\t\t{\n\t\t\tname: \"no FileOwner metadata - should NOT produce ignore rules even if fixed\",\n\t\t\tpkg: pkg.Package{\n\t\t\t\tID:      pkg.ID(uuid.NewString()),\n\t\t\t\tName:    \"python3-requests\",\n\t\t\t\tVersion: \"2.25.1-14.el8\",\n\t\t\t\tType:    syftPkg.RpmPkg,\n\t\t\t\tDistro:  distro.New(distro.RedHat, \"8\", \"\"),\n\t\t\t\t// no Metadata — does not implement FileOwner\n\t\t\t},\n\t\t\tvulnerabilities: []vulnerability.Vulnerability{\n\t\t\t\t{\n\t\t\t\t\tPackageName: \"python3-requests\",\n\t\t\t\t\tConstraint:  version.MustGetConstraint(\"< 2.25.1-14.el8\", version.RpmFormat),\n\t\t\t\t\tReference:   vulnerability.Reference{ID: \"CVE-2023-backported\", Namespace: \"secdb:distro:redhat:8\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectNoIgnoreRules: true,\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tstore := mock.VulnerabilityProvider(test.vulnerabilities...)\n\n\t\t\tmatches, ignoreFilters, err := MatchPackageByDistroWithOwnedFiles(store, test.pkg, nil, match.PythonMatcher, nil)\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// verify matches\n\t\t\tvar gotMatchIDs []string\n\t\t\tfor _, m := range matches {\n\t\t\t\tgotMatchIDs = append(gotMatchIDs, m.Vulnerability.ID)\n\t\t\t}\n\t\t\tif len(test.expectedMatchIDs) > 0 {\n\t\t\t\tassert.ElementsMatch(t, test.expectedMatchIDs, gotMatchIDs, \"unexpected match IDs\")\n\t\t\t}\n\n\t\t\tif test.expectNoIgnoreRules {\n\t\t\t\tassert.Empty(t, ignoreFilters, \"expected no ignore rules\")\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// extract the vulnerability IDs from the ignore rules\n\t\t\tvar gotVulnIDs []string\n\t\t\tfor _, filter := range ignoreFilters {\n\t\t\t\trule, ok := filter.(match.IgnoreRule)\n\t\t\t\trequire.True(t, ok, \"expected IgnoreRule type\")\n\t\t\t\tgotVulnIDs = append(gotVulnIDs, rule.Vulnerability)\n\t\t\t\tassert.True(t, rule.IncludeAliases, \"expected IncludeAliases to be true\")\n\t\t\t\tassert.Equal(t, \"DistroPackageFixed\", rule.Reason)\n\t\t\t\tassert.NotEmpty(t, rule.Package.Location, \"expected location to be set\")\n\t\t\t}\n\n\t\t\tassert.ElementsMatch(t, test.expectedIgnoreVulnIDs, gotVulnIDs, \"unexpected ignore rule vulnerability IDs\")\n\t\t})\n\t}\n}\n\nfunc TestFindMatchesByPackageDistroSles(t *testing.T) {\n\tp := pkg.Package{\n\t\tID:      pkg.ID(uuid.NewString()),\n\t\tName:    \"sles_test_package\",\n\t\tVersion: \"2014.1.3-6\",\n\t\tType:    syftPkg.RpmPkg,\n\t\tUpstreams: []pkg.UpstreamPackage{\n\t\t\t{\n\t\t\t\tName: \"sles_test_package\",\n\t\t\t},\n\t\t},\n\t}\n\n\td := distro.New(distro.SLES, \"12.5\", \"\")\n\tp.Distro = d\n\n\texpected := []match.Match{\n\t\t{\n\n\t\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\tID: \"CVE-2014-fake-4\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tPackage: p,\n\t\t\tDetails: []match.Detail{\n\t\t\t\t{\n\t\t\t\t\tType:       match.ExactDirectMatch,\n\t\t\t\t\tConfidence: 1,\n\t\t\t\t\tSearchedBy: match.DistroParameters{\n\t\t\t\t\t\tDistro: match.DistroIdentification{\n\t\t\t\t\t\t\tType:    \"sles\",\n\t\t\t\t\t\t\tVersion: \"12.5\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tPackage: match.PackageParameter{\n\t\t\t\t\t\t\tName:    \"sles_test_package\",\n\t\t\t\t\t\t\tVersion: \"2014.1.3-6\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tNamespace: \"secdb:distro:sles:12.5\",\n\t\t\t\t\t},\n\t\t\t\t\tFound: match.DistroResult{\n\t\t\t\t\t\tVersionConstraint: \"< 2014.1.5-6 (rpm)\",\n\t\t\t\t\t\tVulnerabilityID:   \"CVE-2014-fake-4\",\n\t\t\t\t\t},\n\t\t\t\t\tMatcher: match.PythonMatcher,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tstore := newMockProviderByDistro()\n\tactual, ignored, err := MatchPackageByDistro(store, p, nil, match.PythonMatcher, nil)\n\tassert.NoError(t, err)\n\trequire.Empty(t, ignored)\n\tassertMatchesUsingIDsForVulnerabilities(t, expected, actual)\n}\n"
  },
  {
    "path": "grype/matcher/internal/eol.go",
    "content": "package internal\n\nimport (\n\t\"time\"\n\n\t\"github.com/anchore/grype/grype/distro\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n\t\"github.com/anchore/grype/internal/log\"\n)\n\n// EOLStatus represents the end-of-life status of a distro\ntype EOLStatus struct {\n\tIsEOL    bool       // true if the distro is past its EOL date\n\tIsEOAS   bool       // true if the distro is past its EOAS date\n\tEOLDate  *time.Time // the EOL date, if known\n\tEOASDate *time.Time // the EOAS date, if known\n}\n\n// CheckDistroEOL checks if the given distro is past its end-of-life date.\n// Returns EOLStatus with the status and dates. If the provider doesn't support\n// EOL checking or the distro has no EOL data, returns a zero EOLStatus.\nfunc CheckDistroEOL(provider vulnerability.Provider, d *distro.Distro) EOLStatus {\n\tif d == nil {\n\t\treturn EOLStatus{}\n\t}\n\n\tchecker, ok := provider.(vulnerability.EOLChecker)\n\tif !ok {\n\t\tlog.Trace(\"vulnerability provider does not support EOL checking\")\n\t\treturn EOLStatus{}\n\t}\n\n\teolDate, eoasDate, err := checker.GetOperatingSystemEOL(d)\n\tif err != nil {\n\t\tlog.WithFields(\"distro\", d.String(), \"error\", err).Debug(\"failed to get EOL status for distro\")\n\t\treturn EOLStatus{}\n\t}\n\n\tnow := time.Now()\n\tstatus := EOLStatus{\n\t\tEOLDate:  eolDate,\n\t\tEOASDate: eoasDate,\n\t}\n\n\tif eolDate != nil && now.After(*eolDate) {\n\t\tstatus.IsEOL = true\n\t}\n\n\tif eoasDate != nil && now.After(*eoasDate) {\n\t\tstatus.IsEOAS = true\n\t}\n\n\treturn status\n}\n\n// IsDistroEOL is a convenience function that returns true if the distro is past its EOL date.\nfunc IsDistroEOL(provider vulnerability.Provider, d *distro.Distro) bool {\n\treturn CheckDistroEOL(provider, d).IsEOL\n}\n"
  },
  {
    "path": "grype/matcher/internal/eol_test.go",
    "content": "package internal\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n\n\t\"github.com/anchore/grype/grype/distro\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n\t\"github.com/anchore/grype/grype/vulnerability/mock\"\n)\n\n// mockEOLProvider wraps mock.VulnerabilityProvider and adds EOLChecker support\ntype mockEOLProvider struct {\n\tvulnerability.Provider\n\teolDate  *time.Time\n\teoasDate *time.Time\n\terr      error\n}\n\nfunc (m *mockEOLProvider) GetOperatingSystemEOL(d *distro.Distro) (eolDate, eoasDate *time.Time, err error) {\n\treturn m.eolDate, m.eoasDate, m.err\n}\n\nfunc newMockEOLProvider(eolDate, eoasDate *time.Time) *mockEOLProvider {\n\treturn &mockEOLProvider{\n\t\tProvider: mock.VulnerabilityProvider(),\n\t\teolDate:  eolDate,\n\t\teoasDate: eoasDate,\n\t}\n}\n\nfunc TestCheckDistroEOL_NilDistro(t *testing.T) {\n\tprovider := newMockEOLProvider(nil, nil)\n\tstatus := CheckDistroEOL(provider, nil)\n\n\tassert.False(t, status.IsEOL)\n\tassert.False(t, status.IsEOAS)\n\tassert.Nil(t, status.EOLDate)\n\tassert.Nil(t, status.EOASDate)\n}\n\nfunc TestCheckDistroEOL_ProviderDoesNotSupportEOL(t *testing.T) {\n\t// use base mock provider without EOLChecker\n\tprovider := mock.VulnerabilityProvider()\n\td := distro.New(distro.Ubuntu, \"18.04\", \"\")\n\n\tstatus := CheckDistroEOL(provider, d)\n\n\tassert.False(t, status.IsEOL)\n\tassert.False(t, status.IsEOAS)\n\tassert.Nil(t, status.EOLDate)\n\tassert.Nil(t, status.EOASDate)\n}\n\nfunc TestCheckDistroEOL_PastEOLDate(t *testing.T) {\n\tpastDate := time.Now().AddDate(-1, 0, 0) // 1 year ago\n\tprovider := newMockEOLProvider(&pastDate, nil)\n\td := distro.New(distro.Ubuntu, \"18.04\", \"\")\n\n\tstatus := CheckDistroEOL(provider, d)\n\n\tassert.True(t, status.IsEOL)\n\tassert.False(t, status.IsEOAS)\n\tassert.NotNil(t, status.EOLDate)\n\tassert.Equal(t, pastDate, *status.EOLDate)\n}\n\nfunc TestCheckDistroEOL_FutureEOLDate(t *testing.T) {\n\tfutureDate := time.Now().AddDate(1, 0, 0) // 1 year from now\n\tprovider := newMockEOLProvider(&futureDate, nil)\n\td := distro.New(distro.Ubuntu, \"22.04\", \"\")\n\n\tstatus := CheckDistroEOL(provider, d)\n\n\tassert.False(t, status.IsEOL)\n\tassert.False(t, status.IsEOAS)\n\tassert.NotNil(t, status.EOLDate)\n\tassert.Equal(t, futureDate, *status.EOLDate)\n}\n\nfunc TestCheckDistroEOL_PastEOASDate(t *testing.T) {\n\tpastEOAS := time.Now().AddDate(-1, 0, 0) // 1 year ago\n\tfutureEOL := time.Now().AddDate(1, 0, 0) // 1 year from now\n\tprovider := newMockEOLProvider(&futureEOL, &pastEOAS)\n\td := distro.New(distro.Ubuntu, \"20.04\", \"\")\n\n\tstatus := CheckDistroEOL(provider, d)\n\n\tassert.False(t, status.IsEOL)\n\tassert.True(t, status.IsEOAS)\n\tassert.NotNil(t, status.EOLDate)\n\tassert.NotNil(t, status.EOASDate)\n}\n\nfunc TestCheckDistroEOL_NoEOLData(t *testing.T) {\n\tprovider := newMockEOLProvider(nil, nil)\n\td := distro.New(distro.Ubuntu, \"24.04\", \"\")\n\n\tstatus := CheckDistroEOL(provider, d)\n\n\tassert.False(t, status.IsEOL)\n\tassert.False(t, status.IsEOAS)\n\tassert.Nil(t, status.EOLDate)\n\tassert.Nil(t, status.EOASDate)\n}\n\nfunc TestIsDistroEOL(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\teolDate  *time.Time\n\t\texpected bool\n\t}{\n\t\t{\n\t\t\tname:     \"past EOL date returns true\",\n\t\t\teolDate:  ptrTime(time.Now().AddDate(-1, 0, 0)),\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"future EOL date returns false\",\n\t\t\teolDate:  ptrTime(time.Now().AddDate(1, 0, 0)),\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"nil EOL date returns false\",\n\t\t\teolDate:  nil,\n\t\t\texpected: false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tprovider := newMockEOLProvider(tt.eolDate, nil)\n\t\t\td := distro.New(distro.Ubuntu, \"18.04\", \"\")\n\n\t\t\tresult := IsDistroEOL(provider, d)\n\n\t\t\tassert.Equal(t, tt.expected, result)\n\t\t})\n\t}\n}\n\nfunc ptrTime(t time.Time) *time.Time {\n\treturn &t\n}\n"
  },
  {
    "path": "grype/matcher/internal/language.go",
    "content": "package internal\n\nimport (\n\t\"fmt\"\n\t\"slices\"\n\n\t\"github.com/anchore/grype/grype/match\"\n\t\"github.com/anchore/grype/grype/matcher/internal/result\"\n\t\"github.com/anchore/grype/grype/pkg\"\n\t\"github.com/anchore/grype/grype/search\"\n\t\"github.com/anchore/grype/grype/version\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n\t\"github.com/anchore/grype/internal/log\"\n)\n\nfunc MatchPackageByLanguage(store vulnerability.Provider, p pkg.Package, matcherType match.MatcherType) ([]match.Match, []match.IgnoreFilter, error) {\n\tvar matches []match.Match\n\tvar ignored []match.IgnoreFilter\n\n\tfor _, name := range store.PackageSearchNames(p) {\n\t\tnameMatches, nameIgnores, err := MatchPackageByEcosystemPackageName(store, p, name, matcherType)\n\t\tif err != nil {\n\t\t\treturn nil, nil, err\n\t\t}\n\t\tmatches = append(matches, nameMatches...)\n\t\tignored = append(ignored, nameIgnores...)\n\t}\n\n\treturn matches, ignored, nil\n}\n\nfunc MatchPackageByEcosystemPackageName(vp vulnerability.Provider, p pkg.Package, packageName string, matcherType match.MatcherType) ([]match.Match, []match.IgnoreFilter, error) {\n\tif isUnknownVersion(p.Version) {\n\t\tlog.WithFields(\"package\", p.Name).Trace(\"skipping package with unknown version\")\n\t\treturn nil, nil, nil\n\t}\n\n\tprovider := result.NewProvider(vp, p, matcherType)\n\n\tcriteria := []vulnerability.Criteria{\n\t\tsearch.ByEcosystem(p.Language, p.Type),\n\t\tsearch.ByPackageName(packageName),\n\t\tOnlyQualifiedPackages(p),\n\t\tOnlyVulnerableVersions(version.New(p.Version, pkg.VersionFormat(p))),\n\t\tOnlyNonWithdrawnVulnerabilities(),\n\t}\n\n\t// TODO: previous impl set confidence to 1, this results in\n\t// a confidence of zero. What should it be?\n\tdisclosures, err := provider.FindResults(criteria...)\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"matcher failed to fetch disclosure language=%q pkg=%q: %w\", p.Language, p.Name, err)\n\t}\n\n\t// we want to perform the same results, but look for explicit naks, which indicates that a vulnerability should not apply\n\tcriteria = append(criteria, search.ForUnaffected())\n\tunaffected, err := provider.FindResults(criteria...)\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"matcher failed to fetch resolution language=%q pkg=%q: %w\", p.Language, p.Name, err)\n\t}\n\n\t// remove any disclosures that have been explicitly nacked\n\tremaining := disclosures.Remove(unaffected)\n\n\treturn remaining.ToMatches(), constructIgnoreFilters(unaffected, p), err\n}\n\nfunc constructIgnoreFilters(unaffectedVulns result.Set, p pkg.Package) []match.IgnoreFilter {\n\tvar ignores []match.IgnoreFilter\n\n\t// collect all IDs to exclude\n\tvar ids []string\n\tfor _, vulnResults := range unaffectedVulns {\n\t\tfor _, vulnResult := range vulnResults {\n\t\t\tids = append(ids, vulnResult.ID)\n\t\t\tfor _, vuln := range vulnResult.Vulnerabilities {\n\t\t\t\tif !slices.Contains(ids, vuln.ID) {\n\t\t\t\t\tids = append(ids, vuln.ID)\n\t\t\t\t}\n\t\t\t\tfor _, id := range vuln.RelatedVulnerabilities {\n\t\t\t\t\tif !slices.Contains(ids, id.ID) {\n\t\t\t\t\t\tids = append(ids, id.ID)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// ignore rules for all IDs\n\tfor _, id := range ids {\n\t\tignores = append(ignores, match.IgnoreRule{\n\t\t\tVulnerability:  id,\n\t\t\tIncludeAliases: true,\n\t\t\tReason:         \"UnaffectedPackageEntry\",\n\t\t\tPackage: match.IgnoreRulePackage{\n\t\t\t\tType:    string(p.Type),\n\t\t\t\tName:    p.Name,\n\t\t\t\tVersion: p.Version,\n\t\t\t},\n\t\t})\n\t}\n\treturn ignores\n}\n"
  },
  {
    "path": "grype/matcher/internal/language_test.go",
    "content": "package internal\n\nimport (\n\t\"slices\"\n\t\"sort\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/google/uuid\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/grype/grype/match\"\n\t\"github.com/anchore/grype/grype/pkg\"\n\t\"github.com/anchore/grype/grype/version\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n\t\"github.com/anchore/grype/grype/vulnerability/mock\"\n\t\"github.com/anchore/syft/syft/cpe\"\n\tsyftPkg \"github.com/anchore/syft/syft/pkg\"\n)\n\nfunc newMockProviderRuby() vulnerability.Provider {\n\treturn mock.VulnerabilityProvider([]vulnerability.Vulnerability{\n\t\t{\n\t\t\tReference: vulnerability.Reference{\n\t\t\t\tID:        \"CVE-2017-fake-1\",\n\t\t\t\tNamespace: \"github:language:ruby\",\n\t\t\t},\n\t\t\tPackageName: \"activerecord\",\n\t\t\t// make sure we find it with semVer constraint\n\t\t\tConstraint: version.MustGetConstraint(\"< 3.7.6\", version.GemFormat),\n\t\t},\n\t\t{\n\t\t\tReference: vulnerability.Reference{\n\t\t\t\tID:        \"CVE-2017-fake-2\",\n\t\t\t\tNamespace: \"github:language:ruby\",\n\t\t\t},\n\t\t\tPackageName: \"activerecord\",\n\t\t\tConstraint:  version.MustGetConstraint(\"< 3.7.4\", version.GemFormat),\n\t\t},\n\t\t{\n\t\t\t// ignore filter entry\n\t\t\tReference: vulnerability.Reference{\n\t\t\t\tID:        \"CVE-2017-fake-2\",\n\t\t\t\tNamespace: \"github:language:ruby\",\n\t\t\t},\n\t\t\tPackageName: \"activerecord\",\n\t\t\tConstraint:  version.MustGetConstraint(\"< 3.7.4\", version.GemFormat),\n\t\t},\n\t\t{\n\t\t\tReference: vulnerability.Reference{\n\t\t\t\tID:        \"CVE-2017-fake-1\",\n\t\t\t\tNamespace: \"github:language:ruby\",\n\t\t\t},\n\t\t\tPackageName: \"nokogiri\",\n\t\t\t// make sure we find it with gem version constraint\n\t\t\tConstraint: version.MustGetConstraint(\"< 1.7.6\", version.GemFormat),\n\t\t\t// detail a fix by vendor \"foo\"\n\t\t\tFix: vulnerability.Fix{\n\t\t\t\tVersions: []string{\"1.7.4+foo.1\"},\n\t\t\t\tState:    vulnerability.FixStateFixed,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tReference: vulnerability.Reference{\n\t\t\t\tID:        \"CVE-2017-fake-2\",\n\t\t\t\tNamespace: \"github:language:ruby\",\n\t\t\t},\n\t\t\tPackageName: \"nokogiri\",\n\t\t\tConstraint:  version.MustGetConstraint(\"< 1.7.4\", version.GemFormat),\n\t\t},\n\t}...)\n}\n\nfunc expectedMatchRuby(p pkg.Package, constraint string) []match.Match {\n\treturn []match.Match{\n\t\t{\n\t\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\tID: \"CVE-2017-fake-1\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tPackage: p,\n\t\t\tDetails: []match.Detail{\n\t\t\t\t{\n\t\t\t\t\tType:       match.ExactDirectMatch,\n\t\t\t\t\tConfidence: 1,\n\t\t\t\t\tSearchedBy: match.EcosystemParameters{\n\t\t\t\t\t\tLanguage:  \"ruby\",\n\t\t\t\t\t\tNamespace: \"github:language:ruby\",\n\t\t\t\t\t\tPackage:   match.PackageParameter{Name: p.Name, Version: p.Version},\n\t\t\t\t\t},\n\t\t\t\t\tFound: match.EcosystemResult{\n\t\t\t\t\t\tVulnerabilityID:   \"CVE-2017-fake-1\",\n\t\t\t\t\t\tVersionConstraint: constraint,\n\t\t\t\t\t},\n\t\t\t\t\tMatcher: match.RubyGemMatcher,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n}\n\nfunc TestFindMatchesByPackageRuby(t *testing.T) {\n\tcases := []struct {\n\t\tp           pkg.Package\n\t\tconstraint  string\n\t\texpIgnores  []match.IgnoreRule\n\t\tassertEmpty bool\n\t}{\n\t\t{\n\t\t\tconstraint: \"< 3.7.6 (gem)\",\n\t\t\tp: pkg.Package{\n\t\t\t\tID:       pkg.ID(uuid.NewString()),\n\t\t\t\tName:     \"activerecord\",\n\t\t\t\tVersion:  \"3.7.5\",\n\t\t\t\tLanguage: syftPkg.Ruby,\n\t\t\t\tType:     syftPkg.GemPkg,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tconstraint: \"< 1.7.6 (gem)\",\n\t\t\t// no ignores expected as version doesn't contain +foo\n\t\t\tp: pkg.Package{\n\t\t\t\tID:       pkg.ID(uuid.NewString()),\n\t\t\t\tName:     \"nokogiri\",\n\t\t\t\tVersion:  \"1.7.5\",\n\t\t\t\tLanguage: syftPkg.Ruby,\n\t\t\t\tType:     syftPkg.GemPkg,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tp: pkg.Package{\n\t\t\t\tID:       pkg.ID(uuid.NewString()),\n\t\t\t\tName:     \"nokogiri\",\n\t\t\t\tVersion:  \"unknown\",\n\t\t\t\tLanguage: syftPkg.Ruby,\n\t\t\t\tType:     syftPkg.GemPkg,\n\t\t\t},\n\t\t\tassertEmpty: true,\n\t\t},\n\t}\n\n\tstore := newMockProviderRuby()\n\tfor _, c := range cases {\n\t\tt.Run(c.p.Name, func(t *testing.T) {\n\t\t\tactual, ignored, err := MatchPackageByLanguage(store, c.p, match.RubyGemMatcher)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.ElementsMatch(t, ignored, c.expIgnores)\n\t\t\tif c.assertEmpty {\n\t\t\t\tassert.Empty(t, actual)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tassertMatchesUsingIDsForVulnerabilities(t, expectedMatchRuby(c.p, c.constraint), actual)\n\t\t})\n\t}\n}\n\n// Golang tests\n\nfunc expectedMatchGolang(p pkg.Package, vulnConstraint map[string]string) []match.Match {\n\tmatches := make([]match.Match, 0, len(vulnConstraint))\n\t// get sorted keys for consistent test results\n\tkeys := make([]string, 0, len(vulnConstraint))\n\tfor k := range vulnConstraint {\n\t\tkeys = append(keys, k)\n\t}\n\tsort.Strings(keys)\n\tfor _, vuln := range keys {\n\t\tconstraint := vulnConstraint[vuln]\n\t\tmatches = append(matches, match.Match{\n\t\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\tID: vuln,\n\t\t\t\t},\n\t\t\t},\n\t\t\tPackage: p,\n\t\t\tDetails: []match.Detail{\n\t\t\t\t{\n\t\t\t\t\tType: match.ExactDirectMatch,\n\t\t\t\t\t// Confidence zero since ecosystems get hardcoded confidence of zero\n\t\t\t\t\tConfidence: 1,\n\t\t\t\t\tSearchedBy: match.EcosystemParameters{\n\t\t\t\t\t\tLanguage:  \"go\",\n\t\t\t\t\t\tNamespace: \"github:language:go\",\n\t\t\t\t\t\tPackage:   match.PackageParameter{Name: p.Name, Version: p.Version},\n\t\t\t\t\t},\n\t\t\t\t\tFound: match.EcosystemResult{\n\t\t\t\t\t\tVulnerabilityID:   vuln,\n\t\t\t\t\t\tVersionConstraint: constraint,\n\t\t\t\t\t},\n\t\t\t\t\tMatcher: match.GoModuleMatcher,\n\t\t\t\t},\n\t\t\t},\n\t\t})\n\t}\n\treturn matches\n}\n\nfunc newMockProviderGolang() vulnerability.Provider {\n\treturn mock.VulnerabilityProvider([]vulnerability.Vulnerability{\n\t\t{\n\t\t\tReference: vulnerability.Reference{\n\t\t\t\tID:        \"CVE-2017-fake-1\",\n\t\t\t\tNamespace: \"github:language:go\",\n\t\t\t},\n\t\t\tPackageName: \"package\",\n\t\t\tConstraint:  version.MustGetConstraint(\"< 1.2.4\", version.GolangFormat),\n\t\t\tFix: vulnerability.Fix{\n\t\t\t\tVersions: []string{\"1.2.4\"},\n\t\t\t\tState:    vulnerability.FixStateFixed,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tReference: vulnerability.Reference{\n\t\t\t\tID:        \"CVE-2017-fake-2\",\n\t\t\t\tNamespace: \"github:language:go\",\n\t\t\t},\n\t\t\tPackageName: \"package\",\n\t\t\tConstraint:  version.MustGetConstraint(\"< 1.3.1\", version.GolangFormat),\n\t\t\tFix: vulnerability.Fix{\n\t\t\t\tVersions: []string{\"1.3.1\"},\n\t\t\t\tState:    vulnerability.FixStateFixed,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t// unaffected entry\n\t\t\tReference: vulnerability.Reference{\n\t\t\t\tID:        \"CVE-2017-fake-1\",\n\t\t\t\tNamespace: \"github:language:go\",\n\t\t\t},\n\t\t\tPackageName: \"package\",\n\t\t\tConstraint:  version.MustGetConstraint(\"= 1.2.1+foo.1\", version.GolangFormat),\n\t\t\tFix: vulnerability.Fix{\n\t\t\t\tVersions: []string{\"1.2.1+foo.1\"},\n\t\t\t\tState:    vulnerability.FixStateFixed,\n\t\t\t},\n\t\t\tUnaffected: true,\n\t\t},\n\t}...)\n}\n\nfunc TestFindMatchesByPackageGolang(t *testing.T) {\n\tcases := []struct {\n\t\tp          pkg.Package\n\t\texpMatches map[string]string\n\t\tunaffected bool\n\t}{\n\t\t{\n\t\t\tp: pkg.Package{\n\t\t\t\tID:       pkg.ID(uuid.NewString()),\n\t\t\t\tName:     \"package\",\n\t\t\t\tVersion:  \"1.2.3\",\n\t\t\t\tLanguage: syftPkg.Go,\n\t\t\t\tType:     syftPkg.GoModulePkg,\n\t\t\t},\n\t\t\texpMatches: map[string]string{\"CVE-2017-fake-2\": \"< 1.3.1 (go)\", \"CVE-2017-fake-1\": \"< 1.2.4 (go)\"},\n\t\t},\n\t\t{\n\t\t\tp: pkg.Package{\n\t\t\t\tID:       pkg.ID(uuid.NewString()),\n\t\t\t\tName:     \"package\",\n\t\t\t\tVersion:  \"1.2.5\",\n\t\t\t\tLanguage: syftPkg.Go,\n\t\t\t\tType:     syftPkg.GoModulePkg,\n\t\t\t},\n\t\t\texpMatches: map[string]string{\"CVE-2017-fake-2\": \"< 1.3.1 (go)\"},\n\t\t},\n\t\t{\n\t\t\tp: pkg.Package{\n\t\t\t\tID:       pkg.ID(uuid.NewString()),\n\t\t\t\tName:     \"package\",\n\t\t\t\tVersion:  \"1.2.1+foo.1\",\n\t\t\t\tLanguage: syftPkg.Go,\n\t\t\t\tType:     syftPkg.GoModulePkg,\n\t\t\t},\n\t\t\texpMatches: map[string]string{\"CVE-2017-fake-2\": \"< 1.3.1 (go)\"},\n\t\t\tunaffected: true, // this vuln matches an unaffected entry\n\t\t},\n\t}\n\n\tstore := newMockProviderGolang()\n\tfor _, c := range cases {\n\t\tt.Run(c.p.Name, func(t *testing.T) {\n\t\t\tactual, ignored, err := MatchPackageByLanguage(store, c.p, match.GoModuleMatcher)\n\t\t\t// sort for consistency\n\t\t\tslices.SortFunc(actual, func(a, b match.Match) int {\n\t\t\t\treturn strings.Compare(a.Vulnerability.ID, b.Vulnerability.ID)\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\t\t\tif c.unaffected {\n\t\t\t\tassert.NotEmpty(t, ignored)\n\t\t\t} else {\n\t\t\t\tassert.Empty(t, ignored)\n\t\t\t}\n\t\t\tassertMatchesUsingIDsForVulnerabilities(t, expectedMatchGolang(c.p, c.expMatches), actual)\n\t\t})\n\t}\n}\n\nfunc Test_unaffectedPackageIgnoreRules(t *testing.T) {\n\tsomeProjectCPE := cpe.Must(`cpe:2.3:a:some_vendor:some_project:*:*:*:*:*:*:*:*`, cpe.DeclaredSource)\n\tprovider := mock.VulnerabilityProvider([]vulnerability.Vulnerability{\n\t\t{\n\t\t\tReference:   vulnerability.Reference{ID: \"vuln1\", Namespace: \"github:language:python\"},\n\t\t\tConstraint:  version.MustGetConstraint(\"< 1.2.3\", version.PythonFormat),\n\t\t\tPackageName: \"some_project\",\n\t\t\tUnaffected:  false,\n\t\t},\n\t\t{\n\t\t\tReference:   vulnerability.Reference{ID: \"vuln2\", Namespace: \"github:language:python\"},\n\t\t\tConstraint:  version.MustGetConstraint(\"< 1.2.3\", version.PythonFormat),\n\t\t\tPackageName: \"some_project\",\n\t\t\tUnaffected:  true,\n\t\t},\n\t\t{\n\t\t\tReference:   vulnerability.Reference{ID: \"vuln2\", Namespace: \"nvd:cpe\"},\n\t\t\tConstraint:  version.MustGetConstraint(\"< 1.2.3\", version.PythonFormat),\n\t\t\tPackageName: \"some_project\",\n\t\t\tCPEs:        []cpe.CPE{someProjectCPE},\n\t\t\tUnaffected:  false,\n\t\t},\n\t}...)\n\n\ttests := []struct {\n\t\tname     string\n\t\tpkg      pkg.Package\n\t\texpected []match.IgnoreFilter\n\t}{\n\t\t{\n\t\t\tname: \"matching unaffected\",\n\t\t\tpkg: pkg.Package{\n\t\t\t\tName:     \"some_project\",\n\t\t\t\tVersion:  \"1.2.2\",\n\t\t\t\tLanguage: syftPkg.Python,\n\t\t\t\tType:     syftPkg.PythonPkg,\n\t\t\t\tCPEs:     []cpe.CPE{someProjectCPE},\n\t\t\t},\n\t\t\texpected: []match.IgnoreFilter{\n\t\t\t\tmatch.IgnoreRule{\n\t\t\t\t\tVulnerability:  \"vuln2\",\n\t\t\t\t\tIncludeAliases: true,\n\t\t\t\t\tReason:         \"UnaffectedPackageEntry\",\n\t\t\t\t\tPackage: match.IgnoreRulePackage{\n\t\t\t\t\t\tName:    \"some_project\",\n\t\t\t\t\t\tVersion: \"1.2.2\",\n\t\t\t\t\t\tType:    string(syftPkg.PythonPkg),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"not unaffected by version\",\n\t\t\tpkg: pkg.Package{\n\t\t\t\tName:     \"some_project\",\n\t\t\t\tVersion:  \"1.2.4\",\n\t\t\t\tLanguage: syftPkg.Python,\n\t\t\t\tType:     syftPkg.PythonPkg,\n\t\t\t\tCPEs:     []cpe.CPE{someProjectCPE},\n\t\t\t},\n\t\t\texpected: nil,\n\t\t},\n\t\t{\n\t\t\tname: \"not unaffected by name\",\n\t\t\tpkg: pkg.Package{\n\t\t\t\tName:     \"some_other_project\",\n\t\t\t\tVersion:  \"1.2.2\",\n\t\t\t\tLanguage: syftPkg.Python,\n\t\t\t\tType:     syftPkg.PythonPkg,\n\t\t\t\tCPEs:     []cpe.CPE{someProjectCPE},\n\t\t\t},\n\t\t\texpected: nil,\n\t\t},\n\t\t{\n\t\t\tname: \"not unaffected by type\",\n\t\t\tpkg: pkg.Package{\n\t\t\t\tName:     \"some_project\",\n\t\t\t\tVersion:  \"1.2.2\",\n\t\t\t\tLanguage: syftPkg.Go,\n\t\t\t\tType:     syftPkg.GoModulePkg,\n\t\t\t\tCPEs:     []cpe.CPE{someProjectCPE},\n\t\t\t},\n\t\t\texpected: nil,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\t_, ignoreRules, err := MatchPackageByEcosystemPackageName(provider, tt.pkg, tt.pkg.Name, \"\")\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, tt.expected, ignoreRules)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/matcher/internal/only_non_withdrawn_vulnerabilities.go",
    "content": "package internal\n\nimport (\n\t\"github.com/anchore/grype/grype/search\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n)\n\n// OnlyNonWithdrawnVulnerabilities returns a criteria object that tests affected vulnerability is not withdrawn/rejected\nfunc OnlyNonWithdrawnVulnerabilities() vulnerability.Criteria {\n\treturn search.ByFunc(func(v vulnerability.Vulnerability) (bool, string, error) {\n\t\t// we should be using enumerations from all supported schema versions, but constants should not be imported here\n\t\tisWithdrawn := v.Status == \"withdrawn\" || v.Status == \"rejected\"\n\t\tif isWithdrawn {\n\t\t\treturn false, \"vulnerability is withdrawn or rejected\", nil\n\t\t}\n\t\treturn true, \"\", nil\n\t})\n}\n"
  },
  {
    "path": "grype/matcher/internal/only_qualified_packages.go",
    "content": "package internal\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/anchore/grype/grype/pkg\"\n\t\"github.com/anchore/grype/grype/search\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n)\n\n// OnlyQualifiedPackages returns a criteria object that tests vulnerability qualifiers against the provided package\nfunc OnlyQualifiedPackages(p pkg.Package) vulnerability.Criteria {\n\treturn search.ByFunc(func(vuln vulnerability.Vulnerability) (bool, string, error) {\n\t\tfor _, qualifier := range vuln.PackageQualifiers {\n\t\t\tsatisfied, err := qualifier.Satisfied(p)\n\t\t\tif err != nil {\n\t\t\t\treturn satisfied, fmt.Sprintf(\"unable to evaluate qualifier: %s\", err.Error()), err\n\t\t\t}\n\t\t\tif !satisfied {\n\t\t\t\t// TODO: qualifiers don't have a good string representation\n\t\t\t\treturn false, fmt.Sprintf(\"package does not satisfy qualifier: %#v\", qualifier), nil\n\t\t\t}\n\t\t}\n\t\treturn true, \"\", nil // all qualifiers passed\n\t})\n}\n"
  },
  {
    "path": "grype/matcher/internal/only_vulnerable_targets.go",
    "content": "package internal\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/facebookincubator/nvdtools/wfn\"\n\t\"github.com/scylladb/go-set/strset\"\n\n\t\"github.com/anchore/grype/grype/pkg\"\n\t\"github.com/anchore/grype/grype/search\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n\t\"github.com/anchore/syft/syft/cpe\"\n\tsyftPkg \"github.com/anchore/syft/syft/pkg\"\n\tsyftCPE \"github.com/anchore/syft/syft/pkg/cataloger/common/cpe\"\n)\n\n// OnlyVulnerableTargets returns a criteria object that tests vulnerability qualifiers against the package vulnerability rules.\n// TODO: in the future this should be moved to underneath the store to avoid the need to recompute CPE comparisons and to leverage ecosystem aliases for target software\nfunc OnlyVulnerableTargets(p pkg.Package) vulnerability.Criteria {\n\treturn search.ByFunc(func(v vulnerability.Vulnerability) (bool, string, error) {\n\t\tmatches, reasons := isVulnerableTarget(p, v)\n\t\treturn matches, reasons, nil\n\t})\n}\n\n// Determines if a vulnerability is an accurate match using the vulnerability's cpes' target software\nfunc isVulnerableTarget(p pkg.Package, vuln vulnerability.Vulnerability) (bool, string) {\n\t// Exclude OS package types from this logic, since they could be embedding any type of ecosystem package\n\tif isOSPackage(p) {\n\t\treturn true, \"\"\n\t}\n\n\tpackageTargetSwSet, vulnTargetSwSet := matchTargetSoftware(p.CPEs, vuln.CPEs)\n\tif len(vuln.CPEs) > 0 && packageTargetSwSet.IsEmpty() {\n\t\treason := fmt.Sprintf(\"vulnerability target software(s) (%q) do not align with %s\", strings.Join(vulnTargetSwSet.List(), \", \"), packageElements(p, packageTargetSwSet.List()))\n\t\treturn false, reason\n\t}\n\n\t// only strictly use CPE attributes to filter binary and unknown package types\n\tif p.Type == syftPkg.BinaryPkg || p.Type == syftPkg.UnknownPkg || p.Type == \"\" {\n\t\tif hasIntersectingTargetSoftware(packageTargetSwSet, vulnTargetSwSet) {\n\t\t\t// we have at least one target software in common\n\t\t\treturn true, \"\"\n\t\t}\n\n\t\t// the package has a * target software, so should match with anything that's on the CPE.\n\t\t// note that this is two way (either the package has a * or the vuln has a * target software).\n\t\tif packageTargetSwSet.Has(wfn.Any) || vulnTargetSwSet.Has(wfn.Any) {\n\t\t\treturn true, \"\"\n\t\t}\n\n\t\treason := fmt.Sprintf(\"vulnerability target software(s) (%q) do not align with %s\", strings.Join(vulnTargetSwSet.List(), \", \"), packageElements(p, packageTargetSwSet.List()))\n\t\treturn false, reason\n\t}\n\n\t// There are quite a few cases within java where other ecosystem components (particularly javascript packages)\n\t// are embedded directly within jar files, so we can't yet make this assumption with java as it will cause dropping\n\t// of valid vulnerabilities that syft has specific logic https://github.com/anchore/syft/blob/main/syft/pkg/cataloger/common/cpe/candidate_by_package_type.go#L48-L75\n\t// to ensure will be surfaced\n\tif p.Language == syftPkg.Java {\n\t\treturn true, \"\"\n\t}\n\n\t// if there are no CPEs then we can't make a decision\n\tif len(vuln.CPEs) == 0 {\n\t\treturn true, \"\"\n\t}\n\n\tif hasIntersectingTargetSoftware(packageTargetSwSet, vulnTargetSwSet) {\n\t\t// we have at least one target software in common\n\t\treturn true, \"\"\n\t}\n\n\treturn refuteTargetSoftwareByPackageAttributes(p, vuln, packageTargetSwSet)\n}\n\nfunc refuteTargetSoftwareByPackageAttributes(p pkg.Package, vuln vulnerability.Vulnerability, packageTargetSwSet *strset.Set) (bool, string) {\n\t// this is purely based on package attributes and does not consider any package CPE target softwares (which the store already considers)\n\tvar mismatchedTargetSoftware []string\n\tfor _, c := range vuln.CPEs {\n\t\ttargetSW := c.Attributes.TargetSW\n\t\tmismatchWithUnknownLanguage := syftPkg.LanguageByName(targetSW) != p.Language && isUnknownTarget(targetSW)\n\t\tunspecifiedTargetSW := targetSW == wfn.Any || targetSW == wfn.NA\n\t\tmatchesByLanguage := syftPkg.LanguageByName(targetSW) == p.Language\n\t\tmatchesByPackageType := syftCPE.TargetSoftwareToPackageType(targetSW) == p.Type\n\t\tif unspecifiedTargetSW || matchesByLanguage || matchesByPackageType || mismatchWithUnknownLanguage {\n\t\t\treturn true, \"\"\n\t\t}\n\t\tmismatchedTargetSoftware = append(mismatchedTargetSoftware, targetSW)\n\t}\n\n\treason := fmt.Sprintf(\"vulnerability target software(s) (%q) do not align with %s\", strings.Join(mismatchedTargetSoftware, \", \"), packageElements(p, packageTargetSwSet.List()))\n\treturn false, reason\n}\n\nfunc isOSPackage(p pkg.Package) bool {\n\treturn p.Type == syftPkg.AlpmPkg || p.Type == syftPkg.ApkPkg || p.Type == syftPkg.DebPkg || p.Type == syftPkg.KbPkg || p.Type == syftPkg.PortagePkg || p.Type == syftPkg.RpmPkg\n}\n\nfunc isUnknownTarget(targetSW string) bool {\n\tif syftPkg.LanguageByName(targetSW) != syftPkg.UnknownLanguage {\n\t\treturn false\n\t}\n\n\t// There are some common target software CPE components which are not currently\n\t// supported by syft but are significant sources of false positives and should be\n\t// considered known for the purposes of filtering here\n\tknown := map[string]bool{\n\t\t\"joomla\":    true,\n\t\t\"joomla\\\\!\": true,\n\t\t\"drupal\":    true,\n\t}\n\n\tif _, ok := known[targetSW]; ok {\n\t\treturn false\n\t}\n\n\treturn true\n}\n\nfunc matchTargetSoftware(pkgCPEs []cpe.CPE, vulnCPEs []cpe.CPE) (*strset.Set, *strset.Set) {\n\tpkgTsw := strset.New()\n\tvulnTsw := strset.New()\n\tfor _, c := range vulnCPEs {\n\t\tfor _, p := range pkgCPEs {\n\t\t\tif matchesAttributesExceptVersionAndTSW(c.Attributes, p.Attributes) {\n\t\t\t\t// include any value including empty string (which means ANY value)\n\t\t\t\tpkgTsw.Add(p.Attributes.TargetSW)\n\t\t\t\tvulnTsw.Add(c.Attributes.TargetSW)\n\t\t\t}\n\t\t}\n\t}\n\treturn pkgTsw, vulnTsw\n}\n\nfunc matchesAttributesExceptVersionAndTSW(a1 cpe.Attributes, a2 cpe.Attributes) bool {\n\t// skip version, update, and target software\n\tif !matchesAttribute(a1.Product, a2.Product) ||\n\t\t!matchesAttribute(a1.Vendor, a2.Vendor) ||\n\t\t!matchesAttribute(a1.Part, a2.Part) ||\n\t\t!matchesAttribute(a1.Language, a2.Language) ||\n\t\t!matchesAttribute(a1.SWEdition, a2.SWEdition) ||\n\t\t!matchesAttribute(a1.TargetHW, a2.TargetHW) ||\n\t\t!matchesAttribute(a1.Other, a2.Other) ||\n\t\t!matchesAttribute(a1.Edition, a2.Edition) {\n\t\treturn false\n\t}\n\treturn true\n}\n\nfunc matchesAttribute(a1, a2 string) bool {\n\treturn a1 == \"\" || a2 == \"\" || strings.EqualFold(a1, a2)\n}\n\nfunc hasIntersectingTargetSoftware(set1, set2 *strset.Set) bool {\n\tset1Pkg := normalizeTargetSoftwares(set1.List())\n\tset2Pkg := normalizeTargetSoftwares(set2.List())\n\tintersection := strset.Intersection(set1Pkg, set2Pkg)\n\treturn !intersection.IsEmpty()\n}\n\nfunc normalizeTargetSoftwares(ts []string) *strset.Set {\n\tnormalizedTargetSWs := strset.New()\n\tfor _, ts := range ts {\n\t\t// Attempt to normalize target sw to package type, e.g. node and nodejs should match\n\t\tpt := string(syftCPE.TargetSoftwareToPackageType(ts))\n\t\tif pt == \"\" && ts != \"*\" && ts != \"?\" && ts != \"-\" {\n\t\t\t// normalizing failed; preserve raw cpe target sw string as the type\n\t\t\t// unless it is wildcard\n\t\t\tpt = strings.ToLower(ts)\n\t\t}\n\t\tif pt != \"\" {\n\t\t\tnormalizedTargetSWs.Add(pt)\n\t\t}\n\t}\n\treturn normalizedTargetSWs\n}\n\nfunc packageElements(p pkg.Package, ts []string) string {\n\tnameVersion := fmt.Sprintf(\"%s@%s\", p.Name, p.Version)\n\n\tpType := string(p.Type)\n\tif pType == \"\" {\n\t\tpType = \"?\"\n\t}\n\n\tpLanguage := string(p.Language)\n\tif pLanguage == \"\" {\n\t\tpLanguage = \"?\"\n\t}\n\n\ttargetSW := strings.Join(ts, \",\")\n\tif (len(ts) == 0) || (len(ts) == 1 && ts[0] == wfn.Any) {\n\t\ttargetSW = \"*\"\n\t}\n\n\treturn fmt.Sprintf(\"pkg(%s type=%q language=%q targets=%q)\", nameVersion, pType, pLanguage, targetSW)\n}\n"
  },
  {
    "path": "grype/matcher/internal/only_vulnerable_targets_test.go",
    "content": "package internal\n\nimport (\n\t\"testing\"\n\n\t\"github.com/scylladb/go-set/strset\"\n\t\"github.com/stretchr/testify/assert\"\n\n\t\"github.com/anchore/grype/grype/pkg\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n\t\"github.com/anchore/syft/syft/cpe\"\n\tsyftPkg \"github.com/anchore/syft/syft/pkg\"\n)\n\nfunc TestIsVulnerableTarget(t *testing.T) {\n\ttests := []struct {\n\t\tname            string\n\t\tpkg             pkg.Package\n\t\tvuln            vulnerability.Vulnerability\n\t\texpectedMatches bool\n\t\texpectedReason  string\n\t}{\n\t\t{\n\t\t\tname: \"OS package should always match\",\n\t\t\tpkg: pkg.Package{\n\t\t\t\tName:     \"openssl\",\n\t\t\t\tVersion:  \"1.1.1k\",\n\t\t\t\tType:     syftPkg.RpmPkg,\n\t\t\t\tLanguage: syftPkg.UnknownLanguage,\n\t\t\t\tCPEs: []cpe.CPE{\n\t\t\t\t\tcpe.Must(\"cpe:2.3:a:openssl:openssl:1.1.1k:*:*:*:*:*:*:*\", \"\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\tvuln: vulnerability.Vulnerability{\n\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\tID:        \"CVE-2021-3449\",\n\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t},\n\t\t\t\tPackageName: \"openssl\",\n\t\t\t\tCPEs: []cpe.CPE{\n\t\t\t\t\tcpe.Must(\"cpe:2.3:a:openssl:openssl:1.1.1k:*:*:*:*:*:*:*\", \"\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedMatches: true,\n\t\t},\n\t\t{\n\t\t\tname: \"binary package should always match\",\n\t\t\tpkg: pkg.Package{\n\t\t\t\tName:     \"bash\",\n\t\t\t\tVersion:  \"5.0.17\",\n\t\t\t\tType:     syftPkg.BinaryPkg,\n\t\t\t\tLanguage: syftPkg.UnknownLanguage,\n\t\t\t\tCPEs: []cpe.CPE{\n\t\t\t\t\tcpe.Must(\"cpe:2.3:a:gnu:bash:5.0.17:*:*:*:*:*:*:*\", \"\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\tvuln: vulnerability.Vulnerability{\n\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\tID:        \"CVE-2020-12345\",\n\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t},\n\t\t\t\tPackageName: \"bash\",\n\t\t\t\tCPEs: []cpe.CPE{\n\t\t\t\t\tcpe.Must(\"cpe:2.3:a:gnu:bash:5.0.17:*:*:*:*:*:*:*\", \"\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedMatches: true,\n\t\t},\n\t\t{\n\t\t\tname: \"unknown package should always match\",\n\t\t\tpkg: pkg.Package{\n\t\t\t\tName:     \"unknown-pkg\",\n\t\t\t\tVersion:  \"1.0.0\",\n\t\t\t\tType:     syftPkg.UnknownPkg,\n\t\t\t\tLanguage: syftPkg.UnknownLanguage,\n\t\t\t\tCPEs: []cpe.CPE{\n\t\t\t\t\tcpe.Must(\"cpe:2.3:a:unknown:unknown-pkg:1.0.0:*:*:*:*:*:*:*\", \"\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\tvuln: vulnerability.Vulnerability{\n\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\tID:        \"CVE-2021-98765\",\n\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t},\n\t\t\t\tPackageName: \"unknown-pkg\",\n\t\t\t\tCPEs: []cpe.CPE{\n\t\t\t\t\tcpe.Must(\"cpe:2.3:a:unknown:unknown-pkg:1.0.0:*:*:*:*:*:*:*\", \"\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedMatches: true,\n\t\t},\n\t\t{\n\t\t\tname: \"java package should always match\",\n\t\t\tpkg: pkg.Package{\n\t\t\t\tName:     \"log4j-core\",\n\t\t\t\tVersion:  \"2.14.1\",\n\t\t\t\tType:     syftPkg.JavaPkg,\n\t\t\t\tLanguage: syftPkg.Java,\n\t\t\t\tCPEs: []cpe.CPE{\n\t\t\t\t\tcpe.Must(\"cpe:2.3:a:apache:log4j:2.14.1:*:*:*:*:*:*:*\", \"\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\tvuln: vulnerability.Vulnerability{\n\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\tID:        \"CVE-2021-44228\",\n\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t},\n\t\t\t\tPackageName: \"log4j-core\",\n\t\t\t\tCPEs: []cpe.CPE{\n\t\t\t\t\tcpe.Must(\"cpe:2.3:a:apache:log4j:2.14.1:*:*:*:*:*:*:*\", \"\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedMatches: true,\n\t\t},\n\t\t{\n\t\t\tname: \"package with no CPEs should fail\",\n\t\t\tpkg: pkg.Package{\n\t\t\t\tName:     \"example-lib\",\n\t\t\t\tVersion:  \"1.0.0\",\n\t\t\t\tType:     syftPkg.NpmPkg,\n\t\t\t\tLanguage: syftPkg.JavaScript,\n\t\t\t},\n\t\t\tvuln: vulnerability.Vulnerability{\n\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\tID:        \"CVE-2021-87654\",\n\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t},\n\t\t\t\tPackageName: \"example-lib\",\n\t\t\t\tCPEs: []cpe.CPE{\n\t\t\t\t\tcpe.Must(\"cpe:2.3:a:example:example-lib:1.0.0:*:*:*:*:*:*:*\", \"\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedMatches: false,\n\t\t\texpectedReason:  `vulnerability target software(s) (\"\") do not align with pkg(example-lib@1.0.0 type=\"npm\" language=\"javascript\" targets=\"*\")`,\n\t\t},\n\t\t{\n\t\t\tname: \"vulnerability with no CPEs should match\",\n\t\t\tpkg: pkg.Package{\n\t\t\t\tName:     \"example-lib\",\n\t\t\t\tVersion:  \"1.0.0\",\n\t\t\t\tType:     syftPkg.NpmPkg,\n\t\t\t\tLanguage: syftPkg.JavaScript,\n\t\t\t\tCPEs: []cpe.CPE{\n\t\t\t\t\tcpe.Must(\"cpe:2.3:a:example:example-lib:1.0.0:*:*:*:*:*:*:*\", \"\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\tvuln: vulnerability.Vulnerability{\n\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\tID:        \"CVE-2021-87654\",\n\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t},\n\t\t\t\tPackageName: \"example-lib\",\n\t\t\t},\n\t\t\texpectedMatches: true,\n\t\t},\n\t\t{\n\t\t\tname: \"package with wildcard targetSW should match\",\n\t\t\tpkg: pkg.Package{\n\t\t\t\tName:     \"react\",\n\t\t\t\tVersion:  \"17.0.2\",\n\t\t\t\tType:     syftPkg.NpmPkg,\n\t\t\t\tLanguage: syftPkg.JavaScript,\n\t\t\t\tCPEs: []cpe.CPE{\n\t\t\t\t\tcpe.Must(\"cpe:2.3:a:facebook:react:17.0.2:*:*:*:*:*:*:*\", \"\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\tvuln: vulnerability.Vulnerability{\n\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\tID:        \"CVE-2021-12345\",\n\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t},\n\t\t\t\tPackageName: \"react\",\n\t\t\t\tCPEs: []cpe.CPE{\n\t\t\t\t\tcpe.Must(\"cpe:2.3:a:facebook:react:17.0.2:*:*:*:*:node.js:*:*\", \"\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedMatches: true,\n\t\t},\n\t\t{\n\t\t\tname: \"intersecting target software should match\",\n\t\t\tpkg: pkg.Package{\n\t\t\t\tName:     \"lodash\",\n\t\t\t\tVersion:  \"4.17.20\",\n\t\t\t\tType:     syftPkg.NpmPkg,\n\t\t\t\tLanguage: syftPkg.JavaScript,\n\t\t\t\tCPEs: []cpe.CPE{\n\t\t\t\t\tcpe.Must(\"cpe:2.3:a:lodash:lodash:4.17.20:*:*:*:*:node.js:*:*\", \"\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\tvuln: vulnerability.Vulnerability{\n\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\tID:        \"CVE-2021-23337\",\n\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t},\n\t\t\t\tPackageName: \"lodash\",\n\t\t\t\tCPEs: []cpe.CPE{\n\t\t\t\t\tcpe.Must(\"cpe:2.3:a:lodash:lodash:4.17.20:*:*:*:*:node.js:*:*\", \"\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedMatches: true,\n\t\t},\n\t\t{\n\t\t\tname: \"non-intersecting target software with matching language should match\",\n\t\t\tpkg: pkg.Package{\n\t\t\t\tName:     \"express\",\n\t\t\t\tVersion:  \"4.17.1\",\n\t\t\t\tType:     syftPkg.RpmPkg,     // important!\n\t\t\t\tLanguage: syftPkg.JavaScript, // we're using this to match against the vuln TSW\n\t\t\t\tCPEs: []cpe.CPE{\n\t\t\t\t\tcpe.Must(\"cpe:2.3:a:expressjs:express:4.17.1:*:*:*:*:react:*:*\", \"\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\tvuln: vulnerability.Vulnerability{\n\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\tID:        \"CVE-2022-24999\",\n\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t},\n\t\t\t\tPackageName: \"express\",\n\t\t\t\tCPEs: []cpe.CPE{\n\t\t\t\t\tcpe.Must(\"cpe:2.3:a:expressjs:express:4.17.1:*:*:*:*:node.js:*:*\", \"\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedMatches: true,\n\t\t},\n\t\t{\n\t\t\tname: \"non-intersecting target software with matching package type should fail\",\n\t\t\tpkg: pkg.Package{\n\t\t\t\tName:     \"moment\",\n\t\t\t\tVersion:  \"2.29.1\",\n\t\t\t\tType:     syftPkg.NpmPkg, // we're using this to match against the vuln TSW\n\t\t\t\tLanguage: syftPkg.CPP,    // important!\n\t\t\t\tCPEs: []cpe.CPE{\n\t\t\t\t\tcpe.Must(\"cpe:2.3:a:moment:moment:2.29.1:*:*:*:*:doesntmatter:*:*\", \"\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\tvuln: vulnerability.Vulnerability{\n\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\tID:        \"CVE-2022-31129\",\n\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t},\n\t\t\t\tPackageName: \"moment\",\n\t\t\t\tCPEs: []cpe.CPE{\n\t\t\t\t\tcpe.Must(\"cpe:2.3:a:moment:moment:2.29.1:*:*:*:*:node.js:*:*\", \"\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedMatches: true,\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tmatches, reason := isVulnerableTarget(test.pkg, test.vuln)\n\n\t\t\tassert.Equal(t, test.expectedMatches, matches, \"matches result should be as expected\")\n\t\t\tassert.Equal(t, test.expectedReason, reason, \"reason should match expected\")\n\t\t})\n\t}\n}\n\nfunc Test_isUnknownTarget(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\ttargetSW string\n\t\texpected bool\n\t}{\n\t\t{name: \"supported syft language\", targetSW: \"python\", expected: false},\n\t\t{name: \"supported non-syft language CPE component\", targetSW: \"joomla\", expected: false},\n\t\t{name: \"unknown component\", targetSW: \"abc\", expected: true},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tu := isUnknownTarget(test.targetSW)\n\t\t\tassert.Equal(t, test.expected, u)\n\t\t})\n\t}\n}\n\nfunc TestPkgTypesFromTargetSoftware(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    []string\n\t\texpected []string\n\t}{\n\t\t{\n\t\t\tname:     \"empty input\",\n\t\t\tinput:    []string{},\n\t\t\texpected: []string{},\n\t\t},\n\t\t{\n\t\t\tname:     \"single input with known mapping\",\n\t\t\tinput:    []string{\"node.js\"},\n\t\t\texpected: []string{string(syftPkg.NpmPkg)},\n\t\t},\n\t\t{\n\t\t\tname:     \"multiple inputs with known mappings\",\n\t\t\tinput:    []string{\"python\", \"ruby\", \"java\"},\n\t\t\texpected: []string{string(syftPkg.PythonPkg), string(syftPkg.GemPkg), string(syftPkg.JavaPkg)},\n\t\t},\n\t\t{\n\t\t\tname:     \"case insensitive input\",\n\t\t\tinput:    []string{\"Python\", \"RUBY\", \"Java\"},\n\t\t\texpected: []string{string(syftPkg.PythonPkg), string(syftPkg.GemPkg), string(syftPkg.JavaPkg)},\n\t\t},\n\t\t{\n\t\t\tname:     \"mixed known and unknown inputs\",\n\t\t\tinput:    []string{\"python\", \"unknown\", \"ruby\"},\n\t\t\texpected: []string{string(syftPkg.PythonPkg), \"unknown\", string(syftPkg.GemPkg)},\n\t\t},\n\t\t{\n\t\t\tname:     \"all unknown inputs\",\n\t\t\tinput:    []string{\"unknown1\", \"unknown2\", \"unknown3\"},\n\t\t\texpected: []string{\"unknown1\", \"unknown2\", \"unknown3\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"inputs with spaces and hyphens\",\n\t\t\tinput:    []string{\"redhat-enterprise-linux\", \"jenkins ci\"},\n\t\t\texpected: []string{string(syftPkg.RpmPkg), string(syftPkg.JavaPkg)},\n\t\t},\n\t\t{\n\t\t\tname:     \"aliases for the same package type\",\n\t\t\tinput:    []string{\"nodejs\", \"npm\", \"javascript\"},\n\t\t\texpected: []string{string(syftPkg.NpmPkg)},\n\t\t},\n\t\t{\n\t\t\tname:     \"wildcards and special characters should be ignored\",\n\t\t\tinput:    []string{\"*\", \"?\", \"-\", \"\"},\n\t\t\texpected: []string{},\n\t\t},\n\t\t{\n\t\t\tname:     \"Linux distributions\",\n\t\t\tinput:    []string{\"alpine\", \"debian\", \"redhat\", \"gentoo\"},\n\t\t\texpected: []string{string(syftPkg.ApkPkg), string(syftPkg.DebPkg), string(syftPkg.RpmPkg), string(syftPkg.PortagePkg)},\n\t\t},\n\t\t{\n\t\t\tname:     \".NET ecosystem\",\n\t\t\tinput:    []string{\".net\", \"asp.net\", \"c#\"},\n\t\t\texpected: []string{string(syftPkg.DotnetPkg)},\n\t\t},\n\t\t{\n\t\t\tname:     \"JavaScript ecosystem\",\n\t\t\tinput:    []string{\"javascript\", \"node.js\", \"jquery\"},\n\t\t\texpected: []string{string(syftPkg.NpmPkg)},\n\t\t},\n\t\t{\n\t\t\tname:     \"Java ecosystem\",\n\t\t\tinput:    []string{\"java\", \"maven\", \"kafka\", \"log4j\"},\n\t\t\texpected: []string{string(syftPkg.JavaPkg)},\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tactual := normalizeTargetSoftwares(test.input)\n\n\t\t\tassert.ElementsMatch(t, test.expected, actual.List(), \"package types should match\")\n\t\t})\n\t}\n}\n\nfunc TestHasIntersectingTargetSoftware(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tset1     []string\n\t\tset2     []string\n\t\texpected bool\n\t}{\n\t\t// basic assertions around sets normalized to package types\n\t\t{\n\t\t\tname:     \"empty sets\",\n\t\t\tset1:     []string{},\n\t\t\tset2:     []string{},\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"first set empty\",\n\t\t\tset1:     []string{},\n\t\t\tset2:     []string{\"nodejs\", \"python\"},\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"second set empty\",\n\t\t\tset1:     []string{\"java\", \"ruby\"},\n\t\t\tset2:     []string{},\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"intersecting sets - direct match\",\n\t\t\tset1:     []string{\"nodejs\", \"python\"},\n\t\t\tset2:     []string{\"nodejs\", \"ruby\"},\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"intersecting sets - aliases\",\n\t\t\tset1:     []string{\"node.js\"},\n\t\t\tset2:     []string{\"npm\"},\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"non-intersecting sets\",\n\t\t\tset1:     []string{\"python\", \"ruby\"},\n\t\t\tset2:     []string{\"java\", \"golang\"},\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"multiple intersections\",\n\t\t\tset1:     []string{\"python\", \"ruby\", \"nodejs\"},\n\t\t\tset2:     []string{\"javascript\", \"python\", \"java\"},\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"case insensitive\",\n\t\t\tset1:     []string{\"Python\", \"Ruby\"},\n\t\t\tset2:     []string{\"python\", \"java\"},\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"wildcard in first set\",\n\t\t\tset1:     []string{\"*\"},\n\t\t\tset2:     []string{\"nodejs\", \"python\"},\n\t\t\texpected: false, // * doesn't map to a package type\n\t\t},\n\t\t{\n\t\t\tname:     \"special linux distro aliases\",\n\t\t\tset1:     []string{\"rhel\", \"opensuse\"},\n\t\t\tset2:     []string{\"redhat\"},\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"different terminology for same ecosystem\",\n\t\t\tset1:     []string{\"c#\"},\n\t\t\tset2:     []string{\"dotnet\"},\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"spaces and hyphens handling\",\n\t\t\tset1:     []string{\"jenkins ci\"},\n\t\t\tset2:     []string{\"jenkins-ci\"},\n\t\t\texpected: true,\n\t\t},\n\n\t\t// ecosystem specific cases\n\t\t{\n\t\t\tname:     \"npm package vs node.js vulnerability\",\n\t\t\tset1:     []string{\"npm\"},\n\t\t\tset2:     []string{\"node.js\"},\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"python package vs django vulnerability\",\n\t\t\tset1:     []string{\"python\"},\n\t\t\tset2:     []string{\"django\"},\n\t\t\texpected: false, // django is not mapped to a package type in the current implementation\n\t\t},\n\t\t{\n\t\t\tname:     \"java package vs multiple java ecosystem vulnerabilities\",\n\t\t\tset1:     []string{\"java\"},\n\t\t\tset2:     []string{\"tomcat\", \"log4j\", \"maven\"},\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"linux distributions match with different aliases\",\n\t\t\tset1:     []string{\"redhat\"},\n\t\t\tset2:     []string{\"centos\", \"fedora\", \"rhel\"},\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"no common package types\",\n\t\t\tset1:     []string{\"python\", \"ruby\"},\n\t\t\tset2:     []string{\"nodejs\", \"php\"},\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"mixed case and formatting\",\n\t\t\tset1:     []string{\"Node.js\", \"Ruby-On-Rails\"},\n\t\t\tset2:     []string{\"javascript\", \"gem\"},\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \".NET ecosystem different terms\",\n\t\t\tset1:     []string{\".net-framework\"},\n\t\t\tset2:     []string{\"c#\", \"nuget\"},\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"WordPress ecosystem\",\n\t\t\tset1:     []string{\"wordpress\"},\n\t\t\tset2:     []string{\"wordpress_plugin\"},\n\t\t\texpected: true,\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tset1 := strset.New(test.set1...)\n\t\t\tset2 := strset.New(test.set2...)\n\n\t\t\tactual := hasIntersectingTargetSoftware(set1, set2)\n\t\t\tassert.Equal(t, test.expected, actual, \"integrated target software intersection should match expected\")\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/matcher/internal/only_vulnerable_versions.go",
    "content": "package internal\n\nimport (\n\t\"github.com/anchore/grype/grype/search\"\n\t\"github.com/anchore/grype/grype/version\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n)\n\n// OnlyVulnerableVersions returns a criteria object that tests affected vulnerability ranges against the provided version\nfunc OnlyVulnerableVersions(v *version.Version) vulnerability.Criteria {\n\tif v == nil || v.Raw == \"\" {\n\t\t// if no version is provided, match everything\n\t\treturn search.ByFunc(func(_ vulnerability.Vulnerability) (bool, string, error) {\n\t\t\treturn true, \"\", nil\n\t\t}) // since we return true the summary is not used\n\t}\n\treturn search.ByVersion(*v)\n}\n"
  },
  {
    "path": "grype/matcher/internal/result/match_details_set.go",
    "content": "package result\n\nimport \"github.com/anchore/grype/grype/match\"\n\ntype MatchDetailsSet struct {\n\torder []match.Detail\n\tseen  map[match.Detail]struct{}\n}\n\nfunc NewMatchDetailsSet(ds ...match.Detail) MatchDetailsSet {\n\ts := MatchDetailsSet{\n\t\torder: []match.Detail{},\n\t\tseen:  make(map[match.Detail]struct{}),\n\t}\n\tfor _, detail := range ds {\n\t\ts.Add(detail)\n\t}\n\treturn s\n}\n\nfunc (ds *MatchDetailsSet) Add(detail match.Detail) {\n\tif _, exists := ds.seen[detail]; !exists {\n\t\tds.order = append(ds.order, detail)\n\t\tds.seen[detail] = struct{}{}\n\t}\n}\n\nfunc (ds MatchDetailsSet) ToSlice() []match.Detail {\n\treturn ds.order\n}\n"
  },
  {
    "path": "grype/matcher/internal/result/provider.go",
    "content": "package result\n\nimport (\n\t\"github.com/anchore/grype/grype/match\"\n\t\"github.com/anchore/grype/grype/pkg\"\n\t\"github.com/anchore/grype/grype/search\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n)\n\nvar _ Provider = (*provider)(nil)\n\ntype Provider interface {\n\tFindResults(criteria ...vulnerability.Criteria) (Set, error)\n}\n\ntype provider struct {\n\tvulnProvider vulnerability.Provider\n\tcatalogedPkg pkg.Package // this is what is passed into the matcher\n\tmatcher      match.MatcherType\n}\n\nfunc NewProvider(vp vulnerability.Provider, catalogedPkg pkg.Package, matcher match.MatcherType) Provider {\n\treturn provider{\n\t\tvulnProvider: vp,\n\t\tcatalogedPkg: catalogedPkg,\n\t\tmatcher:      matcher,\n\t}\n}\n\nfunc (p provider) FindResults(criteria ...vulnerability.Criteria) (Set, error) {\n\tresults := Set{}\n\t// get each iteration here so detailProvider will have the specific values used for searches\n\tfor _, cs := range search.CriteriaIterator(criteria) {\n\t\tvulns, err := p.vulnProvider.FindVulnerabilities(cs...)\n\t\tif err != nil {\n\t\t\treturn Set{}, err\n\t\t}\n\n\t\tfor _, v := range vulns {\n\t\t\tif v.ID == \"\" {\n\t\t\t\tcontinue // skip vulnerabilities without an ID (should never happen)\n\t\t\t}\n\n\t\t\tnewResult := Result{\n\t\t\t\tID:              v.ID,\n\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{v},\n\t\t\t\tDetails:         detailProvider(p.matcher, p.catalogedPkg, criteria, v),\n\t\t\t\tPackage:         &p.catalogedPkg,\n\t\t\t}\n\n\t\t\tresults[v.ID] = append(results[v.ID], newResult)\n\t\t}\n\t}\n\treturn results, nil\n}\n\nfunc detailProvider(matcher match.MatcherType, catalogedPkg pkg.Package, criteriaSet []vulnerability.Criteria, vuln vulnerability.Vulnerability) match.Details {\n\tcpeParams, distroParams, ecosystemParams, pkgParams := extractSearchParameters(criteriaSet, vuln)\n\tdistroMatchType := determineMatchType(catalogedPkg, pkgParams)\n\tapplyPackageParamsToSearchParams(pkgParams, &cpeParams, &distroParams, &ecosystemParams)\n\tconstraintStr := getConstraintString(vuln)\n\n\treturn buildMatchDetails(matcher, distroMatchType, constraintStr, vuln, cpeParams, distroParams, ecosystemParams)\n}\n\n// extractSearchParameters processes criteria set and extracts search parameters for different match types\nfunc extractSearchParameters(criteriaSet []vulnerability.Criteria, vuln vulnerability.Vulnerability) ([]match.CPEParameters, []match.DistroParameters, []match.EcosystemParameters, *match.PackageParameter) {\n\tvar cpeParams []match.CPEParameters\n\tvar distroParams []match.DistroParameters\n\tvar ecosystemParams []match.EcosystemParameters\n\tvar pkgParams *match.PackageParameter\n\n\tfor i := 0; i < len(criteriaSet); i++ {\n\t\tswitch c := criteriaSet[i].(type) {\n\t\tcase *search.PackageNameCriteria:\n\t\t\tif pkgParams == nil {\n\t\t\t\tpkgParams = &match.PackageParameter{}\n\t\t\t}\n\t\t\tpkgParams.Name = c.PackageName\n\n\t\tcase *search.VersionCriteria:\n\t\t\tif pkgParams == nil {\n\t\t\t\tpkgParams = &match.PackageParameter{}\n\t\t\t}\n\t\t\tpkgParams.Version = c.Version.Raw\n\n\t\tcase *search.EcosystemCriteria:\n\t\t\tecosystemParams = append(ecosystemParams, match.EcosystemParameters{\n\t\t\t\tLanguage:  c.Language.String(),\n\t\t\t\tNamespace: vuln.Namespace, // TODO: this is a holdover and will be removed in the future\n\t\t\t})\n\n\t\tcase *search.CPECriteria:\n\t\t\tcpeParams = append(cpeParams, match.CPEParameters{\n\t\t\t\tNamespace: vuln.Namespace, // TODO: this is a holdover and will be removed in the future\n\t\t\t\tCPEs: []string{\n\t\t\t\t\tc.CPE.Attributes.BindToFmtString(),\n\t\t\t\t},\n\t\t\t})\n\n\t\tcase *search.DistroCriteria:\n\t\t\tfor _, d := range c.Distros {\n\t\t\t\tdistroParams = append(distroParams, match.DistroParameters{\n\t\t\t\t\tDistro: match.DistroIdentification{\n\t\t\t\t\t\tType:    d.Type.String(),\n\t\t\t\t\t\tVersion: d.VersionString(),\n\t\t\t\t\t},\n\t\t\t\t\tNamespace: vuln.Namespace, // TODO: this is a holdover and will be removed in the future\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\t}\n\n\treturn cpeParams, distroParams, ecosystemParams, pkgParams\n}\n\n// determineMatchType determines if this is a direct or indirect match based on package names\nfunc determineMatchType(catalogedPkg pkg.Package, pkgParams *match.PackageParameter) match.Type {\n\tif pkgParams != nil && catalogedPkg.Name != pkgParams.Name {\n\t\t// if the cataloged package name does not match the package parameter, then this is an indirect match\n\t\treturn match.ExactIndirectMatch\n\t}\n\treturn match.ExactDirectMatch\n}\n\n// applyPackageParamsToSearchParams applies discovered package parameters to search parameters\nfunc applyPackageParamsToSearchParams(pkgParams *match.PackageParameter, cpeParams *[]match.CPEParameters, distroParams *[]match.DistroParameters, ecosystemParams *[]match.EcosystemParameters) {\n\tif pkgParams == nil {\n\t\treturn\n\t}\n\n\tfor i := range *ecosystemParams {\n\t\t(*ecosystemParams)[i].Package = *pkgParams\n\t}\n\tfor i := range *cpeParams {\n\t\t(*cpeParams)[i].Package = *pkgParams\n\t}\n\tfor i := range *distroParams {\n\t\t(*distroParams)[i].Package = *pkgParams\n\t}\n}\n\n// getConstraintString safely extracts constraint string from vulnerability\nfunc getConstraintString(vuln vulnerability.Vulnerability) string {\n\tif vuln.Constraint != nil {\n\t\treturn vuln.Constraint.String()\n\t}\n\treturn \"\"\n}\n\n// buildMatchDetails creates the final match details from all parameters\nfunc buildMatchDetails(matcher match.MatcherType, distroMatchType match.Type, constraintStr string, vuln vulnerability.Vulnerability, cpeParams []match.CPEParameters, distroParams []match.DistroParameters, ecosystemParams []match.EcosystemParameters) match.Details {\n\tvar details match.Details\n\n\t// add CPE match details\n\tfor _, cpeParam := range cpeParams {\n\t\tdetails = append(details, match.Detail{\n\t\t\tType:       match.CPEMatch,\n\t\t\tMatcher:    matcher,\n\t\t\tSearchedBy: cpeParam,\n\t\t\tFound: match.CPEResult{\n\t\t\t\tVulnerabilityID:   vuln.ID,\n\t\t\t\tVersionConstraint: constraintStr,\n\t\t\t},\n\t\t\tConfidence: 0.9, // TODO: this is hard coded for now\n\t\t})\n\t}\n\n\t// add distro match details\n\tfor _, distroParam := range distroParams {\n\t\tdetails = append(details, match.Detail{\n\t\t\tType:       distroMatchType,\n\t\t\tMatcher:    matcher,\n\t\t\tSearchedBy: distroParam,\n\t\t\tFound: match.DistroResult{\n\t\t\t\tVulnerabilityID:   vuln.ID,\n\t\t\t\tVersionConstraint: constraintStr,\n\t\t\t},\n\t\t\tConfidence: 1.0, // TODO: this is hard coded for now\n\t\t})\n\t}\n\n\t// add ecosystem match details\n\tfor _, ecosystemParam := range ecosystemParams {\n\t\tdetails = append(details, match.Detail{\n\t\t\tType:       match.ExactDirectMatch,\n\t\t\tMatcher:    matcher,\n\t\t\tSearchedBy: ecosystemParam,\n\t\t\tFound: match.EcosystemResult{\n\t\t\t\tVulnerabilityID:   vuln.ID,\n\t\t\t\tVersionConstraint: constraintStr,\n\t\t\t},\n\t\t\tConfidence: 1.0, // TODO: this is hard coded for now\n\t\t})\n\t}\n\n\treturn details\n}\n"
  },
  {
    "path": "grype/matcher/internal/result/results.go",
    "content": "package result\n\nimport (\n\t\"github.com/scylladb/go-set/strset\"\n\n\t\"github.com/anchore/grype/grype/match\"\n\t\"github.com/anchore/grype/grype/pkg\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n\t\"github.com/anchore/grype/internal/log\"\n)\n\n// Result represents a prototype match of a package used to search, a set of vulnerabilities discovered from the search,\n// and match details that describe the search itself. Note that all vulnerabilities in a Result share the same\n// vulnerability ID (in the ID field and `.Vulnerabilities[].ID` fields -- it is invalid to mix vulnerabilities into\n// a Result that have different IDs.\ntype Result struct {\n\t// ID is the vulnerability ID; all vulnerabilities in this Result share the same ID.\n\tID string\n\n\t// Vulnerabilities is a set of vulnerabilities that were discovered from the search.\n\tVulnerabilities []vulnerability.Vulnerability\n\n\t// Details is a set of match details that describe the search itself\n\tDetails []match.Detail\n\n\t// Package is the package that was used to search for vulnerabilities.\n\tPackage *pkg.Package\n}\n\ntype Set map[string][]Result\n\nfunc unionIntoResult(existing []Result) Result {\n\tvar merged Result\n\tfor _, r := range existing {\n\t\tif merged.ID == \"\" {\n\t\t\tmerged.ID = r.ID\n\t\t\tmerged.Package = r.Package\n\t\t}\n\t\tmerged.Vulnerabilities = append(merged.Vulnerabilities, r.Vulnerabilities...)\n\t\tmerged.Details = append(merged.Details, r.Details...)\n\t}\n\tmerged.Details = NewMatchDetailsSet(merged.Details...).ToSlice()\n\treturn merged\n}\n\nfunc (s Set) ToMatches() []match.Match {\n\tvar out []match.Match\n\tfor _, results := range s {\n\t\tmerged := unionIntoResult(results)\n\n\t\tif len(merged.Vulnerabilities) == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tif merged.Package == nil {\n\t\t\tcontinue // skip results without a package\n\t\t}\n\n\t\tfor _, vv := range merged.Vulnerabilities {\n\t\t\tout = append(out,\n\t\t\t\tmatch.Match{\n\t\t\t\t\tVulnerability: vv,\n\t\t\t\t\tPackage:       *merged.Package,\n\t\t\t\t\tDetails:       merged.Details,\n\t\t\t\t},\n\t\t\t)\n\t\t}\n\t}\n\n\treturn out\n}\n\n// Remove will prune elements from the current set that have any ids/aliases in common with the incoming set.\n// For example:\n//\n// set 1:\n//\n//\tEntry A: GHSA-g4mx-q9vg-27p4  (alias CVE-2023-45803)\n//\n// set 2:\n//\n//\tEntry B: CGA-7qjw-ggh3-pp9f (alias CVE-2023-45803)\n//\n// We want to be able to remove Entry A from set 1 because it has the same alias as Entry B in set 2.\n// This is because the vulnerability IDs are different, but they refer to the same underlying vulnerability.\nfunc (s Set) Remove(incoming Set) Set {\n\t// collect all incoming identifiers into one unified set\n\tincomingIdentifiers := strset.New()\n\tfor id, results := range incoming {\n\t\tincomingIdentifiers.Add(getIdentity(id, results).List()...)\n\t}\n\n\t// keep only entries whose identities don't overlap with incoming\n\tout := Set{}\n\tfor id, results := range s {\n\t\tidentity := getIdentity(id, results)\n\t\tif strset.Intersection(identity, incomingIdentifiers).IsEmpty() {\n\t\t\tout[id] = results\n\t\t}\n\t}\n\treturn out\n}\n\nfunc extractAliases(results []Result) *strset.Set {\n\taliases := strset.New()\n\tfor _, r := range results {\n\t\tfor _, v := range r.Vulnerabilities {\n\t\t\tfor _, a := range v.RelatedVulnerabilities {\n\t\t\t\taliases.Add(a.ID)\n\t\t\t}\n\t\t}\n\t}\n\treturn aliases\n}\n\n// getIdentity returns all identifiers (ID + aliases) for a vulnerability entry\nfunc getIdentity(id string, results []Result) *strset.Set {\n\tidentity := strset.New()\n\tidentity.Add(id)\n\tidentity.Add(extractAliases(results).List()...)\n\treturn identity\n}\n\nfunc unionResults(existing, incoming []Result) (n []Result) {\n\tn = append(n, existing...)\n\tn = append(n, incoming...)\n\treturn n\n}\n\nfunc (s Set) Merge(incoming Set, mergeFuncs ...func(existing, incoming []Result) []Result) Set {\n\tout := Set{}\n\tif len(mergeFuncs) == 0 {\n\t\t// with no other merge functions specified, append all vulnerability results and details\n\t\tmergeFuncs = []func(existing, incoming []Result) []Result{\n\t\t\tunionResults,\n\t\t}\n\t}\n\n\t// det all unique IDs from both sets\n\tallIDs := make(map[string]struct{})\n\tfor id := range s {\n\t\tallIDs[id] = struct{}{}\n\t}\n\tfor id := range incoming {\n\t\tallIDs[id] = struct{}{}\n\t}\n\n\t// process each ID, applying all merge functions\n\tfor id := range allIDs {\n\t\texistingResults := s[id]\n\t\tincomingResults := incoming[id]\n\n\t\tmergedResults := append([]Result(nil), existingResults...)\n\t\tfor _, mergeFunc := range mergeFuncs {\n\t\t\tmergedResults = mergeFunc(mergedResults, incomingResults)\n\t\t}\n\n\t\tif len(mergedResults) > 0 {\n\t\t\t// filter out any results with empty vulnerabilities\n\t\t\tfor _, result := range mergedResults {\n\t\t\t\tif result.ID != \"\" && len(result.Vulnerabilities) > 0 {\n\t\t\t\t\tout[result.ID] = append(out[result.ID], result)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn out\n}\n\nfunc (s Set) Contains(id string) bool {\n\tresults, ok := s[id]\n\treturn ok && len(results) > 0\n}\n\nfunc (s Set) ContainsAny(ids ...string) bool {\n\tfor _, id := range ids {\n\t\tresults, ok := s[id]\n\t\tif ok && len(results) > 0 {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// ContainsByIdentity checks if the set contains an entry with overlapping identity (ID or aliases)\nfunc (s Set) ContainsByIdentity(searchID string, searchResults []Result) bool {\n\tsearchIdentity := getIdentity(searchID, searchResults)\n\n\tfor id, results := range s {\n\t\tidentity := getIdentity(id, results)\n\t\tif !strset.Intersection(identity, searchIdentity).IsEmpty() {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// Intersection returns entries that exist in both sets (by identity overlap)\nfunc (s Set) Intersection(other Set) Set {\n\totherIdentifiers := strset.New()\n\tfor id, results := range other {\n\t\totherIdentifiers.Add(getIdentity(id, results).List()...)\n\t}\n\n\tout := Set{}\n\tfor id, results := range s {\n\t\tidentity := getIdentity(id, results)\n\t\tif !strset.Intersection(identity, otherIdentifiers).IsEmpty() {\n\t\t\tout[id] = results\n\t\t}\n\t}\n\treturn out\n}\n\n// IdentitiesOverlap returns true if two results share any common identifiers, where identity\n// includes both the primary ID and any aliases (from RelatedVulnerabilities). This can be used\n// as a shouldUpdate predicate for Update when matching results by ID or alias relationships.\nfunc IdentitiesOverlap(existing Result, incoming Result) bool {\n\texistingIdentity := getIdentity(existing.ID, []Result{existing})\n\tincomingIdentity := getIdentity(incoming.ID, []Result{incoming})\n\treturn !strset.Intersection(existingIdentity, incomingIdentity).IsEmpty()\n}\n\n// Update applies an update function to each result in the set where shouldUpdate returns true\n// for the existing-incoming result pair. The updateFunc can modify fields of the existing result\n// in-place while preserving other fields. Returns a new Set with updated results.\n//\n// Example with identity-based matching:\n//\n//\tupdated := base.Update(incoming, IdentitiesOverlap, func(existing *Result, incoming Result) {\n//\t    existing.Vulnerabilities[0].Fix = incoming.Vulnerabilities[0].Fix\n//\t})\nfunc (s Set) Update(incoming Set, shouldUpdate func(existing Result, incoming Result) bool, updateFunc func(existing *Result, incoming Result)) Set {\n\tout := make(Set)\n\n\t// Copy everything from base set\n\tfor id, results := range s {\n\t\tout[id] = append([]Result(nil), results...)\n\t}\n\n\t// For each entry in base, check all incoming entries with shouldUpdate\n\tfor id, existingResults := range out {\n\t\tfor i := range existingResults {\n\t\t\tfor _, incomingResults := range incoming {\n\t\t\t\tfor _, incomingResult := range incomingResults {\n\t\t\t\t\tif shouldUpdate(existingResults[i], incomingResult) {\n\t\t\t\t\t\tupdateFunc(&existingResults[i], incomingResult)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tout[id] = existingResults\n\t}\n\n\treturn out\n}\n\nfunc (s Set) Filter(criteria ...vulnerability.Criteria) Set {\n\tout := Set{}\n\tfor id, results := range s {\n\t\tvar filteredResults []Result\n\n\t\tfor _, result := range results {\n\t\t\tvulns, err := filterVulns(result.Vulnerabilities, criteria)\n\t\t\tif err != nil {\n\t\t\t\tlog.WithFields(\"vulnerability\", result.ID, \"error\", err).Debug(\"failed to filter vulns\")\n\t\t\t\t// if there was an error filtering vulnerabilities, keep them all\n\t\t\t\tvulns = result.Vulnerabilities\n\t\t\t}\n\t\t\tif len(vulns) == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfilteredResults = append(filteredResults, Result{\n\t\t\t\tID:              result.ID,\n\t\t\t\tVulnerabilities: vulns,\n\t\t\t\tDetails:         result.Details,\n\t\t\t\tPackage:         result.Package,\n\t\t\t})\n\t\t}\n\n\t\tif len(filteredResults) > 0 {\n\t\t\tout[id] = filteredResults\n\t\t} else if len(results) > 0 {\n\t\t\tvulnerability.LogDropped(id, \"filterVulns\", \"no vulnerabilities matched criteria\", criteria)\n\t\t}\n\t}\n\treturn out\n}\n\nfunc filterVulns(vulnerabilities []vulnerability.Vulnerability, criteria []vulnerability.Criteria) ([]vulnerability.Vulnerability, error) {\n\tvar out []vulnerability.Vulnerability\nnextVulnerability:\n\tfor _, v := range vulnerabilities {\n\t\tfor _, c := range criteria {\n\t\t\tmatches, dropReason, err := c.MatchesVulnerability(v)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tif !matches {\n\t\t\t\tvulnerability.LogDropped(v.ID, \"filterVulns\", dropReason, c)\n\t\t\t\tcontinue nextVulnerability\n\t\t\t}\n\t\t}\n\t\tout = append(out, v)\n\t}\n\treturn out, nil\n}\n"
  },
  {
    "path": "grype/matcher/internal/result/results_test.go",
    "content": "package result\n\nimport (\n\t\"testing\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/google/go-cmp/cmp/cmpopts\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/grype/grype/match\"\n\t\"github.com/anchore/grype/grype/pkg\"\n\t\"github.com/anchore/grype/grype/search\"\n\t\"github.com/anchore/grype/grype/version\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n\t\"github.com/anchore/syft/syft/file\"\n\tsyftPkg \"github.com/anchore/syft/syft/pkg\"\n)\n\nfunc TestSet_Remove(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\treceiver Set\n\t\tincoming Set\n\t\twant     Set\n\t}{\n\t\t{\n\t\t\tname: \"remove existing entries\",\n\t\t\treceiver: Set{\n\t\t\t\t\"vuln-1\": []Result{\n\t\t\t\t\t{\n\t\t\t\t\t\tID:              \"vuln-1\",\n\t\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{{Reference: vulnerability.Reference{ID: \"CVE-2021-1\"}}},\n\t\t\t\t\t\tDetails:         match.Details{{Type: match.ExactDirectMatch}},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t\"vuln-2\": []Result{\n\t\t\t\t\t{\n\t\t\t\t\t\tID:              \"vuln-2\",\n\t\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{{Reference: vulnerability.Reference{ID: \"CVE-2021-2\"}}},\n\t\t\t\t\t\tDetails:         match.Details{{Type: match.ExactDirectMatch}},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tincoming: Set{\n\t\t\t\t\"vuln-1\": []Result{\n\t\t\t\t\t{ID: \"vuln-1\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: Set{\n\t\t\t\t\"vuln-2\": []Result{\n\t\t\t\t\t{\n\t\t\t\t\t\tID:              \"vuln-2\",\n\t\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{{Reference: vulnerability.Reference{ID: \"CVE-2021-2\"}}},\n\t\t\t\t\t\tDetails:         match.Details{{Type: match.ExactDirectMatch}},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"remove non-existing entry has no effect\",\n\t\t\treceiver: Set{\n\t\t\t\t\"vuln-1\": []Result{\n\t\t\t\t\t{\n\t\t\t\t\t\tID:              \"vuln-1\",\n\t\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{{Reference: vulnerability.Reference{ID: \"CVE-2021-1\"}}},\n\t\t\t\t\t\tDetails:         match.Details{{Type: match.ExactDirectMatch}},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tincoming: Set{\n\t\t\t\t\"vuln-2\": []Result{\n\t\t\t\t\t{ID: \"vuln-2\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: Set{\n\t\t\t\t\"vuln-1\": []Result{\n\t\t\t\t\t{\n\t\t\t\t\t\tID:              \"vuln-1\",\n\t\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{{Reference: vulnerability.Reference{ID: \"CVE-2021-1\"}}},\n\t\t\t\t\t\tDetails:         match.Details{{Type: match.ExactDirectMatch}},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:     \"remove from empty set\",\n\t\t\treceiver: Set{},\n\t\t\tincoming: Set{\n\t\t\t\t\"vuln-1\": []Result{\n\t\t\t\t\t{ID: \"vuln-1\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: Set{},\n\t\t},\n\t\t{\n\t\t\tname: \"remove with empty incoming set\",\n\t\t\treceiver: Set{\n\t\t\t\t\"vuln-1\": []Result{\n\t\t\t\t\t{\n\t\t\t\t\t\tID:              \"vuln-1\",\n\t\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{{Reference: vulnerability.Reference{ID: \"CVE-2021-1\"}}},\n\t\t\t\t\t\tDetails:         match.Details{{Type: match.ExactDirectMatch}},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tincoming: Set{},\n\t\t\twant: Set{\n\t\t\t\t\"vuln-1\": []Result{\n\t\t\t\t\t{\n\t\t\t\t\t\tID:              \"vuln-1\",\n\t\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{{Reference: vulnerability.Reference{ID: \"CVE-2021-1\"}}},\n\t\t\t\t\t\tDetails:         match.Details{{Type: match.ExactDirectMatch}},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"remove entry with shared alias (comment example)\",\n\t\t\treceiver: Set{\n\t\t\t\t\"GHSA-g4mx-q9vg-27p4\": []Result{\n\t\t\t\t\t{\n\t\t\t\t\t\tID: \"GHSA-g4mx-q9vg-27p4\",\n\t\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tReference: vulnerability.Reference{ID: \"GHSA-g4mx-q9vg-27p4\"},\n\t\t\t\t\t\t\t\tRelatedVulnerabilities: []vulnerability.Reference{\n\t\t\t\t\t\t\t\t\t{ID: \"CVE-2023-45803\"},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tDetails: match.Details{{Type: match.ExactDirectMatch}},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tincoming: Set{\n\t\t\t\t\"CGA-7qjw-ggh3-pp9f\": []Result{\n\t\t\t\t\t{\n\t\t\t\t\t\tID: \"CGA-7qjw-ggh3-pp9f\",\n\t\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tReference: vulnerability.Reference{ID: \"CGA-7qjw-ggh3-pp9f\"},\n\t\t\t\t\t\t\t\tRelatedVulnerabilities: []vulnerability.Reference{\n\t\t\t\t\t\t\t\t\t{ID: \"CVE-2023-45803\"},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tDetails: match.Details{{Type: match.ExactDirectMatch}},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: Set{}, // GHSA-g4mx-q9vg-27p4 should be removed due to shared CVE-2023-45803 alias\n\t\t},\n\t\t{\n\t\t\tname: \"remove entry where receiver ID appears as alias in incoming\",\n\t\t\treceiver: Set{\n\t\t\t\t\"CVE-2023-45803\": []Result{\n\t\t\t\t\t{\n\t\t\t\t\t\tID: \"CVE-2023-45803\",\n\t\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{\n\t\t\t\t\t\t\t{Reference: vulnerability.Reference{ID: \"CVE-2023-45803\"}},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tDetails: match.Details{{Type: match.ExactDirectMatch}},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tincoming: Set{\n\t\t\t\t\"GHSA-main-id\": []Result{\n\t\t\t\t\t{\n\t\t\t\t\t\tID: \"GHSA-main-id\",\n\t\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tReference: vulnerability.Reference{ID: \"GHSA-main-id\"},\n\t\t\t\t\t\t\t\tRelatedVulnerabilities: []vulnerability.Reference{\n\t\t\t\t\t\t\t\t\t{ID: \"CVE-2023-45803\"},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tDetails: match.Details{{Type: match.ExactDirectMatch}},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: Set{}, // CVE-2023-45803 should be removed because it appears as alias in incoming\n\t\t},\n\t\t{\n\t\t\tname: \"multiple aliases with partial overlap\",\n\t\t\treceiver: Set{\n\t\t\t\t\"vuln-1\": []Result{\n\t\t\t\t\t{\n\t\t\t\t\t\tID: \"vuln-1\",\n\t\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tReference: vulnerability.Reference{ID: \"vuln-1\"},\n\t\t\t\t\t\t\t\tRelatedVulnerabilities: []vulnerability.Reference{\n\t\t\t\t\t\t\t\t\t{ID: \"CVE-2021-1\"},\n\t\t\t\t\t\t\t\t\t{ID: \"CVE-2021-2\"},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tDetails: match.Details{{Type: match.ExactDirectMatch}},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t\"vuln-2\": []Result{\n\t\t\t\t\t{\n\t\t\t\t\t\tID: \"vuln-2\",\n\t\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tReference: vulnerability.Reference{ID: \"vuln-2\"},\n\t\t\t\t\t\t\t\tRelatedVulnerabilities: []vulnerability.Reference{\n\t\t\t\t\t\t\t\t\t{ID: \"CVE-2021-3\"},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tDetails: match.Details{{Type: match.ExactDirectMatch}},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tincoming: Set{\n\t\t\t\t\"incoming-vuln\": []Result{\n\t\t\t\t\t{\n\t\t\t\t\t\tID: \"incoming-vuln\",\n\t\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tReference: vulnerability.Reference{ID: \"incoming-vuln\"},\n\t\t\t\t\t\t\t\tRelatedVulnerabilities: []vulnerability.Reference{\n\t\t\t\t\t\t\t\t\t{ID: \"CVE-2021-1\"}, // overlaps with vuln-1\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tDetails: match.Details{{Type: match.ExactDirectMatch}},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: Set{\n\t\t\t\t\"vuln-2\": []Result{ // vuln-1 removed, vuln-2 preserved\n\t\t\t\t\t{\n\t\t\t\t\t\tID: \"vuln-2\",\n\t\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tReference: vulnerability.Reference{ID: \"vuln-2\"},\n\t\t\t\t\t\t\t\tRelatedVulnerabilities: []vulnerability.Reference{\n\t\t\t\t\t\t\t\t\t{ID: \"CVE-2021-3\"},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tDetails: match.Details{{Type: match.ExactDirectMatch}},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"no aliases in vulnerabilities\",\n\t\t\treceiver: Set{\n\t\t\t\t\"vuln-1\": []Result{\n\t\t\t\t\t{\n\t\t\t\t\t\tID: \"vuln-1\",\n\t\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{\n\t\t\t\t\t\t\t{Reference: vulnerability.Reference{ID: \"vuln-1\"}},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tDetails: match.Details{{Type: match.ExactDirectMatch}},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tincoming: Set{\n\t\t\t\t\"vuln-2\": []Result{\n\t\t\t\t\t{\n\t\t\t\t\t\tID: \"vuln-2\",\n\t\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{\n\t\t\t\t\t\t\t{Reference: vulnerability.Reference{ID: \"vuln-2\"}},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tDetails: match.Details{{Type: match.ExactDirectMatch}},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: Set{\n\t\t\t\t\"vuln-1\": []Result{\n\t\t\t\t\t{\n\t\t\t\t\t\tID: \"vuln-1\",\n\t\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{\n\t\t\t\t\t\t\t{Reference: vulnerability.Reference{ID: \"vuln-1\"}},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tDetails: match.Details{{Type: match.ExactDirectMatch}},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"complex transitive relationship chain\",\n\t\t\treceiver: Set{\n\t\t\t\t\"A\": []Result{\n\t\t\t\t\t{\n\t\t\t\t\t\tID: \"A\",\n\t\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tReference: vulnerability.Reference{ID: \"A\"},\n\t\t\t\t\t\t\t\tRelatedVulnerabilities: []vulnerability.Reference{\n\t\t\t\t\t\t\t\t\t{ID: \"CVE-1\"},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tDetails: match.Details{{Type: match.ExactDirectMatch}},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t\"B\": []Result{\n\t\t\t\t\t{\n\t\t\t\t\t\tID: \"B\",\n\t\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tReference: vulnerability.Reference{ID: \"B\"},\n\t\t\t\t\t\t\t\tRelatedVulnerabilities: []vulnerability.Reference{\n\t\t\t\t\t\t\t\t\t{ID: \"CVE-2\"},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tDetails: match.Details{{Type: match.ExactDirectMatch}},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tincoming: Set{\n\t\t\t\t\"C\": []Result{\n\t\t\t\t\t{\n\t\t\t\t\t\tID: \"C\",\n\t\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tReference: vulnerability.Reference{ID: \"C\"},\n\t\t\t\t\t\t\t\tRelatedVulnerabilities: []vulnerability.Reference{\n\t\t\t\t\t\t\t\t\t{ID: \"CVE-1\"}, // matches A's alias\n\t\t\t\t\t\t\t\t\t{ID: \"CVE-3\"},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tDetails: match.Details{{Type: match.ExactDirectMatch}},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: Set{\n\t\t\t\t\"B\": []Result{ // A should be removed, B should remain\n\t\t\t\t\t{\n\t\t\t\t\t\tID: \"B\",\n\t\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tReference: vulnerability.Reference{ID: \"B\"},\n\t\t\t\t\t\t\t\tRelatedVulnerabilities: []vulnerability.Reference{\n\t\t\t\t\t\t\t\t\t{ID: \"CVE-2\"},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tDetails: match.Details{{Type: match.ExactDirectMatch}},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"empty related vulnerabilities field\",\n\t\t\treceiver: Set{\n\t\t\t\t\"vuln-1\": []Result{\n\t\t\t\t\t{\n\t\t\t\t\t\tID: \"vuln-1\",\n\t\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tReference:              vulnerability.Reference{ID: \"vuln-1\"},\n\t\t\t\t\t\t\t\tRelatedVulnerabilities: []vulnerability.Reference{}, // empty\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tDetails: match.Details{{Type: match.ExactDirectMatch}},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tincoming: Set{\n\t\t\t\t\"vuln-2\": []Result{\n\t\t\t\t\t{\n\t\t\t\t\t\tID: \"vuln-2\",\n\t\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tReference: vulnerability.Reference{ID: \"vuln-2\"},\n\t\t\t\t\t\t\t\tRelatedVulnerabilities: []vulnerability.Reference{\n\t\t\t\t\t\t\t\t\t{ID: \"some-cve\"},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tDetails: match.Details{{Type: match.ExactDirectMatch}},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: Set{\n\t\t\t\t\"vuln-1\": []Result{\n\t\t\t\t\t{\n\t\t\t\t\t\tID: \"vuln-1\",\n\t\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tReference:              vulnerability.Reference{ID: \"vuln-1\"},\n\t\t\t\t\t\t\t\tRelatedVulnerabilities: []vulnerability.Reference{},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tDetails: match.Details{{Type: match.ExactDirectMatch}},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot := tt.receiver.Remove(tt.incoming)\n\t\t\tif diff := cmp.Diff(tt.want, got); diff != \"\" {\n\t\t\t\tt.Errorf(\"Set.Remove() mismatch (-want +got):\\n%s\", diff)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSet_Merge(t *testing.T) {\n\ttests := []struct {\n\t\tname       string\n\t\treceiver   Set\n\t\tincoming   Set\n\t\tmergeFuncs []func(existing, incoming []Result) []Result\n\t\twant       Set\n\t}{\n\t\t{\n\t\t\tname: \"merge with default merge function\",\n\t\t\treceiver: Set{\n\t\t\t\t\"vuln-1\": []Result{\n\t\t\t\t\t{\n\t\t\t\t\t\tID:              \"vuln-1\",\n\t\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{{Reference: vulnerability.Reference{ID: \"CVE-2021-1\"}}},\n\t\t\t\t\t\tDetails:         match.Details{{Type: match.ExactDirectMatch}},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tincoming: Set{\n\t\t\t\t\"vuln-1\": []Result{\n\t\t\t\t\t{\n\t\t\t\t\t\tID:              \"vuln-1\",\n\t\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{{Reference: vulnerability.Reference{ID: \"CVE-2021-1-updated\"}}},\n\t\t\t\t\t\tDetails:         match.Details{{Type: match.ExactIndirectMatch}},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: Set{\n\t\t\t\t\"vuln-1\": []Result{\n\t\t\t\t\t{\n\t\t\t\t\t\tID:              \"vuln-1\",\n\t\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{{Reference: vulnerability.Reference{ID: \"CVE-2021-1\"}}},\n\t\t\t\t\t\tDetails:         match.Details{{Type: match.ExactDirectMatch}},\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tID:              \"vuln-1\",\n\t\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{{Reference: vulnerability.Reference{ID: \"CVE-2021-1-updated\"}}},\n\t\t\t\t\t\tDetails:         match.Details{{Type: match.ExactIndirectMatch}},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"merge new entry from incoming\",\n\t\t\treceiver: Set{\n\t\t\t\t\"vuln-1\": []Result{\n\t\t\t\t\t{\n\t\t\t\t\t\tID:              \"vuln-1\",\n\t\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{{Reference: vulnerability.Reference{ID: \"CVE-2021-1\"}}},\n\t\t\t\t\t\tDetails:         match.Details{{Type: match.ExactDirectMatch}},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tincoming: Set{\n\t\t\t\t\"vuln-2\": []Result{\n\t\t\t\t\t{\n\t\t\t\t\t\tID:              \"vuln-2\",\n\t\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{{Reference: vulnerability.Reference{ID: \"CVE-2021-2\"}}},\n\t\t\t\t\t\tDetails:         match.Details{{Type: match.ExactDirectMatch}},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: Set{\n\t\t\t\t\"vuln-1\": []Result{\n\t\t\t\t\t{\n\t\t\t\t\t\tID:              \"vuln-1\",\n\t\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{{Reference: vulnerability.Reference{ID: \"CVE-2021-1\"}}},\n\t\t\t\t\t\tDetails:         match.Details{{Type: match.ExactDirectMatch}},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t\"vuln-2\": []Result{\n\t\t\t\t\t{\n\t\t\t\t\t\tID:              \"vuln-2\",\n\t\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{{Reference: vulnerability.Reference{ID: \"CVE-2021-2\"}}},\n\t\t\t\t\t\tDetails:         match.Details{{Type: match.ExactDirectMatch}},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"merge with custom merge function that filters out results\",\n\t\t\treceiver: Set{\n\t\t\t\t\"vuln-1\": []Result{\n\t\t\t\t\t{\n\t\t\t\t\t\tID:              \"vuln-1\",\n\t\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{{Reference: vulnerability.Reference{ID: \"CVE-2021-1\"}}},\n\t\t\t\t\t\tDetails:         match.Details{{Type: match.ExactDirectMatch}},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tincoming: Set{\n\t\t\t\t\"vuln-1\": []Result{\n\t\t\t\t\t{\n\t\t\t\t\t\tID:              \"vuln-1\",\n\t\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{{Reference: vulnerability.Reference{ID: \"CVE-2021-1-updated\"}}},\n\t\t\t\t\t\tDetails:         match.Details{{Type: match.ExactIndirectMatch}},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tmergeFuncs: []func(existing, incoming []Result) []Result{\n\t\t\t\tfunc(existing, incoming []Result) []Result {\n\t\t\t\t\t// custom merge function that returns empty result to filter out\n\t\t\t\t\treturn []Result{}\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: Set{},\n\t\t},\n\t\t{\n\t\t\tname:     \"merge empty sets\",\n\t\t\treceiver: Set{},\n\t\t\tincoming: Set{},\n\t\t\twant:     Set{},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot := tt.receiver.Merge(tt.incoming, tt.mergeFuncs...)\n\t\t\tif diff := cmp.Diff(tt.want, got); diff != \"\" {\n\t\t\t\tt.Errorf(\"Set.Merge() mismatch (-want +got):\\n%s\", diff)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSet_ToMatches(t *testing.T) {\n\ttestPkg := pkg.Package{\n\t\tName:    \"test-Package\",\n\t\tVersion: \"1.0.0\",\n\t\tType:    syftPkg.DebPkg,\n\t}\n\n\ttests := []struct {\n\t\tname     string\n\t\treceiver Set\n\t\twant     []match.Match\n\t}{\n\t\t{\n\t\t\tname: \"convert results to matches\",\n\t\t\treceiver: Set{\n\t\t\t\t\"vuln-1\": []Result{\n\t\t\t\t\t{\n\t\t\t\t\t\tID: \"vuln-1\",\n\t\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{\n\t\t\t\t\t\t\t{Reference: vulnerability.Reference{ID: \"CVE-2021-1\"}},\n\t\t\t\t\t\t\t{Reference: vulnerability.Reference{ID: \"CVE-2021-2\"}},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tDetails: match.Details{{Type: match.ExactDirectMatch}},\n\t\t\t\t\t\tPackage: &testPkg,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: []match.Match{\n\t\t\t\t{\n\t\t\t\t\tVulnerability: vulnerability.Vulnerability{Reference: vulnerability.Reference{ID: \"CVE-2021-1\"}},\n\t\t\t\t\tPackage:       testPkg,\n\t\t\t\t\tDetails:       match.Details{{Type: match.ExactDirectMatch}},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tVulnerability: vulnerability.Vulnerability{Reference: vulnerability.Reference{ID: \"CVE-2021-2\"}},\n\t\t\t\t\tPackage:       testPkg,\n\t\t\t\t\tDetails:       match.Details{{Type: match.ExactDirectMatch}},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"skip results with no vulnerabilities\",\n\t\t\treceiver: Set{\n\t\t\t\t\"vuln-1\": []Result{\n\t\t\t\t\t{\n\t\t\t\t\t\tID:              \"vuln-1\",\n\t\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{},\n\t\t\t\t\t\tDetails:         match.Details{{Type: match.ExactDirectMatch}},\n\t\t\t\t\t\tPackage:         &testPkg,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t\"vuln-2\": []Result{\n\t\t\t\t\t{\n\t\t\t\t\t\tID: \"vuln-2\",\n\t\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{\n\t\t\t\t\t\t\t{Reference: vulnerability.Reference{ID: \"CVE-2021-2\"}},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tDetails: match.Details{{Type: match.ExactDirectMatch}},\n\t\t\t\t\t\tPackage: &testPkg,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: []match.Match{\n\t\t\t\t{\n\t\t\t\t\tVulnerability: vulnerability.Vulnerability{Reference: vulnerability.Reference{ID: \"CVE-2021-2\"}},\n\t\t\t\t\tPackage:       testPkg,\n\t\t\t\t\tDetails:       match.Details{{Type: match.ExactDirectMatch}},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:     \"empty set returns no matches\",\n\t\t\treceiver: Set{},\n\t\t\twant:     []match.Match{},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot := tt.receiver.ToMatches()\n\t\t\topts := cmp.Options{\n\t\t\t\tcmpopts.IgnoreUnexported(file.LocationSet{}),\n\t\t\t\tcmpopts.EquateEmpty(),\n\t\t\t}\n\t\t\tif diff := cmp.Diff(tt.want, got, opts...); diff != \"\" {\n\t\t\t\tt.Errorf(\"Set.ToMatches() mismatch (-want +got):\\n%s\", diff)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSet_Filter(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\treceiver Set\n\t\tcriteria []vulnerability.Criteria\n\t\twant     Set\n\t\twantErr  require.ErrorAssertionFunc\n\t}{\n\t\t{\n\t\t\tname: \"filter vulnerabilities with matching criteria\",\n\t\t\treceiver: Set{\n\t\t\t\t\"vuln-1\": []Result{\n\t\t\t\t\t{\n\t\t\t\t\t\tID: \"vuln-1\",\n\t\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tReference:   vulnerability.Reference{ID: \"CVE-2021-1\"},\n\t\t\t\t\t\t\t\tPackageName: \"test-Package\",\n\t\t\t\t\t\t\t\tConstraint:  version.MustGetConstraint(\"< 2.0.0\", version.SemanticFormat),\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tReference:   vulnerability.Reference{ID: \"CVE-2021-2\"},\n\t\t\t\t\t\t\t\tPackageName: \"other-Package\",\n\t\t\t\t\t\t\t\tConstraint:  version.MustGetConstraint(\"< 1.0.0\", version.SemanticFormat),\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tDetails: match.Details{{Type: match.ExactDirectMatch}},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tcriteria: []vulnerability.Criteria{\n\t\t\t\tsearch.ByPackageName(\"test-Package\"),\n\t\t\t},\n\t\t\twant: Set{\n\t\t\t\t\"vuln-1\": []Result{\n\t\t\t\t\t{\n\t\t\t\t\t\tID: \"vuln-1\",\n\t\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tReference:   vulnerability.Reference{ID: \"CVE-2021-1\"},\n\t\t\t\t\t\t\t\tPackageName: \"test-Package\",\n\t\t\t\t\t\t\t\tConstraint:  version.MustGetConstraint(\"< 2.0.0\", version.SemanticFormat),\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tDetails: match.Details{{Type: match.ExactDirectMatch}},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"filter out all vulnerabilities removes result\",\n\t\t\treceiver: Set{\n\t\t\t\t\"vuln-1\": []Result{\n\t\t\t\t\t{\n\t\t\t\t\t\tID: \"vuln-1\",\n\t\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tReference:   vulnerability.Reference{ID: \"CVE-2021-1\"},\n\t\t\t\t\t\t\t\tPackageName: \"other-Package\",\n\t\t\t\t\t\t\t\tConstraint:  version.MustGetConstraint(\"< 2.0.0\", version.SemanticFormat),\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tDetails: match.Details{{Type: match.ExactDirectMatch}},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tcriteria: []vulnerability.Criteria{\n\t\t\t\tsearch.ByPackageName(\"test-Package\"),\n\t\t\t},\n\t\t\twant: Set{},\n\t\t},\n\t\t{\n\t\t\tname:     \"filter empty set\",\n\t\t\treceiver: Set{},\n\t\t\tcriteria: []vulnerability.Criteria{\n\t\t\t\tsearch.ByPackageName(\"test-Package\"),\n\t\t\t},\n\t\t\twant: Set{},\n\t\t},\n\t\t{\n\t\t\tname: \"filter with no criteria returns original set\",\n\t\t\treceiver: Set{\n\t\t\t\t\"vuln-1\": []Result{\n\t\t\t\t\t{\n\t\t\t\t\t\tID: \"vuln-1\",\n\t\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tReference:   vulnerability.Reference{ID: \"CVE-2021-1\"},\n\t\t\t\t\t\t\t\tPackageName: \"test-Package\",\n\t\t\t\t\t\t\t\tConstraint:  version.MustGetConstraint(\"< 2.0.0\", version.SemanticFormat),\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tDetails: match.Details{{Type: match.ExactDirectMatch}},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tcriteria: []vulnerability.Criteria{},\n\t\t\twant: Set{\n\t\t\t\t\"vuln-1\": []Result{\n\t\t\t\t\t{\n\t\t\t\t\t\tID: \"vuln-1\",\n\t\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tReference:   vulnerability.Reference{ID: \"CVE-2021-1\"},\n\t\t\t\t\t\t\t\tPackageName: \"test-Package\",\n\t\t\t\t\t\t\t\tConstraint:  version.MustGetConstraint(\"< 2.0.0\", version.SemanticFormat),\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tDetails: match.Details{{Type: match.ExactDirectMatch}},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif tt.wantErr == nil {\n\t\t\t\ttt.wantErr = require.NoError\n\t\t\t}\n\n\t\t\tgot := tt.receiver.Filter(tt.criteria...)\n\n\t\t\topts := cmp.Options{\n\t\t\t\tcmpopts.IgnoreUnexported(file.LocationSet{}),\n\t\t\t\tcmpopts.IgnoreFields(vulnerability.Vulnerability{}, \"Constraint\"),\n\t\t\t\tcmpopts.EquateEmpty(),\n\t\t\t}\n\t\t\tif diff := cmp.Diff(tt.want, got, opts...); diff != \"\" {\n\t\t\t\tt.Errorf(\"Set.Filter() mismatch (-want +got):\\n%s\", diff)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSet_Contains(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\treceiver Set\n\t\tid       string\n\t\twant     bool\n\t}{\n\t\t{\n\t\t\tname: \"contains existing ID\",\n\t\t\treceiver: Set{\n\t\t\t\t\"vuln-1\": []Result{\n\t\t\t\t\t{\n\t\t\t\t\t\tID:              \"vuln-1\",\n\t\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{{Reference: vulnerability.Reference{ID: \"CVE-2021-1\"}}},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t\"vuln-2\": []Result{\n\t\t\t\t\t{\n\t\t\t\t\t\tID:              \"vuln-2\",\n\t\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{{Reference: vulnerability.Reference{ID: \"CVE-2021-2\"}}},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tid:   \"vuln-1\",\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"does not contain non-existing ID\",\n\t\t\treceiver: Set{\n\t\t\t\t\"vuln-1\": []Result{\n\t\t\t\t\t{\n\t\t\t\t\t\tID:              \"vuln-1\",\n\t\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{{Reference: vulnerability.Reference{ID: \"CVE-2021-1\"}}},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tid:   \"vuln-2\",\n\t\t\twant: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"empty set does not contain any ID\",\n\t\t\treceiver: Set{},\n\t\t\tid:       \"vuln-1\",\n\t\t\twant:     false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot := tt.receiver.Contains(tt.id)\n\t\t\trequire.Equal(t, tt.want, got)\n\t\t})\n\t}\n}\n\nfunc TestSet_Update(t *testing.T) {\n\t// Simple update function that replaces the Status field\n\treplaceStatus := func(existing *Result, incoming Result) {\n\t\tfor i := range existing.Vulnerabilities {\n\t\t\tfor _, incomingVuln := range incoming.Vulnerabilities {\n\t\t\t\texisting.Vulnerabilities[i].Status = incomingVuln.Status\n\t\t\t}\n\t\t}\n\t}\n\n\ttests := []struct {\n\t\tname     string\n\t\tbase     Set\n\t\tincoming Set\n\t\twant     Set\n\t}{\n\t\t{\n\t\t\tname: \"update by exact ID match\",\n\t\t\tbase: Set{\n\t\t\t\t\"CVE-2023-1234\": []Result{\n\t\t\t\t\t{\n\t\t\t\t\t\tID: \"CVE-2023-1234\",\n\t\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tReference: vulnerability.Reference{ID: \"CVE-2023-1234\"},\n\t\t\t\t\t\t\t\tStatus:    \"original\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tincoming: Set{\n\t\t\t\t\"CVE-2023-1234\": []Result{\n\t\t\t\t\t{\n\t\t\t\t\t\tID: \"CVE-2023-1234\",\n\t\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tReference: vulnerability.Reference{ID: \"CVE-2023-1234\"},\n\t\t\t\t\t\t\t\tStatus:    \"updated\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: Set{\n\t\t\t\t\"CVE-2023-1234\": []Result{\n\t\t\t\t\t{\n\t\t\t\t\t\tID: \"CVE-2023-1234\",\n\t\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tReference: vulnerability.Reference{ID: \"CVE-2023-1234\"},\n\t\t\t\t\t\t\t\tStatus:    \"updated\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"update by alias - identities overlap\",\n\t\t\tbase: Set{\n\t\t\t\t\"CVE-2023-1234\": []Result{\n\t\t\t\t\t{\n\t\t\t\t\t\tID: \"CVE-2023-1234\",\n\t\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tReference: vulnerability.Reference{ID: \"CVE-2023-1234\"},\n\t\t\t\t\t\t\t\tStatus:    \"original\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tincoming: Set{\n\t\t\t\t\"VULN-2023-001\": []Result{\n\t\t\t\t\t{\n\t\t\t\t\t\tID: \"VULN-2023-001\",\n\t\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tReference: vulnerability.Reference{ID: \"VULN-2023-001\"},\n\t\t\t\t\t\t\t\tRelatedVulnerabilities: []vulnerability.Reference{\n\t\t\t\t\t\t\t\t\t{ID: \"CVE-2023-1234\"},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tStatus: \"updated via alias\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: Set{\n\t\t\t\t\"CVE-2023-1234\": []Result{\n\t\t\t\t\t{\n\t\t\t\t\t\tID: \"CVE-2023-1234\",\n\t\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tReference: vulnerability.Reference{ID: \"CVE-2023-1234\"},\n\t\t\t\t\t\t\t\tStatus:    \"updated via alias\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"no match - base unchanged\",\n\t\t\tbase: Set{\n\t\t\t\t\"CVE-2023-1234\": []Result{\n\t\t\t\t\t{\n\t\t\t\t\t\tID: \"CVE-2023-1234\",\n\t\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tReference: vulnerability.Reference{ID: \"CVE-2023-1234\"},\n\t\t\t\t\t\t\t\tStatus:    \"original\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tincoming: Set{\n\t\t\t\t\"CVE-2023-9999\": []Result{\n\t\t\t\t\t{\n\t\t\t\t\t\tID: \"CVE-2023-9999\",\n\t\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tReference: vulnerability.Reference{ID: \"CVE-2023-9999\"},\n\t\t\t\t\t\t\t\tStatus:    \"different\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: Set{\n\t\t\t\t\"CVE-2023-1234\": []Result{\n\t\t\t\t\t{\n\t\t\t\t\t\tID: \"CVE-2023-1234\",\n\t\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tReference: vulnerability.Reference{ID: \"CVE-2023-1234\"},\n\t\t\t\t\t\t\t\tStatus:    \"original\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"empty incoming set - no updates\",\n\t\t\tbase: Set{\n\t\t\t\t\"CVE-2023-1234\": []Result{\n\t\t\t\t\t{\n\t\t\t\t\t\tID: \"CVE-2023-1234\",\n\t\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tReference: vulnerability.Reference{ID: \"CVE-2023-1234\"},\n\t\t\t\t\t\t\t\tStatus:    \"original\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tincoming: Set{},\n\t\t\twant: Set{\n\t\t\t\t\"CVE-2023-1234\": []Result{\n\t\t\t\t\t{\n\t\t\t\t\t\tID: \"CVE-2023-1234\",\n\t\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tReference: vulnerability.Reference{ID: \"CVE-2023-1234\"},\n\t\t\t\t\t\t\t\tStatus:    \"original\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"empty base set returns empty\",\n\t\t\tbase: Set{},\n\t\t\tincoming: Set{\n\t\t\t\t\"CVE-2023-1234\": []Result{\n\t\t\t\t\t{\n\t\t\t\t\t\tID: \"CVE-2023-1234\",\n\t\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tReference: vulnerability.Reference{ID: \"CVE-2023-1234\"},\n\t\t\t\t\t\t\t\tStatus:    \"some status\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: Set{},\n\t\t},\n\t\t{\n\t\t\tname: \"preserves Details field from base\",\n\t\t\tbase: Set{\n\t\t\t\t\"CVE-2023-1234\": []Result{\n\t\t\t\t\t{\n\t\t\t\t\t\tID: \"CVE-2023-1234\",\n\t\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tReference: vulnerability.Reference{ID: \"CVE-2023-1234\"},\n\t\t\t\t\t\t\t\tStatus:    \"original\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tDetails: match.Details{\n\t\t\t\t\t\t\t{Type: match.ExactDirectMatch, SearchedBy: map[string]interface{}{\"key\": \"value\"}},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tincoming: Set{\n\t\t\t\t\"CVE-2023-1234\": []Result{\n\t\t\t\t\t{\n\t\t\t\t\t\tID: \"CVE-2023-1234\",\n\t\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tReference: vulnerability.Reference{ID: \"CVE-2023-1234\"},\n\t\t\t\t\t\t\t\tStatus:    \"updated\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tDetails: match.Details{\n\t\t\t\t\t\t\t{Type: match.ExactIndirectMatch},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: Set{\n\t\t\t\t\"CVE-2023-1234\": []Result{\n\t\t\t\t\t{\n\t\t\t\t\t\tID: \"CVE-2023-1234\",\n\t\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tReference: vulnerability.Reference{ID: \"CVE-2023-1234\"},\n\t\t\t\t\t\t\t\tStatus:    \"updated\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tDetails: match.Details{\n\t\t\t\t\t\t\t{Type: match.ExactDirectMatch, SearchedBy: map[string]interface{}{\"key\": \"value\"}},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot := tt.base.Update(tt.incoming, IdentitiesOverlap, replaceStatus)\n\t\t\tif diff := cmp.Diff(tt.want, got); diff != \"\" {\n\t\t\t\tt.Errorf(\"Set.Update() mismatch (-want +got):\\n%s\", diff)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/matcher/internal/utils_test.go",
    "content": "package internal\n\nimport (\n\t\"testing\"\n\n\t\"github.com/go-test/deep\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/grype/grype/match\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n)\n\nfunc assertMatchesUsingIDsForVulnerabilities(t testing.TB, expected, actual []match.Match) {\n\tt.Helper()\n\trequire.Len(t, actual, len(expected))\n\tfor idx, a := range actual {\n\t\t// only compare the vulnerability ID, nothing else\n\t\ta.Vulnerability = vulnerability.Vulnerability{Reference: vulnerability.Reference{ID: a.Vulnerability.ID}}\n\t\tfor _, d := range deep.Equal(expected[idx], a) {\n\t\t\tt.Errorf(\"diff idx=%d: %+v\", idx, d)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "grype/matcher/java/matcher.go",
    "content": "package java\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/anchore/grype/grype/match\"\n\t\"github.com/anchore/grype/grype/matcher/internal\"\n\t\"github.com/anchore/grype/grype/pkg\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n\t\"github.com/anchore/grype/internal/log\"\n\tsyftPkg \"github.com/anchore/syft/syft/pkg\"\n)\n\nconst (\n\tsha1Query = `1:\"%s\"`\n)\n\ntype Matcher struct {\n\tMavenSearcher\n\tcfg MatcherConfig\n}\n\ntype ExternalSearchConfig struct {\n\tSearchMavenUpstream bool\n\tMavenBaseURL        string\n\tMavenRateLimit      time.Duration\n}\n\ntype MatcherConfig struct {\n\tExternalSearchConfig\n\tUseCPEs bool\n}\n\nfunc NewJavaMatcher(cfg MatcherConfig) *Matcher {\n\treturn &Matcher{\n\t\tcfg:           cfg,\n\t\tMavenSearcher: newMavenSearch(http.DefaultClient, cfg.MavenBaseURL, cfg.MavenRateLimit),\n\t}\n}\n\nfunc (m *Matcher) PackageTypes() []syftPkg.Type {\n\treturn []syftPkg.Type{syftPkg.JavaPkg, syftPkg.JenkinsPluginPkg}\n}\n\nfunc (m *Matcher) Type() match.MatcherType {\n\treturn match.JavaMatcher\n}\n\nfunc (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoreFilter, error) {\n\tvar matches []match.Match\n\n\tif m.cfg.SearchMavenUpstream {\n\t\tupstreamMatches, err := m.matchUpstreamMavenPackages(store, p)\n\t\tif err != nil {\n\t\t\tif strings.Contains(err.Error(), \"no artifact found\") {\n\t\t\t\tlog.Debugf(\"no upstream maven artifact found for %s\", p.Name)\n\t\t\t} else {\n\t\t\t\treturn nil, nil, match.NewFatalError(match.JavaMatcher, fmt.Errorf(\"resolving details for package %q with maven: %w\", p.Name, err))\n\t\t\t}\n\t\t} else {\n\t\t\tmatches = append(matches, upstreamMatches...)\n\t\t}\n\t}\n\n\tcriteriaMatches, ignores, err := internal.MatchPackageByEcosystemAndCPEs(store, p, m.Type(), m.cfg.UseCPEs)\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"failed to match by exact package: %w\", err)\n\t}\n\n\tmatches = append(matches, criteriaMatches...)\n\n\treturn matches, ignores, nil\n}\n\nfunc (m *Matcher) matchUpstreamMavenPackages(store vulnerability.Provider, p pkg.Package) ([]match.Match, error) {\n\tvar matches []match.Match\n\n\tctx := context.Background()\n\n\t// Check if we need to search Maven by SHA\n\tsearchMaven, digests := m.shouldSearchMavenBySha(p)\n\tif searchMaven {\n\t\t// If the artifact and group ID exist are missing, attempt Maven lookup using SHA-1\n\t\tfor _, digest := range digests {\n\t\t\tlog.Debugf(\"searching maven, POM data missing for %s\", p.Name)\n\t\t\tindirectPackage, err := m.GetMavenPackageBySha(ctx, digest)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tindirectMatches, _, err := internal.MatchPackageByLanguage(store, *indirectPackage, m.Type())\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tmatches = append(matches, indirectMatches...)\n\t\t}\n\t} else {\n\t\tlog.Debugf(\"skipping maven search, POM data present for %s\", p.Name)\n\t\tindirectMatches, _, err := internal.MatchPackageByLanguage(store, p, m.Type())\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tmatches = append(matches, indirectMatches...)\n\t}\n\n\tmatch.ConvertToIndirectMatches(matches, p)\n\n\treturn matches, nil\n}\n\nfunc (m *Matcher) shouldSearchMavenBySha(p pkg.Package) (bool, []string) {\n\tdigests := []string{}\n\n\tif metadata, ok := p.Metadata.(pkg.JavaMetadata); ok {\n\t\t// if either the PomArtifactID or PomGroupID is missing, we need to search Maven\n\t\tif metadata.PomArtifactID == \"\" || metadata.PomGroupID == \"\" {\n\t\t\tfor _, digest := range metadata.ArchiveDigests {\n\t\t\t\tif digest.Algorithm == \"sha1\" && digest.Value != \"\" {\n\t\t\t\t\tdigests = append(digests, digest.Value)\n\t\t\t\t}\n\t\t\t}\n\t\t\t// if we need to search Maven but no valid SHA-1 digests exist, skip search\n\t\t\tif len(digests) == 0 {\n\t\t\t\treturn false, digests\n\t\t\t}\n\t\t}\n\t}\n\n\treturn len(digests) > 0, digests\n}\n"
  },
  {
    "path": "grype/matcher/java/matcher_integration_test.go",
    "content": "//go:build api_limits\n\npackage java\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n)\n\n// TestMavenSearch_GetMavenPackageBySha tests the GetMavenPackageBySha method of the MavenSearch struct.\n// This is an integration test and requires network access to search.maven.org.\n// It is not intended to be run as part of the normal test suite.\n// Use this to validate rate limiting in [maven_search.go] and the ability to fetch package data from maven.org.\nfunc TestMavenSearch_GetMavenPackageBySha(t *testing.T) {\n\tctx := context.Background()\n\n\tms := newMavenSearch(http.DefaultClient, \"https://search.maven.org/solrsearch/select\")\n\n\t// Known SHA1s to test with, using a large number of known good SHA1s to validate rate limiting\n\t// This is not typical but for Images with a large number of Java packages, this is a good test\n\t// to ensure that the rate limiting is working as expected and we don't silently fail and loose scan results\n\tshas := []string{\n\t\t\"bb7b7ec0379982b97c62cd17465cb6d9155f68e8\",\n\t\t\"b45b49c1ec5c5fc48580412d0ca635e1833110ea\",\n\t\t\"245ceca7bdf3190fbb977045c852d5f3c8efece1\",\n\t\t\"485de3a253e23f645037828c07f1d7f1af40763a\",\n\t\t\"97662c999c6b2fbf2ee50e814a34639c1c1d22de\",\n\t\t\"21608dd8b3853da69c4862fbaf9b35b326dc0ddc\",\n\t\t\"a9cd24fe92272ad1f084d98cd7edeffcd9de720f\",\n\t\t\"eab9a4baae8de96a24c04219236363d0ca73e8a9\",\n\t\t\"3647d00620a91360990c9680f29fbcc22d69c2ee\",\n\t\t\"b957089deb654647da320ad7507b0a4b5ce23813\",\n\t\t\"bd0cd7ad1e3791a8a0929df0dcdbffc02fd0bab4\",\n\t\t\"0d1efd839d539481952a9757834054239774f057\",\n\t\t\"f6148c941e4ec2f314b285e6e4e995f61374aa2f\",\n\t\t\"502008366a98296ce95c62397b1cb7e06521a195\",\n\t\t\"92b2a5b7fb0c6a8dcd839d98af2e186f1e98b8ca\",\n\t\t\"64e6d9608f30eefbe807e65c148018065f971ca6\",\n\t\t\"095454c18fb12f8fcdbeae4747adfa29bfe6bf17\",\n\t\t\"0322a158f88b2a18b429133d91459dfa38bf9f55\",\n\t\t\"f18ebbe9a3145b9ce99733f5a0b7d505be9ae71e\",\n\t\t\"526df0db4c22be3eb490dab2b4ef979032e3588d\",\n\t\t\"521694be357010738e7bc612089df8fcc970a0d5\",\n\t\t\"50d87efaed036c7df71f766ca13aa8783a774ce9\",\n\t\t\"e8b2cbfe10d9cdcdc29961943b1c6c40f42e2f32\",\n\t\t\"3c0daebd5f0e1ce72cc50c818321ac957aeb5d70\",\n\t\t\"919f0dfe192fb4e063e7dacadee7f8bb9a2672a9\",\n\t\t\"8ceead41f4e71821919dbdb7a9847608f1a938cb\",\n\t\t\"a1678ba907bf92691d879fef34e1a187038f9259\",\n\t\t\"83cd2cd674a217ade95a4bb83a8a14f351f48bd0\",\n\t\t\"6b0acabea7bb3da058200a77178057e47e25cb69\",\n\t\t\"31c746001016c6226bd7356c9f87a6a084ce3715\",\n\t\t\"cd9cd41361c155f3af0f653009dcecb08d8b4afd\",\n\t\t\"2609e36f18f7e8d593cc1cddfb2ac776dc96b8e0\",\n\t\t\"0235ba8b489512805ac13a8f9ea77a1ca5ebe3e8\",\n\t\t\"ca773f9985c9f4104d76028629026c69c641923c\",\n\t\t\"a231e0d844d2721b0fa1b238006d15c6ded6842a\",\n\t\t\"8e6300ef51c1d801a7ed62d07cd221aca3a90640\",\n\t\t\"379e0250f7a4a42c66c5e94e14d4c4491b3c2ed3\",\n\t\t\"4b071f211b37c38e0e9f5998550197c8593f6ad8\",\n\t\t\"1f2a432d1212f5c352ae607d7b61dcae20c20af5\",\n\t\t\"a3662cf1c1d592893ffe08727f78db35392fa302\",\n\t\t\"78d2ecd61318b5a58cd04fb237636c0e86b77d97\",\n\t\t\"5b0b0f8cdb6c90582302ffcf5c20447206122f48\",\n\t\t\"0d8b504da88975fdc149ed60d551d637d0992aa1\",\n\t\t\"507505543772f54342d6ee855fa8f459d4bc6a11\",\n\t\t\"71abe1781fa182d92e97bf60450026cc72984ac2\",\n\t\t\"e0efa60318229590103e31c69ebdaae56d903644\",\n\t\t\"8ad1147dcd02196e3924013679c6bf4c25d8c351\",\n\t\t\"9679de8286eb0a151db6538ba297a8951c4a1224\",\n\t\t\"73b9a0e7032a5ae89f294091bc6cbb9a67a21101\",\n\t\t\"152f846d9f30a3e026530c2087ecd65c39bb304b\",\n\t\t\"73de3b1233c1da8fd46f9a4bd8ebec97890af9dc\",\n\t\t\"25d54640c4a17aa342490c4c63c172759361bf56\",\n\t\t\"eca76e00f897461f95bbb085f67936417ae03825\",\n\t\t\"802b5b3de0a38e71f07aa3048f532cd1246bc5af\",\n\t\t\"10d40ab670bf1fa53c925462f84f43507cf3b9bc\",\n\t\t\"2a14a2ff74f6ec3546b257889949630d3b2a0dbb\",\n\t\t\"357efe3f93c58bc4a10d40b1301045405b8a9f73\",\n\t\t\"570430f532b1e98c5d72a759ccbe7851099cee5f\",\n\t\t\"3174a146b81819fe2cd42e23081cd902ac743a8d\",\n\t\t\"940873068ea1383f4d962613cc1eca7c8cecc00e\",\n\t\t\"2116ab332c0bedfd038ad9d39c2e17219abf34aa\",\n\t\t\"527f9c5ccc6b76ad6e88ca571272a6a2ea535921\",\n\t\t\"04d21d5e6b71b2634dc67b36bf9b2defce7a7cc3\",\n\t\t\"37a5a4660941852c298e4caf4592b46b98ce512c\",\n\t\t\"780be6395b7c65d8d90ca2e1c3c2a46c46c5a154\",\n\t\t\"6251d68d3039f7b215b205f0e61cb2d732e5bc9b\",\n\t\t\"1d7efb089db2fe7a60526b8ff50b0c681fe1b079\",\n\t\t\"1f21cea72f54a6af3b0bb6831eb3874bd4afd213\",\n\t\t\"cd58e9e1b3ece090edd60a072f66b6cf52bce06d\",\n\t\t\"fcfd07e6ad0b5eadb0af1bddcc7b04097dacad7c\",\n\t\t\"e6fdf0f32f49d2a2380f5b458469052c272f8d9b\",\n\t\t\"324669468c32535f19bc4791fcaa34f2ed82200a\",\n\t\t\"ba584703bd47e9e789343ee3332f0f5a64f7f187\",\n\t\t\"17b3541f736df97465f87d9f5b5dfa4991b37bb3\",\n\t\t\"39e9e45359e20998eb79c1828751f94a818d25f8\",\n\t\t\"5353ca39fe2f148dab9ca1d637a43d0750456254\",\n\t\t\"603d37b2a108e2b437bb9b3b2ffb5962b4aa198c\",\n\t\t\"6000774d7f8412ced005a704188ced78beeed2bb\",\n\t\t\"537a3281dfefbd7939d27785732a2aafddd3abcb\",\n\t\t\"92446d8dfc8e57289e6120a7efc6932650ed3410\",\n\t\t\"eacefc2460e0ac5fe2ad48a9b0ffced5aea451b9\",\n\t\t\"4314021484adf9b32b3ae5421fac6fe0ed56e53e\",\n\t\t\"5786699a0cb71f9dc32e6cca1d665eef07a0882f\",\n\t\t\"2bd4f1921c78c2adffbe2eb01117c7936d0a0789\",\n\t\t\"de2b60b62da487644fc11f734e73c8b0b431238f\",\n\t\t\"e752540aeccb620f23c1e2f15c4c707254f6f596\",\n\t\t\"638ec33f363a94d41a4f03c3e7d3dcfba64e402d\",\n\t\t\"3fe0bed568c62df5e89f4f174c101eab25345b6c\",\n\t\t\"17773f342aabf0b177c9e3b8d8396d851cbfe64e\",\n\t\t\"1ae01f9be1cabf50ee735383a9fc3342e778c17e\",\n\t\t\"bf76d02e2be0dd8f99f106658ea7cacfa8df69d1\",\n\t\t\"f82b463a5c9eadb2a6667a1cb51b46d8d8d8d69b\",\n\t\t\"073e532b7cf87928bcd2512a0faf1151f8bd199a\",\n\t\t\"0912e12e4c7dc1c87ea8574065725a63342cf19d\",\n\t\t\"d52b9abcd97f38c81342bb7e7ae1eee9b73cba51\",\n\t\t\"dc98be5d5390230684a092589d70ea76a147925c\",\n\t\t\"47bd4d333fba53406f6c6c51884ddbca435c8862\",\n\t\t\"8ad72fe39fa8c91eaaf12aadb21e0c3661fe26d5\",\n\t\t\"54ebea0a5b653d3c680131e73fe807bb8f78c4ed\",\n\t\t\"19d5bfd402f91de0e670ef5783bf5c0a3f5ab478\",\n\t\t\"659feffdd12280201c8aacb8f7be94f9a883c824\",\n\t\t\"2b681b3bcddeaa5bf5c2a2939cd77e2f9ad6efda\",\n\t\t\"30be73c965cc990b153a100aaaaafcf239f82d39\",\n\t\t\"dc887691eab129c5728e26b095751fcadd36719d\",\n\t\t\"ddcc8433eb019fb48fe25207c0278143f3e1d7e2\",\n\t\t\"0ce1edb914c94ebc388f086c6827e8bdeec71ac2\",\n\t\t\"c6842c86792ff03b9f1d1fe2aab8dc23aa6c6f0e\",\n\t\t\"5043bfebc3db072ed80fbd362e7caf00e885d8ae\",\n\t\t\"f6f66e966c70a83ffbdb6f17a0919eaf7c8aca7f\",\n\t\t\"e4ba98f1d4b3c80ec46392f25e094a6a2e58fcbf\",\n\t\t\"4572d589699f09d866a226a14b7f4323c6d8f040\",\n\t\t\"bd1a6e384f3cf0f9b9a60e1e6c1c1ecbbee7e0b7\",\n\t\t\"3363381aef8cef2dbc1023b3e3a9433b08b64e01\",\n\t\t\"3833ca68f9f42fd11d4e0a036e9a3faae5d5f1a8\",\n\t\t\"4316d710b6619ffe210c98deb2b0893587dad454\",\n\t\t\"c22383d089321fd0c58a15c1c6ef5d24b5b5ee0c\",\n\t\t\"d858f142ea189c62771c505a6548d8606ac098fe\",\n\t\t\"66d618739859bc75ab9643b96a9839ac7802ec90\",\n\t\t\"e3aa0be212d7a42839a8f3f506f5b990bcce0222\",\n\t\t\"d25497d443d0843dbf2973e802c06722f2cb4578\",\n\t\t\"db2d83bdc0bac7b4f25fc113d8ce3eedc0a4e89c\",\n\t\t\"b706a216e49352103bd2527e83b1ec2410924494\",\n\t\t\"4aa0cfb129c36cd91528fc1b8775705280e60285\",\n\t\t\"4e1cce64b1ec11080a01172a0c296431d9469294\",\n\t\t\"e3fdd7fa9255bba0a206aea059cf133565c48cbd\",\n\t\t\"f0f717ed3495ed2e58d96e0084f73db0c7b3ba3d\",\n\t\t\"a79cf96a15f4b5376fae0024c0b0cd44cfa8a295\",\n\t\t\"49c3df840c2268479fb8f5cfd7df023bd6927bc9\",\n\t\t\"9d9d56fcae37f1b3d48d80f8b7eefabd3477569d\",\n\t\t\"09bfca4ee4f691f3737b3f4f006d0c4770f178eb\",\n\t\t\"0a1ed0a251d22bf528cebfafb94c55e6f3f339cf\",\n\t\t\"bc5b0c72a3755de7f3dca9f059aa19cc9d27a843\",\n\t\t\"a096cfeb58b927dde6b80ad295e564513514f9be\",\n\t\t\"b1e952300954b6d33911ba29a984455fcc3f1024\",\n\t\t\"5774f912db3dca1e9049af15cce6a4f7845a173d\",\n\t\t\"a2f8cf63192ebba929451a221cc382bc0ca5abb7\",\n\t\t\"13e3663d5878001666981eb5ef6efb22fa6799bb\",\n\t\t\"9697b9e1667b4f2daa9ea454b4a0e0f905585c8b\",\n\t\t\"32088dfde15a3f8ad4f2547cb083777afddc12d5\",\n\t\t\"fabcda911ebc80e3a9b6064863da4f2e5094814f\",\n\t\t\"2c3591cf5e2f5de644aae09a73a896f0c7964f43\",\n\t\t\"bce88f90c3341ed14df2ce3919f253334cd834f2\",\n\t\t\"df7bbc5a4c8304aa8aed34cb67e339035ac2c34b\",\n\t\t\"c305f6229dde8f3946de5574ac9779309073f2e3\",\n\t\t\"ad63993db3525be5e290e0ccb3d5122c01bd356d\",\n\t\t\"24b20d4f91c894e19947389d3040adfc174a6af1\",\n\t\t\"58c3d2641b48a9db2e29009f42077dcd70f7e351\",\n\t\t\"8450fb3261e7ec1d734c2b11ca4d875fe82386eb\",\n\t\t\"9a296a2da46d296f3d0b78d3941ec468c64ba3e6\",\n\t\t\"bd7b0f03050125e8dd8bd9498e34561e1e88db03\",\n\t\t\"b6104ad646d672770561918073f1aaacb7c7b341\",\n\t\t\"8c177eb55da21bee1cd654d66241b98fb0e44c86\",\n\t\t\"1c45fbaa5f4d66070b7f1ee5e4653aadb14aa97d\",\n\t\t\"0ed231cd84006f5fdfda7671beae2b9b41a2dafa\",\n\t\t\"8ed4fee000f82e6248f7f8cfdd11d53fe03f98ad\",\n\t\t\"302ebf7b124c9a037333a9b81a5f2ce0880f8a29\",\n\t\t\"eba91bffe866a695d145c5e1692509f92de5b23b\",\n\t\t\"5a1f4a878b75dbcfbf0d4ae783bf1c1229309470\",\n\t\t\"5351a31139b9b5e3f8d50252ac081249b1ad00fb\",\n\t\t\"fdc6f7632078dc5b570f9120d9ab07892e784554\",\n\t\t\"4a0126da8cf7794e913a13e3f8f4ab62ca5e2981\",\n\t\t\"4a3df17312a2ab95a4d75396065079aebfb2a1e7\",\n\t\t\"51fac22c802ae94247664efd95a1e60d138a278d\",\n\t\t\"c6dd14eb5a4abfcf1c8dbc7187c2ec3b8d9be1f9\",\n\t\t\"96d70f8f82a534438b938f86b3a6682eb34824ca\",\n\t\t\"e714165da098686f600d75b914448fdd4a057d60\",\n\t\t\"60ba0670d68758e893870079916954a7f01afe23\",\n\t\t\"320e7d1fdbab2bffb8138d66c24724cc24ea654c\",\n\t\t\"404840df034905ae2b5a9c922639e1d9f694516d\",\n\t\t\"63f0c49628c9695704d0014409f030d82bc10f70\",\n\t\t\"68186dc73e3d123999ebb93a6d5a5d0bbb4d4e91\",\n\t\t\"9ba2ed9f74f5122f25113cd6d5e14fbc442c867f\",\n\t\t\"993a5608c4942b5b81a6c14fd78779d024e6ed41\",\n\t\t\"f07ec0309a1e37629f097408e1cb75f7d0ea58c5\",\n\t\t\"1ffcac9e1bbd3d00db1e2089d8e915f20c0ac568\",\n\t\t\"9450776e99a5a1b413b98cb095f6fe7f81935c3d\",\n\t\t\"c72b35c5dea306de35ad0ff207eff4d14b37b880\",\n\t\t\"5e4e7abcdb8f4101b9aa0ba84658be21c445b1d5\",\n\t\t\"ef74ce50e19736bf72341a572c1ad6fd2ba6c3fe\",\n\t\t\"55a266187baa9d1c68447ff6ab404a4324de7935\",\n\t\t\"8a65a223354726586d95f45aa8f6175ca23b784c\",\n\t\t\"2ddd12523600e8b80d2be0bc003cd447bd2751d4\",\n\t\t\"19160c71c598866e9c96af667045c886c8dc9b48\",\n\t\t\"163372f10bf5f028ccbb122eebc9cd2deb30b094\",\n\t\t\"566ab030e0a0f010dfe0d185b0804b53817db7ec\",\n\t\t\"0c74d5b6c2ef578266361a58ec7c848cd844f2bd\",\n\t\t\"452cd7f4850757ad76710cea53bd9ad8d181d5dc\",\n\t\t\"98cbe204421b538fd2fbf4a1ce689f8398bd2ced\",\n\t\t\"b917f21f99eeacf49f55e8fd089b93119c7dbd9b\",\n\t\t\"bd4d8f4a02886a26b60c76048547a453691fcec3\",\n\t\t\"1dcf1de382a0bf95a3d8b0849546c88bac1292c9\",\n\t\t\"799748e42a644db85394db066af658809f89c523\",\n\t\t\"c693557ee87e311340eb0f8a811b8bca027af421\",\n\t\t\"912b86862ad070dd3d21f51e05e361eba1f515da\",\n\t\t\"67b085271fd9cc0a61eb04fcaf288ad35b2e7995\",\n\t\t\"a41a8b5641dad26c7601ea93818611b4a6465058\",\n\t\t\"5ede807d3bcdace2e25d5614382bfdf1663012e5\",\n\t\t\"8275c3b8829eb16a54fb49ceda2f6fbb44546c26\",\n\t\t\"5a2b47396587b499575782b60cb223a830bc86d7\",\n\t\t\"e113ac14fb2b70c1510f92ea2a0405ba4da01f5c\",\n\t\t\"10e53fd4d987e37190432e896bdaa62e8ea2c628\",\n\t\t\"286c93b65ab3c3a0a257b0a6ebdd99c06c674c88\",\n\t\t\"7b93e7e3c64b837b69da7497fdf4c28b677625bf\",\n\t\t\"0bc23b2c7e6419d3cd7e108d6942b9431bf5c25c\",\n\t\t\"0a5f0e4a16f5b12cde3df1ea413852aeaf176176\",\n\t\t\"44984c2480ac8aaef4a660a06565aa76c577238c\",\n\t\t\"5878d0f20e7cc521a437217dd21c3a84788d3a53\",\n\t\t\"73120785e720701d1142d97bdc72bf5d6b5af4bd\",\n\t\t\"fee8f41ab7f59597e35d8a6eb01b9edc9b04d51e\",\n\t\t\"e0feb1bd93ad9fb1e064706cff96e32b41a57b9c\",\n\t\t\"3dc8cea436c52d0d248abe9648b0e4f1d02bd500\",\n\t\t\"3e224b1b9e18dd28c89a764b1feea498ba952579\",\n\t\t\"09d6cbdde6ea3469a67601a811b4e83de3e68a79\",\n\t\t\"3251b36ee9e9c3effe3293f8d7094aa3841cad55\",\n\t\t\"252e267acf720ef6333488740a696a1d5e204639\",\n\t\t\"789cafde696403b429026bf19071caf46d8c8934\",\n\t\t\"4a4f88c5e13143f882268c98239fb85c3b2c6cb2\",\n\t\t\"8046b9d6b423f24457cfb20210d0ee8abc98e22c\",\n\t\t\"bb4eda2c61102759e7c03ab12ff7c19547e20cbd\",\n\t\t\"52da76c0c8190be88281aec828efd44df176ab34\",\n\t\t\"878e2200222f5e11137d5bfde325a5db30687592\",\n\t\t\"1789190601b7a5361e4fa52b6bc95ec2cd71e854\",\n\t\t\"44f8a2b2c0dfb15b4f112e22de76d837b89bd4d6\",\n\t\t\"aaf681a518ce5c9a048328b86ba5b9c5123375aa\",\n\t\t\"dbd77d2e6c54ed9fafc83f1cf6f48342250996d7\",\n\t\t\"532fd1449686690273222ebd5cb86c233ed19f58\",\n\t\t\"60de19e6c8e44b1a78acb0dd73722b2feaa7ccfe\",\n\t\t\"85289261815e7d2fb1472981652fce50ae8cfc42\",\n\t\t\"71b610fca525744bc70eb96c9f9113cddbc38f4f\",\n\t\t\"2977cca2c82e3c5336805ebb6226c14137585b54\",\n\t\t\"1d2d1a5ed9cfc58d0a7bdc1d9dda5ecb9987da9a\",\n\t\t\"d339db49e637d2a8122a67ad846a294124f1a2f3\",\n\t\t\"02f16015ed9e2689e10f86f1b7c3522e541c0c75\",\n\t\t\"0e7eab15d6b4184921b82fbca6f89dcb60ea972e\",\n\t\t\"362e2295d95b2e2797457760eef1f172d07d7417\",\n\t\t\"c067143934cb76530adbb8fd4e2df1ab737a16e0\",\n\t\t\"b3add478d4382b78ea20b1671390a858002feb6c\",\n\t\t\"907df2bf39d70510951b7bafbf661f286eed90a5\",\n\t\t\"6e5d51a72d142f2d40a57dfb897188b36a95b489\",\n\t\t\"00f6db9a5e6fe2374b5a494b08388c2b6e0792d8\",\n\t\t\"eeb69005da379a10071aa4948c48d89250febb07\",\n\t\t\"af799dd7e23e6fe8c988da12314582072b07edcb\",\n\t\t\"3b27257997ac51b0f8d19676f1ea170427e86d51\",\n\t\t\"90ac2db772d9b85e2b05417b74f7464bcc061dcb\",\n\t\t\"451bc97f7519017cfa96c8f11d79e1e8027968b2\",\n\t\t\"066aaf67a580910de62f92f21f76e3df170483cf\",\n\t\t\"d5e162564701848b0921b80aedef9e64435333cc\",\n\t\t\"12ac6f103a0ff29fce17a078c7c64d25320b6165\",\n\t\t\"21f7a9a2da446f1e5b3e5af16ebf956d3ee43ee0\",\n\t\t\"81065531e63fccbe85fb04a3274709593fb00d3c\",\n\t\t\"09ca864bec94779e74b99e84ea02dba85a641233\",\n\t\t\"d635e3eed4beb74213489ff003ca39dbe47ea44e\",\n\t\t\"b75e5e9feb70f599a6f6232e71bd5b0030608179\",\n\t\t\"02419d851c01139edf9e19b81056382163d9bfab\",\n\t\t\"57b7ba0ca94313c342b03bd31830fe4a8f34bc1a\",\n\t\t\"a68959c06e5f8ff45faff469aa16f232c04af620\",\n\t\t\"70b332574395cde2c56db431b619be9823407aed\",\n\t\t\"45c3bb7696f29655189abb78ec1c97f511643159\",\n\t\t\"99a1348743f3550dd4524408725efab8eb319960\",\n\t\t\"acc766c65cd4e94a5e1fab6a2f85148dfc8613d8\",\n\t\t\"28bdcdcceb92a1ac450b8b6a3d3d0627d839054d\",\n\t\t\"9ea12cb2c426d521b7c4cad5b02ce18e5b614d4e\",\n\t\t\"8347ef8861b75bbffcaebb706a0ae296daabc20e\",\n\t\t\"e844c4278ecba985c08e0dea1181343a07c04c3e\",\n\t\t\"4b151bcdfe290542f27a442ed09be99f815f88e8\",\n\t\t\"1d7200e19d1ffdaf6927ff0be701724c85be07d7\",\n\t\t\"1861e32c3c484a1aa5f55ab109e08dd1b32c6fa2\",\n\t\t\"34c56f43fd3255fc239ffe33d0fbfb8195be6a24\",\n\t\t\"e5f6cae5ca7ecaac1ec2827a9e2d65ae2869cada\",\n\t\t\"0c900514d3446d9ce5d9dbd90c21192048125440\",\n\t\t\"56b53c8f4bcdaada801d311cf2ff8a24d6d96883\",\n\t\t\"de748cf874e4e193b42eceea9fe5574fabb9d4df\",\n\t\t\"4bafcb5aacb1abc193698a13ae99394e09a25101\",\n\t\t\"687cede1a44f70c7741abfab6ee2aa53dd2bfb54\",\n\t\t\"34d8332b975f9e9a8298efe4c883ec43d45b7059\",\n\t\t\"698bd8c759ccc7fd7398f3179ff45d0e5a7ccc16\",\n\t\t\"2872764df7b4857549e2880dd32a6f9009166289\",\n\t\t\"164343da11db817e81e24e0d9869527e069850c9\",\n\t\t\"e1c6222f2fa8d05d7825d8e9af7b9bef089c0b5e\",\n\t\t\"127fc12785c42eeff7da15abca690655add7c710\",\n\t\t\"53c041061964825372701d75b96a67e82bc3b6da\",\n\t\t\"ba4bfac0366399080e878cb5c41023c3eb7f7328\",\n\t\t\"61ad4ef7f9131fcf6d25c34b817f90d6da06c9e9\",\n\t\t\"4a3ee4146a90c619b20977d65951825f5675b560\",\n\t\t\"67613a3a83092588b85b163b9aeb3e87ec46b4ea\",\n\t\t\"c197c86ceec7318b1284bffb49b54226ca774003\",\n\t\t\"ba035118bc8bac37d7eff77700720999acd9986d\",\n\t\t\"c85270e307e7b822f1086b93689124b89768e273\",\n\t\t\"2042461b754cd65ab2dd74a9f19f442b54625f19\",\n\t\t\"04669a54b799c105572aa8de2a1ae0fe64a17745\",\n\t\t\"48d6674adb5a077f2c04b42795e2e7624997b8b9\",\n\t\t\"15177b3e1c91529ba02c056035d71463f9e66a03\",\n\t\t\"241aa26c61f638b7e76787558bb1be49984f2a0d\",\n\t\t\"4605d2f4267388d02d810a3cea448a48371435a3\",\n\t\t\"d1cce505a1dafd5b5d842ec8f91105ccca7d5e4d\",\n\t\t\"0a10ae77a57942352f3d2abe5d58b199b1f83d33\",\n\t\t\"dc6c49c40b1d5acf3ee58784ec34360806f48a22\",\n\t\t\"6d9393fd0ea1e1900451b5ee05351523725392bb\",\n\t\t\"8a603c50591cd2ba1039afa3f28540b0f43c82c5\",\n\t\t\"b4bd511b8cff2cfd83faf37f48b6e256dabcfc6c\",\n\t\t\"c02f6844c6d27c8357257eab162250b54d38390b\",\n\t\t\"46cb8029e744ac128d4ce58c944ab21ab7ef3e25\",\n\t\t\"e27eb84680badf95422bf5798faf8b0b7fa332f4\",\n\t\t\"a974a4972c0ecd2206b045e387bea4713a5451b6\",\n\t\t\"e3480072bc95c202476ffa1de99ff7ee9149f29c\",\n\t\t\"74548703f9851017ce2f556066659438019e7eb5\",\n\t\t\"562a587face36ec7eff2db7f2fc95425c6602bc1\",\n\t\t\"f3cd84cc45f583a0fdc42a8156d6c5b98d625c1a\",\n\t\t\"f48473482c0e3e714f87186d9305bcae30b7f5cb\",\n\t\t\"288f60226c596849c3c57926e8421c83b5abbe87\",\n\t\t\"5eacc6522521f7eacb081f95cee1e231648461e7\",\n\t\t\"5eea182d6651a7257bc8c3614507e1540c766fc2\",\n\t\t\"8d49996a4338670764d7ca4b85a1c4ccf7fe665d\",\n\t\t\"48e3b9cfc10752fba3521d6511f4165bea951801\",\n\t\t\"0422d3543c01df2f1d8bd1f3064adb54fb9e93f3\",\n\t\t\"878a02f3ab98d37206c9852c025a46b86dc882d0\",\n\t\t\"c8de82b962142a5f3d408ecc3920642b166de028\",\n\t\t\"bf744c1e2776ed1de3c55c8dac1057ec331ef744\",\n\t\t\"85262acf3ca9816f9537ca47d5adeabaead7cb16\",\n\t\t\"934c04d3cfef185a8008e7bf34331b79730a9d43\",\n\t\t\"60a59edc89f93d57541da31ee1c83428ab1cdcb3\",\n\t\t\"935151eb71beff17a2ffac15dd80184a99a0514f\",\n\t\t\"3cd63d075497751784b2fa84be59432f4905bf7c\",\n\t\t\"8531ad5ac454cc2deb9d4d32c40c4d7451939b5d\",\n\t\t\"3758e8c1664979749e647a9ca8c7ea1cd83c9b1e\",\n\t\t\"dd6dda9da676a54c5b36ca2806ff95ee017d8738\",\n\t\t\"40fd4d696c55793e996d1ff3c475833f836c2498\",\n\t\t\"ef31541dd28ae2cefdd17c7ebf352d93e9058c63\",\n\t\t\"d877e195a05aca4a2f1ad2ff14bfec1393af4b5e\",\n\t\t\"39e8f0d32258f304928b29bc7e1f7d85fa5ae218\",\n\t\t\"f9414e9ed1a16132c5d3467991a3bebc4367a1bc\",\n\t\t\"984a623e0c0dfec82cb1cd390ee1aab51fa02bee\",\n\t\t\"d60d3f8ccefd848d551d25bcb7f3e9251333648b\",\n\t\t\"e9cb36b5954d1af1593bbc4fecadfa5cb170bd44\",\n\t\t\"058e7a538e020b73871e232eeb064835fd98a492\",\n\t\t\"6f14738ec2e9dd0011e343717fa624a10f8aab64\",\n\t\t\"7aaae28e06aafe63ac94f7c6dee81135b815db92\",\n\t\t\"9b1f3cf3fdd02d313018f1a67c42106e6ce9f60d\",\n\t\t\"21c5319c82ca29705715b315553a16f11b16655e\",\n\t\t\"fc5cdccdeeedb067b8b2a3c7df2907dfb7e8a1b9\",\n\t\t\"44df9a1310c1278b62658509aca3ca53978e8822\",\n\t\t\"fbdcb39db6a6976944a621fe11bf1d2ff048d7c2\",\n\t\t\"1a01a2a1218fcf9faa2cc2a6ced025bdea687262\",\n\t\t\"056dcc8480ecd2c03ec004aa76278d1f2d621561\",\n\t\t\"b33d6d9045da8f0b317162facabcd1dc9ebf751d\",\n\t\t\"be8f9b519d692dfd1e2726ee9e26573dabc99e70\",\n\t\t\"e522a857d234e5ade679abfae807bcf4ecdd6f2e\",\n\t\t\"533e7cbc5efc1d58d14cefb68904cd3af47fc316\",\n\t\t\"fdeb0e5beb9ddbfb49b4aec3daa55d71e0cf1956\",\n\t\t\"966952ede72900ddaa20888beddc86a5a002cbd3\",\n\t\t\"b22ab0397e893d9c092ade34a8e826ef576b285c\",\n\t\t\"65ce500f7cad946ce0e172c6ce90319caa29e787\",\n\t\t\"c8645a939af24f227e4123b89af14264176f7c60\",\n\t\t\"62bbe12f1a737d9b96358a9964466ffea6a6487a\",\n\t\t\"696cf9f9160e3a0ff208db614af118915cbdcf2d\",\n\t\t\"2f2695de1ae62d84ca8336c7e6ddedc80aa2e521\",\n\t\t\"59d5e3e86e2583fba0cf04d4f126a5205a24c4b6\",\n\t\t\"d60bb33b97b968b555ce829961d41971ff826415\",\n\t\t\"1200e7ebeedbe0d10062093f32925a912020e747\",\n\t\t\"88e9a306715e9379f3122415ef4ae759a352640d\",\n\t\t\"0a1cb8dbe71b5a6a0288043c3ba3ca64545be165\",\n\t\t\"a240efb690601cb1ef02a6778a23e450559b0bef\",\n\t\t\"0e7c45d7667feb56e5664247a882451c3d438def\",\n\t\t\"eaa7ec646db93f0096ca8100c361018c2608319b\",\n\t\t\"cef76bca08c1f437150890d1b8bf430a66ebe42a\",\n\t\t\"21743fe8af7bb684d5ebbd4075f397fccc30d158\",\n\t\t\"006936bbd6c5b235665d87bd450f5e13b52d4b48\",\n\t\t\"698ce67b5e58becfb4ef2cf0393422775e59dff4\",\n\t\t\"a698fd936b13588c6747b182bcfdc13885a8ca43\",\n\t\t\"357a3836bb5da16f314f3a1e954518e5468cd915\",\n\t\t\"37fe2217f577b0b68b18e62c4d17a8858ecf9b69\",\n\t\t\"5e303a03d04e6788dddfa3655272580ae0fc13bb\",\n\t\t\"c9ad4a0850ab676c5c64461a05ca524cdfff59f1\",\n\t\t\"cc5888f14a5768f254b97bafe8b9fd29b31e872e\",\n\t\t\"63f943103f250ef1f3a4d5e94d145a0f961f5316\",\n\t\t\"516c03b21d50a644d538de0f0369c620989cd8f0\",\n\t\t\"25ea2e8b0c338a877313bd4672d3fe056ea78f0d\",\n\t\t\"59033da2a1afd56af1ac576750a8d0b1830d59e6\",\n\t\t\"2ca09f0b36ca7d71b762e14ea2ff09d5eac57558\",\n\t\t\"3ff3baa0074445384f9e0068df81fbd0a168395a\",\n\t\t\"4c7018119aeb66335746e6748456c821e304d3a2\",\n\t\t\"75a75c47eb912f3fd06df62a9e4b3b554d5b2bec\",\n\t\t\"2e617bd795b3b55b2ec23543721665a2b1c77b9c\",\n\t\t\"a0f0c4de6cc321130252e86658c21b2e1b6af008\",\n\t\t\"7868b29620b92aa1040fe20d21ba09f2506207aa\",\n\t\t\"a82d2503e718d17628fc9b4db411b001573f61b7\",\n\t\t\"e358016010b6355630e398db20d83925462fa4cd\",\n\t\t\"82357e97a5c1b505beb0f6c227d9f39b2d7fdde0\",\n\t\t\"66eab4bbf91fa01ed4f72ce771db28c59d35a843\",\n\t\t\"eb91bc9b9ff26bfcca077cf1a888fb09e8ce72be\",\n\t\t\"c56ffb4a6541864daf9868895b79c0c33427fd8c\",\n\t\t\"1e39adf7c3f5e87695789994b694d24c1dda5752\",\n\t\t\"93d37f677addd2450b199e8da8fcac243ceb8a88\",\n\t\t\"d54a9712c29c4e6d9d9ba483fad3d450be135fff\",\n\t\t\"a4c3885fa656a92508315aca9b4632197a454b18\",\n\t\t\"4c1fd1f78ba7c16cf6fcd663ddad7eed34b4d911\",\n\t\t\"389b730dc4e454f70d72ec19ddac2528047f157e\",\n\t\t\"7d1b5b69a5ea87fb2f62498710d9d788d17beb2b\",\n\t\t\"b8af3fe6f1ca88526914929add63cf5e7c5049af\",\n\t\t\"dafaf2c27f27c09220cee312df10917d9a5d97ce\",\n\t\t\"7473b8cd3c0ef9932345baf569bc398e8a717046\",\n\t\t\"67f57e154437cd9e6e9cf368394b95814836ff88\",\n\t\t\"3c2af9d14e43d46b541ac1a0cdd7be4980aeed84\",\n\t\t\"aec7142dafa1f96154018eced507854bb544cf41\",\n\t\t\"319d3da49ca42fca687de2accef1d22fba786405\",\n\t\t\"7577f792244cae44227e675aafcf6597a2eeb00d\",\n\t\t\"9df166a4a89314f5281b37e524bb366d7ffadf23\",\n\t\t\"a0b8af84c1ddf5d9dd7d1eef8a19df527864e8cd\",\n\t\t\"ba91c4fa57b566baff698a3354db2b8af8626d3c\",\n\t\t\"7d5c94b0fb6384b91d963d6d398468d96bb4983f\",\n\t\t\"4c54840ac217908029e77a96336c03901a6776d5\",\n\t\t\"6ac92abcc06bb8d52d8179889c6b1173ba7bd027\",\n\t\t\"8bc95edbea781cc09dc6755f570b72a993df1679\",\n\t\t\"a9992b9918cd8582e2fef748a61ec1c46894b13f\",\n\t\t\"bb8b60297070d7c352a06c6bbc3854cfac26d46a\",\n\t\t\"4185a5807b9fdbdcbee813f8e82ceb433ad75c68\",\n\t\t\"9bc1a58a9472452100f873059683a9a37e37063c\",\n\t\t\"4d1701b9c993f5edfd232ea06a6f8ba540113b59\",\n\t\t\"5d5c7c1b342c89b4b0d28cd99572827cbe3e6f15\",\n\t\t\"d641ffaf2a3a84c8c85d24850b916c5baf547a38\",\n\t\t\"c5bf5e88b285eda01f3d2044c76a6f5651dfa4c0\",\n\t\t\"6b7aca9462a226dbdef319e6875523e322c6f80b\",\n\t\t\"b9740e040f5fc06920f38d9fafd966bfd3cabe1e\",\n\t\t\"830a7d5e13edb5f6f81c36877edf68b55a4182fb\",\n\t\t\"751030d0b6c06337bb2870cd174ec83ed8417b3e\",\n\t\t\"b8957915e5d02d9e341eaa07a90019aeb90d546f\",\n\t\t\"82cfcd48e0c239ddbdf4fa122b8715275b761de2\",\n\t\t\"97c73ecd70bc7e8eefb26c5eea84f251a63f1031\",\n\t\t\"5d1abb695642e88558f4e7e0d32aa1925a1fd0b7\",\n\t\t\"0e5af3b6dc164eb2c699b70bf67a0babef507faf\",\n\t\t\"f08a912ce02debbaa803353686964b3c5fcfdb53\",\n\t\t\"8625e8f9b6f49b881fa5fd143172c2833df1ce47\",\n\t\t\"b421526c5f297295adef1c886e5246c39d4ac629\",\n\t\t\"9be9bb9b3a1638dcd948edd6179bd8ee4ffcc137\",\n\t\t\"bea6fede6328fabafd7e68363161a7ea6605abd1\",\n\t\t\"7183a25510a02ad00cc6a95d3b3d2a7d3c5a8dc4\",\n\t\t\"af40e34de7c30e4fef253024a88257f6dddc547a\",\n\t\t\"d2cac68225a25a5486ea848af95680573ee3d393\",\n\t\t\"d952189f6abb148ff72aab246aa8c28cf99b469f\",\n\t\t\"87fa769912b1f738f3c2dd87e3bca4d1d7f0e666\",\n\t\t\"79d7792942fa009316de2d7d1a4d7e8b33548947\",\n\t\t\"6604030f7da573a8c00641f9c7deef6c143b6022\",\n\t\t\"2746f9ec96f9ce3a345b11f03751136073f7869f\",\n\t\t\"f73773fc39d43df7661609b9f7a733ddfd091af7\",\n\t\t\"be8a20787124cf52c56c5928ef970df2d8a26f51\",\n\t\t\"8ec1dce97ba5b616e165068225bba873179482e9\",\n\t\t\"dff1e225fe6bfdf7853663bc48831e9714bf035e\",\n\t\t\"ebe2549568386d5c289ec0eb738172f1a0445259\",\n\t\t\"9d5fdc88f91586bf5d1afa13b9a77302c39b5e7c\",\n\t\t\"ab7c7c3c823cb2f8fb1b54fdc82b3e133e8e8344\",\n\t\t\"bc34429b8d1a620c58639f376bee9ba425a035d3\",\n\t\t\"0bc8d9f00bd34806bc82d01390855ef9dcbea85b\",\n\t\t\"a1e978879d35af3590549437b80679b5c00f27d6\",\n\t\t\"1000c919125bb13f265b101341c34bb5af814fd3\",\n\t\t\"488e5cfdd4d2d30b161fc45819a82a6984eb0f99\",\n\t\t\"4b986a99445e49ea5fbf5d149c4b63f6ed6c6780\",\n\t\t\"64485a221d9095fc7ab9b50cc34c6b4b58467e2e\",\n\t\t\"bf9e9aea47c3d112929fc56abd75a48d31914fda\",\n\t\t\"73334ff5470db03e5b793ce1d5854642b2c21799\",\n\t\t\"7fd8a65d950d0e77dd39cc4ce2776ff9673ae470\",\n\t\t\"d4c0da647de59c9ccc304a112fe1f1474d49e8eb\",\n\t\t\"ccf79a1a63ef35de038a4226a952175c4e9f4f59\",\n\t\t\"5fb53c92da84ebeff403414b667611d6bcd477cf\",\n\t\t\"ef5bccf2a7a22a326c8fe94e1d56f6f15419bedd\",\n\t\t\"311d38cf15ec7f5c713985862632db91b7a827af\",\n\t\t\"e2d5e96ea4bbd4fc463dbb76d07dd8aefac05e3c\",\n\t\t\"61625cf2338fe84464c5d586dbba51d4ff36a2b8\",\n\t\t\"9d8cd3ed749f2c2f846e0c58c485c8a0d5d5181e\",\n\t\t\"2f23beded3e46a3552fc3c1a0fdfb810c24d8f97\",\n\t\t\"54bc99d2b886a868d79d537ee5e7829bb062fbe4\",\n\t\t\"52fd60d5dc3f0fb3ed5c19b63f6f2312cd1f6add\",\n\t\t\"8a8ef1517d27a5b4de1512ef94679bdb59f210b6\",\n\t\t\"f6ea1e9c0a0acf137a8a4c5353bc97ead6b82cf7\",\n\t\t\"ea93fbd2137c797ed8a686737e8bdfeead20f1b1\",\n\t\t\"18ed04a0e502896552854926e908509db2987a00\",\n\t\t\"2a9d06026ed251705e6ab52fa6ebe5f4f15aab7a\",\n\t\t\"c2ef6018eecde345fcddb96e31f651df16dca4c2\",\n\t\t\"93cc78652ed836ef950604139bfb4afb45e0bc7b\",\n\t\t\"dd44733e94f3f6237c896f2bbe9927c1eba48543\",\n\t\t\"ed90430e545529a2df7c1db6c94568ea00867a61\",\n\t\t\"3ad0af28e408092f0d12994802a9f3fe18d45f8c\",\n\t\t\"9da10a9f72e3f87e181d91b525174007a6fc4f11\",\n\t\t\"d186a0be320e6a139c42d9b018596ef9d4a0b4ca\",\n\t\t\"62b6a5dfee2e22ab9015a469cb68e4727596fd4c\",\n\t\t\"84ede759015e7480ca8e6ec2af6e2f596aa92dec\",\n\t\t\"f3085568e45c2ca74118118f792d0d55968aeb13\",\n\t\t\"84d160a3b20f1de896df0cfafe6638199d49efb8\",\n\t\t\"6915c9c6966bf1482ac93236453013535e8c5d80\",\n\t\t\"bd1236bebd1ee50c8d9206e69b1986fac9532a49\",\n\t\t\"70cff2bc010d0c047cf5b167b2c600e42f6863ab\",\n\t\t\"6610ef4a025fcea2a5958724b9493a1b081b8f66\",\n\t\t\"ca018bb3db661230fbf51bc2b3b1559ac7987040\",\n\t\t\"313e89f2da215f0dcc54a638c5749c4ee959e74a\",\n\t\t\"c0dc8a542fd18d372a2ee67a203f2cfe0a345a05\",\n\t\t\"b31c6944d9cfd596b6c25fe17e36780bfa2d7473\",\n\t\t\"3a7aecd4bcaf75c7b0b02c26ea6ceacf3e8f5f4d\",\n\t\t\"1fd80f714c85ca685a80f32e0a4e8fd3b866e310\",\n\t\t\"baf7b939ef71b25713cacbe47bef8caf80ce99c6\",\n\t\t\"118f166726472bd5b5578817503ed0992c9102e2\",\n\t\t\"4c62b2337352073ff41fa9a9857a53999d41b49b\",\n\t\t\"3addc6860c44edcf28c262489f23276b84e11812\",\n\t\t\"0df31f1cd96df8b2882b1e0faf4409b0bd704541\",\n\t\t\"a224b43863a0679f153bec24e1d329c63f1ed234\",\n\t\t\"700f71ffefd60c16bd8ce711a956967ea9071cec\",\n\t\t\"fa9a2e447e2cef4dfda40a854dd7ec35624a7799\",\n\t\t\"6d62b9b4db6228122a5f1cda81b06f156afb04ba\",\n\t\t\"14f50cd1c2f5d29d9b070746c1fcae59b68ca26b\",\n\t\t\"d3e1ce1d2b3119adf270b2d00d947beb03fe3321\",\n\t\t\"2f4525d4a200e97e1b87449c2cd9bd2e25b7e8cd\",\n\t\t\"b0b14b3d12980912723fb8b66afb48dcda742fcb\",\n\t\t\"bc28b5a964c8f5721eb58ee3f3c47a9bcbf4f4d8\",\n\t\t\"49b64e09d81c0cc84b267edd0c2fd7df5a64c78c\",\n\t\t\"8bf9683c80762d7dd47db12b68e99abea2a7ae05\",\n\t\t\"5600569133b7bdefe1daf9ec7f4abeb6d13e1786\",\n\t\t\"66a60c7201c2b8b20ce495f0295b32bb0ccbbc57\",\n\t\t\"3c13fc5715231fadb16a9b74a44d9d59c460cfa8\",\n\t\t\"c05b6b32b69d5d9144087ea0ebc6fab183fb9151\",\n\t\t\"b7ce164e9e75be4b5eb42fd89c9c53ebecea6729\",\n\t\t\"abc7bac20e8b15d5aa38d1c9af5bed4e0ffc7748\",\n\t\t\"928c299530ecce5c25dcf62f72f6aa901d6baea4\",\n\t\t\"87b2ed1c62d42fd9fbbd154095f2387c6a18f880\",\n\t\t\"67336cfb9d93779c02e1fda4c87801d352720eda\",\n\t\t\"074b9950a587f53fbdb48c3f1f84f1ece8c10592\",\n\t\t\"132630f17e198a1748f23ce33597efdf4a807fb9\",\n\t\t\"00b5ec860e174d7a2edb2b46523cdc5401513cbd\",\n\t\t\"3b17774c8087e239542afe1c7976c16c5446af26\",\n\t\t\"cc71779727e9051e59c8a242b4157fc1d3172caf\",\n\t\t\"9aab69982e4a9b91a86743f73dc48db30daf9265\",\n\t\t\"ff0cd44f590a80c5c87aaa85a0d2bab2d350bc4a\",\n\t\t\"7b6c7f676d78f988b01e9841ab18d389886ffa26\",\n\t\t\"5fcf76dda71647a65d6fafbab2bf03065bf3d52d\",\n\t\t\"c63eaf104979cae41c9c5b2ef1a9bbe5d1c05480\",\n\t\t\"a7efb5dce8081d1d96445355d55f05e6e825d41f\",\n\t\t\"89223f29832931516d6c1f00a9ef2263b8674f5a\",\n\t\t\"5506a7066998a2be47b86e28d061863a475a7ca8\",\n\t\t\"a2e83c6e6ad2086f97277540d9d9ef4aebb74a28\",\n\t\t\"da50b1b4177cbadb977d52aa70011713f37a2156\",\n\t\t\"e7d90c28cbbe26b9a31fa8a136c209418ca3c9ba\",\n\t\t\"0ce981aa4a840f84b670bbb3dfd77cd3be87ca84\",\n\t\t\"1c973b3f5c13399e1194724abe421e230c572206\",\n\t\t\"9074f509fbc3df3ad104eca5427d03eece453246\",\n\t\t\"34994ed5371f31eeaa68b294d7f729934280a733\",\n\t\t\"57001fa0b6622767e63c1b9cd2e6db666d180caa\",\n\t\t\"6a999a46cb630f44f1a77ac39213fad57a8c1492\",\n\t\t\"2c3fe5d2e5941e50947ff59c50d201d3968fac02\",\n\t\t\"1149e08b436cca632ddfe8cee39918f23b50dc6f\",\n\t\t\"0b813b7539fae6550541da8caafd6add86d4e22f\",\n\t\t\"ef65452adaf20bf7d12ef55913aba24037b82738\",\n\t\t\"b260ca7a23bb0d209771db7aae35049899433fe3\",\n\t\t\"b4ac9780b37cb1b736eae9fbcef27609b7c911ef\",\n\t\t\"86ed42574cd68662b05d3b00432a34e9a34cb12c\",\n\t\t\"a483da1de9cb174ca327059e9fd8432b0e8666b3\",\n\t\t\"6eb2c27f1b7d048a6912a42a0637e470cdc46562\",\n\t\t\"1d3f5d1fd272883cbc26f3d7fcf9ba58f66d48e0\",\n\t\t\"5204ace0d7b8410a5fb73c17a20a69e616215131\",\n\t\t\"60164baf43273401883c7c0b53b0bc4359b9e94f\",\n\t\t\"7fbf34d79ca897acf21061c2e24b607b090be1c9\",\n\t\t\"5ae5c9ec39930ae9b5a61b32b93288818ec05ec1\",\n\t\t\"f90394c695d47b16f608be5366373eec768597f1\",\n\t\t\"e0d6c62cef4929db66dd6df55bee699b2274a9cc\",\n\t\t\"fff73bb736a3ebf11974ba2ded176f16a1976f0d\",\n\t\t\"a4ad886bfdbd1a872bdb3b25a9893994b78adf11\",\n\t\t\"010245305f4faef0ed473552a58f83d281754e77\",\n\t\t\"b82b13e45d9372296362e0d6dc481f6a0f2ce0c7\",\n\t\t\"e6396ecb39ea2c91dc9901213da1d29b8ae1798c\",\n\t\t\"1ed09f94667962983cce7ec6c7a1df5c0881e08a\",\n\t\t\"ae1536faa3401b0c62f93e29fef9ffcf134a616a\",\n\t\t\"a26d3c16f32cf21cbe24c0d7dc37132c407608bb\",\n\t\t\"d716952ab58aa4369ea15126505a36544d50a333\",\n\t\t\"2949632c1b4acce0d7784f28e3152e9cf3c2ec7a\",\n\t\t\"323964c36556eb0e6209f65c1cef72b53b461ab8\",\n\t\t\"3864a1320d97d7b045f729a326e1e077661f31b7\",\n\t\t\"6f29a4f68e4156358f64f6a060c5e55ad42f5231\",\n\t\t\"e1e99c956a36e619398f9e94d775f51a85c26770\",\n\t\t\"f4d24aa8fe81caab2420dcf4cf9ceb139394b535\",\n\t\t\"f9d9e55d1072d7a697d2bf06e1847e93635a7cf9\",\n\t\t\"df7dc4df69114c694956a0ac537119527ecf1b9c\",\n\t\t\"35e36c0cafbdc3395fd4600a05c613d3073c895e\",\n\t\t\"24d091b80d513846293c00350f46d85f71797aff\",\n\t\t\"f95b25589b40b5b0965deb592445073ff3efa299\",\n\t\t\"6a93ee522c52f5bd54140b6fa0be6a503e00dc96\",\n\t\t\"657d341c197d036dc27d7f8f5b61c6ff6a678df4\",\n\t\t\"cddd306a5010eba20a133b6473d9e8d967884f57\",\n\t\t\"0d825fd2e9e4dd42ac14d5ae6c7f92cbe63de009\",\n\t\t\"8bd7794fbdaa9536354dd2d8d961d9503beb9460\",\n\t\t\"e349501d4275363646a099e1d3baed064aa2eca5\",\n\t\t\"151dbcd21c9ed6b03960a5f0b05c255c9f955618\",\n\t\t\"28b0eaf7c500c506976da8d0fc9cad6c278e8d87\",\n\t\t\"a09a8c790a20309b942a9fdbfe77da22407096e6\",\n\t\t\"2c23f53ca22d7d8885fc4522ddcadcfe7f01a783\",\n\t\t\"65935d9855ece6f85c21ad38634703d0917bf88c\",\n\t\t\"dec00ef7c6155c4ca1109ec8248f7ff58d8f6cd3\",\n\t\t\"cc3d2b7b7cb6f077e3b1ee1d3e99eb54fddfa151\",\n\t\t\"009d724771e339ff7ec6cd7c0cc170d3470904c5\",\n\t\t\"e64aea8b539905fa92fad0e7cf73ffa4375f8b32\",\n\t\t\"6c62681a2f655b49963a5983b8b0950a6120ae14\",\n\t\t\"db708f7d959dee1857ac524636e85ecf2e1781c1\",\n\t\t\"2cd0a87ff7df953f810c344bdf2fe3340b954c69\",\n\t\t\"3aab2116756442bf0d4cd1c089b24d34c3baa253\",\n\t\t\"3af797a25458550a16bf89acc8e4ab2b7f2bfce0\",\n\t\t\"235a7e571b33eda1a81e0f73a3173ef95dd020e5\",\n\t\t\"50d0390056017158bdc75c063efd5c2a898d5f0c\",\n\t\t\"4205e3cf9c44264731ad002fcd2520eb1b2bb801\",\n\t\t\"53fc648efc0c82b1e0cc806ab7abf7dbdf532273\",\n\t\t\"faa8ba85d503da4ab872d17ba8c00da0098ab2f2\",\n\t\t\"7687a145717677e64300adeb44ac29d90f844b59\",\n\t\t\"814ec05f3683b661166055a23e29dca0300cd58c\",\n\t\t\"351719631846db88eb3daf690fca5399aed3fd77\",\n\t\t\"49c100caf72d658aca8e58bd74a4ba90fa2b0d70\",\n\t\t\"8cc35f73da321c29973191f2cf143d29d26a1df7\",\n\t\t\"a3f7325c52240418c2ba257b103c3c550e140c83\",\n\t\t\"7bb85ce2cf23af5b2d7467c2825fa2d0330ec5d5\",\n\t\t\"6fe2e3bb57daebd1555494818909f9664376dd6c\",\n\t\t\"1c63879e1f630e44ad8d2245b8a28a088f387e7c\",\n\t\t\"313913e603eaf3bb2c3b05079046ec07bb61f8c6\",\n\t\t\"4f062ad1aebb1255b84c851d00694cc7949de832\",\n\t\t\"887697058d8464462e8fd6d23c8461e90aec8c08\",\n\t\t\"2ab94758b0276a8a26102adf8d528cf6d0567b9a\",\n\t\t\"5397c9a02f77744da25d4ef63a7ebf01affeca62\",\n\t\t\"d6adb54fefe72482ed049f07af31ddf2c287345f\",\n\t\t\"4c65b7b43f3fe31350f74cb7d0b2461e111e8dd0\",\n\t\t\"e6feb6b7c06600924e8b6bda3263c870cfb0a447\",\n\t\t\"a09d2c48d3285f206fafbffe0e50619284e92126\",\n\t\t\"925720c5d40c4ebf8601e06025e1402251ef71d2\",\n\t\t\"611b82d4c4b4f67cc3d83cf0697ec660fcee2fff\",\n\t\t\"dfd5101b17da36c32ae024b984e0b72712f01a35\",\n\t\t\"68f1af10052713fda01bfb1e5b831dcf6d826ab2\",\n\t\t\"e2133b723d0e42be74880d34de6bf6538ea7f915\",\n\t\t\"e40429d9dd849c5fe0bdf97062b1d9358d99826d\",\n\t\t\"0ac2d2817d649e3203a8f7c93e7c65be0ca9662e\",\n\t\t\"7ef25e94db74d85fa7e9271b064a3c7d9ef7add5\",\n\t\t\"3e05dcce371d3f672feba29f086ad78a93ae3996\",\n\t\t\"16b9f8ab972e67eb21872ea2c40046249d543989\",\n\t\t\"c47579857bbf12c85499f431d4ecf27d77976b7c\",\n\t\t\"1ea4bec1a921180164852c65006d928617bd2caf\",\n\t\t\"d3ebf0f291297649b4c8dc3ecc81d2eddedc100d\",\n\t\t\"0ddae73613ab823639de096c287ea6142749f340\",\n\t\t\"6638e37b887b5a279044afbdc9928e19f678eb2e\",\n\t\t\"de7b8a41bbe1ccdfc009de51fa6d160db3ca8025\",\n\t\t\"f52de0603f31798455e48bd90e10a8f888dd6d93\",\n\t}\n\n\tfor i := 0; i < 5; i++ {\n\t\tt.Logf(\"Iteration %d\", i+1)\n\n\t\tfor _, sha := range shas {\n\t\t\tpkg, err := ms.GetMavenPackageBySha(ctx, sha)\n\n\t\t\tif err != nil {\n\t\t\t\tif strings.Contains(err.Error(), \"no artifact found\") {\n\t\t\t\t\tt.Logf(\"failed to get package by sha: %v\", err)\n\t\t\t\t\tcontinue\n\t\t\t\t} else {\n\t\t\t\t\tt.Fatalf(\"failed to get package by sha: %v\", err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// log human readable timestamp\n\t\t\tti := time.Now()\n\t\t\tt.Logf(\"Time: %s Success: %s:%s\", ti.String(), pkg.Name, pkg.Version)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "grype/matcher/java/matcher_mocks_test.go",
    "content": "package java\n\nimport (\n\t\"context\"\n\t\"errors\"\n\n\t\"github.com/anchore/grype/grype/pkg\"\n\t\"github.com/anchore/grype/grype/version\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n\t\"github.com/anchore/grype/grype/vulnerability/mock\"\n\tsyftPkg \"github.com/anchore/syft/syft/pkg\"\n)\n\nfunc newMockProvider() vulnerability.Provider {\n\treturn mock.VulnerabilityProvider([]vulnerability.Vulnerability{\n\t\t{\n\t\t\tPackageName: \"org.springframework.spring-webmvc\",\n\t\t\tConstraint:  version.MustGetConstraint(\">=5.0.0,<5.1.7\", version.UnknownFormat),\n\t\t\tReference:   vulnerability.Reference{ID: \"CVE-2014-fake-2\", Namespace: \"github:language:\" + syftPkg.Java.String()},\n\t\t},\n\t\t{\n\t\t\tPackageName: \"org.springframework.spring-webmvc\",\n\t\t\tConstraint:  version.MustGetConstraint(\">=5.0.1,<5.1.7\", version.UnknownFormat),\n\t\t\tReference:   vulnerability.Reference{ID: \"CVE-2013-fake-3\", Namespace: \"github:language:\" + syftPkg.Java.String()},\n\t\t},\n\t\t// Package name is expected to resolve to <group-name>:<artifact-name> if pom groupID and artifactID is present\n\t\t// See JavaResolver.Names: https://github.com/anchore/grype/blob/402067e958a4fa9d20384752351d6c54b0436ba1/grype/db/v6/name/java.go#L19\n\t\t{\n\t\t\tPackageName: \"org.springframework:spring-webmvc\",\n\t\t\tConstraint:  version.MustGetConstraint(\">=5.0.0,<5.1.7\", version.UnknownFormat),\n\t\t\tReference:   vulnerability.Reference{ID: \"CVE-2014-fake-2\", Namespace: \"github:language:\" + syftPkg.Java.String()},\n\t\t},\n\t\t{\n\t\t\tPackageName: \"org.springframework:spring-webmvc\",\n\t\t\tConstraint:  version.MustGetConstraint(\">=5.0.1,<5.1.7\", version.UnknownFormat),\n\t\t\tReference:   vulnerability.Reference{ID: \"CVE-2013-fake-3\", Namespace: \"github:language:\" + syftPkg.Java.String()},\n\t\t},\n\t\t// unexpected...\n\t\t{\n\t\t\tPackageName: \"org.springframework.spring-webmvc\",\n\t\t\tConstraint:  version.MustGetConstraint(\">=5.0.0,<5.0.7\", version.UnknownFormat),\n\t\t\tReference:   vulnerability.Reference{ID: \"CVE-2013-fake-BAD\", Namespace: \"github:language:\" + syftPkg.Java.String()},\n\t\t},\n\t}...)\n}\n\ntype mockMavenSearcher struct {\n\tpkg                  pkg.Package\n\tsimulateRateLimiting bool\n}\n\nfunc (m mockMavenSearcher) GetMavenPackageBySha(context.Context, string) (*pkg.Package, error) {\n\tif m.simulateRateLimiting {\n\t\treturn nil, errors.New(\"you been rate limited\")\n\t}\n\treturn &m.pkg, nil\n}\n"
  },
  {
    "path": "grype/matcher/java/matcher_test.go",
    "content": "package java\n\nimport (\n\t\"testing\"\n\n\t\"github.com/google/uuid\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/grype/grype/match\"\n\t\"github.com/anchore/grype/grype/pkg\"\n\t\"github.com/anchore/grype/internal/stringutil\"\n\tsyftPkg \"github.com/anchore/syft/syft/pkg\"\n)\n\nfunc TestMatcherJava_matchUpstreamMavenPackage(t *testing.T) {\n\tnewMatcher := func(searcher MavenSearcher) *Matcher {\n\t\treturn &Matcher{\n\t\t\tcfg: MatcherConfig{\n\t\t\t\tExternalSearchConfig: ExternalSearchConfig{\n\t\t\t\t\tSearchMavenUpstream: true,\n\t\t\t\t},\n\t\t\t},\n\t\t\tMavenSearcher: searcher,\n\t\t}\n\t}\n\tstore := newMockProvider()\n\n\t// Define test cases\n\ttestCases := []struct {\n\t\ttestname            string\n\t\ttestExpectRateLimit bool\n\t\tpackages            []pkg.Package\n\t}{\n\t\t{\n\t\t\ttestname:            \"do not search maven - metadata present\",\n\t\t\ttestExpectRateLimit: false,\n\t\t\tpackages: []pkg.Package{\n\t\t\t\t{\n\t\t\t\t\tID:       pkg.ID(uuid.NewString()),\n\t\t\t\t\tName:     \"org.springframework.spring-webmvc\",\n\t\t\t\t\tVersion:  \"5.1.5.RELEASE\",\n\t\t\t\t\tLanguage: syftPkg.Java,\n\t\t\t\t\tType:     syftPkg.JavaPkg,\n\t\t\t\t\tMetadata: pkg.JavaMetadata{\n\t\t\t\t\t\tPomArtifactID: \"spring-webmvc\",\n\t\t\t\t\t\tPomGroupID:    \"org.springframework\",\n\t\t\t\t\t\tArchiveDigests: []pkg.Digest{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tAlgorithm: \"sha1\",\n\t\t\t\t\t\t\t\tValue:     \"236e3bfdbdc6c86629237a74f0f11414adb4e211\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\ttestname:            \"search maven - missing metadata\",\n\t\t\ttestExpectRateLimit: false,\n\t\t\tpackages: []pkg.Package{\n\t\t\t\t{\n\t\t\t\t\tID:       pkg.ID(uuid.NewString()),\n\t\t\t\t\tName:     \"org.springframework.spring-webmvc\",\n\t\t\t\t\tVersion:  \"5.1.5.RELEASE\",\n\t\t\t\t\tLanguage: syftPkg.Java,\n\t\t\t\t\tType:     syftPkg.JavaPkg,\n\t\t\t\t\tMetadata: pkg.JavaMetadata{\n\t\t\t\t\t\tPomArtifactID: \"\",\n\t\t\t\t\t\tPomGroupID:    \"\",\n\t\t\t\t\t\tArchiveDigests: []pkg.Digest{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tAlgorithm: \"sha1\",\n\t\t\t\t\t\t\t\tValue:     \"236e3bfdbdc6c86629237a74f0f11414adb4e211\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\ttestname:            \"search maven - missing sha1 error\",\n\t\t\ttestExpectRateLimit: false,\n\t\t\tpackages: []pkg.Package{\n\t\t\t\t{\n\t\t\t\t\tID:       pkg.ID(uuid.NewString()),\n\t\t\t\t\tName:     \"org.springframework.spring-webmvc\",\n\t\t\t\t\tVersion:  \"5.1.5.RELEASE\",\n\t\t\t\t\tLanguage: syftPkg.Java,\n\t\t\t\t\tType:     syftPkg.JavaPkg,\n\t\t\t\t\tMetadata: pkg.JavaMetadata{\n\t\t\t\t\t\tPomArtifactID: \"\",\n\t\t\t\t\t\tPomGroupID:    \"\",\n\t\t\t\t\t\tArchiveDigests: []pkg.Digest{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tAlgorithm: \"sha1\",\n\t\t\t\t\t\t\t\tValue:     \"\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tt.Run(\"matching from maven search results\", func(t *testing.T) {\n\t\tfor _, p := range testCases {\n\t\t\t// Adding test isolation\n\t\t\tt.Run(p.testname, func(t *testing.T) {\n\t\t\t\tmatcher := newMatcher(mockMavenSearcher{\n\t\t\t\t\tpkg: p.packages[0],\n\t\t\t\t})\n\t\t\t\tactual, _ := matcher.matchUpstreamMavenPackages(store, p.packages[0])\n\n\t\t\t\tassert.Len(t, actual, 2, \"unexpected matches count\")\n\n\t\t\t\tfoundCVEs := stringutil.NewStringSet()\n\t\t\t\tfor _, v := range actual {\n\t\t\t\t\tfoundCVEs.Add(v.Vulnerability.ID)\n\n\t\t\t\t\trequire.NotEmpty(t, v.Details)\n\t\t\t\t\tfor _, d := range v.Details {\n\t\t\t\t\t\tassert.Equal(t, match.ExactIndirectMatch, d.Type, \"indirect match not indicated\")\n\t\t\t\t\t\tassert.Equal(t, matcher.Type(), d.Matcher, \"failed to capture matcher type\")\n\t\t\t\t\t}\n\t\t\t\t\tassert.Equal(t, p.packages[0].Name, v.Package.Name, \"failed to capture original package name\")\n\t\t\t\t}\n\n\t\t\t\tfor _, id := range []string{\"CVE-2014-fake-2\", \"CVE-2013-fake-3\"} {\n\t\t\t\t\tif !foundCVEs.Contains(id) {\n\t\t\t\t\t\tt.Errorf(\"missing discovered CVE: %s\", id)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif t.Failed() {\n\t\t\t\t\tt.Logf(\"discovered CVES: %+v\", foundCVEs)\n\t\t\t\t}\n\n\t\t\t})\n\t\t}\n\t})\n\n\tt.Run(\"handles maven rate limiting\", func(t *testing.T) {\n\t\tfor _, p := range testCases {\n\t\t\t// Adding test isolation\n\t\t\tt.Run(p.testname, func(t *testing.T) {\n\t\t\t\tmatcher := newMatcher(mockMavenSearcher{simulateRateLimiting: true})\n\n\t\t\t\t_, err := matcher.matchUpstreamMavenPackages(store, p.packages[0])\n\n\t\t\t\tif p.testExpectRateLimit {\n\t\t\t\t\tassert.Errorf(t, err, \"should have gotten an error from the rate limiting\")\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\t})\n}\n\nfunc TestMatcherJava_shouldSearchMavenBySha(t *testing.T) {\n\tnewMatcher := func(searcher MavenSearcher) *Matcher {\n\t\treturn &Matcher{\n\t\t\tcfg: MatcherConfig{\n\t\t\t\tExternalSearchConfig: ExternalSearchConfig{\n\t\t\t\t\tSearchMavenUpstream: true,\n\t\t\t\t},\n\t\t\t},\n\t\t\tMavenSearcher: searcher,\n\t\t}\n\t}\n\n\t// Define test cases\n\ttestCases := []struct {\n\t\ttestname                  string\n\t\texpectedShouldSearchMaven bool\n\t\ttestExpectedError         bool\n\t\tpackages                  []pkg.Package\n\t}{\n\t\t{\n\t\t\ttestname:                  \"do not search maven - metadata present\",\n\t\t\texpectedShouldSearchMaven: false,\n\t\t\ttestExpectedError:         false,\n\t\t\tpackages: []pkg.Package{\n\t\t\t\t{\n\t\t\t\t\tID:       pkg.ID(uuid.NewString()),\n\t\t\t\t\tName:     \"org.springframework.spring-webmvc\",\n\t\t\t\t\tVersion:  \"5.1.5.RELEASE\",\n\t\t\t\t\tLanguage: syftPkg.Java,\n\t\t\t\t\tType:     syftPkg.JavaPkg,\n\t\t\t\t\tMetadata: pkg.JavaMetadata{\n\t\t\t\t\t\tPomArtifactID: \"spring-webmvc\",\n\t\t\t\t\t\tPomGroupID:    \"org.springframework\",\n\t\t\t\t\t\tArchiveDigests: []pkg.Digest{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tAlgorithm: \"sha1\",\n\t\t\t\t\t\t\t\tValue:     \"236e3bfdbdc6c86629237a74f0f11414adb4e211\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\ttestname:                  \"search maven - missing metadata\",\n\t\t\texpectedShouldSearchMaven: true,\n\t\t\ttestExpectedError:         false,\n\t\t\tpackages: []pkg.Package{\n\t\t\t\t{\n\t\t\t\t\tID:       pkg.ID(uuid.NewString()),\n\t\t\t\t\tName:     \"org.springframework.spring-webmvc\",\n\t\t\t\t\tVersion:  \"5.1.5.RELEASE\",\n\t\t\t\t\tLanguage: syftPkg.Java,\n\t\t\t\t\tType:     syftPkg.JavaPkg,\n\t\t\t\t\tMetadata: pkg.JavaMetadata{\n\t\t\t\t\t\tPomArtifactID: \"\",\n\t\t\t\t\t\tPomGroupID:    \"\",\n\t\t\t\t\t\tArchiveDigests: []pkg.Digest{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tAlgorithm: \"sha1\",\n\t\t\t\t\t\t\t\tValue:     \"236e3bfdbdc6c86629237a74f0f11414adb4e211\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\ttestname:                  \"search maven - missing artifactId\",\n\t\t\texpectedShouldSearchMaven: true,\n\t\t\tpackages: []pkg.Package{\n\t\t\t\t{\n\t\t\t\t\tID:       pkg.ID(uuid.NewString()),\n\t\t\t\t\tName:     \"org.springframework.spring-webmvc\",\n\t\t\t\t\tVersion:  \"5.1.5.RELEASE\",\n\t\t\t\t\tLanguage: syftPkg.Java,\n\t\t\t\t\tType:     syftPkg.JavaPkg,\n\t\t\t\t\tMetadata: pkg.JavaMetadata{\n\t\t\t\t\t\tPomArtifactID: \"\",\n\t\t\t\t\t\tPomGroupID:    \"org.springframework\",\n\t\t\t\t\t\tArchiveDigests: []pkg.Digest{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tAlgorithm: \"sha1\",\n\t\t\t\t\t\t\t\tValue:     \"236e3bfdbdc6c86629237a74f0f11414adb4e211\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\ttestname:                  \"do not search maven - missing sha1\",\n\t\t\texpectedShouldSearchMaven: false,\n\t\t\tpackages: []pkg.Package{\n\t\t\t\t{\n\t\t\t\t\tID:       pkg.ID(uuid.NewString()),\n\t\t\t\t\tName:     \"org.springframework.spring-webmvc\",\n\t\t\t\t\tVersion:  \"5.1.5.RELEASE\",\n\t\t\t\t\tLanguage: syftPkg.Java,\n\t\t\t\t\tType:     syftPkg.JavaPkg,\n\t\t\t\t\tMetadata: pkg.JavaMetadata{\n\t\t\t\t\t\tPomArtifactID: \"\",\n\t\t\t\t\t\tPomGroupID:    \"\",\n\t\t\t\t\t\tArchiveDigests: []pkg.Digest{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tAlgorithm: \"sha1\",\n\t\t\t\t\t\t\t\tValue:     \"\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tt.Run(\"matching from Maven search results\", func(t *testing.T) {\n\t\tfor _, p := range testCases {\n\t\t\t// Adding test isolation\n\t\t\tt.Run(p.testname, func(t *testing.T) {\n\t\t\t\tmatcher := newMatcher(mockMavenSearcher{\n\t\t\t\t\tpkg: p.packages[0],\n\t\t\t\t})\n\t\t\t\tactual, digests := matcher.shouldSearchMavenBySha(p.packages[0])\n\n\t\t\t\tassert.Equal(t, p.expectedShouldSearchMaven, actual, \"unexpected decision to search Maven\")\n\n\t\t\t\tif actual {\n\t\t\t\t\tassert.NotEmpty(t, digests, \"sha digests should not be empty when search is expected\")\n\t\t\t\t}\n\n\t\t\t})\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "grype/matcher/java/maven_search.go",
    "content": "package java\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"sort\"\n\t\"time\"\n\n\t\"golang.org/x/time/rate\"\n\n\t\"github.com/anchore/grype/grype/pkg\"\n\tsyftPkg \"github.com/anchore/syft/syft/pkg\"\n)\n\n// MavenSearcher is the interface that wraps the GetMavenPackageBySha method.\ntype MavenSearcher interface {\n\t// GetMavenPackageBySha provides an interface for building a package from maven data based on a sha1 digest\n\tGetMavenPackageBySha(context.Context, string) (*pkg.Package, error)\n}\n\n// mavenSearch implements the MavenSearcher interface\ntype mavenSearch struct {\n\tclient      *http.Client\n\tbaseURL     string\n\trateLimiter *rate.Limiter\n}\n\n// newMavenSearch creates a new mavenSearch instance with rate limiting\n// rate is specified as 1 request per 300ms\nfunc newMavenSearch(client *http.Client, baseURL string, rateLimit time.Duration) *mavenSearch {\n\treturn &mavenSearch{\n\t\tclient:      client,\n\t\tbaseURL:     baseURL,\n\t\trateLimiter: rate.NewLimiter(rate.Every(rateLimit), 1),\n\t}\n}\n\ntype mavenAPIResponse struct {\n\tResponse struct {\n\t\tNumFound int `json:\"numFound\"`\n\t\tDocs     []struct {\n\t\t\tID           string `json:\"id\"`\n\t\t\tGroupID      string `json:\"g\"`\n\t\t\tArtifactID   string `json:\"a\"`\n\t\t\tVersion      string `json:\"v\"`\n\t\t\tP            string `json:\"p\"`\n\t\t\tVersionCount int    `json:\"versionCount\"`\n\t\t} `json:\"docs\"`\n\t} `json:\"response\"`\n}\n\nfunc (ms *mavenSearch) GetMavenPackageBySha(ctx context.Context, sha1 string) (*pkg.Package, error) {\n\tif sha1 == \"\" {\n\t\treturn nil, errors.New(\"empty sha1 digest\")\n\t}\n\tif ms.baseURL == \"\" {\n\t\treturn nil, errors.New(\"empty maven search URL\")\n\t}\n\tif ms.rateLimiter == nil {\n\t\treturn nil, errors.New(\"rate limiter not initialized\")\n\t}\n\tif ms.client == nil {\n\t\treturn nil, errors.New(\"HTTP client not initialized\")\n\t}\n\n\t// Wait for rate limiter\n\terr := ms.rateLimiter.Wait(ctx)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"rate limiter error: %w\", err)\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, ms.baseURL, nil)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to initialize HTTP client: %w\", err)\n\t}\n\n\tq := req.URL.Query()\n\tq.Set(\"q\", fmt.Sprintf(sha1Query, sha1))\n\tq.Set(\"rows\", \"1\")\n\tq.Set(\"wt\", \"json\")\n\treq.URL.RawQuery = q.Encode()\n\n\tresp, err := ms.client.Do(req)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"sha1 search error: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn nil, fmt.Errorf(\"status %s from %s\", resp.Status, req.URL.String())\n\t}\n\n\tvar res mavenAPIResponse\n\tif err = json.NewDecoder(resp.Body).Decode(&res); err != nil {\n\t\treturn nil, fmt.Errorf(\"json decode error: %w\", err)\n\t}\n\n\tif len(res.Response.Docs) == 0 {\n\t\treturn nil, fmt.Errorf(\"digest %s: %w\", sha1, errors.New(\"no artifact found\"))\n\t}\n\n\t// artifacts might have the same SHA-1 digests.\n\t// e.g. \"javax.servlet:jstl\" and \"jstl:jstl\"\n\tdocs := res.Response.Docs\n\tsort.Slice(docs, func(i, j int) bool {\n\t\treturn docs[i].ID < docs[j].ID\n\t})\n\td := docs[0]\n\n\treturn &pkg.Package{\n\t\tName:     fmt.Sprintf(\"%s:%s\", d.GroupID, d.ArtifactID),\n\t\tVersion:  d.Version,\n\t\tLanguage: syftPkg.Java,\n\t\tMetadata: pkg.JavaMetadata{\n\t\t\tPomArtifactID: d.ArtifactID,\n\t\t\tPomGroupID:    d.GroupID,\n\t\t},\n\t}, nil\n}\n"
  },
  {
    "path": "grype/matcher/java/maven_test.go",
    "content": "package java\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\t\"time\"\n\n\t\"golang.org/x/time/rate\"\n)\n\nfunc TestNewMavenSearchRateLimiter(t *testing.T) {\n\t// Create a test server\n\tts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t// We don't need to respond with anything for this test\n\t}))\n\tdefer ts.Close()\n\n\tt.Run(\"custom rate limit initialization\", func(t *testing.T) {\n\t\tcustomDuration := 500 * time.Millisecond\n\t\tms := newMavenSearch(http.DefaultClient, ts.URL, customDuration)\n\n\t\texpectedRate := rate.Every(customDuration)\n\t\tif ms.rateLimiter.Limit() != expectedRate {\n\t\t\tt.Errorf(\"unexpected rate limit: got %v, want %v\", ms.rateLimiter.Limit(), rate.Limit(expectedRate))\n\t\t}\n\t})\n\n\tt.Run(\"default rate limit initialization\", func(t *testing.T) {\n\t\tdefaultDuration := 300 * time.Millisecond\n\t\tms := newMavenSearch(http.DefaultClient, ts.URL, defaultDuration)\n\n\t\texpectedRate := rate.Every(defaultDuration)\n\t\tif ms.rateLimiter.Limit() != expectedRate {\n\t\t\tt.Errorf(\"unexpected rate limit: got %v, want %v\", ms.rateLimiter.Limit(), rate.Limit(expectedRate))\n\t\t}\n\t})\n\n\tt.Run(\"rate limiter behavior\", func(t *testing.T) {\n\t\tms := newMavenSearch(http.DefaultClient, ts.URL, 200*time.Millisecond)\n\t\tctx := context.Background()\n\n\t\t// First request should proceed immediately\n\t\tstart := time.Now()\n\t\terr := ms.rateLimiter.Wait(ctx)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"unexpected error on first wait: %v\", err)\n\t\t}\n\t\tif elapsed := time.Since(start); elapsed > 50*time.Millisecond {\n\t\t\tt.Errorf(\"first request took too long: %v\", elapsed)\n\t\t}\n\n\t\t// Second request should be delayed\n\t\tstart = time.Now()\n\t\terr = ms.rateLimiter.Wait(ctx)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"unexpected error on second wait: %v\", err)\n\t\t}\n\t\tif elapsed := time.Since(start); elapsed < 150*time.Millisecond {\n\t\t\tt.Errorf(\"rate limiting not enforced, second request took: %v\", elapsed)\n\t\t}\n\t})\n\n\tt.Run(\"config integration\", func(t *testing.T) {\n\t\ttestCases := []struct {\n\t\t\tname      string\n\t\t\trateLimit time.Duration\n\t\t\twant      rate.Limit\n\t\t}{\n\t\t\t{\n\t\t\t\tname:      \"with default rate limit\",\n\t\t\t\trateLimit: 300 * time.Millisecond,\n\t\t\t\twant:      rate.Every(300 * time.Millisecond),\n\t\t\t},\n\t\t\t{\n\t\t\t\tname:      \"with custom rate limit\",\n\t\t\t\trateLimit: 500 * time.Millisecond,\n\t\t\t\twant:      rate.Every(500 * time.Millisecond),\n\t\t\t},\n\t\t}\n\n\t\tfor _, tc := range testCases {\n\t\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\t\tms := newMavenSearch(http.DefaultClient, ts.URL, tc.rateLimit)\n\t\t\t\tif ms.rateLimiter.Limit() != tc.want {\n\t\t\t\t\tt.Errorf(\"rate limit = %v, want %v\", ms.rateLimiter.Limit(), tc.want)\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\t})\n}\n\nfunc withinDelta(got, want, delta time.Duration) bool {\n\tdiff := got - want\n\tif diff < 0 {\n\t\tdiff = -diff\n\t}\n\treturn diff <= delta\n}\n"
  },
  {
    "path": "grype/matcher/javascript/matcher.go",
    "content": "package javascript\n\nimport (\n\t\"github.com/anchore/grype/grype/match\"\n\t\"github.com/anchore/grype/grype/matcher/internal\"\n\t\"github.com/anchore/grype/grype/pkg\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n\tsyftPkg \"github.com/anchore/syft/syft/pkg\"\n)\n\ntype Matcher struct {\n\tcfg MatcherConfig\n}\n\ntype MatcherConfig struct {\n\tUseCPEs bool\n}\n\nfunc NewJavascriptMatcher(cfg MatcherConfig) *Matcher {\n\treturn &Matcher{\n\t\tcfg: cfg,\n\t}\n}\n\nfunc (m *Matcher) PackageTypes() []syftPkg.Type {\n\treturn []syftPkg.Type{syftPkg.NpmPkg}\n}\n\nfunc (m *Matcher) Type() match.MatcherType {\n\treturn match.JavascriptMatcher\n}\n\nfunc (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoreFilter, error) {\n\treturn internal.MatchPackageByEcosystemAndCPEs(store, p, m.Type(), m.cfg.UseCPEs)\n}\n"
  },
  {
    "path": "grype/matcher/matchers.go",
    "content": "package matcher\n\nimport (\n\t\"github.com/anchore/grype/grype/match\"\n\t\"github.com/anchore/grype/grype/matcher/apk\"\n\t\"github.com/anchore/grype/grype/matcher/bitnami\"\n\t\"github.com/anchore/grype/grype/matcher/dotnet\"\n\t\"github.com/anchore/grype/grype/matcher/dpkg\"\n\t\"github.com/anchore/grype/grype/matcher/golang\"\n\t\"github.com/anchore/grype/grype/matcher/hex\"\n\t\"github.com/anchore/grype/grype/matcher/java\"\n\t\"github.com/anchore/grype/grype/matcher/javascript\"\n\t\"github.com/anchore/grype/grype/matcher/msrc\"\n\t\"github.com/anchore/grype/grype/matcher/pacman\"\n\t\"github.com/anchore/grype/grype/matcher/portage\"\n\t\"github.com/anchore/grype/grype/matcher/python\"\n\t\"github.com/anchore/grype/grype/matcher/rpm\"\n\t\"github.com/anchore/grype/grype/matcher/ruby\"\n\t\"github.com/anchore/grype/grype/matcher/rust\"\n\t\"github.com/anchore/grype/grype/matcher/stock\"\n)\n\n// Config contains values used by individual matcher structs for advanced configuration\ntype Config struct {\n\tJava       java.MatcherConfig\n\tRuby       ruby.MatcherConfig\n\tPython     python.MatcherConfig\n\tDotnet     dotnet.MatcherConfig\n\tJavascript javascript.MatcherConfig\n\tGolang     golang.MatcherConfig\n\tRust       rust.MatcherConfig\n\tHex        hex.MatcherConfig\n\tStock      stock.MatcherConfig\n\tDpkg       dpkg.MatcherConfig\n\tRpm        rpm.MatcherConfig\n}\n\nfunc NewDefaultMatchers(mc Config) []match.Matcher {\n\treturn []match.Matcher{\n\t\tdpkg.NewDpkgMatcher(mc.Dpkg),\n\t\truby.NewRubyMatcher(mc.Ruby),\n\t\tpython.NewPythonMatcher(mc.Python),\n\t\tdotnet.NewDotnetMatcher(mc.Dotnet),\n\t\trpm.NewRpmMatcher(mc.Rpm),\n\t\tjava.NewJavaMatcher(mc.Java),\n\t\tjavascript.NewJavascriptMatcher(mc.Javascript),\n\t\t&apk.Matcher{},\n\t\tgolang.NewGolangMatcher(mc.Golang),\n\t\t&msrc.Matcher{},\n\t\t&portage.Matcher{},\n\t\trust.NewRustMatcher(mc.Rust),\n\t\thex.NewHexMatcher(mc.Hex),\n\t\tstock.NewStockMatcher(mc.Stock),\n\t\t&bitnami.Matcher{},\n\t\t&pacman.Matcher{},\n\t}\n}\n"
  },
  {
    "path": "grype/matcher/mock/matcher.go",
    "content": "package mock\n\nimport (\n\t\"errors\"\n\n\t\"github.com/anchore/grype/grype/match\"\n\t\"github.com/anchore/grype/grype/pkg\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n\tsyftPkg \"github.com/anchore/syft/syft/pkg\"\n)\n\n// MatchFunc is a function that takes a vulnerability provider and a package,\n// and returns matches, ignored matches, and an error.\ntype MatchFunc func(vp vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoreFilter, error)\n\n// Matcher is a mock implementation of the match.Matcher interface. This is\n// intended for testing purposes only.\ntype Matcher struct {\n\ttyp       syftPkg.Type\n\tmatchFunc MatchFunc\n}\n\n// New creates a new mock Matcher with the given type and match function.\nfunc New(typ syftPkg.Type, matchFunc MatchFunc) *Matcher {\n\treturn &Matcher{\n\t\ttyp:       typ,\n\t\tmatchFunc: matchFunc,\n\t}\n}\n\nfunc (m Matcher) PackageTypes() []syftPkg.Type {\n\treturn []syftPkg.Type{m.typ}\n}\n\nfunc (m Matcher) Type() match.MatcherType {\n\treturn \"MOCK\"\n}\n\nfunc (m Matcher) Match(vp vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoreFilter, error) {\n\tif m.matchFunc != nil {\n\t\treturn m.matchFunc(vp, p)\n\t}\n\n\treturn nil, nil, errors.New(\"no match function provided\")\n}\n"
  },
  {
    "path": "grype/matcher/msrc/matcher.go",
    "content": "package msrc\n\nimport (\n\t\"github.com/anchore/grype/grype/match\"\n\t\"github.com/anchore/grype/grype/matcher/internal\"\n\t\"github.com/anchore/grype/grype/pkg\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n\tsyftPkg \"github.com/anchore/syft/syft/pkg\"\n)\n\ntype Matcher struct {\n}\n\nfunc (m *Matcher) PackageTypes() []syftPkg.Type {\n\t// This looks like there is a special package, but in reality, this is just\n\t// a workaround. MSRC matching is done at the KB-patch level, and so this\n\t// treats KBs as \"packages\" but they aren't packages, they are patches\n\treturn []syftPkg.Type{syftPkg.KbPkg}\n}\n\nfunc (m *Matcher) Type() match.MatcherType {\n\treturn match.MsrcMatcher\n}\n\nfunc (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoreFilter, error) {\n\t// find KB matches for the MSFT version given in the package and version.\n\t// The \"package\" holds the information about the Windows version, and its\n\t// patch (KB)\n\treturn internal.MatchPackageByEcosystemPackageName(store, p, p.Name, m.Type())\n}\n"
  },
  {
    "path": "grype/matcher/pacman/matcher.go",
    "content": "package pacman\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/anchore/grype/grype/match\"\n\t\"github.com/anchore/grype/grype/matcher/internal\"\n\t\"github.com/anchore/grype/grype/pkg\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n\tsyftPkg \"github.com/anchore/syft/syft/pkg\"\n)\n\ntype Matcher struct{}\n\nfunc (m *Matcher) PackageTypes() []syftPkg.Type {\n\treturn []syftPkg.Type{syftPkg.AlpmPkg}\n}\n\nfunc (m *Matcher) Type() match.MatcherType {\n\treturn match.PacmanMatcher\n}\n\nfunc (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoreFilter, error) {\n\tmatches, ignoreFilters, err := internal.MatchPackageByDistro(store, p, nil, m.Type(), nil)\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"failed to match pacman package: %w\", err)\n\t}\n\treturn matches, ignoreFilters, nil\n}\n"
  },
  {
    "path": "grype/matcher/pacman/matcher_test.go",
    "content": "package pacman\n\nimport (\n\t\"testing\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/google/go-cmp/cmp/cmpopts\"\n\t\"github.com/google/uuid\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/grype/grype/distro\"\n\t\"github.com/anchore/grype/grype/match\"\n\t\"github.com/anchore/grype/grype/pkg\"\n\t\"github.com/anchore/grype/grype/version\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n\t\"github.com/anchore/grype/grype/vulnerability/mock\"\n\tsyftPkg \"github.com/anchore/syft/syft/pkg\"\n)\n\nfunc TestMatcherType(t *testing.T) {\n\tm := Matcher{}\n\tassert.Equal(t, match.PacmanMatcher, m.Type())\n}\n\nfunc TestMatcherPackageTypes(t *testing.T) {\n\tm := Matcher{}\n\tassert.Equal(t, []syftPkg.Type{syftPkg.AlpmPkg}, m.PackageTypes())\n}\n\nfunc TestMatch(t *testing.T) {\n\tarchVuln := vulnerability.Vulnerability{\n\t\tReference: vulnerability.Reference{\n\t\t\tID:        \"AVG-1234\",\n\t\t\tNamespace: \"arch:distro:archlinux:rolling\",\n\t\t},\n\t\tPackageName: \"curl\",\n\t\tConstraint:  version.MustGetConstraint(\"< 8.5.0-1\", version.PacmanFormat),\n\t}\n\n\tvp := mock.VulnerabilityProvider(archVuln)\n\n\tm := Matcher{}\n\td := distro.New(distro.ArchLinux, \"\", \"rolling\")\n\n\tp := pkg.Package{\n\t\tID:      pkg.ID(uuid.NewString()),\n\t\tName:    \"curl\",\n\t\tVersion: \"8.4.0-1\",\n\t\tType:    syftPkg.AlpmPkg,\n\t\tDistro:  d,\n\t}\n\n\texpected := []match.Match{\n\t\t{\n\t\t\tVulnerability: archVuln,\n\t\t\tPackage:       p,\n\t\t\tDetails: []match.Detail{\n\t\t\t\t{\n\t\t\t\t\tType:       match.ExactDirectMatch,\n\t\t\t\t\tConfidence: 1.0,\n\t\t\t\t\tSearchedBy: match.DistroParameters{\n\t\t\t\t\t\tDistro: match.DistroIdentification{\n\t\t\t\t\t\t\tType:    d.Type.String(),\n\t\t\t\t\t\t\tVersion: d.Version,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tPackage: match.PackageParameter{\n\t\t\t\t\t\t\tName:    \"curl\",\n\t\t\t\t\t\t\tVersion: \"8.4.0-1\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tNamespace: \"arch:distro:archlinux:rolling\",\n\t\t\t\t\t},\n\t\t\t\t\tFound: match.DistroResult{\n\t\t\t\t\t\tVulnerabilityID:   \"AVG-1234\",\n\t\t\t\t\t\tVersionConstraint: archVuln.Constraint.String(),\n\t\t\t\t\t},\n\t\t\t\t\tMatcher: match.PacmanMatcher,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tactual, _, err := m.Match(vp, p)\n\trequire.NoError(t, err)\n\n\tassertMatches(t, expected, actual)\n}\n\nfunc TestMatchNoVulnerability(t *testing.T) {\n\tarchVuln := vulnerability.Vulnerability{\n\t\tReference: vulnerability.Reference{\n\t\t\tID:        \"AVG-1234\",\n\t\t\tNamespace: \"arch:distro:archlinux:rolling\",\n\t\t},\n\t\tPackageName: \"curl\",\n\t\tConstraint:  version.MustGetConstraint(\"< 8.0.0-1\", version.PacmanFormat),\n\t}\n\n\tvp := mock.VulnerabilityProvider(archVuln)\n\n\tm := Matcher{}\n\td := distro.New(distro.ArchLinux, \"\", \"rolling\")\n\n\t// Package version is newer than the constraint, should not match\n\tp := pkg.Package{\n\t\tID:      pkg.ID(uuid.NewString()),\n\t\tName:    \"curl\",\n\t\tVersion: \"8.5.0-1\",\n\t\tType:    syftPkg.AlpmPkg,\n\t\tDistro:  d,\n\t}\n\n\tactual, _, err := m.Match(vp, p)\n\trequire.NoError(t, err)\n\tassert.Empty(t, actual)\n}\n\nfunc TestMatchWithEpoch(t *testing.T) {\n\tarchVuln := vulnerability.Vulnerability{\n\t\tReference: vulnerability.Reference{\n\t\t\tID:        \"AVG-5678\",\n\t\t\tNamespace: \"arch:distro:archlinux:rolling\",\n\t\t},\n\t\tPackageName: \"openssl\",\n\t\tConstraint:  version.MustGetConstraint(\"< 1:3.0.8-1\", version.PacmanFormat),\n\t}\n\n\tvp := mock.VulnerabilityProvider(archVuln)\n\n\tm := Matcher{}\n\td := distro.New(distro.ArchLinux, \"\", \"rolling\")\n\n\tp := pkg.Package{\n\t\tID:      pkg.ID(uuid.NewString()),\n\t\tName:    \"openssl\",\n\t\tVersion: \"1:3.0.7-4\",\n\t\tType:    syftPkg.AlpmPkg,\n\t\tDistro:  d,\n\t}\n\n\tactual, _, err := m.Match(vp, p)\n\trequire.NoError(t, err)\n\trequire.Len(t, actual, 1)\n\tassert.Equal(t, \"AVG-5678\", actual[0].Vulnerability.ID)\n}\n\nfunc TestMatchNilDistro(t *testing.T) {\n\tm := Matcher{}\n\n\tp := pkg.Package{\n\t\tID:      pkg.ID(uuid.NewString()),\n\t\tName:    \"curl\",\n\t\tVersion: \"8.4.0-1\",\n\t\tType:    syftPkg.AlpmPkg,\n\t\tDistro:  nil,\n\t}\n\n\tactual, _, err := m.Match(mock.VulnerabilityProvider(), p)\n\trequire.NoError(t, err)\n\tassert.Empty(t, actual)\n}\n\nfunc assertMatches(t *testing.T, expected, actual []match.Match) {\n\tt.Helper()\n\topts := []cmp.Option{\n\t\tcmpopts.IgnoreFields(vulnerability.Vulnerability{}, \"Constraint\"),\n\t\tcmpopts.IgnoreFields(pkg.Package{}, \"Locations\"),\n\t\tcmpopts.IgnoreUnexported(distro.Distro{}),\n\t}\n\n\tif diff := cmp.Diff(expected, actual, opts...); diff != \"\" {\n\t\tt.Errorf(\"mismatch (-want +got):\\n%s\", diff)\n\t}\n}\n"
  },
  {
    "path": "grype/matcher/portage/matcher.go",
    "content": "package portage\n\nimport (\n\t\"github.com/anchore/grype/grype/match\"\n\t\"github.com/anchore/grype/grype/matcher/internal\"\n\t\"github.com/anchore/grype/grype/pkg\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n\tsyftPkg \"github.com/anchore/syft/syft/pkg\"\n)\n\ntype Matcher struct {\n}\n\nfunc (m *Matcher) PackageTypes() []syftPkg.Type {\n\treturn []syftPkg.Type{syftPkg.PortagePkg}\n}\n\nfunc (m *Matcher) Type() match.MatcherType {\n\treturn match.PortageMatcher\n}\n\nfunc (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoreFilter, error) {\n\t// Portage doesn't use epochs, so pass nil for the config\n\treturn internal.MatchPackageByDistro(store, p, nil, m.Type(), nil)\n}\n"
  },
  {
    "path": "grype/matcher/portage/matcher_mocks_test.go",
    "content": "package portage\n\nimport (\n\t\"github.com/anchore/grype/grype/version\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n\t\"github.com/anchore/grype/grype/vulnerability/mock\"\n)\n\nfunc newMockProvider() vulnerability.Provider {\n\treturn mock.VulnerabilityProvider([]vulnerability.Vulnerability{\n\t\t// direct...\n\t\t{\n\t\t\tPackageName: \"app-misc/neutron\",\n\t\t\tConstraint:  version.MustGetConstraint(\"< 2014.1.3\", version.PortageFormat),\n\t\t\tReference:   vulnerability.Reference{ID: \"CVE-2014-fake-1\", Namespace: \"secdb:distro:gentoo:\"},\n\t\t},\n\t\t{\n\t\t\tPackageName: \"app-misc/neutron\",\n\t\t\tConstraint:  version.MustGetConstraint(\"< 2014.1.4\", version.PortageFormat),\n\t\t\tReference:   vulnerability.Reference{ID: \"CVE-2014-fake-2\", Namespace: \"secdb:distro:gentoo:\"},\n\t\t},\n\t}...)\n}\n"
  },
  {
    "path": "grype/matcher/portage/matcher_test.go",
    "content": "package portage\n\nimport (\n\t\"testing\"\n\n\t\"github.com/google/uuid\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/grype/grype/distro\"\n\t\"github.com/anchore/grype/grype/pkg\"\n\t\"github.com/anchore/grype/internal/stringutil\"\n\tsyftPkg \"github.com/anchore/syft/syft/pkg\"\n)\n\nfunc TestMatcherPortage_Match(t *testing.T) {\n\tmatcher := Matcher{}\n\n\td := distro.New(distro.Gentoo, \"\", \"\")\n\n\tp := pkg.Package{\n\t\tID:      pkg.ID(uuid.NewString()),\n\t\tName:    \"app-misc/neutron\",\n\t\tVersion: \"2014.1.3\",\n\t\tType:    syftPkg.PortagePkg,\n\t\tDistro:  d,\n\t}\n\n\tstore := newMockProvider()\n\tactual, _, err := matcher.Match(store, p)\n\tassert.NoError(t, err, \"unexpected err from Match\", err)\n\n\tassert.Len(t, actual, 1, \"unexpected indirect matches count\")\n\n\tfoundCVEs := stringutil.NewStringSet()\n\tfor _, a := range actual {\n\t\tfoundCVEs.Add(a.Vulnerability.ID)\n\n\t\trequire.NotEmpty(t, a.Details)\n\t\tassert.Equal(t, p.Name, a.Package.Name, \"failed to capture original package name\")\n\t\tfor _, detail := range a.Details {\n\t\t\tassert.Equal(t, matcher.Type(), detail.Matcher, \"failed to capture matcher type\")\n\t\t}\n\t}\n\n\tfor _, id := range []string{\"CVE-2014-fake-2\"} {\n\t\tif !foundCVEs.Contains(id) {\n\t\t\tt.Errorf(\"missing discovered CVE: %s\", id)\n\t\t}\n\t}\n\tif t.Failed() {\n\t\tt.Logf(\"discovered CVES: %+v\", foundCVEs)\n\t}\n}\n"
  },
  {
    "path": "grype/matcher/python/matcher.go",
    "content": "package python\n\nimport (\n\t\"github.com/anchore/grype/grype/match\"\n\t\"github.com/anchore/grype/grype/matcher/internal\"\n\t\"github.com/anchore/grype/grype/pkg\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n\tsyftPkg \"github.com/anchore/syft/syft/pkg\"\n)\n\ntype Matcher struct {\n\tcfg MatcherConfig\n}\n\ntype MatcherConfig struct {\n\tUseCPEs bool\n}\n\nfunc NewPythonMatcher(cfg MatcherConfig) *Matcher {\n\treturn &Matcher{\n\t\tcfg: cfg,\n\t}\n}\n\nfunc (m *Matcher) PackageTypes() []syftPkg.Type {\n\treturn []syftPkg.Type{syftPkg.PythonPkg}\n}\n\nfunc (m *Matcher) Type() match.MatcherType {\n\treturn match.PythonMatcher\n}\n\nfunc (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoreFilter, error) {\n\treturn internal.MatchPackageByEcosystemAndCPEs(store, p, m.Type(), m.cfg.UseCPEs)\n}\n"
  },
  {
    "path": "grype/matcher/rpm/almalinux.go",
    "content": "package rpm\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/anchore/grype/grype/distro\"\n\t\"github.com/anchore/grype/grype/match\"\n\t\"github.com/anchore/grype/grype/matcher/internal\"\n\t\"github.com/anchore/grype/grype/matcher/internal/result\"\n\t\"github.com/anchore/grype/grype/pkg\"\n\t\"github.com/anchore/grype/grype/search\"\n\t\"github.com/anchore/grype/grype/version\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n\t\"github.com/anchore/grype/internal/log\"\n)\n\n// shouldUseAlmaLinuxMatching determines if AlmaLinux-specific matching should be used\nfunc shouldUseAlmaLinuxMatching(d *distro.Distro) bool {\n\tif d == nil {\n\t\treturn false\n\t}\n\treturn d.Type == distro.AlmaLinux\n}\n\n// markAsIndirectMatches updates all match details in the result set to indicate indirect matches\nfunc markAsIndirectMatches(results result.Set) result.Set {\n\tupdated := make(result.Set)\n\tfor key, resultList := range results {\n\t\tvar updatedResults []result.Result\n\t\tfor _, r := range resultList {\n\t\t\t// Update all Details to ExactIndirectMatch\n\t\t\tupdatedDetails := make([]match.Detail, len(r.Details))\n\t\t\tfor i, detail := range r.Details {\n\t\t\t\tupdatedDetails[i] = detail\n\t\t\t\tupdatedDetails[i].Type = match.ExactIndirectMatch\n\t\t\t}\n\t\t\tr.Details = updatedDetails\n\t\t\tupdatedResults = append(updatedResults, r)\n\t\t}\n\t\tupdated[key] = updatedResults\n\t}\n\treturn updated\n}\n\n// almaLinuxMatchesWithUpstreams handles AlmaLinux matching for both the binary package and its upstream packages\n// This function orchestrates the complete AlmaLinux matching flow:\n// 1. Search for RHEL disclosures for the binary package\n// 2. Search for RHEL disclosures for all upstream (source) packages\n// 3. Search for AlmaLinux unaffected records for the binary package and related packages\n// 4. Apply filtering logic to determine which disclosures are still vulnerable on AlmaLinux\nfunc almaLinuxMatchesWithUpstreams(provider result.Provider, binaryPkg pkg.Package) ([]match.Match, error) {\n\tif strings.HasSuffix(binaryPkg.Name, \"-debuginfo\") || strings.HasSuffix(binaryPkg.Name, \"-debugsource\") {\n\t\treturn nil, nil // almalinux explicitly never publishes advisories for RPMs that are only debug material\n\t}\n\n\t// Create a RHEL-compatible distro for finding base disclosures\n\trhelCompatibleDistro := *binaryPkg.Distro\n\trhelCompatibleDistro.Type = distro.RedHat // treat as RHEL for disclosure lookup\n\n\tpkgVersion := version.New(binaryPkg.Version, pkg.VersionFormat(binaryPkg))\n\n\t// Step 1: Find RHEL disclosures for the binary package (direct match)\n\tbinaryDisclosures, err := provider.FindResults(\n\t\tsearch.ByPackageName(binaryPkg.Name),\n\t\tsearch.ByDistro(rhelCompatibleDistro),\n\t\tinternal.OnlyQualifiedPackages(binaryPkg),\n\t\tinternal.OnlyVulnerableVersions(pkgVersion),\n\t)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"matcher failed to fetch RHEL disclosures for AlmaLinux binary pkg=%q: %w\", binaryPkg.Name, err)\n\t}\n\n\t// Step 2: Find RHEL disclosures for upstream (source) packages (indirect match)\n\t// Note: We do NOT add epochs to upstream package versions because sourceRPMs often omit epochs\n\t// even when the source package has a non-zero epoch. See the comment in matchUpstreamPackages\n\t// in matcher.go for the full explanation of why this is necessary.\n\tupstreamDisclosures := result.Set{}\n\tfor _, upstreamPkg := range pkg.UpstreamPackages(binaryPkg) {\n\t\t// Create a version object from the upstream package WITHOUT adding epoch\n\t\t// This avoids false positives where binary package epochs differ from source package epochs\n\t\tupstreamVersion := version.New(upstreamPkg.Version, pkg.VersionFormat(upstreamPkg))\n\n\t\tupstreamResults, err := provider.FindResults(\n\t\t\tsearch.ByPackageName(upstreamPkg.Name),\n\t\t\tsearch.ByDistro(rhelCompatibleDistro),\n\t\t\tinternal.OnlyQualifiedPackages(upstreamPkg),\n\t\t\tinternal.OnlyVulnerableVersions(upstreamVersion),\n\t\t)\n\t\tif err != nil {\n\t\t\tlog.WithFields(\"error\", err, \"upstreamPkg\", upstreamPkg.Name, \"binaryPkg\", binaryPkg.Name).Debug(\"failed to fetch RHEL disclosures for upstream package\")\n\t\t\tcontinue\n\t\t}\n\n\t\t// Mark these as indirect matches since they came from upstream package search\n\t\tupstreamResults = markAsIndirectMatches(upstreamResults)\n\n\t\tupstreamDisclosures = upstreamDisclosures.Merge(upstreamResults)\n\t}\n\n\t// Merge all disclosures (binary + upstream)\n\tallDisclosures := binaryDisclosures.Merge(upstreamDisclosures)\n\n\tif len(allDisclosures) == 0 {\n\t\treturn nil, nil\n\t}\n\n\t// Step 3: Find AlmaLinux unaffected records for the binary package\n\tdirectUnaffected, err := provider.FindResults(\n\t\tsearch.ByPackageName(binaryPkg.Name),\n\t\tsearch.ByExactDistro(*binaryPkg.Distro), // use exact AlmaLinux distro for unaffected lookup (no aliases)\n\t\tinternal.OnlyQualifiedPackages(binaryPkg),\n\t\tsearch.ForUnaffected(),\n\t)\n\tif err != nil {\n\t\tlog.WithFields(\"error\", err, \"distro\", binaryPkg.Distro, \"pkg\", binaryPkg.Name).Debug(\"failed to fetch AlmaLinux unaffected packages\")\n\t\t// If we can't get unaffected data, return the original disclosures\n\t\treturn allDisclosures.ToMatches(), nil\n\t}\n\n\t// Step 4: Find AlmaLinux unaffected records for related packages (source/binary relationships)\n\t// This handles cases where AlmaLinux publishes unaffected records for binary packages (e.g., python3-tkinter)\n\t// but the disclosure is for the source package (e.g., python3)\n\trelatedUnaffected := findRelatedUnaffectedPackages(provider, binaryPkg)\n\n\t// Merge all unaffected results\n\tallUnaffected := directUnaffected.Merge(relatedUnaffected)\n\n\t// Step 5: Apply filtering logic: if disclosure exists and no fix applies, the package is vulnerable\n\tupdatedDisclosures := applyAlmaLinuxUnaffectedFiltering(allDisclosures, allUnaffected, pkgVersion)\n\n\treturn updatedDisclosures.ToMatches(), nil\n}\n\n// findRelatedUnaffectedPackages searches for unaffected packages using source/binary RPM relationships\nfunc findRelatedUnaffectedPackages(provider result.Provider, searchPkg pkg.Package) result.Set {\n\tallResults := make(result.Set)\n\n\t// Get all related package names (source RPM, binary RPM patterns, etc.)\n\trelatedNames := getRelatedPackageNames(searchPkg)\n\n\tfor _, relatedName := range relatedNames {\n\t\tif relatedName == searchPkg.Name {\n\t\t\tcontinue // skip the main package name as it's already searched\n\t\t}\n\n\t\t// Search for unaffected records using related package names\n\t\trelatedResults, err := provider.FindResults(\n\t\t\tsearch.ByPackageName(relatedName),\n\t\t\tsearch.ByExactDistro(*searchPkg.Distro), // use exact distro to avoid alias mapping\n\t\t\tinternal.OnlyQualifiedPackages(searchPkg),\n\t\t\tsearch.ForUnaffected(),\n\t\t)\n\t\tif err != nil {\n\t\t\tlog.WithFields(\"error\", err, \"relatedName\", relatedName, \"originalPkg\", searchPkg.Name).Debug(\"failed to fetch related unaffected packages\")\n\t\t\tcontinue\n\t\t}\n\n\t\tif len(relatedResults) > 0 {\n\t\t\tlog.WithFields(\"relatedName\", relatedName, \"originalPkg\", searchPkg.Name, \"foundUnaffected\", len(relatedResults)).Trace(\"found unaffected records via package relationship\")\n\t\t\t// Merge results into our set\n\t\t\tfor key, results := range relatedResults {\n\t\t\t\tallResults[key] = append(allResults[key], results...)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn allResults\n}\n\n// applyAlmaLinuxUnaffectedFiltering applies AlmaLinux unaffected filtering and fix updates\nfunc applyAlmaLinuxUnaffectedFiltering(disclosures result.Set, unaffectedResults result.Set, pkgVersion *version.Version) result.Set {\n\tif len(unaffectedResults) == 0 {\n\t\treturn disclosures\n\t}\n\n\t// Filter out vulnerabilities where package version satisfies the unaffected constraint\n\t// (i.e., package IS safe according to AlmaLinux)\n\tfiltered := disclosures.Remove(\n\t\tunaffectedResults.Filter(search.ByVersion(*pkgVersion)),\n\t)\n\n\t// Update remaining vulnerabilities with AlmaLinux fix information\n\treturn filtered.Update(unaffectedResults, result.IdentitiesOverlap, replaceWithAlmaLinuxFixInfo)\n}\n\n// replaceWithAlmaLinuxFixInfo updates the Constraint, Fix, and Advisories fields from AlmaLinux unaffected data\n// while preserving the match Details from the RHEL disclosure. This is used to replace RHEL fix\n// versions with AlmaLinux-specific fix versions when available.\nfunc replaceWithAlmaLinuxFixInfo(existing *result.Result, incoming result.Result) {\n\t// For each vulnerability in the existing result (RHEL disclosure)\n\tfor i := range existing.Vulnerabilities {\n\t\t// Find the corresponding AlmaLinux vulnerability and extract fix info\n\t\tfor _, incomingVuln := range incoming.Vulnerabilities {\n\t\t\t// Extract fix version from the unaffected constraint (e.g., \">= 2.4.48\" -> \"2.4.48\")\n\t\t\tfixVersion := extractFixVersionFromConstraint(incomingVuln.Constraint)\n\t\t\tif fixVersion == \"\" {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// Update constraint to match AlmaLinux fix version (form: \"< fixVersion\")\n\t\t\tnewConstraint, err := version.GetConstraint(fmt.Sprintf(\"< %s\", fixVersion), version.RpmFormat)\n\t\t\tif err != nil {\n\t\t\t\tlog.WithFields(\"error\", err, \"fixVersion\", fixVersion).Debug(\"failed to create constraint from AlmaLinux fix version\")\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\texisting.Vulnerabilities[i].Constraint = newConstraint\n\n\t\t\t// Update fix version and advisories to AlmaLinux's data\n\t\t\texisting.Vulnerabilities[i].Fix = vulnerability.Fix{\n\t\t\t\tVersions: []string{fixVersion},\n\t\t\t\tState:    vulnerability.FixStateFixed,\n\t\t\t}\n\n\t\t\t// Use advisories from database, or construct if missing\n\t\t\tadvisories := incomingVuln.Advisories\n\t\t\tif len(advisories) == 0 {\n\t\t\t\tadvisories = constructAdvisory(incomingVuln, existing.Package)\n\t\t\t}\n\t\t\texisting.Vulnerabilities[i].Advisories = advisories\n\n\t\t\t// Update Details to reflect the AlmaLinux constraint in the match details\n\t\t\tfor j := range existing.Details {\n\t\t\t\tif distroResult, ok := existing.Details[j].Found.(match.DistroResult); ok {\n\t\t\t\t\tdistroResult.VersionConstraint = newConstraint.String()\n\t\t\t\t\texisting.Details[j].Found = distroResult\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tbreak // Only need first match\n\t\t}\n\t}\n}\n\n// constructAdvisory builds advisory information from an ALSA vulnerability\n// This is a fallback for databases that don't yet have advisory IDs in fix references.\n// Once grype-db is updated to include advisory IDs, this will no longer be needed.\nfunc constructAdvisory(vuln vulnerability.Vulnerability, pkg *pkg.Package) []vulnerability.Advisory {\n\t// Only construct for ALSA advisories\n\tif !strings.HasPrefix(vuln.ID, \"ALSA-\") {\n\t\treturn nil\n\t}\n\n\t// Extract major version from package distro\n\tif pkg == nil || pkg.Distro == nil {\n\t\treturn nil\n\t}\n\n\tmajorVersion := pkg.Distro.Version\n\tif idx := strings.Index(majorVersion, \".\"); idx != -1 {\n\t\tmajorVersion = majorVersion[:idx]\n\t}\n\n\tif majorVersion == \"\" {\n\t\treturn nil\n\t}\n\n\t// Format: ALSA-YYYY:NNNN -> https://errata.almalinux.org/{major}/ALSA-YYYY-NNNN.html\n\talsaURLID := strings.Replace(vuln.ID, \":\", \"-\", 1) // ALSA-2025:2686 -> ALSA-2025-2686\n\treturn []vulnerability.Advisory{\n\t\t{\n\t\t\tID:   vuln.ID,\n\t\t\tLink: fmt.Sprintf(\"https://errata.almalinux.org/%s/%s.html\", majorVersion, alsaURLID),\n\t\t},\n\t}\n}\n\n// extractFixVersionFromConstraint extracts a fix version from a version constraint\n// e.g., \">= 2.4.48\" → \"2.4.48\", \"= 1.2.3-4.el8\" → \"1.2.3-4.el8\"\nfunc extractFixVersionFromConstraint(constraint version.Constraint) string {\n\tif constraint == nil {\n\t\treturn \"\"\n\t}\n\n\t// constraint.Value() returns raw constraint without format suffix (e.g., \">= 2.4.48\" instead of \">= 2.4.48 (rpm)\")\n\tconstraintStr := constraint.Value()\n\n\t// Handle common constraint patterns\n\t// \">= version\" → \"version\"\n\tif strings.HasPrefix(constraintStr, \">=\") {\n\t\treturn strings.TrimSpace(strings.TrimPrefix(constraintStr, \">=\"))\n\t}\n\n\t// \"= version\" → \"version\"\n\tif strings.HasPrefix(constraintStr, \"= \") {\n\t\treturn strings.TrimPrefix(constraintStr, \"= \")\n\t}\n\n\t// \"> version\" → we can't determine exact fix version, return empty\n\t// \"< version\" → this wouldn't make sense for a fix constraint\n\n\treturn \"\"\n}\n"
  },
  {
    "path": "grype/matcher/rpm/almalinux_package_utils.go",
    "content": "package rpm\n\nimport (\n\t\"github.com/anchore/grype/grype/pkg\"\n\tsyftPkg \"github.com/anchore/syft/syft/pkg\"\n)\n\n// extractSourceRPMName extracts the source RPM package name from an RPM package.\n// For binary RPMs, this returns the source package name they were built from.\n// For source RPMs, this returns the package name itself.\n// For non-RPM packages, returns empty string.\nfunc extractSourceRPMName(p pkg.Package) string {\n\t// Only process RPM packages\n\tif p.Type != syftPkg.RpmPkg {\n\t\treturn \"\"\n\t}\n\n\t// First, check if this package has upstream information (source RPM)\n\tfor _, upstream := range p.Upstreams {\n\t\tif upstream.Name != \"\" && upstream.Name != p.Name {\n\t\t\treturn upstream.Name\n\t\t}\n\t}\n\n\t// If no upstream info, return the package name itself\n\treturn p.Name\n}\n\n// getRelatedPackageNames returns all possible package names that could be related to the given package.\n// This includes:\n// 1. The package name itself\n// 2. Source RPM name (if this is a binary package)\nfunc getRelatedPackageNames(p pkg.Package) []string {\n\tnames := []string{p.Name}\n\n\t// Add source RPM name if different from package name\n\tsourceRPMName := extractSourceRPMName(p)\n\tif sourceRPMName != \"\" && sourceRPMName != p.Name {\n\t\tnames = append(names, sourceRPMName)\n\t}\n\n\treturn names\n}\n"
  },
  {
    "path": "grype/matcher/rpm/almalinux_package_utils_test.go",
    "content": "package rpm\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/grype/grype/pkg\"\n\tsyftPkg \"github.com/anchore/syft/syft/pkg\"\n)\n\nfunc TestExtractSourceRPMName(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tpkg      pkg.Package\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname: \"binary package with upstream source\",\n\t\t\tpkg: pkg.Package{\n\t\t\t\tName: \"python3-criu\",\n\t\t\t\tType: syftPkg.RpmPkg,\n\t\t\t\tUpstreams: []pkg.UpstreamPackage{\n\t\t\t\t\t{Name: \"criu\", Version: \"3.12\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: \"criu\",\n\t\t},\n\t\t{\n\t\t\tname: \"source package with no upstreams\",\n\t\t\tpkg: pkg.Package{\n\t\t\t\tName:      \"criu\",\n\t\t\t\tType:      syftPkg.RpmPkg,\n\t\t\t\tUpstreams: []pkg.UpstreamPackage{},\n\t\t\t},\n\t\t\texpected: \"criu\",\n\t\t},\n\t\t{\n\t\t\tname: \"package with self-referential upstream\",\n\t\t\tpkg: pkg.Package{\n\t\t\t\tName: \"kernel\",\n\t\t\t\tType: syftPkg.RpmPkg,\n\t\t\t\tUpstreams: []pkg.UpstreamPackage{\n\t\t\t\t\t{Name: \"kernel\", Version: \"5.4.0\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: \"kernel\",\n\t\t},\n\t\t{\n\t\t\tname: \"package with RPM metadata but no upstreams\",\n\t\t\tpkg: pkg.Package{\n\t\t\t\tName: \"util-linux\",\n\t\t\t\tType: syftPkg.RpmPkg,\n\t\t\t\tMetadata: pkg.RpmMetadata{\n\t\t\t\t\tEpoch: intPtr(0),\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: \"util-linux\",\n\t\t},\n\t\t{\n\t\t\tname: \"non-RPM package\",\n\t\t\tpkg: pkg.Package{\n\t\t\t\tName: \"some-deb-package\",\n\t\t\t\tType: syftPkg.DebPkg,\n\t\t\t},\n\t\t\texpected: \"\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := extractSourceRPMName(tt.pkg)\n\t\t\tassert.Equal(t, tt.expected, result)\n\t\t})\n\t}\n}\n\nfunc TestGetRelatedPackageNames(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tpkg      pkg.Package\n\t\texpected []string\n\t}{\n\t\t{\n\t\t\tname: \"binary package with source upstream\",\n\t\t\tpkg: pkg.Package{\n\t\t\t\tName: \"python3-criu\",\n\t\t\t\tType: syftPkg.RpmPkg,\n\t\t\t\tUpstreams: []pkg.UpstreamPackage{\n\t\t\t\t\t{Name: \"criu\", Version: \"3.12\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: []string{\"python3-criu\", \"criu\"}, // should include both binary and source names\n\t\t},\n\t\t{\n\t\t\tname: \"source package with no upstreams\",\n\t\t\tpkg: pkg.Package{\n\t\t\t\tName:      \"httpd\",\n\t\t\t\tType:      syftPkg.RpmPkg,\n\t\t\t\tUpstreams: []pkg.UpstreamPackage{},\n\t\t\t},\n\t\t\texpected: []string{\"httpd\"}, // should only include itself\n\t\t},\n\t\t{\n\t\t\tname: \"package with self-referential upstream\",\n\t\t\tpkg: pkg.Package{\n\t\t\t\tName: \"kernel\",\n\t\t\t\tType: syftPkg.RpmPkg,\n\t\t\t\tUpstreams: []pkg.UpstreamPackage{\n\t\t\t\t\t{Name: \"kernel\", Version: \"5.4.0\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: []string{\"kernel\"}, // should only include itself since source name is same\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := getRelatedPackageNames(tt.pkg)\n\n\t\t\t// Check that all expected names are present\n\t\t\tfor _, expected := range tt.expected {\n\t\t\t\tassert.Contains(t, result, expected, \"Missing expected package name: %s\", expected)\n\t\t\t}\n\n\t\t\t// Check that we don't have unexpected names\n\t\t\tassert.Equal(t, len(tt.expected), len(result), \"Unexpected number of package names\")\n\n\t\t\t// The first name should always be the package name itself\n\t\t\trequire.NotEmpty(t, result)\n\t\t\tassert.Equal(t, tt.pkg.Name, result[0])\n\t\t})\n\t}\n}\n\n// Helper function for tests\nfunc intPtr(i int) *int {\n\treturn &i\n}\n"
  },
  {
    "path": "grype/matcher/rpm/almalinux_test.go",
    "content": "package rpm\n\nimport (\n\t\"testing\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/google/go-cmp/cmp/cmpopts\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/grype/grype/distro\"\n\t\"github.com/anchore/grype/grype/match\"\n\t\"github.com/anchore/grype/grype/matcher/internal/result\"\n\t\"github.com/anchore/grype/grype/pkg\"\n\t\"github.com/anchore/grype/grype/pkg/qualifier\"\n\t\"github.com/anchore/grype/grype/pkg/qualifier/rpmmodularity\"\n\t\"github.com/anchore/grype/grype/version\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n\t\"github.com/anchore/grype/grype/vulnerability/mock\"\n\t\"github.com/anchore/syft/syft/file\"\n\tsyftPkg \"github.com/anchore/syft/syft/pkg\"\n)\n\n// MockProvider is a simple mock implementation of result.Provider for testing\ntype MockProvider struct {\n\tresults         map[string]result.Set\n\tfindResultsFunc func(criteria ...vulnerability.Criteria) (result.Set, error)\n}\n\nfunc (m *MockProvider) FindResults(criteria ...vulnerability.Criteria) (result.Set, error) {\n\tif m.findResultsFunc != nil {\n\t\treturn m.findResultsFunc(criteria...)\n\t}\n\t// Default behavior - return empty set\n\treturn result.Set{}, nil\n}\n\nfunc TestShouldUseAlmaLinuxMatching(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tdistro   *distro.Distro\n\t\texpected bool\n\t}{\n\t\t{\n\t\t\tname:     \"nil distro\",\n\t\t\tdistro:   nil,\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname: \"AlmaLinux distro\",\n\t\t\tdistro: &distro.Distro{\n\t\t\t\tType: distro.AlmaLinux,\n\t\t\t},\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname: \"RHEL distro\",\n\t\t\tdistro: &distro.Distro{\n\t\t\t\tType: distro.RedHat,\n\t\t\t},\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname: \"Ubuntu distro\",\n\t\t\tdistro: &distro.Distro{\n\t\t\t\tType: distro.Ubuntu,\n\t\t\t},\n\t\t\texpected: false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := shouldUseAlmaLinuxMatching(tt.distro)\n\t\t\tassert.Equal(t, tt.expected, result)\n\t\t})\n\t}\n}\n\n// this should be called \"TestModularityQualifiersPassedToVulnProvider\"\nfunc TestModularityExcludesDisclosure(t *testing.T) {\n\t// Test that OnlyQualifiedPackages is used to filter both RHEL disclosures and AlmaLinux advisories\n\tmockProvider := &MockProvider{}\n\n\talmaDistro := &distro.Distro{\n\t\tType:    distro.AlmaLinux,\n\t\tVersion: \"8\",\n\t}\n\ttestPkg := pkg.Package{\n\t\tName:    \"nodejs\",\n\t\tVersion: \"1:20.8.0-1.module_el8.9.0+3775+d8460d29\",\n\t\tType:    syftPkg.RpmPkg,\n\t\tDistro:  almaDistro,\n\t\tMetadata: pkg.RpmMetadata{\n\t\t\tModularityLabel: strRef(\"nodejs:20\"),\n\t\t},\n\t}\n\n\tvar capturedCriteria [][]vulnerability.Criteria\n\tcallCount := 0\n\tmockProvider.findResultsFunc = func(criteria ...vulnerability.Criteria) (result.Set, error) {\n\t\tcapturedCriteria = append(capturedCriteria, criteria)\n\t\tcallCount++\n\t\t// First call: return a disclosure so the matcher continues to fetch unaffected records\n\t\tif callCount == 1 {\n\t\t\treturn result.Set{\n\t\t\t\t\"CVE-2023-1234\": []result.Result{\n\t\t\t\t\t{\n\t\t\t\t\t\tID: \"CVE-2023-1234\",\n\t\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tReference:         vulnerability.Reference{ID: \"CVE-2023-1234\"},\n\t\t\t\t\t\t\t\tConstraint:        createConstraint(t, \"< 1:20.9.0-1.module_el8.9.0+1234+abcd\", version.RpmFormat),\n\t\t\t\t\t\t\t\tPackageQualifiers: []qualifier.Qualifier{rpmmodularity.New(\"nodejs:20\")},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tPackage: &testPkg,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}, nil\n\t\t}\n\t\t// All other calls: return empty\n\t\treturn result.Set{}, nil\n\t}\n\n\t_, err := almaLinuxMatchesWithUpstreams(mockProvider, testPkg)\n\trequire.NoError(t, err)\n\n\trequire.GreaterOrEqual(t, len(capturedCriteria), 2, \"FindResults should be called for both RHEL disclosures and AlmaLinux advisories\")\n\n\t// Helper to check if a criteria set includes OnlyQualifiedPackages\n\thasQualifierCriterion := func(criteriaSet []vulnerability.Criteria) bool {\n\t\tfor _, criterion := range criteriaSet {\n\t\t\t// Test if this criterion filters by qualifiers\n\t\t\tmatches, _, err := criterion.MatchesVulnerability(vulnerability.Vulnerability{\n\t\t\t\tPackageQualifiers: []qualifier.Qualifier{rpmmodularity.New(\"nodejs:22\")},\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\t\t\tif !matches {\n\t\t\t\t// This criterion rejected nodejs:22 when package has nodejs:20\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t\treturn false\n\t}\n\n\t// Call 1: RHEL disclosures for binary package\n\tassert.True(t, hasQualifierCriterion(capturedCriteria[0]),\n\t\t\"RHEL disclosure fetch should include OnlyQualifiedPackages criterion\")\n\n\t// Call 2: AlmaLinux unaffected records\n\tassert.True(t, hasQualifierCriterion(capturedCriteria[1]),\n\t\t\"AlmaLinux advisory fetch should include OnlyQualifiedPackages criterion\")\n}\n\n// Comprehensive table-driven test for AlmaLinux matching\nfunc TestAlmaLinuxMatching(t *testing.T) {\n\ttests := []struct {\n\t\tname        string\n\t\tdescription string\n\n\t\t// Input data\n\t\tpkg       pkg.Package\n\t\trhelVulns []vulnerability.Vulnerability\n\t\talmaVulns []vulnerability.Vulnerability\n\n\t\t// Expected behavior\n\t\texpectedMatches []match.Match\n\t}{\n\t\t{\n\t\t\tname:        \"simple vulnerability match: disclosure without unaffected record\",\n\t\t\tdescription: \"Package version satisfies RHEL vulnerability constraint, no AlmaLinux unaffected record\",\n\n\t\t\tpkg: pkg.Package{\n\t\t\t\tName:    \"httpd\",\n\t\t\t\tVersion: \"2.4.37-10.el8\",\n\t\t\t\tType:    syftPkg.RpmPkg,\n\t\t\t\tDistro: &distro.Distro{\n\t\t\t\t\tType:    distro.AlmaLinux,\n\t\t\t\t\tVersion: \"8\",\n\t\t\t\t},\n\t\t\t},\n\n\t\t\trhelVulns: []vulnerability.Vulnerability{\n\t\t\t\t{\n\t\t\t\t\tPackageName: \"httpd\",\n\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\tID:        \"CVE-2023-1234\",\n\t\t\t\t\t\tNamespace: \"redhat:distro:redhat:8\",\n\t\t\t\t\t},\n\t\t\t\t\tConstraint: createConstraint(t, \">= 0\", version.RpmFormat),\n\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\tState: vulnerability.FixStateNotFixed,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\n\t\t\t// No AlmaLinux unaffected records\n\t\t\talmaVulns: []vulnerability.Vulnerability{},\n\n\t\t\texpectedMatches: []match.Match{\n\t\t\t\t{\n\t\t\t\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\t\t\t\tPackageName: \"httpd\",\n\t\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\t\tID:        \"CVE-2023-1234\",\n\t\t\t\t\t\t\tNamespace: \"redhat:distro:redhat:8\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tConstraint: createConstraint(t, \">= 0\", version.RpmFormat),\n\t\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\t\tState: vulnerability.FixStateNotFixed,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tPackage: pkg.Package{\n\t\t\t\t\t\tName:    \"httpd\",\n\t\t\t\t\t\tVersion: \"2.4.37-10.el8\",\n\t\t\t\t\t\tType:    syftPkg.RpmPkg,\n\t\t\t\t\t\tDistro: &distro.Distro{\n\t\t\t\t\t\t\tType:    distro.AlmaLinux,\n\t\t\t\t\t\t\tVersion: \"8\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tDetails: createExpectedDetails(pkg.Package{\n\t\t\t\t\t\tName:    \"httpd\",\n\t\t\t\t\t\tVersion: \"2.4.37-10.el8\",\n\t\t\t\t\t\tDistro:  &distro.Distro{Type: distro.AlmaLinux, Version: \"8\"},\n\t\t\t\t\t}, vulnerability.Vulnerability{\n\t\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\t\tID:        \"CVE-2023-1234\",\n\t\t\t\t\t\t\tNamespace: \"redhat:distro:redhat:8\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tConstraint: createConstraint(t, \">= 0\", version.RpmFormat),\n\t\t\t\t\t}),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:        \"simple vulnerability filtered by AlmaLinux unaffected record\",\n\t\t\tdescription: \"Package version satisfies AlmaLinux unaffected constraint, should be filtered out\",\n\n\t\t\tpkg: pkg.Package{\n\t\t\t\tName:    \"httpd\",\n\t\t\t\tVersion: \"2.4.37-51.el8\",\n\t\t\t\tType:    syftPkg.RpmPkg,\n\t\t\t\tDistro: &distro.Distro{\n\t\t\t\t\tType:    distro.AlmaLinux,\n\t\t\t\t\tVersion: \"8\",\n\t\t\t\t},\n\t\t\t},\n\n\t\t\trhelVulns: []vulnerability.Vulnerability{\n\t\t\t\t{\n\t\t\t\t\tPackageName: \"httpd\",\n\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\tID:        \"CVE-2023-1234\",\n\t\t\t\t\t\tNamespace: \"redhat:distro:redhat:8\",\n\t\t\t\t\t},\n\t\t\t\t\tConstraint: createConstraint(t, \"< 2.4.37-50.el8\", version.RpmFormat),\n\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\tVersions: []string{\"2.4.37-50.el8\"},\n\t\t\t\t\t\tState:    vulnerability.FixStateFixed,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\n\t\t\talmaVulns: []vulnerability.Vulnerability{\n\t\t\t\t{\n\t\t\t\t\tPackageName: \"httpd\",\n\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\tID:        \"ALSA-2023:1234\",\n\t\t\t\t\t\tNamespace: \"almalinux:distro:almalinux:8\",\n\t\t\t\t\t},\n\t\t\t\t\tRelatedVulnerabilities: []vulnerability.Reference{\n\t\t\t\t\t\t{ID: \"CVE-2023-1234\"},\n\t\t\t\t\t},\n\t\t\t\t\tConstraint: createConstraint(t, \">= 2.4.37-51.el8\", version.RpmFormat),\n\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\tVersions: []string{\"2.4.37-51.el8\"},\n\t\t\t\t\t\tState:    vulnerability.FixStateFixed,\n\t\t\t\t\t},\n\t\t\t\t\tUnaffected: true,\n\t\t\t\t},\n\t\t\t},\n\n\t\t\t// Package version >= fix version, so vulnerability should be filtered\n\t\t\texpectedMatches: nil,\n\t\t},\n\t\t{\n\t\t\tname:        \"fix replacement: simple non-modular package\",\n\t\t\tdescription: \"Non-modular package is vulnerable, RHEL fix info replaced by AlmaLinux fix info\",\n\n\t\t\tpkg: pkg.Package{\n\t\t\t\tName:    \"httpd\",\n\t\t\t\tVersion: \"2.4.37-10.el8\",\n\t\t\t\tType:    syftPkg.RpmPkg,\n\t\t\t\tDistro: &distro.Distro{\n\t\t\t\t\tType:    distro.AlmaLinux,\n\t\t\t\t\tVersion: \"8\",\n\t\t\t\t},\n\t\t\t},\n\n\t\t\trhelVulns: []vulnerability.Vulnerability{\n\t\t\t\t{\n\t\t\t\t\tPackageName: \"httpd\",\n\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\tID:        \"CVE-2021-44790\",\n\t\t\t\t\t\tNamespace: \"redhat:distro:redhat:8\",\n\t\t\t\t\t},\n\t\t\t\t\tConstraint: createConstraint(t, \"< 2.4.37-50.el8\", version.RpmFormat),\n\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\tVersions: []string{\"2.4.37-50.el8\"},\n\t\t\t\t\t\tState:    vulnerability.FixStateFixed,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\n\t\t\talmaVulns: []vulnerability.Vulnerability{\n\t\t\t\t{\n\t\t\t\t\tPackageName: \"httpd\",\n\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\tID:        \"ALSA-2022:0123\",\n\t\t\t\t\t\tNamespace: \"almalinux:distro:almalinux:8\",\n\t\t\t\t\t},\n\t\t\t\t\tRelatedVulnerabilities: []vulnerability.Reference{\n\t\t\t\t\t\t{ID: \"CVE-2021-44790\"},\n\t\t\t\t\t},\n\t\t\t\t\t// Package 2.4.37-10.el8 < fix 2.4.37-43.el8, so it's still vulnerable\n\t\t\t\t\tConstraint: createConstraint(t, \">= 2.4.37-43.el8\", version.RpmFormat),\n\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\tVersions: []string{\"2.4.37-43.el8\"},\n\t\t\t\t\t\tState:    vulnerability.FixStateFixed,\n\t\t\t\t\t},\n\t\t\t\t\tAdvisories: []vulnerability.Advisory{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tID:   \"ALSA-2022:0123\",\n\t\t\t\t\t\t\tLink: \"https://errata.almalinux.org/8/ALSA-2022-0123.html\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tUnaffected: true,\n\t\t\t\t},\n\t\t\t},\n\n\t\t\t// Package is vulnerable but fix info should be from AlmaLinux\n\t\t\texpectedMatches: []match.Match{\n\t\t\t\t{\n\t\t\t\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\t\t\t\tPackageName: \"httpd\",\n\t\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\t\tID:        \"CVE-2021-44790\",\n\t\t\t\t\t\t\tNamespace: \"redhat:distro:redhat:8\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\t// Constraint should be updated to match AlmaLinux fix version\n\t\t\t\t\t\tConstraint: createConstraint(t, \"< 2.4.37-43.el8\", version.RpmFormat),\n\t\t\t\t\t\t// Fix version should be replaced with AlmaLinux version\n\t\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\t\tVersions: []string{\"2.4.37-43.el8\"},\n\t\t\t\t\t\t\tState:    vulnerability.FixStateFixed,\n\t\t\t\t\t\t},\n\t\t\t\t\t\t// Advisory should be from AlmaLinux\n\t\t\t\t\t\tAdvisories: []vulnerability.Advisory{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tID:   \"ALSA-2022:0123\",\n\t\t\t\t\t\t\t\tLink: \"https://errata.almalinux.org/8/ALSA-2022-0123.html\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tPackage: pkg.Package{\n\t\t\t\t\t\tName:    \"httpd\",\n\t\t\t\t\t\tVersion: \"2.4.37-10.el8\",\n\t\t\t\t\t\tType:    syftPkg.RpmPkg,\n\t\t\t\t\t\tDistro: &distro.Distro{\n\t\t\t\t\t\t\tType:    distro.AlmaLinux,\n\t\t\t\t\t\t\tVersion: \"8\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tDetails: createExpectedDetails(pkg.Package{\n\t\t\t\t\t\tName:    \"httpd\",\n\t\t\t\t\t\tVersion: \"2.4.37-10.el8\",\n\t\t\t\t\t\tDistro:  &distro.Distro{Type: distro.AlmaLinux, Version: \"8\"},\n\t\t\t\t\t}, vulnerability.Vulnerability{\n\t\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\t\tID:        \"CVE-2021-44790\",\n\t\t\t\t\t\t\tNamespace: \"redhat:distro:redhat:8\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\t// Details should reflect the AlmaLinux constraint\n\t\t\t\t\t\tConstraint: createConstraint(t, \"< 2.4.37-43.el8\", version.RpmFormat),\n\t\t\t\t\t}),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:        \"fix replacement: modular package with qualifiers\",\n\t\t\tdescription: \"Modular package with modularity qualifiers - RHEL fix replaced by AlmaLinux fix, ALSA maps to multiple CVEs\",\n\n\t\t\tpkg: pkg.Package{\n\t\t\t\tID:      pkg.ID(\"httpd-scenario1\"),\n\t\t\t\tName:    \"httpd\",\n\t\t\t\tVersion: \"2.4.37-50.module_el8.7.0+3405+9516b832\",\n\t\t\t\tType:    syftPkg.RpmPkg,\n\t\t\t\tDistro: &distro.Distro{\n\t\t\t\t\tType:    distro.AlmaLinux,\n\t\t\t\t\tVersion: \"8.7\",\n\t\t\t\t},\n\t\t\t\tMetadata: pkg.RpmMetadata{\n\t\t\t\t\tModularityLabel: strPtr(\"httpd:2.4:8070020220920142155:f8e95b4e\"),\n\t\t\t\t},\n\t\t\t},\n\n\t\t\trhelVulns: []vulnerability.Vulnerability{\n\t\t\t\t{\n\t\t\t\t\tPackageName: \"httpd\",\n\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\tID:        \"CVE-2006-20001\",\n\t\t\t\t\t\tNamespace: \"redhat:distro:redhat:8\",\n\t\t\t\t\t},\n\t\t\t\t\tConstraint: createConstraint(t, \"< 0:2.4.37-51.module+el8.7.0+18026+7b169787.1\", version.RpmFormat),\n\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\tVersions: []string{\"0:2.4.37-51.module+el8.7.0+18026+7b169787.1\"},\n\t\t\t\t\t\tState:    vulnerability.FixStateFixed,\n\t\t\t\t\t},\n\t\t\t\t\tAdvisories: []vulnerability.Advisory{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tID:   \"RHSA-2023:0852\",\n\t\t\t\t\t\t\tLink: \"https://access.redhat.com/errata/RHSA-2023:0852\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tPackageQualifiers: []qualifier.Qualifier{rpmmodularity.New(\"httpd:2.4\")},\n\t\t\t\t},\n\t\t\t},\n\n\t\t\talmaVulns: []vulnerability.Vulnerability{\n\t\t\t\t{\n\t\t\t\t\tPackageName: \"httpd\",\n\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\tID:        \"ALSA-2023:0852\",\n\t\t\t\t\t\tNamespace: \"almalinux:distro:almalinux:8\",\n\t\t\t\t\t},\n\t\t\t\t\tRelatedVulnerabilities: []vulnerability.Reference{\n\t\t\t\t\t\t{ID: \"CVE-2006-20001\"},\n\t\t\t\t\t\t{ID: \"CVE-2022-36760\"},\n\t\t\t\t\t\t{ID: \"CVE-2022-37436\"},\n\t\t\t\t\t},\n\t\t\t\t\tConstraint: createConstraint(t, \">= 2.4.37-51.module_el8.7.0+3405+9516b832.1\", version.RpmFormat),\n\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\tVersions: []string{\"2.4.37-51.module_el8.7.0+3405+9516b832.1\"},\n\t\t\t\t\t\tState:    vulnerability.FixStateFixed,\n\t\t\t\t\t},\n\t\t\t\t\tAdvisories: []vulnerability.Advisory{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tID:   \"ALSA-2023:0852\",\n\t\t\t\t\t\t\tLink: \"https://errata.almalinux.org/8/ALSA-2023-0852.html\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tPackageQualifiers: []qualifier.Qualifier{rpmmodularity.New(\"httpd:2.4\")},\n\t\t\t\t\tUnaffected:        true,\n\t\t\t\t},\n\t\t\t},\n\n\t\t\t// Package version 2.4.37-50 < fix 2.4.37-51, vulnerable but with AlmaLinux fix info\n\t\t\texpectedMatches: []match.Match{\n\t\t\t\t{\n\t\t\t\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\t\t\t\tPackageName: \"httpd\",\n\t\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\t\tID:        \"CVE-2006-20001\",\n\t\t\t\t\t\t\tNamespace: \"redhat:distro:redhat:8\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\t// Constraint should be updated to match AlmaLinux fix version\n\t\t\t\t\t\tConstraint: createConstraint(t, \"< 2.4.37-51.module_el8.7.0+3405+9516b832.1\", version.RpmFormat),\n\t\t\t\t\t\t// Fix version should be from AlmaLinux (not RHEL)\n\t\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\t\tVersions: []string{\"2.4.37-51.module_el8.7.0+3405+9516b832.1\"},\n\t\t\t\t\t\t\tState:    vulnerability.FixStateFixed,\n\t\t\t\t\t\t},\n\t\t\t\t\t\t// Advisory should be from AlmaLinux (not RHEL)\n\t\t\t\t\t\tAdvisories: []vulnerability.Advisory{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tID:   \"ALSA-2023:0852\",\n\t\t\t\t\t\t\t\tLink: \"https://errata.almalinux.org/8/ALSA-2023-0852.html\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\t// PackageQualifiers ignored in comparison\n\t\t\t\t\t},\n\t\t\t\t\tPackage: pkg.Package{\n\t\t\t\t\t\tID:      pkg.ID(\"httpd-scenario1\"),\n\t\t\t\t\t\tName:    \"httpd\",\n\t\t\t\t\t\tVersion: \"2.4.37-50.module_el8.7.0+3405+9516b832\",\n\t\t\t\t\t\tType:    syftPkg.RpmPkg,\n\t\t\t\t\t\tDistro: &distro.Distro{\n\t\t\t\t\t\t\tType:    distro.AlmaLinux,\n\t\t\t\t\t\t\tVersion: \"8.7\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tMetadata: pkg.RpmMetadata{\n\t\t\t\t\t\t\tModularityLabel: strPtr(\"httpd:2.4:8070020220920142155:f8e95b4e\"),\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tDetails: createExpectedDetails(pkg.Package{\n\t\t\t\t\t\tName:    \"httpd\",\n\t\t\t\t\t\tVersion: \"2.4.37-50.module_el8.7.0+3405+9516b832\",\n\t\t\t\t\t\tDistro:  &distro.Distro{Type: distro.AlmaLinux, Version: \"8.7\"},\n\t\t\t\t\t}, vulnerability.Vulnerability{\n\t\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\t\tID:        \"CVE-2006-20001\",\n\t\t\t\t\t\t\tNamespace: \"redhat:distro:redhat:8\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\t// Details should reflect the AlmaLinux constraint\n\t\t\t\t\t\tConstraint: createConstraint(t, \"< 2.4.37-51.module_el8.7.0+3405+9516b832.1\", version.RpmFormat),\n\t\t\t\t\t}),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:        \"Scenario 2A: A-advisory filters vulnerability when package at fix version\",\n\t\t\tdescription: \"RHEL has no fix, AlmaLinux A-advisory has fix, package version >= fix\",\n\n\t\t\tpkg: pkg.Package{\n\t\t\t\tID:      pkg.ID(\"open-vm-tools-scenario2a\"),\n\t\t\t\tName:    \"open-vm-tools\",\n\t\t\t\tVersion: \"12.3.5-2.el8.alma.1\",\n\t\t\t\tType:    syftPkg.RpmPkg,\n\t\t\t\tDistro: &distro.Distro{\n\t\t\t\t\tType:    distro.AlmaLinux,\n\t\t\t\t\tVersion: \"8.0\",\n\t\t\t\t},\n\t\t\t\tMetadata: pkg.RpmMetadata{\n\t\t\t\t\tModularityLabel: nil,\n\t\t\t\t},\n\t\t\t},\n\n\t\t\trhelVulns: []vulnerability.Vulnerability{\n\t\t\t\t{\n\t\t\t\t\tPackageName: \"open-vm-tools\",\n\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\tID:        \"CVE-2025-22247\",\n\t\t\t\t\t\tNamespace: \"redhat:distro:redhat:8\",\n\t\t\t\t\t},\n\t\t\t\t\tConstraint: createConstraint(t, \">= 0\", version.RpmFormat),\n\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\tVersions: []string{},\n\t\t\t\t\t\tState:    vulnerability.FixStateNotFixed,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\n\t\t\talmaVulns: []vulnerability.Vulnerability{\n\t\t\t\t{\n\t\t\t\t\tPackageName: \"open-vm-tools\",\n\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\tID:        \"ALSA-2025:A001\",\n\t\t\t\t\t\tNamespace: \"almalinux:distro:almalinux:8\",\n\t\t\t\t\t},\n\t\t\t\t\tRelatedVulnerabilities: []vulnerability.Reference{\n\t\t\t\t\t\t{ID: \"CVE-2025-22247\"},\n\t\t\t\t\t},\n\t\t\t\t\tConstraint: createConstraint(t, \">= 12.3.5-2.el8.alma.1\", version.RpmFormat),\n\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\tVersions: []string{\"12.3.5-2.el8.alma.1\"},\n\t\t\t\t\t\tState:    vulnerability.FixStateFixed,\n\t\t\t\t\t},\n\t\t\t\t\tAdvisories: []vulnerability.Advisory{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tID:   \"ALSA-2025:A001\",\n\t\t\t\t\t\t\tLink: \"https://errata.almalinux.org/8/ALSA-2025-A001.html\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tUnaffected: true,\n\t\t\t\t},\n\t\t\t},\n\n\t\t\t// Package version >= fix, should be filtered out\n\t\t\texpectedMatches: nil,\n\t\t},\n\t\t{\n\t\t\tname:        \"Scenario 2B: A-advisory reports vulnerability with fix when package below fix version\",\n\t\t\tdescription: \"RHEL has no fix, AlmaLinux A-advisory has fix, package version < fix\",\n\n\t\t\tpkg: pkg.Package{\n\t\t\t\tID:      pkg.ID(\"open-vm-tools-scenario2b\"),\n\t\t\t\tName:    \"open-vm-tools\",\n\t\t\t\tVersion: \"12.3.5-1.el8\",\n\t\t\t\tType:    syftPkg.RpmPkg,\n\t\t\t\tDistro: &distro.Distro{\n\t\t\t\t\tType:    distro.AlmaLinux,\n\t\t\t\t\tVersion: \"8.0\",\n\t\t\t\t},\n\t\t\t\tMetadata: pkg.RpmMetadata{\n\t\t\t\t\tModularityLabel: nil,\n\t\t\t\t},\n\t\t\t},\n\n\t\t\trhelVulns: []vulnerability.Vulnerability{\n\t\t\t\t{\n\t\t\t\t\tPackageName: \"open-vm-tools\",\n\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\tID:        \"CVE-2025-22247\",\n\t\t\t\t\t\tNamespace: \"redhat:distro:redhat:8\",\n\t\t\t\t\t},\n\t\t\t\t\tConstraint: createConstraint(t, \">= 0\", version.RpmFormat),\n\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\tVersions: []string{},\n\t\t\t\t\t\tState:    vulnerability.FixStateNotFixed,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\n\t\t\talmaVulns: []vulnerability.Vulnerability{\n\t\t\t\t{\n\t\t\t\t\tPackageName: \"open-vm-tools\",\n\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\tID:        \"ALSA-2025:A001\",\n\t\t\t\t\t\tNamespace: \"almalinux:distro:almalinux:8\",\n\t\t\t\t\t},\n\t\t\t\t\tRelatedVulnerabilities: []vulnerability.Reference{\n\t\t\t\t\t\t{ID: \"CVE-2025-22247\"},\n\t\t\t\t\t},\n\t\t\t\t\tConstraint: createConstraint(t, \">= 12.3.5-2.el8.alma.1\", version.RpmFormat),\n\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\tVersions: []string{\"12.3.5-2.el8.alma.1\"},\n\t\t\t\t\t\tState:    vulnerability.FixStateFixed,\n\t\t\t\t\t},\n\t\t\t\t\tAdvisories: []vulnerability.Advisory{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tID:   \"ALSA-2025:A001\",\n\t\t\t\t\t\t\tLink: \"https://errata.almalinux.org/8/ALSA-2025-A001.html\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tUnaffected: true,\n\t\t\t\t},\n\t\t\t},\n\n\t\t\t// Package version < fix, should report with AlmaLinux fix info\n\t\t\texpectedMatches: []match.Match{\n\t\t\t\t{\n\t\t\t\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\t\t\t\tPackageName: \"open-vm-tools\",\n\t\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\t\tID:        \"CVE-2025-22247\",\n\t\t\t\t\t\t\tNamespace: \"redhat:distro:redhat:8\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\t// Constraint should be updated to match AlmaLinux fix version\n\t\t\t\t\t\tConstraint: createConstraint(t, \"< 12.3.5-2.el8.alma.1\", version.RpmFormat),\n\t\t\t\t\t\t// Fix should be from AlmaLinux A-advisory (RHEL has no fix)\n\t\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\t\tVersions: []string{\"12.3.5-2.el8.alma.1\"},\n\t\t\t\t\t\t\tState:    vulnerability.FixStateFixed,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tAdvisories: []vulnerability.Advisory{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tID:   \"ALSA-2025:A001\",\n\t\t\t\t\t\t\t\tLink: \"https://errata.almalinux.org/8/ALSA-2025-A001.html\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tPackage: pkg.Package{\n\t\t\t\t\t\tID:      pkg.ID(\"open-vm-tools-scenario2b\"),\n\t\t\t\t\t\tName:    \"open-vm-tools\",\n\t\t\t\t\t\tVersion: \"12.3.5-1.el8\",\n\t\t\t\t\t\tType:    syftPkg.RpmPkg,\n\t\t\t\t\t\tDistro: &distro.Distro{\n\t\t\t\t\t\t\tType:    distro.AlmaLinux,\n\t\t\t\t\t\t\tVersion: \"8.0\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tMetadata: pkg.RpmMetadata{\n\t\t\t\t\t\t\tModularityLabel: nil,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tDetails: createExpectedDetails(pkg.Package{\n\t\t\t\t\t\tName:    \"open-vm-tools\",\n\t\t\t\t\t\tVersion: \"12.3.5-1.el8\",\n\t\t\t\t\t\tDistro:  &distro.Distro{Type: distro.AlmaLinux, Version: \"8.0\"},\n\t\t\t\t\t}, vulnerability.Vulnerability{\n\t\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\t\tID:        \"CVE-2025-22247\",\n\t\t\t\t\t\t\tNamespace: \"redhat:distro:redhat:8\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\t// Details should reflect the AlmaLinux constraint\n\t\t\t\t\t\tConstraint: createConstraint(t, \"< 12.3.5-2.el8.alma.1\", version.RpmFormat),\n\t\t\t\t\t}),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:        \"Scenario 3A: Module build number mismatch - AlmaLinux lower build filters vulnerability\",\n\t\t\tdescription: \"python38 with modularity - AlmaLinux build 3633 vs RHEL build 19642, package at AlmaLinux fix\",\n\n\t\t\tpkg: pkg.Package{\n\t\t\t\tID:      pkg.ID(\"python38-scenario3a\"),\n\t\t\t\tName:    \"python38\",\n\t\t\t\tVersion: \"3.8.17-2.module_el8.9.0+3633+e453b53a\",\n\t\t\t\tType:    syftPkg.RpmPkg,\n\t\t\t\tDistro: &distro.Distro{\n\t\t\t\t\tType:    distro.AlmaLinux,\n\t\t\t\t\tVersion: \"8.9\",\n\t\t\t\t},\n\t\t\t\tMetadata: pkg.RpmMetadata{\n\t\t\t\t\tModularityLabel: strPtr(\"python38:3.8:8090020230810123456:3b72e4d2\"),\n\t\t\t\t},\n\t\t\t},\n\n\t\t\trhelVulns: []vulnerability.Vulnerability{\n\t\t\t\t{\n\t\t\t\t\tPackageName: \"python38\",\n\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\tID:        \"CVE-2007-4559\",\n\t\t\t\t\t\tNamespace: \"redhat:distro:redhat:8\",\n\t\t\t\t\t},\n\t\t\t\t\tConstraint: createConstraint(t, \"< 0:3.8.17-2.module+el8.9.0+19642+a12b4af6\", version.RpmFormat),\n\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\tVersions: []string{\"0:3.8.17-2.module+el8.9.0+19642+a12b4af6\"},\n\t\t\t\t\t\tState:    vulnerability.FixStateFixed,\n\t\t\t\t\t},\n\t\t\t\t\tAdvisories: []vulnerability.Advisory{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tID:   \"RHSA-2023:7050\",\n\t\t\t\t\t\t\tLink: \"https://access.redhat.com/errata/RHSA-2023:7050\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\n\t\t\talmaVulns: []vulnerability.Vulnerability{\n\t\t\t\t{\n\t\t\t\t\tPackageName: \"python38\",\n\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\tID:        \"ALSA-2023:7050\",\n\t\t\t\t\t\tNamespace: \"almalinux:distro:almalinux:8\",\n\t\t\t\t\t},\n\t\t\t\t\tRelatedVulnerabilities: []vulnerability.Reference{\n\t\t\t\t\t\t{ID: \"CVE-2007-4559\"},\n\t\t\t\t\t\t{ID: \"CVE-2023-32681\"},\n\t\t\t\t\t},\n\t\t\t\t\tConstraint: createConstraint(t, \">= 3.8.17-2.module_el8.9.0+3633+e453b53a\", version.RpmFormat),\n\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\tVersions: []string{\"3.8.17-2.module_el8.9.0+3633+e453b53a\"},\n\t\t\t\t\t\tState:    vulnerability.FixStateFixed,\n\t\t\t\t\t},\n\t\t\t\t\tAdvisories: []vulnerability.Advisory{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tID:   \"ALSA-2023:7050\",\n\t\t\t\t\t\t\tLink: \"https://errata.almalinux.org/8/ALSA-2023-7050.html\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tUnaffected: true,\n\t\t\t\t},\n\t\t\t},\n\n\t\t\t// Package has AlmaLinux build 3633, despite being < RHEL's 19642, should be filtered\n\t\t\texpectedMatches: nil,\n\t\t},\n\t\t{\n\t\t\tname:        \"Scenario 3B: Module build number mismatch - vulnerable version still reported\",\n\t\t\tdescription: \"python38 with modularity - package version below AlmaLinux fix (despite lower build number)\",\n\n\t\t\tpkg: pkg.Package{\n\t\t\t\tID:      pkg.ID(\"python38-scenario3b\"),\n\t\t\t\tName:    \"python38\",\n\t\t\t\tVersion: \"3.8.17-1.module_el8.9.0+3633+e453b53a\",\n\t\t\t\tType:    syftPkg.RpmPkg,\n\t\t\t\tDistro: &distro.Distro{\n\t\t\t\t\tType:    distro.AlmaLinux,\n\t\t\t\t\tVersion: \"8.9\",\n\t\t\t\t},\n\t\t\t\tMetadata: pkg.RpmMetadata{\n\t\t\t\t\tModularityLabel: strPtr(\"python38:3.8:8090020230810123456:3b72e4d2\"),\n\t\t\t\t},\n\t\t\t},\n\n\t\t\trhelVulns: []vulnerability.Vulnerability{\n\t\t\t\t{\n\t\t\t\t\tPackageName: \"python38\",\n\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\tID:        \"CVE-2007-4559\",\n\t\t\t\t\t\tNamespace: \"redhat:distro:redhat:8\",\n\t\t\t\t\t},\n\t\t\t\t\tConstraint: createConstraint(t, \"< 0:3.8.17-2.module+el8.9.0+19642+a12b4af6\", version.RpmFormat),\n\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\tVersions: []string{\"0:3.8.17-2.module+el8.9.0+19642+a12b4af6\"},\n\t\t\t\t\t\tState:    vulnerability.FixStateFixed,\n\t\t\t\t\t},\n\t\t\t\t\tAdvisories: []vulnerability.Advisory{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tID:   \"RHSA-2023:7050\",\n\t\t\t\t\t\t\tLink: \"https://access.redhat.com/errata/RHSA-2023:7050\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\n\t\t\talmaVulns: []vulnerability.Vulnerability{\n\t\t\t\t{\n\t\t\t\t\tPackageName: \"python38\",\n\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\tID:        \"ALSA-2023:7050\",\n\t\t\t\t\t\tNamespace: \"almalinux:distro:almalinux:8\",\n\t\t\t\t\t},\n\t\t\t\t\tRelatedVulnerabilities: []vulnerability.Reference{\n\t\t\t\t\t\t{ID: \"CVE-2007-4559\"},\n\t\t\t\t\t\t{ID: \"CVE-2023-32681\"},\n\t\t\t\t\t},\n\t\t\t\t\tConstraint: createConstraint(t, \">= 3.8.17-2.module_el8.9.0+3633+e453b53a\", version.RpmFormat),\n\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\tVersions: []string{\"3.8.17-2.module_el8.9.0+3633+e453b53a\"},\n\t\t\t\t\t\tState:    vulnerability.FixStateFixed,\n\t\t\t\t\t},\n\t\t\t\t\tAdvisories: []vulnerability.Advisory{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tID:   \"ALSA-2023:7050\",\n\t\t\t\t\t\t\tLink: \"https://errata.almalinux.org/8/ALSA-2023-7050.html\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tUnaffected: true,\n\t\t\t\t},\n\t\t\t},\n\n\t\t\t// Package version 3.8.17-1 < fix 3.8.17-2, should report with AlmaLinux fix\n\t\t\texpectedMatches: []match.Match{\n\t\t\t\t{\n\t\t\t\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\t\t\t\tPackageName: \"python38\",\n\t\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\t\tID:        \"CVE-2007-4559\",\n\t\t\t\t\t\t\tNamespace: \"redhat:distro:redhat:8\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\t// Constraint should be updated to match AlmaLinux fix version\n\t\t\t\t\t\tConstraint: createConstraint(t, \"< 3.8.17-2.module_el8.9.0+3633+e453b53a\", version.RpmFormat),\n\t\t\t\t\t\t// Fix should be from AlmaLinux (lower build number 3633, not RHEL's 19642)\n\t\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\t\tVersions: []string{\"3.8.17-2.module_el8.9.0+3633+e453b53a\"},\n\t\t\t\t\t\t\tState:    vulnerability.FixStateFixed,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tAdvisories: []vulnerability.Advisory{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tID:   \"ALSA-2023:7050\",\n\t\t\t\t\t\t\t\tLink: \"https://errata.almalinux.org/8/ALSA-2023-7050.html\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tPackage: pkg.Package{\n\t\t\t\t\t\tID:      pkg.ID(\"python38-scenario3b\"),\n\t\t\t\t\t\tName:    \"python38\",\n\t\t\t\t\t\tVersion: \"3.8.17-1.module_el8.9.0+3633+e453b53a\",\n\t\t\t\t\t\tType:    syftPkg.RpmPkg,\n\t\t\t\t\t\tDistro: &distro.Distro{\n\t\t\t\t\t\t\tType:    distro.AlmaLinux,\n\t\t\t\t\t\t\tVersion: \"8.9\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tMetadata: pkg.RpmMetadata{\n\t\t\t\t\t\t\tModularityLabel: strPtr(\"python38:3.8:8090020230810123456:3b72e4d2\"),\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tDetails: createExpectedDetails(pkg.Package{\n\t\t\t\t\t\tName:    \"python38\",\n\t\t\t\t\t\tVersion: \"3.8.17-1.module_el8.9.0+3633+e453b53a\",\n\t\t\t\t\t\tDistro:  &distro.Distro{Type: distro.AlmaLinux, Version: \"8.9\"},\n\t\t\t\t\t}, vulnerability.Vulnerability{\n\t\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\t\tID:        \"CVE-2007-4559\",\n\t\t\t\t\t\t\tNamespace: \"redhat:distro:redhat:8\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\t// Details should reflect the AlmaLinux constraint\n\t\t\t\t\t\tConstraint: createConstraint(t, \"< 3.8.17-2.module_el8.9.0+3633+e453b53a\", version.RpmFormat),\n\t\t\t\t\t}),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:        \"Scenario 4: Wont-fix vulnerability reported when no AlmaLinux unaffected record\",\n\t\t\tdescription: \"tar package - RHEL wont-fix, no AlmaLinux unaffected record, vulnerability reported\",\n\n\t\t\tpkg: pkg.Package{\n\t\t\t\tID:      pkg.ID(\"tar-scenario4\"),\n\t\t\t\tName:    \"tar\",\n\t\t\t\tVersion: \"2:1.30-5.el8\",\n\t\t\t\tType:    syftPkg.RpmPkg,\n\t\t\t\tDistro: &distro.Distro{\n\t\t\t\t\tType:    distro.AlmaLinux,\n\t\t\t\t\tVersion: \"8.0\",\n\t\t\t\t},\n\t\t\t\tMetadata: pkg.RpmMetadata{\n\t\t\t\t\tModularityLabel: nil,\n\t\t\t\t},\n\t\t\t},\n\n\t\t\trhelVulns: []vulnerability.Vulnerability{\n\t\t\t\t{\n\t\t\t\t\tPackageName: \"tar\",\n\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\tID:        \"CVE-2005-2541\",\n\t\t\t\t\t\tNamespace: \"redhat:distro:redhat:8\",\n\t\t\t\t\t},\n\t\t\t\t\tConstraint: createConstraint(t, \">= 0\", version.RpmFormat),\n\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\tVersions: []string{},\n\t\t\t\t\t\tState:    vulnerability.FixStateWontFix,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\n\t\t\t// NO AlmaLinux unaffected record - AlmaLinux follows RHEL's wont-fix\n\t\t\talmaVulns: []vulnerability.Vulnerability{},\n\n\t\t\t// Vulnerability should be reported with RHEL wont-fix state\n\t\t\texpectedMatches: []match.Match{\n\t\t\t\t{\n\t\t\t\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\t\t\t\tPackageName: \"tar\",\n\t\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\t\tID:        \"CVE-2005-2541\",\n\t\t\t\t\t\t\tNamespace: \"redhat:distro:redhat:8\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tConstraint: createConstraint(t, \">= 0\", version.RpmFormat),\n\t\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\t\tVersions: []string{},\n\t\t\t\t\t\t\tState:    vulnerability.FixStateWontFix,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tPackage: pkg.Package{\n\t\t\t\t\t\tID:      pkg.ID(\"tar-scenario4\"),\n\t\t\t\t\t\tName:    \"tar\",\n\t\t\t\t\t\tVersion: \"2:1.30-5.el8\",\n\t\t\t\t\t\tType:    syftPkg.RpmPkg,\n\t\t\t\t\t\tDistro: &distro.Distro{\n\t\t\t\t\t\t\tType:    distro.AlmaLinux,\n\t\t\t\t\t\t\tVersion: \"8.0\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tMetadata: pkg.RpmMetadata{\n\t\t\t\t\t\t\tModularityLabel: nil,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tDetails: createExpectedDetails(pkg.Package{\n\t\t\t\t\t\tName:    \"tar\",\n\t\t\t\t\t\tVersion: \"2:1.30-5.el8\",\n\t\t\t\t\t\tDistro:  &distro.Distro{Type: distro.AlmaLinux, Version: \"8.0\"},\n\t\t\t\t\t}, vulnerability.Vulnerability{\n\t\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\t\tID:        \"CVE-2005-2541\",\n\t\t\t\t\t\t\tNamespace: \"redhat:distro:redhat:8\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tConstraint: createConstraint(t, \">= 0\", version.RpmFormat),\n\t\t\t\t\t}),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:        \"Upstream match: binary package vulnerable via source package with fix replacement\",\n\t\t\tdescription: \"Binary package python3-tkinter with upstream python3 - RHEL disclosure for source, AlmaLinux fixes binary\",\n\n\t\t\tpkg: pkg.Package{\n\t\t\t\tID:      pkg.ID(\"python3-tkinter-upstream\"),\n\t\t\t\tName:    \"python3-tkinter\",\n\t\t\t\tVersion: \"3.6.8-40.el8_6\",\n\t\t\t\tType:    syftPkg.RpmPkg,\n\t\t\t\tDistro: &distro.Distro{\n\t\t\t\t\tType:    distro.AlmaLinux,\n\t\t\t\t\tVersion: \"8.10\",\n\t\t\t\t},\n\t\t\t\tUpstreams: []pkg.UpstreamPackage{\n\t\t\t\t\t{\n\t\t\t\t\t\tName:    \"python3\",\n\t\t\t\t\t\tVersion: \"3.6.8-40.el8_6\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tMetadata: pkg.RpmMetadata{\n\t\t\t\t\tEpoch: intPtr(0),\n\t\t\t\t},\n\t\t\t},\n\n\t\t\trhelVulns: []vulnerability.Vulnerability{\n\t\t\t\t{\n\t\t\t\t\tPackageName: \"python3\",\n\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\tID:        \"CVE-2007-4559\",\n\t\t\t\t\t\tNamespace: \"redhat:distro:redhat:8\",\n\t\t\t\t\t},\n\t\t\t\t\tConstraint: createConstraint(t, \"< 0:3.6.8-56.el8_9\", version.RpmFormat),\n\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\tVersions: []string{\"0:3.6.8-56.el8_9\"},\n\t\t\t\t\t\tState:    vulnerability.FixStateFixed,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\n\t\t\talmaVulns: []vulnerability.Vulnerability{\n\t\t\t\t{\n\t\t\t\t\tPackageName: \"python3-tkinter\",\n\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\tID:        \"ALSA-2023:7151\",\n\t\t\t\t\t\tNamespace: \"almalinux:distro:almalinux:8\",\n\t\t\t\t\t},\n\t\t\t\t\tRelatedVulnerabilities: []vulnerability.Reference{\n\t\t\t\t\t\t{ID: \"CVE-2007-4559\"},\n\t\t\t\t\t},\n\t\t\t\t\tConstraint: createConstraint(t, \">= 3.6.8-56.el8_9.alma.1\", version.RpmFormat),\n\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\tVersions: []string{\"3.6.8-56.el8_9.alma.1\"},\n\t\t\t\t\t\tState:    vulnerability.FixStateFixed,\n\t\t\t\t\t},\n\t\t\t\t\tUnaffected: true,\n\t\t\t\t},\n\t\t\t},\n\n\t\t\t// Package version 3.6.8-40 < fix 3.6.8-56, vulnerable with AlmaLinux fix info\n\t\t\texpectedMatches: []match.Match{\n\t\t\t\t{\n\t\t\t\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\t\t\t\tPackageName: \"python3\",\n\t\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\t\tID:        \"CVE-2007-4559\",\n\t\t\t\t\t\t\tNamespace: \"redhat:distro:redhat:8\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\t// Constraint should be updated to match AlmaLinux fix version\n\t\t\t\t\t\tConstraint: createConstraint(t, \"< 3.6.8-56.el8_9.alma.1\", version.RpmFormat),\n\t\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\t\tVersions: []string{\"3.6.8-56.el8_9.alma.1\"},\n\t\t\t\t\t\t\tState:    vulnerability.FixStateFixed,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tAdvisories: []vulnerability.Advisory{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tID:   \"ALSA-2023:7151\",\n\t\t\t\t\t\t\t\tLink: \"https://errata.almalinux.org/8/ALSA-2023-7151.html\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tPackage: pkg.Package{\n\t\t\t\t\t\tID:      pkg.ID(\"python3-tkinter-upstream\"),\n\t\t\t\t\t\tName:    \"python3-tkinter\",\n\t\t\t\t\t\tVersion: \"3.6.8-40.el8_6\",\n\t\t\t\t\t\tType:    syftPkg.RpmPkg,\n\t\t\t\t\t\tDistro: &distro.Distro{\n\t\t\t\t\t\t\tType:    distro.AlmaLinux,\n\t\t\t\t\t\t\tVersion: \"8.10\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tUpstreams: []pkg.UpstreamPackage{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tName:    \"python3\",\n\t\t\t\t\t\t\t\tVersion: \"3.6.8-40.el8_6\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tMetadata: pkg.RpmMetadata{\n\t\t\t\t\t\t\tEpoch: intPtr(0),\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\t// Details should show ExactIndirectMatch since vulnerability is via upstream\n\t\t\t\t\tDetails: []match.Detail{{\n\t\t\t\t\t\tType:    match.ExactIndirectMatch,\n\t\t\t\t\t\tMatcher: match.RpmMatcher,\n\t\t\t\t\t\tSearchedBy: match.DistroParameters{\n\t\t\t\t\t\t\tDistro: match.DistroIdentification{\n\t\t\t\t\t\t\t\tType:    \"almalinux\",\n\t\t\t\t\t\t\t\tVersion: \"8.10\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tPackage: match.PackageParameter{\n\t\t\t\t\t\t\t\tName:    \"python3-tkinter\",\n\t\t\t\t\t\t\t\tVersion: \"3.6.8-40.el8_6\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tNamespace: \"redhat:distro:redhat:8\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tFound: match.DistroResult{\n\t\t\t\t\t\t\tVulnerabilityID:   \"CVE-2007-4559\",\n\t\t\t\t\t\t\tVersionConstraint: \"< 3.6.8-56.el8_9.alma.1 (rpm)\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tConfidence: 1.0,\n\t\t\t\t\t}},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:        \"Alias handling: RHEL CVEs filtered by AlmaLinux ALSA with related vulnerabilities\",\n\t\t\tdescription: \"RHEL has 2 CVEs, AlmaLinux ALSA relates to one, package >= fix filters that CVE\",\n\n\t\t\tpkg: pkg.Package{\n\t\t\t\tName:    \"httpd\",\n\t\t\t\tVersion: \"2.4.37-47.el8.alma\",\n\t\t\t\tType:    syftPkg.RpmPkg,\n\t\t\t\tDistro: &distro.Distro{\n\t\t\t\t\tType:    distro.AlmaLinux,\n\t\t\t\t\tVersion: \"8.7\",\n\t\t\t\t},\n\t\t\t},\n\n\t\t\trhelVulns: []vulnerability.Vulnerability{\n\t\t\t\t{\n\t\t\t\t\tPackageName: \"httpd\",\n\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\tID:        \"CVE-2023-1234\",\n\t\t\t\t\t\tNamespace: \"redhat:distro:redhat:8\",\n\t\t\t\t\t},\n\t\t\t\t\tConstraint: createConstraint(t, \"< 2.4.37-50.el8\", version.RpmFormat),\n\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\tVersions: []string{\"2.4.37-50.el8\"},\n\t\t\t\t\t\tState:    vulnerability.FixStateFixed,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tPackageName: \"httpd\",\n\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\tID:        \"CVE-2023-5678\",\n\t\t\t\t\t\tNamespace: \"redhat:distro:redhat:8\",\n\t\t\t\t\t},\n\t\t\t\t\tConstraint: createConstraint(t, \"< 2.4.37-50.el8\", version.RpmFormat),\n\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\tVersions: []string{\"2.4.37-50.el8\"},\n\t\t\t\t\t\tState:    vulnerability.FixStateFixed,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\n\t\t\talmaVulns: []vulnerability.Vulnerability{\n\t\t\t\t{\n\t\t\t\t\tPackageName: \"httpd\",\n\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\tID:        \"ALSA-2023:1234\",\n\t\t\t\t\t\tNamespace: \"almalinux:distro:almalinux:8\",\n\t\t\t\t\t},\n\t\t\t\t\tRelatedVulnerabilities: []vulnerability.Reference{\n\t\t\t\t\t\t{ID: \"CVE-2023-1234\"}, // ALSA aliases CVE\n\t\t\t\t\t},\n\t\t\t\t\t// Package version 47 >= fix version 40, so CVE-2023-1234 is filtered\n\t\t\t\t\tConstraint: createConstraint(t, \">= 2.4.37-40.el8.alma\", version.RpmFormat),\n\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\tVersions: []string{\"2.4.37-40.el8.alma\"},\n\t\t\t\t\t\tState:    vulnerability.FixStateFixed,\n\t\t\t\t\t},\n\t\t\t\t\tUnaffected: true,\n\t\t\t\t},\n\t\t\t},\n\n\t\t\t// CVE-2023-1234 filtered by AlmaLinux unaffected record\n\t\t\t// Only CVE-2023-5678 should remain (no AlmaLinux unaffected record for it)\n\t\t\texpectedMatches: []match.Match{\n\t\t\t\t{\n\t\t\t\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\t\t\t\tPackageName: \"httpd\",\n\t\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\t\tID:        \"CVE-2023-5678\",\n\t\t\t\t\t\t\tNamespace: \"redhat:distro:redhat:8\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tConstraint: createConstraint(t, \"< 2.4.37-50.el8\", version.RpmFormat),\n\t\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\t\tVersions: []string{\"2.4.37-50.el8\"},\n\t\t\t\t\t\t\tState:    vulnerability.FixStateFixed,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tPackage: pkg.Package{\n\t\t\t\t\t\tName:    \"httpd\",\n\t\t\t\t\t\tVersion: \"2.4.37-47.el8.alma\",\n\t\t\t\t\t\tType:    syftPkg.RpmPkg,\n\t\t\t\t\t\tDistro: &distro.Distro{\n\t\t\t\t\t\t\tType:    distro.AlmaLinux,\n\t\t\t\t\t\t\tVersion: \"8.7\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tDetails: createExpectedDetails(pkg.Package{\n\t\t\t\t\t\tName:    \"httpd\",\n\t\t\t\t\t\tVersion: \"2.4.37-47.el8.alma\",\n\t\t\t\t\t\tDistro:  &distro.Distro{Type: distro.AlmaLinux, Version: \"8.7\"},\n\t\t\t\t\t}, vulnerability.Vulnerability{\n\t\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\t\tID:        \"CVE-2023-5678\",\n\t\t\t\t\t\t\tNamespace: \"redhat:distro:redhat:8\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tConstraint: createConstraint(t, \"< 2.4.37-50.el8\", version.RpmFormat),\n\t\t\t\t\t}),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:        \"Epoch mismatch regression: no false positive from binary/source epoch difference\",\n\t\t\tdescription: \"Binary package epoch 0, upstream source package version without epoch, RHEL constraint has epoch 4\",\n\n\t\t\tpkg: pkg.Package{\n\t\t\t\tID:      pkg.ID(\"perl-errno-epoch\"),\n\t\t\t\tName:    \"perl-Errno\",\n\t\t\t\tVersion: \"0:1.28-422.el8.0.1\",\n\t\t\t\tType:    syftPkg.RpmPkg,\n\t\t\t\tDistro: &distro.Distro{\n\t\t\t\t\tType:    distro.AlmaLinux,\n\t\t\t\t\tVersion: \"8.10\",\n\t\t\t\t},\n\t\t\t\tUpstreams: []pkg.UpstreamPackage{\n\t\t\t\t\t{\n\t\t\t\t\t\tName:    \"perl\",\n\t\t\t\t\t\tVersion: \"5.26.3-422.el8.0.1\", // No epoch in sourceRPM metadata\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tMetadata: pkg.RpmMetadata{\n\t\t\t\t\tEpoch: intPtr(0),\n\t\t\t\t},\n\t\t\t},\n\n\t\t\trhelVulns: []vulnerability.Vulnerability{\n\t\t\t\t{\n\t\t\t\t\tPackageName: \"perl\",\n\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\tID:        \"CVE-2020-10543\",\n\t\t\t\t\t\tNamespace: \"redhat:distro:redhat:8\",\n\t\t\t\t\t},\n\t\t\t\t\tConstraint: createConstraint(t, \"< 4:5.26.3-419.el8\", version.RpmFormat),\n\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\tVersions: []string{\"4:5.26.3-419.el8\"},\n\t\t\t\t\t\tState:    vulnerability.FixStateFixed,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\n\t\t\talmaVulns: []vulnerability.Vulnerability{},\n\n\t\t\t// Package version 5.26.3-422 > fix 5.26.3-419, NOT vulnerable\n\t\t\t// This is a regression test: the bug was comparing binary epoch (0) against constraint with source epoch (4),\n\t\t\t// causing false positive: 0:1.28-422.el8.0.1 < 4:5.26.3-419.el8 = true (WRONG!)\n\t\t\t// Correct behavior: Don't add binary epoch to source version, compare without epochs\n\t\t\texpectedMatches: nil,\n\t\t},\n\t\t{\n\t\t\tname:        \"Epoch handling: upstream vulnerable, no AlmaLinux advisory\",\n\t\t\tdescription: \"Binary package with upstream source, source version < RHEL fix, no AlmaLinux advisory → match\",\n\n\t\t\tpkg: pkg.Package{\n\t\t\t\tID:      pkg.ID(\"perl-errno-vulnerable\"),\n\t\t\t\tName:    \"perl-Errno\",\n\t\t\t\tVersion: \"0:1.28-416.el8\",\n\t\t\t\tType:    syftPkg.RpmPkg,\n\t\t\t\tDistro: &distro.Distro{\n\t\t\t\t\tType:    distro.AlmaLinux,\n\t\t\t\t\tVersion: \"8.10\",\n\t\t\t\t},\n\t\t\t\tUpstreams: []pkg.UpstreamPackage{\n\t\t\t\t\t{\n\t\t\t\t\t\tName:    \"perl\",\n\t\t\t\t\t\tVersion: \"5.26.3-416.el8\", // No epoch, version 416 < fix 419\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tMetadata: pkg.RpmMetadata{\n\t\t\t\t\tEpoch: intPtr(0),\n\t\t\t\t},\n\t\t\t},\n\n\t\t\trhelVulns: []vulnerability.Vulnerability{\n\t\t\t\t{\n\t\t\t\t\tPackageName: \"perl\",\n\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\tID:        \"CVE-2020-10543\",\n\t\t\t\t\t\tNamespace: \"redhat:distro:redhat:8\",\n\t\t\t\t\t},\n\t\t\t\t\tConstraint: createConstraint(t, \"< 4:5.26.3-419.el8\", version.RpmFormat),\n\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\tVersions: []string{\"4:5.26.3-419.el8\"},\n\t\t\t\t\t\tState:    vulnerability.FixStateFixed,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\n\t\t\talmaVulns: []vulnerability.Vulnerability{},\n\n\t\t\t// Package version 5.26.3-416 < fix 5.26.3-419, IS vulnerable\n\t\t\t// No AlmaLinux advisory, so RHEL disclosure should be reported\n\t\t\texpectedMatches: []match.Match{\n\t\t\t\t{\n\t\t\t\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\t\t\t\tPackageName: \"perl\",\n\t\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\t\tID:        \"CVE-2020-10543\",\n\t\t\t\t\t\t\tNamespace: \"redhat:distro:redhat:8\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tConstraint: createConstraint(t, \"< 4:5.26.3-419.el8\", version.RpmFormat),\n\t\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\t\tVersions: []string{\"4:5.26.3-419.el8\"},\n\t\t\t\t\t\t\tState:    vulnerability.FixStateFixed,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tPackage: pkg.Package{\n\t\t\t\t\t\tID:      pkg.ID(\"perl-errno-vulnerable\"),\n\t\t\t\t\t\tName:    \"perl-Errno\",\n\t\t\t\t\t\tVersion: \"0:1.28-416.el8\",\n\t\t\t\t\t\tType:    syftPkg.RpmPkg,\n\t\t\t\t\t\tDistro: &distro.Distro{\n\t\t\t\t\t\t\tType:    distro.AlmaLinux,\n\t\t\t\t\t\t\tVersion: \"8.10\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tUpstreams: []pkg.UpstreamPackage{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tName:    \"perl\",\n\t\t\t\t\t\t\t\tVersion: \"5.26.3-416.el8\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tMetadata: pkg.RpmMetadata{\n\t\t\t\t\t\t\tEpoch: intPtr(0),\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\t// Details should show ExactIndirectMatch since vulnerability is via upstream\n\t\t\t\t\tDetails: []match.Detail{{\n\t\t\t\t\t\tType:    match.ExactIndirectMatch,\n\t\t\t\t\t\tMatcher: match.RpmMatcher,\n\t\t\t\t\t\tSearchedBy: match.DistroParameters{\n\t\t\t\t\t\t\tDistro: match.DistroIdentification{\n\t\t\t\t\t\t\t\tType:    \"almalinux\",\n\t\t\t\t\t\t\t\tVersion: \"8.10\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tPackage: match.PackageParameter{\n\t\t\t\t\t\t\t\tName:    \"perl-Errno\",\n\t\t\t\t\t\t\t\tVersion: \"0:1.28-416.el8\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tNamespace: \"redhat:distro:redhat:8\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tFound: match.DistroResult{\n\t\t\t\t\t\t\tVulnerabilityID:   \"CVE-2020-10543\",\n\t\t\t\t\t\t\tVersionConstraint: \"< 4:5.26.3-419.el8 (rpm)\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tConfidence: 1.0,\n\t\t\t\t\t}},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:        \"Epoch handling: upstream vulnerable, AlmaLinux fixes binary package\",\n\t\t\tdescription: \"Binary package with upstream source, source version < RHEL fix, AlmaLinux fixes binary → filtered\",\n\n\t\t\tpkg: pkg.Package{\n\t\t\t\tID:      pkg.ID(\"perl-errno-alma-fixed\"),\n\t\t\t\tName:    \"perl-Errno\",\n\t\t\t\tVersion: \"0:1.28-416.el8\",\n\t\t\t\tType:    syftPkg.RpmPkg,\n\t\t\t\tDistro: &distro.Distro{\n\t\t\t\t\tType:    distro.AlmaLinux,\n\t\t\t\t\tVersion: \"8.10\",\n\t\t\t\t},\n\t\t\t\tUpstreams: []pkg.UpstreamPackage{\n\t\t\t\t\t{\n\t\t\t\t\t\tName:    \"perl\",\n\t\t\t\t\t\tVersion: \"5.26.3-416.el8\", // No epoch, version 416 < RHEL fix 419\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tMetadata: pkg.RpmMetadata{\n\t\t\t\t\tEpoch: intPtr(0),\n\t\t\t\t},\n\t\t\t},\n\n\t\t\trhelVulns: []vulnerability.Vulnerability{\n\t\t\t\t{\n\t\t\t\t\tPackageName: \"perl\",\n\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\tID:        \"CVE-2020-10543\",\n\t\t\t\t\t\tNamespace: \"redhat:distro:redhat:8\",\n\t\t\t\t\t},\n\t\t\t\t\tConstraint: createConstraint(t, \"< 4:5.26.3-419.el8\", version.RpmFormat),\n\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\tVersions: []string{\"4:5.26.3-419.el8\"},\n\t\t\t\t\t\tState:    vulnerability.FixStateFixed,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\n\t\t\talmaVulns: []vulnerability.Vulnerability{\n\t\t\t\t{\n\t\t\t\t\tPackageName: \"perl-Errno\",\n\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\tID:        \"ALSA-2020:4514\",\n\t\t\t\t\t\tNamespace: \"almalinux:distro:almalinux:8\",\n\t\t\t\t\t},\n\t\t\t\t\tRelatedVulnerabilities: []vulnerability.Reference{\n\t\t\t\t\t\t{ID: \"CVE-2020-10543\"},\n\t\t\t\t\t},\n\t\t\t\t\t// AlmaLinux fixes at binary package version 1.28-416.el8, marking it unaffected\n\t\t\t\t\tConstraint: createConstraint(t, \">= 1.28-416.el8\", version.RpmFormat),\n\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\tVersions: []string{\"1.28-416.el8\"},\n\t\t\t\t\t\tState:    vulnerability.FixStateFixed,\n\t\t\t\t\t},\n\t\t\t\t\tAdvisories: []vulnerability.Advisory{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tID:   \"ALSA-2020:4514\",\n\t\t\t\t\t\t\tLink: \"https://errata.almalinux.org/8/ALSA-2020-4514.html\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tUnaffected: true,\n\t\t\t\t},\n\t\t\t},\n\n\t\t\t// Even though upstream source is vulnerable per RHEL,\n\t\t\t// AlmaLinux marks this binary package version as unaffected\n\t\t\t// Package version 1.28-416.el8 >= AlmaLinux fix 1.28-416.el8 → filtered\n\t\t\texpectedMatches: nil,\n\t\t},\n\t\t{\n\t\t\tname:        \"Epoch handling: upstream vulnerable, AlmaLinux fix on binary, package below fix\",\n\t\t\tdescription: \"Binary package with upstream source, source version < RHEL fix, AlmaLinux has fix but package < fix → match with AlmaLinux fix\",\n\n\t\t\tpkg: pkg.Package{\n\t\t\t\tID:      pkg.ID(\"perl-errno-needs-alma-fix\"),\n\t\t\t\tName:    \"perl-Errno\",\n\t\t\t\tVersion: \"0:1.28-415.el8\",\n\t\t\t\tType:    syftPkg.RpmPkg,\n\t\t\t\tDistro: &distro.Distro{\n\t\t\t\t\tType:    distro.AlmaLinux,\n\t\t\t\t\tVersion: \"8.10\",\n\t\t\t\t},\n\t\t\t\tUpstreams: []pkg.UpstreamPackage{\n\t\t\t\t\t{\n\t\t\t\t\t\tName:    \"perl\",\n\t\t\t\t\t\tVersion: \"5.26.3-415.el8\", // No epoch, version 415 < RHEL fix 419\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tMetadata: pkg.RpmMetadata{\n\t\t\t\t\tEpoch: intPtr(0),\n\t\t\t\t},\n\t\t\t},\n\n\t\t\trhelVulns: []vulnerability.Vulnerability{\n\t\t\t\t{\n\t\t\t\t\tPackageName: \"perl\",\n\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\tID:        \"CVE-2020-10543\",\n\t\t\t\t\t\tNamespace: \"redhat:distro:redhat:8\",\n\t\t\t\t\t},\n\t\t\t\t\tConstraint: createConstraint(t, \"< 4:5.26.3-419.el8\", version.RpmFormat),\n\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\tVersions: []string{\"4:5.26.3-419.el8\"},\n\t\t\t\t\t\tState:    vulnerability.FixStateFixed,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\n\t\t\talmaVulns: []vulnerability.Vulnerability{\n\t\t\t\t{\n\t\t\t\t\tPackageName: \"perl-Errno\",\n\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\tID:        \"ALSA-2020:4514\",\n\t\t\t\t\t\tNamespace: \"almalinux:distro:almalinux:8\",\n\t\t\t\t\t},\n\t\t\t\t\tRelatedVulnerabilities: []vulnerability.Reference{\n\t\t\t\t\t\t{ID: \"CVE-2020-10543\"},\n\t\t\t\t\t},\n\t\t\t\t\t// AlmaLinux fixes at binary package version 1.28-418.el8\n\t\t\t\t\tConstraint: createConstraint(t, \">= 1.28-418.el8\", version.RpmFormat),\n\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\tVersions: []string{\"1.28-418.el8\"},\n\t\t\t\t\t\tState:    vulnerability.FixStateFixed,\n\t\t\t\t\t},\n\t\t\t\t\tAdvisories: []vulnerability.Advisory{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tID:   \"ALSA-2020:4514\",\n\t\t\t\t\t\t\tLink: \"https://errata.almalinux.org/8/ALSA-2020-4514.html\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tUnaffected: true,\n\t\t\t\t},\n\t\t\t},\n\n\t\t\t// Upstream source is vulnerable per RHEL (415 < 419)\n\t\t\t// Binary package version 1.28-415.el8 < AlmaLinux fix 1.28-418.el8\n\t\t\t// Should report vulnerability with AlmaLinux fix info (fix replacement)\n\t\t\texpectedMatches: []match.Match{\n\t\t\t\t{\n\t\t\t\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\t\t\t\tPackageName: \"perl\",\n\t\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\t\tID:        \"CVE-2020-10543\",\n\t\t\t\t\t\t\tNamespace: \"redhat:distro:redhat:8\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\t// Constraint should be updated to match AlmaLinux fix version\n\t\t\t\t\t\tConstraint: createConstraint(t, \"< 1.28-418.el8\", version.RpmFormat),\n\t\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\t\tVersions: []string{\"1.28-418.el8\"},\n\t\t\t\t\t\t\tState:    vulnerability.FixStateFixed,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tAdvisories: []vulnerability.Advisory{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tID:   \"ALSA-2020:4514\",\n\t\t\t\t\t\t\t\tLink: \"https://errata.almalinux.org/8/ALSA-2020-4514.html\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tPackage: pkg.Package{\n\t\t\t\t\t\tID:      pkg.ID(\"perl-errno-needs-alma-fix\"),\n\t\t\t\t\t\tName:    \"perl-Errno\",\n\t\t\t\t\t\tVersion: \"0:1.28-415.el8\",\n\t\t\t\t\t\tType:    syftPkg.RpmPkg,\n\t\t\t\t\t\tDistro: &distro.Distro{\n\t\t\t\t\t\t\tType:    distro.AlmaLinux,\n\t\t\t\t\t\t\tVersion: \"8.10\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tUpstreams: []pkg.UpstreamPackage{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tName:    \"perl\",\n\t\t\t\t\t\t\t\tVersion: \"5.26.3-415.el8\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tMetadata: pkg.RpmMetadata{\n\t\t\t\t\t\t\tEpoch: intPtr(0),\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\t// Details should show ExactIndirectMatch since vulnerability is via upstream\n\t\t\t\t\tDetails: []match.Detail{{\n\t\t\t\t\t\tType:    match.ExactIndirectMatch,\n\t\t\t\t\t\tMatcher: match.RpmMatcher,\n\t\t\t\t\t\tSearchedBy: match.DistroParameters{\n\t\t\t\t\t\t\tDistro: match.DistroIdentification{\n\t\t\t\t\t\t\t\tType:    \"almalinux\",\n\t\t\t\t\t\t\t\tVersion: \"8.10\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tPackage: match.PackageParameter{\n\t\t\t\t\t\t\t\tName:    \"perl-Errno\",\n\t\t\t\t\t\t\t\tVersion: \"0:1.28-415.el8\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tNamespace: \"redhat:distro:redhat:8\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tFound: match.DistroResult{\n\t\t\t\t\t\t\tVulnerabilityID:   \"CVE-2020-10543\",\n\t\t\t\t\t\t\tVersionConstraint: \"< 1.28-418.el8 (rpm)\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tConfidence: 1.0,\n\t\t\t\t\t}},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\t// Create mock provider with all vulnerabilities\n\t\t\tallVulns := append(tt.rhelVulns, tt.almaVulns...)\n\t\t\tmockProvider := &MockProvider{\n\t\t\t\tfindResultsFunc: func(criteria ...vulnerability.Criteria) (result.Set, error) {\n\t\t\t\t\t// Use the mock vulnerability provider to filter\n\t\t\t\t\tvulnProvider := mock.VulnerabilityProvider(allVulns...)\n\t\t\t\t\tvulns, err := vulnProvider.FindVulnerabilities(criteria...)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn nil, err\n\t\t\t\t\t}\n\n\t\t\t\t\t// Convert to result.Set with Details fully populated\n\t\t\t\t\tresultSet := make(result.Set)\n\t\t\t\t\tfor _, vuln := range vulns {\n\t\t\t\t\t\tr := result.Result{\n\t\t\t\t\t\t\tID:              vuln.ID,\n\t\t\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{vuln},\n\t\t\t\t\t\t\tPackage:         &tt.pkg,\n\t\t\t\t\t\t\t// Details must be fully populated per the matcher contract\n\t\t\t\t\t\t\tDetails: []match.Detail{{\n\t\t\t\t\t\t\t\tType:    match.ExactDirectMatch,\n\t\t\t\t\t\t\t\tMatcher: match.RpmMatcher,\n\t\t\t\t\t\t\t\tSearchedBy: match.DistroParameters{\n\t\t\t\t\t\t\t\t\tDistro: match.DistroIdentification{\n\t\t\t\t\t\t\t\t\t\tType:    tt.pkg.Distro.Type.String(),\n\t\t\t\t\t\t\t\t\t\tVersion: tt.pkg.Distro.Version,\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\tPackage: match.PackageParameter{\n\t\t\t\t\t\t\t\t\t\tName:    tt.pkg.Name,\n\t\t\t\t\t\t\t\t\t\tVersion: tt.pkg.Version,\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\tNamespace: vuln.Namespace,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tFound: match.DistroResult{\n\t\t\t\t\t\t\t\t\tVulnerabilityID:   vuln.ID,\n\t\t\t\t\t\t\t\t\tVersionConstraint: vuln.Constraint.String(),\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tConfidence: 1.0,\n\t\t\t\t\t\t\t}},\n\t\t\t\t\t\t}\n\t\t\t\t\t\tresultSet[vuln.ID] = append(resultSet[vuln.ID], r)\n\t\t\t\t\t}\n\t\t\t\t\treturn resultSet, nil\n\t\t\t\t},\n\t\t\t}\n\n\t\t\t// Call the matcher\n\t\t\tmatches, err := almaLinuxMatchesWithUpstreams(mockProvider, tt.pkg)\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// Compare matches using cmp.Diff\n\t\t\t// Only ignore:\n\t\t\t// - PackageQualifiers (tested separately, have unexported fields in implementations)\n\t\t\t// - Unexported fields within structs\n\t\t\tif diff := cmp.Diff(tt.expectedMatches, matches,\n\t\t\t\tcmpopts.IgnoreUnexported(match.Match{}, match.Detail{}, pkg.Package{}, pkg.RpmMetadata{}, file.LocationSet{}, distro.Distro{}),\n\t\t\t\tcmpopts.IgnoreFields(vulnerability.Vulnerability{}, \"PackageQualifiers\")); diff != \"\" {\n\t\t\t\tt.Errorf(\"matches mismatch (-want +got):\\n%s\", diff)\n\t\t\t}\n\t\t})\n\t}\n}\n\n// Helper functions for tests\n\nfunc createConstraint(t *testing.T, constraintStr string, format version.Format) version.Constraint {\n\tconstraint, err := version.GetConstraint(constraintStr, format)\n\trequire.NoError(t, err)\n\treturn constraint\n}\n\nfunc createExpectedDetails(pkg pkg.Package, vuln vulnerability.Vulnerability) []match.Detail {\n\treturn []match.Detail{{\n\t\tType:    match.ExactDirectMatch,\n\t\tMatcher: match.RpmMatcher,\n\t\tSearchedBy: match.DistroParameters{\n\t\t\tDistro: match.DistroIdentification{\n\t\t\t\tType:    pkg.Distro.Type.String(),\n\t\t\t\tVersion: pkg.Distro.Version,\n\t\t\t},\n\t\t\tPackage: match.PackageParameter{\n\t\t\t\tName:    pkg.Name,\n\t\t\t\tVersion: pkg.Version,\n\t\t\t},\n\t\t\tNamespace: vuln.Reference.Namespace,\n\t\t},\n\t\tFound: match.DistroResult{\n\t\t\tVulnerabilityID:   vuln.Reference.ID,\n\t\t\tVersionConstraint: vuln.Constraint.String(),\n\t\t},\n\t\tConfidence: 1.0,\n\t}}\n}\n\nfunc strPtr(s string) *string {\n\treturn &s\n}\n"
  },
  {
    "path": "grype/matcher/rpm/matcher.go",
    "content": "package rpm\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/anchore/grype/grype/match\"\n\t\"github.com/anchore/grype/grype/matcher/internal\"\n\t\"github.com/anchore/grype/grype/matcher/internal/result\"\n\t\"github.com/anchore/grype/grype/pkg\"\n\t\"github.com/anchore/grype/grype/search\"\n\t\"github.com/anchore/grype/grype/version\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n\t\"github.com/anchore/grype/internal/log\"\n\tsyftPkg \"github.com/anchore/syft/syft/pkg\"\n)\n\ntype Matcher struct {\n\tcfg MatcherConfig\n}\n\ntype MatcherConfig struct {\n\tMissingEpochStrategy version.MissingEpochStrategy\n\tUseCPEsForEOL        bool\n}\n\nfunc NewRpmMatcher(cfg MatcherConfig) *Matcher {\n\treturn &Matcher{\n\t\tcfg: cfg,\n\t}\n}\n\nfunc (m *Matcher) PackageTypes() []syftPkg.Type {\n\treturn []syftPkg.Type{syftPkg.RpmPkg}\n}\n\nfunc (m *Matcher) Type() match.MatcherType {\n\treturn match.RpmMatcher\n}\n\n//nolint:funlen\nfunc (m *Matcher) Match(vp vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoreFilter, error) {\n\tvar matches []match.Match\n\n\t// Handle AlmaLinux matching at the top level before the binary/upstream split\n\t// AlmaLinux matching needs to handle both binary and upstream packages internally\n\tif p.Distro != nil && shouldUseAlmaLinuxMatching(p.Distro) {\n\t\talmaMatches, err := m.matchAlmaLinux(vp, p)\n\t\tif err != nil {\n\t\t\treturn nil, nil, fmt.Errorf(\"failed to match AlmaLinux: %w\", err)\n\t\t}\n\t\tmatches = append(matches, almaMatches...)\n\t} else {\n\t\t// For non-AlmaLinux distros, use the standard binary/upstream split\n\t\texactMatches, err := m.matchPackage(vp, p)\n\t\tif err != nil {\n\t\t\treturn nil, nil, fmt.Errorf(\"failed to match by exact package name: %w\", err)\n\t\t}\n\n\t\tmatches = append(matches, exactMatches...)\n\n\t\tsourceMatches, err := m.matchUpstreamPackages(vp, p)\n\t\tif err != nil {\n\t\t\treturn nil, nil, fmt.Errorf(\"failed to match by source indirection: %w\", err)\n\t\t}\n\t\tmatches = append(matches, sourceMatches...)\n\t}\n\n\t// if configured, also search by CPEs for packages from EOL distros\n\tif m.cfg.UseCPEsForEOL && internal.IsDistroEOL(vp, p.Distro) {\n\t\tlog.WithFields(\"package\", p.Name, \"distro\", p.Distro).Debug(\"distro is EOL, searching by CPEs\")\n\t\tcpeMatches, err := internal.MatchPackageByCPEs(vp, p, m.Type())\n\t\tswitch {\n\t\tcase errors.Is(err, internal.ErrEmptyCPEMatch):\n\t\t\tlog.WithFields(\"package\", p.Name).Debug(\"package has no CPEs for EOL fallback matching\")\n\t\tcase err != nil:\n\t\t\tlog.WithFields(\"package\", p.Name, \"error\", err).Debug(\"failed to match by CPEs for EOL distro\")\n\t\tdefault:\n\t\t\tmatches = append(matches, cpeMatches...)\n\t\t}\n\t}\n\n\treturn matches, nil, nil\n}\n\n// matchAlmaLinux handles AlmaLinux-specific matching logic that considers both binary and upstream packages\n// This must be called at the top level (before the binary/upstream split) because AlmaLinux matching\n// needs to search for RHEL disclosures for both the binary package and its upstreams, then filter\n// using AlmaLinux unaffected records for both the binary package and related packages\nfunc (m *Matcher) matchAlmaLinux(vp vulnerability.Provider, p pkg.Package) ([]match.Match, error) {\n\tif p.Distro == nil {\n\t\treturn nil, nil\n\t}\n\tif isUnknownVersion(p.Version) {\n\t\tlog.WithFields(\"package\", p.Name).Trace(\"skipping package with unknown version\")\n\t\treturn nil, nil\n\t}\n\n\tprovider := result.NewProvider(vp, p, m.Type())\n\n\t// Add epoch if applicable for the binary package\n\tbinaryPkg := p\n\taddEpochIfApplicable(&binaryPkg)\n\n\t// Call almaLinuxMatches with both the binary package and its upstreams\n\treturn almaLinuxMatchesWithUpstreams(provider, binaryPkg)\n}\n\n// matchPackage matches the given package against the vulnerability provider (direct match).\n//\n// Regarding RPM epochs... we know that the package and vulnerability will have\n// well-specified epochs since both are sourced from either the RPM DB directly or\n// the upstream RedHat vulnerability data. Note: this is very much UNLIKE our\n// matching on a source package above where the epoch could be dropped in the\n// reference data. This means that any missing epoch CAN be assumed to be zero,\n// as it falls into the case of \"the project elected to NOT have an epoch for the\n// first version scheme\" and not into any other case.\n//\n// For this reason match exactly on a package, we should be EXPLICIT about the\n// epoch (since downstream version comparison logic will strip the epoch during\n// comparison for the above-mentioned reasons --essentially for the source RPM\n// case). To do this, we fill in missing epoch values in the package versions with\n// an explicit 0.\nfunc (m *Matcher) matchPackage(vp vulnerability.Provider, p pkg.Package) ([]match.Match, error) {\n\tprovider := result.NewProvider(vp, p, m.Type())\n\n\t// we want to ensure that the version ALWAYS has an epoch specified... but at the same time we do not want to modify the\n\t// original package that was passed in when making matches. This is why we create the provider with the original package\n\t// then patch the epoch into the version of the package that we are searching with.\n\taddEpochIfApplicable(&p)\n\n\tmatches, err := m.findMatches(provider, p)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to find vulnerabilities by dpkg source indirection: %w\", err)\n\t}\n\n\treturn matches, nil\n}\n\n// matchUpstreamPackages finds matches with a synthetic package based on the sourceRPM (indirect match).\n\n// Regarding RPM epoch and comparisons... RedHat is explicit that when an RPM\n// epoch is not specified that it should be assumed to be zero (see\n// https://github.com/rpm-software-management/rpm/issues/450). This comment from\n// RedHat is applicable for a project that has elected to not use epoch and has\n// not changed their version scheme at all --therefore it is safe to assume that\n// the epoch (though not specified) is 0. However, in cases where there may be a\n// non-zero epoch and it has been omitted from the version string, it is NOT safe\n// to assume an epoch of 0... as this could lead to misleading comparison\n// results.\n\n// For example, take the perl-Errno package:\n//\t\tname: \t\tperl-Errno\n//\t\tversion:\t0:1.28-419.el8_4.1\n//\t\tsourceRPM:\tperl-5.26.3-419.el8_4.1.src.rpm\n\n// Say we have a vulnerability with the following information (note this is\n// against the SOURCE package \"perl\", not the target package, \"perl-Errno\"):\n// \t\tID:\t\t\t\t\tCVE-2020-10543\n//\t\tPackage Name:\t\tperl\n//\t\tVersion constraint:\t< 4:5.26.3-419.el8\n\n// Note that the vulnerability information has complete knowledge about the\n// version and it's lineage (epoch + version), however, the source package\n// information for perl-Errno does not include any information about epoch. With\n// the rule from RedHat we should assume a 0 epoch and make the comparison:\n\n//\t\t0:5.26.3-419.el8 < 4:5.26.3-419.el8 = true! ... therefore, we've been vulnerable since epoch 0 < 4.\n//                                                  ... this is an INVALID comparison!\n\n// The problem with this is that sourceRPMs tend to not specify epoch even though\n// there may be a non-zero epoch for that package! This is important. The \"more\n// correct\" thing to do in this case is to drop the epoch:\n\n//\t\t5.26.3-419.el8 < 5.26.3-419.el8 = false!    ... these are the SAME VERSION\n\n// There is still a problem with this approach: it essentially makes an\n// assumption that a missing epoch really is the SAME epoch to the other version\n// being compared (in our example, no perl epoch on one side means we should\n// really assume an epoch of 4 on the other side). This could still lead to\n// problems since an epoch delimits potentially non-comparable version lineages.\nfunc (m *Matcher) matchUpstreamPackages(vp vulnerability.Provider, p pkg.Package) ([]match.Match, error) {\n\tprovider := result.NewProvider(vp, p, m.Type())\n\n\tvar matches []match.Match\n\n\tfor _, indirectPackage := range pkg.UpstreamPackages(p) {\n\t\tindirectMatches, err := m.findMatches(provider, indirectPackage)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to find vulnerabilities for rpm upstream source package: %w\", err)\n\t\t}\n\t\tmatches = append(matches, indirectMatches...)\n\t}\n\n\treturn matches, nil\n}\n\nfunc (m *Matcher) findMatches(provider result.Provider, searchPkg pkg.Package) ([]match.Match, error) {\n\tif searchPkg.Distro == nil {\n\t\treturn nil, nil\n\t}\n\tif isUnknownVersion(searchPkg.Version) {\n\t\tlog.WithFields(\"package\", searchPkg.Name).Trace(\"skipping package with unknown version\")\n\t\treturn nil, nil\n\t}\n\n\tswitch {\n\tcase shouldUseRedhatEUSMatching(searchPkg.Distro):\n\t\treturn redhatEUSMatches(provider, searchPkg, m.cfg.MissingEpochStrategy)\n\tdefault:\n\t\treturn m.standardMatches(provider, searchPkg)\n\t}\n}\n\nfunc (m *Matcher) standardMatches(provider result.Provider, searchPkg pkg.Package) ([]match.Match, error) {\n\t// Create version with config embedded\n\tpkgVersion := version.NewWithConfig(\n\t\tsearchPkg.Version,\n\t\tpkg.VersionFormat(searchPkg),\n\t\tversion.ComparisonConfig{\n\t\t\tMissingEpochStrategy: m.cfg.MissingEpochStrategy,\n\t\t},\n\t)\n\n\tdisclosures, err := provider.FindResults(\n\t\tsearch.ByPackageName(searchPkg.Name),\n\t\tsearch.ByDistro(*searchPkg.Distro),\n\t\tinternal.OnlyQualifiedPackages(searchPkg),\n\t\tinternal.OnlyVulnerableVersions(pkgVersion),\n\t)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"matcher failed to fetch disclosures for distro=%q pkg=%q: %w\", searchPkg.Distro, searchPkg.Name, err)\n\t}\n\n\treturn disclosures.ToMatches(), nil\n}\n\nfunc addEpochIfApplicable(p *pkg.Package) {\n\tmeta, ok := p.Metadata.(pkg.RpmMetadata)\n\tver := p.Version\n\tif ver == \"\" {\n\t\treturn // no version to work with, so we should not bother with an epoch\n\t}\n\tswitch {\n\tcase strings.Contains(ver, \":\"):\n\t\t// we already have an epoch embedded in the version string\n\t\treturn\n\tcase ok && meta.Epoch != nil:\n\t\t// we have an explicit epoch in the metadata\n\t\tp.Version = fmt.Sprintf(\"%d:%s\", *meta.Epoch, ver)\n\tdefault:\n\t\t// no epoch was found, so we will add one\n\t\tp.Version = \"0:\" + ver\n\t}\n}\n\nfunc isUnknownVersion(v string) bool {\n\treturn v == \"\" || strings.ToLower(v) == \"unknown\"\n}\n"
  },
  {
    "path": "grype/matcher/rpm/matcher_mocks_test.go",
    "content": "package rpm\n\nimport (\n\t\"time\"\n\n\t\"github.com/anchore/grype/grype/distro\"\n\t\"github.com/anchore/grype/grype/pkg/qualifier\"\n\t\"github.com/anchore/grype/grype/pkg/qualifier/rpmmodularity\"\n\t\"github.com/anchore/grype/grype/version\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n\t\"github.com/anchore/grype/grype/vulnerability/mock\"\n\tsyftCpe \"github.com/anchore/syft/syft/cpe\"\n)\n\nfunc newMockProvider(packageName, indirectName string, withEpoch bool, withPackageQualifiers bool) vulnerability.Provider {\n\tif withEpoch {\n\t\treturn mock.VulnerabilityProvider(vulnerabilitiesWithEpoch(packageName, indirectName)...)\n\t} else if withPackageQualifiers {\n\t\treturn mock.VulnerabilityProvider(vulnerabilitiesWithPackageQualifiers(packageName)...)\n\t}\n\treturn mock.VulnerabilityProvider(vulnerabilitiesDefaults(packageName, indirectName)...)\n}\n\nconst namespace = \"secdb:distro:centos:8\"\n\nfunc vulnerabilitiesDefaults(packageName, indirectName string) []vulnerability.Vulnerability {\n\treturn []vulnerability.Vulnerability{\n\t\t// direct...\n\t\t{\n\t\t\tPackageName: packageName,\n\t\t\tConstraint:  version.MustGetConstraint(\"<= 7.1.3-6\", version.RpmFormat),\n\t\t\tReference:   vulnerability.Reference{ID: \"CVE-2014-fake-1\", Namespace: namespace},\n\t\t},\n\t\t// indirect...\n\t\t// expected...\n\t\t{\n\t\t\tPackageName: indirectName,\n\t\t\tConstraint:  version.MustGetConstraint(\"< 7.1.4-5\", version.RpmFormat),\n\t\t\tReference:   vulnerability.Reference{ID: \"CVE-2014-fake-2\", Namespace: namespace},\n\t\t},\n\t\t{\n\t\t\tPackageName: indirectName,\n\t\t\tConstraint:  version.MustGetConstraint(\"< 8.0.2-0\", version.RpmFormat),\n\t\t\tReference:   vulnerability.Reference{ID: \"CVE-2013-fake-3\", Namespace: namespace},\n\t\t},\n\t\t// unexpected...\n\t\t{\n\t\t\tPackageName: indirectName,\n\t\t\tConstraint:  version.MustGetConstraint(\"< 7.0.4-1\", version.RpmFormat),\n\t\t\tReference:   vulnerability.Reference{ID: \"CVE-2013-fake-BAD\", Namespace: namespace},\n\t\t},\n\t}\n}\n\nfunc vulnerabilitiesWithEpoch(packageName, indirectName string) []vulnerability.Vulnerability {\n\treturn []vulnerability.Vulnerability{\n\t\t// direct...\n\t\t{\n\t\t\tPackageName: packageName,\n\t\t\tConstraint:  version.MustGetConstraint(\"<= 0:1.0-419.el8.\", version.RpmFormat),\n\t\t\tReference:   vulnerability.Reference{ID: \"CVE-2021-1\", Namespace: namespace},\n\t\t},\n\t\t{\n\t\t\tPackageName: packageName,\n\t\t\tConstraint:  version.MustGetConstraint(\"<= 0:2.28-419.el8.\", version.RpmFormat),\n\t\t\tReference:   vulnerability.Reference{ID: \"CVE-2021-2\", Namespace: namespace},\n\t\t},\n\t\t// indirect...\n\t\t{\n\t\t\tPackageName: indirectName,\n\t\t\tConstraint:  version.MustGetConstraint(\"< 5.28.3-420.el8\", version.RpmFormat),\n\t\t\tReference:   vulnerability.Reference{ID: \"CVE-2021-3\", Namespace: namespace},\n\t\t},\n\t\t// unexpected...\n\t\t{\n\t\t\tPackageName: indirectName,\n\t\t\tConstraint:  version.MustGetConstraint(\"< 4:5.26.3-419.el8\", version.RpmFormat),\n\t\t\tReference:   vulnerability.Reference{ID: \"CVE-2021-4\", Namespace: namespace},\n\t\t},\n\t}\n}\n\nfunc vulnerabilitiesWithPackageQualifiers(packageName string) []vulnerability.Vulnerability {\n\treturn []vulnerability.Vulnerability{\n\t\t// direct...\n\t\t{\n\t\t\tPackageName: packageName,\n\t\t\tConstraint:  version.MustGetConstraint(\"<= 0:1.0-419.el8.\", version.RpmFormat),\n\t\t\tReference:   vulnerability.Reference{ID: \"CVE-2021-1\", Namespace: namespace},\n\t\t\tPackageQualifiers: []qualifier.Qualifier{\n\t\t\t\trpmmodularity.New(\"containertools:3\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tPackageName: packageName,\n\t\t\tConstraint:  version.MustGetConstraint(\"<= 0:1.0-419.el8.\", version.RpmFormat),\n\t\t\tReference:   vulnerability.Reference{ID: \"CVE-2021-2\", Namespace: namespace},\n\t\t\tPackageQualifiers: []qualifier.Qualifier{\n\t\t\t\trpmmodularity.New(\"\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tPackageName: packageName,\n\t\t\tConstraint:  version.MustGetConstraint(\"<= 0:1.0-419.el8.\", version.RpmFormat),\n\t\t\tReference:   vulnerability.Reference{ID: \"CVE-2021-3\", Namespace: namespace},\n\t\t},\n\t\t{\n\t\t\tPackageName: packageName,\n\t\t\tConstraint:  version.MustGetConstraint(\"<= 0:1.0-419.el8.\", version.RpmFormat),\n\t\t\tReference:   vulnerability.Reference{ID: \"CVE-2021-4\", Namespace: namespace},\n\t\t\tPackageQualifiers: []qualifier.Qualifier{\n\t\t\t\trpmmodularity.New(\"containertools:4\"),\n\t\t\t},\n\t\t},\n\t}\n}\n\n// mockEOLProvider wraps mock.VulnerabilityProvider and adds EOLChecker support for testing\ntype mockEOLProvider struct {\n\tvulnerability.Provider\n\teolDate *time.Time\n}\n\nfunc (m *mockEOLProvider) GetOperatingSystemEOL(d *distro.Distro) (eolDate, eoasDate *time.Time, err error) {\n\treturn m.eolDate, nil, nil\n}\n\nfunc newMockEOLProvider(eolDate *time.Time) *mockEOLProvider {\n\t// include CPE vulnerability for testing CPE fallback\n\treturn &mockEOLProvider{\n\t\tProvider: mock.VulnerabilityProvider([]vulnerability.Vulnerability{\n\t\t\t// distro-based vulnerability\n\t\t\t{\n\t\t\t\tPackageName: \"openssl\",\n\t\t\t\tReference:   vulnerability.Reference{ID: \"CVE-2014-distro-1\", Namespace: namespace},\n\t\t\t\tConstraint:  version.MustGetConstraint(\"< 1.0.2\", version.RpmFormat),\n\t\t\t},\n\t\t\t// CPE-based vulnerability\n\t\t\t{\n\t\t\t\tPackageName: \"openssl\",\n\t\t\t\tReference:   vulnerability.Reference{ID: \"CVE-2014-cpe-1\", Namespace: \"nvd:cpe\"},\n\t\t\t\tConstraint:  version.MustGetConstraint(\"< 1.0.2\", version.UnknownFormat),\n\t\t\t\tCPEs: []syftCpe.CPE{\n\t\t\t\t\tsyftCpe.Must(\"cpe:2.3:a:openssl:openssl:*:*:*:*:*:*:*:*\", \"\"),\n\t\t\t\t},\n\t\t\t},\n\t\t}...),\n\t\teolDate: eolDate,\n\t}\n}\n"
  },
  {
    "path": "grype/matcher/rpm/matcher_test.go",
    "content": "package rpm\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/google/uuid\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/grype/grype/distro\"\n\t\"github.com/anchore/grype/grype/match\"\n\t\"github.com/anchore/grype/grype/pkg\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n\tsyftCpe \"github.com/anchore/syft/syft/cpe\"\n\tsyftPkg \"github.com/anchore/syft/syft/pkg\"\n)\n\nfunc TestMatcherRpm(t *testing.T) {\n\ttests := []struct {\n\t\tname            string\n\t\tp               pkg.Package\n\t\tsetup           func() (vulnerability.Provider, *distro.Distro, Matcher)\n\t\texpectedMatches map[string]match.Type\n\t\twantErr         bool\n\t}{\n\t\t{\n\t\t\tname: \"Rpm Match matches by direct and by source indirection\",\n\t\t\tp: pkg.Package{\n\t\t\t\tID:      pkg.ID(uuid.NewString()),\n\t\t\t\tName:    \"neutron-libs\",\n\t\t\t\tVersion: \"7.1.3-6\",\n\t\t\t\tType:    syftPkg.RpmPkg,\n\t\t\t\tUpstreams: []pkg.UpstreamPackage{\n\t\t\t\t\t{\n\t\t\t\t\t\tName:    \"neutron\",\n\t\t\t\t\t\tVersion: \"7.1.3-6.el8\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tsetup: func() (vulnerability.Provider, *distro.Distro, Matcher) {\n\t\t\t\tmatcher := Matcher{}\n\t\t\t\td := distro.New(distro.CentOS, \"8\", \"\")\n\t\t\t\tstore := newMockProvider(\"neutron-libs\", \"neutron\", false, false)\n\n\t\t\t\treturn store, d, matcher\n\t\t\t},\n\t\t\texpectedMatches: map[string]match.Type{\n\t\t\t\t\"CVE-2014-fake-1\": match.ExactDirectMatch,\n\t\t\t\t\"CVE-2014-fake-2\": match.ExactIndirectMatch,\n\t\t\t\t\"CVE-2013-fake-3\": match.ExactIndirectMatch,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Rpm Match matches by direct and ignores the source rpm when the package names are the same\",\n\t\t\tp: pkg.Package{\n\t\t\t\tID:      pkg.ID(uuid.NewString()),\n\t\t\t\tName:    \"neutron\",\n\t\t\t\tVersion: \"7.1.3-6\",\n\t\t\t\tType:    syftPkg.RpmPkg,\n\t\t\t\tUpstreams: []pkg.UpstreamPackage{\n\t\t\t\t\t{\n\t\t\t\t\t\tName:    \"neutron\",\n\t\t\t\t\t\tVersion: \"7.1.3-6.el8\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tsetup: func() (vulnerability.Provider, *distro.Distro, Matcher) {\n\t\t\t\tmatcher := Matcher{}\n\t\t\t\td := distro.New(distro.CentOS, \"8\", \"\")\n\n\t\t\t\tstore := newMockProvider(\"neutron\", \"neutron-devel\", false, false)\n\n\t\t\t\treturn store, d, matcher\n\t\t\t},\n\t\t\texpectedMatches: map[string]match.Type{\n\t\t\t\t\"CVE-2014-fake-1\": match.ExactDirectMatch,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t// Regression against https://github.com/anchore/grype/issues/376\n\t\t\tname: \"Rpm Match matches by direct and by source indirection when the SourceRpm version is desynced from package version\",\n\t\t\tp: pkg.Package{\n\t\t\t\tID:      pkg.ID(uuid.NewString()),\n\t\t\t\tName:    \"neutron-libs\",\n\t\t\t\tVersion: \"7.1.3-6\",\n\t\t\t\tType:    syftPkg.RpmPkg,\n\t\t\t\tUpstreams: []pkg.UpstreamPackage{\n\t\t\t\t\t{\n\t\t\t\t\t\tName:    \"neutron\",\n\t\t\t\t\t\tVersion: \"17.16.3-229.el8\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tsetup: func() (vulnerability.Provider, *distro.Distro, Matcher) {\n\t\t\t\tmatcher := Matcher{}\n\t\t\t\td := distro.New(distro.CentOS, \"8\", \"\")\n\n\t\t\t\tstore := newMockProvider(\"neutron-libs\", \"neutron\", false, false)\n\n\t\t\t\treturn store, d, matcher\n\t\t\t},\n\t\t\texpectedMatches: map[string]match.Type{\n\t\t\t\t\"CVE-2014-fake-1\": match.ExactDirectMatch,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t// Epoch in pkg but not in src package version, epoch found in the vuln record\n\t\t\t// Regression: https://github.com/anchore/grype/issues/437\n\t\t\tname: \"Rpm Match should not occur due to source match even though source has no epoch\",\n\t\t\tp: pkg.Package{\n\t\t\t\tID:      pkg.ID(uuid.NewString()),\n\t\t\t\tName:    \"perl-Errno\",\n\t\t\t\tVersion: \"0:1.28-419.el8_4.1\",\n\t\t\t\tType:    syftPkg.RpmPkg,\n\t\t\t\tMetadata: pkg.RpmMetadata{\n\t\t\t\t\tEpoch: intRef(0),\n\t\t\t\t},\n\t\t\t\tUpstreams: []pkg.UpstreamPackage{\n\t\t\t\t\t{\n\t\t\t\t\t\tName:    \"perl\",\n\t\t\t\t\t\tVersion: \"5.26.3-419.el8_4.1\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tsetup: func() (vulnerability.Provider, *distro.Distro, Matcher) {\n\t\t\t\tmatcher := Matcher{}\n\t\t\t\td := distro.New(distro.CentOS, \"8\", \"\")\n\n\t\t\t\tstore := newMockProvider(\"perl-Errno\", \"perl\", true, false)\n\n\t\t\t\treturn store, d, matcher\n\t\t\t},\n\t\t\texpectedMatches: map[string]match.Type{\n\t\t\t\t\"CVE-2021-2\": match.ExactDirectMatch,\n\t\t\t\t\"CVE-2021-3\": match.ExactIndirectMatch,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"package without epoch is assumed to be 0 - compared against vuln with NO epoch (direct match only)\",\n\t\t\tp: pkg.Package{\n\t\t\t\tID:       pkg.ID(uuid.NewString()),\n\t\t\t\tName:     \"perl-Errno\",\n\t\t\t\tVersion:  \"1.28-419.el8_4.1\",\n\t\t\t\tType:     syftPkg.RpmPkg,\n\t\t\t\tMetadata: pkg.RpmMetadata{},\n\t\t\t},\n\t\t\tsetup: func() (vulnerability.Provider, *distro.Distro, Matcher) {\n\t\t\t\tmatcher := Matcher{}\n\t\t\t\td := distro.New(distro.CentOS, \"8\", \"\")\n\n\t\t\t\tstore := newMockProvider(\"perl-Errno\", \"doesn't-matter\", false, false)\n\n\t\t\t\treturn store, d, matcher\n\t\t\t},\n\t\t\texpectedMatches: map[string]match.Type{\n\t\t\t\t\"CVE-2014-fake-1\": match.ExactDirectMatch,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"package without epoch is assumed to be 0 - compared against vuln WITH epoch (direct match only)\",\n\t\t\tp: pkg.Package{\n\t\t\t\tID:       pkg.ID(uuid.NewString()),\n\t\t\t\tName:     \"perl-Errno\",\n\t\t\t\tVersion:  \"1.28-419.el8_4.1\",\n\t\t\t\tType:     syftPkg.RpmPkg,\n\t\t\t\tMetadata: pkg.RpmMetadata{},\n\t\t\t},\n\t\t\tsetup: func() (vulnerability.Provider, *distro.Distro, Matcher) {\n\t\t\t\tmatcher := Matcher{}\n\t\t\t\td := distro.New(distro.CentOS, \"8\", \"\")\n\n\t\t\t\tstore := newMockProvider(\"perl-Errno\", \"doesn't-matter\", true, false)\n\n\t\t\t\treturn store, d, matcher\n\t\t\t},\n\t\t\texpectedMatches: map[string]match.Type{\n\t\t\t\t\"CVE-2021-2\": match.ExactDirectMatch,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"package WITH epoch - compared against vuln with NO epoch (direct match only)\",\n\t\t\tp: pkg.Package{\n\t\t\t\tID:       pkg.ID(uuid.NewString()),\n\t\t\t\tName:     \"perl-Errno\",\n\t\t\t\tVersion:  \"2:1.28-419.el8_4.1\",\n\t\t\t\tType:     syftPkg.RpmPkg,\n\t\t\t\tMetadata: pkg.RpmMetadata{},\n\t\t\t},\n\t\t\tsetup: func() (vulnerability.Provider, *distro.Distro, Matcher) {\n\t\t\t\tmatcher := Matcher{}\n\t\t\t\td := distro.New(distro.CentOS, \"8\", \"\")\n\n\t\t\t\tstore := newMockProvider(\"perl-Errno\", \"doesn't-matter\", false, false)\n\n\t\t\t\treturn store, d, matcher\n\t\t\t},\n\t\t\texpectedMatches: map[string]match.Type{\n\t\t\t\t\"CVE-2014-fake-1\": match.ExactDirectMatch,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"package WITH epoch - compared against vuln WITH epoch (direct match only)\",\n\t\t\tp: pkg.Package{\n\t\t\t\tID:       pkg.ID(uuid.NewString()),\n\t\t\t\tName:     \"perl-Errno\",\n\t\t\t\tVersion:  \"2:1.28-419.el8_4.1\",\n\t\t\t\tType:     syftPkg.RpmPkg,\n\t\t\t\tMetadata: pkg.RpmMetadata{},\n\t\t\t},\n\t\t\tsetup: func() (vulnerability.Provider, *distro.Distro, Matcher) {\n\t\t\t\tmatcher := Matcher{}\n\t\t\t\td := distro.New(distro.CentOS, \"8\", \"\")\n\n\t\t\t\tstore := newMockProvider(\"perl-Errno\", \"doesn't-matter\", true, false)\n\n\t\t\t\treturn store, d, matcher\n\t\t\t},\n\t\t\texpectedMatches: map[string]match.Type{},\n\t\t},\n\t\t{\n\t\t\tname: \"package with modularity label 1\",\n\t\t\tp: pkg.Package{\n\t\t\t\tID:      pkg.ID(uuid.NewString()),\n\t\t\t\tName:    \"maniac\",\n\t\t\t\tVersion: \"0.1\",\n\t\t\t\tType:    syftPkg.RpmPkg,\n\t\t\t\tMetadata: pkg.RpmMetadata{\n\t\t\t\t\tModularityLabel: strRef(\"containertools:3:1234:5678\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\tsetup: func() (vulnerability.Provider, *distro.Distro, Matcher) {\n\t\t\t\tmatcher := Matcher{}\n\t\t\t\td := distro.New(distro.CentOS, \"8\", \"\")\n\n\t\t\t\tstore := newMockProvider(\"maniac\", \"doesn't-matter\", false, true)\n\n\t\t\t\treturn store, d, matcher\n\t\t\t},\n\t\t\texpectedMatches: map[string]match.Type{\n\t\t\t\t\"CVE-2021-1\": match.ExactDirectMatch,\n\t\t\t\t\"CVE-2021-3\": match.ExactDirectMatch,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"package with modularity label 2\",\n\t\t\tp: pkg.Package{\n\t\t\t\tID:      pkg.ID(uuid.NewString()),\n\t\t\t\tName:    \"maniac\",\n\t\t\t\tVersion: \"0.1\",\n\t\t\t\tType:    syftPkg.RpmPkg,\n\t\t\t\tMetadata: pkg.RpmMetadata{\n\t\t\t\t\tModularityLabel: strRef(\"containertools:1:abc:123\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\tsetup: func() (vulnerability.Provider, *distro.Distro, Matcher) {\n\t\t\t\tmatcher := Matcher{}\n\t\t\t\td := distro.New(distro.CentOS, \"8\", \"\")\n\n\t\t\t\tstore := newMockProvider(\"maniac\", \"doesn't-matter\", false, true)\n\n\t\t\t\treturn store, d, matcher\n\t\t\t},\n\t\t\texpectedMatches: map[string]match.Type{\n\t\t\t\t\"CVE-2021-3\": match.ExactDirectMatch,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"package without modularity label\",\n\t\t\tp: pkg.Package{\n\t\t\t\tID:      pkg.ID(uuid.NewString()),\n\t\t\t\tName:    \"maniac\",\n\t\t\t\tVersion: \"0.1\",\n\t\t\t\tType:    syftPkg.RpmPkg,\n\t\t\t},\n\t\t\tsetup: func() (vulnerability.Provider, *distro.Distro, Matcher) {\n\t\t\t\tmatcher := Matcher{}\n\t\t\t\td := distro.New(distro.CentOS, \"8\", \"\")\n\n\t\t\t\tstore := newMockProvider(\"maniac\", \"doesn't-matter\", false, true)\n\n\t\t\t\treturn store, d, matcher\n\t\t\t},\n\t\t\texpectedMatches: map[string]match.Type{\n\t\t\t\t\"CVE-2021-1\": match.ExactDirectMatch,\n\t\t\t\t\"CVE-2021-2\": match.ExactDirectMatch,\n\t\t\t\t\"CVE-2021-3\": match.ExactDirectMatch,\n\t\t\t\t\"CVE-2021-4\": match.ExactDirectMatch,\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tstore, d, matcher := test.setup()\n\t\t\tif test.p.Distro == nil {\n\t\t\t\ttest.p.Distro = d\n\t\t\t}\n\t\t\tactual, _, err := matcher.Match(store, test.p)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(\"could not find match: \", err)\n\t\t\t}\n\n\t\t\tassert.Len(t, actual, len(test.expectedMatches), \"unexpected matches count\")\n\n\t\t\tfor _, a := range actual {\n\t\t\t\tif val, ok := test.expectedMatches[a.Vulnerability.ID]; !ok {\n\t\t\t\t\tt.Errorf(\"return unknown match CVE: %s\", a.Vulnerability.ID)\n\t\t\t\t\tcontinue\n\t\t\t\t} else {\n\t\t\t\t\trequire.NotEmpty(t, a.Details)\n\t\t\t\t\tfor _, de := range a.Details {\n\t\t\t\t\t\tassert.Equal(t, val, de.Type)\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tassert.Equal(t, test.p.Name, a.Package.Name, \"failed to capture original package name\")\n\t\t\t\tfor _, detail := range a.Details {\n\t\t\t\t\tassert.Equal(t, matcher.Type(), detail.Matcher, \"failed to capture matcher type\")\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif t.Failed() {\n\t\t\t\tt.Logf(\"discovered CVES: %+v\", actual)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_addEpochIfApplicable(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tpkg      pkg.Package\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname: \"assume 0 epoch\",\n\t\t\tpkg: pkg.Package{\n\t\t\t\tVersion: \"3.26.0-6.el8\",\n\t\t\t},\n\t\t\texpected: \"0:3.26.0-6.el8\",\n\t\t},\n\t\t{\n\t\t\tname: \"epoch already exists in version string\",\n\t\t\tpkg: pkg.Package{\n\t\t\t\tVersion: \"7:3.26.0-6.el8\",\n\t\t\t},\n\t\t\texpected: \"7:3.26.0-6.el8\",\n\t\t},\n\t\t{\n\t\t\tname: \"epoch only exists in metadata\",\n\t\t\tpkg: pkg.Package{\n\t\t\t\tVersion: \"3.26.0-6.el8\",\n\t\t\t\tMetadata: pkg.RpmMetadata{\n\t\t\t\t\tEpoch: intRef(7),\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: \"7:3.26.0-6.el8\",\n\t\t},\n\t\t{\n\t\t\tname: \"epoch does not exist in metadata\",\n\t\t\tpkg: pkg.Package{\n\t\t\t\tVersion: \"3.26.0-6.el8\",\n\t\t\t\tMetadata: pkg.RpmMetadata{\n\t\t\t\t\tEpoch: nil, // assume 0 epoch\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: \"0:3.26.0-6.el8\",\n\t\t},\n\t\t{\n\t\t\tname: \"version is empty\",\n\t\t\tpkg: pkg.Package{\n\t\t\t\tVersion: \"\",\n\t\t\t\tMetadata: pkg.RpmMetadata{\n\t\t\t\t\tEpoch: nil, // assume 0 epoch\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: \"\",\n\t\t},\n\t}\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tp := test.pkg\n\t\t\taddEpochIfApplicable(&p)\n\t\t\tassert.Equal(t, test.expected, p.Version)\n\t\t})\n\t}\n}\n\nfunc TestMatcherRpm_CPEFallbackWhenEOL(t *testing.T) {\n\tpastEOL := time.Now().AddDate(-1, 0, 0)  // 1 year ago\n\tfutureEOL := time.Now().AddDate(1, 0, 0) // 1 year from now\n\n\td := distro.New(distro.CentOS, \"8\", \"\")\n\n\t// package with CPEs for CPE-based matching\n\tp := pkg.Package{\n\t\tID:      pkg.ID(uuid.NewString()),\n\t\tName:    \"openssl\",\n\t\tVersion: \"1.0.1\",\n\t\tType:    syftPkg.RpmPkg,\n\t\tDistro:  d,\n\t\tCPEs: []syftCpe.CPE{\n\t\t\tsyftCpe.Must(\"cpe:2.3:a:openssl:openssl:1.0.1:*:*:*:*:*:*:*\", \"\"),\n\t\t},\n\t}\n\n\ttests := []struct {\n\t\tname             string\n\t\tuseCPEsForEOL    bool\n\t\teolDate          *time.Time\n\t\texpectCPEMatches bool\n\t}{\n\t\t{\n\t\t\tname:             \"CPE fallback enabled and distro is EOL - should include CPE matches\",\n\t\t\tuseCPEsForEOL:    true,\n\t\t\teolDate:          &pastEOL,\n\t\t\texpectCPEMatches: true,\n\t\t},\n\t\t{\n\t\t\tname:             \"CPE fallback enabled but distro not EOL - should not include CPE matches\",\n\t\t\tuseCPEsForEOL:    true,\n\t\t\teolDate:          &futureEOL,\n\t\t\texpectCPEMatches: false,\n\t\t},\n\t\t{\n\t\t\tname:             \"CPE fallback disabled and distro is EOL - should not include CPE matches\",\n\t\t\tuseCPEsForEOL:    false,\n\t\t\teolDate:          &pastEOL,\n\t\t\texpectCPEMatches: false,\n\t\t},\n\t\t{\n\t\t\tname:             \"CPE fallback disabled and distro not EOL - should not include CPE matches\",\n\t\t\tuseCPEsForEOL:    false,\n\t\t\teolDate:          &futureEOL,\n\t\t\texpectCPEMatches: false,\n\t\t},\n\t\t{\n\t\t\tname:             \"CPE fallback enabled but no EOL data - should not include CPE matches\",\n\t\t\tuseCPEsForEOL:    true,\n\t\t\teolDate:          nil,\n\t\t\texpectCPEMatches: false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tmatcher := NewRpmMatcher(MatcherConfig{\n\t\t\t\tUseCPEsForEOL: tt.useCPEsForEOL,\n\t\t\t})\n\n\t\t\tvp := newMockEOLProvider(tt.eolDate)\n\t\t\tmatches, _, err := matcher.Match(vp, p)\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// check if any CPE matches were found\n\t\t\thasCPEMatch := false\n\t\t\tfor _, m := range matches {\n\t\t\t\tfor _, detail := range m.Details {\n\t\t\t\t\tif detail.Type == match.CPEMatch {\n\t\t\t\t\t\thasCPEMatch = true\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif tt.expectCPEMatches {\n\t\t\t\tassert.True(t, hasCPEMatch, \"expected CPE matches for EOL distro\")\n\t\t\t} else {\n\t\t\t\tassert.False(t, hasCPEMatch, \"did not expect CPE matches\")\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/matcher/rpm/rhel_eus.go",
    "content": "package rpm\n\nimport (\n\t\"fmt\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/anchore/grype/grype/distro\"\n\t\"github.com/anchore/grype/grype/match\"\n\t\"github.com/anchore/grype/grype/matcher/internal\"\n\t\"github.com/anchore/grype/grype/matcher/internal/result\"\n\t\"github.com/anchore/grype/grype/pkg\"\n\t\"github.com/anchore/grype/grype/search\"\n\t\"github.com/anchore/grype/grype/version\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n\t\"github.com/anchore/grype/internal/log\"\n)\n\n// elVersionPattern matches patterns like \"el9_5\", \"el8_10\", \"el7\" in RPM release strings\nvar elVersionPattern = regexp.MustCompile(`\\.el(\\d+)(?:_(\\d+))?`)\n\n// extractRHELVersionFromRelease parses RHEL major/minor from release string.\n// Examples:\n//\n//\t\"503.11.1.el9_5\" -> (9, 5, true)\n//\t\"82.el8_10.2\" -> (8, 10, true)\n//\t\"1.el7\" -> (7, 0, true)  // missing minor treated as 0\n//\t\"1.0.0\" -> (0, 0, false)\nfunc extractRHELVersionFromRelease(release string) (major, minor int, found bool) {\n\tmatches := elVersionPattern.FindStringSubmatch(release)\n\tif matches == nil {\n\t\treturn 0, 0, false\n\t}\n\n\tmajor, err := strconv.Atoi(matches[1])\n\tif err != nil {\n\t\treturn 0, 0, false\n\t}\n\n\t// If no minor version captured, treat as 0 (base version)\n\tif matches[2] == \"\" {\n\t\treturn major, 0, true\n\t}\n\n\tminor, err = strconv.Atoi(matches[2])\n\tif err != nil {\n\t\treturn major, 0, true\n\t}\n\n\treturn major, minor, true\n}\n\n// extractReleaseFromRPMVersion extracts release portion from full RPM version.\n// Examples:\n//\n//\t\"5.14.0-503.11.1.el9_5\" -> \"503.11.1.el9_5\"\n//\t\"0:5.14.0-503.11.1.el9_5\" -> \"503.11.1.el9_5\" (handles epoch)\nfunc extractReleaseFromRPMVersion(rpmVersion string) string {\n\t// Strip epoch if present (e.g., \"0:5.14.0-...\" -> \"5.14.0-...\")\n\tif idx := strings.Index(rpmVersion, \":\"); idx != -1 {\n\t\trpmVersion = rpmVersion[idx+1:]\n\t}\n\n\t// Extract release portion after the hyphen\n\tif idx := strings.LastIndex(rpmVersion, \"-\"); idx != -1 {\n\t\treturn rpmVersion[idx+1:]\n\t}\n\n\treturn rpmVersion\n}\n\n// isFixReachableForEUS returns true if fix version is reachable for EUS distro.\n// A fix is reachable if:\n//  1. The fix's RHEL version cannot be determined (fail-open)\n//  2. The fix's major version matches AND minor version <= EUS minor version\nfunc isFixReachableForEUS(fixVersion string, eusDistro *distro.Distro) bool {\n\tif eusDistro == nil {\n\t\treturn true\n\t}\n\n\t// Parse EUS distro version (e.g., \"9.4\" -> major=9, minor=4)\n\teusMajor, eusMinor := eusDistro.MajorVersion(), eusDistro.MinorVersion()\n\n\tif eusMajor == \"\" {\n\t\t// Cannot determine EUS version, fail-open\n\t\treturn true\n\t}\n\n\teusMajorInt, err := strconv.Atoi(eusMajor)\n\tif err != nil {\n\t\treturn true\n\t}\n\n\t// Treat missing minor version as 0 (e.g., \"9+eus\" means \"9.0+eus\")\n\teusMinorInt := 0\n\tif eusMinor != \"\" {\n\t\teusMinorInt, _ = strconv.Atoi(eusMinor)\n\t}\n\n\t// Extract release from fix version and parse RHEL version from it\n\trelease := extractReleaseFromRPMVersion(fixVersion)\n\tfixMajor, fixMinor, found := extractRHELVersionFromRelease(release)\n\tif !found {\n\t\t// Cannot determine fix's RHEL version, fail-open (assume reachable)\n\t\treturn true\n\t}\n\n\t// Different major version is not reachable\n\tif fixMajor != eusMajorInt {\n\t\treturn false\n\t}\n\n\t// Fix is reachable only if fix's minor version <= EUS minor version\n\t// (missing minor is treated as 0, so \".el9\" is reachable by any EUS version)\n\treturn fixMinor <= eusMinorInt\n}\n\nfunc shouldUseRedhatEUSMatching(d *distro.Distro) bool {\n\tif d == nil {\n\t\treturn false\n\t}\n\n\tif d.Type != distro.RedHat {\n\t\t// considering EUS fixes on a non-RedHat distro is not valid\n\t\treturn false\n\t}\n\n\tfor _, channel := range d.Channels {\n\t\tif strings.ToLower(channel) == \"eus\" {\n\t\t\t// if the distro has an EUS channel, we should consider EUS fixes\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// redhatEUSMatches returns matches for the given package with Extended Update Support (EUS) fixes considered.\n//\n// RedHat follows the below workflow when incorporating patches:\n//\n//\tRHEL 9 ───▶ 9.1 ───▶ 9.2 ───▶ 9.2-EUS (mainline + 9.2 EUS fixes)\n//\t                     │\n//\t                     ▼\n//\t                     9.3 ───▶ 9.4 ───▶ 9.4-EUS (mainline + 9.4 EUS fixes)\n//\t                              │\n//\t                              ▼\n//\t                              9.5 ───▶ 9.6 ───▶ 9.6-EUS (mainline + 9.6 EUS fixes)\n//\t                                       │\n//\t                                       ▼ ...\n//\n// So...\n// - EUS branches are independent (no cross-EUS fixes)\n// - each EUS branch = mainline fixes up to branch point + its own EUS fixes\n//\n// In grype that means that searching for vulnerabilities should be done in two steps:\n// 1. find disclosures that match the base distro (e.g., '>= 9.0 && < 10').\n// 2. find fixes from the base distro (e.g., '>= 9.0 && < 10') as well as EUS fixes for the specific minor version of the distro (e.g. '9.4+eus').\n//\n// Once searching is complete, we have two collections (matching for each search step above).\n// We then merge these two (disclosure and resolution) collections together, the final result is a collection of\n// prototype matches that the package is vulnerable to that include both the base distro disclosures and the EUS fixes.\n// Any disclosure that does not apply to the original package version (e.g. a fix was found) at this point has been removed.\n//\n// The final step is to render the final matches from the merged collection.\nfunc redhatEUSMatches(provider result.Provider, searchPkg pkg.Package, missingEpochStrategy version.MissingEpochStrategy) ([]match.Match, error) {\n\tdistroWithoutEUS := *searchPkg.Distro\n\tdistroWithoutEUS.Channels = nil // clear the EUS channel so that we can search for the base distro\n\n\t// Create version with config embedded\n\tpkgVersion := version.NewWithConfig(\n\t\tsearchPkg.Version,\n\t\tpkg.VersionFormat(searchPkg),\n\t\tversion.ComparisonConfig{\n\t\t\tMissingEpochStrategy: missingEpochStrategy,\n\t\t},\n\t)\n\n\t// find all disclosures for the package in the base distro (e.g. '>= 9.0 && < 10')\n\tdisclosures, err := provider.FindResults(\n\t\tsearch.ByPackageName(searchPkg.Name),\n\t\tsearch.ByDistro(distroWithoutEUS), // e.g.  >= 9.0 && < 10 (no EUS channel)\n\t\tinternal.OnlyQualifiedPackages(searchPkg),\n\t\tinternal.OnlyVulnerableVersions(pkgVersion), // if these records indicate the version of the package is not vulnerable, do not include them\n\t)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"matcher failed to fetch disclosures for distro=%q pkg=%q: %w\", searchPkg.Distro, searchPkg.Name, err)\n\t}\n\n\tif len(disclosures) == 0 {\n\t\treturn nil, nil\n\t}\n\n\t// find all base distro fixes (e.g. '>= 9.0 && < 10') and EUS fixes for the package in the specific minor version of the distro (e.g. '9.4+eus')\n\tresolutions, err := provider.FindResults(\n\t\tsearch.ByPackageName(searchPkg.Name),\n\t\tsearch.ByDistro(distroWithoutEUS, *searchPkg.Distro), // e.g.  (>= 9.0 && < 10) || 9.4+eus\n\t\tinternal.OnlyQualifiedPackages(searchPkg),\n\t\t// note: we do **not** apply any version criteria to the search as to raise up all possible fixes\n\t\t// and combine within the collection. If we do filter on a fix version, it could result in\n\t\t// false positives (missing EUS fixes that resolve a disclosure).\n\t)\n\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"matcher failed to fetch resolutions for distro=%q pkg=%q: %w\", searchPkg.Distro, searchPkg.Name, err)\n\t}\n\n\teusFixes := resolutions.Filter(search.ByFixedVersion(*pkgVersion))\n\n\t// remove EUS fixed vulns for this version\n\tremaining := disclosures.Remove(eusFixes)\n\n\t// combine disclosures and fixes so that:\n\t// a. disclosures that have EUS fixes that resolve the disclosure for an earlier version of the package (thus we're not vulnerable) are removed.\n\t// b. disclosures that have EUS fixes that resolve the disclosure for future versions of the package (thus we're vulnerable) are kept.\n\t// c. all fixes from the incoming resolutions are patched onto the disclosures in the returned collection, so the\n\t//    final set of vulnerabilities is a fused set of disclosures and fixes together.\n\t// Note: we pass searchPkg.Distro (the EUS distro) to filter out fixes not reachable for this EUS version\n\tremaining = remaining.Merge(resolutions, mergeEUSAdvisoriesIntoMainDisclosures(pkgVersion, searchPkg.Distro))\n\n\treturn remaining.ToMatches(), err\n}\n\n// mergeEUSAdvisoriesIntoMainDisclosures returns a function that will filter disclosures based on the provided advisory information (by fix version only).\n// Additionally, this will merge applicable fixes into one vulnerability record, so that the final result contains only one vulnerability record per disclosure.\nfunc mergeEUSAdvisoriesIntoMainDisclosures(v *version.Version, eusDistro *distro.Distro) func(disclosures, advisoryOverlays []result.Result) []result.Result {\n\treturn func(disclosures, advisoryOverlays []result.Result) []result.Result {\n\t\tvar out []result.Result\n\n\t\tfor _, ds := range disclosures {\n\t\t\tprocessedResult := mergeEUSAdvisoryIntoMainDisclosure(v, ds, advisoryOverlays, eusDistro)\n\t\t\tif len(processedResult.Vulnerabilities) > 0 {\n\t\t\t\tout = append(out, processedResult)\n\t\t\t}\n\t\t}\n\n\t\treturn out\n\t}\n}\n\n// mergeEUSAdvisoryIntoMainDisclosure processes a single disclosure Result against its corresponding advisory overlay Results\nfunc mergeEUSAdvisoryIntoMainDisclosure(v *version.Version, disclosures result.Result, advisoryOverlays []result.Result, eusDistro *distro.Distro) result.Result {\n\tprocessedResult := result.Result{\n\t\tID:      disclosures.ID,\n\t\tPackage: disclosures.Package,\n\t}\n\n\t// process each disclosure vulnerability against advisory overlays\n\tfor _, disclosure := range disclosures.Vulnerabilities {\n\t\tprocessedVuln, advisoryDetails := mergeEUSAdvisoryIntoSingleDisclosure(v, disclosure, advisoryOverlays, eusDistro)\n\t\tif processedVuln != nil {\n\t\t\tprocessedResult.Vulnerabilities = append(processedResult.Vulnerabilities, *processedVuln)\n\t\t\tprocessedResult.Details = append(processedResult.Details, advisoryDetails...)\n\t\t}\n\t}\n\n\tfinalizeMatchDetails(&processedResult, disclosures.Details, v)\n\treturn processedResult\n}\n\n// mergeEUSAdvisoryIntoSingleDisclosure processes a single vulnerability against advisory overlays\nfunc mergeEUSAdvisoryIntoSingleDisclosure(v *version.Version, disclosure vulnerability.Vulnerability, advisoryOverlays []result.Result, eusDistro *distro.Distro) (*vulnerability.Vulnerability, match.Details) {\n\tfixVersions := version.NewSet(true)\n\tvar constraints []version.Constraint\n\tvar state vulnerability.FixState\n\tvar allAdvisoryDetails match.Details\n\n\t// check if we're vulnerable to the original disclosure\n\tif isVulnerableVersion(v, disclosure.Constraint, disclosure.ID) {\n\t\tconstraints = append(constraints, disclosure.Constraint)\n\t}\n\n\t// process advisory overlays, incorporating new fix versions and updating the version constraints\n\tfor _, advisoryOverlay := range advisoryOverlays {\n\t\tcollectMatchingConstraintsDetailsAndFixState(v, advisoryOverlay, fixVersions, &constraints, &state, &allAdvisoryDetails, eusDistro)\n\t}\n\n\tif len(constraints) == 0 {\n\t\t// all of the advisories showed we're not vulnerable, so we can skip this disclosure\n\t\treturn nil, nil\n\t}\n\n\tpatchedRecord := buildPatchedVulnerabilityRecord(v, disclosure, fixVersions, constraints, state)\n\treturn &patchedRecord, allAdvisoryDetails\n}\n\n// collectMatchingConstraintsDetailsAndFixState processes vulnerabilities from advisory overlays, applying any new fix versions and updating the given fix state / constraints.\nfunc collectMatchingConstraintsDetailsAndFixState(v *version.Version, advisoryResult result.Result, fixVersions *version.Set, constraints *[]version.Constraint, state *vulnerability.FixState, allAdvisoryDetails *match.Details, eusDistro *distro.Distro) {\n\tadvisories := advisoryResult.Vulnerabilities\n\tvar keepDetails bool\n\tfor _, advisory := range advisories {\n\t\tif advisory.Fix.State == vulnerability.FixStateWontFix && *state != vulnerability.FixStateFixed {\n\t\t\t*state = advisory.Fix.State\n\t\t}\n\n\t\t// Get all fixes greater than current version (parses versions once)\n\t\tallFixes := neededFixes(v, advisory.Fix.Versions, advisory.Constraint.Format(), advisory.ID)\n\n\t\t// Filter to only those reachable for EUS (quick string-based check)\n\t\tapplicableFixes := filterFixesForEUS(allFixes, eusDistro, advisory.ID)\n\n\t\tif len(applicableFixes) == 0 {\n\t\t\t// If there were fixes but none are reachable for EUS, mark as NotFixed\n\t\t\tif len(allFixes) > 0 && eusDistro != nil && *state != vulnerability.FixStateFixed {\n\t\t\t\t*state = vulnerability.FixStateNotFixed\n\t\t\t\t// Still add the constraint since the user is vulnerable\n\t\t\t\t*constraints = append(*constraints, advisory.Constraint)\n\t\t\t\tkeepDetails = true\n\t\t\t}\n\t\t\t// none of the fixes on this advisory are greater than the current version (or reachable for EUS), so we can skip adding fixes\n\t\t\tcontinue\n\t\t}\n\n\t\t// we're vulnerable! keep any fix versions that could have been applied\n\t\t*constraints = append(*constraints, advisory.Constraint)\n\t\tfixVersions.Add(applicableFixes...)\n\t\tif *state != vulnerability.FixStateFixed {\n\t\t\t*state = advisory.Fix.State\n\t\t}\n\t\tkeepDetails = true\n\t}\n\n\t// collect details from the advisory overlay only if we kept any of the advisory details\n\tif keepDetails && len(advisoryResult.Details) > 0 {\n\t\t*allAdvisoryDetails = append(*allAdvisoryDetails, advisoryResult.Details...)\n\t}\n}\n\n// buildPatchedVulnerabilityRecord creates the final patched vulnerability record from the original disclosure and fix/constraint information from applicable advisories.\nfunc buildPatchedVulnerabilityRecord(v *version.Version, disclosure vulnerability.Vulnerability, fixVersions *version.Set, constraints []version.Constraint, state vulnerability.FixState) vulnerability.Vulnerability {\n\tpatchedRecord := disclosure\n\n\tif state == vulnerability.FixStateFixed {\n\t\tpatchedRecord.Fix.Versions = nil\n\t\tfor _, fixVersion := range fixVersions.Values() {\n\t\t\tpatchedRecord.Fix.Versions = append(patchedRecord.Fix.Versions, fixVersion.Raw)\n\t\t\tfixConstraint, err := version.GetConstraint(fmt.Sprintf(\"< %s\", fixVersion.Raw), v.Format)\n\t\t\tif err != nil {\n\t\t\t\tlog.WithFields(\"vulnerability\", disclosure.ID, \"fixVersion\", fixVersion, \"error\", err).Trace(\"failed to create constraint for fix version\")\n\t\t\t\tcontinue // skip this fix version if we cannot create a constraint\n\t\t\t}\n\t\t\tconstraints = append(constraints, fixConstraint)\n\t\t}\n\t}\n\n\tpatchedRecord.Fix.State = finalizeFixState(disclosure, state)\n\tpatchedRecord.Constraint = version.CombineConstraints(constraints...)\n\treturn patchedRecord\n}\n\n// finalizeMatchDetails patches the processed result details with that of details in the post-processed result.\nfunc finalizeMatchDetails(processedResult *result.Result, originalDetails match.Details, v *version.Version) {\n\tif len(processedResult.Vulnerabilities) == 0 {\n\t\treturn\n\t}\n\n\t// keep details around only if we have vulnerabilities they describe\n\tprocessedResult.Details = append(processedResult.Details, originalDetails...)\n\tprocessedResult.Details = result.NewMatchDetailsSet(processedResult.Details...).ToSlice()\n\n\t// patch the version in the details if it is missing\n\tfor idx := range processedResult.Details {\n\t\td := &processedResult.Details[idx]\n\n\t\tswitch params := d.SearchedBy.(type) {\n\t\tcase match.CPEParameters:\n\t\t\tif params.Package.Version == \"\" {\n\t\t\t\tparams.Package.Version = v.Raw\n\t\t\t\td.SearchedBy = params\n\t\t\t}\n\t\tcase match.DistroParameters:\n\t\t\tif params.Package.Version == \"\" {\n\t\t\t\tparams.Package.Version = v.Raw\n\t\t\t\td.SearchedBy = params\n\t\t\t}\n\t\tcase match.EcosystemParameters:\n\t\t\tif params.Package.Version == \"\" {\n\t\t\t\tparams.Package.Version = v.Raw\n\t\t\t\td.SearchedBy = params\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc isVulnerableVersion(v *version.Version, c version.Constraint, id string) bool {\n\tif c == nil {\n\t\t// nil constraint is different than an empty constraint, so we should not consider this vulnerable\n\t\treturn false\n\t}\n\n\tisVulnerable, err := c.Satisfied(v)\n\tif err != nil {\n\t\tlog.WithFields(\"vulnerability\", id, \"error\", err).Trace(\"failed to check constraint\")\n\t\treturn false // if we cannot determine if the version is vulnerable, we assume it is not\n\t}\n\n\treturn isVulnerable\n}\n\nfunc neededFixes(v *version.Version, fixVersions []string, format version.Format, id string) []*version.Version {\n\tvar needed []*version.Version\n\tfor _, fixVersion := range fixVersions {\n\t\tfixVersionObj := version.New(fixVersion, format) // note: we use the format from the advisory, not the version itself\n\t\tres, err := v.Is(version.LT, fixVersionObj)\n\t\tif err != nil {\n\t\t\tlog.WithFields(\"format\", format, \"version\", fixVersion, \"error\", err, \"vulnerability\", id).Trace(\"failed to evaluate fix version\")\n\t\t\tcontinue\n\t\t}\n\t\tif res {\n\t\t\tneeded = append(needed, fixVersionObj)\n\t\t}\n\t}\n\n\treturn needed\n}\n\n// filterFixesForEUS filters a list of fix versions to only those reachable for the given EUS distro.\nfunc filterFixesForEUS(fixes []*version.Version, eusDistro *distro.Distro, id string) []*version.Version {\n\tif eusDistro == nil {\n\t\treturn fixes\n\t}\n\n\tvar reachable []*version.Version\n\tfor _, fix := range fixes {\n\t\tif isFixReachableForEUS(fix.Raw, eusDistro) {\n\t\t\treachable = append(reachable, fix)\n\t\t} else {\n\t\t\tlog.WithFields(\"vulnerability\", id, \"fixVersion\", fix.Raw, \"eusDistro\", eusDistro.String()).Trace(\"skipping fix not reachable for EUS version\")\n\t\t}\n\t}\n\treturn reachable\n}\n\nfunc finalizeFixState(record vulnerability.Vulnerability, state vulnerability.FixState) vulnerability.FixState {\n\tif state == \"\" {\n\t\tstate = vulnerability.FixStateUnknown\n\t}\n\n\tif state != vulnerability.FixStateUnknown {\n\t\treturn state\n\t}\n\n\tif record.Fix.State != vulnerability.FixStateUnknown {\n\t\treturn record.Fix.State\n\t}\n\n\treturn vulnerability.FixStateUnknown\n}\n"
  },
  {
    "path": "grype/matcher/rpm/rhel_eus_test.go",
    "content": "package rpm\n\nimport (\n\t\"errors\"\n\t\"sort\"\n\t\"testing\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/google/go-cmp/cmp/cmpopts\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/grype/grype/distro\"\n\t\"github.com/anchore/grype/grype/match\"\n\t\"github.com/anchore/grype/grype/matcher/internal/result\"\n\t\"github.com/anchore/grype/grype/pkg\"\n\t\"github.com/anchore/grype/grype/version\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n\tsyftPkg \"github.com/anchore/syft/syft/pkg\"\n)\n\nfunc TestExtractRHELVersionFromRelease(t *testing.T) {\n\ttests := []struct {\n\t\tname      string\n\t\trelease   string\n\t\twantMajor int\n\t\twantMinor int\n\t\twantFound bool\n\t}{\n\t\t{\n\t\t\tname:      \"el9_5 pattern\",\n\t\t\trelease:   \"503.11.1.el9_5\",\n\t\t\twantMajor: 9,\n\t\t\twantMinor: 5,\n\t\t\twantFound: true,\n\t\t},\n\t\t{\n\t\t\tname:      \"el8_10 pattern (double digit minor)\",\n\t\t\trelease:   \"82.el8_10.2\",\n\t\t\twantMajor: 8,\n\t\t\twantMinor: 10,\n\t\t\twantFound: true,\n\t\t},\n\t\t{\n\t\t\tname:      \"el7 pattern (no minor version treated as 0)\",\n\t\t\trelease:   \"1.el7\",\n\t\t\twantMajor: 7,\n\t\t\twantMinor: 0,\n\t\t\twantFound: true,\n\t\t},\n\t\t{\n\t\t\tname:      \"el9 pattern (no minor version treated as 0)\",\n\t\t\trelease:   \"427.79.1.el9\",\n\t\t\twantMajor: 9,\n\t\t\twantMinor: 0,\n\t\t\twantFound: true,\n\t\t},\n\t\t{\n\t\t\tname:      \"el9_4 pattern\",\n\t\t\trelease:   \"427.79.1.el9_4\",\n\t\t\twantMajor: 9,\n\t\t\twantMinor: 4,\n\t\t\twantFound: true,\n\t\t},\n\t\t{\n\t\t\tname:      \"no el pattern\",\n\t\t\trelease:   \"1.0.0\",\n\t\t\twantMajor: 0,\n\t\t\twantMinor: 0,\n\t\t\twantFound: false,\n\t\t},\n\t\t{\n\t\t\tname:      \"empty string\",\n\t\t\trelease:   \"\",\n\t\t\twantMajor: 0,\n\t\t\twantMinor: 0,\n\t\t\twantFound: false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgotMajor, gotMinor, gotFound := extractRHELVersionFromRelease(tt.release)\n\t\t\tassert.Equal(t, tt.wantMajor, gotMajor, \"major version mismatch\")\n\t\t\tassert.Equal(t, tt.wantMinor, gotMinor, \"minor version mismatch\")\n\t\t\tassert.Equal(t, tt.wantFound, gotFound, \"found mismatch\")\n\t\t})\n\t}\n}\n\nfunc TestExtractReleaseFromRPMVersion(t *testing.T) {\n\ttests := []struct {\n\t\tname       string\n\t\trpmVersion string\n\t\twant       string\n\t}{\n\t\t{\n\t\t\tname:       \"version with hyphen\",\n\t\t\trpmVersion: \"5.14.0-503.11.1.el9_5\",\n\t\t\twant:       \"503.11.1.el9_5\",\n\t\t},\n\t\t{\n\t\t\tname:       \"version with epoch and hyphen\",\n\t\t\trpmVersion: \"0:5.14.0-503.11.1.el9_5\",\n\t\t\twant:       \"503.11.1.el9_5\",\n\t\t},\n\t\t{\n\t\t\tname:       \"version without hyphen\",\n\t\t\trpmVersion: \"1.0.0\",\n\t\t\twant:       \"1.0.0\",\n\t\t},\n\t\t{\n\t\t\tname:       \"version with epoch but no hyphen\",\n\t\t\trpmVersion: \"1:2.3.4\",\n\t\t\twant:       \"2.3.4\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot := extractReleaseFromRPMVersion(tt.rpmVersion)\n\t\t\tassert.Equal(t, tt.want, got)\n\t\t})\n\t}\n}\n\nfunc TestIsFixReachableForEUS(t *testing.T) {\n\ttests := []struct {\n\t\tname       string\n\t\tfixVersion string\n\t\teusDistro  *distro.Distro\n\t\twant       bool\n\t}{\n\t\t{\n\t\t\tname:       \"nil distro - always reachable\",\n\t\t\tfixVersion: \"5.14.0-503.11.1.el9_5\",\n\t\t\teusDistro:  nil,\n\t\t\twant:       true,\n\t\t},\n\t\t{\n\t\t\tname:       \"el9_5 fix NOT reachable from 9.4 EUS\",\n\t\t\tfixVersion: \"5.14.0-503.11.1.el9_5\",\n\t\t\teusDistro:  newEUSDistro(\"9.4\"),\n\t\t\twant:       false,\n\t\t},\n\t\t{\n\t\t\tname:       \"el9_4 fix IS reachable from 9.4 EUS (same minor)\",\n\t\t\tfixVersion: \"5.14.0-427.80.1.el9_4\",\n\t\t\teusDistro:  newEUSDistro(\"9.4\"),\n\t\t\twant:       true,\n\t\t},\n\t\t{\n\t\t\t// This is the key test case: mainline 9.2 fixes ARE reachable from 9.4 EUS\n\t\t\t// because EUS 9.4 includes all mainline fixes up to 9.4\n\t\t\tname:       \"el9_2 mainline fix IS reachable from 9.4 EUS (lower minor version)\",\n\t\t\tfixVersion: \"5.14.0-100.el9_2\",\n\t\t\teusDistro:  newEUSDistro(\"9.4\"),\n\t\t\twant:       true,\n\t\t},\n\t\t{\n\t\t\tname:       \"el9 base fix IS reachable from 9.4 EUS (no minor version in fix)\",\n\t\t\tfixVersion: \"5.14.0-100.el9\",\n\t\t\teusDistro:  newEUSDistro(\"9.4\"),\n\t\t\twant:       true,\n\t\t},\n\t\t{\n\t\t\tname:       \"el8 fix NOT reachable from el9 (different major)\",\n\t\t\tfixVersion: \"4.18.0-100.el8_10\",\n\t\t\teusDistro:  newEUSDistro(\"9.4\"),\n\t\t\twant:       false,\n\t\t},\n\t\t{\n\t\t\tname:       \"no el pattern - fail open (assume reachable)\",\n\t\t\tfixVersion: \"1.0.0-1\",\n\t\t\teusDistro:  newEUSDistro(\"9.4\"),\n\t\t\twant:       true,\n\t\t},\n\t\t{\n\t\t\tname:       \"distro without version - fail open\",\n\t\t\tfixVersion: \"5.14.0-503.11.1.el9_5\",\n\t\t\teusDistro:  newEUSDistro(\"\"),\n\t\t\twant:       true,\n\t\t},\n\t\t{\n\t\t\t// \"9+eus\" (no minor) is treated as \"9.0+eus\", so el9_1 fixes are NOT reachable\n\t\t\tname:       \"distro with major only treated as .0 - el9_1 fix NOT reachable from 9+eus\",\n\t\t\tfixVersion: \"5.14.0-100.el9_1\",\n\t\t\teusDistro:  distro.New(distro.RedHat, \"9+eus\", \"\"),\n\t\t\twant:       false,\n\t\t},\n\t\t{\n\t\t\t// \"9+eus\" (no minor) is treated as \"9.0+eus\", so el9_0 fixes ARE reachable\n\t\t\tname:       \"distro with major only treated as .0 - el9_0 fix IS reachable from 9+eus\",\n\t\t\tfixVersion: \"5.14.0-100.el9_0\",\n\t\t\teusDistro:  distro.New(distro.RedHat, \"9+eus\", \"\"),\n\t\t\twant:       true,\n\t\t},\n\t\t{\n\t\t\t// \"9+eus\" (no minor) is treated as \"9.0+eus\", so base el9 fixes ARE reachable\n\t\t\tname:       \"distro with major only treated as .0 - el9 base fix IS reachable from 9+eus\",\n\t\t\tfixVersion: \"5.14.0-100.el9\",\n\t\t\teusDistro:  distro.New(distro.RedHat, \"9+eus\", \"\"),\n\t\t\twant:       true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot := isFixReachableForEUS(tt.fixVersion, tt.eusDistro)\n\t\t\tassert.Equal(t, tt.want, got)\n\t\t})\n\t}\n}\n\nfunc TestResolveEUSDisclosures(t *testing.T) {\n\ttests := []struct {\n\t\tname            string\n\t\tpackageVersion  string\n\t\tdisclosures     []result.Result\n\t\tadvisoryOverlay []result.Result\n\t\twant            []result.Result\n\t}{\n\t\t{\n\t\t\tname:           \"disclosure with fix version - package version is vulnerable\",\n\t\t\tpackageVersion: \"1.0.0\", // vulnerable since 1.0.0 < 1.5.0\n\t\t\tdisclosures: []result.Result{\n\t\t\t\t{\n\t\t\t\t\tID: \"CVE-2021-1\",\n\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tReference:  vulnerability.Reference{ID: \"CVE-2021-1\"},\n\t\t\t\t\t\t\tConstraint: version.MustGetConstraint(\"< 1.6.0\", version.RpmFormat), // important!\n\t\t\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\t\t\tState:    vulnerability.FixStateUnknown,\n\t\t\t\t\t\t\t\tVersions: []string{\"1.6.0\"}, // important! this is the fix version that we should not consider\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tDetails: []match.Detail{{Type: match.ExactDirectMatch}},\n\t\t\t\t},\n\t\t\t},\n\t\t\tadvisoryOverlay: []result.Result{\n\t\t\t\t{\n\t\t\t\t\tID: \"CVE-2021-1\",\n\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tReference:  vulnerability.Reference{ID: \"CVE-2021-1\"},\n\t\t\t\t\t\t\tConstraint: version.MustGetConstraint(\"< 1.5.0\", version.RpmFormat), // important!\n\t\t\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\t\t\tState:    vulnerability.FixStateFixed, // important!\n\t\t\t\t\t\t\t\tVersions: []string{\"1.5.0\"},           // important!\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tDetails: []match.Detail{{Type: match.ExactDirectMatch}},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: []result.Result{\n\t\t\t\t{\n\t\t\t\t\tID: \"CVE-2021-1\",\n\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tReference: vulnerability.Reference{ID: \"CVE-2021-1\"},\n\t\t\t\t\t\t\tConstraint: version.CombineConstraints(\n\t\t\t\t\t\t\t\tversion.MustGetConstraint(\"< 1.6.0\", version.RpmFormat), // from disclosure\n\t\t\t\t\t\t\t\tversion.MustGetConstraint(\"< 1.5.0\", version.RpmFormat), // from advisory\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\t\t\tState:    vulnerability.FixStateFixed, // important! from advisory\n\t\t\t\t\t\t\t\tVersions: []string{\"1.5.0\"},           // important! from advisory, not the disclosure\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tDetails: []match.Detail{{Type: match.ExactDirectMatch}},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:           \"vulnerability not fixed - package version not vulnerable\",\n\t\t\tpackageVersion: \"2.0.0\", // not vulnerable since 2.0.0 > 1.5.0\n\t\t\tdisclosures: []result.Result{\n\t\t\t\t{\n\t\t\t\t\tID: \"CVE-2021-1\",\n\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tReference:  vulnerability.Reference{ID: \"CVE-2021-1\"},\n\t\t\t\t\t\t\tConstraint: version.MustGetConstraint(\"< 1.5.0\", version.RpmFormat), // important!\n\t\t\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\t\t\tState:    vulnerability.FixStateUnknown,\n\t\t\t\t\t\t\t\tVersions: []string{},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tDetails: []match.Detail{{Type: match.ExactDirectMatch}},\n\t\t\t\t},\n\t\t\t},\n\t\t\tadvisoryOverlay: []result.Result{\n\t\t\t\t{\n\t\t\t\t\tID: \"CVE-2021-1\",\n\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tReference:  vulnerability.Reference{ID: \"CVE-2021-1\"},\n\t\t\t\t\t\t\tConstraint: version.MustGetConstraint(\"< 1.5.0\", version.RpmFormat), // important!\n\t\t\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\t\t\tState:    vulnerability.FixStateFixed,\n\t\t\t\t\t\t\t\tVersions: []string{\"1.5.0\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tDetails: []match.Detail{{Type: match.ExactDirectMatch}},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: []result.Result{},\n\t\t},\n\t\t{\n\t\t\tname:           \"multiple advisories with multiple fix versions\",\n\t\t\tpackageVersion: \"1.0.0\",\n\t\t\tdisclosures: []result.Result{\n\t\t\t\t{\n\t\t\t\t\tID: \"CVE-2021-1\",\n\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tReference: vulnerability.Reference{ID: \"CVE-2021-1\"},\n\t\t\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\t\t\tState:    vulnerability.FixStateUnknown,\n\t\t\t\t\t\t\t\tVersions: []string{},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tDetails: []match.Detail{{Type: match.ExactDirectMatch}},\n\t\t\t\t},\n\t\t\t},\n\t\t\tadvisoryOverlay: []result.Result{\n\t\t\t\t{\n\t\t\t\t\tID: \"CVE-2021-1\",\n\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{\n\t\t\t\t\t\t{ // advisory does not apply!\n\t\t\t\t\t\t\tReference:  vulnerability.Reference{ID: \"CVE-2021-1\"},\n\t\t\t\t\t\t\tConstraint: version.MustGetConstraint(\"< 0.9\", version.RpmFormat),\n\t\t\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\t\t\tState:    vulnerability.FixStateFixed,\n\t\t\t\t\t\t\t\tVersions: []string{\"0.9\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tReference:  vulnerability.Reference{ID: \"CVE-2021-1\"},\n\t\t\t\t\t\t\tConstraint: version.MustGetConstraint(\"< 1.5.0\", version.RpmFormat),\n\t\t\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\t\t\tState:    vulnerability.FixStateFixed,\n\t\t\t\t\t\t\t\tVersions: []string{\"1.5.0\", \"1.4.2\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{ // duplicate advisory should already be counted from the first one\n\t\t\t\t\t\t\tReference:  vulnerability.Reference{ID: \"CVE-2021-1\"},\n\t\t\t\t\t\t\tConstraint: version.MustGetConstraint(\"< 1.5.0\", version.RpmFormat),\n\t\t\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\t\t\tState:    vulnerability.FixStateFixed,\n\t\t\t\t\t\t\t\tVersions: []string{\"1.5.0\", \"1.4.2\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{ // duplicate advisory, with a different fix version\n\t\t\t\t\t\t\tReference:  vulnerability.Reference{ID: \"CVE-2021-1\"},\n\t\t\t\t\t\t\tConstraint: version.MustGetConstraint(\"< 1.5.0\", version.RpmFormat),\n\t\t\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\t\t\tState:    vulnerability.FixStateFixed,\n\t\t\t\t\t\t\t\tVersions: []string{\"1.4.3\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tReference:  vulnerability.Reference{ID: \"CVE-2021-1\"},\n\t\t\t\t\t\t\tConstraint: version.MustGetConstraint(\"< 2.0.0\", version.RpmFormat),\n\t\t\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\t\t\tState:    vulnerability.FixStateFixed,\n\t\t\t\t\t\t\t\tVersions: []string{\"2.0.0\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tDetails: []match.Detail{{Type: match.ExactDirectMatch}},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: []result.Result{\n\t\t\t\t{\n\t\t\t\t\tID: \"CVE-2021-1\",\n\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tReference: vulnerability.Reference{ID: \"CVE-2021-1\"},\n\t\t\t\t\t\t\tConstraint: version.CombineConstraints( // important! we are combining the constraints\n\t\t\t\t\t\t\t\tversion.MustGetConstraint(\"< 1.5.0\", version.RpmFormat),\n\t\t\t\t\t\t\t\tversion.MustGetConstraint(\"< 2.0.0\", version.RpmFormat),\n\t\t\t\t\t\t\t\tversion.MustGetConstraint(\"< 1.4.2\", version.RpmFormat),\n\t\t\t\t\t\t\t\tversion.MustGetConstraint(\"< 1.4.3\", version.RpmFormat),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\t\t\tState:    vulnerability.FixStateFixed,\n\t\t\t\t\t\t\t\tVersions: []string{\"1.4.2\", \"1.4.3\", \"1.5.0\", \"2.0.0\"}, // important! we have all fixes for advisories that apply\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tDetails: []match.Detail{{Type: match.ExactDirectMatch}},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:           \"advisory with wont-fix state - disclosure should be kept with patched fix state\",\n\t\t\tpackageVersion: \"1.0.0\", // vulnerable since 1.0.0 < 2.0.0\n\t\t\tdisclosures: []result.Result{\n\t\t\t\t{\n\t\t\t\t\tID: \"CVE-2021-1\",\n\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tReference:  vulnerability.Reference{ID: \"CVE-2021-1\"},\n\t\t\t\t\t\t\tConstraint: version.MustGetConstraint(\"< 2.0.0\", version.RpmFormat),\n\t\t\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\t\t\tState:    vulnerability.FixStateUnknown, // important! the disclosure doesn't have good fix info\n\t\t\t\t\t\t\t\tVersions: []string{},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tDetails: []match.Detail{{Type: match.ExactDirectMatch}},\n\t\t\t\t},\n\t\t\t},\n\t\t\tadvisoryOverlay: []result.Result{\n\t\t\t\t{\n\t\t\t\t\tID: \"CVE-2021-1\",\n\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tReference:  vulnerability.Reference{ID: \"CVE-2021-1\"},\n\t\t\t\t\t\t\tConstraint: version.MustGetConstraint(\"< 2.0.0\", version.RpmFormat),\n\t\t\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\t\t\tState:    vulnerability.FixStateWontFix, // important! we want the match to reflect this property\n\t\t\t\t\t\t\t\tVersions: []string{},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tDetails: []match.Detail{{Type: match.ExactDirectMatch}},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: []result.Result{\n\t\t\t\t{\n\t\t\t\t\tID: \"CVE-2021-1\",\n\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tReference:  vulnerability.Reference{ID: \"CVE-2021-1\"},\n\t\t\t\t\t\t\tConstraint: version.MustGetConstraint(\"< 2.0.0\", version.RpmFormat),\n\t\t\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\t\t\tState:    vulnerability.FixStateWontFix,\n\t\t\t\t\t\t\t\tVersions: []string{},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tDetails: []match.Detail{{Type: match.ExactDirectMatch}},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:           \"advisory with unknown fix state - disclosure should be kept\",\n\t\t\tpackageVersion: \"1.0.0\",\n\t\t\tdisclosures: []result.Result{\n\t\t\t\t{\n\t\t\t\t\tID: \"CVE-2021-1\",\n\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tReference:  vulnerability.Reference{ID: \"CVE-2021-1\"},\n\t\t\t\t\t\t\tConstraint: version.MustGetConstraint(\"< 2.0.0\", version.RpmFormat), // important!\n\t\t\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\t\t\tState:    vulnerability.FixStateUnknown,\n\t\t\t\t\t\t\t\tVersions: []string{},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tDetails: []match.Detail{{Type: match.ExactDirectMatch}},\n\t\t\t\t},\n\t\t\t},\n\t\t\tadvisoryOverlay: []result.Result{\n\t\t\t\t{ // ultimately, this advisory does not apply...\n\t\t\t\t\tID: \"CVE-2021-1\",\n\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tReference:  vulnerability.Reference{ID: \"CVE-2021-1\"},\n\t\t\t\t\t\t\tConstraint: version.MustGetConstraint(\"< 3.0.0\", version.RpmFormat),\n\t\t\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\t\t\tState:    vulnerability.FixStateUnknown, // important!\n\t\t\t\t\t\t\t\tVersions: []string{},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tDetails: []match.Detail{{Type: match.ExactDirectMatch}},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: []result.Result{\n\t\t\t\t{\n\t\t\t\t\tID: \"CVE-2021-1\",\n\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tReference:  vulnerability.Reference{ID: \"CVE-2021-1\"},\n\t\t\t\t\t\t\tConstraint: version.MustGetConstraint(\"< 2.0.0\", version.RpmFormat), // from the disclosure (nothing from the resolution since there was no fix information)\n\t\t\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\t\t\tState:    vulnerability.FixStateUnknown,\n\t\t\t\t\t\t\t\tVersions: []string{},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tDetails: []match.Detail{{Type: match.ExactDirectMatch}},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:           \"empty fix versions are filtered out\",\n\t\t\tpackageVersion: \"1.0.0\",\n\t\t\tdisclosures: []result.Result{\n\t\t\t\t{\n\t\t\t\t\tID: \"CVE-2021-1\",\n\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tReference: vulnerability.Reference{ID: \"CVE-2021-1\"},\n\t\t\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\t\t\tState:    vulnerability.FixStateUnknown,\n\t\t\t\t\t\t\t\tVersions: []string{},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tDetails: []match.Detail{{Type: match.ExactDirectMatch}},\n\t\t\t\t},\n\t\t\t},\n\t\t\tadvisoryOverlay: []result.Result{\n\t\t\t\t{\n\t\t\t\t\tID: \"CVE-2021-1\",\n\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tReference:  vulnerability.Reference{ID: \"CVE-2021-1\"},\n\t\t\t\t\t\t\tConstraint: version.MustGetConstraint(\"< 1.5.0\", version.RpmFormat),\n\t\t\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\t\t\tState:    vulnerability.FixStateFixed,\n\t\t\t\t\t\t\t\tVersions: []string{\"\", \"1.5.0\", \"\"}, // important!\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tDetails: []match.Detail{{Type: match.ExactDirectMatch}},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: []result.Result{\n\t\t\t\t{\n\t\t\t\t\tID: \"CVE-2021-1\",\n\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tReference:  vulnerability.Reference{ID: \"CVE-2021-1\"},\n\t\t\t\t\t\t\tConstraint: version.MustGetConstraint(\"< 1.5.0\", version.RpmFormat),\n\t\t\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\t\t\tState:    vulnerability.FixStateFixed,\n\t\t\t\t\t\t\t\tVersions: []string{\"1.5.0\"}, // note: empty versions are filtered out\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tDetails: []match.Detail{{Type: match.ExactDirectMatch}},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:           \"constraint satisfaction error - advisory skipped\",\n\t\t\tpackageVersion: \"W:1.2.3-456\", // intentionally invalid epoch (will fail to parse)\n\t\t\tdisclosures: []result.Result{\n\t\t\t\t{\n\t\t\t\t\tID: \"CVE-2021-1\",\n\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tReference:  vulnerability.Reference{ID: \"CVE-2021-1\"},\n\t\t\t\t\t\t\tConstraint: version.MustGetConstraint(\"< 1.5.0\", version.RpmFormat),\n\t\t\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\t\t\tState:    vulnerability.FixStateUnknown,\n\t\t\t\t\t\t\t\tVersions: []string{},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tDetails: []match.Detail{{Type: match.ExactDirectMatch}},\n\t\t\t\t},\n\t\t\t},\n\t\t\tadvisoryOverlay: []result.Result{\n\t\t\t\t{\n\t\t\t\t\tID: \"CVE-2021-1\",\n\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tReference:  vulnerability.Reference{ID: \"CVE-2021-1\"},\n\t\t\t\t\t\t\tConstraint: version.MustGetConstraint(\"< 1.5.0\", version.RpmFormat),\n\t\t\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\t\t\tState:    vulnerability.FixStateFixed,\n\t\t\t\t\t\t\t\tVersions: []string{\"1.5.0\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tDetails: []match.Detail{{Type: match.ExactDirectMatch}},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: []result.Result{},\n\t\t},\n\t\t{\n\t\t\tname:           \"no advisory overlay, disclosure has nil constraint - remove disclosure\",\n\t\t\tpackageVersion: \"1.0.0\",\n\t\t\tdisclosures: []result.Result{\n\t\t\t\t{\n\t\t\t\t\tID: \"CVE-2021-1\",\n\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tReference:  vulnerability.Reference{ID: \"CVE-2021-1\"},\n\t\t\t\t\t\t\tConstraint: nil, // important! we're never vulnerable!\n\t\t\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\t\t\tState:    vulnerability.FixStateUnknown,\n\t\t\t\t\t\t\t\tVersions: []string{},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tDetails: []match.Detail{{Type: match.ExactDirectMatch}},\n\t\t\t\t},\n\t\t\t},\n\t\t\tadvisoryOverlay: []result.Result{\n\t\t\t\t{ // does not apply\n\t\t\t\t\tID:              \"CVE-2021-1\",\n\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{},\n\t\t\t\t\tDetails:         []match.Detail{},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: []result.Result{},\n\t\t},\n\t\t{\n\t\t\tname:           \"no advisory overlay, disclosure has empty constraint - keep disclosure\",\n\t\t\tpackageVersion: \"1.0.0\",\n\t\t\tdisclosures: []result.Result{\n\t\t\t\t{\n\t\t\t\t\tID: \"CVE-2021-1\",\n\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tReference:  vulnerability.Reference{ID: \"CVE-2021-1\"},\n\t\t\t\t\t\t\tConstraint: version.MustGetConstraint(\"\", version.RpmFormat), // important! we're always vulnerable\n\t\t\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\t\t\tState:    vulnerability.FixStateUnknown,\n\t\t\t\t\t\t\t\tVersions: []string{},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tDetails: []match.Detail{{Type: match.ExactDirectMatch}},\n\t\t\t\t},\n\t\t\t},\n\t\t\tadvisoryOverlay: []result.Result{\n\t\t\t\t{ // does not apply\n\t\t\t\t\tID:              \"CVE-2021-1\",\n\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{},\n\t\t\t\t\tDetails:         []match.Detail{},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: []result.Result{\n\t\t\t\t{\n\t\t\t\t\tID: \"CVE-2021-1\",\n\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tReference:  vulnerability.Reference{ID: \"CVE-2021-1\"},\n\t\t\t\t\t\t\tConstraint: version.MustGetConstraint(\"\", version.RpmFormat), // important! shows \"none (rpm)\"\n\t\t\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\t\t\tState:    vulnerability.FixStateUnknown,\n\t\t\t\t\t\t\t\tVersions: []string{},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tDetails: []match.Detail{{Type: match.ExactDirectMatch}},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:           \"no advisory overlay, disclosure does not apply - remove all\",\n\t\t\tpackageVersion: \"1.0.0\",\n\t\t\tdisclosures: []result.Result{\n\t\t\t\t{\n\t\t\t\t\tID: \"CVE-2021-1\",\n\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tReference:  vulnerability.Reference{ID: \"CVE-2021-1\"},\n\t\t\t\t\t\t\tConstraint: version.MustGetConstraint(\"< 0.9\", version.RpmFormat), // important! we're not vulnerable!\n\t\t\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\t\t\tState:    vulnerability.FixStateUnknown,\n\t\t\t\t\t\t\t\tVersions: []string{},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tDetails: []match.Detail{{Type: match.ExactDirectMatch}},\n\t\t\t\t},\n\t\t\t},\n\t\t\tadvisoryOverlay: []result.Result{\n\t\t\t\t{ // does not apply\n\t\t\t\t\tID:              \"CVE-2021-1\",\n\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{},\n\t\t\t\t\tDetails:         []match.Detail{},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: []result.Result{},\n\t\t},\n\t\t{\n\t\t\tname:           \"advisory with no fixes - disclosure is preserved\",\n\t\t\tpackageVersion: \"1.0.0\",\n\t\t\tdisclosures: []result.Result{\n\t\t\t\t{\n\t\t\t\t\tID: \"CVE-2021-1\",\n\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tReference: vulnerability.Reference{ID: \"CVE-2021-1\"},\n\t\t\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\t\t\tState:    vulnerability.FixStateUnknown,\n\t\t\t\t\t\t\t\tVersions: []string{},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tDetails: []match.Detail{{Type: match.ExactDirectMatch}},\n\t\t\t\t},\n\t\t\t},\n\t\t\tadvisoryOverlay: []result.Result{\n\t\t\t\t{\n\t\t\t\t\tID: \"CVE-2021-1\",\n\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tReference:  vulnerability.Reference{ID: \"CVE-2021-1\"},\n\t\t\t\t\t\t\tConstraint: version.MustGetConstraint(\"< 1.5.0\", version.RpmFormat),\n\t\t\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\t\t\tState:    vulnerability.FixStateWontFix,\n\t\t\t\t\t\t\t\tVersions: []string{\"1.5.0\"}, // important: this is a wont-fix advisory so this should not be incorporated (an inconsistent advisory)\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tDetails: []match.Detail{{Type: match.ExactDirectMatch}},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: []result.Result{\n\t\t\t\t{\n\t\t\t\t\tID: \"CVE-2021-1\",\n\t\t\t\t\tVulnerabilities: []vulnerability.Vulnerability{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tReference:  vulnerability.Reference{ID: \"CVE-2021-1\"},\n\t\t\t\t\t\t\tConstraint: version.MustGetConstraint(\"< 1.5.0\", version.RpmFormat),\n\t\t\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\t\t\tState:    vulnerability.FixStateWontFix, // wont-fix state is preserved\n\t\t\t\t\t\t\t\tVersions: []string{},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tDetails: []match.Detail{{Type: match.ExactDirectMatch}},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvar v *version.Version\n\t\t\tv = version.New(tt.packageVersion, version.RpmFormat)\n\t\t\tif v.Validate() != nil {\n\t\t\t\tv = nil\n\t\t\t}\n\n\t\t\tresolver := mergeEUSAdvisoriesIntoMainDisclosures(v, nil)\n\n\t\t\tgot := resolver(tt.disclosures, tt.advisoryOverlay)\n\n\t\t\topts := cmp.Options{\n\t\t\t\tcmpopts.IgnoreUnexported(result.Result{}),\n\t\t\t\tcmpopts.IgnoreUnexported(version.Version{}),\n\t\t\t\tcmpopts.EquateEmpty(),\n\t\t\t}\n\t\t\tif diff := cmp.Diff(tt.want, got, opts...); diff != \"\" {\n\t\t\t\tt.Errorf(\"mergeEUSAdvisoriesIntoMainDisclosures() mismatch (-want +got):\\n%s\", diff)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestRedhatEUSMatches(t *testing.T) {\n\ttestPkg1 := pkg.Package{\n\t\tID:      pkg.ID(\"test-pkg-id\"),\n\t\tName:    \"test-pkg\",\n\t\tVersion: \"1.0.0\",\n\t\tType:    syftPkg.RpmPkg,\n\t\tDistro:  newEUSDistro(\"9.4\"),\n\t}\n\n\ttests := []struct {\n\t\tname            string\n\t\tcatalogPkg      pkg.Package\n\t\tsearchPkg       *pkg.Package\n\t\tdisclosureVulns []vulnerability.Vulnerability\n\t\tresolutionVulns []vulnerability.Vulnerability\n\t\tdisclosureError error\n\t\tresolutionError error\n\t\twant            []match.Match\n\t\twantErr         require.ErrorAssertionFunc\n\t}{\n\t\t{\n\t\t\tname:            \"empty set of disclosures and advisories\",\n\t\t\tcatalogPkg:      testPkg1,\n\t\t\tdisclosureVulns: []vulnerability.Vulnerability{},\n\t\t\tresolutionVulns: []vulnerability.Vulnerability{},\n\t\t\twant:            nil,\n\t\t},\n\t\t{\n\t\t\tname:       \"successful EUS match with fix - direct match\",\n\t\t\tcatalogPkg: testPkg1,\n\t\t\tdisclosureVulns: []vulnerability.Vulnerability{\n\t\t\t\t{\n\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\tID:        \"CVE-2021-1\",\n\t\t\t\t\t\tNamespace: \"namespace\",\n\t\t\t\t\t},\n\t\t\t\t\tPackageName: \"test-pkg\", // same as searched package = direct match\n\t\t\t\t\tConstraint:  version.MustGetConstraint(\"< 1.5.0\", version.RpmFormat),\n\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\tState:    vulnerability.FixStateUnknown,\n\t\t\t\t\t\tVersions: []string{},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tresolutionVulns: []vulnerability.Vulnerability{\n\t\t\t\t{\n\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\tID:        \"CVE-2021-1\",\n\t\t\t\t\t\tNamespace: \"namespace\",\n\t\t\t\t\t},\n\t\t\t\t\tPackageName: \"test-pkg\", // same as searched package = direct match\n\t\t\t\t\tConstraint:  version.MustGetConstraint(\"< 1.5.0\", version.RpmFormat),\n\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\tState:    vulnerability.FixStateFixed,\n\t\t\t\t\t\tVersions: []string{\"1.5.0\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: []match.Match{\n\t\t\t\t{\n\t\t\t\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\t\tID:        \"CVE-2021-1\",\n\t\t\t\t\t\t\tNamespace: \"namespace\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tPackageName: \"test-pkg\",\n\t\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\t\tState:    vulnerability.FixStateFixed,\n\t\t\t\t\t\t\tVersions: []string{\"1.5.0\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tPackage: pkg.Package{\n\t\t\t\t\t\tID:      pkg.ID(\"test-pkg-id\"),\n\t\t\t\t\t\tName:    \"test-pkg\",\n\t\t\t\t\t\tVersion: \"1.0.0\",\n\t\t\t\t\t\tType:    syftPkg.RpmPkg,\n\t\t\t\t\t\tDistro:  newEUSDistro(\"9.4\"),\n\t\t\t\t\t},\n\t\t\t\t\tDetails: []match.Detail{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tType: match.ExactDirectMatch,\n\t\t\t\t\t\t\tSearchedBy: match.DistroParameters{\n\t\t\t\t\t\t\t\tDistro: match.DistroIdentification{\n\t\t\t\t\t\t\t\t\tType:    \"redhat\",\n\t\t\t\t\t\t\t\t\tVersion: \"9.4\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tPackage: match.PackageParameter{\n\t\t\t\t\t\t\t\t\tName:    \"test-pkg\",\n\t\t\t\t\t\t\t\t\tVersion: \"1.0.0\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tNamespace: \"namespace\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tFound: match.DistroResult{\n\t\t\t\t\t\t\t\tVulnerabilityID:   \"CVE-2021-1\",\n\t\t\t\t\t\t\t\tVersionConstraint: \"< 1.5.0 (rpm)\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tMatcher:    match.RpmMatcher,\n\t\t\t\t\t\t\tConfidence: 1,\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tType: match.ExactDirectMatch,\n\t\t\t\t\t\t\tSearchedBy: match.DistroParameters{\n\t\t\t\t\t\t\t\tDistro: match.DistroIdentification{\n\t\t\t\t\t\t\t\t\tType:    \"redhat\",\n\t\t\t\t\t\t\t\t\tVersion: \"9.4+eus\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tPackage: match.PackageParameter{\n\t\t\t\t\t\t\t\t\tName:    \"test-pkg\",\n\t\t\t\t\t\t\t\t\tVersion: \"1.0.0\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tNamespace: \"namespace\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tFound: match.DistroResult{\n\t\t\t\t\t\t\t\tVulnerabilityID:   \"CVE-2021-1\",\n\t\t\t\t\t\t\t\tVersionConstraint: \"< 1.5.0 (rpm)\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tMatcher:    match.RpmMatcher,\n\t\t\t\t\t\t\tConfidence: 1,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:       \"successful EUS match with fix - indirect match\",\n\t\t\tcatalogPkg: testPkg1,\n\t\t\tsearchPkg: &pkg.Package{\n\t\t\t\tID:      pkg.ID(\"indirect-test-pkg-id\"),\n\t\t\t\tName:    \"indirect-test-pkg\", // important! this will be detected as an indirect match\n\t\t\t\tVersion: \"1.0.0\",\n\t\t\t\tType:    syftPkg.RpmPkg,\n\t\t\t\tDistro:  newEUSDistro(\"9.4\"),\n\t\t\t},\n\t\t\tdisclosureVulns: []vulnerability.Vulnerability{\n\t\t\t\t{\n\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\tID:        \"CVE-2021-1\",\n\t\t\t\t\t\tNamespace: \"namespace\",\n\t\t\t\t\t},\n\t\t\t\t\tPackageName: \"indirect-test-pkg\", // setup to match search package name\n\t\t\t\t\tConstraint:  version.MustGetConstraint(\"< 1.5.0\", version.RpmFormat),\n\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\tState:    vulnerability.FixStateUnknown,\n\t\t\t\t\t\tVersions: []string{},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tresolutionVulns: []vulnerability.Vulnerability{\n\t\t\t\t{\n\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\tID:        \"CVE-2021-1\",\n\t\t\t\t\t\tNamespace: \"namespace\",\n\t\t\t\t\t},\n\t\t\t\t\tPackageName: \"indirect-test-pkg\", // setup to match search package name\n\t\t\t\t\tConstraint:  version.MustGetConstraint(\"< 1.5.0\", version.RpmFormat),\n\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\tState:    vulnerability.FixStateFixed,\n\t\t\t\t\t\tVersions: []string{\"1.5.0\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: []match.Match{\n\t\t\t\t{\n\t\t\t\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\t\tID:        \"CVE-2021-1\",\n\t\t\t\t\t\t\tNamespace: \"namespace\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tPackageName: \"indirect-test-pkg\",\n\t\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\t\tState:    vulnerability.FixStateFixed,\n\t\t\t\t\t\t\tVersions: []string{\"1.5.0\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tPackage: pkg.Package{\n\t\t\t\t\t\tID:      pkg.ID(\"test-pkg-id\"),\n\t\t\t\t\t\tName:    \"test-pkg\",\n\t\t\t\t\t\tVersion: \"1.0.0\",\n\t\t\t\t\t\tType:    syftPkg.RpmPkg,\n\t\t\t\t\t\tDistro:  newEUSDistro(\"9.4\"),\n\t\t\t\t\t},\n\t\t\t\t\tDetails: []match.Detail{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tType: match.ExactIndirectMatch,\n\t\t\t\t\t\t\tSearchedBy: match.DistroParameters{\n\t\t\t\t\t\t\t\tDistro: match.DistroIdentification{\n\t\t\t\t\t\t\t\t\tType:    \"redhat\",\n\t\t\t\t\t\t\t\t\tVersion: \"9.4\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tPackage: match.PackageParameter{\n\t\t\t\t\t\t\t\t\tName:    \"indirect-test-pkg\", // important! we used the indirect package as input\n\t\t\t\t\t\t\t\t\tVersion: \"1.0.0\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tNamespace: \"namespace\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tFound: match.DistroResult{\n\t\t\t\t\t\t\t\tVulnerabilityID:   \"CVE-2021-1\",\n\t\t\t\t\t\t\t\tVersionConstraint: \"< 1.5.0 (rpm)\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tMatcher:    match.RpmMatcher,\n\t\t\t\t\t\t\tConfidence: 1,\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tType: match.ExactIndirectMatch,\n\t\t\t\t\t\t\tSearchedBy: match.DistroParameters{\n\t\t\t\t\t\t\t\tDistro: match.DistroIdentification{\n\t\t\t\t\t\t\t\t\tType:    \"redhat\",\n\t\t\t\t\t\t\t\t\tVersion: \"9.4+eus\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tPackage: match.PackageParameter{\n\t\t\t\t\t\t\t\t\tName:    \"indirect-test-pkg\", // important! we used the indirect package as input\n\t\t\t\t\t\t\t\t\tVersion: \"1.0.0\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tNamespace: \"namespace\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tFound: match.DistroResult{\n\t\t\t\t\t\t\t\tVulnerabilityID:   \"CVE-2021-1\",\n\t\t\t\t\t\t\t\tVersionConstraint: \"< 1.5.0 (rpm)\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tMatcher:    match.RpmMatcher,\n\t\t\t\t\t\t\tConfidence: 1,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:       \"valid disclosures found but no resolutions\",\n\t\t\tcatalogPkg: testPkg1,\n\t\t\tdisclosureVulns: []vulnerability.Vulnerability{\n\t\t\t\t{\n\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\tID:        \"CVE-2021-1\",\n\t\t\t\t\t\tNamespace: \"namespace\",\n\t\t\t\t\t},\n\t\t\t\t\tPackageName: \"test-pkg\",                                       // direct match\n\t\t\t\t\tConstraint:  version.MustGetConstraint(\"\", version.RpmFormat), // no constraint, so always vulnerable\n\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\tState:    vulnerability.FixStateUnknown,\n\t\t\t\t\t\tVersions: []string{},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tresolutionVulns: []vulnerability.Vulnerability{},\n\t\t\twant: []match.Match{ // keep the original disclosure as a match\n\t\t\t\t{\n\t\t\t\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\t\tID:        \"CVE-2021-1\",\n\t\t\t\t\t\t\tNamespace: \"namespace\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tPackageName: \"test-pkg\",\n\t\t\t\t\t\tConstraint:  version.MustGetConstraint(\"\", version.RpmFormat),\n\t\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\t\tState:    vulnerability.FixStateUnknown,\n\t\t\t\t\t\t\tVersions: []string{},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tPackage: pkg.Package{\n\t\t\t\t\t\tID:      pkg.ID(\"test-pkg-id\"),\n\t\t\t\t\t\tName:    \"test-pkg\",\n\t\t\t\t\t\tVersion: \"1.0.0\",\n\t\t\t\t\t\tType:    syftPkg.RpmPkg,\n\t\t\t\t\t\tDistro:  newEUSDistro(\"9.4\"),\n\t\t\t\t\t},\n\t\t\t\t\tDetails: []match.Detail{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tType: match.ExactDirectMatch,\n\t\t\t\t\t\t\tSearchedBy: match.DistroParameters{\n\t\t\t\t\t\t\t\tDistro: match.DistroIdentification{\n\t\t\t\t\t\t\t\t\tType:    \"redhat\",\n\t\t\t\t\t\t\t\t\tVersion: \"9.4\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tPackage: match.PackageParameter{\n\t\t\t\t\t\t\t\t\tName:    \"test-pkg\",\n\t\t\t\t\t\t\t\t\tVersion: \"1.0.0\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tNamespace: \"namespace\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tFound: match.DistroResult{\n\t\t\t\t\t\t\t\tVulnerabilityID:   \"CVE-2021-1\",\n\t\t\t\t\t\t\t\tVersionConstraint: \"none (rpm)\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tMatcher:    match.RpmMatcher,\n\t\t\t\t\t\t\tConfidence: 1,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"vulnerability resolved by EUS advisory\",\n\t\t\tcatalogPkg: pkg.Package{\n\t\t\t\tID:      pkg.ID(\"test-pkg-id\"),\n\t\t\t\tName:    \"test-pkg\",\n\t\t\t\tVersion: \"2.0.0\", // version higher than fix, so resolved\n\t\t\t\tType:    syftPkg.RpmPkg,\n\t\t\t\tDistro:  newEUSDistro(\"9.4\"),\n\t\t\t},\n\t\t\tdisclosureVulns: []vulnerability.Vulnerability{\n\t\t\t\t{\n\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\tID:        \"CVE-2021-1\",\n\t\t\t\t\t\tNamespace: \"namespace\",\n\t\t\t\t\t},\n\t\t\t\t\tPackageName: \"test-pkg\", // direct match\n\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\tState:    vulnerability.FixStateUnknown,\n\t\t\t\t\t\tVersions: []string{},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tresolutionVulns: []vulnerability.Vulnerability{\n\t\t\t\t{\n\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\tID:        \"CVE-2021-1\",\n\t\t\t\t\t\tNamespace: \"namespace\",\n\t\t\t\t\t},\n\t\t\t\t\tPackageName: \"test-pkg\", // direct match\n\t\t\t\t\tConstraint:  version.MustGetConstraint(\"< 1.5.0\", version.RpmFormat),\n\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\tState:    vulnerability.FixStateFixed,\n\t\t\t\t\t\tVersions: []string{\"1.5.0\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: []match.Match{}, // vulnerability is resolved because package version 2.0.0 > 1.5.0\n\t\t},\n\t\t{\n\t\t\tname:       \"multiple valid disclosures with mixed resolutions\",\n\t\t\tcatalogPkg: testPkg1,\n\t\t\tdisclosureVulns: []vulnerability.Vulnerability{\n\t\t\t\t{\n\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\tID:        \"CVE-2021-1\",\n\t\t\t\t\t\tNamespace: \"namespace\",\n\t\t\t\t\t},\n\t\t\t\t\tPackageName: \"test-pkg\", // direct match\n\t\t\t\t\tConstraint:  version.MustGetConstraint(\"\", version.RpmFormat),\n\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\tState:    vulnerability.FixStateUnknown,\n\t\t\t\t\t\tVersions: []string{},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\tID:        \"CVE-2021-2\",\n\t\t\t\t\t\tNamespace: \"namespace\",\n\t\t\t\t\t},\n\t\t\t\t\tPackageName: \"test-pkg\", // direct match\n\t\t\t\t\tConstraint:  version.MustGetConstraint(\"\", version.RpmFormat),\n\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\tState:    vulnerability.FixStateUnknown,\n\t\t\t\t\t\tVersions: []string{},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\tID:        \"CVE-2021-3\",\n\t\t\t\t\t\tNamespace: \"namespace\",\n\t\t\t\t\t},\n\t\t\t\t\tPackageName: \"test-pkg\", // direct match\n\t\t\t\t\tConstraint:  nil,        // no constraint, so we assume we're never vulnerable to this\n\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\tState:    vulnerability.FixStateUnknown,\n\t\t\t\t\t\tVersions: []string{},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tresolutionVulns: []vulnerability.Vulnerability{\n\t\t\t\t{\n\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\tID:        \"CVE-2021-1\",\n\t\t\t\t\t\tNamespace: \"namespace\",\n\t\t\t\t\t},\n\t\t\t\t\tPackageName: \"test-pkg\", // direct match\n\t\t\t\t\tConstraint:  version.MustGetConstraint(\"< 1.5.0\", version.RpmFormat),\n\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\tState:    vulnerability.FixStateFixed,\n\t\t\t\t\t\tVersions: []string{\"1.5.0\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: []match.Match{\n\t\t\t\t{\n\t\t\t\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\t\tID:        \"CVE-2021-1\",\n\t\t\t\t\t\t\tNamespace: \"namespace\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tPackageName: \"test-pkg\",\n\t\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\t\tState:    vulnerability.FixStateFixed,\n\t\t\t\t\t\t\tVersions: []string{\"1.5.0\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tPackage: pkg.Package{\n\t\t\t\t\t\tID:      pkg.ID(\"test-pkg-id\"),\n\t\t\t\t\t\tName:    \"test-pkg\",\n\t\t\t\t\t\tVersion: \"1.0.0\",\n\t\t\t\t\t\tType:    syftPkg.RpmPkg,\n\t\t\t\t\t\tDistro:  newEUSDistro(\"9.4\"),\n\t\t\t\t\t},\n\t\t\t\t\tDetails: []match.Detail{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tType: match.ExactDirectMatch,\n\t\t\t\t\t\t\tSearchedBy: match.DistroParameters{\n\t\t\t\t\t\t\t\tDistro: match.DistroIdentification{\n\t\t\t\t\t\t\t\t\tType:    \"redhat\",\n\t\t\t\t\t\t\t\t\tVersion: \"9.4\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tPackage: match.PackageParameter{\n\t\t\t\t\t\t\t\t\tName:    \"test-pkg\",\n\t\t\t\t\t\t\t\t\tVersion: \"1.0.0\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tNamespace: \"namespace\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tFound: match.DistroResult{\n\t\t\t\t\t\t\t\tVulnerabilityID:   \"CVE-2021-1\",\n\t\t\t\t\t\t\t\tVersionConstraint: \"< 1.5.0 (rpm)\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tMatcher:    match.RpmMatcher,\n\t\t\t\t\t\t\tConfidence: 1,\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tType: match.ExactDirectMatch,\n\t\t\t\t\t\t\tSearchedBy: match.DistroParameters{\n\t\t\t\t\t\t\t\tDistro: match.DistroIdentification{\n\t\t\t\t\t\t\t\t\tType:    \"redhat\",\n\t\t\t\t\t\t\t\t\tVersion: \"9.4+eus\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tPackage: match.PackageParameter{\n\t\t\t\t\t\t\t\t\tName:    \"test-pkg\",\n\t\t\t\t\t\t\t\t\tVersion: \"1.0.0\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tNamespace: \"namespace\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tFound: match.DistroResult{\n\t\t\t\t\t\t\t\tVulnerabilityID:   \"CVE-2021-1\",\n\t\t\t\t\t\t\t\tVersionConstraint: \"< 1.5.0 (rpm)\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tMatcher:    match.RpmMatcher,\n\t\t\t\t\t\t\tConfidence: 1,\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tType: match.ExactDirectMatch,\n\t\t\t\t\t\t\tSearchedBy: match.DistroParameters{\n\t\t\t\t\t\t\t\tDistro: match.DistroIdentification{\n\t\t\t\t\t\t\t\t\tType:    \"redhat\",\n\t\t\t\t\t\t\t\t\tVersion: \"9.4\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tPackage: match.PackageParameter{\n\t\t\t\t\t\t\t\t\tName:    \"test-pkg\",\n\t\t\t\t\t\t\t\t\tVersion: \"1.0.0\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tNamespace: \"namespace\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tFound: match.DistroResult{\n\t\t\t\t\t\t\t\tVulnerabilityID:   \"CVE-2021-1\",\n\t\t\t\t\t\t\t\tVersionConstraint: \"none (rpm)\", // important! this is the disclosure with no constraint\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tMatcher:    match.RpmMatcher,\n\t\t\t\t\t\t\tConfidence: 1,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\t\tID:        \"CVE-2021-2\",\n\t\t\t\t\t\t\tNamespace: \"namespace\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tPackageName: \"test-pkg\",\n\t\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\t\tState:    vulnerability.FixStateUnknown,\n\t\t\t\t\t\t\tVersions: []string{},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tPackage: pkg.Package{\n\t\t\t\t\t\tID:      pkg.ID(\"test-pkg-id\"),\n\t\t\t\t\t\tName:    \"test-pkg\",\n\t\t\t\t\t\tVersion: \"1.0.0\",\n\t\t\t\t\t\tType:    syftPkg.RpmPkg,\n\t\t\t\t\t\tDistro:  newEUSDistro(\"9.4\"),\n\t\t\t\t\t},\n\t\t\t\t\tDetails: []match.Detail{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tType: match.ExactDirectMatch,\n\t\t\t\t\t\t\tSearchedBy: match.DistroParameters{\n\t\t\t\t\t\t\t\tDistro: match.DistroIdentification{\n\t\t\t\t\t\t\t\t\tType:    \"redhat\",\n\t\t\t\t\t\t\t\t\tVersion: \"9.4\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tPackage: match.PackageParameter{\n\t\t\t\t\t\t\t\t\tName:    \"test-pkg\",\n\t\t\t\t\t\t\t\t\tVersion: \"1.0.0\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tNamespace: \"namespace\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tFound: match.DistroResult{\n\t\t\t\t\t\t\t\tVulnerabilityID:   \"CVE-2021-2\",\n\t\t\t\t\t\t\t\tVersionConstraint: \"none (rpm)\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tMatcher:    match.RpmMatcher,\n\t\t\t\t\t\t\tConfidence: 1,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:       \"multiple advisories with mixed fix state relative to search package\",\n\t\t\tcatalogPkg: testPkg1,\n\t\t\tdisclosureVulns: []vulnerability.Vulnerability{\n\t\t\t\t{\n\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\tID:        \"CVE-2021-1\",\n\t\t\t\t\t\tNamespace: \"namespace\",\n\t\t\t\t\t},\n\t\t\t\t\tPackageName: \"test-pkg\", // direct match\n\t\t\t\t\tConstraint:  version.MustGetConstraint(\"\", version.RpmFormat),\n\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\tState:    vulnerability.FixStateUnknown,\n\t\t\t\t\t\tVersions: []string{},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tresolutionVulns: []vulnerability.Vulnerability{\n\t\t\t\t{\n\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\tID:        \"CVE-2021-1\",\n\t\t\t\t\t\tNamespace: \"namespace\",\n\t\t\t\t\t},\n\t\t\t\t\tPackageName: \"test-pkg\", // direct match\n\t\t\t\t\tConstraint:  version.MustGetConstraint(\"< 1.5.0\", version.RpmFormat),\n\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\tState:    vulnerability.FixStateFixed,\n\t\t\t\t\t\tVersions: []string{\"1.5.0\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\tID:        \"CVE-2021-1\",\n\t\t\t\t\t\tNamespace: \"namespace\",\n\t\t\t\t\t},\n\t\t\t\t\tPackageName: \"test-pkg\", // direct match\n\t\t\t\t\tConstraint:  version.MustGetConstraint(\"< 1.0.0\", version.RpmFormat),\n\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\tState:    vulnerability.FixStateFixed,\n\t\t\t\t\t\tVersions: []string{\"1.0.0\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: []match.Match{},\n\t\t},\n\t\t{\n\t\t\tname:            \"error fetching disclosures\",\n\t\t\tcatalogPkg:      testPkg1,\n\t\t\tdisclosureVulns: []vulnerability.Vulnerability{},\n\t\t\tresolutionVulns: []vulnerability.Vulnerability{},\n\t\t\tdisclosureError: errors.New(\"disclosure error\"),\n\t\t\twant:            nil,\n\t\t\twantErr:         require.Error,\n\t\t},\n\t\t{\n\t\t\t// This test case demonstrates issue #2847: when a user is on RHEL 9.4+eus and\n\t\t\t// the only available fix is for RHEL 9.5 (indicated by el9_5 in the fix version),\n\t\t\t// the vulnerability should NOT be reported as \"Fixed\" when using --only-fixed,\n\t\t\t// because the EUS user cannot upgrade to RHEL 9.5.\n\t\t\tname: \"fix version for higher minor version should not be considered fixed for EUS - issue 2847\",\n\t\t\tcatalogPkg: pkg.Package{\n\t\t\t\tID:      pkg.ID(\"kernel-id\"),\n\t\t\t\tName:    \"kernel\",\n\t\t\t\tVersion: \"5.14.0-427.79.1.el9_4\", // user's current version on RHEL 9.4 EUS\n\t\t\t\tType:    syftPkg.RpmPkg,\n\t\t\t\tDistro:  newEUSDistro(\"9.4\"),\n\t\t\t},\n\t\t\tdisclosureVulns: []vulnerability.Vulnerability{\n\t\t\t\t{\n\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\tID:        \"CVE-2020-10135\",\n\t\t\t\t\t\tNamespace: \"redhat:distro:redhat:9\",\n\t\t\t\t\t},\n\t\t\t\t\tPackageName: \"kernel\",\n\t\t\t\t\tConstraint:  version.MustGetConstraint(\"< 5.14.0-503.11.1.el9_5\", version.RpmFormat),\n\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\tState:    vulnerability.FixStateUnknown,\n\t\t\t\t\t\tVersions: []string{},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tresolutionVulns: []vulnerability.Vulnerability{\n\t\t\t\t{\n\t\t\t\t\t// This fix is for RHEL 9.5 (indicated by el9_5 in the version),\n\t\t\t\t\t// which is NOT available to RHEL 9.4 EUS users\n\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\tID:        \"CVE-2020-10135\",\n\t\t\t\t\t\tNamespace: \"redhat:distro:redhat:9\",\n\t\t\t\t\t},\n\t\t\t\t\tPackageName: \"kernel\",\n\t\t\t\t\tConstraint:  version.MustGetConstraint(\"< 5.14.0-503.11.1.el9_5\", version.RpmFormat),\n\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\tState:    vulnerability.FixStateFixed,\n\t\t\t\t\t\tVersions: []string{\"5.14.0-503.11.1.el9_5\"}, // note: el9_5 indicates RHEL 9.5\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\t// Expected behavior: since the fix requires upgrading to RHEL 9.5 and the user\n\t\t\t// is on RHEL 9.4 EUS (can't upgrade to 9.5), the fix should NOT be considered\n\t\t\t// valid and the FixState should be NotFixed (not Fixed).\n\t\t\twant: []match.Match{\n\t\t\t\t{\n\t\t\t\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\t\tID:        \"CVE-2020-10135\",\n\t\t\t\t\t\t\tNamespace: \"redhat:distro:redhat:9\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tPackageName: \"kernel\",\n\t\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\t\tState:    vulnerability.FixStateNotFixed, // fix exists but not reachable for EUS 9.4\n\t\t\t\t\t\t\tVersions: []string{},                     // no valid fixes for EUS 9.4\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tPackage: pkg.Package{\n\t\t\t\t\t\tID:      pkg.ID(\"kernel-id\"),\n\t\t\t\t\t\tName:    \"kernel\",\n\t\t\t\t\t\tVersion: \"5.14.0-427.79.1.el9_4\",\n\t\t\t\t\t\tType:    syftPkg.RpmPkg,\n\t\t\t\t\t\tDistro:  newEUSDistro(\"9.4\"),\n\t\t\t\t\t},\n\t\t\t\t\tDetails: []match.Detail{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tType: match.ExactDirectMatch,\n\t\t\t\t\t\t\tSearchedBy: match.DistroParameters{\n\t\t\t\t\t\t\t\tDistro: match.DistroIdentification{\n\t\t\t\t\t\t\t\t\tType:    \"redhat\",\n\t\t\t\t\t\t\t\t\tVersion: \"9.4\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tPackage: match.PackageParameter{\n\t\t\t\t\t\t\t\t\tName:    \"kernel\",\n\t\t\t\t\t\t\t\t\tVersion: \"5.14.0-427.79.1.el9_4\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tNamespace: \"redhat:distro:redhat:9\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tFound: match.DistroResult{\n\t\t\t\t\t\t\t\tVulnerabilityID:   \"CVE-2020-10135\",\n\t\t\t\t\t\t\t\tVersionConstraint: \"< 5.14.0-503.11.1.el9_5 (rpm)\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tMatcher:    match.RpmMatcher,\n\t\t\t\t\t\t\tConfidence: 1,\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tType: match.ExactDirectMatch,\n\t\t\t\t\t\t\tSearchedBy: match.DistroParameters{\n\t\t\t\t\t\t\t\tDistro: match.DistroIdentification{\n\t\t\t\t\t\t\t\t\tType:    \"redhat\",\n\t\t\t\t\t\t\t\t\tVersion: \"9.4+eus\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tPackage: match.PackageParameter{\n\t\t\t\t\t\t\t\t\tName:    \"kernel\",\n\t\t\t\t\t\t\t\t\tVersion: \"5.14.0-427.79.1.el9_4\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tNamespace: \"redhat:distro:redhat:9\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tFound: match.DistroResult{\n\t\t\t\t\t\t\t\tVulnerabilityID:   \"CVE-2020-10135\",\n\t\t\t\t\t\t\t\tVersionConstraint: \"< 5.14.0-503.11.1.el9_5 (rpm)\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tMatcher:    match.RpmMatcher,\n\t\t\t\t\t\t\tConfidence: 1,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:       \"error fetching resolutions\",\n\t\t\tcatalogPkg: testPkg1,\n\t\t\tdisclosureVulns: []vulnerability.Vulnerability{\n\t\t\t\t{\n\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\tID:        \"CVE-2021-1\",\n\t\t\t\t\t\tNamespace: \"namespace\",\n\t\t\t\t\t},\n\t\t\t\t\tPackageName: \"test-pkg\", // direct match\n\t\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\t\tState:    vulnerability.FixStateUnknown,\n\t\t\t\t\t\tVersions: []string{},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tresolutionVulns: []vulnerability.Vulnerability{},\n\t\t\tresolutionError: errors.New(\"resolution error\"),\n\t\t\twant:            nil,\n\t\t\twantErr:         require.Error,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif tt.wantErr == nil {\n\t\t\t\ttt.wantErr = require.NoError\n\t\t\t}\n\n\t\t\tif tt.searchPkg == nil {\n\t\t\t\ttt.searchPkg = &tt.catalogPkg\n\t\t\t}\n\n\t\t\tvulnProvider := newMockVulnProvider()\n\t\t\tvulnProvider.setDisclosureVulns(tt.disclosureVulns)\n\t\t\tvulnProvider.setResolutionVulns(tt.resolutionVulns)\n\t\t\tvulnProvider.setDisclosureError(tt.disclosureError)\n\t\t\tvulnProvider.setResolutionError(tt.resolutionError)\n\n\t\t\tresultProvider := result.NewProvider(vulnProvider, tt.catalogPkg, match.RpmMatcher)\n\n\t\t\tgot, err := redhatEUSMatches(resultProvider, *tt.searchPkg, \"zero\")\n\t\t\ttt.wantErr(t, err)\n\n\t\t\tif err != nil {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// need stable results for comparison\n\t\t\tsort.Sort(match.ByElements(got))\n\n\t\t\topts := cmp.Options{\n\t\t\t\tcmpopts.IgnoreUnexported(version.Version{}),\n\t\t\t\tcmpopts.IgnoreUnexported(distro.Distro{}),\n\t\t\t\tcmpopts.IgnoreFields(vulnerability.Vulnerability{}, \"Constraint\"),\n\t\t\t\tcmpopts.IgnoreFields(pkg.Package{}, \"Locations\"),\n\t\t\t\tcmpopts.EquateEmpty(),\n\t\t\t}\n\t\t\tif diff := cmp.Diff(tt.want, got, opts...); diff != \"\" {\n\t\t\t\tt.Errorf(\"redhatEUSMatches() mismatch (-want +got):\\n%s\", diff)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc strRef(s string) *string {\n\treturn &s\n}\n\nfunc intRef(s int) *int {\n\treturn &s\n}\n\n// Mock vulnerability provider for testing\ntype mockVulnProvider struct {\n\t// cheaply get a working interface that will panic when functionality is not overridden\n\tvulnerability.Provider\n\n\tdisclosureVulns []vulnerability.Vulnerability\n\tresolutionVulns []vulnerability.Vulnerability\n\tdisclosureError error\n\tresolutionError error\n\tcallCount       int\n}\n\nfunc newMockVulnProvider() *mockVulnProvider {\n\treturn &mockVulnProvider{}\n}\n\nfunc (m *mockVulnProvider) setDisclosureVulns(vulns []vulnerability.Vulnerability) {\n\tm.disclosureVulns = vulns\n}\n\nfunc (m *mockVulnProvider) setResolutionVulns(vulns []vulnerability.Vulnerability) {\n\tm.resolutionVulns = vulns\n}\n\nfunc (m *mockVulnProvider) setDisclosureError(err error) {\n\tm.disclosureError = err\n}\n\nfunc (m *mockVulnProvider) setResolutionError(err error) {\n\tm.resolutionError = err\n}\n\nfunc (m *mockVulnProvider) FindVulnerabilities(criteria ...vulnerability.Criteria) ([]vulnerability.Vulnerability, error) {\n\tm.callCount++\n\n\t// heuristic: first call is for disclosures (base distro), second is for resolutions (base + eus distro)\n\tif m.callCount == 1 {\n\t\tif m.disclosureError != nil {\n\t\t\treturn nil, m.disclosureError\n\t\t}\n\t\treturn m.disclosureVulns, nil\n\t}\n\n\tif m.resolutionError != nil {\n\t\treturn nil, m.resolutionError\n\t}\n\treturn m.resolutionVulns, nil\n}\n\nfunc channels(s ...string) []string {\n\treturn s\n}\n\n// newEUSDistro creates a properly initialized RHEL EUS distro using distro.New().\n// This ensures MajorVersion()/MinorVersion() work correctly.\n// Pass version like \"9.4\" (the \"+eus\" channel suffix is added automatically).\n// Pass empty string for a distro without a version.\nfunc newEUSDistro(version string) *distro.Distro {\n\tif version == \"\" {\n\t\t// For empty version, we need to set channels manually since \"+eus\" alone\n\t\t// doesn't parse well\n\t\td := distro.New(distro.RedHat, \"\", \"\")\n\t\td.Channels = []string{\"eus\"}\n\t\treturn d\n\t}\n\treturn distro.New(distro.RedHat, version+\"+eus\", \"\")\n}\n"
  },
  {
    "path": "grype/matcher/ruby/matcher.go",
    "content": "package ruby\n\nimport (\n\t\"github.com/anchore/grype/grype/match\"\n\t\"github.com/anchore/grype/grype/matcher/internal\"\n\t\"github.com/anchore/grype/grype/pkg\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n\tsyftPkg \"github.com/anchore/syft/syft/pkg\"\n)\n\ntype Matcher struct {\n\tcfg MatcherConfig\n}\n\ntype MatcherConfig struct {\n\tUseCPEs bool\n}\n\nfunc NewRubyMatcher(cfg MatcherConfig) *Matcher {\n\treturn &Matcher{\n\t\tcfg: cfg,\n\t}\n}\n\nfunc (m *Matcher) PackageTypes() []syftPkg.Type {\n\treturn []syftPkg.Type{syftPkg.GemPkg}\n}\n\nfunc (m *Matcher) Type() match.MatcherType {\n\treturn match.RubyGemMatcher\n}\n\nfunc (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoreFilter, error) {\n\treturn internal.MatchPackageByEcosystemAndCPEs(store, p, m.Type(), m.cfg.UseCPEs)\n}\n"
  },
  {
    "path": "grype/matcher/rust/matcher.go",
    "content": "package rust\n\nimport (\n\t\"github.com/anchore/grype/grype/match\"\n\t\"github.com/anchore/grype/grype/matcher/internal\"\n\t\"github.com/anchore/grype/grype/pkg\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n\tsyftPkg \"github.com/anchore/syft/syft/pkg\"\n)\n\ntype Matcher struct {\n\tcfg MatcherConfig\n}\n\ntype MatcherConfig struct {\n\tUseCPEs bool\n}\n\nfunc NewRustMatcher(cfg MatcherConfig) *Matcher {\n\treturn &Matcher{\n\t\tcfg: cfg,\n\t}\n}\n\nfunc (m *Matcher) PackageTypes() []syftPkg.Type {\n\treturn []syftPkg.Type{syftPkg.RustPkg}\n}\n\nfunc (m *Matcher) Type() match.MatcherType {\n\treturn match.RustMatcher\n}\n\nfunc (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoreFilter, error) {\n\treturn internal.MatchPackageByEcosystemAndCPEs(store, p, m.Type(), m.cfg.UseCPEs)\n}\n"
  },
  {
    "path": "grype/matcher/stock/matcher.go",
    "content": "package stock\n\nimport (\n\t\"github.com/anchore/grype/grype/match\"\n\t\"github.com/anchore/grype/grype/matcher/internal\"\n\t\"github.com/anchore/grype/grype/pkg\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n\tsyftPkg \"github.com/anchore/syft/syft/pkg\"\n)\n\ntype Matcher struct {\n\tcfg MatcherConfig\n}\n\ntype MatcherConfig struct {\n\tUseCPEs bool\n}\n\nfunc NewStockMatcher(cfg MatcherConfig) match.Matcher {\n\treturn &Matcher{\n\t\tcfg: cfg,\n\t}\n}\n\nfunc (m *Matcher) PackageTypes() []syftPkg.Type {\n\treturn nil\n}\n\nfunc (m *Matcher) Type() match.MatcherType {\n\treturn match.StockMatcher\n}\n\nfunc (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Match, []match.IgnoreFilter, error) {\n\treturn internal.MatchPackageByEcosystemAndCPEs(store, p, m.Type(), m.cfg.UseCPEs)\n}\n"
  },
  {
    "path": "grype/matcher/stock/matcher_test.go",
    "content": "package stock\n\nimport (\n\t\"testing\"\n\n\t\"github.com/google/uuid\"\n\t\"github.com/scylladb/go-set/strset\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/grype/grype/match\"\n\t\"github.com/anchore/grype/grype/pkg\"\n\t\"github.com/anchore/grype/grype/version\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n\t\"github.com/anchore/grype/grype/vulnerability/mock\"\n\t\"github.com/anchore/syft/syft/cpe\"\n\tsyftPkg \"github.com/anchore/syft/syft/pkg\"\n)\n\nfunc TestMatcher_JVMPackage(t *testing.T) {\n\tp := pkg.Package{\n\t\tID:      pkg.ID(uuid.NewString()),\n\t\tName:    \"java_se\",\n\t\tVersion: \"1.8.0_400\",\n\t\tType:    syftPkg.BinaryPkg,\n\t\tCPEs: []cpe.CPE{\n\t\t\tcpe.Must(\"cpe:2.3:a:oracle:java_se:1.8.0:update400:*:*:*:*:*:*\", cpe.DeclaredSource),\n\t\t},\n\t}\n\tmatcher := Matcher{\n\t\tcfg: MatcherConfig{\n\t\t\tUseCPEs: true,\n\t\t},\n\t}\n\tstore := newMockProvider()\n\tactual, _, err := matcher.Match(store, p)\n\trequire.NoError(t, err)\n\n\tfoundCVEs := strset.New()\n\tfor _, v := range actual {\n\t\tfoundCVEs.Add(v.Vulnerability.ID)\n\n\t\trequire.NotEmpty(t, v.Details)\n\t\tfor _, d := range v.Details {\n\t\t\tassert.Equal(t, match.CPEMatch, d.Type, \"indirect match not indicated\")\n\t\t\tassert.Equal(t, matcher.Type(), d.Matcher, \"failed to capture matcher type\")\n\t\t}\n\t\tassert.Equal(t, p.Name, v.Package.Name, \"failed to capture original package name\")\n\t}\n\n\texpected := strset.New(\n\t\t\"CVE-2024-20919-real\",\n\t\t\"CVE-2024-20919-bonkers-format\",\n\t\t\"CVE-2024-20919-post-jep223\",\n\t)\n\n\tfor _, id := range expected.List() {\n\t\tif !foundCVEs.Has(id) {\n\t\t\tt.Errorf(\"missing CVE: %s\", id)\n\t\t}\n\t}\n\n\textra := strset.Difference(foundCVEs, expected)\n\n\tfor _, id := range extra.List() {\n\t\tt.Errorf(\"unexpected CVE: %s\", id)\n\t}\n\n\tif t.Failed() {\n\t\tt.Logf(\"discovered CVES: %d\", foundCVEs.Size())\n\t\tfor _, id := range foundCVEs.List() {\n\t\t\tt.Logf(\" - %s\", id)\n\t\t}\n\t}\n}\n\nfunc newMockProvider() vulnerability.Provider {\n\t// derived from vuln data found on CVE-2024-20919\n\thit := \"< 1.8.0_401 || >= 1.9-ea, < 8.0.401 || >= 9-ea, < 11.0.22 || >= 12-ea, < 17.0.10 || >= 18-ea, < 21.0.2\"\n\n\tcpes := []cpe.CPE{cpe.Must(\"cpe:2.3:a:oracle:java_se:*:*:*:*:*:*:*:*\", \"\")}\n\n\treturn mock.VulnerabilityProvider([]vulnerability.Vulnerability{\n\t\t{\n\t\t\t// positive cases\n\t\t\tPackageName: \"java_se\",\n\t\t\tConstraint:  version.MustGetConstraint(hit, version.JVMFormat),\n\t\t\tReference:   vulnerability.Reference{ID: \"CVE-2024-20919-real\", Namespace: \"nvd:cpe\"},\n\t\t\tCPEs:        cpes,\n\t\t},\n\t\t{\n\t\t\t// positive cases\n\t\t\tPackageName: \"java_se\",\n\t\t\tConstraint:  version.MustGetConstraint(\"< 22.22.22\", version.UnknownFormat),\n\t\t\tReference:   vulnerability.Reference{ID: \"CVE-2024-20919-bonkers-format\", Namespace: \"nvd:cpe\"},\n\t\t\tCPEs:        cpes,\n\t\t},\n\t\t{\n\t\t\t// negative case\n\t\t\tPackageName: \"java_se\",\n\t\t\tConstraint:  version.MustGetConstraint(\"< 1.8.0_399 || >= 1.9-ea, < 8.0.399 || >= 9-ea\", version.JVMFormat),\n\t\t\tReference:   vulnerability.Reference{ID: \"CVE-FAKE-bad-update\", Namespace: \"nvd:cpe\"},\n\t\t\tCPEs:        cpes,\n\t\t},\n\t\t{\n\t\t\t// positive case\n\t\t\tPackageName: \"java_se\",\n\t\t\tConstraint:  version.MustGetConstraint(\"< 8.0.401\", version.JVMFormat),\n\t\t\tReference:   vulnerability.Reference{ID: \"CVE-2024-20919-post-jep223\", Namespace: \"nvd:cpe\"},\n\t\t\tCPEs:        cpes,\n\t\t},\n\t\t{\n\t\t\t// negative case\n\t\t\tPackageName: \"java_se\",\n\t\t\tConstraint:  version.MustGetConstraint(\"< 8.0.399\", version.JVMFormat),\n\t\t\tReference:   vulnerability.Reference{ID: \"CVE-FAKE-bad-range-post-jep223\", Namespace: \"nvd:cpe\"},\n\t\t\tCPEs:        cpes,\n\t\t},\n\t\t{\n\t\t\t// negative case\n\t\t\tPackageName: \"java_se\",\n\t\t\tConstraint:  version.MustGetConstraint(\"< 7.0.0\", version.JVMFormat),\n\t\t\tReference:   vulnerability.Reference{ID: \"CVE-FAKE-bad-range-post-jep223\", Namespace: \"nvd:cpe\"},\n\t\t\tCPEs:        cpes,\n\t\t},\n\t}...)\n}\n"
  },
  {
    "path": "grype/pkg/apk_metadata.go",
    "content": "package pkg\n\nimport (\n\t\"sort\"\n\n\t\"github.com/scylladb/go-set/strset\"\n)\n\nvar _ FileOwner = (*ApkMetadata)(nil)\n\ntype ApkMetadata struct {\n\tFiles []ApkFileRecord `json:\"files\"`\n}\n\n// ApkFileRecord represents a single file listing and metadata from a APK DB entry (which may have many of these file records).\ntype ApkFileRecord struct {\n\tPath string `json:\"path\"`\n}\n\nfunc (m ApkMetadata) OwnedFiles() []string {\n\ts := strset.New()\n\tfor _, f := range m.Files {\n\t\tif f.Path != \"\" {\n\t\t\ts.Add(f.Path)\n\t\t}\n\t}\n\tresult := s.List()\n\tsort.Strings(result)\n\treturn result\n}\n"
  },
  {
    "path": "grype/pkg/context.go",
    "content": "package pkg\n\nimport (\n\t\"github.com/anchore/grype/grype/distro\"\n\t\"github.com/anchore/syft/syft/source\"\n)\n\ntype Context struct {\n\tSource *source.Description\n\tDistro *distro.Distro\n\t// DistroDetectionFailed is true when linux release info was present but\n\t// the distro type could not be determined (e.g., unknown distro ID)\n\tDistroDetectionFailed bool\n}\n"
  },
  {
    "path": "grype/pkg/context_test.go",
    "content": "package pkg\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\n\t\"github.com/anchore/grype/grype/distro\"\n)\n\nfunc TestContext_DistroDetectionFailed(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tctx      Context\n\t\texpected bool\n\t}{\n\t\t{\n\t\t\tname:     \"detection failed is false by default\",\n\t\t\tctx:      Context{},\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname: \"detection failed is true when set\",\n\t\t\tctx: Context{\n\t\t\t\tDistroDetectionFailed: true,\n\t\t\t},\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname: \"detection failed with distro present\",\n\t\t\tctx: Context{\n\t\t\t\tDistro:                distro.New(distro.Ubuntu, \"22.04\", \"jammy\"),\n\t\t\t\tDistroDetectionFailed: false,\n\t\t\t},\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname: \"detection failed with nil distro\",\n\t\t\tctx: Context{\n\t\t\t\tDistro:                nil,\n\t\t\t\tDistroDetectionFailed: true,\n\t\t\t},\n\t\t\texpected: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tassert.Equal(t, tt.expected, tt.ctx.DistroDetectionFailed)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/pkg/cpe_provider.go",
    "content": "package pkg\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"strings\"\n\n\t\"github.com/anchore/syft/syft/format\"\n\t\"github.com/anchore/syft/syft/sbom\"\n\t\"github.com/anchore/syft/syft/source\"\n)\n\nconst cpeInputPrefix = \"cpe:\"\nconst cpeListPrefix = \"cpes:\"\n\ntype CPELiteralMetadata struct {\n\tCPE string\n}\n\nfunc cpeProvider(userInput string, config ProviderConfig) ([]Package, Context, *sbom.SBOM, error) {\n\treader, ctx, err := getCPEReader(userInput)\n\tif err != nil {\n\t\treturn nil, Context{}, nil, err\n\t}\n\n\ts, _, _, err := format.Decode(reader)\n\tif s == nil {\n\t\treturn nil, Context{}, nil, fmt.Errorf(\"unable to decode cpe: %w\", err)\n\t}\n\n\treturn FromCollection(s.Artifacts.Packages, config.SynthesisConfig), ctx, s, nil\n}\n\nfunc getCPEReader(userInput string) (r io.Reader, ctx Context, err error) {\n\tif strings.HasPrefix(userInput, cpeInputPrefix) {\n\t\tctx.Source = &source.Description{\n\t\t\tMetadata: CPELiteralMetadata{\n\t\t\t\tCPE: userInput,\n\t\t\t},\n\t\t}\n\t\treturn strings.NewReader(userInput), ctx, nil\n\t}\n\treturn nil, ctx, errDoesNotProvide\n}\n"
  },
  {
    "path": "grype/pkg/cpe_provider_test.go",
    "content": "package pkg\n\nimport (\n\t\"testing\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/google/go-cmp/cmp/cmpopts\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/syft/syft/cpe\"\n\t\"github.com/anchore/syft/syft/file\"\n\t\"github.com/anchore/syft/syft/pkg\"\n\t\"github.com/anchore/syft/syft/sbom\"\n\t\"github.com/anchore/syft/syft/source\"\n)\n\nfunc Test_CPEProvider(t *testing.T) {\n\ttests := []struct {\n\t\tname      string\n\t\tuserInput string\n\t\tcontext   Context\n\t\tpkgs      []Package\n\t\tsbom      *sbom.SBOM\n\t\twantErr   require.ErrorAssertionFunc\n\t}{\n\t\t{\n\t\t\tname:      \"takes a single cpe\",\n\t\t\tuserInput: \"cpe:/a:apache:log4j:2.14.1\",\n\t\t\tcontext: Context{\n\t\t\t\tSource: &source.Description{\n\t\t\t\t\tMetadata: CPELiteralMetadata{\n\t\t\t\t\t\tCPE: \"cpe:/a:apache:log4j:2.14.1\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tpkgs: []Package{\n\t\t\t\t{\n\t\t\t\t\tName:    \"log4j\",\n\t\t\t\t\tVersion: \"2.14.1\",\n\t\t\t\t\tCPEs: []cpe.CPE{\n\t\t\t\t\t\tcpe.Must(\"cpe:/a:apache:log4j:2.14.1\", \"\"),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tsbom: &sbom.SBOM{\n\t\t\t\tArtifacts: sbom.Artifacts{\n\t\t\t\t\tPackages: pkg.NewCollection(pkg.Package{\n\t\t\t\t\t\tName:    \"log4j\",\n\t\t\t\t\t\tVersion: \"2.14.1\",\n\t\t\t\t\t\tCPEs: []cpe.CPE{\n\t\t\t\t\t\t\tcpe.Must(\"cpe:/a:apache:log4j:2.14.1\", \"\"),\n\t\t\t\t\t\t},\n\t\t\t\t\t}),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:      \"takes cpe with no version\",\n\t\t\tuserInput: \"cpe:/a:apache:log4j\",\n\t\t\tcontext: Context{\n\t\t\t\tSource: &source.Description{\n\t\t\t\t\tMetadata: CPELiteralMetadata{\n\t\t\t\t\t\tCPE: \"cpe:/a:apache:log4j\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tpkgs: []Package{\n\t\t\t\t{\n\t\t\t\t\tName: \"log4j\",\n\t\t\t\t\tCPEs: []cpe.CPE{\n\t\t\t\t\t\tcpe.Must(\"cpe:/a:apache:log4j\", \"\"),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tsbom: &sbom.SBOM{\n\t\t\t\tArtifacts: sbom.Artifacts{\n\t\t\t\t\tPackages: pkg.NewCollection(pkg.Package{\n\t\t\t\t\t\tName: \"log4j\",\n\t\t\t\t\t\tCPEs: []cpe.CPE{\n\t\t\t\t\t\t\tcpe.Must(\"cpe:/a:apache:log4j\", \"\"),\n\t\t\t\t\t\t},\n\t\t\t\t\t}),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:      \"takes CPE 2.3 format\",\n\t\t\tuserInput: \"cpe:2.3:a:apache:log4j:2.14.1:*:*:*:*:*:*:*\",\n\t\t\tcontext: Context{\n\t\t\t\tSource: &source.Description{\n\t\t\t\t\tMetadata: CPELiteralMetadata{\n\t\t\t\t\t\tCPE: \"cpe:2.3:a:apache:log4j:2.14.1:*:*:*:*:*:*:*\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tpkgs: []Package{\n\t\t\t\t{\n\t\t\t\t\tName:    \"log4j\",\n\t\t\t\t\tVersion: \"2.14.1\",\n\t\t\t\t\tCPEs: []cpe.CPE{\n\t\t\t\t\t\tcpe.Must(\"cpe:2.3:a:apache:log4j:2.14.1:*:*:*:*:*:*:*\", \"\"),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tsbom: &sbom.SBOM{\n\t\t\t\tArtifacts: sbom.Artifacts{\n\t\t\t\t\tPackages: pkg.NewCollection(pkg.Package{\n\t\t\t\t\t\tName:    \"log4j\",\n\t\t\t\t\t\tVersion: \"2.14.1\",\n\t\t\t\t\t\tCPEs: []cpe.CPE{\n\t\t\t\t\t\t\tcpe.Must(\"cpe:2.3:a:apache:log4j:2.14.1:*:*:*:*:*:*:*\", \"\"),\n\t\t\t\t\t\t},\n\t\t\t\t\t}),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"takes multiple CPEs\",\n\t\t\tuserInput: `cpe:/a:apache:log4j:2.14.1\n\t\t\t\t\t\tcpe:2.3:a:f5:nginx:*:*:*:*:*:*:*:*`,\n\t\t\tcontext: Context{\n\t\t\t\tSource: &source.Description{\n\t\t\t\t\tMetadata: CPELiteralMetadata{\n\t\t\t\t\t\tCPE: `cpe:/a:apache:log4j:2.14.1\n\t\t\t\t\t\tcpe:2.3:a:f5:nginx:*:*:*:*:*:*:*:*`,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tpkgs: []Package{\n\t\t\t\t{\n\t\t\t\t\tName:    \"log4j\",\n\t\t\t\t\tVersion: \"2.14.1\",\n\t\t\t\t\tCPEs: []cpe.CPE{\n\t\t\t\t\t\tcpe.Must(\"cpe:2.3:a:apache:log4j:2.14.1:*:*:*:*:*:*:*\", \"\"),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tName:    \"nginx\",\n\t\t\t\t\tVersion: \"\",\n\t\t\t\t\tCPEs: []cpe.CPE{\n\t\t\t\t\t\tcpe.Must(\"cpe:2.3:a:f5:nginx:*:*:*:*:*:*:*:*\", \"\"),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tsbom: &sbom.SBOM{\n\t\t\t\tArtifacts: sbom.Artifacts{\n\t\t\t\t\tPackages: pkg.NewCollection(pkg.Package{\n\t\t\t\t\t\tName:    \"log4j\",\n\t\t\t\t\t\tVersion: \"2.14.1\",\n\t\t\t\t\t\tCPEs: []cpe.CPE{\n\t\t\t\t\t\t\tcpe.Must(\"cpe:2.3:a:apache:log4j:2.14.1:*:*:*:*:*:*:*\", \"\"),\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\t\tpkg.Package{\n\t\t\t\t\t\t\tName:    \"nginx\",\n\t\t\t\t\t\t\tVersion: \"\",\n\t\t\t\t\t\t\tCPEs: []cpe.CPE{\n\t\t\t\t\t\t\t\tcpe.Must(\"cpe:2.3:a:f5:nginx:*:*:*:*:*:*:*:*\", \"\"),\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:      \"invalid prefix\",\n\t\t\tuserInput: \"dir:testdata/cpe\",\n\t\t\twantErr:   require.Error,\n\t\t},\n\t}\n\n\topts := []cmp.Option{\n\t\tcmpopts.IgnoreFields(Package{}, \"ID\", \"Locations\", \"Licenses\", \"Metadata\", \"Type\", \"Language\"),\n\t}\n\n\tsyftPkgOpts := []cmp.Option{\n\t\tcmpopts.IgnoreFields(pkg.Package{}, \"id\", \"Type\", \"Language\"),\n\t\tcmpopts.IgnoreUnexported(pkg.Package{}, file.LocationSet{}, pkg.LicenseSet{}),\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tif tc.wantErr == nil {\n\t\t\t\ttc.wantErr = require.NoError\n\t\t\t}\n\n\t\t\tpackages, ctx, gotSBOM, err := cpeProvider(tc.userInput, ProviderConfig{})\n\n\t\t\ttc.wantErr(t, err)\n\t\t\tif err != nil {\n\t\t\t\trequire.Nil(t, packages)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif d := cmp.Diff(tc.context, ctx, opts...); d != \"\" {\n\t\t\t\tt.Errorf(\"unexpected context (-want +got):\\n%s\", d)\n\t\t\t}\n\n\t\t\trequire.Len(t, packages, len(tc.pkgs))\n\t\t\tfor idx, expected := range tc.pkgs {\n\t\t\t\tif d := cmp.Diff(expected, packages[idx], opts...); d != \"\" {\n\t\t\t\t\tt.Errorf(\"unexpected package (-want +got):\\n%s\", d)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tgotSyftPkgs := gotSBOM.Artifacts.Packages.Sorted()\n\t\t\twantSyftPkgs := tc.sbom.Artifacts.Packages.Sorted()\n\t\t\trequire.Equal(t, len(gotSyftPkgs), len(wantSyftPkgs))\n\t\t\tfor idx, wantPkg := range wantSyftPkgs {\n\t\t\t\tif d := cmp.Diff(wantPkg, gotSyftPkgs[idx], syftPkgOpts...); d != \"\" {\n\t\t\t\t\tt.Errorf(\"unexpected Syft Package (-want +got):\\n%s\", d)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/pkg/file_owner.go",
    "content": "package pkg\n\ntype FileOwner interface {\n\tOwnedFiles() []string\n}\n"
  },
  {
    "path": "grype/pkg/golang_metadata.go",
    "content": "package pkg\n\nimport \"github.com/anchore/syft/syft/pkg\"\n\ntype GolangBinMetadata struct {\n\tBuildSettings     pkg.KeyValues `json:\"goBuildSettings,omitempty\" cyclonedx:\"goBuildSettings\"`\n\tGoCompiledVersion string        `json:\"goCompiledVersion\" cyclonedx:\"goCompiledVersion\"`\n\tArchitecture      string        `json:\"architecture\" cyclonedx:\"architecture\"`\n\tH1Digest          string        `json:\"h1Digest,omitempty\" cyclonedx:\"h1Digest\"`\n\tMainModule        string        `json:\"mainModule,omitempty\" cyclonedx:\"mainModule\"`\n\tGoCryptoSettings  []string      `json:\"goCryptoSettings,omitempty\" cyclonedx:\"goCryptoSettings\"`\n}\n\ntype GolangModMetadata struct {\n\tH1Digest string `json:\"h1Digest,omitempty\"`\n}\n\ntype GolangSourceMetadata struct {\n\tH1Digest        string `json:\"h1Digest,omitempty\"`\n\tOperatingSystem string `json:\"os,omitempty\"`\n\tArchitecture    string `json:\"architecture,omitempty\"`\n\tBuildTags       string `json:\"buildTags,omitempty\"`\n\tCgoEnabled      bool   `json:\"cgoEnabled\"`\n}\n"
  },
  {
    "path": "grype/pkg/java_metadata.go",
    "content": "package pkg\n\nimport (\n\t\"github.com/scylladb/go-set/strset\"\n\n\tsyftPkg \"github.com/anchore/syft/syft/pkg\"\n)\n\ntype JavaMetadata struct {\n\tVirtualPath    string   `json:\"virtualPath\"`\n\tPomArtifactID  string   `json:\"pomArtifactID\"`\n\tPomGroupID     string   `json:\"pomGroupID\"`\n\tManifestName   string   `json:\"manifestName\"`\n\tArchiveDigests []Digest `json:\"archiveDigests\"`\n}\n\ntype Digest struct {\n\tAlgorithm string `json:\"algorithm\"`\n\tValue     string `json:\"value\"`\n}\n\ntype JavaVMInstallationMetadata struct {\n\tRelease JavaVMReleaseMetadata `json:\"release,omitempty\"`\n}\n\ntype JavaVMReleaseMetadata struct {\n\tJavaRuntimeVersion string `json:\"javaRuntimeVersion,omitempty\"`\n\tJavaVersion        string `json:\"javaVersion,omitempty\"`\n\tFullVersion        string `json:\"fullVersion,omitempty\"`\n\tSemanticVersion    string `json:\"semanticVersion,omitempty\"`\n}\n\nfunc isJvmPackage(p Package) bool {\n\tif _, ok := p.Metadata.(JavaVMInstallationMetadata); ok {\n\t\treturn true\n\t}\n\n\tif p.Type == syftPkg.BinaryPkg {\n\t\tif HasJvmPackageName(p.Name) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\nvar jvmIndications = strset.New(\"java_se\", \"jre\", \"jdk\", \"zulu\", \"openjdk\", \"java\", \"java/jre\", \"java/jdk\")\n\nfunc HasJvmPackageName(name string) bool {\n\treturn jvmIndications.Has(name)\n}\n"
  },
  {
    "path": "grype/pkg/java_metadata_test.go",
    "content": "package pkg\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\n\tsyftPkg \"github.com/anchore/syft/syft/pkg\"\n)\n\nfunc TestIsJvmPackage(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tpkg      Package\n\t\texpected bool\n\t}{\n\t\t{\n\t\t\tname: \"binary package with jdk in name set\",\n\t\t\tpkg: Package{\n\t\t\t\tType: syftPkg.BinaryPkg,\n\t\t\t\tName: \"jdk\",\n\t\t\t},\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname: \"binary package with jre in name set\",\n\t\t\tpkg: Package{\n\t\t\t\tType: syftPkg.BinaryPkg,\n\t\t\t\tName: \"jre\",\n\t\t\t},\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname: \"binary package with java_se in name set\",\n\t\t\tpkg: Package{\n\t\t\t\tType: syftPkg.BinaryPkg,\n\t\t\t\tName: \"java_se\",\n\t\t\t},\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname: \"binary package with zulu in name set\",\n\t\t\tpkg: Package{\n\t\t\t\tType: syftPkg.BinaryPkg,\n\t\t\t\tName: \"zulu\",\n\t\t\t},\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname: \"binary package with openjdk in name set\",\n\t\t\tpkg: Package{\n\t\t\t\tType: syftPkg.BinaryPkg,\n\t\t\t\tName: \"openjdk\",\n\t\t\t},\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname: \"binary package from syft (java/jdk\",\n\t\t\tpkg: Package{\n\t\t\t\tType: syftPkg.BinaryPkg,\n\t\t\t\tName: \"java/jre\",\n\t\t\t},\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname: \"binary package from syft (java/jre)\",\n\t\t\tpkg: Package{\n\t\t\t\tType: syftPkg.BinaryPkg,\n\t\t\t\tName: \"java/jdk\",\n\t\t\t},\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname: \"binary package without jvm-related name\",\n\t\t\tpkg: Package{\n\t\t\t\tType: syftPkg.BinaryPkg,\n\t\t\t\tName: \"nodejs\",\n\t\t\t},\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname: \"non-binary package with jvm-related name\",\n\t\t\tpkg: Package{\n\t\t\t\tType: syftPkg.NpmPkg, // we know this could not be a JVM package installation\n\t\t\t\tName: \"jdk\",\n\t\t\t},\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname: \"package with JavaVMInstallationMetadata\",\n\t\t\tpkg: Package{\n\t\t\t\tType:     syftPkg.RpmPkg,\n\t\t\t\tName:     \"random-package\",\n\t\t\t\tMetadata: JavaVMInstallationMetadata{},\n\t\t\t},\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname: \"package without JavaVMInstallationMetadata\",\n\t\t\tpkg: Package{\n\t\t\t\tType:     syftPkg.RpmPkg,\n\t\t\t\tName:     \"non-jvm-package\",\n\t\t\t\tMetadata: nil,\n\t\t\t},\n\t\t\texpected: false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := isJvmPackage(tt.pkg)\n\t\t\tassert.Equal(t, tt.expected, result)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/pkg/package.go",
    "content": "package pkg\n\nimport (\n\t\"fmt\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/anchore/grype/grype/distro\"\n\t\"github.com/anchore/grype/internal/log\"\n\t\"github.com/anchore/grype/internal/stringutil\"\n\t\"github.com/anchore/packageurl-go\"\n\t\"github.com/anchore/syft/syft/artifact\"\n\t\"github.com/anchore/syft/syft/cpe\"\n\t\"github.com/anchore/syft/syft/file\"\n\tsyftPkg \"github.com/anchore/syft/syft/pkg\"\n\tcpes \"github.com/anchore/syft/syft/pkg/cataloger/common/cpe\"\n)\n\n// the source-rpm field has something akin to \"util-linux-ng-2.17.2-12.28.el6_9.2.src.rpm\"\n// in which case the pattern will extract out the following values for the named capture groups:\n//\n//\tname = \"util-linux-ng\"\n//\tversion = \"2.17.2\" (or, if there's an epoch, we'd expect a value like \"4:2.17.2\")\n//\trelease = \"12.28.el6_9.2\"\n//\tarch = \"src\"\nvar rpmPackageNamePattern = regexp.MustCompile(`^(?P<name>.*)-(?P<version>.*)-(?P<release>.*)\\.(?P<arch>[a-zA-Z][^.]+)(\\.rpm)$`)\n\n// ID represents a unique value for each package added to a package collection.\ntype ID string\n\n// Package represents an application or library that has been bundled into a distributable format.\ntype Package struct {\n\tID        ID\n\tName      string           // the package name\n\tVersion   string           // the version of the package\n\tLocations file.LocationSet // the locations that lead to the discovery of this package (note: this is not necessarily the locations that make up this package)\n\tLanguage  syftPkg.Language // the language ecosystem this package belongs to (e.g. JavaScript, Python, etc)\n\tDistro    *distro.Distro   // a specific distro this package originated from\n\tLicenses  []string\n\tType      syftPkg.Type // the package type (e.g. Npm, Yarn, Python, Rpm, Deb, etc)\n\tCPEs      []cpe.CPE    // all possible Common Platform Enumerators\n\tPURL      string       // the Package URL (see https://github.com/package-url/purl-spec)\n\tUpstreams []UpstreamPackage\n\tMetadata  interface{} // This is NOT 1-for-1 the syft metadata! Only the select data needed for vulnerability matching\n}\n\ntype Enhancer func(out *Package, purl packageurl.PackageURL, pkg syftPkg.Package)\n\nfunc New(p syftPkg.Package, enhancers ...Enhancer) Package {\n\tmetadata, upstreams := dataFromPkg(p)\n\n\tlicenseObjs := p.Licenses.ToSlice()\n\t// note: this is used for presentation downstream and is a collection, thus should always be allocated\n\tlicenses := make([]string, 0, len(licenseObjs))\n\tfor _, l := range licenseObjs {\n\t\tlicenses = append(licenses, l.Value)\n\t}\n\tif licenses == nil {\n\t\tlicenses = []string{}\n\t}\n\n\tout := Package{\n\t\tID:        ID(p.ID()),\n\t\tName:      p.Name,\n\t\tVersion:   p.Version,\n\t\tLocations: p.Locations,\n\t\tLicenses:  licenses,\n\t\tLanguage:  p.Language,\n\t\tType:      p.Type,\n\t\tCPEs:      p.CPEs,\n\t\tPURL:      p.PURL,\n\t\tUpstreams: upstreams,\n\t\tMetadata:  metadata,\n\t}\n\n\tif len(enhancers) > 0 {\n\t\tpurl, err := packageurl.FromString(p.PURL)\n\t\tif err != nil {\n\t\t\tlog.WithFields(\"purl\", purl, \"error\", err).Debug(\"unable to parse PURL\")\n\t\t}\n\t\tfor _, e := range enhancers {\n\t\t\te(&out, purl, p)\n\t\t}\n\t}\n\n\treturn out\n}\n\nfunc FromCollection(catalog *syftPkg.Collection, config SynthesisConfig, enhancers ...Enhancer) []Package {\n\treturn FromPackages(catalog.Sorted(), config, enhancers...)\n}\n\nfunc FromPackages(syftPkgs []syftPkg.Package, config SynthesisConfig, enhancers ...Enhancer) []Package {\n\tvar pkgs []Package\n\n\t// if the user provided a distro explicitly, then use that over any distro that may be inferred from a package url\n\tenhancers = append([]Enhancer{applyDistroOverride(config.Distro.Override)}, enhancers...)\n\n\tfor _, p := range syftPkgs {\n\t\tif len(p.CPEs) == 0 {\n\t\t\t// for SPDX (or any format, really) we may have no CPEs\n\t\t\tif config.GenerateMissingCPEs {\n\t\t\t\tp.CPEs = cpes.Generate(p)\n\t\t\t} else {\n\t\t\t\tlog.Debugf(\"no CPEs for package: %s\", p)\n\t\t\t}\n\t\t}\n\n\t\tpkgs = append(pkgs, New(p, enhancers...))\n\t}\n\n\treturn pkgs\n}\n\nfunc (p Package) String() string {\n\tvar d string\n\tif p.Distro != nil {\n\t\td = fmt.Sprintf(\", distro=%s\", p.Distro.String())\n\t}\n\tvar u string\n\tif len(p.Upstreams) > 0 {\n\t\tu = fmt.Sprintf(\", upstreams=%d\", len(p.Upstreams))\n\t}\n\treturn fmt.Sprintf(\"Pkg(type=%s, name=%s, version=%s%s%s)\", p.Type, p.Name, p.Version, u, d)\n}\n\nfunc removePackagesByOverlap(catalog *syftPkg.Collection, relationships []artifact.Relationship, distro *distro.Distro) *syftPkg.Collection {\n\tbyOverlap := map[artifact.ID]artifact.Relationship{}\n\tfor _, r := range relationships {\n\t\tif r.Type == artifact.OwnershipByFileOverlapRelationship {\n\t\t\tbyOverlap[r.To.ID()] = r\n\t\t}\n\t}\n\n\tout := syftPkg.NewCollection()\n\tcomprehensiveDistroFeed := distroFeedIsComprehensive(distro)\n\tfor p := range catalog.Enumerate() {\n\t\tr, ok := byOverlap[p.ID()]\n\t\tif ok {\n\t\t\tfrom := catalog.Package(r.From.ID())\n\t\t\tif from != nil && excludePackage(comprehensiveDistroFeed, p, *from) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\t\tout.Add(p)\n\t}\n\n\treturn out\n}\n\nfunc excludePackage(comprehensiveDistroFeed bool, p syftPkg.Package, parent syftPkg.Package) bool {\n\t// NOTE: we are not checking the name because we have mismatches like:\n\t// python      3.9.2      binary\n\t// python3.9   3.9.2-1    deb\n\n\t// If the version is not approximately the same, keep both\n\tif !strings.HasPrefix(parent.Version, p.Version) && !strings.HasPrefix(p.Version, parent.Version) {\n\t\treturn false\n\t}\n\n\t// If the parent is an OS package and the child is not, exclude the child\n\t// for distros that have a comprehensive feed. That is, distros that list\n\t// vulnerabilities that aren't fixed. Otherwise, the child package might\n\t// be needed for matching.\n\tif comprehensiveDistroFeed && isOSPackage(parent) && !isOSPackage(p) {\n\t\treturn true\n\t}\n\n\t// filter out binary packages, even for non-comprehensive distros\n\tif p.Type != syftPkg.BinaryPkg {\n\t\treturn false\n\t}\n\n\treturn true\n}\n\n// distroFeedIsComprehensive returns true if the distro feed\n// is comprehensive enough that we can drop packages owned by distro packages\n// before matching.\nfunc distroFeedIsComprehensive(dst *distro.Distro) bool {\n\t// TODO: this mechanism should be re-examined once https://github.com/anchore/grype/issues/1426\n\t// is addressed\n\tif dst == nil {\n\t\treturn false\n\t}\n\tif dst.Type == distro.AmazonLinux {\n\t\t// AmazonLinux shows \"like rhel\" but is not an rhel clone\n\t\t// and does not have an exhaustive vulnerability feed.\n\t\treturn false\n\t}\n\tfor _, d := range comprehensiveDistros {\n\t\tif strings.EqualFold(string(d), dst.Name()) {\n\t\t\treturn true\n\t\t}\n\t\tfor _, n := range dst.IDLike {\n\t\t\tif strings.EqualFold(string(d), n) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t}\n\treturn false\n}\n\n// computed by:\n// sqlite3 vulnerability.db 'select distinct namespace from vulnerability where fix_state in (\"wont-fix\", \"not-fixed\") order by namespace;' | cut -d ':' -f 1 | sort | uniq\n// then removing 'github'\nvar comprehensiveDistros = []distro.Type{\n\tdistro.ArchLinux,\n\tdistro.Azure,\n\tdistro.Debian,\n\tdistro.Mariner,\n\tdistro.RedHat,\n\tdistro.Ubuntu,\n}\n\nfunc isOSPackage(p syftPkg.Package) bool {\n\tswitch p.Type {\n\tcase syftPkg.DebPkg, syftPkg.RpmPkg, syftPkg.PortagePkg, syftPkg.AlpmPkg, syftPkg.ApkPkg:\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc dataFromPkg(p syftPkg.Package) (any, []UpstreamPackage) {\n\tvar metadata interface{}\n\tvar upstreams []UpstreamPackage\n\n\t// use the metadata to determine the type of package\n\tswitch p.Metadata.(type) {\n\tcase syftPkg.GolangModuleEntry, syftPkg.GolangBinaryBuildinfoEntry, syftPkg.GolangSourceEntry:\n\t\tmetadata = golangMetadataFromPkg(p)\n\tcase syftPkg.DpkgDBEntry:\n\t\tupstreams = dpkgDataFromPkg(p)\n\tcase syftPkg.DpkgArchiveEntry:\n\t\tupstreams = dpkgDataFromPkg(p)\n\tcase syftPkg.RpmArchive, syftPkg.RpmDBEntry:\n\t\tm, u := rpmDataFromPkg(p)\n\t\tupstreams = u\n\t\tif m != nil {\n\t\t\tmetadata = *m\n\t\t}\n\tcase syftPkg.JavaArchive:\n\t\tif m := javaDataFromPkgMetadata(p); m != nil {\n\t\t\tmetadata = *m\n\t\t}\n\tcase syftPkg.ApkDBEntry:\n\t\tmetadata = apkMetadataFromPkg(p)\n\t\tupstreams = apkDataFromPkg(p)\n\tcase syftPkg.JavaVMInstallation:\n\t\tmetadata = javaVMDataFromPkg(p)\n\t}\n\n\t// there are still cases where we could still fill the metadata from other info (such as the PURL)\n\tif metadata == nil {\n\t\tif p.Type == syftPkg.JavaPkg {\n\t\t\tmetadata = javaDataFromPkgData(p)\n\t\t}\n\t}\n\n\treturn metadata, upstreams\n}\n\nfunc javaVMDataFromPkg(p syftPkg.Package) any {\n\tif value, ok := p.Metadata.(syftPkg.JavaVMInstallation); ok {\n\t\treturn JavaVMInstallationMetadata{\n\t\t\tRelease: JavaVMReleaseMetadata{\n\t\t\t\tJavaRuntimeVersion: value.Release.JavaRuntimeVersion,\n\t\t\t\tJavaVersion:        value.Release.JavaVersion,\n\t\t\t\tFullVersion:        value.Release.FullVersion,\n\t\t\t\tSemanticVersion:    value.Release.SemanticVersion,\n\t\t\t},\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc apkMetadataFromPkg(p syftPkg.Package) interface{} {\n\tif m, ok := p.Metadata.(syftPkg.ApkDBEntry); ok {\n\t\tmetadata := ApkMetadata{}\n\n\t\tfileRecords := make([]ApkFileRecord, 0, len(m.Files))\n\t\tfor _, record := range m.Files {\n\t\t\tr := ApkFileRecord{Path: record.Path}\n\t\t\tfileRecords = append(fileRecords, r)\n\t\t}\n\n\t\tmetadata.Files = fileRecords\n\n\t\treturn metadata\n\t}\n\n\treturn nil\n}\n\nfunc golangMetadataFromPkg(p syftPkg.Package) interface{} {\n\tswitch value := p.Metadata.(type) {\n\tcase syftPkg.GolangBinaryBuildinfoEntry:\n\t\tmetadata := GolangBinMetadata{}\n\t\tif value.BuildSettings != nil {\n\t\t\tmetadata.BuildSettings = value.BuildSettings\n\t\t}\n\t\tmetadata.GoCompiledVersion = value.GoCompiledVersion\n\t\tmetadata.Architecture = value.Architecture\n\t\tmetadata.H1Digest = value.H1Digest\n\t\tmetadata.MainModule = value.MainModule\n\t\treturn metadata\n\tcase syftPkg.GolangModuleEntry:\n\t\tmetadata := GolangModMetadata{}\n\t\tmetadata.H1Digest = value.H1Digest\n\t\treturn metadata\n\tcase syftPkg.GolangSourceEntry:\n\t\tmetadata := GolangSourceMetadata{}\n\t\tmetadata.H1Digest = value.H1Digest\n\t\tmetadata.OperatingSystem = value.OperatingSystem\n\t\tmetadata.Architecture = value.Architecture\n\t\tmetadata.BuildTags = value.BuildTags\n\t\tmetadata.CgoEnabled = value.CgoEnabled\n\t\treturn metadata\n\t}\n\treturn nil\n}\n\nfunc dpkgDataFromPkg(p syftPkg.Package) (upstreams []UpstreamPackage) {\n\tswitch value := p.Metadata.(type) {\n\tcase syftPkg.DpkgDBEntry:\n\t\tif value.Source != \"\" {\n\t\t\tupstreams = append(upstreams, UpstreamPackage{\n\t\t\t\tName:    value.Source,\n\t\t\t\tVersion: value.SourceVersion,\n\t\t\t})\n\t\t}\n\tcase syftPkg.DpkgArchiveEntry:\n\t\tif value.Source != \"\" {\n\t\t\tupstreams = append(upstreams, UpstreamPackage{\n\t\t\t\tName:    value.Source,\n\t\t\t\tVersion: value.SourceVersion,\n\t\t\t})\n\t\t}\n\tdefault:\n\t\tlog.Debugf(\"unable to extract DPKG metadata for %s\", p)\n\t}\n\n\treturn upstreams\n}\n\nfunc rpmDataFromPkg(p syftPkg.Package) (metadata *RpmMetadata, upstreams []UpstreamPackage) {\n\tswitch m := p.Metadata.(type) {\n\tcase syftPkg.RpmDBEntry:\n\t\tif m.SourceRpm != \"\" {\n\t\t\tupstreams = handleSourceRPM(p.Name, m.SourceRpm)\n\t\t}\n\n\t\tmetadata = &RpmMetadata{\n\t\t\tEpoch:           m.Epoch,\n\t\t\tModularityLabel: m.ModularityLabel,\n\t\t}\n\tcase syftPkg.RpmArchive:\n\t\tif m.SourceRpm != \"\" {\n\t\t\tupstreams = handleSourceRPM(p.Name, m.SourceRpm)\n\t\t}\n\n\t\tmetadata = &RpmMetadata{\n\t\t\tEpoch:           m.Epoch,\n\t\t\tModularityLabel: m.ModularityLabel,\n\t\t}\n\t}\n\treturn metadata, upstreams\n}\n\nfunc handleSourceRPM(pkgName, sourceRpm string) []UpstreamPackage {\n\tvar upstreams []UpstreamPackage\n\tname, version := getNameAndELVersion(sourceRpm)\n\tif name == \"\" && version == \"\" {\n\t\tlog.Debugf(\"unable to extract name and version from SourceRPM=%q\", sourceRpm)\n\t} else if name != pkgName {\n\t\t// don't include matches if the source package name matches the current package name\n\t\tif name != \"\" && version != \"\" {\n\t\t\tupstreams = append(upstreams,\n\t\t\t\tUpstreamPackage{\n\t\t\t\t\tName:    name,\n\t\t\t\t\tVersion: version,\n\t\t\t\t},\n\t\t\t)\n\t\t}\n\t}\n\treturn upstreams\n}\n\nfunc getNameAndELVersion(sourceRpm string) (string, string) {\n\tgroupMatches := stringutil.MatchCaptureGroups(rpmPackageNamePattern, sourceRpm)\n\tversion := groupMatches[\"version\"] + \"-\" + groupMatches[\"release\"]\n\treturn groupMatches[\"name\"], version\n}\n\nfunc javaDataFromPkgMetadata(p syftPkg.Package) (metadata *JavaMetadata) {\n\tif value, ok := p.Metadata.(syftPkg.JavaArchive); ok {\n\t\tvar artifactID, groupID, name string\n\t\tif value.PomProperties != nil {\n\t\t\tartifactID = value.PomProperties.ArtifactID\n\t\t\tgroupID = value.PomProperties.GroupID\n\t\t} else {\n\t\t\t// get the group ID / artifact ID from the PURL\n\t\t\tartifactID, groupID = javaGroupArtifactIDFromPurl(p.PURL)\n\t\t}\n\n\t\tif value.Manifest != nil {\n\t\t\tfor _, kv := range value.Manifest.Main {\n\t\t\t\tif kv.Key == \"Name\" {\n\t\t\t\t\tname = kv.Value\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tvar archiveDigests []Digest\n\t\tif len(value.ArchiveDigests) > 0 {\n\t\t\tfor _, d := range value.ArchiveDigests {\n\t\t\t\tarchiveDigests = append(archiveDigests, Digest{\n\t\t\t\t\tAlgorithm: d.Algorithm,\n\t\t\t\t\tValue:     d.Value,\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\n\t\tmetadata = &JavaMetadata{\n\t\t\tVirtualPath:    value.VirtualPath,\n\t\t\tPomArtifactID:  artifactID,\n\t\t\tPomGroupID:     groupID,\n\t\t\tManifestName:   name,\n\t\t\tArchiveDigests: archiveDigests,\n\t\t}\n\t}\n\treturn metadata\n}\n\nfunc javaDataFromPkgData(p syftPkg.Package) (metadata *JavaMetadata) {\n\tswitch p.Type {\n\tcase syftPkg.JavaPkg:\n\t\tartifactID, groupID := javaGroupArtifactIDFromPurl(p.PURL)\n\t\tif artifactID != \"\" && groupID != \"\" {\n\t\t\tmetadata = &JavaMetadata{\n\t\t\t\tPomArtifactID: artifactID,\n\t\t\t\tPomGroupID:    groupID,\n\t\t\t}\n\t\t}\n\tdefault:\n\t\tlog.Debugf(\"unable to extract metadata for %s\", p)\n\t}\n\n\treturn metadata\n}\n\nfunc javaGroupArtifactIDFromPurl(p string) (string, string) {\n\tpurl, err := packageurl.FromString(p)\n\tif err != nil {\n\t\tlog.WithFields(\"purl\", purl, \"error\", err).Debug(\"unable to parse java PURL\")\n\t\treturn \"\", \"\"\n\t}\n\treturn purl.Name, purl.Namespace\n}\n\nfunc apkDataFromPkg(p syftPkg.Package) (upstreams []UpstreamPackage) {\n\tif value, ok := p.Metadata.(syftPkg.ApkDBEntry); ok {\n\t\tif value.OriginPackage != \"\" {\n\t\t\tupstreams = append(upstreams, UpstreamPackage{\n\t\t\t\tName: value.OriginPackage,\n\t\t\t})\n\t\t}\n\t} else {\n\t\tlog.Debugf(\"unable to extract APK metadata for %s\", p)\n\t}\n\treturn upstreams\n}\n\nfunc ByID(id ID, pkgs []Package) *Package {\n\tfor _, p := range pkgs {\n\t\tif p.ID == id {\n\t\t\treturn &p\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc parseUpstream(pkgName string, value string, pkgType syftPkg.Type) []UpstreamPackage {\n\tif pkgType == syftPkg.RpmPkg {\n\t\treturn handleSourceRPM(pkgName, value)\n\t}\n\treturn handleDefaultUpstream(pkgName, value)\n}\n\nfunc handleDefaultUpstream(pkgName string, value string) []UpstreamPackage {\n\tfields := strings.Split(value, \"@\")\n\tswitch len(fields) {\n\tcase 2:\n\t\tif fields[0] == pkgName {\n\t\t\treturn nil\n\t\t}\n\t\treturn []UpstreamPackage{\n\t\t\t{\n\t\t\t\tName:    fields[0],\n\t\t\t\tVersion: fields[1],\n\t\t\t},\n\t\t}\n\tcase 1:\n\t\tif fields[0] == pkgName {\n\t\t\treturn nil\n\t\t}\n\t\treturn []UpstreamPackage{\n\t\t\t{\n\t\t\t\tName: fields[0],\n\t\t\t},\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc applyDistroOverride(override *distro.Distro) Enhancer {\n\t// the override here comes from either the --distro flag, which already has a channel indication applied\n\treturn func(out *Package, _ packageurl.PackageURL, _ syftPkg.Package) {\n\t\tif override == nil {\n\t\t\treturn\n\t\t}\n\t\t// allow downstream matchers to always consider the given user distro\n\t\tout.Distro = override\n\t}\n}\n"
  },
  {
    "path": "grype/pkg/package_test.go",
    "content": "package pkg\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\n\t\"github.com/anchore/grype/grype/distro\"\n\t\"github.com/anchore/syft/syft/artifact\"\n\t\"github.com/anchore/syft/syft/cpe\"\n\t\"github.com/anchore/syft/syft/file\"\n\tsyftFile \"github.com/anchore/syft/syft/file\"\n\t\"github.com/anchore/syft/syft/linux\"\n\tsyftPkg \"github.com/anchore/syft/syft/pkg\"\n\t\"github.com/anchore/syft/syft/sbom\"\n\t\"github.com/anchore/syft/syft/testutil\"\n)\n\nfunc TestNew(t *testing.T) {\n\ttests := []struct {\n\t\tname      string\n\t\tsyftPkg   syftPkg.Package\n\t\tmetadata  interface{}\n\t\tupstreams []UpstreamPackage\n\t}{\n\t\t{\n\t\t\tname: \"alpm package with source info\",\n\t\t\tsyftPkg: syftPkg.Package{\n\t\t\t\tMetadata: syftPkg.AlpmDBEntry{\n\t\t\t\t\tBasePackage:  \"base-pkg-info\",\n\t\t\t\t\tPackage:      \"pkg-info\",\n\t\t\t\t\tVersion:      \"version-info\",\n\t\t\t\t\tArchitecture: \"arch-info\",\n\t\t\t\t\tFiles: []syftPkg.AlpmFileRecord{{\n\t\t\t\t\t\tPath: \"/this/path/exists\",\n\t\t\t\t\t}},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"dpkg with source info\",\n\t\t\tsyftPkg: syftPkg.Package{\n\t\t\t\tMetadata: syftPkg.DpkgDBEntry{\n\t\t\t\t\tPackage:       \"pkg-info\",\n\t\t\t\t\tSource:        \"src-info\",\n\t\t\t\t\tVersion:       \"version-info\",\n\t\t\t\t\tSourceVersion: \"src-version-info\",\n\t\t\t\t\tArchitecture:  \"arch-info\",\n\t\t\t\t\tMaintainer:    \"maintainer-info\",\n\t\t\t\t\tInstalledSize: 10,\n\t\t\t\t\tFiles: []syftPkg.DpkgFileRecord{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tPath: \"path-info\",\n\t\t\t\t\t\t\tDigest: &file.Digest{\n\t\t\t\t\t\t\t\tAlgorithm: \"algo-info\",\n\t\t\t\t\t\t\t\tValue:     \"digest-info\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tIsConfigFile: true,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tupstreams: []UpstreamPackage{\n\t\t\t\t{\n\t\t\t\t\tName:    \"src-info\",\n\t\t\t\t\tVersion: \"src-version-info\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"dpkg archive with source info\",\n\t\t\tsyftPkg: syftPkg.Package{\n\t\t\t\tMetadata: syftPkg.DpkgArchiveEntry{\n\t\t\t\t\tPackage:       \"pkg-info\",\n\t\t\t\t\tSource:        \"src-info\",\n\t\t\t\t\tVersion:       \"version-info\",\n\t\t\t\t\tSourceVersion: \"src-version-info\",\n\t\t\t\t\tArchitecture:  \"arch-info\",\n\t\t\t\t\tMaintainer:    \"maintainer-info\",\n\t\t\t\t\tInstalledSize: 10,\n\t\t\t\t\tFiles: []syftPkg.DpkgFileRecord{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tPath: \"path-info\",\n\t\t\t\t\t\t\tDigest: &file.Digest{\n\t\t\t\t\t\t\t\tAlgorithm: \"algo-info\",\n\t\t\t\t\t\t\t\tValue:     \"digest-info\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tIsConfigFile: true,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tupstreams: []UpstreamPackage{\n\t\t\t\t{\n\t\t\t\t\tName:    \"src-info\",\n\t\t\t\t\tVersion: \"src-version-info\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"rpm archive with source info\",\n\t\t\tsyftPkg: syftPkg.Package{\n\t\t\t\tMetadata: syftPkg.RpmArchive{\n\t\t\t\t\tName:      \"name-info\",\n\t\t\t\t\tVersion:   \"version-info\",\n\t\t\t\t\tEpoch:     intRef(30),\n\t\t\t\t\tArch:      \"arch-info\",\n\t\t\t\t\tRelease:   \"release-info\",\n\t\t\t\t\tSourceRpm: \"sqlite-3.26.0-6.el8.src.rpm\",\n\t\t\t\t\tSize:      40,\n\t\t\t\t\tVendor:    \"vendor-info\",\n\t\t\t\t\tFiles: []syftPkg.RpmFileRecord{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tPath: \"path-info\",\n\t\t\t\t\t\t\tMode: 20,\n\t\t\t\t\t\t\tSize: 10,\n\t\t\t\t\t\t\tDigest: file.Digest{\n\t\t\t\t\t\t\t\tAlgorithm: \"algo-info\",\n\t\t\t\t\t\t\t\tValue:     \"digest-info\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tUserName:  \"user-info\",\n\t\t\t\t\t\t\tGroupName: \"group-info\",\n\t\t\t\t\t\t\tFlags:     \"flag-info\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tmetadata: RpmMetadata{\n\t\t\t\tEpoch: intRef(30),\n\t\t\t},\n\t\t\tupstreams: []UpstreamPackage{\n\t\t\t\t{\n\t\t\t\t\tName:    \"sqlite\",\n\t\t\t\t\tVersion: \"3.26.0-6.el8\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"rpm db entry with source info\",\n\t\t\tsyftPkg: syftPkg.Package{\n\t\t\t\tMetadata: syftPkg.RpmDBEntry{\n\t\t\t\t\tName:      \"name-info\",\n\t\t\t\t\tVersion:   \"version-info\",\n\t\t\t\t\tEpoch:     intRef(30),\n\t\t\t\t\tArch:      \"arch-info\",\n\t\t\t\t\tRelease:   \"release-info\",\n\t\t\t\t\tSourceRpm: \"sqlite-3.26.0-6.el8.src.rpm\",\n\t\t\t\t\tSize:      40,\n\t\t\t\t\tVendor:    \"vendor-info\",\n\t\t\t\t\tFiles: []syftPkg.RpmFileRecord{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tPath: \"path-info\",\n\t\t\t\t\t\t\tMode: 20,\n\t\t\t\t\t\t\tSize: 10,\n\t\t\t\t\t\t\tDigest: file.Digest{\n\t\t\t\t\t\t\t\tAlgorithm: \"algo-info\",\n\t\t\t\t\t\t\t\tValue:     \"digest-info\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tUserName:  \"user-info\",\n\t\t\t\t\t\t\tGroupName: \"group-info\",\n\t\t\t\t\t\t\tFlags:     \"flag-info\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tmetadata: RpmMetadata{\n\t\t\t\tEpoch: intRef(30),\n\t\t\t},\n\t\t\tupstreams: []UpstreamPackage{\n\t\t\t\t{\n\t\t\t\t\tName:    \"sqlite\",\n\t\t\t\t\tVersion: \"3.26.0-6.el8\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"rpm archive with source info that matches the package info\",\n\t\t\tsyftPkg: syftPkg.Package{\n\t\t\t\tName: \"sqlite\",\n\t\t\t\tMetadata: syftPkg.RpmArchive{\n\t\t\t\t\tSourceRpm: \"sqlite-3.26.0-6.el8.src.rpm\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tmetadata: RpmMetadata{},\n\t\t},\n\t\t{\n\t\t\tname: \"rpm archive with modularity label\",\n\t\t\tsyftPkg: syftPkg.Package{\n\t\t\t\tName: \"sqlite\",\n\t\t\t\tMetadata: syftPkg.RpmArchive{\n\t\t\t\t\tSourceRpm:       \"sqlite-3.26.0-6.el8.src.rpm\",\n\t\t\t\t\tModularityLabel: strRef(\"abc:2\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\tmetadata: RpmMetadata{ModularityLabel: strRef(\"abc:2\")},\n\t\t},\n\t\t{\n\t\t\tname: \"java pkg\",\n\t\t\tsyftPkg: syftPkg.Package{\n\t\t\t\tMetadata: syftPkg.JavaArchive{\n\t\t\t\t\tVirtualPath: \"virtual-path-info\",\n\t\t\t\t\tManifest: &syftPkg.JavaManifest{\n\t\t\t\t\t\tMain: syftPkg.KeyValues{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tKey:   \"Name\",\n\t\t\t\t\t\t\t\tValue: \"main-section-name-info\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tSections: []syftPkg.KeyValues{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tKey:   \"named-section-key\",\n\t\t\t\t\t\t\t\t\tValue: \"named-section-value\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tPomProperties: &syftPkg.JavaPomProperties{\n\t\t\t\t\t\tPath:       \"pom-path-info\",\n\t\t\t\t\t\tName:       \"pom-name-info\",\n\t\t\t\t\t\tGroupID:    \"pom-group-ID-info\",\n\t\t\t\t\t\tArtifactID: \"pom-artifact-ID-info\",\n\t\t\t\t\t\tVersion:    \"pom-version-info\",\n\t\t\t\t\t\tExtra: map[string]string{\n\t\t\t\t\t\t\t\"extra-key\": \"extra-value\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tArchiveDigests: []syftFile.Digest{{\n\t\t\t\t\t\tAlgorithm: \"sha1\",\n\t\t\t\t\t\tValue:     \"236e3bfdbdc6c86629237a74f0f11414adb4e211\",\n\t\t\t\t\t}},\n\t\t\t\t},\n\t\t\t},\n\t\t\tmetadata: JavaMetadata{\n\t\t\t\tVirtualPath:   \"virtual-path-info\",\n\t\t\t\tPomArtifactID: \"pom-artifact-ID-info\",\n\t\t\t\tPomGroupID:    \"pom-group-ID-info\",\n\t\t\t\tManifestName:  \"main-section-name-info\",\n\t\t\t\tArchiveDigests: []Digest{{\n\t\t\t\t\tAlgorithm: \"sha1\",\n\t\t\t\t\tValue:     \"236e3bfdbdc6c86629237a74f0f11414adb4e211\",\n\t\t\t\t}},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"apk with source info\",\n\t\t\tsyftPkg: syftPkg.Package{\n\t\t\t\tMetadata: syftPkg.ApkDBEntry{\n\t\t\t\t\tPackage:       \"libcurl-tools\",\n\t\t\t\t\tOriginPackage: \"libcurl\",\n\t\t\t\t\tMaintainer:    \"somone\",\n\t\t\t\t\tVersion:       \"1.2.3\",\n\t\t\t\t\tArchitecture:  \"a\",\n\t\t\t\t\tURL:           \"a\",\n\t\t\t\t\tDescription:   \"a\",\n\t\t\t\t\tSize:          1,\n\t\t\t\t\tInstalledSize: 1,\n\t\t\t\t},\n\t\t\t},\n\t\t\tupstreams: []UpstreamPackage{\n\t\t\t\t{\n\t\t\t\t\tName: \"libcurl\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tmetadata: ApkMetadata{Files: []ApkFileRecord{}},\n\t\t},\n\t\t// the below packages are those that have no metadata or upstream info to parse out\n\t\t{\n\t\t\tname: \"npm-metadata\",\n\t\t\tsyftPkg: syftPkg.Package{\n\t\t\t\tMetadata: syftPkg.NpmPackage{\n\t\t\t\t\tAuthor:      \"a\",\n\t\t\t\t\tHomepage:    \"a\",\n\t\t\t\t\tDescription: \"a\",\n\t\t\t\t\tURL:         \"a\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"python-metadata\",\n\t\t\tsyftPkg: syftPkg.Package{\n\t\t\t\tMetadata: syftPkg.PythonPackage{\n\t\t\t\t\tName:                 \"a\",\n\t\t\t\t\tVersion:              \"a\",\n\t\t\t\t\tAuthor:               \"a\",\n\t\t\t\t\tAuthorEmail:          \"a\",\n\t\t\t\t\tPlatform:             \"a\",\n\t\t\t\t\tSitePackagesRootPath: \"a\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"gem-metadata\",\n\t\t\tsyftPkg: syftPkg.Package{\n\t\t\t\tMetadata: syftPkg.RubyGemspec{\n\t\t\t\t\tName:     \"a\",\n\t\t\t\t\tVersion:  \"a\",\n\t\t\t\t\tHomepage: \"a\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"kb-metadata\",\n\t\t\tsyftPkg: syftPkg.Package{\n\t\t\t\tMetadata: syftPkg.MicrosoftKbPatch{\n\t\t\t\t\tProductID: \"a\",\n\t\t\t\t\tKb:        \"a\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"rust-metadata\",\n\t\t\tsyftPkg: syftPkg.Package{\n\t\t\t\tMetadata: syftPkg.RustCargoLockEntry{\n\t\t\t\t\tName:     \"a\",\n\t\t\t\t\tVersion:  \"a\",\n\t\t\t\t\tSource:   \"a\",\n\t\t\t\t\tChecksum: \"a\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"github-actions-use-statement\",\n\t\t\tsyftPkg: syftPkg.Package{\n\t\t\t\tMetadata: syftPkg.GitHubActionsUseStatement{\n\t\t\t\t\tValue:   \"a\",\n\t\t\t\t\tComment: \"a\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"golang-metadata\",\n\t\t\tsyftPkg: syftPkg.Package{\n\t\t\t\tMetadata: syftPkg.GolangBinaryBuildinfoEntry{\n\t\t\t\t\tBuildSettings:     syftPkg.KeyValues{},\n\t\t\t\t\tGoCompiledVersion: \"1.0.0\",\n\t\t\t\t\tH1Digest:          \"a\",\n\t\t\t\t\tMainModule:        \"myMainModule\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tmetadata: GolangBinMetadata{\n\t\t\t\tBuildSettings:     syftPkg.KeyValues{},\n\t\t\t\tGoCompiledVersion: \"1.0.0\",\n\t\t\t\tH1Digest:          \"a\",\n\t\t\t\tMainModule:        \"myMainModule\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"golang-mod-metadata\",\n\t\t\tsyftPkg: syftPkg.Package{\n\t\t\t\tMetadata: syftPkg.GolangModuleEntry{\n\t\t\t\t\tH1Digest: \"h1:as234NweNNTNWEtt13nwNENTt\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tmetadata: GolangModMetadata{\n\t\t\t\tH1Digest: \"h1:as234NweNNTNWEtt13nwNENTt\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"php-composer-lock-metadata\",\n\t\t\tsyftPkg: syftPkg.Package{\n\t\t\t\tMetadata: syftPkg.PhpComposerLockEntry{\n\t\t\t\t\tName:    \"a\",\n\t\t\t\t\tVersion: \"a\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"php-composer-installed-metadata\",\n\t\t\tsyftPkg: syftPkg.Package{\n\t\t\t\tMetadata: syftPkg.PhpComposerInstalledEntry{\n\t\t\t\t\tName:    \"a\",\n\t\t\t\t\tVersion: \"a\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"dart-publock-metadata\",\n\t\t\tsyftPkg: syftPkg.Package{\n\t\t\t\tMetadata: syftPkg.DartPubspecLockEntry{\n\t\t\t\t\tName:    \"a\",\n\t\t\t\t\tVersion: \"a\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"dart-pubspec-metadata\",\n\t\t\tsyftPkg: syftPkg.Package{\n\t\t\t\tMetadata: syftPkg.DartPubspec{\n\t\t\t\t\tHomepage:      \"a\",\n\t\t\t\t\tRepository:    \"a\",\n\t\t\t\t\tDocumentation: \"a\",\n\t\t\t\t\tPublishTo:     \"a\",\n\t\t\t\t\tEnvironment: &syftPkg.DartPubspecEnvironment{\n\t\t\t\t\t\tSDK:     \"a\",\n\t\t\t\t\t\tFlutter: \"a\",\n\t\t\t\t\t},\n\t\t\t\t\tPlatforms:         []string{\"a\"},\n\t\t\t\t\tIgnoredAdvisories: []string{\"a\"},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"homebrew-formula-metadata\",\n\t\t\tsyftPkg: syftPkg.Package{\n\t\t\t\tMetadata: syftPkg.HomebrewFormula{\n\t\t\t\t\tTap:         \"a\",\n\t\t\t\t\tHomepage:    \"a\",\n\t\t\t\t\tDescription: \"a\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"dotnet-metadata\",\n\t\t\tsyftPkg: syftPkg.Package{\n\t\t\t\tMetadata: syftPkg.DotnetDepsEntry{\n\t\t\t\t\tName:     \"a\",\n\t\t\t\t\tVersion:  \"a\",\n\t\t\t\t\tPath:     \"a\",\n\t\t\t\t\tSha512:   \"a\",\n\t\t\t\t\tHashPath: \"a\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"cpp conan-metadata\",\n\t\t\tsyftPkg: syftPkg.Package{\n\t\t\t\tMetadata: syftPkg.ConanfileEntry{\n\t\t\t\t\tRef: \"catch2/2.13.8\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"cpp conan v1 lock metadata\",\n\t\t\tsyftPkg: syftPkg.Package{\n\t\t\t\tMetadata: syftPkg.ConanV1LockEntry{\n\t\t\t\t\tRef: \"zlib/1.2.12\",\n\t\t\t\t\tOptions: syftPkg.KeyValues{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tKey:   \"fPIC\",\n\t\t\t\t\t\t\tValue: \"True\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tKey:   \"shared\",\n\t\t\t\t\t\t\tValue: \"false\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tPath:    \"all/conanfile.py\",\n\t\t\t\t\tContext: \"host\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"cpp conan v2 lock metadata\",\n\t\t\tsyftPkg: syftPkg.Package{\n\t\t\t\tMetadata: syftPkg.ConanV2LockEntry{\n\t\t\t\t\tRef:       \"zlib/1.2.12\",\n\t\t\t\t\tPackageID: \"some-id\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"cocoapods cocoapods-metadata\",\n\t\t\tsyftPkg: syftPkg.Package{\n\t\t\t\tMetadata: syftPkg.CocoaPodfileLockEntry{\n\t\t\t\t\tChecksum: \"123eere234\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"portage-metadata\",\n\t\t\tsyftPkg: syftPkg.Package{\n\t\t\t\tMetadata: syftPkg.PortageEntry{\n\t\t\t\t\tInstalledSize: 1,\n\t\t\t\t\tFiles:         []syftPkg.PortageFileRecord{},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"hackage-stack-lock-metadata\",\n\t\t\tsyftPkg: syftPkg.Package{\n\t\t\t\tMetadata: syftPkg.HackageStackYamlLockEntry{\n\t\t\t\t\tPkgHash: \"some-hash\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"hackage-stack-metadata\",\n\t\t\tsyftPkg: syftPkg.Package{\n\t\t\t\tMetadata: syftPkg.HackageStackYamlEntry{\n\t\t\t\t\tPkgHash: \"some-hash\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"rebar-metadata\",\n\t\t\tsyftPkg: syftPkg.Package{\n\t\t\t\tMetadata: syftPkg.ErlangRebarLockEntry{\n\t\t\t\t\tName:    \"rebar\",\n\t\t\t\t\tVersion: \"v0.1.1\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"npm-package-lock-metadata\",\n\t\t\tsyftPkg: syftPkg.Package{\n\t\t\t\tMetadata: syftPkg.NpmPackageLockEntry{\n\t\t\t\t\tResolved:  \"resolved\",\n\t\t\t\t\tIntegrity: \"sha1:ab7d8979989b7a98d97\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"mix-lock-metadata\",\n\t\t\tsyftPkg: syftPkg.Package{\n\t\t\t\tMetadata: syftPkg.ElixirMixLockEntry{\n\t\t\t\t\tName:    \"mix-lock\",\n\t\t\t\t\tVersion: \"v0.1.2\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"pipfile-lock-metadata\",\n\t\t\tsyftPkg: syftPkg.Package{\n\t\t\t\tMetadata: syftPkg.PythonPipfileLockEntry{\n\t\t\t\t\tHashes: []string{\n\t\t\t\t\t\t\"sha1:ab8v88a8b88d8d8c88b8s765s47\",\n\t\t\t\t\t},\n\t\t\t\t\tIndex: \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"python-requirements-metadata\",\n\t\t\tsyftPkg: syftPkg.Package{\n\t\t\t\tMetadata: syftPkg.PythonRequirementsEntry{\n\t\t\t\t\tName:              \"a\",\n\t\t\t\t\tExtras:            []string{\"a\"},\n\t\t\t\t\tVersionConstraint: \"a\",\n\t\t\t\t\tURL:               \"a\",\n\t\t\t\t\tMarkers:           \"a\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"binary-metadata\",\n\t\t\tsyftPkg: syftPkg.Package{\n\t\t\t\tMetadata: syftPkg.BinarySignature{\n\t\t\t\t\tMatches: []syftPkg.ClassifierMatch{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tClassifier: \"node\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"nix-store-metadata\",\n\t\t\tsyftPkg: syftPkg.Package{\n\t\t\t\tMetadata: syftPkg.NixStoreEntry{\n\t\t\t\t\tOutputHash: \"a\",\n\t\t\t\t\tOutput:     \"a\",\n\t\t\t\t\tFiles: []string{\n\t\t\t\t\t\t\"a\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"linux-kernel-metadata\",\n\t\t\tsyftPkg: syftPkg.Package{\n\t\t\t\tMetadata: syftPkg.LinuxKernel{\n\t\t\t\t\tName:            \"a\",\n\t\t\t\t\tArchitecture:    \"a\",\n\t\t\t\t\tVersion:         \"a\",\n\t\t\t\t\tExtendedVersion: \"a\",\n\t\t\t\t\tBuildTime:       \"a\",\n\t\t\t\t\tAuthor:          \"a\",\n\t\t\t\t\tFormat:          \"a\",\n\t\t\t\t\tRWRootFS:        true,\n\t\t\t\t\tSwapDevice:      10,\n\t\t\t\t\tRootDevice:      11,\n\t\t\t\t\tVideoMode:       \"a\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"linux-kernel-module-metadata\",\n\t\t\tsyftPkg: syftPkg.Package{\n\t\t\t\tMetadata: syftPkg.LinuxKernelModule{\n\t\t\t\t\tName:          \"a\",\n\t\t\t\t\tVersion:       \"a\",\n\t\t\t\t\tSourceVersion: \"a\",\n\t\t\t\t\tPath:          \"a\",\n\t\t\t\t\tDescription:   \"a\",\n\t\t\t\t\tAuthor:        \"a\",\n\t\t\t\t\tLicense:       \"a\",\n\t\t\t\t\tKernelVersion: \"a\",\n\t\t\t\t\tVersionMagic:  \"a\",\n\t\t\t\t\tParameters: map[string]syftPkg.LinuxKernelModuleParameter{\n\t\t\t\t\t\t\"a\": {\n\t\t\t\t\t\t\tType:        \"a\",\n\t\t\t\t\t\t\tDescription: \"a\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"r-description-file-metadata\",\n\t\t\tsyftPkg: syftPkg.Package{\n\t\t\t\tMetadata: syftPkg.RDescription{\n\t\t\t\t\tTitle:            \"a\",\n\t\t\t\t\tDescription:      \"a\",\n\t\t\t\t\tAuthor:           \"a\",\n\t\t\t\t\tMaintainer:       \"a\",\n\t\t\t\t\tURL:              []string{\"a\"},\n\t\t\t\t\tRepository:       \"a\",\n\t\t\t\t\tBuilt:            \"a\",\n\t\t\t\t\tNeedsCompilation: true,\n\t\t\t\t\tImports:          []string{\"a\"},\n\t\t\t\t\tDepends:          []string{\"a\"},\n\t\t\t\t\tSuggests:         []string{\"a\"},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"dotnet-portable-executable-metadata\",\n\t\t\tsyftPkg: syftPkg.Package{\n\t\t\t\tMetadata: syftPkg.DotnetPortableExecutableEntry{\n\t\t\t\t\tAssemblyVersion: \"a\",\n\t\t\t\t\tLegalCopyright:  \"a\",\n\t\t\t\t\tComments:        \"a\",\n\t\t\t\t\tInternalName:    \"a\",\n\t\t\t\t\tCompanyName:     \"a\",\n\t\t\t\t\tProductName:     \"a\",\n\t\t\t\t\tProductVersion:  \"a\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"swift-package-manager-metadata\",\n\t\t\tsyftPkg: syftPkg.Package{\n\t\t\t\tMetadata: syftPkg.SwiftPackageManagerResolvedEntry{\n\t\t\t\t\tRevision: \"a\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"swipl-pack-entry\",\n\t\t\tsyftPkg: syftPkg.Package{\n\t\t\t\tMetadata: syftPkg.SwiplPackEntry{\n\t\t\t\t\tName:          \"a\",\n\t\t\t\t\tVersion:       \"a\",\n\t\t\t\t\tAuthor:        \"a\",\n\t\t\t\t\tAuthorEmail:   \"a\",\n\t\t\t\t\tPackager:      \"a\",\n\t\t\t\t\tPackagerEmail: \"a\",\n\t\t\t\t\tHomepage:      \"a\",\n\t\t\t\t\tDependencies: []string{\n\t\t\t\t\t\t\"a\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"conaninfo-entry\",\n\t\t\tsyftPkg: syftPkg.Package{\n\t\t\t\tMetadata: syftPkg.ConaninfoEntry{\n\t\t\t\t\tRef:       \"a\",\n\t\t\t\t\tPackageID: \"a\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"rust-binary-audit-entry\",\n\t\t\tsyftPkg: syftPkg.Package{\n\t\t\t\tMetadata: syftPkg.RustBinaryAuditEntry{\n\t\t\t\t\tName:    \"a\",\n\t\t\t\t\tVersion: \"a\",\n\t\t\t\t\tSource:  \"a\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"python-poetry-lock-entry\",\n\t\t\tsyftPkg: syftPkg.Package{\n\t\t\t\tMetadata: syftPkg.PythonPoetryLockEntry{Index: \"some-index\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"yarn-lock-entry\",\n\t\t\tsyftPkg: syftPkg.Package{\n\t\t\t\tMetadata: syftPkg.YarnLockEntry{\n\t\t\t\t\tResolved:  \"some-resolution\",\n\t\t\t\t\tIntegrity: \"some-digest\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"wordpress-plugin-entry\",\n\t\t\tsyftPkg: syftPkg.Package{\n\t\t\t\tMetadata: syftPkg.WordpressPluginEntry{\n\t\t\t\t\tPluginInstallDirectory: \"a\",\n\t\t\t\t\tAuthor:                 \"a\",\n\t\t\t\t\tAuthorURI:              \"a\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"elf-binary-package\",\n\t\t\tsyftPkg: syftPkg.Package{\n\t\t\t\tMetadata: syftPkg.ELFBinaryPackageNoteJSONPayload{\n\t\t\t\t\tType:       \"a\",\n\t\t\t\t\tVendor:     \"a\",\n\t\t\t\t\tSystem:     \"a\",\n\t\t\t\t\tSourceRepo: \"a\",\n\t\t\t\t\tCommit:     \"a\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"php-pecl-entry\",\n\t\t\tsyftPkg: syftPkg.Package{\n\t\t\t\tMetadata: syftPkg.PhpPeclEntry{\n\t\t\t\t\tName:    \"a\",\n\t\t\t\t\tVersion: \"a\",\n\t\t\t\t\tLicense: []string{\"a\"},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"php-pear-entry\",\n\t\t\tsyftPkg: syftPkg.Package{\n\t\t\t\tMetadata: syftPkg.PhpPearEntry{\n\t\t\t\t\tName:    \"a\",\n\t\t\t\t\tVersion: \"a\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"lua-rocks-entry\",\n\t\t\tsyftPkg: syftPkg.Package{\n\t\t\t\tMetadata: syftPkg.LuaRocksPackage{\n\t\t\t\t\tName:         \"a\",\n\t\t\t\t\tVersion:      \"a\",\n\t\t\t\t\tLicense:      \"a\",\n\t\t\t\t\tHomepage:     \"a\",\n\t\t\t\t\tDescription:  \"a\",\n\t\t\t\t\tURL:          \"a\",\n\t\t\t\t\tDependencies: map[string]string{\"b\": \"c\"},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"ocaml-entry\",\n\t\t\tsyftPkg: syftPkg.Package{\n\t\t\t\tMetadata: syftPkg.OpamPackage{\n\t\t\t\t\tName:         \"a\",\n\t\t\t\t\tVersion:      \"a\",\n\t\t\t\t\tLicenses:     []string{\"a\"},\n\t\t\t\t\tURL:          \"a\",\n\t\t\t\t\tChecksums:    []string{\"a\"},\n\t\t\t\t\tHomepage:     \"a\",\n\t\t\t\t\tDependencies: []string{\"a\"},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"jvm-installation-entry\",\n\t\t\tsyftPkg: syftPkg.Package{\n\t\t\t\tMetadata: syftPkg.JavaVMInstallation{\n\t\t\t\t\tRelease: syftPkg.JavaVMRelease{\n\t\t\t\t\t\tImplementor:        \"a\",\n\t\t\t\t\t\tImplementorVersion: \"a\",\n\t\t\t\t\t\tJavaRuntimeVersion: \"b\",\n\t\t\t\t\t\tJavaVersion:        \"c\",\n\t\t\t\t\t\tJavaVersionDate:    \"a\",\n\t\t\t\t\t\tLibc:               \"a\",\n\t\t\t\t\t\tModules:            []string{\"a\"},\n\t\t\t\t\t\tOsArch:             \"a\",\n\t\t\t\t\t\tOsName:             \"a\",\n\t\t\t\t\t\tOsVersion:          \"a\",\n\t\t\t\t\t\tSource:             \"a\",\n\t\t\t\t\t\tBuildSource:        \"a\",\n\t\t\t\t\t\tBuildSourceRepo:    \"a\",\n\t\t\t\t\t\tSourceRepo:         \"a\",\n\t\t\t\t\t\tFullVersion:        \"d\",\n\t\t\t\t\t\tSemanticVersion:    \"e\",\n\t\t\t\t\t\tBuildInfo:          \"a\",\n\t\t\t\t\t\tJvmVariant:         \"a\",\n\t\t\t\t\t\tJvmVersion:         \"a\",\n\t\t\t\t\t\tImageType:          \"a\",\n\t\t\t\t\t\tBuildType:          \"a\",\n\t\t\t\t\t},\n\t\t\t\t\tFiles: []string{\"a\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tmetadata: JavaVMInstallationMetadata{\n\t\t\t\tRelease: JavaVMReleaseMetadata{\n\t\t\t\t\tJavaRuntimeVersion: \"b\",\n\t\t\t\t\tJavaVersion:        \"c\",\n\t\t\t\t\tFullVersion:        \"d\",\n\t\t\t\t\tSemanticVersion:    \"e\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"dotnet-package-lock-entry\",\n\t\t\tsyftPkg: syftPkg.Package{\n\t\t\t\tMetadata: syftPkg.DotnetPackagesLockEntry{\n\t\t\t\t\tName:        \"AutoMapper\",\n\t\t\t\t\tVersion:     \"13.0.1\",\n\t\t\t\t\tContentHash: \"/Fx1SbJ16qS7dU4i604Sle+U9VLX+WSNVJggk6MupKVkYvvBm4XqYaeFuf67diHefHKHs50uQIS2YEDFhPCakQ==\",\n\t\t\t\t\tType:        \"Direct\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"bitnami-sbom-entry\",\n\t\t\tsyftPkg: syftPkg.Package{\n\t\t\t\tMetadata: syftPkg.BitnamiSBOMEntry{\n\t\t\t\t\tName:    \"a\",\n\t\t\t\t\tVersion: \"1\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"terraform-lock-provider-entry\",\n\t\t\tsyftPkg: syftPkg.Package{\n\t\t\t\tMetadata: syftPkg.TerraformLockProviderEntry{\n\t\t\t\t\tURL:         \"registry.terraform.io/hashicorp/aws\",\n\t\t\t\t\tVersion:     \"5.72.1\",\n\t\t\t\t\tConstraints: \"> 5.72.0\",\n\t\t\t\t\tHashes: []string{\n\t\t\t\t\t\t\"h1:jhd5O5o0CfZCNEwwN0EiDAzb7ApuFrtxJqa6HXW4EKE=\",\n\t\t\t\t\t\t\"zh:0dea6843836e926d33469b48b948744079023816d16a2ff7666bcfb6aa3522d4\",\n\t\t\t\t\t\t\"zh:195fa9513f75800a0d62797ebec75ee73e9b8c28d713fe9b63d3b1d1eec129b3\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"pe binary metadata\",\n\t\t\tsyftPkg: syftPkg.Package{\n\t\t\t\tMetadata: syftPkg.PEBinary{\n\t\t\t\t\tVersionResources: syftPkg.KeyValues{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tKey:   \"k\",\n\t\t\t\t\t\t\tValue: \"k\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"uv lock metadata\",\n\t\t\tsyftPkg: syftPkg.Package{\n\t\t\t\tMetadata: syftPkg.PythonUvLockEntry{\n\t\t\t\t\tIndex: \"https://pypi.org/simple\",\n\t\t\t\t\tDependencies: []syftPkg.PythonUvLockDependencyEntry{\n\t\t\t\t\t\t{Name: \"certifi\"},\n\t\t\t\t\t\t{Name: \"charset-normalizer\"},\n\t\t\t\t\t\t{Name: \"idna\"},\n\t\t\t\t\t\t{Name: \"urllib3\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"conda meta package metadata\",\n\t\t\tsyftPkg: syftPkg.Package{\n\t\t\t\tMetadata: syftPkg.CondaMetaPackage{\n\t\t\t\t\tName:        \"numpy\",\n\t\t\t\t\tVersion:     \"1.21.0\",\n\t\t\t\t\tBuild:       \"py39h20b1b1c_0\",\n\t\t\t\t\tBuildNumber: 0,\n\t\t\t\t\tChannel:     \"conda-forge\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"golang source entry metadata\",\n\t\t\tsyftPkg: syftPkg.Package{\n\t\t\t\tMetadata: syftPkg.GolangSourceEntry{\n\t\t\t\t\tH1Digest:        \"h1:some-hash-value\",\n\t\t\t\t\tOperatingSystem: \"linux\",\n\t\t\t\t\tArchitecture:    \"amd64\",\n\t\t\t\t\tBuildTags:       \"release\",\n\t\t\t\t\tCgoEnabled:      true,\n\t\t\t\t},\n\t\t\t},\n\t\t\tmetadata: GolangSourceMetadata{\n\t\t\t\tH1Digest:        \"h1:some-hash-value\",\n\t\t\t\tOperatingSystem: \"linux\",\n\t\t\t\tArchitecture:    \"amd64\",\n\t\t\t\tBuildTags:       \"release\",\n\t\t\t\tCgoEnabled:      true,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"snap-entry\",\n\t\t\tsyftPkg: syftPkg.Package{\n\t\t\t\tMetadata: syftPkg.SnapEntry{\n\t\t\t\t\tSnapType:     \"app\",\n\t\t\t\t\tBase:         \"core22\",\n\t\t\t\t\tSnapName:     \"test-snap\",\n\t\t\t\t\tSnapVersion:  \"1.0.0\",\n\t\t\t\t\tArchitecture: \"amd64\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"python-pdm-lock-entry\",\n\t\t\tsyftPkg: syftPkg.Package{\n\t\t\t\tMetadata: syftPkg.PythonPdmLockEntry{\n\t\t\t\t\tSummary: \"Test package\",\n\t\t\t\t\tFiles: []syftPkg.PythonPdmFileEntry{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tURL: \"test/file.py\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tDependencies: []string{\"dependency1\", \"dependency2\"},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"javascript-pnpm-lock-entry\",\n\t\t\tsyftPkg: syftPkg.Package{\n\t\t\t\tMetadata: syftPkg.PnpmLockEntry{\n\t\t\t\t\tResolution: syftPkg.PnpmLockResolution{\n\t\t\t\t\t\tIntegrity: \"\",\n\t\t\t\t\t},\n\t\t\t\t\tDependencies: map[string]string{\n\t\t\t\t\t\t\"dependency1\": \"1.2.3\",\n\t\t\t\t\t\t\"dependency2\": \"4.5.6\"},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"gguf-file-header\",\n\t\t\tsyftPkg: syftPkg.Package{\n\t\t\t\tMetadata: syftPkg.GGUFFileHeader{\n\t\t\t\t\tGGUFVersion:  1,\n\t\t\t\t\tFileSize:     2,\n\t\t\t\t\tArchitecture: \"arch\",\n\t\t\t\t\tQuantization: \"quant\",\n\t\t\t\t\tParameters:   3,\n\t\t\t\t\tTensorCount:  4,\n\t\t\t\t\tRemainingKeyValues: map[string]any{\n\t\t\t\t\t\t\"key1\": \"value1\",\n\t\t\t\t\t},\n\t\t\t\t\tMetadataKeyValuesHash: \"f00bar123\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\t// capture each observed metadata type, we should see all of them relate to what syft provides by the end of testing\n\ttester := testutil.NewPackageMetadataCompletionTester(t)\n\n\t// run all of our cases\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\ttester.Tested(t, test.syftPkg.Metadata)\n\t\t\tp := New(test.syftPkg)\n\t\t\tassert.Equal(t, test.metadata, p.Metadata, \"unexpected metadata\")\n\t\t\tassert.Equal(t, test.upstreams, p.Upstreams, \"unexpected upstream\")\n\t\t})\n\t}\n}\n\nfunc TestFromCollection_DoesNotPanic(t *testing.T) {\n\tcollection := syftPkg.NewCollection()\n\n\texamplePackage := syftPkg.Package{\n\t\tName:    \"test\",\n\t\tVersion: \"1.2.3\",\n\t\tLocations: file.NewLocationSet(\n\t\t\tfile.NewLocation(\"/test-path\"),\n\t\t),\n\t\tType: syftPkg.NpmPkg,\n\t}\n\n\tcollection.Add(examplePackage)\n\t// add it again!\n\tcollection.Add(examplePackage)\n\n\tassert.NotPanics(t, func() {\n\t\t_ = FromCollection(collection, SynthesisConfig{})\n\t})\n}\n\nfunc TestFromCollection_GeneratesCPEs(t *testing.T) {\n\tcollection := syftPkg.NewCollection()\n\n\tcollection.Add(syftPkg.Package{\n\t\tName:    \"first\",\n\t\tVersion: \"1\",\n\t\tCPEs: []cpe.CPE{\n\t\t\t{},\n\t\t},\n\t})\n\n\tcollection.Add(syftPkg.Package{\n\t\tName:    \"second\",\n\t\tVersion: \"2\",\n\t})\n\n\t// doesn't generate cpes when no flag\n\tpkgs := FromCollection(collection, SynthesisConfig{})\n\tassert.Len(t, pkgs[0].CPEs, 1)\n\tassert.Len(t, pkgs[1].CPEs, 0)\n\n\t// does generate cpes with the flag\n\tpkgs = FromCollection(collection, SynthesisConfig{\n\t\tGenerateMissingCPEs: true,\n\t})\n\tassert.Len(t, pkgs[0].CPEs, 1)\n\tassert.Len(t, pkgs[1].CPEs, 1)\n}\n\nfunc Test_getNameAndELVersion(t *testing.T) {\n\ttests := []struct {\n\t\tname            string\n\t\tsourceRPM       string\n\t\texpectedName    string\n\t\texpectedVersion string\n\t}{\n\t\t{\n\t\t\tname:            \"sqlite-3.26.0-6.el8.src.rpm\",\n\t\t\tsourceRPM:       \"sqlite-3.26.0-6.el8.src.rpm\",\n\t\t\texpectedName:    \"sqlite\",\n\t\t\texpectedVersion: \"3.26.0-6.el8\",\n\t\t},\n\t\t{\n\t\t\tname:            \"util-linux-ng-2.17.2-12.28.el6_9.src.rpm\",\n\t\t\tsourceRPM:       \"util-linux-ng-2.17.2-12.28.el6_9.src.rpm\",\n\t\t\texpectedName:    \"util-linux-ng\",\n\t\t\texpectedVersion: \"2.17.2-12.28.el6_9\",\n\t\t},\n\t\t{\n\t\t\tname:            \"util-linux-ng-2.17.2-12.28.el6_9.2.src.rpm\",\n\t\t\tsourceRPM:       \"util-linux-ng-2.17.2-12.28.el6_9.2.src.rpm\",\n\t\t\texpectedName:    \"util-linux-ng\",\n\t\t\texpectedVersion: \"2.17.2-12.28.el6_9.2\",\n\t\t},\n\t}\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tactualName, actualVersion := getNameAndELVersion(test.sourceRPM)\n\t\t\tassert.Equal(t, test.expectedName, actualName)\n\t\t\tassert.Equal(t, test.expectedVersion, actualVersion)\n\t\t})\n\t}\n}\n\nfunc intRef(i int) *int {\n\treturn &i\n}\n\nfunc Test_RemovePackagesByOverlap(t *testing.T) {\n\ttests := []struct {\n\t\tname             string\n\t\tsbom             *sbom.SBOM\n\t\texpectedPackages []string\n\t}{\n\t\t{\n\t\t\tname: \"includes all packages without overlap\",\n\t\t\tsbom: catalogWithOverlaps(\n\t\t\t\t[]string{\":go@1.18\", \"apk:node@19.2-r1\", \"binary:python@3.9\"},\n\t\t\t\t[]string{}),\n\t\t\texpectedPackages: []string{\":go@1.18\", \"apk:node@19.2-r1\", \"binary:python@3.9\"},\n\t\t},\n\t\t{\n\t\t\tname: \"excludes single package by overlap\",\n\t\t\tsbom: catalogWithOverlaps(\n\t\t\t\t[]string{\"apk:go@1.18\", \"apk:node@19.2-r1\", \"binary:node@19.2\"},\n\t\t\t\t[]string{\"apk:node@19.2-r1 -> binary:node@19.2\"}),\n\t\t\texpectedPackages: []string{\"apk:go@1.18\", \"apk:node@19.2-r1\"},\n\t\t},\n\t\t{\n\t\t\tname: \"does not exclude if OS package owns OS package\",\n\t\t\tsbom: catalogWithOverlaps(\n\t\t\t\t[]string{\"rpm:perl@5.3-r1\", \"rpm:libperl@5.3\"},\n\t\t\t\t[]string{\"rpm:perl@5.3-r1 -> rpm:libperl@5.3\"}),\n\t\t\texpectedPackages: []string{\"rpm:libperl@5.3\", \"rpm:perl@5.3-r1\"},\n\t\t},\n\t\t{\n\t\t\tname: \"does not exclude if owning package is non-OS\",\n\t\t\tsbom: catalogWithOverlaps(\n\t\t\t\t[]string{\"python:urllib3@1.2.3\", \"python:otherlib@1.2.3\"},\n\t\t\t\t[]string{\"python:urllib3@1.2.3 -> python:otherlib@1.2.3\"}),\n\t\t\texpectedPackages: []string{\"python:otherlib@1.2.3\", \"python:urllib3@1.2.3\"},\n\t\t},\n\t\t{\n\t\t\tname: \"excludes multiple package by overlap\",\n\t\t\tsbom: catalogWithOverlaps(\n\t\t\t\t[]string{\"apk:go@1.18\", \"apk:node@19.2-r1\", \"binary:node@19.2\", \"apk:python@3.9-r9\", \"binary:python@3.9\"},\n\t\t\t\t[]string{\"apk:node@19.2-r1 -> binary:node@19.2\", \"apk:python@3.9-r9 -> binary:python@3.9\"}),\n\t\t\texpectedPackages: []string{\"apk:go@1.18\", \"apk:node@19.2-r1\", \"apk:python@3.9-r9\"},\n\t\t},\n\t\t{\n\t\t\tname: \"does not exclude with different types\",\n\t\t\tsbom: catalogWithOverlaps(\n\t\t\t\t[]string{\"rpm:node@19.2-r1\", \"apk:node@19.2\"},\n\t\t\t\t[]string{\"rpm:node@19.2-r1 -> apk:node@19.2\"}),\n\t\t\texpectedPackages: []string{\"apk:node@19.2\", \"rpm:node@19.2-r1\"},\n\t\t},\n\t\t{\n\t\t\tname: \"does not exclude if OS package owns OS package\",\n\t\t\tsbom: catalogWithOverlaps(\n\t\t\t\t[]string{\"rpm:perl@5.3-r1\", \"rpm:libperl@5.3\"},\n\t\t\t\t[]string{\"rpm:perl@5.3-r1 -> rpm:libperl@5.3\"}),\n\t\t\texpectedPackages: []string{\"rpm:libperl@5.3\", \"rpm:perl@5.3-r1\"},\n\t\t},\n\t\t{\n\t\t\tname: \"does not exclude if owning package is non-OS\",\n\t\t\tsbom: catalogWithOverlaps(\n\t\t\t\t[]string{\"python:urllib3@1.2.3\", \"python:otherlib@1.2.3\"},\n\t\t\t\t[]string{\"python:urllib3@1.2.3 -> python:otherlib@1.2.3\"}),\n\t\t\texpectedPackages: []string{\"python:otherlib@1.2.3\", \"python:urllib3@1.2.3\"},\n\t\t},\n\t\t{\n\t\t\tname: \"python bindings for system RPM install\",\n\t\t\tsbom: withLinuxRelease(catalogWithOverlaps(\n\t\t\t\t[]string{\"rpm:python3-rpm@4.14.3-26.el8\", \"python:rpm@4.14.3\"},\n\t\t\t\t[]string{\"rpm:python3-rpm@4.14.3-26.el8 -> python:rpm@4.14.3\"}), \"rhel\"),\n\t\t\texpectedPackages: []string{\"rpm:python3-rpm@4.14.3-26.el8\"},\n\t\t},\n\t\t{\n\t\t\tname: \"amzn linux doesn't remove packages in this way\",\n\t\t\tsbom: withLinuxRelease(catalogWithOverlaps(\n\t\t\t\t[]string{\"rpm:python3-rpm@4.14.3-26.el8\", \"python:rpm@4.14.3\"},\n\t\t\t\t[]string{\"rpm:python3-rpm@4.14.3-26.el8 -> python:rpm@4.14.3\"}), \"amzn\"),\n\t\t\texpectedPackages: []string{\"rpm:python3-rpm@4.14.3-26.el8\", \"python:rpm@4.14.3\"},\n\t\t},\n\t\t{\n\t\t\tname: \"remove overlapping package when parent version is prefix of child version\",\n\t\t\tsbom: withLinuxRelease(catalogWithOverlaps(\n\t\t\t\t[]string{\"rpm:kernel-rt-core@5.14.0-503.40.1.el9_5\", \"linux-kernel:linux-kernel@5.14.0-503.40.1.el9_5.x86_64+rt\"},\n\t\t\t\t[]string{\"rpm:kernel-rt-core@5.14.0-503.40.1.el9_5 -> linux-kernel:linux-kernel@5.14.0-503.40.1.el9_5.x86_64+rt\"}), \"rhel\"),\n\t\t\texpectedPackages: []string{\"rpm:kernel-rt-core@5.14.0-503.40.1.el9_5\"},\n\t\t},\n\t\t{\n\t\t\tname: \"remove overlapping package when child version is prefix of parent version\",\n\t\t\tsbom: withLinuxRelease(catalogWithOverlaps(\n\t\t\t\t[]string{\"rpm:kernel-rt-core@5.14.0-503.40.1.el9_5+rt\", \"linux-kernel:linux-kernel@5.14.0-503.40.1.el9_5\"},\n\t\t\t\t[]string{\"rpm:kernel-rt-core@5.14.0-503.40.1.el9_5+rt -> linux-kernel:linux-kernel@5.14.0-503.40.1.el9_5\"}), \"rhel\"),\n\t\t\texpectedPackages: []string{\"rpm:kernel-rt-core@5.14.0-503.40.1.el9_5+rt\"},\n\t\t},\n\t\t{\n\t\t\tname: \"do not remove overlapping package when versions are not similar\",\n\t\t\tsbom: withLinuxRelease(catalogWithOverlaps(\n\t\t\t\t[]string{\"rpm:kernel@5.14.0-503.40.1.el9_5\", \"linux-kernel:linux-kernel@6.17\"},\n\t\t\t\t[]string{\"rpm:kernel@5.14.0-503.40.1.el9_5 -> linux-kernel:linux-kernel@6.17\"}), \"rhel\"),\n\t\t\texpectedPackages: []string{\"rpm:kernel@5.14.0-503.40.1.el9_5\", \"linux-kernel:linux-kernel@6.17\"},\n\t\t},\n\t}\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\td := distro.FromRelease(test.sbom.Artifacts.LinuxDistribution, distro.DefaultFixChannels())\n\t\t\tcatalog := removePackagesByOverlap(test.sbom.Artifacts.Packages, test.sbom.Relationships, d)\n\t\t\tpkgs := FromCollection(catalog, SynthesisConfig{})\n\t\t\tvar pkgNames []string\n\t\t\tfor _, p := range pkgs {\n\t\t\t\tpkgNames = append(pkgNames, fmt.Sprintf(\"%s:%s@%s\", p.Type, p.Name, p.Version))\n\t\t\t}\n\t\t\tassert.EqualValues(t, test.expectedPackages, pkgNames)\n\t\t})\n\t}\n}\n\nfunc catalogWithOverlaps(packages []string, overlaps []string) *sbom.SBOM {\n\tvar pkgs []syftPkg.Package\n\tvar relationships []artifact.Relationship\n\n\ttoPkg := func(str string) syftPkg.Package {\n\t\tvar typ, name, version string\n\t\ts := strings.Split(strings.TrimSpace(str), \":\")\n\t\tif len(s) > 1 {\n\t\t\ttyp = s[0]\n\t\t\tstr = s[1]\n\t\t}\n\t\ts = strings.Split(str, \"@\")\n\t\tname = s[0]\n\t\tif len(s) > 1 {\n\t\t\tversion = s[1]\n\t\t}\n\n\t\tp := syftPkg.Package{\n\t\t\tType:    syftPkg.Type(typ),\n\t\t\tName:    name,\n\t\t\tVersion: version,\n\t\t}\n\t\tp.SetID()\n\n\t\treturn p\n\t}\n\n\tfor _, pkg := range packages {\n\t\tp := toPkg(pkg)\n\t\tpkgs = append(pkgs, p)\n\t}\n\n\tfor i, overlap := range overlaps {\n\t\tparts := strings.Split(overlap, \"->\")\n\t\tif len(parts) < 2 {\n\t\t\tpanic(\"invalid overlap, use -> to specify, e.g.: pkg1->pkg2\")\n\t\t}\n\t\tfrom := toPkg(parts[0])\n\t\tto := toPkg(parts[1])\n\n\t\t// The catalog will type check whether To or From is a pkg.Package or a *pkg.Package.\n\t\t// Previously, there was a bug where Grype assumed that From was always a pkg.Package.\n\t\t// Therefore, intentionally mix pointer and non-pointer packages to prevent Grype from\n\t\t// assuming which is which again. (The correct usage, calling catalog.Package, always\n\t\t// returns a *pkg.Package, and doesn't rely on any type assertion.)\n\t\tif i%2 == 0 {\n\t\t\trelationships = append(relationships, artifact.Relationship{\n\t\t\t\tFrom: &from,\n\t\t\t\tTo:   &to,\n\t\t\t\tType: artifact.OwnershipByFileOverlapRelationship,\n\t\t\t})\n\t\t} else {\n\t\t\trelationships = append(relationships, artifact.Relationship{\n\t\t\t\tFrom: from,\n\t\t\t\tTo:   to,\n\t\t\t\tType: artifact.OwnershipByFileOverlapRelationship,\n\t\t\t})\n\t\t}\n\t}\n\n\tcatalog := syftPkg.NewCollection(pkgs...)\n\n\treturn &sbom.SBOM{\n\t\tArtifacts: sbom.Artifacts{\n\t\t\tPackages: catalog,\n\t\t},\n\t\tRelationships: relationships,\n\t}\n}\n\nfunc withLinuxRelease(s *sbom.SBOM, id string) *sbom.SBOM {\n\ts.Artifacts.LinuxDistribution = &linux.Release{\n\t\tID: id,\n\t}\n\treturn s\n}\n\nfunc strRef(s string) *string {\n\treturn &s\n}\n"
  },
  {
    "path": "grype/pkg/provider.go",
    "content": "package pkg\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/bmatcuk/doublestar/v2\"\n\t\"github.com/scylladb/go-set/strset\"\n\n\t\"github.com/anchore/grype/grype/distro\"\n\t\"github.com/anchore/grype/grype/version\"\n\t\"github.com/anchore/grype/internal/log\"\n\t\"github.com/anchore/syft/syft/file\"\n\t\"github.com/anchore/syft/syft/sbom\"\n)\n\nvar errDoesNotProvide = fmt.Errorf(\"cannot provide packages from the given source\")\n\n// Provide a set of packages and context metadata describing where they were sourced from.\nfunc Provide(userInput string, config ProviderConfig) ([]Package, Context, *sbom.SBOM, error) {\n\tapplyChannel := getDistroChannelApplier(config.Distro.FixChannels)\n\tif config.Distro.Override != nil {\n\t\tapplyChannel(config.Distro.Override)\n\t\tlog.Infof(\"using distro: %s\", config.Distro.Override.String())\n\t}\n\n\tpackages, ctx, s, err := provide(userInput, config, applyChannel)\n\tif err != nil {\n\t\treturn nil, Context{}, nil, err\n\t}\n\tsetContextDistro(packages, &ctx)\n\n\t// set the distro on each package if there is not already one set\n\tif ctx.Distro != nil {\n\t\tfor i := range packages {\n\t\t\tif packages[i].Distro == nil {\n\t\t\t\tpackages[i].Distro = ctx.Distro\n\t\t\t}\n\t\t}\n\n\t\tif config.Distro.Override == nil {\n\t\t\tlog.Infof(\"using distro: %s\", ctx.Distro.String())\n\t\t}\n\t}\n\n\treturn packages, ctx, s, nil\n}\n\n// buildChannelIndex creates a map of distro IDs to their applicable fix channels\nfunc buildChannelIndex(channels []distro.FixChannel) map[string]distro.FixChannels {\n\tidx := make(map[string]distro.FixChannels, len(channels))\n\tfor _, c := range channels {\n\t\tif c.Name == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tfor _, id := range c.IDs {\n\t\t\tif id == \"\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tid = strings.ToLower(id)\n\t\t\tidx[id] = append(idx[id], c)\n\t\t}\n\t}\n\treturn idx\n}\n\nfunc getDistroChannelApplier(channels []distro.FixChannel) func(d *distro.Distro) bool {\n\tidx := buildChannelIndex(channels)\n\n\treturn func(d *distro.Distro) bool {\n\t\tif d == nil {\n\t\t\treturn false\n\t\t}\n\n\t\tid := strings.ToLower(d.ID())\n\t\tchannels, ok := idx[id]\n\t\tif !ok {\n\t\t\treturn false\n\t\t}\n\n\t\treturn applyChannelsToDistro(d, channels)\n\t}\n}\n\n// applyChannelsToDistro applies fix channels to a distro based on channel configuration\nfunc applyChannelsToDistro(d *distro.Distro, channels distro.FixChannels) bool {\n\tvar result []string\n\texisting := strset.New(d.Channels...)\n\tver := version.New(d.Version, version.SemanticFormat)\n\n\tshouldReview := func(channel distro.FixChannel) bool {\n\t\tif channel.Versions != nil && ver != nil {\n\t\t\tisApplicable, err := channel.Versions.Satisfied(ver)\n\t\t\tif err != nil {\n\t\t\t\tlog.WithFields(\"error\", err, \"constraint\", channel.Versions).Debugf(\"unable to determine if channel %q is applicable for distro %q with version %q\", channel.Name, d.Type, ver)\n\t\t\t\treturn true\n\t\t\t}\n\t\t\treturn isApplicable\n\t\t}\n\t\treturn true\n\t}\n\n\tvar modified bool\n\tfor _, channel := range channels {\n\t\tif channel.Name == \"\" {\n\t\t\tcontinue\n\t\t}\n\n\t\tif !shouldReview(channel) {\n\t\t\tlog.WithFields(\"channel\", channel.Name, \"distro\", d.Type, \"version\", ver).Debugf(\"skipping channel %q for distro %q with version %q\", channel.Name, d.Type, ver)\n\t\t\tcontinue\n\t\t}\n\n\t\tswitch channel.Apply {\n\t\tcase distro.ChannelNeverEnabled:\n\t\t\tif existing.Has(channel.Name) {\n\t\t\t\tmodified = true\n\t\t\t}\n\t\tcase distro.ChannelAlwaysEnabled:\n\t\t\tresult = append(result, channel.Name)\n\t\t\tif !existing.Has(channel.Name) {\n\t\t\t\tmodified = true\n\t\t\t}\n\t\tcase distro.ChannelConditionallyEnabled:\n\t\t\tif existing.Has(channel.Name) {\n\t\t\t\tresult = append(result, channel.Name)\n\t\t\t}\n\t\t}\n\t}\n\n\td.Channels = result\n\treturn modified\n}\n\n// Provide a set of packages and context metadata describing where they were sourced from.\nfunc provide(userInput string, config ProviderConfig, applyChannel func(d *distro.Distro) bool) ([]Package, Context, *sbom.SBOM, error) {\n\tpackages, ctx, s, err := purlProvider(userInput, config, applyChannel)\n\tif !errors.Is(err, errDoesNotProvide) {\n\t\tlog.WithFields(\"input\", userInput).Trace(\"interpreting input as one or more PURLs\")\n\t\treturn packages, ctx, s, err\n\t}\n\n\tpackages, ctx, s, err = cpeProvider(userInput, config)\n\tif !errors.Is(err, errDoesNotProvide) {\n\t\tlog.WithFields(\"input\", userInput).Trace(\"interpreting input as a one or more CPEs\")\n\t\treturn packages, ctx, s, err\n\t}\n\n\tpackages, ctx, s, err = syftSBOMProvider(userInput, config, applyChannel)\n\tif !errors.Is(err, errDoesNotProvide) {\n\t\tif len(config.Exclusions) > 0 {\n\t\t\tvar exclusionsErr error\n\t\t\tpackages, exclusionsErr = filterPackageExclusions(packages, config.Exclusions)\n\t\t\tif exclusionsErr != nil {\n\t\t\t\treturn nil, ctx, s, exclusionsErr\n\t\t\t}\n\t\t}\n\t\tlog.WithFields(\"input\", userInput).Trace(\"interpreting input as an SBOM document\")\n\t\treturn packages, ctx, s, err\n\t}\n\n\tlog.WithFields(\"input\", userInput).Trace(\"passing input to syft for interpretation\")\n\treturn syftProvider(userInput, config, applyChannel)\n}\n\n// This will filter the provided packages list based on a set of exclusion expressions. Globs\n// are allowed for the exclusions. A package will be *excluded* only if *all locations* match\n// one of the provided exclusions.\nfunc filterPackageExclusions(packages []Package, exclusions []string) ([]Package, error) {\n\tvar out []Package\n\tfor _, pkg := range packages {\n\t\tincludePackage := true\n\t\tlocations := pkg.Locations.ToSlice()\n\t\tif len(locations) > 0 {\n\t\t\tincludePackage = false\n\t\t\t// require ALL locations to be excluded for the package to be excluded\n\t\tlocation:\n\t\t\tfor _, location := range locations {\n\t\t\t\tfor _, exclusion := range exclusions {\n\t\t\t\t\tmatch, err := locationMatches(location, exclusion)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn nil, err\n\t\t\t\t\t}\n\t\t\t\t\tif match {\n\t\t\t\t\t\tcontinue location\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t// if this point is reached, one location has not matched any exclusion, include the package\n\t\t\t\tincludePackage = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif includePackage {\n\t\t\tout = append(out, pkg)\n\t\t}\n\t}\n\treturn out, nil\n}\n\n// Test a location RealPath and VirtualPath for a match against the exclusion parameter.\n// The exclusion allows glob expressions such as `/usr/**` or `**/*.json`. If the exclusion\n// is an invalid pattern, an error is returned; otherwise, the resulting boolean indicates a match.\nfunc locationMatches(location file.Location, exclusion string) (bool, error) {\n\tmatchesRealPath, err := doublestar.Match(exclusion, location.RealPath)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tmatchesVirtualPath, err := doublestar.Match(exclusion, location.AccessPath)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\treturn matchesRealPath || matchesVirtualPath, nil\n}\n\nfunc setContextDistro(packages []Package, ctx *Context) {\n\tif ctx.Distro != nil {\n\t\treturn\n\t}\n\tvar singleDistro *distro.Distro\n\tfor _, p := range packages {\n\t\tif p.Distro == nil {\n\t\t\tcontinue\n\t\t}\n\t\tif singleDistro == nil {\n\t\t\tsingleDistro = p.Distro\n\t\t\tcontinue\n\t\t}\n\t\t// if we have a distro already, ensure that the new one matches...\n\t\tif singleDistro.Type != p.Distro.Type ||\n\t\t\tsingleDistro.Version != p.Distro.Version ||\n\t\t\tsingleDistro.Codename != p.Distro.Codename {\n\t\t\t// ...if not then we bail, not setting a singular distro in the context\n\t\t\treturn\n\t\t}\n\t}\n\n\t// if there is one distro (with one version) represented, use that\n\tif singleDistro != nil {\n\t\tctx.Distro = singleDistro\n\t}\n}\n"
  },
  {
    "path": "grype/pkg/provider_config.go",
    "content": "package pkg\n\nimport (\n\t\"github.com/anchore/grype/grype/distro\"\n\t\"github.com/anchore/stereoscope/pkg/image\"\n\t\"github.com/anchore/syft/syft\"\n)\n\ntype ProviderConfig struct {\n\tSyftProviderConfig\n\tSynthesisConfig\n}\n\ntype SyftProviderConfig struct {\n\tSBOMOptions            *syft.CreateSBOMConfig\n\tRegistryOptions        *image.RegistryOptions\n\tPlatform               string\n\tExclusions             []string\n\tName                   string\n\tDefaultImagePullSource string\n\tSources                []string\n}\n\ntype SynthesisConfig struct {\n\tGenerateMissingCPEs bool\n\tDistro              DistroConfig\n}\n\ntype DistroConfig struct {\n\tOverride    *distro.Distro\n\tFixChannels []distro.FixChannel\n}\n"
  },
  {
    "path": "grype/pkg/provider_test.go",
    "content": "package pkg\n\nimport (\n\t\"testing\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/google/go-cmp/cmp/cmpopts\"\n\t\"github.com/stretchr/testify/assert\"\n\n\t\"github.com/anchore/grype/grype/distro\"\n\t\"github.com/anchore/grype/grype/version\"\n\t\"github.com/anchore/stereoscope/pkg/imagetest\"\n\t\"github.com/anchore/syft/syft\"\n\t\"github.com/anchore/syft/syft/cataloging\"\n\t\"github.com/anchore/syft/syft/file\"\n)\n\nfunc TestProviderLocationExcludes(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tfixture  string\n\t\texcludes []string\n\t\texpected []string\n\t\twantErr  assert.ErrorAssertionFunc\n\t}{\n\t\t{\n\t\t\tname:     \"exclude everything\",\n\t\t\tfixture:  \"testdata/syft-spring.json\",\n\t\t\texcludes: []string{\"**\"},\n\t\t\texpected: []string{},\n\t\t},\n\t\t{\n\t\t\tname:     \"exclude specific real path match\",\n\t\t\tfixture:  \"testdata/syft-spring.json\",\n\t\t\texcludes: []string{\"**/tomcat*.jar\"},\n\t\t\texpected: []string{\"charsets\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"include everything with no match\",\n\t\t\tfixture:  \"testdata/syft-spring.json\",\n\t\t\texcludes: []string{\"**/asdf*.jar\"},\n\t\t\texpected: []string{\"charsets\", \"tomcat-embed-el\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"include everything with no excludes\",\n\t\t\tfixture:  \"testdata/syft-spring.json\",\n\t\t\texcludes: []string{},\n\t\t\texpected: []string{\"charsets\", \"tomcat-embed-el\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"exclusions must not hide parsing error\",\n\t\t\tfixture:  \"testdata/bad-sbom.json\",\n\t\t\texcludes: []string{\"**/some-glob/*\"},\n\t\t\twantErr:  assert.Error,\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tsbomConfig := syft.DefaultCreateSBOMConfig().\n\t\t\t\tWithCatalogerSelection(cataloging.NewSelectionRequest().\n\t\t\t\t\tWithRemovals(\"rpm-db-cataloger\"))\n\t\t\tcfg := ProviderConfig{\n\t\t\t\tSyftProviderConfig: SyftProviderConfig{\n\t\t\t\t\tExclusions:  test.excludes,\n\t\t\t\t\tSBOMOptions: sbomConfig,\n\t\t\t\t},\n\t\t\t}\n\t\t\tif test.wantErr == nil {\n\t\t\t\ttest.wantErr = assert.NoError\n\t\t\t}\n\t\t\tpkgs, _, _, err := Provide(test.fixture, cfg)\n\t\t\ttest.wantErr(t, err)\n\t\t\tif err != nil {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tvar pkgNames []string\n\n\t\t\tfor _, pkg := range pkgs {\n\t\t\t\tpkgNames = append(pkgNames, pkg.Name)\n\t\t\t}\n\n\t\t\tassert.ElementsMatch(t, pkgNames, test.expected)\n\t\t})\n\t}\n}\n\nfunc TestSyftLocationExcludes(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tfixture  string\n\t\texcludes []string\n\t\texpected []string\n\t}{\n\t\t{\n\t\t\tname:     \"exclude everything\",\n\t\t\tfixture:  \"image-simple\",\n\t\t\texcludes: []string{\"**\"},\n\t\t\texpected: []string{},\n\t\t},\n\t\t{\n\t\t\tname:     \"exclude specific real path match\",\n\t\t\tfixture:  \"image-simple\",\n\t\t\texcludes: []string{\"**/nested/package.json\"},\n\t\t\texpected: []string{\"top-level-package\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"include everything with no match\",\n\t\t\tfixture:  \"image-simple\",\n\t\t\texcludes: []string{\"**/asdf*.json\"},\n\t\t\texpected: []string{\"nested-package\", \"top-level-package\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"include everything with no excludes\",\n\t\t\tfixture:  \"image-simple\",\n\t\t\texcludes: []string{},\n\t\t\texpected: []string{\"nested-package\", \"top-level-package\"},\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tuserInput := imagetest.GetFixtureImageTarPath(t, test.fixture)\n\t\t\tsbomConfig := syft.DefaultCreateSBOMConfig().\n\t\t\t\tWithCatalogerSelection(cataloging.NewSelectionRequest().\n\t\t\t\t\tWithRemovals(\"rpm-db-cataloger\"))\n\t\t\tcfg := ProviderConfig{\n\t\t\t\tSyftProviderConfig: SyftProviderConfig{\n\t\t\t\t\tExclusions:  test.excludes,\n\t\t\t\t\tSBOMOptions: sbomConfig,\n\t\t\t\t},\n\t\t\t}\n\t\t\tpkgs, _, _, err := Provide(userInput, cfg)\n\n\t\t\tassert.NoErrorf(t, err, \"error calling Provide function\")\n\n\t\t\tvar pkgNames []string\n\n\t\t\tfor _, pkg := range pkgs {\n\t\t\t\tpkgNames = append(pkgNames, pkg.Name)\n\t\t\t}\n\n\t\t\tassert.ElementsMatch(t, pkgNames, test.expected)\n\t\t})\n\t}\n}\n\nfunc Test_filterPackageExclusions(t *testing.T) {\n\ttests := []struct {\n\t\tname       string\n\t\tlocations  [][]string\n\t\texclusions []string\n\t\texpected   int\n\t}{\n\t\t{\n\t\t\tname:       \"exclude nothing\",\n\t\t\tlocations:  [][]string{{\"/foo\", \"/bar\"}, {\"/foo\", \"/bar\"}},\n\t\t\texclusions: []string{\"/asdf/**\"},\n\t\t\texpected:   2,\n\t\t},\n\t\t{\n\t\t\tname:       \"exclude everything\",\n\t\t\tlocations:  [][]string{{\"/foo\", \"/bar\"}, {\"/foo\", \"/bar\"}},\n\t\t\texclusions: []string{\"**\"},\n\t\t\texpected:   0,\n\t\t},\n\t\t{\n\t\t\tname:       \"exclude based on all location match\",\n\t\t\tlocations:  [][]string{{\"/foo1\", \"/bar1\"}, {\"/foo2\", \"/bar2\"}},\n\t\t\texclusions: []string{\"/foo2\", \"/bar2\"},\n\t\t\texpected:   1,\n\t\t},\n\t\t{\n\t\t\tname:       \"don't exclude with single location match\",\n\t\t\tlocations:  [][]string{{\"/foo1\", \"/bar1\"}, {\"/foo2\", \"/bar2\"}},\n\t\t\texclusions: []string{\"/foo1\", \"/foo2\"},\n\t\t\texpected:   2,\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tvar packages []Package\n\t\t\tfor _, pkg := range test.locations {\n\t\t\t\tlocations := file.NewLocationSet()\n\t\t\t\tfor _, l := range pkg {\n\t\t\t\t\tlocations.Add(\n\t\t\t\t\t\tfile.NewVirtualLocation(l, l),\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t\tpackages = append(packages, Package{Locations: locations})\n\t\t\t}\n\t\t\tfiltered, err := filterPackageExclusions(packages, test.exclusions)\n\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Len(t, filtered, test.expected)\n\t\t})\n\t}\n}\n\nfunc Test_matchesLocation(t *testing.T) {\n\ttests := []struct {\n\t\tname        string\n\t\trealPath    string\n\t\tvirtualPath string\n\t\tmatch       string\n\t\texpected    bool\n\t}{\n\t\t{\n\t\t\tname:        \"doesn't match real\",\n\t\t\trealPath:    \"/asdf\",\n\t\t\tvirtualPath: \"\",\n\t\t\tmatch:       \"/usr\",\n\t\t\texpected:    false,\n\t\t},\n\t\t{\n\t\t\tname:        \"doesn't match virtual\",\n\t\t\trealPath:    \"\",\n\t\t\tvirtualPath: \"/asdf\",\n\t\t\tmatch:       \"/usr\",\n\t\t\texpected:    false,\n\t\t},\n\t\t{\n\t\t\tname:        \"does match real\",\n\t\t\trealPath:    \"/usr/foo\",\n\t\t\tvirtualPath: \"\",\n\t\t\tmatch:       \"/usr/**\",\n\t\t\texpected:    true,\n\t\t},\n\t\t{\n\t\t\tname:        \"does match virtual\",\n\t\t\trealPath:    \"\",\n\t\t\tvirtualPath: \"/usr/bar/oof.txt\",\n\t\t\tmatch:       \"/usr/**\",\n\t\t\texpected:    true,\n\t\t},\n\t\t{\n\t\t\tname:        \"does match file\",\n\t\t\trealPath:    \"\",\n\t\t\tvirtualPath: \"/usr/bar/oof.txt\",\n\t\t\tmatch:       \"**/*.txt\",\n\t\t\texpected:    true,\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tmatches, err := locationMatches(file.NewVirtualLocation(test.realPath, test.virtualPath), test.match)\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, test.expected, matches)\n\t\t})\n\t}\n}\n\nfunc Test_getDistroChannelApplier(t *testing.T) {\n\n\tdefaultOSGen := func() *distro.Distro {\n\t\treturn distro.NewFromNameVersion(\"rhel\", \"8.4\")\n\t}\n\n\ttests := []struct {\n\t\tname     string\n\t\tchannels []distro.FixChannel\n\t\tdistro   func() *distro.Distro\n\t\twant     []string\n\t}{\n\t\t{\n\t\t\tname:     \"nil distro\",\n\t\t\tchannels: distro.DefaultFixChannels(),\n\t\t\tdistro:   func() *distro.Distro { return nil },\n\t\t\twant:     nil,\n\t\t},\n\t\t{\n\t\t\tname:     \"no matching channel\",\n\t\t\tchannels: distro.DefaultFixChannels(),\n\t\t\tdistro: func() *distro.Distro {\n\t\t\t\treturn distro.NewFromNameVersion(\"ubuntu\", \"20.04\")\n\t\t\t},\n\t\t\twant: nil,\n\t\t},\n\t\t{\n\t\t\tname: \"channel never enabled\",\n\t\t\tchannels: []distro.FixChannel{\n\t\t\t\t{\n\t\t\t\t\tName:  \"test-channel\",\n\t\t\t\t\tIDs:   []string{\"rhel\"},\n\t\t\t\t\tApply: distro.ChannelNeverEnabled,\n\t\t\t\t},\n\t\t\t},\n\t\t\tdistro: defaultOSGen,\n\t\t\twant:   nil,\n\t\t},\n\t\t{\n\t\t\tname: \"channel always enabled\",\n\t\t\tchannels: []distro.FixChannel{\n\t\t\t\t{\n\t\t\t\t\tName:  \"eus\",\n\t\t\t\t\tIDs:   []string{\"rhel\"},\n\t\t\t\t\tApply: distro.ChannelAlwaysEnabled,\n\t\t\t\t},\n\t\t\t},\n\t\t\tdistro: defaultOSGen,\n\t\t\twant:   []string{\"eus\"},\n\t\t},\n\t\t{\n\t\t\tname: \"case insensitive matching\",\n\t\t\tchannels: []distro.FixChannel{\n\t\t\t\t{\n\t\t\t\t\tName:  \"eus\",\n\t\t\t\t\tIDs:   []string{\"RHEL\"},\n\t\t\t\t\tApply: distro.ChannelAlwaysEnabled,\n\t\t\t\t},\n\t\t\t},\n\t\t\tdistro: defaultOSGen,\n\t\t\twant:   []string{\"eus\"},\n\t\t},\n\t\t{\n\t\t\tname: \"multiple IDs in channel\",\n\t\t\tchannels: []distro.FixChannel{\n\t\t\t\t{\n\t\t\t\t\tName:  \"test-channel\",\n\t\t\t\t\tIDs:   []string{\"centos\", \"rhel\", \"fedora\"},\n\t\t\t\t\tApply: distro.ChannelAlwaysEnabled,\n\t\t\t\t},\n\t\t\t},\n\t\t\tdistro: defaultOSGen,\n\t\t\twant:   []string{\"test-channel\"},\n\t\t},\n\t\t{\n\t\t\tname: \"empty channel name skipped\",\n\t\t\tchannels: []distro.FixChannel{\n\t\t\t\t{\n\t\t\t\t\tName:  \"\",\n\t\t\t\t\tIDs:   []string{\"rhel\"},\n\t\t\t\t\tApply: distro.ChannelAlwaysEnabled,\n\t\t\t\t},\n\t\t\t},\n\t\t\tdistro: defaultOSGen,\n\t\t\twant:   nil,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tapplier := getDistroChannelApplier(tt.channels)\n\t\t\td := tt.distro()\n\n\t\t\tapplier(d)\n\n\t\t\tif d != nil {\n\t\t\t\tassert.Equal(t, tt.want, d.Channels)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_applyChannelsToDistro(t *testing.T) {\n\ttests := []struct {\n\t\tname             string\n\t\tdistro           func() *distro.Distro\n\t\tchannels         distro.FixChannels\n\t\texpectedResult   []string\n\t\texpectedModified bool\n\t}{\n\t\t{\n\t\t\tname:   \"always enabled channel adds new channel\",\n\t\t\tdistro: func() *distro.Distro { return distro.NewFromNameVersion(\"rhel\", \"8.4\") },\n\t\t\tchannels: distro.FixChannels{\n\t\t\t\t{\n\t\t\t\t\tName:  \"eus\",\n\t\t\t\t\tApply: distro.ChannelAlwaysEnabled,\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedResult:   []string{\"eus\"},\n\t\t\texpectedModified: true,\n\t\t},\n\t\t{\n\t\t\tname: \"always enabled channel keeps existing channel\",\n\t\t\tdistro: func() *distro.Distro {\n\t\t\t\td := distro.NewFromNameVersion(\"rhel\", \"8.4\")\n\t\t\t\td.Channels = []string{\"eus\"}\n\t\t\t\treturn d\n\t\t\t},\n\t\t\tchannels: distro.FixChannels{\n\t\t\t\t{\n\t\t\t\t\tName:  \"eus\",\n\t\t\t\t\tApply: distro.ChannelAlwaysEnabled,\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedResult:   []string{\"eus\"},\n\t\t\texpectedModified: false,\n\t\t},\n\t\t{\n\t\t\tname: \"conditionally enabled channel keeps existing channel\",\n\t\t\tdistro: func() *distro.Distro {\n\t\t\t\td := distro.NewFromNameVersion(\"rhel\", \"8.4\")\n\t\t\t\td.Channels = []string{\"eus\"}\n\t\t\t\treturn d\n\t\t\t},\n\t\t\tchannels: distro.FixChannels{\n\t\t\t\t{\n\t\t\t\t\tName:  \"eus\",\n\t\t\t\t\tApply: distro.ChannelConditionallyEnabled,\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedResult:   []string{\"eus\"},\n\t\t\texpectedModified: false,\n\t\t},\n\t\t{\n\t\t\tname:   \"conditionally enabled channel does not add missing channel\",\n\t\t\tdistro: func() *distro.Distro { return distro.NewFromNameVersion(\"rhel\", \"8.4\") },\n\t\t\tchannels: distro.FixChannels{\n\t\t\t\t{\n\t\t\t\t\tName:  \"eus\",\n\t\t\t\t\tApply: distro.ChannelConditionallyEnabled,\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedResult:   []string{},\n\t\t\texpectedModified: false,\n\t\t},\n\t\t{\n\t\t\tname: \"never enabled channel removes existing channel\",\n\t\t\tdistro: func() *distro.Distro {\n\t\t\t\td := distro.NewFromNameVersion(\"rhel\", \"8.4\")\n\t\t\t\td.Channels = []string{\"eus\"}\n\t\t\t\treturn d\n\t\t\t},\n\t\t\tchannels: distro.FixChannels{\n\t\t\t\t{\n\t\t\t\t\tName:  \"eus\",\n\t\t\t\t\tApply: distro.ChannelNeverEnabled,\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedResult:   []string{},\n\t\t\texpectedModified: true,\n\t\t},\n\t\t{\n\t\t\tname:   \"never enabled channel with no existing channel\",\n\t\t\tdistro: func() *distro.Distro { return distro.NewFromNameVersion(\"rhel\", \"8.4\") },\n\t\t\tchannels: distro.FixChannels{\n\t\t\t\t{\n\t\t\t\t\tName:  \"eus\",\n\t\t\t\t\tApply: distro.ChannelNeverEnabled,\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedResult:   []string{},\n\t\t\texpectedModified: false,\n\t\t},\n\t\t{\n\t\t\tname:   \"empty channel name is skipped\",\n\t\t\tdistro: func() *distro.Distro { return distro.NewFromNameVersion(\"rhel\", \"8.4\") },\n\t\t\tchannels: distro.FixChannels{\n\t\t\t\t{\n\t\t\t\t\tName:  \"\",\n\t\t\t\t\tApply: distro.ChannelAlwaysEnabled,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tName:  \"eus\",\n\t\t\t\t\tApply: distro.ChannelAlwaysEnabled,\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedResult:   []string{\"eus\"},\n\t\t\texpectedModified: true,\n\t\t},\n\t\t{\n\t\t\tname:   \"version constraint allows channel\",\n\t\t\tdistro: func() *distro.Distro { return distro.NewFromNameVersion(\"rhel\", \"8.4\") },\n\t\t\tchannels: distro.FixChannels{\n\t\t\t\t{\n\t\t\t\t\tName:     \"eus\",\n\t\t\t\t\tApply:    distro.ChannelAlwaysEnabled,\n\t\t\t\t\tVersions: version.MustGetConstraint(\">= 8.0\", version.SemanticFormat),\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedResult:   []string{\"eus\"},\n\t\t\texpectedModified: true,\n\t\t},\n\t\t{\n\t\t\tname:   \"version constraint blocks channel\",\n\t\t\tdistro: func() *distro.Distro { return distro.NewFromNameVersion(\"rhel\", \"7.9\") },\n\t\t\tchannels: distro.FixChannels{\n\t\t\t\t{\n\t\t\t\t\tName:     \"eus\",\n\t\t\t\t\tApply:    distro.ChannelAlwaysEnabled,\n\t\t\t\t\tVersions: version.MustGetConstraint(\">= 8.0\", version.SemanticFormat),\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedResult:   []string{},\n\t\t\texpectedModified: false,\n\t\t},\n\t\t{\n\t\t\tname: \"multiple channels with different behaviors\",\n\t\t\tdistro: func() *distro.Distro {\n\t\t\t\td := distro.NewFromNameVersion(\"rhel\", \"8.4\")\n\t\t\t\td.Channels = []string{\"eus\", \"optional\"}\n\t\t\t\treturn d\n\t\t\t},\n\t\t\tchannels: distro.FixChannels{\n\t\t\t\t{\n\t\t\t\t\tName:  \"eus\",\n\t\t\t\t\tApply: distro.ChannelConditionallyEnabled,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tName:  \"main\",\n\t\t\t\t\tApply: distro.ChannelAlwaysEnabled,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tName:  \"optional\",\n\t\t\t\t\tApply: distro.ChannelNeverEnabled,\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedResult:   []string{\"eus\", \"main\"},\n\t\t\texpectedModified: true,\n\t\t},\n\t\t{\n\t\t\tname:   \"invalid version string defaults to allowing channel\",\n\t\t\tdistro: func() *distro.Distro { return distro.NewFromNameVersion(\"rhel\", \"invalid-version\") },\n\t\t\tchannels: distro.FixChannels{\n\t\t\t\t{\n\t\t\t\t\tName:     \"eus\",\n\t\t\t\t\tApply:    distro.ChannelAlwaysEnabled,\n\t\t\t\t\tVersions: version.MustGetConstraint(\">= 8.0\", version.SemanticFormat),\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedResult:   []string{\"eus\"},\n\t\t\texpectedModified: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\td := tt.distro()\n\n\t\t\tmodified := applyChannelsToDistro(d, tt.channels)\n\n\t\t\tif d := cmp.Diff(tt.expectedResult, d.Channels, cmpopts.EquateEmpty()); d != \"\" {\n\t\t\t\tt.Errorf(\"applyChannelsToDistro() mismatch (-want +got):\\n%s\", d)\n\t\t\t}\n\t\t\tassert.Equal(t, tt.expectedModified, modified)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/pkg/purl_provider.go",
    "content": "package pkg\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"slices\"\n\t\"strings\"\n\n\t\"github.com/anchore/grype/grype/distro\"\n\t\"github.com/anchore/packageurl-go\"\n\t\"github.com/anchore/syft/syft/format\"\n\tsyftPkg \"github.com/anchore/syft/syft/pkg\"\n\t\"github.com/anchore/syft/syft/sbom\"\n\t\"github.com/anchore/syft/syft/source\"\n)\n\nconst (\n\tpurlInputPrefix       = \"purl:\"\n\tsinglePurlInputPrefix = \"pkg:\"\n)\n\ntype PURLLiteralMetadata struct {\n\tPURL string\n}\n\nfunc purlEnhancers(applyChannel func(*distro.Distro) bool) []Enhancer {\n\treturn []Enhancer{setUpstreamsFromPURL, setDistroFromPURL(applyChannel)}\n}\n\nfunc purlProvider(userInput string, config ProviderConfig, applyChannel func(*distro.Distro) bool) ([]Package, Context, *sbom.SBOM, error) {\n\treader, ctx, err := getPurlReader(userInput)\n\tif err != nil {\n\t\treturn nil, Context{}, nil, err\n\t}\n\n\ts, _, _, err := format.Decode(reader)\n\tif s == nil {\n\t\treturn nil, Context{}, nil, fmt.Errorf(\"unable to decode purl: %w\", err)\n\t}\n\n\treturn FromCollection(s.Artifacts.Packages, config.SynthesisConfig, purlEnhancers(applyChannel)...), ctx, s, nil\n}\n\nfunc getPurlReader(userInput string) (r io.Reader, ctx Context, err error) {\n\tif strings.HasPrefix(userInput, singlePurlInputPrefix) {\n\t\tctx.Source = &source.Description{\n\t\t\tMetadata: PURLLiteralMetadata{\n\t\t\t\tPURL: userInput,\n\t\t\t},\n\t\t}\n\t\treturn strings.NewReader(userInput), ctx, nil\n\t}\n\treturn nil, ctx, errDoesNotProvide\n}\n\nfunc setUpstreamsFromPURL(out *Package, purl packageurl.PackageURL, syftPkg syftPkg.Package) {\n\tif len(out.Upstreams) == 0 || out.PURL == \"\" {\n\t\tout.Upstreams = upstreamsFromPURL(purl, syftPkg.Type)\n\t}\n}\n\n// upstreamsFromPURL reads any additional data Grype can use, which is ignored by Syft's PURL conversion\nfunc upstreamsFromPURL(purl packageurl.PackageURL, pkgType syftPkg.Type) (upstreams []UpstreamPackage) {\n\tfor _, qualifier := range purl.Qualifiers {\n\t\tif qualifier.Key == syftPkg.PURLQualifierUpstream {\n\t\t\tfor _, newUpstream := range parseUpstream(purl.Name, qualifier.Value, pkgType) {\n\t\t\t\tif slices.Contains(upstreams, newUpstream) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tupstreams = append(upstreams, newUpstream)\n\t\t\t}\n\t\t}\n\t}\n\treturn upstreams\n}\n\nfunc setDistroFromPURL(applyChannel func(*distro.Distro) bool) func(out *Package, purl packageurl.PackageURL, _ syftPkg.Package) {\n\treturn func(out *Package, purl packageurl.PackageURL, _ syftPkg.Package) {\n\t\tif out.Distro == nil {\n\t\t\tout.Distro = distroFromPURL(purl)\n\t\t\tapplyChannel(out.Distro)\n\t\t}\n\t}\n}\n\n// distroFromPURL reads distro data for Grype can use, which is ignored by Syft's PURL conversion\nfunc distroFromPURL(purl packageurl.PackageURL) (d *distro.Distro) {\n\tvar distroName, distroVersion string\n\n\tfor _, qualifier := range purl.Qualifiers {\n\t\tif qualifier.Key == syftPkg.PURLQualifierDistro {\n\t\t\t// Use shared parsing logic to support -, :, and @ separators\n\t\t\tdistroName, distroVersion = distro.ParseDistroString(qualifier.Value)\n\t\t}\n\t}\n\n\tif distroName != \"\" {\n\t\td = distro.NewFromNameVersion(distroName, distroVersion)\n\t}\n\n\treturn d\n}\n"
  },
  {
    "path": "grype/pkg/purl_provider_test.go",
    "content": "package pkg\n\nimport (\n\t\"testing\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/google/go-cmp/cmp/cmpopts\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/grype/grype/distro\"\n\t\"github.com/anchore/grype/grype/version\"\n\t\"github.com/anchore/syft/syft/pkg\"\n\t\"github.com/anchore/syft/syft/source\"\n)\n\nvar diffOpts = []cmp.Option{\n\tcmpopts.IgnoreFields(Package{}, \"ID\", \"Locations\", \"Licenses\", \"Language\", \"CPEs\"),\n\tcmpopts.IgnoreUnexported(distro.Distro{}),\n}\n\nfunc Test_PurlProvider(t *testing.T) {\n\n\ttests := []struct {\n\t\tname        string\n\t\tuserInput   string\n\t\tchannels    []distro.FixChannel\n\t\twantContext Context\n\t\twantPkgs    []Package\n\t\twantErr     require.ErrorAssertionFunc\n\t}{\n\t\t{\n\t\t\tname:      \"takes a single purl\",\n\t\t\tuserInput: \"pkg:apk/curl@7.61.1\",\n\t\t\tchannels:  testFixChannels(),\n\t\t\twantContext: Context{\n\t\t\t\tSource: &source.Description{\n\t\t\t\t\tMetadata: PURLLiteralMetadata{\n\t\t\t\t\t\tPURL: \"pkg:apk/curl@7.61.1\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantPkgs: []Package{\n\t\t\t\t{\n\t\t\t\t\tName:    \"curl\",\n\t\t\t\t\tVersion: \"7.61.1\",\n\t\t\t\t\tType:    pkg.ApkPkg,\n\t\t\t\t\tPURL:    \"pkg:apk/curl@7.61.1\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:      \"java metadata decoded from purl\",\n\t\t\tuserInput: \"pkg:maven/org.apache.commons/commons-lang3@3.12.0\",\n\t\t\tchannels:  testFixChannels(),\n\t\t\twantContext: Context{\n\t\t\t\tSource: &source.Description{\n\t\t\t\t\tMetadata: PURLLiteralMetadata{\n\t\t\t\t\t\tPURL: \"pkg:maven/org.apache.commons/commons-lang3@3.12.0\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantPkgs: []Package{\n\t\t\t\t{\n\t\t\t\t\tName:    \"commons-lang3\",\n\t\t\t\t\tVersion: \"3.12.0\",\n\t\t\t\t\tType:    pkg.JavaPkg,\n\t\t\t\t\tPURL:    \"pkg:maven/org.apache.commons/commons-lang3@3.12.0\",\n\t\t\t\t\tMetadata: JavaMetadata{\n\t\t\t\t\t\tPomArtifactID: \"commons-lang3\",\n\t\t\t\t\t\tPomGroupID:    \"org.apache.commons\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:      \"os with codename\",\n\t\t\tuserInput: \"pkg:deb/debian/sysv-rc@2.88dsf-59?arch=all&distro=debian-jessie&upstream=sysvinit\",\n\t\t\tchannels:  testFixChannels(),\n\t\t\twantContext: Context{\n\t\t\t\tSource: &source.Description{\n\t\t\t\t\tMetadata: PURLLiteralMetadata{\n\t\t\t\t\t\tPURL: \"pkg:deb/debian/sysv-rc@2.88dsf-59?arch=all&distro=debian-jessie&upstream=sysvinit\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantPkgs: []Package{\n\t\t\t\t{\n\t\t\t\t\tName:    \"sysv-rc\",\n\t\t\t\t\tVersion: \"2.88dsf-59\",\n\t\t\t\t\tType:    pkg.DebPkg,\n\t\t\t\t\tPURL:    \"pkg:deb/debian/sysv-rc@2.88dsf-59?arch=all&distro=debian-jessie&upstream=sysvinit\",\n\t\t\t\t\tDistro:  &distro.Distro{Type: distro.Debian, Version: \"\", Codename: \"jessie\", IDLike: []string{\"debian\"}},\n\t\t\t\t\tUpstreams: []UpstreamPackage{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName: \"sysvinit\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:      \"default upstream\",\n\t\t\tuserInput: \"pkg:apk/libcrypto3@3.3.2?upstream=openssl\",\n\t\t\tchannels:  testFixChannels(),\n\t\t\twantContext: Context{\n\t\t\t\tSource: &source.Description{\n\t\t\t\t\tMetadata: PURLLiteralMetadata{\n\t\t\t\t\t\tPURL: \"pkg:apk/libcrypto3@3.3.2?upstream=openssl\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantPkgs: []Package{\n\t\t\t\t{\n\t\t\t\t\tName:    \"libcrypto3\",\n\t\t\t\t\tVersion: \"3.3.2\",\n\t\t\t\t\tType:    pkg.ApkPkg,\n\t\t\t\t\tPURL:    \"pkg:apk/libcrypto3@3.3.2?upstream=openssl\",\n\t\t\t\t\tUpstreams: []UpstreamPackage{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName: \"openssl\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:      \"upstream with version\",\n\t\t\tuserInput: \"pkg:apk/libcrypto3@3.3.2?upstream=openssl%403.2.1\", // %40 is @\n\t\t\tchannels:  testFixChannels(),\n\t\t\twantContext: Context{\n\t\t\t\tSource: &source.Description{\n\t\t\t\t\tMetadata: PURLLiteralMetadata{\n\t\t\t\t\t\tPURL: \"pkg:apk/libcrypto3@3.3.2?upstream=openssl%403.2.1\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantPkgs: []Package{\n\t\t\t\t{\n\t\t\t\t\tName:    \"libcrypto3\",\n\t\t\t\t\tVersion: \"3.3.2\",\n\t\t\t\t\tType:    pkg.ApkPkg,\n\t\t\t\t\tPURL:    \"pkg:apk/libcrypto3@3.3.2?upstream=openssl%403.2.1\",\n\t\t\t\t\tUpstreams: []UpstreamPackage{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName:    \"openssl\",\n\t\t\t\t\t\t\tVersion: \"3.2.1\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:      \"upstream for source RPM\",\n\t\t\tuserInput: \"pkg:rpm/redhat/systemd-x@239-82.el8_10.2?arch=aarch64&distro=rhel-8.10&upstream=systemd-239-82.el8_10.2.src.rpm\",\n\t\t\tchannels:  testFixChannels(),\n\t\t\twantContext: Context{\n\t\t\t\tSource: &source.Description{\n\t\t\t\t\tMetadata: PURLLiteralMetadata{\n\t\t\t\t\t\tPURL: \"pkg:rpm/redhat/systemd-x@239-82.el8_10.2?arch=aarch64&distro=rhel-8.10&upstream=systemd-239-82.el8_10.2.src.rpm\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantPkgs: []Package{\n\t\t\t\t{\n\t\t\t\t\tName:    \"systemd-x\",\n\t\t\t\t\tVersion: \"239-82.el8_10.2\",\n\t\t\t\t\tType:    pkg.RpmPkg,\n\t\t\t\t\tPURL:    \"pkg:rpm/redhat/systemd-x@239-82.el8_10.2?arch=aarch64&distro=rhel-8.10&upstream=systemd-239-82.el8_10.2.src.rpm\",\n\t\t\t\t\tDistro:  &distro.Distro{Type: distro.RedHat, Version: \"8.10\", Codename: \"\", IDLike: []string{\"redhat\"}},\n\t\t\t\t\tUpstreams: []UpstreamPackage{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName:    \"systemd\",\n\t\t\t\t\t\t\tVersion: \"239-82.el8_10.2\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:      \"RPM with epoch\",\n\t\t\tuserInput: \"pkg:rpm/redhat/dbus-common@1.12.8-26.el8?arch=noarch&distro=rhel-8.10&epoch=1&upstream=dbus-1.12.8-26.el8.src.rpm\",\n\t\t\tchannels:  testFixChannels(),\n\t\t\twantContext: Context{\n\t\t\t\tSource: &source.Description{\n\t\t\t\t\tMetadata: PURLLiteralMetadata{\n\t\t\t\t\t\tPURL: \"pkg:rpm/redhat/dbus-common@1.12.8-26.el8?arch=noarch&distro=rhel-8.10&epoch=1&upstream=dbus-1.12.8-26.el8.src.rpm\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantPkgs: []Package{\n\t\t\t\t{\n\t\t\t\t\tName:    \"dbus-common\",\n\t\t\t\t\tVersion: \"1:1.12.8-26.el8\",\n\t\t\t\t\tType:    pkg.RpmPkg,\n\t\t\t\t\tPURL:    \"pkg:rpm/redhat/dbus-common@1.12.8-26.el8?arch=noarch&distro=rhel-8.10&epoch=1&upstream=dbus-1.12.8-26.el8.src.rpm\",\n\t\t\t\t\tDistro:  &distro.Distro{Type: distro.RedHat, Version: \"8.10\", Codename: \"\", IDLike: []string{\"redhat\"}},\n\t\t\t\t\tUpstreams: []UpstreamPackage{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName:    \"dbus\",\n\t\t\t\t\t\t\tVersion: \"1.12.8-26.el8\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:      \"RPM with rpmmod\",\n\t\t\tuserInput: \"pkg:rpm/redhat/httpd@2.4.37-51?arch=x86_64&distro=rhel-8.7&rpmmod=httpd:2.4\",\n\t\t\tchannels:  testFixChannels(),\n\t\t\twantContext: Context{\n\t\t\t\tSource: &source.Description{\n\t\t\t\t\tMetadata: PURLLiteralMetadata{\n\t\t\t\t\t\tPURL: \"pkg:rpm/redhat/httpd@2.4.37-51?arch=x86_64&distro=rhel-8.7&rpmmod=httpd:2.4\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantPkgs: []Package{\n\t\t\t\t{\n\t\t\t\t\tName:    \"httpd\",\n\t\t\t\t\tVersion: \"2.4.37-51\",\n\t\t\t\t\tType:    pkg.RpmPkg,\n\t\t\t\t\tPURL:    \"pkg:rpm/redhat/httpd@2.4.37-51?arch=x86_64&distro=rhel-8.7&rpmmod=httpd:2.4\",\n\t\t\t\t\tDistro:  &distro.Distro{Type: distro.RedHat, Version: \"8.7\", Codename: \"\", IDLike: []string{\"redhat\"}},\n\t\t\t\t\tMetadata: RpmMetadata{\n\t\t\t\t\t\tModularityLabel: strRef(\"httpd:2.4\"),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:      \"infer context when distro is present for single purl\",\n\t\t\tuserInput: \"pkg:apk/curl@7.61.1?arch=aarch64&distro=alpine-3.20.3\",\n\t\t\tchannels:  testFixChannels(),\n\t\t\twantContext: Context{\n\t\t\t\tSource: &source.Description{\n\t\t\t\t\tMetadata: PURLLiteralMetadata{\n\t\t\t\t\t\tPURL: \"pkg:apk/curl@7.61.1?arch=aarch64&distro=alpine-3.20.3\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantPkgs: []Package{\n\t\t\t\t{\n\t\t\t\t\tName:    \"curl\",\n\t\t\t\t\tVersion: \"7.61.1\",\n\t\t\t\t\tType:    pkg.ApkPkg,\n\t\t\t\t\tPURL:    \"pkg:apk/curl@7.61.1?arch=aarch64&distro=alpine-3.20.3\",\n\t\t\t\t\tDistro:  &distro.Distro{Type: distro.Alpine, Version: \"3.20.3\", Codename: \"\", IDLike: []string{\"alpine\"}},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:      \"include namespace in name when purl is type Golang\",\n\t\t\tuserInput: \"pkg:golang/k8s.io/ingress-nginx@v1.11.2\",\n\t\t\tchannels:  testFixChannels(),\n\t\t\twantContext: Context{\n\t\t\t\tSource: &source.Description{\n\t\t\t\t\tMetadata: PURLLiteralMetadata{PURL: \"pkg:golang/k8s.io/ingress-nginx@v1.11.2\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantPkgs: []Package{\n\t\t\t\t{\n\t\t\t\t\tName:    \"k8s.io/ingress-nginx\",\n\t\t\t\t\tVersion: \"v1.11.2\",\n\t\t\t\t\tType:    pkg.GoModulePkg,\n\t\t\t\t\tPURL:    \"pkg:golang/k8s.io/ingress-nginx@v1.11.2\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:      \"include complex namespace in name when purl is type Golang\",\n\t\t\tuserInput: \"pkg:golang/github.com/wazuh/wazuh@v4.5.0\",\n\t\t\tchannels:  testFixChannels(),\n\t\t\twantContext: Context{\n\t\t\t\tSource: &source.Description{\n\t\t\t\t\tMetadata: PURLLiteralMetadata{PURL: \"pkg:golang/github.com/wazuh/wazuh@v4.5.0\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantPkgs: []Package{\n\t\t\t\t{\n\t\t\t\t\tName:    \"github.com/wazuh/wazuh\",\n\t\t\t\t\tVersion: \"v4.5.0\",\n\t\t\t\t\tType:    pkg.GoModulePkg,\n\t\t\t\t\tPURL:    \"pkg:golang/github.com/wazuh/wazuh@v4.5.0\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:      \"do not include namespace when given blank input blank\",\n\t\t\tuserInput: \"pkg:golang/wazuh@v4.5.0\",\n\t\t\tchannels:  testFixChannels(),\n\t\t\twantContext: Context{\n\t\t\t\tSource: &source.Description{\n\t\t\t\t\tMetadata: PURLLiteralMetadata{PURL: \"pkg:golang/wazuh@v4.5.0\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantPkgs: []Package{\n\t\t\t\t{\n\t\t\t\t\tName:    \"wazuh\",\n\t\t\t\t\tVersion: \"v4.5.0\",\n\t\t\t\t\tType:    pkg.GoModulePkg,\n\t\t\t\t\tPURL:    \"pkg:golang/wazuh@v4.5.0\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:      \"RPM with extended support (auto)\",\n\t\t\tuserInput: \"pkg:rpm/redhat/systemd-x@239-82.el8_10.2?distro=rhel-8.10+eus\",\n\t\t\tchannels:  testFixChannels(), // important! auto applies EUS\n\t\t\twantContext: Context{\n\t\t\t\tSource: &source.Description{\n\t\t\t\t\tMetadata: PURLLiteralMetadata{\n\t\t\t\t\t\tPURL: \"pkg:rpm/redhat/systemd-x@239-82.el8_10.2?distro=rhel-8.10+eus\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantPkgs: []Package{\n\t\t\t\t{\n\t\t\t\t\tName:    \"systemd-x\",\n\t\t\t\t\tVersion: \"239-82.el8_10.2\",\n\t\t\t\t\tType:    pkg.RpmPkg,\n\t\t\t\t\tPURL:    \"pkg:rpm/redhat/systemd-x@239-82.el8_10.2?distro=rhel-8.10+eus\",\n\t\t\t\t\tDistro:  &distro.Distro{Type: distro.RedHat, Version: \"8.10\", Channels: names(\"eus\"), IDLike: []string{\"redhat\"}},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:      \"RPM with extended support (never)\",\n\t\t\tuserInput: \"pkg:rpm/redhat/systemd-x@239-82.el8_10.2?distro=rhel-8.10+eus\",\n\t\t\tchannels: []distro.FixChannel{\n\t\t\t\t{\n\t\t\t\t\tName:  \"eus\",\n\t\t\t\t\tIDs:   []string{\"rhel\"},\n\t\t\t\t\tApply: distro.ChannelNeverEnabled, // important!\n\t\t\t\t},\n\t\t\t},\n\t\t\twantContext: Context{\n\t\t\t\tSource: &source.Description{\n\t\t\t\t\tMetadata: PURLLiteralMetadata{\n\t\t\t\t\t\tPURL: \"pkg:rpm/redhat/systemd-x@239-82.el8_10.2?distro=rhel-8.10+eus\", // the input did hint hat eus, so we leave it\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantPkgs: []Package{\n\t\t\t\t{\n\t\t\t\t\tName:    \"systemd-x\",\n\t\t\t\t\tVersion: \"239-82.el8_10.2\",\n\t\t\t\t\tType:    pkg.RpmPkg,\n\t\t\t\t\tPURL:    \"pkg:rpm/redhat/systemd-x@239-82.el8_10.2?distro=rhel-8.10+eus\",                                 // important! we are NOT patching the channel out of the PURL\n\t\t\t\t\tDistro:  &distro.Distro{Type: distro.RedHat, Version: \"8.10\", Channels: nil, IDLike: []string{\"redhat\"}}, // important! no channel applied\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:      \"RPM without extended support (always)\",\n\t\t\tuserInput: \"pkg:rpm/redhat/systemd-x@239-82.el8_10.2?distro=rhel-8.10\", // important! no channel hint\n\t\t\tchannels: []distro.FixChannel{\n\t\t\t\t{\n\t\t\t\t\tName:  \"eus\",\n\t\t\t\t\tIDs:   []string{\"rhel\"},\n\t\t\t\t\tApply: distro.ChannelAlwaysEnabled, // important!\n\t\t\t\t},\n\t\t\t},\n\t\t\twantContext: Context{\n\t\t\t\tSource: &source.Description{\n\t\t\t\t\tMetadata: PURLLiteralMetadata{\n\t\t\t\t\t\tPURL: \"pkg:rpm/redhat/systemd-x@239-82.el8_10.2?distro=rhel-8.10\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantPkgs: []Package{\n\t\t\t\t{\n\t\t\t\t\tName:    \"systemd-x\",\n\t\t\t\t\tVersion: \"239-82.el8_10.2\",\n\t\t\t\t\tType:    pkg.RpmPkg,\n\t\t\t\t\tPURL:    \"pkg:rpm/redhat/systemd-x@239-82.el8_10.2?distro=rhel-8.10\",                                              // important! we are NOT patching the channel into the PURL\n\t\t\t\t\tDistro:  &distro.Distro{Type: distro.RedHat, Version: \"8.10\", Channels: names(\"eus\"), IDLike: []string{\"redhat\"}}, // important! channel applied\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:      \"RPM without extended support (always) outside of version range\",\n\t\t\tuserInput: \"pkg:rpm/redhat/systemd-x@239-82.el8_10.2?distro=rhel-8.10\", // important! no channel hint\n\t\t\tchannels: []distro.FixChannel{\n\t\t\t\t{\n\t\t\t\t\tName:     \"eus\",\n\t\t\t\t\tIDs:      []string{\"rhel\"},\n\t\t\t\t\tApply:    distro.ChannelAlwaysEnabled,                               // important!\n\t\t\t\t\tVersions: version.MustGetConstraint(\">= 9\", version.SemanticFormat), // important! outside of the version range\n\t\t\t\t},\n\t\t\t},\n\t\t\twantContext: Context{\n\t\t\t\tSource: &source.Description{\n\t\t\t\t\tMetadata: PURLLiteralMetadata{\n\t\t\t\t\t\tPURL: \"pkg:rpm/redhat/systemd-x@239-82.el8_10.2?distro=rhel-8.10\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantPkgs: []Package{\n\t\t\t\t{\n\t\t\t\t\tName:    \"systemd-x\",\n\t\t\t\t\tVersion: \"239-82.el8_10.2\",\n\t\t\t\t\tType:    pkg.RpmPkg,\n\t\t\t\t\tPURL:    \"pkg:rpm/redhat/systemd-x@239-82.el8_10.2?distro=rhel-8.10\",                      // important! we are NOT patching the channel into the PURL\n\t\t\t\t\tDistro:  &distro.Distro{Type: distro.RedHat, Version: \"8.10\", IDLike: []string{\"redhat\"}}, // important! channel NOT applied because outside of version range\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:      \"fails on purl list input\",\n\t\t\tuserInput: \"purl:testdata/purl/invalid-purl.txt\",\n\t\t\tchannels:  testFixChannels(),\n\t\t\twantErr:   require.Error,\n\t\t},\n\t\t{\n\t\t\tname:      \"invalid prefix\",\n\t\t\tuserInput: \"dir:testdata/purl\",\n\t\t\tchannels:  testFixChannels(),\n\t\t\twantErr:   require.Error,\n\t\t},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tif tc.wantErr == nil {\n\t\t\t\ttc.wantErr = require.NoError\n\t\t\t}\n\n\t\t\tpackages, ctx, _, err := purlProvider(tc.userInput, ProviderConfig{}, getDistroChannelApplier(tc.channels))\n\n\t\t\ttc.wantErr(t, err)\n\t\t\tif err != nil {\n\t\t\t\trequire.Nil(t, packages)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif d := cmp.Diff(tc.wantContext, ctx, diffOpts...); d != \"\" {\n\t\t\t\tt.Errorf(\"unexpected context (-want +got):\\n%s\", d)\n\t\t\t}\n\t\t\trequire.Len(t, packages, len(tc.wantPkgs))\n\t\t\tfor idx, expected := range tc.wantPkgs {\n\t\t\t\tif d := cmp.Diff(expected, packages[idx], diffOpts...); d != \"\" {\n\t\t\t\t\tt.Errorf(\"unexpected context (-want +got):\\n%s\", d)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc names(ns ...string) []string {\n\treturn ns\n}\n"
  },
  {
    "path": "grype/pkg/qualifier/platformcpe/qualifier.go",
    "content": "package platformcpe\n\nimport (\n\t\"strings\"\n\n\t\"github.com/anchore/grype/grype/distro\"\n\t\"github.com/anchore/grype/grype/pkg\"\n\t\"github.com/anchore/grype/grype/pkg/qualifier\"\n\t\"github.com/anchore/syft/syft/cpe\"\n)\n\ntype platformCPE struct {\n\tcpe string\n}\n\nfunc New(cpe string) qualifier.Qualifier {\n\treturn &platformCPE{cpe: cpe}\n}\n\nfunc isWindowsPlatformCPE(c cpe.CPE) bool {\n\treturn c.Attributes.Vendor == \"microsoft\" && strings.HasPrefix(c.Attributes.Product, \"windows\")\n}\n\nfunc isUbuntuPlatformCPE(c cpe.CPE) bool {\n\tif c.Attributes.Vendor == \"canonical\" && c.Attributes.Product == \"ubuntu_linux\" {\n\t\treturn true\n\t}\n\n\tif c.Attributes.Vendor == \"ubuntu\" {\n\t\treturn true\n\t}\n\n\treturn false\n}\n\nfunc isDebianPlatformCPE(c cpe.CPE) bool {\n\treturn c.Attributes.Vendor == \"debian\" && (c.Attributes.Product == \"debian_linux\" || c.Attributes.Product == \"linux\")\n}\n\nfunc isWordpressPlatformCPE(c cpe.CPE) bool {\n\treturn c.Attributes.Vendor == \"wordpress\" && c.Attributes.Product == \"wordpress\"\n}\n\nfunc (p platformCPE) Satisfied(pk pkg.Package) (bool, error) {\n\tif p.cpe == \"\" {\n\t\treturn true, nil\n\t}\n\n\tc, err := cpe.New(p.cpe, \"\")\n\n\tif err != nil {\n\t\treturn true, err\n\t}\n\n\t// NOTE: if syft ever supports cataloging wordpress plugins there will need to be a\n\t// package type condition check added here\n\tif isWordpressPlatformCPE(c) {\n\t\treturn false, nil\n\t}\n\n\t// The remaining checks are on distro, so if the distro is unknown the condition should\n\t// be considered to be satisfied and avoid filtering matches\n\tif pk.Distro == nil {\n\t\treturn true, nil\n\t}\n\n\tif isWindowsPlatformCPE(c) {\n\t\treturn pk.Distro.Type == distro.Windows, nil\n\t}\n\n\tif isUbuntuPlatformCPE(c) {\n\t\treturn pk.Distro.Type == distro.Ubuntu, nil\n\t}\n\n\tif isDebianPlatformCPE(c) {\n\t\treturn pk.Distro.Type == distro.Debian, nil\n\t}\n\n\treturn true, err\n}\n"
  },
  {
    "path": "grype/pkg/qualifier/platformcpe/qualifier_test.go",
    "content": "package platformcpe\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\n\t\"github.com/anchore/grype/grype/distro\"\n\t\"github.com/anchore/grype/grype/pkg\"\n\t\"github.com/anchore/grype/grype/pkg/qualifier\"\n)\n\nfunc TestPlatformCPE_Satisfied(t *testing.T) {\n\ttests := []struct {\n\t\tname        string\n\t\tplatformCPE qualifier.Qualifier\n\t\tpkg         pkg.Package\n\t\tsatisfied   bool\n\t\thasError    bool\n\t}{\n\t\t{\n\t\t\tname:        \"no filter on nil distro\",\n\t\t\tplatformCPE: New(\"cpe:2.3:o:microsoft:windows:-:*:*:*:*:*:*:*\"),\n\t\t\tpkg:         pkg.Package{},\n\t\t\tsatisfied:   true,\n\t\t\thasError:    false,\n\t\t},\n\t\t{\n\t\t\tname:        \"no filter when platform CPE is empty\",\n\t\t\tplatformCPE: New(\"\"),\n\t\t\tpkg: pkg.Package{\n\t\t\t\tDistro: &distro.Distro{Type: distro.Windows},\n\t\t\t},\n\t\t\tsatisfied: true,\n\t\t\thasError:  false,\n\t\t},\n\t\t{\n\t\t\tname:        \"no filter when platform CPE is invalid\",\n\t\t\tplatformCPE: New(\";;;\"),\n\t\t\tpkg: pkg.Package{\n\t\t\t\tDistro: &distro.Distro{Type: distro.Windows},\n\t\t\t},\n\t\t\tsatisfied: true,\n\t\t\thasError:  true,\n\t\t},\n\t\t// Windows\n\t\t{\n\t\t\tname:        \"filter windows platform vuln when distro is not windows\",\n\t\t\tplatformCPE: New(\"cpe:2.3:o:microsoft:windows:-:*:*:*:*:*:*:*\"),\n\t\t\tpkg: pkg.Package{\n\t\t\t\tDistro: &distro.Distro{Type: distro.Debian},\n\t\t\t},\n\t\t\tsatisfied: false,\n\t\t\thasError:  false,\n\t\t},\n\t\t{\n\t\t\tname:        \"filter windows server platform vuln when distro is not windows\",\n\t\t\tplatformCPE: New(\"cpe:2.3:o:microsoft:windows_server_2022:-:*:*:*:*:*:*:*\"),\n\t\t\tpkg: pkg.Package{\n\t\t\t\tDistro: &distro.Distro{Type: distro.Debian},\n\t\t\t},\n\t\t\tsatisfied: false,\n\t\t\thasError:  false,\n\t\t},\n\t\t{\n\t\t\tname:        \"no filter windows platform vuln when distro is windows\",\n\t\t\tplatformCPE: New(\"cpe:2.3:o:microsoft:windows:-:*:*:*:*:*:*:*\"),\n\t\t\tpkg: pkg.Package{\n\t\t\t\tDistro: &distro.Distro{Type: distro.Windows},\n\t\t\t},\n\t\t\tsatisfied: true,\n\t\t\thasError:  false,\n\t\t},\n\t\t{\n\t\t\tname:        \"no filter windows server platform vuln when distro is windows\",\n\t\t\tplatformCPE: New(\"cpe:2.3:o:microsoft:windows_server_2022:-:*:*:*:*:*:*:*\"),\n\t\t\tpkg: pkg.Package{\n\t\t\t\tDistro: &distro.Distro{Type: distro.Windows},\n\t\t\t},\n\t\t\tsatisfied: true,\n\t\t\thasError:  false,\n\t\t},\n\t\t// Debian\n\t\t{\n\t\t\tname:        \"filter debian platform vuln when distro is not debian\",\n\t\t\tplatformCPE: New(\"cpe:2.3:o:debian:debian_linux:-:*:*:*:*:*:*:*\"),\n\t\t\tpkg: pkg.Package{\n\t\t\t\tDistro: &distro.Distro{Type: distro.Ubuntu},\n\t\t\t},\n\t\t\tsatisfied: false,\n\t\t\thasError:  false,\n\t\t},\n\t\t{\n\t\t\tname:        \"filter debian platform vuln when distro is not debian (alternate encountered cpe)\",\n\t\t\tplatformCPE: New(\"cpe:2.3:o:debian:linux:-:*:*:*:*:*:*:*\"),\n\t\t\tpkg: pkg.Package{\n\t\t\t\tDistro: &distro.Distro{Type: distro.SLES},\n\t\t\t},\n\t\t\tsatisfied: false,\n\t\t\thasError:  false,\n\t\t},\n\t\t{\n\t\t\tname:        \"no filter debian platform vuln when distro is debian\",\n\t\t\tplatformCPE: New(\"cpe:2.3:o:debian:debian_linux:-:*:*:*:*:*:*:*\"),\n\t\t\tpkg: pkg.Package{\n\t\t\t\tDistro: &distro.Distro{Type: distro.Debian},\n\t\t\t},\n\t\t\tsatisfied: true,\n\t\t\thasError:  false,\n\t\t},\n\t\t{\n\t\t\tname:        \"no filter debian platform vuln when distro is debian (alternate encountered cpe)\",\n\t\t\tplatformCPE: New(\"cpe:2.3:o:debian:linux:-:*:*:*:*:*:*:*\"),\n\t\t\tpkg: pkg.Package{\n\t\t\t\tDistro: &distro.Distro{Type: distro.Debian},\n\t\t\t},\n\t\t\tsatisfied: true,\n\t\t\thasError:  false,\n\t\t},\n\t\t// Ubuntu\n\t\t{\n\t\t\tname:        \"filter ubuntu platform vuln when distro is not ubuntu\",\n\t\t\tplatformCPE: New(\"cpe:2.3:o:canonical:ubuntu_linux:-:*:*:*:*:*:*:*\"),\n\t\t\tpkg: pkg.Package{\n\t\t\t\tDistro: &distro.Distro{Type: distro.SLES},\n\t\t\t},\n\t\t\tsatisfied: false,\n\t\t\thasError:  false,\n\t\t},\n\t\t{\n\t\t\tname:        \"filter ubuntu platform vuln when distro is not ubuntu (alternate encountered cpe)\",\n\t\t\tplatformCPE: New(\"cpe:2.3:o:ubuntu:vivid:-:*:*:*:*:*:*:*\"),\n\t\t\tpkg: pkg.Package{\n\t\t\t\tDistro: &distro.Distro{Type: distro.Alpine},\n\t\t\t},\n\t\t\tsatisfied: false,\n\t\t\thasError:  false,\n\t\t},\n\t\t{\n\t\t\tname:        \"no filter ubuntu platform vuln when distro is ubuntu\",\n\t\t\tplatformCPE: New(\"cpe:2.3:o:canonical:ubuntu_linux:-:*:*:*:*:*:*:*\"),\n\t\t\tpkg: pkg.Package{\n\t\t\t\tDistro: &distro.Distro{Type: distro.Ubuntu},\n\t\t\t},\n\t\t\tsatisfied: true,\n\t\t\thasError:  false,\n\t\t},\n\t\t{\n\t\t\tname:        \"no filter ubuntu platform vuln when distro is ubuntu (alternate encountered cpe)\",\n\t\t\tplatformCPE: New(\"cpe:2.3:o:ubuntu:vivid:-:*:*:*:*:*:*:*\"),\n\t\t\tpkg: pkg.Package{\n\t\t\t\tDistro: &distro.Distro{Type: distro.Ubuntu},\n\t\t\t},\n\t\t\tsatisfied: true,\n\t\t\thasError:  false,\n\t\t},\n\t\t// Wordpress\n\t\t{\n\t\t\tname:        \"always filter wordpress platform vulns (no known distro)\",\n\t\t\tplatformCPE: New(\"cpe:2.3:o:wordpress:wordpress:-:*:*:*:*:*:*:*\"),\n\t\t\tpkg:         pkg.Package{},\n\t\t\tsatisfied:   false,\n\t\t\thasError:    false,\n\t\t},\n\t\t{\n\t\t\tname:        \"always filter wordpress platform vulns (known distro)\",\n\t\t\tplatformCPE: New(\"cpe:2.3:o:ubuntu:vivid:-:*:*:*:*:*:*:*\"),\n\t\t\tpkg: pkg.Package{\n\t\t\t\tDistro: &distro.Distro{Type: distro.Alpine},\n\t\t\t},\n\t\t\tsatisfied: false,\n\t\t\thasError:  false,\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\ts, err := test.platformCPE.Satisfied(test.pkg)\n\n\t\t\tif test.hasError {\n\t\t\t\tassert.Error(t, err)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\n\t\t\tassert.Equal(t, test.satisfied, s)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/pkg/qualifier/qualifier.go",
    "content": "package qualifier\n\nimport (\n\t\"github.com/anchore/grype/grype/pkg\"\n)\n\ntype Qualifier interface {\n\tSatisfied(p pkg.Package) (bool, error)\n}\n"
  },
  {
    "path": "grype/pkg/qualifier/rpmmodularity/qualifier.go",
    "content": "package rpmmodularity\n\nimport (\n\t\"strings\"\n\n\t\"github.com/anchore/grype/grype/distro\"\n\t\"github.com/anchore/grype/grype/pkg\"\n\t\"github.com/anchore/grype/grype/pkg/qualifier\"\n)\n\ntype rpmModularity struct {\n\tmodule string\n}\n\nfunc New(module string) qualifier.Qualifier {\n\treturn &rpmModularity{module: module}\n}\n\nfunc (r rpmModularity) Satisfied(p pkg.Package) (bool, error) {\n\tif p.Metadata == nil {\n\t\t// If unable to determine package modularity, the constraint should be considered satisfied\n\t\treturn true, nil\n\t}\n\n\tm, ok := p.Metadata.(pkg.RpmMetadata)\n\tif !ok {\n\t\treturn false, nil\n\t}\n\n\tif m.ModularityLabel == nil {\n\t\t// If the package modularity is empty (null), the constraint should be considered satisfied.\n\t\t// this is the case where the package source does not support expressing modularity.\n\t\treturn true, nil\n\t}\n\n\tif p.Distro != nil && p.Distro.Type == distro.OracleLinux && *m.ModularityLabel == \"\" {\n\t\t// For oraclelinux, the default stream of an installed appstream package does not currently set\n\t\t// the MODULARITYLABEL property in the rpm metadata; however, in their advisory data they do specify\n\t\t// modularity information, so this ends up in a case where the vuln entries have modularity but the\n\t\t// packages coming from the sbom won't, so for now we need to treat the constraint as satisfied when the\n\t\t// modularity label from an oraclelinux package is \"\".\n\t\t// TODO: remove this once we have a way of obtaining and parsing the module information from the DISTTAG\n\t\t// in syft.\n\t\treturn true, nil\n\t}\n\n\tif r.module == \"\" {\n\t\tif *m.ModularityLabel == \"\" {\n\t\t\t// the DB has a modularity label, but it's empty... we also have a modularity label from a package source\n\t\t\t// that supports being able to express modularity, but it's empty. This is a match.\n\t\t\treturn true, nil\n\t\t}\n\n\t\t// The package source is able to express modularity, and the DB has a package quality that is empty.\n\t\t// Since we are doing a prefix match against the modularity label (which is guaranteed to be non-empty),\n\t\t// and we are checking for an empty prefix, this will always match, however, semantically this makes no sense.\n\t\t// We don't want package modularities of any value to match this qualifier.\n\t\treturn false, nil\n\t}\n\n\treturn strings.HasPrefix(*m.ModularityLabel, r.module), nil\n}\n"
  },
  {
    "path": "grype/pkg/qualifier/rpmmodularity/qualifier_test.go",
    "content": "package rpmmodularity\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\n\t\"github.com/anchore/grype/grype/distro\"\n\t\"github.com/anchore/grype/grype/pkg\"\n\t\"github.com/anchore/grype/grype/pkg/qualifier\"\n)\n\nfunc TestRpmModularity_Satisfied(t *testing.T) {\n\toracle := distro.New(distro.OracleLinux, \"8\", \"\")\n\n\ttests := []struct {\n\t\tname          string\n\t\trpmModularity qualifier.Qualifier\n\t\tpkg           pkg.Package\n\t\tsatisfied     bool\n\t}{\n\t\t{\n\t\t\tname:          \"non rpm metadata\",\n\t\t\trpmModularity: New(\"test:1\"),\n\t\t\tpkg: pkg.Package{\n\t\t\t\tDistro:   nil,\n\t\t\t\tMetadata: pkg.JavaMetadata{},\n\t\t\t},\n\t\t\tsatisfied: false,\n\t\t},\n\t\t{\n\t\t\tname:          \"module with package rpm metadata lacking actual metadata 1\",\n\t\t\trpmModularity: New(\"test:1\"),\n\t\t\tpkg: pkg.Package{\n\t\t\t\tDistro:   nil,\n\t\t\t\tMetadata: nil,\n\t\t\t},\n\t\t\tsatisfied: true,\n\t\t},\n\t\t{\n\t\t\tname:          \"empty module with rpm metadata lacking actual metadata 2\",\n\t\t\trpmModularity: New(\"\"),\n\t\t\tpkg:           pkg.Package{Metadata: nil},\n\t\t\tsatisfied:     true,\n\t\t},\n\t\t{\n\t\t\tname:          \"no modularity label with no module\",\n\t\t\trpmModularity: New(\"\"),\n\t\t\tpkg: pkg.Package{\n\t\t\t\tDistro: nil,\n\t\t\t\tMetadata: pkg.RpmMetadata{\n\t\t\t\t\tEpoch: nil,\n\t\t\t\t}},\n\t\t\tsatisfied: true,\n\t\t},\n\t\t{\n\t\t\tname:          \"no modularity label with module\",\n\t\t\trpmModularity: New(\"abc\"),\n\t\t\tpkg: pkg.Package{\n\t\t\t\tDistro: nil,\n\t\t\t\tMetadata: pkg.RpmMetadata{\n\t\t\t\t\tEpoch: nil,\n\t\t\t\t}},\n\t\t\tsatisfied: true,\n\t\t},\n\t\t{\n\t\t\tname:          \"modularity label with no module\",\n\t\t\trpmModularity: New(\"\"),\n\t\t\tpkg: pkg.Package{\n\t\t\t\tDistro: nil,\n\t\t\t\tMetadata: pkg.RpmMetadata{\n\t\t\t\t\tModularityLabel: strRef(\"x:3:1234567:abcd\"),\n\t\t\t\t}},\n\t\t\tsatisfied: false,\n\t\t},\n\t\t{\n\t\t\tname:          \"modularity label in module\",\n\t\t\trpmModularity: New(\"x:3\"),\n\t\t\tpkg: pkg.Package{\n\t\t\t\tDistro: nil,\n\t\t\t\tMetadata: pkg.RpmMetadata{\n\t\t\t\t\tModularityLabel: strRef(\"x:3:1234567:abcd\"),\n\t\t\t\t}},\n\t\t\tsatisfied: true,\n\t\t},\n\t\t{\n\t\t\tname:          \"modularity label not in module\",\n\t\t\trpmModularity: New(\"x:3\"),\n\t\t\tpkg: pkg.Package{\n\t\t\t\tDistro: nil,\n\t\t\t\tMetadata: pkg.RpmMetadata{\n\t\t\t\t\tModularityLabel: strRef(\"x:1:1234567:abcd\"),\n\t\t\t\t}},\n\t\t\tsatisfied: false,\n\t\t},\n\t\t{\n\t\t\tname:          \"modularity label is positively blank\",\n\t\t\trpmModularity: New(\"\"),\n\t\t\tpkg: pkg.Package{\n\t\t\t\tDistro: nil,\n\t\t\t\tMetadata: pkg.RpmMetadata{\n\t\t\t\t\tModularityLabel: strRef(\"\"),\n\t\t\t\t}},\n\t\t\tsatisfied: true,\n\t\t},\n\t\t{\n\t\t\tname:          \"modularity label is missing (assume we cannot verify that capability)\",\n\t\t\trpmModularity: New(\"\"),\n\t\t\tpkg: pkg.Package{\n\t\t\t\tDistro: nil,\n\t\t\t\tMetadata: pkg.RpmMetadata{\n\t\t\t\t\tModularityLabel: nil,\n\t\t\t\t}},\n\t\t\tsatisfied: true,\n\t\t},\n\t\t{\n\t\t\tname:          \"default appstream for oraclelinux (treat as missing)\",\n\t\t\trpmModularity: New(\"nodejs:16\"),\n\t\t\tpkg: pkg.Package{\n\t\t\t\tDistro: oracle,\n\t\t\t\tMetadata: pkg.RpmMetadata{\n\t\t\t\t\tModularityLabel: strRef(\"\"),\n\t\t\t\t}},\n\t\t\tsatisfied: true,\n\t\t},\n\t\t{\n\t\t\tname:          \"non-default appstream for oraclelinux matches vuln modularity\",\n\t\t\trpmModularity: New(\"nodejs:16\"),\n\t\t\tpkg: pkg.Package{\n\t\t\t\tDistro: oracle,\n\t\t\t\tMetadata: pkg.RpmMetadata{\n\t\t\t\t\tModularityLabel: strRef(\"nodejs:16:blah\"),\n\t\t\t\t}},\n\t\t\tsatisfied: true,\n\t\t},\n\t\t{\n\t\t\tname:          \"non-default appstream for oraclelinux does not match vuln modularity\",\n\t\t\trpmModularity: New(\"nodejs:17\"),\n\t\t\tpkg: pkg.Package{\n\t\t\t\tDistro: oracle,\n\t\t\t\tMetadata: pkg.RpmMetadata{\n\t\t\t\t\tModularityLabel: strRef(\"nodejs:16:blah\"),\n\t\t\t\t}},\n\t\t\tsatisfied: false,\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\ts, err := test.rpmModularity.Satisfied(test.pkg)\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, test.satisfied, s)\n\t\t})\n\t}\n}\n\nfunc strRef(s string) *string {\n\treturn &s\n}\n"
  },
  {
    "path": "grype/pkg/rpm_metadata.go",
    "content": "package pkg\n\ntype RpmMetadata struct {\n\tEpoch           *int    `json:\"epoch\" cyclonedx:\"epoch\"`\n\tModularityLabel *string `json:\"modularityLabel\" cyclonedx:\"modularityLabel\"`\n}\n"
  },
  {
    "path": "grype/pkg/syft_provider.go",
    "content": "package pkg\n\nimport (\n\t\"context\"\n\t\"errors\"\n\n\t\"github.com/anchore/go-collections\"\n\t\"github.com/anchore/grype/grype/distro\"\n\t\"github.com/anchore/grype/internal/log\"\n\t\"github.com/anchore/stereoscope\"\n\t\"github.com/anchore/stereoscope/pkg/image\"\n\t\"github.com/anchore/syft/syft\"\n\t\"github.com/anchore/syft/syft/sbom\"\n\t\"github.com/anchore/syft/syft/source\"\n\t\"github.com/anchore/syft/syft/source/sourceproviders\"\n)\n\nfunc syftProvider(userInput string, config ProviderConfig, applyChannel func(*distro.Distro) bool) ([]Package, Context, *sbom.SBOM, error) {\n\tsrc, err := getSource(userInput, config)\n\tif err != nil {\n\t\treturn nil, Context{}, nil, err\n\t}\n\tdefer log.CloseAndLogError(src, \"syft source\")\n\n\ts, err := syft.CreateSBOM(context.Background(), src, config.SBOMOptions)\n\tif err != nil {\n\t\treturn nil, Context{}, nil, err\n\t}\n\n\tif s == nil {\n\t\treturn nil, Context{}, nil, errors.New(\"no SBOM provided\")\n\t}\n\n\tsrcDescription := src.Describe()\n\n\td, distroDetectionFailed := distroFromSBOM(s, config, applyChannel)\n\n\tpkgCatalog := removePackagesByOverlap(s.Artifacts.Packages, s.Relationships, d)\n\n\tpackages := FromCollection(pkgCatalog, config.SynthesisConfig)\n\tpkgCtx := Context{\n\t\tSource:                &srcDescription,\n\t\tDistro:                d,\n\t\tDistroDetectionFailed: distroDetectionFailed,\n\t}\n\n\treturn packages, pkgCtx, s, nil\n}\n\nfunc distroFromSBOM(s *sbom.SBOM, config ProviderConfig, applyChannel func(*distro.Distro) bool) (d *distro.Distro, detectionFailed bool) {\n\tif config.Distro.Override != nil {\n\t\td = config.Distro.Override\n\t} else {\n\t\td = distro.FromRelease(s.Artifacts.LinuxDistribution, config.Distro.FixChannels)\n\t\tapplyChannel(d)\n\t\t// detection failed if we had linux release info but couldn't determine distro type\n\t\tdetectionFailed = s.Artifacts.LinuxDistribution != nil && d == nil\n\t}\n\treturn d, detectionFailed\n}\n\nfunc getSource(userInput string, config ProviderConfig) (source.Source, error) {\n\tif config.SBOMOptions.Search.Scope == \"\" {\n\t\treturn nil, errDoesNotProvide\n\t}\n\n\tvar err error\n\tvar platform *image.Platform\n\tif config.Platform != \"\" {\n\t\tplatform, err = image.NewPlatform(config.Platform)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\t// prioritize explicitly specified sources from --from flag\n\tsources := config.Sources\n\tif len(sources) == 0 {\n\t\t// fallback to extracting from scheme if --from not specified (for backward compatibility)\n\t\tschemeSource, newUserInput := stereoscope.ExtractSchemeSource(userInput, allSourceTags()...)\n\t\tif schemeSource != \"\" {\n\t\t\tsources = []string{schemeSource}\n\t\t\tuserInput = newUserInput\n\t\t}\n\t}\n\n\treturn syft.GetSource(context.Background(), userInput, syft.DefaultGetSourceConfig().\n\t\tWithSources(sources...).\n\t\tWithDefaultImagePullSource(config.DefaultImagePullSource).\n\t\tWithAlias(source.Alias{Name: config.Name}).\n\t\tWithRegistryOptions(config.RegistryOptions).\n\t\tWithPlatform(platform).\n\t\tWithExcludeConfig(source.ExcludeConfig{Paths: config.Exclusions}))\n}\n\nfunc allSourceTags() []string {\n\treturn collections.TaggedValueSet[source.Provider]{}.Join(sourceproviders.All(\"\", nil)...).Tags()\n}\n"
  },
  {
    "path": "grype/pkg/syft_sbom_provider.go",
    "content": "package pkg\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/gabriel-vasile/mimetype\"\n\n\t\"github.com/anchore/go-homedir\"\n\t\"github.com/anchore/grype/grype/distro\"\n\t\"github.com/anchore/grype/internal\"\n\t\"github.com/anchore/grype/internal/log\"\n\t\"github.com/anchore/syft/syft/format\"\n\t\"github.com/anchore/syft/syft/format/syftjson\"\n\t\"github.com/anchore/syft/syft/sbom\"\n)\n\ntype SBOMFileMetadata struct {\n\tPath string\n}\n\nfunc syftSBOMProvider(userInput string, config ProviderConfig, applyChannel func(*distro.Distro) bool) ([]Package, Context, *sbom.SBOM, error) {\n\ts, fmtID, path, err := getSBOM(userInput)\n\tif err != nil {\n\t\treturn nil, Context{}, nil, err\n\t}\n\n\tsrc := s.Source\n\tif src.Metadata == nil && path != \"\" {\n\t\tsrc.Metadata = SBOMFileMetadata{\n\t\t\tPath: path,\n\t\t}\n\t}\n\n\td, distroDetectionFailed := distroFromSBOM(s, config, applyChannel)\n\n\tcatalog := removePackagesByOverlap(s.Artifacts.Packages, s.Relationships, d)\n\n\tvar enhancers []Enhancer\n\tif fmtID != syftjson.ID {\n\t\tenhancers = purlEnhancers(applyChannel)\n\t}\n\n\treturn FromCollection(catalog, config.SynthesisConfig, enhancers...), Context{\n\t\tSource:                &src,\n\t\tDistro:                d,\n\t\tDistroDetectionFailed: distroDetectionFailed,\n\t}, s, nil\n}\n\nfunc getSBOM(userInput string) (*sbom.SBOM, sbom.FormatID, string, error) {\n\treader, path, err := getSBOMReader(userInput)\n\tif err != nil {\n\t\treturn nil, \"\", path, err\n\t}\n\n\ts, fmtID, err := readSBOM(reader)\n\treturn s, fmtID, path, err\n}\n\nfunc readSBOM(reader io.ReadSeeker) (*sbom.SBOM, sbom.FormatID, error) {\n\ts, fmtID, _, err := format.Decode(reader)\n\tif err != nil {\n\t\treturn nil, \"\", fmt.Errorf(\"unable to decode sbom: %w\", err)\n\t}\n\n\tif fmtID == \"\" || s == nil {\n\t\treturn nil, \"\", errDoesNotProvide\n\t}\n\n\treturn s, fmtID, nil\n}\n\nfunc getSBOMReader(userInput string) (io.ReadSeeker, string, error) {\n\tswitch {\n\t// the order of cases matter\n\tcase userInput == \"\":\n\t\t// we only want to attempt reading in from stdin if the user has not specified other\n\t\t// options from the CLI, otherwise we should not assume there is any valid input from stdin.\n\t\tr, err := stdinReader()\n\t\tif err != nil {\n\t\t\treturn nil, \"\", err\n\t\t}\n\t\treturn decodeStdin(r)\n\n\tcase explicitlySpecifyingPurlList(userInput):\n\t\tfilepath := strings.TrimPrefix(userInput, purlInputPrefix)\n\t\treturn openFile(filepath)\n\n\tcase explicitlySpecifyingCPEList(userInput):\n\t\tfilepath := strings.TrimPrefix(userInput, cpeListPrefix)\n\t\treturn openFile(filepath)\n\n\tcase explicitlySpecifyingSBOM(userInput):\n\t\tfilepath := strings.TrimPrefix(userInput, \"sbom:\")\n\t\treturn openFile(filepath)\n\n\tcase isPossibleSBOM(userInput):\n\t\treturn openFile(userInput)\n\n\tdefault:\n\t\treturn nil, \"\", errDoesNotProvide\n\t}\n}\n\nfunc decodeStdin(r io.Reader) (io.ReadSeeker, string, error) {\n\tb, err := io.ReadAll(r)\n\tif err != nil {\n\t\treturn nil, \"\", fmt.Errorf(\"failed reading stdin: %w\", err)\n\t}\n\n\treader := bytes.NewReader(b)\n\t_, err = reader.Seek(0, io.SeekStart)\n\tif err != nil {\n\t\treturn nil, \"\", fmt.Errorf(\"failed to parse stdin: %w\", err)\n\t}\n\n\treturn reader, \"\", nil\n}\n\nfunc stdinReader() (io.Reader, error) {\n\tisStdinPipeOrRedirect, err := internal.IsStdinPipeOrRedirect()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to determine if there is piped input: %w\", err)\n\t}\n\n\tif !isStdinPipeOrRedirect {\n\t\treturn nil, errors.New(\"no input was provided via stdin\")\n\t}\n\n\treturn os.Stdin, nil\n}\n\nfunc openFile(path string) (io.ReadSeekCloser, string, error) {\n\texpandedPath, err := homedir.Expand(path)\n\tif err != nil {\n\t\treturn nil, path, fmt.Errorf(\"unable to open SBOM: %w\", err)\n\t}\n\n\tf, err := os.Open(expandedPath)\n\tif err != nil {\n\t\treturn nil, path, fmt.Errorf(\"unable to open file %s: %w\", expandedPath, err)\n\t}\n\n\treturn f, path, nil\n}\n\nfunc isPossibleSBOM(userInput string) bool {\n\tf, path, err := openFile(userInput)\n\tif err != nil {\n\t\treturn false\n\t}\n\tdefer log.CloseAndLogError(f, path)\n\n\tmType, err := mimetype.DetectReader(f)\n\tif err != nil {\n\t\treturn false\n\t}\n\n\t// we expect application/json, application/xml, and text/plain input documents. All of these are either\n\t// text/plain or a descendant of text/plain. Anything else cannot be an input SBOM document.\n\treturn isAncestorOfMimetype(mType, \"text/plain\")\n}\n\nfunc isAncestorOfMimetype(mType *mimetype.MIME, expected string) bool {\n\tfor cur := mType; cur != nil; cur = cur.Parent() {\n\t\tif cur.Is(expected) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc explicitlySpecifyingSBOM(userInput string) bool {\n\treturn strings.HasPrefix(userInput, \"sbom:\")\n}\n\nfunc explicitlySpecifyingPurlList(userInput string) bool {\n\treturn strings.HasPrefix(userInput, purlInputPrefix)\n}\n\nfunc explicitlySpecifyingCPEList(userInput string) bool {\n\treturn strings.HasPrefix(userInput, cpeListPrefix)\n}\n"
  },
  {
    "path": "grype/pkg/syft_sbom_provider_test.go",
    "content": "package pkg\n\nimport (\n\t\"slices\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/go-test/deep\"\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/grype/grype/distro\"\n\t\"github.com/anchore/syft/syft/cpe\"\n\t\"github.com/anchore/syft/syft/file\"\n\t\"github.com/anchore/syft/syft/pkg\"\n\t\"github.com/anchore/syft/syft/source\"\n)\n\nfunc TestParseSyftJSON(t *testing.T) {\n\tapplyChannel := getDistroChannelApplier(testFixChannels())\n\n\ttests := []struct {\n\t\tFixture  string\n\t\tPackages []Package\n\t\tContext  Context\n\t}{\n\t\t{\n\t\t\tFixture: \"testdata/syft-multiple-ecosystems.json\",\n\t\t\tPackages: []Package{\n\t\t\t\t{\n\t\t\t\t\tName:    \"alpine-baselayout\",\n\t\t\t\t\tVersion: \"3.2.0-r6\",\n\t\t\t\t\tLocations: file.NewLocationSet(\n\t\t\t\t\t\tfile.NewLocationFromCoordinates(file.Coordinates{\n\t\t\t\t\t\t\tRealPath:     \"/lib/apk/db/installed\",\n\t\t\t\t\t\t\tFileSystemID: \"sha256:8d3ac3489996423f53d6087c81180006263b79f206d3fdec9e66f0e27ceb8759\",\n\t\t\t\t\t\t}),\n\t\t\t\t\t),\n\t\t\t\t\tLanguage: \"\",\n\t\t\t\t\tLicenses: []string{\n\t\t\t\t\t\t\"GPL-2.0-only\",\n\t\t\t\t\t},\n\t\t\t\t\tType: \"apk\",\n\t\t\t\t\tCPEs: []cpe.CPE{\n\t\t\t\t\t\tcpe.Must(\"cpe:2.3:a:alpine:alpine_baselayout:3.2.0-r6:*:*:*:*:*:*:*\", \"\"),\n\t\t\t\t\t},\n\t\t\t\t\tPURL: \"pkg:alpine/alpine-baselayout@3.2.0-r6?arch=x86_64\",\n\t\t\t\t\tUpstreams: []UpstreamPackage{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName: \"alpine-baselayout\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tMetadata: ApkMetadata{\n\t\t\t\t\t\tFiles: []ApkFileRecord{\n\t\t\t\t\t\t\t{Path: \"/dev\"},\n\t\t\t\t\t\t\t{Path: \"/dev/pts\"},\n\t\t\t\t\t\t\t{Path: \"/dev/shm\"},\n\t\t\t\t\t\t\t{Path: \"/etc\"},\n\t\t\t\t\t\t\t{Path: \"/etc/fstab\"},\n\t\t\t\t\t\t\t{Path: \"/etc/group\"},\n\t\t\t\t\t\t\t{Path: \"/etc/hostname\"},\n\t\t\t\t\t\t\t{Path: \"/etc/hosts\"},\n\t\t\t\t\t\t\t{Path: \"/etc/inittab\"},\n\t\t\t\t\t\t\t{Path: \"/etc/modules\"},\n\t\t\t\t\t\t\t{Path: \"/etc/motd\"},\n\t\t\t\t\t\t\t{Path: \"/etc/mtab\"},\n\t\t\t\t\t\t\t{Path: \"/etc/passwd\"},\n\t\t\t\t\t\t\t{Path: \"/etc/profile\"},\n\t\t\t\t\t\t\t{Path: \"/etc/protocols\"},\n\t\t\t\t\t\t\t{Path: \"/etc/services\"},\n\t\t\t\t\t\t\t{Path: \"/etc/shadow\"},\n\t\t\t\t\t\t\t{Path: \"/etc/shells\"},\n\t\t\t\t\t\t\t{Path: \"/etc/sysctl.conf\"},\n\t\t\t\t\t\t\t{Path: \"/etc/apk\"},\n\t\t\t\t\t\t\t{Path: \"/etc/conf.d\"},\n\t\t\t\t\t\t\t{Path: \"/etc/crontabs\"},\n\t\t\t\t\t\t\t{Path: \"/etc/crontabs/root\"},\n\t\t\t\t\t\t\t{Path: \"/etc/init.d\"},\n\t\t\t\t\t\t\t{Path: \"/etc/modprobe.d\"},\n\t\t\t\t\t\t\t{Path: \"/etc/modprobe.d/aliases.conf\"},\n\t\t\t\t\t\t\t{Path: \"/etc/modprobe.d/blacklist.conf\"},\n\t\t\t\t\t\t\t{Path: \"/etc/modprobe.d/i386.conf\"},\n\t\t\t\t\t\t\t{Path: \"/etc/modprobe.d/kms.conf\"},\n\t\t\t\t\t\t\t{Path: \"/etc/modules-load.d\"},\n\t\t\t\t\t\t\t{Path: \"/etc/network\"},\n\t\t\t\t\t\t\t{Path: \"/etc/network/if-down.d\"},\n\t\t\t\t\t\t\t{Path: \"/etc/network/if-post-down.d\"},\n\t\t\t\t\t\t\t{Path: \"/etc/network/if-pre-up.d\"},\n\t\t\t\t\t\t\t{Path: \"/etc/network/if-up.d\"},\n\t\t\t\t\t\t\t{Path: \"/etc/opt\"},\n\t\t\t\t\t\t\t{Path: \"/etc/periodic\"},\n\t\t\t\t\t\t\t{Path: \"/etc/periodic/15min\"},\n\t\t\t\t\t\t\t{Path: \"/etc/periodic/daily\"},\n\t\t\t\t\t\t\t{Path: \"/etc/periodic/hourly\"},\n\t\t\t\t\t\t\t{Path: \"/etc/periodic/monthly\"},\n\t\t\t\t\t\t\t{Path: \"/etc/periodic/weekly\"},\n\t\t\t\t\t\t\t{Path: \"/etc/profile.d\"},\n\t\t\t\t\t\t\t{Path: \"/etc/profile.d/README\"},\n\t\t\t\t\t\t\t{Path: \"/etc/profile.d/color_prompt.sh.disabled\"},\n\t\t\t\t\t\t\t{Path: \"/etc/profile.d/locale.sh\"},\n\t\t\t\t\t\t\t{Path: \"/etc/sysctl.d\"},\n\t\t\t\t\t\t\t{Path: \"/home\"},\n\t\t\t\t\t\t\t{Path: \"/lib\"},\n\t\t\t\t\t\t\t{Path: \"/lib/firmware\"},\n\t\t\t\t\t\t\t{Path: \"/lib/mdev\"},\n\t\t\t\t\t\t\t{Path: \"/lib/modules-load.d\"},\n\t\t\t\t\t\t\t{Path: \"/lib/sysctl.d\"},\n\t\t\t\t\t\t\t{Path: \"/lib/sysctl.d/00-alpine.conf\"},\n\t\t\t\t\t\t\t{Path: \"/media\"},\n\t\t\t\t\t\t\t{Path: \"/media/cdrom\"},\n\t\t\t\t\t\t\t{Path: \"/media/floppy\"},\n\t\t\t\t\t\t\t{Path: \"/media/usb\"},\n\t\t\t\t\t\t\t{Path: \"/mnt\"},\n\t\t\t\t\t\t\t{Path: \"/opt\"},\n\t\t\t\t\t\t\t{Path: \"/proc\"},\n\t\t\t\t\t\t\t{Path: \"/root\"},\n\t\t\t\t\t\t\t{Path: \"/run\"},\n\t\t\t\t\t\t\t{Path: \"/sbin\"},\n\t\t\t\t\t\t\t{Path: \"/sbin/mkmntdirs\"},\n\t\t\t\t\t\t\t{Path: \"/srv\"},\n\t\t\t\t\t\t\t{Path: \"/sys\"},\n\t\t\t\t\t\t\t{Path: \"/tmp\"},\n\t\t\t\t\t\t\t{Path: \"/usr\"},\n\t\t\t\t\t\t\t{Path: \"/usr/lib\"},\n\t\t\t\t\t\t\t{Path: \"/usr/lib/modules-load.d\"},\n\t\t\t\t\t\t\t{Path: \"/usr/local\"},\n\t\t\t\t\t\t\t{Path: \"/usr/local/bin\"},\n\t\t\t\t\t\t\t{Path: \"/usr/local/lib\"},\n\t\t\t\t\t\t\t{Path: \"/usr/local/share\"},\n\t\t\t\t\t\t\t{Path: \"/usr/sbin\"},\n\t\t\t\t\t\t\t{Path: \"/usr/share\"},\n\t\t\t\t\t\t\t{Path: \"/usr/share/man\"},\n\t\t\t\t\t\t\t{Path: \"/usr/share/misc\"},\n\t\t\t\t\t\t\t{Path: \"/var\"},\n\t\t\t\t\t\t\t{Path: \"/var/run\"},\n\t\t\t\t\t\t\t{Path: \"/var/cache\"},\n\t\t\t\t\t\t\t{Path: \"/var/cache/misc\"},\n\t\t\t\t\t\t\t{Path: \"/var/empty\"},\n\t\t\t\t\t\t\t{Path: \"/var/lib\"},\n\t\t\t\t\t\t\t{Path: \"/var/lib/misc\"},\n\t\t\t\t\t\t\t{Path: \"/var/local\"},\n\t\t\t\t\t\t\t{Path: \"/var/lock\"},\n\t\t\t\t\t\t\t{Path: \"/var/lock/subsys\"},\n\t\t\t\t\t\t\t{Path: \"/var/log\"},\n\t\t\t\t\t\t\t{Path: \"/var/mail\"},\n\t\t\t\t\t\t\t{Path: \"/var/opt\"},\n\t\t\t\t\t\t\t{Path: \"/var/spool\"},\n\t\t\t\t\t\t\t{Path: \"/var/spool/mail\"},\n\t\t\t\t\t\t\t{Path: \"/var/spool/cron\"},\n\t\t\t\t\t\t\t{Path: \"/var/spool/cron/crontabs\"},\n\t\t\t\t\t\t\t{Path: \"/var/tmp\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tName:    \"fake\",\n\t\t\t\t\tVersion: \"1.2.0\",\n\t\t\t\t\tLocations: file.NewLocationSet(\n\t\t\t\t\t\tfile.NewLocationFromCoordinates(file.Coordinates{\n\t\t\t\t\t\t\tRealPath:     \"/lib/apk/db/installed\",\n\t\t\t\t\t\t\tFileSystemID: \"sha256:93cf4cfb673c7e16a9e74f731d6767b70b92a0b7c9f59d06efd72fbff535371c\",\n\t\t\t\t\t\t}),\n\t\t\t\t\t),\n\t\t\t\t\tLanguage: \"lang\",\n\t\t\t\t\tLicenses: []string{\n\t\t\t\t\t\t\"LGPL-3.0-or-later\",\n\t\t\t\t\t},\n\t\t\t\t\tType: \"dpkg\",\n\t\t\t\t\tCPEs: []cpe.CPE{\n\t\t\t\t\t\tcpe.Must(\"cpe:2.3:a:*:fake:1.2.0:*:*:*:*:*:*:*\", \"\"),\n\t\t\t\t\t\tcpe.Must(\"cpe:2.3:a:fake:fake:1.2.0:*:*:*:*:*:*:*\", \"\"),\n\t\t\t\t\t},\n\t\t\t\t\tPURL: \"pkg:deb/debian/fake@1.2.0?arch=x86_64\",\n\t\t\t\t\tUpstreams: []UpstreamPackage{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName:    \"a-source\",\n\t\t\t\t\t\t\tVersion: \"1.4.5\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tName:    \"gmp\",\n\t\t\t\t\tVersion: \"6.2.0-r0\",\n\t\t\t\t\tLocations: file.NewLocationSet(\n\t\t\t\t\t\tfile.NewLocationFromCoordinates(file.Coordinates{\n\t\t\t\t\t\t\tRealPath:     \"/lib/apk/db/installed\",\n\t\t\t\t\t\t\tFileSystemID: \"sha256:93cf4cfb673c7e16a9e74f731d6767b70b92a0b7c9f59d06efd72fbff535371c\",\n\t\t\t\t\t\t}),\n\t\t\t\t\t),\n\t\t\t\t\tLanguage: \"the-lang\",\n\t\t\t\t\tLicenses: []string{\n\t\t\t\t\t\t\"LGPL-3.0-or-later\",\n\t\t\t\t\t},\n\t\t\t\t\tType: \"java-archive\",\n\t\t\t\t\tCPEs: []cpe.CPE{\n\t\t\t\t\t\tcpe.Must(\"cpe:2.3:a:*:gmp:6.2.0-r0:*:*:*:*:*:*:*\", \"\"),\n\t\t\t\t\t\tcpe.Must(\"cpe:2.3:a:gmp:gmp:6.2.0-r0:*:*:*:*:*:*:*\", \"\"),\n\t\t\t\t\t},\n\t\t\t\t\tPURL: \"pkg:alpine/gmp@6.2.0-r0?arch=x86_64\",\n\t\t\t\t\tMetadata: JavaMetadata{\n\t\t\t\t\t\tPomArtifactID: \"aid\",\n\t\t\t\t\t\tPomGroupID:    \"gid\",\n\t\t\t\t\t\tManifestName:  \"a-name\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tContext: Context{\n\t\t\t\tSource: &source.Description{\n\t\t\t\t\tMetadata: source.ImageMetadata{\n\t\t\t\t\t\tUserInput: \"alpine:fake\",\n\t\t\t\t\t\tLayers: []source.LayerMetadata{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tMediaType: \"application/vnd.docker.image.rootfs.diff.tar.gzip\",\n\t\t\t\t\t\t\t\tDigest:    \"sha256:50644c29ef5a27c9a40c393a73ece2479de78325cae7d762ef3cdc19bf42dd0a\",\n\t\t\t\t\t\t\t\tSize:      5570176,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tSize:           15879684,\n\t\t\t\t\t\tID:             \"sha256:fadf1294c09213b20d4d6fc84109584e1c102d185c2cae15144a87d29de65c6d\",\n\t\t\t\t\t\tManifestDigest: \"sha256:1f6495428fb363e2d233e5df078b2b200635c4e51f0a3be34ecf09d44b547590\",\n\t\t\t\t\t\tMediaType:      \"application/vnd.docker.distribution.manifest.v2+json\",\n\t\t\t\t\t\tTags: []string{\n\t\t\t\t\t\t\t\"alpine:fake\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tDistro: &distro.Distro{\n\t\t\t\t\tType:    \"alpine\",\n\t\t\t\t\tVersion: \"3.12.0\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tspringImageTestCase,\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.Fixture, func(t *testing.T) {\n\t\t\tpkgs, context, _, err := syftSBOMProvider(test.Fixture, ProviderConfig{}, applyChannel)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"unable to parse: %+v\", err)\n\t\t\t}\n\n\t\t\tif m, ok := context.Source.Metadata.(source.ImageMetadata); ok {\n\t\t\t\tm.RawConfig = nil\n\t\t\t\tm.RawManifest = nil\n\n\t\t\t\tcontext.Source.Metadata = m\n\t\t\t}\n\n\t\t\tfor _, d := range deep.Equal(test.Packages, pkgs) {\n\t\t\t\tif strings.Contains(d, \".ID: \") {\n\t\t\t\t\t// today ID's get assigned by the collection, which will change in the future. But in the meantime\n\t\t\t\t\t// that means that these IDs are random and should not be counted as a difference we care about in\n\t\t\t\t\t// this test.\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tt.Errorf(\"pkg diff: %s\", d)\n\t\t\t}\n\n\t\t\tfor _, d := range deep.Equal(test.Context, context) {\n\t\t\t\tif strings.Contains(d, \"Distro.IDLike: <nil slice> != []\") {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tt.Errorf(\"ctx diff: %s\", d)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestParseSyftJSON_BadCPEs(t *testing.T) {\n\tapplyChannel := getDistroChannelApplier(testFixChannels())\n\tpkgs, _, _, err := syftSBOMProvider(\"testdata/syft-java-bad-cpes.json\", ProviderConfig{}, applyChannel)\n\tassert.NoError(t, err)\n\tassert.Len(t, pkgs, 1)\n}\n\n// Note that the fixture has been modified from the real syft output to include fewer packages, CPEs, layers,\n// and package IDs are removed so that the test case variable isn't unwieldingly huge.\nvar springImageTestCase = struct {\n\tFixture  string\n\tPackages []Package\n\tContext  Context\n}{\n\tFixture: \"testdata/syft-spring.json\",\n\tPackages: []Package{\n\t\t{\n\t\t\tName:    \"charsets\",\n\t\t\tVersion: \"\",\n\t\t\tLocations: file.NewLocationSet(\n\t\t\t\tfile.NewLocationFromCoordinates(file.Coordinates{\n\t\t\t\t\tRealPath:     \"/usr/lib/jvm/java-8-openjdk-amd64/jre/lib/charsets.jar\",\n\t\t\t\t\tFileSystemID: \"sha256:a1a6ceadb701ab4e6c93b243dc2a0daedc8cee23a24203845ecccd5784cd1393\",\n\t\t\t\t}),\n\t\t\t),\n\t\t\tLanguage: \"java\",\n\t\t\tLicenses: []string{},\n\t\t\tType:     \"java-archive\",\n\t\t\tCPEs: []cpe.CPE{\n\t\t\t\tcpe.Must(\"cpe:2.3:a:charsets:charsets:*:*:*:*:*:java:*:*\", \"\"),\n\t\t\t\tcpe.Must(\"cpe:2.3:a:charsets:charsets:*:*:*:*:*:maven:*:*\", \"\"),\n\t\t\t},\n\t\t\tPURL:     \"\",\n\t\t\tMetadata: JavaMetadata{VirtualPath: \"/usr/lib/jvm/java-8-openjdk-amd64/jre/lib/charsets.jar\"},\n\t\t},\n\t\t{\n\t\t\tName:    \"tomcat-embed-el\",\n\t\t\tVersion: \"9.0.27\",\n\t\t\tLocations: file.NewLocationSet(\n\t\t\t\tfile.NewLocationFromCoordinates(file.Coordinates{\n\t\t\t\t\tRealPath:     \"/app/libs/tomcat-embed-el-9.0.27.jar\",\n\t\t\t\t\tFileSystemID: \"sha256:89504f083d3f15322f97ae240df44650203f24427860db1b3d32e66dd05940e4\",\n\t\t\t\t}),\n\t\t\t),\n\t\t\tLanguage: \"java\",\n\t\t\tLicenses: []string{},\n\t\t\tType:     \"java-archive\",\n\t\t\tCPEs: []cpe.CPE{\n\t\t\t\tcpe.Must(\"cpe:2.3:a:tomcat_embed_el:tomcat-embed-el:9.0.27:*:*:*:*:java:*:*\", \"\"),\n\t\t\t\tcpe.Must(\"cpe:2.3:a:tomcat-embed-el:tomcat_embed_el:9.0.27:*:*:*:*:maven:*:*\", \"\"),\n\t\t\t},\n\t\t\tPURL:     \"\",\n\t\t\tMetadata: JavaMetadata{VirtualPath: \"/app/libs/tomcat-embed-el-9.0.27.jar\"},\n\t\t},\n\t},\n\tContext: Context{\n\t\tSource: &source.Description{\n\t\t\tMetadata: source.ImageMetadata{\n\t\t\t\tUserInput: \"springio/gs-spring-boot-docker:latest\",\n\t\t\t\tLayers: []source.LayerMetadata{\n\t\t\t\t\t{\n\t\t\t\t\t\tMediaType: \"application/vnd.docker.image.rootfs.diff.tar.gzip\",\n\t\t\t\t\t\tDigest:    \"sha256:42a3027eaac150d2b8f516100921f4bd83b3dbc20bfe64124f686c072b49c602\",\n\t\t\t\t\t\tSize:      1809479,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tSize:           142807921,\n\t\t\t\tID:             \"sha256:9065659c6e537b0364b7b1d3e5442a3a5aa56d755fb883d221e9e8b3637fb58e\",\n\t\t\t\tManifestDigest: \"sha256:be3d8a5f700d4c45f3ed324b95d9f028f587c135bc85cf87e193414db521d533\",\n\t\t\t\tMediaType:      \"application/vnd.docker.distribution.manifest.v2+json\",\n\t\t\t\tTags: []string{\n\t\t\t\t\t\"springio/gs-spring-boot-docker:latest\",\n\t\t\t\t},\n\t\t\t\tRepoDigests: []string{\"springio/gs-spring-boot-docker@sha256:39c2ffc784f5f34862e22c1f2ccdbcb62430736114c13f60111eabdb79decb08\"},\n\t\t\t},\n\t\t},\n\t\tDistro: &distro.Distro{\n\t\t\tType:    \"debian\",\n\t\t\tVersion: \"9\",\n\t\t},\n\t},\n}\n\nfunc Test_PurlList(t *testing.T) {\n\ttests := []struct {\n\t\tname        string\n\t\tconfig      ProviderConfig\n\t\tuserInput   string\n\t\twantContext Context\n\t\twantPkgs    []Package\n\t\twantErr     require.ErrorAssertionFunc\n\t}{\n\t\t{\n\t\t\tname:      \"takes multiple purls\",\n\t\t\tuserInput: \"purl:testdata/purl/valid-purl.txt\",\n\t\t\twantContext: Context{\n\t\t\t\tDistro: &distro.Distro{\n\t\t\t\t\tType:    \"debian\",\n\t\t\t\t\tIDLike:  []string{\"debian\"},\n\t\t\t\t\tVersion: \"8\",\n\t\t\t\t},\n\t\t\t\tSource: &source.Description{\n\t\t\t\t\tMetadata: SBOMFileMetadata{\n\t\t\t\t\t\tPath: \"testdata/purl/valid-purl.txt\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantPkgs: []Package{\n\t\t\t\t{\n\t\t\t\t\tName:    \"ant\",\n\t\t\t\t\tVersion: \"1.10.8\",\n\t\t\t\t\tType:    pkg.JavaPkg,\n\t\t\t\t\tPURL:    \"pkg:maven/org.apache.ant/ant@1.10.8\",\n\t\t\t\t\tDistro:  &distro.Distro{Type: distro.Debian, Version: \"8\", IDLike: []string{\"debian\"}},\n\t\t\t\t\tMetadata: JavaMetadata{\n\t\t\t\t\t\tPomArtifactID: \"ant\",\n\t\t\t\t\t\tPomGroupID:    \"org.apache.ant\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tName:    \"log4j-core\",\n\t\t\t\t\tVersion: \"2.14.1\",\n\t\t\t\t\tType:    pkg.JavaPkg,\n\t\t\t\t\tPURL:    \"pkg:maven/org.apache.logging.log4j/log4j-core@2.14.1\",\n\t\t\t\t\tDistro:  &distro.Distro{Type: distro.Debian, Version: \"8\", IDLike: []string{\"debian\"}},\n\t\t\t\t\tMetadata: JavaMetadata{\n\t\t\t\t\t\tPomArtifactID: \"log4j-core\",\n\t\t\t\t\t\tPomGroupID:    \"org.apache.logging.log4j\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tName:    \"sysv-rc\",\n\t\t\t\t\tVersion: \"2.88dsf-59\",\n\t\t\t\t\tType:    pkg.DebPkg,\n\t\t\t\t\tPURL:    \"pkg:deb/debian/sysv-rc@2.88dsf-59?arch=all&distro=debian-8&upstream=sysvinit\",\n\t\t\t\t\tDistro:  &distro.Distro{Type: distro.Debian, Version: \"8\", IDLike: []string{\"debian\"}},\n\t\t\t\t\tUpstreams: []UpstreamPackage{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName: \"sysvinit\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:      \"infer context when distro is present for multiple similar purls\",\n\t\t\tuserInput: \"purl:testdata/purl/homogeneous-os.txt\",\n\t\t\twantContext: Context{\n\t\t\t\tDistro: &distro.Distro{\n\t\t\t\t\tType:    \"alpine\",\n\t\t\t\t\tIDLike:  []string{\"alpine\"},\n\t\t\t\t\tVersion: \"3.20.3\",\n\t\t\t\t},\n\t\t\t\tSource: &source.Description{\n\t\t\t\t\tMetadata: SBOMFileMetadata{\n\t\t\t\t\t\tPath: \"testdata/purl/homogeneous-os.txt\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantPkgs: []Package{\n\t\t\t\t{\n\t\t\t\t\tName:    \"openssl\",\n\t\t\t\t\tVersion: \"3.2.1\",\n\t\t\t\t\tType:    pkg.ApkPkg,\n\t\t\t\t\tPURL:    \"pkg:apk/openssl@3.2.1?arch=aarch64&distro=alpine-3.20.3\",\n\t\t\t\t\tDistro:  &distro.Distro{Type: distro.Alpine, Version: \"3.20.3\", IDLike: []string{\"alpine\"}},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tName:    \"curl\",\n\t\t\t\t\tVersion: \"7.61.1\",\n\t\t\t\t\tType:    pkg.ApkPkg,\n\t\t\t\t\tPURL:    \"pkg:apk/curl@7.61.1?arch=aarch64&distro=alpine-3.20.3\",\n\t\t\t\t\tDistro:  &distro.Distro{Type: distro.Alpine, Version: \"3.20.3\", IDLike: []string{\"alpine\"}},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:      \"different distro info in purls does not infer context\",\n\t\t\tuserInput: \"purl:testdata/purl/different-os.txt\",\n\t\t\twantContext: Context{\n\t\t\t\t// important: no distro info inferred\n\t\t\t\tSource: &source.Description{\n\t\t\t\t\tMetadata: SBOMFileMetadata{\n\t\t\t\t\t\tPath: \"testdata/purl/different-os.txt\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantPkgs: []Package{\n\t\t\t\t{\n\t\t\t\t\tName:    \"openssl\",\n\t\t\t\t\tVersion: \"3.2.1\",\n\t\t\t\t\tType:    pkg.ApkPkg,\n\t\t\t\t\tPURL:    \"pkg:apk/openssl@3.2.1?arch=aarch64&distro=alpine-3.20.3\",\n\t\t\t\t\tDistro:  &distro.Distro{Type: distro.Alpine, Version: \"3.20.3\", IDLike: []string{\"alpine\"}},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tName:    \"curl\",\n\t\t\t\t\tVersion: \"7.61.1\",\n\t\t\t\t\tType:    pkg.ApkPkg,\n\t\t\t\t\tPURL:    \"pkg:apk/curl@7.61.1?arch=aarch64&distro=alpine-3.20.2\",\n\t\t\t\t\tDistro:  &distro.Distro{Type: distro.Alpine, Version: \"3.20.2\", IDLike: []string{\"alpine\"}},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:      \"fails on path with nonexistent file\",\n\t\t\tuserInput: \"purl:tttt/empty.txt\",\n\t\t\twantErr:   require.Error,\n\t\t},\n\t\t{\n\t\t\tname:      \"fails on invalid path\",\n\t\t\tuserInput: \"purl:~&&\",\n\t\t\twantErr:   require.Error,\n\t\t},\n\t\t{\n\t\t\tname:      \"fails for empty purl file\",\n\t\t\tuserInput: \"purl:testdata/purl/empty.json\",\n\t\t\twantErr:   require.Error,\n\t\t},\n\t\t{\n\t\t\tname:      \"fails on invalid purl in file\",\n\t\t\tuserInput: \"purl:testdata/purl/invalid-purl.txt\",\n\t\t\twantErr:   require.Error,\n\t\t},\n\t\t{\n\t\t\tname: \"honors default channel configuration (no EUS)\",\n\t\t\tconfig: ProviderConfig{\n\t\t\t\tSynthesisConfig: SynthesisConfig{\n\t\t\t\t\tDistro: DistroConfig{\n\t\t\t\t\t\tFixChannels: testFixChannels(),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tuserInput: \"purl:testdata/purl/valid-rhel-9.txt\",\n\t\t\twantContext: Context{\n\t\t\t\tSource: &source.Description{\n\t\t\t\t\tMetadata: SBOMFileMetadata{\n\t\t\t\t\t\tPath: \"testdata/purl/valid-rhel-9.txt\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tDistro: &distro.Distro{\n\t\t\t\t\tType:    distro.RedHat,\n\t\t\t\t\tIDLike:  []string{\"redhat\"},\n\t\t\t\t\tVersion: \"9.4\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twantPkgs: []Package{\n\t\t\t\t{\n\t\t\t\t\tName:    \"kernel\",\n\t\t\t\t\tVersion: \"0:5.14.0-100\",\n\t\t\t\t\tType:    pkg.RpmPkg,\n\t\t\t\t\tDistro:  &distro.Distro{Type: distro.RedHat, Version: \"9.4\", IDLike: []string{\"redhat\"}},\n\t\t\t\t\tPURL:    \"pkg:rpm/redhat/kernel@0:5.14.0-100?distro=rhel-9.4\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"honors default channel configuration (EUS)\",\n\t\t\tconfig: ProviderConfig{\n\t\t\t\tSynthesisConfig: SynthesisConfig{\n\t\t\t\t\tDistro: DistroConfig{\n\t\t\t\t\t\tFixChannels: testFixChannels(),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tuserInput: \"purl:testdata/purl/valid-rhel-9+eus.txt\",\n\t\t\twantContext: Context{\n\t\t\t\tSource: &source.Description{\n\t\t\t\t\tMetadata: SBOMFileMetadata{\n\t\t\t\t\t\tPath: \"testdata/purl/valid-rhel-9+eus.txt\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tDistro: &distro.Distro{\n\t\t\t\t\tType:     distro.RedHat,\n\t\t\t\t\tIDLike:   []string{\"redhat\"},\n\t\t\t\t\tChannels: names(\"eus\"), // important!\n\t\t\t\t\tVersion:  \"9.4\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twantPkgs: []Package{\n\t\t\t\t{\n\t\t\t\t\tName:    \"kernel\",\n\t\t\t\t\tVersion: \"0:5.14.0-100\",\n\t\t\t\t\tType:    pkg.RpmPkg,\n\t\t\t\t\tDistro:  &distro.Distro{Type: distro.RedHat, Version: \"9.4\", Channels: names(\"eus\"), IDLike: []string{\"redhat\"}},\n\t\t\t\t\tPURL:    \"pkg:rpm/redhat/kernel@0:5.14.0-100?distro=rhel-9.4+eus\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tif tc.wantErr == nil {\n\t\t\t\ttc.wantErr = require.NoError\n\t\t\t}\n\n\t\t\tpackages, ctx, _, err := Provide(tc.userInput, tc.config)\n\n\t\t\ttc.wantErr(t, err)\n\t\t\tif err != nil {\n\t\t\t\trequire.Nil(t, packages)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif d := cmp.Diff(tc.wantContext, ctx, diffOpts...); d != \"\" {\n\t\t\t\tt.Errorf(\"unexpected context (-want +got):\\n%s\", d)\n\t\t\t}\n\t\t\trequire.Len(t, packages, len(tc.wantPkgs))\n\n\t\t\tslices.SortFunc(packages, func(a, b Package) int {\n\t\t\t\treturn strings.Compare(a.Name, b.Name)\n\t\t\t})\n\t\t\tslices.SortFunc(tc.wantPkgs, func(a, b Package) int {\n\t\t\t\treturn strings.Compare(a.Name, b.Name)\n\t\t\t})\n\n\t\t\tfor idx, expected := range tc.wantPkgs {\n\t\t\t\tif d := cmp.Diff(expected, packages[idx], diffOpts...); d != \"\" {\n\t\t\t\t\tt.Errorf(\"unexpected context (-want +got):\\n%s\", d)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc testFixChannels() []distro.FixChannel {\n\treturn distro.DefaultFixChannels()\n}\n"
  },
  {
    "path": "grype/pkg/testdata/alpine-tampered.att.json",
    "content": "{\"payloadType\":\"application/vnd.in-toto+json\",\"payload\":\"eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjAuMSIsInByZWRpY2F0ZVR5cGUiOiJodHRwczovL3N5ZnQuZGV2L2JvbSIsInN1YmplY3QiOlt7Im5hbWUiOiIiLCJkaWdlc3QiOnsic2hhMjU2IjoiNmFmMWIxMWJiYjE3ZjRjMzExZTI2OWRiNjUzMGU0ZGEyNzM4MjYyYWY1ZmQ5MDY0Y2NkZjEwOWI3NjU4NjBmYiJ9fV0sInByZWRpY2F0ZSI6eyJhcnRpZmFjdFJlbGF0aW9uc2hpcHMiOlt7ImNoaWxkIjoiNGFjYTZkMTVkZjA5ZGExZCIsInBhcmVudCI6IjRhYzcxMzZiODUzNmNkZWEiLCJ0eXBlIjoiY29udGFpbnMifSx7ImNoaWxkIjoiNGFjYTZkMTVkZjA5ZGExZCIsInBhcmVudCI6IjRhYzcxMzZiODUzNmNkZWEiLCJ0eXBlIjoiY29udGFpbnMifSx7ImNoaWxkIjoiNmM3MzE0NGVhOWVmNGZiOSIsInBhcmVudCI6ImYyMTMyZThkNmNmZTAwNmEiLCJ0eXBlIjoiY29udGFpbnMifSx7ImNoaWxkIjoiNmM3MzE0NGVhOWVmNGZiOSIsInBhcmVudCI6ImYyMTMyZThkNmNmZTAwNmEiLCJ0eXBlIjoiY29udGFpbnMifSx7ImNoaWxkIjoiOWZhOTRlYThlODc0ZWU0OSIsInBhcmVudCI6ImYyMTMyZThkNmNmZTAwNmEiLCJ0eXBlIjoiY29udGFpbnMifSx7ImNoaWxkIjoiNTU5YTlkNTFlMzNkMjQxMCIsInBhcmVudCI6ImYyMTMyZThkNmNmZTAwNmEiLCJ0eXBlIjoiY29udGFpbnMifSx7ImNoaWxkIjoiZDU3ZmUwZGU3OTBmY2Q0YSIsInBhcmVudCI6ImYyMTMyZThkNmNmZTAwNmEiLCJ0eXBlIjoiY29udGFpbnMifSx7ImNoaWxkIjoiNmM2Zjk4Yzg1N2E2OTFjYyIsInBhcmVudCI6ImYyMTMyZThkNmNmZTAwNmEiLCJ0eXBlIjoiY29udGFpbnMifSx7ImNoaWxkIjoiYmIwODE4ZTZjZDI1Zjk1YSIsInBhcmVudCI6ImYyMTMyZThkNmNmZTAwNmEiLCJ0eXBlIjoiY29udGFpbnMifSx7ImNoaWxkIjoiMjgxNGM4ZjJkYWUyZTJlYSIsInBhcmVudCI6IjdmMDI0MWE3MGI2ODE4MTkiLCJ0eXBlIjoiY29udGFpbnMifSx7ImNoaWxkIjoiYzc5NWQzZjdlYmQ2NGU4NCIsInBhcmVudCI6IjdmMDI0MWE3MGI2ODE4MTkiLCJ0eXBlIjoiY29udGFpbnMifSx7ImNoaWxkIjoiYjE3ZTBmZWQzY2I3MDJhZCIsInBhcmVudCI6IjdmMDI0MWE3MGI2ODE4MTkiLCJ0eXBlIjoiY29udGFpbnMifSx7ImNoaWxkIjoiZGYyMmVkMWEzMWZkYmI2OCIsInBhcmVudCI6IjdmMDI0MWE3MGI2ODE4MTkiLCJ0eXBlIjoiY29udGFpbnMifSx7ImNoaWxkIjoiZTg1YWYxNGQ1MDMyNTc0NCIsInBhcmVudCI6IjdmMDI0MWE3MGI2ODE4MTkiLCJ0eXBlIjoiY29udGFpbnMifSx7ImNoaWxkIjoiZTkyNDY5NjBmZDQ4ZjI3MCIsInBhcmVudCI6IjdmMDI0MWE3MGI2ODE4MTkiLCJ0eXBlIjoiY29udGFpbnMifSx7ImNoaWxkIjoiZDFkMjNhNDczMTE5N2Q1ZSIsInBhcmVudCI6IjdmMDI0MWE3MGI2ODE4MTkiLCJ0eXBlIjoiY29udGFpbnMifSx7ImNoaWxkIjoiZGJlNzE2ZTA1ZjkxMWZmOSIsInBhcmVudCI6IjdmMDI0MWE3MGI2ODE4MTkiLCJ0eXBlIjoiY29udGFpbnMifSx7ImNoaWxkIjoiNmVkYzk3MGYwMjQ2OWRlYSIsInBhcmVudCI6IjdmMDI0MWE3MGI2ODE4MTkiLCJ0eXBlIjoiY29udGFpbnMifSx7ImNoaWxkIjoiOWEzNTc2ZTRhYWRiODJmMSIsInBhcmVudCI6IjdmMDI0MWE3MGI2ODE4MTkiLCJ0eXBlIjoiY29udGFpbnMifSx7ImNoaWxkIjoiNWEyNWQwZGJlODBhYjI0IiwicGFyZW50IjoiN2YwMjQxYTcwYjY4MTgxOSIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI1NTU4Zjc2MDM4ZDFjOTdlIiwicGFyZW50IjoiN2YwMjQxYTcwYjY4MTgxOSIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJhNzM5YTA0ZThjMmQ4ODk0IiwicGFyZW50IjoiN2YwMjQxYTcwYjY4MTgxOSIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI4ZGIxNjIwNmIzMzI2MDZlIiwicGFyZW50IjoiN2YwMjQxYTcwYjY4MTgxOSIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiIzODcxMmFkZWFlZGI3MzE2IiwicGFyZW50IjoiN2YwMjQxYTcwYjY4MTgxOSIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiIxNGRkOTg1ZGNlNGNkNWE1IiwicGFyZW50IjoiN2YwMjQxYTcwYjY4MTgxOSIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI1OTBjYzI2MzU4N2UyYzlkIiwicGFyZW50IjoiN2YwMjQxYTcwYjY4MTgxOSIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiIyNmEyMGYxMDZmYTMyNzgzIiwicGFyZW50IjoiN2YwMjQxYTcwYjY4MTgxOSIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJmMjlhZjI0NzU1MGRjYWM4IiwicGFyZW50IjoiN2YwMjQxYTcwYjY4MTgxOSIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI0YTZkNmUwZWE5NTc1MWI3IiwicGFyZW50IjoiN2YwMjQxYTcwYjY4MTgxOSIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiIxY2UxMDM1ZDAwMjE3NmE0IiwicGFyZW50IjoiN2YwMjQxYTcwYjY4MTgxOSIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJiOGYxZTRhNjJiMjE2NjNiIiwicGFyZW50IjoiN2YwMjQxYTcwYjY4MTgxOSIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI0NTZhNzAyZjE2NzMzYjI0IiwicGFyZW50IjoiN2YwMjQxYTcwYjY4MTgxOSIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiIxNGUzZjQxYTc3YjQyODA3IiwicGFyZW50IjoiN2YwMjQxYTcwYjY4MTgxOSIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJlNDFkYWQ4YTYxZmY4M2I2IiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJjOWJlYzUzYzBhM2UyMTA2IiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJhNWRmZDI5MTQ4MWM2OWUwIiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJjMTVmOTJkZGM3N2MxYWU0IiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJkMGMyNDUzZjk4YjMxMWQ4IiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI5MmQzNThlNzI1NmYwYjdhIiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiIyOTVjMjYzYjNkMWM4MWQyIiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJhMmJmNDE0MDJjYzA0MzE1IiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJkYjQ2MmJkNGNlOWJhZWM1IiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJjNWYxMjYyYTY2YmYwNDhhIiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJlZTNhZDljMGE0ZjU0ZTkzIiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI5MmQzNThlNzI1NmYwYjdhIiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI1OGZiZWJhNzc0ZWIyYTU2IiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJmYzgyODFmM2JmZDU3MWU1IiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJkYzFlMDQzYzY1MWRmYjRjIiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI3NmI1YWE4MDVkYzFiMThmIiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJhMDBmNzE2ODE5ZmFhODE5IiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJhM2U3NmYyMTkzM2I1MWM0IiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiIxMWQ0ZTc2OGMyZWI3NjJkIiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI1MjJhNjAyZTljMTAwZTYyIiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJhYzdhMGYyMWRlYzAzYTFhIiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiIzOTExNDVmMGVmZDM5Yjk0IiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiIyOTVjMjYzYjNkMWM4MWQyIiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI0YTk2ZjhjNGNmMzQ2NzU5IiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJjNWYxMjYyYTY2YmYwNDhhIiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiIxMWQ0ZTc2OGMyZWI3NjJkIiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJjNWYxMjYyYTY2YmYwNDhhIiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiIzOTExNDVmMGVmZDM5Yjk0IiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJkYzFlMDQzYzY1MWRmYjRjIiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI1OGZiZWJhNzc0ZWIyYTU2IiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI1MjJhNjAyZTljMTAwZTYyIiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI3NmI1YWE4MDVkYzFiMThmIiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI0YTk2ZjhjNGNmMzQ2NzU5IiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJmYzgyODFmM2JmZDU3MWU1IiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJhYzdhMGYyMWRlYzAzYTFhIiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJhMmJmNDE0MDJjYzA0MzE1IiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJkYjQ2MmJkNGNlOWJhZWM1IiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJhM2U3NmYyMTkzM2I1MWM0IiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJhMmJmNDE0MDJjYzA0MzE1IiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJlZTNhZDljMGE0ZjU0ZTkzIiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJhMDBmNzE2ODE5ZmFhODE5IiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI1YTg0NzZiZDZmNWExM2JmIiwicGFyZW50IjoiOGNlYjI3YTEyYzBiZmU3YiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI1YTg0NzZiZDZmNWExM2JmIiwicGFyZW50IjoiOGNlYjI3YTEyYzBiZmU3YiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI1YTg0NzZiZDZmNWExM2JmIiwicGFyZW50IjoiN2EyY2Y3MjdjYmFiODA3NCIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJiYzY2MzYyYjI1YzY0MzRhIiwicGFyZW50IjoiN2EyY2Y3MjdjYmFiODA3NCIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJkYzVjYmI3OGNjNzQwZmJlIiwicGFyZW50IjoiN2EyY2Y3MjdjYmFiODA3NCIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJkZWMyMzliNTY1ZDE5YjY0IiwicGFyZW50IjoiN2EyY2Y3MjdjYmFiODA3NCIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJiYTFhZDE1Mzk0YzUzOTIwIiwicGFyZW50IjoiN2EyY2Y3MjdjYmFiODA3NCIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJlNjA1NDJmYTMzZGFlYjA1IiwicGFyZW50IjoiN2EyY2Y3MjdjYmFiODA3NCIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiIyZDI0YzQwYTg4YjRjMmVjIiwicGFyZW50IjoiN2EyY2Y3MjdjYmFiODA3NCIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI1ZGIzOGRkY2U4OTEwNWU3IiwicGFyZW50IjoiN2EyY2Y3MjdjYmFiODA3NCIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJiYTFhZDE1Mzk0YzUzOTIwIiwicGFyZW50IjoiN2EyY2Y3MjdjYmFiODA3NCIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI2YjFjOWExOGNhYmFmM2VkIiwicGFyZW50IjoiMzA5NGM0YTYxMGIwYjAwZCIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI2YjFjOWExOGNhYmFmM2VkIiwicGFyZW50IjoiMzA5NGM0YTYxMGIwYjAwZCIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiIyYjAyZjY3OTBjNmVmN2U4IiwicGFyZW50IjoiOTFkYjE1ZDgwNGZlZGU1OSIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiIyYjAyZjY3OTBjNmVmN2U4IiwicGFyZW50IjoiOTFkYjE1ZDgwNGZlZGU1OSIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI5MTUwY2Y2ZGYxYWQ1Zjg2IiwicGFyZW50IjoiYzJlM2NlN2I5ZTcyZDBhZSIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiIxM2ExYWJkMWQyZDM0YTU3IiwicGFyZW50IjoiYmUwNmRmNmUzYmJiZjBhYiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiIxM2ExYWJkMWQyZDM0YTU3IiwicGFyZW50IjoiYmUwNmRmNmUzYmJiZjBhYiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI4NTI0ZGNmMDI5MzZkODE3IiwicGFyZW50IjoiNWVmNjZhMzM1ZGRjMDNhNiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiIyYzYyNGZiZjM5Y2E3ZDY0IiwicGFyZW50IjoiNWVmNjZhMzM1ZGRjMDNhNiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiIyYWY0MTJiMTBmZjJjNWZiIiwicGFyZW50IjoiMWYyOGRlMTIwMDczZDc4YSIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiIyMzcwMjZkMTM0Yjg0ZDZmIiwicGFyZW50IjoiNTNhOTA5ZjRkODcyYjkwIiwidHlwZSI6ImNvbnRhaW5zIn0seyJjaGlsZCI6IjNjYTdjYmZkNTkzMGQ5MGIiLCJwYXJlbnQiOiI1M2E5MDlmNGQ4NzJiOTAiLCJ0eXBlIjoiY29udGFpbnMifSx7ImNoaWxkIjoiOWFjYzQ2ZjlmOWQyNDA1OCIsInBhcmVudCI6IjUzYTkwOWY0ZDg3MmI5MCIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiIxNjJmMzI0MDM2NTYxNjQzIiwicGFyZW50IjoiNTNhOTA5ZjRkODcyYjkwIiwidHlwZSI6ImNvbnRhaW5zIn0seyJjaGlsZCI6ImEyOWI0NWI1YzAyZDAwMDciLCJwYXJlbnQiOiI1M2E5MDlmNGQ4NzJiOTAiLCJ0eXBlIjoiY29udGFpbnMifV0sImFydGlmYWN0cyI6W3siY3BlcyI6WyJjcGU6Mi4zOmE6YWxwaW5lLWJhc2VsYXlvdXQ6YWxwaW5lLWJhc2VsYXlvdXQ6My4yLjAtcjE4Oio6KjoqOio6KjoqOioiLCJjcGU6Mi4zOmE6YWxwaW5lLWJhc2VsYXlvdXQ6YWxwaW5lX2Jhc2VsYXlvdXQ6My4yLjAtcjE4Oio6KjoqOio6KjoqOioiLCJjcGU6Mi4zOmE6YWxwaW5lX2Jhc2VsYXlvdXQ6YWxwaW5lLWJhc2VsYXlvdXQ6My4yLjAtcjE4Oio6KjoqOio6KjoqOioiLCJjcGU6Mi4zOmE6YWxwaW5lX2Jhc2VsYXlvdXQ6YWxwaW5lX2Jhc2VsYXlvdXQ6My4yLjAtcjE4Oio6KjoqOio6KjoqOioiLCJjcGU6Mi4zOmE6YWxwaW5lOmFscGluZS1iYXNlbGF5b3V0OjMuMi4wLXIxODoqOio6KjoqOio6KjoqIiwiY3BlOjIuMzphOmFscGluZTphbHBpbmVfYmFzZWxheW91dDozLjIuMC1yMTg6KjoqOio6KjoqOio6KiJdLCJmb3VuZEJ5IjoiYXBrZGItY2F0YWxvZ2VyIiwiaWQiOiI3ZjAyNDFhNzBiNjgxODE5IiwibGFuZ3VhZ2UiOiIiLCJsaWNlbnNlcyI6WyJHUEwtMi4wLW9ubHkiXSwibG9jYXRpb25zIjpbeyJsYXllcklEIjoic2hhMjU2OmZmNzY4YTE0MTNiYTEwOTMwOGMwZDg5N2ZmYTU1NWUwNTJhNDZkMmNmNDcxMTc4ZjAwMWI4MjViNGMyMWYzNTQiLCJwYXRoIjoiL2xpYi9hcGsvZGIvaW5zdGFsbGVkIn1dLCJtZXRhZGF0YSI6eyJhcmNoaXRlY3R1cmUiOiJ4ODZfNjQiLCJkZXNjcmlwdGlvbiI6IkFscGluZSBiYXNlIGRpciBzdHJ1Y3R1cmUgYW5kIGluaXQgc2NyaXB0cyIsImZpbGVzIjpbeyJwYXRoIjoiL2RldiJ9LHsicGF0aCI6Ii9kZXYvcHRzIn0seyJwYXRoIjoiL2Rldi9zaG0ifSx7InBhdGgiOiIvZXRjIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTExUTdoTmU4UXBEUzUzMWd1cUNkclhCem9BL289In0sInBhdGgiOiIvZXRjL2ZzdGFiIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTEzSytvbEpnNWF5ekhTVk5Va2dnWkpYdUIrOVk9In0sInBhdGgiOiIvZXRjL2dyb3VwIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTE2blZ3WVZYUC90Q2h2VVBkdWtWRDJpZlhPbWM9In0sInBhdGgiOiIvZXRjL2hvc3RuYW1lIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTFCRDZ6SktaVFJXeXFHblBpNHRTZmQza3JzTVU9In0sInBhdGgiOiIvZXRjL2hvc3RzIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTFUc3RoYmhXN1F6V1JlMUUvTkt3VE91RDRwSGM9In0sInBhdGgiOiIvZXRjL2luaXR0YWIifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMXRvb2dqVWlwSEdjTWdFQ2dQSlg2NFN3VVQxTT0ifSwicGF0aCI6Ii9ldGMvbW9kdWxlcyJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExWG1kdVZWTlVSSFEyN1R2WXAxTHI1VE10RmNBPSJ9LCJwYXRoIjoiL2V0Yy9tb3RkIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTFraWxqaFhYSDFMbFFyb0hzRUpJa1BaZzJlaXc9In0sIm93bmVyR2lkIjoiMCIsIm93bmVyVWlkIjoiMCIsInBhdGgiOiIvZXRjL210YWIiLCJwZXJtaXNzaW9ucyI6Ijc3NyJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExVGNodXVMVWZ1cjBpenZmWlFaeGdOL0xKaEI4PSJ9LCJwYXRoIjoiL2V0Yy9wYXNzd2QifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMVZtSFBXUGpqdno0b0NzYm1ZQ1VCNHVXcFNrYz0ifSwicGF0aCI6Ii9ldGMvcHJvZmlsZSJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExb21LbHAzdmdHcTJacVl6eUQvS0hOZG84ckRjPSJ9LCJwYXRoIjoiL2V0Yy9wcm90b2NvbHMifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMTlXTEN2NUl0S2c0TUg3UldmTlJoMUk3YnlRYz0ifSwicGF0aCI6Ii9ldGMvc2VydmljZXMifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMWx0clBJQVcyekhlRGlhanNleDJCZG1xM3VxQT0ifSwib3duZXJHaWQiOiI0MiIsIm93bmVyVWlkIjoiMCIsInBhdGgiOiIvZXRjL3NoYWRvdyIsInBlcm1pc3Npb25zIjoiNjQwIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTFvam0yWWRwQ0o2Qi9hcEdEYVovU2RiMnhKa0E9In0sInBhdGgiOiIvZXRjL3NoZWxscyJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExNHVwejN0Zm5OeFprSUVzVWhXbjdYb2l3OTZnPSJ9LCJwYXRoIjoiL2V0Yy9zeXNjdGwuY29uZiJ9LHsicGF0aCI6Ii9ldGMvYXBrIn0seyJwYXRoIjoiL2V0Yy9jb25mLmQifSx7InBhdGgiOiIvZXRjL2Nyb250YWJzIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTF2ZmsxYXBVV0k0eUxKR2hoTlJkMGtKaXhmdlk9In0sIm93bmVyR2lkIjoiMCIsIm93bmVyVWlkIjoiMCIsInBhdGgiOiIvZXRjL2Nyb250YWJzL3Jvb3QiLCJwZXJtaXNzaW9ucyI6IjYwMCJ9LHsicGF0aCI6Ii9ldGMvaW5pdC5kIn0seyJwYXRoIjoiL2V0Yy9tb2Rwcm9iZS5kIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTFXVWJoNlRCWU5WSzdlNFkrdVV2THMvN3ZpcWs9In0sInBhdGgiOiIvZXRjL21vZHByb2JlLmQvYWxpYXNlcy5jb25mIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTE0VGRnRkhrVGR0M3VRQytOQnRybnRPbm05bjQ9In0sInBhdGgiOiIvZXRjL21vZHByb2JlLmQvYmxhY2tsaXN0LmNvbmYifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMXBuYXkvbmpuNm9sOWNDc3NMN0tpWlo4ZXRsYz0ifSwicGF0aCI6Ii9ldGMvbW9kcHJvYmUuZC9pMzg2LmNvbmYifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMXluYkxuM0dZRHB2YWpiYS9sZHAxbmlheWVvZz0ifSwicGF0aCI6Ii9ldGMvbW9kcHJvYmUuZC9rbXMuY29uZiJ9LHsicGF0aCI6Ii9ldGMvbW9kdWxlcy1sb2FkLmQifSx7InBhdGgiOiIvZXRjL25ldHdvcmsifSx7InBhdGgiOiIvZXRjL25ldHdvcmsvaWYtZG93bi5kIn0seyJwYXRoIjoiL2V0Yy9uZXR3b3JrL2lmLXBvc3QtZG93bi5kIn0seyJwYXRoIjoiL2V0Yy9uZXR3b3JrL2lmLXByZS11cC5kIn0seyJwYXRoIjoiL2V0Yy9uZXR3b3JrL2lmLXVwLmQifSx7InBhdGgiOiIvZXRjL29wdCJ9LHsicGF0aCI6Ii9ldGMvcGVyaW9kaWMifSx7InBhdGgiOiIvZXRjL3BlcmlvZGljLzE1bWluIn0seyJwYXRoIjoiL2V0Yy9wZXJpb2RpYy9kYWlseSJ9LHsicGF0aCI6Ii9ldGMvcGVyaW9kaWMvaG91cmx5In0seyJwYXRoIjoiL2V0Yy9wZXJpb2RpYy9tb250aGx5In0seyJwYXRoIjoiL2V0Yy9wZXJpb2RpYy93ZWVrbHkifSx7InBhdGgiOiIvZXRjL3Byb2ZpbGUuZCJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExMzVPV3NDenp2bkIyZm1GeDYya2JxbTFBeDFrPSJ9LCJwYXRoIjoiL2V0Yy9wcm9maWxlLmQvUkVBRE1FIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTEwd0wyM0d1U0NWZnVtTVJnYWthYlVJNkVzU2s9In0sInBhdGgiOiIvZXRjL3Byb2ZpbGUuZC9jb2xvcl9wcm9tcHQuc2guZGlzYWJsZWQifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMVM4aitXVzcxbVd4ZlZ5OHl0aHFVN0hVVm9Cdz0ifSwicGF0aCI6Ii9ldGMvcHJvZmlsZS5kL2xvY2FsZS5zaCJ9LHsicGF0aCI6Ii9ldGMvc3lzY3RsLmQifSx7InBhdGgiOiIvaG9tZSJ9LHsicGF0aCI6Ii9saWIifSx7InBhdGgiOiIvbGliL2Zpcm13YXJlIn0seyJwYXRoIjoiL2xpYi9tZGV2In0seyJwYXRoIjoiL2xpYi9tb2R1bGVzLWxvYWQuZCJ9LHsicGF0aCI6Ii9saWIvc3lzY3RsLmQifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMUhwRWx6VzF4RWdtS2ZFUnRUeTdvb21tbnE2Yz0ifSwicGF0aCI6Ii9saWIvc3lzY3RsLmQvMDAtYWxwaW5lLmNvbmYifSx7InBhdGgiOiIvbWVkaWEifSx7InBhdGgiOiIvbWVkaWEvY2Ryb20ifSx7InBhdGgiOiIvbWVkaWEvZmxvcHB5In0seyJwYXRoIjoiL21lZGlhL3VzYiJ9LHsicGF0aCI6Ii9tbnQifSx7InBhdGgiOiIvb3B0In0seyJwYXRoIjoiL3Byb2MifSx7Im93bmVyR2lkIjoiMCIsIm93bmVyVWlkIjoiMCIsInBhdGgiOiIvcm9vdCIsInBlcm1pc3Npb25zIjoiNzAwIn0seyJwYXRoIjoiL3J1biJ9LHsicGF0aCI6Ii9zYmluIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTFxamtkeVJKY1libEdDNlJNcVVSNEJkYjVnMTA9In0sIm93bmVyR2lkIjoiMCIsIm93bmVyVWlkIjoiMCIsInBhdGgiOiIvc2Jpbi9ta21udGRpcnMiLCJwZXJtaXNzaW9ucyI6Ijc1NSJ9LHsicGF0aCI6Ii9zcnYifSx7InBhdGgiOiIvc3lzIn0seyJvd25lckdpZCI6IjAiLCJvd25lclVpZCI6IjAiLCJwYXRoIjoiL3RtcCIsInBlcm1pc3Npb25zIjoiMTc3NyJ9LHsicGF0aCI6Ii91c3IifSx7InBhdGgiOiIvdXNyL2xpYiJ9LHsicGF0aCI6Ii91c3IvbGliL21vZHVsZXMtbG9hZC5kIn0seyJwYXRoIjoiL3Vzci9sb2NhbCJ9LHsicGF0aCI6Ii91c3IvbG9jYWwvYmluIn0seyJwYXRoIjoiL3Vzci9sb2NhbC9saWIifSx7InBhdGgiOiIvdXNyL2xvY2FsL3NoYXJlIn0seyJwYXRoIjoiL3Vzci9zYmluIn0seyJwYXRoIjoiL3Vzci9zaGFyZSJ9LHsicGF0aCI6Ii91c3Ivc2hhcmUvbWFuIn0seyJwYXRoIjoiL3Vzci9zaGFyZS9taXNjIn0seyJwYXRoIjoiL3ZhciJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExMS9TTlp6LzhjSzJkU0tLK2NKcFZyWkl1RjRRPSJ9LCJvd25lckdpZCI6IjAiLCJvd25lclVpZCI6IjAiLCJwYXRoIjoiL3Zhci9ydW4iLCJwZXJtaXNzaW9ucyI6Ijc3NyJ9LHsicGF0aCI6Ii92YXIvY2FjaGUifSx7InBhdGgiOiIvdmFyL2NhY2hlL21pc2MifSx7Im93bmVyR2lkIjoiMCIsIm93bmVyVWlkIjoiMCIsInBhdGgiOiIvdmFyL2VtcHR5IiwicGVybWlzc2lvbnMiOiI1NTUifSx7InBhdGgiOiIvdmFyL2xpYiJ9LHsicGF0aCI6Ii92YXIvbGliL21pc2MifSx7InBhdGgiOiIvdmFyL2xvY2FsIn0seyJwYXRoIjoiL3Zhci9sb2NrIn0seyJwYXRoIjoiL3Zhci9sb2NrL3N1YnN5cyJ9LHsicGF0aCI6Ii92YXIvbG9nIn0seyJwYXRoIjoiL3Zhci9tYWlsIn0seyJwYXRoIjoiL3Zhci9vcHQifSx7InBhdGgiOiIvdmFyL3Nwb29sIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTFkemJkYXpZWkEyblR6U0lHM1l5Tnc3ZDRKdWM9In0sIm93bmVyR2lkIjoiMCIsIm93bmVyVWlkIjoiMCIsInBhdGgiOiIvdmFyL3Nwb29sL21haWwiLCJwZXJtaXNzaW9ucyI6Ijc3NyJ9LHsicGF0aCI6Ii92YXIvc3Bvb2wvY3JvbiJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExT0ZadCtaTXA3ajBHbnkwcnFTS3VXSnlxWW1BPSJ9LCJvd25lckdpZCI6IjAiLCJvd25lclVpZCI6IjAiLCJwYXRoIjoiL3Zhci9zcG9vbC9jcm9uL2Nyb250YWJzIiwicGVybWlzc2lvbnMiOiI3NzcifSx7Im93bmVyR2lkIjoiMCIsIm93bmVyVWlkIjoiMCIsInBhdGgiOiIvdmFyL3RtcCIsInBlcm1pc3Npb25zIjoiMTc3NyJ9XSwiZ2l0Q29tbWl0T2ZBcGtQb3J0IjoiZGZhMTM3OTM1N2EzMjFlNjM4ZmVlZjFjZDhkNTVhYjAzZDAyMGY0NSIsImluc3RhbGxlZFNpemUiOjQxMzY5NiwibGljZW5zZSI6IkdQTC0yLjAtb25seSIsIm1haW50YWluZXIiOiJOYXRhbmFlbCBDb3BhIDxuY29wYUBhbHBpbmVsaW51eC5vcmc+Iiwib3JpZ2luUGFja2FnZSI6ImFscGluZS1iYXNlbGF5b3V0IiwicGFja2FnZSI6ImFscGluZS1iYXNlbGF5b3V0IiwicHVsbENoZWNrc3VtIjoiUTFFeW1TNnJBZ21HczdYWWhxZHlFb2lXZ0VaNkE9IiwicHVsbERlcGVuZGVuY2llcyI6Ii9iaW4vc2ggc286bGliYy5tdXNsLXg4Nl82NC5zby4xIiwic2l6ZSI6MjExMDEsInVybCI6Imh0dHBzOi8vZ2l0LmFscGluZWxpbnV4Lm9yZy9jZ2l0L2Fwb3J0cy90cmVlL21haW4vYWxwaW5lLWJhc2VsYXlvdXQiLCJ2ZXJzaW9uIjoiMy4yLjAtcjE4In0sIm1ldGFkYXRhVHlwZSI6IkFwa01ldGFkYXRhIiwibmFtZSI6ImFscGluZS1iYXNlbGF5b3V0IiwicHVybCI6InBrZzphbHBpbmUvYWxwaW5lLWJhc2VsYXlvdXRAMy4yLjAtcjE4P2FyY2g9eDg2XzY0JnVwc3RyZWFtPWFscGluZS1iYXNlbGF5b3V0JmRpc3Rybz1hbHBpbmUtMy4xNS4yIiwidHlwZSI6ImFwayIsInZlcnNpb24iOiIzLjIuMC1yMTgifSx7ImNwZXMiOlsiY3BlOjIuMzphOmFscGluZS1rZXlzOmFscGluZS1rZXlzOjIuNC1yMToqOio6KjoqOio6KjoqIiwiY3BlOjIuMzphOmFscGluZS1rZXlzOmFscGluZV9rZXlzOjIuNC1yMToqOio6KjoqOio6KjoqIiwiY3BlOjIuMzphOmFscGluZV9rZXlzOmFscGluZS1rZXlzOjIuNC1yMToqOio6KjoqOio6KjoqIiwiY3BlOjIuMzphOmFscGluZV9rZXlzOmFscGluZV9rZXlzOjIuNC1yMToqOio6KjoqOio6KjoqIiwiY3BlOjIuMzphOmFscGluZTphbHBpbmUta2V5czoyLjQtcjE6KjoqOio6KjoqOio6KiIsImNwZToyLjM6YTphbHBpbmU6YWxwaW5lX2tleXM6Mi40LXIxOio6KjoqOio6KjoqOioiXSwiZm91bmRCeSI6ImFwa2RiLWNhdGFsb2dlciIsImlkIjoiMTk5N2RkMWM4ZmY3MmRjMiIsImxhbmd1YWdlIjoiIiwibGljZW5zZXMiOlsiTUlUIl0sImxvY2F0aW9ucyI6W3sibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii9saWIvYXBrL2RiL2luc3RhbGxlZCJ9XSwibWV0YWRhdGEiOnsiYXJjaGl0ZWN0dXJlIjoieDg2XzY0IiwiZGVzY3JpcHRpb24iOiJQdWJsaWMga2V5cyBmb3IgQWxwaW5lIExpbnV4IHBhY2thZ2VzIiwiZmlsZXMiOlt7InBhdGgiOiIvZXRjIn0seyJwYXRoIjoiL2V0Yy9hcGsifSx7InBhdGgiOiIvZXRjL2Fway9rZXlzIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTFPdkNGU085NHo5N2M4MG1JREN4cUdraDJPZzQ9In0sInBhdGgiOiIvZXRjL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNGE2YTA4NDAucnNhLnB1YiJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExdjdZV1pZekFXb2NsYUxESTQ1akVndUk3WU4wPSJ9LCJwYXRoIjoiL2V0Yy9hcGsva2V5cy9hbHBpbmUtZGV2ZWxAbGlzdHMuYWxwaW5lbGludXgub3JnLTUyNDNlZjRiLnJzYS5wdWIifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMU5uR3VEc2RRT3g0Wk5ZZkIzTjk3ZUx5R1BrST0ifSwicGF0aCI6Ii9ldGMvYXBrL2tleXMvYWxwaW5lLWRldmVsQGxpc3RzLmFscGluZWxpbnV4Lm9yZy01MjYxY2VjYi5yc2EucHViIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTFsWmxURVNOcmVsV1ROa0wvb1F6bUFVOGE5OUE9In0sInBhdGgiOiIvZXRjL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNjE2NWVlNTkucnNhLnB1YiJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExV05XNlN5ODdIcEozSWRlbVF5OHBqdTMzS21zPSJ9LCJwYXRoIjoiL2V0Yy9hcGsva2V5cy9hbHBpbmUtZGV2ZWxAbGlzdHMuYWxwaW5lbGludXgub3JnLTYxNjY2ZTNmLnJzYS5wdWIifSx7InBhdGgiOiIvdXNyIn0seyJwYXRoIjoiL3Vzci9zaGFyZSJ9LHsicGF0aCI6Ii91c3Ivc2hhcmUvYXBrIn0seyJwYXRoIjoiL3Vzci9zaGFyZS9hcGsva2V5cyJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExT3ZDRlNPOTR6OTdjODBtSURDeHFHa2gyT2c0PSJ9LCJwYXRoIjoiL3Vzci9zaGFyZS9hcGsva2V5cy9hbHBpbmUtZGV2ZWxAbGlzdHMuYWxwaW5lbGludXgub3JnLTRhNmEwODQwLnJzYS5wdWIifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMXY3WVdaWXpBV29jbGFMREk0NWpFZ3VJN1lOMD0ifSwicGF0aCI6Ii91c3Ivc2hhcmUvYXBrL2tleXMvYWxwaW5lLWRldmVsQGxpc3RzLmFscGluZWxpbnV4Lm9yZy01MjQzZWY0Yi5yc2EucHViIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTFCVHFTK0gvVVV5aFF1ekh3aUJsNDcrQlRLdVU9In0sInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNTI0ZDI3YmIucnNhLnB1YiJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExTm5HdURzZFFPeDRaTllmQjNOOTdlTHlHUGtJPSJ9LCJwYXRoIjoiL3Vzci9zaGFyZS9hcGsva2V5cy9hbHBpbmUtZGV2ZWxAbGlzdHMuYWxwaW5lbGludXgub3JnLTUyNjFjZWNiLnJzYS5wdWIifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMU9heGRjc2E2QVlvUGRMaTBVNGxPM0oyd2UxOD0ifSwicGF0aCI6Ii91c3Ivc2hhcmUvYXBrL2tleXMvYWxwaW5lLWRldmVsQGxpc3RzLmFscGluZWxpbnV4Lm9yZy01ODE5OWRjYy5yc2EucHViIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTF5UHErc3U2NWtzTm94M3VYQitEUjdQMTgrUVU9In0sInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNThjYmI0NzYucnNhLnB1YiJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExTXBaRE5YMExlTEh2U093VlV5WGlYeDExTk4wPSJ9LCJwYXRoIjoiL3Vzci9zaGFyZS9hcGsva2V5cy9hbHBpbmUtZGV2ZWxAbGlzdHMuYWxwaW5lbGludXgub3JnLTU4ZTRmMTdkLnJzYS5wdWIifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMWdsQ1EvZUpidkE1eHFjc3dkakZyV3Y1Rm5rMD0ifSwicGF0aCI6Ii91c3Ivc2hhcmUvYXBrL2tleXMvYWxwaW5lLWRldmVsQGxpc3RzLmFscGluZWxpbnV4Lm9yZy01ZTY5Y2E1MC5yc2EucHViIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTFYVWRERW9OVHRqbHZyUytpdW5rNnppRmdJcFU9In0sInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNjBhYzIwOTkucnNhLnB1YiJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExbFpsVEVTTnJlbFdUTmtML29Rem1BVThhOTlBPSJ9LCJwYXRoIjoiL3Vzci9zaGFyZS9hcGsva2V5cy9hbHBpbmUtZGV2ZWxAbGlzdHMuYWxwaW5lbGludXgub3JnLTYxNjVlZTU5LnJzYS5wdWIifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMVdOVzZTeTg3SHBKM0lkZW1ReThwanUzM0ttcz0ifSwicGF0aCI6Ii91c3Ivc2hhcmUvYXBrL2tleXMvYWxwaW5lLWRldmVsQGxpc3RzLmFscGluZWxpbnV4Lm9yZy02MTY2NmUzZi5yc2EucHViIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTFJOUR5NmhyeWFjTDJZV1hnK0tsRTZXdndFZDQ9In0sInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNjE2YTk3MjQucnNhLnB1YiJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExTlNuc2dtY01iVTRnN2o1SmFOczB0VkhwSFZBPSJ9LCJwYXRoIjoiL3Vzci9zaGFyZS9hcGsva2V5cy9hbHBpbmUtZGV2ZWxAbGlzdHMuYWxwaW5lbGludXgub3JnLTYxNmFiYzIzLnJzYS5wdWIifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMVZhTUJCazRSeHY2Ym9QTEtGK0kwODVROHkyRT0ifSwicGF0aCI6Ii91c3Ivc2hhcmUvYXBrL2tleXMvYWxwaW5lLWRldmVsQGxpc3RzLmFscGluZWxpbnV4Lm9yZy02MTZhYzNiYy5yc2EucHViIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTEzaEpCTUhBVXF1UGJwNWpwQVBGalFJMlkxdlE9In0sInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNjE2YWRmZWIucnNhLnB1YiJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExVi9hNVA5cEtSSmI2dGloRTNlOE82eGFQZ0xVPSJ9LCJwYXRoIjoiL3Vzci9zaGFyZS9hcGsva2V5cy9hbHBpbmUtZGV2ZWxAbGlzdHMuYWxwaW5lbGludXgub3JnLTYxNmFlMzUwLnJzYS5wdWIifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMTN3TEpyY0tRYWpxbDVhMXA5UTQ1VStaWEVOQT0ifSwicGF0aCI6Ii91c3Ivc2hhcmUvYXBrL2tleXMvYWxwaW5lLWRldmVsQGxpc3RzLmFscGluZWxpbnV4Lm9yZy02MTZkYjMwZC5yc2EucHViIn0seyJwYXRoIjoiL3Vzci9zaGFyZS9hcGsva2V5cy9hYXJjaDY0In0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTE3ajluV0prUSt3Zkl1VlF6SUZybUZaN2ZTT2M9In0sIm93bmVyR2lkIjoiMCIsIm93bmVyVWlkIjoiMCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FhcmNoNjQvYWxwaW5lLWRldmVsQGxpc3RzLmFscGluZWxpbnV4Lm9yZy01ODE5OWRjYy5yc2EucHViIiwicGVybWlzc2lvbnMiOiI3NzcifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMXNucitRMVViZkh5Q3IvY21tdFZ2TUlTN1NHcz0ifSwib3duZXJHaWQiOiIwIiwib3duZXJVaWQiOiIwIiwicGF0aCI6Ii91c3Ivc2hhcmUvYXBrL2tleXMvYWFyY2g2NC9hbHBpbmUtZGV2ZWxAbGlzdHMuYWxwaW5lbGludXgub3JnLTYxNmFlMzUwLnJzYS5wdWIiLCJwZXJtaXNzaW9ucyI6Ijc3NyJ9LHsicGF0aCI6Ii91c3Ivc2hhcmUvYXBrL2tleXMvYXJtaGYifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMVU5UXRzZE4rcllaOVpoNzZFZlh5MDBKWkhNZz0ifSwib3duZXJHaWQiOiIwIiwib3duZXJVaWQiOiIwIiwicGF0aCI6Ii91c3Ivc2hhcmUvYXBrL2tleXMvYXJtaGYvYWxwaW5lLWRldmVsQGxpc3RzLmFscGluZWxpbnV4Lm9yZy01MjRkMjdiYi5yc2EucHViIiwicGVybWlzc2lvbnMiOiI3NzcifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMWJDK0FkUTBxV0JUbWVmWGlJMFB2bVlPSm9WUT0ifSwib3duZXJHaWQiOiIwIiwib3duZXJVaWQiOiIwIiwicGF0aCI6Ii91c3Ivc2hhcmUvYXBrL2tleXMvYXJtaGYvYWxwaW5lLWRldmVsQGxpc3RzLmFscGluZWxpbnV4Lm9yZy02MTZhOTcyNC5yc2EucHViIiwicGVybWlzc2lvbnMiOiI3NzcifSx7InBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FybXY3In0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTFVOVF0c2ROK3JZWjlaaDc2RWZYeTAwSlpITWc9In0sIm93bmVyR2lkIjoiMCIsIm93bmVyVWlkIjoiMCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FybXY3L2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNTI0ZDI3YmIucnNhLnB1YiIsInBlcm1pc3Npb25zIjoiNzc3In0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTF4YklWdTdTY3dxR0h4WEd3STIyYVNlNU9kVVk9In0sIm93bmVyR2lkIjoiMCIsIm93bmVyVWlkIjoiMCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FybXY3L2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNjE2YWRmZWIucnNhLnB1YiIsInBlcm1pc3Npb25zIjoiNzc3In0seyJwYXRoIjoiL3Vzci9zaGFyZS9hcGsva2V5cy9taXBzNjQifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMWhDWmRGeCtMdnpiTHRQczc1M2plNzhnRUVCUT0ifSwib3duZXJHaWQiOiIwIiwib3duZXJVaWQiOiIwIiwicGF0aCI6Ii91c3Ivc2hhcmUvYXBrL2tleXMvbWlwczY0L2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNWU2OWNhNTAucnNhLnB1YiIsInBlcm1pc3Npb25zIjoiNzc3In0seyJwYXRoIjoiL3Vzci9zaGFyZS9hcGsva2V5cy9wcGM2NGxlIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTF0MjFkaENMYlRKbUFIWFNDZU9NcS8ydmZTZ289In0sIm93bmVyR2lkIjoiMCIsIm93bmVyVWlkIjoiMCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL3BwYzY0bGUvYWxwaW5lLWRldmVsQGxpc3RzLmFscGluZWxpbnV4Lm9yZy01OGNiYjQ3Ni5yc2EucHViIiwicGVybWlzc2lvbnMiOiI3NzcifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMVBTOXpOSVBKYW5DOHFjc2M1cWFyRVdxaFY1UT0ifSwib3duZXJHaWQiOiIwIiwib3duZXJVaWQiOiIwIiwicGF0aCI6Ii91c3Ivc2hhcmUvYXBrL2tleXMvcHBjNjRsZS9hbHBpbmUtZGV2ZWxAbGlzdHMuYWxwaW5lbGludXgub3JnLTYxNmFiYzIzLnJzYS5wdWIiLCJwZXJtaXNzaW9ucyI6Ijc3NyJ9LHsicGF0aCI6Ii91c3Ivc2hhcmUvYXBrL2tleXMvcmlzY3Y2NCJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExTlZQYlphdmFYcHNJdEZ3UVlEV2Jwb3I3eVlFPSJ9LCJvd25lckdpZCI6IjAiLCJvd25lclVpZCI6IjAiLCJwYXRoIjoiL3Vzci9zaGFyZS9hcGsva2V5cy9yaXNjdjY0L2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNjBhYzIwOTkucnNhLnB1YiIsInBlcm1pc3Npb25zIjoiNzc3In0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTFVNnRmdUtSeTVKOEM2aWFLUE1aYVQvZTh0YkE9In0sIm93bmVyR2lkIjoiMCIsIm93bmVyVWlkIjoiMCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL3Jpc2N2NjQvYWxwaW5lLWRldmVsQGxpc3RzLmFscGluZWxpbnV4Lm9yZy02MTZkYjMwZC5yc2EucHViIiwicGVybWlzc2lvbnMiOiI3NzcifSx7InBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL3MzOTB4In0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTFzamJWMnIydzBJaDJ2d2R6QzRKcTZVSTdjTVE9In0sIm93bmVyR2lkIjoiMCIsIm93bmVyVWlkIjoiMCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL3MzOTB4L2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNThlNGYxN2QucnNhLnB1YiIsInBlcm1pc3Npb25zIjoiNzc3In0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTFsMDl4YTdSbmJPSUMxZEk5RnFiYUNmUy9HWFk9In0sIm93bmVyR2lkIjoiMCIsIm93bmVyVWlkIjoiMCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL3MzOTB4L2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNjE2YWMzYmMucnNhLnB1YiIsInBlcm1pc3Npb25zIjoiNzc3In0seyJwYXRoIjoiL3Vzci9zaGFyZS9hcGsva2V5cy94ODYifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMUlpNTFpN05yYzR1ZnQxNEhocXVnYVVxZEg2ND0ifSwib3duZXJHaWQiOiIwIiwib3duZXJVaWQiOiIwIiwicGF0aCI6Ii91c3Ivc2hhcmUvYXBrL2tleXMveDg2L2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNGE2YTA4NDAucnNhLnB1YiIsInBlcm1pc3Npb25zIjoiNzc3In0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTFZNDllVnhocHZmdGJRM3lBZHZsTGZjclBMVFU9In0sIm93bmVyR2lkIjoiMCIsIm93bmVyVWlkIjoiMCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL3g4Ni9hbHBpbmUtZGV2ZWxAbGlzdHMuYWxwaW5lbGludXgub3JnLTUyNDNlZjRiLnJzYS5wdWIiLCJwZXJtaXNzaW9ucyI6Ijc3NyJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExSGpkdmNWa3BCWnpyMWFTZTNwN29RZkF0bS9FPSJ9LCJvd25lckdpZCI6IjAiLCJvd25lclVpZCI6IjAiLCJwYXRoIjoiL3Vzci9zaGFyZS9hcGsva2V5cy94ODYvYWxwaW5lLWRldmVsQGxpc3RzLmFscGluZWxpbnV4Lm9yZy02MTY2NmUzZi5yc2EucHViIiwicGVybWlzc2lvbnMiOiI3NzcifSx7InBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL3g4Nl82NCJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExSWk1MWk3TnJjNHVmdDE0SGhxdWdhVXFkSDY0PSJ9LCJvd25lckdpZCI6IjAiLCJvd25lclVpZCI6IjAiLCJwYXRoIjoiL3Vzci9zaGFyZS9hcGsva2V5cy94ODZfNjQvYWxwaW5lLWRldmVsQGxpc3RzLmFscGluZWxpbnV4Lm9yZy00YTZhMDg0MC5yc2EucHViIiwicGVybWlzc2lvbnMiOiI3NzcifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMUFVRlkrZndTQlRjcllldGpUN05IdmFmclNRYz0ifSwib3duZXJHaWQiOiIwIiwib3duZXJVaWQiOiIwIiwicGF0aCI6Ii91c3Ivc2hhcmUvYXBrL2tleXMveDg2XzY0L2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNTI2MWNlY2IucnNhLnB1YiIsInBlcm1pc3Npb25zIjoiNzc3In0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTFxS0EyM1Z6TVVEbGUrRHFucnI1S3orWHZ0eTQ9In0sIm93bmVyR2lkIjoiMCIsIm93bmVyVWlkIjoiMCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL3g4Nl82NC9hbHBpbmUtZGV2ZWxAbGlzdHMuYWxwaW5lbGludXgub3JnLTYxNjVlZTU5LnJzYS5wdWIiLCJwZXJtaXNzaW9ucyI6Ijc3NyJ9XSwiZ2l0Q29tbWl0T2ZBcGtQb3J0IjoiYWFiNjhmOGM5YWI0MzRhNDY3MTBkZThlMTJmYjMyMDZlMjkzMGE1OSIsImluc3RhbGxlZFNpemUiOjE1OTc0NCwibGljZW5zZSI6Ik1JVCIsIm1haW50YWluZXIiOiJOYXRhbmFlbCBDb3BhIDxuY29wYUBhbHBpbmVsaW51eC5vcmc+Iiwib3JpZ2luUGFja2FnZSI6ImFscGluZS1rZXlzIiwicGFja2FnZSI6ImFscGluZS1rZXlzIiwicHVsbENoZWNrc3VtIjoiUTFrREYyc3RLbzNlL1J1bWxBOFpyUmZDd2RTdjg9IiwicHVsbERlcGVuZGVuY2llcyI6IiIsInNpemUiOjEzMzYyLCJ1cmwiOiJodHRwczovL2FscGluZWxpbnV4Lm9yZyIsInZlcnNpb24iOiIyLjQtcjEifSwibWV0YWRhdGFUeXBlIjoiQXBrTWV0YWRhdGEiLCJuYW1lIjoiYWxwaW5lLWtleXMiLCJwdXJsIjoicGtnOmFscGluZS9hbHBpbmUta2V5c0AyLjQtcjE/YXJjaD14ODZfNjQmdXBzdHJlYW09YWxwaW5lLWtleXMmZGlzdHJvPWFscGluZS0zLjE1LjIiLCJ0eXBlIjoiYXBrIiwidmVyc2lvbiI6IjIuNC1yMSJ9LHsiY3BlcyI6WyJjcGU6Mi4zOmE6YXBrLXRvb2xzOmFway10b29sczoyLjEyLjctcjM6KjoqOio6KjoqOio6KiIsImNwZToyLjM6YTphcGstdG9vbHM6YXBrX3Rvb2xzOjIuMTIuNy1yMzoqOio6KjoqOio6KjoqIiwiY3BlOjIuMzphOmFwa190b29sczphcGstdG9vbHM6Mi4xMi43LXIzOio6KjoqOio6KjoqOioiLCJjcGU6Mi4zOmE6YXBrX3Rvb2xzOmFwa190b29sczoyLjEyLjctcjM6KjoqOio6KjoqOio6KiIsImNwZToyLjM6YTphcGs6YXBrLXRvb2xzOjIuMTIuNy1yMzoqOio6KjoqOio6KjoqIiwiY3BlOjIuMzphOmFwazphcGtfdG9vbHM6Mi4xMi43LXIzOio6KjoqOio6KjoqOioiXSwiZm91bmRCeSI6ImFwa2RiLWNhdGFsb2dlciIsImlkIjoiNWVmNjZhMzM1ZGRjMDNhNiIsImxhbmd1YWdlIjoiIiwibGljZW5zZXMiOlsiR1BMLTIuMC1vbmx5Il0sImxvY2F0aW9ucyI6W3sibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii9saWIvYXBrL2RiL2luc3RhbGxlZCJ9XSwibWV0YWRhdGEiOnsiYXJjaGl0ZWN0dXJlIjoieDg2XzY0IiwiZGVzY3JpcHRpb24iOiJBbHBpbmUgUGFja2FnZSBLZWVwZXIgLSBwYWNrYWdlIG1hbmFnZXIgZm9yIGFscGluZSIsImZpbGVzIjpbeyJwYXRoIjoiL2V0YyJ9LHsicGF0aCI6Ii9ldGMvYXBrIn0seyJwYXRoIjoiL2V0Yy9hcGsva2V5cyJ9LHsicGF0aCI6Ii9ldGMvYXBrL3Byb3RlY3RlZF9wYXRocy5kIn0seyJwYXRoIjoiL2xpYiJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExUzVETDZERk9tampOeEFHTnNzZmo0blVpOFhVPSJ9LCJvd25lckdpZCI6IjAiLCJvd25lclVpZCI6IjAiLCJwYXRoIjoiL2xpYi9saWJhcGsuc28uMy4xMi4wIiwicGVybWlzc2lvbnMiOiI3NTUifSx7InBhdGgiOiIvc2JpbiJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExTzFwQlVCMmtUTS9McUw4ZDBUOThDRWJaWXF3PSJ9LCJvd25lckdpZCI6IjAiLCJvd25lclVpZCI6IjAiLCJwYXRoIjoiL3NiaW4vYXBrIiwicGVybWlzc2lvbnMiOiI3NTUifSx7InBhdGgiOiIvdmFyIn0seyJwYXRoIjoiL3Zhci9jYWNoZSJ9LHsicGF0aCI6Ii92YXIvY2FjaGUvbWlzYyJ9LHsicGF0aCI6Ii92YXIvbGliIn0seyJwYXRoIjoiL3Zhci9saWIvYXBrIn1dLCJnaXRDb21taXRPZkFwa1BvcnQiOiIxYWMzYzFiYjI5ZWVmZjA4M2M2MjFjZjZiMjdhZDEyYWI5M2NiNzNhIiwiaW5zdGFsbGVkU2l6ZSI6MzExMjk2LCJsaWNlbnNlIjoiR1BMLTIuMC1vbmx5IiwibWFpbnRhaW5lciI6Ik5hdGFuYWVsIENvcGEgPG5jb3BhQGFscGluZWxpbnV4Lm9yZz4iLCJvcmlnaW5QYWNrYWdlIjoiYXBrLXRvb2xzIiwicGFja2FnZSI6ImFway10b29scyIsInB1bGxDaGVja3N1bSI6IlExM2ZQZCtGUlhhTHd5TmtsVm4rcXVGV0R5a25NPSIsInB1bGxEZXBlbmRlbmNpZXMiOiJtdXNsPj0xLjIgY2EtY2VydGlmaWNhdGVzLWJ1bmRsZSBzbzpsaWJjLm11c2wteDg2XzY0LnNvLjEgc286bGliY3J5cHRvLnNvLjEuMSBzbzpsaWJzc2wuc28uMS4xIHNvOmxpYnouc28uMSIsInNpemUiOjEyMDM3NywidXJsIjoiaHR0cHM6Ly9naXRsYWIuYWxwaW5lbGludXgub3JnL2FscGluZS9hcGstdG9vbHMiLCJ2ZXJzaW9uIjoiMi4xMi43LXIzIn0sIm1ldGFkYXRhVHlwZSI6IkFwa01ldGFkYXRhIiwibmFtZSI6ImFway10b29scyIsInB1cmwiOiJwa2c6YWxwaW5lL2Fway10b29sc0AyLjEyLjctcjM/YXJjaD14ODZfNjQmdXBzdHJlYW09YXBrLXRvb2xzJmRpc3Rybz1hbHBpbmUtMy4xNS4yIiwidHlwZSI6ImFwayIsInZlcnNpb24iOiIyLjEyLjctcjMifSx7ImNwZXMiOlsiY3BlOjIuMzphOmJ1c3lib3g6YnVzeWJveDoxLjM1LjA6KjoqOio6KjoqOio6KiJdLCJmb3VuZEJ5IjoiYXBrZGItY2F0YWxvZ2VyIiwiaWQiOiJmMjEzMmU4ZDZjZmUwMDZhIiwibGFuZ3VhZ2UiOiIiLCJsaWNlbnNlcyI6WyJHUEwtMi4wLW9ubHkiXSwibG9jYXRpb25zIjpbeyJsYXllcklEIjoic2hhMjU2OmZmNzY4YTE0MTNiYTEwOTMwOGMwZDg5N2ZmYTU1NWUwNTJhNDZkMmNmNDcxMTc4ZjAwMWI4MjViNGMyMWYzNTQiLCJwYXRoIjoiL2xpYi9hcGsvZGIvaW5zdGFsbGVkIn1dLCJtZXRhZGF0YSI6eyJhcmNoaXRlY3R1cmUiOiJ4ODZfNjQiLCJkZXNjcmlwdGlvbiI6IlNpemUgb3B0aW1pemVkIHRvb2xib3ggb2YgbWFueSBjb21tb24gVU5JWCB1dGlsaXRpZXMiLCJmaWxlcyI6W3sicGF0aCI6Ii9iaW4ifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMThHOVhlTkdBVUE0M3ZpVUtzbG1kaW4yekQyOD0ifSwib3duZXJHaWQiOiIwIiwib3duZXJVaWQiOiIwIiwicGF0aCI6Ii9iaW4vYnVzeWJveCIsInBlcm1pc3Npb25zIjoiNzU1In0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTFwY2ZUZkRORWJOS1FjMnMxdGlhN2RhMDVNOFE9In0sIm93bmVyR2lkIjoiMCIsIm93bmVyVWlkIjoiMCIsInBhdGgiOiIvYmluL3NoIiwicGVybWlzc2lvbnMiOiI3NzcifSx7InBhdGgiOiIvZXRjIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTFtQjk1SHEyTlVUWjU5OVJEaVNzajl3NUZyT1U9In0sInBhdGgiOiIvZXRjL3NlY3VyZXR0eSJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExRWdMRmpqNjdvdTNlTXFwNG0zcjJaam5RN1FVPSJ9LCJwYXRoIjoiL2V0Yy91ZGhjcGQuY29uZiJ9LHsicGF0aCI6Ii9ldGMvbG9ncm90YXRlLmQifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMVR5bHlDSU5WbW5TK0EvVGVhZDR2WmhFN0Jrcz0ifSwicGF0aCI6Ii9ldGMvbG9ncm90YXRlLmQvYWNwaWQifSx7InBhdGgiOiIvZXRjL25ldHdvcmsifSx7InBhdGgiOiIvZXRjL25ldHdvcmsvaWYtZG93bi5kIn0seyJwYXRoIjoiL2V0Yy9uZXR3b3JrL2lmLXBvc3QtZG93bi5kIn0seyJwYXRoIjoiL2V0Yy9uZXR3b3JrL2lmLXBvc3QtdXAuZCJ9LHsicGF0aCI6Ii9ldGMvbmV0d29yay9pZi1wcmUtZG93bi5kIn0seyJwYXRoIjoiL2V0Yy9uZXR3b3JrL2lmLXByZS11cC5kIn0seyJwYXRoIjoiL2V0Yy9uZXR3b3JrL2lmLXVwLmQifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMU9SZitsUFJLdVlnZGtCQmNLb2V2UjF0NjBRND0ifSwib3duZXJHaWQiOiIwIiwib3duZXJVaWQiOiIwIiwicGF0aCI6Ii9ldGMvbmV0d29yay9pZi11cC5kL2RhZCIsInBlcm1pc3Npb25zIjoiNzc1In0seyJwYXRoIjoiL3NiaW4ifSx7Im93bmVyR2lkIjoiMCIsIm93bmVyVWlkIjoiMCIsInBhdGgiOiIvdG1wIiwicGVybWlzc2lvbnMiOiIxNzc3In0seyJwYXRoIjoiL3VzciJ9LHsicGF0aCI6Ii91c3Ivc2JpbiJ9LHsicGF0aCI6Ii91c3Ivc2hhcmUifSx7InBhdGgiOiIvdXNyL3NoYXJlL3VkaGNwYyJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExdDl2aXIvWnJYM25iU0lZVDlCRExXWmVua1ZRPSJ9LCJvd25lckdpZCI6IjAiLCJvd25lclVpZCI6IjAiLCJwYXRoIjoiL3Vzci9zaGFyZS91ZGhjcGMvZGVmYXVsdC5zY3JpcHQiLCJwZXJtaXNzaW9ucyI6Ijc1NSJ9LHsicGF0aCI6Ii92YXIifSx7InBhdGgiOiIvdmFyL2NhY2hlIn0seyJwYXRoIjoiL3Zhci9jYWNoZS9taXNjIn0seyJwYXRoIjoiL3Zhci9saWIifSx7InBhdGgiOiIvdmFyL2xpYi91ZGhjcGQifV0sImdpdENvbW1pdE9mQXBrUG9ydCI6ImExNjA1OThkNjJhMGFjNTU4NTFhYWQxOTI1MWMwMWExYmI1ZmIyMmMiLCJpbnN0YWxsZWRTaXplIjo5NDYxNzYsImxpY2Vuc2UiOiJHUEwtMi4wLW9ubHkiLCJtYWludGFpbmVyIjoiTmF0YW5hZWwgQ29wYSA8bmNvcGFAYWxwaW5lbGludXgub3JnPiIsIm9yaWdpblBhY2thZ2UiOiJidXN5Ym94IiwicGFja2FnZSI6ImJ1c3lib3giLCJwdWxsQ2hlY2tzdW0iOiJRMUg2YXBoZGhZWjl1c1J2bVZqOVV0NVhRb2g5OD0iLCJwdWxsRGVwZW5kZW5jaWVzIjoic286bGliYy5tdXNsLXg4Nl82NC5zby4xIiwic2l6ZSI6NTAwNjA2LCJ1cmwiOiJodHRwczovL2J1c3lib3gubmV0LyIsInZlcnNpb24iOiIxLjM1LjAifSwibWV0YWRhdGFUeXBlIjoiQXBrTWV0YWRhdGEiLCJuYW1lIjoiYnVzeWJveCIsInB1cmwiOiJwa2c6YWxwaW5lL2J1c3lib3hAMS4zNS4wP2FyY2g9eDg2XzY0JnVwc3RyZWFtPWJ1c3lib3gmZGlzdHJvPWFscGluZS0zLjE1LjIiLCJ0eXBlIjoiYXBrIiwidmVyc2lvbiI6IjEuMzUuMCJ9LHsiY3BlcyI6WyJjcGU6Mi4zOmE6Y2EtY2VydGlmaWNhdGVzLWJ1bmRsZTpjYS1jZXJ0aWZpY2F0ZXMtYnVuZGxlOjIwMjExMjIwLXIwOio6KjoqOio6KjoqOioiLCJjcGU6Mi4zOmE6Y2EtY2VydGlmaWNhdGVzLWJ1bmRsZTpjYV9jZXJ0aWZpY2F0ZXNfYnVuZGxlOjIwMjExMjIwLXIwOio6KjoqOio6KjoqOioiLCJjcGU6Mi4zOmE6Y2FfY2VydGlmaWNhdGVzX2J1bmRsZTpjYS1jZXJ0aWZpY2F0ZXMtYnVuZGxlOjIwMjExMjIwLXIwOio6KjoqOio6KjoqOioiLCJjcGU6Mi4zOmE6Y2FfY2VydGlmaWNhdGVzX2J1bmRsZTpjYV9jZXJ0aWZpY2F0ZXNfYnVuZGxlOjIwMjExMjIwLXIwOio6KjoqOio6KjoqOioiLCJjcGU6Mi4zOmE6Y2EtY2VydGlmaWNhdGVzOmNhLWNlcnRpZmljYXRlcy1idW5kbGU6MjAyMTEyMjAtcjA6KjoqOio6KjoqOio6KiIsImNwZToyLjM6YTpjYS1jZXJ0aWZpY2F0ZXM6Y2FfY2VydGlmaWNhdGVzX2J1bmRsZToyMDIxMTIyMC1yMDoqOio6KjoqOio6KjoqIiwiY3BlOjIuMzphOmNhX2NlcnRpZmljYXRlczpjYS1jZXJ0aWZpY2F0ZXMtYnVuZGxlOjIwMjExMjIwLXIwOio6KjoqOio6KjoqOioiLCJjcGU6Mi4zOmE6Y2FfY2VydGlmaWNhdGVzOmNhX2NlcnRpZmljYXRlc19idW5kbGU6MjAyMTEyMjAtcjA6KjoqOio6KjoqOio6KiIsImNwZToyLjM6YTpjYTpjYS1jZXJ0aWZpY2F0ZXMtYnVuZGxlOjIwMjExMjIwLXIwOio6KjoqOio6KjoqOioiLCJjcGU6Mi4zOmE6Y2E6Y2FfY2VydGlmaWNhdGVzX2J1bmRsZToyMDIxMTIyMC1yMDoqOio6KjoqOio6KjoqIl0sImZvdW5kQnkiOiJhcGtkYi1jYXRhbG9nZXIiLCJpZCI6IjhjZWIyN2ExMmMwYmZlN2IiLCJsYW5ndWFnZSI6IiIsImxpY2Vuc2VzIjpbIk1QTC0yLjAiLCJBTkQiLCJNSVQiXSwibG9jYXRpb25zIjpbeyJsYXllcklEIjoic2hhMjU2OmZmNzY4YTE0MTNiYTEwOTMwOGMwZDg5N2ZmYTU1NWUwNTJhNDZkMmNmNDcxMTc4ZjAwMWI4MjViNGMyMWYzNTQiLCJwYXRoIjoiL2xpYi9hcGsvZGIvaW5zdGFsbGVkIn1dLCJtZXRhZGF0YSI6eyJhcmNoaXRlY3R1cmUiOiJ4ODZfNjQiLCJkZXNjcmlwdGlvbiI6IlByZSBnZW5lcmF0ZWQgYnVuZGxlIG9mIE1vemlsbGEgY2VydGlmaWNhdGVzIiwiZmlsZXMiOlt7InBhdGgiOiIvZXRjIn0seyJwYXRoIjoiL2V0Yy9zc2wifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMU5qNmdUQmRrWnBURlcvb2JKR2RwZnZLMFN0QT0ifSwib3duZXJHaWQiOiIwIiwib3duZXJVaWQiOiIwIiwicGF0aCI6Ii9ldGMvc3NsL2NlcnQucGVtIiwicGVybWlzc2lvbnMiOiI3NzcifSx7InBhdGgiOiIvZXRjL3NzbC9jZXJ0cyJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExbTJjSm9mb05adENDSzQxeXZqVGdYNEs3ZHZzPSJ9LCJwYXRoIjoiL2V0Yy9zc2wvY2VydHMvY2EtY2VydGlmaWNhdGVzLmNydCJ9XSwiZ2l0Q29tbWl0T2ZBcGtQb3J0IjoiNzA5YjcwYmNiNzI3MzhjZmVkYzUxMGJiYTA4MTQxYjAxMjAzODE2NyIsImluc3RhbGxlZFNpemUiOjIyMTE4NCwibGljZW5zZSI6Ik1QTC0yLjAgQU5EIE1JVCIsIm1haW50YWluZXIiOiJOYXRhbmFlbCBDb3BhIDxuY29wYUBhbHBpbmVsaW51eC5vcmc+Iiwib3JpZ2luUGFja2FnZSI6ImNhLWNlcnRpZmljYXRlcyIsInBhY2thZ2UiOiJjYS1jZXJ0aWZpY2F0ZXMtYnVuZGxlIiwicHVsbENoZWNrc3VtIjoiUTFTVkFXdVdIZFBIdmJCaExUa0FaNjAvMVdzbUk9IiwicHVsbERlcGVuZGVuY2llcyI6IiIsInNpemUiOjExOTc0OCwidXJsIjoiaHR0cHM6Ly93d3cubW96aWxsYS5vcmcvZW4tVVMvYWJvdXQvZ292ZXJuYW5jZS9wb2xpY2llcy9zZWN1cml0eS1ncm91cC9jZXJ0cy8iLCJ2ZXJzaW9uIjoiMjAyMTEyMjAtcjAifSwibWV0YWRhdGFUeXBlIjoiQXBrTWV0YWRhdGEiLCJuYW1lIjoiY2EtY2VydGlmaWNhdGVzLWJ1bmRsZSIsInB1cmwiOiJwa2c6YWxwaW5lL2NhLWNlcnRpZmljYXRlcy1idW5kbGVAMjAyMTEyMjAtcjA/YXJjaD14ODZfNjQmdXBzdHJlYW09Y2EtY2VydGlmaWNhdGVzJmRpc3Rybz1hbHBpbmUtMy4xNS4yIiwidHlwZSI6ImFwayIsInZlcnNpb24iOiIyMDIxMTIyMC1yMCJ9LHsiY3BlcyI6WyJjcGU6Mi4zOmE6bGliYy11dGlsczpsaWJjLXV0aWxzOjAuNy4yLXIzOio6KjoqOio6KjoqOioiLCJjcGU6Mi4zOmE6bGliYy11dGlsczpsaWJjX3V0aWxzOjAuNy4yLXIzOio6KjoqOio6KjoqOioiLCJjcGU6Mi4zOmE6bGliY191dGlsczpsaWJjLXV0aWxzOjAuNy4yLXIzOio6KjoqOio6KjoqOioiLCJjcGU6Mi4zOmE6bGliY191dGlsczpsaWJjX3V0aWxzOjAuNy4yLXIzOio6KjoqOio6KjoqOioiLCJjcGU6Mi4zOmE6bGliYzpsaWJjLXV0aWxzOjAuNy4yLXIzOio6KjoqOio6KjoqOioiLCJjcGU6Mi4zOmE6bGliYzpsaWJjX3V0aWxzOjAuNy4yLXIzOio6KjoqOio6KjoqOioiXSwiZm91bmRCeSI6ImFwa2RiLWNhdGFsb2dlciIsImlkIjoiMTIzN2UwYzMxNWYyNjkwMiIsImxhbmd1YWdlIjoiIiwibGljZW5zZXMiOlsiQlNELTItQ2xhdXNlIiwiQU5EIiwiQlNELTMtQ2xhdXNlIl0sImxvY2F0aW9ucyI6W3sibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii9saWIvYXBrL2RiL2luc3RhbGxlZCJ9XSwibWV0YWRhdGEiOnsiYXJjaGl0ZWN0dXJlIjoieDg2XzY0IiwiZGVzY3JpcHRpb24iOiJNZXRhIHBhY2thZ2UgdG8gcHVsbCBpbiBjb3JyZWN0IGxpYmMiLCJmaWxlcyI6W10sImdpdENvbW1pdE9mQXBrUG9ydCI6IjYwNDI0MTMzYmUyZTc5YmJmZWZmM2Q1ODE0N2EyMjg4NmY4MTdjZTIiLCJpbnN0YWxsZWRTaXplIjo0MDk2LCJsaWNlbnNlIjoiQlNELTItQ2xhdXNlIEFORCBCU0QtMy1DbGF1c2UiLCJtYWludGFpbmVyIjoiTmF0YW5hZWwgQ29wYSA8bmNvcGFAYWxwaW5lbGludXgub3JnPiIsIm9yaWdpblBhY2thZ2UiOiJsaWJjLWRldiIsInBhY2thZ2UiOiJsaWJjLXV0aWxzIiwicHVsbENoZWNrc3VtIjoiUTFlWTNqNjdWL1BpajBDQWdIUnBOZklUb0pseUk9IiwicHVsbERlcGVuZGVuY2llcyI6Im11c2wtdXRpbHMiLCJzaXplIjoxNDg1LCJ1cmwiOiJodHRwczovL2FscGluZWxpbnV4Lm9yZyIsInZlcnNpb24iOiIwLjcuMi1yMyJ9LCJtZXRhZGF0YVR5cGUiOiJBcGtNZXRhZGF0YSIsIm5hbWUiOiJsaWJjLXV0aWxzIiwicHVybCI6InBrZzphbHBpbmUvbGliYy11dGlsc0AwLjcuMi1yMz9hcmNoPXg4Nl82NCZ1cHN0cmVhbT1saWJjLWRldiZkaXN0cm89YWxwaW5lLTMuMTUuMiIsInR5cGUiOiJhcGsiLCJ2ZXJzaW9uIjoiMC43LjItcjMifSx7ImNwZXMiOlsiY3BlOjIuMzphOmxpYmNyeXB0bzEuMTpsaWJjcnlwdG8xLjE6MS4xLjFuLXIwOio6KjoqOio6KjoqOioiXSwiZm91bmRCeSI6ImFwa2RiLWNhdGFsb2dlciIsImlkIjoiN2EyY2Y3MjdjYmFiODA3NCIsImxhbmd1YWdlIjoiIiwibGljZW5zZXMiOlsiT3BlblNTTCJdLCJsb2NhdGlvbnMiOlt7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvbGliL2Fway9kYi9pbnN0YWxsZWQifV0sIm1ldGFkYXRhIjp7ImFyY2hpdGVjdHVyZSI6Ing4Nl82NCIsImRlc2NyaXB0aW9uIjoiQ3J5cHRvIGxpYnJhcnkgZnJvbSBvcGVuc3NsIiwiZmlsZXMiOlt7InBhdGgiOiIvZXRjIn0seyJwYXRoIjoiL2V0Yy9zc2wxLjEifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMU9lVXlPRFlXZTJoQndCbTBxd3Myb0RXL1dRYz0ifSwib3duZXJHaWQiOiIwIiwib3duZXJVaWQiOiIwIiwicGF0aCI6Ii9ldGMvc3NsMS4xL2NlcnQucGVtIiwicGVybWlzc2lvbnMiOiI3NzcifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMU5vQkY3Uk1JaVQ5ZkNYTGovbWJEaCtwbkw5bz0ifSwib3duZXJHaWQiOiIwIiwib3duZXJVaWQiOiIwIiwicGF0aCI6Ii9ldGMvc3NsMS4xL2NlcnRzIiwicGVybWlzc2lvbnMiOiI3NzcifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMWE3VlBSODV3cnVYRm1ORlpFL0RCYTBQeXpxMD0ifSwib3duZXJHaWQiOiIwIiwib3duZXJVaWQiOiIwIiwicGF0aCI6Ii9ldGMvc3NsMS4xL2N0X2xvZ19saXN0LmNuZiIsInBlcm1pc3Npb25zIjoiNzc3In0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTFvbGg4VHBkQWkyUW5UbDRGSzNUamRVaVN3VG89In0sInBhdGgiOiIvZXRjL3NzbDEuMS9jdF9sb2dfbGlzdC5jbmYuZGlzdCJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExd0d1eFZFT0s5aUdMajFpOEQzQlNCblQ3TUpBPSJ9LCJwYXRoIjoiL2V0Yy9zc2wxLjEvb3BlbnNzbC5jbmYifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMXdHdXhWRU9LOWlHTGoxaThEM0JTQm5UN01KQT0ifSwicGF0aCI6Ii9ldGMvc3NsMS4xL29wZW5zc2wuY25mLmRpc3QifSx7InBhdGgiOiIvZXRjL3NzbDEuMS9wcml2YXRlIn0seyJwYXRoIjoiL2xpYiJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExMDBock9ZcmNXekN2MEtHOEhuUndwSE9URnNFPSJ9LCJvd25lckdpZCI6IjAiLCJvd25lclVpZCI6IjAiLCJwYXRoIjoiL2xpYi9saWJjcnlwdG8uc28uMS4xIiwicGVybWlzc2lvbnMiOiI3NTUifSx7InBhdGgiOiIvdXNyIn0seyJwYXRoIjoiL3Vzci9saWIifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMVQyc2krYzd0czdzZ0R4UVl2ZTRCM2kxRGdvMD0ifSwib3duZXJHaWQiOiIwIiwib3duZXJVaWQiOiIwIiwicGF0aCI6Ii91c3IvbGliL2xpYmNyeXB0by5zby4xLjEiLCJwZXJtaXNzaW9ucyI6Ijc3NyJ9LHsicGF0aCI6Ii91c3IvbGliL2VuZ2luZXMtMS4xIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTFMcG8yL1JMVGY1VmxIbVVRWHl2NWMxUE9wR1k9In0sIm93bmVyR2lkIjoiMCIsIm93bmVyVWlkIjoiMCIsInBhdGgiOiIvdXNyL2xpYi9lbmdpbmVzLTEuMS9hZmFsZy5zbyIsInBlcm1pc3Npb25zIjoiNzU1In0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTFsbFBxSE91M3llQVRKaFZtYXgzYUpWOXc0OHc9In0sIm93bmVyR2lkIjoiMCIsIm93bmVyVWlkIjoiMCIsInBhdGgiOiIvdXNyL2xpYi9lbmdpbmVzLTEuMS9jYXBpLnNvIiwicGVybWlzc2lvbnMiOiI3NTUifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMS9HaGpFUTE3S1NWYmd5cEQwNlU1VTJzd2tzWT0ifSwib3duZXJHaWQiOiIwIiwib3duZXJVaWQiOiIwIiwicGF0aCI6Ii91c3IvbGliL2VuZ2luZXMtMS4xL3BhZGxvY2suc28iLCJwZXJtaXNzaW9ucyI6Ijc1NSJ9XSwiZ2l0Q29tbWl0T2ZBcGtQb3J0IjoiNDU1ZTk2Njg5OWE5MzU4ZmM5NGY1YmNlNjMzYWZlOGExOTQyMDk1YyIsImluc3RhbGxlZFNpemUiOjI3NDAyMjQsImxpY2Vuc2UiOiJPcGVuU1NMIiwibWFpbnRhaW5lciI6IlRpbW8gVGVyYXMgPHRpbW8udGVyYXNAaWtpLmZpPiIsIm9yaWdpblBhY2thZ2UiOiJvcGVuc3NsIiwicGFja2FnZSI6ImxpYmNyeXB0bzEuMSIsInB1bGxDaGVja3N1bSI6IlExckFzTGNiWTk2VCtUcW91ME1IMHlQUTExaEdRPSIsInB1bGxEZXBlbmRlbmNpZXMiOiJzbzpsaWJjLm11c2wteDg2XzY0LnNvLjEiLCJzaXplIjoxMjA4MjI4LCJ1cmwiOiJodHRwczovL3d3dy5vcGVuc3NsLm9yZy8iLCJ2ZXJzaW9uIjoiMS4xLjFuLXIwIn0sIm1ldGFkYXRhVHlwZSI6IkFwa01ldGFkYXRhIiwibmFtZSI6ImxpYmNyeXB0bzEuMSIsInB1cmwiOiJwa2c6YWxwaW5lL2xpYmNyeXB0bzEuMUAxLjEuMW4tcjA/YXJjaD14ODZfNjQmdXBzdHJlYW09b3BlbnNzbCZkaXN0cm89YWxwaW5lLTMuMTUuMiIsInR5cGUiOiJhcGsiLCJ2ZXJzaW9uIjoiMS4xLjFuLXIwIn0seyJjcGVzIjpbImNwZToyLjM6YTpsaWJyZXRsczpsaWJyZXRsczozLjMuNC1yMzoqOio6KjoqOio6KjoqIl0sImZvdW5kQnkiOiJhcGtkYi1jYXRhbG9nZXIiLCJpZCI6IjkxZGIxNWQ4MDRmZWRlNTkiLCJsYW5ndWFnZSI6IiIsImxpY2Vuc2VzIjpbIklTQyIsIkFORCIsIihCU0QtMy1DbGF1c2UiLCJPUiIsIk1JVCkiXSwibG9jYXRpb25zIjpbeyJsYXllcklEIjoic2hhMjU2OmZmNzY4YTE0MTNiYTEwOTMwOGMwZDg5N2ZmYTU1NWUwNTJhNDZkMmNmNDcxMTc4ZjAwMWI4MjViNGMyMWYzNTQiLCJwYXRoIjoiL2xpYi9hcGsvZGIvaW5zdGFsbGVkIn1dLCJtZXRhZGF0YSI6eyJhcmNoaXRlY3R1cmUiOiJ4ODZfNjQiLCJkZXNjcmlwdGlvbiI6InBvcnQgb2YgbGlidGxzIGZyb20gbGlicmVzc2wgdG8gb3BlbnNzbCIsImZpbGVzIjpbeyJwYXRoIjoiL3VzciJ9LHsicGF0aCI6Ii91c3IvbGliIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTFuTkVDOVQvdDZXK0VjbTBEeHFNVW5SdmNUNms9In0sIm93bmVyR2lkIjoiMCIsIm93bmVyVWlkIjoiMCIsInBhdGgiOiIvdXNyL2xpYi9saWJ0bHMuc28uMiIsInBlcm1pc3Npb25zIjoiNzc3In0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTFlU1dubXVzU2NsNndHY2t0a3cyLzhjcjl6RkU9In0sIm93bmVyR2lkIjoiMCIsIm93bmVyVWlkIjoiMCIsInBhdGgiOiIvdXNyL2xpYi9saWJ0bHMuc28uMi4wLjMiLCJwZXJtaXNzaW9ucyI6Ijc1NSJ9XSwiZ2l0Q29tbWl0T2ZBcGtQb3J0IjoiOTFjN2E5ZjNhYTI5NmI2ZDQ2MmM1NjM0ZTc2NThlYmRiZmY2NWJiOSIsImluc3RhbGxlZFNpemUiOjg2MDE2LCJsaWNlbnNlIjoiSVNDIEFORCAoQlNELTMtQ2xhdXNlIE9SIE1JVCkiLCJtYWludGFpbmVyIjoiQXJpYWRuZSBDb25pbGwgPGFyaWFkbmVAZGVyZWZlcmVuY2VkLm9yZz4iLCJvcmlnaW5QYWNrYWdlIjoibGlicmV0bHMiLCJwYWNrYWdlIjoibGlicmV0bHMiLCJwdWxsQ2hlY2tzdW0iOiJRMVo5L3Y1VVZzUlJrcllOZHEzcGpGQWJDdWdVOD0iLCJwdWxsRGVwZW5kZW5jaWVzIjoiY2EtY2VydGlmaWNhdGVzLWJ1bmRsZSBzbzpsaWJjLm11c2wteDg2XzY0LnNvLjEgc286bGliY3J5cHRvLnNvLjEuMSBzbzpsaWJzc2wuc28uMS4xIiwic2l6ZSI6MjkxODUsInVybCI6Imh0dHBzOi8vZ2l0LmNhdXNhbC5hZ2VuY3kvbGlicmV0bHMvIiwidmVyc2lvbiI6IjMuMy40LXIzIn0sIm1ldGFkYXRhVHlwZSI6IkFwa01ldGFkYXRhIiwibmFtZSI6ImxpYnJldGxzIiwicHVybCI6InBrZzphbHBpbmUvbGlicmV0bHNAMy4zLjQtcjM/YXJjaD14ODZfNjQmdXBzdHJlYW09bGlicmV0bHMmZGlzdHJvPWFscGluZS0zLjE1LjIiLCJ0eXBlIjoiYXBrIiwidmVyc2lvbiI6IjMuMy40LXIzIn0seyJjcGVzIjpbImNwZToyLjM6YTpsaWJzc2wxLjE6bGlic3NsMS4xOjEuMS4xbi1yMDoqOio6KjoqOio6KjoqIl0sImZvdW5kQnkiOiJhcGtkYi1jYXRhbG9nZXIiLCJpZCI6IjMwOTRjNGE2MTBiMGIwMGQiLCJsYW5ndWFnZSI6IiIsImxpY2Vuc2VzIjpbIk9wZW5TU0wiXSwibG9jYXRpb25zIjpbeyJsYXllcklEIjoic2hhMjU2OmZmNzY4YTE0MTNiYTEwOTMwOGMwZDg5N2ZmYTU1NWUwNTJhNDZkMmNmNDcxMTc4ZjAwMWI4MjViNGMyMWYzNTQiLCJwYXRoIjoiL2xpYi9hcGsvZGIvaW5zdGFsbGVkIn1dLCJtZXRhZGF0YSI6eyJhcmNoaXRlY3R1cmUiOiJ4ODZfNjQiLCJkZXNjcmlwdGlvbiI6IlNTTCBzaGFyZWQgbGlicmFyaWVzIiwiZmlsZXMiOlt7InBhdGgiOiIvbGliIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTF4TmpqN2p4dk9qM2xEUmQzc1JYekhvd1RVc1E9In0sIm93bmVyR2lkIjoiMCIsIm93bmVyVWlkIjoiMCIsInBhdGgiOiIvbGliL2xpYnNzbC5zby4xLjEiLCJwZXJtaXNzaW9ucyI6Ijc1NSJ9LHsicGF0aCI6Ii91c3IifSx7InBhdGgiOiIvdXNyL2xpYiJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExOGozNXBlM3lwNkhPZ01paDF3bEdQMS9tbTJjPSJ9LCJvd25lckdpZCI6IjAiLCJvd25lclVpZCI6IjAiLCJwYXRoIjoiL3Vzci9saWIvbGlic3NsLnNvLjEuMSIsInBlcm1pc3Npb25zIjoiNzc3In1dLCJnaXRDb21taXRPZkFwa1BvcnQiOiI0NTVlOTY2ODk5YTkzNThmYzk0ZjViY2U2MzNhZmU4YTE5NDIwOTVjIiwiaW5zdGFsbGVkU2l6ZSI6NTQwNjcyLCJsaWNlbnNlIjoiT3BlblNTTCIsIm1haW50YWluZXIiOiJUaW1vIFRlcmFzIDx0aW1vLnRlcmFzQGlraS5maT4iLCJvcmlnaW5QYWNrYWdlIjoib3BlbnNzbCIsInBhY2thZ2UiOiJsaWJzc2wxLjEiLCJwdWxsQ2hlY2tzdW0iOiJRMS9LWjAwcURIV1o1Y2ozQVdHL0RQZEFDUk5ZST0iLCJwdWxsRGVwZW5kZW5jaWVzIjoic286bGliYy5tdXNsLXg4Nl82NC5zby4xIHNvOmxpYmNyeXB0by5zby4xLjEiLCJzaXplIjoyMTMyMDksInVybCI6Imh0dHBzOi8vd3d3Lm9wZW5zc2wub3JnLyIsInZlcnNpb24iOiIxLjEuMW4tcjAifSwibWV0YWRhdGFUeXBlIjoiQXBrTWV0YWRhdGEiLCJuYW1lIjoibGlic3NsMS4xIiwicHVybCI6InBrZzphbHBpbmUvbGlic3NsMS4xQDEuMS4xbi1yMD9hcmNoPXg4Nl82NCZ1cHN0cmVhbT1vcGVuc3NsJmRpc3Rybz1hbHBpbmUtMy4xNS4yIiwidHlwZSI6ImFwayIsInZlcnNpb24iOiIxLjEuMW4tcjAifSx7ImNwZXMiOlsiY3BlOjIuMzphOm11c2w6bXVzbDoxLjIuMi1yNzoqOio6KjoqOio6KjoqIl0sImZvdW5kQnkiOiJhcGtkYi1jYXRhbG9nZXIiLCJpZCI6IjRhYzcxMzZiODUzNmNkZWEiLCJsYW5ndWFnZSI6IiIsImxpY2Vuc2VzIjpbIk1JVCJdLCJsb2NhdGlvbnMiOlt7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvbGliL2Fway9kYi9pbnN0YWxsZWQifV0sIm1ldGFkYXRhIjp7ImFyY2hpdGVjdHVyZSI6Ing4Nl82NCIsImRlc2NyaXB0aW9uIjoidGhlIG11c2wgYyBsaWJyYXJ5IChsaWJjKSBpbXBsZW1lbnRhdGlvbiIsImZpbGVzIjpbeyJwYXRoIjoiL2xpYiJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExMmFkd3FRT2pvOWRGbCtWSkQyRWNkOTAxdmhFPSJ9LCJvd25lckdpZCI6IjAiLCJvd25lclVpZCI6IjAiLCJwYXRoIjoiL2xpYi9sZC1tdXNsLXg4Nl82NC5zby4xIiwicGVybWlzc2lvbnMiOiI3NTUifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMTd5SjNKRk55cEE0bXhoSkpyMG91NkN6c0pWST0ifSwib3duZXJHaWQiOiIwIiwib3duZXJVaWQiOiIwIiwicGF0aCI6Ii9saWIvbGliYy5tdXNsLXg4Nl82NC5zby4xIiwicGVybWlzc2lvbnMiOiI3NzcifV0sImdpdENvbW1pdE9mQXBrUG9ydCI6ImJmNWJiZmRiZjc4MDA5MmYzODdiN2FiZTQwMWZiZmNlZGE5MGM4NGQiLCJpbnN0YWxsZWRTaXplIjo2MjI1OTIsImxpY2Vuc2UiOiJNSVQiLCJtYWludGFpbmVyIjoiVGltbyBUZXLDpHMgPHRpbW8udGVyYXNAaWtpLmZpPiIsIm9yaWdpblBhY2thZ2UiOiJtdXNsIiwicGFja2FnZSI6Im11c2wiLCJwdWxsQ2hlY2tzdW0iOiJRMURlYjBqTnl0a3JqUFc0Ti9lS0xaNDNCd09sdz0iLCJwdWxsRGVwZW5kZW5jaWVzIjoiIiwic2l6ZSI6MzgzMTUyLCJ1cmwiOiJodHRwczovL211c2wubGliYy5vcmcvIiwidmVyc2lvbiI6IjEuMi4yLXI3In0sIm1ldGFkYXRhVHlwZSI6IkFwa01ldGFkYXRhIiwibmFtZSI6Im11c2wiLCJwdXJsIjoicGtnOmFscGluZS9tdXNsQDEuMi4yLXI3P2FyY2g9eDg2XzY0JnVwc3RyZWFtPW11c2wmZGlzdHJvPWFscGluZS0zLjE1LjIiLCJ0eXBlIjoiYXBrIiwidmVyc2lvbiI6IjEuMi4yLXI3In0seyJjcGVzIjpbImNwZToyLjM6YTptdXNsLXV0aWxzOm11c2wtdXRpbHM6MS4yLjItcjc6KjoqOio6KjoqOio6KiIsImNwZToyLjM6YTptdXNsLXV0aWxzOm11c2xfdXRpbHM6MS4yLjItcjc6KjoqOio6KjoqOio6KiIsImNwZToyLjM6YTptdXNsX3V0aWxzOm11c2wtdXRpbHM6MS4yLjItcjc6KjoqOio6KjoqOio6KiIsImNwZToyLjM6YTptdXNsX3V0aWxzOm11c2xfdXRpbHM6MS4yLjItcjc6KjoqOio6KjoqOio6KiIsImNwZToyLjM6YTptdXNsOm11c2wtdXRpbHM6MS4yLjItcjc6KjoqOio6KjoqOio6KiIsImNwZToyLjM6YTptdXNsOm11c2xfdXRpbHM6MS4yLjItcjc6KjoqOio6KjoqOio6KiJdLCJmb3VuZEJ5IjoiYXBrZGItY2F0YWxvZ2VyIiwiaWQiOiI1M2E5MDlmNGQ4NzJiOTAiLCJsYW5ndWFnZSI6IiIsImxpY2Vuc2VzIjpbIk1JVCIsIkJTRCIsIkdQTDIrIl0sImxvY2F0aW9ucyI6W3sibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii9saWIvYXBrL2RiL2luc3RhbGxlZCJ9XSwibWV0YWRhdGEiOnsiYXJjaGl0ZWN0dXJlIjoieDg2XzY0IiwiZGVzY3JpcHRpb24iOiJ0aGUgbXVzbCBjIGxpYnJhcnkgKGxpYmMpIGltcGxlbWVudGF0aW9uIiwiZmlsZXMiOlt7InBhdGgiOiIvc2JpbiJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExS2phMitQT1pLeEVrVU9acXdTakM2a21hRUQ0PSJ9LCJvd25lckdpZCI6IjAiLCJvd25lclVpZCI6IjAiLCJwYXRoIjoiL3NiaW4vbGRjb25maWciLCJwZXJtaXNzaW9ucyI6Ijc1NSJ9LHsicGF0aCI6Ii91c3IifSx7InBhdGgiOiIvdXNyL2JpbiJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExTWkyMUJUY0x0TjljWVBWMDdQMGF3SHlUNlhVPSJ9LCJvd25lckdpZCI6IjAiLCJvd25lclVpZCI6IjAiLCJwYXRoIjoiL3Vzci9iaW4vZ2V0Y29uZiIsInBlcm1pc3Npb25zIjoiNzU1In0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTFuWm1ES0tGUTJ2b29JdE5ETEJsZVQ4eDdPTUE9In0sIm93bmVyR2lkIjoiMCIsIm93bmVyVWlkIjoiMCIsInBhdGgiOiIvdXNyL2Jpbi9nZXRlbnQiLCJwZXJtaXNzaW9ucyI6Ijc1NSJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExUThUT09kNVRtMlB0a081RW9vd3ZodkdDSUo0PSJ9LCJvd25lckdpZCI6IjAiLCJvd25lclVpZCI6IjAiLCJwYXRoIjoiL3Vzci9iaW4vaWNvbnYiLCJwZXJtaXNzaW9ucyI6Ijc1NSJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExeUZBaEdnZ21MN0VSZ2JJQTdLUXh5VHpmM2tzPSJ9LCJvd25lckdpZCI6IjAiLCJvd25lclVpZCI6IjAiLCJwYXRoIjoiL3Vzci9iaW4vbGRkIiwicGVybWlzc2lvbnMiOiI3NTUifV0sImdpdENvbW1pdE9mQXBrUG9ydCI6ImJmNWJiZmRiZjc4MDA5MmYzODdiN2FiZTQwMWZiZmNlZGE5MGM4NGQiLCJpbnN0YWxsZWRTaXplIjoxNDMzNjAsImxpY2Vuc2UiOiJNSVQgQlNEIEdQTDIrIiwibWFpbnRhaW5lciI6IlRpbW8gVGVyw6RzIDx0aW1vLnRlcmFzQGlraS5maT4iLCJvcmlnaW5QYWNrYWdlIjoibXVzbCIsInBhY2thZ2UiOiJtdXNsLXV0aWxzIiwicHVsbENoZWNrc3VtIjoiUTFQNTBjZkppU3NIb3FzWVJUeU9FT2xKaUxuM289IiwicHVsbERlcGVuZGVuY2llcyI6InNjYW5lbGYgc286bGliYy5tdXNsLXg4Nl82NC5zby4xIiwic2l6ZSI6MzY3MjMsInVybCI6Imh0dHBzOi8vbXVzbC5saWJjLm9yZy8iLCJ2ZXJzaW9uIjoiMS4yLjItcjcifSwibWV0YWRhdGFUeXBlIjoiQXBrTWV0YWRhdGEiLCJuYW1lIjoibXVzbC11dGlscyIsInB1cmwiOiJwa2c6YWxwaW5lL211c2wtdXRpbHNAMS4yLjItcjc/YXJjaD14ODZfNjQmdXBzdHJlYW09bXVzbCZkaXN0cm89YWxwaW5lLTMuMTUuMiIsInR5cGUiOiJhcGsiLCJ2ZXJzaW9uIjoiMS4yLjItcjcifSx7ImNwZXMiOlsiY3BlOjIuMzphOnNjYW5lbGY6c2NhbmVsZjoxLjMuMy1yMDoqOio6KjoqOio6KjoqIl0sImZvdW5kQnkiOiJhcGtkYi1jYXRhbG9nZXIiLCJpZCI6IjFmMjhkZTEyMDA3M2Q3OGEiLCJsYW5ndWFnZSI6IiIsImxpY2Vuc2VzIjpbIkdQTC0yLjAtb25seSJdLCJsb2NhdGlvbnMiOlt7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvbGliL2Fway9kYi9pbnN0YWxsZWQifV0sIm1ldGFkYXRhIjp7ImFyY2hpdGVjdHVyZSI6Ing4Nl82NCIsImRlc2NyaXB0aW9uIjoiU2NhbiBFTEYgYmluYXJpZXMgZm9yIHN0dWZmIiwiZmlsZXMiOlt7InBhdGgiOiIvdXNyIn0seyJwYXRoIjoiL3Vzci9iaW4ifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMXNGZTU0UmJkZlQ0Q05pbVltNDFEMUR2K05zZz0ifSwib3duZXJHaWQiOiIwIiwib3duZXJVaWQiOiIwIiwicGF0aCI6Ii91c3IvYmluL3NjYW5lbGYiLCJwZXJtaXNzaW9ucyI6Ijc1NSJ9XSwiZ2l0Q29tbWl0T2ZBcGtQb3J0IjoiODZiM2Q0ZmJiMGE3NjBmZWJmMzQ3NmY5YTU4YWJmOGQwZjcyOGQ1YyIsImluc3RhbGxlZFNpemUiOjk0MjA4LCJsaWNlbnNlIjoiR1BMLTIuMC1vbmx5IiwibWFpbnRhaW5lciI6Ik5hdGFuYWVsIENvcGEgPG5jb3BhQGFscGluZWxpbnV4Lm9yZz4iLCJvcmlnaW5QYWNrYWdlIjoicGF4LXV0aWxzIiwicGFja2FnZSI6InNjYW5lbGYiLCJwdWxsQ2hlY2tzdW0iOiJRMTEvZFpEa1VJY0tUM2xuSENOcHN4dGJzSE5Kbz0iLCJwdWxsRGVwZW5kZW5jaWVzIjoic286bGliYy5tdXNsLXg4Nl82NC5zby4xIiwic2l6ZSI6MzY4MzAsInVybCI6Imh0dHBzOi8vd2lraS5nZW50b28ub3JnL3dpa2kvSGFyZGVuZWQvUGFYX1V0aWxpdGllcyIsInZlcnNpb24iOiIxLjMuMy1yMCJ9LCJtZXRhZGF0YVR5cGUiOiJBcGtNZXRhZGF0YSIsIm5hbWUiOiJzY2FuZWxmIiwicHVybCI6InBrZzphbHBpbmUvc2NhbmVsZkAxLjMuMy1yMD9hcmNoPXg4Nl82NCZ1cHN0cmVhbT1wYXgtdXRpbHMmZGlzdHJvPWFscGluZS0zLjE1LjIiLCJ0eXBlIjoiYXBrIiwidmVyc2lvbiI6IjEuMy4zLXIwIn0seyJjcGVzIjpbImNwZToyLjM6YTpzc2wtY2xpZW50OnNzbC1jbGllbnQ6MS4zNS4wOio6KjoqOio6KjoqOioiLCJjcGU6Mi4zOmE6c3NsLWNsaWVudDpzc2xfY2xpZW50OjEuMzUuMDoqOio6KjoqOio6KjoqIiwiY3BlOjIuMzphOnNzbF9jbGllbnQ6c3NsLWNsaWVudDoxLjM1LjA6KjoqOio6KjoqOio6KiIsImNwZToyLjM6YTpzc2xfY2xpZW50OnNzbF9jbGllbnQ6MS4zNS4wOio6KjoqOio6KjoqOioiLCJjcGU6Mi4zOmE6c3NsOnNzbC1jbGllbnQ6MS4zNS4wOio6KjoqOio6KjoqOioiLCJjcGU6Mi4zOmE6c3NsOnNzbF9jbGllbnQ6MS4zNS4wOio6KjoqOio6KjoqOioiXSwiZm91bmRCeSI6ImFwa2RiLWNhdGFsb2dlciIsImlkIjoiYzJlM2NlN2I5ZTcyZDBhZSIsImxhbmd1YWdlIjoiIiwibGljZW5zZXMiOlsiR1BMLTIuMC1vbmx5Il0sImxvY2F0aW9ucyI6W3sibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii9saWIvYXBrL2RiL2luc3RhbGxlZCJ9XSwibWV0YWRhdGEiOnsiYXJjaGl0ZWN0dXJlIjoieDg2XzY0IiwiZGVzY3JpcHRpb24iOiJFWHRlcm5hbCBzc2xfY2xpZW50IGZvciBidXN5Ym94IHdnZXQiLCJmaWxlcyI6W3sicGF0aCI6Ii91c3IifSx7InBhdGgiOiIvdXNyL2JpbiJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExNXVlRi9QL0VtQVZWbVVKMVBYQkFrcENBWDdjPSJ9LCJvd25lckdpZCI6IjAiLCJvd25lclVpZCI6IjAiLCJwYXRoIjoiL3Vzci9iaW4vc3NsX2NsaWVudCIsInBlcm1pc3Npb25zIjoiNzU1In1dLCJnaXRDb21taXRPZkFwa1BvcnQiOiJhMTYwNTk4ZDYyYTBhYzU1ODUxYWFkMTkyNTFjMDFhMWJiNWZiMjJjIiwiaW5zdGFsbGVkU2l6ZSI6Mjg2NzIsImxpY2Vuc2UiOiJHUEwtMi4wLW9ubHkiLCJtYWludGFpbmVyIjoiTmF0YW5hZWwgQ29wYSA8bmNvcGFAYWxwaW5lbGludXgub3JnPiIsIm9yaWdpblBhY2thZ2UiOiJidXN5Ym94IiwicGFja2FnZSI6InNzbF9jbGllbnQiLCJwdWxsQ2hlY2tzdW0iOiJRMTNHdzVIRWVMWmorUWhqN0hSbXd6aTBVT0Fndz0iLCJwdWxsRGVwZW5kZW5jaWVzIjoic286bGliYy5tdXNsLXg4Nl82NC5zby4xIHNvOmxpYnRscy5zby4yIiwic2l6ZSI6NDcwOSwidXJsIjoiaHR0cHM6Ly9idXN5Ym94Lm5ldC8iLCJ2ZXJzaW9uIjoiMS4zNS4wIn0sIm1ldGFkYXRhVHlwZSI6IkFwa01ldGFkYXRhIiwibmFtZSI6InNzbF9jbGllbnQiLCJwdXJsIjoicGtnOmFscGluZS9zc2xfY2xpZW50QDEuMzUuMD9hcmNoPXg4Nl82NCZ1cHN0cmVhbT1idXN5Ym94JmRpc3Rybz1hbHBpbmUtMy4xNS4yIiwidHlwZSI6ImFwayIsInZlcnNpb24iOiIxLjM1LjAifSx7ImNwZXMiOlsiY3BlOjIuMzphOnpsaWI6emxpYjoxLjIuMTEtcjM6KjoqOio6KjoqOio6KiJdLCJmb3VuZEJ5IjoiYXBrZGItY2F0YWxvZ2VyIiwiaWQiOiJiZTA2ZGY2ZTNiYmJmMGFiIiwibGFuZ3VhZ2UiOiIiLCJsaWNlbnNlcyI6WyJabGliIl0sImxvY2F0aW9ucyI6W3sibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii9saWIvYXBrL2RiL2luc3RhbGxlZCJ9XSwibWV0YWRhdGEiOnsiYXJjaGl0ZWN0dXJlIjoieDg2XzY0IiwiZGVzY3JpcHRpb24iOiJBIGNvbXByZXNzaW9uL2RlY29tcHJlc3Npb24gTGlicmFyeSIsImZpbGVzIjpbeyJwYXRoIjoiL2xpYiJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExYTJIOGhQMjRyeUNBR2Y4Zm5jMU5oYTlJSUhjPSJ9LCJvd25lckdpZCI6IjAiLCJvd25lclVpZCI6IjAiLCJwYXRoIjoiL2xpYi9saWJ6LnNvLjEiLCJwZXJtaXNzaW9ucyI6Ijc3NyJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExTTRPSExVWWh2SG9scWdNQjBUYzNrRWxLN3FBPSJ9LCJvd25lckdpZCI6IjAiLCJvd25lclVpZCI6IjAiLCJwYXRoIjoiL2xpYi9saWJ6LnNvLjEuMi4xMSIsInBlcm1pc3Npb25zIjoiNzU1In1dLCJnaXRDb21taXRPZkFwa1BvcnQiOiIzODhhNGZiMzY0MGY4Y2NiZDE4ZTEwNWRmM2FkNzQxZGNhNDI0N2UxLWRpcnR5IiwiaW5zdGFsbGVkU2l6ZSI6MTEwNTkyLCJsaWNlbnNlIjoiWmxpYiIsIm1haW50YWluZXIiOiJOYXRhbmFlbCBDb3BhIDxuY29wYUBhbHBpbmVsaW51eC5vcmc+Iiwib3JpZ2luUGFja2FnZSI6InpsaWIiLCJwYWNrYWdlIjoiemxpYiIsInB1bGxDaGVja3N1bSI6IlExV0JvKzU3SmxkbFZlMGlWdDJuOElQNit2TkdFPSIsInB1bGxEZXBlbmRlbmNpZXMiOiJzbzpsaWJjLm11c2wteDg2XzY0LnNvLjEiLCJzaXplIjo1MTc0MiwidXJsIjoiaHR0cHM6Ly96bGliLm5ldC8iLCJ2ZXJzaW9uIjoiMS4yLjExLXIzIn0sIm1ldGFkYXRhVHlwZSI6IkFwa01ldGFkYXRhIiwibmFtZSI6InpsaWIiLCJwdXJsIjoicGtnOmFscGluZS96bGliQDEuMi4xMS1yMz9hcmNoPXg4Nl82NCZ1cHN0cmVhbT16bGliJmRpc3Rybz1hbHBpbmUtMy4xNS4yIiwidHlwZSI6ImFwayIsInZlcnNpb24iOiIxLjIuMTEtcjMifV0sImRlc2NyaXB0b3IiOnsiY29uZmlndXJhdGlvbiI6eyJhbmNob3JlIjp7ImRvY2tlcmZpbGUiOiIiLCJob3N0IjoiIiwiaW1wb3J0LXRpbWVvdXQiOjMwLCJvdmVyd3JpdGUtZXhpc3RpbmctaW1hZ2UiOmZhbHNlLCJwYXRoIjoiIn0sImF0dGVzdCI6eyJrZXkiOiJjb3NpZ24ua2V5In0sImNoZWNrLWZvci1hcHAtdXBkYXRlIjp0cnVlLCJjb25maWdQYXRoIjoiIiwiZGV2Ijp7InByb2ZpbGUtY3B1IjpmYWxzZSwicHJvZmlsZS1tZW0iOmZhbHNlfSwiZXhjbHVkZSI6W10sImZpbGUiOiIiLCJmaWxlLWNsYXNzaWZpY2F0aW9uIjp7ImNhdGFsb2dlciI6eyJlbmFibGVkIjpmYWxzZSwic2NvcGUiOiJTcXVhc2hlZCJ9fSwiZmlsZS1jb250ZW50cyI6eyJjYXRhbG9nZXIiOnsiZW5hYmxlZCI6ZmFsc2UsInNjb3BlIjoiU3F1YXNoZWQifSwiZ2xvYnMiOltdLCJza2lwLWZpbGVzLWFib3ZlLXNpemUiOjEwNDg1NzZ9LCJmaWxlLW1ldGFkYXRhIjp7ImNhdGFsb2dlciI6eyJlbmFibGVkIjpmYWxzZSwic2NvcGUiOiJTcXVhc2hlZCJ9LCJkaWdlc3RzIjpbInNoYTI1NiJdfSwibG9nIjp7ImZpbGUtbG9jYXRpb24iOiIiLCJsZXZlbCI6ImVycm9yIiwic3RydWN0dXJlZCI6ZmFsc2V9LCJvdXRwdXQiOlsianNvbiJdLCJwYWNrYWdlIjp7ImNhdGFsb2dlciI6eyJlbmFibGVkIjp0cnVlLCJzY29wZSI6IlNxdWFzaGVkIn0sInNlYXJjaC1pbmRleGVkLWFyY2hpdmVzIjp0cnVlLCJzZWFyY2gtdW5pbmRleGVkLWFyY2hpdmVzIjpmYWxzZX0sInBsYXRmb3JtIjoiIiwicXVpZXQiOmZhbHNlLCJyZWdpc3RyeSI6eyJhdXRoIjpbXSwiaW5zZWN1cmUtc2tpcC10bHMtdmVyaWZ5IjpmYWxzZSwiaW5zZWN1cmUtdXNlLWh0dHAiOmZhbHNlfSwic2VjcmV0cyI6eyJhZGRpdGlvbmFsLXBhdHRlcm5zIjp7fSwiY2F0YWxvZ2VyIjp7ImVuYWJsZWQiOmZhbHNlLCJzY29wZSI6IkFsbExheWVycyJ9LCJleGNsdWRlLXBhdHRlcm4tbmFtZXMiOltdLCJyZXZlYWwtdmFsdWVzIjpmYWxzZSwic2tpcC1maWxlcy1hYm92ZS1zaXplIjoxMDQ4NTc2fX0sIm5hbWUiOiJzeWZ0IiwidmVyc2lvbiI6IjAuNDIuMSJ9LCJkaXN0cm8iOnsiYnVnUmVwb3J0VVJMIjoiaHR0cHM6Ly9idWdzLmFscGluZWxpbnV4Lm9yZy8iLCJob21lVVJMIjoiaHR0cHM6Ly9hbHBpbmVsaW51eC5vcmcvIiwiaWQiOiJhbHBpbmUiLCJuYW1lIjoiQWxwaW5lIExpbnV4IiwicHJldHR5TmFtZSI6IkFscGluZSBMaW51eCB2My4xNSIsInZlcnNpb25JRCI6IjMuMTUuMiJ9LCJmaWxlcyI6W3siaWQiOiI2YzczMTQ0ZWE5ZWY0ZmI5IiwibG9jYXRpb24iOnsibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii9iaW4vYnVzeWJveCJ9fSx7ImlkIjoiZTQxZGFkOGE2MWZmODNiNiIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvZXRjL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNGE2YTA4NDAucnNhLnB1YiJ9fSx7ImlkIjoiYzliZWM1M2MwYTNlMjEwNiIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvZXRjL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNTI0M2VmNGIucnNhLnB1YiJ9fSx7ImlkIjoiYTVkZmQyOTE0ODFjNjllMCIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvZXRjL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNTI2MWNlY2IucnNhLnB1YiJ9fSx7ImlkIjoiYzE1ZjkyZGRjNzdjMWFlNCIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvZXRjL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNjE2NWVlNTkucnNhLnB1YiJ9fSx7ImlkIjoiZDBjMjQ1M2Y5OGIzMTFkOCIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvZXRjL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNjE2NjZlM2YucnNhLnB1YiJ9fSx7ImlkIjoiMjgxNGM4ZjJkYWUyZTJlYSIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvZXRjL2Nyb250YWJzL3Jvb3QifX0seyJpZCI6ImM3OTVkM2Y3ZWJkNjRlODQiLCJsb2NhdGlvbiI6eyJsYXllcklEIjoic2hhMjU2OmZmNzY4YTE0MTNiYTEwOTMwOGMwZDg5N2ZmYTU1NWUwNTJhNDZkMmNmNDcxMTc4ZjAwMWI4MjViNGMyMWYzNTQiLCJwYXRoIjoiL2V0Yy9mc3RhYiJ9fSx7ImlkIjoiYjE3ZTBmZWQzY2I3MDJhZCIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvZXRjL2dyb3VwIn19LHsiaWQiOiJkZjIyZWQxYTMxZmRiYjY4IiwibG9jYXRpb24iOnsibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii9ldGMvaG9zdG5hbWUifX0seyJpZCI6ImU4NWFmMTRkNTAzMjU3NDQiLCJsb2NhdGlvbiI6eyJsYXllcklEIjoic2hhMjU2OmZmNzY4YTE0MTNiYTEwOTMwOGMwZDg5N2ZmYTU1NWUwNTJhNDZkMmNmNDcxMTc4ZjAwMWI4MjViNGMyMWYzNTQiLCJwYXRoIjoiL2V0Yy9ob3N0cyJ9fSx7ImlkIjoiZTkyNDY5NjBmZDQ4ZjI3MCIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvZXRjL2luaXR0YWIifX0seyJpZCI6IjlmYTk0ZWE4ZTg3NGVlNDkiLCJsb2NhdGlvbiI6eyJsYXllcklEIjoic2hhMjU2OmZmNzY4YTE0MTNiYTEwOTMwOGMwZDg5N2ZmYTU1NWUwNTJhNDZkMmNmNDcxMTc4ZjAwMWI4MjViNGMyMWYzNTQiLCJwYXRoIjoiL2V0Yy9sb2dyb3RhdGUuZC9hY3BpZCJ9fSx7ImlkIjoiZDFkMjNhNDczMTE5N2Q1ZSIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvZXRjL21vZHByb2JlLmQvYWxpYXNlcy5jb25mIn19LHsiaWQiOiJkYmU3MTZlMDVmOTExZmY5IiwibG9jYXRpb24iOnsibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii9ldGMvbW9kcHJvYmUuZC9ibGFja2xpc3QuY29uZiJ9fSx7ImlkIjoiNmVkYzk3MGYwMjQ2OWRlYSIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvZXRjL21vZHByb2JlLmQvaTM4Ni5jb25mIn19LHsiaWQiOiI5YTM1NzZlNGFhZGI4MmYxIiwibG9jYXRpb24iOnsibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii9ldGMvbW9kcHJvYmUuZC9rbXMuY29uZiJ9fSx7ImlkIjoiNWEyNWQwZGJlODBhYjI0IiwibG9jYXRpb24iOnsibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii9ldGMvbW9kdWxlcyJ9fSx7ImlkIjoiNTU1OGY3NjAzOGQxYzk3ZSIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvZXRjL21vdGQifX0seyJpZCI6IjU1OWE5ZDUxZTMzZDI0MTAiLCJsb2NhdGlvbiI6eyJsYXllcklEIjoic2hhMjU2OmZmNzY4YTE0MTNiYTEwOTMwOGMwZDg5N2ZmYTU1NWUwNTJhNDZkMmNmNDcxMTc4ZjAwMWI4MjViNGMyMWYzNTQiLCJwYXRoIjoiL2V0Yy9uZXR3b3JrL2lmLXVwLmQvZGFkIn19LHsiaWQiOiJhNzM5YTA0ZThjMmQ4ODk0IiwibG9jYXRpb24iOnsibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii9ldGMvcGFzc3dkIn19LHsiaWQiOiI4ZGIxNjIwNmIzMzI2MDZlIiwibG9jYXRpb24iOnsibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii9ldGMvcHJvZmlsZSJ9fSx7ImlkIjoiMzg3MTJhZGVhZWRiNzMxNiIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvZXRjL3Byb2ZpbGUuZC9SRUFETUUifX0seyJpZCI6IjE0ZGQ5ODVkY2U0Y2Q1YTUiLCJsb2NhdGlvbiI6eyJsYXllcklEIjoic2hhMjU2OmZmNzY4YTE0MTNiYTEwOTMwOGMwZDg5N2ZmYTU1NWUwNTJhNDZkMmNmNDcxMTc4ZjAwMWI4MjViNGMyMWYzNTQiLCJwYXRoIjoiL2V0Yy9wcm9maWxlLmQvY29sb3JfcHJvbXB0LnNoLmRpc2FibGVkIn19LHsiaWQiOiI1OTBjYzI2MzU4N2UyYzlkIiwibG9jYXRpb24iOnsibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii9ldGMvcHJvZmlsZS5kL2xvY2FsZS5zaCJ9fSx7ImlkIjoiMjZhMjBmMTA2ZmEzMjc4MyIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvZXRjL3Byb3RvY29scyJ9fSx7ImlkIjoiZDU3ZmUwZGU3OTBmY2Q0YSIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvZXRjL3NlY3VyZXR0eSJ9fSx7ImlkIjoiZjI5YWYyNDc1NTBkY2FjOCIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvZXRjL3NlcnZpY2VzIn19LHsiaWQiOiI0YTZkNmUwZWE5NTc1MWI3IiwibG9jYXRpb24iOnsibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii9ldGMvc2hhZG93In19LHsiaWQiOiIxY2UxMDM1ZDAwMjE3NmE0IiwibG9jYXRpb24iOnsibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii9ldGMvc2hlbGxzIn19LHsiaWQiOiI1YTg0NzZiZDZmNWExM2JmIiwibG9jYXRpb24iOnsibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii9ldGMvc3NsL2NlcnRzL2NhLWNlcnRpZmljYXRlcy5jcnQifX0seyJpZCI6ImJjNjYzNjJiMjVjNjQzNGEiLCJsb2NhdGlvbiI6eyJsYXllcklEIjoic2hhMjU2OmZmNzY4YTE0MTNiYTEwOTMwOGMwZDg5N2ZmYTU1NWUwNTJhNDZkMmNmNDcxMTc4ZjAwMWI4MjViNGMyMWYzNTQiLCJwYXRoIjoiL2V0Yy9zc2wxLjEvY3RfbG9nX2xpc3QuY25mLmRpc3QifX0seyJpZCI6ImRjNWNiYjc4Y2M3NDBmYmUiLCJsb2NhdGlvbiI6eyJsYXllcklEIjoic2hhMjU2OmZmNzY4YTE0MTNiYTEwOTMwOGMwZDg5N2ZmYTU1NWUwNTJhNDZkMmNmNDcxMTc4ZjAwMWI4MjViNGMyMWYzNTQiLCJwYXRoIjoiL2V0Yy9zc2wxLjEvb3BlbnNzbC5jbmYifX0seyJpZCI6ImRlYzIzOWI1NjVkMTliNjQiLCJsb2NhdGlvbiI6eyJsYXllcklEIjoic2hhMjU2OmZmNzY4YTE0MTNiYTEwOTMwOGMwZDg5N2ZmYTU1NWUwNTJhNDZkMmNmNDcxMTc4ZjAwMWI4MjViNGMyMWYzNTQiLCJwYXRoIjoiL2V0Yy9zc2wxLjEvb3BlbnNzbC5jbmYuZGlzdCJ9fSx7ImlkIjoiYjhmMWU0YTYyYjIxNjYzYiIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvZXRjL3N5c2N0bC5jb25mIn19LHsiaWQiOiI2YzZmOThjODU3YTY5MWNjIiwibG9jYXRpb24iOnsibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii9ldGMvdWRoY3BkLmNvbmYifX0seyJpZCI6IjRhY2E2ZDE1ZGYwOWRhMWQiLCJsb2NhdGlvbiI6eyJsYXllcklEIjoic2hhMjU2OmZmNzY4YTE0MTNiYTEwOTMwOGMwZDg5N2ZmYTU1NWUwNTJhNDZkMmNmNDcxMTc4ZjAwMWI4MjViNGMyMWYzNTQiLCJwYXRoIjoiL2xpYi9sZC1tdXNsLXg4Nl82NC5zby4xIn19LHsiaWQiOiI4NTI0ZGNmMDI5MzZkODE3IiwibG9jYXRpb24iOnsibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii9saWIvbGliYXBrLnNvLjMuMTIuMCJ9fSx7ImlkIjoiYmExYWQxNTM5NGM1MzkyMCIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvbGliL2xpYmNyeXB0by5zby4xLjEifX0seyJpZCI6IjZiMWM5YTE4Y2FiYWYzZWQiLCJsb2NhdGlvbiI6eyJsYXllcklEIjoic2hhMjU2OmZmNzY4YTE0MTNiYTEwOTMwOGMwZDg5N2ZmYTU1NWUwNTJhNDZkMmNmNDcxMTc4ZjAwMWI4MjViNGMyMWYzNTQiLCJwYXRoIjoiL2xpYi9saWJzc2wuc28uMS4xIn19LHsiaWQiOiIxM2ExYWJkMWQyZDM0YTU3IiwibG9jYXRpb24iOnsibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii9saWIvbGliei5zby4xLjIuMTEifX0seyJpZCI6IjQ1NmE3MDJmMTY3MzNiMjQiLCJsb2NhdGlvbiI6eyJsYXllcklEIjoic2hhMjU2OmZmNzY4YTE0MTNiYTEwOTMwOGMwZDg5N2ZmYTU1NWUwNTJhNDZkMmNmNDcxMTc4ZjAwMWI4MjViNGMyMWYzNTQiLCJwYXRoIjoiL2xpYi9zeXNjdGwuZC8wMC1hbHBpbmUuY29uZiJ9fSx7ImlkIjoiMmM2MjRmYmYzOWNhN2Q2NCIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvc2Jpbi9hcGsifX0seyJpZCI6IjIzNzAyNmQxMzRiODRkNmYiLCJsb2NhdGlvbiI6eyJsYXllcklEIjoic2hhMjU2OmZmNzY4YTE0MTNiYTEwOTMwOGMwZDg5N2ZmYTU1NWUwNTJhNDZkMmNmNDcxMTc4ZjAwMWI4MjViNGMyMWYzNTQiLCJwYXRoIjoiL3NiaW4vbGRjb25maWcifX0seyJpZCI6IjE0ZTNmNDFhNzdiNDI4MDciLCJsb2NhdGlvbiI6eyJsYXllcklEIjoic2hhMjU2OmZmNzY4YTE0MTNiYTEwOTMwOGMwZDg5N2ZmYTU1NWUwNTJhNDZkMmNmNDcxMTc4ZjAwMWI4MjViNGMyMWYzNTQiLCJwYXRoIjoiL3NiaW4vbWttbnRkaXJzIn19LHsiaWQiOiIzY2E3Y2JmZDU5MzBkOTBiIiwibG9jYXRpb24iOnsibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii91c3IvYmluL2dldGNvbmYifX0seyJpZCI6IjlhY2M0NmY5ZjlkMjQwNTgiLCJsb2NhdGlvbiI6eyJsYXllcklEIjoic2hhMjU2OmZmNzY4YTE0MTNiYTEwOTMwOGMwZDg5N2ZmYTU1NWUwNTJhNDZkMmNmNDcxMTc4ZjAwMWI4MjViNGMyMWYzNTQiLCJwYXRoIjoiL3Vzci9iaW4vZ2V0ZW50In19LHsiaWQiOiIxNjJmMzI0MDM2NTYxNjQzIiwibG9jYXRpb24iOnsibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii91c3IvYmluL2ljb252In19LHsiaWQiOiJhMjliNDViNWMwMmQwMDA3IiwibG9jYXRpb24iOnsibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii91c3IvYmluL2xkZCJ9fSx7ImlkIjoiMmFmNDEyYjEwZmYyYzVmYiIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvdXNyL2Jpbi9zY2FuZWxmIn19LHsiaWQiOiI5MTUwY2Y2ZGYxYWQ1Zjg2IiwibG9jYXRpb24iOnsibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii91c3IvYmluL3NzbF9jbGllbnQifX0seyJpZCI6ImU2MDU0MmZhMzNkYWViMDUiLCJsb2NhdGlvbiI6eyJsYXllcklEIjoic2hhMjU2OmZmNzY4YTE0MTNiYTEwOTMwOGMwZDg5N2ZmYTU1NWUwNTJhNDZkMmNmNDcxMTc4ZjAwMWI4MjViNGMyMWYzNTQiLCJwYXRoIjoiL3Vzci9saWIvZW5naW5lcy0xLjEvYWZhbGcuc28ifX0seyJpZCI6IjJkMjRjNDBhODhiNGMyZWMiLCJsb2NhdGlvbiI6eyJsYXllcklEIjoic2hhMjU2OmZmNzY4YTE0MTNiYTEwOTMwOGMwZDg5N2ZmYTU1NWUwNTJhNDZkMmNmNDcxMTc4ZjAwMWI4MjViNGMyMWYzNTQiLCJwYXRoIjoiL3Vzci9saWIvZW5naW5lcy0xLjEvY2FwaS5zbyJ9fSx7ImlkIjoiNWRiMzhkZGNlODkxMDVlNyIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvdXNyL2xpYi9lbmdpbmVzLTEuMS9wYWRsb2NrLnNvIn19LHsiaWQiOiIyYjAyZjY3OTBjNmVmN2U4IiwibG9jYXRpb24iOnsibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii91c3IvbGliL2xpYnRscy5zby4yLjAuMyJ9fSx7ImlkIjoiYTJiZjQxNDAyY2MwNDMxNSIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNGE2YTA4NDAucnNhLnB1YiJ9fSx7ImlkIjoiZGI0NjJiZDRjZTliYWVjNSIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNTI0M2VmNGIucnNhLnB1YiJ9fSx7ImlkIjoiYzVmMTI2MmE2NmJmMDQ4YSIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNTI0ZDI3YmIucnNhLnB1YiJ9fSx7ImlkIjoiZWUzYWQ5YzBhNGY1NGU5MyIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNTI2MWNlY2IucnNhLnB1YiJ9fSx7ImlkIjoiOTJkMzU4ZTcyNTZmMGI3YSIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNTgxOTlkY2MucnNhLnB1YiJ9fSx7ImlkIjoiNThmYmViYTc3NGViMmE1NiIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNThjYmI0NzYucnNhLnB1YiJ9fSx7ImlkIjoiZmM4MjgxZjNiZmQ1NzFlNSIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNThlNGYxN2QucnNhLnB1YiJ9fSx7ImlkIjoiZGMxZTA0M2M2NTFkZmI0YyIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNWU2OWNhNTAucnNhLnB1YiJ9fSx7ImlkIjoiNzZiNWFhODA1ZGMxYjE4ZiIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNjBhYzIwOTkucnNhLnB1YiJ9fSx7ImlkIjoiYTAwZjcxNjgxOWZhYTgxOSIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNjE2NWVlNTkucnNhLnB1YiJ9fSx7ImlkIjoiYTNlNzZmMjE5MzNiNTFjNCIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNjE2NjZlM2YucnNhLnB1YiJ9fSx7ImlkIjoiMTFkNGU3NjhjMmViNzYyZCIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNjE2YTk3MjQucnNhLnB1YiJ9fSx7ImlkIjoiNTIyYTYwMmU5YzEwMGU2MiIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNjE2YWJjMjMucnNhLnB1YiJ9fSx7ImlkIjoiYWM3YTBmMjFkZWMwM2ExYSIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNjE2YWMzYmMucnNhLnB1YiJ9fSx7ImlkIjoiMzkxMTQ1ZjBlZmQzOWI5NCIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNjE2YWRmZWIucnNhLnB1YiJ9fSx7ImlkIjoiMjk1YzI2M2IzZDFjODFkMiIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNjE2YWUzNTAucnNhLnB1YiJ9fSx7ImlkIjoiNGE5NmY4YzRjZjM0Njc1OSIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNjE2ZGIzMGQucnNhLnB1YiJ9fSx7ImlkIjoiYmIwODE4ZTZjZDI1Zjk1YSIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvdXNyL3NoYXJlL3VkaGNwYy9kZWZhdWx0LnNjcmlwdCJ9fV0sInNjaGVtYSI6eyJ1cmwiOiJodHRwczovL3Jhdy5naXRodWJ1c2VyY29udGVudC5jb20vYW5jaG9yZS9zeWZ0L21haW4vc2NoZW1hL2pzb24vc2NoZW1hLTMuMi4xLmpzb24iLCJ2ZXJzaW9uIjoiMy4yLjEifSwic291cmNlIjp7InRhcmdldCI6eyJhcmNoaXRlY3R1cmUiOiIiLCJjb25maWciOiJleUpoY21Ob2FYUmxZM1IxY21VaU9pSmhiV1EyTkNJc0ltTnZibVpwWnlJNmV5SkliM04wYm1GdFpTSTZJaUlzSWtSdmJXRnBibTVoYldVaU9pSWlMQ0pWYzJWeUlqb2lJaXdpUVhSMFlXTm9VM1JrYVc0aU9tWmhiSE5sTENKQmRIUmhZMmhUZEdSdmRYUWlPbVpoYkhObExDSkJkSFJoWTJoVGRHUmxjbklpT21aaGJITmxMQ0pVZEhraU9tWmhiSE5sTENKUGNHVnVVM1JrYVc0aU9tWmhiSE5sTENKVGRHUnBiazl1WTJVaU9tWmhiSE5sTENKRmJuWWlPbHNpVUVGVVNEMHZkWE55TDJ4dlkyRnNMM05pYVc0NkwzVnpjaTlzYjJOaGJDOWlhVzQ2TDNWemNpOXpZbWx1T2k5MWMzSXZZbWx1T2k5elltbHVPaTlpYVc0aVhTd2lRMjFrSWpwYklpOWlhVzR2YzJnaVhTd2lTVzFoWjJVaU9pSnphR0V5TlRZNlpUaGxNRE5oWldVMlpqRTJaRGhsWW1GbU9UVmhORGs0TXpBd01UTTNNMkkwTkRjNU1HTmpPR1EwTW1NMVpUZzBZV1kwWmpObU9ERTBaVFF4TWpFeVppSXNJbFp2YkhWdFpYTWlPbTUxYkd3c0lsZHZjbXRwYm1kRWFYSWlPaUlpTENKRmJuUnllWEJ2YVc1MElqcHVkV3hzTENKUGJrSjFhV3hrSWpwdWRXeHNMQ0pNWVdKbGJITWlPbTUxYkd4OUxDSmpiMjUwWVdsdVpYSWlPaUprTXpJMk1qQXlOR0ZqTUdZd09EYzVNR1ppTWpNMlpXRXpNbU0xT1dGak1XWmtNamMzTldVeU1qYzFNR1UxWXpCaVlXWTFaV0UxWXpBeE1qTXhOR0ppSWl3aVkyOXVkR0ZwYm1WeVgyTnZibVpwWnlJNmV5SkliM04wYm1GdFpTSTZJbVF6TWpZeU1ESTBZV013WmlJc0lrUnZiV0ZwYm01aGJXVWlPaUlpTENKVmMyVnlJam9pSWl3aVFYUjBZV05vVTNSa2FXNGlPbVpoYkhObExDSkJkSFJoWTJoVGRHUnZkWFFpT21aaGJITmxMQ0pCZEhSaFkyaFRkR1JsY25JaU9tWmhiSE5sTENKVWRIa2lPbVpoYkhObExDSlBjR1Z1VTNSa2FXNGlPbVpoYkhObExDSlRkR1JwYms5dVkyVWlPbVpoYkhObExDSkZibllpT2xzaVVFRlVTRDB2ZFhOeUwyeHZZMkZzTDNOaWFXNDZMM1Z6Y2k5c2IyTmhiQzlpYVc0NkwzVnpjaTl6WW1sdU9pOTFjM0l2WW1sdU9pOXpZbWx1T2k5aWFXNGlYU3dpUTIxa0lqcGJJaTlpYVc0dmMyZ2lMQ0l0WXlJc0lpTW9ibTl3S1NBaUxDSkRUVVFnVzF3aUwySnBiaTl6YUZ3aVhTSmRMQ0pKYldGblpTSTZJbk5vWVRJMU5qcGxPR1V3TTJGbFpUWm1NVFprT0dWaVlXWTVOV0UwT1Rnek1EQXhNemN6WWpRME56a3dZMk00WkRReVl6VmxPRFJoWmpSbU0yWTRNVFJsTkRFeU1USm1JaXdpVm05c2RXMWxjeUk2Ym5Wc2JDd2lWMjl5YTJsdVowUnBjaUk2SWlJc0lrVnVkSEo1Y0c5cGJuUWlPbTUxYkd3c0lrOXVRblZwYkdRaU9tNTFiR3dzSWt4aFltVnNjeUk2ZTMxOUxDSmpjbVZoZEdWa0lqb2lNakF5TWkwd015MHlNMVF4TlRveU1Ub3lNUzR4TVRrNU1EY3pNVGxhSWl3aVpHOWphMlZ5WDNabGNuTnBiMjRpT2lJeU1DNHhNQzR4TWlJc0ltaHBjM1J2Y25raU9sdDdJbU55WldGMFpXUWlPaUl5TURJeUxUQXpMVEl6VkRFMU9qSXhPakl4TGpBeU16SXpOVGM0TmxvaUxDSmpjbVZoZEdWa1gySjVJam9pTDJKcGJpOXphQ0F0WXlBaktHNXZjQ2tnUVVSRUlHWnBiR1U2TnpNNE5tRmtPRGt6TmpjeU1EQTNZMk5oTW1RM00yTmxZekU0TmpKa05UZ3lZVFk1WkRVNE1XTmhNV1F4TlRWa05EVTVPV05pTW1GaE5UUmtOVFE1T0NCcGJpQXZJQ0o5TEhzaVkzSmxZWFJsWkNJNklqSXdNakl0TURNdE1qTlVNVFU2TWpFNk1qRXVNVEU1T1RBM016RTVXaUlzSW1OeVpXRjBaV1JmWW5raU9pSXZZbWx1TDNOb0lDMWpJQ01vYm05d0tTQWdRMDFFSUZ0Y0lpOWlhVzR2YzJoY0lsMGlMQ0psYlhCMGVWOXNZWGxsY2lJNmRISjFaWDFkTENKdmN5STZJbXhwYm5WNElpd2ljbTl2ZEdaeklqcDdJblI1Y0dVaU9pSnNZWGxsY25NaUxDSmthV1ptWDJsa2N5STZXeUp6YUdFeU5UWTZabVkzTmpoaE1UUXhNMkpoTVRBNU16QTRZekJrT0RrM1ptWmhOVFUxWlRBMU1tRTBObVF5WTJZME56RXhOemhtTURBeFlqZ3lOV0kwWXpJeFpqTTFOQ0pkZlgwPSIsImltYWdlSUQiOiJzaGEyNTY6OWM4NDJhYzQ5YTM5ZmU0MmU3MWE2MjMxODNmZTdmYjdjNzU5ZDU5MDI5ZTlhOGU3ODUxYzM1N2M3ZDhhODZmOCIsImltYWdlU2l6ZSI6NTU3MDE0NywibGF5ZXJzIjpbeyJkaWdlc3QiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsIm1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuaW1hZ2Uucm9vdGZzLmRpZmYudGFyLmd6aXAiLCJzaXplIjo1NTcwMTQ3fV0sIm1hbmlmZXN0IjoiZXdvZ0lDQWljMk5vWlcxaFZtVnljMmx2YmlJNklESXNDaUFnSUNKdFpXUnBZVlI1Y0dVaU9pQWlZWEJ3YkdsallYUnBiMjR2ZG01a0xtUnZZMnRsY2k1a2FYTjBjbWxpZFhScGIyNHViV0Z1YVdabGMzUXVkaklyYW5OdmJpSXNDaUFnSUNKamIyNW1hV2NpT2lCN0NpQWdJQ0FnSUNKdFpXUnBZVlI1Y0dVaU9pQWlZWEJ3YkdsallYUnBiMjR2ZG01a0xtUnZZMnRsY2k1amIyNTBZV2x1WlhJdWFXMWhaMlV1ZGpFcmFuTnZiaUlzQ2lBZ0lDQWdJQ0p6YVhwbElqb2dNVFEzTWl3S0lDQWdJQ0FnSW1ScFoyVnpkQ0k2SUNKemFHRXlOVFk2T1dNNE5ESmhZelE1WVRNNVptVTBNbVUzTVdFMk1qTXhPRE5tWlRkbVlqZGpOelU1WkRVNU1ESTVaVGxoT0dVM09EVXhZek0xTjJNM1pEaGhPRFptT0NJS0lDQWdmU3dLSUNBZ0lteGhlV1Z5Y3lJNklGc0tJQ0FnSUNBZ2V3b2dJQ0FnSUNBZ0lDQWliV1ZrYVdGVWVYQmxJam9nSW1Gd2NHeHBZMkYwYVc5dUwzWnVaQzVrYjJOclpYSXVhVzFoWjJVdWNtOXZkR1p6TG1ScFptWXVkR0Z5TG1kNmFYQWlMQW9nSUNBZ0lDQWdJQ0FpYzJsNlpTSTZJREk0TVRJMk9Ea3NDaUFnSUNBZ0lDQWdJQ0prYVdkbGMzUWlPaUFpYzJoaE1qVTJPak5oWVRSa01HSmlaR1V4T1RKaVptRmlZVGMxWmpKa01USTBaRGhqWmpKbE5tUmxORFV5WVdVd00yVTFOV1ExTkRFd05XVTBObUl3Tm1WaU9ERXlOMlVpQ2lBZ0lDQWdJSDBLSUNBZ1hRcDkiLCJtYW5pZmVzdERpZ2VzdCI6InNoYTI1Njo3M2MxNTU2OTZmZTY1YjY4Njk2ZTZlYTI0MDg4NjkzNTQ2YWM0NjhiM2UxNDU0MmYyM2YwZWZiZGUyODljYzk3IiwibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vdm5kLmRvY2tlci5kaXN0cmlidXRpb24ubWFuaWZlc3QudjIranNvbiIsIm9zIjoiIiwicmVwb0RpZ2VzdHMiOlsiaW5kZXguZG9ja2VyLmlvL2xpYnJhcnkvYWxwaW5lQHNoYTI1Njo2YWYxYjExYmJiMTdmNGMzMTFlMjY5ZGI2NTMwZTRkYTI3MzgyNjJhZjVmZDkwNjRjY2RmMTA5Yjc2NTg2MGZiIl0sInRhZ3MiOltdLCJ1c2VySW5wdXQiOiJhbHBpbmU6bGF0ZXN0In0sInR5cGUiOiJpbWFnZSJ9fX0K\",\"signatures\":[{\"keyid\":\"\",\"sig\":\"MEUCIQDBtal1MWSsNl8U1neDA1Ujec8HvbJ5T4tWtuFNY7OkrgIgFY+wklqhg6Y/HhivWlMmcA593sx6pNnusAqTLlNtIP0=\"}]}"
  },
  {
    "path": "grype/pkg/testdata/alpine-tampered.cdx.att.json",
    "content": "{\"payloadType\":\"application/vnd.in-toto+json\",\"payload\":\"eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjAuMSIsInByZWRpY2F0ZVR5cGUiOiIiaHR0cHM6Ly9zeWZ0LmRldi9ib20iIiwic3ViamVjdCI6W3sibmFtZSI6IiIsImRpZ2VzdCI6eyJzaGEyNTYiOiI0ZWRiZDJiZWI1Zjc4YjEwMTQwMjhmNGZiYjk5ZjMyMzdkOTU2MTEwMGI2ODgxYWFiYmY1YWNjZTJjNGY5NDU0In19XSwicHJlZGljYXRlIjp7ImJvbUZvcm1hdCI6IkN5Y2xvbmVEWCIsImNvbXBvbmVudHMiOlt7ImJvbS1yZWYiOiJwa2c6YWxwaW5lL2FscGluZS1iYXNlbGF5b3V0QDMuMi4wLXIxOD9hcmNoPXg4Nl82NFx1MDAyNnVwc3RyZWFtPWFscGluZS1iYXNlbGF5b3V0XHUwMDI2ZGlzdHJvPWFscGluZS0zLjE1LjRcdTAwMjZzeWZ0LWlkPTlmNTI3MjEzZjRkMmE4NzMiLCJjcGUiOiJjcGU6Mi4zOmE6YWxwaW5lLWJhc2VsYXlvdXQ6YWxwaW5lLWJhc2VsYXlvdXQ6My4yLjAtcjE4Oio6KjoqOio6KjoqOioiLCJkZXNjcmlwdGlvbiI6IkFscGluZSBiYXNlIGRpciBzdHJ1Y3R1cmUgYW5kIGluaXQgc2NyaXB0cyIsImV4dGVybmFsUmVmZXJlbmNlcyI6W3sidHlwZSI6ImRpc3RyaWJ1dGlvbiIsInVybCI6Imh0dHBzOi8vZ2l0LmFscGluZWxpbnV4Lm9yZy9jZ2l0L2Fwb3J0cy90cmVlL21haW4vYWxwaW5lLWJhc2VsYXlvdXQifV0sImxpY2Vuc2VzIjpbeyJsaWNlbnNlIjp7ImlkIjoiR1BMLTIuMC1vbmx5In19XSwibmFtZSI6ImFscGluZS1iYXNlbGF5b3V0IiwicHJvcGVydGllcyI6W3sibmFtZSI6InN5ZnQ6cGFja2FnZTpmb3VuZEJ5IiwidmFsdWUiOiJhcGtkYi1jYXRhbG9nZXIifSx7Im5hbWUiOiJzeWZ0OnBhY2thZ2U6bWV0YWRhdGFUeXBlIiwidmFsdWUiOiJBcGtNZXRhZGF0YSJ9LHsibmFtZSI6InN5ZnQ6cGFja2FnZTp0eXBlIiwidmFsdWUiOiJhcGsifSx7Im5hbWUiOiJzeWZ0OmNwZTIzIiwidmFsdWUiOiJjcGU6Mi4zOmE6YWxwaW5lLWJhc2VsYXlvdXQ6YWxwaW5lX2Jhc2VsYXlvdXQ6My4yLjAtcjE4Oio6KjoqOio6KjoqOioifSx7Im5hbWUiOiJzeWZ0OmNwZTIzIiwidmFsdWUiOiJjcGU6Mi4zOmE6YWxwaW5lX2Jhc2VsYXlvdXQ6YWxwaW5lLWJhc2VsYXlvdXQ6My4yLjAtcjE4Oio6KjoqOio6KjoqOioifSx7Im5hbWUiOiJzeWZ0OmNwZTIzIiwidmFsdWUiOiJjcGU6Mi4zOmE6YWxwaW5lX2Jhc2VsYXlvdXQ6YWxwaW5lX2Jhc2VsYXlvdXQ6My4yLjAtcjE4Oio6KjoqOio6KjoqOioifSx7Im5hbWUiOiJzeWZ0OmNwZTIzIiwidmFsdWUiOiJjcGU6Mi4zOmE6YWxwaW5lOmFscGluZS1iYXNlbGF5b3V0OjMuMi4wLXIxODoqOio6KjoqOio6KjoqIn0seyJuYW1lIjoic3lmdDpjcGUyMyIsInZhbHVlIjoiY3BlOjIuMzphOmFscGluZTphbHBpbmVfYmFzZWxheW91dDozLjIuMC1yMTg6KjoqOio6KjoqOio6KiJ9LHsibmFtZSI6InN5ZnQ6bG9jYXRpb246MDpsYXllcklEIiwidmFsdWUiOiJzaGEyNTY6NGZjMjQyZDU4Mjg1Njk5ZWNhMDVkYjNjYzdjNzEyMmEyYjhlMDE0ZDk0ODFmMzIzYmQ5Mjc3YmFhY2ZhMDYyOCJ9LHsibmFtZSI6InN5ZnQ6bG9jYXRpb246MDpwYXRoIiwidmFsdWUiOiIvbGliL2Fway9kYi9pbnN0YWxsZWQifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOmdpdENvbW1pdE9mQXBrUG9ydCIsInZhbHVlIjoiZGZhMTM3OTM1N2EzMjFlNjM4ZmVlZjFjZDhkNTVhYjAzZDAyMGY0NSJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6aW5zdGFsbGVkU2l6ZSIsInZhbHVlIjoiNDEzNjk2In0seyJuYW1lIjoic3lmdDptZXRhZGF0YTpvcmlnaW5QYWNrYWdlIiwidmFsdWUiOiJhbHBpbmUtYmFzZWxheW91dCJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6cHVsbENoZWNrc3VtIiwidmFsdWUiOiJRMUV5bVM2ckFnbUdzN1hZaHFkeUVvaVdnRVo2QT0ifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOnB1bGxEZXBlbmRlbmNpZXMiLCJ2YWx1ZSI6Ii9iaW4vc2ggc286bGliYy5tdXNsLXg4Nl82NC5zby4xIn0seyJuYW1lIjoic3lmdDptZXRhZGF0YTpzaXplIiwidmFsdWUiOiIyMTEwMSJ9XSwicHVibGlzaGVyIjoiTmF0YW5hZWwgQ29wYSBcdTAwM2NuY29wYUBhbHBpbmVsaW51eC5vcmdcdTAwM2UiLCJwdXJsIjoicGtnOmFscGluZS9hbHBpbmUtYmFzZWxheW91dEAzLjIuMC1yMTg/YXJjaD14ODZfNjRcdTAwMjZ1cHN0cmVhbT1hbHBpbmUtYmFzZWxheW91dFx1MDAyNmRpc3Rybz1hbHBpbmUtMy4xNS40IiwidHlwZSI6ImxpYnJhcnkiLCJ2ZXJzaW9uIjoiMy4yLjAtcjE4In0seyJib20tcmVmIjoicGtnOmFscGluZS9hbHBpbmUta2V5c0AyLjQtcjE/YXJjaD14ODZfNjRcdTAwMjZ1cHN0cmVhbT1hbHBpbmUta2V5c1x1MDAyNmRpc3Rybz1hbHBpbmUtMy4xNS40XHUwMDI2c3lmdC1pZD0xYTcyY2EzYjg4ZTFiNjdlIiwiY3BlIjoiY3BlOjIuMzphOmFscGluZS1rZXlzOmFscGluZS1rZXlzOjIuNC1yMToqOio6KjoqOio6KjoqIiwiZGVzY3JpcHRpb24iOiJQdWJsaWMga2V5cyBmb3IgQWxwaW5lIExpbnV4IHBhY2thZ2VzIiwiZXh0ZXJuYWxSZWZlcmVuY2VzIjpbeyJ0eXBlIjoiZGlzdHJpYnV0aW9uIiwidXJsIjoiaHR0cHM6Ly9hbHBpbmVsaW51eC5vcmcifV0sImxpY2Vuc2VzIjpbeyJsaWNlbnNlIjp7ImlkIjoiTUlUIn19XSwibmFtZSI6ImFscGluZS1rZXlzIiwicHJvcGVydGllcyI6W3sibmFtZSI6InN5ZnQ6cGFja2FnZTpmb3VuZEJ5IiwidmFsdWUiOiJhcGtkYi1jYXRhbG9nZXIifSx7Im5hbWUiOiJzeWZ0OnBhY2thZ2U6bWV0YWRhdGFUeXBlIiwidmFsdWUiOiJBcGtNZXRhZGF0YSJ9LHsibmFtZSI6InN5ZnQ6cGFja2FnZTp0eXBlIiwidmFsdWUiOiJhcGsifSx7Im5hbWUiOiJzeWZ0OmNwZTIzIiwidmFsdWUiOiJjcGU6Mi4zOmE6YWxwaW5lLWtleXM6YWxwaW5lX2tleXM6Mi40LXIxOio6KjoqOio6KjoqOioifSx7Im5hbWUiOiJzeWZ0OmNwZTIzIiwidmFsdWUiOiJjcGU6Mi4zOmE6YWxwaW5lX2tleXM6YWxwaW5lLWtleXM6Mi40LXIxOio6KjoqOio6KjoqOioifSx7Im5hbWUiOiJzeWZ0OmNwZTIzIiwidmFsdWUiOiJjcGU6Mi4zOmE6YWxwaW5lX2tleXM6YWxwaW5lX2tleXM6Mi40LXIxOio6KjoqOio6KjoqOioifSx7Im5hbWUiOiJzeWZ0OmNwZTIzIiwidmFsdWUiOiJjcGU6Mi4zOmE6YWxwaW5lOmFscGluZS1rZXlzOjIuNC1yMToqOio6KjoqOio6KjoqIn0seyJuYW1lIjoic3lmdDpjcGUyMyIsInZhbHVlIjoiY3BlOjIuMzphOmFscGluZTphbHBpbmVfa2V5czoyLjQtcjE6KjoqOio6KjoqOio6KiJ9LHsibmFtZSI6InN5ZnQ6bG9jYXRpb246MDpsYXllcklEIiwidmFsdWUiOiJzaGEyNTY6NGZjMjQyZDU4Mjg1Njk5ZWNhMDVkYjNjYzdjNzEyMmEyYjhlMDE0ZDk0ODFmMzIzYmQ5Mjc3YmFhY2ZhMDYyOCJ9LHsibmFtZSI6InN5ZnQ6bG9jYXRpb246MDpwYXRoIiwidmFsdWUiOiIvbGliL2Fway9kYi9pbnN0YWxsZWQifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOmdpdENvbW1pdE9mQXBrUG9ydCIsInZhbHVlIjoiYWFiNjhmOGM5YWI0MzRhNDY3MTBkZThlMTJmYjMyMDZlMjkzMGE1OSJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6aW5zdGFsbGVkU2l6ZSIsInZhbHVlIjoiMTU5NzQ0In0seyJuYW1lIjoic3lmdDptZXRhZGF0YTpvcmlnaW5QYWNrYWdlIiwidmFsdWUiOiJhbHBpbmUta2V5cyJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6cHVsbENoZWNrc3VtIiwidmFsdWUiOiJRMWtERjJzdEtvM2UvUnVtbEE4WnJSZkN3ZFN2OD0ifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOnNpemUiLCJ2YWx1ZSI6IjEzMzYyIn1dLCJwdWJsaXNoZXIiOiJOYXRhbmFlbCBDb3BhIFx1MDAzY25jb3BhQGFscGluZWxpbnV4Lm9yZ1x1MDAzZSIsInB1cmwiOiJwa2c6YWxwaW5lL2FscGluZS1rZXlzQDIuNC1yMT9hcmNoPXg4Nl82NFx1MDAyNnVwc3RyZWFtPWFscGluZS1rZXlzXHUwMDI2ZGlzdHJvPWFscGluZS0zLjE1LjQiLCJ0eXBlIjoibGlicmFyeSIsInZlcnNpb24iOiIyLjQtcjEifSx7ImJvbS1yZWYiOiJwa2c6YWxwaW5lL2Fway10b29sc0AyLjEyLjctcjM/YXJjaD14ODZfNjRcdTAwMjZ1cHN0cmVhbT1hcGstdG9vbHNcdTAwMjZkaXN0cm89YWxwaW5lLTMuMTUuNFx1MDAyNnN5ZnQtaWQ9MWM2ZTA1N2M2OTY1YmRkNiIsImNwZSI6ImNwZToyLjM6YTphcGstdG9vbHM6YXBrLXRvb2xzOjIuMTIuNy1yMzoqOio6KjoqOio6KjoqIiwiZGVzY3JpcHRpb24iOiJBbHBpbmUgUGFja2FnZSBLZWVwZXIgLSBwYWNrYWdlIG1hbmFnZXIgZm9yIGFscGluZSIsImV4dGVybmFsUmVmZXJlbmNlcyI6W3sidHlwZSI6ImRpc3RyaWJ1dGlvbiIsInVybCI6Imh0dHBzOi8vZ2l0bGFiLmFscGluZWxpbnV4Lm9yZy9hbHBpbmUvYXBrLXRvb2xzIn1dLCJsaWNlbnNlcyI6W3sibGljZW5zZSI6eyJpZCI6IkdQTC0yLjAtb25seSJ9fV0sIm5hbWUiOiJhcGstdG9vbHMiLCJwcm9wZXJ0aWVzIjpbeyJuYW1lIjoic3lmdDpwYWNrYWdlOmZvdW5kQnkiLCJ2YWx1ZSI6ImFwa2RiLWNhdGFsb2dlciJ9LHsibmFtZSI6InN5ZnQ6cGFja2FnZTptZXRhZGF0YVR5cGUiLCJ2YWx1ZSI6IkFwa01ldGFkYXRhIn0seyJuYW1lIjoic3lmdDpwYWNrYWdlOnR5cGUiLCJ2YWx1ZSI6ImFwayJ9LHsibmFtZSI6InN5ZnQ6Y3BlMjMiLCJ2YWx1ZSI6ImNwZToyLjM6YTphcGstdG9vbHM6YXBrX3Rvb2xzOjIuMTIuNy1yMzoqOio6KjoqOio6KjoqIn0seyJuYW1lIjoic3lmdDpjcGUyMyIsInZhbHVlIjoiY3BlOjIuMzphOmFwa190b29sczphcGstdG9vbHM6Mi4xMi43LXIzOio6KjoqOio6KjoqOioifSx7Im5hbWUiOiJzeWZ0OmNwZTIzIiwidmFsdWUiOiJjcGU6Mi4zOmE6YXBrX3Rvb2xzOmFwa190b29sczoyLjEyLjctcjM6KjoqOio6KjoqOio6KiJ9LHsibmFtZSI6InN5ZnQ6Y3BlMjMiLCJ2YWx1ZSI6ImNwZToyLjM6YTphcGs6YXBrLXRvb2xzOjIuMTIuNy1yMzoqOio6KjoqOio6KjoqIn0seyJuYW1lIjoic3lmdDpjcGUyMyIsInZhbHVlIjoiY3BlOjIuMzphOmFwazphcGtfdG9vbHM6Mi4xMi43LXIzOio6KjoqOio6KjoqOioifSx7Im5hbWUiOiJzeWZ0OmxvY2F0aW9uOjA6bGF5ZXJJRCIsInZhbHVlIjoic2hhMjU2OjRmYzI0MmQ1ODI4NTY5OWVjYTA1ZGIzY2M3YzcxMjJhMmI4ZTAxNGQ5NDgxZjMyM2JkOTI3N2JhYWNmYTA2MjgifSx7Im5hbWUiOiJzeWZ0OmxvY2F0aW9uOjA6cGF0aCIsInZhbHVlIjoiL2xpYi9hcGsvZGIvaW5zdGFsbGVkIn0seyJuYW1lIjoic3lmdDptZXRhZGF0YTpnaXRDb21taXRPZkFwa1BvcnQiLCJ2YWx1ZSI6IjFhYzNjMWJiMjllZWZmMDgzYzYyMWNmNmIyN2FkMTJhYjkzY2I3M2EifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOmluc3RhbGxlZFNpemUiLCJ2YWx1ZSI6IjMxMTI5NiJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6b3JpZ2luUGFja2FnZSIsInZhbHVlIjoiYXBrLXRvb2xzIn0seyJuYW1lIjoic3lmdDptZXRhZGF0YTpwdWxsQ2hlY2tzdW0iLCJ2YWx1ZSI6IlExM2ZQZCtGUlhhTHd5TmtsVm4rcXVGV0R5a25NPSJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6cHVsbERlcGVuZGVuY2llcyIsInZhbHVlIjoibXVzbFx1MDAzZT0xLjIgY2EtY2VydGlmaWNhdGVzLWJ1bmRsZSBzbzpsaWJjLm11c2wteDg2XzY0LnNvLjEgc286bGliY3J5cHRvLnNvLjEuMSBzbzpsaWJzc2wuc28uMS4xIHNvOmxpYnouc28uMSJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6c2l6ZSIsInZhbHVlIjoiMTIwMzc3In1dLCJwdWJsaXNoZXIiOiJOYXRhbmFlbCBDb3BhIFx1MDAzY25jb3BhQGFscGluZWxpbnV4Lm9yZ1x1MDAzZSIsInB1cmwiOiJwa2c6YWxwaW5lL2Fway10b29sc0AyLjEyLjctcjM/YXJjaD14ODZfNjRcdTAwMjZ1cHN0cmVhbT1hcGstdG9vbHNcdTAwMjZkaXN0cm89YWxwaW5lLTMuMTUuNCIsInR5cGUiOiJsaWJyYXJ5IiwidmVyc2lvbiI6IjIuMTIuNy1yMyJ9LHsiYm9tLXJlZiI6InBrZzphbHBpbmUvYnVzeWJveEAxLjM0LjEtcjU/YXJjaD14ODZfNjRcdTAwMjZ1cHN0cmVhbT1idXN5Ym94XHUwMDI2ZGlzdHJvPWFscGluZS0zLjE1LjRcdTAwMjZzeWZ0LWlkPWE1MjcwMjYxMmFhMTZkZTMiLCJjcGUiOiJjcGU6Mi4zOmE6YnVzeWJveDpidXN5Ym94OjEuMzQuMS1yNToqOio6KjoqOio6KjoqIiwiZGVzY3JpcHRpb24iOiJTaXplIG9wdGltaXplZCB0b29sYm94IG9mIG1hbnkgY29tbW9uIFVOSVggdXRpbGl0aWVzIiwiZXh0ZXJuYWxSZWZlcmVuY2VzIjpbeyJ0eXBlIjoiZGlzdHJpYnV0aW9uIiwidXJsIjoiaHR0cHM6Ly9idXN5Ym94Lm5ldC8ifV0sImxpY2Vuc2VzIjpbeyJsaWNlbnNlIjp7ImlkIjoiR1BMLTIuMC1vbmx5In19XSwibmFtZSI6ImJ1c3lib3giLCJwcm9wZXJ0aWVzIjpbeyJuYW1lIjoic3lmdDpwYWNrYWdlOmZvdW5kQnkiLCJ2YWx1ZSI6ImFwa2RiLWNhdGFsb2dlciJ9LHsibmFtZSI6InN5ZnQ6cGFja2FnZTptZXRhZGF0YVR5cGUiLCJ2YWx1ZSI6IkFwa01ldGFkYXRhIn0seyJuYW1lIjoic3lmdDpwYWNrYWdlOnR5cGUiLCJ2YWx1ZSI6ImFwayJ9LHsibmFtZSI6InN5ZnQ6bG9jYXRpb246MDpsYXllcklEIiwidmFsdWUiOiJzaGEyNTY6NGZjMjQyZDU4Mjg1Njk5ZWNhMDVkYjNjYzdjNzEyMmEyYjhlMDE0ZDk0ODFmMzIzYmQ5Mjc3YmFhY2ZhMDYyOCJ9LHsibmFtZSI6InN5ZnQ6bG9jYXRpb246MDpwYXRoIiwidmFsdWUiOiIvbGliL2Fway9kYi9pbnN0YWxsZWQifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOmdpdENvbW1pdE9mQXBrUG9ydCIsInZhbHVlIjoiMjc0NWRlN2UxYjA5ZTY2M2I0NzdhODE0MWI4NGY3ZDgxYTA0OTk2MyJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6aW5zdGFsbGVkU2l6ZSIsInZhbHVlIjoiOTQ2MTc2In0seyJuYW1lIjoic3lmdDptZXRhZGF0YTpvcmlnaW5QYWNrYWdlIiwidmFsdWUiOiJidXN5Ym94In0seyJuYW1lIjoic3lmdDptZXRhZGF0YTpwdWxsQ2hlY2tzdW0iLCJ2YWx1ZSI6IlExTFV5UEtJS3pVSzZ2cEpjQmorTzQwNGVmYXdnPSJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6cHVsbERlcGVuZGVuY2llcyIsInZhbHVlIjoic286bGliYy5tdXNsLXg4Nl82NC5zby4xIn0seyJuYW1lIjoic3lmdDptZXRhZGF0YTpzaXplIiwidmFsdWUiOiI1MDA2NzgifV0sInB1Ymxpc2hlciI6Ik5hdGFuYWVsIENvcGEgXHUwMDNjbmNvcGFAYWxwaW5lbGludXgub3JnXHUwMDNlIiwicHVybCI6InBrZzphbHBpbmUvYnVzeWJveEAxLjM0LjEtcjU/YXJjaD14ODZfNjRcdTAwMjZ1cHN0cmVhbT1idXN5Ym94XHUwMDI2ZGlzdHJvPWFscGluZS0zLjE1LjQiLCJ0eXBlIjoibGlicmFyeSIsInZlcnNpb24iOiIxLjM0LjEtcjUifSx7ImJvbS1yZWYiOiJwa2c6YWxwaW5lL2NhLWNlcnRpZmljYXRlcy1idW5kbGVAMjAyMTEyMjAtcjA/YXJjaD14ODZfNjRcdTAwMjZ1cHN0cmVhbT1jYS1jZXJ0aWZpY2F0ZXNcdTAwMjZkaXN0cm89YWxwaW5lLTMuMTUuNFx1MDAyNnN5ZnQtaWQ9MmM0NTIyMjkyM2QzMGZjNSIsImNwZSI6ImNwZToyLjM6YTpjYS1jZXJ0aWZpY2F0ZXMtYnVuZGxlOmNhLWNlcnRpZmljYXRlcy1idW5kbGU6MjAyMTEyMjAtcjA6KjoqOio6KjoqOio6KiIsImRlc2NyaXB0aW9uIjoiUHJlIGdlbmVyYXRlZCBidW5kbGUgb2YgTW96aWxsYSBjZXJ0aWZpY2F0ZXMiLCJleHRlcm5hbFJlZmVyZW5jZXMiOlt7InR5cGUiOiJkaXN0cmlidXRpb24iLCJ1cmwiOiJodHRwczovL3d3dy5tb3ppbGxhLm9yZy9lbi1VUy9hYm91dC9nb3Zlcm5hbmNlL3BvbGljaWVzL3NlY3VyaXR5LWdyb3VwL2NlcnRzLyJ9XSwibGljZW5zZXMiOlt7ImxpY2Vuc2UiOnsiaWQiOiJNUEwtMi4wIn19LHsibGljZW5zZSI6eyJpZCI6Ik1JVCJ9fV0sIm5hbWUiOiJjYS1jZXJ0aWZpY2F0ZXMtYnVuZGxlIiwicHJvcGVydGllcyI6W3sibmFtZSI6InN5ZnQ6cGFja2FnZTpmb3VuZEJ5IiwidmFsdWUiOiJhcGtkYi1jYXRhbG9nZXIifSx7Im5hbWUiOiJzeWZ0OnBhY2thZ2U6bWV0YWRhdGFUeXBlIiwidmFsdWUiOiJBcGtNZXRhZGF0YSJ9LHsibmFtZSI6InN5ZnQ6cGFja2FnZTp0eXBlIiwidmFsdWUiOiJhcGsifSx7Im5hbWUiOiJzeWZ0OmNwZTIzIiwidmFsdWUiOiJjcGU6Mi4zOmE6Y2EtY2VydGlmaWNhdGVzLWJ1bmRsZTpjYV9jZXJ0aWZpY2F0ZXNfYnVuZGxlOjIwMjExMjIwLXIwOio6KjoqOio6KjoqOioifSx7Im5hbWUiOiJzeWZ0OmNwZTIzIiwidmFsdWUiOiJjcGU6Mi4zOmE6Y2FfY2VydGlmaWNhdGVzX2J1bmRsZTpjYS1jZXJ0aWZpY2F0ZXMtYnVuZGxlOjIwMjExMjIwLXIwOio6KjoqOio6KjoqOioifSx7Im5hbWUiOiJzeWZ0OmNwZTIzIiwidmFsdWUiOiJjcGU6Mi4zOmE6Y2FfY2VydGlmaWNhdGVzX2J1bmRsZTpjYV9jZXJ0aWZpY2F0ZXNfYnVuZGxlOjIwMjExMjIwLXIwOio6KjoqOio6KjoqOioifSx7Im5hbWUiOiJzeWZ0OmNwZTIzIiwidmFsdWUiOiJjcGU6Mi4zOmE6Y2EtY2VydGlmaWNhdGVzOmNhLWNlcnRpZmljYXRlcy1idW5kbGU6MjAyMTEyMjAtcjA6KjoqOio6KjoqOio6KiJ9LHsibmFtZSI6InN5ZnQ6Y3BlMjMiLCJ2YWx1ZSI6ImNwZToyLjM6YTpjYS1jZXJ0aWZpY2F0ZXM6Y2FfY2VydGlmaWNhdGVzX2J1bmRsZToyMDIxMTIyMC1yMDoqOio6KjoqOio6KjoqIn0seyJuYW1lIjoic3lmdDpjcGUyMyIsInZhbHVlIjoiY3BlOjIuMzphOmNhX2NlcnRpZmljYXRlczpjYS1jZXJ0aWZpY2F0ZXMtYnVuZGxlOjIwMjExMjIwLXIwOio6KjoqOio6KjoqOioifSx7Im5hbWUiOiJzeWZ0OmNwZTIzIiwidmFsdWUiOiJjcGU6Mi4zOmE6Y2FfY2VydGlmaWNhdGVzOmNhX2NlcnRpZmljYXRlc19idW5kbGU6MjAyMTEyMjAtcjA6KjoqOio6KjoqOio6KiJ9LHsibmFtZSI6InN5ZnQ6Y3BlMjMiLCJ2YWx1ZSI6ImNwZToyLjM6YTpjYTpjYS1jZXJ0aWZpY2F0ZXMtYnVuZGxlOjIwMjExMjIwLXIwOio6KjoqOio6KjoqOioifSx7Im5hbWUiOiJzeWZ0OmNwZTIzIiwidmFsdWUiOiJjcGU6Mi4zOmE6Y2E6Y2FfY2VydGlmaWNhdGVzX2J1bmRsZToyMDIxMTIyMC1yMDoqOio6KjoqOio6KjoqIn0seyJuYW1lIjoic3lmdDpsb2NhdGlvbjowOmxheWVySUQiLCJ2YWx1ZSI6InNoYTI1Njo0ZmMyNDJkNTgyODU2OTllY2EwNWRiM2NjN2M3MTIyYTJiOGUwMTRkOTQ4MWYzMjNiZDkyNzdiYWFjZmEwNjI4In0seyJuYW1lIjoic3lmdDpsb2NhdGlvbjowOnBhdGgiLCJ2YWx1ZSI6Ii9saWIvYXBrL2RiL2luc3RhbGxlZCJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6Z2l0Q29tbWl0T2ZBcGtQb3J0IiwidmFsdWUiOiI3MDliNzBiY2I3MjczOGNmZWRjNTEwYmJhMDgxNDFiMDEyMDM4MTY3In0seyJuYW1lIjoic3lmdDptZXRhZGF0YTppbnN0YWxsZWRTaXplIiwidmFsdWUiOiIyMjExODQifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOm9yaWdpblBhY2thZ2UiLCJ2YWx1ZSI6ImNhLWNlcnRpZmljYXRlcyJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6cHVsbENoZWNrc3VtIiwidmFsdWUiOiJRMVNWQVd1V0hkUEh2YkJoTFRrQVo2MC8xV3NtST0ifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOnNpemUiLCJ2YWx1ZSI6IjExOTc0OCJ9XSwicHVibGlzaGVyIjoiTmF0YW5hZWwgQ29wYSBcdTAwM2NuY29wYUBhbHBpbmVsaW51eC5vcmdcdTAwM2UiLCJwdXJsIjoicGtnOmFscGluZS9jYS1jZXJ0aWZpY2F0ZXMtYnVuZGxlQDIwMjExMjIwLXIwP2FyY2g9eDg2XzY0XHUwMDI2dXBzdHJlYW09Y2EtY2VydGlmaWNhdGVzXHUwMDI2ZGlzdHJvPWFscGluZS0zLjE1LjQiLCJ0eXBlIjoibGlicmFyeSIsInZlcnNpb24iOiIyMDIxMTIyMC1yMCJ9LHsiYm9tLXJlZiI6InBrZzphbHBpbmUvbGliYy11dGlsc0AwLjcuMi1yMz9hcmNoPXg4Nl82NFx1MDAyNnVwc3RyZWFtPWxpYmMtZGV2XHUwMDI2ZGlzdHJvPWFscGluZS0zLjE1LjRcdTAwMjZzeWZ0LWlkPWU4N2E3OWZkYWVjYWFiZDIiLCJjcGUiOiJjcGU6Mi4zOmE6bGliYy11dGlsczpsaWJjLXV0aWxzOjAuNy4yLXIzOio6KjoqOio6KjoqOioiLCJkZXNjcmlwdGlvbiI6Ik1ldGEgcGFja2FnZSB0byBwdWxsIGluIGNvcnJlY3QgbGliYyIsImV4dGVybmFsUmVmZXJlbmNlcyI6W3sidHlwZSI6ImRpc3RyaWJ1dGlvbiIsInVybCI6Imh0dHBzOi8vYWxwaW5lbGludXgub3JnIn1dLCJsaWNlbnNlcyI6W3sibGljZW5zZSI6eyJpZCI6IkJTRC0yLUNsYXVzZSJ9fSx7ImxpY2Vuc2UiOnsiaWQiOiJCU0QtMy1DbGF1c2UifX1dLCJuYW1lIjoibGliYy11dGlscyIsInByb3BlcnRpZXMiOlt7Im5hbWUiOiJzeWZ0OnBhY2thZ2U6Zm91bmRCeSIsInZhbHVlIjoiYXBrZGItY2F0YWxvZ2VyIn0seyJuYW1lIjoic3lmdDpwYWNrYWdlOm1ldGFkYXRhVHlwZSIsInZhbHVlIjoiQXBrTWV0YWRhdGEifSx7Im5hbWUiOiJzeWZ0OnBhY2thZ2U6dHlwZSIsInZhbHVlIjoiYXBrIn0seyJuYW1lIjoic3lmdDpjcGUyMyIsInZhbHVlIjoiY3BlOjIuMzphOmxpYmMtdXRpbHM6bGliY191dGlsczowLjcuMi1yMzoqOio6KjoqOio6KjoqIn0seyJuYW1lIjoic3lmdDpjcGUyMyIsInZhbHVlIjoiY3BlOjIuMzphOmxpYmNfdXRpbHM6bGliYy11dGlsczowLjcuMi1yMzoqOio6KjoqOio6KjoqIn0seyJuYW1lIjoic3lmdDpjcGUyMyIsInZhbHVlIjoiY3BlOjIuMzphOmxpYmNfdXRpbHM6bGliY191dGlsczowLjcuMi1yMzoqOio6KjoqOio6KjoqIn0seyJuYW1lIjoic3lmdDpjcGUyMyIsInZhbHVlIjoiY3BlOjIuMzphOmxpYmM6bGliYy11dGlsczowLjcuMi1yMzoqOio6KjoqOio6KjoqIn0seyJuYW1lIjoic3lmdDpjcGUyMyIsInZhbHVlIjoiY3BlOjIuMzphOmxpYmM6bGliY191dGlsczowLjcuMi1yMzoqOio6KjoqOio6KjoqIn0seyJuYW1lIjoic3lmdDpsb2NhdGlvbjowOmxheWVySUQiLCJ2YWx1ZSI6InNoYTI1Njo0ZmMyNDJkNTgyODU2OTllY2EwNWRiM2NjN2M3MTIyYTJiOGUwMTRkOTQ4MWYzMjNiZDkyNzdiYWFjZmEwNjI4In0seyJuYW1lIjoic3lmdDpsb2NhdGlvbjowOnBhdGgiLCJ2YWx1ZSI6Ii9saWIvYXBrL2RiL2luc3RhbGxlZCJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6Z2l0Q29tbWl0T2ZBcGtQb3J0IiwidmFsdWUiOiI2MDQyNDEzM2JlMmU3OWJiZmVmZjNkNTgxNDdhMjI4ODZmODE3Y2UyIn0seyJuYW1lIjoic3lmdDptZXRhZGF0YTppbnN0YWxsZWRTaXplIiwidmFsdWUiOiI0MDk2In0seyJuYW1lIjoic3lmdDptZXRhZGF0YTpvcmlnaW5QYWNrYWdlIiwidmFsdWUiOiJsaWJjLWRldiJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6cHVsbENoZWNrc3VtIiwidmFsdWUiOiJRMWVZM2o2N1YvUGlqMENBZ0hScE5mSVRvSmx5ST0ifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOnB1bGxEZXBlbmRlbmNpZXMiLCJ2YWx1ZSI6Im11c2wtdXRpbHMifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOnNpemUiLCJ2YWx1ZSI6IjE0ODUifV0sInB1Ymxpc2hlciI6Ik5hdGFuYWVsIENvcGEgXHUwMDNjbmNvcGFAYWxwaW5lbGludXgub3JnXHUwMDNlIiwicHVybCI6InBrZzphbHBpbmUvbGliYy11dGlsc0AwLjcuMi1yMz9hcmNoPXg4Nl82NFx1MDAyNnVwc3RyZWFtPWxpYmMtZGV2XHUwMDI2ZGlzdHJvPWFscGluZS0zLjE1LjQiLCJ0eXBlIjoibGlicmFyeSIsInZlcnNpb24iOiIwLjcuMi1yMyJ9LHsiYm9tLXJlZiI6InBrZzphbHBpbmUvbGliY3J5cHRvMS4xQDEuMS4xbi1yMD9hcmNoPXg4Nl82NFx1MDAyNnVwc3RyZWFtPW9wZW5zc2xcdTAwMjZkaXN0cm89YWxwaW5lLTMuMTUuNFx1MDAyNnN5ZnQtaWQ9MTdjNmZiYjBjYWZiYmU1NyIsImNwZSI6ImNwZToyLjM6YTpsaWJjcnlwdG8xLjE6bGliY3J5cHRvMS4xOjEuMS4xbi1yMDoqOio6KjoqOio6KjoqIiwiZGVzY3JpcHRpb24iOiJDcnlwdG8gbGlicmFyeSBmcm9tIG9wZW5zc2wiLCJleHRlcm5hbFJlZmVyZW5jZXMiOlt7InR5cGUiOiJkaXN0cmlidXRpb24iLCJ1cmwiOiJodHRwczovL3d3dy5vcGVuc3NsLm9yZy8ifV0sImxpY2Vuc2VzIjpbeyJsaWNlbnNlIjp7ImlkIjoiT3BlblNTTCJ9fV0sIm5hbWUiOiJsaWJjcnlwdG8xLjEiLCJwcm9wZXJ0aWVzIjpbeyJuYW1lIjoic3lmdDpwYWNrYWdlOmZvdW5kQnkiLCJ2YWx1ZSI6ImFwa2RiLWNhdGFsb2dlciJ9LHsibmFtZSI6InN5ZnQ6cGFja2FnZTptZXRhZGF0YVR5cGUiLCJ2YWx1ZSI6IkFwa01ldGFkYXRhIn0seyJuYW1lIjoic3lmdDpwYWNrYWdlOnR5cGUiLCJ2YWx1ZSI6ImFwayJ9LHsibmFtZSI6InN5ZnQ6bG9jYXRpb246MDpsYXllcklEIiwidmFsdWUiOiJzaGEyNTY6NGZjMjQyZDU4Mjg1Njk5ZWNhMDVkYjNjYzdjNzEyMmEyYjhlMDE0ZDk0ODFmMzIzYmQ5Mjc3YmFhY2ZhMDYyOCJ9LHsibmFtZSI6InN5ZnQ6bG9jYXRpb246MDpwYXRoIiwidmFsdWUiOiIvbGliL2Fway9kYi9pbnN0YWxsZWQifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOmdpdENvbW1pdE9mQXBrUG9ydCIsInZhbHVlIjoiNDU1ZTk2Njg5OWE5MzU4ZmM5NGY1YmNlNjMzYWZlOGExOTQyMDk1YyJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6aW5zdGFsbGVkU2l6ZSIsInZhbHVlIjoiMjc0MDIyNCJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6b3JpZ2luUGFja2FnZSIsInZhbHVlIjoib3BlbnNzbCJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6cHVsbENoZWNrc3VtIiwidmFsdWUiOiJRMXJBc0xjYlk5NlQrVHFvdTBNSDB5UFExMWhHUT0ifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOnB1bGxEZXBlbmRlbmNpZXMiLCJ2YWx1ZSI6InNvOmxpYmMubXVzbC14ODZfNjQuc28uMSJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6c2l6ZSIsInZhbHVlIjoiMTIwODIyOCJ9XSwicHVibGlzaGVyIjoiVGltbyBUZXJhcyBcdTAwM2N0aW1vLnRlcmFzQGlraS5maVx1MDAzZSIsInB1cmwiOiJwa2c6YWxwaW5lL2xpYmNyeXB0bzEuMUAxLjEuMW4tcjA/YXJjaD14ODZfNjRcdTAwMjZ1cHN0cmVhbT1vcGVuc3NsXHUwMDI2ZGlzdHJvPWFscGluZS0zLjE1LjQiLCJ0eXBlIjoibGlicmFyeSIsInZlcnNpb24iOiIxLjEuMW4tcjAifSx7ImJvbS1yZWYiOiJwa2c6YWxwaW5lL2xpYnJldGxzQDMuMy40LXIzP2FyY2g9eDg2XzY0XHUwMDI2dXBzdHJlYW09bGlicmV0bHNcdTAwMjZkaXN0cm89YWxwaW5lLTMuMTUuNFx1MDAyNnN5ZnQtaWQ9NmQwNjU4YzJiNzIzN2VhMiIsImNwZSI6ImNwZToyLjM6YTpsaWJyZXRsczpsaWJyZXRsczozLjMuNC1yMzoqOio6KjoqOio6KjoqIiwiZGVzY3JpcHRpb24iOiJwb3J0IG9mIGxpYnRscyBmcm9tIGxpYnJlc3NsIHRvIG9wZW5zc2wiLCJleHRlcm5hbFJlZmVyZW5jZXMiOlt7InR5cGUiOiJkaXN0cmlidXRpb24iLCJ1cmwiOiJodHRwczovL2dpdC5jYXVzYWwuYWdlbmN5L2xpYnJldGxzLyJ9XSwibGljZW5zZXMiOlt7ImxpY2Vuc2UiOnsiaWQiOiJJU0MifX1dLCJuYW1lIjoibGlicmV0bHMiLCJwcm9wZXJ0aWVzIjpbeyJuYW1lIjoic3lmdDpwYWNrYWdlOmZvdW5kQnkiLCJ2YWx1ZSI6ImFwa2RiLWNhdGFsb2dlciJ9LHsibmFtZSI6InN5ZnQ6cGFja2FnZTptZXRhZGF0YVR5cGUiLCJ2YWx1ZSI6IkFwa01ldGFkYXRhIn0seyJuYW1lIjoic3lmdDpwYWNrYWdlOnR5cGUiLCJ2YWx1ZSI6ImFwayJ9LHsibmFtZSI6InN5ZnQ6bG9jYXRpb246MDpsYXllcklEIiwidmFsdWUiOiJzaGEyNTY6NGZjMjQyZDU4Mjg1Njk5ZWNhMDVkYjNjYzdjNzEyMmEyYjhlMDE0ZDk0ODFmMzIzYmQ5Mjc3YmFhY2ZhMDYyOCJ9LHsibmFtZSI6InN5ZnQ6bG9jYXRpb246MDpwYXRoIiwidmFsdWUiOiIvbGliL2Fway9kYi9pbnN0YWxsZWQifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOmdpdENvbW1pdE9mQXBrUG9ydCIsInZhbHVlIjoiOTFjN2E5ZjNhYTI5NmI2ZDQ2MmM1NjM0ZTc2NThlYmRiZmY2NWJiOSJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6aW5zdGFsbGVkU2l6ZSIsInZhbHVlIjoiODYwMTYifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOm9yaWdpblBhY2thZ2UiLCJ2YWx1ZSI6ImxpYnJldGxzIn0seyJuYW1lIjoic3lmdDptZXRhZGF0YTpwdWxsQ2hlY2tzdW0iLCJ2YWx1ZSI6IlExWjkvdjVVVnNSUmtyWU5kcTNwakZBYkN1Z1U4PSJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6cHVsbERlcGVuZGVuY2llcyIsInZhbHVlIjoiY2EtY2VydGlmaWNhdGVzLWJ1bmRsZSBzbzpsaWJjLm11c2wteDg2XzY0LnNvLjEgc286bGliY3J5cHRvLnNvLjEuMSBzbzpsaWJzc2wuc28uMS4xIn0seyJuYW1lIjoic3lmdDptZXRhZGF0YTpzaXplIiwidmFsdWUiOiIyOTE4NSJ9XSwicHVibGlzaGVyIjoiQXJpYWRuZSBDb25pbGwgXHUwMDNjYXJpYWRuZUBkZXJlZmVyZW5jZWQub3JnXHUwMDNlIiwicHVybCI6InBrZzphbHBpbmUvbGlicmV0bHNAMy4zLjQtcjM/YXJjaD14ODZfNjRcdTAwMjZ1cHN0cmVhbT1saWJyZXRsc1x1MDAyNmRpc3Rybz1hbHBpbmUtMy4xNS40IiwidHlwZSI6ImxpYnJhcnkiLCJ2ZXJzaW9uIjoiMy4zLjQtcjMifSx7ImJvbS1yZWYiOiJwa2c6YWxwaW5lL2xpYnNzbDEuMUAxLjEuMW4tcjA/YXJjaD14ODZfNjRcdTAwMjZ1cHN0cmVhbT1vcGVuc3NsXHUwMDI2ZGlzdHJvPWFscGluZS0zLjE1LjRcdTAwMjZzeWZ0LWlkPTRiMTA2ZTZjM2ZkNzRjMWMiLCJjcGUiOiJjcGU6Mi4zOmE6bGlic3NsMS4xOmxpYnNzbDEuMToxLjEuMW4tcjA6KjoqOio6KjoqOio6KiIsImRlc2NyaXB0aW9uIjoiU1NMIHNoYXJlZCBsaWJyYXJpZXMiLCJleHRlcm5hbFJlZmVyZW5jZXMiOlt7InR5cGUiOiJkaXN0cmlidXRpb24iLCJ1cmwiOiJodHRwczovL3d3dy5vcGVuc3NsLm9yZy8ifV0sImxpY2Vuc2VzIjpbeyJsaWNlbnNlIjp7ImlkIjoiT3BlblNTTCJ9fV0sIm5hbWUiOiJsaWJzc2wxLjEiLCJwcm9wZXJ0aWVzIjpbeyJuYW1lIjoic3lmdDpwYWNrYWdlOmZvdW5kQnkiLCJ2YWx1ZSI6ImFwa2RiLWNhdGFsb2dlciJ9LHsibmFtZSI6InN5ZnQ6cGFja2FnZTptZXRhZGF0YVR5cGUiLCJ2YWx1ZSI6IkFwa01ldGFkYXRhIn0seyJuYW1lIjoic3lmdDpwYWNrYWdlOnR5cGUiLCJ2YWx1ZSI6ImFwayJ9LHsibmFtZSI6InN5ZnQ6bG9jYXRpb246MDpsYXllcklEIiwidmFsdWUiOiJzaGEyNTY6NGZjMjQyZDU4Mjg1Njk5ZWNhMDVkYjNjYzdjNzEyMmEyYjhlMDE0ZDk0ODFmMzIzYmQ5Mjc3YmFhY2ZhMDYyOCJ9LHsibmFtZSI6InN5ZnQ6bG9jYXRpb246MDpwYXRoIiwidmFsdWUiOiIvbGliL2Fway9kYi9pbnN0YWxsZWQifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOmdpdENvbW1pdE9mQXBrUG9ydCIsInZhbHVlIjoiNDU1ZTk2Njg5OWE5MzU4ZmM5NGY1YmNlNjMzYWZlOGExOTQyMDk1YyJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6aW5zdGFsbGVkU2l6ZSIsInZhbHVlIjoiNTQwNjcyIn0seyJuYW1lIjoic3lmdDptZXRhZGF0YTpvcmlnaW5QYWNrYWdlIiwidmFsdWUiOiJvcGVuc3NsIn0seyJuYW1lIjoic3lmdDptZXRhZGF0YTpwdWxsQ2hlY2tzdW0iLCJ2YWx1ZSI6IlExL0taMDBxREhXWjVjajNBV0cvRFBkQUNSTllJPSJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6cHVsbERlcGVuZGVuY2llcyIsInZhbHVlIjoic286bGliYy5tdXNsLXg4Nl82NC5zby4xIHNvOmxpYmNyeXB0by5zby4xLjEifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOnNpemUiLCJ2YWx1ZSI6IjIxMzIwOSJ9XSwicHVibGlzaGVyIjoiVGltbyBUZXJhcyBcdTAwM2N0aW1vLnRlcmFzQGlraS5maVx1MDAzZSIsInB1cmwiOiJwa2c6YWxwaW5lL2xpYnNzbDEuMUAxLjEuMW4tcjA/YXJjaD14ODZfNjRcdTAwMjZ1cHN0cmVhbT1vcGVuc3NsXHUwMDI2ZGlzdHJvPWFscGluZS0zLjE1LjQiLCJ0eXBlIjoibGlicmFyeSIsInZlcnNpb24iOiIxLjEuMW4tcjAifSx7ImJvbS1yZWYiOiJwa2c6YWxwaW5lL211c2xAMS4yLjItcjc/YXJjaD14ODZfNjRcdTAwMjZ1cHN0cmVhbT1tdXNsXHUwMDI2ZGlzdHJvPWFscGluZS0zLjE1LjRcdTAwMjZzeWZ0LWlkPTIwZGMyMGNiYjZkYmVhNiIsImNwZSI6ImNwZToyLjM6YTptdXNsOm11c2w6MS4yLjItcjc6KjoqOio6KjoqOio6KiIsImRlc2NyaXB0aW9uIjoidGhlIG11c2wgYyBsaWJyYXJ5IChsaWJjKSBpbXBsZW1lbnRhdGlvbiIsImV4dGVybmFsUmVmZXJlbmNlcyI6W3sidHlwZSI6ImRpc3RyaWJ1dGlvbiIsInVybCI6Imh0dHBzOi8vbXVzbC5saWJjLm9yZy8ifV0sImxpY2Vuc2VzIjpbeyJsaWNlbnNlIjp7ImlkIjoiTUlUIn19XSwibmFtZSI6Im11c2wiLCJwcm9wZXJ0aWVzIjpbeyJuYW1lIjoic3lmdDpwYWNrYWdlOmZvdW5kQnkiLCJ2YWx1ZSI6ImFwa2RiLWNhdGFsb2dlciJ9LHsibmFtZSI6InN5ZnQ6cGFja2FnZTptZXRhZGF0YVR5cGUiLCJ2YWx1ZSI6IkFwa01ldGFkYXRhIn0seyJuYW1lIjoic3lmdDpwYWNrYWdlOnR5cGUiLCJ2YWx1ZSI6ImFwayJ9LHsibmFtZSI6InN5ZnQ6bG9jYXRpb246MDpsYXllcklEIiwidmFsdWUiOiJzaGEyNTY6NGZjMjQyZDU4Mjg1Njk5ZWNhMDVkYjNjYzdjNzEyMmEyYjhlMDE0ZDk0ODFmMzIzYmQ5Mjc3YmFhY2ZhMDYyOCJ9LHsibmFtZSI6InN5ZnQ6bG9jYXRpb246MDpwYXRoIiwidmFsdWUiOiIvbGliL2Fway9kYi9pbnN0YWxsZWQifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOmdpdENvbW1pdE9mQXBrUG9ydCIsInZhbHVlIjoiYmY1YmJmZGJmNzgwMDkyZjM4N2I3YWJlNDAxZmJmY2VkYTkwYzg0ZCJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6aW5zdGFsbGVkU2l6ZSIsInZhbHVlIjoiNjIyNTkyIn0seyJuYW1lIjoic3lmdDptZXRhZGF0YTpvcmlnaW5QYWNrYWdlIiwidmFsdWUiOiJtdXNsIn0seyJuYW1lIjoic3lmdDptZXRhZGF0YTpwdWxsQ2hlY2tzdW0iLCJ2YWx1ZSI6IlExRGViMGpOeXRrcmpQVzROL2VLTFo0M0J3T2x3PSJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6c2l6ZSIsInZhbHVlIjoiMzgzMTUyIn1dLCJwdWJsaXNoZXIiOiJUaW1vIFRlcsOkcyBcdTAwM2N0aW1vLnRlcmFzQGlraS5maVx1MDAzZSIsInB1cmwiOiJwa2c6YWxwaW5lL211c2xAMS4yLjItcjc/YXJjaD14ODZfNjRcdTAwMjZ1cHN0cmVhbT1tdXNsXHUwMDI2ZGlzdHJvPWFscGluZS0zLjE1LjQiLCJ0eXBlIjoibGlicmFyeSIsInZlcnNpb24iOiIxLjIuMi1yNyJ9LHsiYm9tLXJlZiI6InBrZzphbHBpbmUvbXVzbC11dGlsc0AxLjIuMi1yNz9hcmNoPXg4Nl82NFx1MDAyNnVwc3RyZWFtPW11c2xcdTAwMjZkaXN0cm89YWxwaW5lLTMuMTUuNFx1MDAyNnN5ZnQtaWQ9MzVjMzY4MDU3N2ZhZTBkZiIsImNwZSI6ImNwZToyLjM6YTptdXNsLXV0aWxzOm11c2wtdXRpbHM6MS4yLjItcjc6KjoqOio6KjoqOio6KiIsImRlc2NyaXB0aW9uIjoidGhlIG11c2wgYyBsaWJyYXJ5IChsaWJjKSBpbXBsZW1lbnRhdGlvbiIsImV4dGVybmFsUmVmZXJlbmNlcyI6W3sidHlwZSI6ImRpc3RyaWJ1dGlvbiIsInVybCI6Imh0dHBzOi8vbXVzbC5saWJjLm9yZy8ifV0sImxpY2Vuc2VzIjpbeyJsaWNlbnNlIjp7ImlkIjoiTUlUIn19XSwibmFtZSI6Im11c2wtdXRpbHMiLCJwcm9wZXJ0aWVzIjpbeyJuYW1lIjoic3lmdDpwYWNrYWdlOmZvdW5kQnkiLCJ2YWx1ZSI6ImFwa2RiLWNhdGFsb2dlciJ9LHsibmFtZSI6InN5ZnQ6cGFja2FnZTptZXRhZGF0YVR5cGUiLCJ2YWx1ZSI6IkFwa01ldGFkYXRhIn0seyJuYW1lIjoic3lmdDpwYWNrYWdlOnR5cGUiLCJ2YWx1ZSI6ImFwayJ9LHsibmFtZSI6InN5ZnQ6Y3BlMjMiLCJ2YWx1ZSI6ImNwZToyLjM6YTptdXNsLXV0aWxzOm11c2xfdXRpbHM6MS4yLjItcjc6KjoqOio6KjoqOio6KiJ9LHsibmFtZSI6InN5ZnQ6Y3BlMjMiLCJ2YWx1ZSI6ImNwZToyLjM6YTptdXNsX3V0aWxzOm11c2wtdXRpbHM6MS4yLjItcjc6KjoqOio6KjoqOio6KiJ9LHsibmFtZSI6InN5ZnQ6Y3BlMjMiLCJ2YWx1ZSI6ImNwZToyLjM6YTptdXNsX3V0aWxzOm11c2xfdXRpbHM6MS4yLjItcjc6KjoqOio6KjoqOio6KiJ9LHsibmFtZSI6InN5ZnQ6Y3BlMjMiLCJ2YWx1ZSI6ImNwZToyLjM6YTptdXNsOm11c2wtdXRpbHM6MS4yLjItcjc6KjoqOio6KjoqOio6KiJ9LHsibmFtZSI6InN5ZnQ6Y3BlMjMiLCJ2YWx1ZSI6ImNwZToyLjM6YTptdXNsOm11c2xfdXRpbHM6MS4yLjItcjc6KjoqOio6KjoqOio6KiJ9LHsibmFtZSI6InN5ZnQ6bG9jYXRpb246MDpsYXllcklEIiwidmFsdWUiOiJzaGEyNTY6NGZjMjQyZDU4Mjg1Njk5ZWNhMDVkYjNjYzdjNzEyMmEyYjhlMDE0ZDk0ODFmMzIzYmQ5Mjc3YmFhY2ZhMDYyOCJ9LHsibmFtZSI6InN5ZnQ6bG9jYXRpb246MDpwYXRoIiwidmFsdWUiOiIvbGliL2Fway9kYi9pbnN0YWxsZWQifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOmdpdENvbW1pdE9mQXBrUG9ydCIsInZhbHVlIjoiYmY1YmJmZGJmNzgwMDkyZjM4N2I3YWJlNDAxZmJmY2VkYTkwYzg0ZCJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6aW5zdGFsbGVkU2l6ZSIsInZhbHVlIjoiMTQzMzYwIn0seyJuYW1lIjoic3lmdDptZXRhZGF0YTpvcmlnaW5QYWNrYWdlIiwidmFsdWUiOiJtdXNsIn0seyJuYW1lIjoic3lmdDptZXRhZGF0YTpwdWxsQ2hlY2tzdW0iLCJ2YWx1ZSI6IlExUDUwY2ZKaVNzSG9xc1lSVHlPRU9sSmlMbjNvPSJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6cHVsbERlcGVuZGVuY2llcyIsInZhbHVlIjoic2NhbmVsZiBzbzpsaWJjLm11c2wteDg2XzY0LnNvLjEifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOnNpemUiLCJ2YWx1ZSI6IjM2NzIzIn1dLCJwdWJsaXNoZXIiOiJUaW1vIFRlcsOkcyBcdTAwM2N0aW1vLnRlcmFzQGlraS5maVx1MDAzZSIsInB1cmwiOiJwa2c6YWxwaW5lL211c2wtdXRpbHNAMS4yLjItcjc/YXJjaD14ODZfNjRcdTAwMjZ1cHN0cmVhbT1tdXNsXHUwMDI2ZGlzdHJvPWFscGluZS0zLjE1LjQiLCJ0eXBlIjoibGlicmFyeSIsInZlcnNpb24iOiIxLjIuMi1yNyJ9LHsiYm9tLXJlZiI6InBrZzphbHBpbmUvc2NhbmVsZkAxLjMuMy1yMD9hcmNoPXg4Nl82NFx1MDAyNnVwc3RyZWFtPXBheC11dGlsc1x1MDAyNmRpc3Rybz1hbHBpbmUtMy4xNS40XHUwMDI2c3lmdC1pZD1mMmQ0MjYzNzIzNTY2MDJkIiwiY3BlIjoiY3BlOjIuMzphOnNjYW5lbGY6c2NhbmVsZjoxLjMuMy1yMDoqOio6KjoqOio6KjoqIiwiZGVzY3JpcHRpb24iOiJTY2FuIEVMRiBiaW5hcmllcyBmb3Igc3R1ZmYiLCJleHRlcm5hbFJlZmVyZW5jZXMiOlt7InR5cGUiOiJkaXN0cmlidXRpb24iLCJ1cmwiOiJodHRwczovL3dpa2kuZ2VudG9vLm9yZy93aWtpL0hhcmRlbmVkL1BhWF9VdGlsaXRpZXMifV0sImxpY2Vuc2VzIjpbeyJsaWNlbnNlIjp7ImlkIjoiR1BMLTIuMC1vbmx5In19XSwibmFtZSI6InNjYW5lbGYiLCJwcm9wZXJ0aWVzIjpbeyJuYW1lIjoic3lmdDpwYWNrYWdlOmZvdW5kQnkiLCJ2YWx1ZSI6ImFwa2RiLWNhdGFsb2dlciJ9LHsibmFtZSI6InN5ZnQ6cGFja2FnZTptZXRhZGF0YVR5cGUiLCJ2YWx1ZSI6IkFwa01ldGFkYXRhIn0seyJuYW1lIjoic3lmdDpwYWNrYWdlOnR5cGUiLCJ2YWx1ZSI6ImFwayJ9LHsibmFtZSI6InN5ZnQ6bG9jYXRpb246MDpsYXllcklEIiwidmFsdWUiOiJzaGEyNTY6NGZjMjQyZDU4Mjg1Njk5ZWNhMDVkYjNjYzdjNzEyMmEyYjhlMDE0ZDk0ODFmMzIzYmQ5Mjc3YmFhY2ZhMDYyOCJ9LHsibmFtZSI6InN5ZnQ6bG9jYXRpb246MDpwYXRoIiwidmFsdWUiOiIvbGliL2Fway9kYi9pbnN0YWxsZWQifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOmdpdENvbW1pdE9mQXBrUG9ydCIsInZhbHVlIjoiODZiM2Q0ZmJiMGE3NjBmZWJmMzQ3NmY5YTU4YWJmOGQwZjcyOGQ1YyJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6aW5zdGFsbGVkU2l6ZSIsInZhbHVlIjoiOTQyMDgifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOm9yaWdpblBhY2thZ2UiLCJ2YWx1ZSI6InBheC11dGlscyJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6cHVsbENoZWNrc3VtIiwidmFsdWUiOiJRMTEvZFpEa1VJY0tUM2xuSENOcHN4dGJzSE5Kbz0ifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOnB1bGxEZXBlbmRlbmNpZXMiLCJ2YWx1ZSI6InNvOmxpYmMubXVzbC14ODZfNjQuc28uMSJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6c2l6ZSIsInZhbHVlIjoiMzY4MzAifV0sInB1Ymxpc2hlciI6Ik5hdGFuYWVsIENvcGEgXHUwMDNjbmNvcGFAYWxwaW5lbGludXgub3JnXHUwMDNlIiwicHVybCI6InBrZzphbHBpbmUvc2NhbmVsZkAxLjMuMy1yMD9hcmNoPXg4Nl82NFx1MDAyNnVwc3RyZWFtPXBheC11dGlsc1x1MDAyNmRpc3Rybz1hbHBpbmUtMy4xNS40IiwidHlwZSI6ImxpYnJhcnkiLCJ2ZXJzaW9uIjoiMS4zLjMtcjAifSx7ImJvbS1yZWYiOiJwa2c6YWxwaW5lL3NzbF9jbGllbnRAMS4zNC4xLXI1P2FyY2g9eDg2XzY0XHUwMDI2dXBzdHJlYW09YnVzeWJveFx1MDAyNmRpc3Rybz1hbHBpbmUtMy4xNS40XHUwMDI2c3lmdC1pZD0xZTE4MTJkZWI2NjY5MmM1IiwiY3BlIjoiY3BlOjIuMzphOnNzbC1jbGllbnQ6c3NsLWNsaWVudDoxLjM0LjEtcjU6KjoqOio6KjoqOio6KiIsImRlc2NyaXB0aW9uIjoiRVh0ZXJuYWwgc3NsX2NsaWVudCBmb3IgYnVzeWJveCB3Z2V0IiwiZXh0ZXJuYWxSZWZlcmVuY2VzIjpbeyJ0eXBlIjoiZGlzdHJpYnV0aW9uIiwidXJsIjoiaHR0cHM6Ly9idXN5Ym94Lm5ldC8ifV0sImxpY2Vuc2VzIjpbeyJsaWNlbnNlIjp7ImlkIjoiR1BMLTIuMC1vbmx5In19XSwibmFtZSI6InNzbF9jbGllbnQiLCJwcm9wZXJ0aWVzIjpbeyJuYW1lIjoic3lmdDpwYWNrYWdlOmZvdW5kQnkiLCJ2YWx1ZSI6ImFwa2RiLWNhdGFsb2dlciJ9LHsibmFtZSI6InN5ZnQ6cGFja2FnZTptZXRhZGF0YVR5cGUiLCJ2YWx1ZSI6IkFwa01ldGFkYXRhIn0seyJuYW1lIjoic3lmdDpwYWNrYWdlOnR5cGUiLCJ2YWx1ZSI6ImFwayJ9LHsibmFtZSI6InN5ZnQ6Y3BlMjMiLCJ2YWx1ZSI6ImNwZToyLjM6YTpzc2wtY2xpZW50OnNzbF9jbGllbnQ6MS4zNC4xLXI1Oio6KjoqOio6KjoqOioifSx7Im5hbWUiOiJzeWZ0OmNwZTIzIiwidmFsdWUiOiJjcGU6Mi4zOmE6c3NsX2NsaWVudDpzc2wtY2xpZW50OjEuMzQuMS1yNToqOio6KjoqOio6KjoqIn0seyJuYW1lIjoic3lmdDpjcGUyMyIsInZhbHVlIjoiY3BlOjIuMzphOnNzbF9jbGllbnQ6c3NsX2NsaWVudDoxLjM0LjEtcjU6KjoqOio6KjoqOio6KiJ9LHsibmFtZSI6InN5ZnQ6Y3BlMjMiLCJ2YWx1ZSI6ImNwZToyLjM6YTpzc2w6c3NsLWNsaWVudDoxLjM0LjEtcjU6KjoqOio6KjoqOio6KiJ9LHsibmFtZSI6InN5ZnQ6Y3BlMjMiLCJ2YWx1ZSI6ImNwZToyLjM6YTpzc2w6c3NsX2NsaWVudDoxLjM0LjEtcjU6KjoqOio6KjoqOio6KiJ9LHsibmFtZSI6InN5ZnQ6bG9jYXRpb246MDpsYXllcklEIiwidmFsdWUiOiJzaGEyNTY6NGZjMjQyZDU4Mjg1Njk5ZWNhMDVkYjNjYzdjNzEyMmEyYjhlMDE0ZDk0ODFmMzIzYmQ5Mjc3YmFhY2ZhMDYyOCJ9LHsibmFtZSI6InN5ZnQ6bG9jYXRpb246MDpwYXRoIiwidmFsdWUiOiIvbGliL2Fway9kYi9pbnN0YWxsZWQifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOmdpdENvbW1pdE9mQXBrUG9ydCIsInZhbHVlIjoiMjc0NWRlN2UxYjA5ZTY2M2I0NzdhODE0MWI4NGY3ZDgxYTA0OTk2MyJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6aW5zdGFsbGVkU2l6ZSIsInZhbHVlIjoiMjg2NzIifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOm9yaWdpblBhY2thZ2UiLCJ2YWx1ZSI6ImJ1c3lib3gifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOnB1bGxDaGVja3N1bSIsInZhbHVlIjoiUTFMUXBhVFlaVHpSL2tnS3ZSd0x1WThkRUZZUDQ9In0seyJuYW1lIjoic3lmdDptZXRhZGF0YTpwdWxsRGVwZW5kZW5jaWVzIiwidmFsdWUiOiJzbzpsaWJjLm11c2wteDg2XzY0LnNvLjEgc286bGlidGxzLnNvLjIifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOnNpemUiLCJ2YWx1ZSI6IjQ3MTYifV0sInB1Ymxpc2hlciI6Ik5hdGFuYWVsIENvcGEgXHUwMDNjbmNvcGFAYWxwaW5lbGludXgub3JnXHUwMDNlIiwicHVybCI6InBrZzphbHBpbmUvc3NsX2NsaWVudEAxLjM0LjEtcjU/YXJjaD14ODZfNjRcdTAwMjZ1cHN0cmVhbT1idXN5Ym94XHUwMDI2ZGlzdHJvPWFscGluZS0zLjE1LjQiLCJ0eXBlIjoibGlicmFyeSIsInZlcnNpb24iOiIxLjM0LjEtcjUifSx7ImJvbS1yZWYiOiJwa2c6YWxwaW5lL3psaWJAMS4yLjEyLXIwP2FyY2g9eDg2XzY0XHUwMDI2dXBzdHJlYW09emxpYlx1MDAyNmRpc3Rybz1hbHBpbmUtMy4xNS40XHUwMDI2c3lmdC1pZD1iMzk5MDhlNzI5NzQ2MDkiLCJjcGUiOiJjcGU6Mi4zOmE6emxpYjp6bGliOjEuMi4xMi1yMDoqOio6KjoqOio6KjoqIiwiZGVzY3JpcHRpb24iOiJBIGNvbXByZXNzaW9uL2RlY29tcHJlc3Npb24gTGlicmFyeSIsImV4dGVybmFsUmVmZXJlbmNlcyI6W3sidHlwZSI6ImRpc3RyaWJ1dGlvbiIsInVybCI6Imh0dHBzOi8vemxpYi5uZXQvIn1dLCJsaWNlbnNlcyI6W3sibGljZW5zZSI6eyJpZCI6IlpsaWIifX1dLCJuYW1lIjoiemxpYiIsInByb3BlcnRpZXMiOlt7Im5hbWUiOiJzeWZ0OnBhY2thZ2U6Zm91bmRCeSIsInZhbHVlIjoiYXBrZGItY2F0YWxvZ2VyIn0seyJuYW1lIjoic3lmdDpwYWNrYWdlOm1ldGFkYXRhVHlwZSIsInZhbHVlIjoiQXBrTWV0YWRhdGEifSx7Im5hbWUiOiJzeWZ0OnBhY2thZ2U6dHlwZSIsInZhbHVlIjoiYXBrIn0seyJuYW1lIjoic3lmdDpsb2NhdGlvbjowOmxheWVySUQiLCJ2YWx1ZSI6InNoYTI1Njo0ZmMyNDJkNTgyODU2OTllY2EwNWRiM2NjN2M3MTIyYTJiOGUwMTRkOTQ4MWYzMjNiZDkyNzdiYWFjZmEwNjI4In0seyJuYW1lIjoic3lmdDpsb2NhdGlvbjowOnBhdGgiLCJ2YWx1ZSI6Ii9saWIvYXBrL2RiL2luc3RhbGxlZCJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6Z2l0Q29tbWl0T2ZBcGtQb3J0IiwidmFsdWUiOiI3NDE0ODgwODY3OWY0N2FkOTZkYzk5ZTgzZWY3M2FjZmRlZWMxNjQyIn0seyJuYW1lIjoic3lmdDptZXRhZGF0YTppbnN0YWxsZWRTaXplIiwidmFsdWUiOiIxMTA1OTIifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOm9yaWdpblBhY2thZ2UiLCJ2YWx1ZSI6InpsaWIifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOnB1bGxDaGVja3N1bSIsInZhbHVlIjoiUTFIa3AyekgyYXlBV25RNzNrMFJkMjcwY1FCQjQ9In0seyJuYW1lIjoic3lmdDptZXRhZGF0YTpwdWxsRGVwZW5kZW5jaWVzIiwidmFsdWUiOiJzbzpsaWJjLm11c2wteDg2XzY0LnNvLjEifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOnNpemUiLCJ2YWx1ZSI6IjUzNDg4In1dLCJwdWJsaXNoZXIiOiJOYXRhbmFlbCBDb3BhIFx1MDAzY25jb3BhQGFscGluZWxpbnV4Lm9yZ1x1MDAzZSIsInB1cmwiOiJwa2c6YWxwaW5lL3psaWJAMS4yLjEyLXIwP2FyY2g9eDg2XzY0XHUwMDI2dXBzdHJlYW09emxpYlx1MDAyNmRpc3Rybz1hbHBpbmUtMy4xNS40IiwidHlwZSI6ImxpYnJhcnkiLCJ2ZXJzaW9uIjoiMS4yLjEyLXIwIn0seyJkZXNjcmlwdGlvbiI6IkFscGluZSBMaW51eCB2My4xNSIsImV4dGVybmFsUmVmZXJlbmNlcyI6W3sidHlwZSI6Imlzc3VlLXRyYWNrZXIiLCJ1cmwiOiJodHRwczovL2J1Z3MuYWxwaW5lbGludXgub3JnLyJ9LHsidHlwZSI6IndlYnNpdGUiLCJ1cmwiOiJodHRwczovL2FscGluZWxpbnV4Lm9yZy8ifV0sIm5hbWUiOiJhbHBpbmUiLCJwcm9wZXJ0aWVzIjpbeyJuYW1lIjoic3lmdDpkaXN0cm86aWQiLCJ2YWx1ZSI6ImFscGluZSJ9LHsibmFtZSI6InN5ZnQ6ZGlzdHJvOnByZXR0eU5hbWUiLCJ2YWx1ZSI6IkFscGluZSBMaW51eCB2My4xNSJ9LHsibmFtZSI6InN5ZnQ6ZGlzdHJvOnZlcnNpb25JRCIsInZhbHVlIjoiMy4xNS40In1dLCJzd2lkIjp7Im5hbWUiOiJhbHBpbmUiLCJ0YWdJZCI6ImFscGluZSIsInZlcnNpb24iOiIzLjE1LjQifSwidHlwZSI6Im9wZXJhdGluZy1zeXN0ZW0iLCJ2ZXJzaW9uIjoiMy4xNS40In1dLCJtZXRhZGF0YSI6eyJjb21wb25lbnQiOnsiYm9tLXJlZiI6ImZjNjM4NjY1ZDQwNDdlYTEiLCJuYW1lIjoiYWxwaW5lOmxhdGVzdCIsInR5cGUiOiJjb250YWluZXIiLCJ2ZXJzaW9uIjoic2hhMjU2OmE3NzdjOWM2NmJhMTc3Y2NmZWEyM2YyYTIxNmZmNjcyMWU3OGE2NjJjZDE3MDE5NDg4YzQxNzEzNTI5OWNkODkifSwidGltZXN0YW1wIjoiMjAyMi0wNC0xMVQxNzoxMTo0MS0wNzowMCIsInRvb2xzIjpbeyJuYW1lIjoic3lmdCIsInZlbmRvciI6ImFuY2hvcmUiLCJ2ZXJzaW9uIjoiMC40My4yIn1dfSwic2VyaWFsTnVtYmVyIjoidXJuOnV1aWQ6ZWQ5MjQyNmYtYTkxNi00NDljLTgzYjgtZDcwODYwMTM3NzczIiwic3BlY1ZlcnNpb24iOiIxLjQiLCJ2ZXJzaW9uIjoxfX0=\",\"signatures\":[{\"keyid\":\"\",\"sig\":\"MEQCIDq1ltJFSy/cIzUSQmnqcP4ttqBY6vc92Nld45QY12GoAiBbjJQmixgSc6Hm5fClMXZnoyWbZJjKouQDzX5bvtWd0w==\"}]}"
  },
  {
    "path": "grype/pkg/testdata/alpine.att.json",
    "content": "{\"payloadType\":\"application/vnd.in-toto+json\",\"payload\":\"eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjAuMSIsInByZWRpY2F0ZVR5cGUiOiJodHRwczovL3N5ZnQuZGV2L2JvbSIsInN1YmplY3QiOlt7Im5hbWUiOiIiLCJkaWdlc3QiOnsic2hhMjU2IjoiNmFmMWIxMWJiYjE3ZjRjMzExZTI2OWRiNjUzMGU0ZGEyNzM4MjYyYWY1ZmQ5MDY0Y2NkZjEwOWI3NjU4NjBmYiJ9fV0sInByZWRpY2F0ZSI6eyJhcnRpZmFjdFJlbGF0aW9uc2hpcHMiOlt7ImNoaWxkIjoiNGFjYTZkMTVkZjA5ZGExZCIsInBhcmVudCI6IjRhYzcxMzZiODUzNmNkZWEiLCJ0eXBlIjoiY29udGFpbnMifSx7ImNoaWxkIjoiNGFjYTZkMTVkZjA5ZGExZCIsInBhcmVudCI6IjRhYzcxMzZiODUzNmNkZWEiLCJ0eXBlIjoiY29udGFpbnMifSx7ImNoaWxkIjoiNmM3MzE0NGVhOWVmNGZiOSIsInBhcmVudCI6ImYyMTMyZThkNmNmZTAwNmEiLCJ0eXBlIjoiY29udGFpbnMifSx7ImNoaWxkIjoiNmM3MzE0NGVhOWVmNGZiOSIsInBhcmVudCI6ImYyMTMyZThkNmNmZTAwNmEiLCJ0eXBlIjoiY29udGFpbnMifSx7ImNoaWxkIjoiOWZhOTRlYThlODc0ZWU0OSIsInBhcmVudCI6ImYyMTMyZThkNmNmZTAwNmEiLCJ0eXBlIjoiY29udGFpbnMifSx7ImNoaWxkIjoiNTU5YTlkNTFlMzNkMjQxMCIsInBhcmVudCI6ImYyMTMyZThkNmNmZTAwNmEiLCJ0eXBlIjoiY29udGFpbnMifSx7ImNoaWxkIjoiZDU3ZmUwZGU3OTBmY2Q0YSIsInBhcmVudCI6ImYyMTMyZThkNmNmZTAwNmEiLCJ0eXBlIjoiY29udGFpbnMifSx7ImNoaWxkIjoiNmM2Zjk4Yzg1N2E2OTFjYyIsInBhcmVudCI6ImYyMTMyZThkNmNmZTAwNmEiLCJ0eXBlIjoiY29udGFpbnMifSx7ImNoaWxkIjoiYmIwODE4ZTZjZDI1Zjk1YSIsInBhcmVudCI6ImYyMTMyZThkNmNmZTAwNmEiLCJ0eXBlIjoiY29udGFpbnMifSx7ImNoaWxkIjoiMjgxNGM4ZjJkYWUyZTJlYSIsInBhcmVudCI6IjdmMDI0MWE3MGI2ODE4MTkiLCJ0eXBlIjoiY29udGFpbnMifSx7ImNoaWxkIjoiYzc5NWQzZjdlYmQ2NGU4NCIsInBhcmVudCI6IjdmMDI0MWE3MGI2ODE4MTkiLCJ0eXBlIjoiY29udGFpbnMifSx7ImNoaWxkIjoiYjE3ZTBmZWQzY2I3MDJhZCIsInBhcmVudCI6IjdmMDI0MWE3MGI2ODE4MTkiLCJ0eXBlIjoiY29udGFpbnMifSx7ImNoaWxkIjoiZGYyMmVkMWEzMWZkYmI2OCIsInBhcmVudCI6IjdmMDI0MWE3MGI2ODE4MTkiLCJ0eXBlIjoiY29udGFpbnMifSx7ImNoaWxkIjoiZTg1YWYxNGQ1MDMyNTc0NCIsInBhcmVudCI6IjdmMDI0MWE3MGI2ODE4MTkiLCJ0eXBlIjoiY29udGFpbnMifSx7ImNoaWxkIjoiZTkyNDY5NjBmZDQ4ZjI3MCIsInBhcmVudCI6IjdmMDI0MWE3MGI2ODE4MTkiLCJ0eXBlIjoiY29udGFpbnMifSx7ImNoaWxkIjoiZDFkMjNhNDczMTE5N2Q1ZSIsInBhcmVudCI6IjdmMDI0MWE3MGI2ODE4MTkiLCJ0eXBlIjoiY29udGFpbnMifSx7ImNoaWxkIjoiZGJlNzE2ZTA1ZjkxMWZmOSIsInBhcmVudCI6IjdmMDI0MWE3MGI2ODE4MTkiLCJ0eXBlIjoiY29udGFpbnMifSx7ImNoaWxkIjoiNmVkYzk3MGYwMjQ2OWRlYSIsInBhcmVudCI6IjdmMDI0MWE3MGI2ODE4MTkiLCJ0eXBlIjoiY29udGFpbnMifSx7ImNoaWxkIjoiOWEzNTc2ZTRhYWRiODJmMSIsInBhcmVudCI6IjdmMDI0MWE3MGI2ODE4MTkiLCJ0eXBlIjoiY29udGFpbnMifSx7ImNoaWxkIjoiNWEyNWQwZGJlODBhYjI0IiwicGFyZW50IjoiN2YwMjQxYTcwYjY4MTgxOSIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI1NTU4Zjc2MDM4ZDFjOTdlIiwicGFyZW50IjoiN2YwMjQxYTcwYjY4MTgxOSIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJhNzM5YTA0ZThjMmQ4ODk0IiwicGFyZW50IjoiN2YwMjQxYTcwYjY4MTgxOSIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI4ZGIxNjIwNmIzMzI2MDZlIiwicGFyZW50IjoiN2YwMjQxYTcwYjY4MTgxOSIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiIzODcxMmFkZWFlZGI3MzE2IiwicGFyZW50IjoiN2YwMjQxYTcwYjY4MTgxOSIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiIxNGRkOTg1ZGNlNGNkNWE1IiwicGFyZW50IjoiN2YwMjQxYTcwYjY4MTgxOSIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI1OTBjYzI2MzU4N2UyYzlkIiwicGFyZW50IjoiN2YwMjQxYTcwYjY4MTgxOSIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiIyNmEyMGYxMDZmYTMyNzgzIiwicGFyZW50IjoiN2YwMjQxYTcwYjY4MTgxOSIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJmMjlhZjI0NzU1MGRjYWM4IiwicGFyZW50IjoiN2YwMjQxYTcwYjY4MTgxOSIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI0YTZkNmUwZWE5NTc1MWI3IiwicGFyZW50IjoiN2YwMjQxYTcwYjY4MTgxOSIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiIxY2UxMDM1ZDAwMjE3NmE0IiwicGFyZW50IjoiN2YwMjQxYTcwYjY4MTgxOSIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJiOGYxZTRhNjJiMjE2NjNiIiwicGFyZW50IjoiN2YwMjQxYTcwYjY4MTgxOSIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI0NTZhNzAyZjE2NzMzYjI0IiwicGFyZW50IjoiN2YwMjQxYTcwYjY4MTgxOSIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiIxNGUzZjQxYTc3YjQyODA3IiwicGFyZW50IjoiN2YwMjQxYTcwYjY4MTgxOSIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJlNDFkYWQ4YTYxZmY4M2I2IiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJjOWJlYzUzYzBhM2UyMTA2IiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJhNWRmZDI5MTQ4MWM2OWUwIiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJjMTVmOTJkZGM3N2MxYWU0IiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJkMGMyNDUzZjk4YjMxMWQ4IiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI5MmQzNThlNzI1NmYwYjdhIiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiIyOTVjMjYzYjNkMWM4MWQyIiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJhMmJmNDE0MDJjYzA0MzE1IiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJkYjQ2MmJkNGNlOWJhZWM1IiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJjNWYxMjYyYTY2YmYwNDhhIiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJlZTNhZDljMGE0ZjU0ZTkzIiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI5MmQzNThlNzI1NmYwYjdhIiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI1OGZiZWJhNzc0ZWIyYTU2IiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJmYzgyODFmM2JmZDU3MWU1IiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJkYzFlMDQzYzY1MWRmYjRjIiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI3NmI1YWE4MDVkYzFiMThmIiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJhMDBmNzE2ODE5ZmFhODE5IiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJhM2U3NmYyMTkzM2I1MWM0IiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiIxMWQ0ZTc2OGMyZWI3NjJkIiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI1MjJhNjAyZTljMTAwZTYyIiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJhYzdhMGYyMWRlYzAzYTFhIiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiIzOTExNDVmMGVmZDM5Yjk0IiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiIyOTVjMjYzYjNkMWM4MWQyIiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI0YTk2ZjhjNGNmMzQ2NzU5IiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJjNWYxMjYyYTY2YmYwNDhhIiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiIxMWQ0ZTc2OGMyZWI3NjJkIiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJjNWYxMjYyYTY2YmYwNDhhIiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiIzOTExNDVmMGVmZDM5Yjk0IiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJkYzFlMDQzYzY1MWRmYjRjIiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI1OGZiZWJhNzc0ZWIyYTU2IiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI1MjJhNjAyZTljMTAwZTYyIiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI3NmI1YWE4MDVkYzFiMThmIiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI0YTk2ZjhjNGNmMzQ2NzU5IiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJmYzgyODFmM2JmZDU3MWU1IiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJhYzdhMGYyMWRlYzAzYTFhIiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJhMmJmNDE0MDJjYzA0MzE1IiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJkYjQ2MmJkNGNlOWJhZWM1IiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJhM2U3NmYyMTkzM2I1MWM0IiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJhMmJmNDE0MDJjYzA0MzE1IiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJlZTNhZDljMGE0ZjU0ZTkzIiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJhMDBmNzE2ODE5ZmFhODE5IiwicGFyZW50IjoiMTk5N2RkMWM4ZmY3MmRjMiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI1YTg0NzZiZDZmNWExM2JmIiwicGFyZW50IjoiOGNlYjI3YTEyYzBiZmU3YiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI1YTg0NzZiZDZmNWExM2JmIiwicGFyZW50IjoiOGNlYjI3YTEyYzBiZmU3YiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI1YTg0NzZiZDZmNWExM2JmIiwicGFyZW50IjoiN2EyY2Y3MjdjYmFiODA3NCIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJiYzY2MzYyYjI1YzY0MzRhIiwicGFyZW50IjoiN2EyY2Y3MjdjYmFiODA3NCIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJkYzVjYmI3OGNjNzQwZmJlIiwicGFyZW50IjoiN2EyY2Y3MjdjYmFiODA3NCIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJkZWMyMzliNTY1ZDE5YjY0IiwicGFyZW50IjoiN2EyY2Y3MjdjYmFiODA3NCIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJiYTFhZDE1Mzk0YzUzOTIwIiwicGFyZW50IjoiN2EyY2Y3MjdjYmFiODA3NCIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJlNjA1NDJmYTMzZGFlYjA1IiwicGFyZW50IjoiN2EyY2Y3MjdjYmFiODA3NCIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiIyZDI0YzQwYTg4YjRjMmVjIiwicGFyZW50IjoiN2EyY2Y3MjdjYmFiODA3NCIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI1ZGIzOGRkY2U4OTEwNWU3IiwicGFyZW50IjoiN2EyY2Y3MjdjYmFiODA3NCIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiJiYTFhZDE1Mzk0YzUzOTIwIiwicGFyZW50IjoiN2EyY2Y3MjdjYmFiODA3NCIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI2YjFjOWExOGNhYmFmM2VkIiwicGFyZW50IjoiMzA5NGM0YTYxMGIwYjAwZCIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI2YjFjOWExOGNhYmFmM2VkIiwicGFyZW50IjoiMzA5NGM0YTYxMGIwYjAwZCIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiIyYjAyZjY3OTBjNmVmN2U4IiwicGFyZW50IjoiOTFkYjE1ZDgwNGZlZGU1OSIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiIyYjAyZjY3OTBjNmVmN2U4IiwicGFyZW50IjoiOTFkYjE1ZDgwNGZlZGU1OSIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI5MTUwY2Y2ZGYxYWQ1Zjg2IiwicGFyZW50IjoiYzJlM2NlN2I5ZTcyZDBhZSIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiIxM2ExYWJkMWQyZDM0YTU3IiwicGFyZW50IjoiYmUwNmRmNmUzYmJiZjBhYiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiIxM2ExYWJkMWQyZDM0YTU3IiwicGFyZW50IjoiYmUwNmRmNmUzYmJiZjBhYiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiI4NTI0ZGNmMDI5MzZkODE3IiwicGFyZW50IjoiNWVmNjZhMzM1ZGRjMDNhNiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiIyYzYyNGZiZjM5Y2E3ZDY0IiwicGFyZW50IjoiNWVmNjZhMzM1ZGRjMDNhNiIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiIyYWY0MTJiMTBmZjJjNWZiIiwicGFyZW50IjoiMWYyOGRlMTIwMDczZDc4YSIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiIyMzcwMjZkMTM0Yjg0ZDZmIiwicGFyZW50IjoiNTNhOTA5ZjRkODcyYjkwIiwidHlwZSI6ImNvbnRhaW5zIn0seyJjaGlsZCI6IjNjYTdjYmZkNTkzMGQ5MGIiLCJwYXJlbnQiOiI1M2E5MDlmNGQ4NzJiOTAiLCJ0eXBlIjoiY29udGFpbnMifSx7ImNoaWxkIjoiOWFjYzQ2ZjlmOWQyNDA1OCIsInBhcmVudCI6IjUzYTkwOWY0ZDg3MmI5MCIsInR5cGUiOiJjb250YWlucyJ9LHsiY2hpbGQiOiIxNjJmMzI0MDM2NTYxNjQzIiwicGFyZW50IjoiNTNhOTA5ZjRkODcyYjkwIiwidHlwZSI6ImNvbnRhaW5zIn0seyJjaGlsZCI6ImEyOWI0NWI1YzAyZDAwMDciLCJwYXJlbnQiOiI1M2E5MDlmNGQ4NzJiOTAiLCJ0eXBlIjoiY29udGFpbnMifV0sImFydGlmYWN0cyI6W3siY3BlcyI6WyJjcGU6Mi4zOmE6YWxwaW5lLWJhc2VsYXlvdXQ6YWxwaW5lLWJhc2VsYXlvdXQ6My4yLjAtcjE4Oio6KjoqOio6KjoqOioiLCJjcGU6Mi4zOmE6YWxwaW5lLWJhc2VsYXlvdXQ6YWxwaW5lX2Jhc2VsYXlvdXQ6My4yLjAtcjE4Oio6KjoqOio6KjoqOioiLCJjcGU6Mi4zOmE6YWxwaW5lX2Jhc2VsYXlvdXQ6YWxwaW5lLWJhc2VsYXlvdXQ6My4yLjAtcjE4Oio6KjoqOio6KjoqOioiLCJjcGU6Mi4zOmE6YWxwaW5lX2Jhc2VsYXlvdXQ6YWxwaW5lX2Jhc2VsYXlvdXQ6My4yLjAtcjE4Oio6KjoqOio6KjoqOioiLCJjcGU6Mi4zOmE6YWxwaW5lOmFscGluZS1iYXNlbGF5b3V0OjMuMi4wLXIxODoqOio6KjoqOio6KjoqIiwiY3BlOjIuMzphOmFscGluZTphbHBpbmVfYmFzZWxheW91dDozLjIuMC1yMTg6KjoqOio6KjoqOio6KiJdLCJmb3VuZEJ5IjoiYXBrZGItY2F0YWxvZ2VyIiwiaWQiOiI3ZjAyNDFhNzBiNjgxODE5IiwibGFuZ3VhZ2UiOiIiLCJsaWNlbnNlcyI6WyJHUEwtMi4wLW9ubHkiXSwibG9jYXRpb25zIjpbeyJsYXllcklEIjoic2hhMjU2OmZmNzY4YTE0MTNiYTEwOTMwOGMwZDg5N2ZmYTU1NWUwNTJhNDZkMmNmNDcxMTc4ZjAwMWI4MjViNGMyMWYzNTQiLCJwYXRoIjoiL2xpYi9hcGsvZGIvaW5zdGFsbGVkIn1dLCJtZXRhZGF0YSI6eyJhcmNoaXRlY3R1cmUiOiJ4ODZfNjQiLCJkZXNjcmlwdGlvbiI6IkFscGluZSBiYXNlIGRpciBzdHJ1Y3R1cmUgYW5kIGluaXQgc2NyaXB0cyIsImZpbGVzIjpbeyJwYXRoIjoiL2RldiJ9LHsicGF0aCI6Ii9kZXYvcHRzIn0seyJwYXRoIjoiL2Rldi9zaG0ifSx7InBhdGgiOiIvZXRjIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTExUTdoTmU4UXBEUzUzMWd1cUNkclhCem9BL289In0sInBhdGgiOiIvZXRjL2ZzdGFiIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTEzSytvbEpnNWF5ekhTVk5Va2dnWkpYdUIrOVk9In0sInBhdGgiOiIvZXRjL2dyb3VwIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTE2blZ3WVZYUC90Q2h2VVBkdWtWRDJpZlhPbWM9In0sInBhdGgiOiIvZXRjL2hvc3RuYW1lIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTFCRDZ6SktaVFJXeXFHblBpNHRTZmQza3JzTVU9In0sInBhdGgiOiIvZXRjL2hvc3RzIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTFUc3RoYmhXN1F6V1JlMUUvTkt3VE91RDRwSGM9In0sInBhdGgiOiIvZXRjL2luaXR0YWIifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMXRvb2dqVWlwSEdjTWdFQ2dQSlg2NFN3VVQxTT0ifSwicGF0aCI6Ii9ldGMvbW9kdWxlcyJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExWG1kdVZWTlVSSFEyN1R2WXAxTHI1VE10RmNBPSJ9LCJwYXRoIjoiL2V0Yy9tb3RkIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTFraWxqaFhYSDFMbFFyb0hzRUpJa1BaZzJlaXc9In0sIm93bmVyR2lkIjoiMCIsIm93bmVyVWlkIjoiMCIsInBhdGgiOiIvZXRjL210YWIiLCJwZXJtaXNzaW9ucyI6Ijc3NyJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExVGNodXVMVWZ1cjBpenZmWlFaeGdOL0xKaEI4PSJ9LCJwYXRoIjoiL2V0Yy9wYXNzd2QifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMVZtSFBXUGpqdno0b0NzYm1ZQ1VCNHVXcFNrYz0ifSwicGF0aCI6Ii9ldGMvcHJvZmlsZSJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExb21LbHAzdmdHcTJacVl6eUQvS0hOZG84ckRjPSJ9LCJwYXRoIjoiL2V0Yy9wcm90b2NvbHMifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMTlXTEN2NUl0S2c0TUg3UldmTlJoMUk3YnlRYz0ifSwicGF0aCI6Ii9ldGMvc2VydmljZXMifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMWx0clBJQVcyekhlRGlhanNleDJCZG1xM3VxQT0ifSwib3duZXJHaWQiOiI0MiIsIm93bmVyVWlkIjoiMCIsInBhdGgiOiIvZXRjL3NoYWRvdyIsInBlcm1pc3Npb25zIjoiNjQwIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTFvam0yWWRwQ0o2Qi9hcEdEYVovU2RiMnhKa0E9In0sInBhdGgiOiIvZXRjL3NoZWxscyJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExNHVwejN0Zm5OeFprSUVzVWhXbjdYb2l3OTZnPSJ9LCJwYXRoIjoiL2V0Yy9zeXNjdGwuY29uZiJ9LHsicGF0aCI6Ii9ldGMvYXBrIn0seyJwYXRoIjoiL2V0Yy9jb25mLmQifSx7InBhdGgiOiIvZXRjL2Nyb250YWJzIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTF2ZmsxYXBVV0k0eUxKR2hoTlJkMGtKaXhmdlk9In0sIm93bmVyR2lkIjoiMCIsIm93bmVyVWlkIjoiMCIsInBhdGgiOiIvZXRjL2Nyb250YWJzL3Jvb3QiLCJwZXJtaXNzaW9ucyI6IjYwMCJ9LHsicGF0aCI6Ii9ldGMvaW5pdC5kIn0seyJwYXRoIjoiL2V0Yy9tb2Rwcm9iZS5kIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTFXVWJoNlRCWU5WSzdlNFkrdVV2THMvN3ZpcWs9In0sInBhdGgiOiIvZXRjL21vZHByb2JlLmQvYWxpYXNlcy5jb25mIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTE0VGRnRkhrVGR0M3VRQytOQnRybnRPbm05bjQ9In0sInBhdGgiOiIvZXRjL21vZHByb2JlLmQvYmxhY2tsaXN0LmNvbmYifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMXBuYXkvbmpuNm9sOWNDc3NMN0tpWlo4ZXRsYz0ifSwicGF0aCI6Ii9ldGMvbW9kcHJvYmUuZC9pMzg2LmNvbmYifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMXluYkxuM0dZRHB2YWpiYS9sZHAxbmlheWVvZz0ifSwicGF0aCI6Ii9ldGMvbW9kcHJvYmUuZC9rbXMuY29uZiJ9LHsicGF0aCI6Ii9ldGMvbW9kdWxlcy1sb2FkLmQifSx7InBhdGgiOiIvZXRjL25ldHdvcmsifSx7InBhdGgiOiIvZXRjL25ldHdvcmsvaWYtZG93bi5kIn0seyJwYXRoIjoiL2V0Yy9uZXR3b3JrL2lmLXBvc3QtZG93bi5kIn0seyJwYXRoIjoiL2V0Yy9uZXR3b3JrL2lmLXByZS11cC5kIn0seyJwYXRoIjoiL2V0Yy9uZXR3b3JrL2lmLXVwLmQifSx7InBhdGgiOiIvZXRjL29wdCJ9LHsicGF0aCI6Ii9ldGMvcGVyaW9kaWMifSx7InBhdGgiOiIvZXRjL3BlcmlvZGljLzE1bWluIn0seyJwYXRoIjoiL2V0Yy9wZXJpb2RpYy9kYWlseSJ9LHsicGF0aCI6Ii9ldGMvcGVyaW9kaWMvaG91cmx5In0seyJwYXRoIjoiL2V0Yy9wZXJpb2RpYy9tb250aGx5In0seyJwYXRoIjoiL2V0Yy9wZXJpb2RpYy93ZWVrbHkifSx7InBhdGgiOiIvZXRjL3Byb2ZpbGUuZCJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExMzVPV3NDenp2bkIyZm1GeDYya2JxbTFBeDFrPSJ9LCJwYXRoIjoiL2V0Yy9wcm9maWxlLmQvUkVBRE1FIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTEwd0wyM0d1U0NWZnVtTVJnYWthYlVJNkVzU2s9In0sInBhdGgiOiIvZXRjL3Byb2ZpbGUuZC9jb2xvcl9wcm9tcHQuc2guZGlzYWJsZWQifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMVM4aitXVzcxbVd4ZlZ5OHl0aHFVN0hVVm9Cdz0ifSwicGF0aCI6Ii9ldGMvcHJvZmlsZS5kL2xvY2FsZS5zaCJ9LHsicGF0aCI6Ii9ldGMvc3lzY3RsLmQifSx7InBhdGgiOiIvaG9tZSJ9LHsicGF0aCI6Ii9saWIifSx7InBhdGgiOiIvbGliL2Zpcm13YXJlIn0seyJwYXRoIjoiL2xpYi9tZGV2In0seyJwYXRoIjoiL2xpYi9tb2R1bGVzLWxvYWQuZCJ9LHsicGF0aCI6Ii9saWIvc3lzY3RsLmQifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMUhwRWx6VzF4RWdtS2ZFUnRUeTdvb21tbnE2Yz0ifSwicGF0aCI6Ii9saWIvc3lzY3RsLmQvMDAtYWxwaW5lLmNvbmYifSx7InBhdGgiOiIvbWVkaWEifSx7InBhdGgiOiIvbWVkaWEvY2Ryb20ifSx7InBhdGgiOiIvbWVkaWEvZmxvcHB5In0seyJwYXRoIjoiL21lZGlhL3VzYiJ9LHsicGF0aCI6Ii9tbnQifSx7InBhdGgiOiIvb3B0In0seyJwYXRoIjoiL3Byb2MifSx7Im93bmVyR2lkIjoiMCIsIm93bmVyVWlkIjoiMCIsInBhdGgiOiIvcm9vdCIsInBlcm1pc3Npb25zIjoiNzAwIn0seyJwYXRoIjoiL3J1biJ9LHsicGF0aCI6Ii9zYmluIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTFxamtkeVJKY1libEdDNlJNcVVSNEJkYjVnMTA9In0sIm93bmVyR2lkIjoiMCIsIm93bmVyVWlkIjoiMCIsInBhdGgiOiIvc2Jpbi9ta21udGRpcnMiLCJwZXJtaXNzaW9ucyI6Ijc1NSJ9LHsicGF0aCI6Ii9zcnYifSx7InBhdGgiOiIvc3lzIn0seyJvd25lckdpZCI6IjAiLCJvd25lclVpZCI6IjAiLCJwYXRoIjoiL3RtcCIsInBlcm1pc3Npb25zIjoiMTc3NyJ9LHsicGF0aCI6Ii91c3IifSx7InBhdGgiOiIvdXNyL2xpYiJ9LHsicGF0aCI6Ii91c3IvbGliL21vZHVsZXMtbG9hZC5kIn0seyJwYXRoIjoiL3Vzci9sb2NhbCJ9LHsicGF0aCI6Ii91c3IvbG9jYWwvYmluIn0seyJwYXRoIjoiL3Vzci9sb2NhbC9saWIifSx7InBhdGgiOiIvdXNyL2xvY2FsL3NoYXJlIn0seyJwYXRoIjoiL3Vzci9zYmluIn0seyJwYXRoIjoiL3Vzci9zaGFyZSJ9LHsicGF0aCI6Ii91c3Ivc2hhcmUvbWFuIn0seyJwYXRoIjoiL3Vzci9zaGFyZS9taXNjIn0seyJwYXRoIjoiL3ZhciJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExMS9TTlp6LzhjSzJkU0tLK2NKcFZyWkl1RjRRPSJ9LCJvd25lckdpZCI6IjAiLCJvd25lclVpZCI6IjAiLCJwYXRoIjoiL3Zhci9ydW4iLCJwZXJtaXNzaW9ucyI6Ijc3NyJ9LHsicGF0aCI6Ii92YXIvY2FjaGUifSx7InBhdGgiOiIvdmFyL2NhY2hlL21pc2MifSx7Im93bmVyR2lkIjoiMCIsIm93bmVyVWlkIjoiMCIsInBhdGgiOiIvdmFyL2VtcHR5IiwicGVybWlzc2lvbnMiOiI1NTUifSx7InBhdGgiOiIvdmFyL2xpYiJ9LHsicGF0aCI6Ii92YXIvbGliL21pc2MifSx7InBhdGgiOiIvdmFyL2xvY2FsIn0seyJwYXRoIjoiL3Zhci9sb2NrIn0seyJwYXRoIjoiL3Zhci9sb2NrL3N1YnN5cyJ9LHsicGF0aCI6Ii92YXIvbG9nIn0seyJwYXRoIjoiL3Zhci9tYWlsIn0seyJwYXRoIjoiL3Zhci9vcHQifSx7InBhdGgiOiIvdmFyL3Nwb29sIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTFkemJkYXpZWkEyblR6U0lHM1l5Tnc3ZDRKdWM9In0sIm93bmVyR2lkIjoiMCIsIm93bmVyVWlkIjoiMCIsInBhdGgiOiIvdmFyL3Nwb29sL21haWwiLCJwZXJtaXNzaW9ucyI6Ijc3NyJ9LHsicGF0aCI6Ii92YXIvc3Bvb2wvY3JvbiJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExT0ZadCtaTXA3ajBHbnkwcnFTS3VXSnlxWW1BPSJ9LCJvd25lckdpZCI6IjAiLCJvd25lclVpZCI6IjAiLCJwYXRoIjoiL3Zhci9zcG9vbC9jcm9uL2Nyb250YWJzIiwicGVybWlzc2lvbnMiOiI3NzcifSx7Im93bmVyR2lkIjoiMCIsIm93bmVyVWlkIjoiMCIsInBhdGgiOiIvdmFyL3RtcCIsInBlcm1pc3Npb25zIjoiMTc3NyJ9XSwiZ2l0Q29tbWl0T2ZBcGtQb3J0IjoiZGZhMTM3OTM1N2EzMjFlNjM4ZmVlZjFjZDhkNTVhYjAzZDAyMGY0NSIsImluc3RhbGxlZFNpemUiOjQxMzY5NiwibGljZW5zZSI6IkdQTC0yLjAtb25seSIsIm1haW50YWluZXIiOiJOYXRhbmFlbCBDb3BhIFx1MDAzY25jb3BhQGFscGluZWxpbnV4Lm9yZ1x1MDAzZSIsIm9yaWdpblBhY2thZ2UiOiJhbHBpbmUtYmFzZWxheW91dCIsInBhY2thZ2UiOiJhbHBpbmUtYmFzZWxheW91dCIsInB1bGxDaGVja3N1bSI6IlExRXltUzZyQWdtR3M3WFlocWR5RW9pV2dFWjZBPSIsInB1bGxEZXBlbmRlbmNpZXMiOiIvYmluL3NoIHNvOmxpYmMubXVzbC14ODZfNjQuc28uMSIsInNpemUiOjIxMTAxLCJ1cmwiOiJodHRwczovL2dpdC5hbHBpbmVsaW51eC5vcmcvY2dpdC9hcG9ydHMvdHJlZS9tYWluL2FscGluZS1iYXNlbGF5b3V0IiwidmVyc2lvbiI6IjMuMi4wLXIxOCJ9LCJtZXRhZGF0YVR5cGUiOiJBcGtNZXRhZGF0YSIsIm5hbWUiOiJhbHBpbmUtYmFzZWxheW91dCIsInB1cmwiOiJwa2c6YWxwaW5lL2FscGluZS1iYXNlbGF5b3V0QDMuMi4wLXIxOD9hcmNoPXg4Nl82NFx1MDAyNnVwc3RyZWFtPWFscGluZS1iYXNlbGF5b3V0XHUwMDI2ZGlzdHJvPWFscGluZS0zLjE1LjIiLCJ0eXBlIjoiYXBrIiwidmVyc2lvbiI6IjMuMi4wLXIxOCJ9LHsiY3BlcyI6WyJjcGU6Mi4zOmE6YWxwaW5lLWtleXM6YWxwaW5lLWtleXM6Mi40LXIxOio6KjoqOio6KjoqOioiLCJjcGU6Mi4zOmE6YWxwaW5lLWtleXM6YWxwaW5lX2tleXM6Mi40LXIxOio6KjoqOio6KjoqOioiLCJjcGU6Mi4zOmE6YWxwaW5lX2tleXM6YWxwaW5lLWtleXM6Mi40LXIxOio6KjoqOio6KjoqOioiLCJjcGU6Mi4zOmE6YWxwaW5lX2tleXM6YWxwaW5lX2tleXM6Mi40LXIxOio6KjoqOio6KjoqOioiLCJjcGU6Mi4zOmE6YWxwaW5lOmFscGluZS1rZXlzOjIuNC1yMToqOio6KjoqOio6KjoqIiwiY3BlOjIuMzphOmFscGluZTphbHBpbmVfa2V5czoyLjQtcjE6KjoqOio6KjoqOio6KiJdLCJmb3VuZEJ5IjoiYXBrZGItY2F0YWxvZ2VyIiwiaWQiOiIxOTk3ZGQxYzhmZjcyZGMyIiwibGFuZ3VhZ2UiOiIiLCJsaWNlbnNlcyI6WyJNSVQiXSwibG9jYXRpb25zIjpbeyJsYXllcklEIjoic2hhMjU2OmZmNzY4YTE0MTNiYTEwOTMwOGMwZDg5N2ZmYTU1NWUwNTJhNDZkMmNmNDcxMTc4ZjAwMWI4MjViNGMyMWYzNTQiLCJwYXRoIjoiL2xpYi9hcGsvZGIvaW5zdGFsbGVkIn1dLCJtZXRhZGF0YSI6eyJhcmNoaXRlY3R1cmUiOiJ4ODZfNjQiLCJkZXNjcmlwdGlvbiI6IlB1YmxpYyBrZXlzIGZvciBBbHBpbmUgTGludXggcGFja2FnZXMiLCJmaWxlcyI6W3sicGF0aCI6Ii9ldGMifSx7InBhdGgiOiIvZXRjL2FwayJ9LHsicGF0aCI6Ii9ldGMvYXBrL2tleXMifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMU92Q0ZTTzk0ejk3YzgwbUlEQ3hxR2toMk9nND0ifSwicGF0aCI6Ii9ldGMvYXBrL2tleXMvYWxwaW5lLWRldmVsQGxpc3RzLmFscGluZWxpbnV4Lm9yZy00YTZhMDg0MC5yc2EucHViIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTF2N1lXWll6QVdvY2xhTERJNDVqRWd1STdZTjA9In0sInBhdGgiOiIvZXRjL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNTI0M2VmNGIucnNhLnB1YiJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExTm5HdURzZFFPeDRaTllmQjNOOTdlTHlHUGtJPSJ9LCJwYXRoIjoiL2V0Yy9hcGsva2V5cy9hbHBpbmUtZGV2ZWxAbGlzdHMuYWxwaW5lbGludXgub3JnLTUyNjFjZWNiLnJzYS5wdWIifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMWxabFRFU05yZWxXVE5rTC9vUXptQVU4YTk5QT0ifSwicGF0aCI6Ii9ldGMvYXBrL2tleXMvYWxwaW5lLWRldmVsQGxpc3RzLmFscGluZWxpbnV4Lm9yZy02MTY1ZWU1OS5yc2EucHViIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTFXTlc2U3k4N0hwSjNJZGVtUXk4cGp1MzNLbXM9In0sInBhdGgiOiIvZXRjL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNjE2NjZlM2YucnNhLnB1YiJ9LHsicGF0aCI6Ii91c3IifSx7InBhdGgiOiIvdXNyL3NoYXJlIn0seyJwYXRoIjoiL3Vzci9zaGFyZS9hcGsifSx7InBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTFPdkNGU085NHo5N2M4MG1JREN4cUdraDJPZzQ9In0sInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNGE2YTA4NDAucnNhLnB1YiJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExdjdZV1pZekFXb2NsYUxESTQ1akVndUk3WU4wPSJ9LCJwYXRoIjoiL3Vzci9zaGFyZS9hcGsva2V5cy9hbHBpbmUtZGV2ZWxAbGlzdHMuYWxwaW5lbGludXgub3JnLTUyNDNlZjRiLnJzYS5wdWIifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMUJUcVMrSC9VVXloUXV6SHdpQmw0NytCVEt1VT0ifSwicGF0aCI6Ii91c3Ivc2hhcmUvYXBrL2tleXMvYWxwaW5lLWRldmVsQGxpc3RzLmFscGluZWxpbnV4Lm9yZy01MjRkMjdiYi5yc2EucHViIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTFObkd1RHNkUU94NFpOWWZCM045N2VMeUdQa0k9In0sInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNTI2MWNlY2IucnNhLnB1YiJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExT2F4ZGNzYTZBWW9QZExpMFU0bE8zSjJ3ZTE4PSJ9LCJwYXRoIjoiL3Vzci9zaGFyZS9hcGsva2V5cy9hbHBpbmUtZGV2ZWxAbGlzdHMuYWxwaW5lbGludXgub3JnLTU4MTk5ZGNjLnJzYS5wdWIifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMXlQcStzdTY1a3NOb3gzdVhCK0RSN1AxOCtRVT0ifSwicGF0aCI6Ii91c3Ivc2hhcmUvYXBrL2tleXMvYWxwaW5lLWRldmVsQGxpc3RzLmFscGluZWxpbnV4Lm9yZy01OGNiYjQ3Ni5yc2EucHViIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTFNcFpETlgwTGVMSHZTT3dWVXlYaVh4MTFOTjA9In0sInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNThlNGYxN2QucnNhLnB1YiJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExZ2xDUS9lSmJ2QTV4cWNzd2RqRnJXdjVGbmswPSJ9LCJwYXRoIjoiL3Vzci9zaGFyZS9hcGsva2V5cy9hbHBpbmUtZGV2ZWxAbGlzdHMuYWxwaW5lbGludXgub3JnLTVlNjljYTUwLnJzYS5wdWIifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMVhVZERFb05UdGpsdnJTK2l1bms2emlGZ0lwVT0ifSwicGF0aCI6Ii91c3Ivc2hhcmUvYXBrL2tleXMvYWxwaW5lLWRldmVsQGxpc3RzLmFscGluZWxpbnV4Lm9yZy02MGFjMjA5OS5yc2EucHViIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTFsWmxURVNOcmVsV1ROa0wvb1F6bUFVOGE5OUE9In0sInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNjE2NWVlNTkucnNhLnB1YiJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExV05XNlN5ODdIcEozSWRlbVF5OHBqdTMzS21zPSJ9LCJwYXRoIjoiL3Vzci9zaGFyZS9hcGsva2V5cy9hbHBpbmUtZGV2ZWxAbGlzdHMuYWxwaW5lbGludXgub3JnLTYxNjY2ZTNmLnJzYS5wdWIifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMUk5RHk2aHJ5YWNMMllXWGcrS2xFNld2d0VkND0ifSwicGF0aCI6Ii91c3Ivc2hhcmUvYXBrL2tleXMvYWxwaW5lLWRldmVsQGxpc3RzLmFscGluZWxpbnV4Lm9yZy02MTZhOTcyNC5yc2EucHViIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTFOU25zZ21jTWJVNGc3ajVKYU5zMHRWSHBIVkE9In0sInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNjE2YWJjMjMucnNhLnB1YiJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExVmFNQkJrNFJ4djZib1BMS0YrSTA4NVE4eTJFPSJ9LCJwYXRoIjoiL3Vzci9zaGFyZS9hcGsva2V5cy9hbHBpbmUtZGV2ZWxAbGlzdHMuYWxwaW5lbGludXgub3JnLTYxNmFjM2JjLnJzYS5wdWIifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMTNoSkJNSEFVcXVQYnA1anBBUEZqUUkyWTF2UT0ifSwicGF0aCI6Ii91c3Ivc2hhcmUvYXBrL2tleXMvYWxwaW5lLWRldmVsQGxpc3RzLmFscGluZWxpbnV4Lm9yZy02MTZhZGZlYi5yc2EucHViIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTFWL2E1UDlwS1JKYjZ0aWhFM2U4TzZ4YVBnTFU9In0sInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNjE2YWUzNTAucnNhLnB1YiJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExM3dMSnJjS1FhanFsNWExcDlRNDVVK1pYRU5BPSJ9LCJwYXRoIjoiL3Vzci9zaGFyZS9hcGsva2V5cy9hbHBpbmUtZGV2ZWxAbGlzdHMuYWxwaW5lbGludXgub3JnLTYxNmRiMzBkLnJzYS5wdWIifSx7InBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FhcmNoNjQifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMTdqOW5XSmtRK3dmSXVWUXpJRnJtRlo3ZlNPYz0ifSwib3duZXJHaWQiOiIwIiwib3duZXJVaWQiOiIwIiwicGF0aCI6Ii91c3Ivc2hhcmUvYXBrL2tleXMvYWFyY2g2NC9hbHBpbmUtZGV2ZWxAbGlzdHMuYWxwaW5lbGludXgub3JnLTU4MTk5ZGNjLnJzYS5wdWIiLCJwZXJtaXNzaW9ucyI6Ijc3NyJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExc25yK1ExVWJmSHlDci9jbW10VnZNSVM3U0dzPSJ9LCJvd25lckdpZCI6IjAiLCJvd25lclVpZCI6IjAiLCJwYXRoIjoiL3Vzci9zaGFyZS9hcGsva2V5cy9hYXJjaDY0L2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNjE2YWUzNTAucnNhLnB1YiIsInBlcm1pc3Npb25zIjoiNzc3In0seyJwYXRoIjoiL3Vzci9zaGFyZS9hcGsva2V5cy9hcm1oZiJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExVTlRdHNkTityWVo5Wmg3NkVmWHkwMEpaSE1nPSJ9LCJvd25lckdpZCI6IjAiLCJvd25lclVpZCI6IjAiLCJwYXRoIjoiL3Vzci9zaGFyZS9hcGsva2V5cy9hcm1oZi9hbHBpbmUtZGV2ZWxAbGlzdHMuYWxwaW5lbGludXgub3JnLTUyNGQyN2JiLnJzYS5wdWIiLCJwZXJtaXNzaW9ucyI6Ijc3NyJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExYkMrQWRRMHFXQlRtZWZYaUkwUHZtWU9Kb1ZRPSJ9LCJvd25lckdpZCI6IjAiLCJvd25lclVpZCI6IjAiLCJwYXRoIjoiL3Vzci9zaGFyZS9hcGsva2V5cy9hcm1oZi9hbHBpbmUtZGV2ZWxAbGlzdHMuYWxwaW5lbGludXgub3JnLTYxNmE5NzI0LnJzYS5wdWIiLCJwZXJtaXNzaW9ucyI6Ijc3NyJ9LHsicGF0aCI6Ii91c3Ivc2hhcmUvYXBrL2tleXMvYXJtdjcifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMVU5UXRzZE4rcllaOVpoNzZFZlh5MDBKWkhNZz0ifSwib3duZXJHaWQiOiIwIiwib3duZXJVaWQiOiIwIiwicGF0aCI6Ii91c3Ivc2hhcmUvYXBrL2tleXMvYXJtdjcvYWxwaW5lLWRldmVsQGxpc3RzLmFscGluZWxpbnV4Lm9yZy01MjRkMjdiYi5yc2EucHViIiwicGVybWlzc2lvbnMiOiI3NzcifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMXhiSVZ1N1Njd3FHSHhYR3dJMjJhU2U1T2RVWT0ifSwib3duZXJHaWQiOiIwIiwib3duZXJVaWQiOiIwIiwicGF0aCI6Ii91c3Ivc2hhcmUvYXBrL2tleXMvYXJtdjcvYWxwaW5lLWRldmVsQGxpc3RzLmFscGluZWxpbnV4Lm9yZy02MTZhZGZlYi5yc2EucHViIiwicGVybWlzc2lvbnMiOiI3NzcifSx7InBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL21pcHM2NCJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExaENaZEZ4K0x2emJMdFBzNzUzamU3OGdFRUJRPSJ9LCJvd25lckdpZCI6IjAiLCJvd25lclVpZCI6IjAiLCJwYXRoIjoiL3Vzci9zaGFyZS9hcGsva2V5cy9taXBzNjQvYWxwaW5lLWRldmVsQGxpc3RzLmFscGluZWxpbnV4Lm9yZy01ZTY5Y2E1MC5yc2EucHViIiwicGVybWlzc2lvbnMiOiI3NzcifSx7InBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL3BwYzY0bGUifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMXQyMWRoQ0xiVEptQUhYU0NlT01xLzJ2ZlNnbz0ifSwib3duZXJHaWQiOiIwIiwib3duZXJVaWQiOiIwIiwicGF0aCI6Ii91c3Ivc2hhcmUvYXBrL2tleXMvcHBjNjRsZS9hbHBpbmUtZGV2ZWxAbGlzdHMuYWxwaW5lbGludXgub3JnLTU4Y2JiNDc2LnJzYS5wdWIiLCJwZXJtaXNzaW9ucyI6Ijc3NyJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExUFM5ek5JUEphbkM4cWNzYzVxYXJFV3FoVjVRPSJ9LCJvd25lckdpZCI6IjAiLCJvd25lclVpZCI6IjAiLCJwYXRoIjoiL3Vzci9zaGFyZS9hcGsva2V5cy9wcGM2NGxlL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNjE2YWJjMjMucnNhLnB1YiIsInBlcm1pc3Npb25zIjoiNzc3In0seyJwYXRoIjoiL3Vzci9zaGFyZS9hcGsva2V5cy9yaXNjdjY0In0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTFOVlBiWmF2YVhwc0l0RndRWURXYnBvcjd5WUU9In0sIm93bmVyR2lkIjoiMCIsIm93bmVyVWlkIjoiMCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL3Jpc2N2NjQvYWxwaW5lLWRldmVsQGxpc3RzLmFscGluZWxpbnV4Lm9yZy02MGFjMjA5OS5yc2EucHViIiwicGVybWlzc2lvbnMiOiI3NzcifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMVU2dGZ1S1J5NUo4QzZpYUtQTVphVC9lOHRiQT0ifSwib3duZXJHaWQiOiIwIiwib3duZXJVaWQiOiIwIiwicGF0aCI6Ii91c3Ivc2hhcmUvYXBrL2tleXMvcmlzY3Y2NC9hbHBpbmUtZGV2ZWxAbGlzdHMuYWxwaW5lbGludXgub3JnLTYxNmRiMzBkLnJzYS5wdWIiLCJwZXJtaXNzaW9ucyI6Ijc3NyJ9LHsicGF0aCI6Ii91c3Ivc2hhcmUvYXBrL2tleXMvczM5MHgifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMXNqYlYycjJ3MEloMnZ3ZHpDNEpxNlVJN2NNUT0ifSwib3duZXJHaWQiOiIwIiwib3duZXJVaWQiOiIwIiwicGF0aCI6Ii91c3Ivc2hhcmUvYXBrL2tleXMvczM5MHgvYWxwaW5lLWRldmVsQGxpc3RzLmFscGluZWxpbnV4Lm9yZy01OGU0ZjE3ZC5yc2EucHViIiwicGVybWlzc2lvbnMiOiI3NzcifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMWwwOXhhN1JuYk9JQzFkSTlGcWJhQ2ZTL0dYWT0ifSwib3duZXJHaWQiOiIwIiwib3duZXJVaWQiOiIwIiwicGF0aCI6Ii91c3Ivc2hhcmUvYXBrL2tleXMvczM5MHgvYWxwaW5lLWRldmVsQGxpc3RzLmFscGluZWxpbnV4Lm9yZy02MTZhYzNiYy5yc2EucHViIiwicGVybWlzc2lvbnMiOiI3NzcifSx7InBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL3g4NiJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExSWk1MWk3TnJjNHVmdDE0SGhxdWdhVXFkSDY0PSJ9LCJvd25lckdpZCI6IjAiLCJvd25lclVpZCI6IjAiLCJwYXRoIjoiL3Vzci9zaGFyZS9hcGsva2V5cy94ODYvYWxwaW5lLWRldmVsQGxpc3RzLmFscGluZWxpbnV4Lm9yZy00YTZhMDg0MC5yc2EucHViIiwicGVybWlzc2lvbnMiOiI3NzcifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMVk0OWVWeGhwdmZ0YlEzeUFkdmxMZmNyUExUVT0ifSwib3duZXJHaWQiOiIwIiwib3duZXJVaWQiOiIwIiwicGF0aCI6Ii91c3Ivc2hhcmUvYXBrL2tleXMveDg2L2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNTI0M2VmNGIucnNhLnB1YiIsInBlcm1pc3Npb25zIjoiNzc3In0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTFIamR2Y1ZrcEJaenIxYVNlM3A3b1FmQXRtL0U9In0sIm93bmVyR2lkIjoiMCIsIm93bmVyVWlkIjoiMCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL3g4Ni9hbHBpbmUtZGV2ZWxAbGlzdHMuYWxwaW5lbGludXgub3JnLTYxNjY2ZTNmLnJzYS5wdWIiLCJwZXJtaXNzaW9ucyI6Ijc3NyJ9LHsicGF0aCI6Ii91c3Ivc2hhcmUvYXBrL2tleXMveDg2XzY0In0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTFJaTUxaTdOcmM0dWZ0MTRIaHF1Z2FVcWRINjQ9In0sIm93bmVyR2lkIjoiMCIsIm93bmVyVWlkIjoiMCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL3g4Nl82NC9hbHBpbmUtZGV2ZWxAbGlzdHMuYWxwaW5lbGludXgub3JnLTRhNmEwODQwLnJzYS5wdWIiLCJwZXJtaXNzaW9ucyI6Ijc3NyJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExQVVGWStmd1NCVGNyWWV0alQ3Tkh2YWZyU1FjPSJ9LCJvd25lckdpZCI6IjAiLCJvd25lclVpZCI6IjAiLCJwYXRoIjoiL3Vzci9zaGFyZS9hcGsva2V5cy94ODZfNjQvYWxwaW5lLWRldmVsQGxpc3RzLmFscGluZWxpbnV4Lm9yZy01MjYxY2VjYi5yc2EucHViIiwicGVybWlzc2lvbnMiOiI3NzcifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMXFLQTIzVnpNVURsZStEcW5ycjVLeitYdnR5ND0ifSwib3duZXJHaWQiOiIwIiwib3duZXJVaWQiOiIwIiwicGF0aCI6Ii91c3Ivc2hhcmUvYXBrL2tleXMveDg2XzY0L2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNjE2NWVlNTkucnNhLnB1YiIsInBlcm1pc3Npb25zIjoiNzc3In1dLCJnaXRDb21taXRPZkFwa1BvcnQiOiJhYWI2OGY4YzlhYjQzNGE0NjcxMGRlOGUxMmZiMzIwNmUyOTMwYTU5IiwiaW5zdGFsbGVkU2l6ZSI6MTU5NzQ0LCJsaWNlbnNlIjoiTUlUIiwibWFpbnRhaW5lciI6Ik5hdGFuYWVsIENvcGEgXHUwMDNjbmNvcGFAYWxwaW5lbGludXgub3JnXHUwMDNlIiwib3JpZ2luUGFja2FnZSI6ImFscGluZS1rZXlzIiwicGFja2FnZSI6ImFscGluZS1rZXlzIiwicHVsbENoZWNrc3VtIjoiUTFrREYyc3RLbzNlL1J1bWxBOFpyUmZDd2RTdjg9IiwicHVsbERlcGVuZGVuY2llcyI6IiIsInNpemUiOjEzMzYyLCJ1cmwiOiJodHRwczovL2FscGluZWxpbnV4Lm9yZyIsInZlcnNpb24iOiIyLjQtcjEifSwibWV0YWRhdGFUeXBlIjoiQXBrTWV0YWRhdGEiLCJuYW1lIjoiYWxwaW5lLWtleXMiLCJwdXJsIjoicGtnOmFscGluZS9hbHBpbmUta2V5c0AyLjQtcjE/YXJjaD14ODZfNjRcdTAwMjZ1cHN0cmVhbT1hbHBpbmUta2V5c1x1MDAyNmRpc3Rybz1hbHBpbmUtMy4xNS4yIiwidHlwZSI6ImFwayIsInZlcnNpb24iOiIyLjQtcjEifSx7ImNwZXMiOlsiY3BlOjIuMzphOmFway10b29sczphcGstdG9vbHM6Mi4xMi43LXIzOio6KjoqOio6KjoqOioiLCJjcGU6Mi4zOmE6YXBrLXRvb2xzOmFwa190b29sczoyLjEyLjctcjM6KjoqOio6KjoqOio6KiIsImNwZToyLjM6YTphcGtfdG9vbHM6YXBrLXRvb2xzOjIuMTIuNy1yMzoqOio6KjoqOio6KjoqIiwiY3BlOjIuMzphOmFwa190b29sczphcGtfdG9vbHM6Mi4xMi43LXIzOio6KjoqOio6KjoqOioiLCJjcGU6Mi4zOmE6YXBrOmFway10b29sczoyLjEyLjctcjM6KjoqOio6KjoqOio6KiIsImNwZToyLjM6YTphcGs6YXBrX3Rvb2xzOjIuMTIuNy1yMzoqOio6KjoqOio6KjoqIl0sImZvdW5kQnkiOiJhcGtkYi1jYXRhbG9nZXIiLCJpZCI6IjVlZjY2YTMzNWRkYzAzYTYiLCJsYW5ndWFnZSI6IiIsImxpY2Vuc2VzIjpbIkdQTC0yLjAtb25seSJdLCJsb2NhdGlvbnMiOlt7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvbGliL2Fway9kYi9pbnN0YWxsZWQifV0sIm1ldGFkYXRhIjp7ImFyY2hpdGVjdHVyZSI6Ing4Nl82NCIsImRlc2NyaXB0aW9uIjoiQWxwaW5lIFBhY2thZ2UgS2VlcGVyIC0gcGFja2FnZSBtYW5hZ2VyIGZvciBhbHBpbmUiLCJmaWxlcyI6W3sicGF0aCI6Ii9ldGMifSx7InBhdGgiOiIvZXRjL2FwayJ9LHsicGF0aCI6Ii9ldGMvYXBrL2tleXMifSx7InBhdGgiOiIvZXRjL2Fway9wcm90ZWN0ZWRfcGF0aHMuZCJ9LHsicGF0aCI6Ii9saWIifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMVM1REw2REZPbWpqTnhBR05zc2ZqNG5VaThYVT0ifSwib3duZXJHaWQiOiIwIiwib3duZXJVaWQiOiIwIiwicGF0aCI6Ii9saWIvbGliYXBrLnNvLjMuMTIuMCIsInBlcm1pc3Npb25zIjoiNzU1In0seyJwYXRoIjoiL3NiaW4ifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMU8xcEJVQjJrVE0vTHFMOGQwVDk4Q0ViWllxdz0ifSwib3duZXJHaWQiOiIwIiwib3duZXJVaWQiOiIwIiwicGF0aCI6Ii9zYmluL2FwayIsInBlcm1pc3Npb25zIjoiNzU1In0seyJwYXRoIjoiL3ZhciJ9LHsicGF0aCI6Ii92YXIvY2FjaGUifSx7InBhdGgiOiIvdmFyL2NhY2hlL21pc2MifSx7InBhdGgiOiIvdmFyL2xpYiJ9LHsicGF0aCI6Ii92YXIvbGliL2FwayJ9XSwiZ2l0Q29tbWl0T2ZBcGtQb3J0IjoiMWFjM2MxYmIyOWVlZmYwODNjNjIxY2Y2YjI3YWQxMmFiOTNjYjczYSIsImluc3RhbGxlZFNpemUiOjMxMTI5NiwibGljZW5zZSI6IkdQTC0yLjAtb25seSIsIm1haW50YWluZXIiOiJOYXRhbmFlbCBDb3BhIFx1MDAzY25jb3BhQGFscGluZWxpbnV4Lm9yZ1x1MDAzZSIsIm9yaWdpblBhY2thZ2UiOiJhcGstdG9vbHMiLCJwYWNrYWdlIjoiYXBrLXRvb2xzIiwicHVsbENoZWNrc3VtIjoiUTEzZlBkK0ZSWGFMd3lOa2xWbitxdUZXRHlrbk09IiwicHVsbERlcGVuZGVuY2llcyI6Im11c2xcdTAwM2U9MS4yIGNhLWNlcnRpZmljYXRlcy1idW5kbGUgc286bGliYy5tdXNsLXg4Nl82NC5zby4xIHNvOmxpYmNyeXB0by5zby4xLjEgc286bGlic3NsLnNvLjEuMSBzbzpsaWJ6LnNvLjEiLCJzaXplIjoxMjAzNzcsInVybCI6Imh0dHBzOi8vZ2l0bGFiLmFscGluZWxpbnV4Lm9yZy9hbHBpbmUvYXBrLXRvb2xzIiwidmVyc2lvbiI6IjIuMTIuNy1yMyJ9LCJtZXRhZGF0YVR5cGUiOiJBcGtNZXRhZGF0YSIsIm5hbWUiOiJhcGstdG9vbHMiLCJwdXJsIjoicGtnOmFscGluZS9hcGstdG9vbHNAMi4xMi43LXIzP2FyY2g9eDg2XzY0XHUwMDI2dXBzdHJlYW09YXBrLXRvb2xzXHUwMDI2ZGlzdHJvPWFscGluZS0zLjE1LjIiLCJ0eXBlIjoiYXBrIiwidmVyc2lvbiI6IjIuMTIuNy1yMyJ9LHsiY3BlcyI6WyJjcGU6Mi4zOmE6YnVzeWJveDpidXN5Ym94OjEuMzQuMS1yNDoqOio6KjoqOio6KjoqIl0sImZvdW5kQnkiOiJhcGtkYi1jYXRhbG9nZXIiLCJpZCI6ImYyMTMyZThkNmNmZTAwNmEiLCJsYW5ndWFnZSI6IiIsImxpY2Vuc2VzIjpbIkdQTC0yLjAtb25seSJdLCJsb2NhdGlvbnMiOlt7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvbGliL2Fway9kYi9pbnN0YWxsZWQifV0sIm1ldGFkYXRhIjp7ImFyY2hpdGVjdHVyZSI6Ing4Nl82NCIsImRlc2NyaXB0aW9uIjoiU2l6ZSBvcHRpbWl6ZWQgdG9vbGJveCBvZiBtYW55IGNvbW1vbiBVTklYIHV0aWxpdGllcyIsImZpbGVzIjpbeyJwYXRoIjoiL2JpbiJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExOEc5WGVOR0FVQTQzdmlVS3NsbWRpbjJ6RDI4PSJ9LCJvd25lckdpZCI6IjAiLCJvd25lclVpZCI6IjAiLCJwYXRoIjoiL2Jpbi9idXN5Ym94IiwicGVybWlzc2lvbnMiOiI3NTUifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMXBjZlRmRE5FYk5LUWMyczF0aWE3ZGEwNU04UT0ifSwib3duZXJHaWQiOiIwIiwib3duZXJVaWQiOiIwIiwicGF0aCI6Ii9iaW4vc2giLCJwZXJtaXNzaW9ucyI6Ijc3NyJ9LHsicGF0aCI6Ii9ldGMifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMW1COTVIcTJOVVRaNTk5UkRpU3NqOXc1RnJPVT0ifSwicGF0aCI6Ii9ldGMvc2VjdXJldHR5In0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTFFZ0xGamo2N291M2VNcXA0bTNyMlpqblE3UVU9In0sInBhdGgiOiIvZXRjL3VkaGNwZC5jb25mIn0seyJwYXRoIjoiL2V0Yy9sb2dyb3RhdGUuZCJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExVHlseUNJTlZtblMrQS9UZWFkNHZaaEU3QmtzPSJ9LCJwYXRoIjoiL2V0Yy9sb2dyb3RhdGUuZC9hY3BpZCJ9LHsicGF0aCI6Ii9ldGMvbmV0d29yayJ9LHsicGF0aCI6Ii9ldGMvbmV0d29yay9pZi1kb3duLmQifSx7InBhdGgiOiIvZXRjL25ldHdvcmsvaWYtcG9zdC1kb3duLmQifSx7InBhdGgiOiIvZXRjL25ldHdvcmsvaWYtcG9zdC11cC5kIn0seyJwYXRoIjoiL2V0Yy9uZXR3b3JrL2lmLXByZS1kb3duLmQifSx7InBhdGgiOiIvZXRjL25ldHdvcmsvaWYtcHJlLXVwLmQifSx7InBhdGgiOiIvZXRjL25ldHdvcmsvaWYtdXAuZCJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExT1JmK2xQUkt1WWdka0JCY0tvZXZSMXQ2MFE0PSJ9LCJvd25lckdpZCI6IjAiLCJvd25lclVpZCI6IjAiLCJwYXRoIjoiL2V0Yy9uZXR3b3JrL2lmLXVwLmQvZGFkIiwicGVybWlzc2lvbnMiOiI3NzUifSx7InBhdGgiOiIvc2JpbiJ9LHsib3duZXJHaWQiOiIwIiwib3duZXJVaWQiOiIwIiwicGF0aCI6Ii90bXAiLCJwZXJtaXNzaW9ucyI6IjE3NzcifSx7InBhdGgiOiIvdXNyIn0seyJwYXRoIjoiL3Vzci9zYmluIn0seyJwYXRoIjoiL3Vzci9zaGFyZSJ9LHsicGF0aCI6Ii91c3Ivc2hhcmUvdWRoY3BjIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTF0OXZpci9aclgzbmJTSVlUOUJETFdaZW5rVlE9In0sIm93bmVyR2lkIjoiMCIsIm93bmVyVWlkIjoiMCIsInBhdGgiOiIvdXNyL3NoYXJlL3VkaGNwYy9kZWZhdWx0LnNjcmlwdCIsInBlcm1pc3Npb25zIjoiNzU1In0seyJwYXRoIjoiL3ZhciJ9LHsicGF0aCI6Ii92YXIvY2FjaGUifSx7InBhdGgiOiIvdmFyL2NhY2hlL21pc2MifSx7InBhdGgiOiIvdmFyL2xpYiJ9LHsicGF0aCI6Ii92YXIvbGliL3VkaGNwZCJ9XSwiZ2l0Q29tbWl0T2ZBcGtQb3J0IjoiYTE2MDU5OGQ2MmEwYWM1NTg1MWFhZDE5MjUxYzAxYTFiYjVmYjIyYyIsImluc3RhbGxlZFNpemUiOjk0NjE3NiwibGljZW5zZSI6IkdQTC0yLjAtb25seSIsIm1haW50YWluZXIiOiJOYXRhbmFlbCBDb3BhIFx1MDAzY25jb3BhQGFscGluZWxpbnV4Lm9yZ1x1MDAzZSIsIm9yaWdpblBhY2thZ2UiOiJidXN5Ym94IiwicGFja2FnZSI6ImJ1c3lib3giLCJwdWxsQ2hlY2tzdW0iOiJRMUg2YXBoZGhZWjl1c1J2bVZqOVV0NVhRb2g5OD0iLCJwdWxsRGVwZW5kZW5jaWVzIjoic286bGliYy5tdXNsLXg4Nl82NC5zby4xIiwic2l6ZSI6NTAwNjA2LCJ1cmwiOiJodHRwczovL2J1c3lib3gubmV0LyIsInZlcnNpb24iOiIxLjM0LjEtcjQifSwibWV0YWRhdGFUeXBlIjoiQXBrTWV0YWRhdGEiLCJuYW1lIjoiYnVzeWJveCIsInB1cmwiOiJwa2c6YWxwaW5lL2J1c3lib3hAMS4zNC4xLXI0P2FyY2g9eDg2XzY0XHUwMDI2dXBzdHJlYW09YnVzeWJveFx1MDAyNmRpc3Rybz1hbHBpbmUtMy4xNS4yIiwidHlwZSI6ImFwayIsInZlcnNpb24iOiIxLjM0LjEtcjQifSx7ImNwZXMiOlsiY3BlOjIuMzphOmNhLWNlcnRpZmljYXRlcy1idW5kbGU6Y2EtY2VydGlmaWNhdGVzLWJ1bmRsZToyMDIxMTIyMC1yMDoqOio6KjoqOio6KjoqIiwiY3BlOjIuMzphOmNhLWNlcnRpZmljYXRlcy1idW5kbGU6Y2FfY2VydGlmaWNhdGVzX2J1bmRsZToyMDIxMTIyMC1yMDoqOio6KjoqOio6KjoqIiwiY3BlOjIuMzphOmNhX2NlcnRpZmljYXRlc19idW5kbGU6Y2EtY2VydGlmaWNhdGVzLWJ1bmRsZToyMDIxMTIyMC1yMDoqOio6KjoqOio6KjoqIiwiY3BlOjIuMzphOmNhX2NlcnRpZmljYXRlc19idW5kbGU6Y2FfY2VydGlmaWNhdGVzX2J1bmRsZToyMDIxMTIyMC1yMDoqOio6KjoqOio6KjoqIiwiY3BlOjIuMzphOmNhLWNlcnRpZmljYXRlczpjYS1jZXJ0aWZpY2F0ZXMtYnVuZGxlOjIwMjExMjIwLXIwOio6KjoqOio6KjoqOioiLCJjcGU6Mi4zOmE6Y2EtY2VydGlmaWNhdGVzOmNhX2NlcnRpZmljYXRlc19idW5kbGU6MjAyMTEyMjAtcjA6KjoqOio6KjoqOio6KiIsImNwZToyLjM6YTpjYV9jZXJ0aWZpY2F0ZXM6Y2EtY2VydGlmaWNhdGVzLWJ1bmRsZToyMDIxMTIyMC1yMDoqOio6KjoqOio6KjoqIiwiY3BlOjIuMzphOmNhX2NlcnRpZmljYXRlczpjYV9jZXJ0aWZpY2F0ZXNfYnVuZGxlOjIwMjExMjIwLXIwOio6KjoqOio6KjoqOioiLCJjcGU6Mi4zOmE6Y2E6Y2EtY2VydGlmaWNhdGVzLWJ1bmRsZToyMDIxMTIyMC1yMDoqOio6KjoqOio6KjoqIiwiY3BlOjIuMzphOmNhOmNhX2NlcnRpZmljYXRlc19idW5kbGU6MjAyMTEyMjAtcjA6KjoqOio6KjoqOio6KiJdLCJmb3VuZEJ5IjoiYXBrZGItY2F0YWxvZ2VyIiwiaWQiOiI4Y2ViMjdhMTJjMGJmZTdiIiwibGFuZ3VhZ2UiOiIiLCJsaWNlbnNlcyI6WyJNUEwtMi4wIiwiQU5EIiwiTUlUIl0sImxvY2F0aW9ucyI6W3sibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii9saWIvYXBrL2RiL2luc3RhbGxlZCJ9XSwibWV0YWRhdGEiOnsiYXJjaGl0ZWN0dXJlIjoieDg2XzY0IiwiZGVzY3JpcHRpb24iOiJQcmUgZ2VuZXJhdGVkIGJ1bmRsZSBvZiBNb3ppbGxhIGNlcnRpZmljYXRlcyIsImZpbGVzIjpbeyJwYXRoIjoiL2V0YyJ9LHsicGF0aCI6Ii9ldGMvc3NsIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTFOajZnVEJka1pwVEZXL29iSkdkcGZ2SzBTdEE9In0sIm93bmVyR2lkIjoiMCIsIm93bmVyVWlkIjoiMCIsInBhdGgiOiIvZXRjL3NzbC9jZXJ0LnBlbSIsInBlcm1pc3Npb25zIjoiNzc3In0seyJwYXRoIjoiL2V0Yy9zc2wvY2VydHMifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMW0yY0pvZm9OWnRDQ0s0MXl2alRnWDRLN2R2cz0ifSwicGF0aCI6Ii9ldGMvc3NsL2NlcnRzL2NhLWNlcnRpZmljYXRlcy5jcnQifV0sImdpdENvbW1pdE9mQXBrUG9ydCI6IjcwOWI3MGJjYjcyNzM4Y2ZlZGM1MTBiYmEwODE0MWIwMTIwMzgxNjciLCJpbnN0YWxsZWRTaXplIjoyMjExODQsImxpY2Vuc2UiOiJNUEwtMi4wIEFORCBNSVQiLCJtYWludGFpbmVyIjoiTmF0YW5hZWwgQ29wYSBcdTAwM2NuY29wYUBhbHBpbmVsaW51eC5vcmdcdTAwM2UiLCJvcmlnaW5QYWNrYWdlIjoiY2EtY2VydGlmaWNhdGVzIiwicGFja2FnZSI6ImNhLWNlcnRpZmljYXRlcy1idW5kbGUiLCJwdWxsQ2hlY2tzdW0iOiJRMVNWQVd1V0hkUEh2YkJoTFRrQVo2MC8xV3NtST0iLCJwdWxsRGVwZW5kZW5jaWVzIjoiIiwic2l6ZSI6MTE5NzQ4LCJ1cmwiOiJodHRwczovL3d3dy5tb3ppbGxhLm9yZy9lbi1VUy9hYm91dC9nb3Zlcm5hbmNlL3BvbGljaWVzL3NlY3VyaXR5LWdyb3VwL2NlcnRzLyIsInZlcnNpb24iOiIyMDIxMTIyMC1yMCJ9LCJtZXRhZGF0YVR5cGUiOiJBcGtNZXRhZGF0YSIsIm5hbWUiOiJjYS1jZXJ0aWZpY2F0ZXMtYnVuZGxlIiwicHVybCI6InBrZzphbHBpbmUvY2EtY2VydGlmaWNhdGVzLWJ1bmRsZUAyMDIxMTIyMC1yMD9hcmNoPXg4Nl82NFx1MDAyNnVwc3RyZWFtPWNhLWNlcnRpZmljYXRlc1x1MDAyNmRpc3Rybz1hbHBpbmUtMy4xNS4yIiwidHlwZSI6ImFwayIsInZlcnNpb24iOiIyMDIxMTIyMC1yMCJ9LHsiY3BlcyI6WyJjcGU6Mi4zOmE6bGliYy11dGlsczpsaWJjLXV0aWxzOjAuNy4yLXIzOio6KjoqOio6KjoqOioiLCJjcGU6Mi4zOmE6bGliYy11dGlsczpsaWJjX3V0aWxzOjAuNy4yLXIzOio6KjoqOio6KjoqOioiLCJjcGU6Mi4zOmE6bGliY191dGlsczpsaWJjLXV0aWxzOjAuNy4yLXIzOio6KjoqOio6KjoqOioiLCJjcGU6Mi4zOmE6bGliY191dGlsczpsaWJjX3V0aWxzOjAuNy4yLXIzOio6KjoqOio6KjoqOioiLCJjcGU6Mi4zOmE6bGliYzpsaWJjLXV0aWxzOjAuNy4yLXIzOio6KjoqOio6KjoqOioiLCJjcGU6Mi4zOmE6bGliYzpsaWJjX3V0aWxzOjAuNy4yLXIzOio6KjoqOio6KjoqOioiXSwiZm91bmRCeSI6ImFwa2RiLWNhdGFsb2dlciIsImlkIjoiMTIzN2UwYzMxNWYyNjkwMiIsImxhbmd1YWdlIjoiIiwibGljZW5zZXMiOlsiQlNELTItQ2xhdXNlIiwiQU5EIiwiQlNELTMtQ2xhdXNlIl0sImxvY2F0aW9ucyI6W3sibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii9saWIvYXBrL2RiL2luc3RhbGxlZCJ9XSwibWV0YWRhdGEiOnsiYXJjaGl0ZWN0dXJlIjoieDg2XzY0IiwiZGVzY3JpcHRpb24iOiJNZXRhIHBhY2thZ2UgdG8gcHVsbCBpbiBjb3JyZWN0IGxpYmMiLCJmaWxlcyI6W10sImdpdENvbW1pdE9mQXBrUG9ydCI6IjYwNDI0MTMzYmUyZTc5YmJmZWZmM2Q1ODE0N2EyMjg4NmY4MTdjZTIiLCJpbnN0YWxsZWRTaXplIjo0MDk2LCJsaWNlbnNlIjoiQlNELTItQ2xhdXNlIEFORCBCU0QtMy1DbGF1c2UiLCJtYWludGFpbmVyIjoiTmF0YW5hZWwgQ29wYSBcdTAwM2NuY29wYUBhbHBpbmVsaW51eC5vcmdcdTAwM2UiLCJvcmlnaW5QYWNrYWdlIjoibGliYy1kZXYiLCJwYWNrYWdlIjoibGliYy11dGlscyIsInB1bGxDaGVja3N1bSI6IlExZVkzajY3Vi9QaWowQ0FnSFJwTmZJVG9KbHlJPSIsInB1bGxEZXBlbmRlbmNpZXMiOiJtdXNsLXV0aWxzIiwic2l6ZSI6MTQ4NSwidXJsIjoiaHR0cHM6Ly9hbHBpbmVsaW51eC5vcmciLCJ2ZXJzaW9uIjoiMC43LjItcjMifSwibWV0YWRhdGFUeXBlIjoiQXBrTWV0YWRhdGEiLCJuYW1lIjoibGliYy11dGlscyIsInB1cmwiOiJwa2c6YWxwaW5lL2xpYmMtdXRpbHNAMC43LjItcjM/YXJjaD14ODZfNjRcdTAwMjZ1cHN0cmVhbT1saWJjLWRldlx1MDAyNmRpc3Rybz1hbHBpbmUtMy4xNS4yIiwidHlwZSI6ImFwayIsInZlcnNpb24iOiIwLjcuMi1yMyJ9LHsiY3BlcyI6WyJjcGU6Mi4zOmE6bGliY3J5cHRvMS4xOmxpYmNyeXB0bzEuMToxLjEuMW4tcjA6KjoqOio6KjoqOio6KiJdLCJmb3VuZEJ5IjoiYXBrZGItY2F0YWxvZ2VyIiwiaWQiOiI3YTJjZjcyN2NiYWI4MDc0IiwibGFuZ3VhZ2UiOiIiLCJsaWNlbnNlcyI6WyJPcGVuU1NMIl0sImxvY2F0aW9ucyI6W3sibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii9saWIvYXBrL2RiL2luc3RhbGxlZCJ9XSwibWV0YWRhdGEiOnsiYXJjaGl0ZWN0dXJlIjoieDg2XzY0IiwiZGVzY3JpcHRpb24iOiJDcnlwdG8gbGlicmFyeSBmcm9tIG9wZW5zc2wiLCJmaWxlcyI6W3sicGF0aCI6Ii9ldGMifSx7InBhdGgiOiIvZXRjL3NzbDEuMSJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExT2VVeU9EWVdlMmhCd0JtMHF3czJvRFcvV1FjPSJ9LCJvd25lckdpZCI6IjAiLCJvd25lclVpZCI6IjAiLCJwYXRoIjoiL2V0Yy9zc2wxLjEvY2VydC5wZW0iLCJwZXJtaXNzaW9ucyI6Ijc3NyJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExTm9CRjdSTUlpVDlmQ1hMai9tYkRoK3BuTDlvPSJ9LCJvd25lckdpZCI6IjAiLCJvd25lclVpZCI6IjAiLCJwYXRoIjoiL2V0Yy9zc2wxLjEvY2VydHMiLCJwZXJtaXNzaW9ucyI6Ijc3NyJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExYTdWUFI4NXdydVhGbU5GWkUvREJhMFB5enEwPSJ9LCJvd25lckdpZCI6IjAiLCJvd25lclVpZCI6IjAiLCJwYXRoIjoiL2V0Yy9zc2wxLjEvY3RfbG9nX2xpc3QuY25mIiwicGVybWlzc2lvbnMiOiI3NzcifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMW9saDhUcGRBaTJRblRsNEZLM1RqZFVpU3dUbz0ifSwicGF0aCI6Ii9ldGMvc3NsMS4xL2N0X2xvZ19saXN0LmNuZi5kaXN0In0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTF3R3V4VkVPSzlpR0xqMWk4RDNCU0JuVDdNSkE9In0sInBhdGgiOiIvZXRjL3NzbDEuMS9vcGVuc3NsLmNuZiJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExd0d1eFZFT0s5aUdMajFpOEQzQlNCblQ3TUpBPSJ9LCJwYXRoIjoiL2V0Yy9zc2wxLjEvb3BlbnNzbC5jbmYuZGlzdCJ9LHsicGF0aCI6Ii9ldGMvc3NsMS4xL3ByaXZhdGUifSx7InBhdGgiOiIvbGliIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTEwMGhyT1lyY1d6Q3YwS0c4SG5Sd3BIT1RGc0U9In0sIm93bmVyR2lkIjoiMCIsIm93bmVyVWlkIjoiMCIsInBhdGgiOiIvbGliL2xpYmNyeXB0by5zby4xLjEiLCJwZXJtaXNzaW9ucyI6Ijc1NSJ9LHsicGF0aCI6Ii91c3IifSx7InBhdGgiOiIvdXNyL2xpYiJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExVDJzaStjN3RzN3NnRHhRWXZlNEIzaTFEZ28wPSJ9LCJvd25lckdpZCI6IjAiLCJvd25lclVpZCI6IjAiLCJwYXRoIjoiL3Vzci9saWIvbGliY3J5cHRvLnNvLjEuMSIsInBlcm1pc3Npb25zIjoiNzc3In0seyJwYXRoIjoiL3Vzci9saWIvZW5naW5lcy0xLjEifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMUxwbzIvUkxUZjVWbEhtVVFYeXY1YzFQT3BHWT0ifSwib3duZXJHaWQiOiIwIiwib3duZXJVaWQiOiIwIiwicGF0aCI6Ii91c3IvbGliL2VuZ2luZXMtMS4xL2FmYWxnLnNvIiwicGVybWlzc2lvbnMiOiI3NTUifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMWxsUHFIT3UzeWVBVEpoVm1heDNhSlY5dzQ4dz0ifSwib3duZXJHaWQiOiIwIiwib3duZXJVaWQiOiIwIiwicGF0aCI6Ii91c3IvbGliL2VuZ2luZXMtMS4xL2NhcGkuc28iLCJwZXJtaXNzaW9ucyI6Ijc1NSJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExL0doakVRMTdLU1ZiZ3lwRDA2VTVVMnN3a3NZPSJ9LCJvd25lckdpZCI6IjAiLCJvd25lclVpZCI6IjAiLCJwYXRoIjoiL3Vzci9saWIvZW5naW5lcy0xLjEvcGFkbG9jay5zbyIsInBlcm1pc3Npb25zIjoiNzU1In1dLCJnaXRDb21taXRPZkFwa1BvcnQiOiI0NTVlOTY2ODk5YTkzNThmYzk0ZjViY2U2MzNhZmU4YTE5NDIwOTVjIiwiaW5zdGFsbGVkU2l6ZSI6Mjc0MDIyNCwibGljZW5zZSI6Ik9wZW5TU0wiLCJtYWludGFpbmVyIjoiVGltbyBUZXJhcyBcdTAwM2N0aW1vLnRlcmFzQGlraS5maVx1MDAzZSIsIm9yaWdpblBhY2thZ2UiOiJvcGVuc3NsIiwicGFja2FnZSI6ImxpYmNyeXB0bzEuMSIsInB1bGxDaGVja3N1bSI6IlExckFzTGNiWTk2VCtUcW91ME1IMHlQUTExaEdRPSIsInB1bGxEZXBlbmRlbmNpZXMiOiJzbzpsaWJjLm11c2wteDg2XzY0LnNvLjEiLCJzaXplIjoxMjA4MjI4LCJ1cmwiOiJodHRwczovL3d3dy5vcGVuc3NsLm9yZy8iLCJ2ZXJzaW9uIjoiMS4xLjFuLXIwIn0sIm1ldGFkYXRhVHlwZSI6IkFwa01ldGFkYXRhIiwibmFtZSI6ImxpYmNyeXB0bzEuMSIsInB1cmwiOiJwa2c6YWxwaW5lL2xpYmNyeXB0bzEuMUAxLjEuMW4tcjA/YXJjaD14ODZfNjRcdTAwMjZ1cHN0cmVhbT1vcGVuc3NsXHUwMDI2ZGlzdHJvPWFscGluZS0zLjE1LjIiLCJ0eXBlIjoiYXBrIiwidmVyc2lvbiI6IjEuMS4xbi1yMCJ9LHsiY3BlcyI6WyJjcGU6Mi4zOmE6bGlicmV0bHM6bGlicmV0bHM6My4zLjQtcjM6KjoqOio6KjoqOio6KiJdLCJmb3VuZEJ5IjoiYXBrZGItY2F0YWxvZ2VyIiwiaWQiOiI5MWRiMTVkODA0ZmVkZTU5IiwibGFuZ3VhZ2UiOiIiLCJsaWNlbnNlcyI6WyJJU0MiLCJBTkQiLCIoQlNELTMtQ2xhdXNlIiwiT1IiLCJNSVQpIl0sImxvY2F0aW9ucyI6W3sibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii9saWIvYXBrL2RiL2luc3RhbGxlZCJ9XSwibWV0YWRhdGEiOnsiYXJjaGl0ZWN0dXJlIjoieDg2XzY0IiwiZGVzY3JpcHRpb24iOiJwb3J0IG9mIGxpYnRscyBmcm9tIGxpYnJlc3NsIHRvIG9wZW5zc2wiLCJmaWxlcyI6W3sicGF0aCI6Ii91c3IifSx7InBhdGgiOiIvdXNyL2xpYiJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExbk5FQzlUL3Q2VytFY20wRHhxTVVuUnZjVDZrPSJ9LCJvd25lckdpZCI6IjAiLCJvd25lclVpZCI6IjAiLCJwYXRoIjoiL3Vzci9saWIvbGlidGxzLnNvLjIiLCJwZXJtaXNzaW9ucyI6Ijc3NyJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExZVNXbm11c1NjbDZ3R2NrdGt3Mi84Y3I5ekZFPSJ9LCJvd25lckdpZCI6IjAiLCJvd25lclVpZCI6IjAiLCJwYXRoIjoiL3Vzci9saWIvbGlidGxzLnNvLjIuMC4zIiwicGVybWlzc2lvbnMiOiI3NTUifV0sImdpdENvbW1pdE9mQXBrUG9ydCI6IjkxYzdhOWYzYWEyOTZiNmQ0NjJjNTYzNGU3NjU4ZWJkYmZmNjViYjkiLCJpbnN0YWxsZWRTaXplIjo4NjAxNiwibGljZW5zZSI6IklTQyBBTkQgKEJTRC0zLUNsYXVzZSBPUiBNSVQpIiwibWFpbnRhaW5lciI6IkFyaWFkbmUgQ29uaWxsIFx1MDAzY2FyaWFkbmVAZGVyZWZlcmVuY2VkLm9yZ1x1MDAzZSIsIm9yaWdpblBhY2thZ2UiOiJsaWJyZXRscyIsInBhY2thZ2UiOiJsaWJyZXRscyIsInB1bGxDaGVja3N1bSI6IlExWjkvdjVVVnNSUmtyWU5kcTNwakZBYkN1Z1U4PSIsInB1bGxEZXBlbmRlbmNpZXMiOiJjYS1jZXJ0aWZpY2F0ZXMtYnVuZGxlIHNvOmxpYmMubXVzbC14ODZfNjQuc28uMSBzbzpsaWJjcnlwdG8uc28uMS4xIHNvOmxpYnNzbC5zby4xLjEiLCJzaXplIjoyOTE4NSwidXJsIjoiaHR0cHM6Ly9naXQuY2F1c2FsLmFnZW5jeS9saWJyZXRscy8iLCJ2ZXJzaW9uIjoiMy4zLjQtcjMifSwibWV0YWRhdGFUeXBlIjoiQXBrTWV0YWRhdGEiLCJuYW1lIjoibGlicmV0bHMiLCJwdXJsIjoicGtnOmFscGluZS9saWJyZXRsc0AzLjMuNC1yMz9hcmNoPXg4Nl82NFx1MDAyNnVwc3RyZWFtPWxpYnJldGxzXHUwMDI2ZGlzdHJvPWFscGluZS0zLjE1LjIiLCJ0eXBlIjoiYXBrIiwidmVyc2lvbiI6IjMuMy40LXIzIn0seyJjcGVzIjpbImNwZToyLjM6YTpsaWJzc2wxLjE6bGlic3NsMS4xOjEuMS4xbi1yMDoqOio6KjoqOio6KjoqIl0sImZvdW5kQnkiOiJhcGtkYi1jYXRhbG9nZXIiLCJpZCI6IjMwOTRjNGE2MTBiMGIwMGQiLCJsYW5ndWFnZSI6IiIsImxpY2Vuc2VzIjpbIk9wZW5TU0wiXSwibG9jYXRpb25zIjpbeyJsYXllcklEIjoic2hhMjU2OmZmNzY4YTE0MTNiYTEwOTMwOGMwZDg5N2ZmYTU1NWUwNTJhNDZkMmNmNDcxMTc4ZjAwMWI4MjViNGMyMWYzNTQiLCJwYXRoIjoiL2xpYi9hcGsvZGIvaW5zdGFsbGVkIn1dLCJtZXRhZGF0YSI6eyJhcmNoaXRlY3R1cmUiOiJ4ODZfNjQiLCJkZXNjcmlwdGlvbiI6IlNTTCBzaGFyZWQgbGlicmFyaWVzIiwiZmlsZXMiOlt7InBhdGgiOiIvbGliIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTF4TmpqN2p4dk9qM2xEUmQzc1JYekhvd1RVc1E9In0sIm93bmVyR2lkIjoiMCIsIm93bmVyVWlkIjoiMCIsInBhdGgiOiIvbGliL2xpYnNzbC5zby4xLjEiLCJwZXJtaXNzaW9ucyI6Ijc1NSJ9LHsicGF0aCI6Ii91c3IifSx7InBhdGgiOiIvdXNyL2xpYiJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExOGozNXBlM3lwNkhPZ01paDF3bEdQMS9tbTJjPSJ9LCJvd25lckdpZCI6IjAiLCJvd25lclVpZCI6IjAiLCJwYXRoIjoiL3Vzci9saWIvbGlic3NsLnNvLjEuMSIsInBlcm1pc3Npb25zIjoiNzc3In1dLCJnaXRDb21taXRPZkFwa1BvcnQiOiI0NTVlOTY2ODk5YTkzNThmYzk0ZjViY2U2MzNhZmU4YTE5NDIwOTVjIiwiaW5zdGFsbGVkU2l6ZSI6NTQwNjcyLCJsaWNlbnNlIjoiT3BlblNTTCIsIm1haW50YWluZXIiOiJUaW1vIFRlcmFzIFx1MDAzY3RpbW8udGVyYXNAaWtpLmZpXHUwMDNlIiwib3JpZ2luUGFja2FnZSI6Im9wZW5zc2wiLCJwYWNrYWdlIjoibGlic3NsMS4xIiwicHVsbENoZWNrc3VtIjoiUTEvS1owMHFESFdaNWNqM0FXRy9EUGRBQ1JOWUk9IiwicHVsbERlcGVuZGVuY2llcyI6InNvOmxpYmMubXVzbC14ODZfNjQuc28uMSBzbzpsaWJjcnlwdG8uc28uMS4xIiwic2l6ZSI6MjEzMjA5LCJ1cmwiOiJodHRwczovL3d3dy5vcGVuc3NsLm9yZy8iLCJ2ZXJzaW9uIjoiMS4xLjFuLXIwIn0sIm1ldGFkYXRhVHlwZSI6IkFwa01ldGFkYXRhIiwibmFtZSI6ImxpYnNzbDEuMSIsInB1cmwiOiJwa2c6YWxwaW5lL2xpYnNzbDEuMUAxLjEuMW4tcjA/YXJjaD14ODZfNjRcdTAwMjZ1cHN0cmVhbT1vcGVuc3NsXHUwMDI2ZGlzdHJvPWFscGluZS0zLjE1LjIiLCJ0eXBlIjoiYXBrIiwidmVyc2lvbiI6IjEuMS4xbi1yMCJ9LHsiY3BlcyI6WyJjcGU6Mi4zOmE6bXVzbDptdXNsOjEuMi4yLXI3Oio6KjoqOio6KjoqOioiXSwiZm91bmRCeSI6ImFwa2RiLWNhdGFsb2dlciIsImlkIjoiNGFjNzEzNmI4NTM2Y2RlYSIsImxhbmd1YWdlIjoiIiwibGljZW5zZXMiOlsiTUlUIl0sImxvY2F0aW9ucyI6W3sibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii9saWIvYXBrL2RiL2luc3RhbGxlZCJ9XSwibWV0YWRhdGEiOnsiYXJjaGl0ZWN0dXJlIjoieDg2XzY0IiwiZGVzY3JpcHRpb24iOiJ0aGUgbXVzbCBjIGxpYnJhcnkgKGxpYmMpIGltcGxlbWVudGF0aW9uIiwiZmlsZXMiOlt7InBhdGgiOiIvbGliIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTEyYWR3cVFPam85ZEZsK1ZKRDJFY2Q5MDF2aEU9In0sIm93bmVyR2lkIjoiMCIsIm93bmVyVWlkIjoiMCIsInBhdGgiOiIvbGliL2xkLW11c2wteDg2XzY0LnNvLjEiLCJwZXJtaXNzaW9ucyI6Ijc1NSJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExN3lKM0pGTnlwQTRteGhKSnIwb3U2Q3pzSlZJPSJ9LCJvd25lckdpZCI6IjAiLCJvd25lclVpZCI6IjAiLCJwYXRoIjoiL2xpYi9saWJjLm11c2wteDg2XzY0LnNvLjEiLCJwZXJtaXNzaW9ucyI6Ijc3NyJ9XSwiZ2l0Q29tbWl0T2ZBcGtQb3J0IjoiYmY1YmJmZGJmNzgwMDkyZjM4N2I3YWJlNDAxZmJmY2VkYTkwYzg0ZCIsImluc3RhbGxlZFNpemUiOjYyMjU5MiwibGljZW5zZSI6Ik1JVCIsIm1haW50YWluZXIiOiJUaW1vIFRlcsOkcyBcdTAwM2N0aW1vLnRlcmFzQGlraS5maVx1MDAzZSIsIm9yaWdpblBhY2thZ2UiOiJtdXNsIiwicGFja2FnZSI6Im11c2wiLCJwdWxsQ2hlY2tzdW0iOiJRMURlYjBqTnl0a3JqUFc0Ti9lS0xaNDNCd09sdz0iLCJwdWxsRGVwZW5kZW5jaWVzIjoiIiwic2l6ZSI6MzgzMTUyLCJ1cmwiOiJodHRwczovL211c2wubGliYy5vcmcvIiwidmVyc2lvbiI6IjEuMi4yLXI3In0sIm1ldGFkYXRhVHlwZSI6IkFwa01ldGFkYXRhIiwibmFtZSI6Im11c2wiLCJwdXJsIjoicGtnOmFscGluZS9tdXNsQDEuMi4yLXI3P2FyY2g9eDg2XzY0XHUwMDI2dXBzdHJlYW09bXVzbFx1MDAyNmRpc3Rybz1hbHBpbmUtMy4xNS4yIiwidHlwZSI6ImFwayIsInZlcnNpb24iOiIxLjIuMi1yNyJ9LHsiY3BlcyI6WyJjcGU6Mi4zOmE6bXVzbC11dGlsczptdXNsLXV0aWxzOjEuMi4yLXI3Oio6KjoqOio6KjoqOioiLCJjcGU6Mi4zOmE6bXVzbC11dGlsczptdXNsX3V0aWxzOjEuMi4yLXI3Oio6KjoqOio6KjoqOioiLCJjcGU6Mi4zOmE6bXVzbF91dGlsczptdXNsLXV0aWxzOjEuMi4yLXI3Oio6KjoqOio6KjoqOioiLCJjcGU6Mi4zOmE6bXVzbF91dGlsczptdXNsX3V0aWxzOjEuMi4yLXI3Oio6KjoqOio6KjoqOioiLCJjcGU6Mi4zOmE6bXVzbDptdXNsLXV0aWxzOjEuMi4yLXI3Oio6KjoqOio6KjoqOioiLCJjcGU6Mi4zOmE6bXVzbDptdXNsX3V0aWxzOjEuMi4yLXI3Oio6KjoqOio6KjoqOioiXSwiZm91bmRCeSI6ImFwa2RiLWNhdGFsb2dlciIsImlkIjoiNTNhOTA5ZjRkODcyYjkwIiwibGFuZ3VhZ2UiOiIiLCJsaWNlbnNlcyI6WyJNSVQiLCJCU0QiLCJHUEwyKyJdLCJsb2NhdGlvbnMiOlt7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvbGliL2Fway9kYi9pbnN0YWxsZWQifV0sIm1ldGFkYXRhIjp7ImFyY2hpdGVjdHVyZSI6Ing4Nl82NCIsImRlc2NyaXB0aW9uIjoidGhlIG11c2wgYyBsaWJyYXJ5IChsaWJjKSBpbXBsZW1lbnRhdGlvbiIsImZpbGVzIjpbeyJwYXRoIjoiL3NiaW4ifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMUtqYTIrUE9aS3hFa1VPWnF3U2pDNmttYUVEND0ifSwib3duZXJHaWQiOiIwIiwib3duZXJVaWQiOiIwIiwicGF0aCI6Ii9zYmluL2xkY29uZmlnIiwicGVybWlzc2lvbnMiOiI3NTUifSx7InBhdGgiOiIvdXNyIn0seyJwYXRoIjoiL3Vzci9iaW4ifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMU1pMjFCVGNMdE45Y1lQVjA3UDBhd0h5VDZYVT0ifSwib3duZXJHaWQiOiIwIiwib3duZXJVaWQiOiIwIiwicGF0aCI6Ii91c3IvYmluL2dldGNvbmYiLCJwZXJtaXNzaW9ucyI6Ijc1NSJ9LHsiZGlnZXN0Ijp7ImFsZ29yaXRobSI6InNoYTEiLCJ2YWx1ZSI6IlExblptREtLRlEydm9vSXRORExCbGVUOHg3T01BPSJ9LCJvd25lckdpZCI6IjAiLCJvd25lclVpZCI6IjAiLCJwYXRoIjoiL3Vzci9iaW4vZ2V0ZW50IiwicGVybWlzc2lvbnMiOiI3NTUifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMVE4VE9PZDVUbTJQdGtPNUVvb3d2aHZHQ0lKND0ifSwib3duZXJHaWQiOiIwIiwib3duZXJVaWQiOiIwIiwicGF0aCI6Ii91c3IvYmluL2ljb252IiwicGVybWlzc2lvbnMiOiI3NTUifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMXlGQWhHZ2dtTDdFUmdiSUE3S1F4eVR6ZjNrcz0ifSwib3duZXJHaWQiOiIwIiwib3duZXJVaWQiOiIwIiwicGF0aCI6Ii91c3IvYmluL2xkZCIsInBlcm1pc3Npb25zIjoiNzU1In1dLCJnaXRDb21taXRPZkFwa1BvcnQiOiJiZjViYmZkYmY3ODAwOTJmMzg3YjdhYmU0MDFmYmZjZWRhOTBjODRkIiwiaW5zdGFsbGVkU2l6ZSI6MTQzMzYwLCJsaWNlbnNlIjoiTUlUIEJTRCBHUEwyKyIsIm1haW50YWluZXIiOiJUaW1vIFRlcsOkcyBcdTAwM2N0aW1vLnRlcmFzQGlraS5maVx1MDAzZSIsIm9yaWdpblBhY2thZ2UiOiJtdXNsIiwicGFja2FnZSI6Im11c2wtdXRpbHMiLCJwdWxsQ2hlY2tzdW0iOiJRMVA1MGNmSmlTc0hvcXNZUlR5T0VPbEppTG4zbz0iLCJwdWxsRGVwZW5kZW5jaWVzIjoic2NhbmVsZiBzbzpsaWJjLm11c2wteDg2XzY0LnNvLjEiLCJzaXplIjozNjcyMywidXJsIjoiaHR0cHM6Ly9tdXNsLmxpYmMub3JnLyIsInZlcnNpb24iOiIxLjIuMi1yNyJ9LCJtZXRhZGF0YVR5cGUiOiJBcGtNZXRhZGF0YSIsIm5hbWUiOiJtdXNsLXV0aWxzIiwicHVybCI6InBrZzphbHBpbmUvbXVzbC11dGlsc0AxLjIuMi1yNz9hcmNoPXg4Nl82NFx1MDAyNnVwc3RyZWFtPW11c2xcdTAwMjZkaXN0cm89YWxwaW5lLTMuMTUuMiIsInR5cGUiOiJhcGsiLCJ2ZXJzaW9uIjoiMS4yLjItcjcifSx7ImNwZXMiOlsiY3BlOjIuMzphOnNjYW5lbGY6c2NhbmVsZjoxLjMuMy1yMDoqOio6KjoqOio6KjoqIl0sImZvdW5kQnkiOiJhcGtkYi1jYXRhbG9nZXIiLCJpZCI6IjFmMjhkZTEyMDA3M2Q3OGEiLCJsYW5ndWFnZSI6IiIsImxpY2Vuc2VzIjpbIkdQTC0yLjAtb25seSJdLCJsb2NhdGlvbnMiOlt7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvbGliL2Fway9kYi9pbnN0YWxsZWQifV0sIm1ldGFkYXRhIjp7ImFyY2hpdGVjdHVyZSI6Ing4Nl82NCIsImRlc2NyaXB0aW9uIjoiU2NhbiBFTEYgYmluYXJpZXMgZm9yIHN0dWZmIiwiZmlsZXMiOlt7InBhdGgiOiIvdXNyIn0seyJwYXRoIjoiL3Vzci9iaW4ifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMXNGZTU0UmJkZlQ0Q05pbVltNDFEMUR2K05zZz0ifSwib3duZXJHaWQiOiIwIiwib3duZXJVaWQiOiIwIiwicGF0aCI6Ii91c3IvYmluL3NjYW5lbGYiLCJwZXJtaXNzaW9ucyI6Ijc1NSJ9XSwiZ2l0Q29tbWl0T2ZBcGtQb3J0IjoiODZiM2Q0ZmJiMGE3NjBmZWJmMzQ3NmY5YTU4YWJmOGQwZjcyOGQ1YyIsImluc3RhbGxlZFNpemUiOjk0MjA4LCJsaWNlbnNlIjoiR1BMLTIuMC1vbmx5IiwibWFpbnRhaW5lciI6Ik5hdGFuYWVsIENvcGEgXHUwMDNjbmNvcGFAYWxwaW5lbGludXgub3JnXHUwMDNlIiwib3JpZ2luUGFja2FnZSI6InBheC11dGlscyIsInBhY2thZ2UiOiJzY2FuZWxmIiwicHVsbENoZWNrc3VtIjoiUTExL2RaRGtVSWNLVDNsbkhDTnBzeHRic0hOSm89IiwicHVsbERlcGVuZGVuY2llcyI6InNvOmxpYmMubXVzbC14ODZfNjQuc28uMSIsInNpemUiOjM2ODMwLCJ1cmwiOiJodHRwczovL3dpa2kuZ2VudG9vLm9yZy93aWtpL0hhcmRlbmVkL1BhWF9VdGlsaXRpZXMiLCJ2ZXJzaW9uIjoiMS4zLjMtcjAifSwibWV0YWRhdGFUeXBlIjoiQXBrTWV0YWRhdGEiLCJuYW1lIjoic2NhbmVsZiIsInB1cmwiOiJwa2c6YWxwaW5lL3NjYW5lbGZAMS4zLjMtcjA/YXJjaD14ODZfNjRcdTAwMjZ1cHN0cmVhbT1wYXgtdXRpbHNcdTAwMjZkaXN0cm89YWxwaW5lLTMuMTUuMiIsInR5cGUiOiJhcGsiLCJ2ZXJzaW9uIjoiMS4zLjMtcjAifSx7ImNwZXMiOlsiY3BlOjIuMzphOnNzbC1jbGllbnQ6c3NsLWNsaWVudDoxLjM0LjEtcjQ6KjoqOio6KjoqOio6KiIsImNwZToyLjM6YTpzc2wtY2xpZW50OnNzbF9jbGllbnQ6MS4zNC4xLXI0Oio6KjoqOio6KjoqOioiLCJjcGU6Mi4zOmE6c3NsX2NsaWVudDpzc2wtY2xpZW50OjEuMzQuMS1yNDoqOio6KjoqOio6KjoqIiwiY3BlOjIuMzphOnNzbF9jbGllbnQ6c3NsX2NsaWVudDoxLjM0LjEtcjQ6KjoqOio6KjoqOio6KiIsImNwZToyLjM6YTpzc2w6c3NsLWNsaWVudDoxLjM0LjEtcjQ6KjoqOio6KjoqOio6KiIsImNwZToyLjM6YTpzc2w6c3NsX2NsaWVudDoxLjM0LjEtcjQ6KjoqOio6KjoqOio6KiJdLCJmb3VuZEJ5IjoiYXBrZGItY2F0YWxvZ2VyIiwiaWQiOiJjMmUzY2U3YjllNzJkMGFlIiwibGFuZ3VhZ2UiOiIiLCJsaWNlbnNlcyI6WyJHUEwtMi4wLW9ubHkiXSwibG9jYXRpb25zIjpbeyJsYXllcklEIjoic2hhMjU2OmZmNzY4YTE0MTNiYTEwOTMwOGMwZDg5N2ZmYTU1NWUwNTJhNDZkMmNmNDcxMTc4ZjAwMWI4MjViNGMyMWYzNTQiLCJwYXRoIjoiL2xpYi9hcGsvZGIvaW5zdGFsbGVkIn1dLCJtZXRhZGF0YSI6eyJhcmNoaXRlY3R1cmUiOiJ4ODZfNjQiLCJkZXNjcmlwdGlvbiI6IkVYdGVybmFsIHNzbF9jbGllbnQgZm9yIGJ1c3lib3ggd2dldCIsImZpbGVzIjpbeyJwYXRoIjoiL3VzciJ9LHsicGF0aCI6Ii91c3IvYmluIn0seyJkaWdlc3QiOnsiYWxnb3JpdGhtIjoic2hhMSIsInZhbHVlIjoiUTE1dWVGL1AvRW1BVlZtVUoxUFhCQWtwQ0FYN2M9In0sIm93bmVyR2lkIjoiMCIsIm93bmVyVWlkIjoiMCIsInBhdGgiOiIvdXNyL2Jpbi9zc2xfY2xpZW50IiwicGVybWlzc2lvbnMiOiI3NTUifV0sImdpdENvbW1pdE9mQXBrUG9ydCI6ImExNjA1OThkNjJhMGFjNTU4NTFhYWQxOTI1MWMwMWExYmI1ZmIyMmMiLCJpbnN0YWxsZWRTaXplIjoyODY3MiwibGljZW5zZSI6IkdQTC0yLjAtb25seSIsIm1haW50YWluZXIiOiJOYXRhbmFlbCBDb3BhIFx1MDAzY25jb3BhQGFscGluZWxpbnV4Lm9yZ1x1MDAzZSIsIm9yaWdpblBhY2thZ2UiOiJidXN5Ym94IiwicGFja2FnZSI6InNzbF9jbGllbnQiLCJwdWxsQ2hlY2tzdW0iOiJRMTNHdzVIRWVMWmorUWhqN0hSbXd6aTBVT0Fndz0iLCJwdWxsRGVwZW5kZW5jaWVzIjoic286bGliYy5tdXNsLXg4Nl82NC5zby4xIHNvOmxpYnRscy5zby4yIiwic2l6ZSI6NDcwOSwidXJsIjoiaHR0cHM6Ly9idXN5Ym94Lm5ldC8iLCJ2ZXJzaW9uIjoiMS4zNC4xLXI0In0sIm1ldGFkYXRhVHlwZSI6IkFwa01ldGFkYXRhIiwibmFtZSI6InNzbF9jbGllbnQiLCJwdXJsIjoicGtnOmFscGluZS9zc2xfY2xpZW50QDEuMzQuMS1yND9hcmNoPXg4Nl82NFx1MDAyNnVwc3RyZWFtPWJ1c3lib3hcdTAwMjZkaXN0cm89YWxwaW5lLTMuMTUuMiIsInR5cGUiOiJhcGsiLCJ2ZXJzaW9uIjoiMS4zNC4xLXI0In0seyJjcGVzIjpbImNwZToyLjM6YTp6bGliOnpsaWI6MS4yLjExLXIzOio6KjoqOio6KjoqOioiXSwiZm91bmRCeSI6ImFwa2RiLWNhdGFsb2dlciIsImlkIjoiYmUwNmRmNmUzYmJiZjBhYiIsImxhbmd1YWdlIjoiIiwibGljZW5zZXMiOlsiWmxpYiJdLCJsb2NhdGlvbnMiOlt7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvbGliL2Fway9kYi9pbnN0YWxsZWQifV0sIm1ldGFkYXRhIjp7ImFyY2hpdGVjdHVyZSI6Ing4Nl82NCIsImRlc2NyaXB0aW9uIjoiQSBjb21wcmVzc2lvbi9kZWNvbXByZXNzaW9uIExpYnJhcnkiLCJmaWxlcyI6W3sicGF0aCI6Ii9saWIifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMWEySDhoUDI0cnlDQUdmOGZuYzFOaGE5SUlIYz0ifSwib3duZXJHaWQiOiIwIiwib3duZXJVaWQiOiIwIiwicGF0aCI6Ii9saWIvbGliei5zby4xIiwicGVybWlzc2lvbnMiOiI3NzcifSx7ImRpZ2VzdCI6eyJhbGdvcml0aG0iOiJzaGExIiwidmFsdWUiOiJRMU00T0hMVVlodkhvbHFnTUIwVGMza0VsSzdxQT0ifSwib3duZXJHaWQiOiIwIiwib3duZXJVaWQiOiIwIiwicGF0aCI6Ii9saWIvbGliei5zby4xLjIuMTEiLCJwZXJtaXNzaW9ucyI6Ijc1NSJ9XSwiZ2l0Q29tbWl0T2ZBcGtQb3J0IjoiMzg4YTRmYjM2NDBmOGNjYmQxOGUxMDVkZjNhZDc0MWRjYTQyNDdlMS1kaXJ0eSIsImluc3RhbGxlZFNpemUiOjExMDU5MiwibGljZW5zZSI6IlpsaWIiLCJtYWludGFpbmVyIjoiTmF0YW5hZWwgQ29wYSBcdTAwM2NuY29wYUBhbHBpbmVsaW51eC5vcmdcdTAwM2UiLCJvcmlnaW5QYWNrYWdlIjoiemxpYiIsInBhY2thZ2UiOiJ6bGliIiwicHVsbENoZWNrc3VtIjoiUTFXQm8rNTdKbGRsVmUwaVZ0Mm44SVA2K3ZOR0U9IiwicHVsbERlcGVuZGVuY2llcyI6InNvOmxpYmMubXVzbC14ODZfNjQuc28uMSIsInNpemUiOjUxNzQyLCJ1cmwiOiJodHRwczovL3psaWIubmV0LyIsInZlcnNpb24iOiIxLjIuMTEtcjMifSwibWV0YWRhdGFUeXBlIjoiQXBrTWV0YWRhdGEiLCJuYW1lIjoiemxpYiIsInB1cmwiOiJwa2c6YWxwaW5lL3psaWJAMS4yLjExLXIzP2FyY2g9eDg2XzY0XHUwMDI2dXBzdHJlYW09emxpYlx1MDAyNmRpc3Rybz1hbHBpbmUtMy4xNS4yIiwidHlwZSI6ImFwayIsInZlcnNpb24iOiIxLjIuMTEtcjMifV0sImRlc2NyaXB0b3IiOnsiY29uZmlndXJhdGlvbiI6eyJhbmNob3JlIjp7ImRvY2tlcmZpbGUiOiIiLCJob3N0IjoiIiwiaW1wb3J0LXRpbWVvdXQiOjMwLCJvdmVyd3JpdGUtZXhpc3RpbmctaW1hZ2UiOmZhbHNlLCJwYXRoIjoiIn0sImF0dGVzdCI6eyJrZXkiOiJjb3NpZ24ua2V5In0sImNoZWNrLWZvci1hcHAtdXBkYXRlIjp0cnVlLCJjb25maWdQYXRoIjoiIiwiZGV2Ijp7InByb2ZpbGUtY3B1IjpmYWxzZSwicHJvZmlsZS1tZW0iOmZhbHNlfSwiZXhjbHVkZSI6W10sImZpbGUiOiIiLCJmaWxlLWNsYXNzaWZpY2F0aW9uIjp7ImNhdGFsb2dlciI6eyJlbmFibGVkIjpmYWxzZSwic2NvcGUiOiJTcXVhc2hlZCJ9fSwiZmlsZS1jb250ZW50cyI6eyJjYXRhbG9nZXIiOnsiZW5hYmxlZCI6ZmFsc2UsInNjb3BlIjoiU3F1YXNoZWQifSwiZ2xvYnMiOltdLCJza2lwLWZpbGVzLWFib3ZlLXNpemUiOjEwNDg1NzZ9LCJmaWxlLW1ldGFkYXRhIjp7ImNhdGFsb2dlciI6eyJlbmFibGVkIjpmYWxzZSwic2NvcGUiOiJTcXVhc2hlZCJ9LCJkaWdlc3RzIjpbInNoYTI1NiJdfSwibG9nIjp7ImZpbGUtbG9jYXRpb24iOiIiLCJsZXZlbCI6ImVycm9yIiwic3RydWN0dXJlZCI6ZmFsc2V9LCJvdXRwdXQiOlsianNvbiJdLCJwYWNrYWdlIjp7ImNhdGFsb2dlciI6eyJlbmFibGVkIjp0cnVlLCJzY29wZSI6IlNxdWFzaGVkIn0sInNlYXJjaC1pbmRleGVkLWFyY2hpdmVzIjp0cnVlLCJzZWFyY2gtdW5pbmRleGVkLWFyY2hpdmVzIjpmYWxzZX0sInBsYXRmb3JtIjoiIiwicXVpZXQiOmZhbHNlLCJyZWdpc3RyeSI6eyJhdXRoIjpbXSwiaW5zZWN1cmUtc2tpcC10bHMtdmVyaWZ5IjpmYWxzZSwiaW5zZWN1cmUtdXNlLWh0dHAiOmZhbHNlfSwic2VjcmV0cyI6eyJhZGRpdGlvbmFsLXBhdHRlcm5zIjp7fSwiY2F0YWxvZ2VyIjp7ImVuYWJsZWQiOmZhbHNlLCJzY29wZSI6IkFsbExheWVycyJ9LCJleGNsdWRlLXBhdHRlcm4tbmFtZXMiOltdLCJyZXZlYWwtdmFsdWVzIjpmYWxzZSwic2tpcC1maWxlcy1hYm92ZS1zaXplIjoxMDQ4NTc2fX0sIm5hbWUiOiJzeWZ0IiwidmVyc2lvbiI6IjAuNDIuMSJ9LCJkaXN0cm8iOnsiYnVnUmVwb3J0VVJMIjoiaHR0cHM6Ly9idWdzLmFscGluZWxpbnV4Lm9yZy8iLCJob21lVVJMIjoiaHR0cHM6Ly9hbHBpbmVsaW51eC5vcmcvIiwiaWQiOiJhbHBpbmUiLCJuYW1lIjoiQWxwaW5lIExpbnV4IiwicHJldHR5TmFtZSI6IkFscGluZSBMaW51eCB2My4xNSIsInZlcnNpb25JRCI6IjMuMTUuMiJ9LCJmaWxlcyI6W3siaWQiOiI2YzczMTQ0ZWE5ZWY0ZmI5IiwibG9jYXRpb24iOnsibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii9iaW4vYnVzeWJveCJ9fSx7ImlkIjoiZTQxZGFkOGE2MWZmODNiNiIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvZXRjL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNGE2YTA4NDAucnNhLnB1YiJ9fSx7ImlkIjoiYzliZWM1M2MwYTNlMjEwNiIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvZXRjL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNTI0M2VmNGIucnNhLnB1YiJ9fSx7ImlkIjoiYTVkZmQyOTE0ODFjNjllMCIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvZXRjL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNTI2MWNlY2IucnNhLnB1YiJ9fSx7ImlkIjoiYzE1ZjkyZGRjNzdjMWFlNCIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvZXRjL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNjE2NWVlNTkucnNhLnB1YiJ9fSx7ImlkIjoiZDBjMjQ1M2Y5OGIzMTFkOCIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvZXRjL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNjE2NjZlM2YucnNhLnB1YiJ9fSx7ImlkIjoiMjgxNGM4ZjJkYWUyZTJlYSIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvZXRjL2Nyb250YWJzL3Jvb3QifX0seyJpZCI6ImM3OTVkM2Y3ZWJkNjRlODQiLCJsb2NhdGlvbiI6eyJsYXllcklEIjoic2hhMjU2OmZmNzY4YTE0MTNiYTEwOTMwOGMwZDg5N2ZmYTU1NWUwNTJhNDZkMmNmNDcxMTc4ZjAwMWI4MjViNGMyMWYzNTQiLCJwYXRoIjoiL2V0Yy9mc3RhYiJ9fSx7ImlkIjoiYjE3ZTBmZWQzY2I3MDJhZCIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvZXRjL2dyb3VwIn19LHsiaWQiOiJkZjIyZWQxYTMxZmRiYjY4IiwibG9jYXRpb24iOnsibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii9ldGMvaG9zdG5hbWUifX0seyJpZCI6ImU4NWFmMTRkNTAzMjU3NDQiLCJsb2NhdGlvbiI6eyJsYXllcklEIjoic2hhMjU2OmZmNzY4YTE0MTNiYTEwOTMwOGMwZDg5N2ZmYTU1NWUwNTJhNDZkMmNmNDcxMTc4ZjAwMWI4MjViNGMyMWYzNTQiLCJwYXRoIjoiL2V0Yy9ob3N0cyJ9fSx7ImlkIjoiZTkyNDY5NjBmZDQ4ZjI3MCIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvZXRjL2luaXR0YWIifX0seyJpZCI6IjlmYTk0ZWE4ZTg3NGVlNDkiLCJsb2NhdGlvbiI6eyJsYXllcklEIjoic2hhMjU2OmZmNzY4YTE0MTNiYTEwOTMwOGMwZDg5N2ZmYTU1NWUwNTJhNDZkMmNmNDcxMTc4ZjAwMWI4MjViNGMyMWYzNTQiLCJwYXRoIjoiL2V0Yy9sb2dyb3RhdGUuZC9hY3BpZCJ9fSx7ImlkIjoiZDFkMjNhNDczMTE5N2Q1ZSIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvZXRjL21vZHByb2JlLmQvYWxpYXNlcy5jb25mIn19LHsiaWQiOiJkYmU3MTZlMDVmOTExZmY5IiwibG9jYXRpb24iOnsibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii9ldGMvbW9kcHJvYmUuZC9ibGFja2xpc3QuY29uZiJ9fSx7ImlkIjoiNmVkYzk3MGYwMjQ2OWRlYSIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvZXRjL21vZHByb2JlLmQvaTM4Ni5jb25mIn19LHsiaWQiOiI5YTM1NzZlNGFhZGI4MmYxIiwibG9jYXRpb24iOnsibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii9ldGMvbW9kcHJvYmUuZC9rbXMuY29uZiJ9fSx7ImlkIjoiNWEyNWQwZGJlODBhYjI0IiwibG9jYXRpb24iOnsibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii9ldGMvbW9kdWxlcyJ9fSx7ImlkIjoiNTU1OGY3NjAzOGQxYzk3ZSIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvZXRjL21vdGQifX0seyJpZCI6IjU1OWE5ZDUxZTMzZDI0MTAiLCJsb2NhdGlvbiI6eyJsYXllcklEIjoic2hhMjU2OmZmNzY4YTE0MTNiYTEwOTMwOGMwZDg5N2ZmYTU1NWUwNTJhNDZkMmNmNDcxMTc4ZjAwMWI4MjViNGMyMWYzNTQiLCJwYXRoIjoiL2V0Yy9uZXR3b3JrL2lmLXVwLmQvZGFkIn19LHsiaWQiOiJhNzM5YTA0ZThjMmQ4ODk0IiwibG9jYXRpb24iOnsibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii9ldGMvcGFzc3dkIn19LHsiaWQiOiI4ZGIxNjIwNmIzMzI2MDZlIiwibG9jYXRpb24iOnsibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii9ldGMvcHJvZmlsZSJ9fSx7ImlkIjoiMzg3MTJhZGVhZWRiNzMxNiIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvZXRjL3Byb2ZpbGUuZC9SRUFETUUifX0seyJpZCI6IjE0ZGQ5ODVkY2U0Y2Q1YTUiLCJsb2NhdGlvbiI6eyJsYXllcklEIjoic2hhMjU2OmZmNzY4YTE0MTNiYTEwOTMwOGMwZDg5N2ZmYTU1NWUwNTJhNDZkMmNmNDcxMTc4ZjAwMWI4MjViNGMyMWYzNTQiLCJwYXRoIjoiL2V0Yy9wcm9maWxlLmQvY29sb3JfcHJvbXB0LnNoLmRpc2FibGVkIn19LHsiaWQiOiI1OTBjYzI2MzU4N2UyYzlkIiwibG9jYXRpb24iOnsibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii9ldGMvcHJvZmlsZS5kL2xvY2FsZS5zaCJ9fSx7ImlkIjoiMjZhMjBmMTA2ZmEzMjc4MyIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvZXRjL3Byb3RvY29scyJ9fSx7ImlkIjoiZDU3ZmUwZGU3OTBmY2Q0YSIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvZXRjL3NlY3VyZXR0eSJ9fSx7ImlkIjoiZjI5YWYyNDc1NTBkY2FjOCIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvZXRjL3NlcnZpY2VzIn19LHsiaWQiOiI0YTZkNmUwZWE5NTc1MWI3IiwibG9jYXRpb24iOnsibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii9ldGMvc2hhZG93In19LHsiaWQiOiIxY2UxMDM1ZDAwMjE3NmE0IiwibG9jYXRpb24iOnsibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii9ldGMvc2hlbGxzIn19LHsiaWQiOiI1YTg0NzZiZDZmNWExM2JmIiwibG9jYXRpb24iOnsibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii9ldGMvc3NsL2NlcnRzL2NhLWNlcnRpZmljYXRlcy5jcnQifX0seyJpZCI6ImJjNjYzNjJiMjVjNjQzNGEiLCJsb2NhdGlvbiI6eyJsYXllcklEIjoic2hhMjU2OmZmNzY4YTE0MTNiYTEwOTMwOGMwZDg5N2ZmYTU1NWUwNTJhNDZkMmNmNDcxMTc4ZjAwMWI4MjViNGMyMWYzNTQiLCJwYXRoIjoiL2V0Yy9zc2wxLjEvY3RfbG9nX2xpc3QuY25mLmRpc3QifX0seyJpZCI6ImRjNWNiYjc4Y2M3NDBmYmUiLCJsb2NhdGlvbiI6eyJsYXllcklEIjoic2hhMjU2OmZmNzY4YTE0MTNiYTEwOTMwOGMwZDg5N2ZmYTU1NWUwNTJhNDZkMmNmNDcxMTc4ZjAwMWI4MjViNGMyMWYzNTQiLCJwYXRoIjoiL2V0Yy9zc2wxLjEvb3BlbnNzbC5jbmYifX0seyJpZCI6ImRlYzIzOWI1NjVkMTliNjQiLCJsb2NhdGlvbiI6eyJsYXllcklEIjoic2hhMjU2OmZmNzY4YTE0MTNiYTEwOTMwOGMwZDg5N2ZmYTU1NWUwNTJhNDZkMmNmNDcxMTc4ZjAwMWI4MjViNGMyMWYzNTQiLCJwYXRoIjoiL2V0Yy9zc2wxLjEvb3BlbnNzbC5jbmYuZGlzdCJ9fSx7ImlkIjoiYjhmMWU0YTYyYjIxNjYzYiIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvZXRjL3N5c2N0bC5jb25mIn19LHsiaWQiOiI2YzZmOThjODU3YTY5MWNjIiwibG9jYXRpb24iOnsibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii9ldGMvdWRoY3BkLmNvbmYifX0seyJpZCI6IjRhY2E2ZDE1ZGYwOWRhMWQiLCJsb2NhdGlvbiI6eyJsYXllcklEIjoic2hhMjU2OmZmNzY4YTE0MTNiYTEwOTMwOGMwZDg5N2ZmYTU1NWUwNTJhNDZkMmNmNDcxMTc4ZjAwMWI4MjViNGMyMWYzNTQiLCJwYXRoIjoiL2xpYi9sZC1tdXNsLXg4Nl82NC5zby4xIn19LHsiaWQiOiI4NTI0ZGNmMDI5MzZkODE3IiwibG9jYXRpb24iOnsibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii9saWIvbGliYXBrLnNvLjMuMTIuMCJ9fSx7ImlkIjoiYmExYWQxNTM5NGM1MzkyMCIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvbGliL2xpYmNyeXB0by5zby4xLjEifX0seyJpZCI6IjZiMWM5YTE4Y2FiYWYzZWQiLCJsb2NhdGlvbiI6eyJsYXllcklEIjoic2hhMjU2OmZmNzY4YTE0MTNiYTEwOTMwOGMwZDg5N2ZmYTU1NWUwNTJhNDZkMmNmNDcxMTc4ZjAwMWI4MjViNGMyMWYzNTQiLCJwYXRoIjoiL2xpYi9saWJzc2wuc28uMS4xIn19LHsiaWQiOiIxM2ExYWJkMWQyZDM0YTU3IiwibG9jYXRpb24iOnsibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii9saWIvbGliei5zby4xLjIuMTEifX0seyJpZCI6IjQ1NmE3MDJmMTY3MzNiMjQiLCJsb2NhdGlvbiI6eyJsYXllcklEIjoic2hhMjU2OmZmNzY4YTE0MTNiYTEwOTMwOGMwZDg5N2ZmYTU1NWUwNTJhNDZkMmNmNDcxMTc4ZjAwMWI4MjViNGMyMWYzNTQiLCJwYXRoIjoiL2xpYi9zeXNjdGwuZC8wMC1hbHBpbmUuY29uZiJ9fSx7ImlkIjoiMmM2MjRmYmYzOWNhN2Q2NCIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvc2Jpbi9hcGsifX0seyJpZCI6IjIzNzAyNmQxMzRiODRkNmYiLCJsb2NhdGlvbiI6eyJsYXllcklEIjoic2hhMjU2OmZmNzY4YTE0MTNiYTEwOTMwOGMwZDg5N2ZmYTU1NWUwNTJhNDZkMmNmNDcxMTc4ZjAwMWI4MjViNGMyMWYzNTQiLCJwYXRoIjoiL3NiaW4vbGRjb25maWcifX0seyJpZCI6IjE0ZTNmNDFhNzdiNDI4MDciLCJsb2NhdGlvbiI6eyJsYXllcklEIjoic2hhMjU2OmZmNzY4YTE0MTNiYTEwOTMwOGMwZDg5N2ZmYTU1NWUwNTJhNDZkMmNmNDcxMTc4ZjAwMWI4MjViNGMyMWYzNTQiLCJwYXRoIjoiL3NiaW4vbWttbnRkaXJzIn19LHsiaWQiOiIzY2E3Y2JmZDU5MzBkOTBiIiwibG9jYXRpb24iOnsibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii91c3IvYmluL2dldGNvbmYifX0seyJpZCI6IjlhY2M0NmY5ZjlkMjQwNTgiLCJsb2NhdGlvbiI6eyJsYXllcklEIjoic2hhMjU2OmZmNzY4YTE0MTNiYTEwOTMwOGMwZDg5N2ZmYTU1NWUwNTJhNDZkMmNmNDcxMTc4ZjAwMWI4MjViNGMyMWYzNTQiLCJwYXRoIjoiL3Vzci9iaW4vZ2V0ZW50In19LHsiaWQiOiIxNjJmMzI0MDM2NTYxNjQzIiwibG9jYXRpb24iOnsibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii91c3IvYmluL2ljb252In19LHsiaWQiOiJhMjliNDViNWMwMmQwMDA3IiwibG9jYXRpb24iOnsibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii91c3IvYmluL2xkZCJ9fSx7ImlkIjoiMmFmNDEyYjEwZmYyYzVmYiIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvdXNyL2Jpbi9zY2FuZWxmIn19LHsiaWQiOiI5MTUwY2Y2ZGYxYWQ1Zjg2IiwibG9jYXRpb24iOnsibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii91c3IvYmluL3NzbF9jbGllbnQifX0seyJpZCI6ImU2MDU0MmZhMzNkYWViMDUiLCJsb2NhdGlvbiI6eyJsYXllcklEIjoic2hhMjU2OmZmNzY4YTE0MTNiYTEwOTMwOGMwZDg5N2ZmYTU1NWUwNTJhNDZkMmNmNDcxMTc4ZjAwMWI4MjViNGMyMWYzNTQiLCJwYXRoIjoiL3Vzci9saWIvZW5naW5lcy0xLjEvYWZhbGcuc28ifX0seyJpZCI6IjJkMjRjNDBhODhiNGMyZWMiLCJsb2NhdGlvbiI6eyJsYXllcklEIjoic2hhMjU2OmZmNzY4YTE0MTNiYTEwOTMwOGMwZDg5N2ZmYTU1NWUwNTJhNDZkMmNmNDcxMTc4ZjAwMWI4MjViNGMyMWYzNTQiLCJwYXRoIjoiL3Vzci9saWIvZW5naW5lcy0xLjEvY2FwaS5zbyJ9fSx7ImlkIjoiNWRiMzhkZGNlODkxMDVlNyIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvdXNyL2xpYi9lbmdpbmVzLTEuMS9wYWRsb2NrLnNvIn19LHsiaWQiOiIyYjAyZjY3OTBjNmVmN2U4IiwibG9jYXRpb24iOnsibGF5ZXJJRCI6InNoYTI1NjpmZjc2OGExNDEzYmExMDkzMDhjMGQ4OTdmZmE1NTVlMDUyYTQ2ZDJjZjQ3MTE3OGYwMDFiODI1YjRjMjFmMzU0IiwicGF0aCI6Ii91c3IvbGliL2xpYnRscy5zby4yLjAuMyJ9fSx7ImlkIjoiYTJiZjQxNDAyY2MwNDMxNSIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNGE2YTA4NDAucnNhLnB1YiJ9fSx7ImlkIjoiZGI0NjJiZDRjZTliYWVjNSIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNTI0M2VmNGIucnNhLnB1YiJ9fSx7ImlkIjoiYzVmMTI2MmE2NmJmMDQ4YSIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNTI0ZDI3YmIucnNhLnB1YiJ9fSx7ImlkIjoiZWUzYWQ5YzBhNGY1NGU5MyIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNTI2MWNlY2IucnNhLnB1YiJ9fSx7ImlkIjoiOTJkMzU4ZTcyNTZmMGI3YSIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNTgxOTlkY2MucnNhLnB1YiJ9fSx7ImlkIjoiNThmYmViYTc3NGViMmE1NiIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNThjYmI0NzYucnNhLnB1YiJ9fSx7ImlkIjoiZmM4MjgxZjNiZmQ1NzFlNSIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNThlNGYxN2QucnNhLnB1YiJ9fSx7ImlkIjoiZGMxZTA0M2M2NTFkZmI0YyIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNWU2OWNhNTAucnNhLnB1YiJ9fSx7ImlkIjoiNzZiNWFhODA1ZGMxYjE4ZiIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNjBhYzIwOTkucnNhLnB1YiJ9fSx7ImlkIjoiYTAwZjcxNjgxOWZhYTgxOSIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNjE2NWVlNTkucnNhLnB1YiJ9fSx7ImlkIjoiYTNlNzZmMjE5MzNiNTFjNCIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNjE2NjZlM2YucnNhLnB1YiJ9fSx7ImlkIjoiMTFkNGU3NjhjMmViNzYyZCIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNjE2YTk3MjQucnNhLnB1YiJ9fSx7ImlkIjoiNTIyYTYwMmU5YzEwMGU2MiIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNjE2YWJjMjMucnNhLnB1YiJ9fSx7ImlkIjoiYWM3YTBmMjFkZWMwM2ExYSIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNjE2YWMzYmMucnNhLnB1YiJ9fSx7ImlkIjoiMzkxMTQ1ZjBlZmQzOWI5NCIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNjE2YWRmZWIucnNhLnB1YiJ9fSx7ImlkIjoiMjk1YzI2M2IzZDFjODFkMiIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNjE2YWUzNTAucnNhLnB1YiJ9fSx7ImlkIjoiNGE5NmY4YzRjZjM0Njc1OSIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvdXNyL3NoYXJlL2Fway9rZXlzL2FscGluZS1kZXZlbEBsaXN0cy5hbHBpbmVsaW51eC5vcmctNjE2ZGIzMGQucnNhLnB1YiJ9fSx7ImlkIjoiYmIwODE4ZTZjZDI1Zjk1YSIsImxvY2F0aW9uIjp7ImxheWVySUQiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsInBhdGgiOiIvdXNyL3NoYXJlL3VkaGNwYy9kZWZhdWx0LnNjcmlwdCJ9fV0sInNjaGVtYSI6eyJ1cmwiOiJodHRwczovL3Jhdy5naXRodWJ1c2VyY29udGVudC5jb20vYW5jaG9yZS9zeWZ0L21haW4vc2NoZW1hL2pzb24vc2NoZW1hLTMuMi4xLmpzb24iLCJ2ZXJzaW9uIjoiMy4yLjEifSwic291cmNlIjp7InRhcmdldCI6eyJhcmNoaXRlY3R1cmUiOiIiLCJjb25maWciOiJleUpoY21Ob2FYUmxZM1IxY21VaU9pSmhiV1EyTkNJc0ltTnZibVpwWnlJNmV5SkliM04wYm1GdFpTSTZJaUlzSWtSdmJXRnBibTVoYldVaU9pSWlMQ0pWYzJWeUlqb2lJaXdpUVhSMFlXTm9VM1JrYVc0aU9tWmhiSE5sTENKQmRIUmhZMmhUZEdSdmRYUWlPbVpoYkhObExDSkJkSFJoWTJoVGRHUmxjbklpT21aaGJITmxMQ0pVZEhraU9tWmhiSE5sTENKUGNHVnVVM1JrYVc0aU9tWmhiSE5sTENKVGRHUnBiazl1WTJVaU9tWmhiSE5sTENKRmJuWWlPbHNpVUVGVVNEMHZkWE55TDJ4dlkyRnNMM05pYVc0NkwzVnpjaTlzYjJOaGJDOWlhVzQ2TDNWemNpOXpZbWx1T2k5MWMzSXZZbWx1T2k5elltbHVPaTlpYVc0aVhTd2lRMjFrSWpwYklpOWlhVzR2YzJnaVhTd2lTVzFoWjJVaU9pSnphR0V5TlRZNlpUaGxNRE5oWldVMlpqRTJaRGhsWW1GbU9UVmhORGs0TXpBd01UTTNNMkkwTkRjNU1HTmpPR1EwTW1NMVpUZzBZV1kwWmpObU9ERTBaVFF4TWpFeVppSXNJbFp2YkhWdFpYTWlPbTUxYkd3c0lsZHZjbXRwYm1kRWFYSWlPaUlpTENKRmJuUnllWEJ2YVc1MElqcHVkV3hzTENKUGJrSjFhV3hrSWpwdWRXeHNMQ0pNWVdKbGJITWlPbTUxYkd4OUxDSmpiMjUwWVdsdVpYSWlPaUprTXpJMk1qQXlOR0ZqTUdZd09EYzVNR1ppTWpNMlpXRXpNbU0xT1dGak1XWmtNamMzTldVeU1qYzFNR1UxWXpCaVlXWTFaV0UxWXpBeE1qTXhOR0ppSWl3aVkyOXVkR0ZwYm1WeVgyTnZibVpwWnlJNmV5SkliM04wYm1GdFpTSTZJbVF6TWpZeU1ESTBZV013WmlJc0lrUnZiV0ZwYm01aGJXVWlPaUlpTENKVmMyVnlJam9pSWl3aVFYUjBZV05vVTNSa2FXNGlPbVpoYkhObExDSkJkSFJoWTJoVGRHUnZkWFFpT21aaGJITmxMQ0pCZEhSaFkyaFRkR1JsY25JaU9tWmhiSE5sTENKVWRIa2lPbVpoYkhObExDSlBjR1Z1VTNSa2FXNGlPbVpoYkhObExDSlRkR1JwYms5dVkyVWlPbVpoYkhObExDSkZibllpT2xzaVVFRlVTRDB2ZFhOeUwyeHZZMkZzTDNOaWFXNDZMM1Z6Y2k5c2IyTmhiQzlpYVc0NkwzVnpjaTl6WW1sdU9pOTFjM0l2WW1sdU9pOXpZbWx1T2k5aWFXNGlYU3dpUTIxa0lqcGJJaTlpYVc0dmMyZ2lMQ0l0WXlJc0lpTW9ibTl3S1NBaUxDSkRUVVFnVzF3aUwySnBiaTl6YUZ3aVhTSmRMQ0pKYldGblpTSTZJbk5vWVRJMU5qcGxPR1V3TTJGbFpUWm1NVFprT0dWaVlXWTVOV0UwT1Rnek1EQXhNemN6WWpRME56a3dZMk00WkRReVl6VmxPRFJoWmpSbU0yWTRNVFJsTkRFeU1USm1JaXdpVm05c2RXMWxjeUk2Ym5Wc2JDd2lWMjl5YTJsdVowUnBjaUk2SWlJc0lrVnVkSEo1Y0c5cGJuUWlPbTUxYkd3c0lrOXVRblZwYkdRaU9tNTFiR3dzSWt4aFltVnNjeUk2ZTMxOUxDSmpjbVZoZEdWa0lqb2lNakF5TWkwd015MHlNMVF4TlRveU1Ub3lNUzR4TVRrNU1EY3pNVGxhSWl3aVpHOWphMlZ5WDNabGNuTnBiMjRpT2lJeU1DNHhNQzR4TWlJc0ltaHBjM1J2Y25raU9sdDdJbU55WldGMFpXUWlPaUl5TURJeUxUQXpMVEl6VkRFMU9qSXhPakl4TGpBeU16SXpOVGM0TmxvaUxDSmpjbVZoZEdWa1gySjVJam9pTDJKcGJpOXphQ0F0WXlBaktHNXZjQ2tnUVVSRUlHWnBiR1U2TnpNNE5tRmtPRGt6TmpjeU1EQTNZMk5oTW1RM00yTmxZekU0TmpKa05UZ3lZVFk1WkRVNE1XTmhNV1F4TlRWa05EVTVPV05pTW1GaE5UUmtOVFE1T0NCcGJpQXZJQ0o5TEhzaVkzSmxZWFJsWkNJNklqSXdNakl0TURNdE1qTlVNVFU2TWpFNk1qRXVNVEU1T1RBM016RTVXaUlzSW1OeVpXRjBaV1JmWW5raU9pSXZZbWx1TDNOb0lDMWpJQ01vYm05d0tTQWdRMDFFSUZ0Y0lpOWlhVzR2YzJoY0lsMGlMQ0psYlhCMGVWOXNZWGxsY2lJNmRISjFaWDFkTENKdmN5STZJbXhwYm5WNElpd2ljbTl2ZEdaeklqcDdJblI1Y0dVaU9pSnNZWGxsY25NaUxDSmthV1ptWDJsa2N5STZXeUp6YUdFeU5UWTZabVkzTmpoaE1UUXhNMkpoTVRBNU16QTRZekJrT0RrM1ptWmhOVFUxWlRBMU1tRTBObVF5WTJZME56RXhOemhtTURBeFlqZ3lOV0kwWXpJeFpqTTFOQ0pkZlgwPSIsImltYWdlSUQiOiJzaGEyNTY6OWM4NDJhYzQ5YTM5ZmU0MmU3MWE2MjMxODNmZTdmYjdjNzU5ZDU5MDI5ZTlhOGU3ODUxYzM1N2M3ZDhhODZmOCIsImltYWdlU2l6ZSI6NTU3MDE0NywibGF5ZXJzIjpbeyJkaWdlc3QiOiJzaGEyNTY6ZmY3NjhhMTQxM2JhMTA5MzA4YzBkODk3ZmZhNTU1ZTA1MmE0NmQyY2Y0NzExNzhmMDAxYjgyNWI0YzIxZjM1NCIsIm1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuaW1hZ2Uucm9vdGZzLmRpZmYudGFyLmd6aXAiLCJzaXplIjo1NTcwMTQ3fV0sIm1hbmlmZXN0IjoiZXdvZ0lDQWljMk5vWlcxaFZtVnljMmx2YmlJNklESXNDaUFnSUNKdFpXUnBZVlI1Y0dVaU9pQWlZWEJ3YkdsallYUnBiMjR2ZG01a0xtUnZZMnRsY2k1a2FYTjBjbWxpZFhScGIyNHViV0Z1YVdabGMzUXVkaklyYW5OdmJpSXNDaUFnSUNKamIyNW1hV2NpT2lCN0NpQWdJQ0FnSUNKdFpXUnBZVlI1Y0dVaU9pQWlZWEJ3YkdsallYUnBiMjR2ZG01a0xtUnZZMnRsY2k1amIyNTBZV2x1WlhJdWFXMWhaMlV1ZGpFcmFuTnZiaUlzQ2lBZ0lDQWdJQ0p6YVhwbElqb2dNVFEzTWl3S0lDQWdJQ0FnSW1ScFoyVnpkQ0k2SUNKemFHRXlOVFk2T1dNNE5ESmhZelE1WVRNNVptVTBNbVUzTVdFMk1qTXhPRE5tWlRkbVlqZGpOelU1WkRVNU1ESTVaVGxoT0dVM09EVXhZek0xTjJNM1pEaGhPRFptT0NJS0lDQWdmU3dLSUNBZ0lteGhlV1Z5Y3lJNklGc0tJQ0FnSUNBZ2V3b2dJQ0FnSUNBZ0lDQWliV1ZrYVdGVWVYQmxJam9nSW1Gd2NHeHBZMkYwYVc5dUwzWnVaQzVrYjJOclpYSXVhVzFoWjJVdWNtOXZkR1p6TG1ScFptWXVkR0Z5TG1kNmFYQWlMQW9nSUNBZ0lDQWdJQ0FpYzJsNlpTSTZJREk0TVRJMk9Ea3NDaUFnSUNBZ0lDQWdJQ0prYVdkbGMzUWlPaUFpYzJoaE1qVTJPak5oWVRSa01HSmlaR1V4T1RKaVptRmlZVGMxWmpKa01USTBaRGhqWmpKbE5tUmxORFV5WVdVd00yVTFOV1ExTkRFd05XVTBObUl3Tm1WaU9ERXlOMlVpQ2lBZ0lDQWdJSDBLSUNBZ1hRcDkiLCJtYW5pZmVzdERpZ2VzdCI6InNoYTI1Njo3M2MxNTU2OTZmZTY1YjY4Njk2ZTZlYTI0MDg4NjkzNTQ2YWM0NjhiM2UxNDU0MmYyM2YwZWZiZGUyODljYzk3IiwibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vdm5kLmRvY2tlci5kaXN0cmlidXRpb24ubWFuaWZlc3QudjIranNvbiIsIm9zIjoiIiwicmVwb0RpZ2VzdHMiOlsiaW5kZXguZG9ja2VyLmlvL2xpYnJhcnkvYWxwaW5lQHNoYTI1Njo2YWYxYjExYmJiMTdmNGMzMTFlMjY5ZGI2NTMwZTRkYTI3MzgyNjJhZjVmZDkwNjRjY2RmMTA5Yjc2NTg2MGZiIl0sInRhZ3MiOltdLCJ1c2VySW5wdXQiOiJhbHBpbmU6bGF0ZXN0In0sInR5cGUiOiJpbWFnZSJ9fX0=\",\"signatures\":[{\"keyid\":\"\",\"sig\":\"MEUCIQDBtal1MWSsNl8U1neDA1Ujec8HvbJ5T4tWtuFNY7OkrgIgFY+wklqhg6Y/HhivWlMmcA593sx6pNnusAqTLlNtIP0=\"}]}"
  },
  {
    "path": "grype/pkg/testdata/alpine.cdx.att.json",
    "content": "{\"payloadType\":\"application/vnd.in-toto+json\",\"payload\":\"eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjAuMSIsInByZWRpY2F0ZVR5cGUiOiJodHRwczovL2N5Y2xvbmVkeC5vcmcvYm9tIiwic3ViamVjdCI6W3sibmFtZSI6IiIsImRpZ2VzdCI6eyJzaGEyNTYiOiI0ZWRiZDJiZWI1Zjc4YjEwMTQwMjhmNGZiYjk5ZjMyMzdkOTU2MTEwMGI2ODgxYWFiYmY1YWNjZTJjNGY5NDU0In19XSwicHJlZGljYXRlIjp7ImJvbUZvcm1hdCI6IkN5Y2xvbmVEWCIsImNvbXBvbmVudHMiOlt7ImJvbS1yZWYiOiJwa2c6YWxwaW5lL2FscGluZS1iYXNlbGF5b3V0QDMuMi4wLXIxOD9hcmNoPXg4Nl82NFx1MDAyNnVwc3RyZWFtPWFscGluZS1iYXNlbGF5b3V0XHUwMDI2ZGlzdHJvPWFscGluZS0zLjE1LjRcdTAwMjZzeWZ0LWlkPTlmNTI3MjEzZjRkMmE4NzMiLCJjcGUiOiJjcGU6Mi4zOmE6YWxwaW5lLWJhc2VsYXlvdXQ6YWxwaW5lLWJhc2VsYXlvdXQ6My4yLjAtcjE4Oio6KjoqOio6KjoqOioiLCJkZXNjcmlwdGlvbiI6IkFscGluZSBiYXNlIGRpciBzdHJ1Y3R1cmUgYW5kIGluaXQgc2NyaXB0cyIsImV4dGVybmFsUmVmZXJlbmNlcyI6W3sidHlwZSI6ImRpc3RyaWJ1dGlvbiIsInVybCI6Imh0dHBzOi8vZ2l0LmFscGluZWxpbnV4Lm9yZy9jZ2l0L2Fwb3J0cy90cmVlL21haW4vYWxwaW5lLWJhc2VsYXlvdXQifV0sImxpY2Vuc2VzIjpbeyJsaWNlbnNlIjp7ImlkIjoiR1BMLTIuMC1vbmx5In19XSwibmFtZSI6ImFscGluZS1iYXNlbGF5b3V0IiwicHJvcGVydGllcyI6W3sibmFtZSI6InN5ZnQ6cGFja2FnZTpmb3VuZEJ5IiwidmFsdWUiOiJhcGtkYi1jYXRhbG9nZXIifSx7Im5hbWUiOiJzeWZ0OnBhY2thZ2U6bWV0YWRhdGFUeXBlIiwidmFsdWUiOiJBcGtNZXRhZGF0YSJ9LHsibmFtZSI6InN5ZnQ6cGFja2FnZTp0eXBlIiwidmFsdWUiOiJhcGsifSx7Im5hbWUiOiJzeWZ0OmNwZTIzIiwidmFsdWUiOiJjcGU6Mi4zOmE6YWxwaW5lLWJhc2VsYXlvdXQ6YWxwaW5lX2Jhc2VsYXlvdXQ6My4yLjAtcjE4Oio6KjoqOio6KjoqOioifSx7Im5hbWUiOiJzeWZ0OmNwZTIzIiwidmFsdWUiOiJjcGU6Mi4zOmE6YWxwaW5lX2Jhc2VsYXlvdXQ6YWxwaW5lLWJhc2VsYXlvdXQ6My4yLjAtcjE4Oio6KjoqOio6KjoqOioifSx7Im5hbWUiOiJzeWZ0OmNwZTIzIiwidmFsdWUiOiJjcGU6Mi4zOmE6YWxwaW5lX2Jhc2VsYXlvdXQ6YWxwaW5lX2Jhc2VsYXlvdXQ6My4yLjAtcjE4Oio6KjoqOio6KjoqOioifSx7Im5hbWUiOiJzeWZ0OmNwZTIzIiwidmFsdWUiOiJjcGU6Mi4zOmE6YWxwaW5lOmFscGluZS1iYXNlbGF5b3V0OjMuMi4wLXIxODoqOio6KjoqOio6KjoqIn0seyJuYW1lIjoic3lmdDpjcGUyMyIsInZhbHVlIjoiY3BlOjIuMzphOmFscGluZTphbHBpbmVfYmFzZWxheW91dDozLjIuMC1yMTg6KjoqOio6KjoqOio6KiJ9LHsibmFtZSI6InN5ZnQ6bG9jYXRpb246MDpsYXllcklEIiwidmFsdWUiOiJzaGEyNTY6NGZjMjQyZDU4Mjg1Njk5ZWNhMDVkYjNjYzdjNzEyMmEyYjhlMDE0ZDk0ODFmMzIzYmQ5Mjc3YmFhY2ZhMDYyOCJ9LHsibmFtZSI6InN5ZnQ6bG9jYXRpb246MDpwYXRoIiwidmFsdWUiOiIvbGliL2Fway9kYi9pbnN0YWxsZWQifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOmdpdENvbW1pdE9mQXBrUG9ydCIsInZhbHVlIjoiZGZhMTM3OTM1N2EzMjFlNjM4ZmVlZjFjZDhkNTVhYjAzZDAyMGY0NSJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6aW5zdGFsbGVkU2l6ZSIsInZhbHVlIjoiNDEzNjk2In0seyJuYW1lIjoic3lmdDptZXRhZGF0YTpvcmlnaW5QYWNrYWdlIiwidmFsdWUiOiJhbHBpbmUtYmFzZWxheW91dCJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6cHVsbENoZWNrc3VtIiwidmFsdWUiOiJRMUV5bVM2ckFnbUdzN1hZaHFkeUVvaVdnRVo2QT0ifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOnB1bGxEZXBlbmRlbmNpZXMiLCJ2YWx1ZSI6Ii9iaW4vc2ggc286bGliYy5tdXNsLXg4Nl82NC5zby4xIn0seyJuYW1lIjoic3lmdDptZXRhZGF0YTpzaXplIiwidmFsdWUiOiIyMTEwMSJ9XSwicHVibGlzaGVyIjoiTmF0YW5hZWwgQ29wYSBcdTAwM2NuY29wYUBhbHBpbmVsaW51eC5vcmdcdTAwM2UiLCJwdXJsIjoicGtnOmFscGluZS9hbHBpbmUtYmFzZWxheW91dEAzLjIuMC1yMTg/YXJjaD14ODZfNjRcdTAwMjZ1cHN0cmVhbT1hbHBpbmUtYmFzZWxheW91dFx1MDAyNmRpc3Rybz1hbHBpbmUtMy4xNS40IiwidHlwZSI6ImxpYnJhcnkiLCJ2ZXJzaW9uIjoiMy4yLjAtcjE4In0seyJib20tcmVmIjoicGtnOmFscGluZS9hbHBpbmUta2V5c0AyLjQtcjE/YXJjaD14ODZfNjRcdTAwMjZ1cHN0cmVhbT1hbHBpbmUta2V5c1x1MDAyNmRpc3Rybz1hbHBpbmUtMy4xNS40XHUwMDI2c3lmdC1pZD0xYTcyY2EzYjg4ZTFiNjdlIiwiY3BlIjoiY3BlOjIuMzphOmFscGluZS1rZXlzOmFscGluZS1rZXlzOjIuNC1yMToqOio6KjoqOio6KjoqIiwiZGVzY3JpcHRpb24iOiJQdWJsaWMga2V5cyBmb3IgQWxwaW5lIExpbnV4IHBhY2thZ2VzIiwiZXh0ZXJuYWxSZWZlcmVuY2VzIjpbeyJ0eXBlIjoiZGlzdHJpYnV0aW9uIiwidXJsIjoiaHR0cHM6Ly9hbHBpbmVsaW51eC5vcmcifV0sImxpY2Vuc2VzIjpbeyJsaWNlbnNlIjp7ImlkIjoiTUlUIn19XSwibmFtZSI6ImFscGluZS1rZXlzIiwicHJvcGVydGllcyI6W3sibmFtZSI6InN5ZnQ6cGFja2FnZTpmb3VuZEJ5IiwidmFsdWUiOiJhcGtkYi1jYXRhbG9nZXIifSx7Im5hbWUiOiJzeWZ0OnBhY2thZ2U6bWV0YWRhdGFUeXBlIiwidmFsdWUiOiJBcGtNZXRhZGF0YSJ9LHsibmFtZSI6InN5ZnQ6cGFja2FnZTp0eXBlIiwidmFsdWUiOiJhcGsifSx7Im5hbWUiOiJzeWZ0OmNwZTIzIiwidmFsdWUiOiJjcGU6Mi4zOmE6YWxwaW5lLWtleXM6YWxwaW5lX2tleXM6Mi40LXIxOio6KjoqOio6KjoqOioifSx7Im5hbWUiOiJzeWZ0OmNwZTIzIiwidmFsdWUiOiJjcGU6Mi4zOmE6YWxwaW5lX2tleXM6YWxwaW5lLWtleXM6Mi40LXIxOio6KjoqOio6KjoqOioifSx7Im5hbWUiOiJzeWZ0OmNwZTIzIiwidmFsdWUiOiJjcGU6Mi4zOmE6YWxwaW5lX2tleXM6YWxwaW5lX2tleXM6Mi40LXIxOio6KjoqOio6KjoqOioifSx7Im5hbWUiOiJzeWZ0OmNwZTIzIiwidmFsdWUiOiJjcGU6Mi4zOmE6YWxwaW5lOmFscGluZS1rZXlzOjIuNC1yMToqOio6KjoqOio6KjoqIn0seyJuYW1lIjoic3lmdDpjcGUyMyIsInZhbHVlIjoiY3BlOjIuMzphOmFscGluZTphbHBpbmVfa2V5czoyLjQtcjE6KjoqOio6KjoqOio6KiJ9LHsibmFtZSI6InN5ZnQ6bG9jYXRpb246MDpsYXllcklEIiwidmFsdWUiOiJzaGEyNTY6NGZjMjQyZDU4Mjg1Njk5ZWNhMDVkYjNjYzdjNzEyMmEyYjhlMDE0ZDk0ODFmMzIzYmQ5Mjc3YmFhY2ZhMDYyOCJ9LHsibmFtZSI6InN5ZnQ6bG9jYXRpb246MDpwYXRoIiwidmFsdWUiOiIvbGliL2Fway9kYi9pbnN0YWxsZWQifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOmdpdENvbW1pdE9mQXBrUG9ydCIsInZhbHVlIjoiYWFiNjhmOGM5YWI0MzRhNDY3MTBkZThlMTJmYjMyMDZlMjkzMGE1OSJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6aW5zdGFsbGVkU2l6ZSIsInZhbHVlIjoiMTU5NzQ0In0seyJuYW1lIjoic3lmdDptZXRhZGF0YTpvcmlnaW5QYWNrYWdlIiwidmFsdWUiOiJhbHBpbmUta2V5cyJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6cHVsbENoZWNrc3VtIiwidmFsdWUiOiJRMWtERjJzdEtvM2UvUnVtbEE4WnJSZkN3ZFN2OD0ifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOnNpemUiLCJ2YWx1ZSI6IjEzMzYyIn1dLCJwdWJsaXNoZXIiOiJOYXRhbmFlbCBDb3BhIFx1MDAzY25jb3BhQGFscGluZWxpbnV4Lm9yZ1x1MDAzZSIsInB1cmwiOiJwa2c6YWxwaW5lL2FscGluZS1rZXlzQDIuNC1yMT9hcmNoPXg4Nl82NFx1MDAyNnVwc3RyZWFtPWFscGluZS1rZXlzXHUwMDI2ZGlzdHJvPWFscGluZS0zLjE1LjQiLCJ0eXBlIjoibGlicmFyeSIsInZlcnNpb24iOiIyLjQtcjEifSx7ImJvbS1yZWYiOiJwa2c6YWxwaW5lL2Fway10b29sc0AyLjEyLjctcjM/YXJjaD14ODZfNjRcdTAwMjZ1cHN0cmVhbT1hcGstdG9vbHNcdTAwMjZkaXN0cm89YWxwaW5lLTMuMTUuNFx1MDAyNnN5ZnQtaWQ9MWM2ZTA1N2M2OTY1YmRkNiIsImNwZSI6ImNwZToyLjM6YTphcGstdG9vbHM6YXBrLXRvb2xzOjIuMTIuNy1yMzoqOio6KjoqOio6KjoqIiwiZGVzY3JpcHRpb24iOiJBbHBpbmUgUGFja2FnZSBLZWVwZXIgLSBwYWNrYWdlIG1hbmFnZXIgZm9yIGFscGluZSIsImV4dGVybmFsUmVmZXJlbmNlcyI6W3sidHlwZSI6ImRpc3RyaWJ1dGlvbiIsInVybCI6Imh0dHBzOi8vZ2l0bGFiLmFscGluZWxpbnV4Lm9yZy9hbHBpbmUvYXBrLXRvb2xzIn1dLCJsaWNlbnNlcyI6W3sibGljZW5zZSI6eyJpZCI6IkdQTC0yLjAtb25seSJ9fV0sIm5hbWUiOiJhcGstdG9vbHMiLCJwcm9wZXJ0aWVzIjpbeyJuYW1lIjoic3lmdDpwYWNrYWdlOmZvdW5kQnkiLCJ2YWx1ZSI6ImFwa2RiLWNhdGFsb2dlciJ9LHsibmFtZSI6InN5ZnQ6cGFja2FnZTptZXRhZGF0YVR5cGUiLCJ2YWx1ZSI6IkFwa01ldGFkYXRhIn0seyJuYW1lIjoic3lmdDpwYWNrYWdlOnR5cGUiLCJ2YWx1ZSI6ImFwayJ9LHsibmFtZSI6InN5ZnQ6Y3BlMjMiLCJ2YWx1ZSI6ImNwZToyLjM6YTphcGstdG9vbHM6YXBrX3Rvb2xzOjIuMTIuNy1yMzoqOio6KjoqOio6KjoqIn0seyJuYW1lIjoic3lmdDpjcGUyMyIsInZhbHVlIjoiY3BlOjIuMzphOmFwa190b29sczphcGstdG9vbHM6Mi4xMi43LXIzOio6KjoqOio6KjoqOioifSx7Im5hbWUiOiJzeWZ0OmNwZTIzIiwidmFsdWUiOiJjcGU6Mi4zOmE6YXBrX3Rvb2xzOmFwa190b29sczoyLjEyLjctcjM6KjoqOio6KjoqOio6KiJ9LHsibmFtZSI6InN5ZnQ6Y3BlMjMiLCJ2YWx1ZSI6ImNwZToyLjM6YTphcGs6YXBrLXRvb2xzOjIuMTIuNy1yMzoqOio6KjoqOio6KjoqIn0seyJuYW1lIjoic3lmdDpjcGUyMyIsInZhbHVlIjoiY3BlOjIuMzphOmFwazphcGtfdG9vbHM6Mi4xMi43LXIzOio6KjoqOio6KjoqOioifSx7Im5hbWUiOiJzeWZ0OmxvY2F0aW9uOjA6bGF5ZXJJRCIsInZhbHVlIjoic2hhMjU2OjRmYzI0MmQ1ODI4NTY5OWVjYTA1ZGIzY2M3YzcxMjJhMmI4ZTAxNGQ5NDgxZjMyM2JkOTI3N2JhYWNmYTA2MjgifSx7Im5hbWUiOiJzeWZ0OmxvY2F0aW9uOjA6cGF0aCIsInZhbHVlIjoiL2xpYi9hcGsvZGIvaW5zdGFsbGVkIn0seyJuYW1lIjoic3lmdDptZXRhZGF0YTpnaXRDb21taXRPZkFwa1BvcnQiLCJ2YWx1ZSI6IjFhYzNjMWJiMjllZWZmMDgzYzYyMWNmNmIyN2FkMTJhYjkzY2I3M2EifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOmluc3RhbGxlZFNpemUiLCJ2YWx1ZSI6IjMxMTI5NiJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6b3JpZ2luUGFja2FnZSIsInZhbHVlIjoiYXBrLXRvb2xzIn0seyJuYW1lIjoic3lmdDptZXRhZGF0YTpwdWxsQ2hlY2tzdW0iLCJ2YWx1ZSI6IlExM2ZQZCtGUlhhTHd5TmtsVm4rcXVGV0R5a25NPSJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6cHVsbERlcGVuZGVuY2llcyIsInZhbHVlIjoibXVzbFx1MDAzZT0xLjIgY2EtY2VydGlmaWNhdGVzLWJ1bmRsZSBzbzpsaWJjLm11c2wteDg2XzY0LnNvLjEgc286bGliY3J5cHRvLnNvLjEuMSBzbzpsaWJzc2wuc28uMS4xIHNvOmxpYnouc28uMSJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6c2l6ZSIsInZhbHVlIjoiMTIwMzc3In1dLCJwdWJsaXNoZXIiOiJOYXRhbmFlbCBDb3BhIFx1MDAzY25jb3BhQGFscGluZWxpbnV4Lm9yZ1x1MDAzZSIsInB1cmwiOiJwa2c6YWxwaW5lL2Fway10b29sc0AyLjEyLjctcjM/YXJjaD14ODZfNjRcdTAwMjZ1cHN0cmVhbT1hcGstdG9vbHNcdTAwMjZkaXN0cm89YWxwaW5lLTMuMTUuNCIsInR5cGUiOiJsaWJyYXJ5IiwidmVyc2lvbiI6IjIuMTIuNy1yMyJ9LHsiYm9tLXJlZiI6InBrZzphbHBpbmUvYnVzeWJveEAxLjM0LjEtcjU/YXJjaD14ODZfNjRcdTAwMjZ1cHN0cmVhbT1idXN5Ym94XHUwMDI2ZGlzdHJvPWFscGluZS0zLjE1LjRcdTAwMjZzeWZ0LWlkPWE1MjcwMjYxMmFhMTZkZTMiLCJjcGUiOiJjcGU6Mi4zOmE6YnVzeWJveDpidXN5Ym94OjEuMzQuMS1yNToqOio6KjoqOio6KjoqIiwiZGVzY3JpcHRpb24iOiJTaXplIG9wdGltaXplZCB0b29sYm94IG9mIG1hbnkgY29tbW9uIFVOSVggdXRpbGl0aWVzIiwiZXh0ZXJuYWxSZWZlcmVuY2VzIjpbeyJ0eXBlIjoiZGlzdHJpYnV0aW9uIiwidXJsIjoiaHR0cHM6Ly9idXN5Ym94Lm5ldC8ifV0sImxpY2Vuc2VzIjpbeyJsaWNlbnNlIjp7ImlkIjoiR1BMLTIuMC1vbmx5In19XSwibmFtZSI6ImJ1c3lib3giLCJwcm9wZXJ0aWVzIjpbeyJuYW1lIjoic3lmdDpwYWNrYWdlOmZvdW5kQnkiLCJ2YWx1ZSI6ImFwa2RiLWNhdGFsb2dlciJ9LHsibmFtZSI6InN5ZnQ6cGFja2FnZTptZXRhZGF0YVR5cGUiLCJ2YWx1ZSI6IkFwa01ldGFkYXRhIn0seyJuYW1lIjoic3lmdDpwYWNrYWdlOnR5cGUiLCJ2YWx1ZSI6ImFwayJ9LHsibmFtZSI6InN5ZnQ6bG9jYXRpb246MDpsYXllcklEIiwidmFsdWUiOiJzaGEyNTY6NGZjMjQyZDU4Mjg1Njk5ZWNhMDVkYjNjYzdjNzEyMmEyYjhlMDE0ZDk0ODFmMzIzYmQ5Mjc3YmFhY2ZhMDYyOCJ9LHsibmFtZSI6InN5ZnQ6bG9jYXRpb246MDpwYXRoIiwidmFsdWUiOiIvbGliL2Fway9kYi9pbnN0YWxsZWQifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOmdpdENvbW1pdE9mQXBrUG9ydCIsInZhbHVlIjoiMjc0NWRlN2UxYjA5ZTY2M2I0NzdhODE0MWI4NGY3ZDgxYTA0OTk2MyJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6aW5zdGFsbGVkU2l6ZSIsInZhbHVlIjoiOTQ2MTc2In0seyJuYW1lIjoic3lmdDptZXRhZGF0YTpvcmlnaW5QYWNrYWdlIiwidmFsdWUiOiJidXN5Ym94In0seyJuYW1lIjoic3lmdDptZXRhZGF0YTpwdWxsQ2hlY2tzdW0iLCJ2YWx1ZSI6IlExTFV5UEtJS3pVSzZ2cEpjQmorTzQwNGVmYXdnPSJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6cHVsbERlcGVuZGVuY2llcyIsInZhbHVlIjoic286bGliYy5tdXNsLXg4Nl82NC5zby4xIn0seyJuYW1lIjoic3lmdDptZXRhZGF0YTpzaXplIiwidmFsdWUiOiI1MDA2NzgifV0sInB1Ymxpc2hlciI6Ik5hdGFuYWVsIENvcGEgXHUwMDNjbmNvcGFAYWxwaW5lbGludXgub3JnXHUwMDNlIiwicHVybCI6InBrZzphbHBpbmUvYnVzeWJveEAxLjM0LjEtcjU/YXJjaD14ODZfNjRcdTAwMjZ1cHN0cmVhbT1idXN5Ym94XHUwMDI2ZGlzdHJvPWFscGluZS0zLjE1LjQiLCJ0eXBlIjoibGlicmFyeSIsInZlcnNpb24iOiIxLjM0LjEtcjUifSx7ImJvbS1yZWYiOiJwa2c6YWxwaW5lL2NhLWNlcnRpZmljYXRlcy1idW5kbGVAMjAyMTEyMjAtcjA/YXJjaD14ODZfNjRcdTAwMjZ1cHN0cmVhbT1jYS1jZXJ0aWZpY2F0ZXNcdTAwMjZkaXN0cm89YWxwaW5lLTMuMTUuNFx1MDAyNnN5ZnQtaWQ9MmM0NTIyMjkyM2QzMGZjNSIsImNwZSI6ImNwZToyLjM6YTpjYS1jZXJ0aWZpY2F0ZXMtYnVuZGxlOmNhLWNlcnRpZmljYXRlcy1idW5kbGU6MjAyMTEyMjAtcjA6KjoqOio6KjoqOio6KiIsImRlc2NyaXB0aW9uIjoiUHJlIGdlbmVyYXRlZCBidW5kbGUgb2YgTW96aWxsYSBjZXJ0aWZpY2F0ZXMiLCJleHRlcm5hbFJlZmVyZW5jZXMiOlt7InR5cGUiOiJkaXN0cmlidXRpb24iLCJ1cmwiOiJodHRwczovL3d3dy5tb3ppbGxhLm9yZy9lbi1VUy9hYm91dC9nb3Zlcm5hbmNlL3BvbGljaWVzL3NlY3VyaXR5LWdyb3VwL2NlcnRzLyJ9XSwibGljZW5zZXMiOlt7ImxpY2Vuc2UiOnsiaWQiOiJNUEwtMi4wIn19LHsibGljZW5zZSI6eyJpZCI6Ik1JVCJ9fV0sIm5hbWUiOiJjYS1jZXJ0aWZpY2F0ZXMtYnVuZGxlIiwicHJvcGVydGllcyI6W3sibmFtZSI6InN5ZnQ6cGFja2FnZTpmb3VuZEJ5IiwidmFsdWUiOiJhcGtkYi1jYXRhbG9nZXIifSx7Im5hbWUiOiJzeWZ0OnBhY2thZ2U6bWV0YWRhdGFUeXBlIiwidmFsdWUiOiJBcGtNZXRhZGF0YSJ9LHsibmFtZSI6InN5ZnQ6cGFja2FnZTp0eXBlIiwidmFsdWUiOiJhcGsifSx7Im5hbWUiOiJzeWZ0OmNwZTIzIiwidmFsdWUiOiJjcGU6Mi4zOmE6Y2EtY2VydGlmaWNhdGVzLWJ1bmRsZTpjYV9jZXJ0aWZpY2F0ZXNfYnVuZGxlOjIwMjExMjIwLXIwOio6KjoqOio6KjoqOioifSx7Im5hbWUiOiJzeWZ0OmNwZTIzIiwidmFsdWUiOiJjcGU6Mi4zOmE6Y2FfY2VydGlmaWNhdGVzX2J1bmRsZTpjYS1jZXJ0aWZpY2F0ZXMtYnVuZGxlOjIwMjExMjIwLXIwOio6KjoqOio6KjoqOioifSx7Im5hbWUiOiJzeWZ0OmNwZTIzIiwidmFsdWUiOiJjcGU6Mi4zOmE6Y2FfY2VydGlmaWNhdGVzX2J1bmRsZTpjYV9jZXJ0aWZpY2F0ZXNfYnVuZGxlOjIwMjExMjIwLXIwOio6KjoqOio6KjoqOioifSx7Im5hbWUiOiJzeWZ0OmNwZTIzIiwidmFsdWUiOiJjcGU6Mi4zOmE6Y2EtY2VydGlmaWNhdGVzOmNhLWNlcnRpZmljYXRlcy1idW5kbGU6MjAyMTEyMjAtcjA6KjoqOio6KjoqOio6KiJ9LHsibmFtZSI6InN5ZnQ6Y3BlMjMiLCJ2YWx1ZSI6ImNwZToyLjM6YTpjYS1jZXJ0aWZpY2F0ZXM6Y2FfY2VydGlmaWNhdGVzX2J1bmRsZToyMDIxMTIyMC1yMDoqOio6KjoqOio6KjoqIn0seyJuYW1lIjoic3lmdDpjcGUyMyIsInZhbHVlIjoiY3BlOjIuMzphOmNhX2NlcnRpZmljYXRlczpjYS1jZXJ0aWZpY2F0ZXMtYnVuZGxlOjIwMjExMjIwLXIwOio6KjoqOio6KjoqOioifSx7Im5hbWUiOiJzeWZ0OmNwZTIzIiwidmFsdWUiOiJjcGU6Mi4zOmE6Y2FfY2VydGlmaWNhdGVzOmNhX2NlcnRpZmljYXRlc19idW5kbGU6MjAyMTEyMjAtcjA6KjoqOio6KjoqOio6KiJ9LHsibmFtZSI6InN5ZnQ6Y3BlMjMiLCJ2YWx1ZSI6ImNwZToyLjM6YTpjYTpjYS1jZXJ0aWZpY2F0ZXMtYnVuZGxlOjIwMjExMjIwLXIwOio6KjoqOio6KjoqOioifSx7Im5hbWUiOiJzeWZ0OmNwZTIzIiwidmFsdWUiOiJjcGU6Mi4zOmE6Y2E6Y2FfY2VydGlmaWNhdGVzX2J1bmRsZToyMDIxMTIyMC1yMDoqOio6KjoqOio6KjoqIn0seyJuYW1lIjoic3lmdDpsb2NhdGlvbjowOmxheWVySUQiLCJ2YWx1ZSI6InNoYTI1Njo0ZmMyNDJkNTgyODU2OTllY2EwNWRiM2NjN2M3MTIyYTJiOGUwMTRkOTQ4MWYzMjNiZDkyNzdiYWFjZmEwNjI4In0seyJuYW1lIjoic3lmdDpsb2NhdGlvbjowOnBhdGgiLCJ2YWx1ZSI6Ii9saWIvYXBrL2RiL2luc3RhbGxlZCJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6Z2l0Q29tbWl0T2ZBcGtQb3J0IiwidmFsdWUiOiI3MDliNzBiY2I3MjczOGNmZWRjNTEwYmJhMDgxNDFiMDEyMDM4MTY3In0seyJuYW1lIjoic3lmdDptZXRhZGF0YTppbnN0YWxsZWRTaXplIiwidmFsdWUiOiIyMjExODQifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOm9yaWdpblBhY2thZ2UiLCJ2YWx1ZSI6ImNhLWNlcnRpZmljYXRlcyJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6cHVsbENoZWNrc3VtIiwidmFsdWUiOiJRMVNWQVd1V0hkUEh2YkJoTFRrQVo2MC8xV3NtST0ifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOnNpemUiLCJ2YWx1ZSI6IjExOTc0OCJ9XSwicHVibGlzaGVyIjoiTmF0YW5hZWwgQ29wYSBcdTAwM2NuY29wYUBhbHBpbmVsaW51eC5vcmdcdTAwM2UiLCJwdXJsIjoicGtnOmFscGluZS9jYS1jZXJ0aWZpY2F0ZXMtYnVuZGxlQDIwMjExMjIwLXIwP2FyY2g9eDg2XzY0XHUwMDI2dXBzdHJlYW09Y2EtY2VydGlmaWNhdGVzXHUwMDI2ZGlzdHJvPWFscGluZS0zLjE1LjQiLCJ0eXBlIjoibGlicmFyeSIsInZlcnNpb24iOiIyMDIxMTIyMC1yMCJ9LHsiYm9tLXJlZiI6InBrZzphbHBpbmUvbGliYy11dGlsc0AwLjcuMi1yMz9hcmNoPXg4Nl82NFx1MDAyNnVwc3RyZWFtPWxpYmMtZGV2XHUwMDI2ZGlzdHJvPWFscGluZS0zLjE1LjRcdTAwMjZzeWZ0LWlkPWU4N2E3OWZkYWVjYWFiZDIiLCJjcGUiOiJjcGU6Mi4zOmE6bGliYy11dGlsczpsaWJjLXV0aWxzOjAuNy4yLXIzOio6KjoqOio6KjoqOioiLCJkZXNjcmlwdGlvbiI6Ik1ldGEgcGFja2FnZSB0byBwdWxsIGluIGNvcnJlY3QgbGliYyIsImV4dGVybmFsUmVmZXJlbmNlcyI6W3sidHlwZSI6ImRpc3RyaWJ1dGlvbiIsInVybCI6Imh0dHBzOi8vYWxwaW5lbGludXgub3JnIn1dLCJsaWNlbnNlcyI6W3sibGljZW5zZSI6eyJpZCI6IkJTRC0yLUNsYXVzZSJ9fSx7ImxpY2Vuc2UiOnsiaWQiOiJCU0QtMy1DbGF1c2UifX1dLCJuYW1lIjoibGliYy11dGlscyIsInByb3BlcnRpZXMiOlt7Im5hbWUiOiJzeWZ0OnBhY2thZ2U6Zm91bmRCeSIsInZhbHVlIjoiYXBrZGItY2F0YWxvZ2VyIn0seyJuYW1lIjoic3lmdDpwYWNrYWdlOm1ldGFkYXRhVHlwZSIsInZhbHVlIjoiQXBrTWV0YWRhdGEifSx7Im5hbWUiOiJzeWZ0OnBhY2thZ2U6dHlwZSIsInZhbHVlIjoiYXBrIn0seyJuYW1lIjoic3lmdDpjcGUyMyIsInZhbHVlIjoiY3BlOjIuMzphOmxpYmMtdXRpbHM6bGliY191dGlsczowLjcuMi1yMzoqOio6KjoqOio6KjoqIn0seyJuYW1lIjoic3lmdDpjcGUyMyIsInZhbHVlIjoiY3BlOjIuMzphOmxpYmNfdXRpbHM6bGliYy11dGlsczowLjcuMi1yMzoqOio6KjoqOio6KjoqIn0seyJuYW1lIjoic3lmdDpjcGUyMyIsInZhbHVlIjoiY3BlOjIuMzphOmxpYmNfdXRpbHM6bGliY191dGlsczowLjcuMi1yMzoqOio6KjoqOio6KjoqIn0seyJuYW1lIjoic3lmdDpjcGUyMyIsInZhbHVlIjoiY3BlOjIuMzphOmxpYmM6bGliYy11dGlsczowLjcuMi1yMzoqOio6KjoqOio6KjoqIn0seyJuYW1lIjoic3lmdDpjcGUyMyIsInZhbHVlIjoiY3BlOjIuMzphOmxpYmM6bGliY191dGlsczowLjcuMi1yMzoqOio6KjoqOio6KjoqIn0seyJuYW1lIjoic3lmdDpsb2NhdGlvbjowOmxheWVySUQiLCJ2YWx1ZSI6InNoYTI1Njo0ZmMyNDJkNTgyODU2OTllY2EwNWRiM2NjN2M3MTIyYTJiOGUwMTRkOTQ4MWYzMjNiZDkyNzdiYWFjZmEwNjI4In0seyJuYW1lIjoic3lmdDpsb2NhdGlvbjowOnBhdGgiLCJ2YWx1ZSI6Ii9saWIvYXBrL2RiL2luc3RhbGxlZCJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6Z2l0Q29tbWl0T2ZBcGtQb3J0IiwidmFsdWUiOiI2MDQyNDEzM2JlMmU3OWJiZmVmZjNkNTgxNDdhMjI4ODZmODE3Y2UyIn0seyJuYW1lIjoic3lmdDptZXRhZGF0YTppbnN0YWxsZWRTaXplIiwidmFsdWUiOiI0MDk2In0seyJuYW1lIjoic3lmdDptZXRhZGF0YTpvcmlnaW5QYWNrYWdlIiwidmFsdWUiOiJsaWJjLWRldiJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6cHVsbENoZWNrc3VtIiwidmFsdWUiOiJRMWVZM2o2N1YvUGlqMENBZ0hScE5mSVRvSmx5ST0ifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOnB1bGxEZXBlbmRlbmNpZXMiLCJ2YWx1ZSI6Im11c2wtdXRpbHMifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOnNpemUiLCJ2YWx1ZSI6IjE0ODUifV0sInB1Ymxpc2hlciI6Ik5hdGFuYWVsIENvcGEgXHUwMDNjbmNvcGFAYWxwaW5lbGludXgub3JnXHUwMDNlIiwicHVybCI6InBrZzphbHBpbmUvbGliYy11dGlsc0AwLjcuMi1yMz9hcmNoPXg4Nl82NFx1MDAyNnVwc3RyZWFtPWxpYmMtZGV2XHUwMDI2ZGlzdHJvPWFscGluZS0zLjE1LjQiLCJ0eXBlIjoibGlicmFyeSIsInZlcnNpb24iOiIwLjcuMi1yMyJ9LHsiYm9tLXJlZiI6InBrZzphbHBpbmUvbGliY3J5cHRvMS4xQDEuMS4xbi1yMD9hcmNoPXg4Nl82NFx1MDAyNnVwc3RyZWFtPW9wZW5zc2xcdTAwMjZkaXN0cm89YWxwaW5lLTMuMTUuNFx1MDAyNnN5ZnQtaWQ9MTdjNmZiYjBjYWZiYmU1NyIsImNwZSI6ImNwZToyLjM6YTpsaWJjcnlwdG8xLjE6bGliY3J5cHRvMS4xOjEuMS4xbi1yMDoqOio6KjoqOio6KjoqIiwiZGVzY3JpcHRpb24iOiJDcnlwdG8gbGlicmFyeSBmcm9tIG9wZW5zc2wiLCJleHRlcm5hbFJlZmVyZW5jZXMiOlt7InR5cGUiOiJkaXN0cmlidXRpb24iLCJ1cmwiOiJodHRwczovL3d3dy5vcGVuc3NsLm9yZy8ifV0sImxpY2Vuc2VzIjpbeyJsaWNlbnNlIjp7ImlkIjoiT3BlblNTTCJ9fV0sIm5hbWUiOiJsaWJjcnlwdG8xLjEiLCJwcm9wZXJ0aWVzIjpbeyJuYW1lIjoic3lmdDpwYWNrYWdlOmZvdW5kQnkiLCJ2YWx1ZSI6ImFwa2RiLWNhdGFsb2dlciJ9LHsibmFtZSI6InN5ZnQ6cGFja2FnZTptZXRhZGF0YVR5cGUiLCJ2YWx1ZSI6IkFwa01ldGFkYXRhIn0seyJuYW1lIjoic3lmdDpwYWNrYWdlOnR5cGUiLCJ2YWx1ZSI6ImFwayJ9LHsibmFtZSI6InN5ZnQ6bG9jYXRpb246MDpsYXllcklEIiwidmFsdWUiOiJzaGEyNTY6NGZjMjQyZDU4Mjg1Njk5ZWNhMDVkYjNjYzdjNzEyMmEyYjhlMDE0ZDk0ODFmMzIzYmQ5Mjc3YmFhY2ZhMDYyOCJ9LHsibmFtZSI6InN5ZnQ6bG9jYXRpb246MDpwYXRoIiwidmFsdWUiOiIvbGliL2Fway9kYi9pbnN0YWxsZWQifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOmdpdENvbW1pdE9mQXBrUG9ydCIsInZhbHVlIjoiNDU1ZTk2Njg5OWE5MzU4ZmM5NGY1YmNlNjMzYWZlOGExOTQyMDk1YyJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6aW5zdGFsbGVkU2l6ZSIsInZhbHVlIjoiMjc0MDIyNCJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6b3JpZ2luUGFja2FnZSIsInZhbHVlIjoib3BlbnNzbCJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6cHVsbENoZWNrc3VtIiwidmFsdWUiOiJRMXJBc0xjYlk5NlQrVHFvdTBNSDB5UFExMWhHUT0ifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOnB1bGxEZXBlbmRlbmNpZXMiLCJ2YWx1ZSI6InNvOmxpYmMubXVzbC14ODZfNjQuc28uMSJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6c2l6ZSIsInZhbHVlIjoiMTIwODIyOCJ9XSwicHVibGlzaGVyIjoiVGltbyBUZXJhcyBcdTAwM2N0aW1vLnRlcmFzQGlraS5maVx1MDAzZSIsInB1cmwiOiJwa2c6YWxwaW5lL2xpYmNyeXB0bzEuMUAxLjEuMW4tcjA/YXJjaD14ODZfNjRcdTAwMjZ1cHN0cmVhbT1vcGVuc3NsXHUwMDI2ZGlzdHJvPWFscGluZS0zLjE1LjQiLCJ0eXBlIjoibGlicmFyeSIsInZlcnNpb24iOiIxLjEuMW4tcjAifSx7ImJvbS1yZWYiOiJwa2c6YWxwaW5lL2xpYnJldGxzQDMuMy40LXIzP2FyY2g9eDg2XzY0XHUwMDI2dXBzdHJlYW09bGlicmV0bHNcdTAwMjZkaXN0cm89YWxwaW5lLTMuMTUuNFx1MDAyNnN5ZnQtaWQ9NmQwNjU4YzJiNzIzN2VhMiIsImNwZSI6ImNwZToyLjM6YTpsaWJyZXRsczpsaWJyZXRsczozLjMuNC1yMzoqOio6KjoqOio6KjoqIiwiZGVzY3JpcHRpb24iOiJwb3J0IG9mIGxpYnRscyBmcm9tIGxpYnJlc3NsIHRvIG9wZW5zc2wiLCJleHRlcm5hbFJlZmVyZW5jZXMiOlt7InR5cGUiOiJkaXN0cmlidXRpb24iLCJ1cmwiOiJodHRwczovL2dpdC5jYXVzYWwuYWdlbmN5L2xpYnJldGxzLyJ9XSwibGljZW5zZXMiOlt7ImxpY2Vuc2UiOnsiaWQiOiJJU0MifX1dLCJuYW1lIjoibGlicmV0bHMiLCJwcm9wZXJ0aWVzIjpbeyJuYW1lIjoic3lmdDpwYWNrYWdlOmZvdW5kQnkiLCJ2YWx1ZSI6ImFwa2RiLWNhdGFsb2dlciJ9LHsibmFtZSI6InN5ZnQ6cGFja2FnZTptZXRhZGF0YVR5cGUiLCJ2YWx1ZSI6IkFwa01ldGFkYXRhIn0seyJuYW1lIjoic3lmdDpwYWNrYWdlOnR5cGUiLCJ2YWx1ZSI6ImFwayJ9LHsibmFtZSI6InN5ZnQ6bG9jYXRpb246MDpsYXllcklEIiwidmFsdWUiOiJzaGEyNTY6NGZjMjQyZDU4Mjg1Njk5ZWNhMDVkYjNjYzdjNzEyMmEyYjhlMDE0ZDk0ODFmMzIzYmQ5Mjc3YmFhY2ZhMDYyOCJ9LHsibmFtZSI6InN5ZnQ6bG9jYXRpb246MDpwYXRoIiwidmFsdWUiOiIvbGliL2Fway9kYi9pbnN0YWxsZWQifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOmdpdENvbW1pdE9mQXBrUG9ydCIsInZhbHVlIjoiOTFjN2E5ZjNhYTI5NmI2ZDQ2MmM1NjM0ZTc2NThlYmRiZmY2NWJiOSJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6aW5zdGFsbGVkU2l6ZSIsInZhbHVlIjoiODYwMTYifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOm9yaWdpblBhY2thZ2UiLCJ2YWx1ZSI6ImxpYnJldGxzIn0seyJuYW1lIjoic3lmdDptZXRhZGF0YTpwdWxsQ2hlY2tzdW0iLCJ2YWx1ZSI6IlExWjkvdjVVVnNSUmtyWU5kcTNwakZBYkN1Z1U4PSJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6cHVsbERlcGVuZGVuY2llcyIsInZhbHVlIjoiY2EtY2VydGlmaWNhdGVzLWJ1bmRsZSBzbzpsaWJjLm11c2wteDg2XzY0LnNvLjEgc286bGliY3J5cHRvLnNvLjEuMSBzbzpsaWJzc2wuc28uMS4xIn0seyJuYW1lIjoic3lmdDptZXRhZGF0YTpzaXplIiwidmFsdWUiOiIyOTE4NSJ9XSwicHVibGlzaGVyIjoiQXJpYWRuZSBDb25pbGwgXHUwMDNjYXJpYWRuZUBkZXJlZmVyZW5jZWQub3JnXHUwMDNlIiwicHVybCI6InBrZzphbHBpbmUvbGlicmV0bHNAMy4zLjQtcjM/YXJjaD14ODZfNjRcdTAwMjZ1cHN0cmVhbT1saWJyZXRsc1x1MDAyNmRpc3Rybz1hbHBpbmUtMy4xNS40IiwidHlwZSI6ImxpYnJhcnkiLCJ2ZXJzaW9uIjoiMy4zLjQtcjMifSx7ImJvbS1yZWYiOiJwa2c6YWxwaW5lL2xpYnNzbDEuMUAxLjEuMW4tcjA/YXJjaD14ODZfNjRcdTAwMjZ1cHN0cmVhbT1vcGVuc3NsXHUwMDI2ZGlzdHJvPWFscGluZS0zLjE1LjRcdTAwMjZzeWZ0LWlkPTRiMTA2ZTZjM2ZkNzRjMWMiLCJjcGUiOiJjcGU6Mi4zOmE6bGlic3NsMS4xOmxpYnNzbDEuMToxLjEuMW4tcjA6KjoqOio6KjoqOio6KiIsImRlc2NyaXB0aW9uIjoiU1NMIHNoYXJlZCBsaWJyYXJpZXMiLCJleHRlcm5hbFJlZmVyZW5jZXMiOlt7InR5cGUiOiJkaXN0cmlidXRpb24iLCJ1cmwiOiJodHRwczovL3d3dy5vcGVuc3NsLm9yZy8ifV0sImxpY2Vuc2VzIjpbeyJsaWNlbnNlIjp7ImlkIjoiT3BlblNTTCJ9fV0sIm5hbWUiOiJsaWJzc2wxLjEiLCJwcm9wZXJ0aWVzIjpbeyJuYW1lIjoic3lmdDpwYWNrYWdlOmZvdW5kQnkiLCJ2YWx1ZSI6ImFwa2RiLWNhdGFsb2dlciJ9LHsibmFtZSI6InN5ZnQ6cGFja2FnZTptZXRhZGF0YVR5cGUiLCJ2YWx1ZSI6IkFwa01ldGFkYXRhIn0seyJuYW1lIjoic3lmdDpwYWNrYWdlOnR5cGUiLCJ2YWx1ZSI6ImFwayJ9LHsibmFtZSI6InN5ZnQ6bG9jYXRpb246MDpsYXllcklEIiwidmFsdWUiOiJzaGEyNTY6NGZjMjQyZDU4Mjg1Njk5ZWNhMDVkYjNjYzdjNzEyMmEyYjhlMDE0ZDk0ODFmMzIzYmQ5Mjc3YmFhY2ZhMDYyOCJ9LHsibmFtZSI6InN5ZnQ6bG9jYXRpb246MDpwYXRoIiwidmFsdWUiOiIvbGliL2Fway9kYi9pbnN0YWxsZWQifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOmdpdENvbW1pdE9mQXBrUG9ydCIsInZhbHVlIjoiNDU1ZTk2Njg5OWE5MzU4ZmM5NGY1YmNlNjMzYWZlOGExOTQyMDk1YyJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6aW5zdGFsbGVkU2l6ZSIsInZhbHVlIjoiNTQwNjcyIn0seyJuYW1lIjoic3lmdDptZXRhZGF0YTpvcmlnaW5QYWNrYWdlIiwidmFsdWUiOiJvcGVuc3NsIn0seyJuYW1lIjoic3lmdDptZXRhZGF0YTpwdWxsQ2hlY2tzdW0iLCJ2YWx1ZSI6IlExL0taMDBxREhXWjVjajNBV0cvRFBkQUNSTllJPSJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6cHVsbERlcGVuZGVuY2llcyIsInZhbHVlIjoic286bGliYy5tdXNsLXg4Nl82NC5zby4xIHNvOmxpYmNyeXB0by5zby4xLjEifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOnNpemUiLCJ2YWx1ZSI6IjIxMzIwOSJ9XSwicHVibGlzaGVyIjoiVGltbyBUZXJhcyBcdTAwM2N0aW1vLnRlcmFzQGlraS5maVx1MDAzZSIsInB1cmwiOiJwa2c6YWxwaW5lL2xpYnNzbDEuMUAxLjEuMW4tcjA/YXJjaD14ODZfNjRcdTAwMjZ1cHN0cmVhbT1vcGVuc3NsXHUwMDI2ZGlzdHJvPWFscGluZS0zLjE1LjQiLCJ0eXBlIjoibGlicmFyeSIsInZlcnNpb24iOiIxLjEuMW4tcjAifSx7ImJvbS1yZWYiOiJwa2c6YWxwaW5lL211c2xAMS4yLjItcjc/YXJjaD14ODZfNjRcdTAwMjZ1cHN0cmVhbT1tdXNsXHUwMDI2ZGlzdHJvPWFscGluZS0zLjE1LjRcdTAwMjZzeWZ0LWlkPTIwZGMyMGNiYjZkYmVhNiIsImNwZSI6ImNwZToyLjM6YTptdXNsOm11c2w6MS4yLjItcjc6KjoqOio6KjoqOio6KiIsImRlc2NyaXB0aW9uIjoidGhlIG11c2wgYyBsaWJyYXJ5IChsaWJjKSBpbXBsZW1lbnRhdGlvbiIsImV4dGVybmFsUmVmZXJlbmNlcyI6W3sidHlwZSI6ImRpc3RyaWJ1dGlvbiIsInVybCI6Imh0dHBzOi8vbXVzbC5saWJjLm9yZy8ifV0sImxpY2Vuc2VzIjpbeyJsaWNlbnNlIjp7ImlkIjoiTUlUIn19XSwibmFtZSI6Im11c2wiLCJwcm9wZXJ0aWVzIjpbeyJuYW1lIjoic3lmdDpwYWNrYWdlOmZvdW5kQnkiLCJ2YWx1ZSI6ImFwa2RiLWNhdGFsb2dlciJ9LHsibmFtZSI6InN5ZnQ6cGFja2FnZTptZXRhZGF0YVR5cGUiLCJ2YWx1ZSI6IkFwa01ldGFkYXRhIn0seyJuYW1lIjoic3lmdDpwYWNrYWdlOnR5cGUiLCJ2YWx1ZSI6ImFwayJ9LHsibmFtZSI6InN5ZnQ6bG9jYXRpb246MDpsYXllcklEIiwidmFsdWUiOiJzaGEyNTY6NGZjMjQyZDU4Mjg1Njk5ZWNhMDVkYjNjYzdjNzEyMmEyYjhlMDE0ZDk0ODFmMzIzYmQ5Mjc3YmFhY2ZhMDYyOCJ9LHsibmFtZSI6InN5ZnQ6bG9jYXRpb246MDpwYXRoIiwidmFsdWUiOiIvbGliL2Fway9kYi9pbnN0YWxsZWQifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOmdpdENvbW1pdE9mQXBrUG9ydCIsInZhbHVlIjoiYmY1YmJmZGJmNzgwMDkyZjM4N2I3YWJlNDAxZmJmY2VkYTkwYzg0ZCJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6aW5zdGFsbGVkU2l6ZSIsInZhbHVlIjoiNjIyNTkyIn0seyJuYW1lIjoic3lmdDptZXRhZGF0YTpvcmlnaW5QYWNrYWdlIiwidmFsdWUiOiJtdXNsIn0seyJuYW1lIjoic3lmdDptZXRhZGF0YTpwdWxsQ2hlY2tzdW0iLCJ2YWx1ZSI6IlExRGViMGpOeXRrcmpQVzROL2VLTFo0M0J3T2x3PSJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6c2l6ZSIsInZhbHVlIjoiMzgzMTUyIn1dLCJwdWJsaXNoZXIiOiJUaW1vIFRlcsOkcyBcdTAwM2N0aW1vLnRlcmFzQGlraS5maVx1MDAzZSIsInB1cmwiOiJwa2c6YWxwaW5lL211c2xAMS4yLjItcjc/YXJjaD14ODZfNjRcdTAwMjZ1cHN0cmVhbT1tdXNsXHUwMDI2ZGlzdHJvPWFscGluZS0zLjE1LjQiLCJ0eXBlIjoibGlicmFyeSIsInZlcnNpb24iOiIxLjIuMi1yNyJ9LHsiYm9tLXJlZiI6InBrZzphbHBpbmUvbXVzbC11dGlsc0AxLjIuMi1yNz9hcmNoPXg4Nl82NFx1MDAyNnVwc3RyZWFtPW11c2xcdTAwMjZkaXN0cm89YWxwaW5lLTMuMTUuNFx1MDAyNnN5ZnQtaWQ9MzVjMzY4MDU3N2ZhZTBkZiIsImNwZSI6ImNwZToyLjM6YTptdXNsLXV0aWxzOm11c2wtdXRpbHM6MS4yLjItcjc6KjoqOio6KjoqOio6KiIsImRlc2NyaXB0aW9uIjoidGhlIG11c2wgYyBsaWJyYXJ5IChsaWJjKSBpbXBsZW1lbnRhdGlvbiIsImV4dGVybmFsUmVmZXJlbmNlcyI6W3sidHlwZSI6ImRpc3RyaWJ1dGlvbiIsInVybCI6Imh0dHBzOi8vbXVzbC5saWJjLm9yZy8ifV0sImxpY2Vuc2VzIjpbeyJsaWNlbnNlIjp7ImlkIjoiTUlUIn19XSwibmFtZSI6Im11c2wtdXRpbHMiLCJwcm9wZXJ0aWVzIjpbeyJuYW1lIjoic3lmdDpwYWNrYWdlOmZvdW5kQnkiLCJ2YWx1ZSI6ImFwa2RiLWNhdGFsb2dlciJ9LHsibmFtZSI6InN5ZnQ6cGFja2FnZTptZXRhZGF0YVR5cGUiLCJ2YWx1ZSI6IkFwa01ldGFkYXRhIn0seyJuYW1lIjoic3lmdDpwYWNrYWdlOnR5cGUiLCJ2YWx1ZSI6ImFwayJ9LHsibmFtZSI6InN5ZnQ6Y3BlMjMiLCJ2YWx1ZSI6ImNwZToyLjM6YTptdXNsLXV0aWxzOm11c2xfdXRpbHM6MS4yLjItcjc6KjoqOio6KjoqOio6KiJ9LHsibmFtZSI6InN5ZnQ6Y3BlMjMiLCJ2YWx1ZSI6ImNwZToyLjM6YTptdXNsX3V0aWxzOm11c2wtdXRpbHM6MS4yLjItcjc6KjoqOio6KjoqOio6KiJ9LHsibmFtZSI6InN5ZnQ6Y3BlMjMiLCJ2YWx1ZSI6ImNwZToyLjM6YTptdXNsX3V0aWxzOm11c2xfdXRpbHM6MS4yLjItcjc6KjoqOio6KjoqOio6KiJ9LHsibmFtZSI6InN5ZnQ6Y3BlMjMiLCJ2YWx1ZSI6ImNwZToyLjM6YTptdXNsOm11c2wtdXRpbHM6MS4yLjItcjc6KjoqOio6KjoqOio6KiJ9LHsibmFtZSI6InN5ZnQ6Y3BlMjMiLCJ2YWx1ZSI6ImNwZToyLjM6YTptdXNsOm11c2xfdXRpbHM6MS4yLjItcjc6KjoqOio6KjoqOio6KiJ9LHsibmFtZSI6InN5ZnQ6bG9jYXRpb246MDpsYXllcklEIiwidmFsdWUiOiJzaGEyNTY6NGZjMjQyZDU4Mjg1Njk5ZWNhMDVkYjNjYzdjNzEyMmEyYjhlMDE0ZDk0ODFmMzIzYmQ5Mjc3YmFhY2ZhMDYyOCJ9LHsibmFtZSI6InN5ZnQ6bG9jYXRpb246MDpwYXRoIiwidmFsdWUiOiIvbGliL2Fway9kYi9pbnN0YWxsZWQifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOmdpdENvbW1pdE9mQXBrUG9ydCIsInZhbHVlIjoiYmY1YmJmZGJmNzgwMDkyZjM4N2I3YWJlNDAxZmJmY2VkYTkwYzg0ZCJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6aW5zdGFsbGVkU2l6ZSIsInZhbHVlIjoiMTQzMzYwIn0seyJuYW1lIjoic3lmdDptZXRhZGF0YTpvcmlnaW5QYWNrYWdlIiwidmFsdWUiOiJtdXNsIn0seyJuYW1lIjoic3lmdDptZXRhZGF0YTpwdWxsQ2hlY2tzdW0iLCJ2YWx1ZSI6IlExUDUwY2ZKaVNzSG9xc1lSVHlPRU9sSmlMbjNvPSJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6cHVsbERlcGVuZGVuY2llcyIsInZhbHVlIjoic2NhbmVsZiBzbzpsaWJjLm11c2wteDg2XzY0LnNvLjEifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOnNpemUiLCJ2YWx1ZSI6IjM2NzIzIn1dLCJwdWJsaXNoZXIiOiJUaW1vIFRlcsOkcyBcdTAwM2N0aW1vLnRlcmFzQGlraS5maVx1MDAzZSIsInB1cmwiOiJwa2c6YWxwaW5lL211c2wtdXRpbHNAMS4yLjItcjc/YXJjaD14ODZfNjRcdTAwMjZ1cHN0cmVhbT1tdXNsXHUwMDI2ZGlzdHJvPWFscGluZS0zLjE1LjQiLCJ0eXBlIjoibGlicmFyeSIsInZlcnNpb24iOiIxLjIuMi1yNyJ9LHsiYm9tLXJlZiI6InBrZzphbHBpbmUvc2NhbmVsZkAxLjMuMy1yMD9hcmNoPXg4Nl82NFx1MDAyNnVwc3RyZWFtPXBheC11dGlsc1x1MDAyNmRpc3Rybz1hbHBpbmUtMy4xNS40XHUwMDI2c3lmdC1pZD1mMmQ0MjYzNzIzNTY2MDJkIiwiY3BlIjoiY3BlOjIuMzphOnNjYW5lbGY6c2NhbmVsZjoxLjMuMy1yMDoqOio6KjoqOio6KjoqIiwiZGVzY3JpcHRpb24iOiJTY2FuIEVMRiBiaW5hcmllcyBmb3Igc3R1ZmYiLCJleHRlcm5hbFJlZmVyZW5jZXMiOlt7InR5cGUiOiJkaXN0cmlidXRpb24iLCJ1cmwiOiJodHRwczovL3dpa2kuZ2VudG9vLm9yZy93aWtpL0hhcmRlbmVkL1BhWF9VdGlsaXRpZXMifV0sImxpY2Vuc2VzIjpbeyJsaWNlbnNlIjp7ImlkIjoiR1BMLTIuMC1vbmx5In19XSwibmFtZSI6InNjYW5lbGYiLCJwcm9wZXJ0aWVzIjpbeyJuYW1lIjoic3lmdDpwYWNrYWdlOmZvdW5kQnkiLCJ2YWx1ZSI6ImFwa2RiLWNhdGFsb2dlciJ9LHsibmFtZSI6InN5ZnQ6cGFja2FnZTptZXRhZGF0YVR5cGUiLCJ2YWx1ZSI6IkFwa01ldGFkYXRhIn0seyJuYW1lIjoic3lmdDpwYWNrYWdlOnR5cGUiLCJ2YWx1ZSI6ImFwayJ9LHsibmFtZSI6InN5ZnQ6bG9jYXRpb246MDpsYXllcklEIiwidmFsdWUiOiJzaGEyNTY6NGZjMjQyZDU4Mjg1Njk5ZWNhMDVkYjNjYzdjNzEyMmEyYjhlMDE0ZDk0ODFmMzIzYmQ5Mjc3YmFhY2ZhMDYyOCJ9LHsibmFtZSI6InN5ZnQ6bG9jYXRpb246MDpwYXRoIiwidmFsdWUiOiIvbGliL2Fway9kYi9pbnN0YWxsZWQifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOmdpdENvbW1pdE9mQXBrUG9ydCIsInZhbHVlIjoiODZiM2Q0ZmJiMGE3NjBmZWJmMzQ3NmY5YTU4YWJmOGQwZjcyOGQ1YyJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6aW5zdGFsbGVkU2l6ZSIsInZhbHVlIjoiOTQyMDgifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOm9yaWdpblBhY2thZ2UiLCJ2YWx1ZSI6InBheC11dGlscyJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6cHVsbENoZWNrc3VtIiwidmFsdWUiOiJRMTEvZFpEa1VJY0tUM2xuSENOcHN4dGJzSE5Kbz0ifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOnB1bGxEZXBlbmRlbmNpZXMiLCJ2YWx1ZSI6InNvOmxpYmMubXVzbC14ODZfNjQuc28uMSJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6c2l6ZSIsInZhbHVlIjoiMzY4MzAifV0sInB1Ymxpc2hlciI6Ik5hdGFuYWVsIENvcGEgXHUwMDNjbmNvcGFAYWxwaW5lbGludXgub3JnXHUwMDNlIiwicHVybCI6InBrZzphbHBpbmUvc2NhbmVsZkAxLjMuMy1yMD9hcmNoPXg4Nl82NFx1MDAyNnVwc3RyZWFtPXBheC11dGlsc1x1MDAyNmRpc3Rybz1hbHBpbmUtMy4xNS40IiwidHlwZSI6ImxpYnJhcnkiLCJ2ZXJzaW9uIjoiMS4zLjMtcjAifSx7ImJvbS1yZWYiOiJwa2c6YWxwaW5lL3NzbF9jbGllbnRAMS4zNC4xLXI1P2FyY2g9eDg2XzY0XHUwMDI2dXBzdHJlYW09YnVzeWJveFx1MDAyNmRpc3Rybz1hbHBpbmUtMy4xNS40XHUwMDI2c3lmdC1pZD0xZTE4MTJkZWI2NjY5MmM1IiwiY3BlIjoiY3BlOjIuMzphOnNzbC1jbGllbnQ6c3NsLWNsaWVudDoxLjM0LjEtcjU6KjoqOio6KjoqOio6KiIsImRlc2NyaXB0aW9uIjoiRVh0ZXJuYWwgc3NsX2NsaWVudCBmb3IgYnVzeWJveCB3Z2V0IiwiZXh0ZXJuYWxSZWZlcmVuY2VzIjpbeyJ0eXBlIjoiZGlzdHJpYnV0aW9uIiwidXJsIjoiaHR0cHM6Ly9idXN5Ym94Lm5ldC8ifV0sImxpY2Vuc2VzIjpbeyJsaWNlbnNlIjp7ImlkIjoiR1BMLTIuMC1vbmx5In19XSwibmFtZSI6InNzbF9jbGllbnQiLCJwcm9wZXJ0aWVzIjpbeyJuYW1lIjoic3lmdDpwYWNrYWdlOmZvdW5kQnkiLCJ2YWx1ZSI6ImFwa2RiLWNhdGFsb2dlciJ9LHsibmFtZSI6InN5ZnQ6cGFja2FnZTptZXRhZGF0YVR5cGUiLCJ2YWx1ZSI6IkFwa01ldGFkYXRhIn0seyJuYW1lIjoic3lmdDpwYWNrYWdlOnR5cGUiLCJ2YWx1ZSI6ImFwayJ9LHsibmFtZSI6InN5ZnQ6Y3BlMjMiLCJ2YWx1ZSI6ImNwZToyLjM6YTpzc2wtY2xpZW50OnNzbF9jbGllbnQ6MS4zNC4xLXI1Oio6KjoqOio6KjoqOioifSx7Im5hbWUiOiJzeWZ0OmNwZTIzIiwidmFsdWUiOiJjcGU6Mi4zOmE6c3NsX2NsaWVudDpzc2wtY2xpZW50OjEuMzQuMS1yNToqOio6KjoqOio6KjoqIn0seyJuYW1lIjoic3lmdDpjcGUyMyIsInZhbHVlIjoiY3BlOjIuMzphOnNzbF9jbGllbnQ6c3NsX2NsaWVudDoxLjM0LjEtcjU6KjoqOio6KjoqOio6KiJ9LHsibmFtZSI6InN5ZnQ6Y3BlMjMiLCJ2YWx1ZSI6ImNwZToyLjM6YTpzc2w6c3NsLWNsaWVudDoxLjM0LjEtcjU6KjoqOio6KjoqOio6KiJ9LHsibmFtZSI6InN5ZnQ6Y3BlMjMiLCJ2YWx1ZSI6ImNwZToyLjM6YTpzc2w6c3NsX2NsaWVudDoxLjM0LjEtcjU6KjoqOio6KjoqOio6KiJ9LHsibmFtZSI6InN5ZnQ6bG9jYXRpb246MDpsYXllcklEIiwidmFsdWUiOiJzaGEyNTY6NGZjMjQyZDU4Mjg1Njk5ZWNhMDVkYjNjYzdjNzEyMmEyYjhlMDE0ZDk0ODFmMzIzYmQ5Mjc3YmFhY2ZhMDYyOCJ9LHsibmFtZSI6InN5ZnQ6bG9jYXRpb246MDpwYXRoIiwidmFsdWUiOiIvbGliL2Fway9kYi9pbnN0YWxsZWQifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOmdpdENvbW1pdE9mQXBrUG9ydCIsInZhbHVlIjoiMjc0NWRlN2UxYjA5ZTY2M2I0NzdhODE0MWI4NGY3ZDgxYTA0OTk2MyJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6aW5zdGFsbGVkU2l6ZSIsInZhbHVlIjoiMjg2NzIifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOm9yaWdpblBhY2thZ2UiLCJ2YWx1ZSI6ImJ1c3lib3gifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOnB1bGxDaGVja3N1bSIsInZhbHVlIjoiUTFMUXBhVFlaVHpSL2tnS3ZSd0x1WThkRUZZUDQ9In0seyJuYW1lIjoic3lmdDptZXRhZGF0YTpwdWxsRGVwZW5kZW5jaWVzIiwidmFsdWUiOiJzbzpsaWJjLm11c2wteDg2XzY0LnNvLjEgc286bGlidGxzLnNvLjIifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOnNpemUiLCJ2YWx1ZSI6IjQ3MTYifV0sInB1Ymxpc2hlciI6Ik5hdGFuYWVsIENvcGEgXHUwMDNjbmNvcGFAYWxwaW5lbGludXgub3JnXHUwMDNlIiwicHVybCI6InBrZzphbHBpbmUvc3NsX2NsaWVudEAxLjM0LjEtcjU/YXJjaD14ODZfNjRcdTAwMjZ1cHN0cmVhbT1idXN5Ym94XHUwMDI2ZGlzdHJvPWFscGluZS0zLjE1LjQiLCJ0eXBlIjoibGlicmFyeSIsInZlcnNpb24iOiIxLjM0LjEtcjUifSx7ImJvbS1yZWYiOiJwa2c6YWxwaW5lL3psaWJAMS4yLjEyLXIwP2FyY2g9eDg2XzY0XHUwMDI2dXBzdHJlYW09emxpYlx1MDAyNmRpc3Rybz1hbHBpbmUtMy4xNS40XHUwMDI2c3lmdC1pZD1iMzk5MDhlNzI5NzQ2MDkiLCJjcGUiOiJjcGU6Mi4zOmE6emxpYjp6bGliOjEuMi4xMi1yMDoqOio6KjoqOio6KjoqIiwiZGVzY3JpcHRpb24iOiJBIGNvbXByZXNzaW9uL2RlY29tcHJlc3Npb24gTGlicmFyeSIsImV4dGVybmFsUmVmZXJlbmNlcyI6W3sidHlwZSI6ImRpc3RyaWJ1dGlvbiIsInVybCI6Imh0dHBzOi8vemxpYi5uZXQvIn1dLCJsaWNlbnNlcyI6W3sibGljZW5zZSI6eyJpZCI6IlpsaWIifX1dLCJuYW1lIjoiemxpYiIsInByb3BlcnRpZXMiOlt7Im5hbWUiOiJzeWZ0OnBhY2thZ2U6Zm91bmRCeSIsInZhbHVlIjoiYXBrZGItY2F0YWxvZ2VyIn0seyJuYW1lIjoic3lmdDpwYWNrYWdlOm1ldGFkYXRhVHlwZSIsInZhbHVlIjoiQXBrTWV0YWRhdGEifSx7Im5hbWUiOiJzeWZ0OnBhY2thZ2U6dHlwZSIsInZhbHVlIjoiYXBrIn0seyJuYW1lIjoic3lmdDpsb2NhdGlvbjowOmxheWVySUQiLCJ2YWx1ZSI6InNoYTI1Njo0ZmMyNDJkNTgyODU2OTllY2EwNWRiM2NjN2M3MTIyYTJiOGUwMTRkOTQ4MWYzMjNiZDkyNzdiYWFjZmEwNjI4In0seyJuYW1lIjoic3lmdDpsb2NhdGlvbjowOnBhdGgiLCJ2YWx1ZSI6Ii9saWIvYXBrL2RiL2luc3RhbGxlZCJ9LHsibmFtZSI6InN5ZnQ6bWV0YWRhdGE6Z2l0Q29tbWl0T2ZBcGtQb3J0IiwidmFsdWUiOiI3NDE0ODgwODY3OWY0N2FkOTZkYzk5ZTgzZWY3M2FjZmRlZWMxNjQyIn0seyJuYW1lIjoic3lmdDptZXRhZGF0YTppbnN0YWxsZWRTaXplIiwidmFsdWUiOiIxMTA1OTIifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOm9yaWdpblBhY2thZ2UiLCJ2YWx1ZSI6InpsaWIifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOnB1bGxDaGVja3N1bSIsInZhbHVlIjoiUTFIa3AyekgyYXlBV25RNzNrMFJkMjcwY1FCQjQ9In0seyJuYW1lIjoic3lmdDptZXRhZGF0YTpwdWxsRGVwZW5kZW5jaWVzIiwidmFsdWUiOiJzbzpsaWJjLm11c2wteDg2XzY0LnNvLjEifSx7Im5hbWUiOiJzeWZ0Om1ldGFkYXRhOnNpemUiLCJ2YWx1ZSI6IjUzNDg4In1dLCJwdWJsaXNoZXIiOiJOYXRhbmFlbCBDb3BhIFx1MDAzY25jb3BhQGFscGluZWxpbnV4Lm9yZ1x1MDAzZSIsInB1cmwiOiJwa2c6YWxwaW5lL3psaWJAMS4yLjEyLXIwP2FyY2g9eDg2XzY0XHUwMDI2dXBzdHJlYW09emxpYlx1MDAyNmRpc3Rybz1hbHBpbmUtMy4xNS40IiwidHlwZSI6ImxpYnJhcnkiLCJ2ZXJzaW9uIjoiMS4yLjEyLXIwIn0seyJkZXNjcmlwdGlvbiI6IkFscGluZSBMaW51eCB2My4xNSIsImV4dGVybmFsUmVmZXJlbmNlcyI6W3sidHlwZSI6Imlzc3VlLXRyYWNrZXIiLCJ1cmwiOiJodHRwczovL2J1Z3MuYWxwaW5lbGludXgub3JnLyJ9LHsidHlwZSI6IndlYnNpdGUiLCJ1cmwiOiJodHRwczovL2FscGluZWxpbnV4Lm9yZy8ifV0sIm5hbWUiOiJhbHBpbmUiLCJwcm9wZXJ0aWVzIjpbeyJuYW1lIjoic3lmdDpkaXN0cm86aWQiLCJ2YWx1ZSI6ImFscGluZSJ9LHsibmFtZSI6InN5ZnQ6ZGlzdHJvOnByZXR0eU5hbWUiLCJ2YWx1ZSI6IkFscGluZSBMaW51eCB2My4xNSJ9LHsibmFtZSI6InN5ZnQ6ZGlzdHJvOnZlcnNpb25JRCIsInZhbHVlIjoiMy4xNS40In1dLCJzd2lkIjp7Im5hbWUiOiJhbHBpbmUiLCJ0YWdJZCI6ImFscGluZSIsInZlcnNpb24iOiIzLjE1LjQifSwidHlwZSI6Im9wZXJhdGluZy1zeXN0ZW0iLCJ2ZXJzaW9uIjoiMy4xNS40In1dLCJtZXRhZGF0YSI6eyJjb21wb25lbnQiOnsiYm9tLXJlZiI6ImZjNjM4NjY1ZDQwNDdlYTEiLCJuYW1lIjoiYWxwaW5lOmxhdGVzdCIsInR5cGUiOiJjb250YWluZXIiLCJ2ZXJzaW9uIjoic2hhMjU2OmE3NzdjOWM2NmJhMTc3Y2NmZWEyM2YyYTIxNmZmNjcyMWU3OGE2NjJjZDE3MDE5NDg4YzQxNzEzNTI5OWNkODkifSwidGltZXN0YW1wIjoiMjAyMi0wNC0xMVQxNzoxMTo0MS0wNzowMCIsInRvb2xzIjpbeyJuYW1lIjoic3lmdCIsInZlbmRvciI6ImFuY2hvcmUiLCJ2ZXJzaW9uIjoiMC40My4yIn1dfSwic2VyaWFsTnVtYmVyIjoidXJuOnV1aWQ6ZWQ5MjQyNmYtYTkxNi00NDljLTgzYjgtZDcwODYwMTM3NzczIiwic3BlY1ZlcnNpb24iOiIxLjQiLCJ2ZXJzaW9uIjoxfX0=\",\"signatures\":[{\"keyid\":\"\",\"sig\":\"MEQCIDq1ltJFSy/cIzUSQmnqcP4ttqBY6vc92Nld45QY12GoAiBbjJQmixgSc6Hm5fClMXZnoyWbZJjKouQDzX5bvtWd0w==\"}]}"
  },
  {
    "path": "grype/pkg/testdata/another_cosign.pub",
    "content": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEFkdSiEHXuVIQMJWLeRbj+xuAIdzB\nYNPLm67ahl7GBcPYWirZfssklnwDldY8TLbK4igxT7YisGPMLGyiJsvvhg==\n-----END PUBLIC KEY-----\n"
  },
  {
    "path": "grype/pkg/testdata/bad-sbom.json",
    "content": "{}\n"
  },
  {
    "path": "grype/pkg/testdata/cosign.pub",
    "content": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEFRk//8DKJlhLGay1c2sB5ApHblJB\nZXCNSffjHFH+f061ZuBTuFPQwsh/Hhypo8zj7X0VjdV4+t32neAWeYQBrg==\n-----END PUBLIC KEY-----\n"
  },
  {
    "path": "grype/pkg/testdata/cosign_broken.pub",
    "content": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEFRk//8DKJlhLGay1c2sB5ApHblJB\nZXCNSffjHFH+f061ZuBTuFPQwsh/Hhypo8zj7X0VjdV4+t32neAWeYQBrg=1\n-----END PUBLIC KEY-----\n"
  },
  {
    "path": "grype/pkg/testdata/image-simple/Dockerfile",
    "content": "# Note: changes to this file will necessitate updating test values. alternately, make a new image fixture instead of editing this one.\nFROM scratch\nADD package.json /\nADD target /target\n"
  },
  {
    "path": "grype/pkg/testdata/image-simple/package.json",
    "content": "{\n  \"name\": \"top-level-package\",\n  \"version\": \"5.19.4\",\n  \"dependencies\": {\n    \"left-pad\": \"1.3.0\"\n  }\n}"
  },
  {
    "path": "grype/pkg/testdata/image-simple/target/nested/package.json",
    "content": "{\n  \"name\": \"nested-package\",\n  \"version\": \"2.9.35\",\n  \"dependencies\": {\n    \"lodash\": \"4.17.21\"\n  }\n}"
  },
  {
    "path": "grype/pkg/testdata/invalid.json",
    "content": "{}\n"
  },
  {
    "path": "grype/pkg/testdata/purl/different-os.txt",
    "content": "pkg:apk/openssl@3.2.1?arch=aarch64&distro=alpine-3.20.3\npkg:apk/curl@7.61.1?arch=aarch64&distro=alpine-3.20.2\n"
  },
  {
    "path": "grype/pkg/testdata/purl/empty.json",
    "content": ""
  },
  {
    "path": "grype/pkg/testdata/purl/homogeneous-os.txt",
    "content": "pkg:apk/openssl@3.2.1?arch=aarch64&distro=alpine-3.20.3\npkg:apk/curl@7.61.1?arch=aarch64&distro=alpine-3.20.3\n"
  },
  {
    "path": "grype/pkg/testdata/purl/invalid-cpe.txt",
    "content": "pkg:deb/debian/curl@7.50.3-1?cpes=invalid\n"
  },
  {
    "path": "grype/pkg/testdata/purl/invalid-purl.txt",
    "content": "invalid\n"
  },
  {
    "path": "grype/pkg/testdata/purl/valid-purl.txt",
    "content": "pkg:deb/debian/sysv-rc@2.88dsf-59?arch=all&distro=debian-8&upstream=sysvinit\npkg:maven/org.apache.ant/ant@1.10.8\npkg:maven/org.apache.logging.log4j/log4j-core@2.14.1\n"
  },
  {
    "path": "grype/pkg/testdata/purl/valid-rhel-9+eus.txt",
    "content": "pkg:rpm/redhat/kernel@0:5.14.0-100?distro=rhel-9.4+eus"
  },
  {
    "path": "grype/pkg/testdata/purl/valid-rhel-9.txt",
    "content": "pkg:rpm/redhat/kernel@0:5.14.0-100?distro=rhel-9.4"
  },
  {
    "path": "grype/pkg/testdata/sbom-with-intoto-string.json",
    "content": "{\n  \"artifacts\": [\n    {\n      \"name\": \"gcc-10-base\",\n      \"version\": \"10.2.0-5ubuntu1~20.04\",\n      \"type\": \"deb\",\n      \"foundBy\": \"dpkgdb-cataloger\",\n      \"locations\": [\n        {\n          \"path\": \"/var/lib/dpkg/status\",\n          \"layerID\": \"sha256:9f32931c9d28f10104a8eb1330954ba90e76d92b02c5256521ba864feec14009\"\n        },\n        {\n          \"path\": \"/var/lib/dpkg/info/gcc-10-base:amd64.md5sums\",\n          \"layerID\": \"sha256:9f32931c9d28f10104a8eb1330954ba90e76d92b02c5256521ba864feec14009\"\n        },\n        {\n          \"path\": \"/usr/share/doc/gcc-10-base/copyright\",\n          \"layerID\": \"sha256:9f32931c9d28f10104a8eb1330954ba90e76d92b02c5256521ba864feec14009\"\n        }\n      ],\n      \"licenses\": [],\n      \"language\": \"\",\n      \"cpes\": [\n        \"cpe:2.3:a:gcc-10-base:gcc-10-base:10.2.0-5ubuntu1~20.04:*:*:*:*:*:*:*\",\n        \"cpe:2.3:a:*:gcc-10-base:10.2.0-5ubuntu1~20.04:*:*:*:*:*:*:*\"\n      ],\n      \"purl\": \"pkg:deb/ubuntu/gcc-10-base@10.2.0-5ubuntu1~20.04?arch=amd64\",\n      \"metadataType\": \"DpkgMetadata\",\n      \"metadata\": {\n        \"package\": \"gcc-10-base\",\n        \"source\": \"gcc-10\",\n        \"version\": \"10.2.0-5ubuntu1~20.04\",\n        \"sourceVersion\": \"\",\n        \"architecture\": \"amd64\",\n        \"maintainer\": \"application/vnd.in-toto+json this is here to make grype think it is an attestation\",\n        \"installedSize\": 260,\n        \"files\": [\n          {\n            \"path\": \"/usr/share/doc/gcc-10-base/README.Debian.amd64.gz\",\n            \"md5\": \"3c03902e06eef5dcfe3005376c23a120\"\n          },\n          {\n            \"path\": \"/usr/share/doc/gcc-10-base/TODO.Debian\",\n            \"md5\": \"8afe308ec72834f3c24b209fbc4d149e\"\n          },\n          {\n            \"path\": \"/usr/share/doc/gcc-10-base/changelog.Debian.gz\",\n            \"md5\": \"0e3cbc1152a18bddf7c24fe3913866c6\"\n          },\n          {\n            \"path\": \"/usr/share/doc/gcc-10-base/copyright\",\n            \"md5\": \"a80ca2e181b9eecc3e4d373fd7ca59f2\"\n          }\n        ]\n      }\n    },\n    {\n      \"name\": \"hostname\",\n      \"version\": \"3.23\",\n      \"type\": \"deb\",\n      \"foundBy\": \"dpkgdb-cataloger\",\n      \"locations\": [\n        {\n          \"path\": \"/var/lib/dpkg/status\",\n          \"layerID\": \"sha256:9f32931c9d28f10104a8eb1330954ba90e76d92b02c5256521ba864feec14009\"\n        },\n        {\n          \"path\": \"/var/lib/dpkg/info/hostname.md5sums\",\n          \"layerID\": \"sha256:9f32931c9d28f10104a8eb1330954ba90e76d92b02c5256521ba864feec14009\"\n        },\n        {\n          \"path\": \"/usr/share/doc/hostname/copyright\",\n          \"layerID\": \"sha256:9f32931c9d28f10104a8eb1330954ba90e76d92b02c5256521ba864feec14009\"\n        }\n      ],\n      \"licenses\": [],\n      \"language\": \"\",\n      \"cpes\": [\n        \"cpe:2.3:a:hostname:hostname:3.23:*:*:*:*:*:*:*\",\n        \"cpe:2.3:a:*:hostname:3.23:*:*:*:*:*:*:*\"\n      ],\n      \"purl\": \"pkg:deb/ubuntu/hostname@3.23?arch=amd64\",\n      \"metadataType\": \"DpkgMetadata\",\n      \"metadata\": {\n        \"package\": \"hostname\",\n        \"source\": \"\",\n        \"version\": \"3.23\",\n        \"sourceVersion\": \"\",\n        \"architecture\": \"amd64\",\n        \"maintainer\": \"Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>\",\n        \"installedSize\": 54,\n        \"files\": [\n          {\n            \"path\": \"/bin/hostname\",\n            \"md5\": \"1ce73d718e3dccc1aaa7bce6ae2ef0a7\"\n          },\n          {\n            \"path\": \"/usr/share/doc/hostname/changelog.gz\",\n            \"md5\": \"087a3eabd7427692c216a5d7a4341127\"\n          },\n          {\n            \"path\": \"/usr/share/doc/hostname/copyright\",\n            \"md5\": \"460b6a1df2db2b5e80f05a44ec21c62f\"\n          },\n          {\n            \"path\": \"/usr/share/man/man1/hostname.1.gz\",\n            \"md5\": \"62e6be6a928b4b9f2a985778fee171fd\"\n          }\n        ]\n      }\n    },\n    {\n      \"name\": \"libacl1\",\n      \"version\": \"2.2.53-6\",\n      \"type\": \"deb\",\n      \"foundBy\": \"dpkgdb-cataloger\",\n      \"locations\": [\n        {\n          \"path\": \"/var/lib/dpkg/status\",\n          \"layerID\": \"sha256:9f32931c9d28f10104a8eb1330954ba90e76d92b02c5256521ba864feec14009\"\n        },\n        {\n          \"path\": \"/var/lib/dpkg/info/libacl1:amd64.md5sums\",\n          \"layerID\": \"sha256:9f32931c9d28f10104a8eb1330954ba90e76d92b02c5256521ba864feec14009\"\n        },\n        {\n          \"path\": \"/usr/share/doc/libacl1/copyright\",\n          \"layerID\": \"sha256:9f32931c9d28f10104a8eb1330954ba90e76d92b02c5256521ba864feec14009\"\n        }\n      ],\n      \"licenses\": [\n        \"GPL-2+\",\n        \"LGPL-2+\"\n      ],\n      \"language\": \"\",\n      \"cpes\": [\n        \"cpe:2.3:a:libacl1:libacl1:2.2.53-6:*:*:*:*:*:*:*\",\n        \"cpe:2.3:a:*:libacl1:2.2.53-6:*:*:*:*:*:*:*\"\n      ],\n      \"purl\": \"pkg:deb/ubuntu/libacl1@2.2.53-6?arch=amd64\",\n      \"metadataType\": \"DpkgMetadata\",\n      \"metadata\": {\n        \"package\": \"libacl1\",\n        \"source\": \"acl\",\n        \"version\": \"2.2.53-6\",\n        \"sourceVersion\": \"\",\n        \"architecture\": \"amd64\",\n        \"maintainer\": \"Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>\",\n        \"installedSize\": 70,\n        \"files\": [\n          {\n            \"path\": \"/usr/lib/x86_64-linux-gnu/libacl.so.1.1.2253\",\n            \"md5\": \"e77bf61a72656a594ef49768a7d6097b\"\n          },\n          {\n            \"path\": \"/usr/share/doc/libacl1/changelog.Debian.gz\",\n            \"md5\": \"65de3b787d67d4755ad3ae0584aee9f2\"\n          },\n          {\n            \"path\": \"/usr/share/doc/libacl1/copyright\",\n            \"md5\": \"40822d07cf4c0fb9ab13c2bebf51d981\"\n          }\n        ]\n      }\n    },\n    {\n      \"name\": \"libattr1\",\n      \"version\": \"1:2.4.48-5\",\n      \"type\": \"deb\",\n      \"foundBy\": \"dpkgdb-cataloger\",\n      \"locations\": [\n        {\n          \"path\": \"/var/lib/dpkg/status\",\n          \"layerID\": \"sha256:9f32931c9d28f10104a8eb1330954ba90e76d92b02c5256521ba864feec14009\"\n        },\n        {\n          \"path\": \"/var/lib/dpkg/info/libattr1:amd64.md5sums\",\n          \"layerID\": \"sha256:9f32931c9d28f10104a8eb1330954ba90e76d92b02c5256521ba864feec14009\"\n        },\n        {\n          \"path\": \"/usr/share/doc/libattr1/copyright\",\n          \"layerID\": \"sha256:9f32931c9d28f10104a8eb1330954ba90e76d92b02c5256521ba864feec14009\"\n        }\n      ],\n      \"licenses\": [\n        \"GPL-2+\",\n        \"LGPL-2+\"\n      ],\n      \"language\": \"\",\n      \"cpes\": [\n        \"cpe:2.3:a:libattr1:libattr1:1:2.4.48-5:*:*:*:*:*:*:*\",\n        \"cpe:2.3:a:*:libattr1:1:2.4.48-5:*:*:*:*:*:*:*\"\n      ],\n      \"purl\": \"pkg:deb/ubuntu/libattr1@1:2.4.48-5?arch=amd64\",\n      \"metadataType\": \"DpkgMetadata\",\n      \"metadata\": {\n        \"package\": \"libattr1\",\n        \"source\": \"attr\",\n        \"version\": \"1:2.4.48-5\",\n        \"sourceVersion\": \"\",\n        \"architecture\": \"amd64\",\n        \"maintainer\": \"Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>\",\n        \"installedSize\": 57,\n        \"files\": [\n          {\n            \"path\": \"/usr/lib/x86_64-linux-gnu/libattr.so.1.1.2448\",\n            \"md5\": \"708453da8ebde1aaca2ca69c04d4c0a8\"\n          },\n          {\n            \"path\": \"/usr/share/doc/libattr1/changelog.Debian.gz\",\n            \"md5\": \"6465a4cda28287d4ea9979b530648ee3\"\n          },\n          {\n            \"path\": \"/usr/share/doc/libattr1/copyright\",\n            \"md5\": \"1e0c5c8b55170890f960aad90336aaed\"\n          }\n        ]\n      }\n    }\n  ],\n  \"source\": {\n    \"type\": \"image\",\n    \"target\": {\n      \"userInput\": \"ubuntu:20.04\",\n      \"imageID\": \"sha256:f63181f19b2fe819156dcb068b3b5bc036820bec7014c5f77277cfa341d4cb5e\",\n      \"manifestDigest\": \"sha256:5146935f9248826d44dfc2489abfd5f4bdfbc319a738c04dfe1ef071f228a1ac\",\n      \"mediaType\": \"application/vnd.docker.distribution.manifest.v2+json\",\n      \"tags\": [\n        \"ubuntu:20.04\"\n      ],\n      \"imageSize\": 72898411,\n      \"scope\": \"Squashed\",\n      \"layers\": [\n        {\n          \"mediaType\": \"application/vnd.docker.image.rootfs.diff.tar.gzip\",\n          \"digest\": \"sha256:9f32931c9d28f10104a8eb1330954ba90e76d92b02c5256521ba864feec14009\",\n          \"size\": 72897593\n        },\n        {\n          \"mediaType\": \"application/vnd.docker.image.rootfs.diff.tar.gzip\",\n          \"digest\": \"sha256:dbf2c0f42a39b60301f6d3936f7f8adb59bb97d31ec11cc4a049ce81155fef89\",\n          \"size\": 811\n        },\n        {\n          \"mediaType\": \"application/vnd.docker.image.rootfs.diff.tar.gzip\",\n          \"digest\": \"sha256:02473afd360bd5391fa51b6e7849ce88732ae29f50f3630c3551f528eba66d1e\",\n          \"size\": 7\n        }\n      ]\n    }\n  },\n  \"distro\": {\n    \"name\": \"ubuntu\",\n    \"version\": \"20.04\",\n    \"idLike\": \"debian\"\n  },\n  \"descriptor\": {\n    \"name\": \"syft\",\n    \"version\": \"0.12.7\"\n  },\n  \"schema\": {\n    \"version\": \"1.0.1\",\n    \"url\": \"https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-1.0.1.json\"\n  }\n}\n"
  },
  {
    "path": "grype/pkg/testdata/syft-java-bad-cpes.json",
    "content": "{\n  \"artifacts\": [\n    {\n      \"id\": \"da8f3d4c-fc48-4a79-a775-31c730ce5f97\",\n      \"name\": \"wstx-asl\",\n      \"version\": \"3.2.7\",\n      \"type\": \"java-archive\",\n      \"foundBy\": \"java-cataloger\",\n      \"locations\": [\n        {\n          \"path\": \"/hudson.war\",\n          \"layerID\": \"sha256:540ce9731a52867a29db67bc9b7ead11db04918f00a6e432627d21e1160ed92b\"\n        }\n      ],\n      \"licenses\": [],\n      \"language\": \"java\",\n      \"cpes\": [\n        \"cpe:2.3:a:http://jcp_org/en/jsr/detail?id=173:wstx_asl:3.2.7:*:*:*:*:*:*:*\",\n        \"cpe:2.3:a:http://jcp_org/en/jsr/detail?id=173:wstx-asl:3.2.7:*:*:*:*:*:*:*\",\n        \"cpe:2.3:a:http://jcp-org/en/jsr/detail?id=173:wstx-asl:3.2.7:*:*:*:*:*:*:*\",\n        \"cpe:2.3:a:http://jcp-org/en/jsr/detail?id=173:wstx_asl:3.2.7:*:*:*:*:*:*:*\",\n        \"cpe:2.3:a:woodstox_codehaus_org:wstx-asl:3.2.7:*:*:*:*:*:*:*\",\n        \"cpe:2.3:a:woodstox-codehaus-org:wstx-asl:3.2.7:*:*:*:*:*:*:*\",\n        \"cpe:2.3:a:woodstox-codehaus-org:wstx_asl:3.2.7:*:*:*:*:*:*:*\",\n        \"cpe:2.3:a:woodstox_codehaus_org:wstx_asl:3.2.7:*:*:*:*:*:*:*\",\n        \"cpe:2.3:a:wstx-asl:wstx-asl:3.2.7:*:*:*:*:*:*:*\",\n        \"cpe:2.3:a:wstx-asl:wstx_asl:3.2.7:*:*:*:*:*:*:*\",\n        \"cpe:2.3:a:wstx_asl:wstx-asl:3.2.7:*:*:*:*:*:*:*\",\n        \"cpe:2.3:a:wstx_asl:wstx_asl:3.2.7:*:*:*:*:*:*:*\",\n        \"cpe:2.3:a:wstx:wstx-asl:3.2.7:*:*:*:*:*:*:*\",\n        \"cpe:2.3:a:wstx:wstx_asl:3.2.7:*:*:*:*:*:*:*\"\n      ],\n      \"purl\": \"\",\n      \"metadataType\": \"JavaMetadata\",\n      \"metadata\": {\n        \"virtualPath\": \"/hudson.war:WEB-INF/lib/wstx-asl-3.2.7.jar\",\n        \"manifest\": {\n          \"main\": {\n            \"Ant-Version\": \"Apache Ant 1.6.5\",\n            \"Built-By\": \"tatu\",\n            \"Created-By\": \"1.4.2_03-b02 (Sun Microsystems Inc.)\",\n            \"Implementation-Title\": \"WoodSToX XML-processor\",\n            \"Implementation-Vendor\": \"woodstox.codehaus.org\",\n            \"Implementation-Version\": \"3.2.7\",\n            \"Manifest-Version\": \"1.0\",\n            \"Specification-Title\": \"StAX 1.0 API\",\n            \"Specification-Vendor\": \"http://jcp.org/en/jsr/detail?id=173\",\n            \"Specification-Version\": \"1.0\"\n          }\n        }\n      }\n    }\n  ],\n  \"artifactRelationships\": [],\n  \"source\": {\n    \"type\": \"image\",\n    \"target\": {\n      \"userInput\": \"docker.io/anchore/test_images:java\",\n      \"imageID\": \"sha256:55125c059cf3d9e8a179c3b543deeaf8613b1aa3101515cd51a7918b857b8eea\",\n      \"manifestDigest\": \"sha256:64dc1086a990469c65bf65c28c8d2d9062dd32aa566d045e1fe71e32e6b0d600\",\n      \"mediaType\": \"application/vnd.docker.distribution.manifest.v2+json\",\n      \"tags\": [\n        \"anchore/test_images:java\"\n      ],\n      \"imageSize\": 39383202,\n      \"layers\": [\n        {\n          \"mediaType\": \"application/vnd.docker.image.rootfs.diff.tar.gzip\",\n          \"digest\": \"sha256:8ea3b23f387bedc5e3cee574742d748941443c328a75f511eb37b0d8b6164130\",\n          \"size\": 5608905\n        },\n        {\n          \"mediaType\": \"application/vnd.docker.image.rootfs.diff.tar.gzip\",\n          \"digest\": \"sha256:540ce9731a52867a29db67bc9b7ead11db04918f00a6e432627d21e1160ed92b\",\n          \"size\": 33772593\n        },\n        {\n          \"mediaType\": \"application/vnd.docker.image.rootfs.diff.tar.gzip\",\n          \"digest\": \"sha256:a155363a67edf6e6462aa47445c3d900b84cec3320b62405b495ddf65035c039\",\n          \"size\": 1704\n        }\n      ],\n      \"manifest\": \"eyJzY2hlbWFWZXJzaW9uIjoyLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmRpc3RyaWJ1dGlvbi5tYW5pZmVzdC52Mitqc29uIiwiY29uZmlnIjp7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuY29udGFpbmVyLmltYWdlLnYxK2pzb24iLCJzaXplIjoyMDc5LCJkaWdlc3QiOiJzaGEyNTY6NTUxMjVjMDU5Y2YzZDllOGExNzljM2I1NDNkZWVhZjg2MTNiMWFhMzEwMTUxNWNkNTFhNzkxOGI4NTdiOGVlYSJ9LCJsYXllcnMiOlt7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuaW1hZ2Uucm9vdGZzLmRpZmYudGFyLmd6aXAiLCJzaXplIjo1ODc5ODA4LCJkaWdlc3QiOiJzaGEyNTY6OGVhM2IyM2YzODdiZWRjNWUzY2VlNTc0NzQyZDc0ODk0MTQ0M2MzMjhhNzVmNTExZWIzN2IwZDhiNjE2NDEzMCJ9LHsibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vdm5kLmRvY2tlci5pbWFnZS5yb290ZnMuZGlmZi50YXIuZ3ppcCIsInNpemUiOjMzNzc2MTI4LCJkaWdlc3QiOiJzaGEyNTY6NTQwY2U5NzMxYTUyODY3YTI5ZGI2N2JjOWI3ZWFkMTFkYjA0OTE4ZjAwYTZlNDMyNjI3ZDIxZTExNjBlZDkyYiJ9LHsibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vdm5kLmRvY2tlci5pbWFnZS5yb290ZnMuZGlmZi50YXIuZ3ppcCIsInNpemUiOjM1ODQsImRpZ2VzdCI6InNoYTI1NjphMTU1MzYzYTY3ZWRmNmU2NDYyYWE0NzQ0NWMzZDkwMGI4NGNlYzMzMjBiNjI0MDViNDk1ZGRmNjUwMzVjMDM5In1dfQ==\",\n      \"config\": \"eyJhcmNoaXRlY3R1cmUiOiJhbWQ2NCIsImNvbmZpZyI6eyJIb3N0bmFtZSI6IiIsIkRvbWFpbm5hbWUiOiIiLCJVc2VyIjoiIiwiQXR0YWNoU3RkaW4iOmZhbHNlLCJBdHRhY2hTdGRvdXQiOmZhbHNlLCJBdHRhY2hTdGRlcnIiOmZhbHNlLCJUdHkiOmZhbHNlLCJPcGVuU3RkaW4iOmZhbHNlLCJTdGRpbk9uY2UiOmZhbHNlLCJFbnYiOlsiUEFUSD0vdXNyL2xvY2FsL3NiaW46L3Vzci9sb2NhbC9iaW46L3Vzci9zYmluOi91c3IvYmluOi9zYmluOi9iaW4iXSwiQ21kIjpbIi9iaW4vc2giXSwiSW1hZ2UiOiJzaGEyNTY6NWI1MWNkNTVmNmQ5YmMyOTFkNDIzNGNmYTI2MGIyMWZmM2JkOTFkYzY2MWRlOWQ1ZmE3YzM4ZjYwNzljNGExZCIsIlZvbHVtZXMiOm51bGwsIldvcmtpbmdEaXIiOiIiLCJFbnRyeXBvaW50IjpudWxsLCJPbkJ1aWxkIjpbXSwiTGFiZWxzIjpudWxsfSwiY29udGFpbmVyX2NvbmZpZyI6eyJIb3N0bmFtZSI6IiIsIkRvbWFpbm5hbWUiOiIiLCJVc2VyIjoiIiwiQXR0YWNoU3RkaW4iOmZhbHNlLCJBdHRhY2hTdGRvdXQiOmZhbHNlLCJBdHRhY2hTdGRlcnIiOmZhbHNlLCJUdHkiOmZhbHNlLCJPcGVuU3RkaW4iOmZhbHNlLCJTdGRpbk9uY2UiOmZhbHNlLCJFbnYiOlsiUEFUSD0vdXNyL2xvY2FsL3NiaW46L3Vzci9sb2NhbC9iaW46L3Vzci9zYmluOi91c3IvYmluOi9zYmluOi9iaW4iXSwiQ21kIjpbIi9iaW4vc2giLCItYyIsIiMobm9wKSBDT1BZIGZpbGU6MmRkODU3MzFhNDA1NjliZmVjMDQ4ODBlNDU4OTg3NDhjNDIwMjBlMjM4NmRhMWViZjA5NDhiZjQ4YmIwZmRmZiBpbiAvICJdLCJJbWFnZSI6InNoYTI1Njo1YjUxY2Q1NWY2ZDliYzI5MWQ0MjM0Y2ZhMjYwYjIxZmYzYmQ5MWRjNjYxZGU5ZDVmYTdjMzhmNjA3OWM0YTFkIiwiVm9sdW1lcyI6bnVsbCwiV29ya2luZ0RpciI6IiIsIkVudHJ5cG9pbnQiOm51bGwsIk9uQnVpbGQiOltdLCJMYWJlbHMiOm51bGx9LCJjcmVhdGVkIjoiMjAyMS0wNC0wMVQxNToyODoyNy4zOTE0OTA5NjFaIiwiZG9ja2VyX3ZlcnNpb24iOiIxNy4wOS4wLWNlIiwiaGlzdG9yeSI6W3siY3JlYXRlZCI6IjIwMjEtMDMtMzFUMjA6MTA6MDYuNjg2MzU5MTI0WiIsImNyZWF0ZWRfYnkiOiIvYmluL3NoIC1jICMobm9wKSBBREQgZmlsZTo3MTE5MTY3YjU2ZmYxMjI4YjJmYjYzOWM3Njg5NTVjZTlkYjdhOTk5Y2Q5NDcxNzkyNDBiMjE2ZGZhNWNjYmI5IGluIC8gIn0seyJjcmVhdGVkIjoiMjAyMS0wMy0zMVQyMDoxMDowNi45MzQzNjg2MDRaIiwiY3JlYXRlZF9ieSI6Ii9iaW4vc2ggLWMgIyhub3ApICBDTUQgW1wiL2Jpbi9zaFwiXSIsImVtcHR5X2xheWVyIjp0cnVlfSx7ImNyZWF0ZWQiOiIyMDIxLTA0LTAxVDE1OjI4OjI3LjIyMTEwNTY1NVoiLCJjcmVhdGVkX2J5IjoiL2Jpbi9zaCAtYyB3Z2V0IC1udiBodHRwczovL3JlcG8xLm1hdmVuLm9yZy9tYXZlbjIvanVuaXQvanVuaXQvNC4xMy4xL2p1bml0LTQuMTMuMS5qYXIgXHUwMDI2XHUwMDI2ICAgICB3Z2V0IC1udiBodHRwczovL2dldC5qZW5raW5zLmlvL3BsdWdpbnMvVHdpbGlvTm90aWZpZXIvMC4yLjEvVHdpbGlvTm90aWZpZXIuaHBpIFx1MDAyNlx1MDAyNiAgICAgd2dldCAtbnYgaHR0cHM6Ly91cGRhdGVzLmplbmtpbnMtY2kub3JnL2Rvd25sb2FkL3dhci8xLjM5MC9odWRzb24ud2FyIn0seyJjcmVhdGVkIjoiMjAyMS0wNC0wMVQxNToyODoyNy4zOTE0OTA5NjFaIiwiY3JlYXRlZF9ieSI6Ii9iaW4vc2ggLWMgIyhub3ApIENPUFkgZmlsZToyZGQ4NTczMWE0MDU2OWJmZWMwNDg4MGU0NTg5ODc0OGM0MjAyMGUyMzg2ZGExZWJmMDk0OGJmNDhiYjBmZGZmIGluIC8gIn1dLCJvcyI6ImxpbnV4Iiwicm9vdGZzIjp7InR5cGUiOiJsYXllcnMiLCJkaWZmX2lkcyI6WyJzaGEyNTY6OGVhM2IyM2YzODdiZWRjNWUzY2VlNTc0NzQyZDc0ODk0MTQ0M2MzMjhhNzVmNTExZWIzN2IwZDhiNjE2NDEzMCIsInNoYTI1Njo1NDBjZTk3MzFhNTI4NjdhMjlkYjY3YmM5YjdlYWQxMWRiMDQ5MThmMDBhNmU0MzI2MjdkMjFlMTE2MGVkOTJiIiwic2hhMjU2OmExNTUzNjNhNjdlZGY2ZTY0NjJhYTQ3NDQ1YzNkOTAwYjg0Y2VjMzMyMGI2MjQwNWI0OTVkZGY2NTAzNWMwMzkiXX19\",\n      \"repoDigests\": [\n        \"anchore/test_images@sha256:4f6203a146e4c056f09fd72c687adfb23e75b18b58e3ea7c9a25a8af6699a381\"\n      ],\n      \"scope\": \"Squashed\"\n    }\n  },\n  \"distro\": {\n    \"name\": \"alpine\",\n    \"version\": \"3.13.4\",\n    \"idLike\": \"\"\n  },\n  \"descriptor\": {\n    \"name\": \"syft\",\n    \"version\": \"0.23.0\"\n  },\n  \"schema\": {\n    \"version\": \"1.1.0\",\n    \"url\": \"https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-1.1.0.json\"\n  }\n}\n"
  },
  {
    "path": "grype/pkg/testdata/syft-multiple-ecosystems.json",
    "content": "{\n \"artifacts\": [\n  {\n   \"id\": \"8039c8621bcc1383\",\n   \"name\": \"alpine-baselayout\",\n   \"version\": \"3.2.0-r6\",\n   \"type\": \"apk\",\n   \"foundBy\": \"apkdb-cataloger\",\n   \"locations\": [\n    {\n     \"path\": \"/lib/apk/db/installed\",\n     \"layerID\": \"sha256:8d3ac3489996423f53d6087c81180006263b79f206d3fdec9e66f0e27ceb8759\"\n    }\n   ],\n   \"licenses\": [\n    \"GPL-2.0-only\"\n   ],\n   \"language\": \"\",\n   \"cpes\": [\n    \"cpe:2.3:a:alpine:alpine_baselayout:3.2.0-r6:*:*:*:*:*:*:*\"\n   ],\n   \"purl\": \"pkg:alpine/alpine-baselayout@3.2.0-r6?arch=x86_64\",\n   \"metadataType\": \"ApkMetadata\",\n   \"metadata\": {\n    \"package\": \"alpine-baselayout\",\n    \"originPackage\": \"alpine-baselayout\",\n    \"maintainer\": \"Natanael Copa <ncopa@alpinelinux.org>\",\n    \"version\": \"3.2.0-r6\",\n    \"license\": \"GPL-2.0-only\",\n    \"architecture\": \"x86_64\",\n    \"url\": \"https://git.alpinelinux.org/cgit/aports/tree/main/alpine-baselayout\",\n    \"description\": \"Alpine base dir structure and init scripts\",\n    \"size\": 21101,\n    \"installedSize\": 413696,\n    \"pullDependencies\": \"/bin/sh so:libc.musl-x86_64.so.1\",\n    \"pullChecksum\": \"Q1EymS6rAgmGs7XYhqdyEoiWgEZ6A=\",\n    \"gitCommitOfApkPort\": \"dfa1379357a321e638feef1cd8d55ab03d020f45\",\n    \"files\": [\n     {\n      \"path\": \"/dev\"\n     },\n     {\n      \"path\": \"/dev/pts\"\n     },\n     {\n      \"path\": \"/dev/shm\"\n     },\n     {\n      \"path\": \"/etc\"\n     },\n     {\n      \"path\": \"/etc/fstab\",\n      \"digest\": {\n       \"algorithm\": \"sha1\",\n       \"value\": \"Q11Q7hNe8QpDS531guqCdrXBzoA/o=\"\n      }\n     },\n     {\n      \"path\": \"/etc/group\",\n      \"digest\": {\n       \"algorithm\": \"sha1\",\n       \"value\": \"Q13K+olJg5ayzHSVNUkggZJXuB+9Y=\"\n      }\n     },\n     {\n      \"path\": \"/etc/hostname\",\n      \"digest\": {\n       \"algorithm\": \"sha1\",\n       \"value\": \"Q16nVwYVXP/tChvUPdukVD2ifXOmc=\"\n      }\n     },\n     {\n      \"path\": \"/etc/hosts\",\n      \"digest\": {\n       \"algorithm\": \"sha1\",\n       \"value\": \"Q1BD6zJKZTRWyqGnPi4tSfd3krsMU=\"\n      }\n     },\n     {\n      \"path\": \"/etc/inittab\",\n      \"digest\": {\n       \"algorithm\": \"sha1\",\n       \"value\": \"Q1TsthbhW7QzWRe1E/NKwTOuD4pHc=\"\n      }\n     },\n     {\n      \"path\": \"/etc/modules\",\n      \"digest\": {\n       \"algorithm\": \"sha1\",\n       \"value\": \"Q1toogjUipHGcMgECgPJX64SwUT1M=\"\n      }\n     },\n     {\n      \"path\": \"/etc/motd\",\n      \"digest\": {\n       \"algorithm\": \"sha1\",\n       \"value\": \"Q1XmduVVNURHQ27TvYp1Lr5TMtFcA=\"\n      }\n     },\n     {\n      \"path\": \"/etc/mtab\",\n      \"ownerUid\": \"0\",\n      \"ownerGid\": \"0\",\n      \"permissions\": \"777\",\n      \"digest\": {\n       \"algorithm\": \"sha1\",\n       \"value\": \"Q1kiljhXXH1LlQroHsEJIkPZg2eiw=\"\n      }\n     },\n     {\n      \"path\": \"/etc/passwd\",\n      \"digest\": {\n       \"algorithm\": \"sha1\",\n       \"value\": \"Q1TchuuLUfur0izvfZQZxgN/LJhB8=\"\n      }\n     },\n     {\n      \"path\": \"/etc/profile\",\n      \"digest\": {\n       \"algorithm\": \"sha1\",\n       \"value\": \"Q1VmHPWPjjvz4oCsbmYCUB4uWpSkc=\"\n      }\n     },\n     {\n      \"path\": \"/etc/protocols\",\n      \"digest\": {\n       \"algorithm\": \"sha1\",\n       \"value\": \"Q1omKlp3vgGq2ZqYzyD/KHNdo8rDc=\"\n      }\n     },\n     {\n      \"path\": \"/etc/services\",\n      \"digest\": {\n       \"algorithm\": \"sha1\",\n       \"value\": \"Q19WLCv5ItKg4MH7RWfNRh1I7byQc=\"\n      }\n     },\n     {\n      \"path\": \"/etc/shadow\",\n      \"ownerUid\": \"0\",\n      \"ownerGid\": \"42\",\n      \"permissions\": \"640\",\n      \"digest\": {\n       \"algorithm\": \"sha1\",\n       \"value\": \"Q1ltrPIAW2zHeDiajsex2Bdmq3uqA=\"\n      }\n     },\n     {\n      \"path\": \"/etc/shells\",\n      \"digest\": {\n       \"algorithm\": \"sha1\",\n       \"value\": \"Q1ojm2YdpCJ6B/apGDaZ/Sdb2xJkA=\"\n      }\n     },\n     {\n      \"path\": \"/etc/sysctl.conf\",\n      \"digest\": {\n       \"algorithm\": \"sha1\",\n       \"value\": \"Q14upz3tfnNxZkIEsUhWn7Xoiw96g=\"\n      }\n     },\n     {\n      \"path\": \"/etc/apk\"\n     },\n     {\n      \"path\": \"/etc/conf.d\"\n     },\n     {\n      \"path\": \"/etc/crontabs\"\n     },\n     {\n      \"path\": \"/etc/crontabs/root\",\n      \"ownerUid\": \"0\",\n      \"ownerGid\": \"0\",\n      \"permissions\": \"600\",\n      \"digest\": {\n       \"algorithm\": \"sha1\",\n       \"value\": \"Q1vfk1apUWI4yLJGhhNRd0kJixfvY=\"\n      }\n     },\n     {\n      \"path\": \"/etc/init.d\"\n     },\n     {\n      \"path\": \"/etc/modprobe.d\"\n     },\n     {\n      \"path\": \"/etc/modprobe.d/aliases.conf\",\n      \"digest\": {\n       \"algorithm\": \"sha1\",\n       \"value\": \"Q1WUbh6TBYNVK7e4Y+uUvLs/7viqk=\"\n      }\n     },\n     {\n      \"path\": \"/etc/modprobe.d/blacklist.conf\",\n      \"digest\": {\n       \"algorithm\": \"sha1\",\n       \"value\": \"Q14TdgFHkTdt3uQC+NBtrntOnm9n4=\"\n      }\n     },\n     {\n      \"path\": \"/etc/modprobe.d/i386.conf\",\n      \"digest\": {\n       \"algorithm\": \"sha1\",\n       \"value\": \"Q1pnay/njn6ol9cCssL7KiZZ8etlc=\"\n      }\n     },\n     {\n      \"path\": \"/etc/modprobe.d/kms.conf\",\n      \"digest\": {\n       \"algorithm\": \"sha1\",\n       \"value\": \"Q1ynbLn3GYDpvajba/ldp1niayeog=\"\n      }\n     },\n     {\n      \"path\": \"/etc/modules-load.d\"\n     },\n     {\n      \"path\": \"/etc/network\"\n     },\n     {\n      \"path\": \"/etc/network/if-down.d\"\n     },\n     {\n      \"path\": \"/etc/network/if-post-down.d\"\n     },\n     {\n      \"path\": \"/etc/network/if-pre-up.d\"\n     },\n     {\n      \"path\": \"/etc/network/if-up.d\"\n     },\n     {\n      \"path\": \"/etc/opt\"\n     },\n     {\n      \"path\": \"/etc/periodic\"\n     },\n     {\n      \"path\": \"/etc/periodic/15min\"\n     },\n     {\n      \"path\": \"/etc/periodic/daily\"\n     },\n     {\n      \"path\": \"/etc/periodic/hourly\"\n     },\n     {\n      \"path\": \"/etc/periodic/monthly\"\n     },\n     {\n      \"path\": \"/etc/periodic/weekly\"\n     },\n     {\n      \"path\": \"/etc/profile.d\"\n     },\n     {\n      \"path\": \"/etc/profile.d/README\",\n      \"digest\": {\n       \"algorithm\": \"sha1\",\n       \"value\": \"Q135OWsCzzvnB2fmFx62kbqm1Ax1k=\"\n      }\n     },\n     {\n      \"path\": \"/etc/profile.d/color_prompt.sh.disabled\",\n      \"digest\": {\n       \"algorithm\": \"sha1\",\n       \"value\": \"Q10wL23GuSCVfumMRgakabUI6EsSk=\"\n      }\n     },\n     {\n      \"path\": \"/etc/profile.d/locale.sh\",\n      \"digest\": {\n       \"algorithm\": \"sha1\",\n       \"value\": \"Q1S8j+WW71mWxfVy8ythqU7HUVoBw=\"\n      }\n     },\n     {\n      \"path\": \"/etc/sysctl.d\"\n     },\n     {\n      \"path\": \"/home\"\n     },\n     {\n      \"path\": \"/lib\"\n     },\n     {\n      \"path\": \"/lib/firmware\"\n     },\n     {\n      \"path\": \"/lib/mdev\"\n     },\n     {\n      \"path\": \"/lib/modules-load.d\"\n     },\n     {\n      \"path\": \"/lib/sysctl.d\"\n     },\n     {\n      \"path\": \"/lib/sysctl.d/00-alpine.conf\",\n      \"digest\": {\n       \"algorithm\": \"sha1\",\n       \"value\": \"Q1HpElzW1xEgmKfERtTy7oommnq6c=\"\n      }\n     },\n     {\n      \"path\": \"/media\"\n     },\n     {\n      \"path\": \"/media/cdrom\"\n     },\n     {\n      \"path\": \"/media/floppy\"\n     },\n     {\n      \"path\": \"/media/usb\"\n     },\n     {\n      \"path\": \"/mnt\"\n     },\n     {\n      \"path\": \"/opt\"\n     },\n     {\n      \"path\": \"/proc\"\n     },\n     {\n      \"path\": \"/root\",\n      \"ownerUid\": \"0\",\n      \"ownerGid\": \"0\",\n      \"permissions\": \"700\"\n     },\n     {\n      \"path\": \"/run\"\n     },\n     {\n      \"path\": \"/sbin\"\n     },\n     {\n      \"path\": \"/sbin/mkmntdirs\",\n      \"ownerUid\": \"0\",\n      \"ownerGid\": \"0\",\n      \"permissions\": \"755\",\n      \"digest\": {\n       \"algorithm\": \"sha1\",\n       \"value\": \"Q1qjkdyRJcYblGC6RMqUR4Bdb5g10=\"\n      }\n     },\n     {\n      \"path\": \"/srv\"\n     },\n     {\n      \"path\": \"/sys\"\n     },\n     {\n      \"path\": \"/tmp\",\n      \"ownerUid\": \"0\",\n      \"ownerGid\": \"0\",\n      \"permissions\": \"1777\"\n     },\n     {\n      \"path\": \"/usr\"\n     },\n     {\n      \"path\": \"/usr/lib\"\n     },\n     {\n      \"path\": \"/usr/lib/modules-load.d\"\n     },\n     {\n      \"path\": \"/usr/local\"\n     },\n     {\n      \"path\": \"/usr/local/bin\"\n     },\n     {\n      \"path\": \"/usr/local/lib\"\n     },\n     {\n      \"path\": \"/usr/local/share\"\n     },\n     {\n      \"path\": \"/usr/sbin\"\n     },\n     {\n      \"path\": \"/usr/share\"\n     },\n     {\n      \"path\": \"/usr/share/man\"\n     },\n     {\n      \"path\": \"/usr/share/misc\"\n     },\n     {\n      \"path\": \"/var\"\n     },\n     {\n      \"path\": \"/var/run\",\n      \"ownerUid\": \"0\",\n      \"ownerGid\": \"0\",\n      \"permissions\": \"777\",\n      \"digest\": {\n       \"algorithm\": \"sha1\",\n       \"value\": \"Q11/SNZz/8cK2dSKK+cJpVrZIuF4Q=\"\n      }\n     },\n     {\n      \"path\": \"/var/cache\"\n     },\n     {\n      \"path\": \"/var/cache/misc\"\n     },\n     {\n      \"path\": \"/var/empty\",\n      \"ownerUid\": \"0\",\n      \"ownerGid\": \"0\",\n      \"permissions\": \"555\"\n     },\n     {\n      \"path\": \"/var/lib\"\n     },\n     {\n      \"path\": \"/var/lib/misc\"\n     },\n     {\n      \"path\": \"/var/local\"\n     },\n     {\n      \"path\": \"/var/lock\"\n     },\n     {\n      \"path\": \"/var/lock/subsys\"\n     },\n     {\n      \"path\": \"/var/log\"\n     },\n     {\n      \"path\": \"/var/mail\"\n     },\n     {\n      \"path\": \"/var/opt\"\n     },\n     {\n      \"path\": \"/var/spool\"\n     },\n     {\n      \"path\": \"/var/spool/mail\",\n      \"ownerUid\": \"0\",\n      \"ownerGid\": \"0\",\n      \"permissions\": \"777\",\n      \"digest\": {\n       \"algorithm\": \"sha1\",\n       \"value\": \"Q1dzbdazYZA2nTzSIG3YyNw7d4Juc=\"\n      }\n     },\n     {\n      \"path\": \"/var/spool/cron\"\n     },\n     {\n      \"path\": \"/var/spool/cron/crontabs\",\n      \"ownerUid\": \"0\",\n      \"ownerGid\": \"0\",\n      \"permissions\": \"777\",\n      \"digest\": {\n       \"algorithm\": \"sha1\",\n       \"value\": \"Q1OFZt+ZMp7j0Gny0rqSKuWJyqYmA=\"\n      }\n     },\n     {\n      \"path\": \"/var/tmp\",\n      \"ownerUid\": \"0\",\n      \"ownerGid\": \"0\",\n      \"permissions\": \"1777\"\n     }\n    ]\n   }\n  },\n  {\n   \"name\": \"fake\",\n   \"version\": \"1.2.0\",\n   \"type\": \"dpkg\",\n   \"foundBy\": \"dpkg-cataloger\",\n   \"locations\": [\n    {\n     \"path\": \"/lib/apk/db/installed\",\n     \"layerID\": \"sha256:93cf4cfb673c7e16a9e74f731d6767b70b92a0b7c9f59d06efd72fbff535371c\"\n    }\n   ],\n   \"licenses\": [\n    \"LGPL-3.0-or-later\"\n   ],\n   \"language\": \"lang\",\n   \"cpes\": [\n    \"cpe:2.3:a:*:fake:1.2.0:*:*:*:*:*:*:*\",\n    \"cpe:2.3:a:fake:fake:1.2.0:*:*:*:*:*:*:*\"\n   ],\n   \"purl\": \"pkg:deb/debian/fake@1.2.0?arch=x86_64\",\n   \"metadataType\": \"DpkgMetadata\",\n   \"metadata\": {\n    \"source\": \"a-source\",\n    \"sourceVersion\": \"1.4.5\"\n   }\n  },\n  {\n   \"name\": \"gmp\",\n   \"version\": \"6.2.0-r0\",\n   \"type\": \"java-archive\",\n   \"foundBy\": \"java-cataloger\",\n   \"locations\": [\n    {\n     \"path\": \"/lib/apk/db/installed\",\n     \"layerID\": \"sha256:93cf4cfb673c7e16a9e74f731d6767b70b92a0b7c9f59d06efd72fbff535371c\"\n    }\n   ],\n   \"licenses\": [\n    \"LGPL-3.0-or-later\"\n   ],\n   \"language\": \"the-lang\",\n   \"cpes\": [\n    \"cpe:2.3:a:*:gmp:6.2.0-r0:*:*:*:*:*:*:*\",\n    \"cpe:2.3:a:gmp:gmp:6.2.0-r0:*:*:*:*:*:*:*\"\n   ],\n   \"purl\": \"pkg:alpine/gmp@6.2.0-r0?arch=x86_64\",\n   \"metadataType\": \"JavaMetadata\",\n   \"metadata\": {\n    \"pomProperties\": {\n     \"groupId\": \"gid\",\n     \"artifactId\": \"aid\"\n    },\n    \"manifest\": {\n     \"main\": {\n      \"Name\": \"a-name\"\n     }\n    }\n   }\n  }\n ],\n \"source\": {\n  \"type\": \"image\",\n  \"target\": {\n   \"userInput\": \"alpine:fake\",\n   \"imageID\": \"sha256:fadf1294c09213b20d4d6fc84109584e1c102d185c2cae15144a87d29de65c6d\",\n   \"manifestDigest\": \"sha256:1f6495428fb363e2d233e5df078b2b200635c4e51f0a3be34ecf09d44b547590\",\n   \"mediaType\": \"application/vnd.docker.distribution.manifest.v2+json\",\n   \"tags\": [\n    \"alpine:fake\"\n   ],\n   \"imageSize\": 15879684,\n   \"layers\": [\n    {\n     \"mediaType\": \"application/vnd.docker.image.rootfs.diff.tar.gzip\",\n     \"digest\": \"sha256:50644c29ef5a27c9a40c393a73ece2479de78325cae7d762ef3cdc19bf42dd0a\",\n     \"size\": 5570176\n    }\n   ],\n   \"manifest\": \"eyJzY2hlbWFWZXJzaW9uIjoyLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmRpc3RyaWJ1dGlvbi5tYW5pZmVzdC52Mitqc29uIiwiY29uZmlnIjp7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuY29udGFpbmVyLmltYWdlLnYxK2pzb24iLCJzaXplIjoyMTE2LCJkaWdlc3QiOiJzaGEyNTY6ZmFkZjEyOTRjMDkyMTNiMjBkNGQ2ZmM4NDEwOTU4NGUxYzEwMmQxODVjMmNhZTE1MTQ0YTg3ZDI5ZGU2NWM2ZCJ9LCJsYXllcnMiOlt7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuaW1hZ2Uucm9vdGZzLmRpZmYudGFyLmd6aXAiLCJzaXplIjo1ODQ0OTkyLCJkaWdlc3QiOiJzaGEyNTY6NTA2NDRjMjllZjVhMjdjOWE0MGMzOTNhNzNlY2UyNDc5ZGU3ODMyNWNhZTdkNzYyZWYzY2RjMTliZjQyZGQwYSJ9LHsibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vdm5kLmRvY2tlci5pbWFnZS5yb290ZnMuZGlmZi50YXIuZ3ppcCIsInNpemUiOjE2NzkzNiwiZGlnZXN0Ijoic2hhMjU2OmNjMGZmMWRkYWQ2ZmU0OTc4ZDgzMjYzMGE5MzAzODgzYWRjNTZlZGZjNzdjYWEzNjkyMjM5YzJkODFjZjVkMDAifSx7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuaW1hZ2Uucm9vdGZzLmRpZmYudGFyLmd6aXAiLCJzaXplIjoxMDE2Njc4NCwiZGlnZXN0Ijoic2hhMjU2OjNkZDJkYjQ4M2JjOWQ2YjU2MWNlNWNjMTEwNWUwYjZkMTk2MWNhMjQ5YTczNmJiYTgzNzFhYjI4ZWEzMDRmODQifSx7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuaW1hZ2Uucm9vdGZzLmRpZmYudGFyLmd6aXAiLCJzaXplIjoyMjUyOCwiZGlnZXN0Ijoic2hhMjU2OjkzY2Y0Y2ZiNjczYzdlMTZhOWU3NGY3MzFkNjc2N2I3MGI5MmEwYjdjOWY1OWQwNmVmZDcyZmJmZjUzNTM3MWMifV19\",\n   \"config\": \"eyJhcmNoaXRlY3R1cmUiOiJhbWQ2NCIsImNvbmZpZyI6eyJIb3N0bmFtZSI6IiIsIkRvbWFpbm5hbWUiOiIiLCJVc2VyIjoiIiwiQXR0YWNoU3RkaW4iOmZhbHNlLCJBdHRhY2hTdGRvdXQiOmZhbHNlLCJBdHRhY2hTdGRlcnIiOmZhbHNlLCJUdHkiOmZhbHNlLCJPcGVuU3RkaW4iOmZhbHNlLCJTdGRpbk9uY2UiOmZhbHNlLCJFbnYiOlsiUEFUSD0vdXNyL2xvY2FsL3NiaW46L3Vzci9sb2NhbC9iaW46L3Vzci9zYmluOi91c3IvYmluOi9zYmluOi9iaW4iXSwiQ21kIjpbIi9iaW4vc2giXSwiQXJnc0VzY2FwZWQiOnRydWUsIkltYWdlIjoic2hhMjU2OjJjOWQ1MzNiMmI2NGFiMTI4MmFlYTE2ZGYwZjlkYmYwYjNjZDQ3YWMxZTAyYjc1YTM3NjNiMmY0M2NjOWRlNWUiLCJWb2x1bWVzIjpudWxsLCJXb3JraW5nRGlyIjoiIiwiRW50cnlwb2ludCI6bnVsbCwiT25CdWlsZCI6bnVsbCwiTGFiZWxzIjpudWxsfSwiY29udGFpbmVyIjoiYzJlMTM3OTEyYWU2MzdkNzBlMDJhMDVhYWEyM2U3N2JlY2I3Mzg5MDJmZDNjYWMyMjdkNDRlYjdlYzEwMmQ0OCIsImNvbnRhaW5lcl9jb25maWciOnsiSG9zdG5hbWUiOiIiLCJEb21haW5uYW1lIjoiIiwiVXNlciI6IiIsIkF0dGFjaFN0ZGluIjpmYWxzZSwiQXR0YWNoU3Rkb3V0IjpmYWxzZSwiQXR0YWNoU3RkZXJyIjpmYWxzZSwiVHR5IjpmYWxzZSwiT3BlblN0ZGluIjpmYWxzZSwiU3RkaW5PbmNlIjpmYWxzZSwiRW52IjpbIlBBVEg9L3Vzci9sb2NhbC9zYmluOi91c3IvbG9jYWwvYmluOi91c3Ivc2JpbjovdXNyL2Jpbjovc2JpbjovYmluIl0sIkNtZCI6WyIvYmluL3NoIiwiLWMiLCJzZWQgLWkgJ3MvVjowLjkuMTEtcjMvVjowLjkuOS1yMC8nIC9saWIvYXBrL2RiL2luc3RhbGxlZCJdLCJJbWFnZSI6InNoYTI1NjoyYzlkNTMzYjJiNjRhYjEyODJhZWExNmRmMGY5ZGJmMGIzY2Q0N2FjMWUwMmI3NWEzNzYzYjJmNDNjYzlkZTVlIiwiVm9sdW1lcyI6bnVsbCwiV29ya2luZ0RpciI6IiIsIkVudHJ5cG9pbnQiOm51bGwsIk9uQnVpbGQiOm51bGwsIkxhYmVscyI6bnVsbH0sImNyZWF0ZWQiOiIyMDIwLTA5LTI0VDIyOjI2OjQ2LjE2NzYxOTRaIiwiZG9ja2VyX3ZlcnNpb24iOiIxOS4wMy4xMiIsImhpc3RvcnkiOlt7ImNyZWF0ZWQiOiIyMDIwLTA1LTI5VDIxOjE5OjQ2LjE5MjA0NTk3MloiLCJjcmVhdGVkX2J5IjoiL2Jpbi9zaCAtYyAjKG5vcCkgQUREIGZpbGU6YzkyYzI0ODIzOWY4YzdiOWIzYzA2NzY1MDk1NDgxNWYzOTFiN2JjYjA5MDIzZjk4NDk3MmMwODJhY2UyYThkMCBpbiAvICJ9LHsiY3JlYXRlZCI6IjIwMjAtMDUtMjlUMjE6MTk6NDYuMzYzNTE4MzQ1WiIsImNyZWF0ZWRfYnkiOiIvYmluL3NoIC1jICMobm9wKSAgQ01EIFtcIi9iaW4vc2hcIl0iLCJlbXB0eV9sYXllciI6dHJ1ZX0seyJjcmVhdGVkIjoiMjAyMC0wOS0yNFQyMjoyNjo0NC4zMjk1NTc4WiIsImNyZWF0ZWRfYnkiOiIvYmluL3NoIC1jIHdnZXQgaHR0cDovL2RsLWNkbi5hbHBpbmVsaW51eC5vcmcvYWxwaW5lL3YzLjkvbWFpbi94ODZfNjQvbGlidm5jc2VydmVyLTAuOS4xMS1yMy5hcGsifSx7ImNyZWF0ZWQiOiIyMDIwLTA5LTI0VDIyOjI2OjQ1LjY3MDg1MzhaIiwiY3JlYXRlZF9ieSI6Ii9iaW4vc2ggLWMgYXBrIGFkZCAgbGlidm5jc2VydmVyLTAuOS4xMS1yMy5hcGsifSx7ImNyZWF0ZWQiOiIyMDIwLTA5LTI0VDIyOjI2OjQ2LjE2NzYxOTRaIiwiY3JlYXRlZF9ieSI6Ii9iaW4vc2ggLWMgc2VkIC1pICdzL1Y6MC45LjExLXIzL1Y6MC45LjktcjAvJyAvbGliL2Fway9kYi9pbnN0YWxsZWQifV0sIm9zIjoibGludXgiLCJyb290ZnMiOnsidHlwZSI6ImxheWVycyIsImRpZmZfaWRzIjpbInNoYTI1Njo1MDY0NGMyOWVmNWEyN2M5YTQwYzM5M2E3M2VjZTI0NzlkZTc4MzI1Y2FlN2Q3NjJlZjNjZGMxOWJmNDJkZDBhIiwic2hhMjU2OmNjMGZmMWRkYWQ2ZmU0OTc4ZDgzMjYzMGE5MzAzODgzYWRjNTZlZGZjNzdjYWEzNjkyMjM5YzJkODFjZjVkMDAiLCJzaGEyNTY6M2RkMmRiNDgzYmM5ZDZiNTYxY2U1Y2MxMTA1ZTBiNmQxOTYxY2EyNDlhNzM2YmJhODM3MWFiMjhlYTMwNGY4NCIsInNoYTI1Njo5M2NmNGNmYjY3M2M3ZTE2YTllNzRmNzMxZDY3NjdiNzBiOTJhMGI3YzlmNTlkMDZlZmQ3MmZiZmY1MzUzNzFjIl19fQ==\"\n  }\n },\n \"distro\": {\n  \"name\": \"alpine\",\n  \"version\": \"3.12.0\",\n  \"idLike\": \"\"\n },\n \"descriptor\": {\n  \"name\": \"syft\",\n  \"version\": \"[not provided]\"\n },\n \"schema\": {\n  \"version\": \"1.0.0\",\n  \"url\": \"https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-1.0.0.json\"\n }\n}\n"
  },
  {
    "path": "grype/pkg/testdata/syft-spring.json",
    "content": "{\n \"artifacts\": [\n  {\n   \"name\": \"charsets\",\n   \"version\": \"\",\n   \"type\": \"java-archive\",\n   \"foundBy\": \"java-cataloger\",\n   \"locations\": [\n    {\n     \"path\": \"/usr/lib/jvm/java-8-openjdk-amd64/jre/lib/charsets.jar\",\n     \"layerID\": \"sha256:a1a6ceadb701ab4e6c93b243dc2a0daedc8cee23a24203845ecccd5784cd1393\"\n    }\n   ],\n   \"licenses\": [],\n   \"language\": \"java\",\n   \"cpes\": [\n    \"cpe:2.3:a:charsets:charsets:*:*:*:*:*:java:*:*\",\n    \"cpe:2.3:a:charsets:charsets:*:*:*:*:*:maven:*:*\"\n   ],\n   \"purl\": \"\",\n   \"metadataType\": \"JavaMetadata\",\n   \"metadata\": {\n    \"virtualPath\": \"/usr/lib/jvm/java-8-openjdk-amd64/jre/lib/charsets.jar\",\n    \"manifest\": {\n     \"main\": {\n      \"Created-By\": \"1.8.0_242 (Oracle Corporation)\",\n      \"Manifest-Version\": \"1.0\"\n     }\n    }\n   }\n  },\n  {\n   \"name\": \"tomcat-embed-el\",\n   \"version\": \"9.0.27\",\n   \"type\": \"java-archive\",\n   \"foundBy\": \"java-cataloger\",\n   \"locations\": [\n    {\n     \"path\": \"/app/libs/tomcat-embed-el-9.0.27.jar\",\n     \"layerID\": \"sha256:89504f083d3f15322f97ae240df44650203f24427860db1b3d32e66dd05940e4\"\n    }\n   ],\n   \"licenses\": [],\n   \"language\": \"java\",\n   \"cpes\": [\n    \"cpe:2.3:a:tomcat_embed_el:tomcat-embed-el:9.0.27:*:*:*:*:java:*:*\",\n    \"cpe:2.3:a:tomcat-embed-el:tomcat_embed_el:9.0.27:*:*:*:*:maven:*:*\"\n   ],\n   \"purl\": \"\",\n   \"metadataType\": \"JavaMetadata\",\n   \"metadata\": {\n    \"virtualPath\": \"/app/libs/tomcat-embed-el-9.0.27.jar\",\n    \"manifest\": {\n     \"main\": {\n      \"Ant-Version\": \"Apache Ant 1.9.9\",\n      \"Automatic-Module-Name\": \"org.apache.tomcat.embed.jasper.el\",\n      \"Bnd-LastModified\": \"1570442272460\",\n      \"Bundle-ManifestVersion\": \"2\",\n      \"Bundle-Name\": \"tomcat-embed-jasper-el\",\n      \"Bundle-SymbolicName\": \"org.apache.tomcat-embed-jasper-el\",\n      \"Bundle-Version\": \"9.0.27\",\n      \"Created-By\": \"1.8.0_222 (AdoptOpenJDK)\",\n      \"DSTAMP\": \"20191007\",\n      \"Export-Package\": \"javax.el,org.apache.el;uses:=\\\"javax.el,org.apache.el.parser\\\",org.apache.el.lang;uses:=\\\"javax.el,org.apache.el.parser\\\",org.apache.el.parser;uses:=\\\"javax.el,org.apache.el.lang\\\"\",\n      \"Implementation-Title\": \"Apache Tomcat\",\n      \"Implementation-Vendor\": \"Apache Software Foundation\",\n      \"Implementation-Version\": \"9.0.27\",\n      \"Import-Package\": \"javax.el,javax.servlet.jsp.el\",\n      \"Manifest-Version\": \"1.0\",\n      \"Originally-Created-By\": \"1.8.0_222-b10 ()\",\n      \"Private-Package\": \"org.apache.el.stream,org.apache.el.util\",\n      \"Require-Capability\": \"osgi.ee;filter:=\\\"(&(osgi.ee=JavaSE)(version=1.8))\\\"\",\n      \"Specification-Title\": \"Apache Tomcat\",\n      \"Specification-Vendor\": \"Apache Software Foundation\",\n      \"Specification-Version\": \"9.0\",\n      \"TODAY\": \"October 7 2019\",\n      \"TSTAMP\": \"1057\",\n      \"Tool\": \"Bnd-4.2.0.201903051501\",\n      \"X-Compile-Source-JDK\": \"8\",\n      \"X-Compile-Target-JDK\": \"8\"\n     }\n    }\n   }\n  }\n ],\n \"artifactRelationships\": [],\n \"source\": {\n  \"type\": \"image\",\n  \"target\": {\n   \"userInput\": \"springio/gs-spring-boot-docker:latest\",\n   \"imageID\": \"sha256:9065659c6e537b0364b7b1d3e5442a3a5aa56d755fb883d221e9e8b3637fb58e\",\n   \"manifestDigest\": \"sha256:be3d8a5f700d4c45f3ed324b95d9f028f587c135bc85cf87e193414db521d533\",\n   \"mediaType\": \"application/vnd.docker.distribution.manifest.v2+json\",\n   \"tags\": [\n    \"springio/gs-spring-boot-docker:latest\"\n   ],\n   \"imageSize\": 142807921,\n   \"layers\": [\n    {\n     \"mediaType\": \"application/vnd.docker.image.rootfs.diff.tar.gzip\",\n     \"digest\": \"sha256:42a3027eaac150d2b8f516100921f4bd83b3dbc20bfe64124f686c072b49c602\",\n     \"size\": 1809479\n    }\n   ],\n   \"manifest\": \"eyJzY2hlbWFWZXJzaW9uIjoyLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmRpc3RyaWJ1dGlvbi5tYW5pZmVzdC52Mitqc29uIiwiY29uZmlnIjp7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuY29udGFpbmVyLmltYWdlLnYxK2pzb24iLCJzaXplIjoxNTk1LCJkaWdlc3QiOiJzaGEyNTY6OTA2NTY1OWM2ZTUzN2IwMzY0YjdiMWQzZTU0NDJhM2E1YWE1NmQ3NTVmYjg4M2QyMjFlOWU4YjM2MzdmYjU4ZSJ9LCJsYXllcnMiOlt7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuaW1hZ2Uucm9vdGZzLmRpZmYudGFyLmd6aXAiLCJzaXplIjozMDYxNzYwLCJkaWdlc3QiOiJzaGEyNTY6NDJhMzAyN2VhYWMxNTBkMmI4ZjUxNjEwMDkyMWY0YmQ4M2IzZGJjMjBiZmU2NDEyNGY2ODZjMDcyYjQ5YzYwMiJ9LHsibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vdm5kLmRvY2tlci5pbWFnZS5yb290ZnMuZGlmZi50YXIuZ3ppcCIsInNpemUiOjE1NDQxOTIwLCJkaWdlc3QiOiJzaGEyNTY6ZjQ3MTYzZThkZTU3ZTNlM2NjZmU4OWQ1ZGZiZDljMjUyZDllY2E1M2RjNzkwNmI4ZGI2MGVkZGNiODc2YzU5MiJ9LHsibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vdm5kLmRvY2tlci5pbWFnZS5yb290ZnMuZGlmZi50YXIuZ3ppcCIsInNpemUiOjE5NjYwODAsImRpZ2VzdCI6InNoYTI1Njo2MTg5YWJlMDk1ZDUzYzFjOWYyYmZjOGY1MDEyOGVlODc2YjlhNWQxMGY5ZWRhMTU2NGU1ZjUzNTdkNmZmZTYxIn0seyJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmltYWdlLnJvb3Rmcy5kaWZmLnRhci5nemlwIiwic2l6ZSI6MTA2ODMzOTIwLCJkaWdlc3QiOiJzaGEyNTY6YTFhNmNlYWRiNzAxYWI0ZTZjOTNiMjQzZGMyYTBkYWVkYzhjZWUyM2EyNDIwMzg0NWVjY2NkNTc4NGNkMTM5MyJ9LHsibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vdm5kLmRvY2tlci5pbWFnZS5yb290ZnMuZGlmZi50YXIuZ3ppcCIsInNpemUiOjE3NDg3ODcyLCJkaWdlc3QiOiJzaGEyNTY6ODk1MDRmMDgzZDNmMTUzMjJmOTdhZTI0MGRmNDQ2NTAyMDNmMjQ0Mjc4NjBkYjFiM2QzMmU2NmRkMDU5NDBlNCJ9LHsibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vdm5kLmRvY2tlci5pbWFnZS5yb290ZnMuZGlmZi50YXIuZ3ppcCIsInNpemUiOjM1ODQsImRpZ2VzdCI6InNoYTI1NjoyNDQzNDk3MWNhN2Y0MGUxYTdlNjRlZThlYTFjYTg0MzQzMmRhMWUxYmIxYzU5ODM4NzA4MzUwNjU2ODBkMTU0In0seyJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmltYWdlLnJvb3Rmcy5kaWZmLnRhci5nemlwIiwic2l6ZSI6NDA5NiwiZGlnZXN0Ijoic2hhMjU2OjNjMDEwMjY1NDQ5ZDAwZmFlZTZmMmZhMWM3ZGY0ODEwOWMwZDcwOGM3MWJlZTRhMzhlNGI1ZTBmYTliODdjZTkifV19\",\n   \"config\": \"eyJjcmVhdGVkIjoiMTk3MC0wMS0wMVQwMDowMDowMFoiLCJhcmNoaXRlY3R1cmUiOiJhbWQ2NCIsIm9zIjoibGludXgiLCJjb25maWciOnsiRW52IjpbIlBBVEg9L3Vzci9sb2NhbC9zYmluOi91c3IvbG9jYWwvYmluOi91c3Ivc2JpbjovdXNyL2Jpbjovc2JpbjovYmluIiwiU1NMX0NFUlRfRklMRT0vZXRjL3NzbC9jZXJ0cy9jYS1jZXJ0aWZpY2F0ZXMuY3J0IiwiSkFWQV9WRVJTSU9OPTh1MjQyIl0sIkVudHJ5cG9pbnQiOlsiamF2YSIsIi1jcCIsIi9hcHAvcmVzb3VyY2VzOi9hcHAvY2xhc3NlczovYXBwL2xpYnMvKiIsImhlbGxvLkFwcGxpY2F0aW9uIl0sIkV4cG9zZWRQb3J0cyI6e30sIkxhYmVscyI6e30sIlZvbHVtZXMiOnt9fSwiaGlzdG9yeSI6W3siY3JlYXRlZCI6IjE5NzAtMDEtMDFUMDA6MDA6MDBaIiwiYXV0aG9yIjoiQmF6ZWwiLCJjcmVhdGVkX2J5IjoiYmF6ZWwgYnVpbGQgLi4uIn0seyJjcmVhdGVkIjoiMTk3MC0wMS0wMVQwMDowMDowMFoiLCJhdXRob3IiOiJCYXplbCIsImNyZWF0ZWRfYnkiOiJiYXplbCBidWlsZCAuLi4ifSx7ImNyZWF0ZWQiOiIxOTcwLTAxLTAxVDAwOjAwOjAwWiIsImF1dGhvciI6IkJhemVsIiwiY3JlYXRlZF9ieSI6ImJhemVsIGJ1aWxkIC4uLiJ9LHsiY3JlYXRlZCI6IjE5NzAtMDEtMDFUMDA6MDA6MDBaIiwiYXV0aG9yIjoiQmF6ZWwiLCJjcmVhdGVkX2J5IjoiYmF6ZWwgYnVpbGQgLi4uIn0seyJjcmVhdGVkIjoiMTk3MC0wMS0wMVQwMDowMDowMFoiLCJhdXRob3IiOiJKaWIiLCJjcmVhdGVkX2J5IjoiamliLW1hdmVuLXBsdWdpbjoyLjIuMCIsImNvbW1lbnQiOiJkZXBlbmRlbmNpZXMifSx7ImNyZWF0ZWQiOiIxOTcwLTAxLTAxVDAwOjAwOjAwWiIsImF1dGhvciI6IkppYiIsImNyZWF0ZWRfYnkiOiJqaWItbWF2ZW4tcGx1Z2luOjIuMi4wIiwiY29tbWVudCI6InJlc291cmNlcyJ9LHsiY3JlYXRlZCI6IjE5NzAtMDEtMDFUMDA6MDA6MDBaIiwiYXV0aG9yIjoiSmliIiwiY3JlYXRlZF9ieSI6ImppYi1tYXZlbi1wbHVnaW46Mi4yLjAiLCJjb21tZW50IjoiY2xhc3NlcyJ9XSwicm9vdGZzIjp7InR5cGUiOiJsYXllcnMiLCJkaWZmX2lkcyI6WyJzaGEyNTY6NDJhMzAyN2VhYWMxNTBkMmI4ZjUxNjEwMDkyMWY0YmQ4M2IzZGJjMjBiZmU2NDEyNGY2ODZjMDcyYjQ5YzYwMiIsInNoYTI1NjpmNDcxNjNlOGRlNTdlM2UzY2NmZTg5ZDVkZmJkOWMyNTJkOWVjYTUzZGM3OTA2YjhkYjYwZWRkY2I4NzZjNTkyIiwic2hhMjU2OjYxODlhYmUwOTVkNTNjMWM5ZjJiZmM4ZjUwMTI4ZWU4NzZiOWE1ZDEwZjllZGExNTY0ZTVmNTM1N2Q2ZmZlNjEiLCJzaGEyNTY6YTFhNmNlYWRiNzAxYWI0ZTZjOTNiMjQzZGMyYTBkYWVkYzhjZWUyM2EyNDIwMzg0NWVjY2NkNTc4NGNkMTM5MyIsInNoYTI1Njo4OTUwNGYwODNkM2YxNTMyMmY5N2FlMjQwZGY0NDY1MDIwM2YyNDQyNzg2MGRiMWIzZDMyZTY2ZGQwNTk0MGU0Iiwic2hhMjU2OjI0NDM0OTcxY2E3ZjQwZTFhN2U2NGVlOGVhMWNhODQzNDMyZGExZTFiYjFjNTk4Mzg3MDgzNTA2NTY4MGQxNTQiLCJzaGEyNTY6M2MwMTAyNjU0NDlkMDBmYWVlNmYyZmExYzdkZjQ4MTA5YzBkNzA4YzcxYmVlNGEzOGU0YjVlMGZhOWI4N2NlOSJdfX0=\",\n   \"repoDigests\": [\n    \"springio/gs-spring-boot-docker@sha256:39c2ffc784f5f34862e22c1f2ccdbcb62430736114c13f60111eabdb79decb08\"\n   ],\n   \"scope\": \"Squashed\"\n  }\n },\n \"distro\": {\n  \"name\": \"debian\",\n  \"version\": \"9\",\n  \"idLike\": \"\"\n },\n \"descriptor\": {\n  \"name\": \"syft\",\n  \"version\": \"[not provided]\"\n },\n \"schema\": {\n  \"version\": \"1.1.0\",\n  \"url\": \"https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-1.1.0.json\"\n }\n}\n"
  },
  {
    "path": "grype/pkg/upstream_package.go",
    "content": "package pkg\n\nimport (\n\t\"strings\"\n\n\t\"github.com/scylladb/go-set/strset\"\n\n\t\"github.com/anchore/syft/syft/cpe\"\n)\n\ntype UpstreamPackage struct {\n\tName    string // the package name\n\tVersion string // the version of the package\n}\n\nfunc UpstreamPackages(p Package) (pkgs []Package) {\n\toriginal := p\n\tfor _, u := range p.Upstreams {\n\t\ttmp := original\n\n\t\tif u.Name == \"\" {\n\t\t\tcontinue\n\t\t}\n\n\t\ttmp.Name = u.Name\n\t\tif u.Version != \"\" {\n\t\t\ttmp.Version = u.Version\n\t\t}\n\t\ttmp.Upstreams = nil\n\n\t\t// for each cpe, replace pkg name with origin and add to set\n\t\tcpeStrings := strset.New()\n\t\tfor _, c := range tmp.CPEs {\n\t\t\tif u.Version != \"\" {\n\t\t\t\tc.Attributes.Version = u.Version\n\t\t\t}\n\n\t\t\t// use BindToFmtString because we search against unescaped CPE strings\n\t\t\tupdatedCPEString := strings.ReplaceAll(c.Attributes.BindToFmtString(), p.Name, u.Name)\n\n\t\t\tcpeStrings.Add(updatedCPEString)\n\t\t}\n\n\t\t// with each entry in set, convert string to CPE and update the new CPEs\n\t\tvar updatedCPEs []cpe.CPE\n\t\tfor _, cpeString := range cpeStrings.List() {\n\t\t\tupdatedCPE, _ := cpe.New(cpeString, \"\")\n\t\t\tupdatedCPEs = append(updatedCPEs, updatedCPE)\n\t\t}\n\t\ttmp.CPEs = updatedCPEs\n\n\t\tpkgs = append(pkgs, tmp)\n\t}\n\treturn pkgs\n}\n"
  },
  {
    "path": "grype/pkg/upstream_package_test.go",
    "content": "package pkg\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\n\t\"github.com/anchore/syft/syft/cpe\"\n)\n\nfunc TestUpstreamPackages(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tpkg      Package\n\t\texpected []Package\n\t}{\n\t\t{\n\t\t\tname: \"no upstreams results in empty list\",\n\t\t\tpkg: Package{\n\t\t\t\tName:    \"name\",\n\t\t\t\tVersion: \"version\",\n\t\t\t},\n\t\t\texpected: nil,\n\t\t},\n\t\t{\n\t\t\tname: \"with upstream name\",\n\t\t\tpkg: Package{\n\t\t\t\tName:    \"name\",\n\t\t\t\tVersion: \"version\",\n\t\t\t\tCPEs: []cpe.CPE{\n\t\t\t\t\tcpe.Must(\"cpe:2.3:*:name:name:version:*:*:*:*:*:*:*\", \"\"),\n\t\t\t\t},\n\t\t\t\tUpstreams: []UpstreamPackage{\n\t\t\t\t\t{\n\t\t\t\t\t\tName: \"new-name\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: []Package{\n\t\t\t\t{\n\t\t\t\t\tName:    \"new-name\", // new\n\t\t\t\t\tVersion: \"version\",  // original\n\t\t\t\t\tCPEs: []cpe.CPE{\n\t\t\t\t\t\t// name and vendor replaced\n\t\t\t\t\t\tcpe.Must(\"cpe:2.3:*:new-name:new-name:version:*:*:*:*:*:*:*\", \"\"),\n\t\t\t\t\t},\n\t\t\t\t\t// no upstreams\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"with upstream name and version\",\n\t\t\tpkg: Package{\n\t\t\t\tName:    \"name\",\n\t\t\t\tVersion: \"version\",\n\t\t\t\tCPEs: []cpe.CPE{\n\t\t\t\t\tcpe.Must(\"cpe:2.3:*:name:name:version:*:*:*:*:*:*:*\", \"\"),\n\t\t\t\t},\n\t\t\t\tUpstreams: []UpstreamPackage{\n\t\t\t\t\t{\n\t\t\t\t\t\tName:    \"new-name\",\n\t\t\t\t\t\tVersion: \"new-version\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: []Package{\n\t\t\t\t{\n\t\t\t\t\tName:    \"new-name\",    // new\n\t\t\t\t\tVersion: \"new-version\", // new\n\t\t\t\t\tCPEs: []cpe.CPE{\n\t\t\t\t\t\t// name, vendor, and version replaced\n\t\t\t\t\t\tcpe.Must(\"cpe:2.3:*:new-name:new-name:new-version:*:*:*:*:*:*:*\", \"\"),\n\t\t\t\t\t},\n\t\t\t\t\t// no upstreams\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"no upstream name results in no package\",\n\t\t\tpkg: Package{\n\t\t\t\tName:    \"name\",\n\t\t\t\tVersion: \"version\",\n\t\t\t\tCPEs: []cpe.CPE{\n\t\t\t\t\tcpe.Must(\"cpe:2.3:*:name:name:version:*:*:*:*:*:*:*\", \"\"),\n\t\t\t\t},\n\t\t\t\tUpstreams: []UpstreamPackage{\n\t\t\t\t\t{\n\t\t\t\t\t\t// note: invalid without a name\n\t\t\t\t\t\tVersion: \"new-version\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: nil,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvar actual []Package\n\t\t\tactual = append(actual, UpstreamPackages(tt.pkg)...)\n\t\t\tassert.Equalf(t, tt.expected, actual, \"UpstreamPackages(%v)\", tt.pkg)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/pkg/version_format.go",
    "content": "package pkg\n\nimport (\n\t\"github.com/anchore/grype/grype/version\"\n\tsyftPkg \"github.com/anchore/syft/syft/pkg\"\n)\n\nfunc VersionFormat(p Package) version.Format {\n\tswitch p.Type {\n\tcase syftPkg.ApkPkg:\n\t\treturn version.ApkFormat\n\tcase syftPkg.BitnamiPkg:\n\t\treturn version.BitnamiFormat\n\tcase syftPkg.DebPkg:\n\t\treturn version.DebFormat\n\tcase syftPkg.JavaPkg:\n\t\treturn version.MavenFormat\n\tcase syftPkg.RpmPkg:\n\t\treturn version.RpmFormat\n\tcase syftPkg.GemPkg:\n\t\treturn version.GemFormat\n\tcase syftPkg.PythonPkg:\n\t\treturn version.PythonFormat\n\tcase syftPkg.KbPkg:\n\t\treturn version.KBFormat\n\tcase syftPkg.PortagePkg:\n\t\treturn version.PortageFormat\n\tcase syftPkg.GoModulePkg:\n\t\treturn version.GolangFormat\n\tcase syftPkg.AlpmPkg:\n\t\treturn version.PacmanFormat\n\t}\n\n\tif isJvmPackage(p) {\n\t\treturn version.JVMFormat\n\t}\n\n\treturn version.UnknownFormat\n}\n"
  },
  {
    "path": "grype/pkg/version_format_test.go",
    "content": "package pkg\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/anchore/grype/grype/version\"\n\tsyftPkg \"github.com/anchore/syft/syft/pkg\"\n)\n\nfunc TestVersionFormat(t *testing.T) {\n\ttests := []struct {\n\t\tname   string\n\t\tp      Package\n\t\tformat version.Format\n\t}{\n\t\t{\n\t\t\tname: \"bitnami\",\n\t\t\tp: Package{\n\t\t\t\tType: syftPkg.BitnamiPkg,\n\t\t\t},\n\t\t\tformat: version.BitnamiFormat,\n\t\t},\n\t\t{\n\t\t\tname: \"deb\",\n\t\t\tp: Package{\n\t\t\t\tType: syftPkg.DebPkg,\n\t\t\t},\n\t\t\tformat: version.DebFormat,\n\t\t},\n\t\t{\n\t\t\tname: \"java jar\",\n\t\t\tp: Package{\n\t\t\t\tType: syftPkg.JavaPkg,\n\t\t\t},\n\t\t\tformat: version.MavenFormat,\n\t\t},\n\t\t{\n\t\t\tname: \"gem\",\n\t\t\tp: Package{\n\t\t\t\tType: syftPkg.GemPkg,\n\t\t\t},\n\t\t\tformat: version.GemFormat,\n\t\t},\n\t\t{\n\t\t\tname: \"alpm (arch linux)\",\n\t\t\tp: Package{\n\t\t\t\tType: syftPkg.AlpmPkg,\n\t\t\t},\n\t\t\tformat: version.PacmanFormat,\n\t\t},\n\t\t{\n\t\t\tname: \"jvm by metadata\",\n\t\t\tp: Package{\n\t\t\t\tMetadata: JavaVMInstallationMetadata{},\n\t\t\t},\n\t\t\tformat: version.JVMFormat,\n\t\t},\n\t\t{\n\t\t\tname: \"jvm by type and name (jdk)\",\n\t\t\tp: Package{\n\t\t\t\tType: syftPkg.BinaryPkg,\n\t\t\t\tName: \"jdk\",\n\t\t\t},\n\t\t\tformat: version.JVMFormat,\n\t\t},\n\t\t{\n\t\t\tname: \"jvm by type and name (openjdk)\",\n\t\t\tp: Package{\n\t\t\t\tType: syftPkg.BinaryPkg,\n\t\t\t\tName: \"openjdk\",\n\t\t\t},\n\t\t\tformat: version.JVMFormat,\n\t\t},\n\t\t{\n\t\t\tname: \"jvm by type and name (jre)\",\n\t\t\tp: Package{\n\t\t\t\tType: syftPkg.BinaryPkg,\n\t\t\t\tName: \"jre\",\n\t\t\t},\n\t\t\tformat: version.JVMFormat,\n\t\t},\n\t\t{\n\t\t\tname: \"jvm by type and name (java_se)\",\n\t\t\tp: Package{\n\t\t\t\tType: syftPkg.BinaryPkg,\n\t\t\t\tName: \"java_se\",\n\t\t\t},\n\t\t\tformat: version.JVMFormat,\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tname := fmt.Sprintf(\"pkgType[%s]->format[%s]\", test.p.Type, test.format)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tactual := VersionFormat(test.p)\n\t\t\tif actual != test.format {\n\t\t\t\tt.Errorf(\"mismatched pkgType->format mapping, pkgType='%s': '%s'!='%s'\", test.p.Type, test.format, actual)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/presenter/cyclonedx/presenter.go",
    "content": "package cyclonedx\n\nimport (\n\t\"io\"\n\n\t\"github.com/CycloneDX/cyclonedx-go\"\n\n\t\"github.com/anchore/clio\"\n\t\"github.com/anchore/grype/grype/presenter/models\"\n\t\"github.com/anchore/syft/syft/format/common/cyclonedxhelpers\"\n\t\"github.com/anchore/syft/syft/sbom\"\n\t\"github.com/anchore/syft/syft/source\"\n)\n\n// Presenter writes a CycloneDX report from the given Matches and Scope contents\ntype Presenter struct {\n\tid       clio.Identification\n\tdocument models.Document\n\tsrc      source.Description\n\tformat   cyclonedx.BOMFileFormat\n\tsbom     *sbom.SBOM\n}\n\n// NewJSONPresenter is a *Presenter constructor\nfunc NewJSONPresenter(pb models.PresenterConfig) *Presenter {\n\treturn &Presenter{\n\t\tid:       pb.ID,\n\t\tdocument: pb.Document,\n\t\tsrc:      pb.SBOM.Source,\n\t\tsbom:     pb.SBOM,\n\t\tformat:   cyclonedx.BOMFileFormatJSON,\n\t}\n}\n\n// NewXMLPresenter is a *Presenter constructor\nfunc NewXMLPresenter(pb models.PresenterConfig) *Presenter {\n\treturn &Presenter{\n\t\tid:       pb.ID,\n\t\tdocument: pb.Document,\n\t\tsrc:      pb.SBOM.Source,\n\t\tsbom:     pb.SBOM,\n\t\tformat:   cyclonedx.BOMFileFormatXML,\n\t}\n}\n\n// Present creates a CycloneDX-based reporting\nfunc (p *Presenter) Present(output io.Writer) error {\n\t// note: this uses the syft cyclondx helpers to create\n\t// a consistent cyclondx BOM across syft and grype\n\tcyclonedxBOM := cyclonedxhelpers.ToFormatModel(*p.sbom)\n\n\t// empty the tool metadata and add grype metadata\n\tcyclonedxBOM.Metadata.Tools = &cyclonedx.ToolsChoice{\n\t\tComponents: &[]cyclonedx.Component{\n\t\t\t{\n\t\t\t\tType:    cyclonedx.ComponentTypeApplication,\n\t\t\t\tAuthor:  \"anchore\",\n\t\t\t\tName:    p.id.Name,\n\t\t\t\tVersion: p.id.Version,\n\t\t\t},\n\t\t},\n\t}\n\n\tvulns := make([]cyclonedx.Vulnerability, 0)\n\tfor _, m := range p.document.Matches {\n\t\tv, err := NewVulnerability(m)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tvulns = append(vulns, v)\n\t}\n\tcyclonedxBOM.Vulnerabilities = &vulns\n\tenc := cyclonedx.NewBOMEncoder(output, p.format)\n\tenc.SetPretty(true)\n\tenc.SetEscapeHTML(false)\n\n\treturn enc.EncodeVersion(cyclonedxBOM, cyclonedxBOM.SpecVersion)\n}\n"
  },
  {
    "path": "grype/presenter/cyclonedx/presenter_test.go",
    "content": "package cyclonedx\n\nimport (\n\t\"bytes\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os/exec\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/grype/grype/presenter/internal\"\n\t\"github.com/anchore/grype/grype/presenter/models\"\n\t\"github.com/anchore/grype/internal/testutils\"\n\t\"github.com/anchore/syft/syft/file\"\n\t\"github.com/anchore/syft/syft/sbom\"\n)\n\nvar update = flag.Bool(\"update\", false, \"update the *.golden files for cyclonedx presenters\")\nvar validatorImage = \"cyclonedx/cyclonedx-cli:0.27.2@sha256:829c9ea8f2104698bc3c1228575bfa495f6cc4ec151329323c013ca94408477f\"\n\nfunc Test_CycloneDX_Valid(t *testing.T) {\n\tif _, err := exec.LookPath(\"docker\"); err != nil {\n\t\tt.Skip(\"docker not available\")\n\t}\n\n\ttests := []struct {\n\t\tname   string\n\t\tscheme internal.SyftSource\n\t}{\n\t\t{\n\t\t\tname:   \"json directory\",\n\t\t\tscheme: internal.DirectorySource,\n\t\t},\n\t\t{\n\t\t\tname:   \"json image\",\n\t\t\tscheme: internal.ImageSource,\n\t\t},\n\t\t{\n\t\t\tname:   \"xml directory\",\n\t\t\tscheme: internal.DirectorySource,\n\t\t},\n\t\t{\n\t\t\tname:   \"xml image\",\n\t\t\tscheme: internal.ImageSource,\n\t\t},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tformat := strings.Split(tc.name, \" \")[0]\n\t\t\tvar buffer bytes.Buffer\n\n\t\t\tpb := internal.GeneratePresenterConfig(t, tc.scheme)\n\n\t\t\tvar pres *Presenter\n\t\t\tswitch format {\n\t\t\tcase \"json\":\n\t\t\t\tpres = NewJSONPresenter(pb)\n\t\t\tcase \"xml\":\n\t\t\t\tpres = NewXMLPresenter(pb)\n\t\t\tdefault:\n\t\t\t\tt.Fatalf(\"invalid format: %s\", format)\n\t\t\t}\n\n\t\t\terr := pres.Present(&buffer)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tcontents := buffer.String()\n\n\t\t\tcmd := exec.Command(\"docker\", \"run\", \"--rm\", \"-i\", \"--entrypoint\", \"/bin/sh\", validatorImage,\n\t\t\t\t\"-c\", fmt.Sprintf(\"tee &> /dev/null && cyclonedx validate --input-version v1_6 --fail-on-errors --input-format %s\", format))\n\n\t\t\tout := bytes.Buffer{}\n\t\t\tcmd.Stdout = &out\n\t\t\tcmd.Stderr = &out\n\n\t\t\t// pipe to the docker command\n\t\t\tcmd.Stdin = strings.NewReader(contents)\n\n\t\t\terr = cmd.Run()\n\t\t\tif err != nil || cmd.ProcessState.ExitCode() != 0 {\n\t\t\t\t// not valid\n\t\t\t\tt.Fatalf(\"error validating CycloneDX %s document: %s \\nBOM:\\n%s\", format, out.String(), contents)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_noTypedNils(t *testing.T) {\n\ts := sbom.SBOM{\n\t\tArtifacts: sbom.Artifacts{\n\t\t\tFileMetadata: map[file.Coordinates]file.Metadata{},\n\t\t\tFileDigests:  map[file.Coordinates][]file.Digest{},\n\t\t},\n\t}\n\tc := file.NewCoordinates(\"/file\", \"123\")\n\ts.Artifacts.FileMetadata[c] = file.Metadata{\n\t\tPath: \"/file\",\n\t}\n\ts.Artifacts.FileDigests[c] = []file.Digest{}\n\n\tp := NewJSONPresenter(models.PresenterConfig{\n\t\tSBOM:   &s,\n\t\tPretty: false,\n\t})\n\tcontents := bytes.Buffer{}\n\terr := p.Present(&contents)\n\trequire.NoError(t, err)\n\trequire.NotContains(t, contents.String(), \"null\")\n}\n\nfunc TestCycloneDxPresenterImage(t *testing.T) {\n\tvar buffer bytes.Buffer\n\n\tpb := internal.GeneratePresenterConfig(t, internal.ImageSource)\n\n\tpres := NewJSONPresenter(pb)\n\t// run presenter\n\terr := pres.Present(&buffer)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tactual := buffer.Bytes()\n\tif *update {\n\t\ttestutils.UpdateGoldenFileContents(t, actual)\n\t}\n\n\tvar expected = testutils.GetGoldenFileContents(t)\n\n\t// remove dynamic values, which are tested independently\n\tactual = internal.Redact(actual)\n\texpected = internal.Redact(expected)\n\n\tif d := cmp.Diff(string(expected), string(actual)); d != \"\" {\n\t\tt.Fatalf(\"diff: %s\", d)\n\t}\n}\n\nfunc TestCycloneDxPresenterDir(t *testing.T) {\n\tvar buffer bytes.Buffer\n\n\tpb := internal.GeneratePresenterConfig(t, internal.DirectorySource)\n\n\tpres := NewJSONPresenter(pb)\n\n\t// run presenter\n\terr := pres.Present(&buffer)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tactual := buffer.Bytes()\n\tif *update {\n\t\ttestutils.UpdateGoldenFileContents(t, actual)\n\t}\n\n\tvar expected = testutils.GetGoldenFileContents(t)\n\n\t// remove dynamic values, which are tested independently\n\tactual = internal.Redact(actual)\n\texpected = internal.Redact(expected)\n\n\tif d := cmp.Diff(string(expected), string(actual)); d != \"\" {\n\t\tt.Fatalf(\"diff: %s\", d)\n\t}\n}\n"
  },
  {
    "path": "grype/presenter/cyclonedx/testdata/snapshot/TestCycloneDxPresenterDir.golden",
    "content": "{\n  \"$schema\": \"http://cyclonedx.org/schema/bom-1.6.schema.json\",\n  \"bomFormat\": \"CycloneDX\",\n  \"specVersion\": \"1.6\",\n  \"serialNumber\": \"urn:uuid:802d9c0b-716e-4050-a832-9a697dc0d796\",\n  \"version\": 1,\n  \"metadata\": {\n    \"timestamp\": \"2026-03-19T11:34:26-04:00\",\n    \"tools\": {\n      \"components\": [\n        {\n          \"type\": \"application\",\n          \"author\": \"anchore\",\n          \"name\": \"grype\",\n          \"version\": \"[not provided]\"\n        }\n      ]\n    },\n    \"component\": {\n      \"bom-ref\": \"163686ac6e30c752\",\n      \"type\": \"file\",\n      \"name\": \"/var/folders/09/zjmdnk0n4496cmzrdkxbw1tr0000gn/T/TestCycloneDxPresenterDir688002288/001\"\n    }\n  },\n  \"components\": [\n    {\n      \"bom-ref\": \"bbb0ba712c2b94ea\",\n      \"type\": \"library\",\n      \"name\": \"package-1\",\n      \"version\": \"1.1.1\",\n      \"cpe\": \"cpe:2.3:a:anchore\\\\:oss:anchore\\\\/engine:0.9.2:*:*:en:*:*:*:*\",\n      \"properties\": [\n        {\n          \"name\": \"syft:package:type\",\n          \"value\": \"rpm\"\n        },\n        {\n          \"name\": \"syft:package:metadataType\",\n          \"value\": \"rpm-db-entry\"\n        },\n        {\n          \"name\": \"syft:location:0:path\",\n          \"value\": \"/foo/bar/somefile-1.txt\"\n        },\n        {\n          \"name\": \"syft:metadata:epoch\",\n          \"value\": \"2\"\n        },\n        {\n          \"name\": \"syft:metadata:size\",\n          \"value\": \"0\"\n        },\n        {\n          \"name\": \"syft:metadata:sourceRpm\",\n          \"value\": \"some-source-rpm\"\n        }\n      ]\n    },\n    {\n      \"bom-ref\": \"pkg:deb/package-2@2.2.2?package-id=74378afe15713625\",\n      \"type\": \"library\",\n      \"name\": \"package-2\",\n      \"version\": \"2.2.2\",\n      \"licenses\": [\n        {\n          \"license\": {\n            \"id\": \"Apache-2.0\"\n          }\n        },\n        {\n          \"license\": {\n            \"id\": \"MIT\"\n          }\n        }\n      ],\n      \"cpe\": \"cpe:2.3:a:anchore:engine:2.2.2:*:*:en:*:*:*:*\",\n      \"purl\": \"pkg:deb/package-2@2.2.2\",\n      \"properties\": [\n        {\n          \"name\": \"syft:package:type\",\n          \"value\": \"deb\"\n        },\n        {\n          \"name\": \"syft:location:0:path\",\n          \"value\": \"/foo/bar/somefile-2.txt\"\n        }\n      ]\n    }\n  ],\n  \"vulnerabilities\": [\n    {\n      \"bom-ref\": \"urn:uuid:a73ee074-a643-466a-82ba-1669a7fa5c1d\",\n      \"id\": \"CVE-1999-0001\",\n      \"source\": {},\n      \"references\": [\n        {\n          \"id\": \"CVE-1999-0001\",\n          \"source\": {}\n        }\n      ],\n      \"ratings\": [\n        {\n          \"score\": 8.2,\n          \"severity\": \"low\",\n          \"method\": \"CVSSv31\",\n          \"vector\": \"CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:H\"\n        },\n        {\n          \"source\": {\n            \"name\": \"FIRST\",\n            \"url\": \"https://www.first.org/epss/\"\n          },\n          \"score\": 0.03,\n          \"method\": \"other\"\n        }\n      ],\n      \"affects\": [\n        {\n          \"ref\": \"bbb0ba712c2b94ea\"\n        }\n      ]\n    },\n    {\n      \"bom-ref\": \"urn:uuid:7324150f-e4ab-4d68-8dd8-b9721b4a0eed\",\n      \"id\": \"CVE-1999-0002\",\n      \"source\": {},\n      \"references\": [\n        {\n          \"id\": \"CVE-1999-0002\",\n          \"source\": {}\n        }\n      ],\n      \"ratings\": [\n        {\n          \"score\": 8.5,\n          \"severity\": \"critical\",\n          \"method\": \"CVSSv31\",\n          \"vector\": \"CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:H/A:H\"\n        },\n        {\n          \"source\": {\n            \"name\": \"FIRST\",\n            \"url\": \"https://www.first.org/epss/\"\n          },\n          \"score\": 0.08,\n          \"method\": \"other\"\n        },\n        {\n          \"source\": {\n            \"name\": \"CISA KEV Catalog\",\n            \"url\": \"https://www.cisa.gov/known-exploited-vulnerabilities-catalog\"\n          },\n          \"score\": 1,\n          \"method\": \"other\",\n          \"justification\": \"Listed in CISA KEV\"\n        }\n      ],\n      \"affects\": [\n        {\n          \"ref\": \"pkg:deb/package-2@2.2.2?package-id=74378afe15713625\"\n        }\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": "grype/presenter/cyclonedx/testdata/snapshot/TestCycloneDxPresenterImage.golden",
    "content": "{\n  \"$schema\": \"http://cyclonedx.org/schema/bom-1.6.schema.json\",\n  \"bomFormat\": \"CycloneDX\",\n  \"specVersion\": \"1.6\",\n  \"serialNumber\": \"urn:uuid:491fc8dc-9384-4f3d-a729-d2c5a83791d8\",\n  \"version\": 1,\n  \"metadata\": {\n    \"timestamp\": \"2026-03-19T11:34:26-04:00\",\n    \"tools\": {\n      \"components\": [\n        {\n          \"type\": \"application\",\n          \"author\": \"anchore\",\n          \"name\": \"grype\",\n          \"version\": \"[not provided]\"\n        }\n      ]\n    },\n    \"component\": {\n      \"bom-ref\": \"1882f79f937f7d91\",\n      \"type\": \"container\",\n      \"name\": \"user-input\",\n      \"version\": \"sha256:ca738abb87a8d58f112d3400ebb079b61ceae7dc290beb34bda735be4b1941d5\"\n    }\n  },\n  \"components\": [\n    {\n      \"bom-ref\": \"bbb0ba712c2b94ea\",\n      \"type\": \"library\",\n      \"name\": \"package-1\",\n      \"version\": \"1.1.1\",\n      \"cpe\": \"cpe:2.3:a:anchore\\\\:oss:anchore\\\\/engine:0.9.2:*:*:en:*:*:*:*\",\n      \"properties\": [\n        {\n          \"name\": \"syft:package:type\",\n          \"value\": \"rpm\"\n        },\n        {\n          \"name\": \"syft:package:metadataType\",\n          \"value\": \"rpm-db-entry\"\n        },\n        {\n          \"name\": \"syft:location:0:path\",\n          \"value\": \"/foo/bar/somefile-1.txt\"\n        },\n        {\n          \"name\": \"syft:metadata:epoch\",\n          \"value\": \"2\"\n        },\n        {\n          \"name\": \"syft:metadata:size\",\n          \"value\": \"0\"\n        },\n        {\n          \"name\": \"syft:metadata:sourceRpm\",\n          \"value\": \"some-source-rpm\"\n        }\n      ]\n    },\n    {\n      \"bom-ref\": \"pkg:deb/package-2@2.2.2?package-id=74378afe15713625\",\n      \"type\": \"library\",\n      \"name\": \"package-2\",\n      \"version\": \"2.2.2\",\n      \"licenses\": [\n        {\n          \"license\": {\n            \"id\": \"Apache-2.0\"\n          }\n        },\n        {\n          \"license\": {\n            \"id\": \"MIT\"\n          }\n        }\n      ],\n      \"cpe\": \"cpe:2.3:a:anchore:engine:2.2.2:*:*:en:*:*:*:*\",\n      \"purl\": \"pkg:deb/package-2@2.2.2\",\n      \"properties\": [\n        {\n          \"name\": \"syft:package:type\",\n          \"value\": \"deb\"\n        },\n        {\n          \"name\": \"syft:location:0:path\",\n          \"value\": \"/foo/bar/somefile-2.txt\"\n        }\n      ]\n    }\n  ],\n  \"vulnerabilities\": [\n    {\n      \"bom-ref\": \"urn:uuid:8f52aaa6-907a-43e7-8b48-836453a51bbf\",\n      \"id\": \"CVE-1999-0001\",\n      \"source\": {},\n      \"references\": [\n        {\n          \"id\": \"CVE-1999-0001\",\n          \"source\": {}\n        }\n      ],\n      \"ratings\": [\n        {\n          \"score\": 8.2,\n          \"severity\": \"low\",\n          \"method\": \"CVSSv31\",\n          \"vector\": \"CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:H\"\n        },\n        {\n          \"source\": {\n            \"name\": \"FIRST\",\n            \"url\": \"https://www.first.org/epss/\"\n          },\n          \"score\": 0.03,\n          \"method\": \"other\"\n        }\n      ],\n      \"affects\": [\n        {\n          \"ref\": \"bbb0ba712c2b94ea\"\n        }\n      ]\n    },\n    {\n      \"bom-ref\": \"urn:uuid:166daf49-c343-460e-b66a-c22247147678\",\n      \"id\": \"CVE-1999-0002\",\n      \"source\": {},\n      \"references\": [\n        {\n          \"id\": \"CVE-1999-0002\",\n          \"source\": {}\n        }\n      ],\n      \"ratings\": [\n        {\n          \"score\": 8.5,\n          \"severity\": \"critical\",\n          \"method\": \"CVSSv31\",\n          \"vector\": \"CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:H/A:H\"\n        },\n        {\n          \"source\": {\n            \"name\": \"FIRST\",\n            \"url\": \"https://www.first.org/epss/\"\n          },\n          \"score\": 0.08,\n          \"method\": \"other\"\n        },\n        {\n          \"source\": {\n            \"name\": \"CISA KEV Catalog\",\n            \"url\": \"https://www.cisa.gov/known-exploited-vulnerabilities-catalog\"\n          },\n          \"score\": 1,\n          \"method\": \"other\",\n          \"justification\": \"Listed in CISA KEV\"\n        }\n      ],\n      \"affects\": [\n        {\n          \"ref\": \"pkg:deb/package-2@2.2.2?package-id=74378afe15713625\"\n        }\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": "grype/presenter/cyclonedx/vulnerability.go",
    "content": "package cyclonedx\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/CycloneDX/cyclonedx-go\"\n\t\"github.com/google/uuid\"\n\n\t\"github.com/anchore/grype/grype/presenter/models\"\n\t\"github.com/anchore/packageurl-go\"\n)\n\n// https://cyclonedx.org/docs/1.4/json/#vulnerabilities_items_bom-ref\n\n// NewVulnerability creates a Vulnerability document from a match and the metadata provider\nfunc NewVulnerability(m models.Match) (v cyclonedx.Vulnerability, err error) {\n\tmetadata := m.Vulnerability.VulnerabilityMetadata\n\n\tratings := generateCDXRatings(metadata)\n\n\tsource := &cyclonedx.Source{\n\t\tName: cdxSourceName(metadata.Namespace),\n\t\tURL:  metadata.DataSource,\n\t}\n\n\treferences := &[]cyclonedx.VulnerabilityReference{\n\t\t{\n\t\t\tID:     m.Vulnerability.ID,\n\t\t\tSource: source,\n\t\t},\n\t}\n\n\tadvisories := &[]cyclonedx.Advisory{}\n\tfor _, advisory := range metadata.URLs {\n\t\t*advisories = append(*advisories, cyclonedx.Advisory{\n\t\t\tURL: advisory,\n\t\t})\n\t}\n\n\t// Note: if a field isn't captured here it's usually because the resulting\n\t// reference link contains that information for the consumer\n\treturn cyclonedx.Vulnerability{\n\t\tBOMRef:     uuid.New().URN(),\n\t\tID:         m.Vulnerability.ID,\n\t\tSource:     source,\n\t\tReferences: references,\n\t\tRatings:    &ratings,\n\t\t// We do not capture CWEs in our model\n\t\tCWEs:        nil,\n\t\tDescription: metadata.Description,\n\t\t// We do not capture the full detailed description in our model\n\t\tDetail: \"\",\n\t\t// We do not capture the recommendations in our model\n\t\tRecommendation: \"\",\n\t\tAdvisories:     advisories,\n\t\tAffects: &[]cyclonedx.Affects{\n\t\t\t{\n\t\t\t\tRef: deriveBomRef(m.Artifact),\n\t\t\t},\n\t\t},\n\t\t// Data source creation\n\t\tCreated: \"\",\n\t\t// Vulnerability first published\n\t\tPublished: \"\",\n\t\t// Vulnerability last updated\n\t\tUpdated: \"\",\n\t\t// We do not capture acredited in our model\n\t\tCredits: nil,\n\t\t// We do not capture information about the  method used to determine the vulnerability pre publishing\n\t\tTools: nil,\n\t\t// TODO:  we do not leverage the following fields in our model\n\t\tAnalysis:   nil,\n\t\tProperties: nil,\n\t}, nil\n}\n\nfunc generateCDXRatings(metadata models.VulnerabilityMetadata) []cyclonedx.VulnerabilityRating {\n\tseverity := cdxSeverityFromGrypeSeverity(metadata.Severity)\n\n\tratings := make([]cyclonedx.VulnerabilityRating, 0)\n\tfor _, cvss := range metadata.Cvss {\n\t\tvar rating cyclonedx.VulnerabilityRating\n\t\tscore := cvss.Metrics.BaseScore\n\t\trating.Score = &score\n\n\t\t// Scoring method can be one of the following:\n\t\t// \"CVSSv2\", \"CVSSv3\", \"CVSSv31\", \"OWASP\", \"other\"\n\t\tmethod, err := cvssVersionToMethod(cvss.Version)\n\t\tif err != nil {\n\t\t\t// do not halt execution if one CVSS fails to provide an accurate Version\n\t\t\t// TODO: log warning here?\n\t\t\tcontinue\n\t\t}\n\t\trating.Method = method\n\t\trating.Vector = cvss.Vector\n\t\trating.Severity = severity\n\t\tratings = append(ratings, rating)\n\t}\n\n\t// ensure the severity is always included\n\tif len(ratings) == 0 {\n\t\tratings = append(ratings, cyclonedx.VulnerabilityRating{\n\t\t\tSeverity: severity,\n\t\t})\n\t}\n\n\t// Add EPSS score if available\n\tif len(metadata.EPSS) > 0 {\n\t\tepssScore := metadata.EPSS[0].EPSS\n\n\t\tratings = append(ratings, cyclonedx.VulnerabilityRating{\n\t\t\tMethod: cyclonedx.ScoringMethod(\"EPSS\"),\n\t\t\tScore:  &epssScore,\n\t\t\tSource: &cyclonedx.Source{\n\t\t\t\tName: \"FIRST\",\n\t\t\t\tURL:  \"https://www.first.org/epss/\",\n\t\t\t},\n\t\t})\n\t}\n\t// Add KEV indication if available\n\tif len(metadata.KnownExploited) > 0 {\n\t\tkevScore := 1.0\n\n\t\tratings = append(ratings, cyclonedx.VulnerabilityRating{\n\t\t\tMethod: cyclonedx.ScoringMethodOther,\n\t\t\tScore:  &kevScore,\n\t\t\tSource: &cyclonedx.Source{\n\t\t\t\tName: \"CISA KEV Catalog\",\n\t\t\t\tURL:  \"https://www.cisa.gov/known-exploited-vulnerabilities-catalog\",\n\t\t\t},\n\t\t\tJustification: \"Listed in CISA KEV\",\n\t\t})\n\t}\n\n\treturn ratings\n}\n\n// cvssVersionToMethod accepts a CVSS version as string (e.g. \"3.1\") and converts it to a\n// CycloneDx rating Method, for example \"CVSSv3\"\nfunc cvssVersionToMethod(version string) (cyclonedx.ScoringMethod, error) {\n\tvalue, err := strconv.ParseFloat(version, 64)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tswitch value {\n\tcase 2:\n\t\treturn cyclonedx.ScoringMethodCVSSv2, nil\n\tcase 3:\n\t\treturn cyclonedx.ScoringMethodCVSSv3, nil\n\tcase 3.1:\n\t\treturn cyclonedx.ScoringMethodCVSSv31, nil\n\tdefault:\n\t\treturn cyclonedx.ScoringMethodOther, nil\n\t}\n}\n\n// takes namespace: eg debian:distro:debian:10\n// returns source name: eg debian-distrot-debian-10\nfunc cdxSourceName(namespace string) string {\n\treturn strings.ReplaceAll(namespace, \":\", \"-\")\n}\n\nfunc cdxSeverityFromGrypeSeverity(severity string) cyclonedx.Severity {\n\tswitch severity {\n\tcase \"Negligible\":\n\t\treturn cyclonedx.SeverityNone\n\tcase \"Unknown\":\n\t\treturn cyclonedx.SeverityUnknown\n\tcase \"Info\":\n\t\treturn cyclonedx.SeverityInfo\n\tcase \"Low\":\n\t\treturn cyclonedx.SeverityLow\n\tcase \"Medium\":\n\t\treturn cyclonedx.SeverityMedium\n\tcase \"High\":\n\t\treturn cyclonedx.SeverityHigh\n\tcase \"Critical\":\n\t\treturn cyclonedx.SeverityCritical\n\tdefault:\n\t\treturn cyclonedx.SeverityUnknown\n\t}\n}\n\nfunc deriveBomRef(p models.Package) string {\n\t// try and parse the PURL if possible and append syft id to it, to make\n\t// the purl unique in the BOM.\n\t// TODO: In the future we may want to dedupe by PURL and combine components with\n\t// the same PURL while preserving their unique metadata.\n\tif parsedPURL, err := packageurl.FromString(p.PURL); err == nil {\n\t\tparsedPURL.Qualifiers = append(parsedPURL.Qualifiers, packageurl.Qualifier{Key: \"package-id\", Value: p.ID})\n\t\treturn parsedPURL.ToString()\n\t}\n\t// fallback is to use strictly the ID if there is no valid pURL\n\treturn p.ID\n}\n"
  },
  {
    "path": "grype/presenter/cyclonedx/vulnerability_test.go",
    "content": "package cyclonedx\n\nimport (\n\t\"testing\"\n\n\t\"github.com/CycloneDX/cyclonedx-go\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/grype/grype/presenter/models\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n)\n\nfunc TestCvssVersionToMethod(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc     string\n\t\tinput    string\n\t\texpected cyclonedx.ScoringMethod\n\t\terrors   bool\n\t}{\n\t\t{\n\t\t\tdesc:     \"invalid (not float)\",\n\t\t\tinput:    \"\",\n\t\t\texpected: \"\",\n\t\t\terrors:   true,\n\t\t},\n\t\t{\n\t\t\tdesc:     \"CVSS v2\",\n\t\t\tinput:    \"2.0\",\n\t\t\texpected: cyclonedx.ScoringMethodCVSSv2,\n\t\t\terrors:   false,\n\t\t},\n\t\t{\n\t\t\tdesc:     \"CVSS v31\",\n\t\t\tinput:    \"3.1\",\n\t\t\texpected: cyclonedx.ScoringMethodCVSSv31,\n\t\t\terrors:   false,\n\t\t},\n\t\t{\n\t\t\tdesc:     \"CVSS v3\",\n\t\t\tinput:    \"3\",\n\t\t\texpected: cyclonedx.ScoringMethodCVSSv3,\n\t\t\terrors:   false,\n\t\t},\n\t\t{\n\t\t\tdesc:     \"invalid (no match)\",\n\t\t\tinput:    \"15.4\",\n\t\t\texpected: cyclonedx.ScoringMethodOther,\n\t\t\terrors:   false,\n\t\t},\n\t}\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.desc, func(t *testing.T) {\n\t\t\tactual, err := cvssVersionToMethod(tc.input)\n\t\t\tif !tc.errors {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t} else {\n\t\t\t\tassert.Error(t, err)\n\t\t\t}\n\n\t\t\tassert.Equal(t, tc.expected, actual)\n\t\t})\n\t}\n}\n\ntype metadataProvider struct {\n\tseverity string\n\tcvss     []vulnerability.Cvss\n}\n\nfunc (m metadataProvider) VulnerabilityMetadata(ref vulnerability.Reference) (*vulnerability.Metadata, error) {\n\treturn &vulnerability.Metadata{\n\t\tID:          ref.ID,\n\t\tDataSource:  \"\",\n\t\tNamespace:   ref.Namespace,\n\t\tSeverity:    m.severity,\n\t\tURLs:        nil,\n\t\tDescription: \"\",\n\t\tCvss:        m.cvss,\n\t}, nil\n}\n\nfunc TestNewVulnerability_AlwaysIncludesSeverity(t *testing.T) {\n\ttests := []struct {\n\t\tname  string\n\t\tmatch models.Match\n\t}{\n\t\t{\n\t\t\tname: \"populates severity with missing CVSS records\",\n\t\t\tmatch: models.Match{\n\t\t\t\tVulnerability: models.Vulnerability{\n\t\t\t\t\tVulnerabilityMetadata: models.VulnerabilityMetadata{\n\t\t\t\t\t\tSeverity: \"High\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tArtifact:     models.Package{},\n\t\t\t\tMatchDetails: nil,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"populates severity with all CVSS records\",\n\t\t\tmatch: models.Match{\n\t\t\t\tVulnerability: models.Vulnerability{\n\t\t\t\t\tVulnerabilityMetadata: models.VulnerabilityMetadata{\n\t\t\t\t\t\tSeverity: \"High\",\n\t\t\t\t\t\tCvss: []models.Cvss{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tVersion: \"2.0\",\n\t\t\t\t\t\t\t\tMetrics: models.CvssMetrics{\n\t\t\t\t\t\t\t\t\tBaseScore: 1.1,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tVersion: \"3.0\",\n\t\t\t\t\t\t\t\tMetrics: models.CvssMetrics{\n\t\t\t\t\t\t\t\t\tBaseScore: 2.1,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tVersion: \"3.1\",\n\t\t\t\t\t\t\t\tMetrics: models.CvssMetrics{\n\t\t\t\t\t\t\t\t\tBaseScore: 3.1,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tArtifact:     models.Package{},\n\t\t\t\tMatchDetails: nil,\n\t\t\t},\n\t\t},\n\t}\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tactual, err := NewVulnerability(test.match)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.NotNil(t, actual.Ratings, \"cyclonedx document ratings should not be nil\")\n\t\t\trequire.NotEmpty(t, actual.Ratings)\n\t\t\trequire.Equal(t, cdxSeverityFromGrypeSeverity(test.match.Vulnerability.Severity), (*actual.Ratings)[0].Severity)\n\t\t\tif len(test.match.Vulnerability.Cvss) > 0 {\n\t\t\t\tfor i, rating := range *actual.Ratings {\n\t\t\t\t\trequire.Equal(t, test.match.Vulnerability.Cvss[i].Metrics.BaseScore, *rating.Score)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewVulnerability_IncludesEPSSAndKEV(t *testing.T) {\n\tmatch := models.Match{\n\t\tVulnerability: models.Vulnerability{\n\t\t\tVulnerabilityMetadata: models.VulnerabilityMetadata{\n\t\t\t\tID:       \"CVE-2025-0001\",\n\t\t\t\tSeverity: \"High\",\n\t\t\t\tEPSS: []models.EPSS{\n\t\t\t\t\t{\n\t\t\t\t\t\tEPSS: 0.87,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tKnownExploited: []models.KnownExploited{\n\t\t\t\t\t{\n\t\t\t\t\t\tKnownRansomwareCampaignUse: \"known\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tArtifact:     models.Package{},\n\t\tMatchDetails: nil,\n\t}\n\n\tvuln, err := NewVulnerability(match)\n\trequire.NoError(t, err)\n\n\tratings := *vuln.Ratings\n\trequire.Len(t, ratings, 3, \"should include 1 CVSS + 1 EPSS + 1 KEV rating\")\n\n\tvar foundEPSS, foundKEV bool\n\tfor _, r := range ratings {\n\t\tif r.Method == \"EPSS\" {\n\t\t\tfoundEPSS = true\n\t\t\tassert.NotNil(t, r.Score)\n\t\t\tassert.InDelta(t, 0.87, *r.Score, 0.001)\n\t\t\tassert.Equal(t, \"FIRST\", r.Source.Name)\n\t\t}\n\t\tif r.Method == \"other\" && r.Source != nil && r.Source.Name == \"CISA KEV Catalog\" {\n\t\t\tfoundKEV = true\n\t\t\tassert.NotNil(t, r.Score)\n\t\t\tassert.Equal(t, 1.0, *r.Score)\n\t\t}\n\t}\n\tassert.True(t, foundEPSS, \"should include EPSS rating\")\n\tassert.True(t, foundKEV, \"should include KEV rating\")\n}\n"
  },
  {
    "path": "grype/presenter/explain/__snapshots__/explain_snapshot_test.snap",
    "content": "\n[TestExplainSnapshot/keycloak-CVE-2020-12413 - 1]\nCVE-2020-12413 from nvd:cpe (Medium)\nThe Raccoon attack is a timing attack on DHE ciphersuites inherit in the TLS specification. To mitigate this vulnerability, Firefox disabled support for DHE ciphersuites.\nRelated vulnerabilities:\n    - redhat:distro:redhat:9 CVE-2020-12413 (Low)\nMatched packages:\n    - Package: nss, version: 3.79.0-17.el9_1\n      PURL: pkg:rpm/rhel/nss@3.79.0-17.el9_1?arch=x86_64&upstream=nss-3.79.0-17.el9_1.src.rpm&distro=rhel-9.1\n      Match explanation(s):\n          - redhat:distro:redhat:9:CVE-2020-12413 Direct match (package name, version, and ecosystem) against nss (version 3.79.0-17.el9_1).\n      Locations:\n          - /var/lib/rpm/rpmdb.sqlite\n    - Package: nspr, version: 4.34.0-17.el9_1\n      PURL: pkg:rpm/rhel/nspr@4.34.0-17.el9_1?arch=x86_64&upstream=nss-3.79.0-17.el9_1.src.rpm&distro=rhel-9.1\n      Match explanation(s):\n          - redhat:distro:redhat:9:CVE-2020-12413 Indirect match; this CVE is reported against nss (version 3.79.0-17.el9_1), the source RPM of this rpm package.\n      Locations:\n          - /var/lib/rpm/rpmdb.sqlite\n    - Package: nss-softokn, version: 3.79.0-17.el9_1\n      PURL: pkg:rpm/rhel/nss-softokn@3.79.0-17.el9_1?arch=x86_64&upstream=nss-3.79.0-17.el9_1.src.rpm&distro=rhel-9.1\n      Match explanation(s):\n          - redhat:distro:redhat:9:CVE-2020-12413 Indirect match; this CVE is reported against nss (version 3.79.0-17.el9_1), the source RPM of this rpm package.\n      Locations:\n          - /var/lib/rpm/rpmdb.sqlite\n    - Package: nss-softokn-freebl, version: 3.79.0-17.el9_1\n      PURL: pkg:rpm/rhel/nss-softokn-freebl@3.79.0-17.el9_1?arch=x86_64&upstream=nss-3.79.0-17.el9_1.src.rpm&distro=rhel-9.1\n      Match explanation(s):\n          - redhat:distro:redhat:9:CVE-2020-12413 Indirect match; this CVE is reported against nss (version 3.79.0-17.el9_1), the source RPM of this rpm package.\n      Locations:\n          - /var/lib/rpm/rpmdb.sqlite\n    - Package: nss-sysinit, version: 3.79.0-17.el9_1\n      PURL: pkg:rpm/rhel/nss-sysinit@3.79.0-17.el9_1?arch=x86_64&upstream=nss-3.79.0-17.el9_1.src.rpm&distro=rhel-9.1\n      Match explanation(s):\n          - redhat:distro:redhat:9:CVE-2020-12413 Indirect match; this CVE is reported against nss (version 3.79.0-17.el9_1), the source RPM of this rpm package.\n      Locations:\n          - /var/lib/rpm/rpmdb.sqlite\n    - Package: nss-util, version: 3.79.0-17.el9_1\n      PURL: pkg:rpm/rhel/nss-util@3.79.0-17.el9_1?arch=x86_64&upstream=nss-3.79.0-17.el9_1.src.rpm&distro=rhel-9.1\n      Match explanation(s):\n          - redhat:distro:redhat:9:CVE-2020-12413 Indirect match; this CVE is reported against nss (version 3.79.0-17.el9_1), the source RPM of this rpm package.\n      Locations:\n          - /var/lib/rpm/rpmdb.sqlite\nURLs:\n    - https://nvd.nist.gov/vuln/detail/CVE-2020-12413\n    - https://access.redhat.com/security/cve/CVE-2020-12413\n\n---\n\n[TestExplainSnapshot/chainguard-ruby-CVE-2023-28755 - 1]\nCVE-2023-28755 from nvd:cpe (High)\nA ReDoS issue was discovered in the URI component through 0.12.0 in Ruby through 3.2.1. The URI parser mishandles invalid URLs that have specific characters. It causes an increase in execution time for parsing strings to URI objects. The fixed versions are 0.12.1, 0.11.1, 0.10.2 and 0.10.0.1.\nRelated vulnerabilities:\n    - github:language:ruby GHSA-hv5j-3h9f-99c2 (High)\n    - wolfi:distro:wolfi:rolling CVE-2023-28755 (High)\nMatched packages:\n    - Package: ruby-3.0, version: 3.0.4-r1\n      PURL: pkg:apk/wolfi/ruby-3.0@3.0.4-r1?arch=aarch64&distro=wolfi-20221118\n      Match explanation(s):\n          - wolfi:distro:wolfi:rolling:CVE-2023-28755 Direct match (package name, version, and ecosystem) against ruby-3.0 (version 3.0.4-r1).\n          - nvd:cpe:CVE-2023-28755 CPE match on `cpe:2.3:a:ruby-lang:uri:0.10.1:*:*:*:*:*:*:*`.\n          - wolfi:distro:wolfi:rolling:CVE-2023-28755 Indirect match; this CVE is reported against ruby-3.0 (version 3.0.4-r1), the upstream of this apk package.\n      Locations:\n          - /usr/lib/ruby/gems/3.0.0/specifications/default/uri-0.10.1.gemspec\n          - /lib/apk/db/installed\nURLs:\n    - https://nvd.nist.gov/vuln/detail/CVE-2023-28755\n    - https://github.com/advisories/GHSA-hv5j-3h9f-99c2\n    - http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-28755\n\n---\n\n[TestExplainSnapshot/test_a_GHSA - 1]\nGHSA-cfh5-3ghh-wfjx from github:language:java (Medium)\nModerate severity vulnerability that affects org.apache.httpcomponents:httpclient\nRelated vulnerabilities:\n    - nvd:cpe CVE-2014-3577 (Medium)\nMatched packages:\n    - Package: httpclient, version: 4.1.1\n      PURL: pkg:maven/org.apache.httpcomponents/httpclient@4.1.1\n      Match explanation(s):\n          - github:language:java:GHSA-cfh5-3ghh-wfjx Direct match (package name, version, and ecosystem) against httpclient (version 4.1.1).\n      Locations:\n          - /TwilioNotifier.hpi:WEB-INF/lib/sdk-3.0.jar:httpclient\nURLs:\n    - https://github.com/advisories/GHSA-cfh5-3ghh-wfjx\n    - https://nvd.nist.gov/vuln/detail/CVE-2014-3577\n\n---\n\n[TestExplainSnapshot/test_a_CVE_alias_of_a_GHSA - 1]\nCVE-2014-3577 from nvd:cpe (Medium)\norg.apache.http.conn.ssl.AbstractVerifier in Apache HttpComponents HttpClient before 4.3.5 and HttpAsyncClient before 4.0.2 does not properly verify that the server hostname matches a domain name in the subject's Common Name (CN) or subjectAltName field of the X.509 certificate, which allows man-in-the-middle attackers to spoof SSL servers via a \"CN=\" string in a field in the distinguished name (DN) of a certificate, as demonstrated by the \"foo,CN=www.apache.org\" string in the O field.\nRelated vulnerabilities:\n    - github:language:java GHSA-cfh5-3ghh-wfjx (Medium)\nMatched packages:\n    - Package: httpclient, version: 4.1.1\n      PURL: pkg:maven/org.apache.httpcomponents/httpclient@4.1.1\n      Match explanation(s):\n          - github:language:java:GHSA-cfh5-3ghh-wfjx Direct match (package name, version, and ecosystem) against httpclient (version 4.1.1).\n          - nvd:cpe:CVE-2014-3577 CPE match on `cpe:2.3:a:apache:httpclient:4.1.1:*:*:*:*:*:*:*`.\n      Locations:\n          - /TwilioNotifier.hpi:WEB-INF/lib/sdk-3.0.jar:httpclient\nURLs:\n    - https://nvd.nist.gov/vuln/detail/CVE-2014-3577\n    - https://github.com/advisories/GHSA-cfh5-3ghh-wfjx\n\n---\n"
  },
  {
    "path": "grype/presenter/explain/explain.go",
    "content": "package explain\n\nimport (\n\t_ \"embed\"\n\t\"fmt\"\n\t\"io\"\n\t\"sort\"\n\t\"strings\"\n\t\"text/template\"\n\n\t\"github.com/anchore/grype/grype/match\"\n\t\"github.com/anchore/grype/grype/presenter/models\"\n\t\"github.com/anchore/syft/syft/file\"\n)\n\n//go:embed explain_cve.tmpl\nvar explainTemplate string\n\ntype VulnerabilityExplainer interface {\n\tExplainByID(IDs []string) error\n\tExplainBySeverity(severity string) error\n\tExplainAll() error\n}\n\ntype ViewModel struct {\n\tPrimaryVulnerability   models.VulnerabilityMetadata\n\tRelatedVulnerabilities []models.VulnerabilityMetadata\n\tMatchedPackages        []*explainedPackage // I think this needs a map of artifacts to explained evidence\n\tURLs                   []string\n}\n\ntype viewModelBuilder struct {\n\tPrimaryMatch   models.Match // The match that seems to be the one we're trying to explain\n\tRelatedMatches []models.Match\n\trequestedIDs   []string // the vulnerability IDs the user requested explanations of\n}\n\ntype Findings map[string]ViewModel\n\ntype explainedPackage struct {\n\tPURL                string\n\tName                string\n\tVersion             string\n\tMatchedOnID         string\n\tMatchedOnNamespace  string\n\tIndirectExplanation string\n\tDirectExplanation   string\n\tCPEExplanation      string\n\tLocations           []explainedEvidence\n\tdisplayPriority     int // shows how early it should be displayed; direct matches first\n}\n\ntype explainedEvidence struct {\n\tLocation     string\n\tArtifactID   string\n\tViaVulnID    string\n\tViaNamespace string\n}\n\ntype vulnerabilityExplainer struct {\n\tw   io.Writer\n\tdoc *models.Document\n}\n\nfunc NewVulnerabilityExplainer(w io.Writer, doc *models.Document) VulnerabilityExplainer {\n\treturn &vulnerabilityExplainer{\n\t\tw:   w,\n\t\tdoc: doc,\n\t}\n}\n\nvar funcs = template.FuncMap{\n\t\"trim\": strings.TrimSpace,\n}\n\nfunc (e *vulnerabilityExplainer) ExplainByID(ids []string) error {\n\tfindings, err := Doc(e.doc, ids)\n\tif err != nil {\n\t\treturn err\n\t}\n\tt := template.Must(template.New(\"explanation\").Funcs(funcs).Parse(explainTemplate))\n\tfor _, id := range ids {\n\t\tfinding, ok := findings[id]\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\t\tif err := t.Execute(e.w, finding); err != nil {\n\t\t\treturn fmt.Errorf(\"unable to execute template: %w\", err)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (e *vulnerabilityExplainer) ExplainBySeverity(_ string) error {\n\treturn fmt.Errorf(\"not implemented\")\n}\n\nfunc (e *vulnerabilityExplainer) ExplainAll() error {\n\tfindings, err := Doc(e.doc, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\tt := template.Must(template.New(\"explanation\").Funcs(funcs).Parse(explainTemplate))\n\n\treturn t.Execute(e.w, findings)\n}\n\nfunc Doc(doc *models.Document, requestedIDs []string) (Findings, error) {\n\tresult := make(Findings)\n\tbuilders := make(map[string]*viewModelBuilder)\n\tfor _, m := range doc.Matches {\n\t\tkey := m.Vulnerability.ID\n\t\texisting, ok := builders[key]\n\t\tif !ok {\n\t\t\texisting = newBuilder(requestedIDs)\n\t\t\tbuilders[m.Vulnerability.ID] = existing\n\t\t}\n\t\texisting.WithMatch(m, requestedIDs)\n\t}\n\tfor _, m := range doc.Matches {\n\t\tfor _, related := range m.RelatedVulnerabilities {\n\t\t\tkey := related.ID\n\t\t\texisting, ok := builders[key]\n\t\t\tif !ok {\n\t\t\t\texisting = newBuilder(requestedIDs)\n\t\t\t\tbuilders[key] = existing\n\t\t\t}\n\t\t\texisting.WithMatch(m, requestedIDs)\n\t\t}\n\t}\n\tfor k, v := range builders {\n\t\tresult[k] = v.Build()\n\t}\n\treturn result, nil\n}\n\nfunc newBuilder(requestedIDs []string) *viewModelBuilder {\n\treturn &viewModelBuilder{\n\t\trequestedIDs: requestedIDs,\n\t}\n}\n\n// WithMatch adds a match to the builder\n// accepting enough information to determine whether the match is a primary match or a related match\nfunc (b *viewModelBuilder) WithMatch(m models.Match, userRequestedIDs []string) {\n\tif b.isPrimaryAdd(m, userRequestedIDs) {\n\t\t// Demote the current primary match to related match\n\t\t// if it exists\n\t\tif b.PrimaryMatch.Vulnerability.ID != \"\" {\n\t\t\tb.WithRelatedMatch(b.PrimaryMatch)\n\t\t}\n\t\tb.WithPrimaryMatch(m)\n\t} else {\n\t\tb.WithRelatedMatch(m)\n\t}\n}\n\nfunc (b *viewModelBuilder) isPrimaryAdd(candidate models.Match, userRequestedIDs []string) bool {\n\tif b.PrimaryMatch.Vulnerability.ID == \"\" {\n\t\treturn true\n\t}\n\n\tidWasRequested := false\n\tfor _, id := range userRequestedIDs {\n\t\tif candidate.Vulnerability.ID == id {\n\t\t\tidWasRequested = true\n\t\t\tbreak\n\t\t}\n\t}\n\t// the user didn't ask about this ID, so it's not the primary one\n\tif !idWasRequested && len(userRequestedIDs) > 0 {\n\t\treturn false\n\t}\n\t// NVD CPEs are somewhat canonical IDs for vulnerabilities, so if the user asked about CVE-YYYY-ID\n\t// type number, and we have a record from NVD, consider that the primary record.\n\tif candidate.Vulnerability.Namespace == \"nvd:cpe\" {\n\t\treturn true\n\t}\n\t// Either the user didn't ask for specific IDs, or the candidate has an ID the user asked for.\n\tfor _, related := range b.PrimaryMatch.RelatedVulnerabilities {\n\t\tif related.ID == candidate.Vulnerability.ID {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc (b *viewModelBuilder) WithPrimaryMatch(m models.Match) *viewModelBuilder {\n\tb.PrimaryMatch = m\n\treturn b\n}\n\nfunc (b *viewModelBuilder) WithRelatedMatch(m models.Match) *viewModelBuilder {\n\tb.RelatedMatches = append(b.RelatedMatches, m)\n\treturn b\n}\n\nfunc (b *viewModelBuilder) Build() ViewModel {\n\texplainedPackages := groupAndSortEvidence(append(b.RelatedMatches, b.PrimaryMatch))\n\n\tvar relatedVulnerabilities []models.VulnerabilityMetadata\n\tdedupeRelatedVulnerabilities := make(map[string]models.VulnerabilityMetadata)\n\tvar sortDedupedRelatedVulnerabilities []string\n\tfor _, m := range append(b.RelatedMatches, b.PrimaryMatch) {\n\t\tkey := fmt.Sprintf(\"%s:%s\", m.Vulnerability.Namespace, m.Vulnerability.ID)\n\t\tdedupeRelatedVulnerabilities[key] = m.Vulnerability.VulnerabilityMetadata\n\t\tfor _, r := range m.RelatedVulnerabilities {\n\t\t\tkey := fmt.Sprintf(\"%s:%s\", r.Namespace, r.ID)\n\t\t\tdedupeRelatedVulnerabilities[key] = r\n\t\t}\n\t}\n\n\t// delete the primary vulnerability from the related vulnerabilities so it isn't listed twice\n\tprimary := b.primaryVulnerability()\n\tdelete(dedupeRelatedVulnerabilities, fmt.Sprintf(\"%s:%s\", primary.Namespace, primary.ID))\n\tfor k := range dedupeRelatedVulnerabilities {\n\t\tsortDedupedRelatedVulnerabilities = append(sortDedupedRelatedVulnerabilities, k)\n\t}\n\tsort.Strings(sortDedupedRelatedVulnerabilities)\n\tfor _, k := range sortDedupedRelatedVulnerabilities {\n\t\trelatedVulnerabilities = append(relatedVulnerabilities, dedupeRelatedVulnerabilities[k])\n\t}\n\n\treturn ViewModel{\n\t\tPrimaryVulnerability:   primary,\n\t\tRelatedVulnerabilities: relatedVulnerabilities,\n\t\tMatchedPackages:        explainedPackages,\n\t\tURLs:                   b.dedupeAndSortURLs(primary),\n\t}\n}\n\nfunc (b *viewModelBuilder) primaryVulnerability() models.VulnerabilityMetadata {\n\tvar primaryVulnerability models.VulnerabilityMetadata\n\tfor _, m := range append(b.RelatedMatches, b.PrimaryMatch) {\n\t\tfor _, r := range append(m.RelatedVulnerabilities, m.Vulnerability.VulnerabilityMetadata) {\n\t\t\tif r.ID == b.PrimaryMatch.Vulnerability.ID && r.Namespace == \"nvd:cpe\" {\n\t\t\t\tprimaryVulnerability = r\n\t\t\t}\n\t\t}\n\t}\n\tif primaryVulnerability.ID == \"\" {\n\t\tprimaryVulnerability = b.PrimaryMatch.Vulnerability.VulnerabilityMetadata\n\t}\n\treturn primaryVulnerability\n}\n\n// nolint:funlen\nfunc groupAndSortEvidence(matches []models.Match) []*explainedPackage {\n\tidsToMatchDetails := make(map[string]*explainedPackage)\n\tfor _, m := range matches {\n\t\tkey := m.Artifact.ID\n\t\tvar newLocations []explainedEvidence\n\t\tfor _, l := range m.Artifact.Locations {\n\t\t\tnewLocations = append(newLocations, explainLocation(m, l))\n\t\t}\n\t\tvar directExplanation string\n\t\tvar indirectExplanation string\n\t\tvar cpeExplanation string\n\t\tvar matchTypePriority int\n\t\tfor i, md := range m.MatchDetails {\n\t\t\texplanation := explainMatchDetail(m, i)\n\t\t\tif explanation != \"\" {\n\t\t\t\tswitch md.Type {\n\t\t\t\tcase string(match.CPEMatch):\n\t\t\t\t\tcpeExplanation = fmt.Sprintf(\"%s:%s %s\", m.Vulnerability.Namespace, m.Vulnerability.ID, explanation)\n\t\t\t\t\tmatchTypePriority = 1 // cpes are a type of direct match\n\t\t\t\tcase string(match.ExactIndirectMatch):\n\t\t\t\t\tindirectExplanation = fmt.Sprintf(\"%s:%s %s\", m.Vulnerability.Namespace, m.Vulnerability.ID, explanation)\n\t\t\t\t\tmatchTypePriority = 0 // display indirect matches after direct matches\n\t\t\t\tcase string(match.ExactDirectMatch):\n\t\t\t\t\tdirectExplanation = fmt.Sprintf(\"%s:%s %s\", m.Vulnerability.Namespace, m.Vulnerability.ID, explanation)\n\t\t\t\t\tmatchTypePriority = 2 // exact-direct-matches are high confidence, direct matches; display them first.\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\te, ok := idsToMatchDetails[key]\n\t\tif !ok {\n\t\t\te = &explainedPackage{\n\t\t\t\tPURL:                m.Artifact.PURL,\n\t\t\t\tName:                m.Artifact.Name,\n\t\t\t\tVersion:             m.Artifact.Version,\n\t\t\t\tMatchedOnID:         m.Vulnerability.ID,\n\t\t\t\tMatchedOnNamespace:  m.Vulnerability.Namespace,\n\t\t\t\tDirectExplanation:   directExplanation,\n\t\t\t\tIndirectExplanation: indirectExplanation,\n\t\t\t\tCPEExplanation:      cpeExplanation,\n\t\t\t\tLocations:           newLocations,\n\t\t\t\tdisplayPriority:     matchTypePriority,\n\t\t\t}\n\t\t\tidsToMatchDetails[key] = e\n\t\t} else {\n\t\t\te.Locations = append(e.Locations, newLocations...)\n\t\t\tif e.CPEExplanation == \"\" {\n\t\t\t\te.CPEExplanation = cpeExplanation\n\t\t\t}\n\t\t\tif e.IndirectExplanation == \"\" {\n\t\t\t\te.IndirectExplanation = indirectExplanation\n\t\t\t}\n\t\t\te.displayPriority += matchTypePriority\n\t\t}\n\t}\n\tvar sortIDs []string\n\tfor k, v := range idsToMatchDetails {\n\t\tsortIDs = append(sortIDs, k)\n\t\tdedupeLocations := make(map[string]explainedEvidence)\n\t\tfor _, l := range v.Locations {\n\t\t\tdedupeLocations[l.Location] = l\n\t\t}\n\t\tvar uniqueLocations []explainedEvidence\n\t\tfor _, l := range dedupeLocations {\n\t\t\tuniqueLocations = append(uniqueLocations, l)\n\t\t}\n\t\tsort.Slice(uniqueLocations, func(i, j int) bool {\n\t\t\tif uniqueLocations[i].ViaNamespace == uniqueLocations[j].ViaNamespace {\n\t\t\t\treturn uniqueLocations[i].Location < uniqueLocations[j].Location\n\t\t\t}\n\t\t\treturn uniqueLocations[i].ViaNamespace < uniqueLocations[j].ViaNamespace\n\t\t})\n\t\tv.Locations = uniqueLocations\n\t}\n\n\tsort.Slice(sortIDs, func(i, j int) bool {\n\t\treturn explainedPackageIsLess(idsToMatchDetails[sortIDs[i]], idsToMatchDetails[sortIDs[j]])\n\t})\n\tvar explainedPackages []*explainedPackage\n\tfor _, k := range sortIDs {\n\t\texplainedPackages = append(explainedPackages, idsToMatchDetails[k])\n\t}\n\treturn explainedPackages\n}\n\nfunc explainedPackageIsLess(i, j *explainedPackage) bool {\n\tif i.displayPriority != j.displayPriority {\n\t\treturn i.displayPriority > j.displayPriority\n\t}\n\treturn i.Name < j.Name\n}\n\nfunc explainMatchDetail(m models.Match, index int) string {\n\tif len(m.MatchDetails) <= index {\n\t\treturn \"\"\n\t}\n\tmd := m.MatchDetails[index]\n\texplanation := \"\"\n\tswitch md.Type {\n\tcase string(match.CPEMatch):\n\t\texplanation = formatCPEExplanation(m)\n\tcase string(match.ExactIndirectMatch):\n\t\tsourceName, sourceVersion := sourcePackageNameAndVersion(md)\n\t\texplanation = fmt.Sprintf(\"Indirect match; this CVE is reported against %s (version %s), the %s of this %s package.\", sourceName, sourceVersion, nameForUpstream(string(m.Artifact.Type)), m.Artifact.Type)\n\tcase string(match.ExactDirectMatch):\n\t\texplanation = fmt.Sprintf(\"Direct match (package name, version, and ecosystem) against %s (version %s).\", m.Artifact.Name, m.Artifact.Version)\n\t}\n\treturn explanation\n}\n\n// dedupeAndSortURLs returns a slice of the DataSource fields, deduplicated and sorted\n// the NVD and GHSA URL are given special treatment; they return first and second if present\n// and the rest are sorted by string sort.\nfunc (b *viewModelBuilder) dedupeAndSortURLs(primaryVulnerability models.VulnerabilityMetadata) []string {\n\tshowFirst := primaryVulnerability.DataSource\n\tvar URLs []string\n\tURLs = append(URLs, b.PrimaryMatch.Vulnerability.DataSource)\n\tfor _, v := range b.PrimaryMatch.RelatedVulnerabilities {\n\t\tURLs = append(URLs, v.DataSource)\n\t}\n\tfor _, m := range b.RelatedMatches {\n\t\tURLs = append(URLs, m.Vulnerability.DataSource)\n\t\tfor _, v := range m.RelatedVulnerabilities {\n\t\t\tURLs = append(URLs, v.DataSource)\n\t\t}\n\t}\n\tvar result []string\n\tdeduplicate := make(map[string]bool)\n\tresult = append(result, showFirst)\n\tdeduplicate[showFirst] = true\n\tnvdURL := \"\"\n\tghsaURL := \"\"\n\tfor _, u := range URLs {\n\t\tif strings.HasPrefix(u, \"https://nvd.nist.gov/vuln/detail\") {\n\t\t\tnvdURL = u\n\t\t}\n\t\tif strings.HasPrefix(u, \"https://github.com/advisories\") {\n\t\t\tghsaURL = u\n\t\t}\n\t}\n\tif nvdURL != \"\" && nvdURL != showFirst {\n\t\tresult = append(result, nvdURL)\n\t\tdeduplicate[nvdURL] = true\n\t}\n\tif ghsaURL != \"\" && ghsaURL != showFirst {\n\t\tresult = append(result, ghsaURL)\n\t\tdeduplicate[ghsaURL] = true\n\t}\n\n\tfor _, u := range URLs {\n\t\tif _, ok := deduplicate[u]; !ok {\n\t\t\tresult = append(result, u)\n\t\t\tdeduplicate[u] = true\n\t\t}\n\t}\n\treturn result\n}\n\nfunc explainLocation(match models.Match, location file.Location) explainedEvidence {\n\tpath := location.RealPath\n\tif javaMeta, ok := match.Artifact.Metadata.(map[string]any); ok {\n\t\tif virtPath, ok := javaMeta[\"virtualPath\"].(string); ok {\n\t\t\tpath = virtPath\n\t\t}\n\t}\n\treturn explainedEvidence{\n\t\tLocation:     path,\n\t\tArtifactID:   match.Artifact.ID,\n\t\tViaVulnID:    match.Vulnerability.ID,\n\t\tViaNamespace: match.Vulnerability.Namespace,\n\t}\n}\n\nfunc formatCPEExplanation(m models.Match) string {\n\tsearchedBy := m.MatchDetails[0].SearchedBy\n\tif mapResult, ok := searchedBy.(map[string]interface{}); ok {\n\t\tif cpes, ok := mapResult[\"cpes\"]; ok {\n\t\t\tif cpeSlice, ok := cpes.([]interface{}); ok {\n\t\t\t\tif len(cpeSlice) > 0 {\n\t\t\t\t\treturn fmt.Sprintf(\"CPE match on `%s`.\", cpeSlice[0])\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc sourcePackageNameAndVersion(md models.MatchDetails) (string, string) {\n\tvar name string\n\tvar version string\n\tif mapResult, ok := md.SearchedBy.(map[string]interface{}); ok {\n\t\tif sourcePackage, ok := mapResult[\"package\"]; ok {\n\t\t\tif sourceMap, ok := sourcePackage.(map[string]interface{}); ok {\n\t\t\t\tif maybeName, ok := sourceMap[\"name\"]; ok {\n\t\t\t\t\tname, _ = maybeName.(string)\n\t\t\t\t}\n\t\t\t\tif maybeVersion, ok := sourceMap[\"version\"]; ok {\n\t\t\t\t\tversion, _ = maybeVersion.(string)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn name, version\n}\n\nfunc nameForUpstream(typ string) string {\n\tswitch typ {\n\tcase \"deb\":\n\t\treturn \"origin\"\n\tcase \"rpm\":\n\t\treturn \"source RPM\"\n\t}\n\treturn \"upstream\"\n}\n"
  },
  {
    "path": "grype/presenter/explain/explain_cve.tmpl",
    "content": "{{ .PrimaryVulnerability.ID }} from {{ .PrimaryVulnerability.Namespace }} ({{ .PrimaryVulnerability.Severity }})\n{{ trim .PrimaryVulnerability.Description }}{{ if .RelatedVulnerabilities }}\nRelated vulnerabilities:{{ range .RelatedVulnerabilities }}\n    - {{.Namespace}} {{ .ID }} ({{ .Severity }}){{end}}{{end}}\nMatched packages:{{ range .MatchedPackages }}\n    - Package: {{ .Name }}, version: {{ .Version }}{{ if .PURL }}\n      PURL: {{ .PURL }}{{ end }}\n      Match explanation(s):{{ if .DirectExplanation }}\n          - {{ .DirectExplanation }}{{ end }}{{ if .CPEExplanation }}\n          - {{ .CPEExplanation }}{{ end }}{{ if .IndirectExplanation }}\n          - {{ .IndirectExplanation }}{{ end }}\n      Locations:{{ range .Locations }}\n          - {{ .Location }}{{ end }}{{ end }}\nURLs:{{ range .URLs }}\n    - {{ . }}{{ end }}\n"
  },
  {
    "path": "grype/presenter/explain/explain_snapshot_test.go",
    "content": "package explain_test\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/gkampitakis/go-snaps/snaps\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/grype/grype/presenter/explain\"\n\t\"github.com/anchore/grype/grype/presenter/models\"\n)\n\nfunc TestExplainSnapshot(t *testing.T) {\n\t// load sample json\n\ttestCases := []struct {\n\t\tname             string\n\t\tfixture          string\n\t\tvulnerabilityIDs []string\n\t}{\n\t\t{\n\t\t\tname:             \"keycloak-CVE-2020-12413\",\n\t\t\tfixture:          \"./testdata/keycloak-test.json\",\n\t\t\tvulnerabilityIDs: []string{\"CVE-2020-12413\"},\n\t\t},\n\t\t{\n\t\t\tname:             \"chainguard-ruby-CVE-2023-28755\",\n\t\t\tfixture:          \"testdata/chainguard-ruby-test.json\",\n\t\t\tvulnerabilityIDs: []string{\"CVE-2023-28755\"},\n\t\t},\n\t\t{\n\t\t\tname: \"test a GHSA\",\n\t\t\t/*\n\t\t\t\tfixture created by:\n\t\t\t\tSaving output of\n\t\t\t\tgrype anchore/test_images@sha256:10008791acbc5866de04108746a02a0c4029ce3a4400a9b3dad45d7f2245f9da -o json\n\t\t\t\tThen filtering matches to relevant ones:\n\t\t\t\tjq -c '.matches[]' | rg -e GHSA-cfh5-3ghh-wfjx -e CVE-2014-3577 | jq -s .\n\t\t\t*/\n\t\t\tfixture:          \"testdata/ghsa-test.json\",\n\t\t\tvulnerabilityIDs: []string{\"GHSA-cfh5-3ghh-wfjx\"},\n\t\t},\n\t\t{\n\t\t\tname:             \"test a CVE alias of a GHSA\",\n\t\t\tfixture:          \"testdata/ghsa-test.json\",\n\t\t\tvulnerabilityIDs: []string{\"CVE-2014-3577\"},\n\t\t},\n\t}\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tr, err := os.Open(tc.fixture)\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// parse to models.Document\n\t\t\tdoc := models.Document{}\n\t\t\tdecoder := json.NewDecoder(r)\n\t\t\terr = decoder.Decode(&doc)\n\t\t\trequire.NoError(t, err)\n\t\t\t// create explain.VulnerabilityExplainer\n\t\t\tw := bytes.NewBufferString(\"\")\n\t\t\texplainer := explain.NewVulnerabilityExplainer(w, &doc)\n\t\t\t// call ExplainByID\n\t\t\terr = explainer.ExplainByID(tc.vulnerabilityIDs)\n\t\t\trequire.NoError(t, err)\n\t\t\t// assert output\n\t\t\tsnaps.MatchSnapshot(t, w.String())\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/presenter/explain/testdata/chainguard-ruby-test.json",
    "content": "{\n \"matches\": [\n    {\n      \"vulnerability\": {\n        \"id\": \"CVE-2023-28755\",\n        \"dataSource\": \"http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-28755\",\n        \"namespace\": \"wolfi:distro:wolfi:rolling\",\n        \"severity\": \"High\",\n        \"urls\": [\n          \"http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-28755\"\n        ],\n        \"cvss\": [],\n        \"fix\": {\n          \"versions\": [\n            \"3.0.6-r0\"\n          ],\n          \"state\": \"fixed\"\n        },\n        \"advisories\": []\n      },\n      \"relatedVulnerabilities\": [\n        {\n          \"id\": \"CVE-2023-28755\",\n          \"dataSource\": \"https://nvd.nist.gov/vuln/detail/CVE-2023-28755\",\n          \"namespace\": \"nvd:cpe\",\n          \"severity\": \"High\",\n          \"urls\": [\n            \"https://github.com/ruby/uri/releases/\",\n            \"https://lists.debian.org/debian-lts-announce/2023/04/msg00033.html\",\n            \"https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/FFZANOQA4RYX7XCB42OO3P24DQKWHEKA/\",\n            \"https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/G76GZG3RAGYF4P75YY7J7TGYAU7Z5E2T/\",\n            \"https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/WMIOPLBAAM3FEQNAXA2L7BDKOGSVUT5Z/\",\n            \"https://www.ruby-lang.org/en/downloads/releases/\",\n            \"https://www.ruby-lang.org/en/news/2022/12/25/ruby-3-2-0-released/\",\n            \"https://www.ruby-lang.org/en/news/2023/03/28/redos-in-uri-cve-2023-28755/\"\n          ],\n          \"description\": \"A ReDoS issue was discovered in the URI component through 0.12.0 in Ruby through 3.2.1. The URI parser mishandles invalid URLs that have specific characters. It causes an increase in execution time for parsing strings to URI objects. The fixed versions are 0.12.1, 0.11.1, 0.10.2 and 0.10.0.1.\",\n          \"cvss\": [\n            {\n              \"version\": \"3.1\",\n              \"vector\": \"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H\",\n              \"metrics\": {\n                \"baseScore\": 7.5,\n                \"exploitabilityScore\": 3.9,\n                \"impactScore\": 3.6\n              },\n              \"vendorMetadata\": {}\n            }\n          ]\n        }\n      ],\n      \"matchDetails\": [\n        {\n          \"type\": \"exact-indirect-match\",\n          \"matcher\": \"apk-matcher\",\n          \"searchedBy\": {\n            \"distro\": {\n              \"type\": \"wolfi\",\n              \"version\": \"20221118\"\n            },\n            \"namespace\": \"wolfi:distro:wolfi:rolling\",\n            \"package\": {\n              \"name\": \"ruby-3.0\",\n              \"version\": \"3.0.4-r1\"\n            }\n          },\n          \"found\": {\n            \"versionConstraint\": \"< 3.0.6-r0 (apk)\",\n            \"vulnerabilityID\": \"CVE-2023-28755\"\n          }\n        },\n        {\n          \"type\": \"exact-direct-match\",\n          \"matcher\": \"apk-matcher\",\n          \"searchedBy\": {\n            \"distro\": {\n              \"type\": \"wolfi\",\n              \"version\": \"20221118\"\n            },\n            \"namespace\": \"wolfi:distro:wolfi:rolling\",\n            \"package\": {\n              \"name\": \"ruby-3.0\",\n              \"version\": \"3.0.4-r1\"\n            }\n          },\n          \"found\": {\n            \"versionConstraint\": \"< 3.0.6-r0 (apk)\",\n            \"vulnerabilityID\": \"CVE-2023-28755\"\n          }\n        }\n      ],\n      \"artifact\": {\n        \"name\": \"ruby-3.0\",\n        \"version\": \"3.0.4-r1\",\n        \"type\": \"apk\",\n        \"locations\": [\n          {\n            \"path\": \"/lib/apk/db/installed\",\n            \"layerID\": \"sha256:ed905fc06ed3176315bd1e33075ca5b09cd768ad78142fb45439350469556880\"\n          }\n        ],\n        \"language\": \"\",\n        \"licenses\": [\n          \"PSF-2.0\"\n        ],\n        \"cpes\": [\n          \"cpe:2.3:a:ruby-lang:ruby-3.0:3.0.4-r1:*:*:*:*:*:*:*\",\n          \"cpe:2.3:a:ruby-lang:ruby_3.0:3.0.4-r1:*:*:*:*:*:*:*\",\n          \"cpe:2.3:a:ruby_lang:ruby-3.0:3.0.4-r1:*:*:*:*:*:*:*\",\n          \"cpe:2.3:a:ruby_lang:ruby_3.0:3.0.4-r1:*:*:*:*:*:*:*\",\n          \"cpe:2.3:a:ruby-3.0:ruby-3.0:3.0.4-r1:*:*:*:*:*:*:*\",\n          \"cpe:2.3:a:ruby-3.0:ruby_3.0:3.0.4-r1:*:*:*:*:*:*:*\",\n          \"cpe:2.3:a:ruby_3.0:ruby-3.0:3.0.4-r1:*:*:*:*:*:*:*\",\n          \"cpe:2.3:a:ruby_3.0:ruby_3.0:3.0.4-r1:*:*:*:*:*:*:*\",\n          \"cpe:2.3:a:ruby-lang:ruby:3.0.4-r1:*:*:*:*:*:*:*\",\n          \"cpe:2.3:a:ruby_lang:ruby:3.0.4-r1:*:*:*:*:*:*:*\",\n          \"cpe:2.3:a:ruby-3.0:ruby:3.0.4-r1:*:*:*:*:*:*:*\",\n          \"cpe:2.3:a:ruby:ruby-3.0:3.0.4-r1:*:*:*:*:*:*:*\",\n          \"cpe:2.3:a:ruby:ruby_3.0:3.0.4-r1:*:*:*:*:*:*:*\",\n          \"cpe:2.3:a:ruby_3.0:ruby:3.0.4-r1:*:*:*:*:*:*:*\",\n          \"cpe:2.3:a:ruby:ruby:3.0.4-r1:*:*:*:*:*:*:*\"\n        ],\n        \"purl\": \"pkg:apk/wolfi/ruby-3.0@3.0.4-r1?arch=aarch64&distro=wolfi-20221118\",\n        \"upstreams\": [\n          {\n            \"name\": \"ruby-3.0\"\n          }\n        ]\n      }\n    },\n    {\n      \"vulnerability\": {\n        \"id\": \"CVE-2023-28755\",\n        \"dataSource\": \"https://nvd.nist.gov/vuln/detail/CVE-2023-28755\",\n        \"namespace\": \"nvd:cpe\",\n        \"severity\": \"High\",\n        \"urls\": [\n          \"https://github.com/ruby/uri/releases/\",\n          \"https://lists.debian.org/debian-lts-announce/2023/04/msg00033.html\",\n          \"https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/FFZANOQA4RYX7XCB42OO3P24DQKWHEKA/\",\n          \"https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/G76GZG3RAGYF4P75YY7J7TGYAU7Z5E2T/\",\n          \"https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/WMIOPLBAAM3FEQNAXA2L7BDKOGSVUT5Z/\",\n          \"https://www.ruby-lang.org/en/downloads/releases/\",\n          \"https://www.ruby-lang.org/en/news/2022/12/25/ruby-3-2-0-released/\",\n          \"https://www.ruby-lang.org/en/news/2023/03/28/redos-in-uri-cve-2023-28755/\"\n        ],\n        \"description\": \"A ReDoS issue was discovered in the URI component through 0.12.0 in Ruby through 3.2.1. The URI parser mishandles invalid URLs that have specific characters. It causes an increase in execution time for parsing strings to URI objects. The fixed versions are 0.12.1, 0.11.1, 0.10.2 and 0.10.0.1.\",\n        \"cvss\": [\n          {\n            \"version\": \"3.1\",\n            \"vector\": \"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H\",\n            \"metrics\": {\n              \"baseScore\": 7.5,\n              \"exploitabilityScore\": 3.9,\n              \"impactScore\": 3.6\n            },\n            \"vendorMetadata\": {}\n          }\n        ],\n        \"fix\": {\n          \"versions\": [],\n          \"state\": \"unknown\"\n        },\n        \"advisories\": []\n      },\n      \"relatedVulnerabilities\": [],\n      \"matchDetails\": [\n        {\n          \"type\": \"cpe-match\",\n          \"matcher\": \"ruby-gem-matcher\",\n          \"searchedBy\": {\n            \"namespace\": \"nvd:cpe\",\n            \"cpes\": [\n              \"cpe:2.3:a:ruby-lang:uri:0.10.1:*:*:*:*:*:*:*\"\n            ]\n          },\n          \"found\": {\n            \"vulnerabilityID\": \"CVE-2023-28755\",\n            \"versionConstraint\": \"<= 0.10.0 || = 0.10.1 || = 0.11.0 || = 0.12.0 (unknown)\",\n            \"cpes\": [\n              \"cpe:2.3:a:ruby-lang:uri:*:*:*:*:*:ruby:*:*\",\n              \"cpe:2.3:a:ruby-lang:uri:0.10.1:*:*:*:*:ruby:*:*\"\n            ]\n          }\n        }\n      ],\n      \"artifact\": {\n        \"name\": \"uri\",\n        \"version\": \"0.10.1\",\n        \"type\": \"gem\",\n        \"locations\": [\n          {\n            \"path\": \"/usr/lib/ruby/gems/3.0.0/specifications/default/uri-0.10.1.gemspec\",\n            \"layerID\": \"sha256:ed905fc06ed3176315bd1e33075ca5b09cd768ad78142fb45439350469556880\"\n          }\n        ],\n        \"language\": \"ruby\",\n        \"licenses\": [\n          \"Ruby\",\n          \"BSD-2-Clause\"\n        ],\n        \"cpes\": [\n          \"cpe:2.3:a:akira-yamada:uri:0.10.1:*:*:*:*:*:*:*\",\n          \"cpe:2.3:a:akira_yamada:uri:0.10.1:*:*:*:*:*:*:*\",\n          \"cpe:2.3:a:ruby-lang:uri:0.10.1:*:*:*:*:*:*:*\",\n          \"cpe:2.3:a:ruby_lang:uri:0.10.1:*:*:*:*:*:*:*\",\n          \"cpe:2.3:a:ruby:uri:0.10.1:*:*:*:*:*:*:*\",\n          \"cpe:2.3:a:uri:uri:0.10.1:*:*:*:*:*:*:*\"\n        ],\n        \"purl\": \"pkg:gem/uri@0.10.1\",\n        \"upstreams\": []\n      }\n    },\n    {\n      \"vulnerability\": {\n        \"id\": \"GHSA-hv5j-3h9f-99c2\",\n        \"dataSource\": \"https://github.com/advisories/GHSA-hv5j-3h9f-99c2\",\n        \"namespace\": \"github:language:ruby\",\n        \"severity\": \"High\",\n        \"urls\": [\n          \"https://github.com/advisories/GHSA-hv5j-3h9f-99c2\"\n        ],\n        \"description\": \"Ruby URI component ReDoS issue\",\n        \"cvss\": [],\n        \"fix\": {\n          \"versions\": [\n            \"0.10.2\"\n          ],\n          \"state\": \"fixed\"\n        },\n        \"advisories\": []\n      },\n      \"relatedVulnerabilities\": [\n        {\n          \"id\": \"CVE-2023-28755\",\n          \"dataSource\": \"https://nvd.nist.gov/vuln/detail/CVE-2023-28755\",\n          \"namespace\": \"nvd:cpe\",\n          \"severity\": \"High\",\n          \"urls\": [\n            \"https://github.com/ruby/uri/releases/\",\n            \"https://lists.debian.org/debian-lts-announce/2023/04/msg00033.html\",\n            \"https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/FFZANOQA4RYX7XCB42OO3P24DQKWHEKA/\",\n            \"https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/G76GZG3RAGYF4P75YY7J7TGYAU7Z5E2T/\",\n            \"https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/WMIOPLBAAM3FEQNAXA2L7BDKOGSVUT5Z/\",\n            \"https://www.ruby-lang.org/en/downloads/releases/\",\n            \"https://www.ruby-lang.org/en/news/2022/12/25/ruby-3-2-0-released/\",\n            \"https://www.ruby-lang.org/en/news/2023/03/28/redos-in-uri-cve-2023-28755/\"\n          ],\n          \"description\": \"A ReDoS issue was discovered in the URI component through 0.12.0 in Ruby through 3.2.1. The URI parser mishandles invalid URLs that have specific characters. It causes an increase in execution time for parsing strings to URI objects. The fixed versions are 0.12.1, 0.11.1, 0.10.2 and 0.10.0.1.\",\n          \"cvss\": [\n            {\n              \"version\": \"3.1\",\n              \"vector\": \"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H\",\n              \"metrics\": {\n                \"baseScore\": 7.5,\n                \"exploitabilityScore\": 3.9,\n                \"impactScore\": 3.6\n              },\n              \"vendorMetadata\": {}\n            }\n          ]\n        }\n      ],\n      \"matchDetails\": [\n        {\n          \"type\": \"exact-direct-match\",\n          \"matcher\": \"ruby-gem-matcher\",\n          \"searchedBy\": {\n            \"language\": \"ruby\",\n            \"namespace\": \"github:language:ruby\"\n          },\n          \"found\": {\n            \"versionConstraint\": \"=0.10.1 (unknown)\",\n            \"vulnerabilityID\": \"GHSA-hv5j-3h9f-99c2\"\n          }\n        }\n      ],\n      \"artifact\": {\n        \"name\": \"uri\",\n        \"version\": \"0.10.1\",\n        \"type\": \"gem\",\n        \"locations\": [\n          {\n            \"path\": \"/usr/lib/ruby/gems/3.0.0/specifications/default/uri-0.10.1.gemspec\",\n            \"layerID\": \"sha256:ed905fc06ed3176315bd1e33075ca5b09cd768ad78142fb45439350469556880\"\n          }\n        ],\n        \"language\": \"ruby\",\n        \"licenses\": [\n          \"Ruby\",\n          \"BSD-2-Clause\"\n        ],\n        \"cpes\": [\n          \"cpe:2.3:a:akira-yamada:uri:0.10.1:*:*:*:*:*:*:*\",\n          \"cpe:2.3:a:akira_yamada:uri:0.10.1:*:*:*:*:*:*:*\",\n          \"cpe:2.3:a:ruby-lang:uri:0.10.1:*:*:*:*:*:*:*\",\n          \"cpe:2.3:a:ruby_lang:uri:0.10.1:*:*:*:*:*:*:*\",\n          \"cpe:2.3:a:ruby:uri:0.10.1:*:*:*:*:*:*:*\",\n          \"cpe:2.3:a:uri:uri:0.10.1:*:*:*:*:*:*:*\"\n        ],\n        \"purl\": \"pkg:gem/uri@0.10.1\",\n        \"upstreams\": []\n      }\n    }\n  ],\n \"source\": {\n  \"type\": \"image\",\n  \"target\": {\n   \"userInput\": \"cgr.dev/chainguard/ruby:latest-3.0\",\n   \"imageID\": \"sha256:2f88265cfbc43ca35cd327347a9f59375b9f29ef998b8a54a882e31111266640\",\n   \"manifestDigest\": \"sha256:86abe662dfa3746038eea6b0db91092b0767d78b8b8938a343d614cd1579adc2\",\n   \"mediaType\": \"application/vnd.docker.distribution.manifest.v2+json\",\n   \"tags\": [\n    \"cgr.dev/chainguard/ruby:latest-3.0\"\n   ],\n   \"imageSize\": 38865264,\n   \"layers\": [\n    {\n     \"mediaType\": \"application/vnd.docker.image.rootfs.diff.tar.gzip\",\n     \"digest\": \"sha256:ed905fc06ed3176315bd1e33075ca5b09cd768ad78142fb45439350469556880\",\n     \"size\": 38865264\n    }\n   ],\n   \"manifest\": \"eyJzY2hlbWFWZXJzaW9uIjoyLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmRpc3RyaWJ1dGlvbi5tYW5pZmVzdC52Mitqc29uIiwiY29uZmlnIjp7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuY29udGFpbmVyLmltYWdlLnYxK2pzb24iLCJzaXplIjo1NTMsImRpZ2VzdCI6InNoYTI1NjoyZjg4MjY1Y2ZiYzQzY2EzNWNkMzI3MzQ3YTlmNTkzNzViOWYyOWVmOTk4YjhhNTRhODgyZTMxMTExMjY2NjQwIn0sImxheWVycyI6W3sibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vdm5kLmRvY2tlci5pbWFnZS5yb290ZnMuZGlmZi50YXIuZ3ppcCIsInNpemUiOjQxMTYxNzI4LCJkaWdlc3QiOiJzaGEyNTY6ZWQ5MDVmYzA2ZWQzMTc2MzE1YmQxZTMzMDc1Y2E1YjA5Y2Q3NjhhZDc4MTQyZmI0NTQzOTM1MDQ2OTU1Njg4MCJ9XX0=\",\n   \"config\": \"eyJhcmNoaXRlY3R1cmUiOiJhcm02NCIsImF1dGhvciI6ImdpdGh1Yi5jb20vY2hhaW5ndWFyZC1kZXYvYXBrbyIsImNyZWF0ZWQiOiIyMDIzLTAxLTEzVDAwOjExOjI2WiIsImhpc3RvcnkiOlt7ImF1dGhvciI6ImFwa28iLCJjcmVhdGVkIjoiMjAyMy0wMS0xM1QwMDoxMToyNloiLCJjcmVhdGVkX2J5IjoiYXBrbyIsImNvbW1lbnQiOiJUaGlzIGlzIGFuIGFwa28gc2luZ2xlLWxheWVyIGltYWdlIn1dLCJvcyI6ImxpbnV4Iiwicm9vdGZzIjp7InR5cGUiOiJsYXllcnMiLCJkaWZmX2lkcyI6WyJzaGEyNTY6ZWQ5MDVmYzA2ZWQzMTc2MzE1YmQxZTMzMDc1Y2E1YjA5Y2Q3NjhhZDc4MTQyZmI0NTQzOTM1MDQ2OTU1Njg4MCJdfSwiY29uZmlnIjp7IkNtZCI6WyIvdXNyL2Jpbi9pcmIiXSwiRW52IjpbIlBBVEg9L3Vzci9sb2NhbC9zYmluOi91c3IvbG9jYWwvYmluOi91c3Ivc2JpbjovdXNyL2Jpbjovc2JpbjovYmluIiwiU1NMX0NFUlRfRklMRT0vZXRjL3NzbC9jZXJ0cy9jYS1jZXJ0aWZpY2F0ZXMuY3J0Il0sIlVzZXIiOiI2NTUzMiIsIldvcmtpbmdEaXIiOiIvd29yayJ9fQ==\",\n   \"repoDigests\": [\n    \"cgr.dev/chainguard/ruby@sha256:3c9afb4f188827ea1062ec3b8acea32893236a0d7df31e0498df93486cff0978\"\n   ],\n   \"architecture\": \"arm64\",\n   \"os\": \"linux\"\n  }\n },\n \"distro\": {\n  \"name\": \"wolfi\",\n  \"version\": \"20221118\",\n  \"idLike\": []\n },\n \"descriptor\": {\n  \"name\": \"grype\",\n  \"version\": \"0.61.1\",\n  \"configuration\": {\n   \"configPath\": \"\",\n   \"verbosity\": 0,\n   \"output\": \"json\",\n   \"file\": \"\",\n   \"distro\": \"\",\n   \"add-cpes-if-none\": false,\n   \"output-template-file\": \"\",\n   \"check-for-app-update\": true,\n   \"only-fixed\": false,\n   \"only-notfixed\": false,\n   \"platform\": \"\",\n   \"search\": {\n    \"scope\": \"Squashed\",\n    \"unindexed-archives\": false,\n    \"indexed-archives\": true\n   },\n   \"ignore\": null,\n   \"exclude\": [],\n   \"db\": {\n    \"cache-dir\": \"/Users/willmurphy/Library/Caches/grype/db\",\n    \"update-url\": \"https://toolbox-data.anchore.io/grype/databases/listing.json\",\n    \"ca-cert\": \"\",\n    \"auto-update\": true,\n    \"validate-by-hash-on-start\": false,\n    \"validate-age\": true,\n    \"max-allowed-built-age\": 432000000000000\n   },\n   \"externalSources\": {\n    \"enable\": false,\n    \"maven\": {\n     \"searchUpstreamBySha1\": true,\n     \"baseUrl\": \"https://search.maven.org/solrsearch/select\"\n    }\n   },\n   \"match\": {\n    \"java\": {\n     \"using-cpes\": true\n    },\n    \"dotnet\": {\n     \"using-cpes\": true\n    },\n    \"golang\": {\n     \"using-cpes\": true\n    },\n    \"javascript\": {\n     \"using-cpes\": false\n    },\n    \"python\": {\n     \"using-cpes\": true\n    },\n    \"ruby\": {\n     \"using-cpes\": true\n    },\n    \"stock\": {\n     \"using-cpes\": true\n    }\n   },\n   \"dev\": {\n    \"profile-cpu\": false,\n    \"profile-mem\": false\n   },\n   \"fail-on-severity\": \"\",\n   \"registry\": {\n    \"insecure-skip-tls-verify\": false,\n    \"insecure-use-http\": false,\n    \"auth\": []\n   },\n   \"log\": {\n    \"quiet\": false,\n    \"verbosity\": 0,\n    \"level\": \"warn\",\n    \"file\": \"\"\n   },\n   \"show-suppressed\": false,\n   \"by-cve\": false,\n   \"name\": \"\",\n   \"default-image-pull-source\": \"\"\n  },\n  \"db\": {\n   \"built\": \"2023-05-17T01:32:43Z\",\n   \"schemaVersion\": 5,\n   \"location\": \"/Users/willmurphy/Library/Caches/grype/db/5\",\n   \"checksum\": \"sha256:84ebb8325f426565e7a0cd00b2ea265a0ee0ec69db158a65541a42fddd1e15b0\",\n   \"error\": null\n  },\n  \"timestamp\": \"2023-05-17T21:00:56.783213-04:00\"\n }\n}\n"
  },
  {
    "path": "grype/presenter/explain/testdata/ghsa-test.json",
    "content": "{\n \"matches\": [\n    {\n      \"vulnerability\": {\n        \"id\": \"GHSA-cfh5-3ghh-wfjx\",\n        \"dataSource\": \"https://github.com/advisories/GHSA-cfh5-3ghh-wfjx\",\n        \"namespace\": \"github:language:java\",\n        \"severity\": \"Medium\",\n        \"urls\": [\n          \"https://github.com/advisories/GHSA-cfh5-3ghh-wfjx\"\n        ],\n        \"description\": \"Moderate severity vulnerability that affects org.apache.httpcomponents:httpclient\",\n        \"cvss\": [],\n        \"fix\": {\n          \"versions\": [\n            \"4.3.5\"\n          ],\n          \"state\": \"fixed\"\n        },\n        \"advisories\": []\n      },\n      \"relatedVulnerabilities\": [\n        {\n          \"id\": \"CVE-2014-3577\",\n          \"dataSource\": \"https://nvd.nist.gov/vuln/detail/CVE-2014-3577\",\n          \"namespace\": \"nvd:cpe\",\n          \"severity\": \"Medium\",\n          \"urls\": [\n            \"http://lists.opensuse.org/opensuse-security-announce/2020-11/msg00032.html\",\n            \"http://lists.opensuse.org/opensuse-security-announce/2020-11/msg00033.html\",\n            \"http://packetstormsecurity.com/files/127913/Apache-HttpComponents-Man-In-The-Middle.html\",\n            \"http://rhn.redhat.com/errata/RHSA-2014-1146.html\",\n            \"http://rhn.redhat.com/errata/RHSA-2014-1166.html\",\n            \"http://rhn.redhat.com/errata/RHSA-2014-1833.html\",\n            \"http://rhn.redhat.com/errata/RHSA-2014-1834.html\",\n            \"http://rhn.redhat.com/errata/RHSA-2014-1835.html\",\n            \"http://rhn.redhat.com/errata/RHSA-2014-1836.html\",\n            \"http://rhn.redhat.com/errata/RHSA-2014-1891.html\",\n            \"http://rhn.redhat.com/errata/RHSA-2014-1892.html\",\n            \"http://rhn.redhat.com/errata/RHSA-2015-0125.html\",\n            \"http://rhn.redhat.com/errata/RHSA-2015-0158.html\",\n            \"http://rhn.redhat.com/errata/RHSA-2015-0675.html\",\n            \"http://rhn.redhat.com/errata/RHSA-2015-0720.html\",\n            \"http://rhn.redhat.com/errata/RHSA-2015-0765.html\",\n            \"http://rhn.redhat.com/errata/RHSA-2015-0850.html\",\n            \"http://rhn.redhat.com/errata/RHSA-2015-0851.html\",\n            \"http://rhn.redhat.com/errata/RHSA-2015-1176.html\",\n            \"http://rhn.redhat.com/errata/RHSA-2015-1177.html\",\n            \"http://rhn.redhat.com/errata/RHSA-2015-1888.html\",\n            \"http://rhn.redhat.com/errata/RHSA-2016-1773.html\",\n            \"http://rhn.redhat.com/errata/RHSA-2016-1931.html\",\n            \"http://seclists.org/fulldisclosure/2014/Aug/48\",\n            \"http://secunia.com/advisories/60466\",\n            \"http://www.openwall.com/lists/oss-security/2021/10/06/1\",\n            \"http://www.oracle.com/technetwork/security-advisory/cpujul2018-4258247.html\",\n            \"http://www.osvdb.org/110143\",\n            \"http://www.securityfocus.com/bid/69258\",\n            \"http://www.securitytracker.com/id/1030812\",\n            \"http://www.ubuntu.com/usn/USN-2769-1\",\n            \"https://access.redhat.com/solutions/1165533\",\n            \"https://exchange.xforce.ibmcloud.com/vulnerabilities/95327\",\n            \"https://h20566.www2.hpe.com/portal/site/hpsc/public/kb/docDisplay?docId=emr_na-c05103564\",\n            \"https://h20566.www2.hpe.com/portal/site/hpsc/public/kb/docDisplay?docId=emr_na-c05363782\",\n            \"https://lists.apache.org/thread.html/519eb0fd45642dcecd9ff74cb3e71c20a4753f7d82e2f07864b5108f@%3Cdev.drill.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/b0656d359c7d40ec9f39c8cc61bca66802ef9a2a12ee199f5b0c1442@%3Cdev.drill.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/f9bc3e55f4e28d1dcd1a69aae6d53e609a758e34d2869b4d798e13cc@%3Cissues.drill.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/r36e44ffc1a9b365327df62cdfaabe85b9a5637de102cea07d79b2dbf@%3Ccommits.cxf.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/rc774278135816e7afc943dc9fc78eb0764f2c84a2b96470a0187315c@%3Ccommits.cxf.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/rd49aabd984ed540c8ff7916d4d79405f3fa311d2fdbcf9ed307839a6@%3Ccommits.cxf.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/rec7160382badd3ef4ad017a22f64a266c7188b9ba71394f0d321e2d4@%3Ccommits.cxf.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/rfb87e0bf3995e7d560afeed750fac9329ff5f1ad49da365129b7f89e@%3Ccommits.cxf.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/rff42cfa5e7d75b7c1af0e37589140a8f1999e578a75738740b244bd4@%3Ccommits.cxf.apache.org%3E\"\n          ],\n          \"description\": \"org.apache.http.conn.ssl.AbstractVerifier in Apache HttpComponents HttpClient before 4.3.5 and HttpAsyncClient before 4.0.2 does not properly verify that the server hostname matches a domain name in the subject's Common Name (CN) or subjectAltName field of the X.509 certificate, which allows man-in-the-middle attackers to spoof SSL servers via a \\\"CN=\\\" string in a field in the distinguished name (DN) of a certificate, as demonstrated by the \\\"foo,CN=www.apache.org\\\" string in the O field.\",\n          \"cvss\": [\n            {\n              \"source\": \"nvd@nist.gov\",\n              \"type\": \"Primary\",\n              \"version\": \"2.0\",\n              \"vector\": \"AV:N/AC:M/Au:N/C:P/I:P/A:N\",\n              \"metrics\": {\n                \"baseScore\": 5.8,\n                \"exploitabilityScore\": 8.6,\n                \"impactScore\": 4.9\n              },\n              \"vendorMetadata\": {}\n            }\n          ]\n        }\n      ],\n      \"matchDetails\": [\n        {\n          \"type\": \"exact-direct-match\",\n          \"matcher\": \"java-matcher\",\n          \"searchedBy\": {\n            \"language\": \"java\",\n            \"namespace\": \"github:language:java\",\n            \"package\": {\n              \"name\": \"httpclient\",\n              \"version\": \"4.1.1\"\n            }\n          },\n          \"found\": {\n            \"versionConstraint\": \"<4.3.5 (unknown)\",\n            \"vulnerabilityID\": \"GHSA-cfh5-3ghh-wfjx\"\n          }\n        }\n      ],\n      \"artifact\": {\n        \"id\": \"f09cdae46b001bc5\",\n        \"name\": \"httpclient\",\n        \"version\": \"4.1.1\",\n        \"type\": \"java-archive\",\n        \"locations\": [\n          {\n            \"path\": \"/TwilioNotifier.hpi\",\n            \"layerID\": \"sha256:6cc6db176440e3dc3218d2e325716c1922ea9d900b61d7ad6f388fd0ed2b4ef9\"\n          }\n        ],\n        \"language\": \"java\",\n        \"licenses\": [],\n        \"cpes\": [\n          \"cpe:2.3:a:apache:httpclient:4.1.1:*:*:*:*:*:*:*\"\n        ],\n        \"purl\": \"pkg:maven/org.apache.httpcomponents/httpclient@4.1.1\",\n        \"upstreams\": [],\n        \"metadataType\": \"JavaMetadata\",\n        \"metadata\": {\n          \"virtualPath\": \"/TwilioNotifier.hpi:WEB-INF/lib/sdk-3.0.jar:httpclient\",\n          \"pomArtifactID\": \"httpclient\",\n          \"pomGroupID\": \"org.apache.httpcomponents\",\n          \"manifestName\": \"\",\n          \"archiveDigests\": null\n        }\n      }\n    },\n    {\n      \"vulnerability\": {\n        \"id\": \"CVE-2014-3577\",\n        \"dataSource\": \"https://nvd.nist.gov/vuln/detail/CVE-2014-3577\",\n        \"namespace\": \"nvd:cpe\",\n        \"severity\": \"Medium\",\n        \"urls\": [\n          \"http://lists.opensuse.org/opensuse-security-announce/2020-11/msg00032.html\",\n          \"http://lists.opensuse.org/opensuse-security-announce/2020-11/msg00033.html\",\n          \"http://packetstormsecurity.com/files/127913/Apache-HttpComponents-Man-In-The-Middle.html\",\n          \"http://rhn.redhat.com/errata/RHSA-2014-1146.html\",\n          \"http://rhn.redhat.com/errata/RHSA-2014-1166.html\",\n          \"http://rhn.redhat.com/errata/RHSA-2014-1833.html\",\n          \"http://rhn.redhat.com/errata/RHSA-2014-1834.html\",\n          \"http://rhn.redhat.com/errata/RHSA-2014-1835.html\",\n          \"http://rhn.redhat.com/errata/RHSA-2014-1836.html\",\n          \"http://rhn.redhat.com/errata/RHSA-2014-1891.html\",\n          \"http://rhn.redhat.com/errata/RHSA-2014-1892.html\",\n          \"http://rhn.redhat.com/errata/RHSA-2015-0125.html\",\n          \"http://rhn.redhat.com/errata/RHSA-2015-0158.html\",\n          \"http://rhn.redhat.com/errata/RHSA-2015-0675.html\",\n          \"http://rhn.redhat.com/errata/RHSA-2015-0720.html\",\n          \"http://rhn.redhat.com/errata/RHSA-2015-0765.html\",\n          \"http://rhn.redhat.com/errata/RHSA-2015-0850.html\",\n          \"http://rhn.redhat.com/errata/RHSA-2015-0851.html\",\n          \"http://rhn.redhat.com/errata/RHSA-2015-1176.html\",\n          \"http://rhn.redhat.com/errata/RHSA-2015-1177.html\",\n          \"http://rhn.redhat.com/errata/RHSA-2015-1888.html\",\n          \"http://rhn.redhat.com/errata/RHSA-2016-1773.html\",\n          \"http://rhn.redhat.com/errata/RHSA-2016-1931.html\",\n          \"http://seclists.org/fulldisclosure/2014/Aug/48\",\n          \"http://secunia.com/advisories/60466\",\n          \"http://www.openwall.com/lists/oss-security/2021/10/06/1\",\n          \"http://www.oracle.com/technetwork/security-advisory/cpujul2018-4258247.html\",\n          \"http://www.osvdb.org/110143\",\n          \"http://www.securityfocus.com/bid/69258\",\n          \"http://www.securitytracker.com/id/1030812\",\n          \"http://www.ubuntu.com/usn/USN-2769-1\",\n          \"https://access.redhat.com/solutions/1165533\",\n          \"https://exchange.xforce.ibmcloud.com/vulnerabilities/95327\",\n          \"https://h20566.www2.hpe.com/portal/site/hpsc/public/kb/docDisplay?docId=emr_na-c05103564\",\n          \"https://h20566.www2.hpe.com/portal/site/hpsc/public/kb/docDisplay?docId=emr_na-c05363782\",\n          \"https://lists.apache.org/thread.html/519eb0fd45642dcecd9ff74cb3e71c20a4753f7d82e2f07864b5108f@%3Cdev.drill.apache.org%3E\",\n          \"https://lists.apache.org/thread.html/b0656d359c7d40ec9f39c8cc61bca66802ef9a2a12ee199f5b0c1442@%3Cdev.drill.apache.org%3E\",\n          \"https://lists.apache.org/thread.html/f9bc3e55f4e28d1dcd1a69aae6d53e609a758e34d2869b4d798e13cc@%3Cissues.drill.apache.org%3E\",\n          \"https://lists.apache.org/thread.html/r36e44ffc1a9b365327df62cdfaabe85b9a5637de102cea07d79b2dbf@%3Ccommits.cxf.apache.org%3E\",\n          \"https://lists.apache.org/thread.html/rc774278135816e7afc943dc9fc78eb0764f2c84a2b96470a0187315c@%3Ccommits.cxf.apache.org%3E\",\n          \"https://lists.apache.org/thread.html/rd49aabd984ed540c8ff7916d4d79405f3fa311d2fdbcf9ed307839a6@%3Ccommits.cxf.apache.org%3E\",\n          \"https://lists.apache.org/thread.html/rec7160382badd3ef4ad017a22f64a266c7188b9ba71394f0d321e2d4@%3Ccommits.cxf.apache.org%3E\",\n          \"https://lists.apache.org/thread.html/rfb87e0bf3995e7d560afeed750fac9329ff5f1ad49da365129b7f89e@%3Ccommits.cxf.apache.org%3E\",\n          \"https://lists.apache.org/thread.html/rff42cfa5e7d75b7c1af0e37589140a8f1999e578a75738740b244bd4@%3Ccommits.cxf.apache.org%3E\"\n        ],\n        \"description\": \"org.apache.http.conn.ssl.AbstractVerifier in Apache HttpComponents HttpClient before 4.3.5 and HttpAsyncClient before 4.0.2 does not properly verify that the server hostname matches a domain name in the subject's Common Name (CN) or subjectAltName field of the X.509 certificate, which allows man-in-the-middle attackers to spoof SSL servers via a \\\"CN=\\\" string in a field in the distinguished name (DN) of a certificate, as demonstrated by the \\\"foo,CN=www.apache.org\\\" string in the O field.\",\n        \"cvss\": [\n          {\n            \"source\": \"nvd@nist.gov\",\n            \"type\": \"Primary\",\n            \"version\": \"2.0\",\n            \"vector\": \"AV:N/AC:M/Au:N/C:P/I:P/A:N\",\n            \"metrics\": {\n              \"baseScore\": 5.8,\n              \"exploitabilityScore\": 8.6,\n              \"impactScore\": 4.9\n            },\n            \"vendorMetadata\": {}\n          }\n        ],\n        \"fix\": {\n          \"versions\": [],\n          \"state\": \"unknown\"\n        },\n        \"advisories\": []\n      },\n      \"relatedVulnerabilities\": [],\n      \"matchDetails\": [\n        {\n          \"type\": \"cpe-match\",\n          \"matcher\": \"java-matcher\",\n          \"searchedBy\": {\n            \"namespace\": \"nvd:cpe\",\n            \"cpes\": [\n              \"cpe:2.3:a:apache:httpclient:4.1.1:*:*:*:*:*:*:*\"\n            ],\n            \"Package\": {\n              \"name\": \"httpclient\",\n              \"version\": \"4.1.1\"\n            }\n          },\n          \"found\": {\n            \"vulnerabilityID\": \"CVE-2014-3577\",\n            \"versionConstraint\": \">= 4.0, <= 4.3.4 (unknown)\",\n            \"cpes\": [\n              \"cpe:2.3:a:apache:httpclient:*:*:*:*:*:*:*:*\"\n            ]\n          }\n        }\n      ],\n      \"artifact\": {\n        \"id\": \"f09cdae46b001bc5\",\n        \"name\": \"httpclient\",\n        \"version\": \"4.1.1\",\n        \"type\": \"java-archive\",\n        \"locations\": [\n          {\n            \"path\": \"/TwilioNotifier.hpi\",\n            \"layerID\": \"sha256:6cc6db176440e3dc3218d2e325716c1922ea9d900b61d7ad6f388fd0ed2b4ef9\"\n          }\n        ],\n        \"language\": \"java\",\n        \"licenses\": [],\n        \"cpes\": [\n          \"cpe:2.3:a:apache:httpclient:4.1.1:*:*:*:*:*:*:*\"\n        ],\n        \"purl\": \"pkg:maven/org.apache.httpcomponents/httpclient@4.1.1\",\n        \"upstreams\": [],\n        \"metadataType\": \"JavaMetadata\",\n        \"metadata\": {\n          \"virtualPath\": \"/TwilioNotifier.hpi:WEB-INF/lib/sdk-3.0.jar:httpclient\",\n          \"pomArtifactID\": \"httpclient\",\n          \"pomGroupID\": \"org.apache.httpcomponents\",\n          \"manifestName\": \"\",\n          \"archiveDigests\": null\n        }\n      }\n    }\n  ],\n \"source\": {\n  \"type\": \"image\",\n  \"target\": {\n   \"userInput\": \"anchore/test_images@sha256:10008791acbc5866de04108746a02a0c4029ce3a4400a9b3dad45d7f2245f9da\",\n   \"imageID\": \"sha256:e1a0913e5e6eb346f15791e9627842ae80b14564f9c7a4f2e0910a9433673d8b\",\n   \"manifestDigest\": \"sha256:1212e7636ec0b1a7b90eb354e761e67163c2256de127036f086876e190631b43\",\n   \"mediaType\": \"application/vnd.docker.distribution.manifest.v2+json\",\n   \"tags\": [],\n   \"imageSize\": 42104079,\n   \"layers\": [\n    {\n     \"mediaType\": \"application/vnd.docker.image.rootfs.diff.tar.gzip\",\n     \"digest\": \"sha256:e2eb06d8af8218cfec8210147357a68b7e13f7c485b991c288c2d01dc228bb68\",\n     \"size\": 5590942\n    },\n    {\n     \"mediaType\": \"application/vnd.docker.image.rootfs.diff.tar.gzip\",\n     \"digest\": \"sha256:6cc6db176440e3dc3218d2e325716c1922ea9d900b61d7ad6f388fd0ed2b4ef9\",\n     \"size\": 36511427\n    },\n    {\n     \"mediaType\": \"application/vnd.docker.image.rootfs.diff.tar.gzip\",\n     \"digest\": \"sha256:5d5007f009bb615228db4046d5cae910563859d1e3a37cadb2d691ea783ad8a7\",\n     \"size\": 1710\n    }\n   ],\n   \"manifest\": \"eyJzY2hlbWFWZXJzaW9uIjoyLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmRpc3RyaWJ1dGlvbi5tYW5pZmVzdC52Mitqc29uIiwiY29uZmlnIjp7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuY29udGFpbmVyLmltYWdlLnYxK2pzb24iLCJzaXplIjoyMTU5LCJkaWdlc3QiOiJzaGEyNTY6ZTFhMDkxM2U1ZTZlYjM0NmYxNTc5MWU5NjI3ODQyYWU4MGIxNDU2NGY5YzdhNGYyZTA5MTBhOTQzMzY3M2Q4YiJ9LCJsYXllcnMiOlt7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuaW1hZ2Uucm9vdGZzLmRpZmYudGFyLmd6aXAiLCJzaXplIjo1ODY1NDcyLCJkaWdlc3QiOiJzaGEyNTY6ZTJlYjA2ZDhhZjgyMThjZmVjODIxMDE0NzM1N2E2OGI3ZTEzZjdjNDg1Yjk5MWMyODhjMmQwMWRjMjI4YmI2OCJ9LHsibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vdm5kLmRvY2tlci5pbWFnZS5yb290ZnMuZGlmZi50YXIuZ3ppcCIsInNpemUiOjM2NTE1MzI4LCJkaWdlc3QiOiJzaGEyNTY6NmNjNmRiMTc2NDQwZTNkYzMyMThkMmUzMjU3MTZjMTkyMmVhOWQ5MDBiNjFkN2FkNmYzODhmZDBlZDJiNGVmOSJ9LHsibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vdm5kLmRvY2tlci5pbWFnZS5yb290ZnMuZGlmZi50YXIuZ3ppcCIsInNpemUiOjM1ODQsImRpZ2VzdCI6InNoYTI1Njo1ZDUwMDdmMDA5YmI2MTUyMjhkYjQwNDZkNWNhZTkxMDU2Mzg1OWQxZTNhMzdjYWRiMmQ2OTFlYTc4M2FkOGE3In1dfQ==\",\n   \"config\": \"eyJhcmNoaXRlY3R1cmUiOiJhbWQ2NCIsImNvbmZpZyI6eyJIb3N0bmFtZSI6IiIsIkRvbWFpbm5hbWUiOiIiLCJVc2VyIjoiIiwiQXR0YWNoU3RkaW4iOmZhbHNlLCJBdHRhY2hTdGRvdXQiOmZhbHNlLCJBdHRhY2hTdGRlcnIiOmZhbHNlLCJUdHkiOmZhbHNlLCJPcGVuU3RkaW4iOmZhbHNlLCJTdGRpbk9uY2UiOmZhbHNlLCJFbnYiOlsiUEFUSD0vdXNyL2xvY2FsL3NiaW46L3Vzci9sb2NhbC9iaW46L3Vzci9zYmluOi91c3IvYmluOi9zYmluOi9iaW4iXSwiQ21kIjpbIi9iaW4vc2giXSwiSW1hZ2UiOiJzaGEyNTY6YTUyNzg0NzAxODkzMmE0ZWZlMWYxM2U4MzY3NTE4YzQ0MmI2MzE1OTA3YTE2MDRiZWJhYTJhZjg1NjgwMTc1MSIsIlZvbHVtZXMiOm51bGwsIldvcmtpbmdEaXIiOiIiLCJFbnRyeXBvaW50IjpudWxsLCJPbkJ1aWxkIjpudWxsLCJMYWJlbHMiOm51bGx9LCJjb250YWluZXJfY29uZmlnIjp7Ikhvc3RuYW1lIjoiIiwiRG9tYWlubmFtZSI6IiIsIlVzZXIiOiIiLCJBdHRhY2hTdGRpbiI6ZmFsc2UsIkF0dGFjaFN0ZG91dCI6ZmFsc2UsIkF0dGFjaFN0ZGVyciI6ZmFsc2UsIlR0eSI6ZmFsc2UsIk9wZW5TdGRpbiI6ZmFsc2UsIlN0ZGluT25jZSI6ZmFsc2UsIkVudiI6WyJQQVRIPS91c3IvbG9jYWwvc2JpbjovdXNyL2xvY2FsL2JpbjovdXNyL3NiaW46L3Vzci9iaW46L3NiaW46L2JpbiJdLCJDbWQiOlsiL2Jpbi9zaCIsIi1jIiwiIyhub3ApIENPUFkgZmlsZTo4ZTY2YzA3MmFjYjU4ZTVjN2ViZWU5MGI0ZGVhNjc1YjdjM2VmOTA5MTQ0Yjk3MzA4MzYwMGU3N2NkNDIyNzY5IGluIC8gIl0sIkltYWdlIjoic2hhMjU2OmE1Mjc4NDcwMTg5MzJhNGVmZTFmMTNlODM2NzUxOGM0NDJiNjMxNTkwN2ExNjA0YmViYWEyYWY4NTY4MDE3NTEiLCJWb2x1bWVzIjpudWxsLCJXb3JraW5nRGlyIjoiIiwiRW50cnlwb2ludCI6bnVsbCwiT25CdWlsZCI6bnVsbCwiTGFiZWxzIjpudWxsfSwiY3JlYXRlZCI6IjIwMjEtMTAtMjJUMTc6MDY6MzIuOTIxOTkxNjI5WiIsImRvY2tlcl92ZXJzaW9uIjoiMjAuMTAuNyIsImhpc3RvcnkiOlt7ImNyZWF0ZWQiOiIyMDIxLTA4LTI3VDE3OjE5OjQ1LjU1MzA5MjM2M1oiLCJjcmVhdGVkX2J5IjoiL2Jpbi9zaCAtYyAjKG5vcCkgQUREIGZpbGU6YWFkNDI5MGQyNzU4MGNjMWEwOTRmZmFmOThjM2NhMmZjNWQ2OTlmZTY5NWRmYjhlNmU5ZmFjMjBmMTEyOTQ1MCBpbiAvICJ9LHsiY3JlYXRlZCI6IjIwMjEtMDgtMjdUMTc6MTk6NDUuNzU4NjExNTIzWiIsImNyZWF0ZWRfYnkiOiIvYmluL3NoIC1jICMobm9wKSAgQ01EIFtcIi9iaW4vc2hcIl0iLCJlbXB0eV9sYXllciI6dHJ1ZX0seyJjcmVhdGVkIjoiMjAyMS0xMC0yMlQxNzowNjozMi43MTI3NTA5MjRaIiwiY3JlYXRlZF9ieSI6Ii9iaW4vc2ggLWMgd2dldCAtbnYgaHR0cHM6Ly9yZXBvMS5tYXZlbi5vcmcvbWF2ZW4yL2p1bml0L2p1bml0LzQuMTMuMS9qdW5pdC00LjEzLjEuamFyIFx1MDAyNlx1MDAyNiAgICAgd2dldCAtbnYgaHR0cHM6Ly9nZXQuamVua2lucy5pby9wbHVnaW5zL1R3aWxpb05vdGlmaWVyLzAuMi4xL1R3aWxpb05vdGlmaWVyLmhwaSBcdTAwMjZcdTAwMjYgICAgIHdnZXQgLW52IGh0dHBzOi8vdXBkYXRlcy5qZW5raW5zLWNpLm9yZy9kb3dubG9hZC93YXIvMS4zOTAvaHVkc29uLndhciBcdTAwMjZcdTAwMjYgICAgIHdnZXQgLW52IGh0dHBzOi8vZ2V0LmplbmtpbnMuaW8vcGx1Z2lucy9ub21hZC8wLjcuNC9ub21hZC5ocGkifSx7ImNyZWF0ZWQiOiIyMDIxLTEwLTIyVDE3OjA2OjMyLjkyMTk5MTYyOVoiLCJjcmVhdGVkX2J5IjoiL2Jpbi9zaCAtYyAjKG5vcCkgQ09QWSBmaWxlOjhlNjZjMDcyYWNiNThlNWM3ZWJlZTkwYjRkZWE2NzViN2MzZWY5MDkxNDRiOTczMDgzNjAwZTc3Y2Q0MjI3NjkgaW4gLyAifV0sIm9zIjoibGludXgiLCJyb290ZnMiOnsidHlwZSI6ImxheWVycyIsImRpZmZfaWRzIjpbInNoYTI1NjplMmViMDZkOGFmODIxOGNmZWM4MjEwMTQ3MzU3YTY4YjdlMTNmN2M0ODViOTkxYzI4OGMyZDAxZGMyMjhiYjY4Iiwic2hhMjU2OjZjYzZkYjE3NjQ0MGUzZGMzMjE4ZDJlMzI1NzE2YzE5MjJlYTlkOTAwYjYxZDdhZDZmMzg4ZmQwZWQyYjRlZjkiLCJzaGEyNTY6NWQ1MDA3ZjAwOWJiNjE1MjI4ZGI0MDQ2ZDVjYWU5MTA1NjM4NTlkMWUzYTM3Y2FkYjJkNjkxZWE3ODNhZDhhNyJdfX0=\",\n   \"repoDigests\": [\n    \"anchore/test_images@sha256:10008791acbc5866de04108746a02a0c4029ce3a4400a9b3dad45d7f2245f9da\"\n   ],\n   \"architecture\": \"amd64\",\n   \"os\": \"linux\"\n  }\n },\n \"distro\": {\n  \"name\": \"alpine\",\n  \"version\": \"3.14.2\",\n  \"idLike\": []\n },\n \"descriptor\": {\n  \"name\": \"grype\",\n  \"version\": \"0.65.1\",\n  \"configuration\": {\n   \"configPath\": \"\",\n   \"verbosity\": 0,\n   \"output\": [\n    \"json\"\n   ],\n   \"file\": \"\",\n   \"distro\": \"\",\n   \"add-cpes-if-none\": false,\n   \"output-template-file\": \"\",\n   \"check-for-app-update\": true,\n   \"only-fixed\": false,\n   \"only-notfixed\": false,\n   \"platform\": \"\",\n   \"search\": {\n    \"scope\": \"Squashed\",\n    \"unindexed-archives\": false,\n    \"indexed-archives\": true\n   },\n   \"ignore\": null,\n   \"exclude\": [],\n   \"db\": {\n    \"cache-dir\": \"/Users/willmurphy/Library/Caches/grype/db\",\n    \"update-url\": \"https://toolbox-data.anchore.io/grype/databases/listing.json\",\n    \"ca-cert\": \"\",\n    \"auto-update\": true,\n    \"validate-by-hash-on-start\": false,\n    \"validate-age\": true,\n    \"max-allowed-built-age\": 432000000000000\n   },\n   \"externalSources\": {\n    \"enable\": false,\n    \"maven\": {\n     \"searchUpstreamBySha1\": true,\n     \"baseUrl\": \"https://search.maven.org/solrsearch/select\"\n    }\n   },\n   \"match\": {\n    \"java\": {\n     \"using-cpes\": true\n    },\n    \"dotnet\": {\n     \"using-cpes\": true\n    },\n    \"golang\": {\n     \"using-cpes\": true\n    },\n    \"javascript\": {\n     \"using-cpes\": false\n    },\n    \"python\": {\n     \"using-cpes\": true\n    },\n    \"ruby\": {\n     \"using-cpes\": true\n    },\n    \"stock\": {\n     \"using-cpes\": true\n    }\n   },\n   \"dev\": {\n    \"profile-cpu\": false,\n    \"profile-mem\": false\n   },\n   \"fail-on-severity\": \"\",\n   \"registry\": {\n    \"insecure-skip-tls-verify\": false,\n    \"insecure-use-http\": false,\n    \"auth\": []\n   },\n   \"log\": {\n    \"quiet\": true,\n    \"verbosity\": 0,\n    \"level\": \"\",\n    \"file\": \"\"\n   },\n   \"show-suppressed\": false,\n   \"by-cve\": false,\n   \"name\": \"\",\n   \"default-image-pull-source\": \"\"\n  },\n  \"db\": {\n   \"built\": \"2023-08-31T01:24:19Z\",\n   \"schemaVersion\": 5,\n   \"location\": \"/Users/willmurphy/Library/Caches/grype/db/5\",\n   \"checksum\": \"sha256:911c05ea7c2a5f993758e5428c614914384c2a8265d7e2b0edb843799d62626c\",\n   \"error\": null\n  },\n  \"timestamp\": \"2023-08-31T15:13:32.377177-04:00\"\n }\n}\n"
  },
  {
    "path": "grype/presenter/explain/testdata/keycloak-test.json",
    "content": "{\n \"matches\": [{\n    \"vulnerability\": {\n      \"id\": \"CVE-2020-12413\",\n      \"dataSource\": \"https://access.redhat.com/security/cve/CVE-2020-12413\",\n      \"namespace\": \"redhat:distro:redhat:9\",\n      \"severity\": \"Low\",\n      \"urls\": [\n        \"https://access.redhat.com/security/cve/CVE-2020-12413\"\n      ],\n      \"description\": \"A flaw was found in Mozilla nss. A raccoon attack exploits a flaw in the TLS specification which can lead to an attacker being able to compute the pre-master secret in connections which have used a Diffie-Hellman(DH) based ciphersuite. In such a case this would result in the attacker being able to eavesdrop on all encrypted communications sent over that TLS connection. The highest threat from this vulnerability is to data confidentiality.\",\n      \"cvss\": [\n        {\n          \"version\": \"3.1\",\n          \"vector\": \"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N\",\n          \"metrics\": {\n            \"baseScore\": 5.9,\n            \"exploitabilityScore\": 2.2,\n            \"impactScore\": 3.6\n          },\n          \"vendorMetadata\": {\n            \"base_severity\": \"Medium\",\n            \"status\": \"draft\"\n          }\n        }\n      ],\n      \"fix\": {\n        \"versions\": [],\n        \"state\": \"wont-fix\"\n      },\n      \"advisories\": []\n    },\n    \"relatedVulnerabilities\": [\n      {\n        \"id\": \"CVE-2020-12413\",\n        \"dataSource\": \"https://nvd.nist.gov/vuln/detail/CVE-2020-12413\",\n        \"namespace\": \"nvd:cpe\",\n        \"severity\": \"Medium\",\n        \"urls\": [\n          \"https://bugzilla.mozilla.org/show_bug.cgi?id=CVE-2020-12413\",\n          \"https://raccoon-attack.com/\"\n        ],\n        \"description\": \"The Raccoon attack is a timing attack on DHE ciphersuites inherit in the TLS specification. To mitigate this vulnerability, Firefox disabled support for DHE ciphersuites.\",\n        \"cvss\": [\n          {\n            \"source\": \"nvd@nist.gov\",\n            \"type\": \"Primary\",\n            \"version\": \"3.1\",\n            \"vector\": \"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N\",\n            \"metrics\": {\n              \"baseScore\": 5.9,\n              \"exploitabilityScore\": 2.2,\n              \"impactScore\": 3.6\n            },\n            \"vendorMetadata\": {}\n          }\n        ]\n      }\n    ],\n    \"matchDetails\": [\n      {\n        \"type\": \"exact-indirect-match\",\n        \"matcher\": \"rpm-matcher\",\n        \"searchedBy\": {\n          \"distro\": {\n            \"type\": \"redhat\",\n            \"version\": \"9.1\"\n          },\n          \"namespace\": \"redhat:distro:redhat:9\",\n          \"package\": {\n            \"name\": \"nss\",\n            \"version\": \"3.79.0-17.el9_1\"\n          }\n        },\n        \"found\": {\n          \"versionConstraint\": \"none (rpm)\",\n          \"vulnerabilityID\": \"CVE-2020-12413\"\n        }\n      }\n    ],\n    \"artifact\": {\n      \"id\": \"ff2aefb138ebd4bf\",\n      \"name\": \"nspr\",\n      \"version\": \"4.34.0-17.el9_1\",\n      \"type\": \"rpm\",\n      \"locations\": [\n        {\n          \"path\": \"/var/lib/rpm/rpmdb.sqlite\",\n          \"layerID\": \"sha256:798d91f89858e63627a98d3a196c9ee4d0899259c0f64b68b1e0260a67c9cd2b\"\n        }\n      ],\n      \"language\": \"\",\n      \"licenses\": [\n        \"MPLv2.0\"\n      ],\n      \"cpes\": [\n        \"cpe:2.3:a:redhat:nspr:4.34.0-17.el9_1:*:*:*:*:*:*:*\",\n        \"cpe:2.3:a:nspr:nspr:4.34.0-17.el9_1:*:*:*:*:*:*:*\"\n      ],\n      \"purl\": \"pkg:rpm/rhel/nspr@4.34.0-17.el9_1?arch=x86_64&upstream=nss-3.79.0-17.el9_1.src.rpm&distro=rhel-9.1\",\n      \"upstreams\": [\n        {\n          \"name\": \"nss\",\n          \"version\": \"3.79.0-17.el9_1\"\n        }\n      ],\n      \"metadataType\": \"RpmMetadata\",\n      \"metadata\": {\n        \"epoch\": null,\n        \"modularityLabel\": \"\"\n      }\n    }\n  },\n  {\n    \"vulnerability\": {\n      \"id\": \"CVE-2020-12413\",\n      \"dataSource\": \"https://access.redhat.com/security/cve/CVE-2020-12413\",\n      \"namespace\": \"redhat:distro:redhat:9\",\n      \"severity\": \"Low\",\n      \"urls\": [\n        \"https://access.redhat.com/security/cve/CVE-2020-12413\"\n      ],\n      \"description\": \"A flaw was found in Mozilla nss. A raccoon attack exploits a flaw in the TLS specification which can lead to an attacker being able to compute the pre-master secret in connections which have used a Diffie-Hellman(DH) based ciphersuite. In such a case this would result in the attacker being able to eavesdrop on all encrypted communications sent over that TLS connection. The highest threat from this vulnerability is to data confidentiality.\",\n      \"cvss\": [\n        {\n          \"version\": \"3.1\",\n          \"vector\": \"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N\",\n          \"metrics\": {\n            \"baseScore\": 5.9,\n            \"exploitabilityScore\": 2.2,\n            \"impactScore\": 3.6\n          },\n          \"vendorMetadata\": {\n            \"base_severity\": \"Medium\",\n            \"status\": \"draft\"\n          }\n        }\n      ],\n      \"fix\": {\n        \"versions\": [],\n        \"state\": \"wont-fix\"\n      },\n      \"advisories\": []\n    },\n    \"relatedVulnerabilities\": [\n      {\n        \"id\": \"CVE-2020-12413\",\n        \"dataSource\": \"https://nvd.nist.gov/vuln/detail/CVE-2020-12413\",\n        \"namespace\": \"nvd:cpe\",\n        \"severity\": \"Medium\",\n        \"urls\": [\n          \"https://bugzilla.mozilla.org/show_bug.cgi?id=CVE-2020-12413\",\n          \"https://raccoon-attack.com/\"\n        ],\n        \"description\": \"The Raccoon attack is a timing attack on DHE ciphersuites inherit in the TLS specification. To mitigate this vulnerability, Firefox disabled support for DHE ciphersuites.\",\n        \"cvss\": [\n          {\n            \"source\": \"nvd@nist.gov\",\n            \"type\": \"Primary\",\n            \"version\": \"3.1\",\n            \"vector\": \"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N\",\n            \"metrics\": {\n              \"baseScore\": 5.9,\n              \"exploitabilityScore\": 2.2,\n              \"impactScore\": 3.6\n            },\n            \"vendorMetadata\": {}\n          }\n        ]\n      }\n    ],\n    \"matchDetails\": [\n      {\n        \"type\": \"exact-direct-match\",\n        \"matcher\": \"rpm-matcher\",\n        \"searchedBy\": {\n          \"distro\": {\n            \"type\": \"redhat\",\n            \"version\": \"9.1\"\n          },\n          \"namespace\": \"redhat:distro:redhat:9\",\n          \"package\": {\n            \"name\": \"nss\",\n            \"version\": \"0:3.79.0-17.el9_1\"\n          }\n        },\n        \"found\": {\n          \"versionConstraint\": \"none (rpm)\",\n          \"vulnerabilityID\": \"CVE-2020-12413\"\n        }\n      }\n    ],\n    \"artifact\": {\n      \"id\": \"840f8a931c86688f\",\n      \"name\": \"nss\",\n      \"version\": \"3.79.0-17.el9_1\",\n      \"type\": \"rpm\",\n      \"locations\": [\n        {\n          \"path\": \"/var/lib/rpm/rpmdb.sqlite\",\n          \"layerID\": \"sha256:798d91f89858e63627a98d3a196c9ee4d0899259c0f64b68b1e0260a67c9cd2b\"\n        }\n      ],\n      \"language\": \"\",\n      \"licenses\": [\n        \"MPLv2.0\"\n      ],\n      \"cpes\": [\n        \"cpe:2.3:a:redhat:nss:3.79.0-17.el9_1:*:*:*:*:*:*:*\",\n        \"cpe:2.3:a:nss:nss:3.79.0-17.el9_1:*:*:*:*:*:*:*\"\n      ],\n      \"purl\": \"pkg:rpm/rhel/nss@3.79.0-17.el9_1?arch=x86_64&upstream=nss-3.79.0-17.el9_1.src.rpm&distro=rhel-9.1\",\n      \"upstreams\": [],\n      \"metadataType\": \"RpmMetadata\",\n      \"metadata\": {\n        \"epoch\": null,\n        \"modularityLabel\": \"\"\n      }\n    }\n  },\n  {\n    \"vulnerability\": {\n      \"id\": \"CVE-2020-12413\",\n      \"dataSource\": \"https://access.redhat.com/security/cve/CVE-2020-12413\",\n      \"namespace\": \"redhat:distro:redhat:9\",\n      \"severity\": \"Low\",\n      \"urls\": [\n        \"https://access.redhat.com/security/cve/CVE-2020-12413\"\n      ],\n      \"description\": \"A flaw was found in Mozilla nss. A raccoon attack exploits a flaw in the TLS specification which can lead to an attacker being able to compute the pre-master secret in connections which have used a Diffie-Hellman(DH) based ciphersuite. In such a case this would result in the attacker being able to eavesdrop on all encrypted communications sent over that TLS connection. The highest threat from this vulnerability is to data confidentiality.\",\n      \"cvss\": [\n        {\n          \"version\": \"3.1\",\n          \"vector\": \"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N\",\n          \"metrics\": {\n            \"baseScore\": 5.9,\n            \"exploitabilityScore\": 2.2,\n            \"impactScore\": 3.6\n          },\n          \"vendorMetadata\": {\n            \"base_severity\": \"Medium\",\n            \"status\": \"draft\"\n          }\n        }\n      ],\n      \"fix\": {\n        \"versions\": [],\n        \"state\": \"wont-fix\"\n      },\n      \"advisories\": []\n    },\n    \"relatedVulnerabilities\": [\n      {\n        \"id\": \"CVE-2020-12413\",\n        \"dataSource\": \"https://nvd.nist.gov/vuln/detail/CVE-2020-12413\",\n        \"namespace\": \"nvd:cpe\",\n        \"severity\": \"Medium\",\n        \"urls\": [\n          \"https://bugzilla.mozilla.org/show_bug.cgi?id=CVE-2020-12413\",\n          \"https://raccoon-attack.com/\"\n        ],\n        \"description\": \"The Raccoon attack is a timing attack on DHE ciphersuites inherit in the TLS specification. To mitigate this vulnerability, Firefox disabled support for DHE ciphersuites.\",\n        \"cvss\": [\n          {\n            \"source\": \"nvd@nist.gov\",\n            \"type\": \"Primary\",\n            \"version\": \"3.1\",\n            \"vector\": \"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N\",\n            \"metrics\": {\n              \"baseScore\": 5.9,\n              \"exploitabilityScore\": 2.2,\n              \"impactScore\": 3.6\n            },\n            \"vendorMetadata\": {}\n          }\n        ]\n      }\n    ],\n    \"matchDetails\": [\n      {\n        \"type\": \"exact-indirect-match\",\n        \"matcher\": \"rpm-matcher\",\n        \"searchedBy\": {\n          \"distro\": {\n            \"type\": \"redhat\",\n            \"version\": \"9.1\"\n          },\n          \"namespace\": \"redhat:distro:redhat:9\",\n          \"package\": {\n            \"name\": \"nss\",\n            \"version\": \"3.79.0-17.el9_1\"\n          }\n        },\n        \"found\": {\n          \"versionConstraint\": \"none (rpm)\",\n          \"vulnerabilityID\": \"CVE-2020-12413\"\n        }\n      }\n    ],\n    \"artifact\": {\n      \"id\": \"7d1c659d9eb00024\",\n      \"name\": \"nss-softokn\",\n      \"version\": \"3.79.0-17.el9_1\",\n      \"type\": \"rpm\",\n      \"locations\": [\n        {\n          \"path\": \"/var/lib/rpm/rpmdb.sqlite\",\n          \"layerID\": \"sha256:798d91f89858e63627a98d3a196c9ee4d0899259c0f64b68b1e0260a67c9cd2b\"\n        }\n      ],\n      \"language\": \"\",\n      \"licenses\": [\n        \"MPLv2.0\"\n      ],\n      \"cpes\": [\n        \"cpe:2.3:a:nss-softokn:nss-softokn:3.79.0-17.el9_1:*:*:*:*:*:*:*\",\n        \"cpe:2.3:a:nss-softokn:nss_softokn:3.79.0-17.el9_1:*:*:*:*:*:*:*\",\n        \"cpe:2.3:a:nss_softokn:nss-softokn:3.79.0-17.el9_1:*:*:*:*:*:*:*\",\n        \"cpe:2.3:a:nss_softokn:nss_softokn:3.79.0-17.el9_1:*:*:*:*:*:*:*\",\n        \"cpe:2.3:a:redhat:nss-softokn:3.79.0-17.el9_1:*:*:*:*:*:*:*\",\n        \"cpe:2.3:a:redhat:nss_softokn:3.79.0-17.el9_1:*:*:*:*:*:*:*\",\n        \"cpe:2.3:a:nss:nss-softokn:3.79.0-17.el9_1:*:*:*:*:*:*:*\",\n        \"cpe:2.3:a:nss:nss_softokn:3.79.0-17.el9_1:*:*:*:*:*:*:*\"\n      ],\n      \"purl\": \"pkg:rpm/rhel/nss-softokn@3.79.0-17.el9_1?arch=x86_64&upstream=nss-3.79.0-17.el9_1.src.rpm&distro=rhel-9.1\",\n      \"upstreams\": [\n        {\n          \"name\": \"nss\",\n          \"version\": \"3.79.0-17.el9_1\"\n        }\n      ],\n      \"metadataType\": \"RpmMetadata\",\n      \"metadata\": {\n        \"epoch\": null,\n        \"modularityLabel\": \"\"\n      }\n    }\n  },\n  {\n    \"vulnerability\": {\n      \"id\": \"CVE-2020-12413\",\n      \"dataSource\": \"https://access.redhat.com/security/cve/CVE-2020-12413\",\n      \"namespace\": \"redhat:distro:redhat:9\",\n      \"severity\": \"Low\",\n      \"urls\": [\n        \"https://access.redhat.com/security/cve/CVE-2020-12413\"\n      ],\n      \"description\": \"A flaw was found in Mozilla nss. A raccoon attack exploits a flaw in the TLS specification which can lead to an attacker being able to compute the pre-master secret in connections which have used a Diffie-Hellman(DH) based ciphersuite. In such a case this would result in the attacker being able to eavesdrop on all encrypted communications sent over that TLS connection. The highest threat from this vulnerability is to data confidentiality.\",\n      \"cvss\": [\n        {\n          \"version\": \"3.1\",\n          \"vector\": \"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N\",\n          \"metrics\": {\n            \"baseScore\": 5.9,\n            \"exploitabilityScore\": 2.2,\n            \"impactScore\": 3.6\n          },\n          \"vendorMetadata\": {\n            \"base_severity\": \"Medium\",\n            \"status\": \"draft\"\n          }\n        }\n      ],\n      \"fix\": {\n        \"versions\": [],\n        \"state\": \"wont-fix\"\n      },\n      \"advisories\": []\n    },\n    \"relatedVulnerabilities\": [\n      {\n        \"id\": \"CVE-2020-12413\",\n        \"dataSource\": \"https://nvd.nist.gov/vuln/detail/CVE-2020-12413\",\n        \"namespace\": \"nvd:cpe\",\n        \"severity\": \"Medium\",\n        \"urls\": [\n          \"https://bugzilla.mozilla.org/show_bug.cgi?id=CVE-2020-12413\",\n          \"https://raccoon-attack.com/\"\n        ],\n        \"description\": \"The Raccoon attack is a timing attack on DHE ciphersuites inherit in the TLS specification. To mitigate this vulnerability, Firefox disabled support for DHE ciphersuites.\",\n        \"cvss\": [\n          {\n            \"source\": \"nvd@nist.gov\",\n            \"type\": \"Primary\",\n            \"version\": \"3.1\",\n            \"vector\": \"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N\",\n            \"metrics\": {\n              \"baseScore\": 5.9,\n              \"exploitabilityScore\": 2.2,\n              \"impactScore\": 3.6\n            },\n            \"vendorMetadata\": {}\n          }\n        ]\n      }\n    ],\n    \"matchDetails\": [\n      {\n        \"type\": \"exact-indirect-match\",\n        \"matcher\": \"rpm-matcher\",\n        \"searchedBy\": {\n          \"distro\": {\n            \"type\": \"redhat\",\n            \"version\": \"9.1\"\n          },\n          \"namespace\": \"redhat:distro:redhat:9\",\n          \"package\": {\n            \"name\": \"nss\",\n            \"version\": \"3.79.0-17.el9_1\"\n          }\n        },\n        \"found\": {\n          \"versionConstraint\": \"none (rpm)\",\n          \"vulnerabilityID\": \"CVE-2020-12413\"\n        }\n      }\n    ],\n    \"artifact\": {\n      \"id\": \"cb1f96627e29924e\",\n      \"name\": \"nss-softokn-freebl\",\n      \"version\": \"3.79.0-17.el9_1\",\n      \"type\": \"rpm\",\n      \"locations\": [\n        {\n          \"path\": \"/var/lib/rpm/rpmdb.sqlite\",\n          \"layerID\": \"sha256:798d91f89858e63627a98d3a196c9ee4d0899259c0f64b68b1e0260a67c9cd2b\"\n        }\n      ],\n      \"language\": \"\",\n      \"licenses\": [\n        \"MPLv2.0\"\n      ],\n      \"cpes\": [\n        \"cpe:2.3:a:nss-softokn-freebl:nss-softokn-freebl:3.79.0-17.el9_1:*:*:*:*:*:*:*\",\n        \"cpe:2.3:a:nss-softokn-freebl:nss_softokn_freebl:3.79.0-17.el9_1:*:*:*:*:*:*:*\",\n        \"cpe:2.3:a:nss_softokn_freebl:nss-softokn-freebl:3.79.0-17.el9_1:*:*:*:*:*:*:*\",\n        \"cpe:2.3:a:nss_softokn_freebl:nss_softokn_freebl:3.79.0-17.el9_1:*:*:*:*:*:*:*\",\n        \"cpe:2.3:a:nss-softokn:nss-softokn-freebl:3.79.0-17.el9_1:*:*:*:*:*:*:*\",\n        \"cpe:2.3:a:nss-softokn:nss_softokn_freebl:3.79.0-17.el9_1:*:*:*:*:*:*:*\",\n        \"cpe:2.3:a:nss_softokn:nss-softokn-freebl:3.79.0-17.el9_1:*:*:*:*:*:*:*\",\n        \"cpe:2.3:a:nss_softokn:nss_softokn_freebl:3.79.0-17.el9_1:*:*:*:*:*:*:*\",\n        \"cpe:2.3:a:redhat:nss-softokn-freebl:3.79.0-17.el9_1:*:*:*:*:*:*:*\",\n        \"cpe:2.3:a:redhat:nss_softokn_freebl:3.79.0-17.el9_1:*:*:*:*:*:*:*\",\n        \"cpe:2.3:a:nss:nss-softokn-freebl:3.79.0-17.el9_1:*:*:*:*:*:*:*\",\n        \"cpe:2.3:a:nss:nss_softokn_freebl:3.79.0-17.el9_1:*:*:*:*:*:*:*\"\n      ],\n      \"purl\": \"pkg:rpm/rhel/nss-softokn-freebl@3.79.0-17.el9_1?arch=x86_64&upstream=nss-3.79.0-17.el9_1.src.rpm&distro=rhel-9.1\",\n      \"upstreams\": [\n        {\n          \"name\": \"nss\",\n          \"version\": \"3.79.0-17.el9_1\"\n        }\n      ],\n      \"metadataType\": \"RpmMetadata\",\n      \"metadata\": {\n        \"epoch\": null,\n        \"modularityLabel\": \"\"\n      }\n    }\n  },\n  {\n    \"vulnerability\": {\n      \"id\": \"CVE-2020-12413\",\n      \"dataSource\": \"https://access.redhat.com/security/cve/CVE-2020-12413\",\n      \"namespace\": \"redhat:distro:redhat:9\",\n      \"severity\": \"Low\",\n      \"urls\": [\n        \"https://access.redhat.com/security/cve/CVE-2020-12413\"\n      ],\n      \"description\": \"A flaw was found in Mozilla nss. A raccoon attack exploits a flaw in the TLS specification which can lead to an attacker being able to compute the pre-master secret in connections which have used a Diffie-Hellman(DH) based ciphersuite. In such a case this would result in the attacker being able to eavesdrop on all encrypted communications sent over that TLS connection. The highest threat from this vulnerability is to data confidentiality.\",\n      \"cvss\": [\n        {\n          \"version\": \"3.1\",\n          \"vector\": \"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N\",\n          \"metrics\": {\n            \"baseScore\": 5.9,\n            \"exploitabilityScore\": 2.2,\n            \"impactScore\": 3.6\n          },\n          \"vendorMetadata\": {\n            \"base_severity\": \"Medium\",\n            \"status\": \"draft\"\n          }\n        }\n      ],\n      \"fix\": {\n        \"versions\": [],\n        \"state\": \"wont-fix\"\n      },\n      \"advisories\": []\n    },\n    \"relatedVulnerabilities\": [\n      {\n        \"id\": \"CVE-2020-12413\",\n        \"dataSource\": \"https://nvd.nist.gov/vuln/detail/CVE-2020-12413\",\n        \"namespace\": \"nvd:cpe\",\n        \"severity\": \"Medium\",\n        \"urls\": [\n          \"https://bugzilla.mozilla.org/show_bug.cgi?id=CVE-2020-12413\",\n          \"https://raccoon-attack.com/\"\n        ],\n        \"description\": \"The Raccoon attack is a timing attack on DHE ciphersuites inherit in the TLS specification. To mitigate this vulnerability, Firefox disabled support for DHE ciphersuites.\",\n        \"cvss\": [\n          {\n            \"source\": \"nvd@nist.gov\",\n            \"type\": \"Primary\",\n            \"version\": \"3.1\",\n            \"vector\": \"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N\",\n            \"metrics\": {\n              \"baseScore\": 5.9,\n              \"exploitabilityScore\": 2.2,\n              \"impactScore\": 3.6\n            },\n            \"vendorMetadata\": {}\n          }\n        ]\n      }\n    ],\n    \"matchDetails\": [\n      {\n        \"type\": \"exact-indirect-match\",\n        \"matcher\": \"rpm-matcher\",\n        \"searchedBy\": {\n          \"distro\": {\n            \"type\": \"redhat\",\n            \"version\": \"9.1\"\n          },\n          \"namespace\": \"redhat:distro:redhat:9\",\n          \"package\": {\n            \"name\": \"nss\",\n            \"version\": \"3.79.0-17.el9_1\"\n          }\n        },\n        \"found\": {\n          \"versionConstraint\": \"none (rpm)\",\n          \"vulnerabilityID\": \"CVE-2020-12413\"\n        }\n      }\n    ],\n    \"artifact\": {\n      \"id\": \"d096d490e4fccf36\",\n      \"name\": \"nss-sysinit\",\n      \"version\": \"3.79.0-17.el9_1\",\n      \"type\": \"rpm\",\n      \"locations\": [\n        {\n          \"path\": \"/var/lib/rpm/rpmdb.sqlite\",\n          \"layerID\": \"sha256:798d91f89858e63627a98d3a196c9ee4d0899259c0f64b68b1e0260a67c9cd2b\"\n        }\n      ],\n      \"language\": \"\",\n      \"licenses\": [\n        \"MPLv2.0\"\n      ],\n      \"cpes\": [\n        \"cpe:2.3:a:nss-sysinit:nss-sysinit:3.79.0-17.el9_1:*:*:*:*:*:*:*\",\n        \"cpe:2.3:a:nss-sysinit:nss_sysinit:3.79.0-17.el9_1:*:*:*:*:*:*:*\",\n        \"cpe:2.3:a:nss_sysinit:nss-sysinit:3.79.0-17.el9_1:*:*:*:*:*:*:*\",\n        \"cpe:2.3:a:nss_sysinit:nss_sysinit:3.79.0-17.el9_1:*:*:*:*:*:*:*\",\n        \"cpe:2.3:a:redhat:nss-sysinit:3.79.0-17.el9_1:*:*:*:*:*:*:*\",\n        \"cpe:2.3:a:redhat:nss_sysinit:3.79.0-17.el9_1:*:*:*:*:*:*:*\",\n        \"cpe:2.3:a:nss:nss-sysinit:3.79.0-17.el9_1:*:*:*:*:*:*:*\",\n        \"cpe:2.3:a:nss:nss_sysinit:3.79.0-17.el9_1:*:*:*:*:*:*:*\"\n      ],\n      \"purl\": \"pkg:rpm/rhel/nss-sysinit@3.79.0-17.el9_1?arch=x86_64&upstream=nss-3.79.0-17.el9_1.src.rpm&distro=rhel-9.1\",\n      \"upstreams\": [\n        {\n          \"name\": \"nss\",\n          \"version\": \"3.79.0-17.el9_1\"\n        }\n      ],\n      \"metadataType\": \"RpmMetadata\",\n      \"metadata\": {\n        \"epoch\": null,\n        \"modularityLabel\": \"\"\n      }\n    }\n  },\n  {\n    \"vulnerability\": {\n      \"id\": \"CVE-2020-12413\",\n      \"dataSource\": \"https://access.redhat.com/security/cve/CVE-2020-12413\",\n      \"namespace\": \"redhat:distro:redhat:9\",\n      \"severity\": \"Low\",\n      \"urls\": [\n        \"https://access.redhat.com/security/cve/CVE-2020-12413\"\n      ],\n      \"description\": \"A flaw was found in Mozilla nss. A raccoon attack exploits a flaw in the TLS specification which can lead to an attacker being able to compute the pre-master secret in connections which have used a Diffie-Hellman(DH) based ciphersuite. In such a case this would result in the attacker being able to eavesdrop on all encrypted communications sent over that TLS connection. The highest threat from this vulnerability is to data confidentiality.\",\n      \"cvss\": [\n        {\n          \"version\": \"3.1\",\n          \"vector\": \"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N\",\n          \"metrics\": {\n            \"baseScore\": 5.9,\n            \"exploitabilityScore\": 2.2,\n            \"impactScore\": 3.6\n          },\n          \"vendorMetadata\": {\n            \"base_severity\": \"Medium\",\n            \"status\": \"draft\"\n          }\n        }\n      ],\n      \"fix\": {\n        \"versions\": [],\n        \"state\": \"wont-fix\"\n      },\n      \"advisories\": []\n    },\n    \"relatedVulnerabilities\": [\n      {\n        \"id\": \"CVE-2020-12413\",\n        \"dataSource\": \"https://nvd.nist.gov/vuln/detail/CVE-2020-12413\",\n        \"namespace\": \"nvd:cpe\",\n        \"severity\": \"Medium\",\n        \"urls\": [\n          \"https://bugzilla.mozilla.org/show_bug.cgi?id=CVE-2020-12413\",\n          \"https://raccoon-attack.com/\"\n        ],\n        \"description\": \"The Raccoon attack is a timing attack on DHE ciphersuites inherit in the TLS specification. To mitigate this vulnerability, Firefox disabled support for DHE ciphersuites.\",\n        \"cvss\": [\n          {\n            \"source\": \"nvd@nist.gov\",\n            \"type\": \"Primary\",\n            \"version\": \"3.1\",\n            \"vector\": \"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N\",\n            \"metrics\": {\n              \"baseScore\": 5.9,\n              \"exploitabilityScore\": 2.2,\n              \"impactScore\": 3.6\n            },\n            \"vendorMetadata\": {}\n          }\n        ]\n      }\n    ],\n    \"matchDetails\": [\n      {\n        \"type\": \"exact-indirect-match\",\n        \"matcher\": \"rpm-matcher\",\n        \"searchedBy\": {\n          \"distro\": {\n            \"type\": \"redhat\",\n            \"version\": \"9.1\"\n          },\n          \"namespace\": \"redhat:distro:redhat:9\",\n          \"package\": {\n            \"name\": \"nss\",\n            \"version\": \"3.79.0-17.el9_1\"\n          }\n        },\n        \"found\": {\n          \"versionConstraint\": \"none (rpm)\",\n          \"vulnerabilityID\": \"CVE-2020-12413\"\n        }\n      }\n    ],\n    \"artifact\": {\n      \"id\": \"641950c22b3f5035\",\n      \"name\": \"nss-util\",\n      \"version\": \"3.79.0-17.el9_1\",\n      \"type\": \"rpm\",\n      \"locations\": [\n        {\n          \"path\": \"/var/lib/rpm/rpmdb.sqlite\",\n          \"layerID\": \"sha256:798d91f89858e63627a98d3a196c9ee4d0899259c0f64b68b1e0260a67c9cd2b\"\n        }\n      ],\n      \"language\": \"\",\n      \"licenses\": [\n        \"MPLv2.0\"\n      ],\n      \"cpes\": [\n        \"cpe:2.3:a:nss-util:nss-util:3.79.0-17.el9_1:*:*:*:*:*:*:*\",\n        \"cpe:2.3:a:nss-util:nss_util:3.79.0-17.el9_1:*:*:*:*:*:*:*\",\n        \"cpe:2.3:a:nss_util:nss-util:3.79.0-17.el9_1:*:*:*:*:*:*:*\",\n        \"cpe:2.3:a:nss_util:nss_util:3.79.0-17.el9_1:*:*:*:*:*:*:*\",\n        \"cpe:2.3:a:redhat:nss-util:3.79.0-17.el9_1:*:*:*:*:*:*:*\",\n        \"cpe:2.3:a:redhat:nss_util:3.79.0-17.el9_1:*:*:*:*:*:*:*\",\n        \"cpe:2.3:a:nss:nss-util:3.79.0-17.el9_1:*:*:*:*:*:*:*\",\n        \"cpe:2.3:a:nss:nss_util:3.79.0-17.el9_1:*:*:*:*:*:*:*\"\n      ],\n      \"purl\": \"pkg:rpm/rhel/nss-util@3.79.0-17.el9_1?arch=x86_64&upstream=nss-3.79.0-17.el9_1.src.rpm&distro=rhel-9.1\",\n      \"upstreams\": [\n        {\n          \"name\": \"nss\",\n          \"version\": \"3.79.0-17.el9_1\"\n        }\n      ],\n      \"metadataType\": \"RpmMetadata\",\n      \"metadata\": {\n        \"epoch\": null,\n        \"modularityLabel\": \"\"\n      }\n    }\n  }\n ],\n \"source\": {\n  \"type\": \"image\",\n  \"target\": {\n   \"userInput\": \"docker.io/keycloak/keycloak:21.0.2@sha256:347a0d748d05a050dc64b92de2246d2240db6eb38afbc17c3c08d0acb0db1b50\",\n   \"imageID\": \"sha256:8cf8fd2be2ded92962d52adff75ad06a4c30f69c66facbdf223364c6c9e33b8c\",\n   \"manifestDigest\": \"sha256:d1630d3eb8285a978301bcefc5b223e564ae300750af2fc9ea3f413c5376a47e\",\n   \"mediaType\": \"application/vnd.docker.distribution.manifest.v2+json\",\n   \"tags\": [],\n   \"imageSize\": 433836339,\n   \"layers\": [\n    {\n     \"mediaType\": \"application/vnd.docker.image.rootfs.diff.tar.gzip\",\n     \"digest\": \"sha256:4e37aeaccb4c8016e381d6e2b2a0f22ea59985a7b9b8eca674726e8c60f2f51d\",\n     \"size\": 24302817\n    },\n    {\n     \"mediaType\": \"application/vnd.docker.image.rootfs.diff.tar.gzip\",\n     \"digest\": \"sha256:798d91f89858e63627a98d3a196c9ee4d0899259c0f64b68b1e0260a67c9cd2b\",\n     \"size\": 222622091\n    },\n    {\n     \"mediaType\": \"application/vnd.docker.image.rootfs.diff.tar.gzip\",\n     \"digest\": \"sha256:8329e422b4fd63ffd06518346e5f1f7b33e8190a79e5c321f9c50aba8651d30c\",\n     \"size\": 186910556\n    },\n    {\n     \"mediaType\": \"application/vnd.docker.image.rootfs.diff.tar.gzip\",\n     \"digest\": \"sha256:987fd030ab2cd62944cb487df846c312b64dbb2a6a3131a81253c15a9da2a26c\",\n     \"size\": 875\n    }\n   ],\n   \"manifest\": \"eyJzY2hlbWFWZXJzaW9uIjoyLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmRpc3RyaWJ1dGlvbi5tYW5pZmVzdC52Mitqc29uIiwiY29uZmlnIjp7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuY29udGFpbmVyLmltYWdlLnYxK2pzb24iLCJzaXplIjo2NTY5LCJkaWdlc3QiOiJzaGEyNTY6OGNmOGZkMmJlMmRlZDkyOTYyZDUyYWRmZjc1YWQwNmE0YzMwZjY5YzY2ZmFjYmRmMjIzMzY0YzZjOWUzM2I4YyJ9LCJsYXllcnMiOlt7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuaW1hZ2Uucm9vdGZzLmRpZmYudGFyLmd6aXAiLCJzaXplIjoyNjEyNzg3MiwiZGlnZXN0Ijoic2hhMjU2OjRlMzdhZWFjY2I0YzgwMTZlMzgxZDZlMmIyYTBmMjJlYTU5OTg1YTdiOWI4ZWNhNjc0NzI2ZThjNjBmMmY1MWQifSx7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuaW1hZ2Uucm9vdGZzLmRpZmYudGFyLmd6aXAiLCJzaXplIjoyMjUxMTI1NzYsImRpZ2VzdCI6InNoYTI1Njo3OThkOTFmODk4NThlNjM2MjdhOThkM2ExOTZjOWVlNGQwODk5MjU5YzBmNjRiNjhiMWUwMjYwYTY3YzljZDJiIn0seyJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmltYWdlLnJvb3Rmcy5kaWZmLnRhci5nemlwIiwic2l6ZSI6MTg3MzE1MjAwLCJkaWdlc3QiOiJzaGEyNTY6ODMyOWU0MjJiNGZkNjNmZmQwNjUxODM0NmU1ZjFmN2IzM2U4MTkwYTc5ZTVjMzIxZjljNTBhYmE4NjUxZDMwYyJ9LHsibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vdm5kLmRvY2tlci5pbWFnZS5yb290ZnMuZGlmZi50YXIuZ3ppcCIsInNpemUiOjQwOTYsImRpZ2VzdCI6InNoYTI1Njo5ODdmZDAzMGFiMmNkNjI5NDRjYjQ4N2RmODQ2YzMxMmI2NGRiYjJhNmEzMTMxYTgxMjUzYzE1YTlkYTJhMjZjIn1dfQ==\",\n   \"config\": \"eyJhcmNoaXRlY3R1cmUiOiJhbWQ2NCIsImNvbmZpZyI6eyJVc2VyIjoiMTAwMCIsIkV4cG9zZWRQb3J0cyI6eyI4MDgwL3RjcCI6e30sIjg0NDMvdGNwIjp7fX0sIkVudiI6WyJQQVRIPS91c3IvbG9jYWwvc2JpbjovdXNyL2xvY2FsL2JpbjovdXNyL3NiaW46L3Vzci9iaW46L3NiaW46L2JpbiIsIkxBTkc9ZW5fVVMuVVRGLTgiXSwiRW50cnlwb2ludCI6WyIvb3B0L2tleWNsb2FrL2Jpbi9rYy5zaCJdLCJMYWJlbHMiOnsiYXJjaGl0ZWN0dXJlIjoieDg2XzY0IiwiYnVpbGQtZGF0ZSI6IjIwMjMtMDItMjJUMTM6NTQ6MjUiLCJjb20ucmVkaGF0LmNvbXBvbmVudCI6InViaTktbWljcm8tY29udGFpbmVyIiwiY29tLnJlZGhhdC5saWNlbnNlX3Rlcm1zIjoiaHR0cHM6Ly93d3cucmVkaGF0LmNvbS9lbi9hYm91dC9yZWQtaGF0LWVuZC11c2VyLWxpY2Vuc2UtYWdyZWVtZW50cyNVQkkiLCJkZXNjcmlwdGlvbiI6IlZlcnkgc21hbGwgaW1hZ2Ugd2hpY2ggZG9lc24ndCBpbnN0YWxsIHRoZSBwYWNrYWdlIG1hbmFnZXIuIiwiZGlzdHJpYnV0aW9uLXNjb3BlIjoicHVibGljIiwiaW8uYnVpbGRhaC52ZXJzaW9uIjoiMS4yNy4zIiwiaW8uazhzLmRlc2NyaXB0aW9uIjoiVmVyeSBzbWFsbCBpbWFnZSB3aGljaCBkb2Vzbid0IGluc3RhbGwgdGhlIHBhY2thZ2UgbWFuYWdlci4iLCJpby5rOHMuZGlzcGxheS1uYW1lIjoiVWJpOS1taWNybyIsImlvLm9wZW5zaGlmdC5leHBvc2Utc2VydmljZXMiOiIiLCJtYWludGFpbmVyIjoiUmVkIEhhdCwgSW5jLiIsIm5hbWUiOiJ1Ymk5L3ViaS1taWNybyIsIm9yZy5vcGVuY29udGFpbmVycy5pbWFnZS5jcmVhdGVkIjoiMjAyMy0wMy0zMFQxMToxMjoxOC45ODVaIiwib3JnLm9wZW5jb250YWluZXJzLmltYWdlLmRlc2NyaXB0aW9uIjoiIiwib3JnLm9wZW5jb250YWluZXJzLmltYWdlLmxpY2Vuc2VzIjoiQXBhY2hlLTIuMCIsIm9yZy5vcGVuY29udGFpbmVycy5pbWFnZS5yZXZpc2lvbiI6ImIzNTJhMWY2ZThiYTkyYTA0NWI1OWNjOGRlZDE4NWUzYjFkMjYxNTUiLCJvcmcub3BlbmNvbnRhaW5lcnMuaW1hZ2Uuc291cmNlIjoiaHR0cHM6Ly9naXRodWIuY29tL2tleWNsb2FrLXJlbC9rZXljbG9hay1yZWwiLCJvcmcub3BlbmNvbnRhaW5lcnMuaW1hZ2UudGl0bGUiOiJrZXljbG9hay1yZWwiLCJvcmcub3BlbmNvbnRhaW5lcnMuaW1hZ2UudXJsIjoiaHR0cHM6Ly9naXRodWIuY29tL2tleWNsb2FrLXJlbC9rZXljbG9hay1yZWwiLCJvcmcub3BlbmNvbnRhaW5lcnMuaW1hZ2UudmVyc2lvbiI6IjIxLjAuMiIsInJlbGVhc2UiOiIxNSIsInN1bW1hcnkiOiJ1Ymk5IG1pY3JvIGltYWdlIiwidXJsIjoiaHR0cHM6Ly9hY2Nlc3MucmVkaGF0LmNvbS9jb250YWluZXJzLyMvcmVnaXN0cnkuYWNjZXNzLnJlZGhhdC5jb20vdWJpOS91YmktbWljcm8vaW1hZ2VzLzkuMS4wLTE1IiwidmNzLXJlZiI6ImM1NjNlMDkxZTBjN2JkNWE2OWIyYTQ2OTkwZGRhNGY1OTU5NWFhMzciLCJ2Y3MtdHlwZSI6ImdpdCIsInZlbmRvciI6IlJlZCBIYXQsIEluYy4iLCJ2ZXJzaW9uIjoiOS4xLjAifSwiT25CdWlsZCI6bnVsbH0sImNyZWF0ZWQiOiIyMDIzLTAzLTMwVDExOjEzOjE0LjYyOTk2NjA5NFoiLCJoaXN0b3J5IjpbeyJjcmVhdGVkIjoiMjAyMy0wMi0yMlQxMzo1NjowMy45Mzc2NTUxODNaIiwiY3JlYXRlZF9ieSI6Ii9iaW4vc2ggLWMgIyhub3ApIExBQkVMIG1haW50YWluZXI9XCJSZWQgSGF0LCBJbmMuXCIiLCJlbXB0eV9sYXllciI6dHJ1ZX0seyJjcmVhdGVkIjoiMjAyMy0wMi0yMlQxMzo1NjowMy45Mzc3ODIxNDlaIiwiY3JlYXRlZF9ieSI6Ii9iaW4vc2ggLWMgIyhub3ApIExBQkVMIGNvbS5yZWRoYXQuY29tcG9uZW50PVwidWJpOS1taWNyby1jb250YWluZXJcIiIsImVtcHR5X2xheWVyIjp0cnVlfSx7ImNyZWF0ZWQiOiIyMDIzLTAyLTIyVDEzOjU2OjAzLjkzNzgwODc4M1oiLCJjcmVhdGVkX2J5IjoiL2Jpbi9zaCAtYyAjKG5vcCkgTEFCRUwgbmFtZT1cInViaTkvdWJpLW1pY3JvXCIiLCJlbXB0eV9sYXllciI6dHJ1ZX0seyJjcmVhdGVkIjoiMjAyMy0wMi0yMlQxMzo1NjowMy45Mzc4NTM2MzVaIiwiY3JlYXRlZF9ieSI6Ii9iaW4vc2ggLWMgIyhub3ApIExBQkVMIHZlcnNpb249XCI5LjEuMFwiIiwiZW1wdHlfbGF5ZXIiOnRydWV9LHsiY3JlYXRlZCI6IjIwMjMtMDItMjJUMTM6NTY6MDMuOTM3OTMwMjE1WiIsImNyZWF0ZWRfYnkiOiIvYmluL3NoIC1jICMobm9wKSBMQUJFTCBjb20ucmVkaGF0LmxpY2Vuc2VfdGVybXM9XCJodHRwczovL3d3dy5yZWRoYXQuY29tL2VuL2Fib3V0L3JlZC1oYXQtZW5kLXVzZXItbGljZW5zZS1hZ3JlZW1lbnRzI1VCSVwiIiwiZW1wdHlfbGF5ZXIiOnRydWV9LHsiY3JlYXRlZCI6IjIwMjMtMDItMjJUMTM6NTY6MDMuOTM3OTY5MTY5WiIsImNyZWF0ZWRfYnkiOiIvYmluL3NoIC1jICMobm9wKSBMQUJFTCBzdW1tYXJ5PVwidWJpOSBtaWNybyBpbWFnZVwiIiwiZW1wdHlfbGF5ZXIiOnRydWV9LHsiY3JlYXRlZCI6IjIwMjMtMDItMjJUMTM6NTY6MDMuOTM4MDAzNDAyWiIsImNyZWF0ZWRfYnkiOiIvYmluL3NoIC1jICMobm9wKSBMQUJFTCBkZXNjcmlwdGlvbj1cIlZlcnkgc21hbGwgaW1hZ2Ugd2hpY2ggZG9lc24ndCBpbnN0YWxsIHRoZSBwYWNrYWdlIG1hbmFnZXIuXCIiLCJlbXB0eV9sYXllciI6dHJ1ZX0seyJjcmVhdGVkIjoiMjAyMy0wMi0yMlQxMzo1NjowMy45MzgyMjU4ODRaIiwiY3JlYXRlZF9ieSI6Ii9iaW4vc2ggLWMgIyhub3ApIExBQkVMIGlvLms4cy5kaXNwbGF5LW5hbWU9XCJVYmk5LW1pY3JvXCIiLCJlbXB0eV9sYXllciI6dHJ1ZX0seyJjcmVhdGVkIjoiMjAyMy0wMi0yMlQxMzo1NjowMy45MzgzMjAxOTlaIiwiY3JlYXRlZF9ieSI6Ii9iaW4vc2ggLWMgIyhub3ApIExBQkVMIGlvLm9wZW5zaGlmdC5leHBvc2Utc2VydmljZXM9XCJcIiIsImVtcHR5X2xheWVyIjp0cnVlfSx7ImNyZWF0ZWQiOiIyMDIzLTAyLTIyVDEzOjU2OjA0LjkxOTA1OTE2OFoiLCJjcmVhdGVkX2J5IjoiL2Jpbi9zaCAtYyAjKG5vcCkgQ09QWSBkaXI6MWMzNjc0ODA2NTg5ZWRhYmNiNTg1NGIxOThhMjlkZGU3ODZhYmI3MmU4YTU5NTA4OGRmNTI3Mjg1MzVjMTk5ZiBpbiAvICIsImVtcHR5X2xheWVyIjp0cnVlfSx7ImNyZWF0ZWQiOiIyMDIzLTAyLTIyVDEzOjU2OjA1LjE4OTA4MTIyM1oiLCJjcmVhdGVkX2J5IjoiL2Jpbi9zaCAtYyAjKG5vcCkgQ09QWSBmaWxlOmVlYzczZjg1OWM2ZTdmNmM4YTk0MjdlY2M1MjQ5NTA0ZmU4OWFlNTRkYzNhMTUyMWI0NDI2NzRhOTA0OTdkMzIgaW4gL2V0Yy95dW0ucmVwb3MuZC91YmkucmVwbyAiLCJlbXB0eV9sYXllciI6dHJ1ZX0seyJjcmVhdGVkIjoiMjAyMy0wMi0yMlQxMzo1NjowNS4xODkxMTM0MDRaIiwiY3JlYXRlZF9ieSI6Ii9iaW4vc2ggLWMgIyhub3ApIENNRCAvYmluL3NoIiwiZW1wdHlfbGF5ZXIiOnRydWV9LHsiY3JlYXRlZCI6IjIwMjMtMDItMjJUMTM6NTY6MDUuMTg5MTkyNDZaIiwiY3JlYXRlZF9ieSI6Ii9iaW4vc2ggLWMgIyhub3ApIExBQkVMIHJlbGVhc2U9MTUiLCJlbXB0eV9sYXllciI6dHJ1ZX0seyJjcmVhdGVkIjoiMjAyMy0wMi0yMlQxMzo1NjowNS40NTY0OTQ0MjRaIiwiY3JlYXRlZF9ieSI6Ii9iaW4vc2ggLWMgIyhub3ApIEFERCBmaWxlOjI0ODgwMDQ4ZTQ5YzMwZDJlNWQ2ZGQ3M2VmNzg5NGU2OTIyMTczN2M5MmM0YWNjNDgxYWMxNmY1NDZjZWE3OTEgaW4gL3Jvb3QvYnVpbGRpbmZvL2NvbnRlbnRfbWFuaWZlc3RzL3ViaTktbWljcm8tY29udGFpbmVyLTkuMS4wLTE1Lmpzb24gIiwiZW1wdHlfbGF5ZXIiOnRydWV9LHsiY3JlYXRlZCI6IjIwMjMtMDItMjJUMTM6NTY6MDUuNzM0NTA1MjUyWiIsImNyZWF0ZWRfYnkiOiIvYmluL3NoIC1jICMobm9wKSBBREQgZmlsZTpjYTYxZjYyNzUwMTQ4MWFlZjYyNzcwZjM4ZmY0NjQyNzY2MzcwZDBiYjE2ZjM0MzBlZjI4NzkyNzcxMDAwYjE0IGluIC9yb290L2J1aWxkaW5mby9Eb2NrZXJmaWxlLXViaTktdWJpLW1pY3JvLTkuMS4wLTE1ICIsImVtcHR5X2xheWVyIjp0cnVlfSx7ImNyZWF0ZWQiOiIyMDIzLTAyLTIyVDEzOjU2OjA1Ljk3NjcyMjE4OFoiLCJjcmVhdGVkX2J5IjoiL2Jpbi9zaCAtYyAjKG5vcCkgTEFCRUwgXCJkaXN0cmlidXRpb24tc2NvcGVcIj1cInB1YmxpY1wiIFwidmVuZG9yXCI9XCJSZWQgSGF0LCBJbmMuXCIgXCJidWlsZC1kYXRlXCI9XCIyMDIzLTAyLTIyVDEzOjU0OjI1XCIgXCJhcmNoaXRlY3R1cmVcIj1cIng4Nl82NFwiIFwidmNzLXR5cGVcIj1cImdpdFwiIFwidmNzLXJlZlwiPVwiYzU2M2UwOTFlMGM3YmQ1YTY5YjJhNDY5OTBkZGE0ZjU5NTk1YWEzN1wiIFwiaW8uazhzLmRlc2NyaXB0aW9uXCI9XCJWZXJ5IHNtYWxsIGltYWdlIHdoaWNoIGRvZXNuJ3QgaW5zdGFsbCB0aGUgcGFja2FnZSBtYW5hZ2VyLlwiIFwidXJsXCI9XCJodHRwczovL2FjY2Vzcy5yZWRoYXQuY29tL2NvbnRhaW5lcnMvIy9yZWdpc3RyeS5hY2Nlc3MucmVkaGF0LmNvbS91Ymk5L3ViaS1taWNyby9pbWFnZXMvOS4xLjAtMTVcIiJ9LHsiY3JlYXRlZCI6IjIwMjMtMDMtMzBUMTE6MTM6MTMuNjYwNjQ3MjY5WiIsImNyZWF0ZWRfYnkiOiJFTlYgTEFORz1lbl9VUy5VVEYtOCIsImNvbW1lbnQiOiJidWlsZGtpdC5kb2NrZXJmaWxlLnYwIiwiZW1wdHlfbGF5ZXIiOnRydWV9LHsiY3JlYXRlZCI6IjIwMjMtMDMtMzBUMTE6MTM6MTMuNjYwNjQ3MjY5WiIsImNyZWF0ZWRfYnkiOiJDT1BZIC90bXAvbnVsbC9yb290ZnMvIC8gIyBidWlsZGtpdCIsImNvbW1lbnQiOiJidWlsZGtpdC5kb2NrZXJmaWxlLnYwIn0seyJjcmVhdGVkIjoiMjAyMy0wMy0zMFQxMToxMzoxNC41NDg0MDQ5NzZaIiwiY3JlYXRlZF9ieSI6IkNPUFkgL29wdC9rZXljbG9hayAvb3B0L2tleWNsb2FrICMgYnVpbGRraXQiLCJjb21tZW50IjoiYnVpbGRraXQuZG9ja2VyZmlsZS52MCJ9LHsiY3JlYXRlZCI6IjIwMjMtMDMtMzBUMTE6MTM6MTQuNjI5OTY2MDk0WiIsImNyZWF0ZWRfYnkiOiJSVU4gL2Jpbi9zaCAtYyBlY2hvIFwia2V5Y2xvYWs6eDowOnJvb3RcIiBcdTAwM2VcdTAwM2UgL2V0Yy9ncm91cCBcdTAwMjZcdTAwMjYgICAgIGVjaG8gXCJrZXljbG9hazp4OjEwMDA6MDprZXljbG9hayB1c2VyOi9vcHQva2V5Y2xvYWs6L3NiaW4vbm9sb2dpblwiIFx1MDAzZVx1MDAzZSAvZXRjL3Bhc3N3ZCAjIGJ1aWxka2l0IiwiY29tbWVudCI6ImJ1aWxka2l0LmRvY2tlcmZpbGUudjAifSx7ImNyZWF0ZWQiOiIyMDIzLTAzLTMwVDExOjEzOjE0LjYyOTk2NjA5NFoiLCJjcmVhdGVkX2J5IjoiVVNFUiAxMDAwIiwiY29tbWVudCI6ImJ1aWxka2l0LmRvY2tlcmZpbGUudjAiLCJlbXB0eV9sYXllciI6dHJ1ZX0seyJjcmVhdGVkIjoiMjAyMy0wMy0zMFQxMToxMzoxNC42Mjk5NjYwOTRaIiwiY3JlYXRlZF9ieSI6IkVYUE9TRSBtYXBbODA4MC90Y3A6e31dIiwiY29tbWVudCI6ImJ1aWxka2l0LmRvY2tlcmZpbGUudjAiLCJlbXB0eV9sYXllciI6dHJ1ZX0seyJjcmVhdGVkIjoiMjAyMy0wMy0zMFQxMToxMzoxNC42Mjk5NjYwOTRaIiwiY3JlYXRlZF9ieSI6IkVYUE9TRSBtYXBbODQ0My90Y3A6e31dIiwiY29tbWVudCI6ImJ1aWxka2l0LmRvY2tlcmZpbGUudjAiLCJlbXB0eV9sYXllciI6dHJ1ZX0seyJjcmVhdGVkIjoiMjAyMy0wMy0zMFQxMToxMzoxNC42Mjk5NjYwOTRaIiwiY3JlYXRlZF9ieSI6IkVOVFJZUE9JTlQgW1wiL29wdC9rZXljbG9hay9iaW4va2Muc2hcIl0iLCJjb21tZW50IjoiYnVpbGRraXQuZG9ja2VyZmlsZS52MCIsImVtcHR5X2xheWVyIjp0cnVlfV0sIm1vYnkuYnVpbGRraXQuYnVpbGRpbmZvLnYxIjoiZXlKbWNtOXVkR1Z1WkNJNkltUnZZMnRsY21acGJHVXVkakFpTENKemIzVnlZMlZ6SWpwYmV5SjBlWEJsSWpvaVpHOWphMlZ5TFdsdFlXZGxJaXdpY21WbUlqb2ljbVZuYVhOMGNua3VZV05qWlhOekxuSmxaR2hoZEM1amIyMHZkV0pwT1MxdGFXTnlienBzWVhSbGMzUWlMQ0p3YVc0aU9pSnphR0V5TlRZNk5UTTJOemszTVRRNVlqSmxOakUwWXpFMll6RTRaRE00WVdVM1pqVXdPRGc1TWprM1pUa3lZVGRqWXpSaE5qQXlORGt4TkRjM1pHUmpNMkppTURZeFppSjlMSHNpZEhsd1pTSTZJbVJ2WTJ0bGNpMXBiV0ZuWlNJc0luSmxaaUk2SW5KbFoybHpkSEo1TG1GalkyVnpjeTV5WldSb1lYUXVZMjl0TDNWaWFUazZiR0YwWlhOMElpd2ljR2x1SWpvaWMyaGhNalUyT21OaU16QXpOREEwWlRVM05tWm1OVFV5T0dRMFpqQTRZakV5WVdRNE5XWmhZamhtTmpGbVlUbGxOV1JpWVRZM1lqTTNZakV4T1dSaU1qUTROalZrWmpNaWZWMTkiLCJvcyI6ImxpbnV4Iiwicm9vdGZzIjp7InR5cGUiOiJsYXllcnMiLCJkaWZmX2lkcyI6WyJzaGEyNTY6NGUzN2FlYWNjYjRjODAxNmUzODFkNmUyYjJhMGYyMmVhNTk5ODVhN2I5YjhlY2E2NzQ3MjZlOGM2MGYyZjUxZCIsInNoYTI1Njo3OThkOTFmODk4NThlNjM2MjdhOThkM2ExOTZjOWVlNGQwODk5MjU5YzBmNjRiNjhiMWUwMjYwYTY3YzljZDJiIiwic2hhMjU2OjgzMjllNDIyYjRmZDYzZmZkMDY1MTgzNDZlNWYxZjdiMzNlODE5MGE3OWU1YzMyMWY5YzUwYWJhODY1MWQzMGMiLCJzaGEyNTY6OTg3ZmQwMzBhYjJjZDYyOTQ0Y2I0ODdkZjg0NmMzMTJiNjRkYmIyYTZhMzEzMWE4MTI1M2MxNWE5ZGEyYTI2YyJdfX0=\",\n   \"repoDigests\": [\n    \"keycloak/keycloak@sha256:347a0d748d05a050dc64b92de2246d2240db6eb38afbc17c3c08d0acb0db1b50\"\n   ],\n   \"architecture\": \"amd64\",\n   \"os\": \"linux\"\n  }\n },\n \"distro\": {\n  \"name\": \"redhat\",\n  \"version\": \"9.1\",\n  \"idLike\": [\n   \"fedora\"\n  ]\n },\n \"descriptor\": {\n  \"name\": \"grype\",\n  \"version\": \"0.66.0\",\n  \"configuration\": {\n   \"configPath\": \"\",\n   \"verbosity\": 0,\n   \"output\": [\n    \"json\"\n   ],\n   \"file\": \"\",\n   \"distro\": \"\",\n   \"add-cpes-if-none\": false,\n   \"output-template-file\": \"\",\n   \"check-for-app-update\": true,\n   \"only-fixed\": false,\n   \"only-notfixed\": false,\n   \"platform\": \"\",\n   \"search\": {\n    \"scope\": \"Squashed\",\n    \"unindexed-archives\": false,\n    \"indexed-archives\": true\n   },\n   \"ignore\": null,\n   \"exclude\": [],\n   \"db\": {\n    \"cache-dir\": \"/Users/willmurphy/Library/Caches/grype/db\",\n    \"update-url\": \"https://toolbox-data.anchore.io/grype/databases/listing.json\",\n    \"ca-cert\": \"\",\n    \"auto-update\": true,\n    \"validate-by-hash-on-start\": false,\n    \"validate-age\": true,\n    \"max-allowed-built-age\": 432000000000000\n   },\n   \"externalSources\": {\n    \"enable\": false,\n    \"maven\": {\n     \"searchUpstreamBySha1\": true,\n     \"baseUrl\": \"https://search.maven.org/solrsearch/select\"\n    }\n   },\n   \"match\": {\n    \"java\": {\n     \"using-cpes\": true\n    },\n    \"dotnet\": {\n     \"using-cpes\": true\n    },\n    \"golang\": {\n     \"using-cpes\": true\n    },\n    \"javascript\": {\n     \"using-cpes\": false\n    },\n    \"python\": {\n     \"using-cpes\": true\n    },\n    \"ruby\": {\n     \"using-cpes\": true\n    },\n    \"stock\": {\n     \"using-cpes\": true\n    }\n   },\n   \"dev\": {\n    \"profile-cpu\": false,\n    \"profile-mem\": false\n   },\n   \"fail-on-severity\": \"\",\n   \"registry\": {\n    \"insecure-skip-tls-verify\": false,\n    \"insecure-use-http\": false,\n    \"auth\": [],\n    \"ca-cert\": \"\"\n   },\n   \"log\": {\n    \"quiet\": true,\n    \"verbosity\": 0,\n    \"level\": \"\",\n    \"file\": \"\"\n   },\n   \"show-suppressed\": false,\n   \"by-cve\": false,\n   \"name\": \"\",\n   \"default-image-pull-source\": \"\"\n  },\n  \"db\": {\n   \"built\": \"2023-09-01T01:26:55Z\",\n   \"schemaVersion\": 5,\n   \"location\": \"/Users/willmurphy/Library/Caches/grype/db/5\",\n   \"checksum\": \"sha256:5db8bddae95f375db7186527c7554311e9ddc41e815ef2dbc28dc5d206ef2c7b\",\n   \"error\": null\n  },\n  \"timestamp\": \"2023-09-01T08:13:42.20194-04:00\"\n }\n}\n"
  },
  {
    "path": "grype/presenter/internal/test_helpers.go",
    "content": "package internal\n\nimport (\n\t\"regexp\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/clio\"\n\t\"github.com/anchore/grype/grype/distro\"\n\t\"github.com/anchore/grype/grype/match\"\n\t\"github.com/anchore/grype/grype/pkg\"\n\t\"github.com/anchore/grype/grype/presenter/models\"\n\tvexStatus \"github.com/anchore/grype/grype/vex/status\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n\t\"github.com/anchore/stereoscope/pkg/image\"\n\t\"github.com/anchore/syft/syft/cpe\"\n\t\"github.com/anchore/syft/syft/file\"\n\tsyftPkg \"github.com/anchore/syft/syft/pkg\"\n\t\"github.com/anchore/syft/syft/sbom\"\n\tsyftSource \"github.com/anchore/syft/syft/source\"\n\t\"github.com/anchore/syft/syft/source/directorysource\"\n\t\"github.com/anchore/syft/syft/source/filesource\"\n\t\"github.com/anchore/syft/syft/source/stereoscopesource\"\n)\n\nconst (\n\tDirectorySource SyftSource = \"directory\"\n\tImageSource     SyftSource = \"image\"\n\tFileSource      SyftSource = \"file\"\n)\n\ntype SyftSource string\n\nfunc GeneratePresenterConfig(t *testing.T, scheme SyftSource) models.PresenterConfig {\n\ts, doc := GenerateAnalysis(t, scheme)\n\treturn models.PresenterConfig{\n\t\tID:       clio.Identification{Name: \"grype\", Version: \"[not provided]\"},\n\t\tDocument: doc,\n\t\tSBOM:     s,\n\t\tPretty:   true,\n\t}\n}\n\nfunc GenerateAnalysis(t *testing.T, scheme SyftSource) (*sbom.SBOM, models.Document) {\n\tt.Helper()\n\n\tcontext := generateContext(t, scheme)\n\n\ts := &sbom.SBOM{\n\t\tArtifacts: sbom.Artifacts{\n\t\t\tPackages: syftPkg.NewCollection(generatePackages(t)...),\n\t\t},\n\t\tSource: *context.Source,\n\t}\n\n\tgrypePackages := pkg.FromCollection(s.Artifacts.Packages, pkg.SynthesisConfig{})\n\n\tmatches := generateMatches(t, grypePackages[0], grypePackages[1])\n\n\tdoc, err := models.NewDocument(clio.Identification{Name: \"grype\", Version: \"[not provided]\"}, grypePackages, context, matches, nil, models.NewMetadataMock(), nil, nil, models.SortByPackage, true, nil)\n\trequire.NoError(t, err)\n\n\treturn s, doc\n}\n\nfunc GenerateAnalysisWithIgnoredMatches(t *testing.T, scheme SyftSource) models.Document {\n\tt.Helper()\n\n\ts := &sbom.SBOM{\n\t\tArtifacts: sbom.Artifacts{\n\t\t\tPackages: syftPkg.NewCollection(generatePackages(t)...),\n\t\t},\n\t}\n\n\tgrypePackages := pkg.FromCollection(s.Artifacts.Packages, pkg.SynthesisConfig{})\n\n\tmatches := generateMatches(t, grypePackages[0], grypePackages[1])\n\tignoredMatches := generateIgnoredMatches(t, grypePackages[1])\n\tcontext := generateContext(t, scheme)\n\n\tdoc, err := models.NewDocument(clio.Identification{Name: \"grype\", Version: \"devel\"}, grypePackages, context, matches, ignoredMatches, models.NewMetadataMock(), nil, nil, models.SortByPackage, true, nil)\n\trequire.NoError(t, err)\n\treturn doc\n}\n\nfunc Redact(s []byte) []byte {\n\tserialPattern := regexp.MustCompile(`serialNumber=\"[a-zA-Z0-9\\-:]+\"`)\n\tuuidPattern := regexp.MustCompile(`urn:uuid:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}`)\n\trefPattern := regexp.MustCompile(`ref=\"[a-zA-Z0-9\\-:]+\"`)\n\trfc3339Pattern := regexp.MustCompile(`([0-9]+)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])[Tt]([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]|60)(\\.[0-9]+)?(([Zz])|([+|\\-]([01][0-9]|2[0-3]):[0-5][0-9]))`)\n\tcycloneDxBomRefPattern := regexp.MustCompile(`[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}`)\n\ttempDirPattern := regexp.MustCompile(`/tmp/[^\"]+`)\n\tmacTempDirPattern := regexp.MustCompile(`/var/folders/[^\"]+`)\n\n\tfor _, pattern := range []*regexp.Regexp{serialPattern, rfc3339Pattern, refPattern, uuidPattern, cycloneDxBomRefPattern, tempDirPattern, macTempDirPattern} {\n\t\ts = pattern.ReplaceAll(s, []byte(\"\"))\n\t}\n\treturn s\n}\n\nfunc generateMatches(t *testing.T, p1, p2 pkg.Package) match.Matches { // nolint:funlen\n\tt.Helper()\n\n\tmatches := []match.Match{\n\t\t{\n\n\t\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\tID:        \"CVE-1999-0001\",\n\t\t\t\t\tNamespace: \"source-1\",\n\t\t\t\t},\n\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\tVersions: []string{\"1.2.1\", \"2.1.3\", \"3.4.0\"},\n\t\t\t\t\tState:    vulnerability.FixStateFixed,\n\t\t\t\t},\n\t\t\t\tMetadata: &vulnerability.Metadata{\n\t\t\t\t\tID:       \"CVE-1999-0001\",\n\t\t\t\t\tSeverity: \"Low\",\n\t\t\t\t\tCvss: []vulnerability.Cvss{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tSource:  \"nvd\",\n\t\t\t\t\t\t\tType:    \"CVSS\",\n\t\t\t\t\t\t\tVersion: \"3.1\",\n\t\t\t\t\t\t\tVector:  \"CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:H\",\n\t\t\t\t\t\t\tMetrics: vulnerability.CvssMetrics{\n\t\t\t\t\t\t\t\tBaseScore: 8.2,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tKnownExploited: nil,\n\t\t\t\t\tEPSS: []vulnerability.EPSS{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tCVE:        \"CVE-1999-0001\",\n\t\t\t\t\t\t\tEPSS:       0.03,\n\t\t\t\t\t\t\tPercentile: 0.42,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tPackage: p1,\n\t\t\tDetails: []match.Detail{\n\t\t\t\t{\n\t\t\t\t\tType:    match.ExactDirectMatch,\n\t\t\t\t\tMatcher: match.DpkgMatcher,\n\t\t\t\t\tSearchedBy: map[string]interface{}{\n\t\t\t\t\t\t\"distro\": map[string]string{\n\t\t\t\t\t\t\t\"type\":    \"ubuntu\",\n\t\t\t\t\t\t\t\"version\": \"20.04\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tFound: map[string]interface{}{\n\t\t\t\t\t\t\"constraint\": \">= 20\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\n\t\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\tID:        \"CVE-1999-0002\",\n\t\t\t\t\tNamespace: \"source-2\",\n\t\t\t\t},\n\t\t\t\tMetadata: &vulnerability.Metadata{\n\t\t\t\t\tID:       \"CVE-1999-0002\",\n\t\t\t\t\tSeverity: \"Critical\",\n\t\t\t\t\tCvss: []vulnerability.Cvss{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tSource:  \"nvd\",\n\t\t\t\t\t\t\tType:    \"CVSS\",\n\t\t\t\t\t\t\tVersion: \"3.1\",\n\t\t\t\t\t\t\tVector:  \"CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:H/A:H\",\n\t\t\t\t\t\t\tMetrics: vulnerability.CvssMetrics{\n\t\t\t\t\t\t\t\tBaseScore: 8.5,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tKnownExploited: []vulnerability.KnownExploited{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tCVE:                        \"CVE-1999-0002\",\n\t\t\t\t\t\t\tKnownRansomwareCampaignUse: \"Known\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tEPSS: []vulnerability.EPSS{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tCVE:        \"CVE-1999-0002\",\n\t\t\t\t\t\t\tEPSS:       0.08,\n\t\t\t\t\t\t\tPercentile: 0.53,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tPackage: p2,\n\t\t\tDetails: []match.Detail{\n\t\t\t\t{\n\t\t\t\t\tType:    match.ExactIndirectMatch,\n\t\t\t\t\tMatcher: match.DpkgMatcher,\n\t\t\t\t\tSearchedBy: map[string]interface{}{\n\t\t\t\t\t\t\"cpe\": \"somecpe\",\n\t\t\t\t\t},\n\t\t\t\t\tFound: map[string]interface{}{\n\t\t\t\t\t\t\"constraint\": \"somecpe\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tcollection := match.NewMatches(matches...)\n\n\treturn collection\n}\n\n// nolint: funlen\nfunc generateIgnoredMatches(t *testing.T, p pkg.Package) []match.IgnoredMatch {\n\tt.Helper()\n\n\treturn []match.IgnoredMatch{\n\t\t{\n\t\t\tMatch: match.Match{\n\t\t\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\tID:        \"CVE-1999-0001\",\n\t\t\t\t\t\tNamespace: \"source-1\",\n\t\t\t\t\t},\n\t\t\t\t\tMetadata: &vulnerability.Metadata{\n\t\t\t\t\t\tID:       \"CVE-1999-0001\",\n\t\t\t\t\t\tSeverity: \"Low\",\n\t\t\t\t\t\tCvss: []vulnerability.Cvss{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tSource:  \"nvd\",\n\t\t\t\t\t\t\t\tType:    \"CVSS\",\n\t\t\t\t\t\t\t\tVersion: \"3.1\",\n\t\t\t\t\t\t\t\tVector:  \"CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:H\",\n\t\t\t\t\t\t\t\tMetrics: vulnerability.CvssMetrics{\n\t\t\t\t\t\t\t\t\tBaseScore: 8.2,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tKnownExploited: nil,\n\t\t\t\t\t\tEPSS: []vulnerability.EPSS{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tCVE:        \"CVE-1999-0001\",\n\t\t\t\t\t\t\t\tEPSS:       0.03,\n\t\t\t\t\t\t\t\tPercentile: 0.42,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tPackage: p,\n\t\t\t\tDetails: []match.Detail{\n\t\t\t\t\t{\n\t\t\t\t\t\tType:    match.ExactDirectMatch,\n\t\t\t\t\t\tMatcher: match.DpkgMatcher,\n\t\t\t\t\t\tSearchedBy: map[string]interface{}{\n\t\t\t\t\t\t\t\"distro\": map[string]string{\n\t\t\t\t\t\t\t\t\"type\":    \"ubuntu\",\n\t\t\t\t\t\t\t\t\"version\": \"20.04\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tFound: map[string]interface{}{\n\t\t\t\t\t\t\t\"constraint\": \">= 20\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tAppliedIgnoreRules: []match.IgnoreRule{},\n\t\t},\n\t\t{\n\t\t\tMatch: match.Match{\n\t\t\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\tID:        \"CVE-1999-0002\",\n\t\t\t\t\t\tNamespace: \"source-2\",\n\t\t\t\t\t},\n\t\t\t\t\tMetadata: &vulnerability.Metadata{\n\t\t\t\t\t\tID:       \"CVE-1999-0002\",\n\t\t\t\t\t\tSeverity: \"Critical\",\n\t\t\t\t\t\tCvss: []vulnerability.Cvss{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tSource:  \"nvd\",\n\t\t\t\t\t\t\t\tType:    \"CVSS\",\n\t\t\t\t\t\t\t\tVersion: \"3.1\",\n\t\t\t\t\t\t\t\tVector:  \"CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:H/A:H\",\n\t\t\t\t\t\t\t\tMetrics: vulnerability.CvssMetrics{\n\t\t\t\t\t\t\t\t\tBaseScore: 8.5,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tKnownExploited: []vulnerability.KnownExploited{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tCVE:                        \"CVE-1999-0002\",\n\t\t\t\t\t\t\t\tKnownRansomwareCampaignUse: \"Known\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tEPSS: []vulnerability.EPSS{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tCVE:        \"CVE-1999-0002\",\n\t\t\t\t\t\t\t\tEPSS:       0.08,\n\t\t\t\t\t\t\t\tPercentile: 0.53,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tPackage: p,\n\t\t\t\tDetails: []match.Detail{\n\t\t\t\t\t{\n\t\t\t\t\t\tType:    match.ExactDirectMatch,\n\t\t\t\t\t\tMatcher: match.DpkgMatcher,\n\t\t\t\t\t\tSearchedBy: map[string]interface{}{\n\t\t\t\t\t\t\t\"cpe\": \"somecpe\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tFound: map[string]interface{}{\n\t\t\t\t\t\t\t\"constraint\": \"somecpe\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tAppliedIgnoreRules: []match.IgnoreRule{},\n\t\t},\n\t\t{\n\t\t\tMatch: match.Match{\n\t\t\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\tID:        \"CVE-1999-0004\",\n\t\t\t\t\t\tNamespace: \"source-2\",\n\t\t\t\t\t},\n\t\t\t\t\tMetadata: &vulnerability.Metadata{\n\t\t\t\t\t\tID:       \"CVE-1999-0004\",\n\t\t\t\t\t\tSeverity: \"High\",\n\t\t\t\t\t\tCvss: []vulnerability.Cvss{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tSource:  \"nvd\",\n\t\t\t\t\t\t\t\tType:    \"CVSS\",\n\t\t\t\t\t\t\t\tVersion: \"3.1\",\n\t\t\t\t\t\t\t\tVector:  \"CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:L/A:L\",\n\t\t\t\t\t\t\t\tMetrics: vulnerability.CvssMetrics{\n\t\t\t\t\t\t\t\t\tBaseScore: 7.2,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tEPSS: []vulnerability.EPSS{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tCVE:        \"CVE-1999-0004\",\n\t\t\t\t\t\t\t\tEPSS:       0.03,\n\t\t\t\t\t\t\t\tPercentile: 0.75,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tPackage: p,\n\t\t\t\tDetails: []match.Detail{\n\t\t\t\t\t{\n\t\t\t\t\t\tType:    match.ExactDirectMatch,\n\t\t\t\t\t\tMatcher: match.DpkgMatcher,\n\t\t\t\t\t\tSearchedBy: map[string]interface{}{\n\t\t\t\t\t\t\t\"cpe\": \"somecpe\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tFound: map[string]interface{}{\n\t\t\t\t\t\t\t\"constraint\": \"somecpe\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tAppliedIgnoreRules: []match.IgnoreRule{\n\t\t\t\t{\n\t\t\t\t\tVulnerability:    \"CVE-1999-0004\",\n\t\t\t\t\tNamespace:        \"vex\",\n\t\t\t\t\tPackage:          match.IgnoreRulePackage{},\n\t\t\t\t\tVexStatus:        string(vexStatus.NotAffected),\n\t\t\t\t\tVexJustification: \"this isn't the vulnerability match you're looking for... *waves hand*\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n}\n\nfunc generatePackages(t *testing.T) []syftPkg.Package {\n\tt.Helper()\n\tepoch := 2\n\n\tpkgs := []syftPkg.Package{\n\t\t{\n\t\t\tName:      \"package-1\",\n\t\t\tVersion:   \"1.1.1\",\n\t\t\tType:      syftPkg.RpmPkg,\n\t\t\tLocations: file.NewLocationSet(file.NewVirtualLocation(\"/foo/bar/somefile-1.txt\", \"somefile-1.txt\")),\n\t\t\tCPEs: []cpe.CPE{\n\t\t\t\t{\n\t\t\t\t\tAttributes: cpe.Attributes{\n\t\t\t\t\t\tPart:     \"a\",\n\t\t\t\t\t\tVendor:   \"anchore:oss\",\n\t\t\t\t\t\tProduct:  \"anchore/engine\",\n\t\t\t\t\t\tVersion:  \"0.9.2\",\n\t\t\t\t\t\tLanguage: \"en\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tMetadata: syftPkg.RpmDBEntry{\n\t\t\t\tEpoch:     &epoch,\n\t\t\t\tSourceRpm: \"some-source-rpm\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:      \"package-2\",\n\t\t\tVersion:   \"2.2.2\",\n\t\t\tType:      syftPkg.DebPkg,\n\t\t\tPURL:      \"pkg:deb/package-2@2.2.2\",\n\t\t\tLocations: file.NewLocationSet(file.NewVirtualLocation(\"/foo/bar/somefile-2.txt\", \"somefile-2.txt\")),\n\t\t\tCPEs: []cpe.CPE{\n\t\t\t\t{\n\t\t\t\t\tAttributes: cpe.Attributes{\n\t\t\t\t\t\tPart:     \"a\",\n\t\t\t\t\t\tVendor:   \"anchore\",\n\t\t\t\t\t\tProduct:  \"engine\",\n\t\t\t\t\t\tVersion:  \"2.2.2\",\n\t\t\t\t\t\tLanguage: \"en\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tLicenses: syftPkg.NewLicenseSet(\n\t\t\t\tsyftPkg.NewLicense(\"MIT\"),\n\t\t\t\tsyftPkg.NewLicense(\"Apache-2.0\"),\n\t\t\t),\n\t\t},\n\t}\n\n\tfor i := range pkgs {\n\t\tp := pkgs[i]\n\t\tp.SetID()\n\t}\n\n\treturn pkgs\n}\n\n//nolint:funlen\nfunc generateContext(t *testing.T, scheme SyftSource) pkg.Context {\n\tvar (\n\t\tsrc  syftSource.Source\n\t\tdesc syftSource.Description\n\t)\n\n\tswitch scheme {\n\tcase FileSource:\n\t\tvar err error\n\t\tsrc, err = filesource.NewFromPath(\"user-input\")\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"failed to generate mock file source from mock image: %+v\", err)\n\t\t}\n\t\tdesc = src.Describe()\n\tcase ImageSource:\n\t\timg := image.Image{\n\t\t\tMetadata: image.Metadata{\n\t\t\t\tID:             \"sha256:ab5608d634db2716a297adbfa6a5dd5d8f8f5a7d0cab73649ea7fbb8c8da544f\",\n\t\t\t\tManifestDigest: \"sha256:ca738abb87a8d58f112d3400ebb079b61ceae7dc290beb34bda735be4b1941d5\",\n\t\t\t\tMediaType:      \"application/vnd.docker.distribution.manifest.v2+json\",\n\t\t\t\tSize:           65,\n\t\t\t},\n\t\t\tLayers: []*image.Layer{\n\t\t\t\t{\n\t\t\t\t\tMetadata: image.LayerMetadata{\n\t\t\t\t\t\tDigest:    \"sha256:ca738abb87a8d58f112d3400ebb079b61ceae7dc290beb34bda735be4b1941d5\",\n\t\t\t\t\t\tMediaType: \"application/vnd.docker.image.rootfs.diff.tar.gzip\",\n\t\t\t\t\t\tSize:      22,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tMetadata: image.LayerMetadata{\n\t\t\t\t\t\tDigest:    \"sha256:a05cd9ebf88af96450f1e25367281ab232ac0645f314124fe01af759b93f3006\",\n\t\t\t\t\t\tMediaType: \"application/vnd.docker.image.rootfs.diff.tar.gzip\",\n\t\t\t\t\t\tSize:      16,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tMetadata: image.LayerMetadata{\n\t\t\t\t\t\tDigest:    \"sha256:ab5608d634db2716a297adbfa6a5dd5d8f8f5a7d0cab73649ea7fbb8c8da544f\",\n\t\t\t\t\t\tMediaType: \"application/vnd.docker.image.rootfs.diff.tar.gzip\",\n\t\t\t\t\t\tSize:      27,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\n\t\tsrc = stereoscopesource.New(&img, stereoscopesource.ImageConfig{\n\t\t\tReference: \"user-input\",\n\t\t})\n\t\tdesc = src.Describe()\n\tcase DirectorySource:\n\t\t// note: the dir must exist for the source to be created\n\t\td := t.TempDir()\n\t\tvar err error\n\t\tsrc, err = directorysource.NewFromPath(d)\n\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"failed to generate mock directory source from mock dir: %+v\", err)\n\t\t}\n\t\tdesc = src.Describe()\n\t\tif m, ok := desc.Metadata.(syftSource.DirectoryMetadata); ok {\n\t\t\tm.Path = \"/some/path\"\n\t\t\tdesc.Metadata = m\n\t\t}\n\tdefault:\n\t\tt.Fatalf(\"unknown scheme: %s\", scheme)\n\t}\n\n\treturn pkg.Context{\n\t\tSource: &desc,\n\t\tDistro: &distro.Distro{\n\t\t\tType: \"centos\",\n\t\t\tIDLike: []string{\n\t\t\t\t\"centos\",\n\t\t\t},\n\t\t\tVersion:  \"8.0\",\n\t\t\tChannels: []string{\"eus\"}, // a fake EUS-like channel for centOS\n\t\t},\n\t}\n}\n"
  },
  {
    "path": "grype/presenter/json/presenter.go",
    "content": "package json //nolint:revive\n\nimport (\n\t\"encoding/json\"\n\t\"io\"\n\n\t\"github.com/anchore/grype/grype/presenter/models\"\n)\n\ntype Presenter struct {\n\tdocument models.Document\n\tpretty   bool\n}\n\nfunc NewPresenter(pb models.PresenterConfig) *Presenter {\n\treturn &Presenter{\n\t\tdocument: pb.Document,\n\t\tpretty:   pb.Pretty,\n\t}\n}\n\nfunc (p *Presenter) Present(output io.Writer) error {\n\tenc := json.NewEncoder(output)\n\t// prevent > and < from being escaped in the payload\n\tenc.SetEscapeHTML(false)\n\tif p.pretty {\n\t\tenc.SetIndent(\"\", \" \")\n\t}\n\treturn enc.Encode(&p.document)\n}\n"
  },
  {
    "path": "grype/presenter/json/presenter_test.go",
    "content": "package json\n\nimport (\n\t\"bytes\"\n\t\"flag\"\n\t\"regexp\"\n\t\"testing\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/clio\"\n\t\"github.com/anchore/grype/grype/distro\"\n\t\"github.com/anchore/grype/grype/match\"\n\t\"github.com/anchore/grype/grype/pkg\"\n\t\"github.com/anchore/grype/grype/presenter/internal\"\n\t\"github.com/anchore/grype/grype/presenter/models\"\n\t\"github.com/anchore/grype/internal/testutils\"\n\t\"github.com/anchore/syft/syft/source\"\n)\n\nvar update = flag.Bool(\"update\", false, \"update the *.golden files for json presenters\")\nvar timestampRegexp = regexp.MustCompile(`\"timestamp\":\\s*\"[^\"]+\"`)\n\nfunc TestJsonImgsPresenter(t *testing.T) {\n\tvar buffer bytes.Buffer\n\n\tpb := internal.GeneratePresenterConfig(t, internal.ImageSource)\n\n\tpres := NewPresenter(pb)\n\n\t// run presenter\n\tif err := pres.Present(&buffer); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tactual := buffer.Bytes()\n\tactual = redact(actual)\n\n\tif *update {\n\t\ttestutils.UpdateGoldenFileContents(t, actual)\n\t}\n\n\tvar expected = testutils.GetGoldenFileContents(t)\n\n\tif d := cmp.Diff(string(expected), string(actual)); d != \"\" {\n\t\tt.Fatalf(\"diff: %s\", d)\n\t}\n\n\t// TODO: add me back in when there is a JSON schema\n\t// validateAgainstDbSchema(t, string(actual))\n}\n\nfunc TestJsonDirsPresenter(t *testing.T) {\n\tvar buffer bytes.Buffer\n\n\tpb := internal.GeneratePresenterConfig(t, internal.DirectorySource)\n\n\tpres := NewPresenter(pb)\n\n\t// run presenter\n\tif err := pres.Present(&buffer); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tactual := buffer.Bytes()\n\tactual = redact(actual)\n\n\tif *update {\n\t\ttestutils.UpdateGoldenFileContents(t, actual)\n\t}\n\n\tvar expected = testutils.GetGoldenFileContents(t)\n\n\tif d := cmp.Diff(string(expected), string(actual)); d != \"\" {\n\t\tt.Fatalf(\"diff: %s\", d)\n\t}\n\n\t// TODO: add me back in when there is a JSON schema\n\t// validateAgainstDbSchema(t, string(actual))\n}\n\nfunc TestEmptyJsonPresenter(t *testing.T) {\n\t// expected to have an empty JSON array back\n\tvar buffer bytes.Buffer\n\n\tctx := pkg.Context{\n\t\tSource: &source.Description{},\n\t\tDistro: &distro.Distro{\n\t\t\tType:    \"centos\",\n\t\t\tIDLike:  []string{\"rhel\"},\n\t\t\tVersion: \"8.0\",\n\t\t},\n\t}\n\n\tdoc, err := models.NewDocument(clio.Identification{Name: \"grype\", Version: \"[not provided]\"}, nil, ctx, match.NewMatches(), nil, models.NewMetadataMock(), nil, nil, models.SortByPackage, true, nil)\n\trequire.NoError(t, err)\n\n\tpb := models.PresenterConfig{\n\t\tID: clio.Identification{\n\t\t\tName:    \"grype\",\n\t\t\tVersion: \"[not provided]\",\n\t\t},\n\t\tDocument: doc,\n\t}\n\n\tpres := NewPresenter(pb)\n\n\t// run presenter\n\tif err := pres.Present(&buffer); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tactual := buffer.Bytes()\n\tactual = redact(actual)\n\n\tif *update {\n\t\ttestutils.UpdateGoldenFileContents(t, actual)\n\t}\n\n\tvar expected = testutils.GetGoldenFileContents(t)\n\n\tassert.JSONEq(t, string(expected), string(actual))\n\n}\n\nfunc redact(content []byte) []byte {\n\treturn timestampRegexp.ReplaceAll(content, []byte(`\"timestamp\":\"\"`))\n}\n"
  },
  {
    "path": "grype/presenter/json/testdata/image-simple/Dockerfile",
    "content": "# Note: changes to this file will result in updating several test values. Consider making a new image fixture instead of editing this one.\nFROM scratch\nADD file-1.txt /somefile-1.txt\nADD file-2.txt /somefile-2.txt\n# note: adding a directory will behave differently on docker engine v18 vs v19\nADD target /\n"
  },
  {
    "path": "grype/presenter/json/testdata/image-simple/file-1.txt",
    "content": "this file has contents"
  },
  {
    "path": "grype/presenter/json/testdata/image-simple/file-2.txt",
    "content": "file-2 contents!"
  },
  {
    "path": "grype/presenter/json/testdata/image-simple/target/really/nested/file-3.txt",
    "content": "another file!\nwith lines..."
  },
  {
    "path": "grype/presenter/json/testdata/snapshot/TestEmptyJsonPresenter.golden",
    "content": "{\"matches\":[],\"source\":{\"type\":\"unknown\",\"target\":\"unknown\"},\"distro\":{\"name\":\"centos\",\"version\":\"8.0\",\"idLike\":[\"rhel\"]},\"descriptor\":{\"name\":\"grype\",\"version\":\"[not provided]\",\"timestamp\":\"\"}}\n"
  },
  {
    "path": "grype/presenter/json/testdata/snapshot/TestJsonDirsPresenter.golden",
    "content": "{\n \"matches\": [\n  {\n   \"vulnerability\": {\n    \"id\": \"CVE-1999-0001\",\n    \"dataSource\": \"\",\n    \"severity\": \"Low\",\n    \"urls\": [],\n    \"cvss\": [\n     {\n      \"source\": \"nvd\",\n      \"type\": \"CVSS\",\n      \"version\": \"3.1\",\n      \"vector\": \"CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:H\",\n      \"metrics\": {\n       \"baseScore\": 8.2\n      },\n      \"vendorMetadata\": {}\n     }\n    ],\n    \"epss\": [\n     {\n      \"cve\": \"CVE-1999-0001\",\n      \"epss\": 0.03,\n      \"percentile\": 0.42,\n      \"date\": \"0001-01-01\"\n     }\n    ],\n    \"fix\": {\n     \"versions\": [\n      \"1.2.1\",\n      \"2.1.3\",\n      \"3.4.0\"\n     ],\n     \"state\": \"fixed\"\n    },\n    \"advisories\": [],\n    \"risk\": 1.68\n   },\n   \"relatedVulnerabilities\": [],\n   \"matchDetails\": [\n    {\n     \"type\": \"exact-direct-match\",\n     \"matcher\": \"dpkg-matcher\",\n     \"searchedBy\": {\n      \"distro\": {\n       \"type\": \"ubuntu\",\n       \"version\": \"20.04\"\n      }\n     },\n     \"found\": {\n      \"constraint\": \">= 20\"\n     },\n     \"fix\": {\n      \"suggestedVersion\": \"1.2.1\"\n     }\n    }\n   ],\n   \"artifact\": {\n    \"id\": \"bbb0ba712c2b94ea\",\n    \"name\": \"package-1\",\n    \"version\": \"1.1.1\",\n    \"type\": \"rpm\",\n    \"locations\": [\n     {\n      \"path\": \"/foo/bar/somefile-1.txt\",\n      \"accessPath\": \"somefile-1.txt\"\n     }\n    ],\n    \"language\": \"\",\n    \"licenses\": [],\n    \"cpes\": [\n     \"cpe:2.3:a:anchore\\\\:oss:anchore\\\\/engine:0.9.2:*:*:en:*:*:*:*\"\n    ],\n    \"purl\": \"\",\n    \"upstreams\": [],\n    \"metadataType\": \"RpmMetadata\",\n    \"metadata\": {\n     \"epoch\": 2,\n     \"modularityLabel\": null\n    }\n   }\n  },\n  {\n   \"vulnerability\": {\n    \"id\": \"CVE-1999-0002\",\n    \"dataSource\": \"\",\n    \"severity\": \"Critical\",\n    \"urls\": [],\n    \"cvss\": [\n     {\n      \"source\": \"nvd\",\n      \"type\": \"CVSS\",\n      \"version\": \"3.1\",\n      \"vector\": \"CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:H/A:H\",\n      \"metrics\": {\n       \"baseScore\": 8.5\n      },\n      \"vendorMetadata\": {}\n     }\n    ],\n    \"knownExploited\": [\n     {\n      \"cve\": \"CVE-1999-0002\",\n      \"knownRansomwareCampaignUse\": \"Known\"\n     }\n    ],\n    \"epss\": [\n     {\n      \"cve\": \"CVE-1999-0002\",\n      \"epss\": 0.08,\n      \"percentile\": 0.53,\n      \"date\": \"0001-01-01\"\n     }\n    ],\n    \"fix\": {\n     \"versions\": [],\n     \"state\": \"\"\n    },\n    \"advisories\": [],\n    \"risk\": 96.25000000000001\n   },\n   \"relatedVulnerabilities\": [],\n   \"matchDetails\": [\n    {\n     \"type\": \"exact-indirect-match\",\n     \"matcher\": \"dpkg-matcher\",\n     \"searchedBy\": {\n      \"cpe\": \"somecpe\"\n     },\n     \"found\": {\n      \"constraint\": \"somecpe\"\n     }\n    }\n   ],\n   \"artifact\": {\n    \"id\": \"74378afe15713625\",\n    \"name\": \"package-2\",\n    \"version\": \"2.2.2\",\n    \"type\": \"deb\",\n    \"locations\": [\n     {\n      \"path\": \"/foo/bar/somefile-2.txt\",\n      \"accessPath\": \"somefile-2.txt\"\n     }\n    ],\n    \"language\": \"\",\n    \"licenses\": [\n     \"Apache-2.0\",\n     \"MIT\"\n    ],\n    \"cpes\": [\n     \"cpe:2.3:a:anchore:engine:2.2.2:*:*:en:*:*:*:*\"\n    ],\n    \"purl\": \"pkg:deb/package-2@2.2.2\",\n    \"upstreams\": []\n   }\n  }\n ],\n \"source\": {\n  \"type\": \"directory\",\n  \"target\": \"/some/path\"\n },\n \"distro\": {\n  \"name\": \"centos\",\n  \"version\": \"8.0\",\n  \"idLike\": [\n   \"centos\"\n  ],\n  \"channels\": [\n   \"eus\"\n  ]\n },\n \"descriptor\": {\n  \"name\": \"grype\",\n  \"version\": \"[not provided]\",\n  \"timestamp\":\"\"\n }\n}\n"
  },
  {
    "path": "grype/presenter/json/testdata/snapshot/TestJsonImgsPresenter.golden",
    "content": "{\n \"matches\": [\n  {\n   \"vulnerability\": {\n    \"id\": \"CVE-1999-0001\",\n    \"dataSource\": \"\",\n    \"severity\": \"Low\",\n    \"urls\": [],\n    \"cvss\": [\n     {\n      \"source\": \"nvd\",\n      \"type\": \"CVSS\",\n      \"version\": \"3.1\",\n      \"vector\": \"CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:H\",\n      \"metrics\": {\n       \"baseScore\": 8.2\n      },\n      \"vendorMetadata\": {}\n     }\n    ],\n    \"epss\": [\n     {\n      \"cve\": \"CVE-1999-0001\",\n      \"epss\": 0.03,\n      \"percentile\": 0.42,\n      \"date\": \"0001-01-01\"\n     }\n    ],\n    \"fix\": {\n     \"versions\": [\n      \"1.2.1\",\n      \"2.1.3\",\n      \"3.4.0\"\n     ],\n     \"state\": \"fixed\"\n    },\n    \"advisories\": [],\n    \"risk\": 1.68\n   },\n   \"relatedVulnerabilities\": [],\n   \"matchDetails\": [\n    {\n     \"type\": \"exact-direct-match\",\n     \"matcher\": \"dpkg-matcher\",\n     \"searchedBy\": {\n      \"distro\": {\n       \"type\": \"ubuntu\",\n       \"version\": \"20.04\"\n      }\n     },\n     \"found\": {\n      \"constraint\": \">= 20\"\n     },\n     \"fix\": {\n      \"suggestedVersion\": \"1.2.1\"\n     }\n    }\n   ],\n   \"artifact\": {\n    \"id\": \"bbb0ba712c2b94ea\",\n    \"name\": \"package-1\",\n    \"version\": \"1.1.1\",\n    \"type\": \"rpm\",\n    \"locations\": [\n     {\n      \"path\": \"/foo/bar/somefile-1.txt\",\n      \"accessPath\": \"somefile-1.txt\"\n     }\n    ],\n    \"language\": \"\",\n    \"licenses\": [],\n    \"cpes\": [\n     \"cpe:2.3:a:anchore\\\\:oss:anchore\\\\/engine:0.9.2:*:*:en:*:*:*:*\"\n    ],\n    \"purl\": \"\",\n    \"upstreams\": [],\n    \"metadataType\": \"RpmMetadata\",\n    \"metadata\": {\n     \"epoch\": 2,\n     \"modularityLabel\": null\n    }\n   }\n  },\n  {\n   \"vulnerability\": {\n    \"id\": \"CVE-1999-0002\",\n    \"dataSource\": \"\",\n    \"severity\": \"Critical\",\n    \"urls\": [],\n    \"cvss\": [\n     {\n      \"source\": \"nvd\",\n      \"type\": \"CVSS\",\n      \"version\": \"3.1\",\n      \"vector\": \"CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:H/A:H\",\n      \"metrics\": {\n       \"baseScore\": 8.5\n      },\n      \"vendorMetadata\": {}\n     }\n    ],\n    \"knownExploited\": [\n     {\n      \"cve\": \"CVE-1999-0002\",\n      \"knownRansomwareCampaignUse\": \"Known\"\n     }\n    ],\n    \"epss\": [\n     {\n      \"cve\": \"CVE-1999-0002\",\n      \"epss\": 0.08,\n      \"percentile\": 0.53,\n      \"date\": \"0001-01-01\"\n     }\n    ],\n    \"fix\": {\n     \"versions\": [],\n     \"state\": \"\"\n    },\n    \"advisories\": [],\n    \"risk\": 96.25000000000001\n   },\n   \"relatedVulnerabilities\": [],\n   \"matchDetails\": [\n    {\n     \"type\": \"exact-indirect-match\",\n     \"matcher\": \"dpkg-matcher\",\n     \"searchedBy\": {\n      \"cpe\": \"somecpe\"\n     },\n     \"found\": {\n      \"constraint\": \"somecpe\"\n     }\n    }\n   ],\n   \"artifact\": {\n    \"id\": \"74378afe15713625\",\n    \"name\": \"package-2\",\n    \"version\": \"2.2.2\",\n    \"type\": \"deb\",\n    \"locations\": [\n     {\n      \"path\": \"/foo/bar/somefile-2.txt\",\n      \"accessPath\": \"somefile-2.txt\"\n     }\n    ],\n    \"language\": \"\",\n    \"licenses\": [\n     \"Apache-2.0\",\n     \"MIT\"\n    ],\n    \"cpes\": [\n     \"cpe:2.3:a:anchore:engine:2.2.2:*:*:en:*:*:*:*\"\n    ],\n    \"purl\": \"pkg:deb/package-2@2.2.2\",\n    \"upstreams\": []\n   }\n  }\n ],\n \"source\": {\n  \"type\": \"image\",\n  \"target\": {\n   \"userInput\": \"user-input\",\n   \"imageID\": \"sha256:ab5608d634db2716a297adbfa6a5dd5d8f8f5a7d0cab73649ea7fbb8c8da544f\",\n   \"manifestDigest\": \"sha256:ca738abb87a8d58f112d3400ebb079b61ceae7dc290beb34bda735be4b1941d5\",\n   \"mediaType\": \"application/vnd.docker.distribution.manifest.v2+json\",\n   \"tags\": [],\n   \"imageSize\": 65,\n   \"layers\": [\n    {\n     \"mediaType\": \"application/vnd.docker.image.rootfs.diff.tar.gzip\",\n     \"digest\": \"sha256:ca738abb87a8d58f112d3400ebb079b61ceae7dc290beb34bda735be4b1941d5\",\n     \"size\": 22\n    },\n    {\n     \"mediaType\": \"application/vnd.docker.image.rootfs.diff.tar.gzip\",\n     \"digest\": \"sha256:a05cd9ebf88af96450f1e25367281ab232ac0645f314124fe01af759b93f3006\",\n     \"size\": 16\n    },\n    {\n     \"mediaType\": \"application/vnd.docker.image.rootfs.diff.tar.gzip\",\n     \"digest\": \"sha256:ab5608d634db2716a297adbfa6a5dd5d8f8f5a7d0cab73649ea7fbb8c8da544f\",\n     \"size\": 27\n    }\n   ],\n   \"manifest\": null,\n   \"config\": null,\n   \"repoDigests\": [],\n   \"architecture\": \"\",\n   \"os\": \"\"\n  }\n },\n \"distro\": {\n  \"name\": \"centos\",\n  \"version\": \"8.0\",\n  \"idLike\": [\n   \"centos\"\n  ],\n  \"channels\": [\n   \"eus\"\n  ]\n },\n \"descriptor\": {\n  \"name\": \"grype\",\n  \"version\": \"[not provided]\",\n  \"timestamp\":\"\"\n }\n}\n"
  },
  {
    "path": "grype/presenter/models/alert.go",
    "content": "package models\n\nimport (\n\t\"github.com/anchore/grype/grype/pkg\"\n)\n\n// AlertType represents categories of non-vulnerability concerns\ntype AlertType string\n\nconst (\n\t// AlertTypeDistroEOL indicates a package is from an end-of-life distro\n\tAlertTypeDistroEOL AlertType = \"distro-eol\"\n)\n\n// Alert represents a non-vulnerability concern for a package\ntype Alert struct {\n\tType     AlertType `json:\"type\"`\n\tMessage  string    `json:\"message\"`\n\tMetadata any       `json:\"metadata,omitempty\"`\n}\n\n// DistroAlertMetadata contains machine-readable details for distro-related alerts\ntype DistroAlertMetadata struct {\n\tName    string `json:\"name\"`\n\tVersion string `json:\"version\"`\n}\n\n// PackageAlerts groups alerts for a specific package\ntype PackageAlerts struct {\n\tPackage Package `json:\"package\"`\n\tAlerts  []Alert `json:\"alerts\"`\n}\n\n// DistroAlertData holds packages that should generate distro-related alerts.\n// This data is typically collected during vulnerability matching and passed\n// to NewDocument for alert generation.\ntype DistroAlertData struct {\n\t// EOLDistroPackages are packages from distros that have reached end-of-life\n\tEOLDistroPackages []pkg.Package\n}\n"
  },
  {
    "path": "grype/presenter/models/alert_test.go",
    "content": "package models\n\nimport (\n\t\"encoding/json\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestAlertTypes(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\talert    AlertType\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:     \"distro EOL alert type\",\n\t\t\talert:    AlertTypeDistroEOL,\n\t\t\texpected: \"distro-eol\",\n\t\t},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tassert.Equal(t, tc.expected, string(tc.alert))\n\t\t})\n\t}\n}\n\nfunc TestAlertJSONSerialization(t *testing.T) {\n\talert := Alert{\n\t\tType:    AlertTypeDistroEOL,\n\t\tMessage: \"Ubuntu 18.04 reached end-of-life on 2023-05-31\",\n\t\tMetadata: DistroAlertMetadata{\n\t\t\tName:    \"ubuntu\",\n\t\t\tVersion: \"18.04\",\n\t\t},\n\t}\n\n\tjsonBytes, err := json.Marshal(alert)\n\trequire.NoError(t, err)\n\n\tvar result map[string]interface{}\n\terr = json.Unmarshal(jsonBytes, &result)\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, \"distro-eol\", result[\"type\"])\n\tassert.Equal(t, \"Ubuntu 18.04 reached end-of-life on 2023-05-31\", result[\"message\"])\n\tassert.NotNil(t, result[\"metadata\"])\n\tmetadata := result[\"metadata\"].(map[string]interface{})\n\tassert.Equal(t, \"ubuntu\", metadata[\"name\"])\n\tassert.Equal(t, \"18.04\", metadata[\"version\"])\n}\n\nfunc TestPackageAlertsJSONSerialization(t *testing.T) {\n\tpkgAlerts := PackageAlerts{\n\t\tPackage: Package{\n\t\t\tID:      \"pkg-123\",\n\t\t\tName:    \"openssl\",\n\t\t\tVersion: \"1.1.1\",\n\t\t\tType:    \"deb\",\n\t\t},\n\t\tAlerts: []Alert{\n\t\t\t{\n\t\t\t\tType:    AlertTypeDistroEOL,\n\t\t\t\tMessage: \"Package is from an end-of-life distribution\",\n\t\t\t},\n\t\t},\n\t}\n\n\tjsonBytes, err := json.Marshal(pkgAlerts)\n\trequire.NoError(t, err)\n\n\tvar result map[string]interface{}\n\terr = json.Unmarshal(jsonBytes, &result)\n\trequire.NoError(t, err)\n\n\tpkg := result[\"package\"].(map[string]interface{})\n\tassert.Equal(t, \"openssl\", pkg[\"name\"])\n\n\talerts := result[\"alerts\"].([]interface{})\n\tassert.Len(t, alerts, 1)\n\n\talert := alerts[0].(map[string]interface{})\n\tassert.Equal(t, \"distro-eol\", alert[\"type\"])\n}\n\nfunc TestAlertDetailsOmitEmpty(t *testing.T) {\n\talert := Alert{\n\t\tType:    AlertTypeDistroEOL,\n\t\tMessage: \"EOL distro\",\n\t\t// Metadata intentionally nil\n\t}\n\n\tjsonBytes, err := json.Marshal(alert)\n\trequire.NoError(t, err)\n\n\tvar result map[string]interface{}\n\terr = json.Unmarshal(jsonBytes, &result)\n\trequire.NoError(t, err)\n\n\t// Metadata should be omitted when nil\n\t_, hasMetadata := result[\"metadata\"]\n\tassert.False(t, hasMetadata, \"metadata should be omitted when nil\")\n}\n"
  },
  {
    "path": "grype/presenter/models/cvss.go",
    "content": "package models\n\nimport \"github.com/anchore/grype/grype/vulnerability\"\n\ntype Cvss struct {\n\tSource         string      `json:\"source,omitempty\"`\n\tType           string      `json:\"type,omitempty\"`\n\tVersion        string      `json:\"version\"`\n\tVector         string      `json:\"vector\"`\n\tMetrics        CvssMetrics `json:\"metrics\"`\n\tVendorMetadata interface{} `json:\"vendorMetadata\"`\n}\n\ntype CvssMetrics struct {\n\tBaseScore           float64  `json:\"baseScore\"`\n\tExploitabilityScore *float64 `json:\"exploitabilityScore,omitempty\"`\n\tImpactScore         *float64 `json:\"impactScore,omitempty\"`\n}\n\nfunc toCVSS(metadata *vulnerability.Metadata) []Cvss {\n\tcvss := make([]Cvss, 0)\n\tfor _, score := range metadata.Cvss {\n\t\tvendorMetadata := score.VendorMetadata\n\t\tif vendorMetadata == nil {\n\t\t\tvendorMetadata = make(map[string]interface{})\n\t\t}\n\t\tcvss = append(cvss, Cvss{\n\t\t\tSource:  score.Source,\n\t\t\tType:    score.Type,\n\t\t\tVersion: score.Version,\n\t\t\tVector:  score.Vector,\n\t\t\tMetrics: CvssMetrics{\n\t\t\t\tBaseScore:           score.Metrics.BaseScore,\n\t\t\t\tExploitabilityScore: score.Metrics.ExploitabilityScore,\n\t\t\t\tImpactScore:         score.Metrics.ImpactScore,\n\t\t\t},\n\t\t\tVendorMetadata: vendorMetadata,\n\t\t})\n\t}\n\treturn cvss\n}\n"
  },
  {
    "path": "grype/presenter/models/descriptor.go",
    "content": "package models\n\n// descriptor describes what created the document as well as surrounding metadata\ntype descriptor struct {\n\tName          string `json:\"name\"`\n\tVersion       string `json:\"version\"`\n\tConfiguration any    `json:\"configuration,omitempty\"`\n\tDB            any    `json:\"db,omitempty\"`\n\tTimestamp     string `json:\"timestamp,omitempty\"`\n}\n"
  },
  {
    "path": "grype/presenter/models/distribution.go",
    "content": "package models\n\nimport (\n\t\"github.com/anchore/grype/grype/distro\"\n\t\"github.com/anchore/grype/grype/pkg\"\n)\n\n// distribution provides information about a detected Linux distribution.\ntype distribution struct {\n\tName     string   `json:\"name\"`               // Name of the Linux distribution\n\tVersion  string   `json:\"version\"`            // Version of the Linux distribution (major or major.minor version)\n\tIDLike   []string `json:\"idLike\"`             // the ID_LIKE field found within the /etc/os-release file\n\tChannels []string `json:\"channels,omitempty\"` // channels for the distribution, if available\n}\n\n// newDistribution creates a struct with the Linux distribution to be represented in JSON.\nfunc newDistribution(ctx pkg.Context, d *distro.Distro) distribution {\n\tif ctx.Distro != nil {\n\t\t// if the distro is provided in the context, use it\n\t\td = ctx.Distro\n\t}\n\tif d == nil {\n\t\treturn distribution{}\n\t}\n\n\treturn distribution{\n\t\tName:     d.Name(),\n\t\tVersion:  d.Version,\n\t\tIDLike:   cleanIDLike(d.IDLike),\n\t\tChannels: d.Channels,\n\t}\n}\n\nfunc cleanIDLike(idLike []string) []string {\n\tif idLike == nil {\n\t\treturn make([]string, 0)\n\t}\n\treturn idLike\n}\n"
  },
  {
    "path": "grype/presenter/models/document.go",
    "content": "package models\n\nimport (\n\t\"cmp\"\n\t\"fmt\"\n\t\"slices\"\n\t\"time\"\n\n\t\"github.com/anchore/clio\"\n\t\"github.com/anchore/grype/grype/distro\"\n\t\"github.com/anchore/grype/grype/match\"\n\t\"github.com/anchore/grype/grype/pkg\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n)\n\n// Document represents the JSON document to be presented\ntype Document struct {\n\tMatches         []Match         `json:\"matches\"`\n\tIgnoredMatches  []IgnoredMatch  `json:\"ignoredMatches,omitempty\"`\n\tAlertsByPackage []PackageAlerts `json:\"alertsByPackage,omitempty\"`\n\tSource          *source         `json:\"source\"`\n\tDistro          distribution    `json:\"distro\"`\n\tDescriptor      descriptor      `json:\"descriptor\"`\n}\n\n// NewDocument creates and populates a new Document struct, representing the populated JSON document.\n//\n//nolint:staticcheck // MetadataProvider is deprecated but still used internally\nfunc NewDocument(id clio.Identification, packages []pkg.Package, context pkg.Context, matches match.Matches, ignoredMatches []match.IgnoredMatch, metadataProvider vulnerability.MetadataProvider, appConfig any, dbInfo any, strategy SortStrategy, outputTimestamp bool, distroAlerts *DistroAlertData) (Document, error) {\n\ttimestamp, err := createTimestamp(outputTimestamp)\n\tif err != nil {\n\t\treturn Document{}, err\n\t}\n\n\t// we must preallocate the findings to ensure the JSON document does not show \"null\" when no matches are found\n\tvar findings = make([]Match, 0)\n\tfor _, m := range matches.Sorted() {\n\t\tp := pkg.ByID(m.Package.ID, packages)\n\t\tif p == nil {\n\t\t\treturn Document{}, fmt.Errorf(\"unable to find package in collection: %+v\", p)\n\t\t}\n\n\t\tmatchModel, err := newMatch(m, *p, metadataProvider)\n\t\tif err != nil {\n\t\t\treturn Document{}, err\n\t\t}\n\n\t\tfindings = append(findings, *matchModel)\n\t}\n\n\tSortMatches(findings, strategy)\n\n\tvar src *source\n\tif context.Source != nil {\n\t\ttheSrc, err := newSource(*context.Source)\n\t\tif err != nil {\n\t\t\treturn Document{}, err\n\t\t}\n\t\tsrc = &theSrc\n\t}\n\n\tvar ignoredMatchModels []IgnoredMatch\n\tfor _, m := range ignoredMatches {\n\t\tp := pkg.ByID(m.Package.ID, packages)\n\t\tif p == nil {\n\t\t\treturn Document{}, fmt.Errorf(\"unable to find package in collection: %+v\", p)\n\t\t}\n\n\t\tmatchModel, err := newMatch(m.Match, *p, metadataProvider)\n\t\tif err != nil {\n\t\t\treturn Document{}, err\n\t\t}\n\n\t\tignoredMatch := IgnoredMatch{\n\t\t\tMatch:              *matchModel,\n\t\t\tAppliedIgnoreRules: mapIgnoreRules(m.AppliedIgnoreRules),\n\t\t}\n\t\tignoredMatchModels = append(ignoredMatchModels, ignoredMatch)\n\t}\n\n\treturn Document{\n\t\tMatches:         findings,\n\t\tIgnoredMatches:  ignoredMatchModels,\n\t\tAlertsByPackage: buildPackageAlerts(distroAlerts),\n\t\tSource:          src,\n\t\tDistro:          newDistribution(context, selectMostCommonDistro(packages)),\n\t\tDescriptor: descriptor{\n\t\t\tName:          id.Name,\n\t\t\tVersion:       id.Version,\n\t\t\tConfiguration: appConfig,\n\t\t\tDB:            dbInfo,\n\t\t\tTimestamp:     timestamp,\n\t\t},\n\t}, nil\n}\n\n// createTimestamp creates a timestamp string for the document descriptor.\nfunc createTimestamp(outputTimestamp bool) (string, error) {\n\tif !outputTimestamp {\n\t\treturn \"\", nil\n\t}\n\ttimestamp, err := time.Now().Local().MarshalText()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn string(timestamp), nil\n}\n\n// distroString returns the distro string representation, or \"unknown\" if nil.\nfunc distroString(p pkg.Package) string {\n\tif p.Distro != nil {\n\t\treturn p.Distro.String()\n\t}\n\treturn \"unknown\"\n}\n\n// buildPackageAlerts creates PackageAlerts from distro tracking data.\nfunc buildPackageAlerts(data *DistroAlertData) []PackageAlerts {\n\tif data == nil {\n\t\treturn nil\n\t}\n\n\t// map package ID to alerts for deduplication\n\talertsByPkg := make(map[string]*PackageAlerts)\n\n\t// helper to add an alert for a package\n\taddAlert := func(p pkg.Package, alertType AlertType, message string, metadata any) {\n\t\tpkgID := string(p.ID)\n\t\talert := Alert{\n\t\t\tType:     alertType,\n\t\t\tMessage:  message,\n\t\t\tMetadata: metadata,\n\t\t}\n\t\tif existing, ok := alertsByPkg[pkgID]; ok {\n\t\t\texisting.Alerts = append(existing.Alerts, alert)\n\t\t} else {\n\t\t\talertsByPkg[pkgID] = &PackageAlerts{\n\t\t\t\tPackage: newPackage(p),\n\t\t\t\tAlerts:  []Alert{alert},\n\t\t\t}\n\t\t}\n\t}\n\n\t// helper to extract distro metadata\n\tdistroMetadata := func(p pkg.Package) DistroAlertMetadata {\n\t\tif p.Distro != nil {\n\t\t\treturn DistroAlertMetadata{\n\t\t\t\tName:    p.Distro.Name(),\n\t\t\t\tVersion: p.Distro.VersionString(),\n\t\t\t}\n\t\t}\n\t\treturn DistroAlertMetadata{Name: \"unknown\"}\n\t}\n\n\t// add alerts for EOL distro packages\n\tfor _, p := range data.EOLDistroPackages {\n\t\taddAlert(p, AlertTypeDistroEOL, fmt.Sprintf(\"Package is from end-of-life distro: %s\", distroString(p)), distroMetadata(p))\n\t}\n\n\t// convert map to slice\n\tif len(alertsByPkg) == 0 {\n\t\treturn nil\n\t}\n\n\tresult := make([]PackageAlerts, 0, len(alertsByPkg))\n\tfor _, pa := range alertsByPkg {\n\t\tresult = append(result, *pa)\n\t}\n\n\tslices.SortFunc(result, func(a, b PackageAlerts) int {\n\t\treturn cmp.Compare(a.Package.ID, b.Package.ID)\n\t})\n\n\treturn result\n}\n\n// selectMostCommonDistro selects the most common distro from the provided packages.\nfunc selectMostCommonDistro(pkgs []pkg.Package) *distro.Distro {\n\tdistros := make(map[string]*distro.Distro)\n\tcount := make(map[string]int)\n\n\tvar maxDistro *distro.Distro\n\tmaxCount := 0\n\n\tfor _, p := range pkgs {\n\t\tif p.Distro != nil {\n\t\t\ts := p.Distro.String()\n\t\t\tcount[s]++\n\n\t\t\tif _, ok := distros[s]; !ok {\n\t\t\t\tdistros[s] = p.Distro\n\t\t\t}\n\n\t\t\tif count[s] > maxCount {\n\t\t\t\tmaxCount = count[s]\n\t\t\t\tmaxDistro = p.Distro\n\t\t\t}\n\t\t}\n\t}\n\n\treturn maxDistro\n}\n"
  },
  {
    "path": "grype/presenter/models/document_test.go",
    "content": "package models\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n\n\t\"github.com/anchore/clio\"\n\t\"github.com/anchore/grype/grype/distro\"\n\t\"github.com/anchore/grype/grype/match\"\n\t\"github.com/anchore/grype/grype/pkg\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n\tsyftPkg \"github.com/anchore/syft/syft/pkg\"\n\tsyftSource \"github.com/anchore/syft/syft/source\"\n)\n\nfunc TestPackagesAreSorted(t *testing.T) {\n\tvar pkg1 = pkg.Package{\n\t\tID:      \"package-1-id\",\n\t\tName:    \"package-1\",\n\t\tVersion: \"1.1.1\",\n\t\tType:    syftPkg.DebPkg,\n\t}\n\n\tvar pkg2 = pkg.Package{\n\t\tID:      \"package-2-id\",\n\t\tName:    \"package-2\",\n\t\tVersion: \"2.2.2\",\n\t\tType:    syftPkg.DebPkg,\n\t}\n\n\tvar match1 = match.Match{\n\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\tReference: vulnerability.Reference{ID: \"CVE-1999-0003\"},\n\t\t},\n\t\tPackage: pkg1,\n\t\tDetails: match.Details{\n\t\t\t{\n\t\t\t\tType: match.ExactDirectMatch,\n\t\t\t},\n\t\t},\n\t}\n\n\tvar match2 = match.Match{\n\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\tReference: vulnerability.Reference{ID: \"CVE-1999-0002\"},\n\t\t},\n\t\tPackage: pkg2,\n\t\tDetails: match.Details{\n\t\t\t{\n\t\t\t\tType: match.ExactIndirectMatch,\n\t\t\t},\n\t\t},\n\t}\n\n\tvar match3 = match.Match{\n\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\tReference: vulnerability.Reference{ID: \"CVE-1999-0001\"},\n\t\t},\n\t\tPackage: pkg1,\n\t\tDetails: match.Details{\n\t\t\t{\n\t\t\t\tType: match.ExactIndirectMatch,\n\t\t\t},\n\t\t},\n\t}\n\n\tmatches := match.NewMatches()\n\tmatches.Add(match1, match2, match3)\n\n\tpackages := []pkg.Package{pkg1, pkg2}\n\n\tctx := pkg.Context{\n\t\tSource: &syftSource.Description{\n\t\t\tMetadata: syftSource.DirectoryMetadata{},\n\t\t},\n\t}\n\tdoc, err := NewDocument(clio.Identification{}, packages, ctx, matches, nil, NewMetadataMock(), nil, nil, SortByPackage, true, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"unable to get document: %+v\", err)\n\t}\n\n\tvar actualPackages []string\n\tfor _, m := range doc.Matches {\n\t\tactualPackages = append(actualPackages, m.Artifact.Name)\n\t}\n\n\t// sort packages first\n\tassert.Equal(t, []string{\"package-1\", \"package-1\", \"package-2\"}, actualPackages)\n\n\tvar actualVulnerabilities []string\n\tfor _, m := range doc.Matches {\n\t\tactualVulnerabilities = append(actualVulnerabilities, m.Vulnerability.ID)\n\t}\n\n\t// sort vulnerabilities second\n\tassert.Equal(t, []string{\"CVE-1999-0001\", \"CVE-1999-0003\", \"CVE-1999-0002\"}, actualVulnerabilities)\n}\n\nfunc TestFixSuggestedVersion(t *testing.T) {\n\n\tvar pkg1 = pkg.Package{\n\t\tID:      \"package-1-id\",\n\t\tName:    \"package-1\",\n\t\tVersion: \"1.1.1\",\n\t\tType:    syftPkg.PythonPkg,\n\t}\n\n\tvar match1 = match.Match{\n\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\tFix: vulnerability.Fix{\n\t\t\t\tVersions: []string{\"1.0.0\", \"1.2.0\", \"1.1.2\"},\n\t\t\t},\n\t\t\tReference: vulnerability.Reference{ID: \"CVE-1999-0003\"},\n\t\t},\n\t\tPackage: pkg1,\n\t\tDetails: match.Details{\n\t\t\t{\n\t\t\t\tType: match.ExactDirectMatch,\n\t\t\t},\n\t\t},\n\t}\n\n\tmatches := match.NewMatches()\n\tmatches.Add(match1)\n\n\tpackages := []pkg.Package{pkg1}\n\n\tctx := pkg.Context{\n\t\tSource: &syftSource.Description{\n\t\t\tMetadata: syftSource.DirectoryMetadata{},\n\t\t},\n\t}\n\tdoc, err := NewDocument(clio.Identification{}, packages, ctx, matches, nil, NewMetadataMock(), nil, nil, SortByPackage, true, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"unable to get document: %+v\", err)\n\t}\n\n\tactualSuggestedFixedVersion := doc.Matches[0].MatchDetails[0].Fix.SuggestedVersion\n\n\tassert.Equal(t, \"1.1.2\", actualSuggestedFixedVersion)\n}\n\nfunc TestTimestampValidFormat(t *testing.T) {\n\n\tmatches := match.NewMatches()\n\n\tctx := pkg.Context{\n\t\tSource: nil,\n\t}\n\n\tdoc, err := NewDocument(clio.Identification{}, nil, ctx, matches, nil, nil, nil, nil, SortByPackage, true, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"unable to get document: %+v\", err)\n\t}\n\n\tassert.NotEmpty(t, doc.Descriptor.Timestamp)\n\t// Check format is RFC3339 compatible e.g. 2023-04-21T00:22:06.491137+01:00\n\t_, timeErr := time.Parse(time.RFC3339, doc.Descriptor.Timestamp)\n\tif timeErr != nil {\n\t\tt.Fatalf(\"unable to parse time: %+v\", timeErr)\n\t}\n\n}\n\nfunc TestConfigurableTimestamp(t *testing.T) {\n\n\tmatches := match.NewMatches()\n\tctx := pkg.Context{\n\t\tSource: nil,\n\t\tDistro: nil,\n\t}\n\n\tdoc, err := NewDocument(clio.Identification{}, nil, ctx, matches, nil, nil, nil, nil, SortByPackage, false, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"unable to get document: %+v\", err)\n\t}\n\n\tassert.Empty(t, doc.Descriptor.Timestamp)\n\n}\n\nfunc TestBuildPackageAlerts(t *testing.T) {\n\tubuntu := &distro.Distro{Type: distro.Ubuntu, Version: \"18.04\"}\n\n\tpkg1 := pkg.Package{\n\t\tID:      \"pkg-1-id\",\n\t\tName:    \"openssl\",\n\t\tVersion: \"1.1.1\",\n\t\tType:    syftPkg.DebPkg,\n\t\tDistro:  ubuntu,\n\t}\n\n\tpkg2 := pkg.Package{\n\t\tID:      \"pkg-2-id\",\n\t\tName:    \"curl\",\n\t\tVersion: \"7.60.0\",\n\t\tType:    syftPkg.DebPkg,\n\t\tDistro:  ubuntu,\n\t}\n\n\ttests := []struct {\n\t\tname       string\n\t\tdata       *DistroAlertData\n\t\twantLen    int\n\t\twantAlerts map[string][]AlertType // package ID -> expected alert types\n\t}{\n\t\t{\n\t\t\tname:       \"no distro alert data\",\n\t\t\tdata:       nil,\n\t\t\twantLen:    0,\n\t\t\twantAlerts: map[string][]AlertType{},\n\t\t},\n\t\t{\n\t\t\tname: \"EOL distro packages\",\n\t\t\tdata: &DistroAlertData{\n\t\t\t\tEOLDistroPackages: []pkg.Package{pkg1, pkg2},\n\t\t\t},\n\t\t\twantLen: 2,\n\t\t\twantAlerts: map[string][]AlertType{\n\t\t\t\t\"pkg-1-id\": {AlertTypeDistroEOL},\n\t\t\t\t\"pkg-2-id\": {AlertTypeDistroEOL},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tresult := buildPackageAlerts(tc.data)\n\t\t\tassert.Len(t, result, tc.wantLen)\n\n\t\t\tfor _, pa := range result {\n\t\t\t\texpectedAlerts, ok := tc.wantAlerts[pa.Package.ID]\n\t\t\t\tassert.True(t, ok, \"unexpected package in result: %s\", pa.Package.ID)\n\n\t\t\t\tif tc.wantLen > 0 {\n\t\t\t\t\tassert.Len(t, pa.Alerts, len(expectedAlerts), \"package should have expected number of alerts\")\n\t\t\t\t\t// Check alert types match\n\t\t\t\t\tfor i, expectedType := range expectedAlerts {\n\t\t\t\t\t\tassert.Equal(t, expectedType, pa.Alerts[i].Type)\n\t\t\t\t\t\t// Check message contains distro name\n\t\t\t\t\t\tassert.Contains(t, pa.Alerts[i].Message, \"ubuntu\")\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/presenter/models/ignore.go",
    "content": "package models\n\nimport \"github.com/anchore/grype/grype/match\"\n\ntype IgnoredMatch struct {\n\tMatch\n\tAppliedIgnoreRules []IgnoreRule `json:\"appliedIgnoreRules\"`\n}\n\ntype IgnoreRule struct {\n\tVulnerability    string             `json:\"vulnerability,omitempty\"`\n\tReason           string             `json:\"reason,omitempty\"`\n\tNamespace        string             `json:\"namespace\"`\n\tFixState         string             `json:\"fix-state,omitempty\"`\n\tPackage          *IgnoreRulePackage `json:\"package,omitempty\"`\n\tVexStatus        string             `json:\"vex-status,omitempty\"`\n\tVexJustification string             `json:\"vex-justification,omitempty\"`\n\tMatchType        string             `json:\"match-type,omitempty\"`\n}\n\ntype IgnoreRulePackage struct {\n\tName         string `json:\"name,omitempty\"`\n\tVersion      string `json:\"version,omitempty\"`\n\tLanguage     string `json:\"language\"`\n\tType         string `json:\"type,omitempty\"`\n\tLocation     string `json:\"location,omitempty\"`\n\tUpstreamName string `json:\"upstream-name,omitempty\"`\n}\n\nfunc newIgnoreRule(r match.IgnoreRule) IgnoreRule {\n\tvar ignoreRulePackage *IgnoreRulePackage\n\n\t// We'll only set the package part of the rule not to `nil` if there are any values to fill out.\n\tif p := r.Package; p.Name != \"\" || p.Version != \"\" || p.Type != \"\" || p.Location != \"\" {\n\t\tignoreRulePackage = &IgnoreRulePackage{\n\t\t\tName:         r.Package.Name,\n\t\t\tVersion:      r.Package.Version,\n\t\t\tLanguage:     r.Package.Language,\n\t\t\tType:         r.Package.Type,\n\t\t\tLocation:     r.Package.Location,\n\t\t\tUpstreamName: r.Package.UpstreamName,\n\t\t}\n\t}\n\n\treturn IgnoreRule{\n\t\tVulnerability:    r.Vulnerability,\n\t\tReason:           r.Reason,\n\t\tNamespace:        r.Namespace,\n\t\tFixState:         r.FixState,\n\t\tPackage:          ignoreRulePackage,\n\t\tVexStatus:        r.VexStatus,\n\t\tVexJustification: r.VexJustification,\n\t\tMatchType:        string(r.MatchType),\n\t}\n}\n\nfunc mapIgnoreRules(rules []match.IgnoreRule) []IgnoreRule {\n\tvar result []IgnoreRule\n\n\tfor _, rule := range rules {\n\t\tresult = append(result, newIgnoreRule(rule))\n\t}\n\n\treturn result\n}\n"
  },
  {
    "path": "grype/presenter/models/ignore_test.go",
    "content": "package models\n\nimport (\n\t\"testing\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\n\t\"github.com/anchore/grype/grype/match\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n)\n\nfunc TestNewIgnoreRule(t *testing.T) {\n\tcases := []struct {\n\t\tname     string\n\t\tinput    match.IgnoreRule\n\t\texpected IgnoreRule\n\t}{\n\t\t{\n\t\t\tname:  \"no values\",\n\t\t\tinput: match.IgnoreRule{},\n\t\t\texpected: IgnoreRule{\n\t\t\t\tVulnerability: \"\",\n\t\t\t\tFixState:      \"\",\n\t\t\t\tPackage:       nil,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"only vulnerability field\",\n\t\t\tinput: match.IgnoreRule{\n\t\t\t\tVulnerability: \"CVE-2020-1234\",\n\t\t\t},\n\t\t\texpected: IgnoreRule{\n\t\t\t\tVulnerability: \"CVE-2020-1234\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"only fix state field\",\n\t\t\tinput: match.IgnoreRule{\n\t\t\t\tFixState: string(vulnerability.FixStateNotFixed),\n\t\t\t},\n\t\t\texpected: IgnoreRule{\n\t\t\t\tFixState: string(vulnerability.FixStateNotFixed),\n\t\t\t},\n\t\t},\n\n\t\t{\n\t\t\tname: \"all package fields\",\n\t\t\tinput: match.IgnoreRule{\n\t\t\t\tPackage: match.IgnoreRulePackage{\n\t\t\t\t\tName:     \"libc\",\n\t\t\t\t\tVersion:  \"3.0.0\",\n\t\t\t\t\tType:     \"rpm\",\n\t\t\t\t\tLocation: \"/some/location\",\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: IgnoreRule{\n\t\t\t\tPackage: &IgnoreRulePackage{\n\t\t\t\t\tName:     \"libc\",\n\t\t\t\t\tVersion:  \"3.0.0\",\n\t\t\t\t\tType:     \"rpm\",\n\t\t\t\t\tLocation: \"/some/location\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"only one package field\",\n\t\t\tinput: match.IgnoreRule{\n\t\t\t\tPackage: match.IgnoreRulePackage{\n\t\t\t\t\tType: \"apk\",\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: IgnoreRule{\n\t\t\t\tPackage: &IgnoreRulePackage{\n\t\t\t\t\tType: \"apk\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"all fields\",\n\t\t\tinput: match.IgnoreRule{\n\t\t\t\tVulnerability: \"CVE-2020-1234\",\n\t\t\t\tFixState:      string(vulnerability.FixStateNotFixed),\n\t\t\t\tPackage: match.IgnoreRulePackage{\n\t\t\t\t\tName:     \"libc\",\n\t\t\t\t\tVersion:  \"3.0.0\",\n\t\t\t\t\tType:     \"rpm\",\n\t\t\t\t\tLocation: \"/some/location\",\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: IgnoreRule{\n\t\t\t\tVulnerability: \"CVE-2020-1234\",\n\t\t\t\tFixState:      \"not-fixed\",\n\t\t\t\tPackage: &IgnoreRulePackage{\n\t\t\t\t\tName:     \"libc\",\n\t\t\t\t\tVersion:  \"3.0.0\",\n\t\t\t\t\tType:     \"rpm\",\n\t\t\t\t\tLocation: \"/some/location\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, testCase := range cases {\n\t\tt.Run(testCase.name, func(t *testing.T) {\n\t\t\tactual := newIgnoreRule(testCase.input)\n\t\t\tif diff := cmp.Diff(testCase.expected, actual); diff != \"\" {\n\t\t\t\tt.Errorf(\"(-expected +actual):\\n%s\", diff)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/presenter/models/match.go",
    "content": "package models\n\nimport (\n\t\"fmt\"\n\t\"sort\"\n\n\t\"github.com/anchore/grype/grype/match\"\n\t\"github.com/anchore/grype/grype/pkg\"\n\t\"github.com/anchore/grype/grype/version\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n\t\"github.com/anchore/grype/internal/log\"\n)\n\n// Match is a single item for the JSON array reported\ntype Match struct {\n\tVulnerability          Vulnerability           `json:\"vulnerability\"`\n\tRelatedVulnerabilities []VulnerabilityMetadata `json:\"relatedVulnerabilities\"`\n\tMatchDetails           []MatchDetails          `json:\"matchDetails\"`\n\tArtifact               Package                 `json:\"artifact\"`\n}\n\n// MatchDetails contains all data that indicates how the result match was found\ntype MatchDetails struct {\n\tType       string      `json:\"type\"`\n\tMatcher    string      `json:\"matcher\"`\n\tSearchedBy interface{} `json:\"searchedBy\"` // The specific attributes that were used to search (other than package name and version) --this indicates \"how\" the match was made.\n\tFound      interface{} `json:\"found\"`      // The specific attributes on the vulnerability object that were matched with --this indicates \"what\" was matched on / within.\n\tFix        *FixDetails `json:\"fix,omitempty\"`\n}\n\n// FixDetails contains any data that is relevant to fixing the vulnerability specific to the package searched with\ntype FixDetails struct {\n\tSuggestedVersion string `json:\"suggestedVersion\"`\n}\n\n//nolint:staticcheck // MetadataProvider is deprecated but still used internally\nfunc newMatch(m match.Match, p pkg.Package, metadataProvider vulnerability.MetadataProvider) (*Match, error) {\n\trelatedVulnerabilities := make([]VulnerabilityMetadata, 0)\n\tfor _, r := range m.Vulnerability.RelatedVulnerabilities {\n\t\trelatedMetadata, err := metadataProvider.VulnerabilityMetadata(r) //nolint:staticcheck // deprecated API still used internally\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"unable to fetch related vuln=%q metadata: %+v\", r, err)\n\t\t}\n\t\tif relatedMetadata != nil {\n\t\t\trelatedVulnerabilities = append(relatedVulnerabilities, NewVulnerabilityMetadata(r.ID, r.Namespace, relatedMetadata))\n\t\t}\n\t}\n\n\t// vulnerability.Vulnerability should always have vulnerability.Metadata populated, however, in the case of test mocks\n\t// and other edge cases, it may not be populated. In these cases, we should fetch the metadata from the provider.\n\tmetadata := m.Vulnerability.Metadata\n\tif metadata == nil {\n\t\tvar err error\n\t\tmetadata, err = metadataProvider.VulnerabilityMetadata(m.Vulnerability.Reference) //nolint:staticcheck // deprecated API still used internally\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"unable to fetch related vuln=%q metadata: %+v\", m.Vulnerability.Reference, err)\n\t\t}\n\t}\n\n\tformat := pkg.VersionFormat(p)\n\n\tdetails := make([]MatchDetails, len(m.Details))\n\tfor idx, d := range m.Details {\n\t\tdetails[idx] = MatchDetails{\n\t\t\tType:       string(d.Type),\n\t\t\tMatcher:    string(d.Matcher),\n\t\t\tSearchedBy: d.SearchedBy,\n\t\t\tFound:      d.Found,\n\t\t\tFix:        getFix(m, p, format),\n\t\t}\n\t}\n\n\treturn &Match{\n\t\tVulnerability:          NewVulnerability(m.Vulnerability, metadata, format),\n\t\tArtifact:               newPackage(p),\n\t\tRelatedVulnerabilities: relatedVulnerabilities,\n\t\tMatchDetails:           details,\n\t}, nil\n}\n\nfunc getFix(m match.Match, p pkg.Package, format version.Format) *FixDetails {\n\tsuggested := calculateSuggestedFixedVersion(p, m.Vulnerability.Fix.Versions, format)\n\tif suggested == \"\" {\n\t\treturn nil\n\t}\n\treturn &FixDetails{\n\t\tSuggestedVersion: suggested,\n\t}\n}\n\nfunc calculateSuggestedFixedVersion(p pkg.Package, fixedVersions []string, format version.Format) string {\n\tif len(fixedVersions) == 0 {\n\t\treturn \"\"\n\t}\n\n\tif len(fixedVersions) == 1 {\n\t\treturn fixedVersions[0]\n\t}\n\n\tparseConstraint := func(constStr string) (version.Constraint, error) {\n\t\tconstraint, err := version.GetConstraint(constStr, format)\n\t\tif err != nil {\n\t\t\tlog.WithFields(\"package\", p.Name).Trace(\"skipping sorting fixed versions\")\n\t\t}\n\t\treturn constraint, err\n\t}\n\n\tcheckSatisfaction := func(constraint version.Constraint, v *version.Version) bool {\n\t\tsatisfied, err := constraint.Satisfied(v)\n\t\tif err != nil {\n\t\t\tlog.WithFields(\"package\", p.Name).Trace(\"error while checking version satisfaction for sorting\")\n\t\t}\n\t\treturn satisfied && err == nil\n\t}\n\n\tsort.SliceStable(fixedVersions, func(i, j int) bool {\n\t\tv1 := version.New(fixedVersions[i], format)\n\t\tv2 := version.New(fixedVersions[j], format)\n\t\terr1 := v1.Validate()\n\t\terr2 := v2.Validate()\n\t\tif err1 != nil || err2 != nil {\n\t\t\tlog.WithFields(\"package\", p.Name).Trace(\"error while parsing version for sorting\")\n\t\t\treturn false\n\t\t}\n\n\t\tpackageConstraint, err := parseConstraint(fmt.Sprintf(\"<=%s\", p.Version))\n\t\tif err != nil {\n\t\t\treturn false\n\t\t}\n\n\t\tv1Satisfied := checkSatisfaction(packageConstraint, v1)\n\t\tv2Satisfied := checkSatisfaction(packageConstraint, v2)\n\n\t\tif v1Satisfied != v2Satisfied {\n\t\t\treturn !v1Satisfied\n\t\t}\n\n\t\tinternalConstraint, err := parseConstraint(fmt.Sprintf(\"<=%s\", v1.Raw))\n\t\tif err != nil {\n\t\t\treturn false\n\t\t}\n\t\treturn !checkSatisfaction(internalConstraint, v2)\n\t})\n\n\treturn fixedVersions[0]\n}\n"
  },
  {
    "path": "grype/presenter/models/metadata_mock.go",
    "content": "package models\n\nimport (\n\t\"github.com/anchore/grype/grype/vulnerability\"\n)\n\n//nolint:staticcheck // MetadataProvider is deprecated but still used internally for testing\nvar _ vulnerability.MetadataProvider = (*MetadataMock)(nil)\n\n// MetadataMock provides the behavior required for a vulnerability.Provider for the purpose of testing.\ntype MetadataMock struct {\n\tstore map[string]map[string]vulnerability.Metadata\n}\n\ntype MockVendorMetadata struct {\n\tBaseSeverity string\n\tStatus       string\n}\n\n// NewMetadataMock returns a new instance of MetadataMock.\nfunc NewMetadataMock() *MetadataMock {\n\treturn &MetadataMock{\n\t\tstore: map[string]map[string]vulnerability.Metadata{\n\t\t\t\"CVE-1999-0001\": {\n\t\t\t\t\"source-1\": {\n\t\t\t\t\tDescription: \"1999-01 description\",\n\t\t\t\t\tSeverity:    \"Low\",\n\t\t\t\t\tCvss: []vulnerability.Cvss{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tMetrics: vulnerability.CvssMetrics{\n\t\t\t\t\t\t\t\tBaseScore: 4,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tVector:  \"another vector\",\n\t\t\t\t\t\t\tVersion: \"3.0\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\t\"CVE-1999-0002\": {\n\t\t\t\t\"source-2\": {\n\t\t\t\t\tDescription: \"1999-02 description\",\n\t\t\t\t\tSeverity:    \"Critical\",\n\t\t\t\t\tCvss: []vulnerability.Cvss{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tMetrics: vulnerability.CvssMetrics{\n\t\t\t\t\t\t\t\tBaseScore:           1,\n\t\t\t\t\t\t\t\tExploitabilityScore: ptr(2.0),\n\t\t\t\t\t\t\t\tImpactScore:         ptr(3.0),\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tVector:  \"vector\",\n\t\t\t\t\t\t\tVersion: \"2.0\",\n\t\t\t\t\t\t\tVendorMetadata: MockVendorMetadata{\n\t\t\t\t\t\t\t\tBaseSeverity: \"Low\",\n\t\t\t\t\t\t\t\tStatus:       \"verified\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\t\"CVE-1999-0003\": {\n\t\t\t\t\"source-1\": {\n\t\t\t\t\tDescription: \"1999-03 description\",\n\t\t\t\t\tSeverity:    \"High\",\n\t\t\t\t},\n\t\t\t},\n\t\t\t\"CVE-1999-0004\": {\n\t\t\t\t\"source-2\": {\n\t\t\t\t\tDescription: \"1999-04 description\",\n\t\t\t\t\tSeverity:    \"Critical\",\n\t\t\t\t\tCvss: []vulnerability.Cvss{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tMetrics: vulnerability.CvssMetrics{\n\t\t\t\t\t\t\t\tBaseScore:           1,\n\t\t\t\t\t\t\t\tExploitabilityScore: ptr(2.0),\n\t\t\t\t\t\t\t\tImpactScore:         ptr(3.0),\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tVector:  \"vector\",\n\t\t\t\t\t\t\tVersion: \"2.0\",\n\t\t\t\t\t\t\tVendorMetadata: MockVendorMetadata{\n\t\t\t\t\t\t\t\tBaseSeverity: \"Low\",\n\t\t\t\t\t\t\t\tStatus:       \"verified\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n}\n\nfunc ptr[T any](t T) *T {\n\treturn &t\n}\n\n// VulnerabilityMetadata returns vulnerability metadata for a given id and recordSource.\nfunc (m *MetadataMock) VulnerabilityMetadata(vuln vulnerability.Reference) (*vulnerability.Metadata, error) {\n\tvalue := m.store[vuln.ID][vuln.Namespace]\n\tvalue.ID = vuln.ID\n\tvalue.Namespace = vuln.Namespace\n\treturn &value, nil\n}\n"
  },
  {
    "path": "grype/presenter/models/package.go",
    "content": "package models\n\nimport (\n\t\"github.com/anchore/grype/grype/internal/packagemetadata\"\n\t\"github.com/anchore/grype/grype/pkg\"\n\t\"github.com/anchore/syft/syft/file\"\n\tsyftPkg \"github.com/anchore/syft/syft/pkg\"\n)\n\n// Package is meant to be only the fields that are needed when displaying a single pkg.Package object for the JSON presenter.\ntype Package struct {\n\tID           string            `json:\"id\"`\n\tName         string            `json:\"name\"`\n\tVersion      string            `json:\"version\"`\n\tType         syftPkg.Type      `json:\"type\"`\n\tLocations    file.Locations    `json:\"locations\"`\n\tLanguage     syftPkg.Language  `json:\"language\"`\n\tLicenses     []string          `json:\"licenses\"`\n\tCPEs         []string          `json:\"cpes\"`\n\tPURL         string            `json:\"purl\"`\n\tUpstreams    []UpstreamPackage `json:\"upstreams\"`\n\tMetadataType string            `json:\"metadataType,omitempty\"`\n\tMetadata     interface{}       `json:\"metadata,omitempty\"`\n}\n\ntype UpstreamPackage struct {\n\tName    string `json:\"name\"`\n\tVersion string `json:\"version,omitempty\"`\n}\n\nfunc newPackage(p pkg.Package) Package {\n\tvar cpes = make([]string, 0)\n\tfor _, c := range p.CPEs {\n\t\t// use .String() to ensure proper escaping\n\t\tcpes = append(cpes, c.Attributes.String())\n\t}\n\n\tlicenses := p.Licenses\n\tif licenses == nil {\n\t\tlicenses = make([]string, 0)\n\t}\n\n\tvar upstreams = make([]UpstreamPackage, 0)\n\tfor _, u := range p.Upstreams {\n\t\tupstreams = append(upstreams, UpstreamPackage{\n\t\t\tName:    u.Name,\n\t\t\tVersion: u.Version,\n\t\t})\n\t}\n\n\treturn Package{\n\t\tID:           string(p.ID),\n\t\tName:         p.Name,\n\t\tVersion:      p.Version,\n\t\tLocations:    p.Locations.ToSlice(),\n\t\tLicenses:     licenses,\n\t\tLanguage:     p.Language,\n\t\tType:         p.Type,\n\t\tCPEs:         cpes,\n\t\tPURL:         p.PURL,\n\t\tUpstreams:    upstreams,\n\t\tMetadataType: packagemetadata.JSONName(p.Metadata),\n\t\tMetadata:     p.Metadata,\n\t}\n}\n"
  },
  {
    "path": "grype/presenter/models/presenter_bundle.go",
    "content": "package models\n\nimport (\n\t\"github.com/anchore/clio\"\n\t\"github.com/anchore/syft/syft/sbom\"\n)\n\ntype PresenterConfig struct {\n\tID       clio.Identification\n\tDocument Document\n\tSBOM     *sbom.SBOM\n\tPretty   bool\n}\n"
  },
  {
    "path": "grype/presenter/models/sort.go",
    "content": "package models\n\nimport (\n\t\"sort\"\n\t\"strings\"\n\n\t\"github.com/anchore/grype/internal/log\"\n)\n\ntype SortStrategy string\n\nconst (\n\tSortByPackage       SortStrategy = \"package\"\n\tSortBySeverity      SortStrategy = \"severity\"\n\tSortByThreat        SortStrategy = \"epss\"\n\tSortByRisk          SortStrategy = \"risk\"\n\tSortByKEV           SortStrategy = \"kev\"\n\tSortByVulnerability SortStrategy = \"vulnerability\"\n\n\tDefaultSortStrategy = SortByRisk\n)\n\nfunc SortStrategies() []SortStrategy {\n\treturn []SortStrategy{SortByPackage, SortBySeverity, SortByThreat, SortByRisk, SortByKEV, SortByVulnerability}\n}\n\nfunc (s SortStrategy) String() string {\n\treturn string(s)\n}\n\n// compareFunc defines a comparison function between two Match values\n// Returns:\n//\n//\t-1: if a should come before b\n//\t 0: if a and b are equal for this comparison\n//\t 1: if a should come after b\ntype compareFunc func(a, b Match) int\n\n// sortStrategyImpl defines a strategy for sorting with a slice of comparison functions\ntype sortStrategyImpl []compareFunc\n\n// matchSortStrategy provides predefined sort strategies for Match\nvar matchSortStrategy = map[SortStrategy]sortStrategyImpl{\n\tSortByPackage: {\n\t\tcomparePackageAttributes,\n\t\tcompareVulnerabilityAttributes,\n\t},\n\tSortByVulnerability: {\n\t\tcompareVulnerabilityAttributes,\n\t\tcomparePackageAttributes,\n\t},\n\tSortBySeverity: {\n\t\t// severity and tangential attributes...\n\t\tcompareBySeverity,\n\t\tcompareByRisk,\n\t\tcompareByEPSSPercentile,\n\t\t// followed by package attributes...\n\t\tcomparePackageAttributes,\n\t\t// followed by the remaining vulnerability attributes...\n\t\tcompareByVulnerabilityID,\n\t},\n\tSortByThreat: {\n\t\t// epss and tangential attributes...\n\t\tcompareByEPSSPercentile,\n\t\tcompareByRisk,\n\t\tcompareBySeverity,\n\t\t// followed by package attributes...\n\t\tcomparePackageAttributes,\n\t\t// followed by the remaining vulnerability attributes...\n\t\tcompareByVulnerabilityID,\n\t},\n\tSortByRisk: {\n\t\t// risk and tangential attributes...\n\t\tcompareByRisk,\n\t\tcompareBySeverity,\n\t\tcompareByEPSSPercentile,\n\t\t// followed by package attributes...\n\t\tcomparePackageAttributes,\n\t\t// followed by the remaining vulnerability attributes...\n\t\tcompareByVulnerabilityID,\n\t},\n\tSortByKEV: {\n\t\tcompareByKEV,\n\t\t// risk and tangential attributes...\n\t\tcompareByRisk,\n\t\tcompareBySeverity,\n\t\tcompareByEPSSPercentile,\n\t\t// followed by package attributes...\n\t\tcomparePackageAttributes,\n\t\t// followed by the remaining vulnerability attributes...\n\t\tcompareByVulnerabilityID,\n\t},\n}\n\nfunc compareVulnerabilityAttributes(a, b Match) int {\n\treturn combine(\n\t\tcompareByVulnerabilityID,\n\t\tcompareByRisk,\n\t\tcompareBySeverity,\n\t\tcompareByEPSSPercentile,\n\t)(a, b)\n}\n\nfunc comparePackageAttributes(a, b Match) int {\n\treturn combine(\n\t\tcompareByPackageName,\n\t\tcompareByPackageVersion,\n\t\tcompareByPackageType,\n\t)(a, b)\n}\n\nfunc combine(impls ...compareFunc) compareFunc {\n\treturn func(a, b Match) int {\n\t\tfor _, impl := range impls {\n\t\t\tresult := impl(a, b)\n\t\t\tif result != 0 {\n\t\t\t\treturn result\n\t\t\t}\n\t\t}\n\t\treturn 0\n\t}\n}\n\n// SortMatches sorts matches based on a strategy name\nfunc SortMatches(matches []Match, strategyName SortStrategy) {\n\tsortWithStrategy(matches, getSortStrategy(strategyName))\n}\n\nfunc getSortStrategy(strategyName SortStrategy) sortStrategyImpl {\n\tstrategy, exists := matchSortStrategy[strategyName]\n\tif !exists {\n\t\tlog.WithFields(\"strategy\", strategyName).Debugf(\"unknown sort strategy, falling back to default of %q\", DefaultSortStrategy)\n\t\tstrategy = matchSortStrategy[DefaultSortStrategy]\n\t}\n\treturn strategy\n}\n\nfunc sortWithStrategy(matches []Match, strategy sortStrategyImpl) {\n\tsort.Slice(matches, func(i, j int) bool {\n\t\tfor _, compare := range strategy {\n\t\t\tresult := compare(matches[i], matches[j])\n\t\t\tif result != 0 {\n\t\t\t\t// we are implementing a \"less\" function, so we want to return true if the result is negative\n\t\t\t\treturn result < 0\n\t\t\t}\n\t\t}\n\t\treturn false // all comparisons are equal\n\t})\n}\n\nfunc compareByVulnerabilityID(a, b Match) int {\n\taID := a.Vulnerability.ID\n\tbID := b.Vulnerability.ID\n\n\tswitch {\n\tcase aID < bID:\n\t\treturn -1\n\tcase aID > bID:\n\t\treturn 1\n\tdefault:\n\t\treturn 0\n\t}\n}\n\nfunc compareBySeverity(a, b Match) int {\n\taScore := severityPriority(a.Vulnerability.Severity)\n\tbScore := severityPriority(b.Vulnerability.Severity)\n\n\tswitch {\n\tcase aScore < bScore: // higher severity first\n\t\treturn -1\n\tcase aScore > bScore:\n\t\treturn 1\n\tdefault:\n\t\treturn 0\n\t}\n}\n\nfunc compareByEPSSPercentile(a, b Match) int {\n\taScore := epssPercentile(a.Vulnerability.EPSS)\n\tbScore := epssPercentile(b.Vulnerability.EPSS)\n\n\tswitch {\n\tcase aScore > bScore: // higher severity first\n\t\treturn -1\n\tcase aScore < bScore:\n\t\treturn 1\n\tdefault:\n\t\treturn 0\n\t}\n}\n\nfunc compareByPackageName(a, b Match) int {\n\taName := a.Artifact.Name\n\tbName := b.Artifact.Name\n\n\tswitch {\n\tcase aName < bName:\n\t\treturn -1\n\tcase aName > bName:\n\t\treturn 1\n\tdefault:\n\t\treturn 0\n\t}\n}\n\nfunc compareByPackageVersion(a, b Match) int {\n\taVersion := a.Artifact.Version\n\tbVersion := b.Artifact.Version\n\n\tswitch {\n\tcase aVersion < bVersion:\n\t\treturn -1\n\tcase aVersion > bVersion:\n\t\treturn 1\n\tdefault:\n\t\treturn 0\n\t}\n}\n\nfunc compareByPackageType(a, b Match) int {\n\taType := a.Artifact.Type\n\tbType := b.Artifact.Type\n\n\tswitch {\n\tcase aType < bType:\n\t\treturn -1\n\tcase aType > bType:\n\t\treturn 1\n\tdefault:\n\t\treturn 0\n\t}\n}\n\nfunc compareByRisk(a, b Match) int {\n\taRisk := a.Vulnerability.Risk\n\tbRisk := b.Vulnerability.Risk\n\n\tswitch {\n\tcase aRisk > bRisk:\n\t\treturn -1\n\tcase aRisk < bRisk:\n\t\treturn 1\n\tdefault:\n\t\treturn 0\n\t}\n}\n\nfunc compareByKEV(a, b Match) int {\n\taKEV := len(a.Vulnerability.KnownExploited)\n\tbKEV := len(b.Vulnerability.KnownExploited)\n\n\tswitch {\n\tcase aKEV > bKEV:\n\t\treturn -1\n\tcase aKEV < bKEV:\n\t\treturn 1\n\tdefault:\n\t\treturn 0\n\t}\n}\n\nfunc epssPercentile(es []EPSS) float64 {\n\tswitch len(es) {\n\tcase 0:\n\t\treturn 0.0\n\tcase 1:\n\t\treturn es[0].Percentile\n\t}\n\tsort.Slice(es, func(i, j int) bool {\n\t\treturn es[i].Percentile > es[j].Percentile\n\t})\n\treturn es[0].Percentile\n}\n\n// severityPriority maps severity strings to numeric priority for comparison (the lowest value is most severe)\nfunc severityPriority(severity string) int {\n\tswitch strings.ToLower(severity) {\n\tcase \"critical\":\n\t\treturn 1\n\tcase \"high\":\n\t\treturn 2\n\tcase \"medium\":\n\t\treturn 3\n\tcase \"low\":\n\t\treturn 4\n\tcase \"negligible\":\n\t\treturn 5\n\tdefault:\n\t\treturn 100 // least severe\n\t}\n}\n"
  },
  {
    "path": "grype/presenter/models/sort_test.go",
    "content": "package models\n\nimport (\n\t\"testing\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestSortStrategies(t *testing.T) {\n\tstrategies := SortStrategies()\n\texpected := []SortStrategy{\n\t\tSortByPackage,\n\t\tSortBySeverity,\n\t\tSortByThreat,\n\t\tSortByRisk,\n\t\tSortByKEV,\n\t\tSortByVulnerability,\n\t}\n\tassert.Equal(t, expected, strategies)\n}\n\nfunc TestSortStrategyString(t *testing.T) {\n\tassert.Equal(t, \"package\", SortByPackage.String())\n\tassert.Equal(t, \"severity\", SortBySeverity.String())\n\tassert.Equal(t, \"epss\", SortByThreat.String())\n\tassert.Equal(t, \"risk\", SortByRisk.String())\n\tassert.Equal(t, \"kev\", SortByKEV.String())\n\tassert.Equal(t, \"vulnerability\", SortByVulnerability.String())\n}\n\nfunc TestGetSortStrategy(t *testing.T) {\n\ttests := []struct {\n\t\tname         string\n\t\tstrategyName SortStrategy\n\t\texpected     bool\n\t}{\n\t\t{\n\t\t\tname:         \"Valid strategy\",\n\t\t\tstrategyName: SortByPackage,\n\t\t\texpected:     true,\n\t\t},\n\t\t{\n\t\t\tname:         \"Invalid strategy\",\n\t\t\tstrategyName: \"invalid\",\n\t\t\texpected:     false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tstrategy := getSortStrategy(tt.strategyName)\n\t\t\tvalidStrategy, _ := matchSortStrategy[tt.strategyName]\n\n\t\t\tif tt.expected {\n\t\t\t\trequire.NotNil(t, strategy)\n\t\t\t\tassert.Equal(t, validStrategy, strategy)\n\t\t\t} else {\n\t\t\t\t// Should fallback to default strategy\n\t\t\t\tassert.NotNil(t, strategy)\n\t\t\t\tassert.Equal(t, matchSortStrategy[DefaultSortStrategy], strategy)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestEPSSPercentile(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tepss     []EPSS\n\t\texpected float64\n\t}{\n\t\t{\n\t\t\tname:     \"Empty slice\",\n\t\t\tepss:     []EPSS{},\n\t\t\texpected: 0.0,\n\t\t},\n\t\t{\n\t\t\tname: \"Single item\",\n\t\t\tepss: []EPSS{\n\t\t\t\t{Percentile: 0.75},\n\t\t\t},\n\t\t\texpected: 0.75,\n\t\t},\n\t\t{\n\t\t\tname: \"Multiple items, already sorted\",\n\t\t\tepss: []EPSS{\n\t\t\t\t{Percentile: 0.95},\n\t\t\t\t{Percentile: 0.75},\n\t\t\t\t{Percentile: 0.50},\n\t\t\t},\n\t\t\texpected: 0.95,\n\t\t},\n\t\t{\n\t\t\tname: \"Multiple items, unsorted\",\n\t\t\tepss: []EPSS{\n\t\t\t\t{Percentile: 0.50},\n\t\t\t\t{Percentile: 0.95},\n\t\t\t\t{Percentile: 0.75},\n\t\t\t},\n\t\t\texpected: 0.95,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := epssPercentile(tt.epss)\n\t\t\tassert.Equal(t, tt.expected, result)\n\t\t})\n\t}\n}\n\nfunc TestSeverityPriority(t *testing.T) {\n\ttests := []struct {\n\t\tseverity string\n\t\texpected int\n\t}{\n\t\t{\"critical\", 1},\n\t\t{\"CRITICAL\", 1},\n\t\t{\"high\", 2},\n\t\t{\"HIGH\", 2},\n\t\t{\"medium\", 3},\n\t\t{\"MEDIUM\", 3},\n\t\t{\"low\", 4},\n\t\t{\"LOW\", 4},\n\t\t{\"negligible\", 5},\n\t\t{\"NEGLIGIBLE\", 5},\n\t\t{\"unknown\", 100},\n\t\t{\"\", 100},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.severity, func(t *testing.T) {\n\t\t\tresult := severityPriority(tt.severity)\n\t\t\tassert.Equal(t, tt.expected, result)\n\t\t})\n\t}\n}\n\nfunc createTestMatches() []Match {\n\treturn []Match{\n\t\t{\n\t\t\t// match 0: medium severity, high risk, high EPSS, no KEV\n\t\t\tVulnerability: Vulnerability{\n\t\t\t\tVulnerabilityMetadata: VulnerabilityMetadata{\n\t\t\t\t\tID:       \"CVE-2023-1111\",\n\t\t\t\t\tSeverity: \"medium\",\n\t\t\t\t\tEPSS: []EPSS{\n\t\t\t\t\t\t{Percentile: 0.90},\n\t\t\t\t\t},\n\t\t\t\t\tKnownExploited: []KnownExploited{}, // empty KEV\n\t\t\t\t},\n\t\t\t\tRisk: 75.0,\n\t\t\t},\n\t\t\tArtifact: Package{\n\t\t\t\tName:    \"package-b\",\n\t\t\t\tVersion: \"1.2.0\",\n\t\t\t\tType:    \"npm\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t// match 1: critical severity, medium risk, medium EPSS, no KEV\n\t\t\tVulnerability: Vulnerability{\n\t\t\t\tVulnerabilityMetadata: VulnerabilityMetadata{\n\t\t\t\t\tID:       \"CVE-2023-2222\",\n\t\t\t\t\tSeverity: \"critical\",\n\t\t\t\t\tEPSS: []EPSS{\n\t\t\t\t\t\t{Percentile: 0.70},\n\t\t\t\t\t},\n\t\t\t\t\tKnownExploited: []KnownExploited{}, // empty KEV\n\t\t\t\t},\n\t\t\t\tRisk: 50.0,\n\t\t\t},\n\t\t\tArtifact: Package{\n\t\t\t\tName:    \"package-a\",\n\t\t\t\tVersion: \"2.0.0\",\n\t\t\t\tType:    \"docker\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t// match 2: high severity, low risk, low EPSS, has KEV\n\t\t\tVulnerability: Vulnerability{\n\t\t\t\tVulnerabilityMetadata: VulnerabilityMetadata{\n\t\t\t\t\tID:       \"CVE-2023-3333\",\n\t\t\t\t\tSeverity: \"high\",\n\t\t\t\t\tEPSS: []EPSS{\n\t\t\t\t\t\t{Percentile: 0.30},\n\t\t\t\t\t},\n\t\t\t\t\tKnownExploited: []KnownExploited{\n\t\t\t\t\t\t{CVE: \"CVE-2023-3333\", KnownRansomwareCampaignUse: \"No\"},\n\t\t\t\t\t}, // has KEV\n\t\t\t\t},\n\t\t\t\tRisk: 25.0,\n\t\t\t},\n\t\t\tArtifact: Package{\n\t\t\t\tName:    \"package-a\",\n\t\t\t\tVersion: \"1.0.0\",\n\t\t\t\tType:    \"npm\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t// match 3: low severity, very low risk, very low EPSS, no KEV\n\t\t\tVulnerability: Vulnerability{\n\t\t\t\tVulnerabilityMetadata: VulnerabilityMetadata{\n\t\t\t\t\tID:       \"CVE-2023-4444\",\n\t\t\t\t\tSeverity: \"low\",\n\t\t\t\t\tEPSS: []EPSS{\n\t\t\t\t\t\t{Percentile: 0.10},\n\t\t\t\t\t},\n\t\t\t\t\tKnownExploited: []KnownExploited{}, // empty KEV\n\t\t\t\t},\n\t\t\t\tRisk: 10.0,\n\t\t\t},\n\t\t\tArtifact: Package{\n\t\t\t\tName:    \"package-c\",\n\t\t\t\tVersion: \"3.1.0\",\n\t\t\t\tType:    \"gem\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t// match 4: critical severity, very low risk, medium EPSS, has KEV with ransomware\n\t\t\tVulnerability: Vulnerability{\n\t\t\t\tVulnerabilityMetadata: VulnerabilityMetadata{\n\t\t\t\t\tID:       \"CVE-2023-5555\",\n\t\t\t\t\tSeverity: \"critical\",\n\t\t\t\t\tEPSS: []EPSS{\n\t\t\t\t\t\t{Percentile: 0.50},\n\t\t\t\t\t},\n\t\t\t\t\tKnownExploited: []KnownExploited{\n\t\t\t\t\t\t{CVE: \"CVE-2023-5555\", KnownRansomwareCampaignUse: \"Known\"},\n\t\t\t\t\t\t{CVE: \"CVE-2023-5555\", KnownRansomwareCampaignUse: \"Known\", Product: \"Different Product\"},\n\t\t\t\t\t}, // has multiple KEV entries with ransomware\n\t\t\t\t},\n\t\t\t\tRisk: 5.0,\n\t\t\t},\n\t\t\tArtifact: Package{\n\t\t\t\tName:    \"package-a\",\n\t\t\t\tVersion: \"1.0.0\",\n\t\t\t\tType:    \"docker\",\n\t\t\t},\n\t\t},\n\t}\n}\n\nfunc TestAllSortStrategies(t *testing.T) {\n\tmatches := createTestMatches()\n\n\ttests := []struct {\n\t\tstrategy SortStrategy\n\t\texpected []int // indexes into the original matches slice\n\t}{\n\t\t{\n\t\t\tstrategy: SortByPackage,\n\t\t\texpected: []int{4, 2, 1, 0, 3}, // sorted by package name, version, type\n\t\t},\n\t\t{\n\t\t\tstrategy: SortByVulnerability,\n\t\t\texpected: []int{0, 1, 2, 3, 4}, // sorted by vulnerability ID\n\t\t},\n\t\t{\n\t\t\tstrategy: SortBySeverity,\n\t\t\texpected: []int{1, 4, 2, 0, 3}, // sorted by severity: critical, critical, high, medium, low\n\t\t},\n\t\t{\n\t\t\tstrategy: SortByThreat,\n\t\t\texpected: []int{0, 1, 4, 2, 3}, // sorted by EPSS percentile: 0.90, 0.70, 0.50, 0.30, 0.10\n\t\t},\n\t\t{\n\t\t\tstrategy: SortByRisk,\n\t\t\texpected: []int{0, 1, 2, 3, 4}, // sorted by risk: 75.0, 50.0, 25.0, 10.0, 5.0\n\t\t},\n\t\t{\n\t\t\tstrategy: SortByKEV,\n\t\t\texpected: []int{4, 2, 0, 1, 3}, // sorted by KEV count: 2, 1, 0, 0, 0 (with ties broken by risk)\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(string(tt.strategy), func(t *testing.T) {\n\t\t\ttestMatches := deepCopyMatches(matches)\n\t\t\tSortMatches(testMatches, tt.strategy)\n\n\t\t\texpected := make([]Match, len(tt.expected))\n\t\t\tfor i, idx := range tt.expected {\n\t\t\t\texpected[i] = matches[idx]\n\t\t\t}\n\n\t\t\tif diff := cmp.Diff(expected, testMatches); diff != \"\" {\n\t\t\t\tt.Errorf(\"%s mismatch (-want +got):\\n%s\", tt.strategy, diff)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestIndividualCompareFunctions(t *testing.T) {\n\tms := createTestMatches()\n\tm0 := ms[0] // medium severity, high risk, high EPSS, no KEV\n\tm1 := ms[1] // critical severity, medium risk, medium EPSS, no KEV\n\tm2 := ms[2] // high severity, low risk, low EPSS, has KEV\n\tm3 := ms[3] // low severity, very low risk, very low EPSS, no KEV\n\tm4 := ms[4] // critical severity, very low risk, medium EPSS, has KEV with ransomware\n\n\ttests := []struct {\n\t\tname        string\n\t\tcompareFunc compareFunc\n\t\tpairs       []struct {\n\t\t\ta, b     Match\n\t\t\texpected int\n\t\t}\n\t}{\n\t\t{\n\t\t\tname:        \"compareByVulnerabilityID\",\n\t\t\tcompareFunc: compareByVulnerabilityID,\n\t\t\tpairs: []struct {\n\t\t\t\ta, b     Match\n\t\t\t\texpected int\n\t\t\t}{\n\t\t\t\t{m0, m1, -1}, // CVE-2023-1111 < CVE-2023-2222\n\t\t\t\t{m1, m0, 1},  // CVE-2023-2222 > CVE-2023-1111\n\t\t\t\t{m0, m0, 0},  // Same ID\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:        \"compareBySeverity\",\n\t\t\tcompareFunc: compareBySeverity,\n\t\t\tpairs: []struct {\n\t\t\t\ta, b     Match\n\t\t\t\texpected int\n\t\t\t}{\n\t\t\t\t{m0, m1, 1},  // medium > critical\n\t\t\t\t{m1, m0, -1}, // critical < medium\n\t\t\t\t{m1, m4, 0},  // both critical\n\t\t\t\t{m2, m3, -1}, // high < low\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:        \"compareByEPSSPercentile\",\n\t\t\tcompareFunc: compareByEPSSPercentile,\n\t\t\tpairs: []struct {\n\t\t\t\ta, b     Match\n\t\t\t\texpected int\n\t\t\t}{\n\t\t\t\t{m0, m1, -1}, // 0.90 > 0.70\n\t\t\t\t{m1, m0, 1},  // 0.70 < 0.90\n\t\t\t\t{m1, m4, -1}, // 0.70 > 0.50\n\t\t\t\t{m4, m1, 1},  // 0.50 < 0.70\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:        \"compareByPackageName\",\n\t\t\tcompareFunc: compareByPackageName,\n\t\t\tpairs: []struct {\n\t\t\t\ta, b     Match\n\t\t\t\texpected int\n\t\t\t}{\n\t\t\t\t{m0, m1, 1},  // package-b > package-a\n\t\t\t\t{m1, m0, -1}, // package-a < package-b\n\t\t\t\t{m1, m2, 0},  // both package-a\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:        \"compareByPackageVersion\",\n\t\t\tcompareFunc: compareByPackageVersion,\n\t\t\tpairs: []struct {\n\t\t\t\ta, b     Match\n\t\t\t\texpected int\n\t\t\t}{\n\t\t\t\t{m1, m2, 1},  // 2.0.0 > 1.0.0\n\t\t\t\t{m2, m1, -1}, // 1.0.0 < 2.0.0\n\t\t\t\t{m2, m4, 0},  // both 1.0.0\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:        \"compareByPackageType\",\n\t\t\tcompareFunc: compareByPackageType,\n\t\t\tpairs: []struct {\n\t\t\t\ta, b     Match\n\t\t\t\texpected int\n\t\t\t}{\n\t\t\t\t{m0, m1, 1},  // npm > docker\n\t\t\t\t{m1, m0, -1}, // docker < npm\n\t\t\t\t{m0, m2, 0},  // both npm\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:        \"compareByRisk\",\n\t\t\tcompareFunc: compareByRisk,\n\t\t\tpairs: []struct {\n\t\t\t\ta, b     Match\n\t\t\t\texpected int\n\t\t\t}{\n\t\t\t\t{m0, m1, -1}, // 75.0 > 50.0\n\t\t\t\t{m1, m0, 1},  // 50.0 < 75.0\n\t\t\t\t{m3, m4, -1}, // 10.0 > 5.0\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:        \"compareByKEV\",\n\t\t\tcompareFunc: compareByKEV,\n\t\t\tpairs: []struct {\n\t\t\t\ta, b     Match\n\t\t\t\texpected int\n\t\t\t}{\n\t\t\t\t{m0, m2, 1},  // 0 < 1 KEV entry\n\t\t\t\t{m2, m0, -1}, // 1 > 0 KEV entry\n\t\t\t\t{m2, m4, 1},  // 1 < 2 KEV entries\n\t\t\t\t{m4, m2, -1}, // 2 > 1 KEV entry\n\t\t\t\t{m0, m1, 0},  // both 0 KEV entries\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tfor _, pair := range tt.pairs {\n\t\t\t\tresult := tt.compareFunc(pair.a, pair.b)\n\t\t\t\tassert.Equal(t, pair.expected, result, \"comparing %v and %v\", pair.a.Vulnerability.ID, pair.b.Vulnerability.ID)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestCombinedCompareFunctions(t *testing.T) {\n\tms := createTestMatches()\n\tm0 := ms[0] // medium severity, high risk, high EPSS, no KEV, package-b\n\tm1 := ms[1] // critical severity, medium risk, medium EPSS, no KEV, package-a\n\tm2 := ms[2] // high severity, low risk, low EPSS, has KEV, package-a\n\n\tt.Run(\"compareVulnerabilityAttributes\", func(t *testing.T) {\n\t\tresult := compareVulnerabilityAttributes(m0, m1)\n\t\tassert.Equal(t, -1, result, \"CVE-2023-1111 should come before CVE-2023-2222\")\n\n\t\tresult = compareVulnerabilityAttributes(m1, m0)\n\t\tassert.Equal(t, 1, result, \"CVE-2023-2222 should come after CVE-2023-1111\")\n\t})\n\n\tt.Run(\"comparePackageAttributes\", func(t *testing.T) {\n\t\tresult := comparePackageAttributes(m0, m1)\n\t\tassert.Equal(t, 1, result, \"package-b should come after package-a\")\n\n\t\tresult = comparePackageAttributes(m1, m2)\n\t\tassert.Equal(t, 1, result, \"package-a 2.0.0 should come after package-a 1.0.0\")\n\n\t\tresult = comparePackageAttributes(m1, m1)\n\t\tassert.Equal(t, 0, result, \"same package should be equal\")\n\t})\n\n\tt.Run(\"combine function\", func(t *testing.T) {\n\t\t// create a combined function that first compares by severity, then by risk if severity is equal\n\t\tcombined := combine(compareBySeverity, compareByRisk)\n\n\t\tresult := combined(m0, m1)\n\t\tassert.Equal(t, 1, result, \"medium should come after critical regardless of risk\")\n\n\t\t// create two matches with the same severity but different risk\n\t\tm5 := m1 // critical severity, risk 50.0\n\t\tm6 := m1\n\t\tm6.Vulnerability.Risk = 60.0 // critical severity, risk 60.0\n\n\t\tresult = combined(m5, m6)\n\t\tassert.Equal(t, 1, result, \"with equal severity, lower risk (50.0) should come after higher risk (60.0)\")\n\n\t\tresult = combined(m6, m5)\n\t\tassert.Equal(t, -1, result, \"with equal severity, higher risk (60.0) should come before lower risk (50.0)\")\n\t})\n}\n\nfunc TestSortWithStrategy(t *testing.T) {\n\tmatches := createTestMatches()\n\n\t// create a custom strategy that sorts only by vulnerability ID\n\tcustomStrategy := sortStrategyImpl{compareByVulnerabilityID}\n\n\texpected := []Match{\n\t\tmatches[0], // CVE-2023-1111\n\t\tmatches[1], // CVE-2023-2222\n\t\tmatches[2], // CVE-2023-3333\n\t\tmatches[3], // CVE-2023-4444\n\t\tmatches[4], // CVE-2023-5555\n\t}\n\n\ttestMatches := deepCopyMatches(matches)\n\tsortWithStrategy(testMatches, customStrategy)\n\n\tif diff := cmp.Diff(expected, testMatches); diff != \"\" {\n\t\tt.Errorf(\"sortWithStrategy mismatch (-want +got):\\n%s\", diff)\n\t}\n\n\t// create an empty strategy (should not change the order)\n\temptyStrategy := sortStrategyImpl{}\n\toriginalMatches := deepCopyMatches(matches)\n\tsortWithStrategy(originalMatches, emptyStrategy)\n\n\tif diff := cmp.Diff(matches, originalMatches); diff != \"\" {\n\t\tt.Errorf(\"Empty strategy should not change order (-original +after):\\n%s\", diff)\n\t}\n}\n\nfunc deepCopyMatches(matches []Match) []Match {\n\tresult := make([]Match, len(matches))\n\tcopy(result, matches)\n\treturn result\n}\n"
  },
  {
    "path": "grype/presenter/models/source.go",
    "content": "package models\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/anchore/grype/grype/pkg\"\n\tsyftSource \"github.com/anchore/syft/syft/source\"\n)\n\ntype source struct {\n\tType   string      `json:\"type\"`\n\tTarget interface{} `json:\"target\"`\n}\n\n// newSource creates a new source object to be represented into JSON.\nfunc newSource(src syftSource.Description) (source, error) {\n\tswitch m := src.Metadata.(type) {\n\tcase pkg.SBOMFileMetadata:\n\t\treturn source{\n\t\t\tType:   \"sbom-file\",\n\t\t\tTarget: m.Path,\n\t\t}, nil\n\tcase pkg.PURLLiteralMetadata:\n\t\treturn source{\n\t\t\tType:   \"purl\",\n\t\t\tTarget: m.PURL,\n\t\t}, nil\n\tcase pkg.CPELiteralMetadata:\n\t\treturn source{\n\t\t\tType:   \"cpe\",\n\t\t\tTarget: m.CPE,\n\t\t}, nil\n\tcase syftSource.ImageMetadata:\n\t\t// ensure that empty collections are not shown as null\n\t\tif m.RepoDigests == nil {\n\t\t\tm.RepoDigests = []string{}\n\t\t}\n\t\tif m.Tags == nil {\n\t\t\tm.Tags = []string{}\n\t\t}\n\n\t\treturn source{\n\t\t\tType:   \"image\",\n\t\t\tTarget: m,\n\t\t}, nil\n\tcase syftSource.OCIModelMetadata:\n\t\t// ensure that empty collections are not shown as null\n\t\tif m.RepoDigests == nil {\n\t\t\tm.RepoDigests = []string{}\n\t\t}\n\t\tif m.Tags == nil {\n\t\t\tm.Tags = []string{}\n\t\t}\n\n\t\treturn source{\n\t\t\tType:   \"oci-model\",\n\t\t\tTarget: m,\n\t\t}, nil\n\tcase syftSource.DirectoryMetadata:\n\t\treturn source{\n\t\t\tType:   \"directory\",\n\t\t\tTarget: m.Path,\n\t\t}, nil\n\tcase syftSource.FileMetadata:\n\t\treturn source{\n\t\t\tType:   \"file\",\n\t\t\tTarget: m.Path,\n\t\t}, nil\n\tcase syftSource.SnapMetadata:\n\t\treturn source{\n\t\t\tType:   \"snap\",\n\t\t\tTarget: fmt.Sprintf(\"%s@%s\", src.Name, src.Version),\n\t\t}, nil\n\tcase nil:\n\t\t// we may be showing results from a input source that does not support source information\n\t\treturn source{\n\t\t\tType:   \"unknown\",\n\t\t\tTarget: \"unknown\",\n\t\t}, nil\n\tdefault:\n\t\treturn source{}, fmt.Errorf(\"unsupported source: %T\", src.Metadata)\n\t}\n}\n"
  },
  {
    "path": "grype/presenter/models/source_test.go",
    "content": "package models\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/grype/grype/pkg\"\n\tsyftSource \"github.com/anchore/syft/syft/source\"\n\t\"github.com/anchore/syft/syft/testutil\"\n)\n\nfunc TestNewSource(t *testing.T) {\n\t// there isn't a great way to programmatically find only source metadata types in the pkg package, so we'll add them here.\n\tgrypeOnlySources := []any{\n\t\tpkg.SBOMFileMetadata{},\n\t\tpkg.PURLLiteralMetadata{},\n\t\tpkg.CPELiteralMetadata{},\n\t}\n\n\ttracker := testutil.NewSourceMetadataCompletionTester(t)\n\ttracker.Expect(grypeOnlySources...)\n\n\ttestCases := []struct {\n\t\tname     string\n\t\tmetadata syftSource.Description\n\t\texpected source\n\t}{\n\t\t{\n\t\t\tname: \"image\",\n\t\t\tmetadata: syftSource.Description{\n\t\t\t\tMetadata: syftSource.ImageMetadata{\n\t\t\t\t\tUserInput:      \"abc\",\n\t\t\t\t\tID:             \"def\",\n\t\t\t\t\tManifestDigest: \"abcdef\",\n\t\t\t\t\tSize:           100,\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: source{\n\t\t\t\tType: \"image\",\n\t\t\t\tTarget: syftSource.ImageMetadata{\n\t\t\t\t\tUserInput:      \"abc\",\n\t\t\t\t\tID:             \"def\",\n\t\t\t\t\tManifestDigest: \"abcdef\",\n\t\t\t\t\tSize:           100,\n\t\t\t\t\tRepoDigests:    []string{},\n\t\t\t\t\tTags:           []string{},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"directory\",\n\t\t\tmetadata: syftSource.Description{\n\t\t\t\tMetadata: syftSource.DirectoryMetadata{\n\t\t\t\t\tPath: \"/foo/bar\",\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: source{\n\t\t\t\tType:   \"directory\",\n\t\t\t\tTarget: \"/foo/bar\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"file\",\n\t\t\tmetadata: syftSource.Description{\n\t\t\t\tMetadata: syftSource.FileMetadata{\n\t\t\t\t\tPath: \"/foo/bar/test.zip\",\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: source{\n\t\t\t\tType:   \"file\",\n\t\t\t\tTarget: \"/foo/bar/test.zip\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"purl-file\",\n\t\t\tmetadata: syftSource.Description{\n\t\t\t\tMetadata: pkg.SBOMFileMetadata{\n\t\t\t\t\tPath: \"/path/to/purls.txt\",\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: source{\n\t\t\t\tType:   \"sbom-file\",\n\t\t\t\tTarget: \"/path/to/purls.txt\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"purl-literal\",\n\t\t\tmetadata: syftSource.Description{\n\t\t\t\tMetadata: pkg.PURLLiteralMetadata{\n\t\t\t\t\tPURL: \"pkg:maven/org.apache.logging.log4j/log4j-core@2.14.1\",\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: source{\n\t\t\t\tType:   \"purl\",\n\t\t\t\tTarget: \"pkg:maven/org.apache.logging.log4j/log4j-core@2.14.1\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"cpe-literal\",\n\t\t\tmetadata: syftSource.Description{\n\t\t\t\tMetadata: pkg.CPELiteralMetadata{\n\t\t\t\t\tCPE: \"cpe:/a:apache:log4j:2.14.1\",\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: source{\n\t\t\t\tType:   \"cpe\",\n\t\t\t\tTarget: \"cpe:/a:apache:log4j:2.14.1\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"snap metadata\",\n\t\t\tmetadata: syftSource.Description{\n\t\t\t\tName:     \"a-snap\",\n\t\t\t\tVersion:  \"10.2.3\",\n\t\t\t\tMetadata: syftSource.SnapMetadata{},\n\t\t\t},\n\t\t\texpected: source{\n\t\t\t\tType:   \"snap\",\n\t\t\t\tTarget: \"a-snap@10.2.3\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"oci model metadata\",\n\t\t\tmetadata: syftSource.Description{\n\t\t\t\tMetadata: syftSource.OCIModelMetadata{\n\t\t\t\t\tUserInput:      \"ai-model\",\n\t\t\t\t\tID:             \"ai-model-edf\",\n\t\t\t\t\tManifestDigest: \"abcdef\",\n\t\t\t\t\tSize:           100,\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: source{\n\t\t\t\tType: \"oci-model\",\n\t\t\t\tTarget: syftSource.OCIModelMetadata{\n\t\t\t\t\tUserInput:      \"ai-model\",\n\t\t\t\t\tID:             \"ai-model-edf\",\n\t\t\t\t\tManifestDigest: \"abcdef\",\n\t\t\t\t\tSize:           100,\n\t\t\t\t\tRepoDigests:    []string{},\n\t\t\t\t\tTags:           []string{},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"nil metadata\",\n\t\t\tmetadata: syftSource.Description{\n\t\t\t\tMetadata: nil,\n\t\t\t},\n\t\t\texpected: source{\n\t\t\t\tType:   \"unknown\",\n\t\t\t\tTarget: \"unknown\",\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, testCase := range testCases {\n\t\tt.Run(testCase.name, func(t *testing.T) {\n\t\t\tactual, err := newSource(testCase.metadata)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.Equal(t, testCase.expected, actual)\n\t\t\ttracker.Tested(t, testCase.metadata.Metadata)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/presenter/models/vulnerability.go",
    "content": "package models\n\nimport (\n\t\"sort\"\n\n\t\"github.com/anchore/grype/grype/version\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n\t\"github.com/anchore/grype/internal/log\"\n)\n\ntype Vulnerability struct {\n\tVulnerabilityMetadata\n\tFix        Fix        `json:\"fix\"`\n\tAdvisories []Advisory `json:\"advisories\"`\n\tRisk       float64    `json:\"risk\"`\n}\n\ntype Fix struct {\n\tVersions  []string       `json:\"versions\"`\n\tState     string         `json:\"state\"`\n\tAvailable []FixAvailable `json:\"available,omitempty\"`\n}\n\ntype FixAvailable struct {\n\tVersion string `json:\"version\"`\n\tDate    string `json:\"date\"`\n\tKind    string `json:\"kind,omitempty\"`\n}\n\ntype Advisory struct {\n\tID   string `json:\"id\"`\n\tLink string `json:\"link\"`\n}\n\nfunc NewVulnerability(vuln vulnerability.Vulnerability, metadata *vulnerability.Metadata, versionFormat version.Format) Vulnerability {\n\tif metadata == nil {\n\t\treturn Vulnerability{\n\t\t\tVulnerabilityMetadata: NewVulnerabilityMetadata(vuln.ID, vuln.Namespace, metadata),\n\t\t}\n\t}\n\n\tadvisories := make([]Advisory, len(vuln.Advisories))\n\tfor idx, advisory := range vuln.Advisories {\n\t\tadvisories[idx] = Advisory{\n\t\t\tID:   advisory.ID,\n\t\t\tLink: advisory.Link,\n\t\t}\n\t}\n\n\tfixedInVersions := vuln.Fix.Versions\n\tif fixedInVersions == nil {\n\t\t// always allocate collections\n\t\tfixedInVersions = make([]string, 0)\n\t}\n\n\treturn Vulnerability{\n\t\tVulnerabilityMetadata: NewVulnerabilityMetadata(vuln.ID, vuln.Namespace, metadata),\n\t\tFix: Fix{\n\t\t\tVersions:  sortVersions(fixedInVersions, versionFormat),\n\t\t\tState:     string(vuln.Fix.State),\n\t\t\tAvailable: getFixAvailable(vuln.Fix.Available),\n\t\t},\n\t\tAdvisories: advisories,\n\t\tRisk:       metadata.RiskScore(),\n\t}\n}\n\nfunc getFixAvailable(fixesAvailable []vulnerability.FixAvailable) []FixAvailable {\n\tif len(fixesAvailable) == 0 {\n\t\treturn nil\n\t}\n\n\tvar results []FixAvailable\n\tfor _, fix := range fixesAvailable {\n\t\tif fix.Date.IsZero() {\n\t\t\tcontinue\n\t\t}\n\t\tf := FixAvailable{\n\t\t\tVersion: fix.Version,\n\t\t\tDate:    fix.Date.Format(\"2006-01-02\"), // just extract the\n\t\t\tKind:    fix.Kind,\n\t\t}\n\t\tresults = append(results, f)\n\t}\n\n\treturn results\n}\n\nfunc sortVersions(fixedVersions []string, format version.Format) []string {\n\tif len(fixedVersions) <= 1 {\n\t\treturn fixedVersions\n\t}\n\n\t// first, create Version objects from strings (only once)\n\tversionObjs := make([]*version.Version, 0, len(fixedVersions))\n\tvar invalidVersions []string\n\tfor _, vStr := range fixedVersions {\n\t\tv := version.New(vStr, format)\n\t\terr := v.Validate()\n\t\tif err != nil {\n\t\t\tlog.WithFields(\"version\", vStr, \"error\", err).Trace(\"error parsing version, skipping\")\n\t\t\tinvalidVersions = append(invalidVersions, vStr)\n\t\t\tcontinue\n\t\t}\n\t\tversionObjs = append(versionObjs, v)\n\t}\n\n\t// sort the Version objects\n\tsort.Slice(versionObjs, func(i, j int) bool {\n\t\tcomparison, err := versionObjs[i].Compare(versionObjs[j])\n\t\tif err != nil {\n\t\t\tlog.WithFields(\"error\", err).Trace(\"error comparing versions\")\n\t\t\treturn false\n\t\t}\n\t\treturn comparison < 0\n\t})\n\n\t// convert back to strings\n\tvar result []string\n\tfor _, v := range versionObjs {\n\t\tresult = append(result, v.Raw)\n\t}\n\n\tresult = append(result, invalidVersions...)\n\n\treturn result\n}\n"
  },
  {
    "path": "grype/presenter/models/vulnerability_metadata.go",
    "content": "package models\n\nimport (\n\t\"time\"\n\n\t\"github.com/anchore/grype/grype/vulnerability\"\n)\n\ntype VulnerabilityMetadata struct {\n\tID             string           `json:\"id\"`\n\tDataSource     string           `json:\"dataSource\"`\n\tNamespace      string           `json:\"namespace,omitempty\"`\n\tSeverity       string           `json:\"severity,omitempty\"`\n\tURLs           []string         `json:\"urls\"`\n\tDescription    string           `json:\"description,omitempty\"`\n\tCvss           []Cvss           `json:\"cvss\"`\n\tKnownExploited []KnownExploited `json:\"knownExploited,omitempty\"`\n\tEPSS           []EPSS           `json:\"epss,omitempty\"`\n\tCWEs           []CWE            `json:\"cwes,omitempty\"`\n}\n\ntype KnownExploited struct {\n\tCVE                        string   `json:\"cve\"`\n\tVendorProject              string   `json:\"vendorProject,omitempty\"`\n\tProduct                    string   `json:\"product,omitempty\"`\n\tDateAdded                  string   `json:\"dateAdded,omitempty\"`\n\tRequiredAction             string   `json:\"requiredAction,omitempty\"`\n\tDueDate                    string   `json:\"dueDate,omitempty\"`\n\tKnownRansomwareCampaignUse string   `json:\"knownRansomwareCampaignUse\"`\n\tNotes                      string   `json:\"notes,omitempty\"`\n\tURLs                       []string `json:\"urls,omitempty\"`\n\tCWEs                       []string `json:\"cwes,omitempty\"`\n}\n\ntype EPSS struct {\n\tCVE        string  `json:\"cve\"`\n\tEPSS       float64 `json:\"epss\"`\n\tPercentile float64 `json:\"percentile\"`\n\tDate       string  `json:\"date\"`\n}\n\ntype CWE struct {\n\tCve    string `json:\"cve\"`\n\tCWE    string `json:\"cwe,omitempty\"`\n\tSource string `json:\"source,omitempty\"`\n\tType   string `json:\"type,omitempty\"`\n}\n\nfunc NewVulnerabilityMetadata(id, namespace string, metadata *vulnerability.Metadata) VulnerabilityMetadata {\n\tif metadata == nil {\n\t\treturn VulnerabilityMetadata{\n\t\t\tID:        id,\n\t\t\tNamespace: namespace,\n\t\t}\n\t}\n\n\turls := metadata.URLs\n\tif urls == nil {\n\t\turls = make([]string, 0)\n\t}\n\n\treturn VulnerabilityMetadata{\n\t\tID:             id,\n\t\tDataSource:     metadata.DataSource,\n\t\tNamespace:      metadata.Namespace,\n\t\tSeverity:       metadata.Severity,\n\t\tURLs:           urls,\n\t\tDescription:    metadata.Description,\n\t\tCvss:           toCVSS(metadata),\n\t\tKnownExploited: toKnownExploited(metadata.KnownExploited),\n\t\tEPSS:           toEPSS(metadata.EPSS),\n\t\tCWEs:           toCWE(metadata.CWEs),\n\t}\n}\n\nfunc toKnownExploited(knownExploited []vulnerability.KnownExploited) []KnownExploited {\n\tresult := make([]KnownExploited, len(knownExploited))\n\tfor idx, ke := range knownExploited {\n\t\tresult[idx] = KnownExploited{\n\t\t\tCVE:                        ke.CVE,\n\t\t\tVendorProject:              ke.VendorProject,\n\t\t\tProduct:                    ke.Product,\n\t\t\tDateAdded:                  formatDate(ke.DateAdded),\n\t\t\tRequiredAction:             ke.RequiredAction,\n\t\t\tDueDate:                    formatDate(ke.DueDate),\n\t\t\tKnownRansomwareCampaignUse: ke.KnownRansomwareCampaignUse,\n\t\t\tNotes:                      ke.Notes,\n\t\t\tURLs:                       ke.URLs,\n\t\t\tCWEs:                       ke.CWEs,\n\t\t}\n\t}\n\treturn result\n}\n\nfunc formatDate(t *time.Time) string {\n\tif t == nil {\n\t\treturn \"\"\n\t}\n\treturn t.Format(time.DateOnly)\n}\n\nfunc toEPSS(epss []vulnerability.EPSS) []EPSS {\n\tresult := make([]EPSS, len(epss))\n\tfor idx, e := range epss {\n\t\tresult[idx] = EPSS{\n\t\t\tCVE:        e.CVE,\n\t\t\tEPSS:       e.EPSS,\n\t\t\tPercentile: e.Percentile,\n\t\t\tDate:       e.Date.Format(time.DateOnly),\n\t\t}\n\t}\n\treturn result\n}\n\nfunc toCWE(cwes []vulnerability.CWE) []CWE {\n\tresult := make([]CWE, len(cwes))\n\tfor idx, e := range cwes {\n\t\tresult[idx] = CWE{\n\t\t\tCve:    e.CVE,\n\t\t\tCWE:    e.CWE,\n\t\t\tSource: e.Source,\n\t\t\tType:   e.Type,\n\t\t}\n\t}\n\treturn result\n}\n"
  },
  {
    "path": "grype/presenter/models/vulnerability_test.go",
    "content": "package models\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\n\t\"github.com/anchore/grype/grype/version\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n)\n\nfunc Test_sortVersions(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tversions []string\n\t\texpected []string\n\t}{\n\t\t{\n\t\t\tname:     \"empty slice\",\n\t\t\tversions: []string{},\n\t\t\texpected: []string{},\n\t\t},\n\t\t{\n\t\t\tname:     \"single version\",\n\t\t\tversions: []string{\"1.0.0\"},\n\t\t\texpected: []string{\"1.0.0\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"already sorted versions\",\n\t\t\tversions: []string{\"1.0.0\", \"1.1.0\", \"2.0.0\"},\n\t\t\texpected: []string{\"1.0.0\", \"1.1.0\", \"2.0.0\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"unsorted versions\",\n\t\t\tversions: []string{\"2.0.0\", \"1.0.0\", \"1.1.0\"},\n\t\t\texpected: []string{\"1.0.0\", \"1.1.0\", \"2.0.0\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"patch versions\",\n\t\t\tversions: []string{\"1.0.2\", \"1.0.1\", \"1.0.0\"},\n\t\t\texpected: []string{\"1.0.0\", \"1.0.1\", \"1.0.2\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"versions with pre-release\",\n\t\t\tversions: []string{\"1.0.0\", \"1.0.0-alpha\", \"1.0.0-beta\"},\n\t\t\texpected: []string{\"1.0.0-alpha\", \"1.0.0-beta\", \"1.0.0\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"mixed pre-release and regular\",\n\t\t\tversions: []string{\"2.0.0\", \"1.0.0-alpha\", \"1.0.0\", \"1.0.0-beta\"},\n\t\t\texpected: []string{\"1.0.0-alpha\", \"1.0.0-beta\", \"1.0.0\", \"2.0.0\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"versions with build metadata\",\n\t\t\tversions: []string{\"1.0.0+build.2\", \"1.0.0+build.1\", \"1.0.0\"},\n\t\t\texpected: []string{\"1.0.0+build.2\", \"1.0.0+build.1\", \"1.0.0\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"complex semantic versions\",\n\t\t\tversions: []string{\"1.0.0-alpha.1\", \"1.0.0-alpha\", \"1.0.0-beta.2\", \"1.0.0-beta.11\", \"1.0.0-rc.1\"},\n\t\t\texpected: []string{\"1.0.0-alpha\", \"1.0.0-alpha.1\", \"1.0.0-beta.2\", \"1.0.0-beta.11\", \"1.0.0-rc.1\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"invalid versions are appended to the end (in the order they were found in)\",\n\t\t\tversions: []string{\"invalid\", \"2.0.0\", \"also-invalid\", \"1.0.0\"},\n\t\t\texpected: []string{\"1.0.0\", \"2.0.0\", \"invalid\", \"also-invalid\"},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := sortVersions(tt.versions, version.SemanticFormat)\n\n\t\t\tif d := cmp.Diff(tt.expected, result); d != \"\" {\n\t\t\t\tt.Errorf(\"sortVersions() mismatch (-want +got):\\n%s\", d)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_getFixAvailable(t *testing.T) {\n\tvalidDate1 := time.Date(2023, 1, 15, 0, 0, 0, 0, time.UTC)\n\tvalidDate2 := time.Date(2023, 3, 10, 0, 0, 0, 0, time.UTC)\n\tzeroDate := time.Time{}\n\n\ttests := []struct {\n\t\tname     string\n\t\tinput    []vulnerability.FixAvailable\n\t\texpected []FixAvailable\n\t}{\n\t\t{\n\t\t\tname:     \"empty input returns nil\",\n\t\t\tinput:    []vulnerability.FixAvailable{},\n\t\t\texpected: nil,\n\t\t},\n\t\t{\n\t\t\tname: \"single fix with valid date\",\n\t\t\tinput: []vulnerability.FixAvailable{\n\t\t\t\t{Version: \"1.2.3\", Date: validDate1, Kind: \"first-observed\"},\n\t\t\t},\n\t\t\texpected: []FixAvailable{\n\t\t\t\t{Version: \"1.2.3\", Date: \"2023-01-15\", Kind: \"first-observed\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"multiple fixes with valid dates\",\n\t\t\tinput: []vulnerability.FixAvailable{\n\t\t\t\t{Version: \"1.2.3\", Date: validDate1, Kind: \"first-observed\"},\n\t\t\t\t{Version: \"2.0.0\", Date: validDate2, Kind: \"first-observed\"},\n\t\t\t},\n\t\t\texpected: []FixAvailable{\n\t\t\t\t{Version: \"1.2.3\", Date: \"2023-01-15\", Kind: \"first-observed\"},\n\t\t\t\t{Version: \"2.0.0\", Date: \"2023-03-10\", Kind: \"first-observed\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"filters out fixes with zero dates\",\n\t\t\tinput: []vulnerability.FixAvailable{\n\t\t\t\t{Version: \"1.2.3\", Date: validDate1, Kind: \"first-observed\"},\n\t\t\t\t{Version: \"1.2.4\", Date: zeroDate, Kind: \"first-observed\"},\n\t\t\t\t{Version: \"2.0.0\", Date: validDate2, Kind: \"first-observed\"},\n\t\t\t},\n\t\t\texpected: []FixAvailable{\n\t\t\t\t{Version: \"1.2.3\", Date: \"2023-01-15\", Kind: \"first-observed\"},\n\t\t\t\t{Version: \"2.0.0\", Date: \"2023-03-10\", Kind: \"first-observed\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"all fixes with zero dates returns nil\",\n\t\t\tinput: []vulnerability.FixAvailable{\n\t\t\t\t{Version: \"1.2.3\", Date: zeroDate, Kind: \"first-observed\"},\n\t\t\t\t{Version: \"1.2.4\", Date: zeroDate, Kind: \"first-observed\"},\n\t\t\t},\n\t\t\texpected: nil,\n\t\t},\n\t\t{\n\t\t\tname: \"empty kind is preserved\",\n\t\t\tinput: []vulnerability.FixAvailable{\n\t\t\t\t{Version: \"1.0.0\", Date: validDate1, Kind: \"\"},\n\t\t\t},\n\t\t\texpected: []FixAvailable{\n\t\t\t\t{Version: \"1.0.0\", Date: \"2023-01-15\", Kind: \"\"},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := getFixAvailable(tt.input)\n\n\t\t\tif d := cmp.Diff(tt.expected, result); d != \"\" {\n\t\t\t\tt.Errorf(\"getFixAvailable() mismatch (-want +got):\\n%s\", d)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/presenter/presenter.go",
    "content": "package presenter\n\nimport (\n\t\"github.com/wagoodman/go-presenter\"\n\n\t\"github.com/anchore/grype/grype/presenter/models\"\n\t\"github.com/anchore/grype/internal/format\"\n)\n\n// GetPresenter retrieves a Presenter that matches a CLI option.\n//\n// Deprecated: this will be removed in v1.0\nfunc GetPresenter(f string, templatePath string, showSuppressed bool, pb models.PresenterConfig) presenter.Presenter {\n\treturn format.GetPresenter(format.Parse(f), format.PresentationConfig{\n\t\tTemplateFilePath: templatePath,\n\t\tShowSuppressed:   showSuppressed,\n\t}, pb)\n}\n"
  },
  {
    "path": "grype/presenter/sarif/presenter.go",
    "content": "package sarif\n\nimport (\n\t\"crypto/sha256\"\n\t\"fmt\"\n\t\"hash\"\n\t\"io\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/owenrumney/go-sarif/sarif\"\n\n\t\"github.com/anchore/clio\"\n\t\"github.com/anchore/grype/grype/pkg\"\n\t\"github.com/anchore/grype/grype/presenter/models\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n\t\"github.com/anchore/syft/syft/file\"\n\t\"github.com/anchore/syft/syft/source\"\n)\n\n// Presenter holds the data for generating a report and implements the presenter.Presenter interface\ntype Presenter struct {\n\tid       clio.Identification\n\tdocument models.Document\n\tsrc      source.Description\n}\n\n// NewPresenter is a Presenter constructor\nfunc NewPresenter(pb models.PresenterConfig) *Presenter {\n\treturn &Presenter{\n\t\tid:       pb.ID,\n\t\tdocument: pb.Document,\n\t\tsrc:      pb.SBOM.Source,\n\t}\n}\n\n// Present creates a SARIF-based report\nfunc (p Presenter) Present(output io.Writer) error {\n\tdoc, err := p.toSarifReport()\n\tif err != nil {\n\t\treturn err\n\t}\n\terr = doc.PrettyWrite(output)\n\treturn err\n}\n\n// toSarifReport outputs a sarif report object\nfunc (p Presenter) toSarifReport() (*sarif.Report, error) {\n\tdoc, err := sarif.New(sarif.Version210)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tv := p.id.Version\n\tif v == \"[not provided]\" || v == \"\" {\n\t\t// Need a semver to pass the MS SARIF validator\n\t\tv = \"0.0.0-dev\"\n\t}\n\n\tdoc.AddRun(&sarif.Run{\n\t\tTool: sarif.Tool{\n\t\t\tDriver: &sarif.ToolComponent{\n\t\t\t\tName:           p.id.Name,\n\t\t\t\tVersion:        sp(v),\n\t\t\t\tInformationURI: sp(\"https://github.com/anchore/grype\"),\n\t\t\t\tRules:          p.sarifRules(),\n\t\t\t},\n\t\t},\n\t\tResults: p.sarifResults(),\n\t})\n\n\treturn doc, nil\n}\n\n// sarifRules generates the set of rules to include in this run\nfunc (p Presenter) sarifRules() (out []*sarif.ReportingDescriptor) {\n\tif len(p.document.Matches) > 0 {\n\t\truleIDs := map[string]bool{}\n\n\t\tfor _, m := range p.document.Matches {\n\t\t\truleID := p.ruleID(m)\n\t\t\tif ruleIDs[ruleID] {\n\t\t\t\t// here, we're only outputting information about the vulnerabilities, not where we matched them\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\truleIDs[ruleID] = true\n\n\t\t\t// Entirely possible to not have any links whatsoever\n\t\t\tlink := m.Vulnerability.ID\n\t\t\tswitch {\n\t\t\tcase m.Vulnerability.DataSource != \"\":\n\t\t\t\tlink = fmt.Sprintf(\"[%s](%s)\", m.Vulnerability.ID, m.Vulnerability.DataSource)\n\t\t\tcase len(m.Vulnerability.URLs) > 0:\n\t\t\t\tlink = fmt.Sprintf(\"[%s](%s)\", m.Vulnerability.ID, m.Vulnerability.URLs[0])\n\t\t\t}\n\n\t\t\tdescriptor := sarif.ReportingDescriptor{\n\t\t\t\tID:      ruleID,\n\t\t\t\tName:    sp(ruleName(m)),\n\t\t\t\tHelpURI: sp(\"https://github.com/anchore/grype\"),\n\t\t\t\t// Title of the SARIF report\n\t\t\t\tShortDescription: &sarif.MultiformatMessageString{\n\t\t\t\t\tText: sp(shortDescription(m)),\n\t\t\t\t},\n\t\t\t\t// Subtitle of the SARIF report\n\t\t\t\tFullDescription: &sarif.MultiformatMessageString{\n\t\t\t\t\tText: sp(subtitle(m)),\n\t\t\t\t},\n\t\t\t\tHelp: p.helpText(m, link),\n\t\t\t\tProperties: sarif.Properties{\n\t\t\t\t\t// For GitHub reportingDescriptor object:\n\t\t\t\t\t// https://docs.github.com/en/code-security/code-scanning/integrating-with-code-scanning/sarif-support-for-code-scanning#reportingdescriptor-object\n\t\t\t\t\t\"security-severity\": securitySeverityValue(m),\n\t\t\t\t},\n\t\t\t}\n\n\t\t\tif len(m.Artifact.PURL) != 0 {\n\t\t\t\tdescriptor.Properties[\"purls\"] = []string{m.Artifact.PURL}\n\t\t\t}\n\n\t\t\tout = append(out, &descriptor)\n\t\t}\n\t}\n\treturn out\n}\n\n// ruleID creates a unique rule ID for a given match\nfunc (p Presenter) ruleID(m models.Match) string {\n\t// TODO if we support configuration, we may want to allow addition of another qualifier such that if multiple\n\t// vuln scans are run on multiple containers we can identify unique rules for each\n\treturn fmt.Sprintf(\"%s-%s\", m.Vulnerability.ID, m.Artifact.Name)\n}\n\n// helpText gets the help text for a rule, this is displayed in GitHub if you click on the title in a list of vulns\nfunc (p Presenter) helpText(m models.Match, link string) *sarif.MultiformatMessageString {\n\t// TODO we shouldn't necessarily be adding a location here, there may be multiple referencing the same vulnerability\n\t// we could instead add some list of all affected locations in the case there are a number found within an image,\n\t// for example but this might get more complicated if there are multiple vuln scans for a particular branch\n\ttext := fmt.Sprintf(\"Vulnerability %s\\nSeverity: %s\\nPackage: %s\\nVersion: %s\\nFix Version: %s\\nType: %s\\nLocation: %s\\nData Namespace: %s\\nLink: %s\",\n\t\tm.Vulnerability.ID, severityText(m), m.Artifact.Name, m.Artifact.Version, fixVersions(m), m.Artifact.Type, p.packagePath(m.Artifact), m.Vulnerability.Namespace, link,\n\t)\n\tmarkdown := fmt.Sprintf(\n\t\t\"**Vulnerability %s**\\n\"+\n\t\t\t\"| Severity | Package | Version | Fix Version | Type | Location | Data Namespace | Link |\\n\"+\n\t\t\t\"| --- | --- | --- | --- | --- | --- | --- | --- |\\n\"+\n\t\t\t\"| %s  | %s  | %s  | %s  | %s  | %s  | %s  | %s  |\\n\",\n\t\tm.Vulnerability.ID, severityText(m), m.Artifact.Name, m.Artifact.Version, fixVersions(m), m.Artifact.Type, p.packagePath(m.Artifact), m.Vulnerability.Namespace, link,\n\t)\n\treturn &sarif.MultiformatMessageString{\n\t\tText:     &text,\n\t\tMarkdown: &markdown,\n\t}\n}\n\n// packagePath attempts to get the relative path of the package to the \"scan root\"\nfunc (p Presenter) packagePath(a models.Package) string {\n\tif len(a.Locations) > 0 {\n\t\treturn p.locationPath(a.Locations[0])\n\t}\n\treturn p.inputPath()\n}\n\n// inputPath returns a friendlier relative path or absolute path depending on the input, not prefixed by . or ./\nfunc (p Presenter) inputPath() string {\n\tvar inputPath string\n\tswitch m := p.src.Metadata.(type) {\n\tcase source.FileMetadata:\n\t\tinputPath = m.Path\n\tcase source.DirectoryMetadata:\n\t\tinputPath = m.Path\n\tdefault:\n\t\treturn \"\"\n\t}\n\tinputPath = strings.TrimPrefix(inputPath, \"./\")\n\tif inputPath == \".\" {\n\t\treturn \"\"\n\t}\n\treturn inputPath\n}\n\n// locationPath returns a path for the location, relative to the cwd\nfunc (p Presenter) locationPath(l file.Location) string {\n\tpath := l.Path()\n\tin := p.inputPath()\n\tpath = strings.TrimPrefix(path, \"./\")\n\t// trimmed off any ./ and accounted for dir:. for both path and input path\n\t_, ok := p.src.Metadata.(source.DirectoryMetadata)\n\tif ok {\n\t\tif filepath.IsAbs(path) || in == \"\" {\n\t\t\treturn path\n\t\t}\n\t\t// return a path relative to the cwd, if it's not absolute\n\t\treturn fmt.Sprintf(\"%s/%s\", in, path)\n\t}\n\n\treturn path\n}\n\n// locations the locations array is a single \"physical\" location with potentially multiple logical locations\nfunc (p Presenter) locations(m models.Match) []*sarif.Location {\n\tphysicalLocation := p.packagePath(m.Artifact)\n\n\tvar logicalLocations []*sarif.LogicalLocation\n\n\tswitch metadata := p.src.Metadata.(type) {\n\tcase source.ImageMetadata:\n\t\timg := metadata.UserInput\n\t\tlocations := m.Artifact.Locations\n\t\tfor _, l := range locations {\n\t\t\ttrimmedPath := strings.TrimLeft(p.locationPath(l), \"/\")\n\t\t\tlogicalLocations = append(logicalLocations, &sarif.LogicalLocation{\n\t\t\t\tFullyQualifiedName: sp(fmt.Sprintf(\"%s@%s:/%s\", img, l.FileSystemID, trimmedPath)),\n\t\t\t\tName:               sp(l.RealPath),\n\t\t\t})\n\t\t}\n\n\t\t// GitHub requires paths for the location, but we really don't have any information about what\n\t\t// file(s) these originated from in the repository. e.g. which Dockerfile was used to build an image,\n\t\t// so we just use a short path-compatible image name here, not the entire user input as it may include\n\t\t// sha and/or tags which are likely to change between runs and aren't really necessary for a general\n\t\t// path to find file where the package originated\n\t\tphysicalLocation = fmt.Sprintf(\"%s/%s\", imageShortPathName(p.src), physicalLocation)\n\tcase source.FileMetadata:\n\t\tlocations := m.Artifact.Locations\n\t\tfor _, l := range locations {\n\t\t\tlogicalLocations = append(logicalLocations, &sarif.LogicalLocation{\n\t\t\t\tFullyQualifiedName: sp(fmt.Sprintf(\"%s:/%s\", metadata.Path, p.locationPath(l))),\n\t\t\t\tName:               sp(l.RealPath),\n\t\t\t})\n\t\t}\n\tcase source.DirectoryMetadata:\n\t\t// DirectoryScheme is already handled, with input prepended if needed\n\t}\n\n\treturn []*sarif.Location{\n\t\t{\n\t\t\tPhysicalLocation: &sarif.PhysicalLocation{\n\t\t\t\tArtifactLocation: &sarif.ArtifactLocation{\n\t\t\t\t\tURI: sp(physicalLocation),\n\t\t\t\t},\n\t\t\t\t// TODO When grype starts reporting line numbers this will need to get updated\n\t\t\t\tRegion: &sarif.Region{\n\t\t\t\t\tStartLine:   ip(1),\n\t\t\t\t\tStartColumn: ip(1),\n\t\t\t\t\tEndLine:     ip(1),\n\t\t\t\t\tEndColumn:   ip(1),\n\t\t\t\t},\n\t\t\t},\n\t\t\tLogicalLocations: logicalLocations,\n\t\t},\n\t}\n}\n\n// severityText provides a textual representation of the severity level of the match\nfunc severityText(m models.Match) string {\n\tseverity := vulnerability.ParseSeverity(m.Vulnerability.Severity)\n\tswitch severity {\n\tcase vulnerability.CriticalSeverity:\n\t\treturn \"critical\"\n\tcase vulnerability.HighSeverity:\n\t\treturn \"high\"\n\tcase vulnerability.MediumSeverity:\n\t\treturn \"medium\"\n\t}\n\n\treturn \"low\"\n}\n\n// cvssScore attempts to get the best CVSS score that our vulnerability data contains\nfunc cvssScore(m models.Match) float64 {\n\tall := []models.VulnerabilityMetadata{\n\t\tm.Vulnerability.VulnerabilityMetadata,\n\t}\n\n\tall = append(all, m.RelatedVulnerabilities...)\n\n\tscore := -1.0\n\n\t// first check vendor-specific entries\n\tfor _, m := range all {\n\t\tif m.Namespace == \"nvd:cpe\" {\n\t\t\tcontinue\n\t\t}\n\t\tfor _, cvss := range m.Cvss {\n\t\t\tif cvss.Metrics.BaseScore > score {\n\t\t\t\tscore = cvss.Metrics.BaseScore\n\t\t\t}\n\t\t}\n\t}\n\n\tif score > 0 {\n\t\treturn score\n\t}\n\n\t// next, check nvd entries\n\tfor _, m := range all {\n\t\tfor _, cvss := range m.Cvss {\n\t\t\tif cvss.Metrics.BaseScore > score {\n\t\t\t\tscore = cvss.Metrics.BaseScore\n\t\t\t}\n\t\t}\n\t}\n\n\treturn score\n}\n\n// securitySeverityValue GitHub security-severity property uses a numeric severity value to determine whether things\n// are critical, high, etc.; this converts our vulnerability to a value within the ranges\nfunc securitySeverityValue(m models.Match) string {\n\t// this corresponds directly to the CVSS score, so we return this if we have it\n\tscore := cvssScore(m)\n\tif score > 0 {\n\t\treturn fmt.Sprintf(\"%.1f\", score)\n\t}\n\tseverity := vulnerability.ParseSeverity(m.Vulnerability.Severity)\n\tswitch severity {\n\tcase vulnerability.CriticalSeverity:\n\t\treturn \"9.0\"\n\tcase vulnerability.HighSeverity:\n\t\treturn \"7.0\"\n\tcase vulnerability.MediumSeverity:\n\t\treturn \"4.0\"\n\tcase vulnerability.LowSeverity:\n\t\treturn \"1.0\"\n\t}\n\n\treturn \"0.0\"\n}\n\nfunc levelValue(m models.Match) string {\n\tseverity := vulnerability.ParseSeverity(m.Vulnerability.Severity)\n\tswitch severity {\n\tcase vulnerability.CriticalSeverity:\n\t\treturn \"error\"\n\tcase vulnerability.HighSeverity:\n\t\treturn \"error\"\n\tcase vulnerability.MediumSeverity:\n\t\treturn \"warning\"\n\t}\n\n\treturn \"note\"\n}\n\n// subtitle generates a subtitle for the given match\nfunc subtitle(m models.Match) string {\n\tsubtitle := m.Vulnerability.Description\n\tif subtitle != \"\" {\n\t\treturn subtitle\n\t}\n\n\tfixVersion := fixVersions(m)\n\tif fixVersion != \"\" {\n\t\treturn fmt.Sprintf(\"Version %s is affected with an available fix in versions %s\", m.Artifact.Version, fixVersion)\n\t}\n\n\treturn fmt.Sprintf(\"Version %s is affected with no fixes reported yet.\", m.Artifact.Version)\n}\n\nfunc fixVersions(m models.Match) string {\n\tif m.Vulnerability.Fix.State == vulnerability.FixStateFixed.String() && len(m.Vulnerability.Fix.Versions) > 0 {\n\t\treturn strings.Join(m.Vulnerability.Fix.Versions, \",\")\n\t}\n\treturn \"\"\n}\n\nfunc shortDescription(m models.Match) string {\n\treturn fmt.Sprintf(\"%s %s vulnerability for %s package\", m.Vulnerability.ID, severityText(m), m.Artifact.Name)\n}\n\nfunc (p Presenter) sarifResults() []*sarif.Result {\n\tout := make([]*sarif.Result, 0) // make sure we have at least an empty array\n\tfor _, m := range p.document.Matches {\n\t\tout = append(out, &sarif.Result{\n\t\t\tRuleID:  sp(p.ruleID(m)),\n\t\t\tLevel:   sp(levelValue(m)),\n\t\t\tMessage: p.resultMessage(m),\n\t\t\t// According to the SARIF spec, it may be correct to use AnalysisTarget.URI to indicate a logical\n\t\t\t// file such as a \"Dockerfile\" but GitHub does not work well with this\n\t\t\t// GitHub requires partialFingerprints to upload to the API; these are automatically filled in\n\t\t\t// when using the CodeQL upload action. See: https://docs.github.com/en/code-security/code-scanning/integrating-with-code-scanning/sarif-support-for-code-scanning#providing-data-to-track-code-scanning-alerts-across-runs\n\t\t\tPartialFingerprints: p.partialFingerprints(m),\n\t\t\tLocations:           p.locations(m),\n\t\t})\n\t}\n\treturn out\n}\n\n// ip returns an int pointer based on the provided value\nfunc ip(i int) *int {\n\treturn &i\n}\n\n// sp returns a string pointer based on the provided value\nfunc sp(sarif string) *string {\n\treturn &sarif\n}\n\nfunc (p Presenter) resultMessage(m models.Match) sarif.Message {\n\tpath := p.packagePath(m.Artifact)\n\tsrc := p.inputPath()\n\tswitch meta := p.src.Metadata.(type) {\n\tcase source.ImageMetadata:\n\t\tsrc = fmt.Sprintf(\"in image %s at: %s\", meta.UserInput, path)\n\tcase source.FileMetadata, source.DirectoryMetadata:\n\t\tsrc = fmt.Sprintf(\"at: %s\", path)\n\tcase pkg.PURLLiteralMetadata:\n\t\tsrc = fmt.Sprintf(\"from purl literal %q\", meta.PURL)\n\tcase pkg.SBOMFileMetadata:\n\t\tsrc = fmt.Sprintf(\"from SBOM file %s\", meta.Path)\n\t}\n\tmessage := fmt.Sprintf(\"A %s vulnerability in %s package: %s, version %s was found %s\",\n\t\tseverityText(m), m.Artifact.Type, m.Artifact.Name, m.Artifact.Version, src)\n\n\treturn sarif.Message{\n\t\tText: &message,\n\t}\n}\n\nfunc (p Presenter) partialFingerprints(m models.Match) map[string]any {\n\ta := m.Artifact\n\thasher := sha256.New()\n\tif meta, ok := p.src.Metadata.(source.ImageMetadata); ok {\n\t\thashWrite(hasher, p.src.Name, meta.Architecture, meta.OS)\n\t}\n\thashWrite(hasher, string(a.Type), a.Name, a.Version, p.packagePath(a))\n\treturn map[string]any{\n\t\t// this is meant to include <hash>:<line>, but there isn't line information here, so just include :1\n\t\t\"primaryLocationLineHash\": fmt.Sprintf(\"%x:1\", hasher.Sum([]byte{})),\n\t}\n}\n\nfunc hashWrite(hasher hash.Hash, values ...string) {\n\tfor _, value := range values {\n\t\t_, _ = hasher.Write([]byte(value))\n\t}\n}\n\nfunc ruleName(m models.Match) string {\n\tif len(m.MatchDetails) > 0 {\n\t\td := m.MatchDetails[0]\n\t\tbuf := strings.Builder{}\n\t\tfor _, segment := range []string{d.Matcher, d.Type} {\n\t\t\tfor _, part := range strings.Split(segment, \"-\") {\n\t\t\t\tbuf.WriteString(strings.ToUpper(part[:1]))\n\t\t\t\tbuf.WriteString(part[1:])\n\t\t\t}\n\t\t}\n\t\treturn buf.String()\n\t}\n\treturn m.Vulnerability.ID\n}\n\nvar nonPathChars = regexp.MustCompile(\"[^a-zA-Z0-9-_.]\")\n\n// imageShortPathName returns path-compatible text describing the image. if the image name is the form\n// some/path/to/image, it will return the image portion of the name.\nfunc imageShortPathName(s source.Description) string {\n\timageName := s.Name\n\tparts := strings.Split(imageName, \"/\")\n\timageName = parts[len(parts)-1]\n\timageName = nonPathChars.ReplaceAllString(imageName, \"\")\n\treturn imageName\n}\n"
  },
  {
    "path": "grype/presenter/sarif/presenter_test.go",
    "content": "package sarif\n\nimport (\n\t\"bytes\"\n\t\"flag\"\n\t\"os/exec\"\n\t\"testing\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/grype/grype/presenter/internal\"\n\t\"github.com/anchore/grype/grype/presenter/models\"\n\t\"github.com/anchore/grype/internal/testutils\"\n\t\"github.com/anchore/syft/syft/file\"\n\t\"github.com/anchore/syft/syft/source\"\n\t\"github.com/anchore/syft/syft/source/directorysource\"\n)\n\nvar updateSnapshot = flag.Bool(\"update\", false, \"update .golden files for sarif presenters\")\nvar validatorImage = \"ghcr.io/anchore/sarif-validator:0.1.0@sha256:a0729d695e023740f5df6bcb50d134e88149bea59c63a896a204e88f62b564c6\"\n\nfunc TestSarifPresenter(t *testing.T) {\n\ttests := []struct {\n\t\tname   string\n\t\tscheme internal.SyftSource\n\t}{\n\t\t{\n\t\t\tname:   \"directory\",\n\t\t\tscheme: internal.DirectorySource,\n\t\t},\n\t\t{\n\t\t\tname:   \"image\",\n\t\t\tscheme: internal.ImageSource,\n\t\t},\n\t}\n\n\tfor _, tc := range tests {\n\t\ttc := tc\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tvar buffer bytes.Buffer\n\n\t\t\tpb := internal.GeneratePresenterConfig(t, tc.scheme)\n\n\t\t\tpres := NewPresenter(pb)\n\t\t\terr := pres.Present(&buffer)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\tactual := buffer.Bytes()\n\t\t\tif *updateSnapshot {\n\t\t\t\ttestutils.UpdateGoldenFileContents(t, actual)\n\t\t\t}\n\n\t\t\tvar expected = testutils.GetGoldenFileContents(t)\n\t\t\tactual = internal.Redact(actual)\n\t\t\texpected = internal.Redact(expected)\n\n\t\t\tif d := cmp.Diff(string(expected), string(actual)); d != \"\" {\n\t\t\t\tt.Fatalf(\"(-want +got):\\n%s\", d)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_SarifIsValid(t *testing.T) {\n\tif _, err := exec.LookPath(\"docker\"); err != nil {\n\t\tt.Skip(\"docker not available\")\n\t}\n\n\ttests := []struct {\n\t\tname   string\n\t\tscheme internal.SyftSource\n\t}{\n\t\t{\n\t\t\tname:   \"directory\",\n\t\t\tscheme: internal.DirectorySource,\n\t\t},\n\t\t{\n\t\t\tname:   \"image\",\n\t\t\tscheme: internal.ImageSource,\n\t\t},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tvar buffer bytes.Buffer\n\n\t\t\tpb := internal.GeneratePresenterConfig(t, tc.scheme)\n\n\t\t\tpres := NewPresenter(pb)\n\t\t\terr := pres.Present(&buffer)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tcmd := exec.Command(\"docker\", \"run\", \"--rm\", \"-i\", validatorImage)\n\n\t\t\tout := bytes.Buffer{}\n\t\t\tcmd.Stdout = &out\n\t\t\tcmd.Stderr = &out\n\n\t\t\t// pipe to the docker command\n\t\t\tcmd.Stdin = &buffer\n\n\t\t\terr = cmd.Run()\n\t\t\tif err != nil || cmd.ProcessState.ExitCode() != 0 {\n\t\t\t\t// valid\n\t\t\t\tt.Fatalf(\"error validating SARIF document: %s\", out.String())\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_locationPath(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tmetadata any\n\t\treal     string\n\t\tvirtual  string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname: \"dir:.\",\n\t\t\tmetadata: source.DirectoryMetadata{\n\t\t\t\tPath: \".\",\n\t\t\t},\n\t\t\treal:     \"/home/usr/file\",\n\t\t\tvirtual:  \"file\",\n\t\t\texpected: \"file\",\n\t\t},\n\t\t{\n\t\t\tname: \"dir:./\",\n\t\t\tmetadata: source.DirectoryMetadata{\n\t\t\t\tPath: \"./\",\n\t\t\t},\n\t\t\treal:     \"/home/usr/file\",\n\t\t\tvirtual:  \"file\",\n\t\t\texpected: \"file\",\n\t\t},\n\t\t{\n\t\t\tname: \"dir:./someplace\",\n\t\t\tmetadata: source.DirectoryMetadata{\n\t\t\t\tPath: \"./someplace\",\n\t\t\t},\n\t\t\treal:     \"/home/usr/file\",\n\t\t\tvirtual:  \"file\",\n\t\t\texpected: \"someplace/file\",\n\t\t},\n\t\t{\n\t\t\tname: \"dir:/someplace\",\n\t\t\tmetadata: source.DirectoryMetadata{\n\t\t\t\tPath: \"/someplace\",\n\t\t\t},\n\t\t\treal:     \"file\",\n\t\t\texpected: \"/someplace/file\",\n\t\t},\n\t\t{\n\t\t\tname: \"dir:/someplace symlink\",\n\t\t\tmetadata: source.DirectoryMetadata{\n\t\t\t\tPath: \"/someplace\",\n\t\t\t},\n\t\t\treal:     \"/someplace/usr/file\",\n\t\t\tvirtual:  \"file\",\n\t\t\texpected: \"/someplace/file\",\n\t\t},\n\t\t{\n\t\t\tname: \"dir:/someplace absolute\",\n\t\t\tmetadata: source.DirectoryMetadata{\n\t\t\t\tPath: \"/someplace\",\n\t\t\t},\n\t\t\treal:     \"/usr/file\",\n\t\t\texpected: \"/usr/file\",\n\t\t},\n\t\t{\n\t\t\tname: \"file:/someplace/file\",\n\t\t\tmetadata: source.FileMetadata{\n\t\t\t\tPath: \"/someplace/file\",\n\t\t\t},\n\t\t\treal:     \"/usr/file\",\n\t\t\texpected: \"/usr/file\",\n\t\t},\n\t\t{\n\t\t\tname: \"file:/someplace/file relative\",\n\t\t\tmetadata: source.FileMetadata{\n\t\t\t\tPath: \"/someplace/file\",\n\t\t\t},\n\t\t\treal:     \"file\",\n\t\t\texpected: \"file\",\n\t\t},\n\t\t{\n\t\t\tname: \"image\",\n\t\t\tmetadata: source.ImageMetadata{\n\t\t\t\tUserInput: \"alpine:latest\",\n\t\t\t},\n\t\t\treal:     \"/etc/file\",\n\t\t\texpected: \"/etc/file\",\n\t\t},\n\t\t{\n\t\t\tname: \"image symlink\",\n\t\t\tmetadata: source.ImageMetadata{\n\t\t\t\tUserInput: \"alpine:latest\",\n\t\t\t},\n\t\t\treal:     \"/etc/elsewhere/file\",\n\t\t\tvirtual:  \"/etc/file\",\n\t\t\texpected: \"/etc/file\",\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tpres := createDirPresenter(t)\n\t\t\tpres.src = source.Description{\n\t\t\t\tMetadata: test.metadata,\n\t\t\t}\n\n\t\t\tpath := pres.packagePath(models.Package{\n\t\t\t\tLocations: file.NewLocationSet(\n\t\t\t\t\tfile.NewVirtualLocation(test.real, test.virtual),\n\t\t\t\t).ToSlice(),\n\t\t\t})\n\n\t\t\tassert.Equal(t, test.expected, path)\n\t\t})\n\t}\n}\n\nfunc createDirPresenter(t *testing.T) *Presenter {\n\td := t.TempDir()\n\tnewSrc, err := directorysource.NewFromPath(d)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tpb := internal.GeneratePresenterConfig(t, internal.DirectorySource)\n\tpb.SBOM.Source = newSrc.Describe()\n\n\tpres := NewPresenter(pb)\n\n\treturn pres\n}\n\nfunc TestToSarifReport(t *testing.T) {\n\ttt := []struct {\n\t\tname      string\n\t\tscheme    internal.SyftSource\n\t\tlocations map[string]string\n\t}{\n\t\t{\n\t\t\tname:   \"directory\",\n\t\t\tscheme: internal.DirectorySource,\n\t\t\tlocations: map[string]string{\n\t\t\t\t\"CVE-1999-0001-package-1\": \"/some/path/somefile-1.txt\",\n\t\t\t\t\"CVE-1999-0002-package-2\": \"/some/path/somefile-2.txt\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:   \"image\",\n\t\t\tscheme: internal.ImageSource,\n\t\t\tlocations: map[string]string{\n\t\t\t\t\"CVE-1999-0001-package-1\": \"user-input/somefile-1.txt\",\n\t\t\t\t\"CVE-1999-0002-package-2\": \"user-input/somefile-2.txt\",\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tc := range tt {\n\t\ttc := tc\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tpb := internal.GeneratePresenterConfig(t, tc.scheme)\n\n\t\t\tpres := NewPresenter(pb)\n\n\t\t\treport, err := pres.toSarifReport()\n\t\t\tassert.NoError(t, err)\n\n\t\t\tassert.Len(t, report.Runs, 1)\n\t\t\tassert.NotEmpty(t, report.Runs)\n\t\t\tassert.NotEmpty(t, report.Runs[0].Results)\n\t\t\tassert.NotEmpty(t, report.Runs[0].Tool.Driver)\n\t\t\tassert.NotEmpty(t, report.Runs[0].Tool.Driver.Rules)\n\n\t\t\t// Sorted by vulnID, pkg name, ...\n\t\t\trun := report.Runs[0]\n\t\t\tassert.Len(t, run.Tool.Driver.Rules, 2)\n\t\t\tassert.Equal(t, \"CVE-1999-0001-package-1\", run.Tool.Driver.Rules[0].ID)\n\t\t\tassert.Equal(t, \"CVE-1999-0002-package-2\", run.Tool.Driver.Rules[1].ID)\n\n\t\t\tassert.Len(t, run.Results, 2)\n\t\t\tresult := run.Results[0]\n\t\t\tassert.Equal(t, \"CVE-1999-0001-package-1\", *result.RuleID)\n\t\t\tassert.Equal(t, \"note\", *result.Level)\n\t\t\tassert.Len(t, result.Locations, 1)\n\t\t\tlocation := result.Locations[0]\n\t\t\texpectedLocation, ok := tc.locations[*result.RuleID]\n\t\t\tif !ok {\n\t\t\t\tt.Fatalf(\"no expected location for %s\", *result.RuleID)\n\t\t\t}\n\t\t\tassert.Equal(t, expectedLocation, *location.PhysicalLocation.ArtifactLocation.URI)\n\n\t\t\tresult = run.Results[1]\n\t\t\tassert.Equal(t, \"CVE-1999-0002-package-2\", *result.RuleID)\n\t\t\tassert.Equal(t, \"error\", *result.Level)\n\t\t\tassert.Len(t, result.Locations, 1)\n\t\t\tlocation = result.Locations[0]\n\t\t\texpectedLocation, ok = tc.locations[*result.RuleID]\n\t\t\tif !ok {\n\t\t\t\tt.Fatalf(\"no expected location for %s\", *result.RuleID)\n\t\t\t}\n\t\t\tassert.Equal(t, expectedLocation, *location.PhysicalLocation.ArtifactLocation.URI)\n\t\t})\n\t}\n\n}\n\nfunc Test_cvssScoreWithMissingMetadata(t *testing.T) {\n\tscore := cvssScore(models.Match{\n\t\tVulnerability: models.Vulnerability{\n\t\t\tVulnerabilityMetadata: models.VulnerabilityMetadata{\n\t\t\t\tID:        \"id\",\n\t\t\t\tNamespace: \"namespace\",\n\t\t\t},\n\t\t},\n\t})\n\tassert.Equal(t, float64(-1), score)\n}\n\nfunc Test_cvssScore(t *testing.T) {\n\n\tcvss := func(id string, namespace string, scores ...float64) models.VulnerabilityMetadata {\n\t\tvalues := make([]models.Cvss, 0, len(scores))\n\t\tfor _, score := range scores {\n\t\t\tvalues = append(values, models.Cvss{\n\t\t\t\tMetrics: models.CvssMetrics{\n\t\t\t\t\tBaseScore: score,\n\t\t\t\t},\n\t\t\t})\n\t\t}\n\t\treturn models.VulnerabilityMetadata{\n\t\t\tID:        id,\n\t\t\tNamespace: namespace,\n\t\t\tCvss:      values,\n\t\t}\n\t}\n\n\tnvd1 := cvss(\"1\", \"nvd:cpe\", 1)\n\tnotNvd1 := cvss(\"1\", \"not-nvd\", 2)\n\tnotNvd2 := cvss(\"2\", \"not-nvd\", 3, 4)\n\n\ttests := []struct {\n\t\tname     string\n\t\tmatch    models.Match\n\t\texpected float64\n\t}{\n\t\t{\n\t\t\tname: \"none\",\n\t\t\tmatch: models.Match{\n\t\t\t\tVulnerability: models.Vulnerability{\n\t\t\t\t\tVulnerabilityMetadata: models.VulnerabilityMetadata{\n\t\t\t\t\t\tID: \"4\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tRelatedVulnerabilities: []models.VulnerabilityMetadata{\n\t\t\t\t\t{\n\t\t\t\t\t\tID:        \"7\",\n\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\t// intentionally missing info...\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: -1,\n\t\t},\n\t\t{\n\t\t\tname: \"direct\",\n\t\t\tmatch: models.Match{\n\t\t\t\tVulnerability: models.Vulnerability{\n\t\t\t\t\tVulnerabilityMetadata: notNvd2,\n\t\t\t\t},\n\t\t\t\tRelatedVulnerabilities: []models.VulnerabilityMetadata{\n\t\t\t\t\tnvd1,\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: 4,\n\t\t},\n\t\t{\n\t\t\tname: \"related not nvd\",\n\t\t\tmatch: models.Match{\n\t\t\t\tVulnerability: models.Vulnerability{\n\t\t\t\t\tVulnerabilityMetadata: nvd1,\n\t\t\t\t},\n\t\t\t\tRelatedVulnerabilities: []models.VulnerabilityMetadata{\n\t\t\t\t\tnvd1,\n\t\t\t\t\tnotNvd1,\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: 2,\n\t\t},\n\t\t{\n\t\t\tname: \"related nvd\",\n\t\t\tmatch: models.Match{\n\t\t\t\tVulnerability: models.Vulnerability{\n\t\t\t\t\tVulnerabilityMetadata: models.VulnerabilityMetadata{\n\t\t\t\t\t\tID:        \"4\",\n\t\t\t\t\t\tNamespace: \"not-nvd\",\n\t\t\t\t\t\t// intentionally missing info...\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tRelatedVulnerabilities: []models.VulnerabilityMetadata{\n\t\t\t\t\tnvd1,\n\t\t\t\t\t{\n\t\t\t\t\t\tID:        \"7\",\n\t\t\t\t\t\tNamespace: \"not-nvd\",\n\t\t\t\t\t\t// intentionally missing info...\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: 1,\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tscore := cvssScore(test.match)\n\t\t\tassert.Equal(t, test.expected, score)\n\t\t})\n\t}\n}\n\nfunc Test_imageShortPathName(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tin       string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:     \"valid single name\",\n\t\t\tin:       \"simple.-_name\",\n\t\t\texpected: \"simple.-_name\",\n\t\t},\n\t\t{\n\t\t\tname:     \"valid name in org\",\n\t\t\tin:       \"some-org/some-image\",\n\t\t\texpected: \"some-image\",\n\t\t},\n\t\t{\n\t\t\tname:     \"name and org with many invalid chars\",\n\t\t\tin:       \"some/*^&$#%$#@*(}{<><./,valid-()(#)@!(~@#$#%^&**[]{-chars\",\n\t\t\texpected: \"valid--chars\",\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tgot := imageShortPathName(\n\t\t\t\tsource.Description{\n\t\t\t\t\tName:     test.in,\n\t\t\t\t\tMetadata: nil,\n\t\t\t\t},\n\t\t\t)\n\n\t\t\tassert.Equal(t, test.expected, got)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/presenter/sarif/testdata/image-simple/Dockerfile",
    "content": "# Note: changes to this file will result in updating several test values. Consider making a new image fixture instead of editing this one.\nFROM scratch\nADD file-1.txt /somefile-1.txt\nADD file-2.txt /somefile-2.txt\n# note: adding a directory will behave differently on docker engine v18 vs v19\nADD target /\n"
  },
  {
    "path": "grype/presenter/sarif/testdata/image-simple/file-1.txt",
    "content": "this file has contents"
  },
  {
    "path": "grype/presenter/sarif/testdata/image-simple/file-2.txt",
    "content": "file-2 contents!"
  },
  {
    "path": "grype/presenter/sarif/testdata/image-simple/target/really/nested/file-3.txt",
    "content": "another file!\nwith lines..."
  },
  {
    "path": "grype/presenter/sarif/testdata/snapshot/TestSarifPresenter_directory.golden",
    "content": "{\n  \"version\": \"2.1.0\",\n  \"$schema\": \"https://docs.oasis-open.org/sarif/sarif/v2.1.0/errata01/os/schemas/sarif-schema-2.1.0.json\",\n  \"runs\": [\n    {\n      \"tool\": {\n        \"driver\": {\n          \"name\": \"grype\",\n          \"version\": \"0.0.0-dev\",\n          \"informationUri\": \"https://github.com/anchore/grype\",\n          \"rules\": [\n            {\n              \"id\": \"CVE-1999-0001-package-1\",\n              \"name\": \"DpkgMatcherExactDirectMatch\",\n              \"shortDescription\": {\n                \"text\": \"CVE-1999-0001 low vulnerability for package-1 package\"\n              },\n              \"fullDescription\": {\n                \"text\": \"Version 1.1.1 is affected with an available fix in versions 1.2.1,2.1.3,3.4.0\"\n              },\n              \"helpUri\": \"https://github.com/anchore/grype\",\n              \"help\": {\n                \"text\": \"Vulnerability CVE-1999-0001\\nSeverity: low\\nPackage: package-1\\nVersion: 1.1.1\\nFix Version: 1.2.1,2.1.3,3.4.0\\nType: rpm\\nLocation: /some/path/somefile-1.txt\\nData Namespace: \\nLink: CVE-1999-0001\",\n                \"markdown\": \"**Vulnerability CVE-1999-0001**\\n| Severity | Package | Version | Fix Version | Type | Location | Data Namespace | Link |\\n| --- | --- | --- | --- | --- | --- | --- | --- |\\n| low  | package-1  | 1.1.1  | 1.2.1,2.1.3,3.4.0  | rpm  | /some/path/somefile-1.txt  |   | CVE-1999-0001  |\\n\"\n              },\n              \"properties\": {\n                \"security-severity\": \"8.2\"\n              }\n            },\n            {\n              \"id\": \"CVE-1999-0002-package-2\",\n              \"name\": \"DpkgMatcherExactIndirectMatch\",\n              \"shortDescription\": {\n                \"text\": \"CVE-1999-0002 critical vulnerability for package-2 package\"\n              },\n              \"fullDescription\": {\n                \"text\": \"Version 2.2.2 is affected with no fixes reported yet.\"\n              },\n              \"helpUri\": \"https://github.com/anchore/grype\",\n              \"help\": {\n                \"text\": \"Vulnerability CVE-1999-0002\\nSeverity: critical\\nPackage: package-2\\nVersion: 2.2.2\\nFix Version: \\nType: deb\\nLocation: /some/path/somefile-2.txt\\nData Namespace: \\nLink: CVE-1999-0002\",\n                \"markdown\": \"**Vulnerability CVE-1999-0002**\\n| Severity | Package | Version | Fix Version | Type | Location | Data Namespace | Link |\\n| --- | --- | --- | --- | --- | --- | --- | --- |\\n| critical  | package-2  | 2.2.2  |   | deb  | /some/path/somefile-2.txt  |   | CVE-1999-0002  |\\n\"\n              },\n              \"properties\": {\n                \"purls\": [\n                  \"pkg:deb/package-2@2.2.2\"\n                ],\n                \"security-severity\": \"8.5\"\n              }\n            }\n          ]\n        }\n      },\n      \"results\": [\n        {\n          \"ruleId\": \"CVE-1999-0001-package-1\",\n          \"level\": \"note\",\n          \"message\": {\n            \"text\": \"A low vulnerability in rpm package: package-1, version 1.1.1 was found at: /some/path/somefile-1.txt\"\n          },\n          \"locations\": [\n            {\n              \"physicalLocation\": {\n                \"artifactLocation\": {\n                  \"uri\": \"/some/path/somefile-1.txt\"\n                },\n                \"region\": {\n                  \"startLine\": 1,\n                  \"startColumn\": 1,\n                  \"endLine\": 1,\n                  \"endColumn\": 1\n                }\n              }\n            }\n          ],\n          \"partialFingerprints\": {\n            \"primaryLocationLineHash\": \"0eefd3962fe456b80e5ddad4ec777c7f75b3c0586db887eff1c98f376fff60ba:1\"\n          }\n        },\n        {\n          \"ruleId\": \"CVE-1999-0002-package-2\",\n          \"level\": \"error\",\n          \"message\": {\n            \"text\": \"A critical vulnerability in deb package: package-2, version 2.2.2 was found at: /some/path/somefile-2.txt\"\n          },\n          \"locations\": [\n            {\n              \"physicalLocation\": {\n                \"artifactLocation\": {\n                  \"uri\": \"/some/path/somefile-2.txt\"\n                },\n                \"region\": {\n                  \"startLine\": 1,\n                  \"startColumn\": 1,\n                  \"endLine\": 1,\n                  \"endColumn\": 1\n                }\n              }\n            }\n          ],\n          \"partialFingerprints\": {\n            \"primaryLocationLineHash\": \"0d4ef10dce50e71641e9314195020cea18febe4c6a4a8145a485154383d4fe0b:1\"\n          }\n        }\n      ]\n    }\n  ]\n}"
  },
  {
    "path": "grype/presenter/sarif/testdata/snapshot/TestSarifPresenter_image.golden",
    "content": "{\n  \"version\": \"2.1.0\",\n  \"$schema\": \"https://docs.oasis-open.org/sarif/sarif/v2.1.0/errata01/os/schemas/sarif-schema-2.1.0.json\",\n  \"runs\": [\n    {\n      \"tool\": {\n        \"driver\": {\n          \"name\": \"grype\",\n          \"version\": \"0.0.0-dev\",\n          \"informationUri\": \"https://github.com/anchore/grype\",\n          \"rules\": [\n            {\n              \"id\": \"CVE-1999-0001-package-1\",\n              \"name\": \"DpkgMatcherExactDirectMatch\",\n              \"shortDescription\": {\n                \"text\": \"CVE-1999-0001 low vulnerability for package-1 package\"\n              },\n              \"fullDescription\": {\n                \"text\": \"Version 1.1.1 is affected with an available fix in versions 1.2.1,2.1.3,3.4.0\"\n              },\n              \"helpUri\": \"https://github.com/anchore/grype\",\n              \"help\": {\n                \"text\": \"Vulnerability CVE-1999-0001\\nSeverity: low\\nPackage: package-1\\nVersion: 1.1.1\\nFix Version: 1.2.1,2.1.3,3.4.0\\nType: rpm\\nLocation: somefile-1.txt\\nData Namespace: \\nLink: CVE-1999-0001\",\n                \"markdown\": \"**Vulnerability CVE-1999-0001**\\n| Severity | Package | Version | Fix Version | Type | Location | Data Namespace | Link |\\n| --- | --- | --- | --- | --- | --- | --- | --- |\\n| low  | package-1  | 1.1.1  | 1.2.1,2.1.3,3.4.0  | rpm  | somefile-1.txt  |   | CVE-1999-0001  |\\n\"\n              },\n              \"properties\": {\n                \"security-severity\": \"8.2\"\n              }\n            },\n            {\n              \"id\": \"CVE-1999-0002-package-2\",\n              \"name\": \"DpkgMatcherExactIndirectMatch\",\n              \"shortDescription\": {\n                \"text\": \"CVE-1999-0002 critical vulnerability for package-2 package\"\n              },\n              \"fullDescription\": {\n                \"text\": \"Version 2.2.2 is affected with no fixes reported yet.\"\n              },\n              \"helpUri\": \"https://github.com/anchore/grype\",\n              \"help\": {\n                \"text\": \"Vulnerability CVE-1999-0002\\nSeverity: critical\\nPackage: package-2\\nVersion: 2.2.2\\nFix Version: \\nType: deb\\nLocation: somefile-2.txt\\nData Namespace: \\nLink: CVE-1999-0002\",\n                \"markdown\": \"**Vulnerability CVE-1999-0002**\\n| Severity | Package | Version | Fix Version | Type | Location | Data Namespace | Link |\\n| --- | --- | --- | --- | --- | --- | --- | --- |\\n| critical  | package-2  | 2.2.2  |   | deb  | somefile-2.txt  |   | CVE-1999-0002  |\\n\"\n              },\n              \"properties\": {\n                \"purls\": [\n                  \"pkg:deb/package-2@2.2.2\"\n                ],\n                \"security-severity\": \"8.5\"\n              }\n            }\n          ]\n        }\n      },\n      \"results\": [\n        {\n          \"ruleId\": \"CVE-1999-0001-package-1\",\n          \"level\": \"note\",\n          \"message\": {\n            \"text\": \"A low vulnerability in rpm package: package-1, version 1.1.1 was found in image user-input at: somefile-1.txt\"\n          },\n          \"locations\": [\n            {\n              \"physicalLocation\": {\n                \"artifactLocation\": {\n                  \"uri\": \"user-input/somefile-1.txt\"\n                },\n                \"region\": {\n                  \"startLine\": 1,\n                  \"startColumn\": 1,\n                  \"endLine\": 1,\n                  \"endColumn\": 1\n                }\n              },\n              \"logicalLocations\": [\n                {\n                  \"name\": \"/foo/bar/somefile-1.txt\",\n                  \"fullyQualifiedName\": \"user-input@:/somefile-1.txt\"\n                }\n              ]\n            }\n          ],\n          \"partialFingerprints\": {\n            \"primaryLocationLineHash\": \"efe125c0a2b4bdafe476b69ba51a49734780c62b93803950319056acebe4323f:1\"\n          }\n        },\n        {\n          \"ruleId\": \"CVE-1999-0002-package-2\",\n          \"level\": \"error\",\n          \"message\": {\n            \"text\": \"A critical vulnerability in deb package: package-2, version 2.2.2 was found in image user-input at: somefile-2.txt\"\n          },\n          \"locations\": [\n            {\n              \"physicalLocation\": {\n                \"artifactLocation\": {\n                  \"uri\": \"user-input/somefile-2.txt\"\n                },\n                \"region\": {\n                  \"startLine\": 1,\n                  \"startColumn\": 1,\n                  \"endLine\": 1,\n                  \"endColumn\": 1\n                }\n              },\n              \"logicalLocations\": [\n                {\n                  \"name\": \"/foo/bar/somefile-2.txt\",\n                  \"fullyQualifiedName\": \"user-input@:/somefile-2.txt\"\n                }\n              ]\n            }\n          ],\n          \"partialFingerprints\": {\n            \"primaryLocationLineHash\": \"bafe9890c7cda00bf4d1b1a57d1d20b08e27162e718235a3d38a9a8d2f449ed1:1\"\n          }\n        }\n      ]\n    }\n  ]\n}"
  },
  {
    "path": "grype/presenter/table/__snapshots__/presenter_test.snap",
    "content": "\n[TestTablePresenter/no_color - 1]\nNAME       INSTALLED  FIXED IN              TYPE  VULNERABILITY  SEVERITY  EPSS         RISK         \npackage-1  1.1.1      *1.2.1, 2.1.3, 3.4.0  rpm   CVE-1999-0001  Low       3.0% (42nd)  1.7          \npackage-2  2.2.2                            deb   CVE-1999-0002  Critical  8.0% (53rd)  96.3  (kev)  \n\n---\n\n[TestTablePresenter/with_color - 1]\nNAME       INSTALLED  FIXED IN             TYPE  VULNERABILITY  SEVERITY  EPSS         RISK         \npackage-1  1.1.1      1.2.1\u001b[38;5;240m, \u001b[0m\u001b[38;5;240m2.1.3\u001b[0m\u001b[38;5;240m, \u001b[0m\u001b[38;5;240m3.4.0\u001b[0m  \u001b[0mrpm   CVE-1999-0001  \u001b[38;5;36mLow\u001b[0m       \u001b[0m3.0% (42nd)  1.7          \npackage-2  2.2.2                           deb   CVE-1999-0002  \u001b[1;38;5;198mCritical\u001b[0m  \u001b[0m8.0% (53rd)  96.3  \u001b[1;7;38;5;198m KEV \u001b[0m  \u001b[0m\n\n---\n\n[TestEmptyTablePresenter - 1]\nNo vulnerabilities found\n\n---\n\n[TestHidesIgnoredMatches - 1]\nNAME       INSTALLED  FIXED IN              TYPE  VULNERABILITY  SEVERITY  EPSS         RISK         \npackage-1  1.1.1      *1.2.1, 2.1.3, 3.4.0  rpm   CVE-1999-0001  Low       3.0% (42nd)  1.7          \npackage-2  2.2.2                            deb   CVE-1999-0002  Critical  8.0% (53rd)  96.3  (kev)  \n\n---\n\n[TestDisplaysIgnoredMatches - 1]\nNAME       INSTALLED  FIXED IN              TYPE  VULNERABILITY  SEVERITY  EPSS         RISK                       \npackage-1  1.1.1      *1.2.1, 2.1.3, 3.4.0  rpm   CVE-1999-0001  Low       3.0% (42nd)  1.7                        \npackage-2  2.2.2                            deb   CVE-1999-0002  Critical  8.0% (53rd)  96.3  (kev)                \npackage-2  2.2.2                            deb   CVE-1999-0001  Low       3.0% (42nd)  1.7   (suppressed)         \npackage-2  2.2.2                            deb   CVE-1999-0002  Critical  8.0% (53rd)  96.3  (kev, suppressed)    \npackage-2  2.2.2                            deb   CVE-1999-0004  High      3.0% (75th)  2.2   (suppressed by VEX)  \n\n---\n\n[TestDisplaysDistro - 1]\nNAME       INSTALLED  FIXED IN              TYPE  VULNERABILITY  SEVERITY  EPSS         RISK                     \npackage-1  1.1.1      *1.2.1, 2.1.3, 3.4.0  rpm   CVE-1999-0001  Low       3.0% (42nd)  1.7   (ubuntu:2.5)       \npackage-2  2.2.2                            deb   CVE-1999-0002  Critical  8.0% (53rd)  96.3  (kev, ubuntu:3.5)  \n\n---\n\n[TestDisplaysIgnoredMatchesAndDistro - 1]\nNAME       INSTALLED  FIXED IN              TYPE  VULNERABILITY  SEVERITY  EPSS         RISK                                 \npackage-1  1.1.1      *1.2.1, 2.1.3, 3.4.0  rpm   CVE-1999-0001  Low       3.0% (42nd)  1.7   (ubuntu:2.5)                   \npackage-2  2.2.2                            deb   CVE-1999-0002  Critical  8.0% (53rd)  96.3  (kev, ubuntu:3.5)              \npackage-2  2.2.2                            deb   CVE-1999-0001  Low       3.0% (42nd)  1.7   (ubuntu:2.5, suppressed)       \npackage-2  2.2.2                            deb   CVE-1999-0002  Critical  8.0% (53rd)  96.3  (kev, ubuntu:3.5, suppressed)  \npackage-2  2.2.2                            deb   CVE-1999-0004  High      3.0% (75th)  2.2   (suppressed by VEX)            \n\n---\n"
  },
  {
    "path": "grype/presenter/table/presenter.go",
    "content": "package table\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"strings\"\n\n\t\"github.com/charmbracelet/lipgloss\"\n\t\"github.com/olekukonko/tablewriter\"\n\t\"github.com/olekukonko/tablewriter/renderer\"\n\t\"github.com/olekukonko/tablewriter/tw\"\n\t\"github.com/scylladb/go-set/strset\"\n\n\t\"github.com/anchore/grype/grype/db/v5/namespace/distro\"\n\t\"github.com/anchore/grype/grype/presenter/models\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n)\n\nconst (\n\tappendSuppressed    = \"suppressed\"\n\tappendSuppressedVEX = \"suppressed by VEX\"\n)\n\n// Presenter is a generic struct for holding fields needed for reporting\ntype Presenter struct {\n\tdocument       models.Document\n\tshowSuppressed bool\n\twithColor      bool\n\n\trecommendedFixStyle lipgloss.Style\n\tkevStyle            lipgloss.Style\n\tcriticalStyle       lipgloss.Style\n\thighStyle           lipgloss.Style\n\tmediumStyle         lipgloss.Style\n\tlowStyle            lipgloss.Style\n\tnegligibleStyle     lipgloss.Style\n\tauxiliaryStyle      lipgloss.Style\n\tunknownStyle        lipgloss.Style\n}\n\ntype rows []row\n\ntype row struct {\n\tName            string\n\tVersion         string\n\tFix             string\n\tPackageType     string\n\tVulnerabilityID string\n\tSeverity        string\n\tEPSS            epss\n\tRisk            string\n\tAnnotation      string\n}\n\ntype epss struct {\n\tScore      float64\n\tPercentile float64\n}\n\nfunc (e epss) String() string {\n\tif e.Percentile == 0 {\n\t\treturn \"N/A\"\n\t}\n\n\tprobability := e.Score * 100\n\tpercentile := e.Percentile * 100\n\n\tif probability < 0.1 {\n\t\treturn fmt.Sprintf(\"< 0.1%% (%s)\", formatPercentileWithSuffix(percentile))\n\t}\n\n\treturn fmt.Sprintf(\"%.1f%% (%s)\", probability, formatPercentileWithSuffix(percentile))\n}\n\nfunc formatPercentileWithSuffix(percentile float64) string {\n\tp := int(percentile)\n\n\t// Handle special cases for 11th, 12th, 13th\n\tif p%100 >= 11 && p%100 <= 13 {\n\t\treturn fmt.Sprintf(\"%dth\", p)\n\t}\n\n\t// Handle other cases\n\tswitch p % 10 {\n\tcase 1:\n\t\treturn fmt.Sprintf(\"%dst\", p)\n\tcase 2:\n\t\treturn fmt.Sprintf(\"%dnd\", p)\n\tcase 3:\n\t\treturn fmt.Sprintf(\"%drd\", p)\n\tdefault:\n\t\treturn fmt.Sprintf(\"%dth\", p)\n\t}\n}\n\n// NewPresenter is a *Presenter constructor\nfunc NewPresenter(pb models.PresenterConfig, showSuppressed bool) *Presenter {\n\twithColor := supportsColor()\n\tfixStyle := lipgloss.NewStyle().Border(lipgloss.Border{Left: \"*\"}, false, false, false, true)\n\tif withColor {\n\t\tfixStyle = lipgloss.NewStyle()\n\t}\n\treturn &Presenter{\n\t\tdocument:            pb.Document,\n\t\tshowSuppressed:      showSuppressed,\n\t\twithColor:           withColor,\n\t\trecommendedFixStyle: fixStyle,\n\t\tnegligibleStyle:     lipgloss.NewStyle().Foreground(lipgloss.Color(\"240\")),                          // dark gray\n\t\tlowStyle:            lipgloss.NewStyle().Foreground(lipgloss.Color(\"36\")),                           // cyan/teal\n\t\tmediumStyle:         lipgloss.NewStyle().Foreground(lipgloss.Color(\"178\")),                          // gold/amber\n\t\thighStyle:           lipgloss.NewStyle().Foreground(lipgloss.Color(\"203\")),                          // salmon/light red\n\t\tcriticalStyle:       lipgloss.NewStyle().Foreground(lipgloss.Color(\"198\")).Bold(true),               // bright pink\n\t\tkevStyle:            lipgloss.NewStyle().Foreground(lipgloss.Color(\"198\")).Reverse(true).Bold(true), // white on bright pink\n\t\t//kevStyle:       lipgloss.NewStyle().Foreground(lipgloss.Color(\"198\")),             // bright pink\n\t\tauxiliaryStyle: lipgloss.NewStyle().Foreground(lipgloss.Color(\"240\")), // dark gray\n\t\tunknownStyle:   lipgloss.NewStyle().Foreground(lipgloss.Color(\"12\")),  // light blue\n\t}\n}\n\n// Present creates a JSON-based reporting\nfunc (p *Presenter) Present(output io.Writer) error {\n\trs := p.getRows(p.document, p.showSuppressed)\n\n\tif len(rs) == 0 {\n\t\t_, err := io.WriteString(output, \"No vulnerabilities found\\n\")\n\t\treturn err\n\t}\n\n\ttable := newTable(output, []string{\"Name\", \"Installed\", \"Fixed In\", \"Type\", \"Vulnerability\", \"Severity\", \"EPSS\", \"Risk\"})\n\n\tif err := table.Bulk(rs.Render()); err != nil {\n\t\treturn fmt.Errorf(\"failed to add table rows: %w\", err)\n\t}\n\n\treturn table.Render()\n}\n\nfunc newTable(output io.Writer, columns []string) *tablewriter.Table {\n\treturn tablewriter.NewTable(output,\n\t\ttablewriter.WithHeader(columns),\n\t\ttablewriter.WithHeaderAlignment(tw.AlignLeft),\n\t\ttablewriter.WithHeaderAutoWrap(tw.WrapNone),\n\t\ttablewriter.WithRowAutoWrap(tw.WrapNone),\n\t\ttablewriter.WithAutoHide(tw.On),\n\t\ttablewriter.WithRenderer(renderer.NewBlueprint()),\n\t\ttablewriter.WithBehavior(\n\t\t\ttw.Behavior{\n\t\t\t\tTrimSpace: tw.On,\n\t\t\t\tAutoHide:  tw.On,\n\t\t\t},\n\t\t),\n\t\ttablewriter.WithPadding(\n\t\t\ttw.Padding{\n\t\t\t\tRight: \"  \",\n\t\t\t},\n\t\t),\n\t\ttablewriter.WithRendition(\n\t\t\ttw.Rendition{\n\t\t\t\tSymbols: tw.NewSymbols(tw.StyleNone),\n\t\t\t\tSettings: tw.Settings{\n\t\t\t\t\tLines: tw.Lines{\n\t\t\t\t\t\tShowTop:        tw.Off,\n\t\t\t\t\t\tShowBottom:     tw.Off,\n\t\t\t\t\t\tShowHeaderLine: tw.Off,\n\t\t\t\t\t\tShowFooterLine: tw.Off,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t),\n\t)\n}\n\nfunc (p *Presenter) getRows(doc models.Document, showSuppressed bool) rows {\n\tvar rs rows\n\n\tmultipleDistros := false\n\texistingDistro := \"\"\n\tfor _, m := range doc.Matches {\n\t\tif _, err := distro.FromString(m.Vulnerability.Namespace); err == nil {\n\t\t\tif existingDistro == \"\" {\n\t\t\t\texistingDistro = m.Vulnerability.Namespace\n\t\t\t} else if existingDistro != m.Vulnerability.Namespace {\n\t\t\t\tmultipleDistros = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\t// generate rows for matching vulnerabilities\n\tfor _, m := range doc.Matches {\n\t\trs = append(rs, p.newRow(m, \"\", multipleDistros))\n\t}\n\n\t// generate rows for suppressed vulnerabilities\n\tif showSuppressed {\n\t\tfor _, m := range doc.IgnoredMatches {\n\t\t\tmsg := appendSuppressed\n\t\t\tif m.AppliedIgnoreRules != nil {\n\t\t\t\tfor i := range m.AppliedIgnoreRules {\n\t\t\t\t\tif m.AppliedIgnoreRules[i].Namespace == \"vex\" {\n\t\t\t\t\t\tmsg = appendSuppressedVEX\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\trs = append(rs, p.newRow(m.Match, msg, multipleDistros))\n\t\t}\n\t}\n\treturn rs\n}\n\nfunc supportsColor() bool {\n\treturn lipgloss.NewStyle().Foreground(lipgloss.Color(\"5\")).Render(\"\") != \"\"\n}\n\nfunc (p *Presenter) newRow(m models.Match, extraAnnotation string, showDistro bool) row {\n\tvar annotations []string\n\n\tif showDistro {\n\t\tif d, err := distro.FromString(m.Vulnerability.Namespace); err == nil {\n\t\t\tannotations = append(annotations, p.auxiliaryStyle.Render(fmt.Sprintf(\"%s:%s\", d.DistroType(), d.Version())))\n\t\t}\n\t}\n\n\tif extraAnnotation != \"\" {\n\t\tannotations = append(annotations, p.auxiliaryStyle.Render(extraAnnotation))\n\t}\n\n\tvar kev, annotation string\n\tif len(m.Vulnerability.KnownExploited) > 0 {\n\t\tif p.withColor {\n\t\t\tkev = p.kevStyle.Render(\" KEV \") // ⚡❋◆◉፨⿻⨳✖• (requires non-standard fonts:  )\n\t\t} else {\n\t\t\tannotations = append([]string{\"kev\"}, annotations...)\n\t\t}\n\t}\n\n\tif len(annotations) > 0 {\n\t\tannotation = p.auxiliaryStyle.Render(\"(\") + strings.Join(annotations, p.auxiliaryStyle.Render(\", \")) + p.auxiliaryStyle.Render(\")\")\n\t}\n\n\tif kev != \"\" {\n\t\tannotation = kev + \" \" + annotation\n\t}\n\n\treturn row{\n\t\tName:            m.Artifact.Name,\n\t\tVersion:         m.Artifact.Version,\n\t\tFix:             p.formatFix(m),\n\t\tPackageType:     string(m.Artifact.Type),\n\t\tVulnerabilityID: m.Vulnerability.ID,\n\t\tSeverity:        p.formatSeverity(m.Vulnerability.Severity),\n\t\tEPSS:            newEPSS(m.Vulnerability.EPSS),\n\t\tRisk:            p.formatRisk(m.Vulnerability.Risk),\n\t\tAnnotation:      annotation,\n\t}\n}\n\nfunc newEPSS(es []models.EPSS) epss {\n\tif len(es) == 0 {\n\t\treturn epss{}\n\t}\n\treturn epss{\n\t\tScore:      es[0].EPSS,\n\t\tPercentile: es[0].Percentile,\n\t}\n}\n\nfunc (p *Presenter) formatSeverity(severity string) string {\n\tvar severityStyle *lipgloss.Style\n\tswitch strings.ToLower(severity) {\n\tcase \"critical\":\n\t\tseverityStyle = &p.criticalStyle\n\tcase \"high\":\n\t\tseverityStyle = &p.highStyle\n\tcase \"medium\":\n\t\tseverityStyle = &p.mediumStyle\n\tcase \"low\":\n\t\tseverityStyle = &p.lowStyle\n\tcase \"negligible\":\n\t\tseverityStyle = &p.negligibleStyle\n\t}\n\n\tif severityStyle == nil {\n\t\tseverityStyle = &p.unknownStyle\n\t}\n\n\treturn severityStyle.Render(severity)\n}\n\nfunc (p *Presenter) formatRisk(risk float64) string {\n\t// TODO: add color to risk?\n\tswitch {\n\tcase risk == 0:\n\t\treturn \"  N/A\"\n\tcase risk < 0.1:\n\t\treturn \"< 0.1\"\n\t}\n\treturn fmt.Sprintf(\"%5.1f\", risk)\n}\n\nfunc (p *Presenter) formatFix(m models.Match) string {\n\t// adjust the model fix state values for better presentation\n\tswitch m.Vulnerability.Fix.State {\n\tcase vulnerability.FixStateWontFix.String():\n\t\treturn \"(won't fix)\"\n\tcase vulnerability.FixStateUnknown.String():\n\t\treturn \"\"\n\t}\n\n\t// do our best to summarize the fixed versions, de-epmhasize non-recommended versions\n\t// also, since there is not a lot of screen real estate, we will truncate the list of fixed versions\n\t// to ~30 characters (or so) to avoid wrapping.\n\treturn p.applyTruncation(\n\t\tp.formatVersionsToDisplay(\n\t\t\tm,\n\t\t\tgetRecommendedVersions(m),\n\t\t),\n\t\tm.Vulnerability.Fix.Versions,\n\t)\n}\n\nfunc getRecommendedVersions(m models.Match) *strset.Set {\n\trecommended := strset.New()\n\tfor _, d := range m.MatchDetails {\n\t\tif d.Fix == nil {\n\t\t\tcontinue\n\t\t}\n\t\tif d.Fix.SuggestedVersion != \"\" {\n\t\t\trecommended.Add(d.Fix.SuggestedVersion)\n\t\t}\n\t}\n\treturn recommended\n}\n\nconst maxVersionFieldLength = 30\n\nfunc (p *Presenter) formatVersionsToDisplay(m models.Match, recommendedVersions *strset.Set) []string {\n\thasMultipleVersions := len(m.Vulnerability.Fix.Versions) > 1\n\tshouldHighlightRecommended := hasMultipleVersions && recommendedVersions.Size() > 0\n\n\tvar currentCharacterCount int\n\tadded := strset.New()\n\tvar vers []string\n\n\tfor _, v := range m.Vulnerability.Fix.Versions {\n\t\tif added.Has(v) {\n\t\t\tcontinue // skip duplicates\n\t\t}\n\n\t\tif shouldHighlightRecommended {\n\t\t\tif recommendedVersions.Has(v) {\n\t\t\t\t// recommended versions always get added\n\t\t\t\tadded.Add(v)\n\t\t\t\tcurrentCharacterCount += len(v)\n\t\t\t\tvers = append(vers, p.recommendedFixStyle.Render(v))\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// skip not-necessarily-recommended versions if we're running out of space\n\t\t\tif currentCharacterCount+len(v) > maxVersionFieldLength {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// add not-necessarily-recommended versions with auxiliary styling\n\t\t\tcurrentCharacterCount += len(v)\n\t\t\tadded.Add(v)\n\t\t\tvers = append(vers, p.auxiliaryStyle.Render(v))\n\t\t} else {\n\t\t\t// when not prioritizing, add all versions\n\t\t\tadded.Add(v)\n\t\t\tvers = append(vers, v)\n\t\t}\n\t}\n\n\treturn vers\n}\n\nfunc (p *Presenter) applyTruncation(formattedVersions []string, allVersions []string) string {\n\tfinalVersions := strings.Join(formattedVersions, p.auxiliaryStyle.Render(\", \"))\n\n\tvar characterCount int\n\tfor _, v := range allVersions {\n\t\tcharacterCount += len(v)\n\t}\n\n\tif characterCount > maxVersionFieldLength && len(allVersions) > 1 {\n\t\tfinalVersions += p.auxiliaryStyle.Render(\", ...\")\n\t}\n\n\treturn finalVersions\n}\n\nfunc (r row) Columns() []string {\n\tif r.Annotation != \"\" {\n\t\treturn []string{r.Name, r.Version, r.Fix, r.PackageType, r.VulnerabilityID, r.Severity, r.EPSS.String(), r.Risk, r.Annotation}\n\t}\n\treturn []string{r.Name, r.Version, r.Fix, r.PackageType, r.VulnerabilityID, r.Severity, r.EPSS.String(), r.Risk}\n}\n\nfunc (r row) String() string {\n\treturn strings.Join(r.Columns(), \"|\")\n}\n\nfunc (rs rows) Render() [][]string {\n\tdeduped := rs.Deduplicate()\n\tout := make([][]string, len(deduped))\n\tfor idx, r := range deduped {\n\t\tout[idx] = r.Columns()\n\t}\n\treturn out\n}\n\nfunc (rs rows) Deduplicate() []row {\n\t// deduplicate\n\tseen := map[string]row{}\n\tvar deduped rows\n\n\tfor _, v := range rs {\n\t\tkey := v.String()\n\t\tif _, ok := seen[key]; ok {\n\t\t\t// dup!\n\t\t\tcontinue\n\t\t}\n\n\t\tseen[key] = v\n\t\tdeduped = append(deduped, v)\n\t}\n\n\t// render final columns\n\treturn deduped\n}\n"
  },
  {
    "path": "grype/presenter/table/presenter_test.go",
    "content": "package table\n\nimport (\n\t\"bytes\"\n\t\"testing\"\n\n\t\"github.com/charmbracelet/lipgloss\"\n\t\"github.com/gkampitakis/go-snaps/snaps\"\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/muesli/termenv\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/clio\"\n\t\"github.com/anchore/grype/grype/match\"\n\t\"github.com/anchore/grype/grype/pkg\"\n\t\"github.com/anchore/grype/grype/presenter/internal\"\n\t\"github.com/anchore/grype/grype/presenter/models\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n\tsyftPkg \"github.com/anchore/syft/syft/pkg\"\n)\n\nfunc TestCreateRow(t *testing.T) {\n\tpkg1 := models.Package{\n\t\tID:      \"package-1-id\",\n\t\tName:    \"package-1\",\n\t\tVersion: \"2.0.0\",\n\t\tType:    syftPkg.DebPkg,\n\t}\n\tmatch1 := models.Match{\n\t\tVulnerability: models.Vulnerability{\n\t\t\tFix: models.Fix{\n\t\t\t\tVersions: []string{\"1.0.2\", \"2.0.1\", \"3.0.4\"},\n\t\t\t\tState:    vulnerability.FixStateFixed.String(),\n\t\t\t},\n\t\t\tRisk: 87.2,\n\t\t\tVulnerabilityMetadata: models.VulnerabilityMetadata{\n\t\t\t\tID:          \"CVE-1999-0001\",\n\t\t\t\tNamespace:   \"source-1\",\n\t\t\t\tDescription: \"1999-01 description\",\n\t\t\t\tSeverity:    \"Medium\",\n\t\t\t\tCvss: []models.Cvss{\n\t\t\t\t\t{\n\t\t\t\t\t\tMetrics: models.CvssMetrics{\n\t\t\t\t\t\t\tBaseScore: 7,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tVector:  \"CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:H\",\n\t\t\t\t\t\tVersion: \"3.1\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tEPSS: []models.EPSS{\n\t\t\t\t\t{\n\t\t\t\t\t\tCVE:        \"CVE-1999-0001\",\n\t\t\t\t\t\tEPSS:       0.3,\n\t\t\t\t\t\tPercentile: 0.5,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tArtifact: pkg1,\n\t\tMatchDetails: []models.MatchDetails{\n\t\t\t{\n\t\t\t\tType:    match.ExactDirectMatch.String(),\n\t\t\t\tMatcher: match.DpkgMatcher.String(),\n\t\t\t\tFix: &models.FixDetails{\n\t\t\t\t\tSuggestedVersion: \"2.0.1\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tmatchWithKev := match1\n\tmatchWithKev.Vulnerability.KnownExploited = append(matchWithKev.Vulnerability.KnownExploited, models.KnownExploited{\n\t\tCVE:                        \"CVE-1999-0001\",\n\t\tKnownRansomwareCampaignUse: \"Known\",\n\t})\n\n\tcases := []struct {\n\t\tname            string\n\t\tmatch           models.Match\n\t\textraAnnotation string\n\t\texpectedRow     []string\n\t}{\n\t\t{\n\t\t\tname:            \"create row for vulnerability\",\n\t\t\tmatch:           match1,\n\t\t\textraAnnotation: \"\",\n\t\t\texpectedRow:     []string{match1.Artifact.Name, match1.Artifact.Version, \"1.0.2, *2.0.1, 3.0.4\", string(match1.Artifact.Type), match1.Vulnerability.ID, \"Medium\", \"30.0% (50th)\", \" 87.2\"},\n\t\t},\n\t\t{\n\t\t\tname:            \"create row for suppressed vulnerability\",\n\t\t\tmatch:           match1,\n\t\t\textraAnnotation: appendSuppressed,\n\t\t\texpectedRow:     []string{match1.Artifact.Name, match1.Artifact.Version, \"1.0.2, *2.0.1, 3.0.4\", string(match1.Artifact.Type), match1.Vulnerability.ID, \"Medium\", \"30.0% (50th)\", \" 87.2\", \"(suppressed)\"},\n\t\t},\n\t\t{\n\t\t\tname:            \"create row for suppressed vulnerability + Kev\",\n\t\t\tmatch:           matchWithKev,\n\t\t\textraAnnotation: appendSuppressed,\n\t\t\texpectedRow:     []string{match1.Artifact.Name, match1.Artifact.Version, \"1.0.2, *2.0.1, 3.0.4\", string(match1.Artifact.Type), match1.Vulnerability.ID, \"Medium\", \"30.0% (50th)\", \" 87.2\", \"(kev, suppressed)\"},\n\t\t},\n\t}\n\n\tfor _, testCase := range cases {\n\t\tt.Run(testCase.name, func(t *testing.T) {\n\t\t\tp := NewPresenter(models.PresenterConfig{}, false)\n\t\t\trow := p.newRow(testCase.match, testCase.extraAnnotation, false)\n\t\t\tcols := rows{row}.Render()[0]\n\n\t\t\tassert.Equal(t, testCase.expectedRow, cols)\n\t\t})\n\t}\n}\n\nfunc TestTablePresenter(t *testing.T) {\n\tpb := internal.GeneratePresenterConfig(t, internal.ImageSource)\n\n\tt.Run(\"no color\", func(t *testing.T) {\n\t\tvar buffer bytes.Buffer\n\t\tlipgloss.SetColorProfile(termenv.Ascii)\n\t\tpres := NewPresenter(pb, false)\n\n\t\terr := pres.Present(&buffer)\n\t\trequire.NoError(t, err)\n\n\t\tactual := buffer.String()\n\t\tsnaps.MatchSnapshot(t, actual)\n\t})\n\n\tt.Run(\"with color\", func(t *testing.T) {\n\t\tvar buffer bytes.Buffer\n\t\tlipgloss.SetColorProfile(termenv.TrueColor)\n\t\tt.Cleanup(func() {\n\t\t\t// don't affect other tests\n\t\t\tlipgloss.SetColorProfile(termenv.Ascii)\n\t\t})\n\t\tpres := NewPresenter(pb, false)\n\n\t\terr := pres.Present(&buffer)\n\t\trequire.NoError(t, err)\n\n\t\tactual := buffer.String()\n\t\tsnaps.MatchSnapshot(t, actual)\n\t})\n}\n\nfunc TestEmptyTablePresenter(t *testing.T) {\n\t// Expected to have no output\n\n\tvar buffer bytes.Buffer\n\n\tdoc, err := models.NewDocument(clio.Identification{}, nil, pkg.Context{}, match.NewMatches(), nil, nil, nil, nil, models.SortByPackage, true, nil)\n\trequire.NoError(t, err)\n\tpb := models.PresenterConfig{\n\t\tDocument: doc,\n\t}\n\n\tpres := NewPresenter(pb, false)\n\n\t// run presenter\n\terr = pres.Present(&buffer)\n\trequire.NoError(t, err)\n\n\tactual := buffer.String()\n\tsnaps.MatchSnapshot(t, actual)\n}\n\nfunc TestHidesIgnoredMatches(t *testing.T) {\n\tvar buffer bytes.Buffer\n\n\tpb := models.PresenterConfig{\n\t\tDocument: internal.GenerateAnalysisWithIgnoredMatches(t, internal.ImageSource),\n\t}\n\n\tpres := NewPresenter(pb, false)\n\n\terr := pres.Present(&buffer)\n\trequire.NoError(t, err)\n\n\tactual := buffer.String()\n\tsnaps.MatchSnapshot(t, actual)\n}\n\nfunc TestDisplaysIgnoredMatches(t *testing.T) {\n\tvar buffer bytes.Buffer\n\tpb := models.PresenterConfig{\n\t\tDocument: internal.GenerateAnalysisWithIgnoredMatches(t, internal.ImageSource),\n\t}\n\n\tpres := NewPresenter(pb, true)\n\n\terr := pres.Present(&buffer)\n\trequire.NoError(t, err)\n\n\tactual := buffer.String()\n\tsnaps.MatchSnapshot(t, actual)\n}\n\nfunc TestDisplaysDistro(t *testing.T) {\n\tvar buffer bytes.Buffer\n\tpb := models.PresenterConfig{\n\t\tDocument: internal.GenerateAnalysisWithIgnoredMatches(t, internal.ImageSource),\n\t}\n\n\tpb.Document.Matches[0].Vulnerability.Namespace = \"ubuntu:distro:ubuntu:2.5\"\n\tpb.Document.Matches[1].Vulnerability.Namespace = \"ubuntu:distro:ubuntu:3.5\"\n\n\tpres := NewPresenter(pb, false)\n\n\terr := pres.Present(&buffer)\n\trequire.NoError(t, err)\n\n\tactual := buffer.String()\n\tsnaps.MatchSnapshot(t, actual)\n}\n\nfunc TestDisplaysIgnoredMatchesAndDistro(t *testing.T) {\n\tvar buffer bytes.Buffer\n\tpb := models.PresenterConfig{\n\t\tDocument: internal.GenerateAnalysisWithIgnoredMatches(t, internal.ImageSource),\n\t}\n\n\tpb.Document.Matches[0].Vulnerability.Namespace = \"ubuntu:distro:ubuntu:2.5\"\n\tpb.Document.Matches[1].Vulnerability.Namespace = \"ubuntu:distro:ubuntu:3.5\"\n\n\tpb.Document.IgnoredMatches[0].Vulnerability.Namespace = \"ubuntu:distro:ubuntu:2.5\"\n\tpb.Document.IgnoredMatches[1].Vulnerability.Namespace = \"ubuntu:distro:ubuntu:3.5\"\n\n\tpres := NewPresenter(pb, true)\n\n\terr := pres.Present(&buffer)\n\trequire.NoError(t, err)\n\n\tactual := buffer.String()\n\tsnaps.MatchSnapshot(t, actual)\n}\n\nfunc TestRowsRender(t *testing.T) {\n\n\tt.Run(\"empty rows returns empty slice\", func(t *testing.T) {\n\t\tvar rs rows\n\t\tresult := rs.Render()\n\t\tassert.Empty(t, result)\n\t})\n\n\tt.Run(\"deduplicates identical rows\", func(t *testing.T) {\n\t\trs := rows{\n\t\t\tmustRow(t, \"pkg1\", \"1.0.0\", \"1.1.0\", \"os\", \"CVE-2023-1234\", \"critical\", vulnerability.FixStateFixed),\n\t\t\tmustRow(t, \"pkg1\", \"1.0.0\", \"1.1.0\", \"os\", \"CVE-2023-1234\", \"critical\", vulnerability.FixStateFixed),\n\t\t}\n\t\tresult := rs.Render()\n\n\t\texpected := [][]string{\n\t\t\t{\"pkg1\", \"1.0.0\", \"1.1.0\", \"os\", \"CVE-2023-1234\", \"critical\", \"3.0% (75th)\", \"  N/A\"},\n\t\t}\n\n\t\tif diff := cmp.Diff(expected, result); diff != \"\" {\n\t\t\tt.Errorf(\"Render() mismatch (-want +got):\\n%s\", diff)\n\t\t}\n\t})\n\n\tt.Run(\"renders won't fix and empty fix versions correctly\", func(t *testing.T) {\n\t\t// Create rows with different fix states\n\t\trow1 := mustRow(t, \"pkgA\", \"1.0.0\", \"\", \"os\", \"CVE-2023-1234\", \"critical\", vulnerability.FixStateUnknown)\n\t\trow2 := mustRow(t, \"pkgB\", \"2.0.0\", \"\", \"os\", \"CVE-2023-5678\", \"high\", vulnerability.FixStateWontFix)\n\t\trow3 := mustRow(t, \"pkgC\", \"3.0.0\", \"3.1.0\", \"os\", \"CVE-2023-9012\", \"medium\", vulnerability.FixStateFixed)\n\n\t\trs := rows{row1, row2, row3}\n\t\tresult := rs.Render()\n\n\t\texpected := [][]string{\n\t\t\t{\"pkgA\", \"1.0.0\", \"\", \"os\", \"CVE-2023-1234\", \"critical\", \"3.0% (75th)\", \"  N/A\"},\n\t\t\t{\"pkgB\", \"2.0.0\", \"(won't fix)\", \"os\", \"CVE-2023-5678\", \"high\", \"3.0% (75th)\", \"  N/A\"},\n\t\t\t{\"pkgC\", \"3.0.0\", \"3.1.0\", \"os\", \"CVE-2023-9012\", \"medium\", \"3.0% (75th)\", \"  N/A\"},\n\t\t}\n\n\t\tif diff := cmp.Diff(expected, result); diff != \"\" {\n\t\t\tt.Errorf(\"Render() mismatch (-want +got):\\n%s\", diff)\n\t\t}\n\t})\n\n\tt.Run(\"column count matches expectations\", func(t *testing.T) {\n\t\trs := rows{\n\t\t\tmustRow(t, \"pkg1\", \"1.0.0\", \"1.1.0\", \"os\", \"CVE-2023-1234\", \"critical\", vulnerability.FixStateFixed),\n\t\t}\n\t\tresult := rs.Render()\n\n\t\texpected := [][]string{\n\t\t\t{\"pkg1\", \"1.0.0\", \"1.1.0\", \"os\", \"CVE-2023-1234\", \"critical\", \"3.0% (75th)\", \"  N/A\"},\n\t\t}\n\n\t\tif diff := cmp.Diff(expected, result); diff != \"\" {\n\t\t\tt.Errorf(\"Render() mismatch (-want +got):\\n%s\", diff)\n\t\t}\n\n\t\t// expected columns: name, version, fix, packageType, vulnID, severity, epss, risk\n\t\tassert.Len(t, result[0], 8)\n\n\t})\n}\n\nfunc createTestRow(name, version, fix, pkgType, vulnID, severity string, fixState vulnerability.FixState) (row, error) {\n\tm := models.Match{\n\t\tVulnerability: models.Vulnerability{\n\t\t\tFix: models.Fix{\n\t\t\t\tVersions: []string{fix},\n\t\t\t\tState:    fixState.String(),\n\t\t\t},\n\t\t\tVulnerabilityMetadata: models.VulnerabilityMetadata{\n\t\t\t\tID:       vulnID,\n\t\t\t\tSeverity: severity,\n\t\t\t\tCvss: []models.Cvss{\n\t\t\t\t\t{\n\t\t\t\t\t\tSource:  \"nvd\",\n\t\t\t\t\t\tType:    \"CVSS\",\n\t\t\t\t\t\tVersion: \"3.1\",\n\t\t\t\t\t\tVector:  \"CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:L/A:L\",\n\t\t\t\t\t\tMetrics: models.CvssMetrics{\n\t\t\t\t\t\t\tBaseScore: 7.2,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tEPSS: []models.EPSS{\n\t\t\t\t\t{\n\t\t\t\t\t\tCVE:        vulnID,\n\t\t\t\t\t\tEPSS:       0.03,\n\t\t\t\t\t\tPercentile: 0.75,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tArtifact: models.Package{\n\t\t\tName:    name,\n\t\t\tVersion: version,\n\t\t\tType:    syftPkg.Type(pkgType),\n\t\t},\n\t}\n\n\tp := NewPresenter(models.PresenterConfig{}, false)\n\tr := p.newRow(m, \"\", false)\n\n\treturn r, nil\n}\n\nfunc TestEPSS_String(t *testing.T) {\n\ttests := []struct {\n\t\tname       string\n\t\tscore      float64\n\t\tpercentile float64\n\t\texpected   string\n\t}{\n\t\t{\n\t\t\tname:       \"zero percentile should return N/A\",\n\t\t\tscore:      0.0,\n\t\t\tpercentile: 0.0,\n\t\t\texpected:   \"N/A\",\n\t\t},\n\t\t{\n\t\t\tname:       \"very low probability less than 0.1%\",\n\t\t\tscore:      0.0005,\n\t\t\tpercentile: 0.15,\n\t\t\texpected:   \"< 0.1% (15th)\",\n\t\t},\n\t\t{\n\t\t\tname:       \"low probability with 1st percentile\",\n\t\t\tscore:      0.02,\n\t\t\tpercentile: 0.01,\n\t\t\texpected:   \"2.0% (1st)\",\n\t\t},\n\t\t{\n\t\t\tname:       \"medium probability with 2nd percentile\",\n\t\t\tscore:      0.153,\n\t\t\tpercentile: 0.92,\n\t\t\texpected:   \"15.3% (92nd)\",\n\t\t},\n\t\t{\n\t\t\tname:       \"high probability with 3rd percentile\",\n\t\t\tscore:      0.456,\n\t\t\tpercentile: 0.93,\n\t\t\texpected:   \"45.6% (93rd)\",\n\t\t},\n\t\t{\n\t\t\tname:       \"probability with 4th percentile\",\n\t\t\tscore:      0.234,\n\t\t\tpercentile: 0.84,\n\t\t\texpected:   \"23.4% (84th)\",\n\t\t},\n\t\t{\n\t\t\tname:       \"probability with 11th percentile (special case)\",\n\t\t\tscore:      0.125,\n\t\t\tpercentile: 0.11,\n\t\t\texpected:   \"12.5% (11th)\",\n\t\t},\n\t\t{\n\t\t\tname:       \"probability with 12th percentile (special case)\",\n\t\t\tscore:      0.187,\n\t\t\tpercentile: 0.12,\n\t\t\texpected:   \"18.7% (12th)\",\n\t\t},\n\t\t{\n\t\t\tname:       \"probability with 13th percentile (special case)\",\n\t\t\tscore:      0.203,\n\t\t\tpercentile: 0.13,\n\t\t\texpected:   \"20.3% (13th)\",\n\t\t},\n\t\t{\n\t\t\tname:       \"probability with 21st percentile\",\n\t\t\tscore:      0.312,\n\t\t\tpercentile: 0.21,\n\t\t\texpected:   \"31.2% (21st)\",\n\t\t},\n\t\t{\n\t\t\tname:       \"probability with 22nd percentile\",\n\t\t\tscore:      0.345,\n\t\t\tpercentile: 0.22,\n\t\t\texpected:   \"34.5% (22nd)\",\n\t\t},\n\t\t{\n\t\t\tname:       \"probability with 23rd percentile\",\n\t\t\tscore:      0.378,\n\t\t\tpercentile: 0.23,\n\t\t\texpected:   \"37.8% (23rd)\",\n\t\t},\n\t\t{\n\t\t\tname:       \"high percentile with 99th\",\n\t\t\tscore:      0.789,\n\t\t\tpercentile: 0.99,\n\t\t\texpected:   \"78.9% (99th)\",\n\t\t},\n\t\t{\n\t\t\tname:       \"maximum probability and percentile\",\n\t\t\tscore:      1.0,\n\t\t\tpercentile: 1.0,\n\t\t\texpected:   \"100.0% (100th)\",\n\t\t},\n\t\t{\n\t\t\tname:       \"very small non-zero probability\",\n\t\t\tscore:      0.001,\n\t\t\tpercentile: 0.05,\n\t\t\texpected:   \"0.1% (5th)\",\n\t\t},\n\t\t{\n\t\t\tname:       \"edge case: exactly 0.1% probability\",\n\t\t\tscore:      0.001,\n\t\t\tpercentile: 0.08,\n\t\t\texpected:   \"0.1% (8th)\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\te := epss{\n\t\t\t\tScore:      tt.score,\n\t\t\t\tPercentile: tt.percentile,\n\t\t\t}\n\t\t\tresult := e.String()\n\t\t\tassert.Equal(t, tt.expected, result)\n\t\t})\n\t}\n}\n\nfunc mustRow(t *testing.T, name, version, fix, pkgType, vulnID, severity string, fixState vulnerability.FixState) row {\n\tr, err := createTestRow(name, version, fix, pkgType, vulnID, severity, fixState)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to create test row: %v\", err)\n\t}\n\treturn r\n}\n"
  },
  {
    "path": "grype/presenter/table/testdata/snapshot/TestTablePresenter_Color.golden",
    "content": "NAME       INSTALLED  FIXED-IN          TYPE  VULNERABILITY  SEVERITY \npackage-1  1.1.1      the-next-version  rpm   CVE-1999-0001  \u001b[0;32mLow\u001b[0m       \npackage-2  2.2.2                        deb   CVE-1999-0002  \u001b[1;31mCritical\u001b[0m  \n"
  },
  {
    "path": "grype/presenter/template/presenter.go",
    "content": "package template\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"reflect\"\n\t\"text/template\"\n\n\t\"github.com/Masterminds/sprig/v3\"\n\n\t\"github.com/anchore/clio\"\n\t\"github.com/anchore/go-homedir\"\n\t\"github.com/anchore/grype/grype/presenter/models\"\n)\n\n// Presenter is an implementation of presenter.Presenter that formats output according to a user-provided Go text template.\ntype Presenter struct {\n\tid                 clio.Identification\n\tdocument           models.Document\n\tpathToTemplateFile string\n}\n\n// NewPresenter returns a new template.Presenter.\nfunc NewPresenter(pb models.PresenterConfig, templateFile string) *Presenter {\n\treturn &Presenter{\n\t\tid:                 pb.ID,\n\t\tdocument:           pb.Document,\n\t\tpathToTemplateFile: templateFile,\n\t}\n}\n\n// Present creates output using a user-supplied Go template.\nfunc (pres *Presenter) Present(output io.Writer) error {\n\texpandedPathToTemplateFile, err := homedir.Expand(pres.pathToTemplateFile)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to expand path %q\", pres.pathToTemplateFile)\n\t}\n\n\ttemplateContents, err := os.ReadFile(expandedPathToTemplateFile)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to get output template: %w\", err)\n\t}\n\n\ttemplateName := expandedPathToTemplateFile\n\ttmpl, err := template.New(templateName).Funcs(FuncMap).Parse(string(templateContents))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to parse template: %w\", err)\n\t}\n\n\terr = tmpl.Execute(output, pres.document)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to execute supplied template: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// FuncMap is a function that returns template.FuncMap with custom functions available to template authors.\nvar FuncMap = func() template.FuncMap {\n\tf := sprig.HermeticTxtFuncMap()\n\tf[\"getLastIndex\"] = func(collection interface{}) int {\n\t\tif v := reflect.ValueOf(collection); v.Kind() == reflect.Slice {\n\t\t\treturn v.Len() - 1\n\t\t}\n\n\t\treturn 0\n\t}\n\tf[\"byMatchName\"] = func(collection interface{}) interface{} {\n\t\tmatches, ok := collection.([]models.Match)\n\t\tif !ok {\n\t\t\treturn collection\n\t\t}\n\n\t\tmodels.SortMatches(matches, models.SortByPackage)\n\t\treturn matches\n\t}\n\treturn f\n}()\n"
  },
  {
    "path": "grype/presenter/template/presenter_test.go",
    "content": "package template\n\nimport (\n\t\"bytes\"\n\t\"flag\"\n\t\"os\"\n\t\"path\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/grype/grype/presenter/internal\"\n\t\"github.com/anchore/grype/internal/testutils\"\n)\n\nvar update = flag.Bool(\"update\", false, \"update the *.golden files for template presenters\")\n\nfunc TestPresenter_Present(t *testing.T) {\n\tworkingDirectory, err := os.Getwd()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\ttemplateFilePath := path.Join(workingDirectory, \"./testdata/test.template\")\n\n\tpb := internal.GeneratePresenterConfig(t, internal.ImageSource)\n\n\ttemplatePresenter := NewPresenter(pb, templateFilePath)\n\n\tvar buffer bytes.Buffer\n\tif err := templatePresenter.Present(&buffer); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tactual := buffer.Bytes()\n\n\tif *update {\n\t\ttestutils.UpdateGoldenFileContents(t, actual)\n\t}\n\texpected := testutils.GetGoldenFileContents(t)\n\n\tassert.Equal(t, string(expected), string(actual))\n}\n\nfunc TestPresenter_SprigDate_Fails(t *testing.T) {\n\tworkingDirectory, err := os.Getwd()\n\trequire.NoError(t, err)\n\n\t// this template has the generic sprig date function, which is intentionally not supported for security reasons\n\ttemplateFilePath := path.Join(workingDirectory, \"./testdata/test.template.sprig.date\")\n\n\tpb := internal.GeneratePresenterConfig(t, internal.ImageSource)\n\n\ttemplatePresenter := NewPresenter(pb, templateFilePath)\n\n\tvar buffer bytes.Buffer\n\terr = templatePresenter.Present(&buffer)\n\trequire.ErrorContains(t, err, `function \"now\" not defined`)\n}\n"
  },
  {
    "path": "grype/presenter/template/testdata/snapshot/TestPresenter_Present.golden",
    "content": "Identified distro as centos version 8.0.\n    Vulnerability: CVE-1999-0001\n    Severity: Low\n    Package: package-1 version 1.1.1 (rpm)\n    CPEs: [\"cpe:2.3:a:anchore\\\\:oss:anchore\\\\/engine:0.9.2:*:*:en:*:*:*:*\"]\n    Matched by: dpkg-matcher\n    Vulnerability: CVE-1999-0002\n    Severity: Critical\n    Package: package-2 version 2.2.2 (deb)\n    CPEs: [\"cpe:2.3:a:anchore:engine:2.2.2:*:*:en:*:*:*:*\"]\n    Matched by: dpkg-matcher\n\n"
  },
  {
    "path": "grype/presenter/template/testdata/test.template",
    "content": "Identified distro as {{.Distro.Name}} version {{.Distro.Version}}.\n{{- range .Matches}}\n    Vulnerability: {{.Vulnerability.ID}}\n    Severity: {{.Vulnerability.Severity}}\n    Package: {{.Artifact.Name}} version {{.Artifact.Version}} ({{.Artifact.Type}})\n    CPEs: {{ toJson .Artifact.CPEs }}\n    {{- range .MatchDetails}}\n    Matched by: {{.Matcher}}\n    {{- end}}\n{{- end}}\n\n"
  },
  {
    "path": "grype/presenter/template/testdata/test.template.sprig.date",
    "content": "Identified distro as {{.Distro.Name}} version {{.Distro.Version}}.\nDate: {{ now | date \"2006-01-02\" }}\n{{- range .Matches}}\n    Vulnerability: {{.Vulnerability.ID}}\n    Severity: {{.Vulnerability.Severity}}\n    Package: {{.Artifact.Name}} version {{.Artifact.Version}} ({{.Artifact.Type}})\n    CPEs: {{ toJson .Artifact.CPEs }}\n    {{- range .MatchDetails}}\n    Matched by: {{.Matcher}}\n    {{- end}}\n{{- end}}\n"
  },
  {
    "path": "grype/presenter/template/testdata/test.valid.template",
    "content": "Identified distro as {{.Distro.Name}} version {{.Distro.Version}}.\n{{- range .Matches}}\n    Vulnerability: {{.Vulnerability.ID}}\n    CVE: {{trimPrefix \"CVE-\" .Vulnerability.ID}}\n    Severity: {{.Vulnerability.Severity}}\n    Package: {{.Artifact.Name}} version {{.Artifact.Version}} ({{.Artifact.Type}})\n    {{- range .MatchDetails}}\n    Matched by: {{.Matcher}}\n    {{- end}}\n{{- end}}\n\n"
  },
  {
    "path": "grype/search/cpe.go",
    "content": "package search\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/anchore/grype/grype/vulnerability\"\n\t\"github.com/anchore/syft/syft/cpe\"\n)\n\nvar _ interface {\n\tvulnerability.Criteria\n} = (*CPECriteria)(nil)\n\ntype CPECriteria struct {\n\tCPE cpe.CPE\n}\n\n// ByCPE returns criteria which will search based on any of the provided CPEs\nfunc ByCPE(c cpe.CPE) vulnerability.Criteria {\n\treturn &CPECriteria{\n\t\tCPE: c,\n\t}\n}\n\nfunc (v *CPECriteria) MatchesVulnerability(vuln vulnerability.Vulnerability) (bool, string, error) {\n\tif containsCPE(vuln.CPEs, v.CPE) {\n\t\treturn true, \"\", nil\n\t}\n\treturn false, \"CPE attributes do not match\", nil\n}\n\nfunc (v *CPECriteria) Summarize() string {\n\treturn fmt.Sprintf(\"does not match CPE: %s\", v.CPE.Attributes.BindToFmtString())\n}\n\n// containsCPE returns true if the provided slice contains a matching CPE based on attributes matching\nfunc containsCPE(cpes []cpe.CPE, cpe cpe.CPE) bool {\n\tfor _, c := range cpes {\n\t\tif matchesAttributes(cpe.Attributes, c.Attributes) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc matchesAttributes(a1 cpe.Attributes, a2 cpe.Attributes) bool {\n\tif !matchesAttribute(a1.Product, a2.Product) ||\n\t\t!matchesAttribute(a1.Vendor, a2.Vendor) ||\n\t\t!matchesAttribute(a1.Part, a2.Part) ||\n\t\t!matchesAttribute(a1.Language, a2.Language) ||\n\t\t!matchesAttribute(a1.SWEdition, a2.SWEdition) ||\n\t\t!matchesAttribute(a1.TargetSW, a2.TargetSW) ||\n\t\t!matchesAttribute(a1.TargetHW, a2.TargetHW) ||\n\t\t!matchesAttribute(a1.Other, a2.Other) ||\n\t\t!matchesAttribute(a1.Edition, a2.Edition) {\n\t\treturn false\n\t}\n\treturn true\n}\n\nfunc matchesAttribute(a1, a2 string) bool {\n\treturn a1 == \"\" || a2 == \"\" || strings.EqualFold(a1, a2)\n}\n"
  },
  {
    "path": "grype/search/cpe_test.go",
    "content": "package search\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/grype/grype/vulnerability\"\n\t\"github.com/anchore/syft/syft/cpe\"\n)\n\nfunc Test_ByCPE(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\tcpe     cpe.CPE\n\t\tinput   vulnerability.Vulnerability\n\t\twantErr require.ErrorAssertionFunc\n\t\tmatches bool\n\t\treason  string\n\t}{\n\t\t{\n\t\t\tname: \"match\",\n\t\t\tcpe:  cpe.Must(\"cpe:2.3:a:a-vendor:a-product:*:*:*:*:*:*:*:*\", \"\"),\n\t\t\tinput: vulnerability.Vulnerability{\n\t\t\t\tCPEs: []cpe.CPE{cpe.Must(\"cpe:2.3:a:a-vendor:a-product:*:*:*:*:*:*:*:*\", \"\")},\n\t\t\t},\n\t\t\tmatches: true,\n\t\t},\n\t\t{\n\t\t\tname: \"not match\",\n\t\t\tcpe:  cpe.Must(\"cpe:2.3:a:a-vendor:b-product:*:*:*:*:*:*:*:*\", \"\"),\n\t\t\tinput: vulnerability.Vulnerability{\n\t\t\t\tCPEs: []cpe.CPE{cpe.Must(\"cpe:2.3:a:a-vendor:a-product:*:*:*:*:*:*:*:*\", \"\")},\n\t\t\t},\n\t\t\tmatches: false,\n\t\t\treason:  \"CPE attributes do not match\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tconstraint := ByCPE(tt.cpe)\n\t\t\tmatches, reason, err := constraint.MatchesVulnerability(tt.input)\n\t\t\twantErr := require.NoError\n\t\t\tif tt.wantErr != nil {\n\t\t\t\twantErr = tt.wantErr\n\t\t\t}\n\t\t\twantErr(t, err)\n\t\t\tassert.Equal(t, tt.matches, matches)\n\t\t\tassert.Equal(t, tt.reason, reason)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/search/criteria.go",
    "content": "package search\n\nimport (\n\t\"fmt\"\n\t\"iter\"\n\t\"reflect\"\n\t\"slices\"\n\t\"strings\"\n\n\t\"github.com/anchore/grype/grype/vulnerability\"\n)\n\n// ------- Utilities -------\n\n// CriteriaIterator processes all conditions into distinct sets of flattened criteria\nfunc CriteriaIterator(criteria []vulnerability.Criteria) iter.Seq2[int, []vulnerability.Criteria] {\n\tif len(criteria) == 0 {\n\t\treturn func(_ func(int, []vulnerability.Criteria) bool) {}\n\t}\n\treturn func(yield func(int, []vulnerability.Criteria) bool) {\n\t\tidx := 0\n\t\tfn := func(criteria []vulnerability.Criteria) bool {\n\t\t\tout := yield(idx, criteria)\n\t\t\tidx++\n\t\t\treturn out\n\t\t}\n\t\t_ = processRemaining(nil, criteria, fn)\n\t}\n}\n\nfunc processRemaining(row, criteria []vulnerability.Criteria, yield func([]vulnerability.Criteria) bool) bool {\n\tif len(criteria) == 0 {\n\t\treturn yield(row)\n\t}\n\treturn processRemainingItem(row, criteria[1:], criteria[0], yield)\n}\n\nfunc processRemainingItem(row, criteria []vulnerability.Criteria, item vulnerability.Criteria, yield func([]vulnerability.Criteria) bool) bool {\n\tswitch item := item.(type) {\n\tcase and:\n\t\t// we replace this criteria object with its constituent parts\n\t\treturn processRemaining(row, append(item, criteria...), yield)\n\tcase or:\n\t\tfor _, option := range item {\n\t\t\tif !processRemainingItem(row, criteria, option, yield) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\tdefault:\n\t\treturn processRemaining(append(row, item), criteria, yield)\n\t}\n\treturn true // continue\n}\n\nvar allowedMultipleCriteria = []reflect.Type{reflect.TypeOf(funcCriteria{})}\n\n// ValidateCriteria asserts that there are no incorrect duplications of criteria\n// e.g. multiple ByPackageName() which would result in no matches, while Or(pkgName1, pkgName2) is allowed\nfunc ValidateCriteria(criteria []vulnerability.Criteria) error {\n\tfor _, row := range CriteriaIterator(criteria) { // process OR conditions into flattened lists of AND conditions\n\t\tseenTypes := make(map[reflect.Type]interface{})\n\n\t\tfor _, criterion := range row {\n\t\t\tcriterionType := reflect.TypeOf(criterion)\n\n\t\t\tif slices.Contains(allowedMultipleCriteria, criterionType) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif previous, exists := seenTypes[criterionType]; exists {\n\t\t\t\treturn fmt.Errorf(\"multiple conflicting criteria specified: %+v %+v\", previous, criterion)\n\t\t\t}\n\n\t\t\tseenTypes[criterionType] = criterion\n\t\t}\n\t}\n\treturn nil\n}\n\nvar _ interface {\n\tvulnerability.Criteria\n} = (*or)(nil)\n\n// orCriteria provides a way to specify multiple criteria to be used, only requiring one to match\ntype or []vulnerability.Criteria\n\nfunc Or(criteria ...vulnerability.Criteria) vulnerability.Criteria {\n\treturn or(criteria)\n}\n\nfunc (c or) MatchesVulnerability(v vulnerability.Vulnerability) (bool, string, error) {\n\tvar reasons []string\n\tfor _, crit := range c {\n\t\tmatches, reason, err := crit.MatchesVulnerability(v)\n\t\tif matches || err != nil {\n\t\t\treturn matches, reason, err\n\t\t}\n\t\treasons = append(reasons, reason)\n\t}\n\treturn false, fmt.Sprintf(\"any(%s)\", strings.Join(reasons, \"; \")), nil\n}\n\nvar _ interface {\n\tvulnerability.Criteria\n} = (*and)(nil)\n\n// andCriteria provides a way to specify multiple criteria to be used, all required\ntype and []vulnerability.Criteria\n\nfunc And(criteria ...vulnerability.Criteria) vulnerability.Criteria {\n\treturn and(criteria)\n}\n\nfunc (c and) MatchesVulnerability(v vulnerability.Vulnerability) (bool, string, error) {\n\tvar reasons []string\n\n\tfor _, crit := range c {\n\t\tmatches, reason, err := crit.MatchesVulnerability(v)\n\t\tif matches || err != nil {\n\t\t\treturn matches, reason, err\n\t\t}\n\t\treasons = append(reasons, reason)\n\t}\n\treturn false, fmt.Sprintf(\"all(%s)\", strings.Join(reasons, \"; \")), nil\n}\n"
  },
  {
    "path": "grype/search/criteria_test.go",
    "content": "package search\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/grype/grype/distro\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n)\n\nfunc Test_CriteriaIterator(t *testing.T) {\n\tname1 := ByPackageName(\"name1\")\n\tname2 := ByPackageName(\"name2\")\n\tname3 := ByPackageName(\"name3\")\n\n\ttests := []struct {\n\t\tname     string\n\t\tin       []vulnerability.Criteria\n\t\texpected [][]vulnerability.Criteria\n\t}{\n\t\t{\n\t\t\tname:     \"empty\",\n\t\t\tin:       nil,\n\t\t\texpected: nil,\n\t\t},\n\t\t{\n\t\t\tname:     \"one\",\n\t\t\tin:       []vulnerability.Criteria{name1},\n\t\t\texpected: [][]vulnerability.Criteria{{name1}},\n\t\t},\n\t\t{\n\t\t\tname:     \"name1 or name2\",\n\t\t\tin:       []vulnerability.Criteria{Or(name1, name2)},\n\t\t\texpected: [][]vulnerability.Criteria{{name1}, {name2}},\n\t\t},\n\t\t{\n\t\t\tname:     \"name1 AND (name2 or name3)\",\n\t\t\tin:       []vulnerability.Criteria{name1, Or(name2, name3)},\n\t\t\texpected: [][]vulnerability.Criteria{{name1, name2}, {name1, name3}},\n\t\t},\n\t\t{\n\t\t\tname: \"name1 AND (name2 or name3) AND (name1 or name2 or name3)\",\n\t\t\tin:   []vulnerability.Criteria{name1, Or(name2, name3), Or(name1, name2, name3)},\n\t\t\texpected: [][]vulnerability.Criteria{\n\t\t\t\t{name1, name2, name1}, {name1, name3, name1},\n\t\t\t\t{name1, name2, name2}, {name1, name3, name2},\n\t\t\t\t{name1, name2, name3}, {name1, name3, name3},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"(name1 AND name2) OR (name1 AND name3)\",\n\t\t\tin:   []vulnerability.Criteria{Or(And(name1, name2), And(name1, name3))},\n\t\t\texpected: [][]vulnerability.Criteria{\n\t\t\t\t{name1, name2}, {name1, name3},\n\t\t\t},\n\t\t},\n\t}\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tvar got [][]vulnerability.Criteria\n\t\t\tfor _, row := range CriteriaIterator(test.in) {\n\t\t\t\tgot = append(got, row)\n\t\t\t}\n\t\t\trequire.ElementsMatch(t, test.expected, got)\n\t\t})\n\t}\n}\n\nfunc Test_ValidateCriteria(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\tin      []vulnerability.Criteria\n\t\twantErr require.ErrorAssertionFunc\n\t}{\n\t\t{\n\t\t\tname:    \"no error\",\n\t\t\tin:      []vulnerability.Criteria{ByPackageName(\"steve\"), ByDistro(distro.Distro{})},\n\t\t\twantErr: require.NoError,\n\t\t},\n\t\t{\n\t\t\tname:    \"package name error\",\n\t\t\tin:      []vulnerability.Criteria{ByPackageName(\"steve\"), ByPackageName(\"bob\")},\n\t\t\twantErr: require.Error,\n\t\t},\n\t\t{\n\t\t\tname:    \"multiple distros error\",\n\t\t\tin:      []vulnerability.Criteria{ByDistro(distro.Distro{}), ByDistro(distro.Distro{})},\n\t\t\twantErr: require.Error,\n\t\t},\n\t\t{\n\t\t\tname:    \"multiple package name in or condition not error\",\n\t\t\tin:      []vulnerability.Criteria{Or(ByPackageName(\"steve\"), ByPackageName(\"bob\"))},\n\t\t\twantErr: require.NoError,\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\terr := ValidateCriteria(test.in)\n\t\t\ttest.wantErr(t, err)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/search/distro.go",
    "content": "package search\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/anchore/grype/grype/db/v5/namespace\"\n\tdistroNs \"github.com/anchore/grype/grype/db/v5/namespace/distro\"\n\t\"github.com/anchore/grype/grype/distro\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n)\n\n// ByDistro returns criteria which will match vulnerabilities based on any of the provided Distros\nfunc ByDistro(d ...distro.Distro) vulnerability.Criteria {\n\treturn &DistroCriteria{\n\t\tDistros: d,\n\t\tExact:   false,\n\t}\n}\n\n// ByExactDistro returns criteria which will match vulnerabilities based on any of the provided Distros\n// without applying alias mappings. This is useful when you need to find records specific to a distro\n// that would normally be aliased to another (e.g., AlmaLinux-specific records vs RHEL records).\nfunc ByExactDistro(d ...distro.Distro) vulnerability.Criteria {\n\treturn &DistroCriteria{\n\t\tDistros: d,\n\t\tExact:   true,\n\t}\n}\n\ntype DistroCriteria struct {\n\tDistros []distro.Distro\n\tExact   bool // if true, disable alias mappings (e.g., AlmaLinux -> RHEL)\n}\n\nfunc (c *DistroCriteria) MatchesVulnerability(value vulnerability.Vulnerability) (bool, string, error) {\n\tns, err := namespace.FromString(value.Namespace)\n\tif err != nil {\n\t\treturn false, fmt.Sprintf(\"unable to determine namespace for vulnerability %v: %v\", value.ID, err), nil\n\t}\n\tdns, ok := ns.(*distroNs.Namespace)\n\tif !ok || dns == nil {\n\t\t// not a Distro-based vulnerability\n\t\treturn false, \"not a distro-based vulnerability\", nil\n\t}\n\tif len(c.Distros) == 0 {\n\t\treturn true, \"\", nil\n\t}\n\tvar distroStrs []string\n\tfor _, d := range c.Distros {\n\t\tvar matches bool\n\t\tif c.Exact {\n\t\t\tmatches = matchesExactDistro(&d, dns)\n\t\t} else {\n\t\t\tmatches = matchesDistro(&d, dns)\n\t\t}\n\t\tif matches {\n\t\t\treturn true, \"\", nil\n\t\t}\n\t\tdistroStrs = append(distroStrs, d.String())\n\t}\n\n\treturn false, fmt.Sprintf(\"does not match any known distro: %q\", strings.Join(distroStrs, \", \")), nil\n}\n\nfunc (c *DistroCriteria) Summarize() string {\n\tvar distroStrs []string\n\tfor _, d := range c.Distros {\n\t\tdistroStrs = append(distroStrs, d.String())\n\t}\n\treturn \"does not match distro(s): \" + strings.Join(distroStrs, \", \")\n}\n\nvar _ interface {\n\tvulnerability.Criteria\n} = (*DistroCriteria)(nil)\n\n// matchesDistro returns true distro types are equal and versions are compatible\nfunc matchesDistro(d *distro.Distro, ns *distroNs.Namespace) bool {\n\tif d == nil || ns == nil {\n\t\treturn false\n\t}\n\n\tdistroType := mimicV6DistroTypeOverrides(ns.DistroType())\n\ttargetType := mimicV6DistroTypeOverrides(d.Type)\n\tif distroType != targetType {\n\t\treturn false\n\t}\n\n\treturn compatibleVersion(d.Version, ns.Version())\n}\n\n// compatibleVersion returns true when the versions are the same or the partial version describes the matching parts\n// of the fullVersion\nfunc compatibleVersion(fullVersion string, partialVersion string) bool {\n\tif fullVersion == \"\" {\n\t\treturn true\n\t}\n\tif fullVersion == partialVersion {\n\t\treturn true\n\t}\n\tif strings.HasPrefix(fullVersion, partialVersion) && len(fullVersion) > len(partialVersion) && fullVersion[len(partialVersion)] == '.' {\n\t\treturn true\n\t}\n\treturn false\n}\n\n// TODO: this is a temporary workaround... in the long term the mock should more strongly enforce\n// data overrides and not require this kind of logic being baked into mocks directly.\nfunc mimicV6DistroTypeOverrides(t distro.Type) distro.Type {\n\toverrideMap := map[string]string{\n\t\t\"centos\":      \"rhel\",\n\t\t\"rocky\":       \"rhel\",\n\t\t\"rockylinux\":  \"rhel\",\n\t\t\"alma\":        \"rhel\",\n\t\t\"almalinux\":   \"rhel\",\n\t\t\"gentoo\":      \"rhel\",\n\t\t\"archlinux\":   \"arch\",\n\t\t\"oracle\":      \"ol\",\n\t\t\"oraclelinux\": \"ol\",\n\t\t\"amazon\":      \"amzn\",\n\t\t\"amazonlinux\": \"amzn\",\n\t}\n\n\tapplyMapping := func(i string) distro.Type {\n\t\tif replacement, exists := distro.IDMapping[i]; exists {\n\t\t\treturn replacement\n\t\t}\n\t\treturn distro.Type(i)\n\t}\n\n\tif replacement, exists := overrideMap[string(t)]; exists {\n\t\treturn applyMapping(replacement)\n\t}\n\n\treturn applyMapping(string(t))\n}\n\n// matchesExactDistro returns true when distro types are equal and versions are compatible,\n// without applying any alias mappings\nfunc matchesExactDistro(d *distro.Distro, ns *distroNs.Namespace) bool {\n\tif d == nil || ns == nil {\n\t\treturn false\n\t}\n\n\t// Compare distro types directly without any alias mappings\n\tif string(d.Type) != string(ns.DistroType()) {\n\t\treturn false\n\t}\n\n\treturn compatibleVersion(d.Version, ns.Version())\n}\n"
  },
  {
    "path": "grype/search/distro_test.go",
    "content": "package search\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/grype/grype/distro\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n)\n\nfunc Test_ByDistro(t *testing.T) {\n\tdeb8 := distro.New(distro.Debian, \"8\", \"\")\n\n\ttests := []struct {\n\t\tname    string\n\t\tdistro  distro.Distro\n\t\tinput   vulnerability.Vulnerability\n\t\twantErr require.ErrorAssertionFunc\n\t\tmatches bool\n\t\treason  string\n\t}{\n\t\t{\n\t\t\tname:   \"match\",\n\t\t\tdistro: *deb8,\n\t\t\tinput: vulnerability.Vulnerability{\n\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\tNamespace: \"debian:distro:debian:8\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tmatches: true,\n\t\t},\n\t\t{\n\t\t\tname:   \"not match\",\n\t\t\tdistro: *deb8,\n\t\t\tinput: vulnerability.Vulnerability{\n\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\tNamespace: \"debian:distro:ubuntu:8\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tmatches: false,\n\t\t\treason:  `does not match any known distro: \"debian 8\"`,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tconstraint := ByDistro(tt.distro)\n\t\t\tmatches, reason, err := constraint.MatchesVulnerability(tt.input)\n\t\t\twantErr := require.NoError\n\t\t\tif tt.wantErr != nil {\n\t\t\t\twantErr = tt.wantErr\n\t\t\t}\n\t\t\twantErr(t, err)\n\t\t\tassert.Equal(t, tt.matches, matches)\n\t\t\tassert.Equal(t, tt.reason, reason)\n\t\t})\n\t}\n}\n\nfunc Test_ByExactDistro(t *testing.T) {\n\talma8 := distro.New(distro.AlmaLinux, \"8\", \"\")\n\trhel8 := distro.New(distro.RedHat, \"8\", \"\")\n\n\ttests := []struct {\n\t\tname    string\n\t\tdistro  distro.Distro\n\t\tinput   vulnerability.Vulnerability\n\t\twantErr require.ErrorAssertionFunc\n\t\tmatches bool\n\t\treason  string\n\t}{\n\t\t{\n\t\t\tname:   \"exact match - AlmaLinux\",\n\t\t\tdistro: *alma8,\n\t\t\tinput: vulnerability.Vulnerability{\n\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\tNamespace: \"almalinux:distro:almalinux:8\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tmatches: true,\n\t\t},\n\t\t{\n\t\t\tname:   \"no alias mapping - AlmaLinux vs RHEL\",\n\t\t\tdistro: *alma8,\n\t\t\tinput: vulnerability.Vulnerability{\n\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\tNamespace: \"redhat:distro:redhat:8\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tmatches: false,\n\t\t\treason:  `does not match any known distro: \"almalinux 8\"`,\n\t\t},\n\t\t{\n\t\t\tname:   \"exact match - RHEL\",\n\t\t\tdistro: *rhel8,\n\t\t\tinput: vulnerability.Vulnerability{\n\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\tNamespace: \"redhat:distro:redhat:8\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tmatches: true,\n\t\t},\n\t\t{\n\t\t\tname:   \"no alias mapping - RHEL vs AlmaLinux\",\n\t\t\tdistro: *rhel8,\n\t\t\tinput: vulnerability.Vulnerability{\n\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\tNamespace: \"almalinux:distro:almalinux:8\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tmatches: false,\n\t\t\treason:  `does not match any known distro: \"rhel 8\"`,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tconstraint := ByExactDistro(tt.distro)\n\t\t\tmatches, reason, err := constraint.MatchesVulnerability(tt.input)\n\t\t\twantErr := require.NoError\n\t\t\tif tt.wantErr != nil {\n\t\t\t\twantErr = tt.wantErr\n\t\t\t}\n\t\t\twantErr(t, err)\n\t\t\tassert.Equal(t, tt.matches, matches)\n\t\t\tassert.Equal(t, tt.reason, reason)\n\t\t})\n\t}\n}\n\nfunc Test_ByDistro_vs_ByExactDistro_AliasMapping(t *testing.T) {\n\talma8 := distro.New(distro.AlmaLinux, \"8\", \"\")\n\n\trhelVuln := vulnerability.Vulnerability{\n\t\tReference: vulnerability.Reference{\n\t\t\tNamespace: \"redhat:distro:redhat:8\",\n\t\t},\n\t}\n\n\t// Test that ByDistro applies alias mapping (AlmaLinux -> RHEL)\n\tregularConstraint := ByDistro(*alma8)\n\tmatches, _, err := regularConstraint.MatchesVulnerability(rhelVuln)\n\trequire.NoError(t, err)\n\tassert.True(t, matches, \"ByDistro should match RHEL vulns for AlmaLinux due to alias mapping\")\n\n\t// Test that ByExactDistro does NOT apply alias mapping\n\texactConstraint := ByExactDistro(*alma8)\n\tmatches, _, err = exactConstraint.MatchesVulnerability(rhelVuln)\n\trequire.NoError(t, err)\n\tassert.False(t, matches, \"ByExactDistro should NOT match RHEL vulns for AlmaLinux (no alias mapping)\")\n}\n"
  },
  {
    "path": "grype/search/ecosystem.go",
    "content": "package search\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/anchore/grype/grype/db/v5/namespace\"\n\t\"github.com/anchore/grype/grype/db/v5/namespace/language\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n\tsyftPkg \"github.com/anchore/syft/syft/pkg\"\n)\n\n// ByEcosystem returns criteria which will search based on the package Language and or package type\nfunc ByEcosystem(lang syftPkg.Language, t syftPkg.Type) vulnerability.Criteria {\n\treturn &EcosystemCriteria{\n\t\tLanguage:    lang,\n\t\tPackageType: t,\n\t}\n}\n\ntype EcosystemCriteria struct {\n\tLanguage    syftPkg.Language\n\tPackageType syftPkg.Type\n}\n\nfunc (c *EcosystemCriteria) MatchesVulnerability(value vulnerability.Vulnerability) (bool, string, error) {\n\tns, err := namespace.FromString(value.Namespace)\n\tif err != nil {\n\t\treturn false, fmt.Sprintf(\"unable to determine namespace for vulnerability %v: %v\", value.ID, err), nil\n\t}\n\tlang, ok := ns.(*language.Namespace)\n\tif !ok || lang == nil {\n\t\t// not a language-based vulnerability\n\t\treturn false, \"not a language-based vulnerability\", nil\n\t}\n\n\t// TODO: add package type?\n\n\tvulnLanguage := lang.Language()\n\tmatchesLanguage := c.Language == vulnLanguage\n\tif !matchesLanguage {\n\t\treturn false, fmt.Sprintf(\"vulnerability language %q does not match package language %q\", vulnLanguage, c.Language), nil\n\t}\n\n\treturn true, \"\", nil\n}\n\nvar _ interface {\n\tvulnerability.Criteria\n} = (*EcosystemCriteria)(nil)\n"
  },
  {
    "path": "grype/search/ecosystem_test.go",
    "content": "package search\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/grype/grype/vulnerability\"\n\tsyftPkg \"github.com/anchore/syft/syft/pkg\"\n)\n\nfunc Test_ByLanguage(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\tlang    syftPkg.Language\n\t\tpkgType syftPkg.Type\n\t\tinput   vulnerability.Vulnerability\n\t\twantErr require.ErrorAssertionFunc\n\t\tmatches bool\n\t\treason  string\n\t}{\n\t\t{\n\t\t\tname:    \"match\",\n\t\t\tlang:    syftPkg.Java,\n\t\t\tpkgType: syftPkg.JavaPkg,\n\t\t\tinput: vulnerability.Vulnerability{\n\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\tNamespace: \"github:language:java\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tmatches: true,\n\t\t},\n\t\t{\n\t\t\tname:    \"not match\",\n\t\t\tlang:    syftPkg.Java,\n\t\t\tpkgType: syftPkg.JavaPkg,\n\t\t\tinput: vulnerability.Vulnerability{\n\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\tNamespace: \"github:language:javascript\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tmatches: false,\n\t\t\treason:  `vulnerability language \"javascript\" does not match package language \"java\"`,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tconstraint := ByEcosystem(tt.lang, tt.pkgType)\n\t\t\tmatches, reason, err := constraint.MatchesVulnerability(tt.input)\n\t\t\twantErr := require.NoError\n\t\t\tif tt.wantErr != nil {\n\t\t\t\twantErr = tt.wantErr\n\t\t\t}\n\t\t\twantErr(t, err)\n\t\t\tassert.Equal(t, tt.matches, matches)\n\t\t\tassert.Equal(t, tt.reason, reason)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/search/func.go",
    "content": "package search\n\nimport \"github.com/anchore/grype/grype/vulnerability\"\n\n// ByFunc returns criteria which will use the provided function to filter vulnerabilities\nfunc ByFunc(criteriaFunc func(vulnerability.Vulnerability) (bool, string, error)) vulnerability.Criteria {\n\treturn funcCriteria{fn: criteriaFunc}\n}\n\n// funcCriteria implements vulnerability.Criteria by providing a function implementing the same signature as MatchVulnerability\ntype funcCriteria struct {\n\tfn func(vulnerability.Vulnerability) (bool, string, error)\n}\n\nfunc (f funcCriteria) MatchesVulnerability(value vulnerability.Vulnerability) (bool, string, error) {\n\treturn f.fn(value)\n}\n\nvar _ vulnerability.Criteria = (*funcCriteria)(nil)\n"
  },
  {
    "path": "grype/search/func_test.go",
    "content": "package search\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/grype/grype/vulnerability\"\n)\n\nfunc Test_ByFunc(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\tfn      func(vulnerability.Vulnerability) (bool, string, error)\n\t\tinput   vulnerability.Vulnerability\n\t\twantErr require.ErrorAssertionFunc\n\t\tmatches bool\n\t\treason  string\n\t}{\n\t\t{\n\t\t\tname: \"match\",\n\t\t\tfn: func(v vulnerability.Vulnerability) (bool, string, error) {\n\t\t\t\treturn true, \"\", nil\n\t\t\t},\n\t\t\tinput:   vulnerability.Vulnerability{},\n\t\t\tmatches: true,\n\t\t},\n\t\t{\n\t\t\tname: \"not match\",\n\t\t\tfn: func(v vulnerability.Vulnerability) (bool, string, error) {\n\t\t\t\treturn false, \"reason!\", nil\n\t\t\t},\n\t\t\tinput:   vulnerability.Vulnerability{},\n\t\t\tmatches: false,\n\t\t\treason:  \"reason!\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tconstraint := ByFunc(tt.fn)\n\t\t\tmatches, reason, err := constraint.MatchesVulnerability(tt.input)\n\t\t\twantErr := require.NoError\n\t\t\tif tt.wantErr != nil {\n\t\t\t\twantErr = tt.wantErr\n\t\t\t}\n\t\t\twantErr(t, err)\n\t\t\tassert.Equal(t, tt.matches, matches)\n\t\t\tassert.Equal(t, tt.reason, reason)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/search/id.go",
    "content": "package search\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/anchore/grype/grype/vulnerability\"\n)\n\n// ByID returns criteria to search by vulnerability ID, such as CVE-2024-9143\nfunc ByID(id string) vulnerability.Criteria {\n\treturn &IDCriteria{\n\t\tID: id,\n\t}\n}\n\n// IDCriteria is able to match vulnerabilities to the assigned ID, such as CVE-2024-1000 or GHSA-g2x7-ar59-85z5\ntype IDCriteria struct {\n\tID string\n}\n\nfunc (v *IDCriteria) MatchesVulnerability(vuln vulnerability.Vulnerability) (bool, string, error) {\n\tmatchesID := vuln.ID == v.ID\n\tif !matchesID {\n\t\treturn false, fmt.Sprintf(\"vulnerability ID %q does not match expected ID %q\", vuln.ID, v.ID), nil\n\t}\n\treturn true, \"\", nil\n}\n\nvar _ interface {\n\tvulnerability.Criteria\n} = (*IDCriteria)(nil)\n"
  },
  {
    "path": "grype/search/id_test.go",
    "content": "package search\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/grype/grype/vulnerability\"\n)\n\nfunc Test_ByID(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\tid      string\n\t\tinput   vulnerability.Vulnerability\n\t\twantErr require.ErrorAssertionFunc\n\t\tmatches bool\n\t\treason  string\n\t}{\n\t\t{\n\t\t\tname: \"match\",\n\t\t\tid:   \"CVE-YEAR-1\",\n\t\t\tinput: vulnerability.Vulnerability{\n\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\tID: \"CVE-YEAR-1\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tmatches: true,\n\t\t},\n\t\t{\n\t\t\tname: \"not match\",\n\t\t\tid:   \"CVE-YEAR-1\",\n\t\t\tinput: vulnerability.Vulnerability{\n\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\tID: \"CVE-YEAR-2\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tmatches: false,\n\t\t\treason:  `vulnerability ID \"CVE-YEAR-2\" does not match expected ID \"CVE-YEAR-1\"`,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tconstraint := ByID(tt.id)\n\t\t\tmatches, reason, err := constraint.MatchesVulnerability(tt.input)\n\t\t\twantErr := require.NoError\n\t\t\tif tt.wantErr != nil {\n\t\t\t\twantErr = tt.wantErr\n\t\t\t}\n\t\t\twantErr(t, err)\n\t\t\tassert.Equal(t, tt.matches, matches)\n\t\t\tassert.Equal(t, tt.reason, reason)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/search/package_name.go",
    "content": "package search\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/anchore/grype/grype/vulnerability\"\n)\n\n// ByPackageName returns criteria restricting vulnerabilities to match the package name provided\nfunc ByPackageName(packageName string) vulnerability.Criteria {\n\treturn &PackageNameCriteria{\n\t\tPackageName: packageName,\n\t}\n}\n\ntype PackageNameCriteria struct {\n\tPackageName string\n}\n\nfunc (v *PackageNameCriteria) MatchesVulnerability(vuln vulnerability.Vulnerability) (bool, string, error) {\n\tmatchesPackageName := strings.EqualFold(vuln.PackageName, v.PackageName)\n\tif !matchesPackageName {\n\t\treturn false, fmt.Sprintf(\"vulnerability package name %q does not match expected package name %q\", vuln.PackageName, v.PackageName), nil\n\t}\n\treturn true, \"\", nil\n}\n\nvar _ interface {\n\tvulnerability.Criteria\n} = (*PackageNameCriteria)(nil)\n"
  },
  {
    "path": "grype/search/package_name_test.go",
    "content": "package search\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/grype/grype/vulnerability\"\n)\n\nfunc Test_ByPackageName(t *testing.T) {\n\ttests := []struct {\n\t\tname        string\n\t\tpackageName string\n\t\tinput       vulnerability.Vulnerability\n\t\twantErr     require.ErrorAssertionFunc\n\t\tmatches     bool\n\t\treason      string\n\t}{\n\t\t{\n\t\t\tname:        \"match\",\n\t\t\tpackageName: \"some-name\",\n\t\t\tinput: vulnerability.Vulnerability{\n\t\t\t\tPackageName: \"some-name\",\n\t\t\t},\n\t\t\tmatches: true,\n\t\t},\n\t\t{\n\t\t\tname:        \"match case insensitive\",\n\t\t\tpackageName: \"some-name\",\n\t\t\tinput: vulnerability.Vulnerability{\n\t\t\t\tPackageName: \"SomE-NaMe\",\n\t\t\t},\n\t\t\tmatches: true,\n\t\t},\n\t\t{\n\t\t\tname:        \"not match\",\n\t\t\tpackageName: \"some-name\",\n\t\t\tinput: vulnerability.Vulnerability{\n\t\t\t\tPackageName: \"other-name\",\n\t\t\t},\n\t\t\tmatches: false,\n\t\t\treason:  `vulnerability package name \"other-name\" does not match expected package name \"some-name\"`,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tconstraint := ByPackageName(tt.packageName)\n\t\t\tmatches, reason, err := constraint.MatchesVulnerability(tt.input)\n\t\t\twantErr := require.NoError\n\t\t\tif tt.wantErr != nil {\n\t\t\t\twantErr = tt.wantErr\n\t\t\t}\n\t\t\twantErr(t, err)\n\t\t\tassert.Equal(t, tt.matches, matches)\n\t\t\tassert.Equal(t, tt.reason, reason)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/search/unaffected.go",
    "content": "package search\n\nimport (\n\t\"github.com/anchore/grype/grype/vulnerability\"\n)\n\nvar _ vulnerability.Criteria = (*UnaffectedCriteria)(nil)\n\n// ForUnaffected returns criteria which will cause the search to be against unaffected packages / vulnerabilities.\nfunc ForUnaffected() vulnerability.Criteria {\n\treturn &UnaffectedCriteria{}\n}\n\ntype UnaffectedCriteria struct {\n\tUnaffectedValue bool\n}\n\nfunc (c *UnaffectedCriteria) MatchesVulnerability(v vulnerability.Vulnerability) (bool, string, error) {\n\t// unaffected criteria filtering happens at the store level, so all vulnerabilities returned\n\t// from unaffected stores _should_ already match this criteria. Boolean indicator is a backup\n\t// sanity check.\n\treturn v.Unaffected, \"\", nil\n}\n"
  },
  {
    "path": "grype/search/version_constraint.go",
    "content": "package search\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\n\t\"github.com/anchore/grype/grype/version\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n\t\"github.com/anchore/grype/internal/log\"\n)\n\nvar _ interface {\n\tvulnerability.Criteria\n\tVersionConstraintMatcher\n} = (*VersionCriteria)(nil)\n\n// VersionConstraintMatcher is used for searches which include version.Constraints; this should be used instead of\n// post-filtering vulnerabilities in order to most efficiently hydrate data in memory\ntype VersionConstraintMatcher interface {\n\tMatchesConstraint(constraint version.Constraint) (bool, error)\n}\n\n// ByConstraintFunc returns criteria which will use the provided function as inclusion criteria\nfunc ByConstraintFunc(constraintFunc func(constraint version.Constraint) (bool, error)) vulnerability.Criteria {\n\treturn &constraintFuncCriteria{fn: constraintFunc}\n}\n\ntype VersionCriteria struct {\n\tVersion version.Version\n}\n\nfunc (v VersionCriteria) MatchesVulnerability(value vulnerability.Vulnerability) (bool, string, error) {\n\treturn ByConstraintFunc(v.criteria).MatchesVulnerability(value)\n}\n\nfunc (v VersionCriteria) MatchesConstraint(constraint version.Constraint) (bool, error) {\n\treturn v.criteria(constraint)\n}\n\nfunc (v VersionCriteria) criteria(constraint version.Constraint) (bool, error) {\n\t// The config is now embedded in the version itself, so just call Satisfied\n\tsatisfied, err := constraint.Satisfied(&v.Version)\n\n\tif err != nil {\n\t\tvar unsupportedError *version.UnsupportedComparisonError\n\t\tif errors.As(err, &unsupportedError) {\n\t\t\t// if the format is unsupported, then the constraint is not satisfied, but this should not be conveyed as an error\n\t\t\tlog.WithFields(\"reason\", err).Trace(\"unsatisfied constraint\")\n\t\t\treturn false, nil\n\t\t}\n\n\t\tvar e *version.NonFatalConstraintError\n\t\tif errors.As(err, &e) {\n\t\t\tlog.Warn(e)\n\t\t} else {\n\t\t\treturn false, fmt.Errorf(\"failed to check constraint=%v version=%v: %w\", constraint, v, err)\n\t\t}\n\t}\n\treturn satisfied, nil\n}\n\n// ByFixedVersion returns criteria which constrains vulnerabilities to those that are fixed based on the provided version,\n// in other words: vulnerabilities where the fix version is less than v\nfunc ByFixedVersion(v version.Version) vulnerability.Criteria {\n\treturn &funcCriteria{\n\t\tfunc(vuln vulnerability.Vulnerability) (bool, string, error) {\n\t\t\tvar err error\n\t\t\tif vuln.Fix.State != vulnerability.FixStateFixed {\n\t\t\t\treturn false, \"\", nil\n\t\t\t}\n\t\t\tfor _, fixVersion := range vuln.Fix.Versions {\n\t\t\t\tcmp, e := version.New(fixVersion, v.Format).Compare(&v)\n\t\t\t\tif e != nil {\n\t\t\t\t\terr = e\n\t\t\t\t}\n\t\t\t\tif cmp <= 0 {\n\t\t\t\t\t// fix version is less than or equal to the provided version, so is considered fixed\n\t\t\t\t\treturn true, fmt.Sprintf(\"fix version %v is less than %v\", v, fixVersion), err\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn false, \"\", err\n\t\t},\n\t}\n}\n\n// ByVersion returns criteria which constrains vulnerabilities to those with matching version constraints\nfunc ByVersion(v version.Version) vulnerability.Criteria {\n\treturn &VersionCriteria{\n\t\tVersion: v,\n\t}\n}\n\n// constraintFuncCriteria implements vulnerability.Criteria by providing a function implementing the same signature as MatchVulnerability\ntype constraintFuncCriteria struct {\n\tfn      func(constraint version.Constraint) (bool, error)\n\tsummary string\n}\n\nfunc (f *constraintFuncCriteria) MatchesConstraint(constraint version.Constraint) (bool, error) {\n\treturn f.fn(constraint)\n}\n\nfunc (f *constraintFuncCriteria) MatchesVulnerability(value vulnerability.Vulnerability) (bool, string, error) {\n\tif value.Constraint == nil {\n\t\t// if there is no constraint, then we cannot match against it\n\t\treturn false, \"no version constraint\", nil\n\t}\n\tmatches, err := f.fn(value.Constraint)\n\t// TODO: should we do something about this?\n\treturn matches, \"\", err\n}\n\nfunc (f *constraintFuncCriteria) Summarize() string {\n\treturn f.summary\n}\n\nvar _ interface {\n\tvulnerability.Criteria\n\tVersionConstraintMatcher\n} = (*constraintFuncCriteria)(nil)\n\nfunc MultiConstraintMatcher(a, b VersionConstraintMatcher) VersionConstraintMatcher {\n\treturn &multiConstraintMatcher{\n\t\ta: a,\n\t\tb: b,\n\t}\n}\n\n// multiConstraintMatcher is used internally when multiple version constraint matchers are specified\ntype multiConstraintMatcher struct {\n\ta, b VersionConstraintMatcher\n}\n\nfunc (m *multiConstraintMatcher) MatchesConstraint(constraint version.Constraint) (bool, error) {\n\ta, err := m.a.MatchesConstraint(constraint)\n\tif a || err != nil {\n\t\treturn a, err\n\t}\n\treturn m.b.MatchesConstraint(constraint)\n}\n\nvar _ interface {\n\tVersionConstraintMatcher\n} = (*multiConstraintMatcher)(nil)\n"
  },
  {
    "path": "grype/search/version_constraint_test.go",
    "content": "package search\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/grype/grype/version\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n)\n\nfunc Test_ByVersion(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\tversion string\n\t\tinput   vulnerability.Vulnerability\n\t\twantErr require.ErrorAssertionFunc\n\t\tmatches bool\n\t\treason  string\n\t}{\n\t\t{\n\t\t\tname:    \"match\",\n\t\t\tversion: \"1.0\",\n\t\t\tinput: vulnerability.Vulnerability{\n\t\t\t\tConstraint: version.MustGetConstraint(\"< 2.0\", version.SemanticFormat),\n\t\t\t},\n\t\t\tmatches: true,\n\t\t},\n\t\t{\n\t\t\tname:    \"not match\",\n\t\t\tversion: \"2.0\",\n\t\t\tinput: vulnerability.Vulnerability{\n\t\t\t\tConstraint: version.MustGetConstraint(\"< 2.0\", version.SemanticFormat),\n\t\t\t},\n\t\t\tmatches: false,\n\t\t\treason:  \"\", // we don't expect a reason to be raised up at this level\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tv := version.New(tt.version, version.SemanticFormat)\n\t\t\tconstraint := ByVersion(*v)\n\t\t\tmatches, reason, err := constraint.MatchesVulnerability(tt.input)\n\t\t\twantErr := require.NoError\n\t\t\tif tt.wantErr != nil {\n\t\t\t\twantErr = tt.wantErr\n\t\t\t}\n\t\t\twantErr(t, err)\n\t\t\tassert.Equal(t, tt.matches, matches)\n\t\t\tassert.Equal(t, tt.reason, reason)\n\t\t})\n\t}\n}\n\nfunc Test_ByConstraintFunc(t *testing.T) {\n\ttests := []struct {\n\t\tname           string\n\t\tconstraintFunc func(version.Constraint) (bool, error)\n\t\tinput          vulnerability.Vulnerability\n\t\twantErr        require.ErrorAssertionFunc\n\t\tmatches        bool\n\t\treason         string\n\t}{\n\t\t{\n\t\t\tname: \"match\",\n\t\t\tconstraintFunc: func(version.Constraint) (bool, error) {\n\t\t\t\treturn true, nil\n\t\t\t},\n\t\t\tinput: vulnerability.Vulnerability{\n\t\t\t\tConstraint: version.MustGetConstraint(\"< 2.0\", version.SemanticFormat),\n\t\t\t},\n\t\t\tmatches: true,\n\t\t},\n\t\t{\n\t\t\tname: \"not match\",\n\t\t\tconstraintFunc: func(version.Constraint) (bool, error) {\n\t\t\t\treturn false, nil\n\t\t\t},\n\t\t\tinput: vulnerability.Vulnerability{\n\t\t\t\tConstraint: version.MustGetConstraint(\"< 2.0\", version.SemanticFormat),\n\t\t\t},\n\t\t\tmatches: false,\n\t\t\treason:  \"\", // we don't expect a reason to be raised up at this level\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tconstraint := ByConstraintFunc(tt.constraintFunc)\n\t\t\tmatches, reason, err := constraint.MatchesVulnerability(tt.input)\n\t\t\twantErr := require.NoError\n\t\t\tif tt.wantErr != nil {\n\t\t\t\twantErr = tt.wantErr\n\t\t\t}\n\t\t\twantErr(t, err)\n\t\t\tassert.Equal(t, tt.matches, matches)\n\t\t\tassert.Equal(t, tt.reason, reason)\n\t\t})\n\t}\n}\n\nfunc Test_ByFixedVersion(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\tversion string\n\t\tinput   vulnerability.Vulnerability\n\t\tmatches bool\n\t}{\n\t\t{\n\t\t\tname:    \"fixed version is lower\",\n\t\t\tversion: \"1.1.0\",\n\t\t\tinput: vulnerability.Vulnerability{\n\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\tVersions: []string{\"1.0.0\"},\n\t\t\t\t\tState:    vulnerability.FixStateFixed,\n\t\t\t\t},\n\t\t\t},\n\t\t\tmatches: true,\n\t\t},\n\t\t{\n\t\t\tname:    \"fixed version is equal\",\n\t\t\tversion: \"1.1.0\",\n\t\t\tinput: vulnerability.Vulnerability{\n\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\tVersions: []string{\"1.1.0\"},\n\t\t\t\t\tState:    vulnerability.FixStateFixed,\n\t\t\t\t},\n\t\t\t},\n\t\t\tmatches: true,\n\t\t},\n\t\t{\n\t\t\tname:    \"one of multiple fix versions matches\",\n\t\t\tversion: \"1.1.0\",\n\t\t\tinput: vulnerability.Vulnerability{\n\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\tVersions: []string{\"1.0.0\", \"1.2.0\"},\n\t\t\t\t\tState:    vulnerability.FixStateFixed,\n\t\t\t\t},\n\t\t\t},\n\t\t\tmatches: true,\n\t\t},\n\t\t{\n\t\t\tname:    \"fixed version is higher\",\n\t\t\tversion: \"1.1.0\",\n\t\t\tinput: vulnerability.Vulnerability{\n\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\tVersions: []string{\"1.2.0\"},\n\t\t\t\t\tState:    vulnerability.FixStateFixed,\n\t\t\t\t},\n\t\t\t},\n\t\t\tmatches: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"no fix versions\",\n\t\t\tversion: \"1.1.0\",\n\t\t\tinput: vulnerability.Vulnerability{\n\t\t\t\tFix: vulnerability.Fix{\n\t\t\t\t\tVersions: []string{},\n\t\t\t\t\tState:    vulnerability.FixStateWontFix,\n\t\t\t\t},\n\t\t\t},\n\t\t\tmatches: false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tv := version.New(tt.version, version.SemanticFormat)\n\t\t\tconstraint := ByFixedVersion(*v)\n\t\t\tmatches, reason, err := constraint.MatchesVulnerability(tt.input)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, tt.matches, matches)\n\t\t\tif matches {\n\t\t\t\tassert.NotEmpty(t, reason)\n\t\t\t} else {\n\t\t\t\tassert.Empty(t, reason)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/version/apk_version.go",
    "content": "package version\n\nimport (\n\tapk \"github.com/knqyf263/go-apk-version\"\n)\n\nvar _ Comparator = (*apkVersion)(nil)\n\ntype apkVersion struct {\n\tobj apk.Version\n}\n\nfunc newApkVersion(raw string) (apkVersion, error) {\n\tver, err := apk.NewVersion(trimLeadingV(raw))\n\tif err != nil {\n\t\treturn apkVersion{}, invalidFormatError(ApkFormat, raw, err)\n\t}\n\n\treturn apkVersion{\n\t\tobj: ver,\n\t}, nil\n}\n\n// trimLeadingV removes a single leading 'v' or 'V' prefix only if it's followed by a digit.\n// This allows versions like \"v1.5.0\" to be treated as \"1.5.0\" while preserving other strings as-is.\nfunc trimLeadingV(raw string) string {\n\tif len(raw) >= 2 && (raw[0] == 'v' || raw[0] == 'V') && raw[1] >= '0' && raw[1] <= '9' {\n\t\treturn raw[1:]\n\t}\n\treturn raw\n}\n\nfunc (v apkVersion) Compare(other *Version) (int, error) {\n\tif other == nil {\n\t\treturn -1, ErrNoVersionProvided\n\t}\n\n\tapkVer, err := newApkVersion(other.Raw)\n\tif err != nil {\n\t\treturn -1, err\n\t}\n\n\treturn v.obj.Compare(apkVer.obj), nil\n}\n"
  },
  {
    "path": "grype/version/apk_version_test.go",
    "content": "package version\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc intRef(i int) *int {\n\treturn &i\n}\n\nfunc TestTrimLeadingV(t *testing.T) {\n\ttests := []struct {\n\t\tname  string\n\t\tinput string\n\t\twant  string\n\t}{\n\t\t{\n\t\t\tname:  \"lowercase v followed by digit\",\n\t\t\tinput: \"v1.5.0\",\n\t\t\twant:  \"1.5.0\",\n\t\t},\n\t\t{\n\t\t\tname:  \"uppercase V followed by digit\",\n\t\t\tinput: \"V1.5.0\",\n\t\t\twant:  \"1.5.0\",\n\t\t},\n\t\t{\n\t\t\tname:  \"no prefix\",\n\t\t\tinput: \"1.5.0\",\n\t\t\twant:  \"1.5.0\",\n\t\t},\n\t\t{\n\t\t\tname:  \"v not followed by digit\",\n\t\t\tinput: \"version1.0\",\n\t\t\twant:  \"version1.0\",\n\t\t},\n\t\t{\n\t\t\tname:  \"empty string\",\n\t\t\tinput: \"\",\n\t\t\twant:  \"\",\n\t\t},\n\t\t{\n\t\t\tname:  \"single v\",\n\t\t\tinput: \"v\",\n\t\t\twant:  \"v\",\n\t\t},\n\t\t{\n\t\t\tname:  \"v followed by non-digit\",\n\t\t\tinput: \"va.b.c\",\n\t\t\twant:  \"va.b.c\",\n\t\t},\n\t\t{\n\t\t\tname:  \"double v prefix\",\n\t\t\tinput: \"vv1.0.0\",\n\t\t\twant:  \"vv1.0.0\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot := trimLeadingV(tt.input)\n\t\t\tassert.Equal(t, tt.want, got)\n\t\t})\n\t}\n}\n\nfunc TestApkVersion_Constraint(t *testing.T) {\n\ttests := []testCase{\n\t\t{version: \"2.3.1\", constraint: \"\", satisfied: true},\n\t\t// compound conditions\n\t\t{version: \"2.3.1\", constraint: \"> 1.0.0, < 2.0.0\", satisfied: false},\n\t\t{version: \"1.3.1\", constraint: \"> 1.0.0, < 2.0.0\", satisfied: true},\n\t\t{version: \"2.0.0\", constraint: \"> 1.0.0, <= 2.0.0\", satisfied: true},\n\t\t{version: \"2.0.0\", constraint: \"> 1.0.0, < 2.0.0\", satisfied: false},\n\t\t{version: \"1.0.0\", constraint: \">= 1.0.0, < 2.0.0\", satisfied: true},\n\t\t{version: \"1.0.0\", constraint: \"> 1.0.0, < 2.0.0\", satisfied: false},\n\t\t{version: \"0.9.0\", constraint: \"> 1.0.0, < 2.0.0\", satisfied: false},\n\t\t{version: \"1.5.0\", constraint: \"> 0.1.0, < 0.5.0 || > 1.0.0, < 2.0.0\", satisfied: true},\n\t\t{version: \"0.2.0\", constraint: \"> 0.1.0, < 0.5.0 || > 1.0.0, < 2.0.0\", satisfied: true},\n\t\t{version: \"0.0.1\", constraint: \"> 0.1.0, < 0.5.0 || > 1.0.0, < 2.0.0\", satisfied: false},\n\t\t{version: \"0.6.0\", constraint: \"> 0.1.0, < 0.5.0 || > 1.0.0, < 2.0.0\", satisfied: false},\n\t\t{version: \"2.5.0\", constraint: \"> 0.1.0, < 0.5.0 || > 1.0.0, < 2.0.0\", satisfied: false},\n\t\t// fixed-in scenarios\n\t\t{version: \"2.3.1\", constraint: \"< 2.0.0\", satisfied: false},\n\t\t{version: \"2.3.1\", constraint: \"< 2.0\", satisfied: false},\n\t\t{version: \"2.3.1\", constraint: \"< 2\", satisfied: false},\n\t\t{version: \"2.3.1\", constraint: \"< 2.3\", satisfied: false},\n\t\t{version: \"2.3.1\", constraint: \"< 2.3.1\", satisfied: false},\n\t\t{version: \"2.3.1\", constraint: \"< 2.3.2\", satisfied: true},\n\t\t{version: \"2.3.1\", constraint: \"< 2.4\", satisfied: true},\n\t\t{version: \"2.3.1\", constraint: \"< 3\", satisfied: true},\n\t\t{version: \"2.3.1\", constraint: \"< 3.0\", satisfied: true},\n\t\t{version: \"2.3.1\", constraint: \"< 3.0.0\", satisfied: true},\n\t\t// alpine specific scenarios\n\t\t// https://wiki.alpinelinux.org/wiki/APKBUILD_Reference#pkgver\n\t\t{version: \"1.5.1-r1\", constraint: \"< 1.5.1\", satisfied: false},\n\t\t{version: \"1.5.1-r1\", constraint: \"> 1.5.1\", satisfied: true},\n\t\t{version: \"9.3.2-r4\", constraint: \"< 9.3.4-r2\", satisfied: true},\n\t\t{version: \"9.3.4-r2\", constraint: \"> 9.3.4\", satisfied: true},\n\t\t{version: \"4.2.52_p2-r1\", constraint: \"< 4.2.52_p4-r2\", satisfied: true},\n\t\t{version: \"4.2.52_p2-r1\", constraint: \"> 4.2.52_p4-r2\", satisfied: false},\n\t\t{version: \"0.1.0_alpha\", constraint: \"< 0.1.3_alpha\", satisfied: true},\n\t\t{version: \"0.1.0_alpha2\", constraint: \"> 0.1.0_alpha\", satisfied: true},\n\t\t{version: \"1.1\", constraint: \"> 1.1_alpha1\", satisfied: true},\n\t\t{version: \"1.1\", constraint: \"< 1.1_alpha1\", satisfied: false},\n\t\t{version: \"2.3.0b-r1\", constraint: \"< 2.3.0b-r2\", satisfied: true},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.tName(), func(t *testing.T) {\n\t\t\tconstraint, err := GetConstraint(test.constraint, ApkFormat)\n\n\t\t\tassert.NoError(t, err, \"unexpected error from newApkConstraint: %v\", err)\n\t\t\ttest.assertVersionConstraint(t, ApkFormat, constraint)\n\n\t\t})\n\t}\n}\n\nfunc TestApkVersion_Compare(t *testing.T) {\n\ttests := []struct {\n\t\tname           string\n\t\tthisVersion    string\n\t\totherVersion   string\n\t\totherFormat    Format\n\t\texpectError    bool\n\t\terrorSubstring string\n\t\texpectResult   *int // nil means don't check specific result\n\t}{\n\t\t{\n\t\t\tname:         \"same format successful comparison\",\n\t\t\tthisVersion:  \"1.2.3-r4\",\n\t\t\totherVersion: \"1.2.3-r5\",\n\t\t\totherFormat:  ApkFormat,\n\t\t\texpectError:  false,\n\t\t},\n\t\t{\n\t\t\tname:         \"different format does not return error\",\n\t\t\tthisVersion:  \"1.2.3-r4\",\n\t\t\totherVersion: \"1.2.3\",\n\t\t\totherFormat:  SemanticFormat,\n\t\t\texpectError:  false,\n\t\t},\n\t\t{\n\t\t\tname:           \"different format does not return error - deb\",\n\t\t\tthisVersion:    \"1.2.3-r4\",\n\t\t\totherVersion:   \"1.2.3-1\",\n\t\t\totherFormat:    DebFormat,\n\t\t\texpectError:    false,\n\t\t\terrorSubstring: \"unsupported version comparison\",\n\t\t},\n\t\t{\n\t\t\tname:         \"unknown format attempts upgrade - valid apk format\",\n\t\t\tthisVersion:  \"1.2.3-r4\",\n\t\t\totherVersion: \"1.2.3-r5\",\n\t\t\totherFormat:  UnknownFormat,\n\t\t\texpectError:  false,\n\t\t},\n\t\t{\n\t\t\tname:           \"unknown format attempts upgrade - invalid apk format\",\n\t\t\tthisVersion:    \"1.2.3-r4\",\n\t\t\totherVersion:   \"not-valid-apk-format\",\n\t\t\totherFormat:    UnknownFormat,\n\t\t\texpectError:    true,\n\t\t\terrorSubstring: \"invalid version\",\n\t\t},\n\t\t{\n\t\t\tname:         \"lowercase v prefix on other version is stripped\",\n\t\t\tthisVersion:  \"1.5.0\",\n\t\t\totherVersion: \"v1.5.0\",\n\t\t\totherFormat:  ApkFormat,\n\t\t\texpectResult: intRef(0),\n\t\t},\n\t\t{\n\t\t\tname:         \"uppercase V prefix on other version is stripped\",\n\t\t\tthisVersion:  \"1.5.0\",\n\t\t\totherVersion: \"V1.5.0\",\n\t\t\totherFormat:  ApkFormat,\n\t\t\texpectResult: intRef(0),\n\t\t},\n\t\t{\n\t\t\tname:         \"v prefix on this version is stripped\",\n\t\t\tthisVersion:  \"v1.5.0\",\n\t\t\totherVersion: \"1.5.0\",\n\t\t\totherFormat:  ApkFormat,\n\t\t\texpectResult: intRef(0),\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tthisVer, err := newApkVersion(test.thisVersion)\n\t\t\trequire.NoError(t, err)\n\n\t\t\totherVer := New(test.otherVersion, test.otherFormat)\n\n\t\t\tresult, err := thisVer.Compare(otherVer)\n\n\t\t\tif test.expectError {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\tif test.errorSubstring != \"\" {\n\t\t\t\t\tassert.True(t, strings.Contains(err.Error(), test.errorSubstring),\n\t\t\t\t\t\t\"Expected error to contain '%s', got: %v\", test.errorSubstring, err)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tif test.expectResult != nil {\n\t\t\t\t\tassert.Equal(t, *test.expectResult, result)\n\t\t\t\t} else {\n\t\t\t\t\tassert.Contains(t, []int{-1, 0, 1}, result, \"Expected comparison result to be -1, 0, or 1\")\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestApkVersion_Compare_EdgeCases(t *testing.T) {\n\ttests := []struct {\n\t\tname           string\n\t\tsetupFunc      func(testing.TB) (*Version, *Version)\n\t\texpectError    bool\n\t\terrorSubstring string\n\t}{\n\t\t{\n\t\t\tname: \"nil version object\",\n\t\t\tsetupFunc: func(t testing.TB) (*Version, *Version) {\n\t\t\t\tthisVer := New(\"1.2.3-r4\", ApkFormat)\n\t\t\t\treturn thisVer, nil\n\t\t\t},\n\t\t\texpectError:    true,\n\t\t\terrorSubstring: \"no version provided for comparison\",\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tthisVer, otherVer := test.setupFunc(t)\n\n\t\t\t_, err := thisVer.Compare(otherVer)\n\n\t\t\trequire.Error(t, err)\n\t\t\tif test.errorSubstring != \"\" {\n\t\t\t\tassert.True(t, strings.Contains(err.Error(), test.errorSubstring),\n\t\t\t\t\t\"Expected error to contain '%s', got: %v\", test.errorSubstring, err)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/version/bitnami_version.go",
    "content": "package version\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\tbitnami \"github.com/bitnami/go-version/pkg/version\"\n\n\thashiVer \"github.com/anchore/go-version\"\n)\n\nvar _ Comparator = (*bitnamiVersion)(nil)\n\ntype bitnamiVersion struct {\n\tobj *hashiVer.Version\n}\n\nfunc newBitnamiVersion(raw string) (bitnamiVersion, error) {\n\tbv, err := bitnami.Parse(raw)\n\tif err != nil {\n\t\tfmtErr := err\n\t\tverObj, err := hashiVer.NewVersion(raw)\n\t\tif err != nil {\n\t\t\treturn bitnamiVersion{}, invalidFormatError(BitnamiFormat, raw, fmtErr)\n\t\t}\n\t\tvar segments []string\n\t\tfor _, segment := range verObj.Segments() {\n\t\t\tsegments = append(segments, fmt.Sprintf(\"%d\", segment))\n\t\t}\n\t\t// drop any pre-release info\n\t\traw = strings.Join(segments, \".\")\n\t} else {\n\t\traw = fmt.Sprintf(\"%d.%d.%d\", bv.Major(), bv.Minor(), bv.Patch())\n\t}\n\n\t// We can't assume Bitnami revisions can potentially address a\n\t// known vulnerability given Bitnami package revisions use\n\t// exactly the same upstream source code used to create the\n\t// previous version. Then, we discard it.\n\tverObj, err := hashiVer.NewVersion(raw)\n\tif err != nil {\n\t\treturn bitnamiVersion{}, invalidFormatError(BitnamiFormat, raw, err)\n\t}\n\treturn bitnamiVersion{\n\t\tobj: verObj,\n\t}, nil\n}\n\nfunc (v bitnamiVersion) Compare(other *Version) (int, error) {\n\tif other == nil {\n\t\treturn -1, ErrNoVersionProvided\n\t}\n\n\tbv, err := newBitnamiVersion(other.Raw)\n\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\treturn v.obj.Compare(bv.obj), nil\n}\n"
  },
  {
    "path": "grype/version/bitnami_version_test.go",
    "content": "package version\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestBitnamiVersion_Constraint(t *testing.T) {\n\ttests := []testCase{\n\t\t// empty values\n\t\t{version: \"2.3.1\", constraint: \"\", satisfied: true},\n\t\t// typical cases\n\t\t{version: \"1.5.0\", constraint: \"> 0.1.0, < 0.5.0 || > 1.0.0, < 2.0.0\", satisfied: true},\n\t\t{version: \"0.2.0\", constraint: \"> 0.1.0, < 0.5.0 || > 1.0.0, < 2.0.0\", satisfied: true},\n\t\t{version: \"0.0.1\", constraint: \"> 0.1.0, < 0.5.0 || > 1.0.0, < 2.0.0\", satisfied: false},\n\t\t{version: \"0.6.0\", constraint: \"> 0.1.0, < 0.5.0 || > 1.0.0, < 2.0.0\", satisfied: false},\n\t\t{version: \"2.5.0\", constraint: \"> 0.1.0, < 0.5.0 || > 1.0.0, < 2.0.0\", satisfied: false},\n\t\t{version: \"2.3.1\", constraint: \"2.3.1\", satisfied: true},\n\t\t{version: \"2.3.1\", constraint: \"= 2.3.1\", satisfied: true},\n\t\t{version: \"2.3.1\", constraint: \"  =   2.3.1\", satisfied: true},\n\t\t{version: \"2.3.1\", constraint: \">= 2.3.1\", satisfied: true},\n\t\t{version: \"2.3.1\", constraint: \"> 2.0.0\", satisfied: true},\n\t\t{version: \"2.3.1\", constraint: \"> 2.0\", satisfied: true},\n\t\t{version: \"2.3.1\", constraint: \"> 2\", satisfied: true},\n\t\t{version: \"2.3.1\", constraint: \"> 2, < 3\", satisfied: true},\n\t\t{version: \"2.3.1\", constraint: \"> 2.3, < 3.1\", satisfied: true},\n\t\t{version: \"2.3.1\", constraint: \"> 2.3.0, < 3.1\", satisfied: true},\n\t\t{version: \"2.3.1\", constraint: \">= 2.3.1, < 3.1\", satisfied: true},\n\t\t{version: \"2.3.1\", constraint: \"  =  2.3.2\", satisfied: false},\n\t\t{version: \"2.3.1\", constraint: \">= 2.3.2\", satisfied: false},\n\t\t{version: \"2.3.1\", constraint: \"> 2.3.1\", satisfied: false},\n\t\t{version: \"2.3.1\", constraint: \"< 2.0.0\", satisfied: false},\n\t\t{version: \"2.3.1\", constraint: \"< 2.0\", satisfied: false},\n\t\t{version: \"2.3.1\", constraint: \"< 2\", satisfied: false},\n\t\t{version: \"2.3.1\", constraint: \"< 2, > 3\", satisfied: false},\n\t\t{version: \"2.3.1-1\", constraint: \"2.3.1\", satisfied: true},\n\t\t{version: \"2.3.1-1\", constraint: \"= 2.3.1\", satisfied: true},\n\t\t{version: \"2.3.1-1\", constraint: \"  =   2.3.1\", satisfied: true},\n\t\t{version: \"2.3.1-1\", constraint: \">= 2.3.1\", satisfied: true},\n\t\t{version: \"2.3.1-1\", constraint: \"> 2.0.0\", satisfied: true},\n\t\t{version: \"2.3.1-1\", constraint: \"> 2.0\", satisfied: true},\n\t\t{version: \"2.3.1-1\", constraint: \"> 2\", satisfied: true},\n\t\t{version: \"2.3.1-1\", constraint: \"> 2, < 3\", satisfied: true},\n\t\t{version: \"2.3.1-1\", constraint: \"> 2.3, < 3.1\", satisfied: true},\n\t\t{version: \"2.3.1-1\", constraint: \"> 2.3.0, < 3.1\", satisfied: true},\n\t\t{version: \"2.3.1-1\", constraint: \">= 2.3.1, < 3.1\", satisfied: true},\n\t\t{version: \"2.3.1-1\", constraint: \"  =  2.3.2\", satisfied: false},\n\t\t{version: \"2.3.1-1\", constraint: \">= 2.3.2\", satisfied: false},\n\t\t{version: \"2.3.1-1\", constraint: \"< 2.0.0\", satisfied: false},\n\t\t{version: \"2.3.1-1\", constraint: \"< 2.0\", satisfied: false},\n\t\t{version: \"2.3.1-1\", constraint: \"< 2\", satisfied: false},\n\t\t{version: \"2.3.1-1\", constraint: \"< 2, > 3\", satisfied: false},\n\t\t// ignoring revisions\n\t\t{version: \"2.3.1-1\", constraint: \"> 2.3.1\", satisfied: false},\n\t\t{version: \"2.3.1-1\", constraint: \"< 2.3.1-2\", satisfied: false},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.tName(), func(t *testing.T) {\n\t\t\tconstraint, err := GetConstraint(test.constraint, BitnamiFormat)\n\n\t\t\trequire.NoError(t, err)\n\t\t\ttest.assertVersionConstraint(t, BitnamiFormat, constraint)\n\t\t})\n\t}\n}\n\nfunc TestBitnamiVersion_Compare(t *testing.T) {\n\ttests := []struct {\n\t\tname           string\n\t\tthisVersion    string\n\t\totherVersion   string\n\t\totherFormat    Format\n\t\texpectError    bool\n\t\terrorSubstring string\n\t}{\n\t\t{\n\t\t\tname:         \"same format successful comparison\",\n\t\t\tthisVersion:  \"1.2.3-4\",\n\t\t\totherVersion: \"1.2.3-5\",\n\t\t\totherFormat:  BitnamiFormat,\n\t\t\texpectError:  false,\n\t\t},\n\t\t{\n\t\t\tname:         \"semantic versioning successful comparison\",\n\t\t\tthisVersion:  \"1.2.3-4\",\n\t\t\totherVersion: \"1.2.3\",\n\t\t\totherFormat:  SemanticFormat,\n\t\t\texpectError:  false,\n\t\t},\n\t\t{\n\t\t\tname:         \"unknown format attempts upgrade - valid semver format\",\n\t\t\tthisVersion:  \"1.2.3-4\",\n\t\t\totherVersion: \"1.2.3-5\",\n\t\t\totherFormat:  UnknownFormat,\n\t\t\texpectError:  false,\n\t\t},\n\t\t{\n\t\t\tname:           \"unknown format attempts upgrade - invalid semver format\",\n\t\t\tthisVersion:    \"1.2.3-4\",\n\t\t\totherVersion:   \"not-valid-semver-format\",\n\t\t\totherFormat:    UnknownFormat,\n\t\t\texpectError:    true,\n\t\t\terrorSubstring: \"invalid semantic version\",\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tthisVer, err := newBitnamiVersion(test.thisVersion)\n\t\t\trequire.NoError(t, err)\n\n\t\t\totherVer := New(test.otherVersion, test.otherFormat)\n\n\t\t\tresult, err := thisVer.Compare(otherVer)\n\n\t\t\tif test.expectError {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\tif test.errorSubstring != \"\" {\n\t\t\t\t\tassert.True(t, strings.Contains(err.Error(), test.errorSubstring),\n\t\t\t\t\t\t\"Expected error to contain '%s', got: %v\", test.errorSubstring, err)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.Contains(t, []int{-1, 0, 1}, result, \"Expected comparison result to be -1, 0, or 1\")\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestBitnamiVersion_Compare_EdgeCases(t *testing.T) {\n\ttests := []struct {\n\t\tname           string\n\t\tsetupFunc      func(testing.TB) (*Version, *Version)\n\t\texpectError    bool\n\t\terrorSubstring string\n\t}{\n\t\t{\n\t\t\tname: \"nil version object\",\n\t\t\tsetupFunc: func(t testing.TB) (*Version, *Version) {\n\t\t\t\tthisVer := New(\"1.2.3-4\", BitnamiFormat)\n\n\t\t\t\treturn thisVer, nil\n\t\t\t},\n\t\t\texpectError:    true,\n\t\t\terrorSubstring: \"no version provided for comparison\",\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tthisVer, otherVer := test.setupFunc(t)\n\n\t\t\t_, err := thisVer.Compare(otherVer)\n\n\t\t\trequire.Error(t, err)\n\t\t\tif test.errorSubstring != \"\" {\n\t\t\t\tassert.True(t, strings.Contains(err.Error(), test.errorSubstring),\n\t\t\t\t\t\"Expected error to contain '%s', got: %v\", test.errorSubstring, err)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/version/combined_constraint.go",
    "content": "package version\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/scylladb/go-set/strset\"\n)\n\nfunc CombineConstraints(constraints ...Constraint) Constraint {\n\tconstraints = uniqueConstraints(constraints...)\n\n\tif len(constraints) == 0 {\n\t\treturn nil\n\t}\n\tif len(constraints) == 1 {\n\t\treturn constraints[0]\n\t}\n\n\treturn combinedConstraint{\n\t\tOrOperands: constraints,\n\t}\n}\n\ntype combinedConstraint struct {\n\tOrOperands []Constraint\n}\n\nfunc (c combinedConstraint) String() string {\n\treturn fmt.Sprintf(\"%s (%s)\", c.Value(), strings.ToLower(c.Format().String()))\n}\n\nfunc (c combinedConstraint) Value() string {\n\t// TODO: there is room for improvement here to make this more readable (filter out redundant constraints... e.g. <1.0 || < 2.0 should just be < 2.0)\n\tvar str string\n\tfor i, op := range c.OrOperands {\n\t\tif i > 0 {\n\t\t\tstr += \" || \"\n\t\t}\n\t\tstr += op.Value()\n\t}\n\treturn str\n}\n\nfunc (c combinedConstraint) Format() Format {\n\tformat := UnknownFormat\n\tif len(c.OrOperands) > 0 {\n\t\tformat = c.OrOperands[0].Format()\n\t}\n\treturn format\n}\n\nfunc (c combinedConstraint) Satisfied(version *Version) (bool, error) {\n\tif version == nil {\n\t\treturn false, fmt.Errorf(\"cannot evaluate combined constraint with nil version\")\n\t}\n\n\tfor _, op := range c.OrOperands {\n\t\tsatisfied, err := op.Satisfied(version)\n\t\tif err != nil {\n\t\t\treturn false, fmt.Errorf(\"error evaluating constraint %s: %w\", op, err)\n\t\t}\n\t\tif satisfied {\n\t\t\treturn true, nil\n\t\t}\n\t}\n\n\treturn false, nil\n}\n\nfunc uniqueConstraints(constraints ...Constraint) []Constraint {\n\tvar nonNil []Constraint\n\tseen := strset.New()\n\tfor _, c := range constraints {\n\t\tif c == nil || seen.Has(c.Value()) {\n\t\t\tcontinue\n\t\t}\n\t\tseen.Add(c.Value())\n\t\tnonNil = append(nonNil, c)\n\t}\n\treturn nonNil\n}\n"
  },
  {
    "path": "grype/version/combined_constraint_test.go",
    "content": "package version\n\nimport (\n\t\"errors\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestCombineConstraints(t *testing.T) {\n\ttests := []struct {\n\t\tname        string\n\t\tconstraints []Constraint\n\t\twant        Constraint\n\t}{\n\t\t{\n\t\t\tname:        \"no constraints returns nil\",\n\t\t\tconstraints: []Constraint{},\n\t\t\twant:        nil,\n\t\t},\n\t\t{\n\t\t\tname: \"single constraint returns same constraint\",\n\t\t\tconstraints: []Constraint{\n\t\t\t\tMustGetConstraint(\">= 1.0.0\", SemanticFormat),\n\t\t\t},\n\t\t\twant: MustGetConstraint(\">= 1.0.0\", SemanticFormat),\n\t\t},\n\t\t{\n\t\t\tname: \"multiple constraints returns combined constraint\",\n\t\t\tconstraints: []Constraint{\n\t\t\t\tMustGetConstraint(\">= 1.0.0\", SemanticFormat),\n\t\t\t\tMustGetConstraint(\"< 2.0.0\", SemanticFormat),\n\t\t\t},\n\t\t\twant: combinedConstraint{\n\t\t\t\tOrOperands: []Constraint{\n\t\t\t\t\tMustGetConstraint(\">= 1.0.0\", SemanticFormat),\n\t\t\t\t\tMustGetConstraint(\"< 2.0.0\", SemanticFormat),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"nil constraints are filtered out\",\n\t\t\tconstraints: []Constraint{\n\t\t\t\tnil,\n\t\t\t\tMustGetConstraint(\">= 1.0.0\", SemanticFormat),\n\t\t\t\tnil,\n\t\t\t},\n\t\t\twant: MustGetConstraint(\">= 1.0.0\", SemanticFormat),\n\t\t},\n\t\t{\n\t\t\tname: \"duplicate constraints are filtered out\",\n\t\t\tconstraints: []Constraint{\n\t\t\t\tMustGetConstraint(\">= 1.0.0\", SemanticFormat),\n\t\t\t\tMustGetConstraint(\">= 1.0.0\", SemanticFormat),\n\t\t\t\tMustGetConstraint(\"< 2.0.0\", SemanticFormat),\n\t\t\t},\n\t\t\twant: combinedConstraint{\n\t\t\t\tOrOperands: []Constraint{\n\t\t\t\t\tMustGetConstraint(\">= 1.0.0\", SemanticFormat),\n\t\t\t\t\tMustGetConstraint(\"< 2.0.0\", SemanticFormat),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"all nil constraints returns nil\",\n\t\t\tconstraints: []Constraint{\n\t\t\t\tnil,\n\t\t\t\tnil,\n\t\t\t},\n\t\t\twant: nil,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot := CombineConstraints(tt.constraints...)\n\t\t\trequire.Equal(t, tt.want, got)\n\t\t})\n\t}\n}\n\nfunc TestCombinedConstraint_Methods(t *testing.T) {\n\ttests := []struct {\n\t\tname          string\n\t\tconstraint    combinedConstraint\n\t\tversion       *Version\n\t\twantValue     string\n\t\twantString    string\n\t\twantFormat    Format\n\t\twantSatisfied bool\n\t\twantErr       require.ErrorAssertionFunc\n\t}{\n\t\t{\n\t\t\tname: \"single operand semantic constraint satisfied\",\n\t\t\tconstraint: combinedConstraint{\n\t\t\t\tOrOperands: []Constraint{\n\t\t\t\t\tMustGetConstraint(\">= 1.0.0\", SemanticFormat),\n\t\t\t\t},\n\t\t\t},\n\t\t\tversion:       New(\"1.5.0\", SemanticFormat),\n\t\t\twantValue:     \">= 1.0.0\",\n\t\t\twantString:    \">= 1.0.0 (semantic)\",\n\t\t\twantFormat:    SemanticFormat,\n\t\t\twantSatisfied: true,\n\t\t},\n\t\t{\n\t\t\tname: \"single operand semantic constraint not satisfied\",\n\t\t\tconstraint: combinedConstraint{\n\t\t\t\tOrOperands: []Constraint{\n\t\t\t\t\tMustGetConstraint(\">= 2.0.0\", SemanticFormat),\n\t\t\t\t},\n\t\t\t},\n\t\t\tversion:       New(\"1.5.0\", SemanticFormat),\n\t\t\twantValue:     \">= 2.0.0\",\n\t\t\twantString:    \">= 2.0.0 (semantic)\",\n\t\t\twantFormat:    SemanticFormat,\n\t\t\twantSatisfied: false,\n\t\t},\n\t\t{\n\t\t\tname: \"multiple operands with OR logic - first satisfies\",\n\t\t\tconstraint: combinedConstraint{\n\t\t\t\tOrOperands: []Constraint{\n\t\t\t\t\tMustGetConstraint(\">= 1.0.0\", SemanticFormat),\n\t\t\t\t\tMustGetConstraint(\">= 3.0.0\", SemanticFormat),\n\t\t\t\t},\n\t\t\t},\n\t\t\tversion:       New(\"1.5.0\", SemanticFormat),\n\t\t\twantValue:     \">= 1.0.0 || >= 3.0.0\",\n\t\t\twantString:    \">= 1.0.0 || >= 3.0.0 (semantic)\",\n\t\t\twantFormat:    SemanticFormat,\n\t\t\twantSatisfied: true,\n\t\t},\n\t\t{\n\t\t\tname: \"multiple operands with OR logic - second satisfies\",\n\t\t\tconstraint: combinedConstraint{\n\t\t\t\tOrOperands: []Constraint{\n\t\t\t\t\tMustGetConstraint(\">= 2.0.0\", SemanticFormat),\n\t\t\t\t\tMustGetConstraint(\"< 2.0.0\", SemanticFormat),\n\t\t\t\t},\n\t\t\t},\n\t\t\tversion:       New(\"1.5.0\", SemanticFormat),\n\t\t\twantValue:     \">= 2.0.0 || < 2.0.0\",\n\t\t\twantString:    \">= 2.0.0 || < 2.0.0 (semantic)\",\n\t\t\twantFormat:    SemanticFormat,\n\t\t\twantSatisfied: true,\n\t\t},\n\t\t{\n\t\t\tname: \"multiple operands with OR logic - none satisfy\",\n\t\t\tconstraint: combinedConstraint{\n\t\t\t\tOrOperands: []Constraint{\n\t\t\t\t\tMustGetConstraint(\">= 2.0.0\", SemanticFormat),\n\t\t\t\t\tMustGetConstraint(\">= 3.0.0\", SemanticFormat),\n\t\t\t\t},\n\t\t\t},\n\t\t\tversion:       New(\"1.5.0\", SemanticFormat),\n\t\t\twantValue:     \">= 2.0.0 || >= 3.0.0\",\n\t\t\twantString:    \">= 2.0.0 || >= 3.0.0 (semantic)\",\n\t\t\twantFormat:    SemanticFormat,\n\t\t\twantSatisfied: false,\n\t\t},\n\t\t{\n\t\t\tname: \"empty operands returns unknown format\",\n\t\t\tconstraint: combinedConstraint{\n\t\t\t\tOrOperands: []Constraint{},\n\t\t\t},\n\t\t\tversion:       New(\"1.5.0\", SemanticFormat),\n\t\t\twantValue:     \"\",\n\t\t\twantString:    \" (unknown)\",\n\t\t\twantFormat:    UnknownFormat,\n\t\t\twantSatisfied: false,\n\t\t},\n\t\t{\n\t\t\tname: \"rpm format constraint\",\n\t\t\tconstraint: combinedConstraint{\n\t\t\t\tOrOperands: []Constraint{\n\t\t\t\t\tMustGetConstraint(\">= 1.0.0\", RpmFormat),\n\t\t\t\t\tMustGetConstraint(\"< 0.5.0\", RpmFormat),\n\t\t\t\t},\n\t\t\t},\n\t\t\tversion:       New(\"1.5.0\", RpmFormat),\n\t\t\twantValue:     \">= 1.0.0 || < 0.5.0\",\n\t\t\twantString:    \">= 1.0.0 || < 0.5.0 (rpm)\",\n\t\t\twantFormat:    RpmFormat,\n\t\t\twantSatisfied: true,\n\t\t},\n\t\t{\n\t\t\tname: \"nil version returns error\",\n\t\t\tconstraint: combinedConstraint{\n\t\t\t\tOrOperands: []Constraint{\n\t\t\t\t\tMustGetConstraint(\">= 1.0.0\", SemanticFormat),\n\t\t\t\t},\n\t\t\t},\n\t\t\tversion:       nil,\n\t\t\twantValue:     \">= 1.0.0\",\n\t\t\twantString:    \">= 1.0.0 (semantic)\",\n\t\t\twantFormat:    SemanticFormat,\n\t\t\twantSatisfied: false,\n\t\t\twantErr:       require.Error,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif tt.wantErr == nil {\n\t\t\t\ttt.wantErr = require.NoError\n\t\t\t}\n\n\t\t\t// test Value() method\n\t\t\tgotValue := tt.constraint.Value()\n\t\t\trequire.Equal(t, tt.wantValue, gotValue)\n\n\t\t\t// test String() method\n\t\t\tgotString := tt.constraint.String()\n\t\t\trequire.Equal(t, tt.wantString, gotString)\n\n\t\t\t// test Format() method\n\t\t\tgotFormat := tt.constraint.Format()\n\t\t\trequire.Equal(t, tt.wantFormat, gotFormat)\n\n\t\t\t// test Satisfied() method\n\t\t\tgotSatisfied, err := tt.constraint.Satisfied(tt.version)\n\t\t\ttt.wantErr(t, err)\n\n\t\t\tif err != nil {\n\t\t\t\treturn\n\t\t\t}\n\t\t\trequire.Equal(t, tt.wantSatisfied, gotSatisfied)\n\t\t})\n\t}\n}\n\nfunc TestCombinedConstraint_Satisfied_WithErrors(t *testing.T) {\n\ttests := []struct {\n\t\tname       string\n\t\tconstraint combinedConstraint\n\t\tversion    *Version\n\t\twantErr    require.ErrorAssertionFunc\n\t}{\n\t\t{\n\t\t\tname: \"error from first constraint\",\n\t\t\tconstraint: combinedConstraint{\n\t\t\t\tOrOperands: []Constraint{\n\t\t\t\t\tmockConstraint{value: \">= 1.0.0\", format: SemanticFormat, returnErr: true},\n\t\t\t\t\tmockConstraint{value: \"< 2.0.0\", format: SemanticFormat, satisfied: true},\n\t\t\t\t},\n\t\t\t},\n\t\t\tversion: New(\"1.5.0\", SemanticFormat),\n\t\t\twantErr: require.Error,\n\t\t},\n\t\t{\n\t\t\tname: \"error from second constraint when first doesn't satisfy\",\n\t\t\tconstraint: combinedConstraint{\n\t\t\t\tOrOperands: []Constraint{\n\t\t\t\t\tmockConstraint{value: \">= 1.0.0\", format: SemanticFormat, satisfied: false},\n\t\t\t\t\tmockConstraint{value: \"< 2.0.0\", format: SemanticFormat, returnErr: true},\n\t\t\t\t},\n\t\t\t},\n\t\t\tversion: New(\"1.5.0\", SemanticFormat),\n\t\t\twantErr: require.Error,\n\t\t},\n\t\t{\n\t\t\tname: \"no error when first constraint satisfies\",\n\t\t\tconstraint: combinedConstraint{\n\t\t\t\tOrOperands: []Constraint{\n\t\t\t\t\tmockConstraint{value: \">= 1.0.0\", format: SemanticFormat, satisfied: true},\n\t\t\t\t\tmockConstraint{value: \"< 2.0.0\", format: SemanticFormat, returnErr: true},\n\t\t\t\t},\n\t\t\t},\n\t\t\tversion: New(\"1.5.0\", SemanticFormat),\n\t\t\twantErr: require.NoError,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\t_, err := tt.constraint.Satisfied(tt.version)\n\t\t\ttt.wantErr(t, err)\n\t\t})\n\t}\n}\n\ntype mockConstraint struct {\n\tvalue     string\n\tformat    Format\n\tsatisfied bool\n\treturnErr bool\n}\n\nfunc (m mockConstraint) String() string {\n\treturn m.value + \" (\" + strings.ToLower(m.format.String()) + \")\"\n}\n\nfunc (m mockConstraint) Value() string {\n\treturn m.value\n}\n\nfunc (m mockConstraint) Format() Format {\n\treturn m.format\n}\n\nfunc (m mockConstraint) Satisfied(*Version) (bool, error) {\n\tif m.returnErr {\n\t\treturn false, errors.New(\"mock constraint error\")\n\t}\n\treturn m.satisfied, nil\n}\n"
  },
  {
    "path": "grype/version/comparator.go",
    "content": "package version\n\n// MissingEpochStrategy defines how missing epochs in package versions are handled\n// during vulnerability matching.\ntype MissingEpochStrategy string\n\nconst (\n\t// MissingEpochStrategyZero treats missing epochs as 0 (default, backward compatible)\n\tMissingEpochStrategyZero MissingEpochStrategy = \"zero\"\n\t// MissingEpochStrategyAuto assumes missing epoch matches the constraint's epoch\n\tMissingEpochStrategyAuto MissingEpochStrategy = \"auto\"\n)\n\n// ComparisonConfig contains configuration for version comparison behavior.\ntype ComparisonConfig struct {\n\t// MissingEpochStrategy controls how missing epochs in package versions are handled\n\t// during vulnerability matching.\n\t//\n\t// Valid values:\n\t//   - MissingEpochStrategyZero (\"zero\"): Treat missing epochs as 0 (default, backward compatible)\n\t//   - MissingEpochStrategyAuto (\"auto\"): Assume missing epoch matches the constraint's epoch\n\tMissingEpochStrategy MissingEpochStrategy\n}\n\ntype Comparator interface {\n\t// Compare compares this version to another version.\n\t// This returns -1, 0, or 1 if this version is smaller,\n\t// equal, or larger than the other version, respectively.\n\tCompare(*Version) (int, error)\n}\n"
  },
  {
    "path": "grype/version/constraint.go",
    "content": "package version\n\nimport \"fmt\"\n\ntype Constraint interface {\n\tfmt.Stringer\n\tValue() string\n\tFormat() Format\n\tSatisfied(*Version) (bool, error)\n}\n\nfunc GetConstraint(constStr string, format Format) (Constraint, error) {\n\tvar c Constraint\n\tvar err error\n\n\tswitch format {\n\tcase ApkFormat:\n\t\tc, err = newGenericConstraint(ApkFormat, constStr)\n\tcase SemanticFormat:\n\t\tc, err = newGenericConstraint(SemanticFormat, constStr)\n\tcase BitnamiFormat:\n\t\tc, err = newGenericConstraint(BitnamiFormat, constStr)\n\tcase GemFormat:\n\t\tc, err = newGenericConstraint(GemFormat, constStr)\n\tcase DebFormat:\n\t\tc, err = newGenericConstraint(DebFormat, constStr)\n\tcase GolangFormat:\n\t\tc, err = newGenericConstraint(GolangFormat, constStr)\n\tcase MavenFormat:\n\t\tc, err = newGenericConstraint(MavenFormat, constStr)\n\tcase RpmFormat:\n\t\tc, err = newGenericConstraint(RpmFormat, constStr)\n\tcase PythonFormat:\n\t\tc, err = newGenericConstraint(PythonFormat, constStr)\n\tcase KBFormat:\n\t\tc, err = newKBConstraint(constStr)\n\tcase PortageFormat:\n\t\tc, err = newGenericConstraint(PortageFormat, constStr)\n\tcase PacmanFormat:\n\t\tc, err = newGenericConstraint(PacmanFormat, constStr)\n\tcase JVMFormat:\n\t\tc, err = newGenericConstraint(JVMFormat, constStr)\n\tcase UnknownFormat:\n\t\tc, err = newFuzzyConstraint(constStr, \"unknown\")\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"could not find constraint for given format: %s\", format)\n\t}\n\n\treturn c, err\n}\n\n// MustGetConstraint is meant for testing only, do not use within the library\nfunc MustGetConstraint(constStr string, format Format) Constraint {\n\tc, err := GetConstraint(constStr, format)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn c\n}\n"
  },
  {
    "path": "grype/version/deb_version.go",
    "content": "package version\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n\n\tdeb \"github.com/knqyf263/go-deb-version\"\n)\n\nvar _ Comparator = (*debVersion)(nil)\n\ntype debVersion struct {\n\tobj   deb.Version\n\tepoch *int // extracted manually since library doesn't export it\n\traw   string\n}\n\nfunc newDebVersion(raw string) (debVersion, error) {\n\tver, err := deb.NewVersion(raw)\n\tif err != nil {\n\t\treturn debVersion{}, invalidFormatError(DebFormat, raw, err)\n\t}\n\n\t// Extract epoch manually for auto strategy support\n\tepoch := extractDebEpoch(raw)\n\n\treturn debVersion{\n\t\tobj:   ver,\n\t\tepoch: epoch,\n\t\traw:   raw,\n\t}, nil\n}\n\nfunc (v debVersion) Compare(other *Version) (int, error) {\n\tif other == nil {\n\t\treturn -1, ErrNoVersionProvided\n\t}\n\n\to, err := newDebVersion(other.Raw)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\treturn v.obj.Compare(o.obj), nil\n}\n\n// CompareWithConfig compares two deb versions using the provided comparison\n// configuration. The config controls behavior for missing epochs:\n//   - \"zero\" strategy: missing epochs are treated as 0\n//   - \"auto\" strategy: missing epochs in the package version match the constraint's epoch\n//\n// Returns:\n//\n//\t-1 if v < other\n//\t 0 if v == other\n//\t 1 if v > other\nfunc (v debVersion) CompareWithConfig(other *Version, cfg ComparisonConfig) (int, error) {\n\tif other == nil {\n\t\treturn -1, ErrNoVersionProvided\n\t}\n\n\to, err := newDebVersion(other.Raw)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\t// Handle auto strategy: if package (v) is missing epoch but constraint (other) has one,\n\t// temporarily inject the constraint's epoch into the package version\n\tif cfg.MissingEpochStrategy == MissingEpochStrategyAuto {\n\t\tif v.epoch == nil && o.epoch != nil {\n\t\t\t// Create a temporary version string with the constraint's epoch\n\t\t\tversionWithEpoch := strconv.Itoa(*o.epoch) + \":\" + v.raw\n\t\t\tvWithEpoch, err := deb.NewVersion(versionWithEpoch)\n\t\t\tif err != nil {\n\t\t\t\t// Fall back to normal comparison if we can't create the modified version\n\t\t\t\treturn normalizeComparison(v.obj.Compare(o.obj)), nil\n\t\t\t}\n\t\t\treturn normalizeComparison(vWithEpoch.Compare(o.obj)), nil\n\t\t}\n\t}\n\n\treturn normalizeComparison(v.obj.Compare(o.obj)), nil\n}\n\n// normalizeComparison normalizes a comparison result to -1, 0, or 1\nfunc normalizeComparison(cmp int) int {\n\tif cmp < 0 {\n\t\treturn -1\n\t}\n\tif cmp > 0 {\n\t\treturn 1\n\t}\n\treturn 0\n}\n\n// extractDebEpoch extracts the epoch from a Debian version string.\n// Returns nil if no epoch is present.\nfunc extractDebEpoch(raw string) *int {\n\t// Debian version format: [epoch:]upstream_version[-debian_revision]\n\t// Epoch is optional and separated by a colon\n\tcolonIndex := strings.Index(raw, \":\")\n\tif colonIndex == -1 {\n\t\treturn nil\n\t}\n\n\tepochStr := raw[:colonIndex]\n\tepoch, err := strconv.Atoi(epochStr)\n\tif err != nil {\n\t\treturn nil\n\t}\n\n\treturn &epoch\n}\n"
  },
  {
    "path": "grype/version/deb_version_test.go",
    "content": "package version\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestDebVersion_Constraint(t *testing.T) {\n\ttests := []testCase{\n\t\t// empty values\n\t\t{version: \"2.3.1\", constraint: \"\", satisfied: true},\n\t\t// compound conditions\n\t\t{version: \"2.3.1\", constraint: \"> 1.0.0, < 2.0.0\", satisfied: false},\n\t\t{version: \"1.3.1\", constraint: \"> 1.0.0, < 2.0.0\", satisfied: true},\n\t\t{version: \"2.0.0\", constraint: \"> 1.0.0, <= 2.0.0\", satisfied: true},\n\t\t{version: \"2.0.0\", constraint: \"> 1.0.0, < 2.0.0\", satisfied: false},\n\t\t{version: \"1.0.0\", constraint: \">= 1.0.0, < 2.0.0\", satisfied: true},\n\t\t{version: \"1.0.0\", constraint: \"> 1.0.0, < 2.0.0\", satisfied: false},\n\t\t{version: \"0.9.0\", constraint: \"> 1.0.0, < 2.0.0\", satisfied: false},\n\t\t{version: \"1.5.0\", constraint: \"> 0.1.0, < 0.5.0 || > 1.0.0, < 2.0.0\", satisfied: true},\n\t\t{version: \"0.2.0\", constraint: \"> 0.1.0, < 0.5.0 || > 1.0.0, < 2.0.0\", satisfied: true},\n\t\t{version: \"0.0.1\", constraint: \"> 0.1.0, < 0.5.0 || > 1.0.0, < 2.0.0\", satisfied: false},\n\t\t{version: \"0.6.0\", constraint: \"> 0.1.0, < 0.5.0 || > 1.0.0, < 2.0.0\", satisfied: false},\n\t\t{version: \"2.5.0\", constraint: \"> 0.1.0, < 0.5.0 || > 1.0.0, < 2.0.0\", satisfied: false},\n\t\t// fixed-in scenarios\n\t\t{version: \"2.3.1\", constraint: \"< 2.0.0\", satisfied: false},\n\t\t{version: \"2.3.1\", constraint: \"< 2.0\", satisfied: false},\n\t\t{version: \"2.3.1\", constraint: \"< 2\", satisfied: false},\n\t\t{version: \"2.3.1\", constraint: \"< 2.3\", satisfied: false},\n\t\t{version: \"2.3.1\", constraint: \"< 2.3.1\", satisfied: false},\n\t\t{version: \"2.3.1\", constraint: \"< 2.3.2\", satisfied: true},\n\t\t{version: \"2.3.1\", constraint: \"< 2.4\", satisfied: true},\n\t\t{version: \"2.3.1\", constraint: \"< 3\", satisfied: true},\n\t\t{version: \"2.3.1\", constraint: \"< 3.0\", satisfied: true},\n\t\t{version: \"2.3.1\", constraint: \"< 3.0.0\", satisfied: true},\n\t\t{version: \"2.3.1-1ubuntu0.14.04.1\", constraint: \" <2.0.0\", satisfied: false},\n\t\t{version: \"2.3.1-1ubuntu0.14.04.1\", constraint: \" <2.0\", satisfied: false},\n\t\t{version: \"2.3.1-1ubuntu0.14.04.1\", constraint: \" <2\", satisfied: false},\n\t\t{version: \"2.3.1-1ubuntu0.14.04.1\", constraint: \" <2.3\", satisfied: false},\n\t\t{version: \"2.3.1-1ubuntu0.14.04.1\", constraint: \" <2.3.1\", satisfied: false},\n\t\t{version: \"2.3.1-1ubuntu0.14.04.1\", constraint: \" <2.3.2\", satisfied: true},\n\t\t{version: \"2.3.1-1ubuntu0.14.04.1\", constraint: \" <2.4\", satisfied: true},\n\t\t{version: \"2.3.1-1ubuntu0.14.04.1\", constraint: \" <3\", satisfied: true},\n\t\t{version: \"2.3.1-1ubuntu0.14.04.1\", constraint: \" <3.0\", satisfied: true},\n\t\t{version: \"2.3.1-1ubuntu0.14.04.1\", constraint: \" <3.0.0\", satisfied: true},\n\t\t{version: \"7u151-2.6.11-2ubuntu0.14.04.1\", constraint: \" < 7u151-2.6.11-2ubuntu0.14.04.1\", satisfied: false},\n\t\t{version: \"7u151-2.6.11-2ubuntu0.14.04.1\", constraint: \" < 7u151-2.6.11\", satisfied: false},\n\t\t{version: \"7u151-2.6.11-2ubuntu0.14.04.1\", constraint: \" < 7u151-2.7\", satisfied: false},\n\t\t{version: \"7u151-2.6.11-2ubuntu0.14.04.1\", constraint: \" < 7u151\", satisfied: false},\n\t\t{version: \"7u151-2.6.11-2ubuntu0.14.04.1\", constraint: \" < 7u150\", satisfied: false},\n\t\t{version: \"7u151-2.6.11-2ubuntu0.14.04.1\", constraint: \" < 7u152\", satisfied: true},\n\t\t{version: \"7u151-2.6.11-2ubuntu0.14.04.1\", constraint: \" < 7u152-2.6.11-2ubuntu0.14.04.1\", satisfied: true},\n\t\t{version: \"7u151-2.6.11-2ubuntu0.14.04.1\", constraint: \" < 8u1-2.6.11-2ubuntu0.14.04.1\", satisfied: true},\n\t\t{version: \"43.0.2357.81-0ubuntu0.14.04.1.1089\", constraint: \"<43\", satisfied: false},\n\t\t{version: \"43.0.2357.81-0ubuntu0.14.04.1.1089\", constraint: \"<43.0\", satisfied: false},\n\t\t{version: \"43.0.2357.81-0ubuntu0.14.04.1.1089\", constraint: \"<43.0.2357\", satisfied: false},\n\t\t{version: \"43.0.2357.81-0ubuntu0.14.04.1.1089\", constraint: \"<43.0.2357.81\", satisfied: false},\n\t\t{version: \"43.0.2357.81-0ubuntu0.14.04.1.1089\", constraint: \"<43.0.2357.81-0ubuntu0.14.04.1.1089\", satisfied: false},\n\t\t{version: \"43.0.2357.81-0ubuntu0.14.04.1.1089\", constraint: \"<43.0.2357.82-0ubuntu0.14.04.1.1089\", satisfied: true},\n\t\t{version: \"43.0.2357.81-0ubuntu0.14.04.1.1089\", constraint: \"<43.0.2358-0ubuntu0.14.04.1.1089\", satisfied: true},\n\t\t{version: \"43.0.2357.81-0ubuntu0.14.04.1.1089\", constraint: \"<43.1-0ubuntu0.14.04.1.1089\", satisfied: true},\n\t\t{version: \"43.0.2357.81-0ubuntu0.14.04.1.1089\", constraint: \"<44-0ubuntu0.14.04.1.1089\", satisfied: true},\n\t\t// epoch - both sides have epoch\n\t\t{version: \"1:0\", constraint: \"< 0:1\", satisfied: false},\n\t\t{version: \"2:4.19.01-1\", constraint: \"< 2:4.19.1-1\", satisfied: false},\n\t\t{version: \"2:4.19.01-1\", constraint: \"<= 2:4.19.1-1\", satisfied: true},\n\t\t{version: \"0:4.19.1-1\", constraint: \"< 2:4.19.1-1\", satisfied: true},\n\t\t{version: \"11:4.19.0-1\", constraint: \"< 12:4.19.0-1\", satisfied: true},\n\t\t{version: \"13:4.19.0-1\", constraint: \"< 12:4.19.0-1\", satisfied: false},\n\t\t// epoch - missing epoch treated as 0 (standard dpkg behavior)\n\t\t// This differs from RPM which ignores epochs when only one side has them\n\t\t{version: \"1:0\", constraint: \"< 1\", satisfied: false},                 // 1:0 vs 0:1 -> epoch 1 > 0, not satisfied\n\t\t{version: \"0:0\", constraint: \"< 0\", satisfied: false},                 // 0:0 vs 0:0 -> equal, not satisfied\n\t\t{version: \"0:0\", constraint: \"= 0\", satisfied: true},                  // 0:0 vs 0:0 -> equal\n\t\t{version: \"0\", constraint: \"= 0:0\", satisfied: true},                  // 0:0 vs 0:0 -> equal\n\t\t{version: \"1.0\", constraint: \"< 2:1.0\", satisfied: true},              // 0:1.0 vs 2:1.0 -> epoch 0 < 2, satisfied\n\t\t{version: \"1.0\", constraint: \"<= 2:1.0\", satisfied: true},             // 0:1.0 vs 2:1.0 -> epoch 0 < 2, satisfied\n\t\t{version: \"1:2\", constraint: \"< 1\", satisfied: false},                 // 1:2 vs 0:1 -> epoch 1 > 0, not satisfied\n\t\t{version: \"1:2\", constraint: \"> 1\", satisfied: true},                  // 1:2 vs 0:1 -> epoch 1 > 0, satisfied\n\t\t{version: \"2:4.19.01-1\", constraint: \"< 4.19.1-1\", satisfied: false},  // epoch 2 > 0\n\t\t{version: \"2:4.19.01-1\", constraint: \"<= 4.19.1-1\", satisfied: false}, // epoch 2 > 0\n\t\t{version: \"4.19.01-1\", constraint: \"< 2:4.19.1-1\", satisfied: true},   // epoch 0 < 2\n\t\t{version: \"4.19.0-1\", constraint: \"< 12:4.19.0-1\", satisfied: true},   // epoch 0 < 12\n\t\t{version: \"4.19.0-1\", constraint: \"<= 12:4.19.0-1\", satisfied: true},  // epoch 0 < 12\n\t\t{version: \"3:4.19.0-1\", constraint: \"< 4.21.0-1\", satisfied: false},   // epoch 3 > 0\n\t\t// real-world debian version formats with epochs\n\t\t{version: \"1.5.4-2+deb9u1\", constraint: \"< 0:1.5.4-2+deb9u1\", satisfied: false},\n\t\t{version: \"1.5.4-2+deb9u1\", constraint: \"<= 0:1.5.4-2+deb9u1\", satisfied: true},\n\t\t{version: \"1.5.4-2+deb9u1\", constraint: \"< 1:1.5.4-2+deb9u1\", satisfied: true}, // epoch 0 < 1\n\t\t{version: \"8.3.1-5ubuntu1\", constraint: \"< 0:8.3.1-5ubuntu2\", satisfied: true},\n\t\t{version: \"8.3.1-5ubuntu1.40\", constraint: \"< 0:8.3.1-5ubuntu1.5\", satisfied: false},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.tName(), func(t *testing.T) {\n\t\t\tconstraint, err := GetConstraint(test.constraint, DebFormat)\n\t\t\trequire.NoError(t, err, \"unexpected error from GetConstraint: %v\", err)\n\n\t\t\ttest.assertVersionConstraint(t, DebFormat, constraint)\n\t\t})\n\t}\n}\n\nfunc TestDebVersion_Compare(t *testing.T) {\n\ttests := []struct {\n\t\tname           string\n\t\tthisVersion    string\n\t\totherVersion   string\n\t\totherFormat    Format\n\t\texpectError    bool\n\t\terrorSubstring string\n\t}{\n\t\t{\n\t\t\tname:         \"same format successful comparison\",\n\t\t\tthisVersion:  \"1.2.3-1\",\n\t\t\totherVersion: \"1.2.3-2\",\n\t\t\totherFormat:  DebFormat,\n\t\t\texpectError:  false,\n\t\t},\n\t\t{\n\t\t\tname:         \"unknown format attempts upgrade - valid deb format\",\n\t\t\tthisVersion:  \"1.2.3-1\",\n\t\t\totherVersion: \"1.2.3-2\",\n\t\t\totherFormat:  UnknownFormat,\n\t\t\texpectError:  false,\n\t\t},\n\t\t{\n\t\t\tname:         \"with epochs\",\n\t\t\tthisVersion:  \"1:1.2.3-1\",\n\t\t\totherVersion: \"1:1.2.3-2\",\n\t\t\totherFormat:  DebFormat,\n\t\t\texpectError:  false,\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tthisVer := New(test.thisVersion, DebFormat)\n\n\t\t\totherVer := New(test.otherVersion, test.otherFormat)\n\n\t\t\tresult, err := thisVer.Compare(otherVer)\n\n\t\t\tif test.expectError {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\tif test.errorSubstring != \"\" {\n\t\t\t\t\tassert.True(t, strings.Contains(err.Error(), test.errorSubstring),\n\t\t\t\t\t\t\"Expected error to contain '%s', got: %v\", test.errorSubstring, err)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.Contains(t, []int{-1, 0, 1}, result, \"Expected comparison result to be -1, 0, or 1\")\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDebVersion_Compare_Epochs(t *testing.T) {\n\t// Test epoch comparison behavior - this is critical for vulnerability matching\n\t// The deb library treats missing epochs as 0, which differs from RPM behavior\n\ttests := []struct {\n\t\tname   string\n\t\tv1     string\n\t\tv2     string\n\t\texpect string // \"less\", \"equal\", \"greater\"\n\t}{\n\t\t// both have epochs - standard comparison\n\t\t{\"epoch 1 vs 0, same version\", \"1:0\", \"0:1\", \"greater\"},\n\t\t{\"same epoch, different version\", \"1:2\", \"1:1\", \"greater\"},\n\t\t{\"different epochs, same version\", \"0:4.19.1-1\", \"2:4.19.1-1\", \"less\"},\n\t\t{\"equal with epochs\", \"2:1.0-1\", \"2:1.0-1\", \"equal\"},\n\n\t\t// missing epoch treated as 0 (differs from RPM which ignores one-sided epochs)\n\t\t{\"epoch 1 vs missing (treated as 0)\", \"1:0\", \"1\", \"greater\"},    // 1:0 vs 0:1 -> epoch 1 > 0\n\t\t{\"epoch 2 vs missing\", \"2:4.19.01-1\", \"4.19.1-1\", \"greater\"},    // epoch 2 > 0\n\t\t{\"missing vs epoch 2\", \"4.19.01-1\", \"2:4.19.1-1\", \"less\"},       // epoch 0 < 2\n\t\t{\"missing vs epoch 12\", \"4.19.0-1\", \"12:4.19.0-1\", \"less\"},      // epoch 0 < 12\n\t\t{\"epoch 3 vs missing\", \"3:4.19.0-1\", \"4.21.0-1\", \"greater\"},     // epoch 3 > 0\n\t\t{\"missing epoch equal to 0 epoch\", \"1.0-1\", \"0:1.0-1\", \"equal\"}, // 0:1.0-1 == 0:1.0-1\n\t\t{\"explicit 0 epoch vs missing\", \"0:1.0-1\", \"1.0-1\", \"equal\"},    // 0:1.0-1 == 0:1.0-1\n\n\t\t// tilde behavior (not epoch-specific but important for deb)\n\t\t{\"tilde sorts before everything\", \"1.0~rc1-1\", \"1.0-1\", \"less\"},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tv1 := New(test.v1, DebFormat)\n\t\t\tv2 := New(test.v2, DebFormat)\n\n\t\t\tactual, err := v1.Compare(v2)\n\t\t\trequire.NoError(t, err, \"unexpected error comparing versions: %s vs %s\", test.v1, test.v2)\n\n\t\t\tswitch test.expect {\n\t\t\tcase \"less\":\n\t\t\t\tassert.Less(t, actual, 0, \"expected %s < %s\", test.v1, test.v2)\n\t\t\tcase \"equal\":\n\t\t\t\tassert.Equal(t, 0, actual, \"expected %s == %s\", test.v1, test.v2)\n\t\t\tcase \"greater\":\n\t\t\t\tassert.Greater(t, actual, 0, \"expected %s > %s\", test.v1, test.v2)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDebVersion_CompareWithConfig(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tversion  string\n\t\tother    string\n\t\tstrategy MissingEpochStrategy\n\t\twant     int // -1, 0, or 1\n\t}{\n\t\t{\n\t\t\tname:     \"package has epoch, no behavior change with auto\",\n\t\t\tversion:  \"1:2.0.0\",\n\t\t\tother:    \"1:1.5.0\",\n\t\t\tstrategy: MissingEpochStrategyAuto,\n\t\t\twant:     1, // 1:2.0.0 > 1:1.5.0\n\t\t},\n\t\t{\n\t\t\tname:     \"package has epoch, no behavior change with zero\",\n\t\t\tversion:  \"1:2.0.0\",\n\t\t\tother:    \"1:1.5.0\",\n\t\t\tstrategy: \"zero\",\n\t\t\twant:     1, // 1:2.0.0 > 1:1.5.0\n\t\t},\n\t\t{\n\t\t\tname:     \"package missing epoch, constraint has epoch, auto strategy - no match\",\n\t\t\tversion:  \"2.0.0\",\n\t\t\tother:    \"1:1.5.0\",\n\t\t\tstrategy: MissingEpochStrategyAuto,\n\t\t\twant:     1, // Treated as 1:2.0.0 > 1:1.5.0\n\t\t},\n\t\t{\n\t\t\tname:     \"package missing epoch, constraint has epoch, zero strategy - match\",\n\t\t\tversion:  \"2.0.0\",\n\t\t\tother:    \"1:1.5.0\",\n\t\t\tstrategy: \"zero\",\n\t\t\twant:     -1, // Treated as 0:2.0.0 < 1:1.5.0\n\t\t},\n\t\t{\n\t\t\tname:     \"both missing epoch, auto strategy\",\n\t\t\tversion:  \"2.0.0\",\n\t\t\tother:    \"1.5.0\",\n\t\t\tstrategy: MissingEpochStrategyAuto,\n\t\t\twant:     1, // 2.0.0 > 1.5.0\n\t\t},\n\t\t{\n\t\t\tname:     \"both missing epoch, zero strategy\",\n\t\t\tversion:  \"2.0.0\",\n\t\t\tother:    \"1.5.0\",\n\t\t\tstrategy: \"zero\",\n\t\t\twant:     1, // 2.0.0 > 1.5.0\n\t\t},\n\t\t{\n\t\t\tname:     \"constraint missing epoch, package has epoch\",\n\t\t\tversion:  \"1:2.0.0-1\",\n\t\t\tother:    \"1.5.0-1\",\n\t\t\tstrategy: MissingEpochStrategyAuto,\n\t\t\twant:     1, // 1:2.0.0 > 0:1.5.0 (constraint gets epoch 0)\n\t\t},\n\t\t{\n\t\t\tname:     \"auto strategy, package less than constraint\",\n\t\t\tversion:  \"1.0.0\",\n\t\t\tother:    \"1:1.5.0\",\n\t\t\tstrategy: MissingEpochStrategyAuto,\n\t\t\twant:     -1, // Treated as 1:1.0.0 < 1:1.5.0\n\t\t},\n\t\t{\n\t\t\tname:     \"auto strategy, different epochs on constraints\",\n\t\t\tversion:  \"1.2.0\",\n\t\t\tother:    \"2:1.5.0\",\n\t\t\tstrategy: MissingEpochStrategyAuto,\n\t\t\twant:     -1, // Treated as 2:1.2.0 < 2:1.5.0\n\t\t},\n\t\t{\n\t\t\tname:     \"zero strategy, package version newer but lower epoch\",\n\t\t\tversion:  \"3.0.0\",\n\t\t\tother:    \"1:1.0.0\",\n\t\t\tstrategy: \"zero\",\n\t\t\twant:     -1, // 0:3.0.0 < 1:1.0.0 because epoch 0 < 1\n\t\t},\n\t\t{\n\t\t\tname:     \"auto strategy, equal versions different missing epochs\",\n\t\t\tversion:  \"1.2.3-1\",\n\t\t\tother:    \"1:1.2.3-1\",\n\t\t\tstrategy: MissingEpochStrategyAuto,\n\t\t\twant:     0, // Treated as 1:1.2.3 == 1:1.2.3\n\t\t},\n\t\t{\n\t\t\tname:     \"zero strategy, equal versions different missing epochs\",\n\t\t\tversion:  \"1.2.3-1\",\n\t\t\tother:    \"1:1.2.3-1\",\n\t\t\tstrategy: \"zero\",\n\t\t\twant:     -1, // 0:1.2.3 < 1:1.2.3\n\t\t},\n\t\t{\n\t\t\tname:     \"auto strategy, large epoch difference\",\n\t\t\tversion:  \"1.0.0\",\n\t\t\tother:    \"999:0.5.0\",\n\t\t\tstrategy: MissingEpochStrategyAuto,\n\t\t\twant:     1, // Treated as 999:1.0.0 > 999:0.5.0\n\t\t},\n\t\t{\n\t\t\tname:     \"zero strategy, large epoch difference\",\n\t\t\tversion:  \"1.0.0\",\n\t\t\tother:    \"999:0.5.0\",\n\t\t\tstrategy: \"zero\",\n\t\t\twant:     -1, // 0:1.0.0 < 999:0.5.0\n\t\t},\n\t\t{\n\t\t\tname:     \"both have epochs, strategy should not matter\",\n\t\t\tversion:  \"2:1.5.0-1\",\n\t\t\tother:    \"1:2.0.0-1\",\n\t\t\tstrategy: MissingEpochStrategyAuto,\n\t\t\twant:     1, // 2:1.5.0 > 1:2.0.0 (epoch takes precedence)\n\t\t},\n\t\t{\n\t\t\tname:     \"both have same epoch, strategy should not matter\",\n\t\t\tversion:  \"3:2.0.0-1\",\n\t\t\tother:    \"3:1.5.0-1\",\n\t\t\tstrategy: \"zero\",\n\t\t\twant:     1, // 3:2.0.0 > 3:1.5.0\n\t\t},\n\t\t{\n\t\t\tname:     \"debian revision comparison with auto\",\n\t\t\tversion:  \"1.0-1ubuntu1\",\n\t\t\tother:    \"1:1.0-1ubuntu1\",\n\t\t\tstrategy: MissingEpochStrategyAuto,\n\t\t\twant:     0, // Treated as 1:1.0-1ubuntu1 == 1:1.0-1ubuntu1\n\t\t},\n\t\t{\n\t\t\tname:     \"empty strategy uses default behavior (zero-like)\",\n\t\t\tversion:  \"2.0.0\",\n\t\t\tother:    \"1:1.5.0\",\n\t\t\tstrategy: \"\",\n\t\t\twant:     -1, // Should behave like zero strategy when empty\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tv1, err := newDebVersion(tt.version)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tv2 := &Version{\n\t\t\t\tFormat: DebFormat,\n\t\t\t\tRaw:    tt.other,\n\t\t\t}\n\n\t\t\tcfg := ComparisonConfig{\n\t\t\t\tMissingEpochStrategy: tt.strategy,\n\t\t\t}\n\n\t\t\tresult, err := v1.CompareWithConfig(v2, cfg)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, tt.want, result,\n\t\t\t\t\"comparing %s vs %s with strategy %s\",\n\t\t\t\ttt.version, tt.other, tt.strategy)\n\t\t})\n\t}\n}\n\nfunc TestDebVersion_CompareWithConfig_ErrorCases(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tversion  string\n\t\tother    *Version\n\t\tstrategy MissingEpochStrategy\n\t\twantErr  bool\n\t}{\n\t\t{\n\t\t\tname:     \"nil other version\",\n\t\t\tversion:  \"1.0.0\",\n\t\t\tother:    nil,\n\t\t\tstrategy: MissingEpochStrategyAuto,\n\t\t\twantErr:  true,\n\t\t},\n\t\t{\n\t\t\tname:     \"invalid other version format\",\n\t\t\tversion:  \"1.0.0\",\n\t\t\tother:    &Version{Format: DebFormat, Raw: \"not-a-valid-debian-version!@#$%\"},\n\t\t\tstrategy: MissingEpochStrategyAuto,\n\t\t\twantErr:  true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tv1, err := newDebVersion(tt.version)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tcfg := ComparisonConfig{\n\t\t\t\tMissingEpochStrategy: tt.strategy,\n\t\t\t}\n\n\t\t\t_, err = v1.CompareWithConfig(tt.other, cfg)\n\t\t\tif tt.wantErr {\n\t\t\t\tassert.Error(t, err)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDebVersion_CompareWithConfig_ConsistencyWithCompare(t *testing.T) {\n\t// Test that when both versions have epochs, CompareWithConfig gives same result as Compare\n\ttests := []struct {\n\t\tv1 string\n\t\tv2 string\n\t}{\n\t\t{\"1:2.0.0-1\", \"1:1.5.0-1\"},\n\t\t{\"2:1.0.0-1ubuntu1\", \"1:2.0.0-1ubuntu1\"},\n\t\t{\"0:1.2.3-1\", \"0:1.2.3-1\"},\n\t\t{\"5:1.0.0-1\", \"5:1.0.0-2\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.v1+\"_vs_\"+tt.v2, func(t *testing.T) {\n\t\t\tv1, _ := newDebVersion(tt.v1)\n\t\t\tv2 := &Version{Format: DebFormat, Raw: tt.v2}\n\n\t\t\t// Test with both strategies\n\t\t\tfor _, strategy := range []MissingEpochStrategy{MissingEpochStrategyZero, MissingEpochStrategyAuto} {\n\t\t\t\tcfg := ComparisonConfig{MissingEpochStrategy: strategy}\n\n\t\t\t\tresultWithConfig, err1 := v1.CompareWithConfig(v2, cfg)\n\t\t\t\trequire.NoError(t, err1)\n\n\t\t\t\tresultNormal, err2 := v1.Compare(v2)\n\t\t\t\trequire.NoError(t, err2)\n\n\t\t\t\tassert.Equal(t, resultNormal, resultWithConfig,\n\t\t\t\t\t\"when both versions have epochs, CompareWithConfig should match Compare (strategy: %s)\", strategy)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestExtractDebEpoch(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tversion  string\n\t\twantNil  bool\n\t\texpected int\n\t}{\n\t\t{\n\t\t\tname:     \"no epoch\",\n\t\t\tversion:  \"1.2.3-1\",\n\t\t\twantNil:  true,\n\t\t\texpected: 0,\n\t\t},\n\t\t{\n\t\t\tname:     \"epoch 0\",\n\t\t\tversion:  \"0:1.2.3-1\",\n\t\t\twantNil:  false,\n\t\t\texpected: 0,\n\t\t},\n\t\t{\n\t\t\tname:     \"epoch 1\",\n\t\t\tversion:  \"1:1.2.3-1\",\n\t\t\twantNil:  false,\n\t\t\texpected: 1,\n\t\t},\n\t\t{\n\t\t\tname:     \"large epoch\",\n\t\t\tversion:  \"999:1.0.0\",\n\t\t\twantNil:  false,\n\t\t\texpected: 999,\n\t\t},\n\t\t{\n\t\t\tname:     \"epoch with complex version\",\n\t\t\tversion:  \"5:2.0.0-1ubuntu0.14.04.1\",\n\t\t\twantNil:  false,\n\t\t\texpected: 5,\n\t\t},\n\t\t{\n\t\t\tname:     \"multiple colons - only first is epoch\",\n\t\t\tversion:  \"1:2:3.4.5\",\n\t\t\twantNil:  false,\n\t\t\texpected: 1,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := extractDebEpoch(tt.version)\n\t\t\tif tt.wantNil {\n\t\t\t\tassert.Nil(t, result, \"expected nil epoch for version %s\", tt.version)\n\t\t\t} else {\n\t\t\t\trequire.NotNil(t, result, \"expected non-nil epoch for version %s\", tt.version)\n\t\t\t\tassert.Equal(t, tt.expected, *result, \"epoch value mismatch for version %s\", tt.version)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/version/deprecated.go",
    "content": "package version\n\n// NewVersion creates a new Version instance with the provided raw version string and format.\n//\n// Deprecated: NewVersion is deprecated, use New instead.\nfunc NewVersion(raw string, format Format) *Version {\n\treturn New(raw, format)\n}\n"
  },
  {
    "path": "grype/version/error.go",
    "content": "package version\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n)\n\n// ErrUnsupportedVersion is returned when a version string cannot be parsed because the value is known\n// to cause issues or is otherwise problematic (e.g. golang \"devel\" version).\nvar ErrUnsupportedVersion = fmt.Errorf(\"unsupported version value\")\n\n// ErrNoVersionProvided is returned when a version is attempted to be compared, but no other version is provided to compare against.\nvar ErrNoVersionProvided = errors.New(\"no version provided for comparison\")\n\n// UnsupportedComparisonError represents an error when a format doesn't match the expected format\ntype UnsupportedComparisonError struct {\n\tLeft  Format\n\tRight *Version\n}\n\n// newUnsupportedFormatError creates a new UnsupportedComparisonError\nfunc newUnsupportedFormatError(left Format, right *Version) *UnsupportedComparisonError {\n\treturn &UnsupportedComparisonError{\n\t\tLeft:  left,\n\t\tRight: right,\n\t}\n}\n\nfunc (e *UnsupportedComparisonError) Error() string {\n\treturn fmt.Sprintf(\"(%s) unsupported version comparison: value=%q format=%q\", e.Left, e.Right.Raw, e.Right.Format)\n}\n\nfunc (e *UnsupportedComparisonError) Is(target error) bool {\n\tvar t *UnsupportedComparisonError\n\tok := errors.As(target, &t)\n\tif !ok {\n\t\treturn false\n\t}\n\treturn (t.Left == UnknownFormat || t.Left == e.Left) &&\n\t\t(t.Right.Format == UnknownFormat || t.Right == e.Right)\n}\n\nfunc invalidFormatError(format Format, raw string, err error) error {\n\treturn fmt.Errorf(\"invalid %s version from '%s': %w\", format.String(), raw, err)\n}\n\n// NonFatalConstraintError should be used any time an unexpected but recoverable condition is encountered while\n// checking version constraint satisfaction. The error should get returned by any implementer of the Constraint\n// interface. If returned by the Satisfied method on the Constraint interface, this error will be caught and\n// logged as a warning in the FindMatchesByPackageDistro function in grype/matcher/common/distro_matchers.go\ntype NonFatalConstraintError struct {\n\tconstraint Constraint\n\tversion    *Version\n\tmessage    string\n}\n\nfunc (e NonFatalConstraintError) Error() string {\n\treturn fmt.Sprintf(\"matching raw constraint %s against version %s caused a non-fatal error: %s\", e.constraint, e.version, e.message)\n}\n"
  },
  {
    "path": "grype/version/format.go",
    "content": "package version\n\nimport (\n\t\"strings\"\n\n\t\"github.com/anchore/packageurl-go\"\n)\n\nconst (\n\tUnknownFormat Format = iota\n\tSemanticFormat\n\tApkFormat\n\tDebFormat\n\tMavenFormat\n\tRpmFormat\n\tPythonFormat\n\tKBFormat\n\tGemFormat\n\tPortageFormat\n\tGolangFormat\n\tJVMFormat\n\tBitnamiFormat\n\tPacmanFormat\n)\n\ntype Format int\n\nvar formatStr = []string{\n\t\"Unknown\",\n\t\"Semantic\",\n\t\"Apk\",\n\t\"Deb\",\n\t\"Maven\",\n\t\"RPM\",\n\t\"Python\",\n\t\"KB\",\n\t\"Gem\",\n\t\"Portage\",\n\t\"Go\",\n\t\"JVM\",\n\t\"Bitnami\",\n\t\"Pacman\",\n}\n\nvar Formats = []Format{\n\tSemanticFormat,\n\tApkFormat,\n\tDebFormat,\n\tMavenFormat,\n\tRpmFormat,\n\tPythonFormat,\n\tKBFormat,\n\tGemFormat,\n\tPortageFormat,\n\tGolangFormat,\n\tJVMFormat,\n\tBitnamiFormat,\n\tPacmanFormat,\n}\n\nfunc ParseFormat(userStr string) Format {\n\tswitch strings.ToLower(userStr) {\n\t// sever includes known ecosystem types that use semver or a very semver-like schemes\n\tcase strings.ToLower(SemanticFormat.String()), \"semver\", packageurl.TypeNPM, packageurl.TypeNuget, packageurl.TypeComposer, packageurl.TypeHex, packageurl.TypePub, packageurl.TypeSwift, packageurl.TypeConan, packageurl.TypeCocoapods, packageurl.TypeHackage:\n\t\treturn SemanticFormat\n\tcase strings.ToLower(ApkFormat.String()), \"apk\":\n\t\treturn ApkFormat\n\tcase strings.ToLower(BitnamiFormat.String()), \"bitnami\":\n\t\treturn BitnamiFormat\n\tcase strings.ToLower(DebFormat.String()), \"dpkg\", packageurl.TypeDebian:\n\t\treturn DebFormat\n\tcase strings.ToLower(GolangFormat.String()), \"go\", packageurl.TypeGolang:\n\t\treturn GolangFormat\n\tcase strings.ToLower(MavenFormat.String()), \"maven\":\n\t\treturn MavenFormat\n\tcase strings.ToLower(RpmFormat.String()), \"rpm\":\n\t\treturn RpmFormat\n\tcase strings.ToLower(PythonFormat.String()), \"python\", packageurl.TypePyPi, \"pep440\":\n\t\treturn PythonFormat\n\tcase strings.ToLower(KBFormat.String()), \"kb\":\n\t\treturn KBFormat\n\tcase strings.ToLower(GemFormat.String()), \"gem\":\n\t\treturn GemFormat\n\tcase strings.ToLower(PortageFormat.String()), \"portage\":\n\t\treturn PortageFormat\n\tcase strings.ToLower(JVMFormat.String()), \"jvm\", \"jre\", \"jdk\", \"openjdk\", \"jep223\":\n\t\treturn JVMFormat\n\tcase strings.ToLower(PacmanFormat.String()), \"pacman\":\n\t\treturn PacmanFormat\n\t}\n\treturn UnknownFormat\n}\n\nfunc (f Format) String() string {\n\tif int(f) >= len(formatStr) || f < 0 {\n\t\treturn formatStr[0]\n\t}\n\n\treturn formatStr[f]\n}\n"
  },
  {
    "path": "grype/version/format_test.go",
    "content": "package version\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n)\n\nfunc TestParseFormat(t *testing.T) {\n\ttests := []struct {\n\t\tinput  string\n\t\tformat Format\n\t}{\n\t\t// SemanticFormat cases\n\t\t{\n\t\t\tinput:  \"semantic\",\n\t\t\tformat: SemanticFormat,\n\t\t},\n\t\t{\n\t\t\tinput:  \"semver\",\n\t\t\tformat: SemanticFormat,\n\t\t},\n\t\t{\n\t\t\tinput:  \"npm\",\n\t\t\tformat: SemanticFormat,\n\t\t},\n\t\t{\n\t\t\tinput:  \"nuget\",\n\t\t\tformat: SemanticFormat,\n\t\t},\n\t\t{\n\t\t\tinput:  \"composer\",\n\t\t\tformat: SemanticFormat,\n\t\t},\n\t\t{\n\t\t\tinput:  \"hex\",\n\t\t\tformat: SemanticFormat,\n\t\t},\n\t\t{\n\t\t\tinput:  \"pub\",\n\t\t\tformat: SemanticFormat,\n\t\t},\n\t\t{\n\t\t\tinput:  \"swift\",\n\t\t\tformat: SemanticFormat,\n\t\t},\n\t\t{\n\t\t\tinput:  \"conan\",\n\t\t\tformat: SemanticFormat,\n\t\t},\n\t\t{\n\t\t\tinput:  \"cocoapods\",\n\t\t\tformat: SemanticFormat,\n\t\t},\n\t\t{\n\t\t\tinput:  \"hackage\",\n\t\t\tformat: SemanticFormat,\n\t\t},\n\t\t// ApkFormat cases\n\t\t{\n\t\t\tinput:  \"apk\",\n\t\t\tformat: ApkFormat,\n\t\t},\n\t\t// BitnamiFormat cases\n\t\t{\n\t\t\tinput:  \"bitnami\",\n\t\t\tformat: BitnamiFormat,\n\t\t},\n\t\t// DebFormat cases\n\t\t{\n\t\t\tinput:  \"deb\",\n\t\t\tformat: DebFormat,\n\t\t},\n\t\t{\n\t\t\tinput:  \"dpkg\",\n\t\t\tformat: DebFormat,\n\t\t},\n\t\t// GolangFormat cases\n\t\t{\n\t\t\tinput:  \"golang\",\n\t\t\tformat: GolangFormat,\n\t\t},\n\t\t{\n\t\t\tinput:  \"go\",\n\t\t\tformat: GolangFormat,\n\t\t},\n\t\t// MavenFormat cases\n\t\t{\n\t\t\tinput:  \"maven\",\n\t\t\tformat: MavenFormat,\n\t\t},\n\t\t// RpmFormat cases\n\t\t{\n\t\t\tinput:  \"rpm\",\n\t\t\tformat: RpmFormat,\n\t\t},\n\t\t// PythonFormat cases\n\t\t{\n\t\t\tinput:  \"python\",\n\t\t\tformat: PythonFormat,\n\t\t},\n\t\t{\n\t\t\tinput:  \"pypi\",\n\t\t\tformat: PythonFormat,\n\t\t},\n\t\t{\n\t\t\tinput:  \"pep440\",\n\t\t\tformat: PythonFormat,\n\t\t},\n\t\t// KBFormat cases\n\t\t{\n\t\t\tinput:  \"kb\",\n\t\t\tformat: KBFormat,\n\t\t},\n\t\t// GemFormat cases\n\t\t{\n\t\t\tinput:  \"gem\",\n\t\t\tformat: GemFormat,\n\t\t},\n\t\t// PortageFormat cases\n\t\t{\n\t\t\tinput:  \"portage\",\n\t\t\tformat: PortageFormat,\n\t\t},\n\t\t// JVMFormat cases\n\t\t{\n\t\t\tinput:  \"jvm\",\n\t\t\tformat: JVMFormat,\n\t\t},\n\t\t{\n\t\t\tinput:  \"jre\",\n\t\t\tformat: JVMFormat,\n\t\t},\n\t\t{\n\t\t\tinput:  \"jdk\",\n\t\t\tformat: JVMFormat,\n\t\t},\n\t\t{\n\t\t\tinput:  \"openjdk\",\n\t\t\tformat: JVMFormat,\n\t\t},\n\t\t{\n\t\t\tinput:  \"jep223\",\n\t\t\tformat: JVMFormat,\n\t\t},\n\t\t// PacmanFormat cases\n\t\t{\n\t\t\tinput:  \"pacman\",\n\t\t\tformat: PacmanFormat,\n\t\t},\n\t\t// UnknownFormat case\n\t\t{\n\t\t\tinput:  \"unknown\",\n\t\t\tformat: UnknownFormat,\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tname := fmt.Sprintf(\"'%s'->format[%s]\", test.input, test.format)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tactual := ParseFormat(test.input)\n\t\t\tif actual != test.format {\n\t\t\t\tt.Errorf(\"mismatched user string -> format mapping, pkgType='%s': '%s'!='%s'\", test.input, test.format, actual)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/version/fuzzy_constraint.go",
    "content": "package version\n\nimport (\n\t\"fmt\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n\t\"unicode\"\n\n\thashiVer \"github.com/anchore/go-version\"\n)\n\n// derived from https://semver.org/, but additionally matches:\n// - partial versions (e.g. \"2.0\")\n// - optional prefix \"v\" (e.g. \"v1.0.0\")\nvar pseudoSemverPattern = regexp.MustCompile(`^v?(0|[1-9]\\d*)(\\.(0|[1-9]\\d*))?(\\.(0|[1-9]\\d*))?(?:(-|alpha|beta|rc)((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$`)\n\ntype fuzzyConstraint struct {\n\tRawPhrase          string\n\tPhraseHint         string\n\tSemanticConstraint *hashiVer.Constraints\n\tConstraints        simpleRangeExpression\n}\n\nfunc newFuzzyConstraint(phrase, hint string) (fuzzyConstraint, error) {\n\tif phrase == \"\" {\n\t\t// an empty constraint is always satisfied\n\t\treturn fuzzyConstraint{\n\t\t\tRawPhrase:  phrase,\n\t\t\tPhraseHint: hint,\n\t\t}, nil\n\t}\n\n\tconstraints, err := parseRangeExpression(phrase)\n\tif err != nil {\n\t\treturn fuzzyConstraint{}, fmt.Errorf(\"could not create fuzzy constraint: %+v\", err)\n\t}\n\tvar semverConstraint *hashiVer.Constraints\n\n\t// check all version unit phrases to see if this is a valid semver constraint\n\tvalid := true\ncheck:\n\tfor _, units := range constraints.Units {\n\t\tfor _, unit := range units {\n\t\t\tif !pseudoSemverPattern.MatchString(unit.Version) {\n\t\t\t\tvalid = false\n\t\t\t\tbreak check\n\t\t\t}\n\t\t}\n\t}\n\n\tif value, err := hashiVer.NewConstraint(phrase); err == nil && valid {\n\t\tsemverConstraint = &value\n\t}\n\n\treturn fuzzyConstraint{\n\t\tRawPhrase:          phrase,\n\t\tPhraseHint:         hint,\n\t\tConstraints:        constraints,\n\t\tSemanticConstraint: semverConstraint,\n\t}, nil\n}\n\nfunc (f fuzzyConstraint) Satisfied(verObj *Version) (bool, error) {\n\tif f.RawPhrase == \"\" && verObj != nil {\n\t\t// an empty constraint is always satisfied\n\t\treturn true, nil\n\t} else if verObj == nil {\n\t\tif f.RawPhrase != \"\" {\n\t\t\t// a non-empty constraint with no version given should always fail\n\t\t\treturn false, nil\n\t\t}\n\t\treturn true, nil\n\t}\n\n\tversion := verObj.Raw\n\n\t// rebuild temp constraint based off of ver obj\n\tif verObj.Format != UnknownFormat {\n\t\tnewConstraint, err := GetConstraint(f.RawPhrase, verObj.Format)\n\t\t// check if constraint is not fuzzyConstraint\n\t\t_, ok := newConstraint.(fuzzyConstraint)\n\t\tif err == nil && !ok {\n\t\t\tsatisfied, err := newConstraint.Satisfied(verObj)\n\t\t\tif err == nil {\n\t\t\t\treturn satisfied, nil\n\t\t\t}\n\t\t}\n\t}\n\n\t// attempt semver first, then fallback to fuzzy part matching...\n\tif f.SemanticConstraint != nil {\n\t\tif pseudoSemverPattern.MatchString(version) {\n\t\t\t// we're stricter about accepting looser semver rules here since we have no context about\n\t\t\t// the true format of the version, thus we want to reduce the change of false negatives\n\t\t\tif semver, err := newSemanticVersion(version, true); err == nil {\n\t\t\t\treturn f.SemanticConstraint.Check(semver.obj), nil\n\t\t\t}\n\t\t}\n\t}\n\t// semver didn't work, use fuzzy part matching instead...\n\treturn f.Constraints.satisfied(UnknownFormat, verObj)\n}\n\nfunc (f fuzzyConstraint) Format() Format {\n\treturn UnknownFormat\n}\n\nfunc (f fuzzyConstraint) String() string {\n\tif f.RawPhrase == \"\" {\n\t\treturn \"none (unknown)\"\n\t}\n\tif f.PhraseHint != \"\" {\n\t\treturn fmt.Sprintf(\"%s (%s)\", f.RawPhrase, f.PhraseHint)\n\t}\n\treturn fmt.Sprintf(\"%s (unknown)\", f.RawPhrase)\n}\n\nfunc (f fuzzyConstraint) Value() string {\n\treturn f.RawPhrase\n}\n\n// Note: the below code is from https://github.com/facebookincubator/nvdtools/blob/688794c4d3a41929eeca89304e198578d4595d53/cvefeed/nvd/smartvercmp.go (apache V2)\n// I'd prefer to import this functionality instead of copying it, however, these functions are not exported from the package\n\n// fuzzyVersionComparison compares stringified versions of software.\n// It tries to do the right thing for any unspecified version type,\n// assuming v1 and v2 have the same version convention.\n// It will return meaningful result for \"95SE\" vs \"98SP1\" or for \"16.3.2\" vs. \"3.7.0\",\n// but not for \"2000\" vs \"11.7\".\n// Returns -1 if v1 < v2, 1 if v1 > v2 and 0 if v1 == v2.\nfunc fuzzyVersionComparison(v1, v2 string) int {\n\tv1 = stripLeadingV(v1)\n\tv2 = stripLeadingV(v2)\n\tfor s1, s2 := v1, v2; len(s1) > 0 && len(s2) > 0; {\n\t\tnum1, cmpTo1, skip1 := parseVersionParts(s1)\n\t\tnum2, cmpTo2, skip2 := parseVersionParts(s2)\n\n\t\tns1 := s1[:cmpTo1]\n\t\tns2 := s2[:cmpTo2]\n\t\tdiff := num1 - num2\n\t\tswitch {\n\t\tcase diff > 0: // ns1 has longer numeric part\n\t\t\tns2 = leftPad(ns2, diff)\n\t\tcase diff < 0: // ns2 has longer numeric part\n\t\t\tns1 = leftPad(ns1, -diff)\n\t\t}\n\n\t\t// Check if both parts look like they have patch numbers (e.g., \"p9\" vs \"p15\")\n\t\tif hasPatchNumber(ns1) && hasPatchNumber(ns2) {\n\t\t\tif cmp := comparePatchNumbers(ns1, ns2); cmp != 0 {\n\t\t\t\treturn cmp\n\t\t\t}\n\t\t} else if cmp := strings.Compare(ns1, ns2); cmp != 0 {\n\t\t\treturn cmp\n\t\t}\n\n\t\ts1 = s1[skip1:]\n\t\ts2 = s2[skip2:]\n\t}\n\t// everything is equal so far, the longest wins\n\tif len(v1) > len(v2) {\n\t\treturn 1\n\t}\n\tif len(v2) > len(v1) {\n\t\treturn -1\n\t}\n\treturn 0\n}\n\n// parseVersionParts returns the length of consecutive run of digits in the beginning of the string,\n// the last non-separator character (which should be compared), and index at which the version part (major, minor etc.) ends,\n// i.e. the position of the dot or end of the line.\n// E.g. parseVersionParts(\"11.b4.16-New_Year_Edition\") will return (2, 3, 4)\nfunc parseVersionParts(v string) (int, int, int) {\n\tvar num int\n\tfor num = 0; num < len(v); num++ {\n\t\tif v[num] < '0' || v[num] > '9' {\n\t\t\tbreak\n\t\t}\n\t}\n\tif num == len(v) {\n\t\treturn num, num, num\n\t}\n\t// Any punctuation separates the parts.\n\tskip := strings.IndexFunc(v, func(b rune) bool {\n\t\t// !\"#$%&'()*+,-./ are dec 33 to 47, :;<=>?@ are dec 58 to 64, [\\]^_` are dec 91 to 96 and {|}~ are dec 123 to 126.\n\t\t// So, punctuation is in dec 33-126 range except 48-57, 65-90 and 97-122 gaps.\n\t\t// This inverse logic allows for early short-circuiting for most of the chars and shaves ~20ns in benchmarks.\n\t\t// linters might yell about De Morgan's law here - we ignore them in this case\n\t\t//nolint:staticcheck\n\t\treturn b >= '!' && b <= '~' &&\n\t\t\t!(b > '/' && b < ':' ||\n\t\t\t\tb > '@' && b < '[' ||\n\t\t\t\tb > '`' && b < '{')\n\t})\n\tif skip == -1 {\n\t\treturn num, len(v), len(v)\n\t}\n\treturn num, skip, skip + 1\n}\n\n// leftPad pads s with n '0's\nfunc leftPad(s string, n int) string {\n\tvar sb strings.Builder\n\tfor i := 0; i < n; i++ {\n\t\tsb.WriteByte('0')\n\t}\n\tsb.WriteString(s)\n\treturn sb.String()\n}\n\nfunc stripLeadingV(ver string) string {\n\treturn strings.TrimPrefix(ver, \"v\")\n}\n\n// hasPatchNumber returns true if the version segment looks like it has a patch number\n// e.g., \"p9\", \"p15\", \"rc1\", \"alpha2\", \"8p9\", \"8p15\"\nfunc hasPatchNumber(segment string) bool {\n\tfor i, r := range segment {\n\t\tif unicode.IsLetter(r) && i < len(segment)-1 {\n\t\t\tnext := rune(segment[i+1])\n\t\t\tif unicode.IsDigit(next) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t}\n\treturn false\n}\n\n// comparePatchNumbers compares version segments with patch numbers numerically\n// e.g., \"p9\" < \"p15\", \"rc1\" < \"rc10\", \"8p9\" < \"8p15\"\nfunc comparePatchNumbers(left, right string) int {\n\tfindLetterDigitBoundary := func(s string) int {\n\t\tfor i, r := range s {\n\t\t\tif unicode.IsLetter(r) && i < len(s)-1 && unicode.IsDigit(rune(s[i+1])) {\n\t\t\t\treturn i + 1\n\t\t\t}\n\t\t}\n\t\treturn -1\n\t}\n\n\tleftPos := findLetterDigitBoundary(left)\n\trightPos := findLetterDigitBoundary(right)\n\n\tif leftPos > 0 && rightPos > 0 {\n\t\tleftPrefix, leftNumStr := left[:leftPos], left[leftPos:]\n\t\trightPrefix, rightNumStr := right[:rightPos], right[rightPos:]\n\n\t\tif cmp := strings.Compare(leftPrefix, rightPrefix); cmp != 0 {\n\t\t\treturn cmp\n\t\t}\n\n\t\tif leftNum, err1 := strconv.Atoi(leftNumStr); err1 == nil {\n\t\t\tif rightNum, err2 := strconv.Atoi(rightNumStr); err2 == nil {\n\t\t\t\tif leftNum < rightNum {\n\t\t\t\t\treturn -1\n\t\t\t\t} else if leftNum > rightNum {\n\t\t\t\t\treturn 1\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn strings.Compare(left, right)\n}\n"
  },
  {
    "path": "grype/version/fuzzy_constraint_test.go",
    "content": "package version\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestFuzzyVersionComparison(t *testing.T) {\n\tcases := []struct {\n\t\tv1, v2 string\n\t\tret    int\n\t}{\n\t\t// Python PEP440 craziness\n\t\t{\"1.5+1\", \"1.5+1.git.abc123de\", -1},\n\t\t{\"1.0.0-post1\", \"1.0.0-post2\", -1},\n\t\t{\"1.0.0\", \"1.0.0-post1\", -1},\n\t\t{\"1.0.0-dev1\", \"1.0.0-post1\", -1},\n\t\t{\"1.0.0-dev2\", \"1.0.0-post1\", -1},\n\t\t{\"1.0.0\", \"1.0.0-dev1\", -1},\n\t\t{\"5\", \"8\", -1},\n\t\t{\"15\", \"3\", 1},\n\t\t{\"4a\", \"4c\", -1},\n\t\t{\"1.0\", \"1.0\", 0},\n\t\t{\"1.0.1\", \"1.0\", 1},\n\t\t{\"1.0.14\", \"1.0.4\", 1},\n\t\t{\"95SE\", \"98SP1\", -1},\n\t\t{\"98SE\", \"98SP1\", -1},\n\t\t{\"98SP1\", \"98SP3\", -1},\n\t\t{\"16.0.0\", \"3.2.7\", 1},\n\t\t{\"10.23\", \"10.21\", 1},\n\t\t{\"64.0\", \"3.6.24\", 1},\n\t\t{\"5-1.15\", \"5-1.16\", -1},\n\t\t{\"5-1.15.2\", \"5-1.16\", -1},\n\t\t{\"5-appl_1.16.1\", \"5-1.0.1\", -1}, // this is wrong, but seems to be impossible to account for\n\t\t{\"5-1.16\", \"5_1.0.6\", 1},\n\t\t{\"5-6\", \"5-16\", -1},\n\t\t{\"5a1\", \"5a2\", -1},\n\t\t{\"5a1\", \"6a1\", -1},\n\t\t{\"5-a1\", \"5a1\", -1}, // meh, kind of makes sense\n\t\t{\"5-a1\", \"5.a1\", 0},\n\t\t{\"1.4\", \"1.02\", 1},\n\t\t{\"5.0\", \"08.0\", -1},\n\t\t{\"10.0\", \"1.0\", 1},\n\t\t{\"10.0\", \"1.000\", 1},\n\t\t{\"10.0\", \"1.000.0.1\", 1},\n\t\t{\"1.0.4\", \"1.0.4+metadata\", -1}, // this is also somewhat wrong, however, there is a semver parser that can handle this case (which should be leveraged when possible)\n\t\t{\"1.3.2-r0\", \"1.3.3-r0\", -1},    // regression: regression for https://github.com/anchore/go-version/pull/2\n\t\t// Java JRE/JDK versioning prior to the implementing https://openjdk.org/jeps/223 for >= version 9\n\t\t{\"1.8.0_456\", \"1.8.0\", 1},\n\t\t{\"1.8.0_456\", \"1.8.0_234\", 1},\n\t\t{\"1.8.0_456\", \"1.8.0_457\", -1},\n\t\t{\"1.8.0_456-b1\", \"1.8.0_456-b2\", -1},\n\t\t{\"1.8.0_456\", \"1.8.0_456-b1\", -1},\n\t\t// Also check the semver equivalents of pre java version 9 work as expected:\n\t\t{\"8.0.456\", \"8.0\", 1},\n\t\t{\"8.0.456\", \"8.0.234\", 1},\n\t\t{\"8.0.456\", \"8.0.457\", -1},\n\t\t{\"8.0.456+1\", \"8.0.456+2\", -1},\n\t\t{\"8.0.456\", \"8.0.456+1\", -1},\n\t\t// Test case for fuzzy version comparison bug with patch numbers\n\t\t// This should pass: 4.2.8p9 < 4.2.8p15 (p9 comes before p15 numerically)\n\t\t// But currently fails due to lexicographic fallback where \"p9\" > \"p15\" (string comparison)\n\t\t{\"4.2.8p9\", \"4.2.8p15\", -1},\n\t\t// douple check openssl's unusual versioning\n\t\t// 1.0.2k is an earlier patch release than 1.0.2l\n\t\t{\"1.0.2k\", \"1.0.2l\", -1},\n\t\t// 1.1.1w is a later patch on 1.1.1\n\t\t{\"1.1.1\", \"1.1.1w\", -1},\n\t}\n\tfor _, c := range cases {\n\t\tt.Run(fmt.Sprintf(\"%q vs %q\", c.v1, c.v2), func(t *testing.T) {\n\t\t\tif ret := fuzzyVersionComparison(c.v1, c.v2); ret != c.ret {\n\t\t\t\tt.Fatalf(\"expected %d, got %d\", c.ret, ret)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestFuzzyVersion_Constraint(t *testing.T) {\n\ttests := []testCase{\n\t\t{\n\t\t\tname:       \"empty constraint\",\n\t\t\tversion:    \"2.3.1\",\n\t\t\tconstraint: \"\",\n\t\t\tsatisfied:  true,\n\t\t},\n\t\t{\n\t\t\tname:       \"version range within\",\n\t\t\tconstraint: \">1.0, <2.0\",\n\t\t\tversion:    \"1.2+beta-3\",\n\t\t\tsatisfied:  true,\n\t\t},\n\t\t{\n\t\t\tname:       \"version within compound range\",\n\t\t\tconstraint: \">1.0, <2.0 || > 3.0\",\n\t\t\tversion:    \"3.2+beta-3\",\n\t\t\tsatisfied:  true,\n\t\t},\n\t\t{\n\t\t\tname:       \"version within compound range (2)\",\n\t\t\tconstraint: \">1.0, <2.0 || > 3.0\",\n\t\t\tversion:    \"1.2+beta-3\",\n\t\t\tsatisfied:  true,\n\t\t},\n\t\t{\n\t\t\tname:       \"version not within compound range\",\n\t\t\tconstraint: \">1.0, <2.0 || > 3.0\",\n\t\t\tversion:    \"2.2+beta-3\",\n\t\t\tsatisfied:  false,\n\t\t},\n\t\t{\n\t\t\tname:       \"version range within (prerelease)\",\n\t\t\tconstraint: \">1.0, <2.0\",\n\t\t\tversion:    \"1.2.0-beta-prerelease\",\n\t\t\tsatisfied:  true,\n\t\t},\n\t\t{\n\t\t\tname:       \"version range within (prerelease)\",\n\t\t\tconstraint: \">=1.0, <2.0\",\n\t\t\tversion:    \"1.0.0-beta-prerelease\",\n\t\t\tsatisfied:  false,\n\t\t},\n\t\t{\n\t\t\tname:       \"version range outside (right)\",\n\t\t\tconstraint: \">1.0, <2.0\",\n\t\t\tversion:    \"2.1-beta-3\",\n\t\t\tsatisfied:  false,\n\t\t},\n\t\t{\n\t\t\tname:       \"version range outside (left)\",\n\t\t\tconstraint: \">1.0, <2.0\",\n\t\t\tversion:    \"0.9-beta-2\",\n\t\t\tsatisfied:  false,\n\t\t},\n\t\t{\n\t\t\tname:       \"version range within (excluding left, prerelease)\",\n\t\t\tconstraint: \">=1.0, <2.0\",\n\t\t\tversion:    \"1.0-beta-3\",\n\t\t\tsatisfied:  false,\n\t\t},\n\t\t{\n\t\t\tname:       \"version range within (including left)\",\n\t\t\tconstraint: \">=1.1, <2.0\",\n\t\t\tversion:    \"1.1\",\n\t\t\tsatisfied:  true,\n\t\t},\n\t\t{\n\t\t\tname:       \"version range within (excluding right, 1)\",\n\t\t\tconstraint: \">1.0, <=2.0\",\n\t\t\tversion:    \"2.0-beta-3\",\n\t\t\tsatisfied:  true,\n\t\t},\n\t\t{\n\t\t\tname:       \"version range within (excluding right, 2)\",\n\t\t\tconstraint: \">1.0, <2.0\",\n\t\t\tversion:    \"2.0-beta-3\",\n\t\t\tsatisfied:  true,\n\t\t},\n\t\t{\n\t\t\tname:       \"version range within (including right)\",\n\t\t\tconstraint: \">1.0, <=2.0\",\n\t\t\tversion:    \"2.0\",\n\t\t\tsatisfied:  true,\n\t\t},\n\t\t{\n\t\t\tname:       \"version range within (including right, longer version [valid semver, bad fuzzy])\",\n\t\t\tconstraint: \">1.0, <=2.0\",\n\t\t\tversion:    \"2.0.0\",\n\t\t\tsatisfied:  true,\n\t\t},\n\t\t{\n\t\t\tname:       \"version range not within range (prefix)\",\n\t\t\tconstraint: \">1.0, <2.0\",\n\t\t\tversion:    \"5-1.2+beta-3\",\n\t\t\tsatisfied:  false,\n\t\t},\n\t\t{\n\t\t\tname:       \"odd major prefix wide constraint range\",\n\t\t\tconstraint: \">4, <6\",\n\t\t\tversion:    \"5-1.2+beta-3\",\n\t\t\tsatisfied:  true,\n\t\t},\n\t\t{\n\t\t\tname:       \"odd major prefix narrow constraint\",\n\t\t\tconstraint: \">5-1.15\",\n\t\t\tversion:    \"5-1.16\",\n\t\t\tsatisfied:  true,\n\t\t},\n\t\t{\n\t\t\tname:       \"odd major prefix narrow constraint range\",\n\t\t\tconstraint: \">5-1.15, <=5-1.16\",\n\t\t\tversion:    \"5-1.16\",\n\t\t\tsatisfied:  true,\n\t\t},\n\t\t{\n\t\t\tname:       \"odd major prefix narrow constraint range (excluding)\",\n\t\t\tconstraint: \">4, <5-1.16\",\n\t\t\tversion:    \"5-1.16\",\n\t\t\tsatisfied:  false,\n\t\t},\n\t\t{\n\t\t\tname:       \"bad semver (eq)\",\n\t\t\tversion:    \"5a2\", // with the hashicorp lib, without the strict check, this is interpreted as 5.0.0-alpha.2\n\t\t\tconstraint: \"=5a2\",\n\t\t\tsatisfied:  true,\n\t\t},\n\t\t{\n\t\t\tname:       \"bad semver (gt)\",\n\t\t\tversion:    \"5a2\",\n\t\t\tconstraint: \">5a1\",\n\t\t\tsatisfied:  true,\n\t\t},\n\t\t{\n\t\t\tname:       \"bad semver (lt)\",\n\t\t\tversion:    \"5a2\",\n\t\t\tconstraint: \"<6a1\",\n\t\t\tsatisfied:  true,\n\t\t},\n\t\t{\n\t\t\tname:       \"bad semver (lte)\",\n\t\t\tversion:    \"5a2\",\n\t\t\tconstraint: \"<=5a2\",\n\t\t\tsatisfied:  true,\n\t\t},\n\t\t{\n\t\t\tname:       \"bad semver (gte)\",\n\t\t\tversion:    \"5a2\",\n\t\t\tconstraint: \">=5a2\",\n\t\t\tsatisfied:  true,\n\t\t},\n\t\t{\n\t\t\tname:       \"bad semver (lt boundary)\",\n\t\t\tversion:    \"5a2\",\n\t\t\tconstraint: \"<5a2\",\n\t\t\tsatisfied:  false,\n\t\t},\n\t\t// regression for https://github.com/anchore/go-version/pull/2\n\t\t{\n\t\t\tname:       \"indirect package match\",\n\t\t\tversion:    \"1.3.2-r0\",\n\t\t\tconstraint: \"<= 1.3.3-r0\",\n\t\t\tsatisfied:  true,\n\t\t},\n\t\t{\n\t\t\tname:       \"indirect package no match\",\n\t\t\tversion:    \"1.3.4-r0\",\n\t\t\tconstraint: \"<= 1.3.3-r0\",\n\t\t\tsatisfied:  false,\n\t\t},\n\t\t{\n\t\t\tname:       \"vulndb fuzzy constraint single quoted\",\n\t\t\tversion:    \"4.5.2\",\n\t\t\tconstraint: \"'4.5.1' || '4.5.2'\",\n\t\t\tsatisfied:  true,\n\t\t},\n\t\t{\n\t\t\tname:       \"vulndb fuzzy constraint double quoted\",\n\t\t\tversion:    \"4.5.2\",\n\t\t\tconstraint: \"\\\"4.5.1\\\" || \\\"4.5.2\\\"\",\n\t\t\tsatisfied:  true,\n\t\t},\n\t\t{\n\t\t\tname:       \"strip unbalanced v from left side <\",\n\t\t\tversion:    \"v17.12.0-ce-rc1.0.20200309214505-aa6a9891b09c+incompatible\",\n\t\t\tconstraint: \"< 1.5\",\n\t\t\tsatisfied:  false,\n\t\t},\n\t\t{\n\t\t\tname:       \"strip unbalanced v from left side >\",\n\t\t\tversion:    \"v17.12.0-ce-rc1.0.20200309214505-aa6a9891b09c+incompatible\",\n\t\t\tconstraint: \"> 1.5\",\n\t\t\tsatisfied:  true,\n\t\t},\n\t\t{\n\t\t\tname:       \"strip unbalanced v from right side <\",\n\t\t\tversion:    \"17.12.0-ce-rc1.0.20200309214505-aa6a9891b09c+incompatible\",\n\t\t\tconstraint: \"< v1.5\",\n\t\t\tsatisfied:  false,\n\t\t},\n\t\t{\n\t\t\tname:       \"strip unbalanced v from right side >\",\n\t\t\tversion:    \"17.12.0-ce-rc1.0.20200309214505-aa6a9891b09c+incompatible\",\n\t\t\tconstraint: \"> v1.5\",\n\t\t\tsatisfied:  true,\n\t\t},\n\t\t{\n\t\t\tname:       \"rc candidates with no '-' can match semver pattern\",\n\t\t\tversion:    \"1.20rc1\",\n\t\t\tconstraint: \" = 1.20.0-rc1\",\n\t\t\tsatisfied:  true,\n\t\t},\n\t\t{\n\t\t\tname:       \"candidates ahead of alpha\",\n\t\t\tversion:    \"3.11.0\",\n\t\t\tconstraint: \"> 3.11.0-alpha1\",\n\t\t\tsatisfied:  true,\n\t\t},\n\t\t{\n\t\t\tname:       \"candidates ahead of beta\",\n\t\t\tversion:    \"3.11.0\",\n\t\t\tconstraint: \"> 3.11.0-beta1\",\n\t\t\tsatisfied:  true,\n\t\t},\n\t\t{\n\t\t\tname:       \"candidates ahead of same alpha versions\",\n\t\t\tversion:    \"3.11.0-alpha5\",\n\t\t\tconstraint: \"> 3.11.0-alpha1\",\n\t\t\tsatisfied:  true,\n\t\t},\n\t\t{\n\t\t\tname:       \"candidates are placed correctly between alpha and release\",\n\t\t\tversion:    \"3.11.0-beta5\",\n\t\t\tconstraint: \"3.11.0 || = 3.11.0-alpha1\",\n\t\t\tsatisfied:  false,\n\t\t},\n\t\t{\n\t\t\tname:       \"candidates with letter suffix are alphabetically greater than their versions\",\n\t\t\tversion:    \"1.0.2a\",\n\t\t\tconstraint: \" < 1.0.2w\",\n\t\t\tsatisfied:  true,\n\t\t},\n\t\t{\n\t\t\tname:       \"candidates with multiple letter suffix are alphabetically greater than their versions\",\n\t\t\tversion:    \"1.0.2zg\",\n\t\t\tconstraint: \" < 1.0.2zh\",\n\t\t\tsatisfied:  true,\n\t\t},\n\t\t{\n\t\t\tname:       \"candidates with pre suffix are sorted numerically\",\n\t\t\tversion:    \"1.0.2pre1\",\n\t\t\tconstraint: \" < 1.0.2pre2\",\n\t\t\tsatisfied:  true,\n\t\t},\n\t\t{\n\t\t\tname:       \"candidates with letter suffix and r0 are alphabetically greater than their versions\",\n\t\t\tversion:    \"1.0.2k-r0\",\n\t\t\tconstraint: \" < 1.0.2l-r0\",\n\t\t\tsatisfied:  true,\n\t\t},\n\t\t{\n\t\t\tname:       \"openssl version with letter suffix and r0 are alphabetically greater than their versions\",\n\t\t\tversion:    \"1.0.2k-r0\", // the lib is saying the there is a prerelese starting at \"k-r0\"\n\t\t\tconstraint: \">= 1.0.2\",\n\t\t\tsatisfied:  true,\n\t\t},\n\t\t{\n\t\t\tname:       \"openssl versions with letter suffix and r0 are alphabetically greater than their versions and compared equally to other lettered versions\",\n\t\t\tversion:    \"1.0.2k-r0\",\n\t\t\tconstraint: \">= 1.0.2, < 1.0.2m\",\n\t\t\tsatisfied:  true,\n\t\t},\n\t\t{\n\t\t\tname:       \"openssl pre2 is still considered less than release\",\n\t\t\tversion:    \"1.1.1-pre2\",\n\t\t\tconstraint: \"> 1.1.1-pre1, < 1.1.1\",\n\t\t\tsatisfied:  true,\n\t\t},\n\t\t{\n\t\t\tname:       \"major version releases are less than their subsequent patch releases with letter suffixes\",\n\t\t\tversion:    \"1.1.1\",\n\t\t\tconstraint: \"> 1.1.1-a\",\n\t\t\tsatisfied:  true,\n\t\t},\n\t\t{\n\t\t\tname:       \"go pseudoversion vulnerable: version is less, want less\",\n\t\t\tversion:    \"0.0.0-20230716120725-531d2d74bc12\",\n\t\t\tconstraint: \"<0.0.0-20230922105210-14b16010c2ee\",\n\t\t\tsatisfied:  true,\n\t\t},\n\t\t{\n\t\t\tname:       \"go pseudoversion not vulnerable: same version but constraint is less\",\n\t\t\tversion:    \"0.0.0-20230922105210-14b16010c2ee\",\n\t\t\tconstraint: \"<0.0.0-20230922105210-14b16010c2ee\",\n\t\t\tsatisfied:  false,\n\t\t},\n\t\t{\n\t\t\tname:       \"go pseudoversion not vulnerable: greater version\",\n\t\t\tversion:    \"0.0.0-20230922112808-5421fefb8386\",\n\t\t\tconstraint: \"<0.0.0-20230922105210-14b16010c2ee\",\n\t\t\tsatisfied:  false,\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tconstraint, err := GetConstraint(test.constraint, UnknownFormat)\n\t\t\tassert.NoError(t, err, \"unexpected error from newFuzzyConstraint: %v\", err)\n\n\t\t\ttest.assertVersionConstraint(t, UnknownFormat, constraint)\n\t\t})\n\t}\n}\n\nfunc TestPseudoSemverPattern(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\tversion string\n\t\tvalid   bool\n\t}{\n\t\t{name: \"rc candidates are valid semver\", version: \"1.2.3-rc1\", valid: true},\n\t\t{name: \"rc candidates with no dash are valid semver\", version: \"1.2.3rc1\", valid: true},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tassert.Equal(t, test.valid, pseudoSemverPattern.MatchString(test.version))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/version/fuzzy_version.go",
    "content": "package version\n\nvar _ Comparator = (*fuzzyVersion)(nil)\n\ntype fuzzyVersion struct {\n\tsemVer *semanticVersion\n\traw    string\n}\n\n//nolint:unparam\nfunc newFuzzyVersion(raw string) (fuzzyVersion, error) {\n\treturn fuzzyVersion{\n\t\tsemVer: newFuzzySemver(raw),\n\t\traw:    raw,\n\t}, nil\n}\n\nfunc (v fuzzyVersion) Compare(other *Version) (int, error) {\n\tif other == nil {\n\t\treturn -1, ErrNoVersionProvided\n\t}\n\n\tsemver := newFuzzySemver(other.Raw)\n\tif semver != nil && v.semVer != nil && v.semVer.obj != nil && semver.obj != nil {\n\t\treturn v.semVer.obj.Compare(semver.obj), nil\n\t}\n\n\t// one or both are no semver compliant, use fuzzy comparison\n\treturn fuzzyVersionComparison(v.raw, other.Raw), nil\n}\n\nfunc newFuzzySemver(raw string) *semanticVersion {\n\t// we need to be a little more strict here than the hashicorp lib, but not as strict as the semver spec.\n\t// a good example of this is being able to reason about openssl versions like \"1.0.2k\" or \"1.0.2l\" which are\n\t// not semver compliant, but we still want to be able to compare them. But the hashicorp lib will not parse\n\t// the postfix letter as a prerelease version, which is wrong. In these cases we want a true fuzzy version\n\t// comparison.\n\tif pseudoSemverPattern.MatchString(raw) {\n\t\tcandidate, err := newSemanticVersion(raw, false)\n\t\tif err == nil {\n\t\t\treturn &candidate\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "grype/version/fuzzy_version_test.go",
    "content": "package version\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestFuzzyVersion_Compare(t *testing.T) {\n\ttests := []struct {\n\t\tname           string\n\t\tthisVersion    string\n\t\totherVersion   string\n\t\totherFormat    Format\n\t\texpectError    bool\n\t\terrorSubstring string\n\t}{\n\t\t{\n\t\t\tname:         \"fuzzy comparison with semantic version\",\n\t\t\tthisVersion:  \"1.2.3\",\n\t\t\totherVersion: \"1.2.4\",\n\t\t\totherFormat:  SemanticFormat,\n\t\t\texpectError:  false,\n\t\t},\n\t\t{\n\t\t\tname:         \"fuzzy comparison with unknown format\",\n\t\t\tthisVersion:  \"1.2.3\",\n\t\t\totherVersion: \"1.2.4\",\n\t\t\totherFormat:  UnknownFormat,\n\t\t\texpectError:  false,\n\t\t},\n\t\t{\n\t\t\tname:         \"fuzzy comparison with different format\",\n\t\t\tthisVersion:  \"1.2.3\",\n\t\t\totherVersion: \"1.2.3-r4\",\n\t\t\totherFormat:  ApkFormat,\n\t\t\texpectError:  false,\n\t\t},\n\t\t{\n\t\t\tname:         \"fuzzy comparison with non-semantic string\",\n\t\t\tthisVersion:  \"1.2.3\",\n\t\t\totherVersion: \"abc123\",\n\t\t\totherFormat:  UnknownFormat,\n\t\t\texpectError:  false,\n\t\t},\n\t\t{\n\t\t\tname:         \"fuzzy comparison with empty strings\",\n\t\t\tthisVersion:  \"1.2.3\",\n\t\t\totherVersion: \"\",\n\t\t\totherFormat:  UnknownFormat,\n\t\t\texpectError:  false,\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tthisVer := New(test.thisVersion, UnknownFormat) // explicitly use the fuzzy version format\n\n\t\t\totherVer := New(test.otherVersion, test.otherFormat)\n\n\t\t\tresult, err := thisVer.Compare(otherVer)\n\n\t\t\tif test.expectError {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\tif test.errorSubstring != \"\" {\n\t\t\t\t\tassert.True(t, strings.Contains(err.Error(), test.errorSubstring),\n\t\t\t\t\t\t\"Expected error to contain '%s', got: %v\", test.errorSubstring, err)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.Contains(t, []int{-1, 0, 1}, result, \"Expected comparison result to be -1, 0, or 1\")\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestFuzzyVersion_Compare_EdgeCases(t *testing.T) {\n\ttests := []struct {\n\t\tname           string\n\t\tsetupFunc      func(tb testing.TB) (*Version, *Version)\n\t\texpectError    require.ErrorAssertionFunc\n\t\terrorSubstring string\n\t\twantComparison int\n\t}{\n\t\t{\n\t\t\tname: \"nil version object\",\n\t\t\tsetupFunc: func(t testing.TB) (*Version, *Version) {\n\t\t\t\tthisVer := New(\"1.2.3\", UnknownFormat)\n\n\t\t\t\treturn thisVer, nil\n\t\t\t},\n\t\t\texpectError:    require.Error,\n\t\t\terrorSubstring: \"no version provided for comparison\",\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tif test.expectError == nil {\n\t\t\t\ttest.expectError = require.NoError\n\t\t\t}\n\t\t\tthisVer, otherVer := test.setupFunc(t)\n\n\t\t\tn, err := thisVer.Compare(otherVer)\n\t\t\ttest.expectError(t, err)\n\t\t\tif test.errorSubstring != \"\" {\n\t\t\t\tassert.True(t, strings.Contains(err.Error(), test.errorSubstring),\n\t\t\t\t\t\"Expected error to contain '%s', got: %v\", test.errorSubstring, err)\n\t\t\t}\n\t\t\tif err != nil {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tassert.Equal(t, test.wantComparison, n, \"Expected comparison result to be %d\", test.wantComparison)\n\t\t})\n\t}\n}\n\nfunc TestFuzzyVersion_Compare_NilScenarios(t *testing.T) {\n\ttests := []struct {\n\t\tname           string\n\t\tsetupFunc      func(tb testing.TB) (fuzzyVersion, *Version)\n\t\texpectFallback bool // expect fuzzy comparison fallback\n\t}{\n\t\t{\n\t\t\tname: \"both v.semVer and other semver are nil\",\n\t\t\tsetupFunc: func(t testing.TB) (fuzzyVersion, *Version) {\n\t\t\t\t// create fuzzyVersion with nil semVer\n\t\t\t\tfv := fuzzyVersion{\n\t\t\t\t\tsemVer: nil,\n\t\t\t\t\traw:    \"abc123\",\n\t\t\t\t}\n\n\t\t\t\totherVer := New(\"def456\", UnknownFormat)\n\n\t\t\t\treturn fv, otherVer\n\t\t\t},\n\t\t\texpectFallback: true,\n\t\t},\n\t\t{\n\t\t\tname: \"v.semVer is nil, other semver is not nil\",\n\t\t\tsetupFunc: func(t testing.TB) (fuzzyVersion, *Version) {\n\t\t\t\t// create fuzzyVersion with nil semVer\n\t\t\t\tfv := fuzzyVersion{\n\t\t\t\t\tsemVer: nil,\n\t\t\t\t\traw:    \"abc123\",\n\t\t\t\t}\n\n\t\t\t\totherVer := New(\"1.2.3\", UnknownFormat)\n\n\t\t\t\treturn fv, otherVer\n\t\t\t},\n\t\t\texpectFallback: true,\n\t\t},\n\t\t{\n\t\t\tname: \"v.semVer is not nil but v.semVer.obj is nil\",\n\t\t\tsetupFunc: func(t testing.TB) (fuzzyVersion, *Version) {\n\t\t\t\t// create fuzzyVersion with semVer that has nil obj\n\t\t\t\tfv := fuzzyVersion{\n\t\t\t\t\tsemVer: &semanticVersion{obj: nil},\n\t\t\t\t\traw:    \"abc123\",\n\t\t\t\t}\n\n\t\t\t\totherVer := New(\"1.2.3\", UnknownFormat)\n\n\t\t\t\treturn fv, otherVer\n\t\t\t},\n\t\t\texpectFallback: true,\n\t\t},\n\t\t{\n\t\t\tname: \"v.semVer is valid but other semver is nil\",\n\t\t\tsetupFunc: func(t testing.TB) (fuzzyVersion, *Version) {\n\t\t\t\t// create fuzzyVersion with valid semVer\n\t\t\t\tsemVer, err := newSemanticVersion(\"1.2.3\", false)\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\tfv := fuzzyVersion{\n\t\t\t\t\tsemVer: &semVer,\n\t\t\t\t\traw:    \"1.2.3\",\n\t\t\t\t}\n\n\t\t\t\t// create other version that will result in nil semver from newFuzzySemver\n\t\t\t\totherVer := New(\"abc123\", UnknownFormat)\n\n\t\t\t\treturn fv, otherVer\n\t\t\t},\n\t\t\texpectFallback: true,\n\t\t},\n\t\t{\n\t\t\tname: \"v.semVer is valid but other semver.obj is nil\",\n\t\t\tsetupFunc: func(t testing.TB) (fuzzyVersion, *Version) {\n\t\t\t\t// create fuzzyVersion with valid semVer\n\t\t\t\tsemVer, err := newSemanticVersion(\"1.2.3\", false)\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\tfv := fuzzyVersion{\n\t\t\t\t\tsemVer: &semVer,\n\t\t\t\t\traw:    \"1.2.3\",\n\t\t\t\t}\n\n\t\t\t\t// this should create a version that when passed to newFuzzySemver\n\t\t\t\t// results in a semanticVersion with nil obj (this might be hard to achieve\n\t\t\t\t// but we'll test the logic path)\n\t\t\t\totherVer := New(\"not-semver-compliant\", UnknownFormat)\n\n\t\t\t\treturn fv, otherVer\n\t\t\t},\n\t\t\texpectFallback: true,\n\t\t},\n\t\t{\n\t\t\tname: \"both semvers are valid - should use semver comparison\",\n\t\t\tsetupFunc: func(t testing.TB) (fuzzyVersion, *Version) {\n\t\t\t\t// create fuzzyVersion with valid semVer\n\t\t\t\tsemVer, err := newSemanticVersion(\"1.2.3\", false)\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\tfv := fuzzyVersion{\n\t\t\t\t\tsemVer: &semVer,\n\t\t\t\t\traw:    \"1.2.3\",\n\t\t\t\t}\n\n\t\t\t\totherVer := New(\"1.2.4\", UnknownFormat)\n\n\t\t\t\treturn fv, otherVer\n\t\t\t},\n\t\t\texpectFallback: false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tfv, otherVer := tt.setupFunc(t)\n\n\t\t\tresult, err := fv.Compare(otherVer)\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// verify that the result is a valid comparison result\n\t\t\tassert.Contains(t, []int{-1, 0, 1}, result, \"Expected comparison result to be -1, 0, or 1\")\n\n\t\t\t// we can't easily test which path was taken without modifying the source,\n\t\t\t// but we can at least verify the function doesn't panic and returns valid results\n\t\t\tif tt.expectFallback {\n\t\t\t\t// when falling back to fuzzy comparison, we should get a result\n\t\t\t\t// the exact value depends on the fuzzyVersionComparison implementation\n\t\t\t\tassert.NotPanics(t, func() {\n\t\t\t\t\t_, _ = fv.Compare(otherVer)\n\t\t\t\t})\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/version/gem_version.go",
    "content": "package version\n\nimport (\n\t\"fmt\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n)\n\nvar _ Comparator = (*gemVersion)(nil)\n\ntype gemVersion struct {\n\toriginal     string\n\tsegments     []any\n\tcanonical    []any\n\tisPrerelease bool\n}\n\nconst (\n\trubySegmentPattern     = `(\\d+|[a-zA-Z]+)`\n\trubyCorrectnessPattern = `^[0-9a-zA-Z.\\-]+$`\n)\n\nvar (\n\tsegmentRegexp     = regexp.MustCompile(rubySegmentPattern)\n\tcorrectnessRegexp = regexp.MustCompile(rubyCorrectnessPattern)\n)\n\nfunc newGemVersion(raw string) (gemVersion, error) {\n\toriginal := raw\n\tprocessed := cleanArchFromVersion(raw)\n\tif processed == \"\" || strings.TrimSpace(processed) == \"\" {\n\t\tprocessed = \"0\"\n\t} else {\n\t\tprocessed = strings.TrimSpace(processed)\n\t}\n\n\tif !correctnessRegexp.MatchString(processed) {\n\t\treturn gemVersion{}, fmt.Errorf(\"malformed version number string %q\", original)\n\t}\n\tprocessed = strings.ReplaceAll(processed, \"-\", \".pre.\")\n\n\tisPrerelease := strings.ContainsAny(processed, \"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ\")\n\n\tsegments, err := partitionSegments(processed)\n\tif err != nil {\n\t\treturn gemVersion{}, fmt.Errorf(\"malformed version number string %q: %w\", original, err)\n\t}\n\tif len(segments) == 0 {\n\t\tif processed == \"0\" {\n\t\t\tsegments = []any{0}\n\t\t} else {\n\t\t\treturn gemVersion{}, fmt.Errorf(\"malformed version number string %q (no valid segments after processing)\", original)\n\t\t}\n\t}\n\tcanonical := make([]any, len(segments))\n\tcopy(canonical, segments)\n\n\tcanonical = trimTrailingZeros(canonical)\n\tcanonical = trimIntermediateZeros(canonical, isPrerelease)\n\n\tif len(canonical) == 0 {\n\t\tcanonical = []any{0}\n\t}\n\n\treturn gemVersion{\n\t\toriginal:     original,\n\t\tsegments:     segments,\n\t\tcanonical:    canonical,\n\t\tisPrerelease: isPrerelease,\n\t}, nil\n}\n\nfunc (v gemVersion) Compare(other *Version) (int, error) {\n\tif other == nil {\n\t\treturn -1, ErrNoVersionProvided\n\t}\n\n\to, err := newGemVersion(other.Raw)\n\tif err != nil {\n\t\treturn 0, invalidFormatError(GemFormat, other.Raw, err)\n\t}\n\n\treturn v.compare(o)\n}\n\nfunc (v gemVersion) compare(other gemVersion) (int, error) {\n\tresult, commonSegmentsAreEqual, err := compareSegments(v.canonical, other.canonical)\n\tif err != nil {\n\t\treturn -1, err\n\t}\n\n\tif commonSegmentsAreEqual {\n\t\treturn compareLengths(v.canonical, other.canonical, result), nil\n\t}\n\n\treturn result, nil\n}\n\nfunc (v *gemVersion) String() string {\n\treturn v.original\n}\n\nfunc partitionSegments(versionString string) ([]any, error) {\n\tif versionString == \"\" {\n\t\treturn []any{}, fmt.Errorf(\"cannot partition empty version string\")\n\t}\n\tif strings.Contains(versionString, \"..\") {\n\t\treturn nil, fmt.Errorf(\"invalid version string (double dot): %q\", versionString)\n\t}\n\tif (strings.HasPrefix(versionString, \".\") && versionString != \".\") ||\n\t\t(strings.HasSuffix(versionString, \".\") && versionString != \".\") {\n\t\tif len(versionString) > 1 {\n\t\t\treturn nil, fmt.Errorf(\"invalid version string (leading/trailing dot): %q\", versionString)\n\t\t}\n\t}\n\n\tparts := segmentRegexp.FindAllString(versionString, -1)\n\tif len(parts) == 0 {\n\t\tif versionString == \"0\" {\n\t\t\treturn []any{0}, nil\n\t\t}\n\t\treturn nil, fmt.Errorf(\"no valid segments found in %q\", versionString)\n\t}\n\n\tsegments := make([]any, 0, len(parts))\n\tfor _, s := range parts {\n\t\tif n, err := strconv.Atoi(s); err == nil {\n\t\t\tsegments = append(segments, n)\n\t\t} else {\n\t\t\tsegments = append(segments, s)\n\t\t}\n\t}\n\treturn segments, nil\n}\n\nfunc trimTrailingZeros(segments []any) []any {\n\tif len(segments) <= 1 {\n\t\tif len(segments) == 1 {\n\t\t\tif num, ok := segments[0].(int); ok && num == 0 {\n\t\t\t\treturn []any{0}\n\t\t\t}\n\t\t}\n\t\treturn segments\n\t}\n\n\tlastSignificantIdx := -1\n\tfor i := len(segments) - 1; i >= 0; i-- {\n\t\tnum, ok := segments[i].(int)\n\t\tif !ok || num != 0 {\n\t\t\tlastSignificantIdx = i\n\t\t\tbreak\n\t\t}\n\t\t// It's a numeric zero, continue looking\n\t}\n\n\tif lastSignificantIdx == -1 {\n\t\treturn []any{0}\n\t}\n\treturn segments[:lastSignificantIdx+1]\n}\n\nfunc trimIntermediateZeros(segments []any, isPrerelease bool) []any {\n\tif !isPrerelease || len(segments) == 0 {\n\t\treturn segments\n\t}\n\n\tfirstLetterIdx := -1\n\tfor i, seg := range segments {\n\t\tif _, ok := seg.(string); ok {\n\t\t\tfirstLetterIdx = i\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif firstLetterIdx == -1 {\n\t\treturn segments\n\t}\n\n\tsegmentsBeforeLetter := []any{}\n\tif firstLetterIdx > 0 {\n\t\tsegmentsBeforeLetter = segments[:firstLetterIdx]\n\t}\n\n\ttrimmedPrefix := []any{}\n\tif len(segmentsBeforeLetter) > 0 {\n\t\tlastNonZeroInPrefixIdx := -1\n\t\tfor i := len(segmentsBeforeLetter) - 1; i >= 0; i-- {\n\t\t\tnum, ok := segmentsBeforeLetter[i].(int)\n\t\t\tif !ok || num != 0 {\n\t\t\t\tlastNonZeroInPrefixIdx = i\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif lastNonZeroInPrefixIdx != -1 {\n\t\t\ttrimmedPrefix = segmentsBeforeLetter[:lastNonZeroInPrefixIdx+1]\n\t\t}\n\t}\n\n\treconstructed := make([]any, 0, len(trimmedPrefix)+len(segments)-firstLetterIdx)\n\treconstructed = append(reconstructed, trimmedPrefix...)\n\treconstructed = append(reconstructed, segments[firstLetterIdx:]...)\n\n\treturn reconstructed\n}\n\nfunc compareSegments(left, right []any) (result int, allEqual bool, err error) {\n\tlimit := len(left)\n\tif len(right) < limit {\n\t\tlimit = len(right)\n\t}\n\n\tfor i := 0; i < limit; i++ {\n\t\tl := left[i]\n\t\tr := right[i]\n\n\t\tlNum, lIsNum := l.(int)\n\t\tlStr, lIsStr := l.(string)\n\t\trNum, rIsNum := r.(int)\n\t\trStr, rIsStr := r.(string)\n\n\t\tif lIsNum && rIsNum {\n\t\t\tif lNum != rNum {\n\t\t\t\tif lNum < rNum {\n\t\t\t\t\treturn -1, false, nil\n\t\t\t\t}\n\t\t\t\treturn 1, false, nil\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tif lIsStr && rIsStr {\n\t\t\tif cmp := strings.Compare(lStr, rStr); cmp != 0 {\n\t\t\t\treturn cmp, false, nil\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tif lIsNum && rIsStr {\n\t\t\treturn 1, false, nil\n\t\t}\n\t\tif lIsStr && rIsNum {\n\t\t\treturn -1, false, nil\n\t\t}\n\n\t\treturn 0, false, fmt.Errorf(\"internal comparison error: unexpected types %T vs %T\", l, r)\n\t}\n\treturn 0, true, nil\n}\n\nfunc compareLengths(left, right []any, commonResult int) int {\n\tif commonResult != 0 {\n\t\treturn commonResult\n\t}\n\n\tlLen := len(left)\n\trLen := len(right)\n\n\tif lLen == rLen {\n\t\treturn 0\n\t}\n\n\tif lLen > rLen {\n\t\tfor i := rLen; i < lLen; i++ {\n\t\t\tseg := left[i]\n\t\t\tif _, isStr := seg.(string); isStr {\n\t\t\t\treturn -1\n\t\t\t}\n\t\t\tif num, isNum := seg.(int); isNum && num != 0 {\n\t\t\t\treturn 1\n\t\t\t}\n\t\t}\n\t\treturn 0\n\t}\n\n\tfor i := lLen; i < rLen; i++ {\n\t\tseg := right[i]\n\t\tif _, isStr := seg.(string); isStr {\n\t\t\treturn 1\n\t\t}\n\t\tif num, isNum := seg.(int); isNum && num != 0 {\n\t\t\treturn -1\n\t\t}\n\t}\n\treturn 0\n}\n\nfunc cleanArchFromVersion(raw string) string {\n\tplatforms := []string{\"x86\", \"universal\", \"arm\", \"java\", \"dalvik\", \"x64\", \"powerpc\", \"sparc\", \"mswin\"}\n\tdash := \"-\"\n\tfor _, p := range platforms {\n\t\tvals := strings.SplitN(raw, dash+p, 2)\n\t\tif len(vals) == 2 {\n\t\t\treturn vals[0]\n\t\t}\n\t}\n\n\treturn raw\n}\n"
  },
  {
    "path": "grype/version/gem_version_test.go",
    "content": "package version\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestGemVersion_Constraint(t *testing.T) {\n\ttests := []testCase{\n\t\t// empty values\n\t\t{version: \"2.3.1\", constraint: \"\", satisfied: true},\n\t\t// typical cases\n\t\t{version: \"0.9.9-r0\", constraint: \"< 0.9.12-r1\", satisfied: true}, // regression case\n\t\t{version: \"1.5.0-arm-windows\", constraint: \"> 0.1.0, < 0.5.0 || > 1.0.0, < 2.0.0\", satisfied: true},\n\t\t{version: \"0.2.0-arm-windows\", constraint: \"> 0.1.0, < 0.5.0 || > 1.0.0, < 2.0.0\", satisfied: true},\n\t\t{version: \"0.0.1-armv5-window\", constraint: \"> 0.1.0, < 0.5.0 || > 1.0.0, < 2.0.0\", satisfied: false},\n\t\t{version: \"0.0.1-armv7-linux\", constraint: \"> 0.1.0, < 0.5.0 || > 1.0.0, < 2.0.0\", satisfied: false},\n\t\t{version: \"0.6.0-universal-darwin-9\", constraint: \"> 0.1.0, < 0.5.0 || > 1.0.0, < 2.0.0\", satisfied: false},\n\t\t{version: \"0.6.0-universal-darwin-10\", constraint: \"> 0.1.0, < 0.5.0 || > 1.0.0, < 2.0.0\", satisfied: false},\n\t\t{version: \"0.6.0-x86_64-darwin-10\", constraint: \"> 0.1.0, < 0.5.0 || > 1.0.0, < 2.0.0\", satisfied: false},\n\t\t{version: \"2.5.0\", constraint: \"> 0.1.0, < 0.5.0 || > 1.0.0, < 2.0.0\", satisfied: false},\n\t\t{version: \"1.2.0\", constraint: \">1.0, <2.0\", satisfied: true},\n\t\t{version: \"1.2.0-x86\", constraint: \">1.0, <2.0\", satisfied: true},\n\t\t{version: \"1.2.0-x86-linux\", constraint: \">1.0, <2.0\", satisfied: true},\n\t\t{version: \"1.2.0-x86-linux\", constraint: \"= 1.2.0\", satisfied: true},\n\t\t{version: \"1.2.0-x86_64-linux\", constraint: \"= 1.2.0\", satisfied: true},\n\t\t{version: \"1.2.0-x86_64-linux\", constraint: \"< 1.2.1\", satisfied: true},\n\t\t// https://semver.org/#spec-item-11\n\t\t{version: \"1.2.0-alpha-x86-linux\", constraint: \"<1.2.0\", satisfied: true},\n\t\t{version: \"1.2.0-alpha-1-x86-linux\", constraint: \"<1.2.0\", satisfied: true},\n\t\t// gem versions seem to respect the order: {sem-version}+{meta}-{arch}-{os}\n\t\t// but let's check the extraction works even when the order of {meta}-{arch} varies.\n\t\t{version: \"1.2.0-alpha-1-x86-linux-meta\", constraint: \"<1.2.0\", satisfied: true},\n\t\t{version: \"1.2.0-alpha-1-meta-x86-linux\", constraint: \"<1.2.0\", satisfied: true},\n\t\t{version: \"1.2.0-alpha-1-x86-linux-meta\", constraint: \">1.1.0\", satisfied: true},\n\t\t{version: \"1.2.0-alpha-1-arm-linux-meta\", constraint: \">1.1.0\", satisfied: true},\n\t\t{version: \"1.0.0-alpha-a.b-c-somethinglong-build.1-aef.1-its-okay\", constraint: \"<1.0.0\", satisfied: true},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.tName(), func(t *testing.T) {\n\t\t\tconstraint, err := GetConstraint(test.constraint, GemFormat)\n\t\t\tassert.NoError(t, err, \"unexpected error from newSemanticConstraint: %v\", err)\n\n\t\t\ttest.assertVersionConstraint(t, GemFormat, constraint)\n\t\t})\n\t}\n\n}\n\nfunc Test_cleanPlatformMakesEqualVersions(t *testing.T) {\n\ttests := []struct {\n\t\tinput   string\n\t\ttrimmed string\n\t\twant    *gemVersion\n\t}{\n\t\t{input: \"1.13.1\", trimmed: \"1.13.1\"},\n\t\t{input: \"1.13.1-arm-linux\", trimmed: \"1.13.1\"},\n\t\t{input: \"1.13.1-armv6-linux\", trimmed: \"1.13.1\"},\n\t\t{input: \"1.13.1-armv7-linux\", trimmed: \"1.13.1\"},\n\t\t{input: \"1.13.1-java\", trimmed: \"1.13.1\"},\n\t\t{input: \"1.13.1-dalvik\", trimmed: \"1.13.1\"},\n\t\t{input: \"1.13.1-mswin32\", trimmed: \"1.13.1\"},\n\t\t{input: \"1.13.1-x64-mswin64\", trimmed: \"1.13.1\"},\n\t\t{input: \"1.13.1-sparc-unix\", trimmed: \"1.13.1\"},\n\t\t{input: \"1.13.1-powerpc-darwin\", trimmed: \"1.13.1\"},\n\t\t{input: \"1.13.1-x86-linux\", trimmed: \"1.13.1\"},\n\t\t{input: \"1.13.1-x86_64-linux\", trimmed: \"1.13.1\"},\n\t\t{input: \"1.13.1-x86-freebsd\", trimmed: \"1.13.1\"},\n\t\t{input: \"1.13.1-x86-mswin32-80\", trimmed: \"1.13.1\"},\n\t\t{input: \"1.13.1-universal-darwin-8\", trimmed: \"1.13.1\"},\n\t\t// ruby versions get the canonical segment \"pre\" if there are any segments that are all\n\t\t// alphabetic characters.\n\t\t{input: \"1.13.1-beta-universal-darwin-8\", trimmed: \"1.13.1.pre.beta\"},\n\t\t{input: \"1.13.1-alpha-1-meta-arm-linux\", trimmed: \"1.13.1-alpha-1-meta\"},\n\t\t{input: \"1.13.1-alpha-1-build.12-arm-linux\", trimmed: \"1.13.1-alpha-1-build.12\"},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.input, func(t *testing.T) {\n\t\t\toriginal := New(tt.input, GemFormat)\n\t\t\ttrimmed := New(tt.trimmed, GemFormat)\n\t\t\tcomp, err := original.Compare(trimmed)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, 0, comp)\n\t\t\tcomp, err = trimmed.Compare(original)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, 0, comp)\n\t\t})\n\t}\n}\n\nfunc TestNewGemVersion_ValidInputs(t *testing.T) {\n\ttests := []struct {\n\t\tinput              string\n\t\texpectedOriginal   string // What v.original should be\n\t\texpectedSegments   []any  // What v.segments should be (after .pre. processing)\n\t\texpectedPrerelease bool\n\t}{\n\t\t{\"1.0\", \"1.0\", []any{1, 0}, false},\n\t\t{\"1.0 \", \"1.0 \", []any{1, 0}, false}, // original preserves space\n\t\t{\" 1.0 \", \" 1.0 \", []any{1, 0}, false},\n\t\t{\"1.2.3\", \"1.2.3\", []any{1, 2, 3}, false},\n\t\t{\"1.2.3.a\", \"1.2.3.a\", []any{1, 2, 3, \"a\"}, true},\n\t\t{\"1.2.3-b4\", \"1.2.3-b4\", []any{1, 2, 3, \"pre\", \"b\", 4}, true},\n\t\t{\"1\", \"1\", []any{1}, false},\n\t\t{\"0\", \"0\", []any{0}, false},\n\t\t{\"\", \"\", []any{0}, false},     // Empty string becomes \"0\" effectively, original is \"\"\n\t\t{\"  \", \"  \", []any{0}, false}, // Whitespace string becomes \"0\" effectively\n\t\t{\"1.0-alpha\", \"1.0-alpha\", []any{1, 0, \"pre\", \"alpha\"}, true},\n\t\t{\"1-1\", \"1-1\", []any{1, \"pre\", 1}, true},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(fmt.Sprintf(\"Input_%s\", tt.input), func(t *testing.T) {\n\t\t\tv, err := newGemVersion(tt.input)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, tt.expectedOriginal, v.original, \"Original string mismatch\")\n\t\t\tassert.Equal(t, tt.expectedSegments, v.segments, \"Initial segments mismatch\")\n\t\t\tassert.Equal(t, tt.expectedPrerelease, v.isPrerelease, \"Prerelease flag mismatch\")\n\t\t})\n\t}\n}\n\nfunc TestNewGemVersion_InvalidInputs(t *testing.T) {\n\tinvalidVersions := []struct {\n\t\tname           string\n\t\tinput          string\n\t\terrorSubstring string\n\t}{\n\t\t{\"newline\", \"1.0\\n2.0\", \"malformed version number string\"},\n\t\t{\"double_dot\", \"1..2\", \"malformed version number string\"},\n\t\t{\"space_separated\", \"1.2 3.4\", \"malformed version number string\"},\n\t\t{\"trailing_dot_long\", \"1.2.\", \"leading/trailing dot\"},\n\t\t{\"leading_dot_long\", \".1.2\", \"leading/trailing dot\"},\n\t\t{\"just_dot\", \".\", \"no valid segments\"},\n\t\t{\"double_hyphen\", \"--\", \"malformed version number string\"},\n\t\t{\"hyphen_dot\", \"1.-2\", \"malformed version number string\"},\n\t\t{\"dot_hyphen\", \"1.-pre\", \"malformed version number string\"},\n\t\t{\"underscore\", \"1_2\", \"malformed version number string\"},\n\t\t{\"empty_segments\", \"...\", \"malformed version number string\"},\n\t\t{\"invalid_segment_char\", \"1.2.a@b\", \"malformed version number string\"},\n\t}\n\n\tfor _, tt := range invalidVersions {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\t_, err := newGemVersion(tt.input)\n\t\t\trequire.Error(t, err)\n\t\t\tif tt.errorSubstring != \"\" {\n\t\t\t\tassert.Contains(t, err.Error(), tt.errorSubstring, \"Error message mismatch for input: %s\", tt.input)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestGemVersion_Compare(t *testing.T) {\n\ttests := []struct {\n\t\tv1   string\n\t\tv2   string\n\t\twant int // expected result of v1.Compare(v2)\n\t}{\n\t\t// Basic comparisons (from Ruby's test_spaceship)\n\t\t{\"1.0\", \"1.0.0\", 0},\n\t\t{\"1.0\", \"1.0.a\", 1},\n\t\t{\"1.8.2\", \"0.0.0\", 1},\n\t\t{\"1.8.2\", \"1.8.2.a\", 1},\n\t\t{\"1.8.2.b\", \"1.8.2.a\", 1},\n\t\t{\"1.8.2.a\", \"1.8.2\", -1},\n\t\t{\"1.8.2.a10\", \"1.8.2.a9\", 1},\n\t\t{\"\", \"0\", 0}, // \"\" is treated as \"0\"\n\n\t\t// Canonicalization leading to equality\n\t\t{\"0.beta.1\", \"0.0.beta.1\", 0},  // Ruby: 0.beta.1 <=> 0.0.beta.1 is 0. Canonical for both is [\"beta\", -1]\n\t\t{\"0.0.beta\", \"0.0.beta.1\", -1}, // Ruby: 0.0.beta <=> 0.0.beta.1 is -1. Canonical [\"beta\"] vs [\"beta\", -1]\n\n\t\t// String segments comparison\n\t\t{\"5.a\", \"5.0.0.rc2\", -1}, // \"a\" < \"rc\"\n\t\t{\"5.x\", \"5.0.0.rc2\", 1},  // \"x\" > \"rc\"\n\n\t\t// Direct string comparison from Ruby test\n\t\t{\"1.9.3\", \"1.9.3\", 0},\n\t\t{\"1.9.3\", \"1.9.2.99\", 1},\n\t\t{\"1.9.3\", \"1.9.3.1\", -1},\n\n\t\t// Additional common cases\n\t\t{\"1.0\", \"1.1\", -1},\n\t\t{\"1.1\", \"1.0\", 1},\n\t\t{\"1\", \"1.0\", 0},\n\t\t{\"1.0.1\", \"1.0.0\", 1},\n\t\t{\"1.0.0\", \"1.0.1\", -1},\n\n\t\t// Prerelease vs Prerelease (length diff)\n\t\t{\"1.0.alpha.1\", \"1.0.alpha\", 1},\n\t\t{\"1.0.alpha\", \"1.0.alpha.1\", -1},\n\n\t\t// Hyphen handling (SemVer-like via .pre.)\n\t\t{\"1.0.0-alpha\", \"1.0.0-alpha.1\", -1},\n\t\t{\"1.0.0-alpha.1\", \"1.0.0-beta.2\", -1},\n\t\t{\"1.0.0-beta.2\", \"1.0.0-beta.11\", -1},\n\t\t{\"1.0.0-beta.11\", \"1.0.0-rc.1\", -1}, // beta < rc\n\t\t{\"1.0.0-rc1\", \"1.0.0\", -1},\n\t\t{\"1.0.0-1\", \"1\", -1}, // 1.0.0.pre.1 vs 1\n\t\t{\"1-1\", \"1\", -1},     // 1.pre.1 vs 1\n\n\t\t// From Ruby's test_semver (some overlap, ensure coverage)\n\t\t{\"1.0.0-alpha\", \"1.0.0-alpha.1\", -1},\n\t\t{\"1.0.0-alpha.1\", \"1.0.0-beta.2\", -1}, // alpha < beta\n\t\t{\"1.0.0-beta.2\", \"1.0.0-beta.11\", -1}, // 2 < 11\n\t\t{\"1.0.0-beta.11\", \"1.0.0-rc.1\", -1},   // beta < rc\n\t\t{\"1.0.0-rc1\", \"1.0.0\", -1},            // 1.0.0.pre.rc.1 < 1.0.0 (release)\n\t\t{\"1.0.0-1\", \"1\", -1},                  // 1.0.0.pre.1 < 1 (release)\n\n\t\t// Edge cases with canonicalization\n\t\t{\"1.0\", \"1\", 0},\n\t\t{\"1.0.0\", \"1\", 0},\n\t\t{\"1.a\", \"1.0.0.a\", 0}, // Canonical [1,\"a\"] for both\n\t\t{\"1.a.0\", \"1.a\", 0},   // Canonical [1,\"a\"] for both\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(fmt.Sprintf(\"%s_vs_%s\", tt.v1, tt.v2), func(t *testing.T) {\n\t\t\tver1 := New(tt.v1, GemFormat)\n\t\t\tver2 := New(tt.v2, GemFormat)\n\n\t\t\t// Test v1 vs v2\n\t\t\tgot1, err1 := ver1.Compare(ver2)\n\t\t\trequire.NoError(t, err1, \"v1.Compare(v2) failed for %s vs %s\", tt.v1, tt.v2)\n\t\t\tassert.Equal(t, tt.want, got1, \"Compare(%q, %q) == %d, want %d\", tt.v1, tt.v2, got1, tt.want)\n\n\t\t\t// Test symmetry: v2 vs v1\n\t\t\texpectedSymmetric := 0\n\t\t\tif tt.want != 0 {\n\t\t\t\texpectedSymmetric = -tt.want\n\t\t\t}\n\t\t\tgot2, err2 := ver2.Compare(ver1)\n\t\t\trequire.NoError(t, err2, \"v2.Compare(v1) failed for %s vs %s\", tt.v2, tt.v1)\n\t\t\tassert.Equal(t, expectedSymmetric, got2, \"Compare(%q, %q) == %d, want %d (symmetric)\", tt.v2, tt.v1, got2, expectedSymmetric)\n\n\t\t\t// Test reflexivity: v1 vs v1\n\t\t\tgotReflexive1, errReflexive1 := ver1.Compare(ver1)\n\t\t\trequire.NoError(t, errReflexive1, \"v1.Compare(v1) failed for %s\", tt.v1)\n\t\t\tassert.Equal(t, 0, gotReflexive1, \"Compare(%q, %q) == %d, want 0 (reflexive)\", tt.v1, tt.v1, gotReflexive1)\n\t\t})\n\t}\n}\n\nfunc TestGemVersion_Compare_Errors(t *testing.T) {\n\tvGem1_0, err := newGemVersion(\"1.0\")\n\trequire.NoError(t, err)\n\n\tt.Run(\"CompareWithNil\", func(t *testing.T) {\n\t\t_, err := vGem1_0.Compare(nil)\n\t\tassert.ErrorIs(t, err, ErrNoVersionProvided)\n\t})\n\n\tt.Run(\"CompareWithDifferentFormat\", func(t *testing.T) {\n\t\t// Assuming SemanticFormat is a distinct, incompatible format\n\t\t// and that the Format type has a String() method for user-friendly error messages.\n\t\tvOther := &Version{Raw: \"1.0.0\", Format: SemanticFormat}\n\t\t_, err := vGem1_0.Compare(vOther)\n\t\trequire.NoError(t, err)\n\t})\n\n\tt.Run(\"CompareWithUnknownFormat_ParsableAsGem\", func(t *testing.T) {\n\t\tvOther := &Version{Raw: \"1.1\", Format: UnknownFormat} // Parsable as Gem\n\t\tres, err := vGem1_0.Compare(vOther)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, -1, res) // 1.0 < 1.1\n\t})\n\n\tt.Run(\"CompareWithUnknownFormat_UnparsableAsGem\", func(t *testing.T) {\n\t\tvOther := &Version{Raw: \"invalid..version\", Format: UnknownFormat}\n\t\t_, err := vGem1_0.Compare(vOther)\n\t\trequire.Error(t, err)\n\t\trequire.ErrorContains(t, err, \"malformed version number string\")\n\t})\n}\n\nfunc TestGemVersion_canonical(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\tversion string\n\t\twant    []any\n\t}{\n\t\t// obtained from a simple ruby program like this:\n\t\t/*\n\t\t\trequire 'rubygems/version'\n\t\t\tv = Gem::Version.new(input)\n\t\t\tv.canonical_segments\n\t\t*/\n\t\t{\"simple ints\", \"1.2.3\", []any{1, 2, 3}},\n\t\t{\"leading zeros preserved\", \"0.1.2\", []any{0, 1, 2}},\n\t\t{\"drop intermediate zeros in pre-release\", \"5.0.0.a1\", []any{5, \"a\", 1}},\n\t\t{\"preserve intermedia zeros in regular release\", \"1.0.0.1\", []any{1, 0, 0, 1}},\n\t\t{\"drop trailing zeros\", \"1.0.0\", []any{1}},\n\t\t{\"alpha version\", \"1.6.1.a\", []any{1, 6, 1, \"a\"}},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tv, err := newGemVersion(tt.version)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tif d := cmp.Diff(v.canonical, tt.want); d != \"\" {\n\t\t\t\tt.Errorf(\"canonical mismatch (-want +got):\\n%s\", d)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/version/generic_constraint.go",
    "content": "package version\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n)\n\nvar _ Constraint = (*genericConstraint)(nil)\n\ntype genericConstraint struct {\n\tRaw        string\n\tExpression simpleRangeExpression\n\tFmt        Format\n}\n\nfunc newGenericConstraint(format Format, raw string) (genericConstraint, error) {\n\tconstraints, err := parseRangeExpression(raw)\n\tif err != nil {\n\t\treturn genericConstraint{}, invalidFormatError(format, raw, err)\n\t}\n\treturn genericConstraint{\n\t\tExpression: constraints,\n\t\tRaw:        raw,\n\t\tFmt:        format,\n\t}, nil\n}\n\nfunc (g genericConstraint) String() string {\n\tvalue := g.Value()\n\tif g.Raw == \"\" {\n\t\tvalue = \"none\"\n\t}\n\treturn fmt.Sprintf(\"%s (%s)\", value, strings.ToLower(g.Fmt.String()))\n}\n\nfunc (g genericConstraint) Value() string {\n\treturn g.Raw\n}\n\nfunc (g genericConstraint) Format() Format {\n\treturn g.Fmt\n}\n\nfunc (g genericConstraint) Satisfied(version *Version) (bool, error) {\n\tif g.Raw == \"\" && version != nil {\n\t\t// empty constraints are always satisfied\n\t\treturn true, nil\n\t}\n\tif version == nil {\n\t\tif g.Raw != \"\" {\n\t\t\t// a non-empty constraint with no version given should always fail\n\t\t\treturn false, nil\n\t\t}\n\t\treturn true, nil\n\t}\n\t// we want to prevent against two known formats that are different from being compared.\n\t// if the passed in version is unknown, we allow the comparison to proceed\n\tif version.Format != g.Fmt && version.Format != UnknownFormat {\n\t\treturn false, newUnsupportedFormatError(g.Fmt, version)\n\t}\n\treturn g.Expression.satisfied(g.Fmt, version)\n}\n"
  },
  {
    "path": "grype/version/generic_constraint_test.go",
    "content": "package version\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestGenericConstraint_String(t *testing.T) {\n\ttests := []struct {\n\t\tname       string\n\t\tconstraint string\n\t\tformat     Format\n\t\texpected   string\n\t}{\n\t\t{\n\t\t\tname:       \"empty constraint\",\n\t\t\tconstraint: \"\",\n\t\t\tformat:     SemanticFormat,\n\t\t\texpected:   \"none (semantic)\",\n\t\t},\n\t\t{\n\t\t\tname:       \"simple constraint\",\n\t\t\tconstraint: \"> 1.0.0\",\n\t\t\tformat:     SemanticFormat,\n\t\t\texpected:   \"> 1.0.0 (semantic)\",\n\t\t},\n\t\t{\n\t\t\tname:       \"complex constraint\",\n\t\t\tconstraint: \">= 1.0.0, < 2.0.0\",\n\t\t\tformat:     MavenFormat,\n\t\t\texpected:   \">= 1.0.0, < 2.0.0 (maven)\",\n\t\t},\n\t\t{\n\t\t\tname:       \"jvm format name\",\n\t\t\tconstraint: \"< 11\",\n\t\t\tformat:     JVMFormat,\n\t\t\texpected:   \"< 11 (jvm)\",\n\t\t},\n\t\t{\n\t\t\tname:       \"go format name\",\n\t\t\tconstraint: \"> v1.2.3\",\n\t\t\tformat:     GolangFormat,\n\t\t\texpected:   \"> v1.2.3 (go)\",\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tconstraint, err := newGenericConstraint(test.format, test.constraint)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tresult := constraint.String()\n\t\t\tassert.Equal(t, test.expected, result)\n\t\t})\n\t}\n}\n\nfunc TestGenericConstraint_Satisfied_EmptyConstraint(t *testing.T) {\n\tconstraint, err := newGenericConstraint(SemanticFormat, \"\")\n\trequire.NoError(t, err)\n\n\ttests := []struct {\n\t\tname    string\n\t\tversion *Version\n\t}{\n\t\t{\n\t\t\tname:    \"with valid version\",\n\t\t\tversion: New(\"1.2.3\", SemanticFormat),\n\t\t},\n\t\t{\n\t\t\tname:    \"with nil version\",\n\t\t\tversion: nil,\n\t\t},\n\t\t{\n\t\t\tname:    \"with different format version\",\n\t\t\tversion: New(\"1.2.3-r1\", ApkFormat),\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tsatisfied, err := constraint.Satisfied(test.version)\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.True(t, satisfied, \"empty constraint should always be satisfied\")\n\t\t})\n\t}\n}\n\nfunc TestGenericConstraint_Satisfied_WithConstraint(t *testing.T) {\n\ttests := []struct {\n\t\tname        string\n\t\tconstraint  string\n\t\tversion     string\n\t\tsatisfied   bool\n\t\tshouldError bool\n\t}{\n\t\t{\n\t\t\tname:       \"simple greater than - satisfied\",\n\t\t\tconstraint: \"> 1.0.0\",\n\t\t\tversion:    \"1.2.3\",\n\t\t\tsatisfied:  true,\n\t\t},\n\t\t{\n\t\t\tname:       \"simple greater than - not satisfied\",\n\t\t\tconstraint: \"> 2.0.0\",\n\t\t\tversion:    \"1.2.3\",\n\t\t\tsatisfied:  false,\n\t\t},\n\t\t{\n\t\t\tname:       \"complex constraint - satisfied\",\n\t\t\tconstraint: \">= 1.0.0, < 2.0.0\",\n\t\t\tversion:    \"1.5.0\",\n\t\t\tsatisfied:  true,\n\t\t},\n\t\t{\n\t\t\tname:       \"complex constraint - not satisfied\",\n\t\t\tconstraint: \">= 1.0.0, < 2.0.0\",\n\t\t\tversion:    \"2.5.0\",\n\t\t\tsatisfied:  false,\n\t\t},\n\t\t{\n\t\t\tname:       \"equality constraint - satisfied\",\n\t\t\tconstraint: \"= 1.2.3\",\n\t\t\tversion:    \"1.2.3\",\n\t\t\tsatisfied:  true,\n\t\t},\n\t\t{\n\t\t\tname:       \"equality constraint - not satisfied\",\n\t\t\tconstraint: \"= 1.2.3\",\n\t\t\tversion:    \"1.2.4\",\n\t\t\tsatisfied:  false,\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tconstraint, err := newGenericConstraint(SemanticFormat, test.constraint)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tversion := New(test.version, SemanticFormat)\n\n\t\t\tsatisfied, err := constraint.Satisfied(version)\n\t\t\tif test.shouldError {\n\t\t\t\trequire.Error(t, err)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.Equal(t, test.satisfied, satisfied)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestGenericConstraint_Invalid(t *testing.T) {\n\ttests := []struct {\n\t\tname       string\n\t\tconstraint string\n\t\tgen        func(unit rangeUnit) (Comparator, error)\n\t}{\n\t\t{\n\t\t\tname:       \"invalid operator\",\n\t\t\tconstraint: \"~~ 1.0.0\",\n\t\t},\n\t\t{\n\t\t\tname:       \"malformed constraint\",\n\t\t\tconstraint: \"> 1.0.0 < 2.0.0\", // missing comma\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\t_, err := newGenericConstraint(SemanticFormat, test.constraint)\n\t\t\trequire.Error(t, err)\n\t\t})\n\t}\n}\n\nfunc TestGenericConstraint_Satisfied_UnknownFormatComparison(t *testing.T) {\n\ttests := []struct {\n\t\tname           string\n\t\tconstraintFmt  Format\n\t\tconstraint     string\n\t\tversionStr     string\n\t\tversionFmt     Format\n\t\texpectedResult bool\n\t\twantErr        require.ErrorAssertionFunc\n\t}{\n\t\t{\n\t\t\tname:           \"semantic constraint with unknown format version - satisfied\",\n\t\t\tconstraintFmt:  SemanticFormat,\n\t\t\tconstraint:     \"> 1.0.0\",\n\t\t\tversionStr:     \"1.2.3\",\n\t\t\tversionFmt:     UnknownFormat,\n\t\t\texpectedResult: true,\n\t\t},\n\t\t{\n\t\t\tname:           \"semantic constraint with unknown format version - not satisfied\",\n\t\t\tconstraintFmt:  SemanticFormat,\n\t\t\tconstraint:     \"> 2.0.0\",\n\t\t\tversionStr:     \"1.2.3\",\n\t\t\tversionFmt:     UnknownFormat,\n\t\t\texpectedResult: false,\n\t\t},\n\t\t{\n\t\t\tname:           \"maven constraint with unknown format version - satisfied\",\n\t\t\tconstraintFmt:  MavenFormat,\n\t\t\tconstraint:     \">= 1.0.0\",\n\t\t\tversionStr:     \"1.5.0\",\n\t\t\tversionFmt:     UnknownFormat,\n\t\t\texpectedResult: true,\n\t\t},\n\t\t{\n\t\t\tname:           \"different known formats should error\",\n\t\t\tconstraintFmt:  SemanticFormat,\n\t\t\tconstraint:     \"> 1.0.0\",\n\t\t\tversionStr:     \"1.2.3-r1\",\n\t\t\tversionFmt:     ApkFormat,\n\t\t\texpectedResult: false,\n\t\t\twantErr:        require.Error,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif tt.wantErr == nil {\n\t\t\t\ttt.wantErr = require.NoError\n\t\t\t}\n\n\t\t\tconstraint, err := newGenericConstraint(tt.constraintFmt, tt.constraint)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tversion := New(tt.versionStr, tt.versionFmt)\n\n\t\t\tsatisfied, err := constraint.Satisfied(version)\n\t\t\ttt.wantErr(t, err)\n\n\t\t\tif err != nil {\n\t\t\t\treturn\n\t\t\t}\n\t\t\trequire.Equal(t, tt.expectedResult, satisfied)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/version/golang_version.go",
    "content": "package version\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\thashiVer \"github.com/anchore/go-version\"\n)\n\nvar _ Comparator = (*golangVersion)(nil)\n\ntype golangVersion struct {\n\traw string\n\tobj *hashiVer.Version\n}\n\nfunc newGolangVersion(v string) (golangVersion, error) {\n\tif v == \"(devel)\" {\n\t\treturn golangVersion{}, ErrUnsupportedVersion\n\t}\n\n\t// Invalid Semver fix ups\n\n\t// go stdlib is reported by syft as a go package with version like \"go1.24.1\"\n\t// other versions have \"v\" as a prefix, which the semver lib handles automatically\n\tfixedUp := strings.TrimPrefix(v, \"go\")\n\n\t// go1.24 creates non-dot separated build metadata fields, e.g. +incompatible+dirty\n\t// Fix up as per semver spec\n\tbefore, after, found := strings.Cut(fixedUp, \"+\")\n\tif found {\n\t\tfixedUp = before + \"+\" + strings.ReplaceAll(after, \"+\", \".\")\n\t}\n\n\tsemver, err := hashiVer.NewSemver(fixedUp)\n\tif err != nil {\n\t\treturn golangVersion{}, err\n\t}\n\treturn golangVersion{\n\t\traw: v,\n\t\tobj: semver,\n\t}, nil\n}\n\nfunc (v golangVersion) Compare(other *Version) (int, error) {\n\tif other == nil {\n\t\treturn -1, ErrNoVersionProvided\n\t}\n\n\to, err := newGolangVersion(other.Raw)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\tif o.raw == v.raw {\n\t\treturn 0, nil\n\t}\n\n\tif o.raw == \"(devel)\" {\n\t\treturn -1, fmt.Errorf(\"cannot compare a non-development version %q with a default development version of %q\", v.raw, o.raw)\n\t}\n\n\treturn v.compare(o), nil\n}\n\nfunc (v golangVersion) compare(o golangVersion) int {\n\tswitch {\n\tcase v.obj != nil && o.obj != nil:\n\t\treturn v.obj.Compare(o.obj)\n\tcase v.obj != nil && o.obj == nil:\n\t\treturn 1\n\tcase v.obj == nil && o.obj != nil:\n\t\treturn -1\n\tdefault:\n\t\treturn strings.Compare(v.raw, o.raw)\n\t}\n}\n"
  },
  {
    "path": "grype/version/golang_version_test.go",
    "content": "package version\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\thashiVer \"github.com/anchore/go-version\"\n)\n\nfunc TestGolangVersion_Constraint(t *testing.T) {\n\ttests := []struct {\n\t\tname       string\n\t\tversion    string\n\t\tconstraint string\n\t\tsatisfied  bool\n\t}{\n\t\t{\n\t\t\tname:       \"regular semantic version satisfied\",\n\t\t\tversion:    \"v1.2.3\",\n\t\t\tconstraint: \"< 1.2.4\",\n\t\t\tsatisfied:  true,\n\t\t},\n\t\t{\n\t\t\tname:       \"regular semantic version unsatisfied\",\n\t\t\tversion:    \"v1.2.3\",\n\t\t\tconstraint: \"> 1.2.4\",\n\t\t\tsatisfied:  false,\n\t\t},\n\t\t{\n\t\t\tname:       \"+incompatible added to version\", // see grype#1581\n\t\t\tversion:    \"v3.2.0+incompatible\",\n\t\t\tconstraint: \"<=3.2.0\",\n\t\t\tsatisfied:  true,\n\t\t},\n\t\t{\n\t\t\tname:       \"the empty constraint is always satisfied\",\n\t\t\tversion:    \"v1.0.0\",\n\t\t\tconstraint: \"\",\n\t\t\tsatisfied:  true,\n\t\t},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tc, err := GetConstraint(tc.constraint, GolangFormat)\n\t\t\trequire.NoError(t, err)\n\t\t\tv := New(tc.version, GolangFormat)\n\t\t\tsat, err := c.Satisfied(v)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, tc.satisfied, sat)\n\t\t})\n\t}\n}\n\nfunc TestGolangVersion_String(t *testing.T) {\n\ttests := []struct {\n\t\tname       string\n\t\tconstraint string\n\t\texpected   string\n\t}{\n\t\t{\n\t\t\tname:       \"empty string\",\n\t\t\tconstraint: \"\",\n\t\t\texpected:   \"none (go)\",\n\t\t},\n\t\t{\n\t\t\tname:       \"basic constraint\",\n\t\t\tconstraint: \"< 1.3.4\",\n\t\t\texpected:   \"< 1.3.4 (go)\",\n\t\t},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tc, err := GetConstraint(tc.constraint, GolangFormat)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, tc.expected, c.String())\n\t\t})\n\t}\n}\n\nfunc TestGolangVersion_Compare(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tversion1 string\n\t\tversion2 string\n\t\texpected int\n\t}{\n\t\t{\n\t\t\tname:     \"same basic version\",\n\t\t\tversion1: \"v1.2.3\",\n\t\t\tversion2: \"v1.2.3\",\n\t\t\texpected: 0,\n\t\t},\n\t\t{\n\t\t\tname:     \"same version with incompatible\",\n\t\t\tversion1: \"v3.2.0+incompatible\",\n\t\t\tversion2: \"v3.2.0+incompatible\",\n\t\t\texpected: 0,\n\t\t},\n\t\t{\n\t\t\tname:     \"same go stdlib version\",\n\t\t\tversion1: \"go1.24.1\",\n\t\t\tversion2: \"go1.24.1\",\n\t\t\texpected: 0,\n\t\t},\n\t\t{\n\t\t\tname:     \"version1 less than version2\",\n\t\t\tversion1: \"v1.2.3\",\n\t\t\tversion2: \"v1.2.4\",\n\t\t\texpected: -1,\n\t\t},\n\t\t{\n\t\t\tname:     \"version1 greater than version2\",\n\t\t\tversion1: \"v1.2.4\",\n\t\t\tversion2: \"v1.2.3\",\n\t\t\texpected: 1,\n\t\t},\n\t\t{\n\t\t\tname:     \"version1 equal to version2\",\n\t\t\tversion1: \"v1.2.3\",\n\t\t\tversion2: \"v1.2.3\",\n\t\t\texpected: 0,\n\t\t},\n\t\t{\n\t\t\tname:     \"go stdlib versions\",\n\t\t\tversion1: \"go1.23.1\",\n\t\t\tversion2: \"go1.24.1\",\n\t\t\texpected: -1,\n\t\t},\n\t\t{\n\t\t\tname:     \"incompatible versions\",\n\t\t\tversion1: \"v3.1.0+incompatible\",\n\t\t\tversion2: \"v3.2.0+incompatible\",\n\t\t\texpected: -1,\n\t\t},\n\t\t{\n\t\t\tname:     \"semver this version less\",\n\t\t\tversion1: \"v1.2.3\",\n\t\t\tversion2: \"v1.2.4\",\n\t\t\texpected: -1,\n\t\t},\n\t\t{\n\t\t\tname:     \"semver this version more\",\n\t\t\tversion1: \"v1.3.4\",\n\t\t\tversion2: \"v1.2.4\",\n\t\t\texpected: 1,\n\t\t},\n\t\t{\n\t\t\tname:     \"semver equal\",\n\t\t\tversion1: \"v1.2.4\",\n\t\t\tversion2: \"v1.2.4\",\n\t\t\texpected: 0,\n\t\t},\n\t\t{\n\t\t\tname:     \"commit-sha this version less\",\n\t\t\tversion1: \"v0.0.0-20180116102854-5a71ef0e047d\",\n\t\t\tversion2: \"v0.0.0-20190116102854-somehash\",\n\t\t\texpected: -1,\n\t\t},\n\t\t{\n\t\t\tname:     \"commit-sha this version more\",\n\t\t\tversion1: \"v0.0.0-20180216102854-5a71ef0e047d\",\n\t\t\tversion2: \"v0.0.0-20180116102854-somehash\",\n\t\t\texpected: 1,\n\t\t},\n\t\t{\n\t\t\tname:     \"commit-sha this version equal\",\n\t\t\tversion1: \"v0.0.0-20180116102854-5a71ef0e047d\",\n\t\t\tversion2: \"v0.0.0-20180116102854-5a71ef0e047d\",\n\t\t\texpected: 0,\n\t\t},\n\t\t{\n\t\t\tname:     \"this pre-semver is less than any semver\",\n\t\t\tversion1: \"v0.0.0-20180116102854-5a71ef0e047d\",\n\t\t\tversion2: \"v0.0.1\",\n\t\t\texpected: -1,\n\t\t},\n\t\t{\n\t\t\tname:     \"semver is greater than timestamp\",\n\t\t\tversion1: \"v2.1.0\",\n\t\t\tversion2: \"v0.0.0-20180116102854-5a71ef0e047d\",\n\t\t\texpected: 1,\n\t\t},\n\t\t{\n\t\t\tname:     \"pseudoversion less than other pseudoversion\",\n\t\t\tversion1: \"v0.0.0-20170116102854-1ef0e047d5a7\",\n\t\t\tversion2: \"v0.0.0-20180116102854-5a71ef0e047d\",\n\t\t\texpected: -1,\n\t\t},\n\t\t{\n\t\t\tname:     \"pseudoversion greater than other pseudoversion\",\n\t\t\tversion1: \"v0.0.0-20190116102854-8a3f0e047d5a\",\n\t\t\tversion2: \"v0.0.0-20180116102854-5a71ef0e047d\",\n\t\t\texpected: 1,\n\t\t},\n\t\t{\n\t\t\tname:     \"+incompatible doesn't break equality\",\n\t\t\tversion1: \"v3.2.0\",\n\t\t\tversion2: \"v3.2.0+incompatible\",\n\t\t\texpected: 0,\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tversion1 := New(test.version1, GolangFormat)\n\n\t\t\tversion2 := New(test.version2, GolangFormat)\n\n\t\t\tresult, err := version1.Compare(version2)\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, test.expected, result)\n\t\t})\n\t}\n}\n\nfunc TestGolangVersion_Compare_NilVersion(t *testing.T) {\n\tversion := New(\"v1.2.3\", GolangFormat)\n\n\tresult, err := version.Compare(nil)\n\trequire.Error(t, err)\n\tassert.Equal(t, ErrNoVersionProvided, err)\n\tassert.Equal(t, -1, result)\n}\n\nfunc TestGolangVersion_Compare_DifferentFormat(t *testing.T) {\n\tgolangVer, err := newGolangVersion(\"v1.2.3\")\n\trequire.NoError(t, err)\n\n\tsemanticVer := New(\"1.2.3\", SemanticFormat)\n\n\tresult, err := golangVer.Compare(semanticVer)\n\trequire.NoError(t, err)\n\tassert.Equal(t, 0, result)\n}\n\nfunc TestGolangVersion(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    string\n\t\texpected golangVersion\n\t\twantErr  require.ErrorAssertionFunc\n\t}{\n\t\t{\n\t\t\tname:  \"normal semantic version\",\n\t\t\tinput: \"v1.8.0\",\n\t\t\texpected: golangVersion{\n\t\t\t\traw: \"v1.8.0\",\n\t\t\t\tobj: hashiVer.Must(hashiVer.NewSemver(\"v1.8.0\")),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"v0.0.0 date and hash version\",\n\t\t\tinput: \"v0.0.0-20180116102854-5a71ef0e047d\",\n\t\t\texpected: golangVersion{\n\t\t\t\traw: \"v0.0.0-20180116102854-5a71ef0e047d\",\n\t\t\t\tobj: hashiVer.Must(hashiVer.NewSemver(\"v0.0.0-20180116102854-5a71ef0e047d\")),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"semver with +incompatible\",\n\t\t\tinput: \"v24.0.7+incompatible\",\n\t\t\texpected: golangVersion{\n\t\t\t\traw: \"v24.0.7+incompatible\",\n\t\t\t\tobj: hashiVer.Must(hashiVer.NewSemver(\"v24.0.7+incompatible\")),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"semver with +incompatible+dirty\",\n\t\t\tinput: \"v24.0.7+incompatible+dirty\",\n\t\t\texpected: golangVersion{\n\t\t\t\traw: \"v24.0.7+incompatible+dirty\",\n\t\t\t\tobj: hashiVer.Must(hashiVer.NewSemver(\"v24.0.7+incompatible.dirty\")),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"standard library\",\n\t\t\tinput: \"go1.21.4\",\n\t\t\texpected: golangVersion{\n\t\t\t\traw: \"go1.21.4\",\n\t\t\t\tobj: hashiVer.Must(hashiVer.NewSemver(\"1.21.4\")),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t// \"(devel)\" is the main module of a go program.\n\t\t\t// If we get a package with this version, it means the SBOM\n\t\t\t// doesn't have a real version number for the built package, so\n\t\t\t// we can't compare it and should just return an error.\n\t\t\tname:  \"devel\",\n\t\t\tinput: \"(devel)\",\n\t\t\twantErr: func(t require.TestingT, err error, msgAndArgs ...interface{}) {\n\t\t\t\trequire.ErrorIs(t, err, ErrUnsupportedVersion)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:    \"invalid\",\n\t\t\tinput:   \"invalid\",\n\t\t\twantErr: require.Error,\n\t\t},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tif tc.wantErr == nil {\n\t\t\t\ttc.wantErr = require.NoError\n\t\t\t}\n\t\t\tv, err := newGolangVersion(tc.input)\n\t\t\ttc.wantErr(t, err)\n\t\t\tif err != nil {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tassert.Equal(t, tc.expected, v)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/version/helper_test.go",
    "content": "package version\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\ntype testCase struct {\n\tname       string\n\tversion    string\n\tconstraint string\n\tsatisfied  bool\n\twantError  require.ErrorAssertionFunc\n}\n\nfunc (c *testCase) tName() string {\n\tif c.name != \"\" {\n\t\treturn c.name\n\t}\n\n\treturn fmt.Sprintf(\"ver='%s'const='%s'\", c.version, strings.ReplaceAll(c.constraint, \" \", \"\"))\n}\n\nfunc (c *testCase) assertVersionConstraint(t *testing.T, format Format, constraint Constraint) {\n\tt.Helper()\n\tif c.wantError == nil {\n\t\tc.wantError = require.NoError\n\t}\n\n\tversion := New(c.version, format)\n\n\tisSatisfied, err := constraint.Satisfied(version)\n\tc.wantError(t, err)\n\tif err != nil {\n\t\treturn\n\t}\n\tassert.Equal(t, c.satisfied, isSatisfied, \"unexpected constraint check result\")\n}\n"
  },
  {
    "path": "grype/version/jvm_version.go",
    "content": "package version\n\nimport (\n\t\"fmt\"\n\t\"regexp\"\n\t\"strings\"\n\n\thashiVer \"github.com/anchore/go-version\"\n\t\"github.com/anchore/grype/internal\"\n\t\"github.com/anchore/grype/internal/log\"\n)\n\nvar _ interface {\n\tComparator\n} = (*jvmVersion)(nil)\n\nvar (\n\tpreJep223VersionPattern = regexp.MustCompile(`^1\\.(?P<major>\\d+)(\\.(?P<minor>\\d+)([_-](update)?(_)?(?P<patch>\\d+))?(-(?P<prerelease>[^b][^-]+))?(-b(?P<build>\\d+))?)?`)\n\tnonCompliantSemverIsh   = regexp.MustCompile(`^(?P<major>\\d+)(\\.(?P<minor>\\d+)(\\.(?P<patch>\\d+))?([_-](update)?(_)?(?P<update>\\d+))?(-(?P<prerelease>[^b][^-]+))?(-b(?P<build>\\d+))?)?`)\n)\n\ntype jvmVersion struct {\n\tisPreJep223 bool\n\tsemVer      *hashiVer.Version\n}\n\nfunc newJvmVersion(raw string) (jvmVersion, error) {\n\tisPreJep233 := strings.HasPrefix(raw, \"1.\")\n\n\tif isPreJep233 {\n\t\t// convert the pre-JEP 223 version to semver\n\t\traw = convertPreJep223Version(raw)\n\t} else {\n\t\traw = convertNonCompliantSemver(raw)\n\t}\n\tverObj, err := hashiVer.NewVersion(raw)\n\tif err != nil {\n\t\treturn jvmVersion{}, invalidFormatError(JVMFormat, raw, err)\n\t}\n\n\treturn jvmVersion{\n\t\tisPreJep223: isPreJep233,\n\t\tsemVer:      verObj,\n\t}, nil\n}\n\nfunc (v jvmVersion) Compare(other *Version) (int, error) {\n\tif other == nil {\n\t\treturn -1, ErrNoVersionProvided\n\t}\n\n\to, err := newJvmVersion(other.Raw)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\treturn v.compare(o), nil\n}\n\nfunc (v jvmVersion) compare(other jvmVersion) int {\n\treturn v.semVer.Compare(other.semVer)\n}\n\nfunc convertNonCompliantSemver(version string) string {\n\t// if there is -update as a prerelease, and the patch version is missing or 0, then we should parse the prerelease\n\t// info that has the update value and extract the version. This should be used as the patch version.\n\n\t// 8.0-update302 --> 8.0.302\n\t// 8.0-update302-b08 --> 8.0.302+8\n\t// 8.0-update_302-b08 --> 8.0.302+8\n\n\tmatches := internal.MatchNamedCaptureGroups(nonCompliantSemverIsh, version)\n\tif len(matches) == 0 {\n\t\tlog.WithFields(\"version\", version).Trace(\"unable to convert pre-JEP 223 JVM version\")\n\t\treturn version\n\t}\n\n\t// extract relevant parts from the matches\n\tmajorVersion := trim0sFromLeft(matches[\"major\"])\n\tminorVersion := trim0sFromLeft(matches[\"minor\"])\n\tpatchVersion := trim0sFromLeft(matches[\"patch\"])\n\tupdate := trim0sFromLeft(matches[\"update\"])\n\tpreRelease := trim0sFromLeft(matches[\"prerelease\"])\n\tbuild := trim0sFromLeft(matches[\"build\"])\n\n\tif (patchVersion == \"\" || patchVersion == \"0\") && update != \"\" {\n\t\tpatchVersion = update\n\t}\n\n\treturn buildSemVer(majorVersion, minorVersion, patchVersion, preRelease, build)\n}\n\nfunc convertPreJep223Version(version string) string {\n\t// convert the following pre JEP 223 version strings to semvers\n\t// 1.8.0_302-b08 --> 8.0.302+8\n\t// 1.9.0-ea-b19  --> 9.0.0-ea+19\n\t// NOTE: this makes an assumption that the old update field is the patch version in semver...\n\t// this is NOT strictly in the spec, but for 1.8 this tends to be true (especially for temurin-based builds)\n\tversion = strings.TrimSpace(version)\n\n\tmatches := internal.MatchNamedCaptureGroups(preJep223VersionPattern, version)\n\tif len(matches) == 0 {\n\t\tlog.WithFields(\"version\", version).Trace(\"unable to convert pre-JEP 223 JVM version\")\n\t\treturn version\n\t}\n\n\t// extract relevant parts from the matches\n\tmajorVersion := trim0sFromLeft(matches[\"major\"])\n\tminorVersion := trim0sFromLeft(matches[\"minor\"])\n\tpatchVersion := trim0sFromLeft(matches[\"patch\"])\n\tpreRelease := trim0sFromLeft(matches[\"prerelease\"])\n\tbuild := trim0sFromLeft(matches[\"build\"])\n\n\tif patchVersion == \"\" {\n\t\tpatchVersion = \"0\"\n\t}\n\n\treturn buildSemVer(majorVersion, minorVersion, patchVersion, preRelease, build)\n}\nfunc buildSemVer(majorVersion, minorVersion, patchVersion, preRelease, build string) string {\n\tif minorVersion == \"\" {\n\t\tminorVersion = \"0\"\n\t}\n\n\tsegs := []string{majorVersion, minorVersion}\n\tif patchVersion != \"\" {\n\t\tsegs = append(segs, patchVersion)\n\t}\n\n\tvar semver strings.Builder\n\tsemver.WriteString(strings.Join(segs, \".\"))\n\n\tif preRelease != \"\" {\n\t\tfmt.Fprintf(&semver, \"-%s\", preRelease)\n\t}\n\tif build != \"\" {\n\t\tfmt.Fprintf(&semver, \"+%s\", build)\n\t}\n\n\treturn semver.String()\n}\n\nfunc trim0sFromLeft(v string) string {\n\tif v == \"0\" {\n\t\treturn v\n\t}\n\treturn strings.TrimLeft(v, \"0\")\n}\n"
  },
  {
    "path": "grype/version/jvm_version_test.go",
    "content": "package version\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestJVMVersion_Constraint(t *testing.T) {\n\ttests := []testCase{\n\t\t// pre jep 223 versions\n\t\t{version: \"1.7.0_80\", constraint: \"< 1.8.0\", satisfied: true},\n\t\t{version: \"1.8.0_131\", constraint: \"> 1.8.0\", satisfied: true},\n\t\t{version: \"1.8.0_131\", constraint: \"< 1.8.0_132\", satisfied: true},\n\t\t{version: \"1.8.0_131-b11\", constraint: \"< 1.8.0_132\", satisfied: true},\n\n\t\t{version: \"1.7.0_80\", constraint: \"> 1.8.0\", satisfied: false},\n\t\t{version: \"1.8.0_131\", constraint: \"< 1.8.0\", satisfied: false},\n\t\t{version: \"1.8.0_131\", constraint: \"> 1.8.0_132\", satisfied: false},\n\t\t{version: \"1.8.0_131-b11\", constraint: \"> 1.8.0_132\", satisfied: false},\n\n\t\t{version: \"1.7.0_80\", constraint: \"= 1.8.0\", satisfied: false},\n\t\t{version: \"1.8.0_131\", constraint: \"= 1.8.0\", satisfied: false},\n\t\t{version: \"1.8.0_131\", constraint: \"= 1.8.0_132\", satisfied: false},\n\t\t{version: \"1.8.0_131-b11\", constraint: \"= 1.8.0_132\", satisfied: false},\n\n\t\t{version: \"1.8.0_80\", constraint: \"= 1.8.0_80\", satisfied: true},\n\t\t{version: \"1.8.0_131\", constraint: \">= 1.8.0_131\", satisfied: true},\n\t\t{version: \"1.8.0_131\", constraint: \"= 1.8.0_131-b001\", satisfied: true}, // builds should not matter\n\t\t{version: \"1.8.0_131-ea-b11\", constraint: \"= 1.8.0_131-ea\", satisfied: true},\n\n\t\t// jep 223 versions\n\t\t{version: \"8.0.4\", constraint: \"> 8.0.3\", satisfied: true},\n\t\t{version: \"8.0.4\", constraint: \"< 8.0.5\", satisfied: true},\n\t\t{version: \"9.0.0\", constraint: \"> 8.0.5\", satisfied: true},\n\t\t{version: \"9.0.0\", constraint: \"< 9.1.0\", satisfied: true},\n\t\t{version: \"11.0.4\", constraint: \"<= 11.0.4\", satisfied: true},\n\t\t{version: \"11.0.5\", constraint: \"> 11.0.4\", satisfied: true},\n\n\t\t{version: \"8.0.4\", constraint: \"< 8.0.3\", satisfied: false},\n\t\t{version: \"8.0.4\", constraint: \"> 8.0.5\", satisfied: false},\n\t\t{version: \"9.0.0\", constraint: \"< 8.0.5\", satisfied: false},\n\t\t{version: \"9.0.0\", constraint: \"> 9.1.0\", satisfied: false},\n\t\t{version: \"11.0.4\", constraint: \"> 11.0.4\", satisfied: false},\n\t\t{version: \"11.0.5\", constraint: \"< 11.0.4\", satisfied: false},\n\n\t\t// mixed versions\n\t\t{version: \"1.8.0_131\", constraint: \"< 9.0.0\", satisfied: true}, // 1.8.0_131 -> 8.0.131\n\t\t{version: \"9.0.0\", constraint: \"> 1.8.0_131\", satisfied: true}, // 1.8.0_131 -> 8.0.131\n\t\t{version: \"1.8.0_131\", constraint: \"<= 8.0.131\", satisfied: true},\n\t\t{version: \"1.8.0_131\", constraint: \"> 7.0.79\", satisfied: true},\n\t\t{version: \"1.8.0_131\", constraint: \"= 8.0.131\", satisfied: true},\n\t\t{version: \"1.8.0_131\", constraint: \">= 9.0.0\", satisfied: false},\n\t\t{version: \"9.0.1\", constraint: \"< 8.0.131\", satisfied: false},\n\n\t\t// pre-release versions\n\t\t{version: \"1.8.0_131-ea\", constraint: \"< 1.8.0_131\", satisfied: true},\n\t\t{version: \"1.8.0_131\", constraint: \"> 1.8.0_131-ea\", satisfied: true},\n\t\t{version: \"9.0.0-ea\", constraint: \"< 9.0.0\", satisfied: true},\n\t\t{version: \"9.0.0-ea\", constraint: \"> 1.8.0_131\", satisfied: true},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.version+\"_constraint_\"+test.constraint, func(t *testing.T) {\n\t\t\tconstraint, err := GetConstraint(test.constraint, JVMFormat)\n\t\t\trequire.NoError(t, err)\n\t\t\ttest.assertVersionConstraint(t, JVMFormat, constraint)\n\t\t})\n\t}\n}\n\nfunc TestJVMVersion_Compare(t *testing.T) {\n\ttests := []struct {\n\t\tv1       string\n\t\tv2       string\n\t\texpected int\n\t}{\n\t\t// pre jep223 versions\n\t\t{\"1.8\", \"1.8.0\", 0},\n\t\t{\"1.8.0\", \"1.8.0_0\", 0},\n\t\t{\"1.8.0\", \"1.8.0\", 0},\n\t\t{\"1.7.0\", \"1.8.0\", -1},\n\t\t{\"1.8.0_131\", \"1.8.0_131\", 0},\n\t\t{\"1.8.0_131\", \"1.8.0_132\", -1},\n\n\t\t// builds should not matter\n\t\t{\"1.8.0_131\", \"1.8.0_130\", 1},\n\t\t{\"1.8.0_131\", \"1.8.0_132-b11\", -1},\n\t\t{\"1.8.0_131-b11\", \"1.8.0_132-b11\", -1},\n\t\t{\"1.8.0_131-b11\", \"1.8.0_131-b12\", 0},\n\t\t{\"1.8.0_131-b11\", \"1.8.0_131-b10\", 0},\n\t\t{\"1.8.0_131-b11\", \"1.8.0_131\", 0},\n\t\t{\"1.8.0_131-b11\", \"1.8.0_131-b11\", 0},\n\n\t\t// jep223 versions (semver)\n\t\t{\"8.0.4\", \"8.0.4\", 0},\n\t\t{\"8.0.4\", \"8.0.5\", -1},\n\t\t{\"8.0.4\", \"8.0.3\", 1},\n\t\t{\"8.0.4\", \"8.0.4+b1\", 0},\n\n\t\t// mix comparison\n\t\t{\"1.8.0_131\", \"8.0.4\", 1},           // 1.8.0_131 --> 8.0.131\n\t\t{\"8.0.4\", \"1.8.0_131\", -1},          // doesn't matter which side the comparison is on\n\t\t{\"1.8.0_131-b002\", \"8.0.131+b2\", 0}, // builds should not matter\n\t\t{\"1.8.0_131-b002\", \"8.0.131+b1\", 0}, // builds should not matter\n\t\t{\"1.6.0\", \"8.0.1\", -1},              // 1.6.0 --> 6.0.0\n\n\t\t// prerelease\n\t\t{\"1.8.0_13-ea-b002\", \"1.8.0_13-ea-b001\", 0},\n\t\t{\"1.8.0_13-ea\", \"1.8.0_13-ea-b001\", 0},\n\t\t{\"1.8.0_13-ea-b002\", \"8.0.13-ea+b2\", 0},\n\t\t{\"1.8.0_13-ea-b002\", \"8.0.13+b2\", -1},\n\t\t{\"1.8.0_13-b002\", \"8.0.13-ea+b2\", 1},\n\n\t\t// pre 1.8 (when the jep 223 was introduced)\n\t\t{\"1.7.0\", \"7.0.0\", 0}, // there is no v7 of the JVM, but we want to honor this comparison since it may be someone mistakenly using the wrong version format\n\n\t\t// invalid but we should work with these\n\t\t{\"1.8.0_131\", \"1.8.0-update131-b02\", 0},\n\t\t{\"1.8.0_131\", \"1.8.0-update_131-b02\", 0},\n\t}\n\n\tfor _, test := range tests {\n\t\tname := test.v1 + \"_vs_\" + test.v2\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tv1 := New(test.v1, JVMFormat)\n\t\t\trequire.NotNil(t, v1)\n\n\t\t\tv2 := New(test.v2, JVMFormat)\n\t\t\trequire.NotNil(t, v2)\n\n\t\t\tactual, err := v1.Compare(v2)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, test.expected, actual)\n\t\t})\n\t}\n}\n\nfunc TestJVMVersion_ConvertNonCompliantSemver(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:     \"simple update\",\n\t\t\tinput:    \"8.0-update302\",\n\t\t\texpected: \"8.0.302\",\n\t\t},\n\t\t{\n\t\t\tname:     \"update with build\",\n\t\t\tinput:    \"8.0-update302-b08\",\n\t\t\texpected: \"8.0.302+8\",\n\t\t},\n\t\t{\n\t\t\tname:     \"update with underscore and build\",\n\t\t\tinput:    \"8.0-update_302-b08\",\n\t\t\texpected: \"8.0.302+8\",\n\t\t},\n\t\t{\n\t\t\tname:     \"version without patch and prerelease\",\n\t\t\tinput:    \"8.0.0\",\n\t\t\texpected: \"8.0.0\",\n\t\t},\n\t\t{\n\t\t\tname:     \"version with patch, no update\",\n\t\t\tinput:    \"8.0.100\",\n\t\t\texpected: \"8.0.100\",\n\t\t},\n\t\t{\n\t\t\tname:     \"version with patch and prerelease\",\n\t\t\tinput:    \"8.0.0-rc1\",\n\t\t\texpected: \"8.0.0-rc1\",\n\t\t},\n\t\t{\n\t\t\tname:     \"invalid update format, no update keyword\",\n\t\t\tinput:    \"8.0-foo302\",\n\t\t\texpected: \"8.0-foo302\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := convertNonCompliantSemver(tt.input)\n\t\t\trequire.Equal(t, tt.expected, result)\n\t\t})\n\t}\n}\n\nfunc TestJVMVersion_Invalid(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\tversion string\n\t\twantErr require.ErrorAssertionFunc\n\t}{\n\t\t{\n\t\t\tname:    \"invalid version\",\n\t\t\tversion: \"1.a\",\n\t\t\twantErr: require.Error,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif tt.wantErr == nil {\n\t\t\t\ttt.wantErr = require.NoError\n\t\t\t}\n\t\t\t_, err := newJvmVersion(tt.version)\n\t\t\ttt.wantErr(t, err)\n\t\t})\n\t}\n}\n\nfunc TestJvmVersion_Compare_Formats(t *testing.T) {\n\ttests := []struct {\n\t\tname           string\n\t\tthisVersion    string\n\t\totherVersion   string\n\t\totherFormat    Format\n\t\texpectError    bool\n\t\terrorSubstring string\n\t}{\n\t\t{\n\t\t\tname:         \"same format successful comparison\",\n\t\t\tthisVersion:  \"1.8.0_275\",\n\t\t\totherVersion: \"1.8.0_281\",\n\t\t\totherFormat:  JVMFormat,\n\t\t\texpectError:  false,\n\t\t},\n\t\t{\n\t\t\tname:         \"semantic format successful comparison\",\n\t\t\tthisVersion:  \"1.8.0_275\",\n\t\t\totherVersion: \"1.8.1\",\n\t\t\totherFormat:  SemanticFormat,\n\t\t\texpectError:  false,\n\t\t},\n\t\t{\n\t\t\tname:         \"unknown format attempts upgrade to JVM - valid\",\n\t\t\tthisVersion:  \"1.8.0_275\",\n\t\t\totherVersion: \"1.8.0_281\",\n\t\t\totherFormat:  UnknownFormat,\n\t\t\texpectError:  false,\n\t\t},\n\t\t{\n\t\t\tname:         \"unknown format attempts upgrade to Semantic - valid\",\n\t\t\tthisVersion:  \"1.8.0_275\",\n\t\t\totherVersion: \"1.9.0\",\n\t\t\totherFormat:  UnknownFormat,\n\t\t\texpectError:  false,\n\t\t},\n\t\t{\n\t\t\tname:           \"unknown format fails all upgrades - invalid\",\n\t\t\tthisVersion:    \"1.8.0_275\",\n\t\t\totherVersion:   \"not-valid-jvm-or-semver\",\n\t\t\totherFormat:    UnknownFormat,\n\t\t\texpectError:    true,\n\t\t\terrorSubstring: \"invalid\",\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tthisVer, err := newJvmVersion(test.thisVersion)\n\t\t\trequire.NoError(t, err)\n\n\t\t\totherVer := New(test.otherVersion, test.otherFormat)\n\n\t\t\tresult, err := thisVer.Compare(otherVer)\n\n\t\t\tif test.expectError {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\tif test.errorSubstring != \"\" {\n\t\t\t\t\trequire.ErrorContains(t, err, test.errorSubstring)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.Contains(t, []int{-1, 0, 1}, result, \"Expected comparison result to be -1, 0, or 1\")\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestJvmVersion_Compare_EdgeCases(t *testing.T) {\n\ttests := []struct {\n\t\tname           string\n\t\tsetupFunc      func(testing.TB) (*Version, *Version)\n\t\texpectError    bool\n\t\terrorSubstring string\n\t}{\n\t\t{\n\t\t\tname: \"nil version object\",\n\t\t\tsetupFunc: func(t testing.TB) (*Version, *Version) {\n\t\t\t\tthisVer := New(\"1.8.0_275\", JVMFormat)\n\t\t\t\treturn thisVer, nil\n\t\t\t},\n\t\t\texpectError:    true,\n\t\t\terrorSubstring: \"no version provided for comparison\",\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tthisVer, otherVer := test.setupFunc(t)\n\n\t\t\t_, err := thisVer.Compare(otherVer)\n\n\t\t\trequire.Error(t, err)\n\t\t\tif test.errorSubstring != \"\" {\n\t\t\t\trequire.True(t, strings.Contains(err.Error(), test.errorSubstring),\n\t\t\t\t\t\"Expected error to contain '%s', got: %v\", test.errorSubstring, err)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/version/kb_constraint.go",
    "content": "package version\n\nimport \"fmt\"\n\ntype kbConstraint struct {\n\tRaw        string\n\tExpression simpleRangeExpression\n}\n\nfunc newKBConstraint(raw string) (kbConstraint, error) {\n\tif raw == \"\" {\n\t\t// an empty constraint is always satisfied\n\t\treturn kbConstraint{}, nil\n\t}\n\n\tconstraints, err := parseRangeExpression(raw)\n\tif err != nil {\n\t\treturn kbConstraint{}, fmt.Errorf(\"unable to parse kb constraint phrase: %w\", err)\n\t}\n\n\treturn kbConstraint{\n\t\tRaw:        raw,\n\t\tExpression: constraints,\n\t}, nil\n}\n\nfunc (c kbConstraint) Satisfied(version *Version) (bool, error) {\n\tif c.Raw == \"\" {\n\t\t// an empty constraint is never satisfied\n\t\treturn false, &NonFatalConstraintError{\n\t\t\tconstraint: c,\n\t\t\tversion:    version,\n\t\t\tmessage:    \"unexpected data in DB: empty raw version constraint\",\n\t\t}\n\t}\n\n\tif version == nil {\n\t\treturn true, nil\n\t}\n\n\tif version.Format != KBFormat {\n\t\treturn false, newUnsupportedFormatError(KBFormat, version)\n\t}\n\n\treturn c.Expression.satisfied(KBFormat, version)\n}\n\nfunc (c kbConstraint) Format() Format {\n\treturn KBFormat\n}\n\nfunc (c kbConstraint) String() string {\n\tif c.Raw == \"\" {\n\t\treturn fmt.Sprintf(\"%q (kb)\", c.Raw) // with quotes\n\t}\n\treturn fmt.Sprintf(\"%s (kb)\", c.Raw) // no quotes\n}\n\nfunc (c kbConstraint) Value() string {\n\treturn c.Raw\n}\n"
  },
  {
    "path": "grype/version/kb_constraint_test.go",
    "content": "package version\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestKbVersion_Constraint(t *testing.T) {\n\ttests := []testCase{\n\t\t{\n\t\t\tname:    \"no constraint no version raises error\",\n\t\t\tversion: \"\", constraint: \"\",\n\t\t\tsatisfied: false,\n\t\t\twantError: func(t require.TestingT, err error, msgAndArgs ...interface{}) {\n\t\t\t\tvar expectedError *NonFatalConstraintError\n\t\t\t\tassert.ErrorAs(t, err, &expectedError, \"Unexpected error type from kbConstraint.Satisfied: %v\", err)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:    \"no constraint with version raises error\",\n\t\t\tversion: \"878787\", constraint: \"\",\n\t\t\tsatisfied: false,\n\t\t\twantError: func(t require.TestingT, err error, msgAndArgs ...interface{}) {\n\t\t\t\tvar expectedError *NonFatalConstraintError\n\t\t\t\tassert.ErrorAs(t, err, &expectedError, \"Unexpected error type from kbConstraint.Satisfied: %v\", err)\n\t\t\t},\n\t\t},\n\t\t{name: \"no version is unsatisfied\", version: \"\", constraint: \"foo\", satisfied: false},\n\t\t{name: \"version constraint mismatch\", version: \"1\", constraint: \"foo\", satisfied: false},\n\t\t{name: \"matching version and constraint\", version: \"1\", constraint: \"1\", satisfied: true},\n\t\t{name: \"base keyword matching version and constraint\", version: \"base\", constraint: \"base\", satisfied: true},\n\t\t{name: \"version and OR constraint match\", version: \"878787\", constraint: \"979797 || 101010 || 878787\", satisfied: true},\n\t\t{name: \"version and OR constraint mismatch\", version: \"478787\", constraint: \"979797 || 101010 || 878787\", satisfied: false},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tconstraint, err := GetConstraint(test.constraint, KBFormat)\n\t\t\tassert.NoError(t, err, \"unexpected error from newKBConstraint: %v\", err)\n\n\t\t\ttest.assertVersionConstraint(t, KBFormat, constraint)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/version/kb_version.go",
    "content": "package version\n\nimport (\n\t\"reflect\"\n)\n\nvar _ Comparator = (*kbVersion)(nil)\n\ntype kbVersion struct {\n\tversion string\n}\n\nfunc newKBVersion(raw string) kbVersion {\n\treturn kbVersion{\n\t\tversion: raw,\n\t}\n}\n\nfunc (v kbVersion) Compare(other *Version) (int, error) {\n\tif other == nil {\n\t\treturn -1, ErrNoVersionProvided\n\t}\n\n\treturn v.compare(newKBVersion(other.Raw)), nil\n}\n\n// compare returns 0 if v == v2, 1 otherwise\nfunc (v kbVersion) compare(other kbVersion) int {\n\tif reflect.DeepEqual(v, other) {\n\t\treturn 0\n\t}\n\n\treturn 1\n}\n\nfunc (v kbVersion) String() string {\n\treturn v.version\n}\n"
  },
  {
    "path": "grype/version/kb_version_test.go",
    "content": "package version\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestKbVersion_Compare(t *testing.T) {\n\ttests := []struct {\n\t\tname           string\n\t\tthisVersion    string\n\t\totherVersion   string\n\t\totherFormat    Format\n\t\texpectError    bool\n\t\terrorSubstring string\n\t}{\n\t\t{\n\t\t\tname:         \"same format successful comparison\",\n\t\t\tthisVersion:  \"KB4562562\",\n\t\t\totherVersion: \"KB4562563\",\n\t\t\totherFormat:  KBFormat,\n\t\t\texpectError:  false,\n\t\t},\n\t\t{\n\t\t\tname:         \"different format does not return error\",\n\t\t\tthisVersion:  \"KB4562562\",\n\t\t\totherVersion: \"1.2.3\",\n\t\t\totherFormat:  SemanticFormat,\n\t\t\texpectError:  false,\n\t\t},\n\t\t{\n\t\t\tname:         \"unknown format attempts upgrade - valid kb format\",\n\t\t\tthisVersion:  \"KB4562562\",\n\t\t\totherVersion: \"KB4562563\",\n\t\t\totherFormat:  UnknownFormat,\n\t\t\texpectError:  false,\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tthisVer := newKBVersion(test.thisVersion)\n\n\t\t\totherVer := New(test.otherVersion, test.otherFormat)\n\n\t\t\tresult, err := thisVer.Compare(otherVer)\n\n\t\t\tif test.expectError {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\tif test.errorSubstring != \"\" {\n\t\t\t\t\tassert.True(t, strings.Contains(err.Error(), test.errorSubstring),\n\t\t\t\t\t\t\"Expected error to contain '%s', got: %v\", test.errorSubstring, err)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.Contains(t, []int{-1, 0, 1}, result, \"Expected comparison result to be -1, 0, or 1\")\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestKbVersion_Compare_EdgeCases(t *testing.T) {\n\ttests := []struct {\n\t\tname           string\n\t\tsetupFunc      func(testing.TB) (*Version, *Version)\n\t\texpectError    bool\n\t\terrorSubstring string\n\t}{\n\t\t{\n\t\t\tname: \"nil version object\",\n\t\t\tsetupFunc: func(t testing.TB) (*Version, *Version) {\n\t\t\t\tv := New(\"KB4562562\", KBFormat)\n\t\t\t\treturn v, nil\n\t\t\t},\n\t\t\texpectError:    true,\n\t\t\terrorSubstring: \"no version provided for comparison\",\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tthisVer, otherVer := test.setupFunc(t)\n\n\t\t\t_, err := thisVer.Compare(otherVer)\n\n\t\t\trequire.Error(t, err)\n\t\t\tif test.errorSubstring != \"\" {\n\t\t\t\tassert.True(t, strings.Contains(err.Error(), test.errorSubstring),\n\t\t\t\t\t\"Expected error to contain '%s', got: %v\", test.errorSubstring, err)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/version/maven_version.go",
    "content": "package version\n\nimport (\n\t\"fmt\"\n\t\"regexp\"\n\n\tmvnv \"github.com/masahiro331/go-mvn-version\"\n)\n\nvar _ Comparator = (*mavenVersion)(nil)\n\n// javaRuntimeQualifierPattern matches .jreNN or .jdkNN suffixes (case-insensitive) at the end of version strings\nvar javaRuntimeQualifierPattern = regexp.MustCompile(`(?i)\\.(jre|jdk)\\d+$`)\n\ntype mavenVersion struct {\n\traw string\n\tobj mvnv.Version\n}\n\n// stripJavaRuntimeQualifier removes .jreNN or .jdkNN suffixes from version strings.\n// These are runtime-specific qualifiers that don't affect version comparison.\n//\n// The pattern matches 'jre' or 'jdk' (case-insensitive) followed by one or more digits\n// at the END of the version string only. This means:\n//   - Case-insensitive: Both .jre11 and .JRE11 will be stripped\n//   - Requires digits: .jre or .jdk without numbers will NOT be stripped\n//   - End-anchored: .jre11-SNAPSHOT or .jdk17.beta will NOT be stripped\n//\n// Examples:\n//   - \"12.10.2.jre11\" -> \"12.10.2\" (stripped)\n//   - \"12.10.2.JRE11\" -> \"12.10.2\" (stripped)\n//   - \"12.10.2.jdk17\" -> \"12.10.2\" (stripped)\n//   - \"12.10.2.JDK17\" -> \"12.10.2\" (stripped)\n//   - \"12.10.2\" -> \"12.10.2\" (no change)\n//   - \"12.10.2.jre\" -> \"12.10.2.jre\" (no digits, not stripped)\n//   - \"12.10.2.jre11-SNAPSHOT\" -> \"12.10.2.jre11-SNAPSHOT\" (not at end, not stripped)\nfunc stripJavaRuntimeQualifier(version string) string {\n\treturn javaRuntimeQualifierPattern.ReplaceAllString(version, \"\")\n}\n\nfunc newMavenVersion(raw string) (mavenVersion, error) {\n\t// strip Java runtime qualifiers (e.g., .jre11, .jdk17) before parsing to ensure\n\t// versions like \"12.10.2\" and \"12.10.2.jre11\" are treated as equivalent for comparison.\n\t// The original raw version is preserved for display purposes.\n\tnormalized := stripJavaRuntimeQualifier(raw)\n\n\tver, err := mvnv.NewVersion(normalized)\n\tif err != nil {\n\t\treturn mavenVersion{}, fmt.Errorf(\"could not generate new java version from: %s; %w\", raw, err)\n\t}\n\n\treturn mavenVersion{\n\t\traw: raw,\n\t\tobj: ver,\n\t}, nil\n}\n\n// Compare returns 0 if other == j, 1 if other > j, and -1 if other < j.\n// If an error is returned, the int value is -1\nfunc (v mavenVersion) Compare(other *Version) (int, error) {\n\tif other == nil {\n\t\treturn -1, fmt.Errorf(\"cannot compare nil version with %v\", other)\n\t}\n\n\to, err := newMavenVersion(other.Raw)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\treturn v.compare(o.obj)\n}\n\nfunc (v mavenVersion) compare(other mvnv.Version) (int, error) {\n\tif v.obj.Equal(other) {\n\t\treturn 0, nil\n\t}\n\tif v.obj.LessThan(other) {\n\t\treturn -1, nil\n\t}\n\tif v.obj.GreaterThan(other) {\n\t\treturn 1, nil\n\t}\n\n\treturn -1, fmt.Errorf(\n\t\t\"could not compare java versions: %v with %v\",\n\t\tother.String(),\n\t\tv.obj.String())\n}\n"
  },
  {
    "path": "grype/version/maven_version_test.go",
    "content": "package version\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestStripJavaRuntimeQualifier(t *testing.T) {\n\ttests := []struct {\n\t\tname  string\n\t\tinput string\n\t\twant  string\n\t}{\n\t\t{\n\t\t\tname:  \"version with jre11\",\n\t\t\tinput: \"12.10.2.jre11\",\n\t\t\twant:  \"12.10.2\",\n\t\t},\n\t\t{\n\t\t\tname:  \"version with jdk17\",\n\t\t\tinput: \"12.10.2.jdk17\",\n\t\t\twant:  \"12.10.2\",\n\t\t},\n\t\t{\n\t\t\tname:  \"version with uppercase JRE11\",\n\t\t\tinput: \"12.10.2.JRE11\",\n\t\t\twant:  \"12.10.2\",\n\t\t},\n\t\t{\n\t\t\tname:  \"version with uppercase JDK17\",\n\t\t\tinput: \"12.10.2.JDK17\",\n\t\t\twant:  \"12.10.2\",\n\t\t},\n\t\t{\n\t\t\tname:  \"version with mixed case Jre11\",\n\t\t\tinput: \"12.10.2.Jre11\",\n\t\t\twant:  \"12.10.2\",\n\t\t},\n\t\t{\n\t\t\tname:  \"version without qualifier\",\n\t\t\tinput: \"12.10.2\",\n\t\t\twant:  \"12.10.2\",\n\t\t},\n\t\t{\n\t\t\tname:  \"version with jre but no digits\",\n\t\t\tinput: \"12.10.2.jre\",\n\t\t\twant:  \"12.10.2.jre\",\n\t\t},\n\t\t{\n\t\t\tname:  \"version with jdk but no digits\",\n\t\t\tinput: \"12.10.2.jdk\",\n\t\t\twant:  \"12.10.2.jdk\",\n\t\t},\n\t\t{\n\t\t\tname:  \"version with jre0 (zero)\",\n\t\t\tinput: \"12.10.2.jre0\",\n\t\t\twant:  \"12.10.2\",\n\t\t},\n\t\t{\n\t\t\tname:  \"version with jdk999 (large number)\",\n\t\t\tinput: \"12.10.2.jdk999\",\n\t\t\twant:  \"12.10.2\",\n\t\t},\n\t\t{\n\t\t\tname:  \"version with jre11 followed by SNAPSHOT\",\n\t\t\tinput: \"12.10.2.jre11-SNAPSHOT\",\n\t\t\twant:  \"12.10.2.jre11-SNAPSHOT\",\n\t\t},\n\t\t{\n\t\t\tname:  \"version with jdk17 followed by beta\",\n\t\t\tinput: \"12.10.2.jdk17.beta\",\n\t\t\twant:  \"12.10.2.jdk17.beta\",\n\t\t},\n\t\t{\n\t\t\tname:  \"version with JRE uppercase followed by SNAPSHOT\",\n\t\t\tinput: \"12.10.2.JRE11-SNAPSHOT\",\n\t\t\twant:  \"12.10.2.JRE11-SNAPSHOT\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot := stripJavaRuntimeQualifier(tt.input)\n\t\t\trequire.Equal(t, tt.want, got)\n\t\t})\n\t}\n}\n\nfunc TestMavenVersion_Constraint(t *testing.T) {\n\ttests := []testCase{\n\t\t// range expressions\n\t\t{version: \"1\", constraint: \"< 2.5\", satisfied: true},\n\t\t{version: \"1.0\", constraint: \"< 1.1\", satisfied: true},\n\t\t{version: \"1.1\", constraint: \"< 1.2\", satisfied: true},\n\t\t{version: \"1.0.0\", constraint: \"< 1.1\", satisfied: true},\n\t\t{version: \"1.0.1\", constraint: \"< 1.1\", satisfied: true},\n\t\t{version: \"1.1\", constraint: \"> 1.2.0\", satisfied: false},\n\t\t{version: \"1.0-alpha-1\", constraint: \"> 1.0\", satisfied: false},\n\t\t{version: \"1.0-alpha-1\", constraint: \"> 1.0-alpha-2\", satisfied: false},\n\t\t{version: \"1.0-alpha-1\", constraint: \"< 1.0-beta-1\", satisfied: true},\n\t\t{version: \"1.0-beta-1\", constraint: \"< 1.0-SNAPSHOT\", satisfied: true},\n\t\t{version: \"1.0-SNAPSHOT\", constraint: \"< 1.0\", satisfied: true},\n\t\t{version: \"1.0-alpha-1-SNAPSHOT\", constraint: \"> 1.0-alpha-1\", satisfied: false},\n\t\t{version: \"1.0\", constraint: \"< 1.0-1\", satisfied: true},\n\t\t{version: \"1.0-1\", constraint: \"< 1.0-2\", satisfied: true},\n\t\t{version: \"1.0.0\", constraint: \"< 1.0-1\", satisfied: true},\n\t\t{version: \"2.0-1\", constraint: \"> 2.0.1\", satisfied: false},\n\t\t{version: \"2.0.1-klm\", constraint: \"> 2.0.1-lmn\", satisfied: false},\n\t\t{version: \"2.0.1\", constraint: \"< 2.0.1-xyz\", satisfied: true},\n\t\t{version: \"2.0.1\", constraint: \"< 2.0.1-123\", satisfied: true},\n\t\t{version: \"2.0.1-xyz\", constraint: \"< 2.0.1-123\", satisfied: true},\n\t\t{version: \"2.414.2-cb-5\", constraint: \"> 2.414.2\", satisfied: true},\n\t\t{version: \"5.2.25.RELEASE\", constraint: \"< 5.2.25\", satisfied: false},\n\t\t{version: \"5.2.25.RELEASE\", constraint: \"<= 5.2.25\", satisfied: true},\n\n\t\t// equality expressions\n\t\t{version: \"1\", constraint: \"1\", satisfied: true},\n\t\t{version: \"1\", constraint: \"1.0\", satisfied: true},\n\t\t{version: \"1\", constraint: \"1.0.0\", satisfied: true},\n\t\t{version: \"1.0\", constraint: \"1.0.0\", satisfied: true},\n\t\t{version: \"1\", constraint: \"1-0\", satisfied: true},\n\t\t{version: \"1\", constraint: \"1.0-0\", satisfied: true},\n\t\t{version: \"1.0\", constraint: \"1.0-0\", satisfied: true},\n\t\t{version: \"1a\", constraint: \"1-a\", satisfied: true},\n\t\t{version: \"1a\", constraint: \"1.0-a\", satisfied: true},\n\t\t{version: \"1a\", constraint: \"1.0.0-a\", satisfied: true},\n\t\t{version: \"1.0a\", constraint: \"1-a\", satisfied: true},\n\t\t{version: \"1.0.0a\", constraint: \"1-a\", satisfied: true},\n\t\t{version: \"1x\", constraint: \"1-x\", satisfied: true},\n\t\t{version: \"1x\", constraint: \"1.0-x\", satisfied: true},\n\t\t{version: \"1x\", constraint: \"1.0.0-x\", satisfied: true},\n\t\t{version: \"1.0x\", constraint: \"1-x\", satisfied: true},\n\t\t{version: \"1.0.0x\", constraint: \"1-x\", satisfied: true},\n\t\t{version: \"1ga\", constraint: \"1\", satisfied: true},\n\t\t{version: \"1release\", constraint: \"1\", satisfied: true},\n\t\t{version: \"1final\", constraint: \"1\", satisfied: true},\n\t\t{version: \"1cr\", constraint: \"1rc\", satisfied: true},\n\t\t{version: \"1a1\", constraint: \"1-alpha-1\", satisfied: true},\n\t\t{version: \"1b2\", constraint: \"1-beta-2\", satisfied: true},\n\t\t{version: \"1m3\", constraint: \"1-milestone-3\", satisfied: true},\n\t\t{version: \"1X\", constraint: \"1x\", satisfied: true},\n\t\t{version: \"1A\", constraint: \"1a\", satisfied: true},\n\t\t{version: \"1B\", constraint: \"1b\", satisfied: true},\n\t\t{version: \"1M\", constraint: \"1m\", satisfied: true},\n\t\t{version: \"1Ga\", constraint: \"1\", satisfied: true},\n\t\t{version: \"1GA\", constraint: \"1\", satisfied: true},\n\t\t{version: \"1RELEASE\", constraint: \"1\", satisfied: true},\n\t\t{version: \"1release\", constraint: \"1\", satisfied: true},\n\t\t{version: \"1RELeaSE\", constraint: \"1\", satisfied: true},\n\t\t{version: \"1Final\", constraint: \"1\", satisfied: true},\n\t\t{version: \"1FinaL\", constraint: \"1\", satisfied: true},\n\t\t{version: \"1FINAL\", constraint: \"1\", satisfied: true},\n\t\t{version: \"1Cr\", constraint: \"1Rc\", satisfied: true},\n\t\t{version: \"1cR\", constraint: \"1rC\", satisfied: true},\n\t\t{version: \"1m3\", constraint: \"1Milestone3\", satisfied: true},\n\t\t{version: \"1m3\", constraint: \"1MileStone3\", satisfied: true},\n\t\t{version: \"1m3\", constraint: \"1MILESTONE3\", satisfied: true},\n\t\t{version: \"1\", constraint: \"01\", satisfied: true},\n\t\t{version: \"1\", constraint: \"001\", satisfied: true},\n\t\t{version: \"1.1\", constraint: \"1.01\", satisfied: true},\n\t\t{version: \"1.1\", constraint: \"1.001\", satisfied: true},\n\t\t{version: \"1-1\", constraint: \"1-01\", satisfied: true},\n\t\t{version: \"1-1\", constraint: \"1-001\", satisfied: true},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tconstraint, err := GetConstraint(test.constraint, MavenFormat)\n\n\t\t\tassert.NoError(t, err, \"unexpected error from newMavenConstraint %s: %v\", test.version, err)\n\t\t\ttest.assertVersionConstraint(t, MavenFormat, constraint)\n\n\t\t})\n\t}\n}\n\nfunc TestMavenVersion_Compare(t *testing.T) {\n\ttests := []struct {\n\t\tv1   string\n\t\tv2   string\n\t\twant int\n\t}{\n\t\t{\n\t\t\tv1:   \"1\",\n\t\t\tv2:   \"2\",\n\t\t\twant: -1,\n\t\t},\n\t\t{\n\t\t\tv1:   \"1.8.0_282\",\n\t\t\tv2:   \"1.8.0_282\",\n\t\t\twant: 0,\n\t\t},\n\t\t{\n\t\t\tv1:   \"2.5\",\n\t\t\tv2:   \"2.0\",\n\t\t\twant: 1,\n\t\t},\n\t\t{\n\t\t\tv1:   \"2.414.2-cb-5\",\n\t\t\tv2:   \"2.414.2\",\n\t\t\twant: 1,\n\t\t},\n\t\t{\n\t\t\tv1:   \"5.2.25.RELEASE\", // see https://mvnrepository.com/artifact/org.springframework/spring-web\n\t\t\tv2:   \"5.2.25\",\n\t\t\twant: 0,\n\t\t},\n\t\t{\n\t\t\tv1:   \"5.2.25.release\",\n\t\t\tv2:   \"5.2.25\",\n\t\t\twant: 0,\n\t\t},\n\t\t{\n\t\t\tv1:   \"5.2.25.FINAL\",\n\t\t\tv2:   \"5.2.25\",\n\t\t\twant: 0,\n\t\t},\n\t\t{\n\t\t\tv1:   \"5.2.25.final\",\n\t\t\tv2:   \"5.2.25\",\n\t\t\twant: 0,\n\t\t},\n\t\t{\n\t\t\tv1:   \"5.2.25.GA\",\n\t\t\tv2:   \"5.2.25\",\n\t\t\twant: 0,\n\t\t},\n\t\t{\n\t\t\tv1:   \"5.2.25.ga\",\n\t\t\tv2:   \"5.2.25\",\n\t\t\twant: 0,\n\t\t},\n\t\t// JRE/JDK qualifier tests (GitHub issue: JRE version matching)\n\t\t{\n\t\t\tv1:   \"12.10.2\",\n\t\t\tv2:   \"12.10.2.jre11\",\n\t\t\twant: 0,\n\t\t},\n\t\t{\n\t\t\tv1:   \"12.10.2.jre11\",\n\t\t\tv2:   \"12.10.2\",\n\t\t\twant: 0,\n\t\t},\n\t\t{\n\t\t\tv1:   \"12.10.2.jdk17\",\n\t\t\tv2:   \"12.10.2\",\n\t\t\twant: 0,\n\t\t},\n\t\t{\n\t\t\tv1:   \"12.10.2.jre11\",\n\t\t\tv2:   \"12.10.2.jdk17\",\n\t\t\twant: 0,\n\t\t},\n\t\t{\n\t\t\tv1:   \"12.10.1\",\n\t\t\tv2:   \"12.10.2.jre11\",\n\t\t\twant: -1,\n\t\t},\n\t\t{\n\t\t\tv1:   \"12.10.2.jre11\",\n\t\t\tv2:   \"12.10.1\",\n\t\t\twant: 1,\n\t\t},\n\t\t{\n\t\t\tv1:   \"1.2.3.jre8\",\n\t\t\tv2:   \"1.2.4.jre8\",\n\t\t\twant: -1,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.v1+\" vs \"+tt.v2, func(t *testing.T) {\n\t\t\tv1 := New(tt.v1, MavenFormat)\n\t\t\tv2 := New(tt.v2, MavenFormat)\n\n\t\t\tif got, _ := v1.Compare(v2); got != tt.want {\n\t\t\t\tt.Errorf(\"Compare() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestMavenVersion_Compare_Format(t *testing.T) {\n\ttests := []struct {\n\t\tname           string\n\t\tthisVersion    string\n\t\totherVersion   string\n\t\totherFormat    Format\n\t\texpectError    bool\n\t\terrorSubstring string\n\t}{\n\t\t{\n\t\t\tname:         \"same format successful comparison\",\n\t\t\tthisVersion:  \"1.2.3\",\n\t\t\totherVersion: \"1.2.4\",\n\t\t\totherFormat:  MavenFormat,\n\t\t\texpectError:  false,\n\t\t},\n\t\t{\n\t\t\tname:         \"same format successful comparison with qualifiers\",\n\t\t\tthisVersion:  \"1.2.3-SNAPSHOT\",\n\t\t\totherVersion: \"1.2.3-RELEASE\",\n\t\t\totherFormat:  MavenFormat,\n\t\t\texpectError:  false,\n\t\t},\n\t\t{\n\t\t\tname:         \"unknown format attempts upgrade - valid maven format\",\n\t\t\tthisVersion:  \"1.2.3\",\n\t\t\totherVersion: \"1.2.4\",\n\t\t\totherFormat:  UnknownFormat,\n\t\t\texpectError:  false,\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tthisVer := New(test.thisVersion, MavenFormat)\n\t\t\totherVer := New(test.otherVersion, test.otherFormat)\n\n\t\t\tresult, err := thisVer.Compare(otherVer)\n\n\t\t\tif test.expectError {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\tif test.errorSubstring != \"\" {\n\t\t\t\t\tassert.True(t, strings.Contains(err.Error(), test.errorSubstring),\n\t\t\t\t\t\t\"Expected error to contain '%s', got: %v\", test.errorSubstring, err)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.Contains(t, []int{-1, 0, 1}, result, \"Expected comparison result to be -1, 0, or 1\")\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestMavenVersion_Compare_EdgeCases(t *testing.T) {\n\ttests := []struct {\n\t\tname           string\n\t\tsetupFunc      func(testing.TB) (*Version, *Version)\n\t\texpectError    bool\n\t\terrorSubstring string\n\t}{\n\t\t{\n\t\t\tname: \"nil version object\",\n\t\t\tsetupFunc: func(t testing.TB) (*Version, *Version) {\n\t\t\t\tthisVer := New(\"1.2.3\", MavenFormat)\n\t\t\t\treturn thisVer, nil\n\t\t\t},\n\t\t\texpectError:    true,\n\t\t\terrorSubstring: \"no version provided for comparison\",\n\t\t},\n\t\t{\n\t\t\tname: \"incomparable maven versions\",\n\t\t\tsetupFunc: func(t testing.TB) (*Version, *Version) {\n\t\t\t\t// This test would be hard to construct in practice since the Maven\n\t\t\t\t// version library handles most comparisons, but we can simulate the\n\t\t\t\t// error condition by creating a mock that would trigger the last\n\t\t\t\t// error condition in the Compare function\n\t\t\t\tthisVer := New(\"1.2.3\", MavenFormat)\n\n\t\t\t\t// We'd need to modify the otherVer manually to create a scenario\n\t\t\t\t// where none of the comparison methods return true, which is unlikely\n\t\t\t\t// in real usage but could be simulated for test coverage\n\t\t\t\totherVer := New(\"1.2.4\", MavenFormat)\n\n\t\t\t\treturn thisVer, otherVer\n\t\t\t},\n\t\t\texpectError:    false, // Changed to false since we can't easily trigger the last error condition\n\t\t\terrorSubstring: \"could not compare java versions\",\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tthisVer, otherVer := test.setupFunc(t)\n\n\t\t\t_, err := thisVer.Compare(otherVer)\n\n\t\t\tif test.expectError {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\tif test.errorSubstring != \"\" {\n\t\t\t\t\tassert.True(t, strings.Contains(err.Error(), test.errorSubstring),\n\t\t\t\t\t\t\"Expected error to contain '%s', got: %v\", test.errorSubstring, err)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/version/operator.go",
    "content": "package version\n\nimport \"fmt\"\n\nconst (\n\tEQ  Operator = \"=\"\n\tGT  Operator = \">\"\n\tLT  Operator = \"<\"\n\tGTE Operator = \">=\"\n\tLTE Operator = \"<=\"\n)\n\ntype Operator string\n\nfunc parseOperator(op string) (Operator, error) {\n\tswitch op {\n\tcase string(EQ), \"\":\n\t\treturn EQ, nil\n\tcase string(GT):\n\t\treturn GT, nil\n\tcase string(GTE):\n\t\treturn GTE, nil\n\tcase string(LT):\n\t\treturn LT, nil\n\tcase string(LTE):\n\t\treturn LTE, nil\n\t}\n\treturn \"\", fmt.Errorf(\"unknown operator: '%s'\", op)\n}\n"
  },
  {
    "path": "grype/version/pacman_version.go",
    "content": "package version\n\nimport (\n\t\"fmt\"\n\t\"reflect\"\n\t\"strings\"\n\t\"unicode\"\n)\n\nvar _ Comparator = (*pacmanVersion)(nil)\n\ntype pacmanVersion struct {\n\tepoch   *int\n\tversion string\n\trelease string\n}\n\nfunc newPacmanVersion(raw string) (pacmanVersion, error) {\n\tepoch, remainingVersion, err := splitEpochFromVersion(raw)\n\tif err != nil {\n\t\treturn pacmanVersion{}, err\n\t}\n\n\tfields := strings.SplitN(remainingVersion, \"-\", 2)\n\tversion := fields[0]\n\n\tvar release string\n\tif len(fields) > 1 {\n\t\t// there is a release\n\t\trelease = fields[1]\n\t}\n\n\treturn pacmanVersion{\n\t\tepoch:   epoch,\n\t\tversion: version,\n\t\trelease: release,\n\t}, nil\n}\n\nfunc (v pacmanVersion) Compare(other *Version) (int, error) {\n\tif other == nil {\n\t\treturn -1, ErrNoVersionProvided\n\t}\n\n\to, err := newPacmanVersion(other.Raw)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\treturn v.compare(o), nil\n}\n\n// Compare returns 0 if v == v2, -1 if v < v2, and +1 if v > v2.\n// Pacman uses a similar scheme to RPM: epoch:version-release\n// If epochs are NOT present and explicit in both versions then they are ignored for the comparison.\nfunc (v pacmanVersion) compare(v2 pacmanVersion) int {\n\tif reflect.DeepEqual(v, v2) {\n\t\treturn 0\n\t}\n\n\t// Only compare epochs if both are present and explicit\n\tif epochIsPresent(v.epoch) && epochIsPresent(v2.epoch) {\n\t\tepochResult := compareEpochs(*v.epoch, *v2.epoch)\n\t\tif epochResult != 0 {\n\t\t\treturn epochResult\n\t\t}\n\t}\n\n\tret := comparePacmanVersions(v.version, v2.version)\n\tif ret != 0 {\n\t\treturn ret\n\t}\n\n\treturn comparePacmanVersions(v.release, v2.release)\n}\n\nfunc (v pacmanVersion) String() string {\n\tversion := \"\"\n\tif v.epoch != nil {\n\t\tversion += fmt.Sprintf(\"%d:\", *v.epoch)\n\t}\n\tversion += v.version\n\n\tif v.release != \"\" {\n\t\tversion += fmt.Sprintf(\"-%s\", v.release)\n\t}\n\treturn version\n}\n\n// comparePacmanVersions compares two version or release strings without the epoch.\n// Pacman version comparison is similar to RPM, comparing alphanumeric segments.\n// Source: https://wiki.archlinux.org/title/Pacman/Tips_and_tricks#Version_comparison\n// The scheme is based on RPM's algorithm.\n//\n// Note: dupl lint is suppressed because although pacman's vercmp is based on rpm's vercmp,\n// they are not identical and may diverge in the future. We intentionally keep them decoupled.\n//\n//nolint:funlen,gocognit,dupl\nfunc comparePacmanVersions(a, b string) int {\n\t// shortcut for equality\n\tif a == b {\n\t\treturn 0\n\t}\n\n\t// get alpha/numeric segments\n\tsegsa := alphanumPattern.FindAllString(a, -1)\n\tsegsb := alphanumPattern.FindAllString(b, -1)\n\tmaxSegs := max(len(segsa), len(segsb))\n\tminSegs := min(len(segsa), len(segsb))\n\n\t// compare each segment\n\tfor i := 0; i < minSegs; i++ {\n\t\ta := segsa[i]\n\t\tb := segsb[i]\n\n\t\t// compare tildes\n\t\tif []rune(a)[0] == '~' || []rune(b)[0] == '~' {\n\t\t\tif []rune(a)[0] != '~' {\n\t\t\t\treturn 1\n\t\t\t}\n\t\t\tif []rune(b)[0] != '~' {\n\t\t\t\treturn -1\n\t\t\t}\n\t\t}\n\n\t\tif unicode.IsNumber([]rune(a)[0]) {\n\t\t\t// numbers are always greater than alphas\n\t\t\tif !unicode.IsNumber([]rune(b)[0]) {\n\t\t\t\t// a is numeric, b is alpha\n\t\t\t\treturn 1\n\t\t\t}\n\n\t\t\t// trim leading zeros\n\t\t\ta = strings.TrimLeft(a, \"0\")\n\t\t\tb = strings.TrimLeft(b, \"0\")\n\n\t\t\t// longest string wins without further comparison\n\t\t\tif len(a) > len(b) {\n\t\t\t\treturn 1\n\t\t\t} else if len(b) > len(a) {\n\t\t\t\treturn -1\n\t\t\t}\n\t\t} else if unicode.IsNumber([]rune(b)[0]) {\n\t\t\t// a is alpha, b is numeric\n\t\t\treturn -1\n\t\t}\n\n\t\t// string compare\n\t\tif a < b {\n\t\t\treturn -1\n\t\t} else if a > b {\n\t\t\treturn 1\n\t\t}\n\t}\n\n\t// segments were all the same but separators must have been different\n\tif len(segsa) == len(segsb) {\n\t\treturn 0\n\t}\n\n\t// If there is a tilde in a segment past the min number of segments, find it.\n\tif len(segsa) > minSegs && []rune(segsa[minSegs])[0] == '~' {\n\t\treturn -1\n\t} else if len(segsb) > minSegs && []rune(segsb[minSegs])[0] == '~' {\n\t\treturn 1\n\t}\n\t// are the remaining segments 0s?\n\tsegaAll0s := true\n\tsegbAll0s := true\n\tfor i := minSegs; i < maxSegs; i++ {\n\t\tif i < len(segsa) && segsa[i] != \"0\" {\n\t\t\tsegaAll0s = false\n\t\t}\n\t\tif i < len(segsb) && segsb[i] != \"0\" {\n\t\t\tsegbAll0s = false\n\t\t}\n\t}\n\n\tif segaAll0s && segbAll0s {\n\t\treturn 0\n\t}\n\n\t// whoever has the most segments wins\n\tif len(segsa) > len(segsb) {\n\t\treturn 1\n\t}\n\treturn -1\n}\n"
  },
  {
    "path": "grype/version/pacman_version_test.go",
    "content": "package version\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestPacmanVersionCompare(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\tv1      string\n\t\tv2      string\n\t\twant    int\n\t\twantErr bool\n\t}{\n\t\t{\n\t\t\tname:    \"equal versions\",\n\t\t\tv1:      \"1.0.0\",\n\t\t\tv2:      \"1.0.0\",\n\t\t\twant:    0,\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"first greater\",\n\t\t\tv1:      \"1.0.1\",\n\t\t\tv2:      \"1.0.0\",\n\t\t\twant:    1,\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"second greater\",\n\t\t\tv1:      \"1.0.0\",\n\t\t\tv2:      \"1.0.1\",\n\t\t\twant:    -1,\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"with release numbers\",\n\t\t\tv1:      \"1.0.0-1\",\n\t\t\tv2:      \"1.0.0-2\",\n\t\t\twant:    -1,\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"with release numbers greater\",\n\t\t\tv1:      \"1.0.0-2\",\n\t\t\tv2:      \"1.0.0-1\",\n\t\t\twant:    1,\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"complex version\",\n\t\t\tv1:      \"5.6.0-1\",\n\t\t\tv2:      \"5.6.0-2\",\n\t\t\twant:    -1,\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"alpha vs release\",\n\t\t\tv1:      \"1.0.0alpha\",\n\t\t\tv2:      \"1.0.0\",\n\t\t\twant:    1,\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"with epoch\",\n\t\t\tv1:      \"1:1.0.0\",\n\t\t\tv2:      \"2:1.0.0\",\n\t\t\twant:    -1,\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"epoch takes precedence\",\n\t\t\tv1:      \"2:1.0.0\",\n\t\t\tv2:      \"1:2.0.0\",\n\t\t\twant:    1,\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"tilde version\",\n\t\t\tv1:      \"1.0.0~rc1\",\n\t\t\tv2:      \"1.0.0\",\n\t\t\twant:    -1,\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"leading zeros\",\n\t\t\tv1:      \"1.0.001\",\n\t\t\tv2:      \"1.0.1\",\n\t\t\twant:    0,\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"version with plus sign\",\n\t\t\tv1:      \"0.115+24+g5230646-1\",\n\t\t\tv2:      \"0.116-1\",\n\t\t\twant:    -1,\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"version with git hash suffix\",\n\t\t\tv1:      \"0.12.8+8+ga957a90b-1\",\n\t\t\tv2:      \"0.12.8+8+ga957a90b-2\",\n\t\t\twant:    -1,\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"real arch versions curl\",\n\t\t\tv1:      \"8.4.0-1\",\n\t\t\tv2:      \"8.5.0-1\",\n\t\t\twant:    -1,\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"real arch versions openssl with epoch\",\n\t\t\tv1:      \"1:3.0.7-4\",\n\t\t\tv2:      \"1:3.0.8-1\",\n\t\t\twant:    -1,\n\t\t\twantErr: false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tv1, err := newPacmanVersion(tt.v1)\n\t\t\tassert.NoError(t, err)\n\n\t\t\tv2 := New(tt.v2, PacmanFormat)\n\t\t\tresult, err := v1.Compare(v2)\n\n\t\t\tif tt.wantErr {\n\t\t\t\tassert.Error(t, err)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.Equal(t, tt.want, result)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestPacmanVersionString(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\traw  string\n\t\twant string\n\t}{\n\t\t{\n\t\t\tname: \"simple version\",\n\t\t\traw:  \"1.0.0\",\n\t\t\twant: \"1.0.0\",\n\t\t},\n\t\t{\n\t\t\tname: \"with release\",\n\t\t\traw:  \"1.0.0-1\",\n\t\t\twant: \"1.0.0-1\",\n\t\t},\n\t\t{\n\t\t\tname: \"with epoch\",\n\t\t\traw:  \"1:1.0.0\",\n\t\t\twant: \"1:1.0.0\",\n\t\t},\n\t\t{\n\t\t\tname: \"with epoch and release\",\n\t\t\traw:  \"1:1.0.0-1\",\n\t\t\twant: \"1:1.0.0-1\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tv, err := newPacmanVersion(tt.raw)\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, tt.want, v.String())\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/version/pep440_version.go",
    "content": "package version\n\nimport (\n\tgoPepVersion \"github.com/aquasecurity/go-pep440-version\"\n)\n\nvar _ Comparator = (*pep440Version)(nil)\n\ntype pep440Version struct {\n\t// public is the public portion of the version (without local segment), used for most comparisons\n\tpublic goPepVersion.Version\n\t// full is the complete parsed version including local segment, used when constraint has local\n\tfull goPepVersion.Version\n}\n\nfunc newPep440Version(raw string) (pep440Version, error) {\n\t// lets ensure this is a valid PEP 440 version\n\tparsed, err := goPepVersion.Parse(raw)\n\tif err != nil {\n\t\treturn pep440Version{}, invalidFormatError(SemanticFormat, raw, err)\n\t}\n\n\t// we want to use the \"public\" portion of the version for comparison purposes (for specifier matching, not local versions).\n\t// Note per PEP 440:\n\t//   <public version identifier>[+<local version label>]\n\t// see:\n\t// - https://peps.python.org/pep-0440/#public-version-identifiers\n\t// - https://peps.python.org/pep-0440/#local-version-identifiers\n\t//\n\t// This means that for a version like \"1.0.0+abc.1\", we only want to consider \"1.0.0\" for comparison purposes.\n\tpublic, err := goPepVersion.Parse(parsed.Public())\n\tif err != nil {\n\t\treturn pep440Version{}, invalidFormatError(SemanticFormat, raw, err)\n\t}\n\treturn pep440Version{\n\t\tpublic: public,\n\t\tfull:   parsed,\n\t}, nil\n}\n\nfunc (v pep440Version) Compare(other *Version) (int, error) {\n\tif other == nil {\n\t\treturn -1, ErrNoVersionProvided\n\t}\n\n\to, err := newPep440Version(other.Raw)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\tresult := v.public.Compare(o.public)\n\tif result != 0 {\n\t\treturn result, nil\n\t}\n\n\t// Public portions are equal - handle local version segments per PEP 440 specifier semantics.\n\t// If constraint has no local segment, ignore package's local (they're equal).\n\t// If constraint has a local segment, require exact match.\n\tif o.full.Local() == \"\" {\n\t\treturn 0, nil\n\t}\n\n\treturn v.full.Compare(o.full), nil\n}\n"
  },
  {
    "path": "grype/version/pep440_version_test.go",
    "content": "package version\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestPep440Version_Constraint(t *testing.T) {\n\ttests := []testCase{\n\t\t{\n\t\t\tname:       \"empty constraint\",\n\t\t\tversion:    \"2.3.1\",\n\t\t\tconstraint: \"\",\n\t\t\tsatisfied:  true,\n\t\t},\n\t\t{\n\t\t\tname:       \"version range within\",\n\t\t\tconstraint: \">1.0, <2.0\",\n\t\t\tversion:    \"1.2+beta-3\",\n\t\t\tsatisfied:  true,\n\t\t},\n\t\t{\n\t\t\tname:       \"version within compound range\",\n\t\t\tconstraint: \">1.0, <2.0 || > 3.0\",\n\t\t\tversion:    \"3.2+beta-3\",\n\t\t\tsatisfied:  true,\n\t\t},\n\t\t{\n\t\t\tname:       \"version within compound range (2)\",\n\t\t\tconstraint: \">1.0, <2.0 || > 3.0\",\n\t\t\tversion:    \"1.2+beta-3\",\n\t\t\tsatisfied:  true,\n\t\t},\n\t\t{\n\t\t\tname:       \"version not within compound range\",\n\t\t\tconstraint: \">1.0, <2.0 || > 3.0\",\n\t\t\tversion:    \"2.2+beta-3\",\n\t\t\tsatisfied:  false,\n\t\t},\n\t\t{\n\t\t\tname:       \"version range outside (right)\",\n\t\t\tconstraint: \">1.0, <2.0\",\n\t\t\tversion:    \"2.1-beta-3\",\n\t\t\tsatisfied:  false,\n\t\t},\n\t\t{\n\t\t\tname:       \"version range outside (left)\",\n\t\t\tconstraint: \">1.0, <2.0\",\n\t\t\tversion:    \"0.9-beta-2\",\n\t\t\tsatisfied:  false,\n\t\t},\n\t\t{\n\t\t\tname:       \"version range within (excluding left, prerelease)\",\n\t\t\tconstraint: \">=1.0, <2.0\",\n\t\t\tversion:    \"1.0-beta-3\",\n\t\t\tsatisfied:  false,\n\t\t},\n\t\t{\n\t\t\tname:       \"version range within (including left)\",\n\t\t\tconstraint: \">=1.1, <2.0\",\n\t\t\tversion:    \"1.1\",\n\t\t\tsatisfied:  true,\n\t\t},\n\t\t{\n\t\t\tname:       \"version range within (excluding right, 1)\",\n\t\t\tconstraint: \">1.0, <=2.0\",\n\t\t\tversion:    \"2.0-beta-3\",\n\t\t\tsatisfied:  true,\n\t\t},\n\t\t{\n\t\t\tname:       \"version range within (excluding right, 2)\",\n\t\t\tconstraint: \">1.0, <2.0\",\n\t\t\tversion:    \"2.0-beta-3\",\n\t\t\tsatisfied:  true,\n\t\t},\n\t\t{\n\t\t\tname:       \"version range within (including right)\",\n\t\t\tconstraint: \">1.0, <=2.0\",\n\t\t\tversion:    \"2.0\",\n\t\t\tsatisfied:  true,\n\t\t},\n\t\t{\n\t\t\tname:       \"version range within (including right, longer version [valid semver, bad fuzzy])\",\n\t\t\tconstraint: \">1.0, <=2.0\",\n\t\t\tversion:    \"2.0.0\",\n\t\t\tsatisfied:  true,\n\t\t},\n\t\t{\n\t\t\tname:       \"bad semver (eq)\",\n\t\t\tversion:    \"5a2\",\n\t\t\tconstraint: \"=5a2\",\n\t\t\tsatisfied:  true,\n\t\t},\n\t\t{\n\t\t\tname:       \"bad semver (gt)\",\n\t\t\tversion:    \"5a2\",\n\t\t\tconstraint: \">5a1\",\n\t\t\tsatisfied:  true,\n\t\t},\n\t\t{\n\t\t\tname:       \"bad semver (lt)\",\n\t\t\tversion:    \"5a2\",\n\t\t\tconstraint: \"<6a1\",\n\t\t\tsatisfied:  true,\n\t\t},\n\t\t{\n\t\t\tname:       \"bad semver (lte)\",\n\t\t\tversion:    \"5a2\",\n\t\t\tconstraint: \"<=5a2\",\n\t\t\tsatisfied:  true,\n\t\t},\n\t\t{\n\t\t\tname:       \"bad semver (gte)\",\n\t\t\tversion:    \"5a2\",\n\t\t\tconstraint: \">=5a2\",\n\t\t\tsatisfied:  true,\n\t\t},\n\t\t{\n\t\t\tname:       \"bad semver (lt boundary)\",\n\t\t\tversion:    \"5a2\",\n\t\t\tconstraint: \"<5a2\",\n\t\t\tsatisfied:  false,\n\t\t},\n\t\t// regression for https://github.com/anchore/go-version/pull/2\n\t\t{\n\t\t\tname:       \"indirect package match\",\n\t\t\tversion:    \"1.3.2-r0\",\n\t\t\tconstraint: \"<= 1.3.3-r0\",\n\t\t\tsatisfied:  true,\n\t\t},\n\t\t{\n\t\t\tname:       \"indirect package no match\",\n\t\t\tversion:    \"1.3.4-r0\",\n\t\t\tconstraint: \"<= 1.3.3-r0\",\n\t\t\tsatisfied:  false,\n\t\t},\n\t\t{\n\t\t\tname:       \"vulndb fuzzy constraint single quoted\",\n\t\t\tversion:    \"4.5.2\",\n\t\t\tconstraint: \"'4.5.1' || '4.5.2'\",\n\t\t\tsatisfied:  true,\n\t\t},\n\t\t{\n\t\t\tname:       \"vulndb fuzzy constraint double quoted\",\n\t\t\tversion:    \"4.5.2\",\n\t\t\tconstraint: \"\\\"4.5.1\\\" || \\\"4.5.2\\\"\",\n\t\t\tsatisfied:  true,\n\t\t},\n\t\t{\n\t\t\tname:       \"rc candidates with no '-' can match semver pattern\",\n\t\t\tversion:    \"1.20rc1\",\n\t\t\tconstraint: \" = 1.20.0-rc1\",\n\t\t\tsatisfied:  true,\n\t\t},\n\t\t{\n\t\t\tname:       \"candidates ahead of alpha\",\n\t\t\tversion:    \"3.11.0\",\n\t\t\tconstraint: \"> 3.11.0-alpha1\",\n\t\t\tsatisfied:  true,\n\t\t},\n\t\t{\n\t\t\tname:       \"candidates ahead of beta\",\n\t\t\tversion:    \"3.11.0\",\n\t\t\tconstraint: \"> 3.11.0-beta1\",\n\t\t\tsatisfied:  true,\n\t\t},\n\t\t{\n\t\t\tname:       \"candidates ahead of same alpha versions\",\n\t\t\tversion:    \"3.11.0-alpha5\",\n\t\t\tconstraint: \"> 3.11.0-alpha1\",\n\t\t\tsatisfied:  true,\n\t\t},\n\t\t{\n\t\t\tname:       \"candidates are placed correctly between alpha and release\",\n\t\t\tversion:    \"3.11.0-beta5\",\n\t\t\tconstraint: \"3.11.0 || = 3.11.0-alpha1\",\n\t\t\tsatisfied:  false,\n\t\t},\n\t\t{\n\t\t\tname:       \"candidates with pre suffix are sorted numerically\",\n\t\t\tversion:    \"1.0.2pre1\",\n\t\t\tconstraint: \" < 1.0.2pre2\",\n\t\t\tsatisfied:  true,\n\t\t},\n\t\t{\n\t\t\tname:       \"openssl pre2 is still considered less than release\",\n\t\t\tversion:    \"1.1.1-pre2\",\n\t\t\tconstraint: \"> 1.1.1-pre1, < 1.1.1\",\n\t\t\tsatisfied:  true,\n\t\t},\n\t\t{\n\t\t\tname:       \"major version releases are less than their subsequent patch releases with letter suffixes\",\n\t\t\tversion:    \"1.1.1\",\n\t\t\tconstraint: \"> 1.1.1-a\",\n\t\t\tsatisfied:  true,\n\t\t},\n\t\t{\n\t\t\tname:       \"date based pep440 version string boundary condition\",\n\t\t\tversion:    \"2022.12.7\",\n\t\t\tconstraint: \">=2017.11.05,<2022.12.07\",\n\t\t},\n\t\t{\n\t\t\tname:       \"certifi false positive is fixed\",\n\t\t\tversion:    \"2022.12.7\",\n\t\t\tconstraint: \">=2017.11.05,<2022.12.07\",\n\t\t},\n\t\t// regression (partial version with metadata should be valid)\n\t\t// this is a fun one! PEP 440 has two different use cases for ordering semantics: direct versions and version specifiers.\n\t\t// Take this python code for example:\n\t\t//\n\t\t// ```python\n\t\t// from packaging.version import Version\n\t\t// from packaging.specifiers import SpecifierSet\n\t\t//\n\t\t// # direct ordering comparison\n\t\t// Version('6.4+cgr.1') <= Version('6.4.0')  # False\n\t\t//\n\t\t// # specifier matching\n\t\t// Version('6.4+cgr.1') in SpecifierSet('<=6.4.0')  # True\n\t\t// ```\n\t\t//\n\t\t// The root cause of the regression is that we have been doing direct version comparisons instead of specifier matching.\n\t\t// The fix is to treat constraint matching as specifier matching (only consider the public version segment for\n\t\t// constraint matching, not the local version segment).\n\t\t//\n\t\t// We want specifier semantics (ignore local) since 6.4+cgr.1 should be considered \"the same release\" as\n\t\t// 6.4 for vulnerability matching applicability.\n\t\t{\n\t\t\tname:       \"partial version with metadata\",\n\t\t\tversion:    \"6.4+cgr.1\",\n\t\t\tconstraint: \"<=6.4.0\",\n\t\t\tsatisfied:  true,\n\t\t},\n\t\t// When constraint has a local version, require exact match (important for unaffected entries)\n\t\t{\n\t\t\tname:       \"local version in constraint should not match version without local segment\",\n\t\t\tversion:    \"2.0.0\",\n\t\t\tconstraint: \"= 2.0.0+cgr.1\",\n\t\t\tsatisfied:  false,\n\t\t},\n\t\t{\n\t\t\tname:       \"local version in constraint should match same local version\",\n\t\t\tversion:    \"2.0.0+cgr.1\",\n\t\t\tconstraint: \"= 2.0.0+cgr.1\",\n\t\t\tsatisfied:  true,\n\t\t},\n\t\t{\n\t\t\tname:       \"version with local segment should match constraint without local segment\",\n\t\t\tversion:    \"2.0.0+cgr.1\",\n\t\t\tconstraint: \"= 2.0.0\",\n\t\t\tsatisfied:  true,\n\t\t},\n\t\t{\n\t\t\tname:       \"version with local segment should satisfy less-than constraint\",\n\t\t\tversion:    \"2.0.0+cgr.1\",\n\t\t\tconstraint: \"< 2.0.1\",\n\t\t\tsatisfied:  true,\n\t\t},\n\t\t{\n\t\t\tname:       \"different local versions should not match on equality\",\n\t\t\tversion:    \"2.0.0+other\",\n\t\t\tconstraint: \"= 2.0.0+cgr.1\",\n\t\t\tsatisfied:  false,\n\t\t},\n\t\t// Local version segments compared per PEP 440 (numeric segments as integers)\n\t\t{\n\t\t\tname:       \"local version segments compared numerically not lexicographically\",\n\t\t\tversion:    \"2.0.0+cgr.12\",\n\t\t\tconstraint: \"> 2.0.0+cgr.2\",\n\t\t\tsatisfied:  true,\n\t\t},\n\t\t{\n\t\t\tname:       \"local version segment numeric comparison - less than\",\n\t\t\tversion:    \"2.0.0+cgr.2\",\n\t\t\tconstraint: \"< 2.0.0+cgr.12\",\n\t\t\tsatisfied:  true,\n\t\t},\n\t}\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tc, err := GetConstraint(tc.constraint, PythonFormat)\n\t\t\trequire.NoError(t, err)\n\t\t\tv := New(tc.version, PythonFormat)\n\t\t\tsat, err := c.Satisfied(v)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, tc.satisfied, sat)\n\t\t})\n\t}\n}\n\nfunc TestPep440Version_Compare(t *testing.T) {\n\ttests := []struct {\n\t\tname           string\n\t\tthisVersion    string\n\t\totherVersion   string\n\t\totherFormat    Format\n\t\texpectError    bool\n\t\terrorSubstring string\n\t}{\n\t\t{\n\t\t\tname:         \"same format successful comparison\",\n\t\t\tthisVersion:  \"1.2.3\",\n\t\t\totherVersion: \"1.2.4\",\n\t\t\totherFormat:  PythonFormat,\n\t\t\texpectError:  false,\n\t\t},\n\t\t{\n\t\t\tname:         \"same format successful comparison with pre-release\",\n\t\t\tthisVersion:  \"1.2.3a1\",\n\t\t\totherVersion: \"1.2.3b2\",\n\t\t\totherFormat:  PythonFormat,\n\t\t\texpectError:  false,\n\t\t},\n\t\t{\n\t\t\tname:         \"unknown format attempts upgrade - valid python format\",\n\t\t\tthisVersion:  \"1.2.3\",\n\t\t\totherVersion: \"1.2.4\",\n\t\t\totherFormat:  UnknownFormat,\n\t\t\texpectError:  false,\n\t\t},\n\t\t{\n\t\t\tname:           \"unknown format attempts upgrade - invalid python format\",\n\t\t\tthisVersion:    \"1.2.3\",\n\t\t\totherVersion:   \"not/valid/python-format\",\n\t\t\totherFormat:    UnknownFormat,\n\t\t\texpectError:    true,\n\t\t\terrorSubstring: \"invalid\",\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tthisVer, err := newPep440Version(test.thisVersion)\n\t\t\trequire.NoError(t, err)\n\n\t\t\totherVer := New(test.otherVersion, test.otherFormat)\n\n\t\t\tresult, err := thisVer.Compare(otherVer)\n\n\t\t\tif test.expectError {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\tif test.errorSubstring != \"\" {\n\t\t\t\t\tassert.True(t, strings.Contains(err.Error(), test.errorSubstring),\n\t\t\t\t\t\t\"Expected error to contain '%s', got: %v\", test.errorSubstring, err)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.Contains(t, []int{-1, 0, 1}, result, \"Expected comparison result to be -1, 0, or 1\")\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestPep440Version_Compare_EdgeCases(t *testing.T) {\n\ttests := []struct {\n\t\tname           string\n\t\tsetupFunc      func(testing.TB) (*Version, *Version)\n\t\texpectError    bool\n\t\terrorSubstring string\n\t}{\n\t\t{\n\t\t\tname: \"nil version object\",\n\t\t\tsetupFunc: func(t testing.TB) (*Version, *Version) {\n\t\t\t\tthisVer := New(\"1.2.3\", PythonFormat)\n\t\t\t\treturn thisVer, nil\n\t\t\t},\n\t\t\texpectError:    true,\n\t\t\terrorSubstring: \"no version provided for comparison\",\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tthisVer, otherVer := test.setupFunc(t)\n\n\t\t\t_, err := thisVer.Compare(otherVer)\n\n\t\t\trequire.Error(t, err)\n\t\t\tif test.errorSubstring != \"\" {\n\t\t\t\tassert.True(t, strings.Contains(err.Error(), test.errorSubstring),\n\t\t\t\t\t\"Expected error to contain '%s', got: %v\", test.errorSubstring, err)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/version/portage_version.go",
    "content": "package version\n\nimport (\n\t\"math/big\"\n\t\"regexp\"\n\t\"strings\"\n)\n\nvar _ Comparator = (*portageVersion)(nil)\n\n// for the original python implementation, see:\n// https://github.com/gentoo/portage/blob/master/lib/portage/versions.py\nvar (\n\tversionRegexp = regexp.MustCompile(`(\\d+)((\\.\\d+)*)([a-z]?)((_(pre|p|beta|alpha|rc)\\d*)*)(-r(\\d+))?`)\n\tsuffixRegexp  = regexp.MustCompile(`^(alpha|beta|rc|pre|p)(\\d*)$`)\n\tsuffixValue   = map[string]int{\"pre\": -2, \"p\": 0, \"alpha\": -4, \"beta\": -3, \"rc\": -1}\n)\n\ntype portageVersion struct {\n\tversion string\n}\n\nfunc newPortageVersion(raw string) portageVersion {\n\treturn portageVersion{\n\t\tversion: raw,\n\t}\n}\n\nfunc (v portageVersion) Compare(other *Version) (int, error) {\n\tif other == nil {\n\t\treturn -1, ErrNoVersionProvided\n\t}\n\n\treturn v.compare(newPortageVersion(other.Raw)), nil\n}\n\n// Compare returns 0 if v == v2, -1 if v < v2, and +1 if v > v2.\nfunc (v portageVersion) compare(v2 portageVersion) int {\n\tif v.version == v2.version {\n\t\treturn 0\n\t}\n\treturn comparePortageVersions(v.version, v2.version)\n}\n\n//nolint:funlen,gocognit\nfunc comparePortageVersions(a, b string) int {\n\tmatch1 := versionRegexp.FindStringSubmatch(a)\n\tmatch2 := versionRegexp.FindStringSubmatch(b)\n\tlist1 := []*big.Int{big.NewInt(0)}\n\tlist2 := []*big.Int{big.NewInt(0)}\n\tlist1[0].SetString(match1[1], 10)\n\tlist2[0].SetString(match2[1], 10)\n\tvlist1 := strings.Split(match1[2], \".\")[1:]\n\tvlist2 := strings.Split(match2[2], \".\")[1:]\n\tvlistMaxLen := len(vlist1)\n\tif len(vlist2) > vlistMaxLen {\n\t\tvlistMaxLen = len(vlist2)\n\t}\n\n\tfor index := 0; index < vlistMaxLen; index++ {\n\t\tswitch {\n\t\tcase len(vlist1) <= index:\n\t\t\tlist1 = append(list1, big.NewInt(-1))\n\t\t\ti := big.NewInt(0)\n\t\t\ti.SetString(vlist2[index], 10)\n\t\t\tlist2 = append(list2, i)\n\t\tcase len(vlist2) <= index:\n\t\t\tlist2 = append(list2, big.NewInt(-1))\n\t\t\ti := big.NewInt(0)\n\t\t\ti.SetString(vlist1[index], 10)\n\t\t\tlist1 = append(list1, i)\n\t\tcase !strings.HasPrefix(vlist1[index], \"0\") && !strings.HasPrefix(vlist2[index], \"0\"):\n\t\t\ti := big.NewInt(0)\n\t\t\ti.SetString(vlist1[index], 10)\n\t\t\tlist1 = append(list1, i)\n\t\t\tj := big.NewInt(0)\n\t\t\tj.SetString(vlist2[index], 10)\n\t\t\tlist2 = append(list2, j)\n\t\tdefault:\n\t\t\tmaxLen := len(vlist1[index])\n\t\t\tif len(vlist2[index]) > maxLen {\n\t\t\t\tmaxLen = len(vlist2[index])\n\t\t\t}\n\t\t\tif len(vlist1[index]) < maxLen {\n\t\t\t\tvlist1[index] += strings.Repeat(\"0\", maxLen-len(vlist1[index]))\n\t\t\t}\n\t\t\tif len(vlist2[index]) < maxLen {\n\t\t\t\tvlist2[index] += strings.Repeat(\"0\", maxLen-len(vlist2[index]))\n\t\t\t}\n\t\t\ti := big.NewInt(0)\n\t\t\ti.SetString(vlist1[index], 10)\n\t\t\tlist1 = append(list1, i)\n\t\t\tj := big.NewInt(0)\n\t\t\tj.SetString(vlist2[index], 10)\n\t\t\tlist2 = append(list2, j)\n\t\t}\n\t}\n\n\tif len(match1[4]) != 0 {\n\t\tr := []rune(match1[4])\n\t\ti := big.NewInt(int64(r[0]))\n\t\tlist1 = append(list1, i)\n\t}\n\n\tif len(match2[4]) != 0 {\n\t\tr := []rune(match2[4])\n\t\ti := big.NewInt(int64(r[0]))\n\t\tlist2 = append(list2, i)\n\t}\n\n\tmaxLen := len(list1)\n\tif len(list2) > maxLen {\n\t\tmaxLen = len(list2)\n\t}\n\tfor index := 0; index < maxLen; index++ {\n\t\tif len(list1) <= index {\n\t\t\treturn -1\n\t\t}\n\t\tif len(list2) <= index {\n\t\t\treturn 1\n\t\t}\n\t\tc := list1[index].Cmp(list2[index])\n\t\tif c != 0 {\n\t\t\treturn c\n\t\t}\n\t}\n\n\tslist1 := strings.Split(match1[5], \"_\")[1:]\n\tslist2 := strings.Split(match2[5], \"_\")[1:]\n\tmaxLen = len(slist1)\n\tif len(slist2) > maxLen {\n\t\tmaxLen = len(slist2)\n\t}\n\tfor index := 0; index < maxLen; index++ {\n\t\ts1 := []string{\"p\", \"-1\"}\n\t\ts2 := []string{\"p\", \"-1\"}\n\t\tif len(slist1) > index {\n\t\t\ts1 = suffixRegexp.FindStringSubmatch(slist1[index])[1:]\n\t\t\tif s1[1] == \"\" {\n\t\t\t\ts1[1] = \"0\"\n\t\t\t}\n\t\t}\n\t\tif len(slist2) > index {\n\t\t\ts2 = suffixRegexp.FindStringSubmatch(slist2[index])[1:]\n\t\t\tif s2[1] == \"\" {\n\t\t\t\ts2[1] = \"0\"\n\t\t\t}\n\t\t}\n\t\tif s1[0] != s2[0] {\n\t\t\tv1 := suffixValue[s1[0]]\n\t\t\tv2 := suffixValue[s2[0]]\n\t\t\tif v1 > v2 {\n\t\t\t\treturn 1\n\t\t\t}\n\n\t\t\treturn -1\n\t\t}\n\t\tif s1[1] != s2[1] {\n\t\t\ti := big.NewInt(0)\n\t\t\ti.SetString(s1[1], 10)\n\t\t\tj := big.NewInt(0)\n\t\t\tj.SetString(s2[1], 10)\n\t\t\tc := i.Cmp(j)\n\t\t\tif c != 0 {\n\t\t\t\treturn c\n\t\t\t}\n\t\t}\n\t}\n\n\tr1 := big.NewInt(0)\n\tif match1[9] != \"\" {\n\t\tr1.SetString(match1[9], 10)\n\t}\n\tr2 := big.NewInt(0)\n\tif match2[9] != \"\" {\n\t\tr2.SetString(match2[9], 10)\n\t}\n\n\treturn r1.Cmp(r2)\n}\n"
  },
  {
    "path": "grype/version/portage_version_test.go",
    "content": "package version\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestPortageVersion_Constraint(t *testing.T) {\n\ttests := []testCase{\n\t\t// empty constraint is always satisfied\n\t\t{version: \"1.2.3\", constraint: \"\", satisfied: true},\n\t\t{version: \"1.2.3-r1\", constraint: \"\", satisfied: true},\n\t\t{version: \"1.2.3_alpha1\", constraint: \"\", satisfied: true},\n\n\t\t// simple equality\n\t\t{version: \"1.2.3\", constraint: \"= 1.2.3\", satisfied: true},\n\t\t{version: \"1.2.3-r1\", constraint: \"= 1.2.3-r1\", satisfied: true},\n\t\t{version: \"1.2.3\", constraint: \"= 1.2.4\", satisfied: false},\n\n\t\t// less than\n\t\t{version: \"1.2.3\", constraint: \"< 1.2.4\", satisfied: true},\n\t\t{version: \"1.2.3\", constraint: \"< 1.2.3\", satisfied: false},\n\t\t{version: \"1.2.3\", constraint: \"< 1.2.2\", satisfied: false},\n\t\t{version: \"1.2.3-r1\", constraint: \"< 1.2.3-r2\", satisfied: true},\n\t\t{version: \"1.2.3-r2\", constraint: \"< 1.2.3-r1\", satisfied: false},\n\n\t\t// less than or equal\n\t\t{version: \"1.2.3\", constraint: \"<= 1.2.3\", satisfied: true},\n\t\t{version: \"1.2.3\", constraint: \"<= 1.2.4\", satisfied: true},\n\t\t{version: \"1.2.3\", constraint: \"<= 1.2.2\", satisfied: false},\n\t\t{version: \"1.2.3-r1\", constraint: \"<= 1.2.3-r1\", satisfied: true},\n\n\t\t// greater than\n\t\t{version: \"1.2.4\", constraint: \"> 1.2.3\", satisfied: true},\n\t\t{version: \"1.2.3\", constraint: \"> 1.2.3\", satisfied: false},\n\t\t{version: \"1.2.2\", constraint: \"> 1.2.3\", satisfied: false},\n\t\t{version: \"1.2.3-r2\", constraint: \"> 1.2.3-r1\", satisfied: true},\n\t\t{version: \"1.2.3-r1\", constraint: \"> 1.2.3-r2\", satisfied: false},\n\n\t\t// greater than or equal\n\t\t{version: \"1.2.3\", constraint: \">= 1.2.3\", satisfied: true},\n\t\t{version: \"1.2.4\", constraint: \">= 1.2.3\", satisfied: true},\n\t\t{version: \"1.2.2\", constraint: \">= 1.2.3\", satisfied: false},\n\t\t{version: \"1.2.3-r1\", constraint: \">= 1.2.3-r1\", satisfied: true},\n\n\t\t// compound conditions with AND (comma)\n\t\t{version: \"1.5.0\", constraint: \"> 1.0.0, < 2.0.0\", satisfied: true},\n\t\t{version: \"0.5.0\", constraint: \"> 1.0.0, < 2.0.0\", satisfied: false},\n\t\t{version: \"2.5.0\", constraint: \"> 1.0.0, < 2.0.0\", satisfied: false},\n\t\t{version: \"1.2.3-r5\", constraint: \">= 1.2.3-r1, <= 1.2.3-r10\", satisfied: true},\n\n\t\t// compound conditions with OR\n\t\t{version: \"0.5.0\", constraint: \"< 1.0.0 || > 2.0.0\", satisfied: true},\n\t\t{version: \"3.0.0\", constraint: \"< 1.0.0 || > 2.0.0\", satisfied: true},\n\t\t{version: \"1.5.0\", constraint: \"< 1.0.0 || > 2.0.0\", satisfied: false},\n\n\t\t// complex compound conditions\n\t\t{version: \"1.5.0\", constraint: \"> 0.1.0, < 0.5.0 || > 1.0.0, < 2.0.0\", satisfied: true},\n\t\t{version: \"0.3.0\", constraint: \"> 0.1.0, < 0.5.0 || > 1.0.0, < 2.0.0\", satisfied: true},\n\t\t{version: \"0.8.0\", constraint: \"> 0.1.0, < 0.5.0 || > 1.0.0, < 2.0.0\", satisfied: false},\n\n\t\t// portage-specific version features\n\t\t// letter suffixes (a, b, c etc.)\n\t\t{version: \"1.2a\", constraint: \"< 1.2b\", satisfied: true},\n\t\t{version: \"1.2b\", constraint: \"< 1.2a\", satisfied: false},\n\t\t{version: \"12.2.5\", constraint: \"> 12.2b\", satisfied: true},\n\n\t\t// revision numbers (-r suffix)\n\t\t{version: \"1.0.0-r1\", constraint: \"> 1.0.0\", satisfied: true},\n\t\t{version: \"1.0.0\", constraint: \"> 1.0.0-r1\", satisfied: false},\n\t\t{version: \"1.2.3-r2\", constraint: \"> 1.2.3-r1\", satisfied: true},\n\t\t{version: \"1.2.3-r1\", constraint: \"< 1.2.3-r2\", satisfied: true},\n\n\t\t// version suffixes (alpha, beta, pre, rc, p)\n\t\t{version: \"1.0.0_alpha1\", constraint: \"< 1.0.0_beta1\", satisfied: true},\n\t\t{version: \"1.0.0_beta1\", constraint: \"< 1.0.0_rc1\", satisfied: true},\n\t\t{version: \"1.0.0_rc1\", constraint: \"< 1.0.0\", satisfied: true},\n\t\t{version: \"1.0.0\", constraint: \"< 1.0.0_p1\", satisfied: true},\n\t\t{version: \"1.0.0_pre1\", constraint: \"> 1.0.0_alpha1\", satisfied: true},\n\n\t\t// patch level suffixes\n\t\t{version: \"1_p1\", constraint: \"> 1_p0\", satisfied: true},\n\t\t{version: \"1_p0\", constraint: \"> 1\", satisfied: true},\n\n\t\t// decimal versions with leading zeros\n\t\t{version: \"1.01\", constraint: \"< 1.1\", satisfied: true},\n\t\t{version: \"1.1\", constraint: \"> 1.01\", satisfied: true},\n\n\t\t// version with missing patch components\n\t\t{version: \"12.2\", constraint: \"< 12.2.0\", satisfied: true}, // 12.2 < 12.2.0 is true in portage 🤯\n\t\t{version: \"12.2.0\", constraint: \"> 12.2\", satisfied: true},\n\n\t\t// edge cases - versions that should not match\n\t\t{version: \"1.2.3\", constraint: \"= 1.2.4\", satisfied: false},\n\t\t{version: \"1.2.3\", constraint: \"> 1.2.3\", satisfied: false},\n\t\t{version: \"1.2.3\", constraint: \"< 1.2.3\", satisfied: false},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.tName(), func(t *testing.T) {\n\t\t\tconstraint, err := GetConstraint(test.constraint, PortageFormat)\n\t\t\tassert.NoError(t, err)\n\n\t\t\ttest.assertVersionConstraint(t, PortageFormat, constraint)\n\t\t})\n\t}\n}\n\nfunc TestPortageConstraint_Constraint_NilVersion(t *testing.T) {\n\ttests := []struct {\n\t\tname        string\n\t\tconstraint  string\n\t\texpected    bool\n\t\tshouldError bool\n\t}{\n\t\t{\n\t\t\tname:        \"empty constraint with nil version\",\n\t\t\tconstraint:  \"\",\n\t\t\texpected:    true,\n\t\t\tshouldError: false,\n\t\t},\n\t\t{\n\t\t\tname:        \"non-empty constraint with nil version\",\n\t\t\tconstraint:  \"> 1.0.0\",\n\t\t\texpected:    false,\n\t\t\tshouldError: false,\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tc, err := GetConstraint(test.constraint, PortageFormat)\n\t\t\tassert.NoError(t, err)\n\n\t\t\tsatisfied, err := c.Satisfied(nil)\n\t\t\tif test.shouldError {\n\t\t\t\trequire.Error(t, err)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.Equal(t, test.expected, satisfied)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestPortageVersion_Constraint_UnsupportedFormat(t *testing.T) {\n\tc, err := GetConstraint(\"> 1.0.0\", PortageFormat)\n\tassert.NoError(t, err)\n\n\t// test with a semantic version (wrong format)\n\tversion := New(\"1.2.3\", SemanticFormat)\n\n\tsatisfied, err := c.Satisfied(version)\n\trequire.Error(t, err)\n\tassert.False(t, satisfied)\n\tassert.Contains(t, err.Error(), \"unsupported version comparison\")\n}\n\nfunc TestPortageConstraint_String(t *testing.T) {\n\ttests := []struct {\n\t\tname       string\n\t\tconstraint string\n\t\texpected   string\n\t}{\n\t\t{\n\t\t\tname:       \"empty constraint\",\n\t\t\tconstraint: \"\",\n\t\t\texpected:   \"none (portage)\",\n\t\t},\n\t\t{\n\t\t\tname:       \"simple constraint\",\n\t\t\tconstraint: \"> 1.0.0\",\n\t\t\texpected:   \"> 1.0.0 (portage)\",\n\t\t},\n\t\t{\n\t\t\tname:       \"complex constraint\",\n\t\t\tconstraint: \"> 1.0.0, < 2.0.0\",\n\t\t\texpected:   \"> 1.0.0, < 2.0.0 (portage)\",\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tconstraint, err := GetConstraint(test.constraint, PortageFormat)\n\t\t\tassert.NoError(t, err)\n\n\t\t\tresult := constraint.String()\n\t\t\tassert.Equal(t, test.expected, result)\n\t\t})\n\t}\n}\n\nfunc TestPortageVersion_Compare(t *testing.T) {\n\ttests := []struct {\n\t\tv1     string\n\t\tv2     string\n\t\tresult int\n\t}{\n\t\t{\"1\", \"1\", 0},\n\t\t{\"12.2.5\", \"12.2b\", 1},\n\t\t{\"12.2a\", \"12.2b\", -1},\n\t\t{\"12.2\", \"12.2.0\", -1},\n\t\t{\"1.01\", \"1.1\", -1},\n\t\t{\"1_p1\", \"1_p0\", 1},\n\t\t{\"1_p0\", \"1\", 1},\n\t\t{\"1-r1\", \"1\", 1},\n\t\t{\"1.2.3-r2\", \"1.2.3-r1\", 1},\n\t\t{\"1.2.3-r1\", \"1.2.3-r2\", -1},\n\t}\n\n\tfor _, test := range tests {\n\t\tname := test.v1 + \"_vs_\" + test.v2\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tv1 := New(test.v1, PortageFormat)\n\t\t\tv2 := New(test.v2, PortageFormat)\n\n\t\t\tactual, err := v1.Compare(v2)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, test.result, actual, \"expected comparison result to match\")\n\t\t})\n\t}\n}\n\nfunc TestPortageVersion_Compare_Format(t *testing.T) {\n\ttests := []struct {\n\t\tname           string\n\t\tthisVersion    string\n\t\totherVersion   string\n\t\totherFormat    Format\n\t\texpectError    bool\n\t\terrorSubstring string\n\t}{\n\t\t{\n\t\t\tname:         \"same format successful comparison\",\n\t\t\tthisVersion:  \"1.2.3\",\n\t\t\totherVersion: \"1.2.4\",\n\t\t\totherFormat:  PortageFormat,\n\t\t\texpectError:  false,\n\t\t},\n\t\t{\n\t\t\tname:         \"same format successful comparison with suffixes\",\n\t\t\tthisVersion:  \"1.2.3-r1\",\n\t\t\totherVersion: \"1.2.3-r2\",\n\t\t\totherFormat:  PortageFormat,\n\t\t\texpectError:  false,\n\t\t},\n\t\t{\n\t\t\tname:         \"unknown format attempts upgrade - valid portage format\",\n\t\t\tthisVersion:  \"1.2.3\",\n\t\t\totherVersion: \"1.2.4\",\n\t\t\totherFormat:  UnknownFormat,\n\t\t\texpectError:  false,\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tthisVer := New(test.thisVersion, PortageFormat)\n\t\t\totherVer := New(test.otherVersion, test.otherFormat)\n\n\t\t\tresult, err := thisVer.Compare(otherVer)\n\n\t\t\tif test.expectError {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\tif test.errorSubstring != \"\" {\n\t\t\t\t\tassert.True(t, strings.Contains(err.Error(), test.errorSubstring),\n\t\t\t\t\t\t\"Expected error to contain '%s', got: %v\", test.errorSubstring, err)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.Contains(t, []int{-1, 0, 1}, result, \"Expected comparison result to be -1, 0, or 1\")\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestPortageVersion_Compare_EdgeCases(t *testing.T) {\n\ttests := []struct {\n\t\tname           string\n\t\tsetupFunc      func(testing.TB) (*Version, *Version)\n\t\texpectError    bool\n\t\terrorSubstring string\n\t}{\n\t\t{\n\t\t\tname: \"nil version object\",\n\t\t\tsetupFunc: func(t testing.TB) (*Version, *Version) {\n\t\t\t\tthisVer := New(\"1.2.3\", PortageFormat)\n\t\t\t\treturn thisVer, nil\n\t\t\t},\n\t\t\texpectError:    true,\n\t\t\terrorSubstring: \"no version provided for comparison\",\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tthisVer, otherVer := test.setupFunc(t)\n\n\t\t\t_, err := thisVer.Compare(otherVer)\n\n\t\t\trequire.Error(t, err)\n\t\t\tif test.errorSubstring != \"\" {\n\t\t\t\tassert.True(t, strings.Contains(err.Error(), test.errorSubstring),\n\t\t\t\t\t\"Expected error to contain '%s', got: %v\", test.errorSubstring, err)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/version/range.go",
    "content": "package version\n\nimport (\n\t\"fmt\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/anchore/grype/internal/stringutil\"\n)\n\n// Operator group only matches on range operators (GT, LT, GTE, LTE, E)\n// version group matches on everything except for whitespace and operators (range or boolean)\nvar constraintPartPattern = regexp.MustCompile(`\\s*(?P<prefix>[^><=a-zA-Z0-9().'\"]*)(?P<operator>[><=]*)\\s*(?P<version>.+)`)\n\ntype rangeUnit struct {\n\tOperator Operator\n\tVersion  string\n}\n\nfunc parseRange(phrase string) (*rangeUnit, error) {\n\tmatch := stringutil.MatchCaptureGroups(constraintPartPattern, phrase)\n\tversion, exists := match[\"version\"]\n\tif !exists {\n\t\treturn nil, nil\n\t}\n\n\topStr := match[\"operator\"]\n\n\tprefix := match[\"prefix\"]\n\n\tif prefix != \"\" && opStr == \"\" {\n\t\treturn nil, fmt.Errorf(\"constraint has an unprocessable prefix %q\", prefix)\n\t}\n\n\tversion = strings.Trim(version, \" \")\n\n\tif err := validateVersion(version); err != nil {\n\t\treturn nil, err\n\t}\n\n\t// version may have quotes, attempt to unquote it (ignore errors)\n\tunquoted, err := trimQuotes(version)\n\tif err == nil {\n\t\tversion = unquoted\n\t}\n\n\top, err := parseOperator(opStr)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to parse constraint operator=%q: %+v\", opStr, err)\n\t}\n\treturn &rangeUnit{\n\t\tOperator: op,\n\t\tVersion:  version,\n\t}, nil\n}\n\n// trimQuotes will attempt to remove double quotes.\n// If removing double quotes is unsuccessful, it will attempt to remove single quotes.\n// If neither operation is successful, it will return an error.\nfunc trimQuotes(s string) (string, error) {\n\tunquoted, err := strconv.Unquote(s)\n\tswitch {\n\tcase err == nil:\n\t\treturn unquoted, nil\n\tcase strings.HasPrefix(s, \"'\") && strings.HasSuffix(s, \"'\"):\n\t\treturn strings.Trim(s, \"'\"), nil\n\tdefault:\n\t\treturn s, fmt.Errorf(\"string %s is not single or double quoted\", s)\n\t}\n}\n\nfunc (c *rangeUnit) Satisfied(comparison int) bool {\n\tswitch c.Operator {\n\tcase EQ:\n\t\treturn comparison == 0\n\tcase GT:\n\t\treturn comparison > 0\n\tcase GTE:\n\t\treturn comparison >= 0\n\tcase LT:\n\t\treturn comparison < 0\n\tcase LTE:\n\t\treturn comparison <= 0\n\tdefault:\n\t\tpanic(fmt.Errorf(\"unknown operator: %s\", c.Operator))\n\t}\n}\n\n// validateVersion scans the version string and validates characters outside of quotes.\n// invalid characters within quotes are allowed, but unbalanced quotes are not allowed.\nfunc validateVersion(version string) error {\n\tvar inQuotes bool\n\tvar quoteChar rune\n\n\tfor _, r := range version {\n\t\tswitch {\n\t\tcase !inQuotes && (r == '\"' || r == '\\''):\n\t\t\t// start of quoted section\n\t\t\tinQuotes = true\n\t\t\tquoteChar = r\n\t\tcase inQuotes && r == quoteChar:\n\t\t\t// end of quoted section\n\t\t\tinQuotes = false\n\t\t\tquoteChar = 0\n\t\tcase !inQuotes && strings.ContainsRune(\"><=\", r):\n\t\t\t// invalid character outside of quotes\n\t\t\treturn fmt.Errorf(\"version %q potentially is a version constraint expression (should not contain '><=' outside of quotes)\", version)\n\t\t}\n\t}\n\n\tif inQuotes {\n\t\treturn fmt.Errorf(\"version %q has unbalanced quotes\", version)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "grype/version/range_expression.go",
    "content": "package version\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"strings\"\n\t\"text/scanner\"\n)\n\ntype simpleRangeExpression struct {\n\tUnits [][]rangeUnit // only supports or'ing a group of and'ed groups\n}\n\nfunc parseRangeExpression(phrase string) (simpleRangeExpression, error) {\n\torParts, err := scanExpression(phrase)\n\tif err != nil {\n\t\treturn simpleRangeExpression{}, fmt.Errorf(\"unable to create constraint expression from=%q : %w\", phrase, err)\n\t}\n\n\torUnits := make([][]rangeUnit, len(orParts))\n\tvar fuzzyErr error\n\tfor orIdx, andParts := range orParts {\n\t\tandUnits := make([]rangeUnit, len(andParts))\n\t\tfor andIdx, part := range andParts {\n\t\t\tunit, err := parseRange(part)\n\t\t\tif err != nil {\n\t\t\t\treturn simpleRangeExpression{}, err\n\t\t\t}\n\t\t\tif unit == nil {\n\t\t\t\treturn simpleRangeExpression{}, fmt.Errorf(\"unable to parse unit: %q\", part)\n\t\t\t}\n\t\t\tandUnits[andIdx] = *unit\n\t\t}\n\n\t\torUnits[orIdx] = andUnits\n\t}\n\n\treturn simpleRangeExpression{\n\t\tUnits: orUnits,\n\t}, fuzzyErr\n}\n\nfunc (c *simpleRangeExpression) satisfied(format Format, version *Version) (bool, error) {\n\t// Use the version's embedded config if present, otherwise use empty config\n\tcfg := ComparisonConfig{}\n\tif version != nil {\n\t\tcfg = version.Config\n\t}\n\treturn c.satisfiedWithConfig(format, version, cfg)\n}\n\nfunc (c *simpleRangeExpression) satisfiedWithConfig(format Format, version *Version, cfg ComparisonConfig) (bool, error) {\n\toneSatisfied := false\n\tfor i, andOperand := range c.Units {\n\t\tallSatisfied := true\n\t\tfor j, andUnit := range andOperand {\n\t\t\tconstraintVersion := &Version{\n\t\t\t\tFormat: format,\n\t\t\t\tRaw:    andUnit.Version,\n\t\t\t}\n\n\t\t\tvar result int\n\t\t\tvar err error\n\n\t\t\t// Use config-aware comparison for RPM and Deb formats when config is provided\n\t\t\tif cfg.MissingEpochStrategy != \"\" && (format == RpmFormat || format == DebFormat) {\n\t\t\t\tresult, err = compareWithConfig(version, constraintVersion, cfg)\n\t\t\t} else {\n\t\t\t\tresult, err = version.Compare(constraintVersion)\n\t\t\t}\n\n\t\t\tif err != nil {\n\t\t\t\treturn false, fmt.Errorf(\"uncomparable %T vs %q: %w\", andUnit, version.String(), err)\n\t\t\t}\n\t\t\tunit := c.Units[i][j]\n\n\t\t\tif !unit.Satisfied(result) {\n\t\t\t\tallSatisfied = false\n\t\t\t}\n\t\t}\n\n\t\toneSatisfied = oneSatisfied || allSatisfied\n\t}\n\treturn oneSatisfied, nil\n}\n\n// compareWithConfig performs a version comparison using the provided configuration.\n// This function extracts the comparator and calls CompareWithConfig if the comparator\n// supports it (RPM and Deb versions).\nfunc compareWithConfig(version *Version, constraintVersion *Version, cfg ComparisonConfig) (int, error) {\n\tcomparator, err := version.getComparator(version.Format)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\t// Check if the comparator supports config-aware comparison\n\tswitch v := comparator.(type) {\n\tcase rpmVersion:\n\t\treturn v.CompareWithConfig(constraintVersion, cfg)\n\tcase debVersion:\n\t\treturn v.CompareWithConfig(constraintVersion, cfg)\n\tdefault:\n\t\t// Fall back to regular comparison for other formats\n\t\treturn comparator.Compare(constraintVersion)\n\t}\n}\n\nfunc scanExpression(phrase string) ([][]string, error) {\n\tvar scnr scanner.Scanner\n\tvar orGroups [][]string // all versions a group of and'd groups or'd together\n\tvar andGroup []string   // most current group of and'd versions\n\tvar buf bytes.Buffer    // most current single version value\n\tvar lastToken string\n\n\tcaptureVersionOperatorPair := func() {\n\t\tif buf.Len() > 0 {\n\t\t\tver := buf.String()\n\t\t\tandGroup = append(andGroup, ver)\n\t\t\tbuf.Reset()\n\t\t}\n\t}\n\n\tcaptureAndGroup := func() {\n\t\tif len(andGroup) > 0 {\n\t\t\torGroups = append(orGroups, andGroup)\n\t\t\tandGroup = nil\n\t\t}\n\t}\n\n\tscnr.Init(strings.NewReader(phrase))\n\n\tscnr.Error = func(*scanner.Scanner, string) {\n\t\t// scanner has the ability to invoke a callback upon tokenization errors. By default, if no handler is provided\n\t\t// then errors are printed to stdout. This handler is provided to suppress this output.\n\n\t\t// Suppressing these errors is not a problem in this case since the scanExpression function should see all tokens\n\t\t// and accumulate them as part of a version value if it is not a token of interest. The text/scanner splits on\n\t\t// a pre-configured set of \"common\" tokens (which we cannot provide). We are only interested in a sub-set of\n\t\t// these tokens, thus allow for input that would seemingly be invalid for this common set of tokens.\n\t\t// For example, the scanner finding `3.e` would interpret this as a float with no valid exponent. However,\n\t\t// this function accumulates all tokens into the version component (and versions are not guaranteed to have\n\t\t// valid tokens).\n\t}\n\n\ttokenRune := scnr.Scan()\n\tfor tokenRune != scanner.EOF {\n\t\tcurrentToken := scnr.TokenText()\n\t\tswitch {\n\t\tcase currentToken == \",\":\n\t\t\tcaptureVersionOperatorPair()\n\t\tcase currentToken == \"|\" && lastToken == \"|\":\n\t\t\tcaptureVersionOperatorPair()\n\t\t\tcaptureAndGroup()\n\t\tcase currentToken == \"(\" || currentToken == \")\":\n\t\t\treturn nil, fmt.Errorf(\"parenthetical expressions are not supported yet\")\n\t\tcase currentToken != \"|\":\n\t\t\tbuf.Write([]byte(currentToken))\n\t\t}\n\t\tlastToken = currentToken\n\t\ttokenRune = scnr.Scan()\n\t}\n\tcaptureVersionOperatorPair()\n\tcaptureAndGroup()\n\n\treturn orGroups, nil\n}\n"
  },
  {
    "path": "grype/version/range_expression_test.go",
    "content": "package version\n\nimport (\n\t\"testing\"\n\n\t\"github.com/go-test/deep\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestScanExpression(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tphrase   string\n\t\texpected [][]string\n\t\twantErr  require.ErrorAssertionFunc\n\t}{\n\t\t{\n\t\t\tname:   \"simple AND and OR expression\",\n\t\t\tphrase: \"x,y||z\",\n\t\t\texpected: [][]string{\n\t\t\t\t{\n\t\t\t\t\t\"x\",\n\t\t\t\t\t\"y\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"z\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:   \"complex version constraints with operators\",\n\t\t\tphrase: \"<1.0, >=2.0|| 3.0 || =4.0\",\n\t\t\texpected: [][]string{\n\t\t\t\t{\n\t\t\t\t\t\"<1.0\",\n\t\t\t\t\t\">=2.0\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"3.0\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"=4.0\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:    \"parenthetical expression not supported\",\n\t\t\tphrase:  \"(<1.0, >=2.0|| 3.0) || =4.0\",\n\t\t\twantErr: require.Error,\n\t\t},\n\t\t{\n\t\t\tname:   \"whitespace handling\",\n\t\t\tphrase: ` > 1.0,  <=   2.0,,,    || = 3.0 `,\n\t\t\texpected: [][]string{\n\t\t\t\t{\n\t\t\t\t\t\">1.0\",\n\t\t\t\t\t\"<=2.0\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"=3.0\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:   \"quoted version with special characters\",\n\t\t\tphrase: ` > 1.0,  <= \"  (2.0||),,, \",   || = 3.0 `,\n\t\t\texpected: [][]string{\n\t\t\t\t{\n\t\t\t\t\t\">1.0\",\n\t\t\t\t\t`<=\"  (2.0||),,, \"`,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"=3.0\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:     \"empty string\",\n\t\t\tphrase:   \"\",\n\t\t\texpected: nil,\n\t\t},\n\t\t{\n\t\t\tname:   \"single version\",\n\t\t\tphrase: \"1.0\",\n\t\t\texpected: [][]string{\n\t\t\t\t{\n\t\t\t\t\t\"1.0\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:   \"only AND operators\",\n\t\t\tphrase: \">=1.0, <2.0, !=1.5\",\n\t\t\texpected: [][]string{\n\t\t\t\t{\n\t\t\t\t\t\">=1.0\",\n\t\t\t\t\t\"<2.0\",\n\t\t\t\t\t\"!=1.5\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:   \"only OR operators\",\n\t\t\tphrase: \"1.0 || 2.0 || 3.0\",\n\t\t\texpected: [][]string{\n\t\t\t\t{\n\t\t\t\t\t\"1.0\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"2.0\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"3.0\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:   \"single pipe character should be treated as version\",\n\t\t\tphrase: \"1.0|2.0\",\n\t\t\texpected: [][]string{\n\t\t\t\t{\n\t\t\t\t\t\"1.02.0\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:   \"multiple consecutive commas\",\n\t\t\tphrase: \"1.0,,,2.0\",\n\t\t\texpected: [][]string{\n\t\t\t\t{\n\t\t\t\t\t\"1.0\",\n\t\t\t\t\t\"2.0\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:   \"trailing comma\",\n\t\t\tphrase: \"1.0,2.0,\",\n\t\t\texpected: [][]string{\n\t\t\t\t{\n\t\t\t\t\t\"1.0\",\n\t\t\t\t\t\"2.0\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:   \"leading comma\",\n\t\t\tphrase: \",1.0,2.0\",\n\t\t\texpected: [][]string{\n\t\t\t\t{\n\t\t\t\t\t\"1.0\",\n\t\t\t\t\t\"2.0\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:   \"complex version numbers\",\n\t\t\tphrase: \"1.0.0-alpha+build.1,2.0.0-beta.2||3.0.0-rc.1\",\n\t\t\texpected: [][]string{\n\t\t\t\t{\n\t\t\t\t\t\"1.0.0-alpha+build.1\",\n\t\t\t\t\t\"2.0.0-beta.2\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"3.0.0-rc.1\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:    \"parentheses at start\",\n\t\t\tphrase:  \"(1.0\",\n\t\t\twantErr: require.Error,\n\t\t},\n\t\t{\n\t\t\tname:    \"parentheses at end\",\n\t\t\tphrase:  \"1.0)\",\n\t\t\twantErr: require.Error,\n\t\t},\n\t\t{\n\t\t\tname:   \"special characters in version\",\n\t\t\tphrase: \"1.0.0+build.123-abc_def\",\n\t\t\texpected: [][]string{\n\t\t\t\t{\n\t\t\t\t\t\"1.0.0+build.123-abc_def\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif tt.wantErr == nil {\n\t\t\t\ttt.wantErr = require.NoError\n\t\t\t}\n\n\t\t\tactual, err := scanExpression(tt.phrase)\n\t\t\ttt.wantErr(t, err)\n\n\t\t\tif err != nil {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tfor _, d := range deep.Equal(tt.expected, actual) {\n\t\t\t\tt.Errorf(\"difference: %+v\", d)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/version/range_test.go",
    "content": "package version\n\nimport (\n\t\"reflect\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestParseRangeUnit(t *testing.T) {\n\ttests := []struct {\n\t\tphrase    string\n\t\texpected  *rangeUnit\n\t\twantError require.ErrorAssertionFunc\n\t}{\n\t\t{\n\t\t\tphrase: \"\",\n\t\t},\n\t\t{\n\t\t\tphrase: `=\"in<(b e t w e e n)>quotes<=||>=not!=\"`,\n\t\t\texpected: &rangeUnit{\n\t\t\t\tOperator: EQ,\n\t\t\t\tVersion:  \"in<(b e t w e e n)>quotes<=||>=not!=\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tphrase: ` >= \"in<(b e t w e e n)>quotes<=||>=not!=\" `,\n\t\t\texpected: &rangeUnit{\n\t\t\t\tOperator: GTE,\n\t\t\t\tVersion:  \"in<(b e t w e e n)>quotes<=||>=not!=\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t// to cover a version that has quotes within it, but not necessarily surrounding the entire version\n\t\t\tphrase: ` >= inbet\"ween)>quotes\" with trailing words `,\n\t\t\texpected: &rangeUnit{\n\t\t\t\tOperator: GTE,\n\t\t\t\tVersion:  `inbet\"ween)>quotes\" with trailing words`,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tphrase:    `=\"unbalandedquotes`,\n\t\t\twantError: require.Error,\n\t\t},\n\t\t{\n\t\t\tphrase: `=\"something\"`,\n\t\t\texpected: &rangeUnit{\n\t\t\t\tOperator: EQ,\n\t\t\t\tVersion:  \"something\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tphrase: \"=something\",\n\t\t\texpected: &rangeUnit{\n\t\t\t\tOperator: EQ,\n\t\t\t\tVersion:  \"something\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tphrase: \"= something\",\n\t\t\texpected: &rangeUnit{\n\t\t\t\tOperator: EQ,\n\t\t\t\tVersion:  \"something\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tphrase: \"something\",\n\t\t\texpected: &rangeUnit{\n\n\t\t\t\tOperator: EQ,\n\t\t\t\tVersion:  \"something\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tphrase: \"> something\",\n\t\t\texpected: &rangeUnit{\n\n\t\t\t\tOperator: GT,\n\t\t\t\tVersion:  \"something\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tphrase: \">= 2.3\",\n\t\t\texpected: &rangeUnit{\n\n\t\t\t\tOperator: GTE,\n\t\t\t\tVersion:  \"2.3\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tphrase: \"< 2.3\",\n\t\t\texpected: &rangeUnit{\n\n\t\t\t\tOperator: LT,\n\t\t\t\tVersion:  \"2.3\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tphrase: \"<=2.3\",\n\t\t\texpected: &rangeUnit{\n\n\t\t\t\tOperator: LTE,\n\t\t\t\tVersion:  \"2.3\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tphrase: \"  >=   1.0 \",\n\t\t\texpected: &rangeUnit{\n\n\t\t\t\tOperator: GTE,\n\t\t\t\tVersion:  \"1.0\",\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.phrase, func(t *testing.T) {\n\t\t\tif test.wantError == nil {\n\t\t\t\ttest.wantError = require.NoError\n\t\t\t}\n\t\t\tactual, err := parseRange(test.phrase)\n\t\t\ttest.wantError(t, err)\n\t\t\tif err != nil {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif !reflect.DeepEqual(test.expected, actual) {\n\t\t\t\tt.Errorf(\"expected: '%+v' got: '%+v'\", test.expected, actual)\n\t\t\t}\n\n\t\t})\n\t}\n}\n\nfunc TestTrimQuotes(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    string\n\t\texpected string\n\t\terr      bool\n\t}{\n\t\t{\n\t\t\tname:     \"no quotes\",\n\t\t\tinput:    \"test\",\n\t\t\texpected: \"test\",\n\t\t\terr:      true,\n\t\t},\n\t\t{\n\t\t\tname:     \"double quotes\",\n\t\t\tinput:    \"\\\"test\\\"\",\n\t\t\texpected: \"test\",\n\t\t\terr:      false,\n\t\t},\n\t\t{\n\t\t\tname:     \"single quotes\",\n\t\t\tinput:    \"'test'\",\n\t\t\texpected: \"test\",\n\t\t\terr:      false,\n\t\t},\n\t\t{\n\t\t\tname:     \"leading_single_quote\",\n\t\t\tinput:    \"'test\",\n\t\t\texpected: \"'test\",\n\t\t\terr:      true,\n\t\t},\n\t\t{\n\t\t\tname:     \"trailing_single_quote\",\n\t\t\tinput:    \"test'\",\n\t\t\texpected: \"test'\",\n\t\t\terr:      true,\n\t\t},\n\t\t{\n\t\t\tname:     \"leading_double_quote\",\n\t\t\tinput:    \"'test\",\n\t\t\texpected: \"'test\",\n\t\t\terr:      true,\n\t\t},\n\t\t{\n\t\t\tname:     \"trailing_double_quote\",\n\t\t\tinput:    \"test'\",\n\t\t\texpected: \"test'\",\n\t\t\terr:      true,\n\t\t},\n\t\t{\n\t\t\t// this raises an error, but I do not believe that this is a scenario that we need to account for, so should be ok.\n\t\t\tname:     \"nested double/double quotes\",\n\t\t\tinput:    \"\\\"t\\\"es\\\"t\\\"\",\n\t\t\texpected: \"\\\"t\\\"es\\\"t\\\"\",\n\t\t\terr:      true,\n\t\t},\n\t\t{\n\t\t\tname:     \"nested single/single quotes\",\n\t\t\tinput:    \"'t'es't'\",\n\t\t\texpected: \"t'es't\",\n\t\t\terr:      false,\n\t\t},\n\t\t{\n\t\t\tname:     \"nested single/double quotes\",\n\t\t\tinput:    \"'t\\\"es\\\"t'\",\n\t\t\texpected: \"t\\\"es\\\"t\",\n\t\t\terr:      false,\n\t\t},\n\t\t{\n\t\t\tname:     \"nested double/single quotes\",\n\t\t\tinput:    \"\\\"t'es't\\\"\",\n\t\t\texpected: \"t'es't\",\n\t\t\terr:      false,\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tactual, err := trimQuotes(test.input)\n\t\t\tif test.err {\n\t\t\t\tassert.NotNil(t, err, \"expected an error but did not get one\")\n\t\t\t} else {\n\t\t\t\tassert.Nil(t, err, \"expected no error, got \\\"%+v\\\"\", err)\n\t\t\t}\n\t\t\tassert.Equal(t, actual, test.expected, \"output does not match expected: exp:%v got:%v\", test.expected, actual)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/version/rpm_version.go",
    "content": "package version\n\nimport (\n\t\"fmt\"\n\t\"reflect\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n\t\"unicode\"\n)\n\nvar _ Comparator = (*rpmVersion)(nil)\n\ntype rpmVersion struct {\n\tepoch   *int\n\tversion string\n\trelease string\n}\n\nfunc newRpmVersion(raw string) (rpmVersion, error) {\n\tepoch, remainingVersion, err := splitEpochFromVersion(raw)\n\tif err != nil {\n\t\treturn rpmVersion{}, err\n\t}\n\n\tfields := strings.SplitN(remainingVersion, \"-\", 2)\n\tversion := fields[0]\n\n\tvar release string\n\tif len(fields) > 1 {\n\t\t// there is a release\n\t\trelease = fields[1]\n\t}\n\n\treturn rpmVersion{\n\t\tepoch:   epoch,\n\t\tversion: version,\n\t\trelease: release,\n\t}, nil\n}\n\nfunc (v rpmVersion) Compare(other *Version) (int, error) {\n\tif other == nil {\n\t\treturn -1, ErrNoVersionProvided\n\t}\n\n\to, err := newRpmVersion(other.Raw)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\treturn v.compare(o), nil\n}\n\n// CompareWithConfig compares two RPM versions using the provided comparison\n// configuration. The config controls behavior for missing epochs:\n//   - \"zero\" strategy: missing epochs are treated as 0\n//   - \"auto\" strategy: missing epochs in the package version match the constraint's epoch\n//\n// Returns:\n//\n//\t-1 if v < other\n//\t 0 if v == other\n//\t 1 if v > other\n//\n// Only the package version's (v) missing epoch is handled by the auto strategy. If the\n// constraint (other) is missing an epoch, it is always treated as 0 per RPM specification.\nfunc (v rpmVersion) CompareWithConfig(other *Version, cfg ComparisonConfig) (int, error) {\n\tif other == nil {\n\t\treturn -1, ErrNoVersionProvided\n\t}\n\n\to, err := newRpmVersion(other.Raw)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\treturn v.compareWithConfig(o, cfg), nil\n}\n\nfunc (v rpmVersion) compareWithConfig(v2 rpmVersion, cfg ComparisonConfig) int {\n\tif reflect.DeepEqual(v, v2) {\n\t\treturn 0\n\t}\n\n\t// Handle epoch comparison based on strategy\n\tswitch cfg.MissingEpochStrategy {\n\tcase MissingEpochStrategyAuto:\n\t\t// If v (package) is missing epoch but v2 (constraint) has one, temporarily use v2's epoch for v\n\t\tif !epochIsPresent(v.epoch) && epochIsPresent(v2.epoch) {\n\t\t\tvWithEpoch := v\n\t\t\tvWithEpoch.epoch = v2.epoch\n\t\t\treturn vWithEpoch.compare(v2)\n\t\t}\n\tcase MissingEpochStrategyZero:\n\t\t// If v (package) is missing epoch, treat it as 0\n\t\t// This differs from the default compare() behavior which ignores one-sided epochs\n\t\tif !epochIsPresent(v.epoch) && epochIsPresent(v2.epoch) {\n\t\t\tvWithEpoch := v\n\t\t\tzero := 0\n\t\t\tvWithEpoch.epoch = &zero\n\t\t\treturn vWithEpoch.compare(v2)\n\t\t}\n\t}\n\n\treturn v.compare(v2)\n}\n\n// Compare returns 0 if v == v2, -1 if v < v2, and +1 if v > v2.\n// This a pragmatic adaptation of comparison for the messy data\n// encountered in vuln scanning. If epochs are NOT present and explicit\n// (e.g. >= 0) in both versions then they are ignored for the comparison.\n// For a rpm spec-compliant comparison, see strictCompare() instead\nfunc (v rpmVersion) compare(v2 rpmVersion) int {\n\tif reflect.DeepEqual(v, v2) {\n\t\treturn 0\n\t}\n\n\t// Only compare epochs if both are present and explicit. This is technically\n\t// against what RedHat says to do with missing epoch (which is to assume a 0 epoch).\n\t// However, since we may be dealing with upstream data sources where there is an epoch\n\t// for a package but the value was stripped, the best we can do is to compare only the\n\t// version values without the epoch values.\n\tif epochIsPresent(v.epoch) && epochIsPresent(v2.epoch) {\n\t\tepochResult := compareEpochs(*v.epoch, *v2.epoch)\n\t\tif epochResult != 0 {\n\t\t\treturn epochResult\n\t\t}\n\t}\n\n\tret := compareRpmVersions(v.version, v2.version)\n\tif ret != 0 {\n\t\treturn ret\n\t}\n\n\treturn compareRpmVersions(v.release, v2.release)\n}\n\nfunc epochIsPresent(epoch *int) bool {\n\treturn epoch != nil\n}\n\n// Epoch comparison, standard int comparison for sorting\nfunc compareEpochs(e1 int, e2 int) int {\n\tswitch {\n\tcase e1 > e2:\n\t\treturn 1\n\tcase e1 < e2:\n\t\treturn -1\n\tdefault:\n\t\treturn 0\n\t}\n}\n\nfunc (v rpmVersion) String() string {\n\tversion := \"\"\n\tif v.epoch != nil {\n\t\tversion += fmt.Sprintf(\"%d:\", *v.epoch)\n\t}\n\tversion += v.version\n\n\tif v.release != \"\" {\n\t\tversion += fmt.Sprintf(\"-%s\", v.release)\n\t}\n\treturn version\n}\n\nfunc splitEpochFromVersion(rawVersion string) (*int, string, error) {\n\tfields := strings.SplitN(rawVersion, \":\", 2)\n\n\t// When the epoch is not included, should be considered to be 0 during\n\t// comparisons (see https://github.com/rpm-software-management/rpm/issues/450).\n\t// But, often the inclusion of the epoch in vuln databases or source RPM\n\t// filenames is not consistent so, represent a missing epoch as nil. This allows\n\t// the comparison logic itself to determine if it should use a zero or another\n\t// value which supports more flexible comparison options because the version\n\t// creation is not lossy\n\n\tif len(fields) == 1 {\n\t\treturn nil, rawVersion, nil\n\t}\n\n\t// there is an epoch\n\tepochStr := strings.TrimLeft(fields[0], \" \")\n\n\tepoch, err := strconv.Atoi(epochStr)\n\tif err != nil {\n\t\treturn nil, \"\", fmt.Errorf(\"unable to parse epoch (%s): %w\", epochStr, err)\n\t}\n\n\treturn &epoch, fields[1], nil\n}\n\n// compareRpmVersions compares two version or release strings without the epoch.\n// Source: https://github.com/cavaliercoder/go-rpm/blob/master/version.go\n//\n// For the original C implementation, see:\n// https://github.com/rpm-software-management/rpm/blob/master/lib/rpmvercmp.c#L16\nvar alphanumPattern = regexp.MustCompile(\"([a-zA-Z]+)|([0-9]+)|(~)\")\n\n//nolint:funlen,gocognit,dupl // see comparePacmanVersions for why we keep these decoupled\nfunc compareRpmVersions(a, b string) int {\n\t// shortcut for equality\n\tif a == b {\n\t\treturn 0\n\t}\n\n\t// get alpha/numeric segments\n\tsegsa := alphanumPattern.FindAllString(a, -1)\n\tsegsb := alphanumPattern.FindAllString(b, -1)\n\tmaxSegs := max(len(segsa), len(segsb))\n\tminSegs := min(len(segsa), len(segsb))\n\n\t// compare each segment\n\tfor i := 0; i < minSegs; i++ {\n\t\ta := segsa[i]\n\t\tb := segsb[i]\n\n\t\t// compare tildes\n\t\tif []rune(a)[0] == '~' || []rune(b)[0] == '~' {\n\t\t\tif []rune(a)[0] != '~' {\n\t\t\t\treturn 1\n\t\t\t}\n\t\t\tif []rune(b)[0] != '~' {\n\t\t\t\treturn -1\n\t\t\t}\n\t\t}\n\n\t\tif unicode.IsNumber([]rune(a)[0]) {\n\t\t\t// numbers are always greater than alphas\n\t\t\tif !unicode.IsNumber([]rune(b)[0]) {\n\t\t\t\t// a is numeric, b is alpha\n\t\t\t\treturn 1\n\t\t\t}\n\n\t\t\t// trim leading zeros\n\t\t\ta = strings.TrimLeft(a, \"0\")\n\t\t\tb = strings.TrimLeft(b, \"0\")\n\n\t\t\t// longest string wins without further comparison\n\t\t\tif len(a) > len(b) {\n\t\t\t\treturn 1\n\t\t\t} else if len(b) > len(a) {\n\t\t\t\treturn -1\n\t\t\t}\n\t\t} else if unicode.IsNumber([]rune(b)[0]) {\n\t\t\t// a is alpha, b is numeric\n\t\t\treturn -1\n\t\t}\n\n\t\t// string compare\n\t\tif a < b {\n\t\t\treturn -1\n\t\t} else if a > b {\n\t\t\treturn 1\n\t\t}\n\t}\n\n\t// segments were all the same but separators must have been different\n\tif len(segsa) == len(segsb) {\n\t\treturn 0\n\t}\n\n\t// If there is a tilde in a segment past the min number of segments, find it.\n\tif len(segsa) > minSegs && []rune(segsa[minSegs])[0] == '~' {\n\t\treturn -1\n\t} else if len(segsb) > minSegs && []rune(segsb[minSegs])[0] == '~' {\n\t\treturn 1\n\t}\n\t// are the remaining segments 0s?\n\tsegaAll0s := true\n\tsegbAll0s := true\n\tfor i := minSegs; i < maxSegs; i++ {\n\t\tif i < len(segsa) && segsa[i] != \"0\" {\n\t\t\tsegaAll0s = false\n\t\t}\n\t\tif i < len(segsb) && segsb[i] != \"0\" {\n\t\t\tsegbAll0s = false\n\t\t}\n\t}\n\n\tif segaAll0s && segbAll0s {\n\t\treturn 0\n\t}\n\n\t// whoever has the most segments wins\n\tif len(segsa) > len(segsb) {\n\t\treturn 1\n\t}\n\treturn -1\n}\n"
  },
  {
    "path": "grype/version/rpm_version_test.go",
    "content": "package version\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestRpmVersion_Constraint(t *testing.T) {\n\ttests := []testCase{\n\t\t// empty values\n\t\t{version: \"2.3.1\", constraint: \"\", satisfied: true},\n\t\t// trivial compound conditions\n\t\t{version: \"2.3.1\", constraint: \"> 1.0.0, < 2.0.0\", satisfied: false},\n\t\t{version: \"1.3.1\", constraint: \"> 1.0.0, < 2.0.0\", satisfied: true},\n\t\t{version: \"2.0.0\", constraint: \"> 1.0.0, <= 2.0.0\", satisfied: true},\n\t\t{version: \"2.0.0\", constraint: \"> 1.0.0, < 2.0.0\", satisfied: false},\n\t\t{version: \"1.0.0\", constraint: \">= 1.0.0, < 2.0.0\", satisfied: true},\n\t\t{version: \"1.0.0\", constraint: \"> 1.0.0, < 2.0.0\", satisfied: false},\n\t\t{version: \"0.9.0\", constraint: \"> 1.0.0, < 2.0.0\", satisfied: false},\n\t\t{version: \"1.5.0\", constraint: \"> 0.1.0, < 0.5.0 || > 1.0.0, < 2.0.0\", satisfied: true},\n\t\t{version: \"0.2.0\", constraint: \"> 0.1.0, < 0.5.0 || > 1.0.0, < 2.0.0\", satisfied: true},\n\t\t{version: \"0.0.1\", constraint: \"> 0.1.0, < 0.5.0 || > 1.0.0, < 2.0.0\", satisfied: false},\n\t\t{version: \"0.6.0\", constraint: \"> 0.1.0, < 0.5.0 || > 1.0.0, < 2.0.0\", satisfied: false},\n\t\t{version: \"2.5.0\", constraint: \"> 0.1.0, < 0.5.0 || > 1.0.0, < 2.0.0\", satisfied: false},\n\t\t// trivial scenarios\n\t\t{version: \"2.3.1\", constraint: \"< 2.0.0\", satisfied: false},\n\t\t{version: \"2.3.1\", constraint: \"< 2.0\", satisfied: false},\n\t\t{version: \"2.3.1\", constraint: \"< 2\", satisfied: false},\n\t\t{version: \"2.3.1\", constraint: \"< 2.3\", satisfied: false},\n\t\t{version: \"2.3.1\", constraint: \"< 2.3.1\", satisfied: false},\n\t\t{version: \"2.3.1\", constraint: \"< 2.3.2\", satisfied: true},\n\t\t{version: \"2.3.1\", constraint: \"< 2.4\", satisfied: true},\n\t\t{version: \"2.3.1\", constraint: \"< 3\", satisfied: true},\n\t\t{version: \"2.3.1\", constraint: \"< 3.0\", satisfied: true},\n\t\t{version: \"2.3.1\", constraint: \"< 3.0.0\", satisfied: true},\n\t\t// epoch\n\t\t{version: \"1:0\", constraint: \"< 0:1\", satisfied: false},\n\t\t{version: \"2:4.19.01-1.el7_5\", constraint: \"< 2:4.19.1-1.el7_5\", satisfied: false},\n\t\t{version: \"2:4.19.01-1.el7_5\", constraint: \"<= 2:4.19.1-1.el7_5\", satisfied: true},\n\t\t{version: \"0:4.19.1-1.el7_5\", constraint: \"< 2:4.19.1-1.el7_5\", satisfied: true},\n\t\t{version: \"11:4.19.0-1.el7_5\", constraint: \"< 12:4.19.0-1.el7\", satisfied: true},\n\t\t{version: \"13:4.19.0-1.el7_5\", constraint: \"< 12:4.19.0-1.el7\", satisfied: false},\n\t\t// regression: https://github.com/anchore/grype/issues/316\n\t\t{version: \"1.5.4-2.el7_9\", constraint: \"< 0:1.5.4-2.el7_9\", satisfied: false},\n\t\t{version: \"1.5.4-2.el7\", constraint: \"< 0:1.5.4-2.el7_9\", satisfied: true},\n\t\t// Non-standard epoch handling. In comparisons with epoch on only one side, they are both ignored\n\t\t{version: \"1:0\", constraint: \"< 1\", satisfied: true},\n\t\t{version: \"0:0\", constraint: \"< 0\", satisfied: false},\n\t\t{version: \"0:0\", constraint: \"= 0\", satisfied: true},\n\t\t{version: \"0\", constraint: \"= 0:0\", satisfied: true},\n\t\t{version: \"1.0\", constraint: \"< 2:1.0\", satisfied: false},\n\t\t{version: \"1.0\", constraint: \"<= 2:1.0\", satisfied: true},\n\t\t{version: \"1:2\", constraint: \"< 1\", satisfied: false},\n\t\t{version: \"1:2\", constraint: \"> 1\", satisfied: true},\n\t\t{version: \"2:4.19.01-1.el7_5\", constraint: \"< 4.19.1-1.el7_5\", satisfied: false},\n\t\t{version: \"2:4.19.01-1.el7_5\", constraint: \"<= 4.19.1-1.el7_5\", satisfied: true},\n\t\t{version: \"4.19.01-1.el7_5\", constraint: \"< 2:4.19.1-1.el7_5\", satisfied: false},\n\t\t{version: \"4.19.0-1.el7_5\", constraint: \"< 12:4.19.0-1.el7\", satisfied: false},\n\t\t{version: \"4.19.0-1.el7_5\", constraint: \"<= 12:4.19.0-1.el7\", satisfied: false},\n\t\t{version: \"3:4.19.0-1.el7_5\", constraint: \"< 4.21.0-1.el7\", satisfied: true},\n\t\t{version: \"4:1.2.3-3-el7_5\", constraint: \"< 1.2.3-el7_5~snapshot1\", satisfied: false},\n\t\t// regression https://github.com/anchore/grype/issues/398\n\t\t{version: \"8.3.1-5.el8.4\", constraint: \"< 0:8.3.1-5.el8.5\", satisfied: true},\n\t\t{version: \"8.3.1-5.el8.40\", constraint: \"< 0:8.3.1-5.el8.5\", satisfied: false},\n\t\t{version: \"8.3.1-5.el8\", constraint: \"< 0:8.3.1-5.el8.0.0\", satisfied: false},\n\t\t{version: \"8.3.1-5.el8\", constraint: \"<= 0:8.3.1-5.el8.0.0\", satisfied: true},\n\t\t{version: \"8.3.1-5.el8.0.0\", constraint: \"> 0:8.3.1-5.el8\", satisfied: false},\n\t\t{version: \"8.3.1-5.el8.0.0\", constraint: \">= 0:8.3.1-5.el8\", satisfied: true},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.tName(), func(t *testing.T) {\n\t\t\tconstraint, err := GetConstraint(test.constraint, RpmFormat)\n\t\t\tassert.NoError(t, err, \"unexpected error from newRpmConstraint: %v\", err)\n\n\t\t\ttest.assertVersionConstraint(t, RpmFormat, constraint)\n\t\t})\n\t}\n}\n\nfunc TestRpmVersion_Compare(t *testing.T) {\n\ttests := []struct {\n\t\tv1     string\n\t\tv2     string\n\t\tresult int\n\t}{\n\t\t// from https://github.com/anchore/anchore-engine/blob/a447ee951c2d4e17c2672553d7280cfdb5e5f193/tests/unit/anchore_engine/util/test_rpm.py\n\t\t{\"1\", \"1\", 0},\n\t\t{\"4.19.0a-1.el7_5\", \"4.19.0c-1.el7\", -1},\n\t\t{\"4.19.0-1.el7_5\", \"4.21.0-1.el7\", -1},\n\t\t{\"4.19.01-1.el7_5\", \"4.19.10-1.el7_5\", -1},\n\t\t{\"4.19.0-1.el7_5\", \"4.19.0-1.el7\", 1},\n\t\t{\"4.19.0-1.el7_5\", \"4.17.0-1.el7\", 1},\n\t\t{\"4.19.01-1.el7_5\", \"4.19.1-1.el7_5\", 0},\n\t\t{\"4.19.1-1.el7_5\", \"4.19.1-01.el7_5\", 0},\n\t\t{\"4.19.1\", \"4.19.1\", 0},\n\t\t{\"1.2.3-el7_5~snapshot1\", \"1.2.3-3-el7_5\", -1},\n\t\t{\"1:0\", \"0:1\", 1},\n\t\t{\"1:2\", \"1\", 1},\n\t\t{\"0:4.19.1-1.el7_5\", \"2:4.19.1-1.el7_5\", -1},\n\t\t{\"4:1.2.3-3-el7_5\", \"1.2.3-el7_5~snapshot1\", 1},\n\n\t\t// non-standard comparisons that ignore epochs due to only one being available\n\t\t{\"1:0\", \"1\", -1},\n\t\t{\"2:4.19.01-1.el7_5\", \"4.19.1-1.el7_5\", 0},\n\t\t{\"4.19.01-1.el7_5\", \"2:4.19.1-1.el7_5\", 0},\n\t\t{\"4.19.0-1.el7_5\", \"12:4.19.0-1.el7\", 1},\n\t\t{\"3:4.19.0-1.el7_5\", \"4.21.0-1.el7\", -1},\n\t}\n\n\tfor _, test := range tests {\n\t\tname := test.v1 + \"_vs_\" + test.v2\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tv1 := New(test.v1, RpmFormat)\n\t\t\tv2 := New(test.v2, RpmFormat)\n\n\t\t\tactual, err := v1.Compare(v2)\n\t\t\trequire.NoError(t, err, \"unexpected error comparing versions: %s vs %s\", test.v1, test.v2)\n\t\t\tassert.Equal(t, test.result, actual, \"expected comparison result to match for %s vs %s\", test.v1, test.v2)\n\t\t})\n\t}\n}\n\nfunc TestRpmVersion_Compare_Format(t *testing.T) {\n\ttests := []struct {\n\t\tname           string\n\t\tthisVersion    string\n\t\totherVersion   string\n\t\totherFormat    Format\n\t\texpectError    bool\n\t\terrorSubstring string\n\t}{\n\t\t{\n\t\t\tname:         \"same format successful comparison\",\n\t\t\tthisVersion:  \"1.2.3-1\",\n\t\t\totherVersion: \"1.2.3-2\",\n\t\t\totherFormat:  RpmFormat,\n\t\t\texpectError:  false,\n\t\t},\n\t\t{\n\t\t\tname:         \"same format successful comparison with epoch\",\n\t\t\tthisVersion:  \"1:1.2.3-1\",\n\t\t\totherVersion: \"1:1.2.3-2\",\n\t\t\totherFormat:  RpmFormat,\n\t\t\texpectError:  false,\n\t\t},\n\t\t{\n\t\t\tname:         \"unknown format attempts upgrade - valid rpm format\",\n\t\t\tthisVersion:  \"1.2.3-1\",\n\t\t\totherVersion: \"1.2.3-2\",\n\t\t\totherFormat:  UnknownFormat,\n\t\t\texpectError:  false,\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tthisVer := New(test.thisVersion, RpmFormat)\n\n\t\t\totherVer := New(test.otherVersion, test.otherFormat)\n\n\t\t\tresult, err := thisVer.Compare(otherVer)\n\n\t\t\tif test.expectError {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\tif test.errorSubstring != \"\" {\n\t\t\t\t\tassert.True(t, strings.Contains(err.Error(), test.errorSubstring),\n\t\t\t\t\t\t\"Expected error to contain '%s', got: %v\", test.errorSubstring, err)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.Contains(t, []int{-1, 0, 1}, result, \"Expected comparison result to be -1, 0, or 1\")\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestRpmVersion_Compare_EdgeCases(t *testing.T) {\n\ttests := []struct {\n\t\tname           string\n\t\tsetupFunc      func(testing.TB) (*Version, *Version)\n\t\texpectError    bool\n\t\terrorSubstring string\n\t}{\n\t\t{\n\t\t\tname: \"nil version object\",\n\t\t\tsetupFunc: func(t testing.TB) (*Version, *Version) {\n\t\t\t\tthisVer := New(\"1.2.3-1\", RpmFormat)\n\t\t\t\treturn thisVer, nil\n\t\t\t},\n\t\t\texpectError:    true,\n\t\t\terrorSubstring: \"no version provided for comparison\",\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tthisVer, otherVer := test.setupFunc(t)\n\n\t\t\t_, err := thisVer.Compare(otherVer)\n\n\t\t\trequire.Error(t, err)\n\t\t\tif test.errorSubstring != \"\" {\n\t\t\t\tassert.True(t, strings.Contains(err.Error(), test.errorSubstring),\n\t\t\t\t\t\"Expected error to contain '%s', got: %v\", test.errorSubstring, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestRpmVersion_CompareWithConfig(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tversion  string\n\t\tother    string\n\t\tstrategy MissingEpochStrategy\n\t\twant     int // -1, 0, or 1\n\t}{\n\t\t{\n\t\t\tname:     \"package has epoch, no behavior change with auto\",\n\t\t\tversion:  \"1:2.0.0\",\n\t\t\tother:    \"1:1.5.0\",\n\t\t\tstrategy: MissingEpochStrategyAuto,\n\t\t\twant:     1, // 1:2.0.0 > 1:1.5.0\n\t\t},\n\t\t{\n\t\t\tname:     \"package has epoch, no behavior change with zero\",\n\t\t\tversion:  \"1:2.0.0\",\n\t\t\tother:    \"1:1.5.0\",\n\t\t\tstrategy: \"zero\",\n\t\t\twant:     1, // 1:2.0.0 > 1:1.5.0\n\t\t},\n\t\t{\n\t\t\tname:     \"package missing epoch, constraint has epoch, auto strategy - no match\",\n\t\t\tversion:  \"2.0.0\",\n\t\t\tother:    \"1:1.5.0\",\n\t\t\tstrategy: MissingEpochStrategyAuto,\n\t\t\twant:     1, // Treated as 1:2.0.0 > 1:1.5.0\n\t\t},\n\t\t{\n\t\t\tname:     \"package missing epoch, constraint has epoch, zero strategy\",\n\t\t\tversion:  \"2.0.0\",\n\t\t\tother:    \"1:1.5.0\",\n\t\t\tstrategy: \"zero\",\n\t\t\twant:     -1, // Treated as 0:2.0.0 < 1:1.5.0 (epoch 0 < 1)\n\t\t},\n\t\t{\n\t\t\tname:     \"both missing epoch, auto strategy\",\n\t\t\tversion:  \"2.0.0\",\n\t\t\tother:    \"1.5.0\",\n\t\t\tstrategy: MissingEpochStrategyAuto,\n\t\t\twant:     1, // 2.0.0 > 1.5.0\n\t\t},\n\t\t{\n\t\t\tname:     \"both missing epoch, zero strategy\",\n\t\t\tversion:  \"2.0.0\",\n\t\t\tother:    \"1.5.0\",\n\t\t\tstrategy: \"zero\",\n\t\t\twant:     1, // 2.0.0 > 1.5.0\n\t\t},\n\t\t{\n\t\t\tname:     \"constraint missing epoch, package has epoch\",\n\t\t\tversion:  \"1:2.0.0\",\n\t\t\tother:    \"1.5.0\",\n\t\t\tstrategy: MissingEpochStrategyAuto,\n\t\t\twant:     1, // 1:2.0.0 > 0:1.5.0 (constraint gets epoch 0)\n\t\t},\n\t\t{\n\t\t\tname:     \"auto strategy, package less than constraint\",\n\t\t\tversion:  \"1.0.0\",\n\t\t\tother:    \"1:1.5.0\",\n\t\t\tstrategy: MissingEpochStrategyAuto,\n\t\t\twant:     -1, // Treated as 1:1.0.0 < 1:1.5.0\n\t\t},\n\t\t{\n\t\t\tname:     \"auto strategy, different epochs on constraints\",\n\t\t\tversion:  \"1.2.0\",\n\t\t\tother:    \"2:1.5.0\",\n\t\t\tstrategy: MissingEpochStrategyAuto,\n\t\t\twant:     -1, // Treated as 2:1.2.0 < 2:1.5.0\n\t\t},\n\t\t{\n\t\t\tname:     \"zero strategy, package version newer but lower epoch\",\n\t\t\tversion:  \"3.0.0\",\n\t\t\tother:    \"1:1.0.0\",\n\t\t\tstrategy: \"zero\",\n\t\t\twant:     -1, // Treated as 0:3.0.0 < 1:1.0.0 (epoch 0 < 1)\n\t\t},\n\t\t{\n\t\t\tname:     \"auto strategy, equal versions different missing epochs\",\n\t\t\tversion:  \"1.2.3\",\n\t\t\tother:    \"1:1.2.3\",\n\t\t\tstrategy: MissingEpochStrategyAuto,\n\t\t\twant:     0, // Treated as 1:1.2.3 == 1:1.2.3\n\t\t},\n\t\t{\n\t\t\tname:     \"zero strategy, equal versions different missing epochs\",\n\t\t\tversion:  \"1.2.3\",\n\t\t\tother:    \"1:1.2.3\",\n\t\t\tstrategy: \"zero\",\n\t\t\twant:     -1, // Treated as 0:1.2.3 < 1:1.2.3 (epoch 0 < 1)\n\t\t},\n\t\t{\n\t\t\tname:     \"auto strategy, large epoch difference\",\n\t\t\tversion:  \"1.0.0\",\n\t\t\tother:    \"999:0.5.0\",\n\t\t\tstrategy: MissingEpochStrategyAuto,\n\t\t\twant:     1, // Treated as 999:1.0.0 > 999:0.5.0\n\t\t},\n\t\t{\n\t\t\tname:     \"zero strategy, large epoch difference\",\n\t\t\tversion:  \"1.0.0\",\n\t\t\tother:    \"999:0.5.0\",\n\t\t\tstrategy: \"zero\",\n\t\t\twant:     -1, // Treated as 0:1.0.0 < 999:0.5.0 (epoch 0 < 999)\n\t\t},\n\t\t{\n\t\t\tname:     \"both have epochs, strategy should not matter\",\n\t\t\tversion:  \"2:1.5.0\",\n\t\t\tother:    \"1:2.0.0\",\n\t\t\tstrategy: MissingEpochStrategyAuto,\n\t\t\twant:     1, // 2:1.5.0 > 1:2.0.0 (epoch takes precedence)\n\t\t},\n\t\t{\n\t\t\tname:     \"both have same epoch, strategy should not matter\",\n\t\t\tversion:  \"3:2.0.0\",\n\t\t\tother:    \"3:1.5.0\",\n\t\t\tstrategy: \"zero\",\n\t\t\twant:     1, // 3:2.0.0 > 3:1.5.0\n\t\t},\n\t\t{\n\t\t\tname:     \"empty strategy uses default compare behavior (ignores one-sided epochs)\",\n\t\t\tversion:  \"2.0.0\",\n\t\t\tother:    \"1:1.5.0\",\n\t\t\tstrategy: \"\",\n\t\t\twant:     1, // Falls through to compare() which ignores one-sided epochs: 2.0.0 > 1.5.0\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tv1, err := newRpmVersion(tt.version)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tv2 := &Version{\n\t\t\t\tFormat: RpmFormat,\n\t\t\t\tRaw:    tt.other,\n\t\t\t}\n\n\t\t\tcfg := ComparisonConfig{\n\t\t\t\tMissingEpochStrategy: tt.strategy,\n\t\t\t}\n\n\t\t\tresult, err := v1.CompareWithConfig(v2, cfg)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, tt.want, result,\n\t\t\t\t\"comparing %s vs %s with strategy %s\",\n\t\t\t\ttt.version, tt.other, tt.strategy)\n\t\t})\n\t}\n}\n\nfunc TestRpmVersion_CompareWithConfig_ErrorCases(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tversion  string\n\t\tother    *Version\n\t\tstrategy MissingEpochStrategy\n\t\twantErr  bool\n\t}{\n\t\t{\n\t\t\tname:     \"nil other version\",\n\t\t\tversion:  \"1.0.0\",\n\t\t\tother:    nil,\n\t\t\tstrategy: MissingEpochStrategyAuto,\n\t\t\twantErr:  true,\n\t\t},\n\t\t{\n\t\t\tname:     \"invalid other version format\",\n\t\t\tversion:  \"1.0.0\",\n\t\t\tother:    &Version{Format: RpmFormat, Raw: \"not:a:valid:version:string:with:too:many:colons\"},\n\t\t\tstrategy: MissingEpochStrategyAuto,\n\t\t\twantErr:  true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tv1, err := newRpmVersion(tt.version)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tcfg := ComparisonConfig{\n\t\t\t\tMissingEpochStrategy: tt.strategy,\n\t\t\t}\n\n\t\t\t_, err = v1.CompareWithConfig(tt.other, cfg)\n\t\t\tif tt.wantErr {\n\t\t\t\tassert.Error(t, err)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestRpmVersion_CompareWithConfig_ConsistencyWithCompare(t *testing.T) {\n\t// Test that when both versions have epochs, CompareWithConfig gives same result as Compare\n\ttests := []struct {\n\t\tv1 string\n\t\tv2 string\n\t}{\n\t\t{\"1:2.0.0\", \"1:1.5.0\"},\n\t\t{\"2:1.0.0\", \"1:2.0.0\"},\n\t\t{\"0:1.2.3\", \"0:1.2.3\"},\n\t\t{\"5:1.0.0-1.el7\", \"5:1.0.0-2.el7\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.v1+\"_vs_\"+tt.v2, func(t *testing.T) {\n\t\t\tv1, _ := newRpmVersion(tt.v1)\n\t\t\tv2 := &Version{Format: RpmFormat, Raw: tt.v2}\n\n\t\t\t// Test with both strategies\n\t\t\tfor _, strategy := range []MissingEpochStrategy{MissingEpochStrategyZero, MissingEpochStrategyAuto} {\n\t\t\t\tcfg := ComparisonConfig{MissingEpochStrategy: strategy}\n\n\t\t\t\tresultWithConfig, err1 := v1.CompareWithConfig(v2, cfg)\n\t\t\t\trequire.NoError(t, err1)\n\n\t\t\t\tresultNormal, err2 := v1.Compare(v2)\n\t\t\t\trequire.NoError(t, err2)\n\n\t\t\t\tassert.Equal(t, resultNormal, resultWithConfig,\n\t\t\t\t\t\"when both versions have epochs, CompareWithConfig should match Compare (strategy: %s)\", strategy)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/version/semantic_version.go",
    "content": "package version\n\nimport (\n\t\"regexp\"\n\t\"strings\"\n\n\thashiVer \"github.com/anchore/go-version\"\n)\n\nvar _ Comparator = (*semanticVersion)(nil)\n\n// semverPrereleaseNormalizer are meant to replace common pre-release suffixes with standard semver pre-release suffixes.\n// this is primarily intended for to cover ruby packages such as activerecord and sprockets, which don't strictly\n// follow semver, however, this can generally be applied to other cases using semver as well.\n// note: this may result in missed matches for versioned betas\nvar semverPrereleaseNormalizer = strings.NewReplacer(\".alpha\", \"-alpha\", \".beta\", \"-beta\", \".rc\", \"-rc\")\n\ntype semanticVersion struct {\n\tobj *hashiVer.Version\n}\n\nvar versionStartsWithV = regexp.MustCompile(`^v\\d+`)\n\nfunc newSemanticVersion(raw string, strict bool) (semanticVersion, error) {\n\tclean := semverPrereleaseNormalizer.Replace(raw)\n\n\tvar verObj *hashiVer.Version\n\tvar err error\n\tif strict {\n\t\t// we still want v-prefix processing\n\t\tif versionStartsWithV.MatchString(clean) {\n\t\t\tclean = strings.TrimPrefix(clean, \"v\")\n\t\t}\n\t\tverObj, err = hashiVer.NewSemver(clean)\n\t} else {\n\t\tverObj, err = hashiVer.NewVersion(clean)\n\t}\n\tif err != nil {\n\t\treturn semanticVersion{}, invalidFormatError(SemanticFormat, raw, err)\n\t}\n\treturn semanticVersion{\n\t\tobj: verObj,\n\t}, nil\n}\n\nfunc (v semanticVersion) Compare(other *Version) (int, error) {\n\tif other == nil {\n\t\treturn -1, ErrNoVersionProvided\n\t}\n\n\to, err := newSemanticVersion(other.Raw, false)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\treturn v.obj.Compare(o.obj), nil\n}\n"
  },
  {
    "path": "grype/version/semantic_version_test.go",
    "content": "package version\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestSemanticVersion_Constraint(t *testing.T) {\n\ttests := []testCase{\n\t\t// empty values\n\t\t{version: \"2.3.1\", constraint: \"\", satisfied: true},\n\t\t// typical cases\n\t\t{version: \"0.9.9-r0\", constraint: \"< 0.9.12-r1\", satisfied: true}, // regression case\n\t\t{version: \"1.5.0\", constraint: \"> 0.1.0, < 0.5.0 || > 1.0.0, < 2.0.0\", satisfied: true},\n\t\t{version: \"0.2.0\", constraint: \"> 0.1.0, < 0.5.0 || > 1.0.0, < 2.0.0\", satisfied: true},\n\t\t{version: \"0.0.1\", constraint: \"> 0.1.0, < 0.5.0 || > 1.0.0, < 2.0.0\", satisfied: false},\n\t\t{version: \"0.6.0\", constraint: \"> 0.1.0, < 0.5.0 || > 1.0.0, < 2.0.0\", satisfied: false},\n\t\t{version: \"2.5.0\", constraint: \"> 0.1.0, < 0.5.0 || > 1.0.0, < 2.0.0\", satisfied: false},\n\t\t{version: \"2.3.1\", constraint: \"2.3.1\", satisfied: true},\n\t\t{version: \"2.3.1\", constraint: \"= 2.3.1\", satisfied: true},\n\t\t{version: \"2.3.1\", constraint: \"  =   2.3.1\", satisfied: true},\n\t\t{version: \"2.3.1\", constraint: \">= 2.3.1\", satisfied: true},\n\t\t{version: \"2.3.1\", constraint: \"> 2.0.0\", satisfied: true},\n\t\t{version: \"2.3.1\", constraint: \"> 2.0\", satisfied: true},\n\t\t{version: \"2.3.1\", constraint: \"> 2\", satisfied: true},\n\t\t{version: \"2.3.1\", constraint: \"> 2, < 3\", satisfied: true},\n\t\t{version: \"2.3.1\", constraint: \"> 2.3, < 3.1\", satisfied: true},\n\t\t{version: \"2.3.1\", constraint: \"> 2.3.0, < 3.1\", satisfied: true},\n\t\t{version: \"2.3.1\", constraint: \">= 2.3.1, < 3.1\", satisfied: true},\n\t\t{version: \"2.3.1\", constraint: \"  =  2.3.2\", satisfied: false},\n\t\t{version: \"2.3.1\", constraint: \">= 2.3.2\", satisfied: false},\n\t\t{version: \"2.3.1\", constraint: \"> 2.3.1\", satisfied: false},\n\t\t{version: \"2.3.1\", constraint: \"< 2.0.0\", satisfied: false},\n\t\t{version: \"2.3.1\", constraint: \"< 2.0\", satisfied: false},\n\t\t{version: \"2.3.1\", constraint: \"< 2\", satisfied: false},\n\t\t{version: \"2.3.1\", constraint: \"< 2, > 3\", satisfied: false},\n\t\t{version: \"2.3.1+meta\", constraint: \"2.3.1\", satisfied: true},\n\t\t{version: \"2.3.1+meta\", constraint: \"= 2.3.1\", satisfied: true},\n\t\t{version: \"2.3.1+meta\", constraint: \"  =   2.3.1\", satisfied: true},\n\t\t{version: \"2.3.1+meta\", constraint: \">= 2.3.1\", satisfied: true},\n\t\t{version: \"2.3.1+meta\", constraint: \"> 2.0.0\", satisfied: true},\n\t\t{version: \"2.3.1+meta\", constraint: \"> 2.0\", satisfied: true},\n\t\t{version: \"2.3.1+meta\", constraint: \"> 2\", satisfied: true},\n\t\t{version: \"2.3.1+meta\", constraint: \"> 2, < 3\", satisfied: true},\n\t\t{version: \"2.3.1+meta\", constraint: \"> 2.3, < 3.1\", satisfied: true},\n\t\t{version: \"2.3.1+meta\", constraint: \"> 2.3.0, < 3.1\", satisfied: true},\n\t\t{version: \"2.3.1+meta\", constraint: \">= 2.3.1, < 3.1\", satisfied: true},\n\t\t{version: \"2.3.1+meta\", constraint: \"  =  2.3.2\", satisfied: false},\n\t\t{version: \"2.3.1+meta\", constraint: \">= 2.3.2\", satisfied: false},\n\t\t{version: \"2.3.1+meta\", constraint: \"> 2.3.1\", satisfied: false},\n\t\t{version: \"2.3.1+meta\", constraint: \"< 2.0.0\", satisfied: false},\n\t\t{version: \"2.3.1+meta\", constraint: \"< 2.0\", satisfied: false},\n\t\t{version: \"2.3.1+meta\", constraint: \"< 2\", satisfied: false},\n\t\t{version: \"2.3.1+meta\", constraint: \"< 2, > 3\", satisfied: false},\n\t\t// from https://github.com/hashicorp/go-version/issues/61\n\t\t// and https://semver.org/#spec-item-11\n\t\t// A larger set of pre-release fields has a higher precedence than a smaller set, if all of the preceding identifiers are equal.\n\t\t{version: \"1.0.0-alpha\", constraint: \"> 1.0.0-alpha.1\", satisfied: false},\n\t\t{version: \"1.0.0-alpha\", constraint: \"< 1.0.0-alpha.1\", satisfied: true},\n\t\t{version: \"1.0.0-alpha.1\", constraint: \"> 1.0.0-alpha.beta\", satisfied: false},\n\t\t{version: \"1.0.0-alpha.1\", constraint: \"< 1.0.0-alpha.beta\", satisfied: true},\n\t\t{version: \"1.0.0-alpha.beta\", constraint: \"> 1.0.0-beta\", satisfied: false},\n\t\t{version: \"1.0.0-alpha.beta\", constraint: \"< 1.0.0-beta\", satisfied: true},\n\t\t{version: \"1.0.0-beta\", constraint: \"> 1.0.0-beta.2\", satisfied: false},\n\t\t{version: \"1.0.0-beta\", constraint: \"< 1.0.0-beta.2\", satisfied: true},\n\t\t{version: \"1.0.0-beta.2\", constraint: \"> 1.0.0-beta.11\", satisfied: false},\n\t\t{version: \"1.0.0-beta.2\", constraint: \"< 1.0.0-beta.11\", satisfied: true},\n\t\t{version: \"1.0.0-beta.11\", constraint: \"> 1.0.0-rc.1\", satisfied: false},\n\t\t{version: \"1.0.0-beta.11\", constraint: \"< 1.0.0-rc.1\", satisfied: true},\n\t\t{version: \"1.0.0-rc.1\", constraint: \"> 1.0.0\", satisfied: false},\n\t\t{version: \"1.0.0-rc.1\", constraint: \"< 1.0.0\", satisfied: true},\n\t\t{version: \"1.20rc1\", constraint: \" = 1.20.0-rc1\", satisfied: true},\n\t\t{version: \"1.21rc2\", constraint: \" = 1.21.1\", satisfied: false},\n\t\t{version: \"1.21rc2\", constraint: \" = 1.21\", satisfied: false},\n\t\t{version: \"1.21rc2\", constraint: \" = 1.21-rc2\", satisfied: true},\n\t\t{version: \"1.21rc2\", constraint: \" = 1.21.0-rc2\", satisfied: true},\n\t\t{version: \"1.21rc2\", constraint: \" = 1.21.0rc2\", satisfied: true},\n\t\t{version: \"1.0.0-alpha.1\", constraint: \"> 1.0.0-alpha.1\", satisfied: false},\n\t\t{version: \"1.0.0-alpha.2\", constraint: \"> 1.0.0-alpha.1\", satisfied: true},\n\t\t{version: \"1.2.0-beta\", constraint: \">1.0, <2.0\", satisfied: true},\n\t\t{version: \"1.2.0-beta\", constraint: \">1.0\", satisfied: true},\n\t\t{version: \"1.2.0-beta\", constraint: \"<2.0\", satisfied: true},\n\t\t{version: \"1.2.0\", constraint: \">1.0, <2.0\", satisfied: true},\n\n\t\t// below are test cases for the ruby version normalizer that converts .alpha, .beta, .rc to -alpha, -beta, -rc\n\n\t\t// prerelease normalizer - alpha versions\n\t\t{version: \"1.0.0.alpha\", constraint: \"< 1.0.0\", satisfied: true},\n\t\t{version: \"1.0.0.alpha\", constraint: \"> 1.0.0-alpha\", satisfied: false}, // should be equal after normalization\n\t\t{version: \"1.0.0.alpha\", constraint: \"= 1.0.0-alpha\", satisfied: true},\n\t\t{version: \"1.0.0.alpha1\", constraint: \"= 1.0.0-alpha1\", satisfied: true},\n\t\t{version: \"1.0.0.alpha.1\", constraint: \"= 1.0.0-alpha.1\", satisfied: true},\n\n\t\t// prerelease normalizer - beta versions\n\t\t{version: \"1.0.0.beta\", constraint: \"< 1.0.0\", satisfied: true},\n\t\t{version: \"1.0.0.beta\", constraint: \"> 1.0.0-alpha\", satisfied: true},\n\t\t{version: \"1.0.0.beta\", constraint: \"= 1.0.0-beta\", satisfied: true},\n\t\t{version: \"1.0.0.beta2\", constraint: \"= 1.0.0-beta2\", satisfied: true},\n\t\t{version: \"1.0.0.beta.2\", constraint: \"= 1.0.0-beta.2\", satisfied: true},\n\n\t\t// prerelease normalizer - rc versions\n\t\t{version: \"1.0.0.rc\", constraint: \"< 1.0.0\", satisfied: true},\n\t\t{version: \"1.0.0.rc\", constraint: \"> 1.0.0-beta\", satisfied: true},\n\t\t{version: \"1.0.0.rc\", constraint: \"= 1.0.0-rc\", satisfied: true},\n\t\t{version: \"1.0.0.rc1\", constraint: \"= 1.0.0-rc1\", satisfied: true},\n\t\t{version: \"1.0.0.rc.1\", constraint: \"= 1.0.0-rc.1\", satisfied: true},\n\n\t\t// prerelease normalizer - ordering tests to ensure normalization doesn't break semver precedence\n\t\t{version: \"1.0.0.alpha\", constraint: \"< 1.0.0-beta\", satisfied: true},\n\t\t{version: \"1.0.0.beta\", constraint: \"< 1.0.0-rc\", satisfied: true},\n\t\t{version: \"1.0.0.rc\", constraint: \"< 1.0.0\", satisfied: true},\n\t\t{version: \"1.0.0.alpha1\", constraint: \"< 1.0.0-alpha2\", satisfied: true},\n\n\t\t// prerelease normalizer - mixed ruby and standard semver styles in constraints\n\t\t{version: \"1.0.0.alpha\", constraint: \"< 1.0.0-beta\", satisfied: true},\n\t\t{version: \"1.0.0-alpha\", constraint: \"< 1.0.0-beta\", satisfied: true},\n\n\t\t// prerelease normalizer - complex constraints with ruby-style versions\n\t\t{version: \"1.0.0.alpha\", constraint: \"> 0.9.0, < 1.0.0\", satisfied: true},\n\t\t{version: \"1.0.0.beta\", constraint: \"> 1.0.0-alpha, < 1.0.0\", satisfied: true},\n\t\t{version: \"2.1.0.rc1\", constraint: \"> 2.0.0, < 2.1.0\", satisfied: true},\n\n\t\t// prerelease normalizer - edge cases\n\t\t{version: \"1.0.0.alpha.beta\", constraint: \"= 1.0.0-alpha-beta\", satisfied: true}, // multiple replacements\n\t\t{version: \"1.0.0.rc.alpha\", constraint: \"= 1.0.0-rc-alpha\", satisfied: true},     // mixed order\n\n\t\t// prerelease normalizer - ensure regular versions still work\n\t\t{version: \"1.0.0-alpha\", constraint: \"< 1.0.0\", satisfied: true},\n\t\t{version: \"1.0.0-beta\", constraint: \"> 1.0.0-alpha\", satisfied: true},\n\t\t{version: \"1.0.0-rc\", constraint: \"> 1.0.0-beta\", satisfied: true},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.tName(), func(t *testing.T) {\n\t\t\tconstraint, err := GetConstraint(test.constraint, SemanticFormat)\n\t\t\tassert.NoError(t, err)\n\n\t\t\ttest.assertVersionConstraint(t, SemanticFormat, constraint)\n\t\t})\n\t}\n}\n\nfunc TestSemanticVersion_PrereleaseNormalizer_EdgeCases(t *testing.T) {\n\t// test edge cases to ensure the normalizer can be safely retained\n\ttests := []struct {\n\t\tname      string\n\t\tversion   string\n\t\twantError require.ErrorAssertionFunc\n\t}{\n\t\t{\n\t\t\tname:      \"version with only alpha\",\n\t\t\tversion:   \"alpha\",\n\t\t\twantError: require.Error, // invalid semver\n\t\t},\n\t\t{\n\t\t\tname:      \"version with leading alpha\",\n\t\t\tversion:   \"alpha.1.0.0\",\n\t\t\twantError: require.Error, // invalid semver\n\t\t},\n\t\t{\n\t\t\tname:      \"empty version\",\n\t\t\tversion:   \"\",\n\t\t\twantError: require.Error,\n\t\t},\n\t\t{\n\t\t\tname:    \"version with multiple dots in prerelease\",\n\t\t\tversion: \"1.0.0.alpha.beta.rc\", // should normalize to 1.0.0-alpha-beta-rc\n\t\t},\n\t\t{\n\t\t\tname:    \"version already in correct format\",\n\t\t\tversion: \"1.0.0-alpha\",\n\t\t},\n\t\t{\n\t\t\tname:    \"version with build metadata\",\n\t\t\tversion: \"1.0.0.alpha+build\",\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tif test.wantError == nil {\n\t\t\t\ttest.wantError = require.NoError\n\t\t\t}\n\t\t\t_, err := newSemanticVersion(test.version, false)\n\t\t\ttest.wantError(t, err, \"expected error for version: %s\", test.version)\n\t\t})\n\t}\n}\n\nfunc TestSemanticVersion_PrereleaseNormalizer_WithGemFormat(t *testing.T) {\n\t// ensure that the prerelease normalizer in semantic format doesn't conflict with gem format\n\trubyStyleVersions := []string{\n\t\t\"1.0.0.alpha\",\n\t\t\"1.0.0.beta.1\",\n\t\t\"1.0.0.rc2\",\n\t}\n\n\tfor _, version := range rubyStyleVersions {\n\t\tt.Run(version, func(t *testing.T) {\n\t\t\t// both semantic and gem formats should be able to handle these versions\n\t\t\tsemanticVer := New(version, SemanticFormat)\n\t\t\tgemVer := New(version, GemFormat)\n\n\t\t\t// they might have different comparison behavior, but both should be valid\n\t\t\tassert.NotNil(t, semanticVer)\n\t\t\tassert.NotNil(t, gemVer)\n\t\t})\n\t}\n}\n\nfunc TestSemanticVersion_Compare_Format(t *testing.T) {\n\ttests := []struct {\n\t\tname           string\n\t\tthisVersion    string\n\t\totherVersion   string\n\t\totherFormat    Format\n\t\texpectError    bool\n\t\terrorSubstring string\n\t}{\n\t\t{\n\t\t\tname:         \"same format successful comparison\",\n\t\t\tthisVersion:  \"1.2.3\",\n\t\t\totherVersion: \"1.2.4\",\n\t\t\totherFormat:  SemanticFormat,\n\t\t\texpectError:  false,\n\t\t},\n\t\t{\n\t\t\tname:         \"same format successful comparison with prerelease\",\n\t\t\tthisVersion:  \"1.2.3-alpha\",\n\t\t\totherVersion: \"1.2.3-beta\",\n\t\t\totherFormat:  SemanticFormat,\n\t\t\texpectError:  false,\n\t\t},\n\t\t{\n\t\t\tname:         \"same format successful comparison with build metadata\",\n\t\t\tthisVersion:  \"1.2.3+build.1\",\n\t\t\totherVersion: \"1.2.3+build.2\",\n\t\t\totherFormat:  SemanticFormat,\n\t\t\texpectError:  false,\n\t\t},\n\t\t{\n\t\t\tname:         \"unknown format attempts upgrade - valid semantic format\",\n\t\t\tthisVersion:  \"1.2.3\",\n\t\t\totherVersion: \"1.2.4\",\n\t\t\totherFormat:  UnknownFormat,\n\t\t\texpectError:  false,\n\t\t},\n\t\t{\n\t\t\tname:           \"unknown format attempts upgrade - invalid semantic format\",\n\t\t\tthisVersion:    \"1.2.3\",\n\t\t\totherVersion:   \"not.valid.semver\",\n\t\t\totherFormat:    UnknownFormat,\n\t\t\texpectError:    true,\n\t\t\terrorSubstring: \"invalid\",\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tthisVer, err := newSemanticVersion(test.thisVersion, true)\n\t\t\trequire.NoError(t, err)\n\n\t\t\totherVer := New(test.otherVersion, test.otherFormat)\n\n\t\t\tresult, err := thisVer.Compare(otherVer)\n\n\t\t\tif test.expectError {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\tif test.errorSubstring != \"\" {\n\t\t\t\t\tassert.True(t, strings.Contains(err.Error(), test.errorSubstring),\n\t\t\t\t\t\t\"Expected error to contain '%s', got: %v\", test.errorSubstring, err)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.Contains(t, []int{-1, 0, 1}, result, \"Expected comparison result to be -1, 0, or 1\")\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSemanticVersion_Compare_EdgeCases(t *testing.T) {\n\ttests := []struct {\n\t\tname           string\n\t\tsetupFunc      func(testing.TB) (*Version, *Version)\n\t\texpectError    bool\n\t\terrorSubstring string\n\t}{\n\t\t{\n\t\t\tname: \"nil version object\",\n\t\t\tsetupFunc: func(t testing.TB) (*Version, *Version) {\n\t\t\t\tthisVer := New(\"1.2.3\", SemanticFormat)\n\t\t\t\treturn thisVer, nil\n\t\t\t},\n\t\t\texpectError:    true,\n\t\t\terrorSubstring: \"no version provided for comparison\",\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tthisVer, otherVer := test.setupFunc(t)\n\n\t\t\t_, err := thisVer.Compare(otherVer)\n\n\t\t\trequire.Error(t, err)\n\t\t\tif test.errorSubstring != \"\" {\n\t\t\t\tassert.True(t, strings.Contains(err.Error(), test.errorSubstring),\n\t\t\t\t\t\"Expected error to contain '%s', got: %v\", test.errorSubstring, err)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/version/set.go",
    "content": "package version\n\nimport (\n\t\"sort\"\n)\n\ntype Set struct {\n\tversions map[string]*Version\n\tgetKey   func(v *Version) string\n}\n\nfunc NewSet(ignoreFormat bool, vs ...*Version) *Set {\n\tvar getKey func(v *Version) string\n\tif ignoreFormat {\n\t\tgetKey = func(v *Version) string {\n\t\t\tif v == nil {\n\t\t\t\treturn \"\"\n\t\t\t}\n\t\t\treturn v.Raw\n\t\t}\n\t} else {\n\t\tgetKey = func(v *Version) string {\n\t\t\tif v == nil {\n\t\t\t\treturn \"\"\n\t\t\t}\n\t\t\treturn v.Raw + \":\" + v.Format.String()\n\t\t}\n\t}\n\ts := &Set{\n\t\tversions: make(map[string]*Version),\n\t\tgetKey:   getKey,\n\t}\n\ts.Add(vs...)\n\treturn s\n}\n\nfunc (s *Set) Add(vs ...*Version) {\n\tif s.versions == nil {\n\t\ts.versions = make(map[string]*Version)\n\t}\n\n\tfor _, v := range vs {\n\t\tif v == nil {\n\t\t\tcontinue\n\t\t}\n\t\tkey := s.getKey(v)\n\t\ts.versions[key] = v\n\t}\n}\n\nfunc (s *Set) Remove(vs ...*Version) {\n\tif s.versions == nil {\n\t\treturn\n\t}\n\n\tfor _, v := range vs {\n\t\tif v == nil {\n\t\t\tcontinue\n\t\t}\n\t\tkey := s.getKey(v)\n\t\tdelete(s.versions, key)\n\t}\n}\n\nfunc (s *Set) Contains(v *Version) bool {\n\tif v == nil || s.versions == nil {\n\t\treturn false\n\t}\n\n\tkey := s.getKey(v)\n\t_, exists := s.versions[key]\n\treturn exists\n}\n\nfunc (s *Set) Values() []*Version {\n\tif len(s.versions) == 0 {\n\t\treturn nil\n\t}\n\n\tout := make([]*Version, 0, len(s.versions))\n\tfor _, v := range s.versions {\n\t\tout = append(out, v)\n\t}\n\n\tsort.Slice(out, func(i, j int) bool {\n\t\tif out[i] == nil && out[j] == nil {\n\t\t\treturn false\n\t\t}\n\t\tif out[i] == nil {\n\t\t\treturn true\n\t\t}\n\t\tif out[j] == nil {\n\t\t\treturn false\n\t\t}\n\t\tcmp, err := out[i].Compare(out[j])\n\t\tif err != nil {\n\t\t\treturn false // if we can't compare, don't change the order\n\t\t}\n\t\treturn cmp < 0\n\t})\n\n\treturn out\n}\n\nfunc (s *Set) Size() int {\n\tif s.versions == nil {\n\t\treturn 0\n\t}\n\treturn len(s.versions)\n}\n\nfunc (s *Set) Clear() {\n\tif s.versions != nil {\n\t\ts.versions = make(map[string]*Version)\n\t}\n}\n"
  },
  {
    "path": "grype/version/set_test.go",
    "content": "package version\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestNewSet(t *testing.T) {\n\ttests := []struct {\n\t\tname         string\n\t\tignoreFormat bool\n\t\tversions     []*Version\n\t\texpectedSize int\n\t}{\n\t\t{\n\t\t\tname:         \"empty set\",\n\t\t\tignoreFormat: false,\n\t\t\tversions:     nil,\n\t\t\texpectedSize: 0,\n\t\t},\n\t\t{\n\t\t\tname:         \"set with versions\",\n\t\t\tignoreFormat: false,\n\t\t\tversions: []*Version{\n\t\t\t\tNew(\"1.0.0\", SemanticFormat),\n\t\t\t\tNew(\"2.0.0\", SemanticFormat),\n\t\t\t},\n\t\t\texpectedSize: 2,\n\t\t},\n\t\t{\n\t\t\tname:         \"set with duplicate versions ignoring format\",\n\t\t\tignoreFormat: true,\n\t\t\tversions: []*Version{\n\t\t\t\tNew(\"1.0.0\", SemanticFormat),\n\t\t\t\tNew(\"1.0.0\", ApkFormat),\n\t\t\t},\n\t\t\texpectedSize: 1,\n\t\t},\n\t\t{\n\t\t\tname:         \"set with duplicate versions not ignoring format\",\n\t\t\tignoreFormat: false,\n\t\t\tversions: []*Version{\n\t\t\t\tNew(\"1.0.0\", SemanticFormat),\n\t\t\t\tNew(\"1.0.0\", ApkFormat),\n\t\t\t},\n\t\t\texpectedSize: 2,\n\t\t},\n\t\t{\n\t\t\tname:         \"set with nil versions\",\n\t\t\tignoreFormat: false,\n\t\t\tversions: []*Version{\n\t\t\t\tNew(\"1.0.0\", SemanticFormat),\n\t\t\t\tnil,\n\t\t\t\tNew(\"2.0.0\", SemanticFormat),\n\t\t\t},\n\t\t\texpectedSize: 2,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ts := NewSet(tt.ignoreFormat, tt.versions...)\n\t\t\tassert.Equal(t, tt.expectedSize, s.Size())\n\t\t})\n\t}\n}\n\nfunc TestSet_Add(t *testing.T) {\n\ttests := []struct {\n\t\tname               string\n\t\tignoreFormat       bool\n\t\tinitialVersions    []*Version\n\t\tversionsToAdd      []*Version\n\t\texpectedSize       int\n\t\texpectedContains   *Version\n\t\texpectedNotContain *Version\n\t}{\n\t\t{\n\t\t\tname:         \"add to empty set\",\n\t\t\tignoreFormat: false,\n\t\t\tversionsToAdd: []*Version{\n\t\t\t\tNew(\"1.0.0\", SemanticFormat),\n\t\t\t},\n\t\t\texpectedSize:     1,\n\t\t\texpectedContains: New(\"1.0.0\", SemanticFormat),\n\t\t},\n\t\t{\n\t\t\tname:         \"add nil version\",\n\t\t\tignoreFormat: false,\n\t\t\tversionsToAdd: []*Version{\n\t\t\t\tnil,\n\t\t\t},\n\t\t\texpectedSize: 0,\n\t\t},\n\t\t{\n\t\t\tname:         \"add duplicate version\",\n\t\t\tignoreFormat: false,\n\t\t\tinitialVersions: []*Version{\n\t\t\t\tNew(\"1.0.0\", SemanticFormat),\n\t\t\t},\n\t\t\tversionsToAdd: []*Version{\n\t\t\t\tNew(\"1.0.0\", SemanticFormat),\n\t\t\t},\n\t\t\texpectedSize:     1,\n\t\t\texpectedContains: New(\"1.0.0\", SemanticFormat),\n\t\t},\n\t\t{\n\t\t\tname:         \"add same version different format with ignoreFormat=true\",\n\t\t\tignoreFormat: true,\n\t\t\tinitialVersions: []*Version{\n\t\t\t\tNew(\"1.0.0\", SemanticFormat),\n\t\t\t},\n\t\t\tversionsToAdd: []*Version{\n\t\t\t\tNew(\"1.0.0\", ApkFormat),\n\t\t\t},\n\t\t\texpectedSize:     1,\n\t\t\texpectedContains: New(\"1.0.0\", ApkFormat), // latest added wins\n\t\t},\n\t\t{\n\t\t\tname:         \"add same version different format with ignoreFormat=false\",\n\t\t\tignoreFormat: false,\n\t\t\tinitialVersions: []*Version{\n\t\t\t\tNew(\"1.0.0\", SemanticFormat),\n\t\t\t},\n\t\t\tversionsToAdd: []*Version{\n\t\t\t\tNew(\"1.0.0\", ApkFormat),\n\t\t\t},\n\t\t\texpectedSize:     2,\n\t\t\texpectedContains: New(\"1.0.0\", SemanticFormat),\n\t\t},\n\t\t{\n\t\t\tname:         \"add to set with nil versions map\",\n\t\t\tignoreFormat: false,\n\t\t\tversionsToAdd: []*Version{\n\t\t\t\tNew(\"1.0.0\", SemanticFormat),\n\t\t\t},\n\t\t\texpectedSize:     1,\n\t\t\texpectedContains: New(\"1.0.0\", SemanticFormat),\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ts := NewSet(tt.ignoreFormat, tt.initialVersions...)\n\t\t\t// for testing nil versions map case\n\t\t\tif tt.name == \"add to set with nil versions map\" {\n\t\t\t\ts.versions = nil\n\t\t\t}\n\n\t\t\ts.Add(tt.versionsToAdd...)\n\n\t\t\tassert.Equal(t, tt.expectedSize, s.Size())\n\t\t\tif tt.expectedContains != nil {\n\t\t\t\tassert.True(t, s.Contains(tt.expectedContains))\n\t\t\t}\n\t\t\tif tt.expectedNotContain != nil {\n\t\t\t\tassert.False(t, s.Contains(tt.expectedNotContain))\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSet_Remove(t *testing.T) {\n\ttests := []struct {\n\t\tname             string\n\t\tignoreFormat     bool\n\t\tinitialVersions  []*Version\n\t\tversionsToRemove []*Version\n\t\texpectedSize     int\n\t\tshouldContain    *Version\n\t\tshouldNotContain *Version\n\t}{\n\t\t{\n\t\t\tname:         \"remove from empty set\",\n\t\t\tignoreFormat: false,\n\t\t\tversionsToRemove: []*Version{\n\t\t\t\tNew(\"1.0.0\", SemanticFormat),\n\t\t\t},\n\t\t\texpectedSize: 0,\n\t\t},\n\t\t{\n\t\t\tname:         \"remove existing version\",\n\t\t\tignoreFormat: false,\n\t\t\tinitialVersions: []*Version{\n\t\t\t\tNew(\"1.0.0\", SemanticFormat),\n\t\t\t\tNew(\"2.0.0\", SemanticFormat),\n\t\t\t},\n\t\t\tversionsToRemove: []*Version{\n\t\t\t\tNew(\"1.0.0\", SemanticFormat),\n\t\t\t},\n\t\t\texpectedSize:     1,\n\t\t\tshouldContain:    New(\"2.0.0\", SemanticFormat),\n\t\t\tshouldNotContain: New(\"1.0.0\", SemanticFormat),\n\t\t},\n\t\t{\n\t\t\tname:         \"remove nil version\",\n\t\t\tignoreFormat: false,\n\t\t\tinitialVersions: []*Version{\n\t\t\t\tNew(\"1.0.0\", SemanticFormat),\n\t\t\t},\n\t\t\tversionsToRemove: []*Version{\n\t\t\t\tnil,\n\t\t\t},\n\t\t\texpectedSize:  1,\n\t\t\tshouldContain: New(\"1.0.0\", SemanticFormat),\n\t\t},\n\t\t{\n\t\t\tname:         \"remove non-existing version\",\n\t\t\tignoreFormat: false,\n\t\t\tinitialVersions: []*Version{\n\t\t\t\tNew(\"1.0.0\", SemanticFormat),\n\t\t\t},\n\t\t\tversionsToRemove: []*Version{\n\t\t\t\tNew(\"2.0.0\", SemanticFormat),\n\t\t\t},\n\t\t\texpectedSize:  1,\n\t\t\tshouldContain: New(\"1.0.0\", SemanticFormat),\n\t\t},\n\t\t{\n\t\t\tname:         \"remove from set with nil versions map\",\n\t\t\tignoreFormat: false,\n\t\t\tversionsToRemove: []*Version{\n\t\t\t\tNew(\"1.0.0\", SemanticFormat),\n\t\t\t},\n\t\t\texpectedSize: 0,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ts := NewSet(tt.ignoreFormat, tt.initialVersions...)\n\t\t\t// for testing nil versions map case\n\t\t\tif tt.name == \"remove from set with nil versions map\" {\n\t\t\t\ts.versions = nil\n\t\t\t}\n\n\t\t\ts.Remove(tt.versionsToRemove...)\n\n\t\t\tassert.Equal(t, tt.expectedSize, s.Size())\n\t\t\tif tt.shouldContain != nil {\n\t\t\t\tassert.True(t, s.Contains(tt.shouldContain))\n\t\t\t}\n\t\t\tif tt.shouldNotContain != nil {\n\t\t\t\tassert.False(t, s.Contains(tt.shouldNotContain))\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSet_Contains(t *testing.T) {\n\ttests := []struct {\n\t\tname         string\n\t\tignoreFormat bool\n\t\tversions     []*Version\n\t\tcheckVersion *Version\n\t\texpected     bool\n\t}{\n\t\t{\n\t\t\tname:         \"contains existing version\",\n\t\t\tignoreFormat: false,\n\t\t\tversions: []*Version{\n\t\t\t\tNew(\"1.0.0\", SemanticFormat),\n\t\t\t\tNew(\"2.0.0\", SemanticFormat),\n\t\t\t},\n\t\t\tcheckVersion: New(\"1.0.0\", SemanticFormat),\n\t\t\texpected:     true,\n\t\t},\n\t\t{\n\t\t\tname:         \"does not contain non-existing version\",\n\t\t\tignoreFormat: false,\n\t\t\tversions: []*Version{\n\t\t\t\tNew(\"1.0.0\", SemanticFormat),\n\t\t\t},\n\t\t\tcheckVersion: New(\"2.0.0\", SemanticFormat),\n\t\t\texpected:     false,\n\t\t},\n\t\t{\n\t\t\tname:         \"check nil version\",\n\t\t\tignoreFormat: false,\n\t\t\tversions: []*Version{\n\t\t\t\tNew(\"1.0.0\", SemanticFormat),\n\t\t\t},\n\t\t\tcheckVersion: nil,\n\t\t\texpected:     false,\n\t\t},\n\t\t{\n\t\t\tname:         \"check version in empty set\",\n\t\t\tignoreFormat: false,\n\t\t\tversions:     nil,\n\t\t\tcheckVersion: New(\"1.0.0\", SemanticFormat),\n\t\t\texpected:     false,\n\t\t},\n\t\t{\n\t\t\tname:         \"contains same version different format with ignoreFormat=true\",\n\t\t\tignoreFormat: true,\n\t\t\tversions: []*Version{\n\t\t\t\tNew(\"1.0.0\", SemanticFormat),\n\t\t\t},\n\t\t\tcheckVersion: New(\"1.0.0\", ApkFormat),\n\t\t\texpected:     true,\n\t\t},\n\t\t{\n\t\t\tname:         \"does not contain same version different format with ignoreFormat=false\",\n\t\t\tignoreFormat: false,\n\t\t\tversions: []*Version{\n\t\t\t\tNew(\"1.0.0\", SemanticFormat),\n\t\t\t},\n\t\t\tcheckVersion: New(\"1.0.0\", ApkFormat),\n\t\t\texpected:     false,\n\t\t},\n\t\t{\n\t\t\tname:         \"check version with nil versions map\",\n\t\t\tignoreFormat: false,\n\t\t\tversions:     []*Version{},\n\t\t\tcheckVersion: New(\"1.0.0\", SemanticFormat),\n\t\t\texpected:     false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ts := NewSet(tt.ignoreFormat, tt.versions...)\n\t\t\t// for testing nil versions map case\n\t\t\tif tt.name == \"check version with nil versions map\" {\n\t\t\t\ts.versions = nil\n\t\t\t}\n\n\t\t\tresult := s.Contains(tt.checkVersion)\n\t\t\tassert.Equal(t, tt.expected, result)\n\t\t})\n\t}\n}\n\nfunc TestSet_Values(t *testing.T) {\n\ttests := []struct {\n\t\tname           string\n\t\tignoreFormat   bool\n\t\tversions       []*Version\n\t\texpectedLength int\n\t\texpectedNil    bool\n\t\tcheckSorted    bool\n\t}{\n\t\t{\n\t\t\tname:           \"empty set returns nil\",\n\t\t\tignoreFormat:   false,\n\t\t\tversions:       nil,\n\t\t\texpectedNil:    true,\n\t\t\texpectedLength: 0,\n\t\t},\n\t\t{\n\t\t\tname:         \"set with versions returns sorted list\",\n\t\t\tignoreFormat: false,\n\t\t\tversions: []*Version{\n\t\t\t\tNew(\"2.0.0\", SemanticFormat),\n\t\t\t\tNew(\"1.0.0\", SemanticFormat),\n\t\t\t\tNew(\"3.0.0\", SemanticFormat),\n\t\t\t},\n\t\t\texpectedLength: 3,\n\t\t\tcheckSorted:    true,\n\t\t},\n\t\t{\n\t\t\tname:         \"set with nil versions map returns nil\",\n\t\t\tignoreFormat: false,\n\t\t\tversions:     []*Version{},\n\t\t\texpectedNil:  true,\n\t\t},\n\t\t{\n\t\t\tname:         \"set with single version\",\n\t\t\tignoreFormat: false,\n\t\t\tversions: []*Version{\n\t\t\t\tNew(\"1.0.0\", SemanticFormat),\n\t\t\t},\n\t\t\texpectedLength: 1,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ts := NewSet(tt.ignoreFormat, tt.versions...)\n\t\t\t// for testing nil versions map case\n\t\t\tif tt.name == \"set with nil versions map returns nil\" {\n\t\t\t\ts.versions = nil\n\t\t\t}\n\n\t\t\tresult := s.Values()\n\n\t\t\tif tt.expectedNil {\n\t\t\t\tassert.Nil(t, result)\n\t\t\t} else {\n\t\t\t\trequire.NotNil(t, result)\n\t\t\t\tassert.Equal(t, tt.expectedLength, len(result))\n\n\t\t\t\tif tt.checkSorted && len(result) > 1 {\n\t\t\t\t\t// verify sorting - versions should be in ascending order\n\t\t\t\t\tfor i := 0; i < len(result)-1; i++ {\n\t\t\t\t\t\tcmp, err := result[i].Compare(result[i+1])\n\t\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\t\tassert.True(t, cmp < 0, \"versions should be sorted in ascending order\")\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSet_Size(t *testing.T) {\n\ttests := []struct {\n\t\tname         string\n\t\tignoreFormat bool\n\t\tversions     []*Version\n\t\texpected     int\n\t}{\n\t\t{\n\t\t\tname:         \"empty set size is zero\",\n\t\t\tignoreFormat: false,\n\t\t\tversions:     nil,\n\t\t\texpected:     0,\n\t\t},\n\t\t{\n\t\t\tname:         \"set with versions\",\n\t\t\tignoreFormat: false,\n\t\t\tversions: []*Version{\n\t\t\t\tNew(\"1.0.0\", SemanticFormat),\n\t\t\t\tNew(\"2.0.0\", SemanticFormat),\n\t\t\t},\n\t\t\texpected: 2,\n\t\t},\n\t\t{\n\t\t\tname:         \"set with duplicate versions\",\n\t\t\tignoreFormat: false,\n\t\t\tversions: []*Version{\n\t\t\t\tNew(\"1.0.0\", SemanticFormat),\n\t\t\t\tNew(\"1.0.0\", SemanticFormat),\n\t\t\t},\n\t\t\texpected: 1,\n\t\t},\n\t\t{\n\t\t\tname:         \"set with nil versions map\",\n\t\t\tignoreFormat: false,\n\t\t\tversions:     []*Version{},\n\t\t\texpected:     0,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ts := NewSet(tt.ignoreFormat, tt.versions...)\n\t\t\t// for testing nil versions map case\n\t\t\tif tt.name == \"set with nil versions map\" {\n\t\t\t\ts.versions = nil\n\t\t\t}\n\n\t\t\tresult := s.Size()\n\t\t\tassert.Equal(t, tt.expected, result)\n\t\t})\n\t}\n}\n\nfunc TestSet_Clear(t *testing.T) {\n\ttests := []struct {\n\t\tname         string\n\t\tignoreFormat bool\n\t\tversions     []*Version\n\t}{\n\t\t{\n\t\t\tname:         \"clear non-empty set\",\n\t\t\tignoreFormat: false,\n\t\t\tversions: []*Version{\n\t\t\t\tNew(\"1.0.0\", SemanticFormat),\n\t\t\t\tNew(\"2.0.0\", SemanticFormat),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:         \"clear empty set\",\n\t\t\tignoreFormat: false,\n\t\t\tversions:     nil,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ts := NewSet(tt.ignoreFormat, tt.versions...)\n\n\t\t\toriginalSize := s.Size()\n\t\t\ts.Clear()\n\n\t\t\tassert.Equal(t, 0, s.Size())\n\t\t\tassert.NotNil(t, s.versions) // should have empty map, not nil\n\n\t\t\t// verify all previous versions are gone\n\t\t\tif originalSize > 0 {\n\t\t\t\tfor _, v := range tt.versions {\n\t\t\t\t\tif v != nil {\n\t\t\t\t\t\tassert.False(t, s.Contains(v))\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSet_Integration(t *testing.T) {\n\t// test combining multiple operations\n\ts := NewSet(false)\n\n\tv1 := New(\"1.0.0\", SemanticFormat)\n\tv2 := New(\"2.0.0\", SemanticFormat)\n\tv3 := New(\"3.0.0\", SemanticFormat)\n\n\t// add versions\n\ts.Add(v1, v2, v3)\n\tassert.Equal(t, 3, s.Size())\n\n\t// check contains\n\tassert.True(t, s.Contains(v1))\n\tassert.True(t, s.Contains(v2))\n\tassert.True(t, s.Contains(v3))\n\n\t// remove one version\n\ts.Remove(v2)\n\tassert.Equal(t, 2, s.Size())\n\tassert.False(t, s.Contains(v2))\n\n\t// get values\n\tvalues := s.Values()\n\trequire.Len(t, values, 2)\n\n\t// verify sorting\n\tcmp, err := values[0].Compare(values[1])\n\trequire.NoError(t, err)\n\tassert.True(t, cmp < 0)\n\n\t// clear all\n\ts.Clear()\n\tassert.Equal(t, 0, s.Size())\n\tassert.Nil(t, s.Values())\n}\n"
  },
  {
    "path": "grype/version/version.go",
    "content": "package version\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n)\n\nvar _ Comparator = (*Version)(nil)\n\ntype Version struct {\n\tRaw         string\n\tFormat      Format\n\tcomparators map[Format]Comparator\n\tConfig      ComparisonConfig\n}\n\n// New creates a new Version with the default comparison configuration.\n// The default uses no explicit MissingEpochStrategy, which preserves the native\n// behavior of each format's comparator (RPM ignores one-sided epochs, DEB treats\n// missing as 0). Use NewWithConfig to explicitly control epoch handling.\nfunc New(raw string, format Format) *Version {\n\treturn NewWithConfig(raw, format, ComparisonConfig{})\n}\n\n// NewWithConfig creates a new Version with a specific comparison configuration.\n// This allows control over how missing epochs are handled during version comparison.\nfunc NewWithConfig(raw string, format Format, cfg ComparisonConfig) *Version {\n\treturn &Version{\n\t\tRaw:    raw,\n\t\tFormat: format,\n\t\tConfig: cfg,\n\t}\n}\n\nfunc (v *Version) Validate() error {\n\t_, err := v.getComparator(v.Format)\n\treturn err\n}\n\n//nolint:funlen\nfunc (v *Version) getComparator(format Format) (Comparator, error) {\n\tif v.comparators == nil {\n\t\tv.comparators = make(map[Format]Comparator)\n\t}\n\tif comparator, ok := v.comparators[format]; ok {\n\t\treturn comparator, nil\n\t}\n\n\tvar comparator Comparator\n\tvar err error\n\tswitch format {\n\tcase SemanticFormat:\n\t\t// not enforcing strict semver here, so that we can parse versions like \"v1.0.0\", \"1.0\", or \"1.0a\", which aren't strictly semver compliant\n\t\tcomparator, err = newSemanticVersion(v.Raw, false)\n\tcase ApkFormat:\n\t\tcomparator, err = newApkVersion(v.Raw)\n\tcase BitnamiFormat:\n\t\tcomparator, err = newBitnamiVersion(v.Raw)\n\tcase DebFormat:\n\t\tcomparator, err = newDebVersion(v.Raw)\n\tcase GolangFormat:\n\t\tcomparator, err = newGolangVersion(v.Raw)\n\tcase MavenFormat:\n\t\tcomparator, err = newMavenVersion(v.Raw)\n\tcase RpmFormat:\n\t\tcomparator, err = newRpmVersion(v.Raw)\n\tcase PythonFormat:\n\t\tcomparator, err = newPep440Version(v.Raw)\n\tcase KBFormat:\n\t\tcomparator = newKBVersion(v.Raw)\n\tcase GemFormat:\n\t\tcomparator, err = newGemVersion(v.Raw)\n\tcase PortageFormat:\n\t\tcomparator = newPortageVersion(v.Raw)\n\tcase JVMFormat:\n\t\tcomparator, err = newJvmVersion(v.Raw)\n\tcase PacmanFormat:\n\t\tcomparator, err = newPacmanVersion(v.Raw)\n\tcase UnknownFormat:\n\t\tcomparator, err = newFuzzyVersion(v.Raw)\n\tdefault:\n\t\terr = fmt.Errorf(\"no comparator available for format %q\", v.Format)\n\t}\n\n\tv.comparators[format] = comparator\n\n\treturn comparator, err\n}\nfunc (v Version) String() string {\n\treturn fmt.Sprintf(\"%s (%s)\", v.Raw, v.Format)\n}\n\n// Compare compares this version to another version.\n// This returns -1, 0, or 1 if this version is smaller,\n// equal, or larger than the other version, respectively.\nfunc (v Version) Compare(other *Version) (int, error) {\n\tif other == nil {\n\t\treturn -1, ErrNoVersionProvided\n\t}\n\n\tvar result int\n\tcomparator, err := v.getComparator(v.Format)\n\tif err == nil {\n\t\t// if the package version, v was able to compare without error, return the result\n\t\tresult, err = comparator.Compare(other)\n\t\tif err == nil {\n\t\t\t// no error returned for package version or db version, return the result\n\t\t\treturn result, nil\n\t\t}\n\t}\n\t// we were unable to parse the package or db version as v.Format, try other.Format if they differ\n\tif v.Format != other.Format {\n\t\toriginalErr := err\n\t\tcomparator, err = v.getComparator(other.Format)\n\t\tif err == nil {\n\t\t\tresult, err = comparator.Compare(other)\n\t\t\tif err == nil {\n\t\t\t\treturn result, nil\n\t\t\t}\n\t\t}\n\t\terr = errors.Join(originalErr, err)\n\t}\n\n\t// all formats returned error, return all errors\n\treturn 0, fmt.Errorf(\"unable to compare versions: %v %v due to %w\", v, other, err)\n}\n\nfunc (v *Version) Is(op Operator, other *Version) (bool, error) {\n\tif v == nil {\n\t\treturn false, fmt.Errorf(\"cannot evaluate version with nil version\")\n\t}\n\tif other == nil {\n\t\treturn false, ErrNoVersionProvided\n\t}\n\n\tcomparator, err := v.getComparator(v.Format)\n\tif err != nil {\n\t\treturn false, fmt.Errorf(\"unable to get comparator for %s: %w\", v.Format, err)\n\t}\n\n\tresult, err := comparator.Compare(other)\n\tif err != nil {\n\t\treturn false, fmt.Errorf(\"unable to compare versions %s and %s: %w\", v, other, err)\n\t}\n\n\tswitch op {\n\tcase EQ, \"\":\n\t\treturn result == 0, nil\n\tcase GT:\n\t\treturn result > 0, nil\n\tcase LT:\n\t\treturn result < 0, nil\n\tcase GTE:\n\t\treturn result >= 0, nil\n\tcase LTE:\n\t\treturn result <= 0, nil\n\t}\n\treturn false, fmt.Errorf(\"unknown operator %s\", op)\n}\n"
  },
  {
    "path": "grype/version/version_test.go",
    "content": "package version\n\nimport (\n\t\"slices\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestVersionCompare(t *testing.T) {\n\ttests := []struct {\n\t\tname           string\n\t\tv1             string\n\t\tv2             string\n\t\texpectedResult int\n\t\texpectErr      require.ErrorAssertionFunc\n\t}{\n\t\t{\n\t\t\tname:           \"v1 greater than v2\",\n\t\t\tv1:             \"2.0.0\",\n\t\t\tv2:             \"1.0.0\",\n\t\t\texpectedResult: 1,\n\t\t},\n\t\t{\n\t\t\tname:           \"v1 less than v2\",\n\t\t\tv1:             \"1.0.0\",\n\t\t\tv2:             \"2.0.0\",\n\t\t\texpectedResult: -1,\n\t\t},\n\t\t{\n\t\t\tname:           \"v1 equal to v2\",\n\t\t\tv1:             \"1.0.0\",\n\t\t\tv2:             \"1.0.0\",\n\t\t\texpectedResult: 0,\n\t\t},\n\t\t{\n\t\t\tname:           \"compare with nil version\",\n\t\t\tv1:             \"1.0.0\",\n\t\t\tv2:             \"\",\n\t\t\texpectedResult: -1,\n\t\t\texpectErr:      require.Error,\n\t\t},\n\t}\n\n\t// the above test cases are pretty tame value-wise, so we can use (almost) all formats\n\tvar formats []Format\n\tformats = append(formats, Formats...)\n\n\t// leave out some formats...\n\tslices.DeleteFunc(formats, func(f Format) bool {\n\t\treturn f == KBFormat\n\t})\n\n\tfor _, format := range formats {\n\t\tt.Run(format.String(), func(t *testing.T) {\n\t\t\tfor _, tc := range tests {\n\t\t\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\t\t\tif tc.expectErr == nil {\n\t\t\t\t\t\ttc.expectErr = require.NoError\n\t\t\t\t\t}\n\t\t\t\t\tv1 := New(tc.v1, format)\n\t\t\t\t\trequire.Equal(t, format, v1.Format)\n\n\t\t\t\t\tvar v2 *Version\n\t\t\t\t\tif tc.v2 != \"\" {\n\t\t\t\t\t\tv2 = New(tc.v2, format)\n\t\t\t\t\t\trequire.Equal(t, format, v2.Format)\n\t\t\t\t\t}\n\n\t\t\t\t\tresult, err := v1.Compare(v2)\n\t\t\t\t\ttc.expectErr(t, err, \"unexpected error during comparison\")\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn // skip further checks if there was an error\n\t\t\t\t\t}\n\n\t\t\t\t\tassert.NoError(t, err, \"unexpected error during comparison\")\n\t\t\t\t\tassert.Equal(t, tc.expectedResult, result, \"comparison result mismatch\")\n\t\t\t\t})\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestVersion_UpgradeUnknownRightSideComparison(t *testing.T) {\n\tv1 := New(\"1.0.0\", SemanticFormat)\n\n\t// test if we can upgrade an unknown format to a known format when the left hand side is known\n\tv2 := New(\"1.0.0\", UnknownFormat)\n\n\tresult, err := v1.Compare(v2)\n\tassert.NoError(t, err)\n\tassert.Equal(t, 0, result, \"versions should be equal after format conversion\")\n}\n\nfunc TestVersionCompareSameFormat(t *testing.T) {\n\tformats := []struct {\n\t\tname   string\n\t\tformat Format\n\t}{\n\t\t{\"Semantic\", SemanticFormat},\n\t\t{\"APK\", ApkFormat},\n\t\t{\"Deb\", DebFormat},\n\t\t{\"Golang\", GolangFormat},\n\t\t{\"Maven\", MavenFormat},\n\t\t{\"RPM\", RpmFormat},\n\t\t{\"Python\", PythonFormat},\n\t\t{\"KB\", KBFormat},\n\t\t{\"Gem\", GemFormat},\n\t\t{\"Portage\", PortageFormat},\n\t\t{\"JVM\", JVMFormat},\n\t\t{\"Unknown\", UnknownFormat},\n\t}\n\n\tfor _, fmt := range formats {\n\t\tt.Run(fmt.name, func(t *testing.T) {\n\t\t\t// just test that we can create and compare versions of this format\n\t\t\t// without errors - not testing the actual comparison logic\n\t\t\tv1 := New(\"1.0.0\", fmt.format)\n\t\t\tv2 := New(\"1.0.0\", fmt.format)\n\n\t\t\tresult, err := v1.Compare(v2)\n\t\t\tassert.NoError(t, err, \"comparison error\")\n\t\t\tassert.Equal(t, 0, result, \"equal versions should return 0\")\n\t\t})\n\t}\n}\n\nfunc TestVersion_Is(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tversion  *Version\n\t\toperator Operator\n\t\tother    *Version\n\t\texpected bool\n\t\twantErr  require.ErrorAssertionFunc\n\t}{\n\t\t{\n\t\t\tname:     \"equal versions - EQ operator\",\n\t\t\tversion:  New(\"1.0.0\", SemanticFormat),\n\t\t\toperator: EQ,\n\t\t\tother:    New(\"1.0.0\", SemanticFormat),\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"equal versions - empty operator (defaults to EQ)\",\n\t\t\tversion:  New(\"1.0.0\", SemanticFormat),\n\t\t\toperator: \"\",\n\t\t\tother:    New(\"1.0.0\", SemanticFormat),\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"unequal versions - EQ operator\",\n\t\t\tversion:  New(\"1.0.0\", SemanticFormat),\n\t\t\toperator: EQ,\n\t\t\tother:    New(\"2.0.0\", SemanticFormat),\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"greater than - GT operator true\",\n\t\t\tversion:  New(\"2.0.0\", SemanticFormat),\n\t\t\toperator: GT,\n\t\t\tother:    New(\"1.0.0\", SemanticFormat),\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"greater than - GT operator false\",\n\t\t\tversion:  New(\"1.0.0\", SemanticFormat),\n\t\t\toperator: GT,\n\t\t\tother:    New(\"2.0.0\", SemanticFormat),\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"greater than or equal - GTE operator true (greater)\",\n\t\t\tversion:  New(\"2.0.0\", SemanticFormat),\n\t\t\toperator: GTE,\n\t\t\tother:    New(\"1.0.0\", SemanticFormat),\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"greater than or equal - GTE operator true (equal)\",\n\t\t\tversion:  New(\"1.0.0\", SemanticFormat),\n\t\t\toperator: GTE,\n\t\t\tother:    New(\"1.0.0\", SemanticFormat),\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"greater than or equal - GTE operator false\",\n\t\t\tversion:  New(\"1.0.0\", SemanticFormat),\n\t\t\toperator: GTE,\n\t\t\tother:    New(\"2.0.0\", SemanticFormat),\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"less than - LT operator true\",\n\t\t\tversion:  New(\"1.0.0\", SemanticFormat),\n\t\t\toperator: LT,\n\t\t\tother:    New(\"2.0.0\", SemanticFormat),\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"less than - LT operator false\",\n\t\t\tversion:  New(\"2.0.0\", SemanticFormat),\n\t\t\toperator: LT,\n\t\t\tother:    New(\"1.0.0\", SemanticFormat),\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"less than or equal - LTE operator true (less)\",\n\t\t\tversion:  New(\"1.0.0\", SemanticFormat),\n\t\t\toperator: LTE,\n\t\t\tother:    New(\"2.0.0\", SemanticFormat),\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"less than or equal - LTE operator true (equal)\",\n\t\t\tversion:  New(\"1.0.0\", SemanticFormat),\n\t\t\toperator: LTE,\n\t\t\tother:    New(\"1.0.0\", SemanticFormat),\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"less than or equal - LTE operator false\",\n\t\t\tversion:  New(\"2.0.0\", SemanticFormat),\n\t\t\toperator: LTE,\n\t\t\tother:    New(\"1.0.0\", SemanticFormat),\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"nil other version should return ErrNoVersionProvided\",\n\t\t\tversion:  New(\"1.0.0\", SemanticFormat),\n\t\t\toperator: EQ,\n\t\t\tother:    nil,\n\t\t\twantErr:  require.Error,\n\t\t},\n\t\t{\n\t\t\tname:     \"unknown operator should return error\",\n\t\t\tversion:  New(\"1.0.0\", SemanticFormat),\n\t\t\toperator: \"unknown\",\n\t\t\tother:    New(\"1.0.0\", SemanticFormat),\n\t\t\twantErr:  require.Error,\n\t\t},\n\t\t{\n\t\t\tname:     \"invalid version format should return error\",\n\t\t\tversion:  New(\"invalid\", SemanticFormat),\n\t\t\toperator: EQ,\n\t\t\tother:    New(\"1.0.0\", SemanticFormat),\n\t\t\twantErr:  require.Error,\n\t\t},\n\t\t{\n\t\t\tname:     \"different formats - semantic vs apk\",\n\t\t\tversion:  New(\"1.0.0\", SemanticFormat),\n\t\t\toperator: EQ,\n\t\t\tother:    New(\"1.0.0\", ApkFormat),\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"complex semantic versions\",\n\t\t\tversion:  New(\"1.2.3-alpha.1\", SemanticFormat),\n\t\t\toperator: LT,\n\t\t\tother:    New(\"1.2.3\", SemanticFormat),\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"version with v prefix\",\n\t\t\tversion:  New(\"v1.0.0\", SemanticFormat),\n\t\t\toperator: EQ,\n\t\t\tother:    New(\"1.0.0\", SemanticFormat),\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"nil other version is ErrNoVersionProvided\",\n\t\t\tversion:  New(\"1.0.0\", SemanticFormat),\n\t\t\toperator: EQ,\n\t\t\tother:    nil,\n\t\t\twantErr: func(t require.TestingT, err error, a ...interface{}) {\n\t\t\t\trequire.ErrorIs(t, err, ErrNoVersionProvided, a...)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:     \"unknown operator error\",\n\t\t\tversion:  New(\"1.0.0\", SemanticFormat),\n\t\t\toperator: \"!@#\",\n\t\t\tother:    New(\"1.0.0\", SemanticFormat),\n\t\t\twantErr: func(t require.TestingT, err error, a ...interface{}) {\n\t\t\t\trequire.ErrorContains(t, err, \"unknown operator !@#\", a...)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:     \"invalid version format error contains format\",\n\t\t\tversion:  New(\"not-a-valid-version\", SemanticFormat),\n\t\t\toperator: EQ,\n\t\t\tother:    New(\"1.0.0\", SemanticFormat),\n\t\t\twantErr: func(t require.TestingT, err error, a ...interface{}) {\n\t\t\t\trequire.ErrorContains(t, err, \"unable to get comparator for Semantic\", a...)\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif tt.wantErr == nil {\n\t\t\t\ttt.wantErr = require.NoError\n\t\t\t}\n\n\t\t\tresult, err := tt.version.Is(tt.operator, tt.other)\n\t\t\ttt.wantErr(t, err)\n\n\t\t\tif err != nil {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.Equal(t, tt.expected, result)\n\t\t})\n\t}\n}\n\nfunc TestVersion_Is_AllOperators(t *testing.T) {\n\tv1 := New(\"1.0.0\", SemanticFormat)\n\tv2 := New(\"2.0.0\", SemanticFormat)\n\tv1dup := New(\"1.0.0\", SemanticFormat)\n\n\ttests := []struct {\n\t\tname     string\n\t\tleft     *Version\n\t\toperator Operator\n\t\tright    *Version\n\t\texpected bool\n\t}{\n\t\t// v1 (1.0.0) vs v2 (2.0.0)\n\t\t{\"1.0.0 = 2.0.0\", v1, EQ, v2, false},\n\t\t{\"1.0.0 > 2.0.0\", v1, GT, v2, false},\n\t\t{\"1.0.0 >= 2.0.0\", v1, GTE, v2, false},\n\t\t{\"1.0.0 < 2.0.0\", v1, LT, v2, true},\n\t\t{\"1.0.0 <= 2.0.0\", v1, LTE, v2, true},\n\n\t\t// v2 (2.0.0) vs v1 (1.0.0)\n\t\t{\"2.0.0 = 1.0.0\", v2, EQ, v1, false},\n\t\t{\"2.0.0 > 1.0.0\", v2, GT, v1, true},\n\t\t{\"2.0.0 >= 1.0.0\", v2, GTE, v1, true},\n\t\t{\"2.0.0 < 1.0.0\", v2, LT, v1, false},\n\t\t{\"2.0.0 <= 1.0.0\", v2, LTE, v1, false},\n\n\t\t// v1 (1.0.0) vs v1dup (1.0.0)\n\t\t{\"1.0.0 = 1.0.0\", v1, EQ, v1dup, true},\n\t\t{\"1.0.0 > 1.0.0\", v1, GT, v1dup, false},\n\t\t{\"1.0.0 >= 1.0.0\", v1, GTE, v1dup, true},\n\t\t{\"1.0.0 < 1.0.0\", v1, LT, v1dup, false},\n\t\t{\"1.0.0 <= 1.0.0\", v1, LTE, v1dup, true},\n\n\t\t// empty operator should default to EQ\n\t\t{\"1.0.0 (empty) 1.0.0\", v1, \"\", v1dup, true},\n\t\t{\"1.0.0 (empty) 2.0.0\", v1, \"\", v2, false},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult, err := tt.left.Is(tt.operator, tt.right)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, tt.expected, result)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/vex/csaf/csaf.go",
    "content": "package csaf\n\nimport (\n\t\"slices\"\n\n\t\"github.com/gocsaf/csaf/v3/csaf\"\n)\n\n// advisoryMatch captures the criteria that caused a vulnerability to match a CSAF advisory\ntype advisoryMatch struct {\n\tVulnerability *csaf.Vulnerability\n\tStatus        status\n\tProductID     csaf.ProductID\n}\n\n// cve returns the CVE of the vulnerability that matched\nfunc (m *advisoryMatch) cve() string {\n\tif m == nil || m.Vulnerability == nil || m.Vulnerability.CVE == nil {\n\t\treturn \"\"\n\t}\n\n\treturn string(*m.Vulnerability.CVE)\n}\n\n// statement returns the statement of the vulnerability that matched\nfunc (m *advisoryMatch) statement() string {\n\tif m == nil || m.Vulnerability == nil {\n\t\treturn \"\"\n\t}\n\n\t// an impact statement SHALL exist as machine readable flag in /vulnerabilities[]/flags (...)\n\tfor _, flag := range m.Vulnerability.Flags {\n\t\tif flag == nil || flag.ProductIds == nil || flag.Label == nil {\n\t\t\tcontinue\n\t\t}\n\t\tfor _, pID := range *flag.ProductIds {\n\t\t\tif pID == nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif *pID == m.ProductID {\n\t\t\t\treturn string(*flag.Label)\n\t\t\t}\n\t\t}\n\t}\n\t// (...) or as human readable justification in /vulnerabilities[]/threats\n\tfor _, th := range m.Vulnerability.Threats {\n\t\tif th == nil || th.Category == nil || th.Details == nil {\n\t\t\tcontinue\n\t\t}\n\t\tif *th.Category != csaf.CSAFThreatCategoryImpact {\n\t\t\tcontinue\n\t\t}\n\t\tfor _, pID := range *th.ProductIds {\n\t\t\tif pID == nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif *pID == m.ProductID {\n\t\t\t\treturn *th.Details\n\t\t\t}\n\t\t}\n\t}\n\n\treturn \"\"\n}\n\ntype advisories []*csaf.Advisory\n\n// matches returns the first CSAF advisory to match for a given vulnerability ID and package URL\n//\n//nolint:gocognit\nfunc (advisories advisories) matches(vulnID, purl string) *advisoryMatch {\n\tfor _, adv := range advisories {\n\t\tif adv == nil || adv.Vulnerabilities == nil {\n\t\t\tcontinue\n\t\t}\n\n\t\t// Auxiliary function to find in the advisory the 1st product ID that matches a given pURL\n\t\tfindProductID := func(products csaf.Products, purl string) csaf.ProductID {\n\t\t\tfor _, p := range products {\n\t\t\t\tif p == nil {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif slices.Contains(purlsFromProductIdentificationHelpers(adv.ProductTree.CollectProductIdentificationHelpers(*p)), purl) {\n\t\t\t\t\treturn *p\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn \"\"\n\t\t}\n\n\t\tfor _, vuln := range adv.Vulnerabilities {\n\t\t\tif vuln == nil || vuln.CVE == nil || string(*vuln.CVE) != vulnID {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tproductsByStatus := map[status]*csaf.Products{\n\t\t\t\tfirstAffected:      vuln.ProductStatus.FirstAffected,\n\t\t\t\tfirstFixed:         vuln.ProductStatus.FirstFixed,\n\t\t\t\tfixed:              vuln.ProductStatus.Fixed,\n\t\t\t\tknownAffected:      vuln.ProductStatus.KnownAffected,\n\t\t\t\tknownNotAffected:   vuln.ProductStatus.KnownNotAffected,\n\t\t\t\tlastAffected:       vuln.ProductStatus.LastAffected,\n\t\t\t\trecommended:        vuln.ProductStatus.Recommended,\n\t\t\t\tunderInvestigation: vuln.ProductStatus.UnderInvestigation,\n\t\t\t}\n\t\t\tfor status, products := range productsByStatus {\n\t\t\t\tif products == nil {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif productID := findProductID(*products, purl); productID != \"\" {\n\t\t\t\t\treturn &advisoryMatch{vuln, status, productID}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// purlsFromProductIdentificationHelpers returns a slice of PackageURLs (string format) given a slice of ProductIdentificationHelpers.\nfunc purlsFromProductIdentificationHelpers(helpers []*csaf.ProductIdentificationHelper) []string {\n\tvar purls []string\n\tfor _, helper := range helpers {\n\t\tif helper == nil || helper.PURL == nil {\n\t\t\tcontinue\n\t\t}\n\t\tpurls = append(purls, string(*helper.PURL))\n\t}\n\treturn purls\n}\n"
  },
  {
    "path": "grype/vex/csaf/csaf_test.go",
    "content": "package csaf\n\nimport (\n\t\"reflect\"\n\t\"testing\"\n\n\t\"github.com/gocsaf/csaf/v3/csaf\"\n)\n\nfunc Test_advisoryMatch_statement(t *testing.T) {\n\ttype fields struct {\n\t\tVulnerability *csaf.Vulnerability\n\t\tProductID     csaf.ProductID\n\t}\n\ttests := []struct {\n\t\tname   string\n\t\tfields fields\n\t\twant   string\n\t}{\n\t\t{\n\t\t\tname: \"no vulnerability\",\n\t\t\tfields: fields{\n\t\t\t\tVulnerability: nil,\n\t\t\t\tProductID:     \"SPB-00260\",\n\t\t\t},\n\t\t\twant: \"\",\n\t\t},\n\t\t{\n\t\t\tname: \"no flags or threats\",\n\t\t\tfields: fields{\n\t\t\t\tVulnerability: &csaf.Vulnerability{\n\t\t\t\t\tCVE: &[]csaf.CVE{\"CVE-1234-5678\"}[0],\n\t\t\t\t},\n\t\t\t\tProductID: \"SPB-00260\",\n\t\t\t},\n\t\t\twant: \"\",\n\t\t},\n\t\t{\n\t\t\tname: \"flag with label\",\n\t\t\tfields: fields{\n\t\t\t\tVulnerability: &csaf.Vulnerability{\n\t\t\t\t\tCVE: &[]csaf.CVE{\"CVE-1234-5678\"}[0],\n\t\t\t\t\tFlags: []*csaf.Flag{{\n\t\t\t\t\t\tProductIds: &csaf.Products{&[]csaf.ProductID{\"SPB-00260\"}[0]},\n\t\t\t\t\t\tLabel:      &[]csaf.FlagLabel{\"vulnerable_code_not_present\"}[0],\n\t\t\t\t\t}},\n\t\t\t\t},\n\t\t\t\tProductID: \"SPB-00260\",\n\t\t\t},\n\t\t\twant: \"vulnerable_code_not_present\",\n\t\t},\n\t\t{\n\t\t\tname: \"flag with label, different product ID\",\n\t\t\tfields: fields{\n\t\t\t\tVulnerability: &csaf.Vulnerability{\n\t\t\t\t\tCVE: &[]csaf.CVE{\"CVE-1234-5678\"}[0],\n\t\t\t\t\tFlags: []*csaf.Flag{{\n\t\t\t\t\t\tProductIds: &csaf.Products{&[]csaf.ProductID{\"SPB-00260\"}[0]},\n\t\t\t\t\t\tLabel:      &[]csaf.FlagLabel{\"vulnerable_code_not_present\"}[0],\n\t\t\t\t\t}},\n\t\t\t\t},\n\t\t\t\tProductID: \"SPB-00261\",\n\t\t\t},\n\t\t\twant: \"\",\n\t\t},\n\t\t{\n\t\t\tname: \"threat with details\",\n\t\t\tfields: fields{\n\t\t\t\tVulnerability: &csaf.Vulnerability{\n\t\t\t\t\tCVE: &[]csaf.CVE{\"CVE-1234-5678\"}[0],\n\t\t\t\t\tThreats: []*csaf.Threat{{\n\t\t\t\t\t\tCategory:   &[]csaf.ThreatCategory{csaf.CSAFThreatCategoryImpact}[0],\n\t\t\t\t\t\tDetails:    &[]string{\"Class with vulnerable code was removed before shipping\"}[0],\n\t\t\t\t\t\tProductIds: &csaf.Products{&[]csaf.ProductID{\"SPB-00260\"}[0]},\n\t\t\t\t\t}},\n\t\t\t\t},\n\t\t\t\tProductID: \"SPB-00260\",\n\t\t\t},\n\t\t\twant: \"Class with vulnerable code was removed before shipping\",\n\t\t},\n\t\t{\n\t\t\tname: \"threat with details, different product ID\",\n\t\t\tfields: fields{\n\t\t\t\tVulnerability: &csaf.Vulnerability{\n\t\t\t\t\tCVE: &[]csaf.CVE{\"CVE-1234-5678\"}[0],\n\t\t\t\t\tThreats: []*csaf.Threat{{\n\t\t\t\t\t\tCategory:   &[]csaf.ThreatCategory{csaf.CSAFThreatCategoryImpact}[0],\n\t\t\t\t\t\tDetails:    &[]string{\"Class with vulnerable code was removed before shipping\"}[0],\n\t\t\t\t\t\tProductIds: &csaf.Products{&[]csaf.ProductID{\"SPB-00260\"}[0]},\n\t\t\t\t\t}},\n\t\t\t\t},\n\t\t\t\tProductID: \"SPB-00261\",\n\t\t\t},\n\t\t\twant: \"\",\n\t\t},\n\t}\n\tt.Parallel()\n\tfor _, testToRun := range tests {\n\t\ttest := testToRun\n\t\tt.Run(test.name, func(tt *testing.T) {\n\t\t\ttt.Parallel()\n\t\t\tm := &advisoryMatch{\n\t\t\t\tVulnerability: test.fields.Vulnerability,\n\t\t\t\tProductID:     test.fields.ProductID,\n\t\t\t}\n\t\t\tif got := m.statement(); got != test.want {\n\t\t\t\ttt.Errorf(\"advisoryMatch.statement() = %v, want %v\", got, test.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_advisories_matches(t *testing.T) {\n\tsampleAdv := &csaf.Advisory{\n\t\tProductTree: &csaf.ProductTree{\n\t\t\tBranches: csaf.Branches{\n\t\t\t\t&[]csaf.Branch{{\n\t\t\t\t\tBranches: csaf.Branches{\n\t\t\t\t\t\t&[]csaf.Branch{{\n\t\t\t\t\t\t\tCategory: &[]csaf.BranchCategory{csaf.CSAFBranchCategoryProductVersion}[0],\n\t\t\t\t\t\t\tName:     &[]string{\"2.6.0\"}[0],\n\t\t\t\t\t\t\tProduct: &csaf.FullProductName{\n\t\t\t\t\t\t\t\tName:      &[]string{\"Spring Boot 2.6.0\"}[0],\n\t\t\t\t\t\t\t\tProductID: &[]csaf.ProductID{\"SPB-00260\"}[0],\n\t\t\t\t\t\t\t\tProductIdentificationHelper: &[]csaf.ProductIdentificationHelper{{\n\t\t\t\t\t\t\t\t\tPURL: &[]csaf.PURL{\"pkg:apk/alpine/libssl3@3.0.8-r3\"}[0],\n\t\t\t\t\t\t\t\t}}[0],\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t}}[0],\n\t\t\t\t\t},\n\t\t\t\t\tCategory: &[]csaf.BranchCategory{csaf.CSAFBranchCategoryProductName}[0],\n\t\t\t\t\tName:     &[]string{\"Spring\"}[0],\n\t\t\t\t}}[0],\n\t\t\t},\n\t\t},\n\t\tVulnerabilities: []*csaf.Vulnerability{{\n\t\t\tCVE: &[]csaf.CVE{\"CVE-1234-5678\"}[0],\n\t\t\tProductStatus: &[]csaf.ProductStatus{{\n\t\t\t\tKnownNotAffected: &csaf.Products{\n\t\t\t\t\t&[]csaf.ProductID{\"SPB-00260\"}[0],\n\t\t\t\t},\n\t\t\t}}[0],\n\t\t}},\n\t}\n\n\ttype args struct {\n\t\tvulnID string\n\t\tpurl   string\n\t}\n\ttests := []struct {\n\t\tname       string\n\t\tadvisories advisories\n\t\targs       args\n\t\twant       *advisoryMatch\n\t}{\n\t\t{\n\t\t\tname:       \"no advisories\",\n\t\t\tadvisories: advisories{},\n\t\t\targs:       args{vulnID: \"CVE-1234-5678\", purl: \"pkg:apk/alpine/libssl3@3.0.8-r3\"},\n\t\t\twant:       nil,\n\t\t},\n\t\t{\n\t\t\tname:       \"no matching advisory\",\n\t\t\tadvisories: advisories{sampleAdv},\n\t\t\targs:       args{vulnID: \"CVE-1234-5678\", purl: \"pkg:apk/alpine/libcrypto3@3.0.8-r3\"},\n\t\t\twant:       nil,\n\t\t},\n\t\t{\n\t\t\tname:       \"advisory matches vulnerability for given pURL\",\n\t\t\tadvisories: advisories{sampleAdv},\n\t\t\targs:       args{vulnID: \"CVE-1234-5678\", purl: \"pkg:apk/alpine/libssl3@3.0.8-r3\"},\n\t\t\twant: &advisoryMatch{\n\t\t\t\tVulnerability: &csaf.Vulnerability{\n\t\t\t\t\tCVE: &[]csaf.CVE{\"CVE-1234-5678\"}[0],\n\t\t\t\t\tProductStatus: &[]csaf.ProductStatus{{\n\t\t\t\t\t\tKnownNotAffected: &csaf.Products{\n\t\t\t\t\t\t\t&[]csaf.ProductID{\"SPB-00260\"}[0],\n\t\t\t\t\t\t},\n\t\t\t\t\t}}[0],\n\t\t\t\t},\n\t\t\t\tProductID: \"SPB-00260\",\n\t\t\t\tStatus:    knownNotAffected,\n\t\t\t},\n\t\t},\n\t}\n\tt.Parallel()\n\tfor _, testToRun := range tests {\n\t\ttest := testToRun\n\t\tt.Run(test.name, func(tt *testing.T) {\n\t\t\ttt.Parallel()\n\t\t\tif got := test.advisories.matches(test.args.vulnID, test.args.purl); !reflect.DeepEqual(got, test.want) {\n\t\t\t\ttt.Errorf(\"advisories.matches() = %v, want %v\", got, test.want)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/vex/csaf/implementation.go",
    "content": "package csaf\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"slices\"\n\t\"time\"\n\n\t\"github.com/gocsaf/csaf/v3/csaf\"\n\n\t\"github.com/anchore/grype/grype/match\"\n\t\"github.com/anchore/grype/grype/pkg\"\n\tvexStatus \"github.com/anchore/grype/grype/vex/status\"\n)\n\n// searchedBy captures the parameters used to search through the VEX data\ntype searchedBy struct {\n\tVulnerability string\n\tPurl          string\n}\n\ntype Processor struct{}\n\nfunc New() *Processor {\n\treturn &Processor{}\n}\n\n// IsCSAF checks if the provided document is a CSAF document\nfunc IsCSAF(document string) bool {\n\tif _, err := csaf.LoadAdvisory(document); err == nil {\n\t\treturn true\n\t}\n\treturn false\n}\n\n// ReadVexDocuments reads different files and creates a collection of advisories based on them.\nfunc (*Processor) ReadVexDocuments(docs []string) (interface{}, error) {\n\tvar advs advisories\n\n\tfor _, doc := range docs {\n\t\tadv, err := csaf.LoadAdvisory(doc)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"error loading VEX CSAF document: %w\", err)\n\t\t}\n\t\tadvs = append(advs, adv)\n\t}\n\n\tslices.SortStableFunc(advs, newerCurrentReleaseDateFirst)\n\n\treturn advs, nil\n}\n\n// newerCurrentReleaseDateFirst compares csaf.Advisories by the document.Tracking.CurrentReleaseDate\n// field, treating newer dates as earlier values, and nil and invalid dates as later than\n// all valid dates\nfunc newerCurrentReleaseDateFirst(a, b *csaf.Advisory) int {\n\tparseDate := func(datePtr *string) (time.Time, bool) {\n\t\tif datePtr == nil {\n\t\t\treturn time.Time{}, false\n\t\t}\n\t\tt, err := time.Parse(time.RFC3339, *datePtr)\n\t\treturn t, err == nil\n\t}\n\n\taT, aValid := parseDate(a.Document.Tracking.CurrentReleaseDate)\n\tbT, bValid := parseDate(b.Document.Tracking.CurrentReleaseDate)\n\n\tswitch {\n\tcase !aValid && !bValid:\n\t\treturn 0\n\tcase !bValid:\n\t\treturn -1\n\tcase !aValid:\n\t\treturn 1\n\tdefault:\n\t\treturn bT.Compare(aT) // newer first\n\t}\n}\n\n// FilterMatches takes a set of scanning results and moves any results marked in\n// the VEX data as fixed or not_affected to the ignored list.\nfunc (*Processor) FilterMatches(\n\tdocRaw interface{}, ignoreRules []match.IgnoreRule, _ *pkg.Context, matches *match.Matches, ignoredMatches []match.IgnoredMatch,\n) (*match.Matches, []match.IgnoredMatch, error) {\n\tadvisories, ok := docRaw.(advisories)\n\tif !ok {\n\t\treturn nil, nil, errors.New(\"unable to cast vex document as CSAF Advisories\")\n\t}\n\n\tremainingMatches := match.NewMatches()\n\tfor _, m := range matches.Sorted() {\n\t\t// Seek if our advisories have information about a vulnerability affecting\n\t\t// the product for which we have a match.\n\t\tadvMatch := advisories.matches(m.Vulnerability.ID, m.Package.PURL)\n\t\tif advMatch == nil {\n\t\t\tremainingMatches.Add(m)\n\t\t\tcontinue\n\t\t}\n\n\t\t// Filtering only applies to not_affected and fixed statuses\n\t\tif !matchesVexStatus(advMatch.Status, vexStatus.NotAffected) && !matchesVexStatus(advMatch.Status, vexStatus.Fixed) {\n\t\t\tremainingMatches.Add(m)\n\t\t\tcontinue\n\t\t}\n\n\t\t// Check if there's any ignore rule that matches the current match statement\n\t\trule := matchingRule(ignoreRules, m, advMatch, vexStatus.IgnoreList())\n\t\tif rule == nil {\n\t\t\tremainingMatches.Add(m)\n\t\t\tcontinue\n\t\t}\n\n\t\tignoredMatches = append(ignoredMatches, match.IgnoredMatch{\n\t\t\tMatch:              m,\n\t\t\tAppliedIgnoreRules: []match.IgnoreRule{*rule},\n\t\t})\n\t}\n\n\treturn &remainingMatches, ignoredMatches, nil\n}\n\n// AugmentMatches adds results to the match.Matches array when matching data\n// about an affected VEX product is found on loaded VEX documents. Matches\n// are moved from the ignore list back to active matches.\nfunc (*Processor) AugmentMatches(\n\tdocRaw interface{}, ignoreRules []match.IgnoreRule, _ *pkg.Context, matches *match.Matches, ignoredMatches []match.IgnoredMatch,\n) (*match.Matches, []match.IgnoredMatch, error) {\n\tadvisories, ok := docRaw.(advisories)\n\tif !ok {\n\t\treturn nil, nil, errors.New(\"unable to cast vex document as CSAF Advisories\")\n\t}\n\n\tremainingIgnoredMatches := []match.IgnoredMatch{}\n\tfor _, m := range ignoredMatches {\n\t\tif advMatch := advisories.matches(m.Vulnerability.ID, m.Package.PURL); advMatch != nil {\n\t\t\tif rule := matchingRule(ignoreRules, m.Match, advMatch, vexStatus.AugmentList()); rule != nil {\n\t\t\t\tnewMatch := m.Match\n\t\t\t\tnewMatch.Details = append(newMatch.Details, match.Detail{\n\t\t\t\t\tType: match.ExactDirectMatch,\n\t\t\t\t\tSearchedBy: &searchedBy{\n\t\t\t\t\t\tVulnerability: m.Vulnerability.ID,\n\t\t\t\t\t\tPurl:          m.Package.PURL,\n\t\t\t\t\t},\n\t\t\t\t\tFound:   advMatch,\n\t\t\t\t\tMatcher: match.CsafVexMatcher,\n\t\t\t\t})\n\t\t\t\tmatches.Add(newMatch)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\n\t\tremainingIgnoredMatches = append(remainingIgnoredMatches, m)\n\t}\n\n\treturn matches, remainingIgnoredMatches, nil\n}\n\n// matchingRule cycles through a set of ignore rules and returns the first\n// one that matches the statement and the match. Returns nil if none match.\nfunc matchingRule(ignoreRules []match.IgnoreRule, m match.Match, advMatch *advisoryMatch, allowedStatuses []vexStatus.Status) *match.IgnoreRule {\n\tms := match.NewMatches()\n\tms.Add(m)\n\n\t// By default, if there are no ignore rules (which means the user didn't provide\n\t// any custom VEX rule), a matching rule should be returned if the advisory\n\t// match status is one of the allowed statuses.\n\tif len(ignoreRules) == 0 {\n\t\tfor _, status := range allowedStatuses {\n\t\t\tif matchesVexStatus(advMatch.Status, status) {\n\t\t\t\treturn &match.IgnoreRule{\n\t\t\t\t\tNamespace:        \"vex\",\n\t\t\t\t\tVulnerability:    advMatch.cve(),\n\t\t\t\t\tVexJustification: advMatch.statement(),\n\t\t\t\t\tVexStatus:        string(status),\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tfor _, rule := range ignoreRules {\n\t\t// If the rule has more conditions than just the VEX statement, check if\n\t\t// it applies to the current match.\n\t\tif rule.HasConditions() {\n\t\t\tr := rule\n\t\t\tr.VexStatus = \"\"\n\t\t\tif _, ignored := match.ApplyIgnoreRules(ms, []match.IgnoreRule{r}); len(ignored) == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\n\t\t// If the advisory match status is not the same as the rule status,\n\t\t// it does not apply\n\t\tif !matchesVexStatus(advMatch.Status, vexStatus.Status(rule.VexStatus)) {\n\t\t\tcontinue\n\t\t}\n\n\t\t// If the rule has a status other than the allowed ones, skip:\n\t\tif rule.VexStatus != \"\" && !slices.Contains(allowedStatuses, vexStatus.Status(rule.VexStatus)) {\n\t\t\tcontinue\n\t\t}\n\n\t\t// If the vulnerability is blank in the rule it means we will honor\n\t\t// any status with any vulnerability. Alternatively, if the vulnerability\n\t\t// is set, the rule applies if it is the same in the advisory match and the rule.\n\t\tif rule.Vulnerability == \"\" || advMatch.cve() == rule.Vulnerability {\n\t\t\treturn &rule\n\t\t}\n\n\t\t// If the rule applies to a VEX justification it needs to match the\n\t\t// advisory match statement, note that justifications only apply to not_affected:\n\t\tif matchesVexStatus(advMatch.Status, vexStatus.NotAffected) && rule.VexJustification != \"\" &&\n\t\t\trule.VexJustification != advMatch.statement() {\n\t\t\tcontinue\n\t\t}\n\n\t\tif advMatch.cve() == rule.Vulnerability {\n\t\t\treturn &rule\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "grype/vex/csaf/implementation_test.go",
    "content": "package csaf\n\nimport (\n\t\"slices\"\n\t\"testing\"\n\n\t\"github.com/gocsaf/csaf/v3/csaf\"\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/grype/grype/match\"\n\t\"github.com/anchore/grype/grype/pkg\"\n\tvexStatus \"github.com/anchore/grype/grype/vex/status\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n)\n\nfunc Test_newerCurrentReleaseDateFirst(t *testing.T) {\n\ttype dateIDPair struct {\n\t\tdate string\n\t\tid   string\n\t}\n\n\ttests := []struct {\n\t\tname     string\n\t\tinput    []dateIDPair\n\t\texpected []string\n\t}{\n\t\t{\n\t\t\tname: \"simple sort newest first\",\n\t\t\tinput: []dateIDPair{\n\t\t\t\t{\"2023-01-01T00:00:00Z\", \"doc1\"},\n\t\t\t\t{\"2024-01-01T00:00:00Z\", \"doc2\"},\n\t\t\t\t{\"2022-01-01T00:00:00Z\", \"doc3\"},\n\t\t\t},\n\t\t\texpected: []string{\"doc2\", \"doc1\", \"doc3\"},\n\t\t},\n\t\t{\n\t\t\tname: \"already sorted\",\n\t\t\tinput: []dateIDPair{\n\t\t\t\t{\"2024-01-01T00:00:00Z\", \"doc1\"},\n\t\t\t\t{\"2023-01-01T00:00:00Z\", \"doc2\"},\n\t\t\t},\n\t\t\texpected: []string{\"doc1\", \"doc2\"},\n\t\t},\n\t\t{\n\t\t\tname: \"same dates maintain order\",\n\t\t\tinput: []dateIDPair{\n\t\t\t\t{\"2023-01-01T00:00:00Z\", \"first\"},\n\t\t\t\t{\"2023-01-01T00:00:00Z\", \"second\"},\n\t\t\t},\n\t\t\texpected: []string{\"first\", \"second\"},\n\t\t},\n\t\t{\n\t\t\tname: \"nil dates go last\",\n\t\t\tinput: []dateIDPair{\n\t\t\t\t{\"\", \"nil1\"},\n\t\t\t\t{\"2023-01-01T00:00:00Z\", \"valid1\"},\n\t\t\t\t{\"2024-01-01T00:00:00Z\", \"valid2\"},\n\t\t\t},\n\t\t\texpected: []string{\"valid2\", \"valid1\", \"nil1\"},\n\t\t},\n\t\t{\n\t\t\tname: \"multiple nils maintain order\",\n\t\t\tinput: []dateIDPair{\n\t\t\t\t{\"\", \"nil1\"},\n\t\t\t\t{\"2023-01-01T00:00:00Z\", \"valid\"},\n\t\t\t\t{\"\", \"nil2\"},\n\t\t\t},\n\t\t\texpected: []string{\"valid\", \"nil1\", \"nil2\"},\n\t\t},\n\t\t{\n\t\t\tname: \"all nils\",\n\t\t\tinput: []dateIDPair{\n\t\t\t\t{\"\", \"first\"},\n\t\t\t\t{\"\", \"second\"},\n\t\t\t\t{\"\", \"third\"},\n\t\t\t},\n\t\t\texpected: []string{\"first\", \"second\", \"third\"},\n\t\t},\n\t\t{\n\t\t\tname: \"invalid date format goes last\",\n\t\t\tinput: []dateIDPair{\n\t\t\t\t{\"invalid-date\", \"bad\"},\n\t\t\t\t{\"2023-01-01T00:00:00Z\", \"good\"},\n\t\t\t},\n\t\t\texpected: []string{\"good\", \"bad\"},\n\t\t},\n\t\t{\n\t\t\tname: \"mix of nil invalid and valid\",\n\t\t\tinput: []dateIDPair{\n\t\t\t\t{\"\", \"nil\"},\n\t\t\t\t{\"invalid\", \"bad\"},\n\t\t\t\t{\"2024-01-01T00:00:00Z\", \"new\"},\n\t\t\t\t{\"2023-01-01T00:00:00Z\", \"old\"},\n\t\t\t},\n\t\t\texpected: []string{\"new\", \"old\", \"nil\", \"bad\"},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tadvs := make(advisories, len(tt.input))\n\t\t\tfor i, pair := range tt.input {\n\t\t\t\tvar datePtr *string\n\t\t\t\tif pair.date == \"\" {\n\t\t\t\t\tdatePtr = nil\n\t\t\t\t} else {\n\t\t\t\t\tdatePtr = &pair.date\n\t\t\t\t}\n\n\t\t\t\tadvs[i] = &csaf.Advisory{\n\t\t\t\t\tDocument: &csaf.Document{\n\t\t\t\t\t\tTracking: &csaf.Tracking{\n\t\t\t\t\t\t\tID:                 (*csaf.TrackingID)(&pair.id),\n\t\t\t\t\t\t\tCurrentReleaseDate: datePtr,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tslices.SortStableFunc(advs, newerCurrentReleaseDateFirst)\n\n\t\t\tresult := make([]string, len(advs))\n\t\t\tfor i, adv := range advs {\n\t\t\t\tresult[i] = string(*adv.Document.Tracking.ID)\n\t\t\t}\n\n\t\t\tassert.Equal(t, tt.expected, result)\n\t\t})\n\t}\n}\n\nfunc Test_matchingRule(t *testing.T) {\n\ttests := []struct {\n\t\tname            string\n\t\tignoreRules     []match.IgnoreRule\n\t\tm               match.Match\n\t\tadvMatch        *advisoryMatch\n\t\tallowedStatuses []vexStatus.Status\n\t\texpected        *match.IgnoreRule\n\t}{\n\t\t{\n\t\t\tname:        \"no ignore rules, not_affected status with inline mitigations\",\n\t\t\tignoreRules: []match.IgnoreRule{}, // No existing ignore rules\n\t\t\tm: match.Match{\n\t\t\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\t\t\tReference: vulnerability.Reference{ID: \"CVE-2023-1234\"},\n\t\t\t\t},\n\t\t\t\tPackage: pkg.Package{Name: \"test-package\"},\n\t\t\t},\n\t\t\tadvMatch: &advisoryMatch{\n\t\t\t\tVulnerability: &csaf.Vulnerability{\n\t\t\t\t\tCVE: func() *csaf.CVE { cve := csaf.CVE(\"CVE-2023-1234\"); return &cve }(),\n\t\t\t\t},\n\t\t\t\tStatus:    knownNotAffected, // CSAF status\n\t\t\t\tProductID: \"test-product-1\",\n\t\t\t},\n\t\t\tallowedStatuses: vexStatus.IgnoreList(), // [Fixed, NotAffected]\n\t\t\texpected: &match.IgnoreRule{\n\t\t\t\tNamespace:        \"vex\",\n\t\t\t\tVulnerability:    \"CVE-2023-1234\",\n\t\t\t\tVexJustification: \"\", // Will be empty since no flags/threats in this simple case\n\t\t\t\tVexStatus:        \"not_affected\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:        \"no ignore rules, under_investigation status should return nil\",\n\t\t\tignoreRules: []match.IgnoreRule{}, // No existing ignore rules\n\t\t\tm: match.Match{\n\t\t\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\t\t\tReference: vulnerability.Reference{ID: \"CVE-2023-5678\"},\n\t\t\t\t},\n\t\t\t\tPackage: pkg.Package{Name: \"another-package\"},\n\t\t\t},\n\t\t\tadvMatch: &advisoryMatch{\n\t\t\t\tVulnerability: &csaf.Vulnerability{\n\t\t\t\t\tCVE: func() *csaf.CVE { cve := csaf.CVE(\"CVE-2023-5678\"); return &cve }(),\n\t\t\t\t},\n\t\t\t\tStatus:    underInvestigation, // CSAF status\n\t\t\t\tProductID: \"test-product-2\",\n\t\t\t},\n\t\t\tallowedStatuses: vexStatus.IgnoreList(), // [Fixed, NotAffected] - doesn't include UnderInvestigation\n\t\t\texpected:        nil,                    // Should return nil since under_investigation is not in allowed statuses\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := matchingRule(tt.ignoreRules, tt.m, tt.advMatch, tt.allowedStatuses)\n\t\t\tif tt.expected == nil {\n\t\t\t\trequire.Nil(t, result)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif d := cmp.Diff(*result, *tt.expected); d != \"\" {\n\t\t\t\tt.Errorf(\"mismatch (-want +got):\\n%s\", d)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/vex/csaf/status.go",
    "content": "package csaf\n\nimport vexStatus \"github.com/anchore/grype/grype/vex/status\"\n\ntype status string\n\nconst (\n\tfirstAffected      status = \"first_affected\"\n\tfirstFixed         status = \"first_fixed\"\n\tfixed              status = \"fixed\"\n\tknownAffected      status = \"known_affected\"\n\tknownNotAffected   status = \"known_not_affected\"\n\tlastAffected       status = \"last_affected\"\n\trecommended        status = \"recommended\"\n\tunderInvestigation status = \"under_investigation\"\n)\n\n// matchesVexStatus returns true if the given CSAF status matches the given VEX status.\nfunc matchesVexStatus(csafStatus status, status vexStatus.Status) bool {\n\t// CSAF implementation has slightly different, richer statuses than the original VEX proposed by CISA\n\tswitch csafStatus {\n\tcase firstAffected, knownAffected, lastAffected, recommended:\n\t\treturn status == vexStatus.Affected\n\tcase firstFixed, fixed:\n\t\treturn status == vexStatus.Fixed\n\tcase knownNotAffected:\n\t\treturn status == vexStatus.NotAffected\n\tcase underInvestigation:\n\t\treturn status == vexStatus.UnderInvestigation\n\tdefault:\n\t\treturn false\n\t}\n}\n"
  },
  {
    "path": "grype/vex/openvex/implementation.go",
    "content": "package openvex\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"slices\"\n\t\"strings\"\n\n\t\"github.com/google/go-containerregistry/pkg/name\"\n\topenvex \"github.com/openvex/go-vex/pkg/vex\"\n\n\t\"github.com/anchore/grype/grype/match\"\n\t\"github.com/anchore/grype/grype/pkg\"\n\tvexStatus \"github.com/anchore/grype/grype/vex/status\"\n\t\"github.com/anchore/packageurl-go\"\n\t\"github.com/anchore/syft/syft/source\"\n)\n\ntype Processor struct{}\n\nfunc New() *Processor {\n\treturn &Processor{}\n}\n\n// Match captures the criteria that caused a vulnerability to match\ntype Match struct {\n\tStatement openvex.Statement\n}\n\n// SearchedBy captures the parameters used to search through the VEX data\ntype SearchedBy struct {\n\tVulnerability string\n\tProduct       string\n\tSubcomponents []string\n}\n\n// IsOpenVex checks if the provided document is a VEX document\nfunc IsOpenVex(document string) bool {\n\tif _, err := openvex.Load(document); err == nil {\n\t\treturn true\n\t}\n\treturn false\n}\n\n// ReadVexDocuments reads and merges VEX documents\nfunc (ovm *Processor) ReadVexDocuments(docs []string) (interface{}, error) {\n\t// Combine all VEX documents into a single VEX document\n\tvexdata, err := openvex.MergeFiles(docs)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"merging vex documents: %w\", err)\n\t}\n\n\treturn vexdata, nil\n}\n\n// productIdentifiersFromContext reads the package context and returns software\n// identifiers identifying the scanned image.\nfunc productIdentifiersFromContext(pkgContext *pkg.Context) []string {\n\tswitch v := pkgContext.Source.Metadata.(type) {\n\tcase source.ImageMetadata:\n\t\ttagIdentifiers := identifiersFromTags(v.Tags, pkgContext.Source.Name)\n\t\tdigestIdentifiers := identifiersFromDigests(v.RepoDigests)\n\t\tidentifiers := slices.Concat(tagIdentifiers, digestIdentifiers)\n\t\treturn identifiers\n\tdefault:\n\t\tif pkgContext.Source.Name != \"\" && pkgContext.Source.Version != \"\" {\n\t\t\treturn []string{\"pkg:generic/\" + strings.ToLower(pkgContext.Source.Name) + \"@\" + pkgContext.Source.Version}\n\t\t}\n\t\t// return an empty list so matching can be attempted using the\n\t\t// package's own identifiers as the product\n\t\treturn []string{}\n\t}\n}\n\nfunc normalizeDockerHubRepositoryURL(repoURL string) string {\n\trepoURL = strings.TrimSpace(repoURL)\n\tif repoURL == \"\" {\n\t\treturn repoURL\n\t}\n\n\trepoURL = strings.TrimPrefix(repoURL, \"https://\")\n\trepoURL = strings.TrimPrefix(repoURL, \"http://\")\n\n\trepoURL = strings.TrimSuffix(repoURL, \"/\")\n\n\thost, rest, hasSlash := strings.Cut(repoURL, \"/\")\n\n\tswitch strings.ToLower(host) {\n\tcase \"docker.io\", \"index.docker.io\", \"registry-1.docker.io\":\n\t\thost = \"index.docker.io\"\n\t}\n\n\tif !hasSlash || rest == \"\" {\n\t\treturn host\n\t}\n\treturn host + \"/\" + rest\n}\n\nfunc identifiersFromTags(tags []string, name string) []string {\n\tidentifiers := []string{}\n\n\tfor _, tag := range tags {\n\t\tidentifiers = append(identifiers, tag)\n\n\t\ttagMap := map[string]string{}\n\t\t_, splitTag, found := strings.Cut(tag, \":\")\n\t\tif found {\n\t\t\ttagMap[\"tag\"] = splitTag\n\t\t\tqualifiers := packageurl.QualifiersFromMap(tagMap)\n\n\t\t\tidentifiers = append(identifiers, packageurl.NewPackageURL(\"oci\", \"\", name, \"\", qualifiers, \"\").String())\n\t\t}\n\t}\n\n\treturn identifiers\n}\n\nfunc identifiersFromDigests(digests []string) []string {\n\tidentifiers := []string{}\n\n\tfor _, d := range digests {\n\t\t// The first identifier is the original image reference:\n\t\tidentifiers = append(identifiers, d)\n\n\t\t// Not an image reference, skip\n\t\tref, err := name.ParseReference(d)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tvar repoURL string\n\t\tshaString := ref.Identifier()\n\n\t\t// If not a digest, we can't form a purl, so skip it\n\t\tif !strings.HasPrefix(shaString, \"sha256:\") {\n\t\t\tcontinue\n\t\t}\n\n\t\tpts := strings.Split(ref.Context().RepositoryStr(), \"/\")\n\t\tname := pts[len(pts)-1]\n\t\trepoURL = strings.TrimSuffix(\n\t\t\tref.Context().RegistryStr()+\"/\"+ref.Context().RepositoryStr(),\n\t\t\tfmt.Sprintf(\"/%s\", name),\n\t\t)\n\n\t\trepoURL = normalizeDockerHubRepositoryURL(repoURL)\n\n\t\tqMap := map[string]string{}\n\n\t\tif repoURL != \"\" {\n\t\t\tqMap[\"repository_url\"] = repoURL\n\t\t}\n\n\t\tqs := packageurl.QualifiersFromMap(qMap)\n\t\tidentifiers = append(identifiers, packageurl.NewPackageURL(\n\t\t\t\"oci\", \"\", name, shaString, qs, \"\",\n\t\t).String())\n\n\t\t// Add a hash to the identifier list in case people want to vex\n\t\t// using the value of the image digest\n\t\tidentifiers = append(identifiers, strings.TrimPrefix(shaString, \"sha256:\"))\n\t}\n\treturn identifiers\n}\n\n// subcomponentIdentifiersFromMatch returns the list of identifiers from the\n// package where grype did the match.\nfunc subcomponentIdentifiersFromMatch(m *match.Match) []string {\n\tret := []string{}\n\tif m.Package.PURL != \"\" {\n\t\tret = append(ret, m.Package.PURL)\n\t}\n\n\t// TODO(puerco):Implement CPE matching in openvex/go-vex\n\t/*\n\t\tfor _, c := range m.Package.CPEs {\n\t\t\tret = append(ret, c.String())\n\t\t}\n\t*/\n\treturn ret\n}\n\n// findMatchingStatement searches a VEX document for a statement matching the\n// given vulnerability. It performs a two-pass search:\n//  1. Try SBOM/context product identifiers (handles image-as-product cases)\n//  2. Try the match's own package identifiers as the product (handles\n//     package-as-product cases, where the VEX product is a package PURL)\nfunc findMatchingStatement(doc *openvex.VEX, vulnID string, products []string, subcmp []string) (stmt *openvex.Statement, product string, subcomponents []string) {\n\tfor _, product := range products {\n\t\tif stmts := doc.Matches(vulnID, product, subcmp); len(stmts) != 0 {\n\t\t\treturn &stmts[0], product, subcmp\n\t\t}\n\t}\n\n\tfor _, pkgID := range subcmp {\n\t\tif stmts := doc.Matches(vulnID, pkgID, nil); len(stmts) != 0 {\n\t\t\treturn &stmts[0], pkgID, nil\n\t\t}\n\t}\n\n\treturn nil, \"\", nil\n}\n\n// FilterMatches takes a set of scanning results and moves any results marked in\n// the VEX data as fixed or not_affected to the ignored list.\nfunc (ovm *Processor) FilterMatches(\n\tdocRaw interface{}, ignoreRules []match.IgnoreRule, pkgContext *pkg.Context, matches *match.Matches, ignoredMatches []match.IgnoredMatch,\n) (*match.Matches, []match.IgnoredMatch, error) {\n\tdoc, ok := docRaw.(*openvex.VEX)\n\tif !ok {\n\t\treturn nil, nil, errors.New(\"unable to cast vex document as openvex\")\n\t}\n\n\tremainingMatches := match.NewMatches()\n\n\tproducts := productIdentifiersFromContext(pkgContext)\n\n\t// TODO(alex): should we apply the vex ignore rules to the already ignored matches?\n\t// that way the end user sees all of the reasons a match was ignored in case multiple apply\n\n\t// Now, let's go through grype's matches\n\tsorted := matches.Sorted()\n\tfor i := range sorted {\n\t\tsubcmp := subcomponentIdentifiersFromMatch(&sorted[i])\n\t\tstatement, _, _ := findMatchingStatement(doc, sorted[i].Vulnerability.ID, products, subcmp)\n\n\t\t// No data about this match's component. Next.\n\t\tif statement == nil {\n\t\t\tremainingMatches.Add(sorted[i])\n\t\t\tcontinue\n\t\t}\n\n\t\trule := matchingRule(ignoreRules, sorted[i], statement, vexStatus.IgnoreList())\n\t\tif rule == nil {\n\t\t\tremainingMatches.Add(sorted[i])\n\t\t\tcontinue\n\t\t}\n\n\t\t// Filtering only applies to not_affected and fixed statuses\n\t\tif statement.Status != openvex.StatusNotAffected && statement.Status != openvex.StatusFixed {\n\t\t\tremainingMatches.Add(sorted[i])\n\t\t\tcontinue\n\t\t}\n\n\t\tignoredMatches = append(ignoredMatches, match.IgnoredMatch{\n\t\t\tMatch:              sorted[i],\n\t\t\tAppliedIgnoreRules: []match.IgnoreRule{*rule},\n\t\t})\n\t}\n\treturn &remainingMatches, ignoredMatches, nil\n}\n\n// matchingRule cycles through a set of ignore rules and returns the first\n// one that matches the statement and the match. Returns nil if none match.\nfunc matchingRule(ignoreRules []match.IgnoreRule, m match.Match, statement *openvex.Statement, allowedStatuses []vexStatus.Status) *match.IgnoreRule {\n\tms := match.NewMatches()\n\tms.Add(m)\n\n\t// By default, if there are no ignore rules (which means the user didn't provide\n\t// any custom VEX rule), a matching rule should be returned if the statement\n\t// status is one of the allowed statuses.\n\tif len(ignoreRules) == 0 && slices.Contains(allowedStatuses, vexStatus.Status(statement.Status)) {\n\t\treturn &match.IgnoreRule{\n\t\t\tNamespace:        \"vex\",\n\t\t\tVulnerability:    statement.Vulnerability.ID,\n\t\t\tVexJustification: string(statement.Justification),\n\t\t\tVexStatus:        string(statement.Status),\n\t\t}\n\t}\n\n\tfor _, rule := range ignoreRules {\n\t\t// If the rule has more conditions than just the VEX statement, check if\n\t\t// it applies to the current match.\n\t\tif rule.HasConditions() {\n\t\t\tr := rule\n\t\t\tr.VexStatus = \"\"\n\t\t\tif _, ignored := match.ApplyIgnoreRules(ms, []match.IgnoreRule{r}); len(ignored) == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\n\t\t// If the status in the statement is not the same in the rule\n\t\t// and the vex statement, it does not apply\n\t\tif string(statement.Status) != rule.VexStatus {\n\t\t\tcontinue\n\t\t}\n\n\t\t// If the rule has a statement other than the allowed ones, skip:\n\t\tif rule.VexStatus != \"\" && !slices.Contains(allowedStatuses, vexStatus.Status(rule.VexStatus)) {\n\t\t\tcontinue\n\t\t}\n\n\t\t// If the rule applies to a VEX justification it needs to match the\n\t\t// statement, note that justifications only apply to not_affected:\n\t\tif statement.Status == openvex.StatusNotAffected && rule.VexJustification != \"\" &&\n\t\t\trule.VexJustification != string(statement.Justification) {\n\t\t\tcontinue\n\t\t}\n\n\t\t// If the vulnerability is blank in the rule it means we will honor\n\t\t// any status with any vulnerability.\n\t\tif rule.Vulnerability == \"\" {\n\t\t\treturn &rule\n\t\t}\n\n\t\t// If the vulnerability is set, the rule applies if it is the same\n\t\t// in the statement and the rule.\n\t\tif statement.Vulnerability.Matches(rule.Vulnerability) {\n\t\t\treturn &rule\n\t\t}\n\t}\n\treturn nil\n}\n\n// AugmentMatches adds results to the match.Matches array when matching data\n// about an affected VEX product is found on loaded VEX documents. Matches\n// are moved from the ignore list or synthesized when no previous data is found.\nfunc (ovm *Processor) AugmentMatches(\n\tdocRaw interface{}, ignoreRules []match.IgnoreRule, pkgContext *pkg.Context, remainingMatches *match.Matches, ignoredMatches []match.IgnoredMatch,\n) (*match.Matches, []match.IgnoredMatch, error) {\n\tdoc, ok := docRaw.(*openvex.VEX)\n\tif !ok {\n\t\treturn nil, nil, errors.New(\"unable to cast vex document as openvex\")\n\t}\n\n\tadditionalIgnoredMatches := []match.IgnoredMatch{}\n\n\tproducts := productIdentifiersFromContext(pkgContext)\n\n\t// Now, let's go through grype's matches\n\tfor i := range ignoredMatches {\n\t\tsubcmp := subcomponentIdentifiersFromMatch(&ignoredMatches[i].Match)\n\n\t\tstatement, matchedProduct, matchedSubcmp := findMatchingStatement(doc, ignoredMatches[i].Vulnerability.ID, products, subcmp)\n\n\t\t// Only augment for affected or under_investigation statuses\n\t\tif statement == nil || (statement.Status != openvex.StatusAffected && statement.Status != openvex.StatusUnderInvestigation) {\n\t\t\tadditionalIgnoredMatches = append(additionalIgnoredMatches, ignoredMatches[i])\n\t\t\tcontinue\n\t\t}\n\n\t\t// Only match if rules to augment are configured\n\t\trule := matchingRule(ignoreRules, ignoredMatches[i].Match, statement, vexStatus.AugmentList())\n\t\tif rule == nil {\n\t\t\tadditionalIgnoredMatches = append(additionalIgnoredMatches, ignoredMatches[i])\n\t\t\tcontinue\n\t\t}\n\n\t\tnewMatch := ignoredMatches[i].Match\n\t\tnewMatch.Details = append(newMatch.Details, match.Detail{\n\t\t\tType: match.ExactDirectMatch,\n\t\t\tSearchedBy: &SearchedBy{\n\t\t\t\tVulnerability: ignoredMatches[i].Vulnerability.ID,\n\t\t\t\tProduct:       matchedProduct,\n\t\t\t\tSubcomponents: matchedSubcmp,\n\t\t\t},\n\t\t\tFound: Match{\n\t\t\t\tStatement: *statement,\n\t\t\t},\n\t\t\tMatcher: match.OpenVexMatcher,\n\t\t})\n\n\t\tremainingMatches.Add(newMatch)\n\t}\n\n\treturn remainingMatches, additionalIgnoredMatches, nil\n}\n"
  },
  {
    "path": "grype/vex/openvex/implementation_test.go",
    "content": "package openvex\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\topenvex \"github.com/openvex/go-vex/pkg/vex\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/grype/grype/match\"\n\t\"github.com/anchore/grype/grype/pkg\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n\t\"github.com/anchore/packageurl-go\"\n\t\"github.com/anchore/syft/syft/source\"\n)\n\nfunc TestIdentifiersFromTags(t *testing.T) {\n\tfor _, tc := range []struct {\n\t\tsut      string\n\t\tname     string\n\t\texpected []string\n\t}{\n\t\t{\n\t\t\t\"alpine:v1.2.3\",\n\t\t\t\"alpine\",\n\t\t\t[]string{\"alpine:v1.2.3\", \"pkg:oci/alpine?tag=v1.2.3\"},\n\t\t},\n\t\t{\n\t\t\t\"alpine\",\n\t\t\t\"alpine\",\n\t\t\t[]string{\"alpine\"},\n\t\t},\n\t} {\n\t\tres := identifiersFromTags([]string{tc.sut}, tc.name)\n\t\trequire.Equal(t, tc.expected, res)\n\t}\n}\n\nfunc TestIdentifiersFromDigests(t *testing.T) {\n\tfor _, tc := range []struct {\n\t\tsut      string\n\t\texpected []string\n\t}{\n\t\t{\n\t\t\t\"alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126\",\n\t\t\t[]string{\n\t\t\t\t\"alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126\",\n\t\t\t\t\"pkg:oci/alpine@sha256%3A124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126?repository_url=index.docker.io%2Flibrary\",\n\t\t\t\t\"124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"cgr.dev/chainguard/curl@sha256:9543ed09a38605c25c75486573cf530bd886615b993d5e1d1aa58fe5491287bc\",\n\t\t\t[]string{\n\t\t\t\t\"cgr.dev/chainguard/curl@sha256:9543ed09a38605c25c75486573cf530bd886615b993d5e1d1aa58fe5491287bc\",\n\t\t\t\t\"pkg:oci/curl@sha256%3A9543ed09a38605c25c75486573cf530bd886615b993d5e1d1aa58fe5491287bc?repository_url=cgr.dev%2Fchainguard\",\n\t\t\t\t\"9543ed09a38605c25c75486573cf530bd886615b993d5e1d1aa58fe5491287bc\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"alpine\",\n\t\t\t[]string{\"alpine\"},\n\t\t},\n\t} {\n\t\tres := identifiersFromDigests([]string{tc.sut})\n\t\trequire.Equal(t, tc.expected, res)\n\t}\n}\n\nfunc TestFilterMatches_NoErrorOnEmptyProducts(t *testing.T) {\n\ttests := []struct {\n\t\tname        string\n\t\tpkgContext  *pkg.Context\n\t\tvexDoc      *openvex.VEX\n\t\tmatches     *match.Matches\n\t\tignoreRules []match.IgnoreRule\n\t\twantErr     require.ErrorAssertionFunc\n\t}{\n\t\t{\n\t\t\tname: \"no error when context has empty products and VEX document has products\",\n\t\t\t// when context returns empty products, the code should fall back to VEX products without error\n\t\t\tpkgContext: &pkg.Context{\n\t\t\t\tSource: &source.Description{\n\t\t\t\t\tName: \"alpine\",\n\t\t\t\t\tMetadata: source.ImageMetadata{\n\t\t\t\t\t\tTags:        []string{},\n\t\t\t\t\t\tRepoDigests: []string{},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tvexDoc: &openvex.VEX{\n\t\t\t\tStatements: []openvex.Statement{\n\t\t\t\t\t{\n\t\t\t\t\t\tVulnerability: openvex.Vulnerability{Name: \"CVE-2024-1234\"},\n\t\t\t\t\t\tProducts: []openvex.Product{\n\t\t\t\t\t\t\t{Component: openvex.Component{ID: \"pkg:oci/alpine@sha256:abc123\"}},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tStatus: openvex.StatusNotAffected,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tmatches: func() *match.Matches {\n\t\t\t\tm := match.NewMatches()\n\t\t\t\tm.Add(match.Match{\n\t\t\t\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\t\tID: \"CVE-2024-1234\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tPackage: pkg.Package{\n\t\t\t\t\t\tPURL: \"pkg:npm/test@1.0.0\",\n\t\t\t\t\t},\n\t\t\t\t})\n\t\t\t\treturn &m\n\t\t\t}(),\n\t\t\tignoreRules: []match.IgnoreRule{\n\t\t\t\t{VexStatus: string(openvex.StatusNotAffected)},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"no error when VEX document has multiple products\",\n\t\t\tpkgContext: &pkg.Context{\n\t\t\t\tSource: &source.Description{\n\t\t\t\t\tName: \"ubuntu\",\n\t\t\t\t\tMetadata: source.ImageMetadata{\n\t\t\t\t\t\tTags:        []string{},\n\t\t\t\t\t\tRepoDigests: []string{},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tvexDoc: &openvex.VEX{\n\t\t\t\tStatements: []openvex.Statement{\n\t\t\t\t\t{\n\t\t\t\t\t\tVulnerability: openvex.Vulnerability{Name: \"CVE-2024-5678\"},\n\t\t\t\t\t\tProducts: []openvex.Product{\n\t\t\t\t\t\t\t{Component: openvex.Component{ID: \"pkg:oci/ubuntu@sha256:def456\"}},\n\t\t\t\t\t\t\t{Component: openvex.Component{ID: \"pkg:oci/debian@sha256:abc789\"}},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tStatus: openvex.StatusFixed,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tmatches: func() *match.Matches {\n\t\t\t\tm := match.NewMatches()\n\t\t\t\treturn &m\n\t\t\t}(),\n\t\t\tignoreRules: []match.IgnoreRule{\n\t\t\t\t{VexStatus: string(openvex.StatusFixed)},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif tt.wantErr == nil {\n\t\t\t\ttt.wantErr = require.NoError\n\t\t\t}\n\n\t\t\tprocessor := New()\n\t\t\tremainingMatches, _, err := processor.FilterMatches(\n\t\t\t\ttt.vexDoc,\n\t\t\t\ttt.ignoreRules,\n\t\t\t\ttt.pkgContext,\n\t\t\t\ttt.matches,\n\t\t\t\tnil,\n\t\t\t)\n\t\t\ttt.wantErr(t, err)\n\n\t\t\tif err != nil {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// basic sanity checks - we're mainly testing that the fallback doesn't cause errors\n\t\t\trequire.NotNil(t, remainingMatches)\n\t\t})\n\t}\n}\n\nfunc TestFilterMatches_ImageProductNoSubcomponents(t *testing.T) {\n\t// Scenario 1: Image product, no subcomponents → applies to entire scan.\n\t// When a VEX statement specifies an image product with no subcomponents,\n\t// ALL matches for that products CVE should be filtered, regardless of which package.\n\tprocessor := New()\n\n\tpkgCtx := &pkg.Context{\n\t\tSource: &source.Description{\n\t\t\tName: \"alpine\",\n\t\t\tMetadata: source.ImageMetadata{\n\t\t\t\tRepoDigests: []string{\n\t\t\t\t\t\"alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tvexDoc := &openvex.VEX{\n\t\tStatements: []openvex.Statement{\n\t\t\t{\n\t\t\t\tVulnerability: openvex.Vulnerability{Name: \"CVE-2023-1255\"},\n\t\t\t\tProducts: []openvex.Product{\n\t\t\t\t\t{\n\t\t\t\t\t\tComponent: openvex.Component{\n\t\t\t\t\t\t\tID: \"pkg:oci/alpine@sha256%3A124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\t// No subcomponents — applies to entire product\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tStatus: openvex.StatusFixed,\n\t\t\t},\n\t\t},\n\t}\n\n\tmatchLibcrypto := match.Match{\n\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\tReference: vulnerability.Reference{\n\t\t\t\tID: \"CVE-2023-1255\",\n\t\t\t},\n\t\t},\n\t\tPackage: pkg.Package{\n\t\t\tID:   \"cc8f90662d91481d\",\n\t\t\tName: \"libcrypto3\",\n\t\t\tPURL: \"pkg:apk/alpine/libcrypto3@3.0.8-r3\",\n\t\t},\n\t}\n\tmatchLibssl := match.Match{\n\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\tReference: vulnerability.Reference{\n\t\t\t\tID: \"CVE-2023-1255\",\n\t\t\t},\n\t\t},\n\t\tPackage: pkg.Package{\n\t\t\tID:   \"aa1234567890abcd\",\n\t\t\tName: \"libssl3\",\n\t\t\tPURL: \"pkg:apk/alpine/libssl3@3.0.8-r3\",\n\t\t},\n\t}\n\n\tmatches := match.NewMatches(matchLibcrypto, matchLibssl)\n\n\tremaining, ignored, err := processor.FilterMatches(\n\t\tvexDoc, nil, pkgCtx, &matches, nil,\n\t)\n\trequire.NoError(t, err)\n\n\t// Both matches should be filtered because there are no subcomponents\n\trequire.Empty(t, remaining.Sorted(), \"all matches for the CVE should be filtered when no subcomponents are specified\")\n\trequire.Len(t, ignored, 2, \"both matches should be in the ignored list\")\n}\n\nfunc TestFilterMatches_PackageProductDirectoryScan(t *testing.T) {\n\t// When the source is a directory scan and the VEX product is a package PURL,\n\t// the second pass of findMatchingStatement matches the package PURL as the product.\n\tprocessor := New()\n\n\tpkgCtx := &pkg.Context{\n\t\tSource: &source.Description{\n\t\t\tMetadata: source.DirectoryMetadata{\n\t\t\t\tPath: \"/some/project\",\n\t\t\t},\n\t\t},\n\t}\n\n\tvexDoc := &openvex.VEX{\n\t\tStatements: []openvex.Statement{\n\t\t\t{\n\t\t\t\tVulnerability: openvex.Vulnerability{Name: \"CVE-2023-1255\"},\n\t\t\t\tProducts: []openvex.Product{\n\t\t\t\t\t{\n\t\t\t\t\t\tComponent: openvex.Component{\n\t\t\t\t\t\t\tID: \"pkg:apk/alpine/libcrypto3@3.0.8-r3\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tStatus: openvex.StatusFixed,\n\t\t\t},\n\t\t},\n\t}\n\n\tmatchLibcrypto := match.Match{\n\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\tReference: vulnerability.Reference{\n\t\t\t\tID: \"CVE-2023-1255\",\n\t\t\t},\n\t\t},\n\t\tPackage: pkg.Package{\n\t\t\tID:   \"cc8f90662d91481d\",\n\t\t\tName: \"libcrypto3\",\n\t\t\tPURL: \"pkg:apk/alpine/libcrypto3@3.0.8-r3\",\n\t\t},\n\t}\n\n\tmatches := match.NewMatches(matchLibcrypto)\n\n\tremaining, ignored, err := processor.FilterMatches(\n\t\tvexDoc, nil, pkgCtx, &matches, nil,\n\t)\n\trequire.NoError(t, err)\n\n\trequire.Empty(t, remaining.Sorted(), \"match should be filtered when package PURL matches VEX product\")\n\trequire.Len(t, ignored, 1, \"match should be in the ignored list\")\n}\n\nfunc TestFilterMatches_PackageProductNoOverMatch(t *testing.T) {\n\t// When the VEX product is a package PURL (not an image), only the matching\n\t// package should be filtered — not other packages with the same CVE.\n\tvexDoc := &openvex.VEX{\n\t\tStatements: []openvex.Statement{\n\t\t\t{\n\t\t\t\tVulnerability: openvex.Vulnerability{Name: \"CVE-2023-1255\"},\n\t\t\t\tProducts: []openvex.Product{\n\t\t\t\t\t{\n\t\t\t\t\t\tComponent: openvex.Component{\n\t\t\t\t\t\t\tID: \"pkg:apk/alpine/libcrypto3@3.0.8-r3\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tStatus: openvex.StatusFixed,\n\t\t\t},\n\t\t},\n\t}\n\n\tmatchLibcrypto := match.Match{\n\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\tReference: vulnerability.Reference{\n\t\t\t\tID: \"CVE-2023-1255\",\n\t\t\t},\n\t\t},\n\t\tPackage: pkg.Package{\n\t\t\tID:   \"cc8f90662d91481d\",\n\t\t\tName: \"libcrypto3\",\n\t\t\tPURL: \"pkg:apk/alpine/libcrypto3@3.0.8-r3\",\n\t\t},\n\t}\n\tmatchCurl := match.Match{\n\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\tReference: vulnerability.Reference{\n\t\t\t\tID: \"CVE-2023-1255\",\n\t\t\t},\n\t\t},\n\t\tPackage: pkg.Package{\n\t\t\tID:   \"bb9876543210fedc\",\n\t\t\tName: \"curl\",\n\t\t\tPURL: \"pkg:apk/alpine/curl@8.1.2-r0\",\n\t\t},\n\t}\n\n\ttests := []struct {\n\t\tname       string\n\t\tpkgContext *pkg.Context\n\t}{\n\t\t{\n\t\t\tname: \"image scan\",\n\t\t\tpkgContext: &pkg.Context{\n\t\t\t\tSource: &source.Description{\n\t\t\t\t\tName: \"alpine\",\n\t\t\t\t\tMetadata: source.ImageMetadata{\n\t\t\t\t\t\tRepoDigests: []string{\n\t\t\t\t\t\t\t\"alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"directory scan\",\n\t\t\tpkgContext: &pkg.Context{\n\t\t\t\tSource: &source.Description{\n\t\t\t\t\tMetadata: source.DirectoryMetadata{\n\t\t\t\t\t\tPath: \"/some/project\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tprocessor := New()\n\t\t\tmatches := match.NewMatches(matchLibcrypto, matchCurl)\n\n\t\t\tremaining, ignored, err := processor.FilterMatches(\n\t\t\t\tvexDoc, nil, tt.pkgContext, &matches, nil,\n\t\t\t)\n\t\t\trequire.NoError(t, err)\n\n\t\t\trequire.Len(t, remaining.Sorted(), 1, \"only the non-matching package should remain\")\n\t\t\trequire.Equal(t, \"curl\", remaining.Sorted()[0].Package.Name)\n\t\t\trequire.Len(t, ignored, 1, \"only the matching package should be ignored\")\n\t\t\trequire.Equal(t, \"libcrypto3\", ignored[0].Match.Package.Name)\n\t\t})\n\t}\n}\n\nfunc TestProductIdentifiersFromContext(t *testing.T) {\n\ttests := []struct {\n\t\tname       string\n\t\tpkgContext *pkg.Context\n\t\twant       []string\n\t}{\n\t\t{\n\t\t\tname: \"image metadata with tags and digests\",\n\t\t\tpkgContext: &pkg.Context{\n\t\t\t\tSource: &source.Description{\n\t\t\t\t\tName: \"alpine\",\n\t\t\t\t\tMetadata: source.ImageMetadata{\n\t\t\t\t\t\tTags: []string{\"alpine:3.18\", \"alpine:latest\"},\n\t\t\t\t\t\tRepoDigests: []string{\n\t\t\t\t\t\t\t\"alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: []string{\n\t\t\t\t\"alpine:3.18\",\n\t\t\t\t\"pkg:oci/alpine?tag=3.18\",\n\t\t\t\t\"alpine:latest\",\n\t\t\t\t\"pkg:oci/alpine?tag=latest\",\n\t\t\t\t\"alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126\",\n\t\t\t\t\"pkg:oci/alpine@sha256%3A124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126?repository_url=index.docker.io%2Flibrary\",\n\t\t\t\t\"124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"image metadata with only tags\",\n\t\t\tpkgContext: &pkg.Context{\n\t\t\t\tSource: &source.Description{\n\t\t\t\t\tName: \"ubuntu\",\n\t\t\t\t\tMetadata: source.ImageMetadata{\n\t\t\t\t\t\tTags:        []string{\"ubuntu:22.04\"},\n\t\t\t\t\t\tRepoDigests: []string{},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: []string{\n\t\t\t\t\"ubuntu:22.04\",\n\t\t\t\t\"pkg:oci/ubuntu?tag=22.04\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"image metadata with only digests\",\n\t\t\tpkgContext: &pkg.Context{\n\t\t\t\tSource: &source.Description{\n\t\t\t\t\tName: \"nginx\",\n\t\t\t\t\tMetadata: source.ImageMetadata{\n\t\t\t\t\t\tTags: []string{},\n\t\t\t\t\t\tRepoDigests: []string{\n\t\t\t\t\t\t\t\"nginx@sha256:abc123\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: []string{\n\t\t\t\t\"nginx@sha256:abc123\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"image metadata with no tags or digests\",\n\t\t\tpkgContext: &pkg.Context{\n\t\t\t\tSource: &source.Description{\n\t\t\t\t\tName: \"busybox\",\n\t\t\t\t\tMetadata: source.ImageMetadata{\n\t\t\t\t\t\tTags:        []string{},\n\t\t\t\t\t\tRepoDigests: []string{},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: nil,\n\t\t},\n\t\t{\n\t\t\tname: \"generic source with name and version\",\n\t\t\tpkgContext: &pkg.Context{\n\t\t\t\tSource: &source.Description{\n\t\t\t\t\tName:    \"MyApp\",\n\t\t\t\t\tVersion: \"1.2.3\",\n\t\t\t\t\tMetadata: source.DirectoryMetadata{\n\t\t\t\t\t\tPath: \"/some/path\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: []string{\"pkg:generic/myapp@1.2.3\"},\n\t\t},\n\t\t{\n\t\t\tname: \"generic source with lowercase name\",\n\t\t\tpkgContext: &pkg.Context{\n\t\t\t\tSource: &source.Description{\n\t\t\t\t\tName:    \"my-service\",\n\t\t\t\t\tVersion: \"2.0.0\",\n\t\t\t\t\tMetadata: source.FileMetadata{\n\t\t\t\t\t\tPath: \"/path/to/file\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: []string{\"pkg:generic/my-service@2.0.0\"},\n\t\t},\n\t\t{\n\t\t\tname: \"generic source with only name\",\n\t\t\tpkgContext: &pkg.Context{\n\t\t\t\tSource: &source.Description{\n\t\t\t\t\tName:    \"MyApp\",\n\t\t\t\t\tVersion: \"\",\n\t\t\t\t\tMetadata: source.DirectoryMetadata{\n\t\t\t\t\t\tPath: \"/some/path\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: []string{},\n\t\t},\n\t\t{\n\t\t\tname: \"generic source with only version\",\n\t\t\tpkgContext: &pkg.Context{\n\t\t\t\tSource: &source.Description{\n\t\t\t\t\tName:    \"\",\n\t\t\t\t\tVersion: \"1.0.0\",\n\t\t\t\t\tMetadata: source.DirectoryMetadata{\n\t\t\t\t\t\tPath: \"/some/path\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: []string{},\n\t\t},\n\t\t{\n\t\t\tname: \"generic source with neither name nor version\",\n\t\t\tpkgContext: &pkg.Context{\n\t\t\t\tSource: &source.Description{\n\t\t\t\t\tName:    \"\",\n\t\t\t\t\tVersion: \"\",\n\t\t\t\t\tMetadata: source.DirectoryMetadata{\n\t\t\t\t\t\tPath: \"/some/path\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: []string{},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot := productIdentifiersFromContext(tt.pkgContext)\n\n\t\t\trequire.Equal(t, tt.want, got)\n\t\t})\n\t}\n}\n\nfunc TestIdentifiersFromDigests_NormalizesDockerHubRepositoryURL(t *testing.T) {\n\tconst hash = \"124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126\"\n\tconst digest = \"docker.io/library/alpine@sha256:\" + hash\n\n\tids := identifiersFromDigests([]string{digest})\n\n\tvar repoURL string\n\tfor _, id := range ids {\n\t\tif !strings.HasPrefix(id, \"pkg:oci/\") {\n\t\t\tcontinue\n\t\t}\n\n\t\tp, err := packageurl.FromString(id)\n\t\trequire.NoError(t, err)\n\n\t\tif p.Name == \"alpine\" && p.Version == \"sha256:\"+hash {\n\t\t\trepoURL = p.Qualifiers.Map()[\"repository_url\"]\n\t\t\tbreak\n\t\t}\n\t}\n\n\trequire.NotEmpty(t, repoURL, \"expected to find alpine purl in identifiers: %#v\", ids)\n\trequire.Equal(t, \"index.docker.io/library\", repoURL)\n}\n\nfunc TestNormalizeDockerHubRepositoryURL(t *testing.T) {\n\ttests := []struct {\n\t\tinput    string\n\t\texpected string\n\t}{\n\t\t{\"docker.io/library\", \"index.docker.io/library\"},\n\t\t{\"index.docker.io/library\", \"index.docker.io/library\"},\n\t\t{\"registry-1.docker.io/library\", \"index.docker.io/library\"},\n\t\t{\"https://docker.io/library\", \"index.docker.io/library\"},\n\t\t{\"http://docker.io/library\", \"index.docker.io/library\"},\n\t\t{\"gcr.io/myorg\", \"gcr.io/myorg\"},\n\t\t{\"\", \"\"},\n\t\t{\"DOCKER.IO/Library\", \"index.docker.io/Library\"},\n\t\t{\"docker.io\", \"index.docker.io\"},\n\t\t{\"docker.io/\", \"index.docker.io\"},\n\t\t{\"  docker.io/library  \", \"index.docker.io/library\"},\n\t}\n\tfor _, tc := range tests {\n\t\tt.Run(tc.input, func(t *testing.T) {\n\t\t\tgot := normalizeDockerHubRepositoryURL(tc.input)\n\t\t\trequire.Equal(t, tc.expected, got)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/vex/processor.go",
    "content": "package vex\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/anchore/grype/grype/match\"\n\t\"github.com/anchore/grype/grype/pkg\"\n\t\"github.com/anchore/grype/grype/vex/csaf\"\n\t\"github.com/anchore/grype/grype/vex/openvex\"\n)\n\ntype Processor struct {\n\tOptions ProcessorOptions\n\timpl    vexProcessorImplementation\n}\n\ntype vexProcessorImplementation interface {\n\t// ReadVexDocuments takes a list of vex filenames and returns a single\n\t// value representing the VEX information in the underlying implementation's\n\t// format. Returns an error if the files cannot be processed.\n\tReadVexDocuments(docs []string) (interface{}, error)\n\n\t// FilterMatches matches receives the underlying VEX implementation VEX data and\n\t// the scanning context and matching results and filters the fixed and\n\t// not_affected results,moving them to the list of ignored matches.\n\tFilterMatches(interface{}, []match.IgnoreRule, *pkg.Context, *match.Matches, []match.IgnoredMatch) (*match.Matches, []match.IgnoredMatch, error)\n\n\t// AugmentMatches reads known affected VEX products from loaded documents and\n\t// adds new results to the scanner results when the product is marked as\n\t// affected in the VEX data.\n\tAugmentMatches(interface{}, []match.IgnoreRule, *pkg.Context, *match.Matches, []match.IgnoredMatch) (*match.Matches, []match.IgnoredMatch, error)\n}\n\n// getVexImplementation this function returns the vex processor implementation\n// at some point it can read the options and choose a user configured implementation.\nfunc getVexImplementation(documents []string) (vexProcessorImplementation, error) {\n\t// No documents, no implementation\n\tif len(documents) == 0 {\n\t\treturn nil, nil\n\t}\n\n\tfirstDoc := documents[0]\n\n\tif _, err := os.Stat(firstDoc); err != nil {\n\t\treturn nil, fmt.Errorf(\"VEX document %q not found\", firstDoc)\n\t}\n\n\tif csaf.IsCSAF(firstDoc) {\n\t\treturn csaf.New(), nil\n\t}\n\tif openvex.IsOpenVex(firstDoc) {\n\t\treturn openvex.New(), nil\n\t}\n\n\treturn nil, fmt.Errorf(\"unsupported VEX document format for %q\", firstDoc)\n}\n\n// NewProcessor returns a new VEX processor\nfunc NewProcessor(opts ProcessorOptions) (*Processor, error) {\n\timplementation, err := getVexImplementation(opts.Documents)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create VEX processor: %w\", err)\n\t}\n\n\treturn &Processor{\n\t\tOptions: opts,\n\t\timpl:    implementation,\n\t}, nil\n}\n\n// ProcessorOptions captures the options of the VEX processor.\ntype ProcessorOptions struct {\n\tDocuments   []string\n\tIgnoreRules []match.IgnoreRule\n}\n\n// ApplyVEX receives the results from a scan run and applies any VEX information\n// in the files specified in the grype invocation. Any filtered results will\n// be moved to the ignored matches slice.\nfunc (vm *Processor) ApplyVEX(pkgContext *pkg.Context, remainingMatches *match.Matches, ignoredMatches []match.IgnoredMatch) (*match.Matches, []match.IgnoredMatch, error) {\n\tvar err error\n\n\t// If no VEX documents are loaded, just pass through the matches, effectively NOOP\n\tif len(vm.Options.Documents) == 0 {\n\t\treturn remainingMatches, ignoredMatches, nil\n\t}\n\n\t// Read VEX data from all passed documents\n\trawVexData, err := vm.impl.ReadVexDocuments(vm.Options.Documents)\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"parsing vex document: %w\", err)\n\t}\n\n\tvexRules := extractVexRules(vm.Options.IgnoreRules)\n\n\tremainingMatches, ignoredMatches, err = vm.impl.FilterMatches(\n\t\trawVexData, vexRules, pkgContext, remainingMatches, ignoredMatches,\n\t)\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"checking matches against VEX data: %w\", err)\n\t}\n\n\tremainingMatches, ignoredMatches, err = vm.impl.AugmentMatches(\n\t\trawVexData, vexRules, pkgContext, remainingMatches, ignoredMatches,\n\t)\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"checking matches to augment from VEX data: %w\", err)\n\t}\n\n\treturn remainingMatches, ignoredMatches, nil\n}\n\n// extractVexRules is a utility function that takes a set of ignore rules and\n// extracts those that act on VEX statuses.\nfunc extractVexRules(rules []match.IgnoreRule) []match.IgnoreRule {\n\tnewRules := []match.IgnoreRule{}\n\tfor _, r := range rules {\n\t\tif r.VexStatus != \"\" {\n\t\t\tnewRules = append(newRules, r)\n\t\t\tnewRules[len(newRules)-1].Namespace = \"vex\"\n\t\t}\n\t}\n\n\treturn newRules\n}\n"
  },
  {
    "path": "grype/vex/processor_test.go",
    "content": "package vex\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/grype/grype/match\"\n\t\"github.com/anchore/grype/grype/pkg\"\n\t\"github.com/anchore/grype/grype/vex/status\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n\t\"github.com/anchore/syft/syft/source\"\n)\n\nfunc TestProcessor_ApplyVEX(t *testing.T) {\n\tpkgContext := &pkg.Context{\n\t\tSource: &source.Description{\n\t\t\tName:    \"alpine\",\n\t\t\tVersion: \"3.17\",\n\t\t\tMetadata: source.ImageMetadata{\n\t\t\t\tRepoDigests: []string{\n\t\t\t\t\t\"alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tlibCryptoPackage := pkg.Package{\n\t\tID:      \"cc8f90662d91481d\",\n\t\tName:    \"libcrypto3\",\n\t\tVersion: \"3.0.8-r3\",\n\n\t\tType: \"apk\",\n\t\tPURL: \"pkg:apk/alpine/libcrypto3@3.0.8-r3?arch=x86_64&upstream=openssl&distro=alpine-3.17.3\",\n\t\tUpstreams: []pkg.UpstreamPackage{\n\t\t\t{\n\t\t\t\tName: \"openssl\",\n\t\t\t},\n\t\t},\n\t}\n\n\tlibCryptoCVE_2023_3817 := match.Match{\n\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\tReference: vulnerability.Reference{\n\t\t\t\tID:        \"CVE-2023-3817\",\n\t\t\t\tNamespace: \"alpine:distro:alpine:3.17\",\n\t\t\t},\n\t\t\tFix: vulnerability.Fix{\n\t\t\t\tVersions: []string{\"3.0.10-r0\"},\n\t\t\t\tState:    vulnerability.FixStateFixed,\n\t\t\t},\n\t\t},\n\t\tPackage: libCryptoPackage,\n\t}\n\n\tlibCryptoCVE_2023_1255 := match.Match{\n\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\tReference: vulnerability.Reference{\n\t\t\t\tID:        \"CVE-2023-1255\",\n\t\t\t\tNamespace: \"alpine:distro:alpine:3.17\",\n\t\t\t},\n\t\t\tFix: vulnerability.Fix{\n\t\t\t\tVersions: []string{\"3.0.8-r4\"},\n\t\t\t\tState:    vulnerability.FixStateFixed,\n\t\t\t},\n\t\t},\n\t\tPackage: libCryptoPackage,\n\t}\n\n\tlibCryptoCVE_2023_2975 := match.Match{\n\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\tReference: vulnerability.Reference{\n\t\t\t\tID:        \"CVE-2023-2975\",\n\t\t\t\tNamespace: \"alpine:distro:alpine:3.17\",\n\t\t\t},\n\t\t\tFix: vulnerability.Fix{\n\t\t\t\tVersions: []string{\"3.0.9-r2\"},\n\t\t\t\tState:    vulnerability.FixStateFixed,\n\t\t\t},\n\t\t},\n\t\tPackage: libCryptoPackage,\n\t}\n\n\tgetSubject := func() *match.Matches {\n\t\ts := match.NewMatches(\n\t\t\t// not-affected justification example\n\t\t\tlibCryptoCVE_2023_3817,\n\n\t\t\t// fixed status example + matching CVE\n\t\t\tlibCryptoCVE_2023_1255,\n\n\t\t\t// fixed status example\n\t\t\tlibCryptoCVE_2023_2975,\n\t\t)\n\n\t\treturn &s\n\t}\n\n\tmatchesRef := func(ms ...match.Match) *match.Matches {\n\t\tm := match.NewMatches(ms...)\n\t\treturn &m\n\t}\n\n\ttype args struct {\n\t\tpkgContext     *pkg.Context\n\t\tmatches        *match.Matches\n\t\tignoredMatches []match.IgnoredMatch\n\t}\n\n\ttests := []struct {\n\t\tname               string\n\t\toptions            ProcessorOptions\n\t\targs               args\n\t\twantMatches        *match.Matches\n\t\twantIgnoredMatches []match.IgnoredMatch\n\t\twantErr            require.ErrorAssertionFunc\n\t}{\n\t\t{\n\t\t\tname: \"csaf-demo1 - ignore by fixed status\",\n\t\t\toptions: ProcessorOptions{\n\t\t\t\tDocuments: []string{\n\t\t\t\t\t\"testdata/vex-docs/csaf-demo1.json\",\n\t\t\t\t},\n\t\t\t\tIgnoreRules: []match.IgnoreRule{{\n\t\t\t\t\tVexStatus: string(status.Fixed),\n\t\t\t\t}},\n\t\t\t},\n\t\t\targs: args{\n\t\t\t\tpkgContext: pkgContext,\n\t\t\t\tmatches:    getSubject(),\n\t\t\t},\n\t\t\twantMatches: matchesRef(libCryptoCVE_2023_3817, libCryptoCVE_2023_2975),\n\t\t\twantIgnoredMatches: []match.IgnoredMatch{{\n\t\t\t\tMatch: libCryptoCVE_2023_1255,\n\t\t\t\tAppliedIgnoreRules: []match.IgnoreRule{{\n\t\t\t\t\tNamespace: \"vex\",\n\t\t\t\t\tVexStatus: string(status.Fixed),\n\t\t\t\t}},\n\t\t\t}},\n\t\t},\n\t\t{\n\t\t\tname: \"csaf-demo1 - ignore by fixed status and CVE\",\n\t\t\toptions: ProcessorOptions{\n\t\t\t\tDocuments: []string{\n\t\t\t\t\t\"testdata/vex-docs/csaf-demo1.json\",\n\t\t\t\t},\n\t\t\t\tIgnoreRules: []match.IgnoreRule{{\n\t\t\t\t\tVexStatus:     string(status.Fixed),\n\t\t\t\t\tVulnerability: \"CVE-2023-1255\", // note: and previous tests\n\t\t\t\t}},\n\t\t\t},\n\t\t\targs: args{\n\t\t\t\tpkgContext: pkgContext,\n\t\t\t\tmatches:    getSubject(),\n\t\t\t},\n\t\t\twantMatches: matchesRef(libCryptoCVE_2023_3817, libCryptoCVE_2023_2975),\n\t\t\twantIgnoredMatches: []match.IgnoredMatch{{\n\t\t\t\tMatch: libCryptoCVE_2023_1255,\n\t\t\t\tAppliedIgnoreRules: []match.IgnoreRule{{\n\t\t\t\t\tNamespace:     \"vex\",\n\t\t\t\t\tVulnerability: \"CVE-2023-1255\",\n\t\t\t\t\tVexStatus:     string(status.Fixed),\n\t\t\t\t}},\n\t\t\t}},\n\t\t},\n\t\t{\n\t\t\tname: \"csaf-demo2 - ignore by not_affected status and vulnerable_code_not_present justification\",\n\t\t\toptions: ProcessorOptions{\n\t\t\t\tDocuments: []string{\n\t\t\t\t\t\"testdata/vex-docs/csaf-demo2.json\",\n\t\t\t\t},\n\t\t\t\tIgnoreRules: []match.IgnoreRule{{\n\t\t\t\t\tVexStatus:        string(status.NotAffected),\n\t\t\t\t\tVexJustification: \"vulnerable_code_not_present\", // note: this is the difference between this test and previous tests\n\t\t\t\t}},\n\t\t\t},\n\t\t\targs: args{\n\t\t\t\tpkgContext: pkgContext,\n\t\t\t\tmatches:    getSubject(),\n\t\t\t},\n\t\t\twantMatches: matchesRef(libCryptoCVE_2023_1255, libCryptoCVE_2023_2975),\n\t\t\twantIgnoredMatches: []match.IgnoredMatch{{\n\t\t\t\tMatch: libCryptoCVE_2023_3817,\n\t\t\t\tAppliedIgnoreRules: []match.IgnoreRule{{\n\t\t\t\t\tNamespace:        \"vex\",\n\t\t\t\t\tVexJustification: \"vulnerable_code_not_present\",\n\t\t\t\t\tVexStatus:        string(status.NotAffected),\n\t\t\t\t}},\n\t\t\t}},\n\t\t},\n\t\t{\n\t\t\tname: \"openvex-demo1 - ignore by fixed status\",\n\t\t\toptions: ProcessorOptions{\n\t\t\t\tDocuments: []string{\n\t\t\t\t\t\"testdata/vex-docs/openvex-demo1.json\",\n\t\t\t\t},\n\t\t\t\tIgnoreRules: []match.IgnoreRule{{\n\t\t\t\t\tVexStatus: string(status.Fixed),\n\t\t\t\t}},\n\t\t\t},\n\t\t\targs: args{\n\t\t\t\tpkgContext: pkgContext,\n\t\t\t\tmatches:    getSubject(),\n\t\t\t},\n\t\t\twantMatches: matchesRef(libCryptoCVE_2023_3817, libCryptoCVE_2023_2975),\n\t\t\twantIgnoredMatches: []match.IgnoredMatch{{\n\t\t\t\tMatch: libCryptoCVE_2023_1255,\n\t\t\t\tAppliedIgnoreRules: []match.IgnoreRule{{\n\t\t\t\t\tNamespace: \"vex\",\n\t\t\t\t\tVexStatus: string(status.Fixed),\n\t\t\t\t}},\n\t\t\t}},\n\t\t},\n\t\t{\n\t\t\tname: \"openvex-demo1 - ignore by fixed status and CVE\", // no real difference from the first test other than the AppliedIgnoreRules\n\t\t\toptions: ProcessorOptions{\n\t\t\t\tDocuments: []string{\n\t\t\t\t\t\"testdata/vex-docs/openvex-demo1.json\",\n\t\t\t\t},\n\t\t\t\tIgnoreRules: []match.IgnoreRule{{\n\t\t\t\t\tVulnerability: \"CVE-2023-1255\", // note: this is the difference between this test and the last test\n\t\t\t\t\tVexStatus:     string(status.Fixed),\n\t\t\t\t}},\n\t\t\t},\n\t\t\targs: args{\n\t\t\t\tpkgContext: pkgContext,\n\t\t\t\tmatches:    getSubject(),\n\t\t\t},\n\t\t\twantMatches: matchesRef(libCryptoCVE_2023_3817, libCryptoCVE_2023_2975),\n\t\t\twantIgnoredMatches: []match.IgnoredMatch{{\n\t\t\t\tMatch: libCryptoCVE_2023_1255,\n\t\t\t\tAppliedIgnoreRules: []match.IgnoreRule{{\n\t\t\t\t\tNamespace:     \"vex\",\n\t\t\t\t\tVulnerability: \"CVE-2023-1255\", // note: this is the difference between this test and the last test\n\t\t\t\t\tVexStatus:     string(status.Fixed),\n\t\t\t\t}},\n\t\t\t}},\n\t\t},\n\t\t{\n\t\t\tname: \"openvex-demo2 - ignore by fixed status\",\n\t\t\toptions: ProcessorOptions{\n\t\t\t\tDocuments: []string{\n\t\t\t\t\t\"testdata/vex-docs/openvex-demo2.json\",\n\t\t\t\t},\n\t\t\t\tIgnoreRules: []match.IgnoreRule{{\n\t\t\t\t\tVexStatus: string(status.Fixed),\n\t\t\t\t}},\n\t\t\t},\n\t\t\targs: args{\n\t\t\t\tpkgContext: pkgContext,\n\t\t\t\tmatches:    getSubject(),\n\t\t\t},\n\t\t\twantMatches: matchesRef(libCryptoCVE_2023_3817),\n\t\t\twantIgnoredMatches: []match.IgnoredMatch{{\n\t\t\t\tMatch: libCryptoCVE_2023_1255,\n\t\t\t\tAppliedIgnoreRules: []match.IgnoreRule{{\n\t\t\t\t\tNamespace: \"vex\",\n\t\t\t\t\tVexStatus: string(status.Fixed),\n\t\t\t\t}},\n\t\t\t}, {\n\t\t\t\tMatch: libCryptoCVE_2023_2975,\n\t\t\t\tAppliedIgnoreRules: []match.IgnoreRule{{\n\t\t\t\t\tNamespace: \"vex\",\n\t\t\t\t\tVexStatus: string(status.Fixed),\n\t\t\t\t}},\n\t\t\t}},\n\t\t},\n\t\t{\n\t\t\tname: \"openvex-demo2 - ignore by fixed status and CVE\",\n\t\t\toptions: ProcessorOptions{\n\t\t\t\tDocuments: []string{\n\t\t\t\t\t\"testdata/vex-docs/openvex-demo2.json\",\n\t\t\t\t},\n\t\t\t\tIgnoreRules: []match.IgnoreRule{{\n\t\t\t\t\tVulnerability: \"CVE-2023-1255\", // note: this is the difference between this test and the last test\n\t\t\t\t\tVexStatus:     string(status.Fixed),\n\t\t\t\t}},\n\t\t\t},\n\t\t\targs: args{\n\t\t\t\tpkgContext: pkgContext,\n\t\t\t\tmatches:    getSubject(),\n\t\t\t},\n\t\t\twantMatches: matchesRef(libCryptoCVE_2023_3817, libCryptoCVE_2023_2975),\n\t\t\twantIgnoredMatches: []match.IgnoredMatch{{\n\t\t\t\tMatch: libCryptoCVE_2023_1255,\n\t\t\t\tAppliedIgnoreRules: []match.IgnoreRule{{\n\t\t\t\t\tNamespace:     \"vex\",\n\t\t\t\t\tVulnerability: \"CVE-2023-1255\", // note: this is the difference between this test and the last test\n\t\t\t\t\tVexStatus:     string(status.Fixed),\n\t\t\t\t}},\n\t\t\t}},\n\t\t},\n\t\t{\n\t\t\tname: \"openvex-demo1 - ignore by not_affected status and vulnerable_code_not_present justification\",\n\t\t\toptions: ProcessorOptions{\n\t\t\t\tDocuments: []string{\n\t\t\t\t\t\"testdata/vex-docs/openvex-demo1.json\",\n\t\t\t\t},\n\t\t\t\tIgnoreRules: []match.IgnoreRule{{\n\t\t\t\t\tVexStatus:        \"not_affected\",\n\t\t\t\t\tVexJustification: \"vulnerable_code_not_present\",\n\t\t\t\t}},\n\t\t\t},\n\t\t\targs: args{\n\t\t\t\tpkgContext: pkgContext,\n\t\t\t\tmatches:    getSubject(),\n\t\t\t},\n\t\t\t// nothing gets ignored!\n\t\t\twantMatches:        matchesRef(libCryptoCVE_2023_3817, libCryptoCVE_2023_2975, libCryptoCVE_2023_1255),\n\t\t\twantIgnoredMatches: []match.IgnoredMatch{},\n\t\t},\n\t\t{\n\t\t\tname: \"openvex-demo2 - ignore by not_affected status and vulnerable_code_not_present justification\",\n\t\t\toptions: ProcessorOptions{\n\t\t\t\tDocuments: []string{\n\t\t\t\t\t\"testdata/vex-docs/openvex-demo2.json\",\n\t\t\t\t},\n\t\t\t\tIgnoreRules: []match.IgnoreRule{{\n\t\t\t\t\tVexStatus:        \"not_affected\",\n\t\t\t\t\tVexJustification: \"vulnerable_code_not_present\",\n\t\t\t\t}},\n\t\t\t},\n\t\t\targs: args{\n\t\t\t\tpkgContext: pkgContext,\n\t\t\t\tmatches:    getSubject(),\n\t\t\t},\n\t\t\twantMatches: matchesRef(libCryptoCVE_2023_2975, libCryptoCVE_2023_1255),\n\t\t\twantIgnoredMatches: []match.IgnoredMatch{{\n\t\t\t\tMatch: libCryptoCVE_2023_3817,\n\t\t\t\tAppliedIgnoreRules: []match.IgnoreRule{{\n\t\t\t\t\tNamespace:        \"vex\",\n\t\t\t\t\tVexStatus:        \"not_affected\",\n\t\t\t\t\tVexJustification: \"vulnerable_code_not_present\",\n\t\t\t\t}},\n\t\t\t}},\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif tt.wantErr == nil {\n\t\t\t\ttt.wantErr = require.NoError\n\t\t\t}\n\n\t\t\tp, err := NewProcessor(tt.options)\n\t\t\ttt.wantErr(t, err)\n\t\t\tif err != nil {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tactualMatches, actualIgnoredMatches, err := p.ApplyVEX(tt.args.pkgContext, tt.args.matches, tt.args.ignoredMatches)\n\t\t\ttt.wantErr(t, err)\n\t\t\tif err != nil {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.Equal(t, tt.wantMatches.Sorted(), actualMatches.Sorted())\n\t\t\tassert.Equal(t, tt.wantIgnoredMatches, actualIgnoredMatches)\n\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/vex/status/status.go",
    "content": "package status\n\ntype Status string\n\n// VEX statuses as defined by CISA\n// https://www.cisa.gov/sites/default/files/2023-04/minimum-requirements-for-vex-508c.pdf\n//\n// Different VEX implementation can use different names to refer to them\nconst (\n\tNotAffected        Status = \"not_affected\"\n\tAffected           Status = \"affected\"\n\tFixed              Status = \"fixed\"\n\tUnderInvestigation Status = \"under_investigation\"\n)\n\n// AugmentList returns the VEX statuses that augment results\nfunc AugmentList() []Status {\n\treturn []Status{Affected, UnderInvestigation}\n}\n\n// IgnoreList returns the VEX statuses that should be ignored\nfunc IgnoreList() []Status {\n\treturn []Status{Fixed, NotAffected}\n}\n"
  },
  {
    "path": "grype/vex/testdata/vex-docs/csaf-demo1.json",
    "content": "{\n  \"document\": {\n    \"category\": \"csaf_vex\",\n    \"csaf_version\": \"2.0\",\n    \"notes\": [\n      {\n        \"category\": \"summary\",\n        \"text\": \"Example Company VEX document. Unofficial content for demonstration purposes only.\",\n        \"title\": \"Author comment\"\n      }\n    ],\n    \"publisher\": {\n      \"category\": \"vendor\",\n      \"name\": \"Example Company ProductCERT\",\n      \"namespace\": \"https://psirt.example.com\"\n    },\n    \"title\": \"AquaSecurity example VEX document\",\n    \"tracking\": {\n      \"current_release_date\": \"2022-03-03T11:00:00.000Z\",\n      \"generator\": {\n        \"date\": \"2022-03-03T11:00:00.000Z\",\n        \"engine\": {\n          \"name\": \"Secvisogram\",\n          \"version\": \"1.11.0\"\n        }\n      },\n      \"id\": \"2022-EVD-UC-01-A-001\",\n      \"initial_release_date\": \"2022-03-03T11:00:00.000Z\",\n      \"revision_history\": [\n        {\n          \"date\": \"2022-03-03T11:00:00.000Z\",\n          \"number\": \"1\",\n          \"summary\": \"Initial version.\"\n        }\n      ],\n      \"status\": \"final\",\n      \"version\": \"1\"\n    }\n  },\n  \"product_tree\": {\n    \"branches\": [\n      {\n        \"branches\": [\n          {\n            \"branches\": [\n              {\n                \"category\": \"product_version\",\n                \"name\": \"2.6.0\",\n                \"product\": {\n                  \"name\": \"Spring Boot 2.6.0\",\n                  \"product_id\": \"SPB-00260\",\n                  \"product_identification_helper\": {\n                    \"purl\": \"pkg:apk/alpine/libcrypto3@3.0.8-r3?arch=x86_64&upstream=openssl&distro=alpine-3.17.3\"\n                  }\n                }\n              }\n            ],\n            \"category\": \"product_name\",\n            \"name\": \"Spring Boot\"\n          }\n        ],\n        \"category\": \"vendor\",\n        \"name\": \"Spring\"\n      }\n    ]\n  },\n  \"vulnerabilities\": [\n    {\n      \"cve\": \"CVE-2023-1255\",\n      \"product_status\": {\n        \"fixed\": [\n          \"SPB-00260\"\n        ]\n      },\n      \"threats\": [\n        {\n          \"category\": \"impact\",\n          \"details\": \"Class with vulnerable code was removed before shipping.\",\n          \"product_ids\": [\n            \"SPB-00260\"\n          ]\n        }\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": "grype/vex/testdata/vex-docs/csaf-demo2.json",
    "content": "{\n  \"document\": {\n    \"category\": \"csaf_vex\",\n    \"csaf_version\": \"2.0\",\n    \"notes\": [\n      {\n        \"category\": \"summary\",\n        \"text\": \"Example Company VEX document. Unofficial content for demonstration purposes only.\",\n        \"title\": \"Author comment\"\n      }\n    ],\n    \"publisher\": {\n      \"category\": \"vendor\",\n      \"name\": \"Example Company ProductCERT\",\n      \"namespace\": \"https://psirt.example.com\"\n    },\n    \"title\": \"AquaSecurity example VEX document\",\n    \"tracking\": {\n      \"current_release_date\": \"2022-03-03T11:00:00.000Z\",\n      \"generator\": {\n        \"date\": \"2022-03-03T11:00:00.000Z\",\n        \"engine\": {\n          \"name\": \"Secvisogram\",\n          \"version\": \"1.11.0\"\n        }\n      },\n      \"id\": \"2022-EVD-UC-01-A-001\",\n      \"initial_release_date\": \"2022-03-03T11:00:00.000Z\",\n      \"revision_history\": [\n        {\n          \"date\": \"2022-03-03T11:00:00.000Z\",\n          \"number\": \"1\",\n          \"summary\": \"Initial version.\"\n        }\n      ],\n      \"status\": \"final\",\n      \"version\": \"1\"\n    }\n  },\n  \"product_tree\": {\n    \"branches\": [\n      {\n        \"branches\": [\n          {\n            \"branches\": [\n              {\n                \"category\": \"product_version\",\n                \"name\": \"2.6.0\",\n                \"product\": {\n                  \"name\": \"Spring Boot 2.6.0\",\n                  \"product_id\": \"SPB-00260\",\n                  \"product_identification_helper\": {\n                    \"purl\": \"pkg:apk/alpine/libcrypto3@3.0.8-r3?arch=x86_64&upstream=openssl&distro=alpine-3.17.3\"\n                  }\n                }\n              }\n            ],\n            \"category\": \"product_name\",\n            \"name\": \"Spring Boot\"\n          }\n        ],\n        \"category\": \"vendor\",\n        \"name\": \"Spring\"\n      }\n    ]\n  },\n  \"vulnerabilities\": [\n    {\n      \"cve\": \"CVE-2023-3817\",\n      \"flags\": [\n        {\n          \"label\": \"vulnerable_code_not_present\",\n          \"product_ids\": [\n            \"SPB-00260\"\n          ]\n        }\n      ],\n      \"product_status\": {\n        \"known_not_affected\": [\n          \"SPB-00260\"\n        ]\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "grype/vex/testdata/vex-docs/openvex-debian.json",
    "content": "{\n    \"@context\": \"https://openvex.dev/ns/v0.2.0\",\n    \"@id\": \"https://openvex.dev/docs/public/vex-d4e9020b6d0d26f131d535e055902dd6ccf3e2088bce3079a8cd3588a4b14c78\",\n    \"author\": \"The OpenVEX Project <openvex@openssf.org>\",\n    \"timestamp\": \"2023-07-17T18:28:47.696004345-06:00\",\n    \"version\": 1,\n    \"statements\": [\n      {\n        \"vulnerability\": {\n          \"name\": \"CVE-2014-fake-1\"\n        },\n        \"products\": [\n          {\n            \"@id\": \"pkg:oci/debian@sha256%3A124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126?repository_url=index.docker.io/library\"\n          }\n        ],\n        \"status\": \"fixed\"\n      }\n    ]\n  }\n"
  },
  {
    "path": "grype/vex/testdata/vex-docs/openvex-demo1.json",
    "content": "{\n  \"@context\": \"https://openvex.dev/ns/v0.2.0\",\n  \"@id\": \"https://openvex.dev/docs/public/vex-d4e9020b6d0d26f131d535e055902dd6ccf3e2088bce3079a8cd3588a4b14c78\",\n  \"author\": \"The OpenVEX Project <openvex@openssf.org>\",\n  \"timestamp\": \"2023-07-17T18:28:47.696004345-06:00\",\n  \"version\": 1,\n  \"statements\": [\n    {\n      \"vulnerability\": {\n        \"name\": \"CVE-2023-1255\"\n      },\n      \"products\": [\n        {\n          \"@id\": \"pkg:oci/alpine@sha256%3A124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126\",\n          \"subcomponents\": [\n            { \"@id\": \"pkg:apk/alpine/libssl3@3.0.8-r3\" },\n            { \"@id\": \"pkg:apk/alpine/libcrypto3@3.0.8-r3\" }\n          ]\n        }\n      ],\n      \"status\": \"fixed\"\n    }\n  ]\n}\n"
  },
  {
    "path": "grype/vex/testdata/vex-docs/openvex-demo2.json",
    "content": "{\n  \"@context\": \"https://openvex.dev/ns/v0.2.0\",\n  \"@id\": \"https://openvex.dev/docs/public/vex-d4e9020b6d0d26f131d535e055902dd6ccf3e2088bce3079a8cd3588a4b14c78\",\n  \"author\": \"The OpenVEX Project <openvex@openssf.org>\",\n  \"role\": \"Demo Writer\",\n  \"timestamp\": \"2023-07-17T18:28:47.696004345-06:00\",\n  \"version\": 1,\n  \"statements\": [\n    {\n      \"vulnerability\": {\n        \"name\": \"CVE-2023-1255\"\n      },\n      \"products\": [\n        {\n          \"@id\": \"pkg:oci/alpine@sha256%3A124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126\",\n          \"subcomponents\": [\n            { \"@id\": \"pkg:apk/alpine/libssl3@3.0.8-r3\" },\n            { \"@id\": \"pkg:apk/alpine/libcrypto3@3.0.8-r3\" }\n          ]\n        }\n      ],\n      \"status\": \"fixed\"\n    },\n    {\n      \"vulnerability\": {\n        \"name\": \"CVE-2023-2650\"\n      },\n      \"products\": [\n        {\n          \"@id\": \"pkg:oci/alpine@sha256%3A124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126\",\n          \"subcomponents\": [\n            { \"@id\": \"pkg:apk/alpine/libssl3@3.0.8-r3\" },\n            { \"@id\": \"pkg:apk/alpine/libcrypto3@3.0.8-r3\" }\n          ]\n        }\n      ],\n      \"status\": \"fixed\"\n    },\n    {\n        \"vulnerability\": {\n          \"name\": \"CVE-2023-2975\"\n        },\n        \"products\": [\n          {\n            \"@id\": \"pkg:oci/alpine@sha256%3A124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126\",\n            \"subcomponents\": [\n              { \"@id\": \"pkg:apk/alpine/libssl3@3.0.8-r3\" },\n              { \"@id\": \"pkg:apk/alpine/libcrypto3@3.0.8-r3\" }\n            ]\n          }\n        ],\n        \"status\": \"fixed\"\n      },\n      {\n        \"vulnerability\": {\n          \"name\": \"CVE-2023-3446\"\n        },\n        \"products\": [\n          {\n            \"@id\": \"pkg:oci/alpine@sha256%3A124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126\",\n            \"subcomponents\": [\n              { \"@id\": \"pkg:apk/alpine/libssl3@3.0.8-r3\" },\n              { \"@id\": \"pkg:apk/alpine/libcrypto3@3.0.8-r3\" }\n            ]\n          }\n        ],\n        \"status\": \"not_affected\",\n        \"justification\": \"vulnerable_code_not_present\",\n        \"impact_statement\": \"affected functions were removed before packaging\"\n      },\n      {\n        \"vulnerability\": {\n          \"name\": \"CVE-2023-3817\"\n        },\n        \"products\": [\n          {\n            \"@id\": \"pkg:oci/alpine@sha256%3A124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126\",\n            \"subcomponents\": [\n              { \"@id\": \"pkg:apk/alpine/libssl3@3.0.8-r3\" },\n              { \"@id\": \"pkg:apk/alpine/libcrypto3@3.0.8-r3\" }\n            ]\n          }\n        ],\n        \"status\": \"not_affected\",\n        \"justification\": \"vulnerable_code_not_present\",\n        \"impact_statement\": \"affected functions were removed before packaging\"\n      }\n  ]\n}\n"
  },
  {
    "path": "grype/vex/testdata/vex-docs/openvex-image-no-subcomponents.json",
    "content": "{\n  \"@context\": \"https://openvex.dev/ns/v0.2.0\",\n  \"@id\": \"https://openvex.dev/docs/public/vex-image-no-subcomponents\",\n  \"author\": \"The OpenVEX Project <openvex@openssf.org>\",\n  \"timestamp\": \"2023-07-17T18:28:47.696004345-06:00\",\n  \"version\": 1,\n  \"statements\": [\n    {\n      \"vulnerability\": {\n        \"name\": \"CVE-2023-1255\"\n      },\n      \"products\": [\n        {\n          \"@id\": \"pkg:oci/alpine@sha256%3A124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126\"\n        }\n      ],\n      \"status\": \"fixed\"\n    }\n  ]\n}\n"
  },
  {
    "path": "grype/vex/testdata/vex-docs/openvex-package-product.json",
    "content": "{\n  \"@context\": \"https://openvex.dev/ns/v0.2.0\",\n  \"@id\": \"https://openvex.dev/docs/public/vex-package-product\",\n  \"author\": \"The OpenVEX Project <openvex@openssf.org>\",\n  \"timestamp\": \"2023-07-17T18:28:47.696004345-06:00\",\n  \"version\": 1,\n  \"statements\": [\n    {\n      \"vulnerability\": {\n        \"name\": \"CVE-2023-1255\"\n      },\n      \"products\": [\n        {\n          \"@id\": \"pkg:apk/alpine/libcrypto3@3.0.8-r3\"\n        }\n      ],\n      \"status\": \"fixed\"\n    }\n  ]\n}\n"
  },
  {
    "path": "grype/vulnerability/advisory.go",
    "content": "package vulnerability\n\ntype Advisory struct {\n\tID   string\n\tLink string\n}\n"
  },
  {
    "path": "grype/vulnerability/fix.go",
    "content": "package vulnerability\n\nimport \"time\"\n\ntype FixState string\n\nconst (\n\tFixStateUnknown  FixState = \"unknown\"\n\tFixStateFixed    FixState = \"fixed\"\n\tFixStateNotFixed FixState = \"not-fixed\"\n\tFixStateWontFix  FixState = \"wont-fix\"\n)\n\nfunc AllFixStates() []FixState {\n\treturn []FixState{\n\t\tFixStateFixed,\n\t\tFixStateNotFixed,\n\t\tFixStateUnknown,\n\t\tFixStateWontFix,\n\t}\n}\n\ntype Fix struct {\n\tVersions  []string\n\tState     FixState\n\tAvailable []FixAvailable\n}\n\ntype FixAvailable struct {\n\tVersion string\n\tDate    time.Time\n\tKind    string\n}\n\nfunc (f FixState) String() string {\n\treturn string(f)\n}\n"
  },
  {
    "path": "grype/vulnerability/metadata.go",
    "content": "package vulnerability\n\nimport (\n\t\"strings\"\n\t\"time\"\n)\n\ntype Metadata struct {\n\tID             string\n\tDataSource     string // the primary reference URL, i.e. where the data originated\n\tNamespace      string\n\tSeverity       string\n\tURLs           []string // secondary reference URLs a vulnerability may provide\n\tDescription    string\n\tCvss           []Cvss\n\tKnownExploited []KnownExploited\n\tEPSS           []EPSS\n\tCWEs           []CWE\n\n\t// calculated as-needed\n\trisk float64\n}\n\n// RiskScore computes a basic quantitative risk by combining threat and severity.\n// Threat is represented by epss (likelihood of exploitation), and severity by the cvss base score + string severity.\n// Impact is currently fixed at 1 and may be integrated into the calculation in future versions.\n// Raw risk is epss * (cvss / 10) * impact, then scaled to 0–100 for readability.\n// If a vulnerability appears in the KEV list, apply an additional boost to reflect known exploitation.\n// Known ransomware campaigns receive a further, distinct boost.\nfunc (m *Metadata) RiskScore() float64 {\n\tif m == nil {\n\t\treturn 0\n\t}\n\tif m.risk != 0 {\n\t\treturn m.risk\n\t}\n\tm.risk = riskScore(*m)\n\treturn m.risk\n}\n\nfunc riskScore(m Metadata) float64 {\n\treturn min(threat(m)*severity(m)*kevModifier(m), 1.0) * 100.0\n}\n\nfunc kevModifier(m Metadata) float64 {\n\tif len(m.KnownExploited) > 0 {\n\t\tfor _, kev := range m.KnownExploited {\n\t\t\tif strings.ToLower(kev.KnownRansomwareCampaignUse) == \"known\" {\n\t\t\t\t// consider ransomware campaigns to be a greater kevModifier than other KEV threats\n\t\t\t\treturn 1.1\n\t\t\t}\n\t\t}\n\t\treturn 1.05 // boost the final result, as if there is a greater kevModifier inherently from KEV threats\n\t}\n\treturn 1.0\n}\n\nfunc threat(m Metadata) float64 {\n\tif len(m.KnownExploited) > 0 {\n\t\t// per the EPSS guidance, any evidence of exploitation in the wild (not just PoC) should be considered over EPSS data\n\t\treturn 1.0\n\t}\n\tif len(m.EPSS) == 0 {\n\t\treturn 0.0\n\t}\n\treturn m.EPSS[0].EPSS\n}\n\n// severity returns a 0-1 value, which is a combination of the string severity and the average of the cvss base scores.\n// If there are no cvss scores, the string severity is used. Some vendors only update the string severity and not the\n// cvss scores, so it's important to consider all sources. We are also not biasing towards any one source (multiple\n// cvss scores won't over-weigh the string severity).\nfunc severity(m Metadata) float64 {\n\t// TODO: summarization should take a policy: prefer NVD over CNA or vice versa...\n\n\tstringSeverityScore := severityToScore(m.Severity) / 10.0\n\tavgBaseScore := average(validBaseScores(m.Cvss...)...) / 10.0\n\tif avgBaseScore == 0 {\n\t\treturn stringSeverityScore\n\t}\n\treturn average(stringSeverityScore, avgBaseScore)\n}\n\nfunc severityToScore(severity string) float64 {\n\t// use the middle of the range for each severity\n\tswitch strings.ToLower(severity) {\n\tcase \"negligible\":\n\t\treturn 0.5\n\tcase \"low\":\n\t\treturn 3.0\n\tcase \"medium\":\n\t\treturn 5.0\n\tcase \"high\":\n\t\treturn 7.5\n\tcase \"critical\":\n\t\treturn 9.0\n\t}\n\t// the severity value might be \"unknown\" or an unexpected value. These should not be lost\n\t// in the noise and placed at the bottom of the list... instead we compromise to the middle of the list.\n\treturn 5.0\n}\n\nfunc validBaseScores(as ...Cvss) []float64 {\n\tvar out []float64\n\tfor _, a := range as {\n\t\tif a.Metrics.BaseScore == 0 {\n\t\t\t// this is a mistake... base scores cannot be 0. Don't include this value and bring down the average\n\t\t\tcontinue\n\t\t}\n\t\tout = append(out, a.Metrics.BaseScore)\n\t}\n\treturn out\n}\n\nfunc average(as ...float64) float64 {\n\tif len(as) == 0 {\n\t\treturn 0\n\t}\n\tsum := 0.0\n\tfor _, a := range as {\n\t\tsum += a\n\t}\n\treturn sum / float64(len(as))\n}\n\ntype Cvss struct {\n\tSource         string\n\tType           string\n\tVersion        string\n\tVector         string\n\tMetrics        CvssMetrics\n\tVendorMetadata interface{}\n}\n\ntype CvssMetrics struct {\n\tBaseScore           float64\n\tExploitabilityScore *float64\n\tImpactScore         *float64\n}\n\ntype KnownExploited struct {\n\tCVE                        string\n\tVendorProject              string\n\tProduct                    string\n\tDateAdded                  *time.Time\n\tRequiredAction             string\n\tDueDate                    *time.Time\n\tKnownRansomwareCampaignUse string\n\tNotes                      string\n\tURLs                       []string\n\tCWEs                       []string\n}\n\ntype EPSS struct {\n\tCVE        string\n\tEPSS       float64\n\tPercentile float64\n\tDate       time.Time\n}\n\ntype CWE struct {\n\tCVE    string\n\tCWE    string\n\tSource string\n\tType   string\n}\n"
  },
  {
    "path": "grype/vulnerability/metadata_test.go",
    "content": "package vulnerability\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestRiskScore(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tmetadata Metadata\n\t\texpected float64\n\t}{\n\t\t{\n\t\t\tname:     \"nil metadata\",\n\t\t\tmetadata: Metadata{},\n\t\t\texpected: 0,\n\t\t},\n\t\t{\n\t\t\tname: \"already calculated risk\",\n\t\t\tmetadata: Metadata{\n\t\t\t\trisk: 42.5,\n\t\t\t},\n\t\t\texpected: 42.5,\n\t\t},\n\t\t{\n\t\t\tname: \"no EPSS data, no KEV\",\n\t\t\tmetadata: Metadata{\n\t\t\t\tSeverity: \"high\",\n\t\t\t\tCvss: []Cvss{\n\t\t\t\t\t{\n\t\t\t\t\t\tMetrics: CvssMetrics{\n\t\t\t\t\t\t\tBaseScore: 7.5,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: 0, // threat is 0 without EPSS or KEV\n\t\t},\n\t\t{\n\t\t\tname: \"with EPSS data, no KEV\",\n\t\t\tmetadata: Metadata{\n\t\t\t\tSeverity: \"high\",\n\t\t\t\tEPSS: []EPSS{\n\t\t\t\t\t{\n\t\t\t\t\t\tEPSS:       0.5,\n\t\t\t\t\t\tPercentile: 0.95,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tCvss: []Cvss{\n\t\t\t\t\t{\n\t\t\t\t\t\tMetrics: CvssMetrics{\n\t\t\t\t\t\t\tBaseScore: 7.5,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: 37.5, // 0.5 * (7.5/10) * 1 * 100\n\t\t},\n\t\t{\n\t\t\tname: \"with KEV, no EPSS\",\n\t\t\tmetadata: Metadata{\n\t\t\t\tSeverity: \"high\",\n\t\t\t\tKnownExploited: []KnownExploited{\n\t\t\t\t\t{\n\t\t\t\t\t\tCVE:                        \"CVE-2023-1234\",\n\t\t\t\t\t\tKnownRansomwareCampaignUse: \"No\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tCvss: []Cvss{\n\t\t\t\t\t{\n\t\t\t\t\t\tMetrics: CvssMetrics{\n\t\t\t\t\t\t\tBaseScore: 7.5,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: 78.75, // 1.0 * (7.5/10) * 1.05* 100\n\t\t},\n\t\t{\n\t\t\tname: \"with KEV ransomware\",\n\t\t\tmetadata: Metadata{\n\t\t\t\tSeverity: \"high\",\n\t\t\t\tKnownExploited: []KnownExploited{\n\t\t\t\t\t{\n\t\t\t\t\t\tCVE:                        \"CVE-2023-1234\",\n\t\t\t\t\t\tKnownRansomwareCampaignUse: \"Known\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tCvss: []Cvss{\n\t\t\t\t\t{\n\t\t\t\t\t\tMetrics: CvssMetrics{\n\t\t\t\t\t\t\tBaseScore: 7.5,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: 82.5, // 1.0 * (7.5/10) * 1.1 * 100\n\t\t},\n\t\t{\n\t\t\tname: \"with severity string only\",\n\t\t\tmetadata: Metadata{\n\t\t\t\tSeverity: \"critical\",\n\t\t\t\tEPSS: []EPSS{\n\t\t\t\t\t{\n\t\t\t\t\t\tEPSS:       0.8,\n\t\t\t\t\t\tPercentile: 0.99,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: 72, // 0.8 * (9.0/10) * 1.0 * 100\n\t\t},\n\t\t{\n\t\t\tname: \"with multiple CVSS scores + string severity\",\n\t\t\tmetadata: Metadata{\n\t\t\t\tSeverity: \"medium\",\n\t\t\t\tEPSS: []EPSS{\n\t\t\t\t\t{\n\t\t\t\t\t\tEPSS:       0.6,\n\t\t\t\t\t\tPercentile: 0.90,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tCvss: []Cvss{\n\t\t\t\t\t{\n\t\t\t\t\t\tSource: \"NVD\",\n\t\t\t\t\t\tMetrics: CvssMetrics{\n\t\t\t\t\t\t\tBaseScore: 6.5,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tSource: \"Vendor\",\n\t\t\t\t\t\tMetrics: CvssMetrics{\n\t\t\t\t\t\t\tBaseScore: 5.5,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: 33, // 0.6 * ( (((6.5+5.5)/2)+5)/2 /10) * 1.0 * 100\n\t\t},\n\t\t{\n\t\t\tname: \"with some invalid CVSS scores + string severity\",\n\t\t\tmetadata: Metadata{\n\t\t\t\tSeverity: \"medium\",\n\t\t\t\tEPSS: []EPSS{\n\t\t\t\t\t{\n\t\t\t\t\t\tEPSS:       0.4,\n\t\t\t\t\t\tPercentile: 0.85,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tCvss: []Cvss{\n\t\t\t\t\t{\n\t\t\t\t\t\tSource: \"NVD\",\n\t\t\t\t\t\tMetrics: CvssMetrics{\n\t\t\t\t\t\t\tBaseScore: 0, // invalid, should be ignored\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tSource: \"Vendor\",\n\t\t\t\t\t\tMetrics: CvssMetrics{\n\t\t\t\t\t\t\tBaseScore: 6.0,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: 22, // 0.4 * ((6.0+5)/2 /10) * 1.0 * 100\n\t\t},\n\t\t{\n\t\t\tname: \"unknown severity\",\n\t\t\tmetadata: Metadata{\n\t\t\t\tSeverity: \"unknown\",\n\t\t\t\tEPSS: []EPSS{\n\t\t\t\t\t{\n\t\t\t\t\t\tEPSS:       0.3,\n\t\t\t\t\t\tPercentile: 0.80,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: 15, // 0.3 * (5.0/10) * 1.0 * 100\n\t\t},\n\t\t{\n\t\t\tname: \"maximum risk clamp\",\n\t\t\tmetadata: Metadata{\n\t\t\t\tSeverity: \"critical\",\n\t\t\t\tKnownExploited: []KnownExploited{\n\t\t\t\t\t{\n\t\t\t\t\t\tCVE:                        \"CVE-2023-1234\",\n\t\t\t\t\t\tKnownRansomwareCampaignUse: \"Known\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tCvss: []Cvss{\n\t\t\t\t\t{\n\t\t\t\t\t\tMetrics: CvssMetrics{\n\t\t\t\t\t\t\tBaseScore: 10.0,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: 100, // clamped to 100 as it would be 1.0 * 1.0 * 1.1 * 100 = 120\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := tt.metadata.RiskScore()\n\t\t\tassert.InDelta(t, tt.expected, result, 0.01, \"RiskScore method returned incorrect value\")\n\n\t\t\t// test the calculated value is cached\n\t\t\tif tt.name != \"already calculated risk\" && tt.name != \"nil metadata\" {\n\t\t\t\trequire.InDelta(t, tt.expected, tt.metadata.risk, 0.01, \"risk was not cached\")\n\t\t\t}\n\n\t\t\t// test the standalone function\n\t\t\tif tt.name != \"nil metadata\" && tt.name != \"already calculated risk\" {\n\t\t\t\tfuncResult := riskScore(tt.metadata)\n\t\t\t\tassert.InDelta(t, tt.expected, funcResult, 0.0001, \"riskScore function returned incorrect value\")\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSeverityToScore(t *testing.T) {\n\ttests := []struct {\n\t\tseverity string\n\t\texpected float64\n\t}{\n\t\t{\"negligible\", 0.5},\n\t\t{\"NEGLIGIBLE\", 0.5},\n\t\t{\"low\", 3.0},\n\t\t{\"LOW\", 3.0},\n\t\t{\"medium\", 5.0},\n\t\t{\"MEDIUM\", 5.0},\n\t\t{\"high\", 7.5},\n\t\t{\"HIGH\", 7.5},\n\t\t{\"critical\", 9.0},\n\t\t{\"CRITICAL\", 9.0},\n\t\t{\"unknown\", 5.0},\n\t\t{\"\", 5.0},\n\t\t{\"something-else\", 5.0},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.severity, func(t *testing.T) {\n\t\t\tresult := severityToScore(tt.severity)\n\t\t\tassert.Equal(t, tt.expected, result)\n\t\t})\n\t}\n}\n\nfunc TestAverageCVSS(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tcvss     []Cvss\n\t\texpected float64\n\t}{\n\t\t{\n\t\t\tname:     \"empty slice\",\n\t\t\tcvss:     []Cvss{},\n\t\t\texpected: 0,\n\t\t},\n\t\t{\n\t\t\tname: \"single valid score\",\n\t\t\tcvss: []Cvss{\n\t\t\t\t{Metrics: CvssMetrics{BaseScore: 7.5}},\n\t\t\t},\n\t\t\texpected: 7.5,\n\t\t},\n\t\t{\n\t\t\tname: \"multiple valid scores\",\n\t\t\tcvss: []Cvss{\n\t\t\t\t{Metrics: CvssMetrics{BaseScore: 7.5}},\n\t\t\t\t{Metrics: CvssMetrics{BaseScore: 8.5}},\n\t\t\t\t{Metrics: CvssMetrics{BaseScore: 9.0}},\n\t\t\t},\n\t\t\texpected: 8.33333,\n\t\t},\n\t\t{\n\t\t\tname: \"with invalid scores\",\n\t\t\tcvss: []Cvss{\n\t\t\t\t{Metrics: CvssMetrics{BaseScore: 0}}, // invalid\n\t\t\t\t{Metrics: CvssMetrics{BaseScore: 7.5}},\n\t\t\t\t{Metrics: CvssMetrics{BaseScore: 0}}, // invalid\n\t\t\t\t{Metrics: CvssMetrics{BaseScore: 8.5}},\n\t\t\t},\n\t\t\texpected: 8.0,\n\t\t},\n\t\t{\n\t\t\tname: \"all invalid scores\",\n\t\t\tcvss: []Cvss{\n\t\t\t\t{Metrics: CvssMetrics{BaseScore: 0}},\n\t\t\t\t{Metrics: CvssMetrics{BaseScore: 0}},\n\t\t\t},\n\t\t\texpected: 0,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := average(validBaseScores(tt.cvss...)...)\n\t\t\tassert.InDelta(t, tt.expected, result, 0.00001)\n\t\t})\n\t}\n}\n\nfunc TestThreat(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tmetadata Metadata\n\t\texpected float64\n\t}{\n\t\t{\n\t\t\tname:     \"no EPSS, no KEV\",\n\t\t\tmetadata: Metadata{},\n\t\t\texpected: 0,\n\t\t},\n\t\t{\n\t\t\tname: \"with EPSS, no KEV\",\n\t\t\tmetadata: Metadata{\n\t\t\t\tEPSS: []EPSS{\n\t\t\t\t\t{EPSS: 0.75},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: 0.75,\n\t\t},\n\t\t{\n\t\t\tname: \"with KEV, no EPSS\",\n\t\t\tmetadata: Metadata{\n\t\t\t\tKnownExploited: []KnownExploited{\n\t\t\t\t\t{CVE: \"CVE-2023-1234\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: 1.0,\n\t\t},\n\t\t{\n\t\t\tname: \"with KEV and EPSS\",\n\t\t\tmetadata: Metadata{\n\t\t\t\tEPSS: []EPSS{\n\t\t\t\t\t{EPSS: 0.5},\n\t\t\t\t},\n\t\t\t\tKnownExploited: []KnownExploited{\n\t\t\t\t\t{CVE: \"CVE-2023-1234\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: 1.0, // KEV takes precedence\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := threat(tt.metadata)\n\t\t\tassert.Equal(t, tt.expected, result)\n\t\t})\n\t}\n}\n\nfunc TestImpact(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tmetadata Metadata\n\t\texpected float64\n\t}{\n\t\t{\n\t\t\tname:     \"no KEV\",\n\t\t\tmetadata: Metadata{},\n\t\t\texpected: 1.0,\n\t\t},\n\t\t{\n\t\t\tname: \"KEV without ransomware\",\n\t\t\tmetadata: Metadata{\n\t\t\t\tKnownExploited: []KnownExploited{\n\t\t\t\t\t{KnownRansomwareCampaignUse: \"No\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: 1.05,\n\t\t},\n\t\t{\n\t\t\tname: \"KEV with ransomware\",\n\t\t\tmetadata: Metadata{\n\t\t\t\tKnownExploited: []KnownExploited{\n\t\t\t\t\t{KnownRansomwareCampaignUse: \"Known\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: 1.1,\n\t\t},\n\t\t{\n\t\t\tname: \"KEV with case insensitive ransomware\",\n\t\t\tmetadata: Metadata{\n\t\t\t\tKnownExploited: []KnownExploited{\n\t\t\t\t\t{KnownRansomwareCampaignUse: \"KNOWN\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: 1.1,\n\t\t},\n\t\t{\n\t\t\tname: \"multiple KEV entries, one with ransomware\",\n\t\t\tmetadata: Metadata{\n\t\t\t\tKnownExploited: []KnownExploited{\n\t\t\t\t\t{KnownRansomwareCampaignUse: \"No\"},\n\t\t\t\t\t{KnownRansomwareCampaignUse: \"Known\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: 1.1, // highest wins\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := kevModifier(tt.metadata)\n\t\t\tassert.Equal(t, tt.expected, result)\n\t\t})\n\t}\n}\n\nfunc TestSeverity(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tmetadata Metadata\n\t\texpected float64\n\t}{\n\t\t{\n\t\t\tname: \"no CVSS, medium severity\",\n\t\t\tmetadata: Metadata{\n\t\t\t\tSeverity: \"medium\",\n\t\t\t},\n\t\t\texpected: 0.5,\n\t\t},\n\t\t{\n\t\t\tname: \"with CVSS + severity string\",\n\t\t\tmetadata: Metadata{\n\t\t\t\tSeverity: \"medium\",\n\t\t\t\tCvss: []Cvss{\n\t\t\t\t\t{Metrics: CvssMetrics{BaseScore: 8.0}},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: 0.65,\n\t\t},\n\t\t{\n\t\t\tname: \"multiple CVSS scores + severity string\",\n\t\t\tmetadata: Metadata{\n\t\t\t\tSeverity: \"medium\",\n\t\t\t\tCvss: []Cvss{\n\t\t\t\t\t{Metrics: CvssMetrics{BaseScore: 6.0}},\n\t\t\t\t\t{Metrics: CvssMetrics{BaseScore: 8.0}},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: 0.6,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := severity(tt.metadata)\n\t\t\tassert.InDelta(t, tt.expected, result, 0.00001)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "grype/vulnerability/mock/vulnerability_provider.go",
    "content": "package mock\n\nimport (\n\t\"github.com/anchore/grype/grype/db/v6/name\"\n\tgrypePkg \"github.com/anchore/grype/grype/pkg\"\n\t\"github.com/anchore/grype/grype/search\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n)\n\n// VulnerabilityProvider returns a new mock implementation of a vulnerability Provider, with the provided set of vulnerabilities\nfunc VulnerabilityProvider(vulnerabilities ...vulnerability.Vulnerability) vulnerability.Provider {\n\treturn &mockProvider{\n\t\tVulnerabilities: vulnerabilities,\n\t}\n}\n\ntype mockProvider struct {\n\tVulnerabilities []vulnerability.Vulnerability\n\tUnaffected      bool\n}\n\nfunc (s *mockProvider) Close() error {\n\treturn nil\n}\n\nfunc (s *mockProvider) PackageSearchNames(p grypePkg.Package) []string {\n\treturn name.PackageNames(p)\n}\n\n// VulnerabilityMetadata returns the metadata associated with a vulnerability\nfunc (s *mockProvider) VulnerabilityMetadata(ref vulnerability.Reference) (*vulnerability.Metadata, error) {\n\tfor _, vuln := range s.Vulnerabilities {\n\t\tif vuln.ID == ref.ID && vuln.Namespace == ref.Namespace {\n\t\t\tvar meta *vulnerability.Metadata\n\t\t\tif m, ok := vuln.Internal.(vulnerability.Metadata); ok {\n\t\t\t\tmeta = &m\n\t\t\t}\n\t\t\tif m, ok := vuln.Internal.(*vulnerability.Metadata); ok {\n\t\t\t\tmeta = m\n\t\t\t}\n\t\t\tif meta != nil {\n\t\t\t\tif meta.ID != vuln.ID {\n\t\t\t\t\tmeta.ID = vuln.ID\n\t\t\t\t}\n\t\t\t\tif meta.Namespace != vuln.Namespace {\n\t\t\t\t\tmeta.Namespace = vuln.Namespace\n\t\t\t\t}\n\t\t\t\treturn meta, nil\n\t\t\t}\n\t\t}\n\t}\n\treturn nil, nil\n}\n\nfunc (s *mockProvider) FindVulnerabilities(criteria ...vulnerability.Criteria) ([]vulnerability.Vulnerability, error) {\n\tif err := search.ValidateCriteria(criteria); err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar out []vulnerability.Vulnerability\n\tout = append(out, s.Vulnerabilities...)\n\treturn filterE(out, func(v vulnerability.Vulnerability) (bool, error) {\n\t\tfor _, row := range search.CriteriaIterator(criteria) {\n\t\t\tfor _, c := range row {\n\t\t\t\tmatches, _, err := c.MatchesVulnerability(v)\n\t\t\t\tif !matches || err != nil {\n\t\t\t\t\treturn false, err\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn true, nil\n\t})\n}\n\nfunc filterE[T any](out []T, keep func(v T) (bool, error)) ([]T, error) {\n\tfor i := 0; i < len(out); i++ {\n\t\tok, err := keep(out[i])\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif !ok {\n\t\t\tout = append(out[:i], out[i+1:]...)\n\t\t\ti--\n\t\t}\n\t}\n\treturn out, nil\n}\n"
  },
  {
    "path": "grype/vulnerability/provider.go",
    "content": "package vulnerability\n\nimport (\n\t\"encoding/json\"\n\t\"io\"\n\t\"time\"\n\n\t\"github.com/anchore/grype/grype/distro\"\n\tgrypePkg \"github.com/anchore/grype/grype/pkg\"\n)\n\n// Criteria interfaces are used for FindVulnerabilities calls\ntype Criteria interface {\n\t// MatchesVulnerability returns true if the provided value meets the criteria\n\tMatchesVulnerability(value Vulnerability) (bool, string, error)\n}\n\n// MetadataProvider implementations provide ways to look up vulnerability metadata\n//\n// Deprecated: vulnerability.Vulnerability objects now have metadata included\ntype MetadataProvider interface {\n\t// VulnerabilityMetadata returns the metadata associated with a vulnerability\n\t//\n\t// Deprecated: vulnerability.Vulnerability objects now have metadata included\n\tVulnerabilityMetadata(ref Reference) (*Metadata, error)\n}\n\n// Provider is the common interface for vulnerability sources to provide searching and metadata, such as a database\ntype Provider interface {\n\tPackageSearchNames(grypePkg.Package) []string\n\t// FindVulnerabilities returns vulnerabilities matching all the provided criteria\n\tFindVulnerabilities(criteria ...Criteria) ([]Vulnerability, error)\n\n\tMetadataProvider\n\n\tio.Closer\n}\n\ntype StoreMetadataProvider interface {\n\tDataProvenance() (map[string]DataProvenance, error)\n}\n\n// EOLChecker is an optional interface that vulnerability providers can implement\n// to check the end-of-life status for an operating system. EOL lookups use exact\n// matching (no aliasing) since each distro has its own EOL dates.\ntype EOLChecker interface {\n\t// GetOperatingSystemEOL returns the EOL date for the given distro.\n\t// Returns nil dates if no EOL data is available for this distro.\n\t// Uses exact matching (no aliasing) since distros like CentOS have\n\t// different EOL dates than their alias targets like RHEL.\n\tGetOperatingSystemEOL(d *distro.Distro) (eolDate, eoasDate *time.Time, err error)\n}\n\ntype DataProvenance struct {\n\tDateCaptured time.Time `json:\"captured,omitempty\"`\n\tInputDigest  string    `json:\"input,omitempty\"`\n}\n\ntype ProviderStatus struct {\n\tSchemaVersion string    `json:\"schemaVersion\"`\n\tFrom          string    `json:\"from,omitempty\"`\n\tBuilt         time.Time `json:\"built,omitempty\"`\n\tPath          string    `json:\"path,omitempty\"`\n\tError         error     `json:\"error,omitempty\"`\n}\n\nfunc (s ProviderStatus) MarshalJSON() ([]byte, error) {\n\terrStr := \"\"\n\tif s.Error != nil {\n\t\terrStr = s.Error.Error()\n\t}\n\n\tvar t string\n\tif !s.Built.IsZero() {\n\t\tt = s.Built.Format(time.RFC3339)\n\t}\n\n\treturn json.Marshal(&struct {\n\t\tSchemaVersion string `json:\"schemaVersion\"`\n\t\tFrom          string `json:\"from,omitempty\"`\n\t\tBuilt         string `json:\"built,omitempty\"`\n\t\tPath          string `json:\"path,omitempty\"`\n\t\tValid         bool   `json:\"valid\"`\n\t\tError         string `json:\"error,omitempty\"`\n\t}{\n\t\tSchemaVersion: s.SchemaVersion,\n\t\tFrom:          s.From,\n\t\tBuilt:         t,\n\t\tPath:          s.Path,\n\t\tValid:         s.Error == nil,\n\t\tError:         errStr,\n\t})\n}\n\nfunc (s DataProvenance) MarshalJSON() ([]byte, error) {\n\tvar t string\n\tif !s.DateCaptured.IsZero() {\n\t\tt = s.DateCaptured.Format(time.RFC3339)\n\t}\n\n\treturn json.Marshal(&struct {\n\t\tDateCaptured string `json:\"captured,omitempty\"`\n\t\tInputDigest  string `json:\"input,omitempty\"`\n\t}{\n\t\tDateCaptured: t,\n\t\tInputDigest:  s.InputDigest,\n\t})\n}\n"
  },
  {
    "path": "grype/vulnerability/severity.go",
    "content": "package vulnerability\n\nimport \"strings\"\n\nconst (\n\tUnknownSeverity Severity = iota\n\tNegligibleSeverity\n\tLowSeverity\n\tMediumSeverity\n\tHighSeverity\n\tCriticalSeverity\n)\n\nvar matcherTypeStr = []string{\n\t\"unknown\", // \"unknown severity\",\n\t\"negligible\",\n\t\"low\",\n\t\"medium\",\n\t\"high\",\n\t\"critical\",\n}\n\nfunc AllSeverities() []Severity {\n\treturn []Severity{\n\t\tNegligibleSeverity,\n\t\tLowSeverity,\n\t\tMediumSeverity,\n\t\tHighSeverity,\n\t\tCriticalSeverity,\n\t}\n}\n\ntype Severity int\n\ntype Severities []Severity\n\nfunc (f Severity) String() string {\n\tif int(f) >= len(matcherTypeStr) || f < 0 {\n\t\treturn matcherTypeStr[0]\n\t}\n\n\treturn matcherTypeStr[f]\n}\n\nfunc ParseSeverity(severity string) Severity {\n\tswitch strings.ToLower(severity) {\n\tcase NegligibleSeverity.String():\n\t\treturn NegligibleSeverity\n\tcase LowSeverity.String():\n\t\treturn LowSeverity\n\tcase MediumSeverity.String():\n\t\treturn MediumSeverity\n\tcase HighSeverity.String():\n\t\treturn HighSeverity\n\tcase CriticalSeverity.String():\n\t\treturn CriticalSeverity\n\tdefault:\n\t\treturn UnknownSeverity\n\t}\n}\n\nfunc (s Severities) Len() int {\n\treturn len(s)\n}\n\nfunc (s Severities) Swap(i, j int) {\n\ts[i], s[j] = s[j], s[i]\n}\n\nfunc (s Severities) Less(i, j int) bool {\n\treturn s[i] < s[j]\n}\n"
  },
  {
    "path": "grype/vulnerability/vulnerability.go",
    "content": "package vulnerability\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/anchore/grype/grype/pkg/qualifier\"\n\t\"github.com/anchore/grype/grype/version\"\n\t\"github.com/anchore/grype/internal/log\"\n\t\"github.com/anchore/syft/syft/cpe\"\n)\n\ntype Reference struct {\n\tID        string\n\tNamespace string\n\tInternal  any\n}\n\ntype Vulnerability struct {\n\tReference\n\tStatus                 string\n\tPackageName            string\n\tConstraint             version.Constraint\n\tPackageQualifiers      []qualifier.Qualifier\n\tCPEs                   []cpe.CPE\n\tFix                    Fix\n\tAdvisories             []Advisory\n\tRelatedVulnerabilities []Reference\n\tMetadata               *Metadata\n\tUnaffected             bool\n}\n\nfunc (v Vulnerability) String() string {\n\tconstraint := \"(none)\"\n\tif v.Constraint != nil {\n\t\tconstraint = v.Constraint.String()\n\t}\n\treturn fmt.Sprintf(\"Vuln(id=%s constraint=%q qualifiers=%+v)\", v.ID, constraint, v.PackageQualifiers)\n}\n\n// LogDropped should be called with a properly resolved vulnerability ID in every location in-memory vulnerabilities'\n// are filtered out by any process. this can be the first stop to diagnosing why certain vulnerabilities do not show up\n//\n//go:noinline\nfunc LogDropped(id, op, dropReason string, context any) {\n\tlog.WithFields(\"op\", op, \"reason\", dropReason, \"vulnerability\", id, \"context\", context).Trace(\"dropped vuln\")\n}\n"
  },
  {
    "path": "grype/vulnerability_matcher.go",
    "content": "package grype\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"runtime/debug\"\n\t\"slices\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/wagoodman/go-partybus\"\n\t\"github.com/wagoodman/go-progress\"\n\n\t\"github.com/anchore/grype/grype/event\"\n\t\"github.com/anchore/grype/grype/event/monitor\"\n\t\"github.com/anchore/grype/grype/grypeerr\"\n\t\"github.com/anchore/grype/grype/match\"\n\t\"github.com/anchore/grype/grype/matcher/stock\"\n\t\"github.com/anchore/grype/grype/pkg\"\n\t\"github.com/anchore/grype/grype/vex\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n\t\"github.com/anchore/grype/internal/bus\"\n\t\"github.com/anchore/grype/internal/log\"\n\tsyftPkg \"github.com/anchore/syft/syft/pkg\"\n)\n\nconst (\n\tbranch = \"├──\"\n\tleaf   = \"└──\"\n)\n\n// AlertsConfig controls which alerts are tracked and reported during vulnerability matching.\ntype AlertsConfig struct {\n\t// EnableEOLDistroWarnings enables tracking packages from end-of-life distros\n\tEnableEOLDistroWarnings bool\n}\n\ntype VulnerabilityMatcher struct {\n\tVulnerabilityProvider vulnerability.Provider\n\tExclusionProvider     match.ExclusionProvider\n\tMatchers              []match.Matcher\n\tIgnoreRules           []match.IgnoreRule\n\tFailSeverity          *vulnerability.Severity\n\tNormalizeByCVE        bool\n\tVexProcessor          *vex.Processor\n\tAlerts                AlertsConfig\n\n\t// tracked packages with distro issues (populated during FindMatches)\n\teolDistroPackages     []pkg.Package\n\tdistroDetectionFailed bool\n}\n\nfunc (m *VulnerabilityMatcher) FailAtOrAboveSeverity(severity *vulnerability.Severity) *VulnerabilityMatcher {\n\tm.FailSeverity = severity\n\treturn m\n}\n\nfunc (m *VulnerabilityMatcher) WithMatchers(matchers []match.Matcher) *VulnerabilityMatcher {\n\tm.Matchers = matchers\n\treturn m\n}\n\nfunc (m *VulnerabilityMatcher) WithIgnoreRules(ignoreRules []match.IgnoreRule) *VulnerabilityMatcher {\n\tm.IgnoreRules = ignoreRules\n\treturn m\n}\n\n// DistroDetectionFailed returns true if distro detection failed during scanning\n// (linux release info was present but distro type could not be determined).\nfunc (m *VulnerabilityMatcher) DistroDetectionFailed() bool {\n\treturn m.distroDetectionFailed\n}\n\n// EOLDistroPackages returns packages from distros that have reached end-of-life.\nfunc (m *VulnerabilityMatcher) EOLDistroPackages() []pkg.Package {\n\treturn m.eolDistroPackages\n}\n\n// FindMatches finds vulnerabilities for the given packages and package context.\n// FindMatches does not support context cancellation; for that, use\n// FindMatchesContext.\nfunc (m *VulnerabilityMatcher) FindMatches(\n\tpkgs []pkg.Package,\n\tpkgContext pkg.Context,\n) (remainingMatches *match.Matches, ignoredMatches []match.IgnoredMatch, err error) {\n\treturn m.FindMatchesContext(context.Background(), pkgs, pkgContext)\n}\n\n// FindMatchesContext finds vulnerabilities for the given packages and package\n// context, and supports context cancellation.\nfunc (m *VulnerabilityMatcher) FindMatchesContext(\n\tctx context.Context,\n\tpkgs []pkg.Package,\n\tpkgContext pkg.Context,\n) (remainingMatches *match.Matches, ignoredMatches []match.IgnoredMatch, err error) {\n\tprogressMonitor := trackMatcher(len(pkgs))\n\n\t// capture distro detection failure from pkgContext for alerting\n\tm.distroDetectionFailed = pkgContext.DistroDetectionFailed\n\tif m.distroDetectionFailed {\n\t\tlog.Warn(\"distro detection failed: linux release info was present but distro type could not be determined\")\n\t}\n\n\tdefer func() {\n\t\tprogressMonitor.Ignored.Set(int64(len(ignoredMatches)))\n\t\tprogressMonitor.SetCompleted()\n\t\tif err != nil {\n\t\t\tprogressMonitor.MatchesDiscovered.SetError(err)\n\t\t}\n\t}()\n\n\tremainingMatches, ignoredMatches, err = m.findDBMatches(ctx, pkgs, progressMonitor)\n\tif err != nil {\n\t\terr = fmt.Errorf(\"unable to find matches against vulnerability database: %w\", err)\n\t\treturn remainingMatches, ignoredMatches, err\n\t}\n\n\tremainingMatches, ignoredMatches, err = m.findVEXMatches(pkgContext, remainingMatches, ignoredMatches, progressMonitor)\n\tif err != nil {\n\t\terr = fmt.Errorf(\"unable to find matches against VEX sources: %w\", err)\n\t\treturn remainingMatches, ignoredMatches, err\n\t}\n\n\tif m.FailSeverity != nil && hasSeverityAtOrAbove(m.VulnerabilityProvider, *m.FailSeverity, *remainingMatches) {\n\t\terr = grypeerr.ErrAboveSeverityThreshold\n\t\treturn remainingMatches, ignoredMatches, err\n\t}\n\n\tlogListSummary(progressMonitor)\n\n\tlogIgnoredMatches(ignoredMatches)\n\n\treturn remainingMatches, ignoredMatches, nil\n}\n\nfunc (m *VulnerabilityMatcher) findDBMatches(ctx context.Context, pkgs []pkg.Package, progressMonitor *monitorWriter) (*match.Matches, []match.IgnoredMatch, error) {\n\tvar ignoredMatches []match.IgnoredMatch\n\n\tlog.Trace(\"finding matches against DB\")\n\tmatches, err := m.searchDBForMatches(ctx, pkgs, progressMonitor)\n\tif err != nil {\n\t\tif match.IsFatalError(err) {\n\t\t\treturn nil, nil, err\n\t\t}\n\n\t\t// other errors returned from matchers during searchDBForMatches were being\n\t\t// logged and not returned, so just log them here\n\t\tlog.WithFields(\"error\", err).Debug(\"error(s) returned from searchDBForMatches\")\n\t}\n\n\tmatches, ignoredMatches = m.applyIgnoreRules(matches)\n\n\tif m.NormalizeByCVE {\n\t\tnormalizedMatches := match.NewMatches()\n\t\tfor originalMatch := range matches.Enumerate() {\n\t\t\tnormalizedMatches.Add(m.normalizeByCVE(originalMatch))\n\t\t}\n\n\t\t// we apply the ignore rules again in case any of the transformations done during normalization\n\t\t// regresses the results (relative to the already applied ignore rules). Why do we additionally apply\n\t\t// the ignore rules before normalizing? In case the user has a rule that ignores a non-normalized\n\t\t// vulnerability ID, we wantMatches to ensure that the rule is honored.\n\t\toriginalIgnoredMatches := ignoredMatches\n\t\tmatches, ignoredMatches = m.applyIgnoreRules(normalizedMatches)\n\t\tignoredMatches = m.mergeIgnoredMatches(originalIgnoredMatches, ignoredMatches)\n\t}\n\n\treturn &matches, ignoredMatches, nil\n}\n\nfunc (m *VulnerabilityMatcher) mergeIgnoredMatches(allIgnoredMatches ...[]match.IgnoredMatch) []match.IgnoredMatch {\n\tvar out []match.IgnoredMatch\n\tfor _, ignoredMatches := range allIgnoredMatches {\n\t\tfor _, ignored := range ignoredMatches {\n\t\t\tif m.NormalizeByCVE {\n\t\t\t\tignored.Match = m.normalizeByCVE(ignored.Match)\n\t\t\t}\n\t\t\tout = append(out, ignored)\n\t\t}\n\t}\n\treturn out\n}\n\n//nolint:funlen\nfunc (m *VulnerabilityMatcher) searchDBForMatches(\n\tctx context.Context,\n\tpackages []pkg.Package,\n\tprogressMonitor *monitorWriter,\n) (match.Matches, error) {\n\tvar allMatches []match.Match\n\tvar allIgnorers []match.IgnoreFilter\n\tmatcherIndex, defaultMatcher := newMatcherIndex(m.Matchers)\n\n\tif defaultMatcher == nil {\n\t\tdefaultMatcher = stock.NewStockMatcher(stock.MatcherConfig{UseCPEs: true})\n\t}\n\n\t// reset tracked distro packages\n\tm.eolDistroPackages = nil\n\n\t// setup EOL tracking if enabled\n\teolTracker := newEOLTracker(m.Alerts.EnableEOLDistroWarnings, m.VulnerabilityProvider)\n\n\tvar matcherErrs []error\n\tfor _, p := range packages {\n\t\tprogressMonitor.PackagesProcessed.Increment()\n\t\tlog.WithFields(\"package\", displayPackage(p)).Trace(\"searching for vulnerability matches\")\n\n\t\t// track EOL distro packages\n\t\tif eolTracker.checkAndTrack(p) {\n\t\t\tm.eolDistroPackages = append(m.eolDistroPackages, p)\n\t\t}\n\n\t\tmatchAgainst, ok := matcherIndex[p.Type]\n\t\tif !ok {\n\t\t\tmatchAgainst = []match.Matcher{defaultMatcher}\n\t\t}\n\t\tfor _, theMatcher := range matchAgainst {\n\t\t\tif err := ctx.Err(); err != nil {\n\t\t\t\treturn match.Matches{}, err\n\t\t\t}\n\n\t\t\tmatches, ignorers, err := callMatcherSafely(theMatcher, m.VulnerabilityProvider, p)\n\t\t\tif err != nil {\n\t\t\t\tif match.IsFatalError(err) {\n\t\t\t\t\treturn match.Matches{}, err\n\t\t\t\t}\n\n\t\t\t\tlog.WithFields(\"error\", err, \"package\", displayPackage(p)).Warn(\"matcher returned error\")\n\t\t\t\tmatcherErrs = append(matcherErrs, err)\n\t\t\t}\n\n\t\t\tallIgnorers = append(allIgnorers, ignorers...)\n\n\t\t\t// Filter out matches based on records in the database exclusion table and hard-coded rules\n\t\t\tfiltered, dropped := match.ApplyExplicitIgnoreRules(m.ExclusionProvider, match.NewMatches(matches...))\n\n\t\t\tadditionalMatches := filtered.Sorted()\n\t\t\tlogPackageMatches(p, additionalMatches)\n\t\t\tlogExplicitDroppedPackageMatches(p, dropped)\n\t\t\tallMatches = append(allMatches, additionalMatches...)\n\n\t\t\tprogressMonitor.MatchesDiscovered.Add(int64(len(additionalMatches)))\n\n\t\t\t// note: there is a difference between \"ignore\" and \"dropped\" matches.\n\t\t\t// ignored: matches that are filtered out due to user-provided ignore rules\n\t\t\t// dropped: matches that are filtered out due to hard-coded rules\n\t\t\tupdateVulnerabilityList(progressMonitor, additionalMatches, nil, dropped, m.VulnerabilityProvider)\n\t\t}\n\t}\n\n\t// apply ignores based on matchers returning ignore rules\n\tfiltered, dropped := match.ApplyIgnoreFilters(allMatches, ignoredMatchFilter(allIgnorers))\n\tlogIgnoredMatches(dropped)\n\n\t// get deduplicated set of matches\n\tres := match.NewMatches(filtered...)\n\n\t// update the total discovered matches after removing all duplicates and ignores\n\tprogressMonitor.MatchesDiscovered.Set(int64(res.Count()))\n\n\treturn res, errors.Join(matcherErrs...)\n}\n\nfunc callMatcherSafely(m match.Matcher, vp vulnerability.Provider, p pkg.Package) (matches []match.Match, ignoredMatches []match.IgnoreFilter, err error) {\n\t// handle individual matcher panics\n\tdefer func() {\n\t\tif e := recover(); e != nil {\n\t\t\terr = match.NewFatalError(m.Type(), fmt.Errorf(\"%v at:\\n%s\", e, string(debug.Stack())))\n\t\t}\n\t}()\n\treturn m.Match(vp, p)\n}\n\nfunc (m *VulnerabilityMatcher) findVEXMatches(pkgContext pkg.Context, remainingMatches *match.Matches, ignoredMatches []match.IgnoredMatch, progressMonitor *monitorWriter) (*match.Matches, []match.IgnoredMatch, error) {\n\tif m.VexProcessor == nil {\n\t\tlog.Trace(\"no VEX documents provided, skipping VEX matching\")\n\t\treturn remainingMatches, ignoredMatches, nil\n\t}\n\n\tlog.Trace(\"finding matches against available VEX documents\")\n\tmatchesAfterVex, ignoredMatchesAfterVex, err := m.VexProcessor.ApplyVEX(&pkgContext, remainingMatches, ignoredMatches)\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"unable to find matches against VEX documents: %w\", err)\n\t}\n\n\tdiffMatches := matchesAfterVex.Diff(*remainingMatches)\n\t// note: this assumes that the diff can only be additive\n\tdiffIgnoredMatches := ignoredMatchesDiff(ignoredMatchesAfterVex, ignoredMatches)\n\n\tupdateVulnerabilityList(progressMonitor, diffMatches.Sorted(), diffIgnoredMatches, nil, m.VulnerabilityProvider)\n\n\treturn matchesAfterVex, ignoredMatchesAfterVex, nil\n}\n\n// applyIgnoreRules applies the user-provided ignore rules, splitting ignored matches into a separate set\nfunc (m *VulnerabilityMatcher) applyIgnoreRules(matches match.Matches) (match.Matches, []match.IgnoredMatch) {\n\tvar ignoredMatches []match.IgnoredMatch\n\tif len(m.IgnoreRules) == 0 {\n\t\treturn matches, ignoredMatches\n\t}\n\n\tmatches, ignoredMatches = match.ApplyIgnoreRules(matches, m.IgnoreRules)\n\n\tif count := len(ignoredMatches); count > 0 {\n\t\tlog.Infof(\"ignoring %d matches due to user-provided ignore rules\", count)\n\t}\n\treturn matches, ignoredMatches\n}\n\nfunc (m *VulnerabilityMatcher) normalizeByCVE(match match.Match) match.Match {\n\tif isCVE(match.Vulnerability.ID) {\n\t\treturn match\n\t}\n\n\tvar effectiveCVERecordRefs []vulnerability.Reference\n\tfor _, ref := range match.Vulnerability.RelatedVulnerabilities {\n\t\tif isCVE(ref.ID) {\n\t\t\teffectiveCVERecordRefs = append(effectiveCVERecordRefs, ref)\n\t\t\tbreak\n\t\t}\n\t}\n\n\tswitch len(effectiveCVERecordRefs) {\n\tcase 0:\n\t\tlog.WithFields(\n\t\t\t\"vuln\", match.Vulnerability.ID,\n\t\t\t\"package\", displayPackage(match.Package),\n\t\t).Trace(\"unable to find CVE record for vulnerability, skipping normalization\")\n\t\treturn match\n\tcase 1:\n\t\tbreak\n\tdefault:\n\t\tlog.WithFields(\n\t\t\t\"refs\", fmt.Sprintf(\"%+v\", effectiveCVERecordRefs),\n\t\t\t\"vuln\", match.Vulnerability.ID,\n\t\t\t\"package\", displayPackage(match.Package),\n\t\t).Trace(\"found multiple CVE records for vulnerability, skipping normalization\")\n\t\treturn match\n\t}\n\n\tref := effectiveCVERecordRefs[0]\n\n\tupstreamMetadata, err := m.VulnerabilityProvider.VulnerabilityMetadata(ref) //nolint:staticcheck // deprecated API still used internally\n\tif err != nil {\n\t\tlog.WithFields(\"id\", ref.ID, \"namespace\", ref.Namespace, \"error\", err).Warn(\"unable to fetch effective CVE metadata\")\n\t\treturn match\n\t}\n\n\tif upstreamMetadata == nil {\n\t\treturn match\n\t}\n\n\toriginalRef := vulnerability.Reference{\n\t\tID:        match.Vulnerability.ID,\n\t\tNamespace: match.Vulnerability.Namespace,\n\t}\n\n\tmatch.Vulnerability.ID = upstreamMetadata.ID\n\tmatch.Vulnerability.Namespace = upstreamMetadata.Namespace\n\tmatch.Vulnerability.RelatedVulnerabilities = []vulnerability.Reference{originalRef}\n\n\treturn match\n}\n\n// ignoreRulesByIndex implements match.IgnoreFilter to filter each matching\n// package that overlaps by location and have the same vulnerability ID (CVE)\ntype ignoreRulesByIndex struct {\n\tremainingFilters       []match.IgnoreFilter\n\tlocationIgnoreRules    map[string][]match.IgnoreRule\n\tpackageNameIgnoreRules map[string][]match.IgnoreRule\n}\n\nfunc (i ignoreRulesByIndex) IgnoreMatch(m match.Match) []match.IgnoreRule {\n\tif nameRules := i.packageNameIgnoreRules[m.Package.Name]; nameRules != nil {\n\t\tfor _, rule := range nameRules {\n\t\t\tif matched := rule.IgnoreMatch(m); matched != nil {\n\t\t\t\treturn matched\n\t\t\t}\n\t\t}\n\t}\n\tfor _, l := range m.Package.Locations.ToSlice() {\n\t\tfor _, rule := range i.locationIgnoreRules[l.RealPath] {\n\t\t\tif matched := rule.IgnoreMatch(m); matched != nil {\n\t\t\t\treturn matched\n\t\t\t}\n\t\t}\n\t}\n\tfor _, f := range i.remainingFilters {\n\t\tif matched := f.IgnoreMatch(m); matched != nil {\n\t\t\treturn matched\n\t\t}\n\t}\n\treturn nil\n}\n\n// ignoredMatchFilter creates an ignore filter based on location-based IgnoredMatches to filter out \"the same\"\n// vulnerabilities reported by other matchers based on overlapping file locations\nfunc ignoredMatchFilter(ignores []match.IgnoreFilter) match.IgnoreFilter {\n\tout := ignoreRulesByIndex{\n\t\tlocationIgnoreRules:    map[string][]match.IgnoreRule{},\n\t\tpackageNameIgnoreRules: map[string][]match.IgnoreRule{},\n\t}\n\t// the returned slice of remaining rules are not location-based rules\n\tout.remainingFilters = slices.DeleteFunc(ignores, func(ignore match.IgnoreFilter) bool {\n\t\tif rule, ok := ignore.(match.IgnoreRule); ok {\n\t\t\t// return true to remove rules handled with index lookups from the remaining filter list\n\t\t\tif rule.Package.Location != \"\" && !strings.ContainsRune(rule.Package.Location, '*') {\n\t\t\t\tout.locationIgnoreRules[rule.Package.Location] = append(out.locationIgnoreRules[rule.Package.Location], rule)\n\t\t\t\treturn true\n\t\t\t}\n\t\t\tif rule.Package.Name != \"\" {\n\t\t\t\t// this rule is handled with location lookups, remove it from the remaining filter list\n\t\t\t\tout.packageNameIgnoreRules[rule.Package.Name] = append(out.packageNameIgnoreRules[rule.Package.Name], rule)\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t\treturn false\n\t})\n\treturn out\n}\n\nfunc displayPackage(p pkg.Package) string {\n\tif p.PURL != \"\" {\n\t\treturn p.PURL\n\t}\n\tty := p.Type\n\tif p.Type == \"\" {\n\t\tty = \"unknown\"\n\t}\n\n\treturn fmt.Sprintf(\"%s@%s (type=%s)\", p.Name, p.Version, ty)\n}\n\nfunc ignoredMatchesDiff(subject []match.IgnoredMatch, other []match.IgnoredMatch) []match.IgnoredMatch {\n\t// TODO(alex): the downside with this implementation is that it does not account for the same ignored match being\n\t// ignored for different reasons (the appliedIgnoreRules field).\n\n\totherMap := make(map[match.Fingerprint]struct{})\n\tfor _, a := range other {\n\t\totherMap[a.Fingerprint()] = struct{}{}\n\t}\n\n\tvar diff []match.IgnoredMatch\n\tfor _, b := range subject {\n\t\tif _, ok := otherMap[b.Fingerprint()]; !ok {\n\t\t\tdiff = append(diff, b)\n\t\t}\n\t}\n\n\treturn diff\n}\n\nfunc newMatcherIndex(matchers []match.Matcher) (map[syftPkg.Type][]match.Matcher, match.Matcher) {\n\tmatcherIndex := make(map[syftPkg.Type][]match.Matcher)\n\tvar defaultMatcher match.Matcher\n\tfor _, m := range matchers {\n\t\tif m.Type() == match.StockMatcher {\n\t\t\tdefaultMatcher = m\n\t\t\tcontinue\n\t\t}\n\t\tfor _, t := range m.PackageTypes() {\n\t\t\tif _, ok := matcherIndex[t]; !ok {\n\t\t\t\tmatcherIndex[t] = make([]match.Matcher, 0)\n\t\t\t}\n\n\t\t\tmatcherIndex[t] = append(matcherIndex[t], m)\n\t\t\tlog.Tracef(\"adding matcher: %+v\", t)\n\t\t}\n\t}\n\n\treturn matcherIndex, defaultMatcher\n}\n\nfunc isCVE(id string) bool {\n\treturn strings.HasPrefix(strings.ToLower(id), \"cve-\")\n}\n\n//nolint:staticcheck // MetadataProvider is deprecated but still used internally\nfunc hasSeverityAtOrAbove(store vulnerability.MetadataProvider, severity vulnerability.Severity, matches match.Matches) bool {\n\tif severity == vulnerability.UnknownSeverity {\n\t\treturn false\n\t}\n\tfor m := range matches.Enumerate() {\n\t\tmetadata, err := store.VulnerabilityMetadata(m.Vulnerability.Reference) //nolint:staticcheck // deprecated API still used internally\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tif vulnerability.ParseSeverity(metadata.Severity) >= severity {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc logListSummary(vl *monitorWriter) {\n\tlog.Infof(\"found %d vulnerability matches across %d packages\", vl.MatchesDiscovered.Current(), vl.PackagesProcessed.Current())\n\tlog.Debugf(\"  ├── fixed: %d\", vl.Fixed.Current())\n\tlog.Debugf(\"  ├── ignored: %d (due to user-provided rule)\", vl.Ignored.Current())\n\tlog.Debugf(\"  ├── dropped: %d (due to hard-coded correction)\", vl.Dropped.Current())\n\tlog.Debugf(\"  └── matched: %d\", vl.MatchesDiscovered.Current())\n\n\tvar unknownCount int64\n\tif count, ok := vl.BySeverity[vulnerability.UnknownSeverity]; ok {\n\t\tunknownCount = count.Current()\n\t}\n\tlog.Debugf(\"      ├── %s: %d\", vulnerability.UnknownSeverity.String(), unknownCount)\n\n\tallSeverities := vulnerability.AllSeverities()\n\tfor idx, sev := range allSeverities {\n\t\tarm := selectArm(idx, len(allSeverities))\n\t\tlog.Debugf(\"      %s %s: %d\", arm, sev.String(), vl.BySeverity[sev].Current())\n\t}\n}\n\n//nolint:staticcheck // MetadataProvider is deprecated but still used internally\nfunc updateVulnerabilityList(mon *monitorWriter, matches []match.Match, ignores []match.IgnoredMatch, dropped []match.IgnoredMatch, metadataProvider vulnerability.MetadataProvider) {\n\tfor _, m := range matches {\n\t\tmetadata, err := metadataProvider.VulnerabilityMetadata(m.Vulnerability.Reference) //nolint:staticcheck // deprecated API still used internally\n\t\tif err != nil || metadata == nil {\n\t\t\tmon.BySeverity[vulnerability.UnknownSeverity].Increment()\n\t\t\tcontinue\n\t\t}\n\n\t\tsevManualProgress, ok := mon.BySeverity[vulnerability.ParseSeverity(metadata.Severity)]\n\t\tif !ok {\n\t\t\tmon.BySeverity[vulnerability.UnknownSeverity].Increment()\n\t\t\tcontinue\n\t\t}\n\t\tsevManualProgress.Increment()\n\n\t\tif m.Vulnerability.Fix.State == vulnerability.FixStateFixed {\n\t\t\tmon.Fixed.Increment()\n\t\t}\n\t}\n\n\tmon.Ignored.Add(int64(len(ignores)))\n\tmon.Dropped.Add(int64(len(dropped)))\n}\n\nfunc logPackageMatches(p pkg.Package, matches []match.Match) {\n\tif len(matches) == 0 {\n\t\treturn\n\t}\n\n\tlog.WithFields(\"package\", displayPackage(p)).Debugf(\"found %d vulnerabilities\", len(matches))\n\tfor idx, m := range matches {\n\t\tarm := selectArm(idx, len(matches))\n\t\tlog.WithFields(\"vuln\", m.Vulnerability.ID, \"namespace\", m.Vulnerability.Namespace).Tracef(\"  %s\", arm)\n\t}\n}\n\nfunc selectArm(idx, total int) string {\n\tif idx == total-1 {\n\t\treturn leaf\n\t}\n\treturn branch\n}\n\nfunc logExplicitDroppedPackageMatches(p pkg.Package, ignored []match.IgnoredMatch) {\n\tif len(ignored) == 0 {\n\t\treturn\n\t}\n\n\tlog.WithFields(\"package\", displayPackage(p)).Debugf(\"dropped %d vulnerability matches due to hard-coded correction\", len(ignored))\n\tfor idx, i := range ignored {\n\t\tarm := selectArm(idx, len(ignored))\n\n\t\tlog.WithFields(\"vuln\", i.Match.Vulnerability.ID, \"rules\", len(i.AppliedIgnoreRules)).Tracef(\"  %s\", arm)\n\t}\n}\n\nfunc logIgnoredMatches(ignored []match.IgnoredMatch) {\n\tif len(ignored) == 0 {\n\t\treturn\n\t}\n\n\tlog.Infof(\"ignored %d vulnerability matches\", len(ignored))\n\tfor idx, i := range ignored {\n\t\tarm := selectArm(idx, len(ignored))\n\t\trule := \"\"\n\t\tif len(i.AppliedIgnoreRules) > 0 {\n\t\t\trule = i.AppliedIgnoreRules[0].Reason\n\t\t\tif rule == \"\" {\n\t\t\t\trule = i.AppliedIgnoreRules[0].Vulnerability\n\t\t\t}\n\t\t}\n\t\tvulnerability.LogDropped(i.Vulnerability.ID, \"ignoreRules\", rule, i)\n\t\tlog.WithFields(\"vuln\", i.Match.Vulnerability.ID, \"rules\", len(i.AppliedIgnoreRules), \"package\", displayPackage(i.Package)).Debugf(\"  %s\", arm)\n\t}\n}\n\ntype monitorWriter struct {\n\tPackagesProcessed *progress.Manual\n\tMatchesDiscovered *progress.Manual\n\tFixed             *progress.Manual\n\tIgnored           *progress.Manual\n\tDropped           *progress.Manual\n\tBySeverity        map[vulnerability.Severity]*progress.Manual\n}\n\nfunc newMonitor(pkgCount int) (monitorWriter, monitor.Matching) {\n\tmanualBySev := make(map[vulnerability.Severity]*progress.Manual)\n\tfor _, severity := range vulnerability.AllSeverities() {\n\t\tmanualBySev[severity] = progress.NewManual(-1)\n\t}\n\tmanualBySev[vulnerability.UnknownSeverity] = progress.NewManual(-1)\n\n\tm := monitorWriter{\n\t\tPackagesProcessed: progress.NewManual(int64(pkgCount)),\n\t\tMatchesDiscovered: progress.NewManual(-1),\n\t\tFixed:             progress.NewManual(-1),\n\t\tIgnored:           progress.NewManual(-1),\n\t\tDropped:           progress.NewManual(-1),\n\t\tBySeverity:        manualBySev,\n\t}\n\n\tmonitorableBySev := make(map[vulnerability.Severity]progress.Monitorable)\n\tfor sev, manual := range manualBySev {\n\t\tmonitorableBySev[sev] = manual\n\t}\n\n\treturn m, monitor.Matching{\n\t\tPackagesProcessed: m.PackagesProcessed,\n\t\tMatchesDiscovered: m.MatchesDiscovered,\n\t\tFixed:             m.Fixed,\n\t\tIgnored:           m.Ignored,\n\t\tDropped:           m.Dropped,\n\t\tBySeverity:        monitorableBySev,\n\t}\n}\n\nfunc (m *monitorWriter) SetCompleted() {\n\tm.PackagesProcessed.SetCompleted()\n\tm.MatchesDiscovered.SetCompleted()\n\tm.Fixed.SetCompleted()\n\tm.Ignored.SetCompleted()\n\tm.Dropped.SetCompleted()\n\tfor _, v := range m.BySeverity {\n\t\tv.SetCompleted()\n\t}\n}\n\nfunc trackMatcher(pkgCount int) *monitorWriter {\n\twriter, reader := newMonitor(pkgCount)\n\n\tbus.Publish(partybus.Event{\n\t\tType:  event.VulnerabilityScanningStarted,\n\t\tValue: reader,\n\t})\n\n\treturn &writer\n}\n\n// eolTracker handles checking and caching EOL status for distros\ntype eolTracker struct {\n\tchecker vulnerability.EOLChecker\n\tcache   map[string]eolCacheEntry\n}\n\ntype eolCacheEntry struct {\n\tisEOL   bool\n\teolDate *time.Time\n}\n\nfunc newEOLTracker(enabled bool, provider vulnerability.Provider) *eolTracker {\n\tif !enabled {\n\t\treturn &eolTracker{}\n\t}\n\tchecker, ok := provider.(vulnerability.EOLChecker)\n\tif !ok {\n\t\treturn &eolTracker{}\n\t}\n\treturn &eolTracker{\n\t\tchecker: checker,\n\t\tcache:   make(map[string]eolCacheEntry),\n\t}\n}\n\n// checkAndTrack checks if the package is from an EOL distro and returns true if so.\n// Results are cached per distro.\nfunc (t *eolTracker) checkAndTrack(p pkg.Package) bool {\n\tif t.checker == nil || p.Distro == nil {\n\t\treturn false\n\t}\n\n\tdistroKey := p.Distro.String()\n\tentry, checked := t.cache[distroKey]\n\tif !checked {\n\t\teolDate, _, err := t.checker.GetOperatingSystemEOL(p.Distro)\n\t\tif err != nil {\n\t\t\tlog.WithFields(\"distro\", distroKey, \"error\", err).Debug(\"error checking EOL status\")\n\t\t}\n\t\tentry = eolCacheEntry{\n\t\t\tisEOL:   eolDate != nil && eolDate.Before(time.Now()),\n\t\t\teolDate: eolDate,\n\t\t}\n\t\tt.cache[distroKey] = entry\n\t}\n\n\tif entry.isEOL {\n\t\tlog.WithFields(\"package\", displayPackage(p), \"distro\", distroKey, \"eol_date\", entry.eolDate).Debug(\"package from EOL distro\")\n\t\treturn true\n\t}\n\treturn false\n}\n"
  },
  {
    "path": "grype/vulnerability_matcher_test.go",
    "content": "package grype\n\nimport (\n\t\"errors\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/google/go-cmp/cmp/cmpopts\"\n\t\"github.com/google/uuid\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/wagoodman/go-partybus\"\n\n\t\"github.com/anchore/grype/grype/distro\"\n\t\"github.com/anchore/grype/grype/event\"\n\t\"github.com/anchore/grype/grype/event/monitor\"\n\t\"github.com/anchore/grype/grype/grypeerr\"\n\t\"github.com/anchore/grype/grype/match\"\n\t\"github.com/anchore/grype/grype/matcher\"\n\tmatcherMock \"github.com/anchore/grype/grype/matcher/mock\"\n\t\"github.com/anchore/grype/grype/matcher/ruby\"\n\t\"github.com/anchore/grype/grype/pkg\"\n\t\"github.com/anchore/grype/grype/pkg/qualifier\"\n\t\"github.com/anchore/grype/grype/version\"\n\t\"github.com/anchore/grype/grype/vex\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n\t\"github.com/anchore/grype/grype/vulnerability/mock\"\n\t\"github.com/anchore/grype/internal/bus\"\n\t\"github.com/anchore/syft/syft/cpe\"\n\t\"github.com/anchore/syft/syft/file\"\n\tsyftPkg \"github.com/anchore/syft/syft/pkg\"\n\t\"github.com/anchore/syft/syft/source\"\n)\n\nfunc testVulnerabilities() []vulnerability.Vulnerability {\n\treturn []vulnerability.Vulnerability{\n\t\t{\n\t\t\tReference: vulnerability.Reference{\n\t\t\t\tID:        \"CVE-2014-fake-1\",\n\t\t\t\tNamespace: \"debian:distro:debian:8\",\n\t\t\t\tInternal: vulnerability.Metadata{\n\t\t\t\t\tSeverity: \"medium\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tPackageName: \"neutron\",\n\t\t\tConstraint:  version.MustGetConstraint(\"< 2014.1.3-6\", version.DebFormat),\n\t\t},\n\t\t{\n\t\t\tReference: vulnerability.Reference{\n\t\t\t\tID:        \"CVE-2013-fake-2\",\n\t\t\t\tNamespace: \"debian:distro:debian:8\",\n\t\t\t},\n\t\t\tPackageName: \"neutron\",\n\t\t\tConstraint:  version.MustGetConstraint(\"< 2013.0.2-1\", version.DebFormat),\n\t\t},\n\t\t{\n\t\t\tReference: vulnerability.Reference{\n\t\t\t\tID:        \"GHSA-2014-fake-3\",\n\t\t\t\tNamespace: \"github:language:ruby\",\n\t\t\t\tInternal: vulnerability.Metadata{\n\t\t\t\t\tSeverity: \"medium\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tPackageName: \"activerecord\",\n\t\t\tConstraint:  version.MustGetConstraint(\"< 3.7.6\", version.UnknownFormat),\n\t\t\tRelatedVulnerabilities: []vulnerability.Reference{\n\t\t\t\t{\n\t\t\t\t\tID:        \"CVE-2014-fake-3\",\n\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tReference: vulnerability.Reference{\n\t\t\t\tID:        \"CVE-2014-fake-3\",\n\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\tInternal: vulnerability.Metadata{\n\t\t\t\t\tSeverity: \"critical\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tPackageName: \"activerecord\",\n\t\t\tConstraint:  version.MustGetConstraint(\"< 3.7.6\", version.UnknownFormat),\n\t\t\tCPEs: []cpe.CPE{\n\t\t\t\tcpe.Must(\"cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*\", \"\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tReference: vulnerability.Reference{\n\t\t\t\tID:        \"CVE-2014-fake-4\",\n\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t},\n\t\t\tPackageName: \"activerecord\",\n\t\t\tConstraint:  version.MustGetConstraint(\"< 3.7.4\", version.UnknownFormat),\n\t\t\tCPEs: []cpe.CPE{\n\t\t\t\tcpe.Must(\"cpe:2.3:*:activerecord:activerecord:*:*:something:*:*:ruby:*:*\", \"\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tReference: vulnerability.Reference{\n\t\t\t\tID:        \"CVE-2014-fake-5\",\n\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t},\n\t\t\tPackageName: \"activerecord\",\n\t\t\tConstraint:  version.MustGetConstraint(\"= 4.0.1\", version.UnknownFormat),\n\t\t\tCPEs: []cpe.CPE{\n\t\t\t\tcpe.Must(\"cpe:2.3:*:couldntgetthisrightcouldyou:activerecord:4.0.1:*:*:*:*:*:*:*\", \"\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tReference: vulnerability.Reference{\n\t\t\t\tID:        \"CVE-2014-fake-6\",\n\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t},\n\t\t\tPackageName: \"activerecord\",\n\t\t\tConstraint:  version.MustGetConstraint(\"< 98SP3\", version.UnknownFormat),\n\t\t\tCPEs: []cpe.CPE{\n\t\t\t\tcpe.Must(\"cpe:2.3:*:awesome:awesome:*:*:*:*:*:*:*:*\", \"\"),\n\t\t\t},\n\t\t},\n\t}\n}\n\nfunc Test_HasSeverityAtOrAbove(t *testing.T) {\n\tthePkg := pkg.Package{\n\t\tID:      pkg.ID(uuid.NewString()),\n\t\tName:    \"the-package\",\n\t\tVersion: \"v0.1\",\n\t\tType:    syftPkg.RpmPkg,\n\t}\n\n\tmatches := match.NewMatches()\n\tmatches.Add(match.Match{\n\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\tReference: vulnerability.Reference{\n\t\t\t\tID:        \"CVE-2014-fake-1\",\n\t\t\t\tNamespace: \"debian:distro:debian:8\",\n\t\t\t},\n\t\t},\n\t\tPackage: thePkg,\n\t\tDetails: match.Details{\n\t\t\t{\n\t\t\t\tType: match.ExactDirectMatch,\n\t\t\t},\n\t\t},\n\t})\n\n\ttests := []struct {\n\t\tname           string\n\t\tfailOnSeverity string\n\t\tmatches        match.Matches\n\t\texpectedResult bool\n\t}{\n\t\t{\n\t\t\tname:           \"no-severity-set\",\n\t\t\tfailOnSeverity: \"\",\n\t\t\tmatches:        matches,\n\t\t\texpectedResult: false,\n\t\t},\n\t\t{\n\t\t\tname:           \"below-threshold\",\n\t\t\tfailOnSeverity: \"high\",\n\t\t\tmatches:        matches,\n\t\t\texpectedResult: false,\n\t\t},\n\t\t{\n\t\t\tname:           \"at-threshold\",\n\t\t\tfailOnSeverity: \"medium\",\n\t\t\tmatches:        matches,\n\t\t\texpectedResult: true,\n\t\t},\n\t\t{\n\t\t\tname:           \"above-threshold\",\n\t\t\tfailOnSeverity: \"low\",\n\t\t\tmatches:        matches,\n\t\t\texpectedResult: true,\n\t\t},\n\t}\n\n\tmetadataProvider := mock.VulnerabilityProvider(testVulnerabilities()...)\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tvar failOnSeverity vulnerability.Severity\n\t\t\tif test.failOnSeverity != \"\" {\n\t\t\t\tsev := vulnerability.ParseSeverity(test.failOnSeverity)\n\t\t\t\tif sev == vulnerability.UnknownSeverity {\n\t\t\t\t\tt.Fatalf(\"could not parse severity\")\n\t\t\t\t}\n\t\t\t\tfailOnSeverity = sev\n\t\t\t}\n\n\t\t\tactual := hasSeverityAtOrAbove(metadataProvider, failOnSeverity, test.matches)\n\n\t\t\tif test.expectedResult != actual {\n\t\t\t\tt.Errorf(\"expected: %v got : %v\", test.expectedResult, actual)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestVulnerabilityMatcher_FindMatches(t *testing.T) {\n\tvp := mock.VulnerabilityProvider(testVulnerabilities()...)\n\n\tneutron2013Pkg := pkg.Package{\n\t\tID:      pkg.ID(uuid.NewString()),\n\t\tName:    \"neutron\",\n\t\tVersion: \"2013.1.1-1\",\n\t\tType:    syftPkg.DebPkg,\n\t\tDistro: &distro.Distro{\n\t\t\tType:    \"debian\",\n\t\t\tVersion: \"8\",\n\t\t},\n\t}\n\n\tmustCPE := func(c string) cpe.CPE {\n\t\tcp, err := cpe.New(c, \"\")\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\treturn cp\n\t}\n\n\tactiverecordPkg := pkg.Package{\n\t\tID:      pkg.ID(uuid.NewString()),\n\t\tName:    \"activerecord\",\n\t\tVersion: \"3.7.5\",\n\t\tCPEs: []cpe.CPE{\n\t\t\tmustCPE(\"cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*\"),\n\t\t},\n\t\tType:     syftPkg.GemPkg,\n\t\tLanguage: syftPkg.Ruby,\n\t}\n\n\topenvexProcessor, _ := vex.NewProcessor(vex.ProcessorOptions{\n\t\tDocuments: []string{\n\t\t\t\"vex/testdata/vex-docs/openvex-debian.json\",\n\t\t},\n\t\tIgnoreRules: []match.IgnoreRule{\n\t\t\t{\n\t\t\t\tVexStatus: \"fixed\",\n\t\t\t},\n\t\t},\n\t})\n\n\ttype fields struct {\n\t\tMatchers       []match.Matcher\n\t\tIgnoreRules    []match.IgnoreRule\n\t\tFailSeverity   *vulnerability.Severity\n\t\tNormalizeByCVE bool\n\t\tVexProcessor   *vex.Processor\n\t}\n\ttype args struct {\n\t\tpkgs    []pkg.Package\n\t\tcontext pkg.Context\n\t}\n\n\ttests := []struct {\n\t\tname               string\n\t\tfields             fields\n\t\targs               args\n\t\twantMatches        match.Matches\n\t\twantIgnoredMatches []match.IgnoredMatch\n\t\twantErr            error\n\t}{\n\t\t{\n\t\t\tname: \"no matches\",\n\t\t\tfields: fields{\n\t\t\t\tMatchers: matcher.NewDefaultMatchers(matcher.Config{}),\n\t\t\t},\n\t\t\targs: args{\n\t\t\t\tpkgs: []pkg.Package{\n\t\t\t\t\t{\n\t\t\t\t\t\tID:      pkg.ID(uuid.NewString()),\n\t\t\t\t\t\tName:    \"neutrino\",\n\t\t\t\t\t\tVersion: \"2099.1.1-1\",\n\t\t\t\t\t\tType:    syftPkg.DebPkg,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tcontext: pkg.Context{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"matches by exact-direct match (OS)\",\n\t\t\tfields: fields{\n\t\t\t\tMatchers: matcher.NewDefaultMatchers(matcher.Config{}),\n\t\t\t},\n\t\t\targs: args{\n\t\t\t\tpkgs: []pkg.Package{\n\t\t\t\t\tneutron2013Pkg,\n\t\t\t\t},\n\t\t\t\tcontext: pkg.Context{},\n\t\t\t},\n\t\t\twantMatches: match.NewMatches(\n\t\t\t\tmatch.Match{\n\t\t\t\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\t\t\t\tPackageName: \"neutron\",\n\t\t\t\t\t\tConstraint:  version.MustGetConstraint(\"< 2014.1.3-6\", version.DebFormat),\n\t\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\t\tID:        \"CVE-2014-fake-1\",\n\t\t\t\t\t\t\tNamespace: \"debian:distro:debian:8\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tPackageQualifiers: []qualifier.Qualifier{},\n\t\t\t\t\t\tCPEs:              []cpe.CPE{},\n\t\t\t\t\t\tAdvisories:        []vulnerability.Advisory{},\n\t\t\t\t\t},\n\t\t\t\t\tPackage: neutron2013Pkg,\n\t\t\t\t\tDetails: match.Details{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tType: match.ExactDirectMatch,\n\t\t\t\t\t\t\tSearchedBy: match.DistroParameters{\n\t\t\t\t\t\t\t\tDistro:    match.DistroIdentification{Type: \"debian\", Version: \"8\"},\n\t\t\t\t\t\t\t\tNamespace: \"debian:distro:debian:8\",\n\t\t\t\t\t\t\t\tPackage:   match.PackageParameter{Name: \"neutron\", Version: \"2013.1.1-1\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tFound: match.DistroResult{\n\t\t\t\t\t\t\t\tVulnerabilityID:   \"CVE-2014-fake-1\",\n\t\t\t\t\t\t\t\tVersionConstraint: \"< 2014.1.3-6 (deb)\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tMatcher:    \"dpkg-matcher\",\n\t\t\t\t\t\t\tConfidence: 1,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t),\n\t\t\twantIgnoredMatches: nil,\n\t\t\twantErr:            nil,\n\t\t},\n\t\t{\n\t\t\tname: \"fail on severity threshold\",\n\t\t\tfields: fields{\n\t\t\t\tMatchers: matcher.NewDefaultMatchers(matcher.Config{}),\n\t\t\t\tFailSeverity: func() *vulnerability.Severity {\n\t\t\t\t\tx := vulnerability.LowSeverity\n\t\t\t\t\treturn &x\n\t\t\t\t}(),\n\t\t\t},\n\t\t\targs: args{\n\t\t\t\tpkgs: []pkg.Package{\n\t\t\t\t\tneutron2013Pkg,\n\t\t\t\t},\n\t\t\t\tcontext: pkg.Context{},\n\t\t\t},\n\t\t\twantMatches: match.NewMatches(\n\t\t\t\tmatch.Match{\n\t\t\t\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\t\t\t\tPackageName: \"neutron\",\n\t\t\t\t\t\tConstraint:  version.MustGetConstraint(\"< 2014.1.3-6\", version.DebFormat),\n\t\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\t\tID:        \"CVE-2014-fake-1\",\n\t\t\t\t\t\t\tNamespace: \"debian:distro:debian:8\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tPackageQualifiers: []qualifier.Qualifier{},\n\t\t\t\t\t\tCPEs:              []cpe.CPE{},\n\t\t\t\t\t\tAdvisories:        []vulnerability.Advisory{},\n\t\t\t\t\t},\n\t\t\t\t\tPackage: neutron2013Pkg,\n\t\t\t\t\tDetails: match.Details{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tType: match.ExactDirectMatch,\n\t\t\t\t\t\t\tSearchedBy: match.DistroParameters{\n\t\t\t\t\t\t\t\tDistro:    match.DistroIdentification{Type: \"debian\", Version: \"8\"},\n\t\t\t\t\t\t\t\tNamespace: \"debian:distro:debian:8\",\n\t\t\t\t\t\t\t\tPackage:   match.PackageParameter{Name: \"neutron\", Version: \"2013.1.1-1\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tFound: match.DistroResult{\n\t\t\t\t\t\t\t\tVulnerabilityID:   \"CVE-2014-fake-1\",\n\t\t\t\t\t\t\t\tVersionConstraint: \"< 2014.1.3-6 (deb)\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tMatcher:    \"dpkg-matcher\",\n\t\t\t\t\t\t\tConfidence: 1,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t),\n\t\t\twantIgnoredMatches: nil,\n\t\t\twantErr:            grypeerr.ErrAboveSeverityThreshold,\n\t\t},\n\t\t{\n\t\t\tname: \"pass on severity threshold with VEX\",\n\t\t\tfields: fields{\n\t\t\t\tMatchers: matcher.NewDefaultMatchers(matcher.Config{}),\n\t\t\t\tFailSeverity: func() *vulnerability.Severity {\n\t\t\t\t\tx := vulnerability.LowSeverity\n\t\t\t\t\treturn &x\n\t\t\t\t}(),\n\t\t\t\tVexProcessor: openvexProcessor,\n\t\t\t},\n\t\t\targs: args{\n\t\t\t\tpkgs: []pkg.Package{\n\t\t\t\t\tneutron2013Pkg,\n\t\t\t\t},\n\t\t\t\tcontext: pkg.Context{\n\t\t\t\t\tSource: &source.Description{\n\t\t\t\t\t\tName:    \"debian\",\n\t\t\t\t\t\tVersion: \"2013.1.1-1\",\n\t\t\t\t\t\tMetadata: source.ImageMetadata{\n\t\t\t\t\t\t\tRepoDigests: []string{\n\t\t\t\t\t\t\t\t\"debian@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantMatches: match.NewMatches(),\n\t\t\twantIgnoredMatches: []match.IgnoredMatch{\n\t\t\t\t{\n\t\t\t\t\tAppliedIgnoreRules: []match.IgnoreRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tNamespace: \"vex\",\n\t\t\t\t\t\t\tVexStatus: \"fixed\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tMatch: match.Match{\n\t\t\t\t\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\t\t\t\t\tPackageName: \"neutron\",\n\t\t\t\t\t\t\tConstraint:  version.MustGetConstraint(\"< 2014.1.3-6\", version.DebFormat),\n\t\t\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\t\t\tID:        \"CVE-2014-fake-1\",\n\t\t\t\t\t\t\t\tNamespace: \"debian:distro:debian:8\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tPackageQualifiers: []qualifier.Qualifier{},\n\t\t\t\t\t\t\tCPEs:              []cpe.CPE{},\n\t\t\t\t\t\t\tAdvisories:        []vulnerability.Advisory{},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tPackage: neutron2013Pkg,\n\t\t\t\t\t\tDetails: match.Details{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tType: match.ExactDirectMatch,\n\t\t\t\t\t\t\t\tSearchedBy: match.DistroParameters{\n\t\t\t\t\t\t\t\t\tDistro:    match.DistroIdentification{Type: \"debian\", Version: \"8\"},\n\t\t\t\t\t\t\t\t\tNamespace: \"debian:distro:debian:8\",\n\t\t\t\t\t\t\t\t\tPackage:   match.PackageParameter{Name: \"neutron\", Version: \"2013.1.1-1\"},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tFound: match.DistroResult{\n\t\t\t\t\t\t\t\t\tVulnerabilityID:   \"CVE-2014-fake-1\",\n\t\t\t\t\t\t\t\t\tVersionConstraint: \"< 2014.1.3-6 (deb)\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tMatcher:    \"dpkg-matcher\",\n\t\t\t\t\t\t\t\tConfidence: 1,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantErr: nil,\n\t\t},\n\t\t{\n\t\t\tname: \"matches by exact-direct match (language)\",\n\t\t\tfields: fields{\n\t\t\t\tMatchers: matcher.NewDefaultMatchers(matcher.Config{\n\t\t\t\t\tRuby: ruby.MatcherConfig{\n\t\t\t\t\t\tUseCPEs: true,\n\t\t\t\t\t},\n\t\t\t\t}),\n\t\t\t},\n\t\t\targs: args{\n\t\t\t\tpkgs: []pkg.Package{\n\t\t\t\t\tactiverecordPkg,\n\t\t\t\t},\n\t\t\t\tcontext: pkg.Context{},\n\t\t\t},\n\t\t\twantMatches: match.NewMatches(\n\t\t\t\tmatch.Match{\n\t\t\t\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\t\t\t\tPackageName: \"activerecord\",\n\t\t\t\t\t\tConstraint:  version.MustGetConstraint(\"< 3.7.6\", version.UnknownFormat),\n\t\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\t\tID:        \"CVE-2014-fake-3\",\n\t\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tCPEs: []cpe.CPE{\n\t\t\t\t\t\t\tmustCPE(\"cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*\"),\n\t\t\t\t\t\t},\n\t\t\t\t\t\tPackageQualifiers: []qualifier.Qualifier{},\n\t\t\t\t\t\tAdvisories:        []vulnerability.Advisory{},\n\t\t\t\t\t},\n\t\t\t\t\tPackage: activerecordPkg,\n\t\t\t\t\tDetails: match.Details{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tType: match.CPEMatch,\n\t\t\t\t\t\t\tSearchedBy: match.CPEParameters{\n\t\t\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\t\t\tCPEs: []string{\n\t\t\t\t\t\t\t\t\t\"cpe:2.3:*:activerecord:activerecord:3.7.5:*:*:*:*:rails:*:*\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tPackage: match.PackageParameter{\n\t\t\t\t\t\t\t\t\tName:    \"activerecord\",\n\t\t\t\t\t\t\t\t\tVersion: \"3.7.5\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tFound: match.CPEResult{\n\t\t\t\t\t\t\t\tVulnerabilityID:   \"CVE-2014-fake-3\",\n\t\t\t\t\t\t\t\tVersionConstraint: \"< 3.7.6 (unknown)\",\n\t\t\t\t\t\t\t\tCPEs: []string{\n\t\t\t\t\t\t\t\t\t\"cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tMatcher:    \"ruby-gem-matcher\",\n\t\t\t\t\t\t\tConfidence: 0.9,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tmatch.Match{\n\t\t\t\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\t\t\t\tPackageName: \"activerecord\",\n\t\t\t\t\t\tConstraint:  version.MustGetConstraint(\"< 3.7.6\", version.UnknownFormat),\n\t\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\t\tID:        \"GHSA-2014-fake-3\",\n\t\t\t\t\t\t\tNamespace: \"github:language:ruby\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tRelatedVulnerabilities: []vulnerability.Reference{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tID:        \"CVE-2014-fake-3\",\n\t\t\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tPackageQualifiers: []qualifier.Qualifier{},\n\t\t\t\t\t\tAdvisories:        []vulnerability.Advisory{},\n\t\t\t\t\t\tCPEs:              []cpe.CPE{},\n\t\t\t\t\t},\n\t\t\t\t\tPackage: activerecordPkg,\n\t\t\t\t\tDetails: match.Details{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tType: match.ExactDirectMatch,\n\t\t\t\t\t\t\tSearchedBy: match.EcosystemParameters{\n\t\t\t\t\t\t\t\tLanguage:  \"ruby\",\n\t\t\t\t\t\t\t\tNamespace: \"github:language:ruby\",\n\t\t\t\t\t\t\t\tPackage:   match.PackageParameter{Name: \"activerecord\", Version: \"3.7.5\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tFound: match.EcosystemResult{\n\t\t\t\t\t\t\t\tVulnerabilityID:   \"GHSA-2014-fake-3\",\n\t\t\t\t\t\t\t\tVersionConstraint: \"< 3.7.6 (unknown)\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tMatcher:    \"ruby-gem-matcher\",\n\t\t\t\t\t\t\tConfidence: 1,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t),\n\t\t\twantIgnoredMatches: nil,\n\t\t\twantErr:            nil,\n\t\t},\n\t\t{\n\t\t\tname: \"normalize by cve\",\n\t\t\tfields: fields{\n\t\t\t\tMatchers: matcher.NewDefaultMatchers(\n\t\t\t\t\tmatcher.Config{\n\t\t\t\t\t\tRuby: ruby.MatcherConfig{\n\t\t\t\t\t\t\tUseCPEs: true,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t),\n\t\t\t\tNormalizeByCVE: true, // IMPORTANT!\n\t\t\t},\n\t\t\targs: args{\n\t\t\t\tpkgs: []pkg.Package{\n\t\t\t\t\tactiverecordPkg,\n\t\t\t\t},\n\t\t\t\tcontext: pkg.Context{},\n\t\t\t},\n\t\t\twantMatches: match.NewMatches(\n\t\t\t\tmatch.Match{\n\t\t\t\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\t\t\t\tPackageName: \"activerecord\",\n\t\t\t\t\t\tConstraint:  version.MustGetConstraint(\"< 3.7.6\", version.UnknownFormat),\n\t\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\t\tID:        \"CVE-2014-fake-3\",\n\t\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tCPEs: []cpe.CPE{\n\t\t\t\t\t\t\tmustCPE(\"cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*\"),\n\t\t\t\t\t\t},\n\t\t\t\t\t\tPackageQualifiers: []qualifier.Qualifier{},\n\t\t\t\t\t\tAdvisories:        []vulnerability.Advisory{},\n\t\t\t\t\t\tRelatedVulnerabilities: []vulnerability.Reference{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tID:        \"GHSA-2014-fake-3\",\n\t\t\t\t\t\t\t\tNamespace: \"github:language:ruby\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tPackage: activerecordPkg,\n\t\t\t\t\tDetails: match.Details{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tType: match.ExactDirectMatch,\n\t\t\t\t\t\t\tSearchedBy: match.EcosystemParameters{\n\t\t\t\t\t\t\t\tLanguage:  \"ruby\",\n\t\t\t\t\t\t\t\tNamespace: \"github:language:ruby\",\n\t\t\t\t\t\t\t\tPackage:   match.PackageParameter{Name: \"activerecord\", Version: \"3.7.5\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tFound: match.EcosystemResult{\n\t\t\t\t\t\t\t\tVulnerabilityID:   \"GHSA-2014-fake-3\",\n\t\t\t\t\t\t\t\tVersionConstraint: \"< 3.7.6 (unknown)\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tMatcher:    \"ruby-gem-matcher\",\n\t\t\t\t\t\t\tConfidence: 1,\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tType: match.CPEMatch,\n\t\t\t\t\t\t\tSearchedBy: match.CPEParameters{\n\t\t\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\t\t\tCPEs: []string{\n\t\t\t\t\t\t\t\t\t\"cpe:2.3:*:activerecord:activerecord:3.7.5:*:*:*:*:rails:*:*\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tPackage: match.PackageParameter{\n\t\t\t\t\t\t\t\t\tName:    \"activerecord\",\n\t\t\t\t\t\t\t\t\tVersion: \"3.7.5\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tFound: match.CPEResult{\n\t\t\t\t\t\t\t\tVulnerabilityID:   \"CVE-2014-fake-3\",\n\t\t\t\t\t\t\t\tVersionConstraint: \"< 3.7.6 (unknown)\",\n\t\t\t\t\t\t\t\tCPEs: []string{\n\t\t\t\t\t\t\t\t\t\"cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tMatcher:    \"ruby-gem-matcher\",\n\t\t\t\t\t\t\tConfidence: 0.9,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t),\n\t\t\twantIgnoredMatches: nil,\n\t\t\twantErr:            nil,\n\t\t},\n\t\t{\n\t\t\tname: \"normalize by cve -- ignore GHSA\",\n\t\t\tfields: fields{\n\t\t\t\tMatchers: matcher.NewDefaultMatchers(\n\t\t\t\t\tmatcher.Config{\n\t\t\t\t\t\tRuby: ruby.MatcherConfig{\n\t\t\t\t\t\t\tUseCPEs: true,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t),\n\t\t\t\tIgnoreRules: []match.IgnoreRule{\n\t\t\t\t\t{\n\t\t\t\t\t\tVulnerability: \"GHSA-2014-fake-3\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tNormalizeByCVE: true, // IMPORTANT!\n\t\t\t},\n\t\t\targs: args{\n\t\t\t\tpkgs: []pkg.Package{\n\t\t\t\t\tactiverecordPkg,\n\t\t\t\t},\n\t\t\t\tcontext: pkg.Context{},\n\t\t\t},\n\t\t\twantMatches: match.NewMatches(\n\t\t\t\tmatch.Match{\n\t\t\t\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\t\t\t\tPackageName: \"activerecord\",\n\t\t\t\t\t\tConstraint:  version.MustGetConstraint(\"< 3.7.6\", version.UnknownFormat),\n\t\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\t\tID:        \"CVE-2014-fake-3\",\n\t\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tCPEs: []cpe.CPE{\n\t\t\t\t\t\t\tmustCPE(\"cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*\"),\n\t\t\t\t\t\t},\n\t\t\t\t\t\tPackageQualifiers: []qualifier.Qualifier{},\n\t\t\t\t\t\tAdvisories:        []vulnerability.Advisory{},\n\t\t\t\t\t},\n\t\t\t\t\tPackage: activerecordPkg,\n\t\t\t\t\tDetails: match.Details{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tType: match.CPEMatch,\n\t\t\t\t\t\t\tSearchedBy: match.CPEParameters{\n\t\t\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\t\t\tCPEs: []string{\n\t\t\t\t\t\t\t\t\t\"cpe:2.3:*:activerecord:activerecord:3.7.5:*:*:*:*:rails:*:*\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tPackage: match.PackageParameter{\n\t\t\t\t\t\t\t\t\tName:    \"activerecord\",\n\t\t\t\t\t\t\t\t\tVersion: \"3.7.5\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tFound: match.CPEResult{\n\t\t\t\t\t\t\t\tVulnerabilityID:   \"CVE-2014-fake-3\",\n\t\t\t\t\t\t\t\tVersionConstraint: \"< 3.7.6 (unknown)\",\n\t\t\t\t\t\t\t\tCPEs: []string{\n\t\t\t\t\t\t\t\t\t\"cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tMatcher:    \"ruby-gem-matcher\",\n\t\t\t\t\t\t\tConfidence: 0.9,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t),\n\t\t\twantErr: nil,\n\t\t\twantIgnoredMatches: []match.IgnoredMatch{\n\t\t\t\t{\n\t\t\t\t\tMatch: match.Match{\n\t\t\t\t\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\t\t\t\t\tPackageName: \"activerecord\",\n\t\t\t\t\t\t\tConstraint:  version.MustGetConstraint(\"< 3.7.6\", version.UnknownFormat),\n\t\t\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\t\t\tID:        \"CVE-2014-fake-3\",\n\t\t\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tCPEs:              []cpe.CPE{},\n\t\t\t\t\t\t\tPackageQualifiers: []qualifier.Qualifier{},\n\t\t\t\t\t\t\tAdvisories:        []vulnerability.Advisory{},\n\t\t\t\t\t\t\tRelatedVulnerabilities: []vulnerability.Reference{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tID:        \"GHSA-2014-fake-3\",\n\t\t\t\t\t\t\t\t\tNamespace: \"github:language:ruby\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tPackage: activerecordPkg,\n\t\t\t\t\t\tDetails: match.Details{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tType: match.ExactDirectMatch,\n\t\t\t\t\t\t\t\tSearchedBy: match.EcosystemParameters{\n\t\t\t\t\t\t\t\t\tLanguage:  \"ruby\",\n\t\t\t\t\t\t\t\t\tNamespace: \"github:language:ruby\",\n\t\t\t\t\t\t\t\t\tPackage:   match.PackageParameter{Name: \"activerecord\", Version: \"3.7.5\"},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tFound: match.EcosystemResult{\n\t\t\t\t\t\t\t\t\tVulnerabilityID:   \"GHSA-2014-fake-3\",\n\t\t\t\t\t\t\t\t\tVersionConstraint: \"< 3.7.6 (unknown)\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tMatcher:    \"ruby-gem-matcher\",\n\t\t\t\t\t\t\t\tConfidence: 1,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tAppliedIgnoreRules: []match.IgnoreRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tVulnerability: \"GHSA-2014-fake-3\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"normalize by cve -- ignore CVE\",\n\t\t\tfields: fields{\n\t\t\t\tMatchers: matcher.NewDefaultMatchers(\n\t\t\t\t\tmatcher.Config{\n\t\t\t\t\t\tRuby: ruby.MatcherConfig{\n\t\t\t\t\t\t\tUseCPEs: true,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t),\n\t\t\t\tIgnoreRules: []match.IgnoreRule{\n\t\t\t\t\t{\n\t\t\t\t\t\tVulnerability: \"CVE-2014-fake-3\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tNormalizeByCVE: true, // IMPORTANT!\n\t\t\t},\n\t\t\targs: args{\n\t\t\t\tpkgs: []pkg.Package{\n\t\t\t\t\tactiverecordPkg,\n\t\t\t\t},\n\t\t\t\tcontext: pkg.Context{},\n\t\t\t},\n\t\t\twantMatches: match.NewMatches(),\n\t\t\twantIgnoredMatches: []match.IgnoredMatch{\n\t\t\t\t{\n\t\t\t\t\tMatch: match.Match{\n\t\t\t\t\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\t\t\t\t\tPackageName: \"activerecord\",\n\t\t\t\t\t\t\tConstraint:  version.MustGetConstraint(\"< 3.7.6\", version.UnknownFormat),\n\t\t\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\t\t\tID:        \"CVE-2014-fake-3\",\n\t\t\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tCPEs: []cpe.CPE{\n\t\t\t\t\t\t\t\tmustCPE(\"cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*\"),\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tPackageQualifiers:      []qualifier.Qualifier{},\n\t\t\t\t\t\t\tAdvisories:             []vulnerability.Advisory{},\n\t\t\t\t\t\t\tRelatedVulnerabilities: nil,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tPackage: activerecordPkg,\n\t\t\t\t\t\tDetails: match.Details{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tType: match.CPEMatch,\n\t\t\t\t\t\t\t\tSearchedBy: match.CPEParameters{\n\t\t\t\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\t\t\t\tCPEs: []string{\n\t\t\t\t\t\t\t\t\t\t\"cpe:2.3:*:activerecord:activerecord:3.7.5:*:*:*:*:rails:*:*\",\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\tPackage: match.PackageParameter{\n\t\t\t\t\t\t\t\t\t\tName:    \"activerecord\",\n\t\t\t\t\t\t\t\t\t\tVersion: \"3.7.5\",\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tFound: match.CPEResult{\n\t\t\t\t\t\t\t\t\tVulnerabilityID:   \"CVE-2014-fake-3\",\n\t\t\t\t\t\t\t\t\tVersionConstraint: \"< 3.7.6 (unknown)\",\n\t\t\t\t\t\t\t\t\tCPEs: []string{\n\t\t\t\t\t\t\t\t\t\t\"cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*\",\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tMatcher:    \"ruby-gem-matcher\",\n\t\t\t\t\t\t\t\tConfidence: 0.9,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tAppliedIgnoreRules: []match.IgnoreRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tVulnerability: \"CVE-2014-fake-3\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tAppliedIgnoreRules: []match.IgnoreRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tVulnerability: \"CVE-2014-fake-3\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tMatch: match.Match{\n\t\t\t\t\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\t\t\t\t\tPackageName: \"activerecord\",\n\t\t\t\t\t\t\tConstraint:  version.MustGetConstraint(\"< 3.7.6\", version.UnknownFormat),\n\t\t\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\t\t\tID:        \"CVE-2014-fake-3\",\n\t\t\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tCPEs:              []cpe.CPE{},\n\t\t\t\t\t\t\tPackageQualifiers: []qualifier.Qualifier{},\n\t\t\t\t\t\t\tAdvisories:        []vulnerability.Advisory{},\n\t\t\t\t\t\t\tRelatedVulnerabilities: []vulnerability.Reference{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tID:        \"GHSA-2014-fake-3\",\n\t\t\t\t\t\t\t\t\tNamespace: \"github:language:ruby\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tPackage: activerecordPkg,\n\t\t\t\t\t\tDetails: match.Details{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tType: match.ExactDirectMatch,\n\t\t\t\t\t\t\t\tSearchedBy: match.EcosystemParameters{\n\t\t\t\t\t\t\t\t\tLanguage:  \"ruby\",\n\t\t\t\t\t\t\t\t\tNamespace: \"github:language:ruby\",\n\t\t\t\t\t\t\t\t\tPackage:   match.PackageParameter{Name: \"activerecord\", Version: \"3.7.5\"},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tFound: match.EcosystemResult{\n\t\t\t\t\t\t\t\t\tVulnerabilityID:   \"GHSA-2014-fake-3\",\n\t\t\t\t\t\t\t\t\tVersionConstraint: \"< 3.7.6 (unknown)\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tMatcher:    \"ruby-gem-matcher\",\n\t\t\t\t\t\t\t\tConfidence: 1,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantErr: nil,\n\t\t},\n\t\t{\n\t\t\tname: \"ignore CVE (not normalized by CVE)\",\n\t\t\tfields: fields{\n\t\t\t\tMatchers: matcher.NewDefaultMatchers(matcher.Config{\n\t\t\t\t\tRuby: ruby.MatcherConfig{\n\t\t\t\t\t\tUseCPEs: true,\n\t\t\t\t\t},\n\t\t\t\t}),\n\t\t\t\tIgnoreRules: []match.IgnoreRule{\n\t\t\t\t\t{\n\t\t\t\t\t\tVulnerability: \"CVE-2014-fake-3\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\targs: args{\n\t\t\t\tpkgs: []pkg.Package{\n\t\t\t\t\tactiverecordPkg,\n\t\t\t\t},\n\t\t\t},\n\t\t\twantMatches: match.NewMatches(\n\t\t\t\tmatch.Match{\n\t\t\t\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\t\t\t\tPackageName: \"activerecord\",\n\t\t\t\t\t\tConstraint:  version.MustGetConstraint(\"< 3.7.6\", version.UnknownFormat),\n\t\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\t\tID:        \"GHSA-2014-fake-3\",\n\t\t\t\t\t\t\tNamespace: \"github:language:ruby\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tRelatedVulnerabilities: []vulnerability.Reference{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tID:        \"CVE-2014-fake-3\",\n\t\t\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tPackageQualifiers: []qualifier.Qualifier{},\n\t\t\t\t\t\tAdvisories:        []vulnerability.Advisory{},\n\t\t\t\t\t\tCPEs:              []cpe.CPE{},\n\t\t\t\t\t},\n\t\t\t\t\tPackage: activerecordPkg,\n\t\t\t\t\tDetails: match.Details{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tType: match.ExactDirectMatch,\n\t\t\t\t\t\t\tSearchedBy: match.EcosystemParameters{\n\t\t\t\t\t\t\t\tLanguage:  \"ruby\",\n\t\t\t\t\t\t\t\tNamespace: \"github:language:ruby\",\n\t\t\t\t\t\t\t\tPackage:   match.PackageParameter{Name: \"activerecord\", Version: \"3.7.5\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tFound: match.EcosystemResult{\n\t\t\t\t\t\t\t\tVulnerabilityID:   \"GHSA-2014-fake-3\",\n\t\t\t\t\t\t\t\tVersionConstraint: \"< 3.7.6 (unknown)\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tMatcher:    \"ruby-gem-matcher\",\n\t\t\t\t\t\t\tConfidence: 1,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t),\n\t\t\twantIgnoredMatches: []match.IgnoredMatch{\n\t\t\t\t{\n\t\t\t\t\tAppliedIgnoreRules: []match.IgnoreRule{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tVulnerability: \"CVE-2014-fake-3\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tMatch: match.Match{\n\t\t\t\t\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\t\t\t\t\tPackageName: \"activerecord\",\n\t\t\t\t\t\t\tConstraint:  version.MustGetConstraint(\"< 3.7.6\", version.UnknownFormat),\n\t\t\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\t\t\tID:        \"CVE-2014-fake-3\",\n\t\t\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tCPEs: []cpe.CPE{\n\t\t\t\t\t\t\t\tmustCPE(\"cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*\"),\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tPackageQualifiers: []qualifier.Qualifier{},\n\t\t\t\t\t\t\tAdvisories:        []vulnerability.Advisory{},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tPackage: activerecordPkg,\n\t\t\t\t\t\tDetails: match.Details{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tType: match.CPEMatch,\n\t\t\t\t\t\t\t\tSearchedBy: match.CPEParameters{\n\t\t\t\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\t\t\t\tCPEs: []string{\n\t\t\t\t\t\t\t\t\t\t\"cpe:2.3:*:activerecord:activerecord:3.7.5:*:*:*:*:rails:*:*\",\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\tPackage: match.PackageParameter{\n\t\t\t\t\t\t\t\t\t\tName:    \"activerecord\",\n\t\t\t\t\t\t\t\t\t\tVersion: \"3.7.5\",\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tFound: match.CPEResult{\n\t\t\t\t\t\t\t\t\tVulnerabilityID:   \"CVE-2014-fake-3\",\n\t\t\t\t\t\t\t\t\tVersionConstraint: \"< 3.7.6 (unknown)\",\n\t\t\t\t\t\t\t\t\tCPEs: []string{\n\t\t\t\t\t\t\t\t\t\t\"cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*\",\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tMatcher:    \"ruby-gem-matcher\",\n\t\t\t\t\t\t\t\tConfidence: 0.9,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantErr: nil,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tm := &VulnerabilityMatcher{\n\t\t\t\tVulnerabilityProvider: vp,\n\t\t\t\tMatchers:              tt.fields.Matchers,\n\t\t\t\tIgnoreRules:           tt.fields.IgnoreRules,\n\t\t\t\tFailSeverity:          tt.fields.FailSeverity,\n\t\t\t\tNormalizeByCVE:        tt.fields.NormalizeByCVE,\n\t\t\t\tVexProcessor:          tt.fields.VexProcessor,\n\t\t\t}\n\n\t\t\tlistener := &busListener{}\n\t\t\tbus.Set(listener)\n\t\t\tdefer bus.Set(nil)\n\n\t\t\tactualMatches, actualIgnoreMatches, err := m.FindMatches(tt.args.pkgs, tt.args.context)\n\t\t\tif tt.wantErr != nil {\n\t\t\t\trequire.ErrorIs(t, err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t} else if err != nil {\n\t\t\t\tt.Errorf(\"FindMatches() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tvar opts = []cmp.Option{\n\t\t\t\tcmpopts.EquateEmpty(),\n\t\t\t\tcmpopts.IgnoreUnexported(match.Match{}),\n\t\t\t\tcmpopts.IgnoreFields(vulnerability.Vulnerability{}, \"Constraint\"),\n\t\t\t\tcmpopts.IgnoreFields(vulnerability.Reference{}, \"Internal\"),\n\t\t\t\tcmpopts.IgnoreFields(pkg.Package{}, \"Locations\", \"Distro\"),\n\t\t\t\tcmpopts.IgnoreUnexported(match.IgnoredMatch{}),\n\t\t\t}\n\n\t\t\tif d := cmp.Diff(tt.wantMatches.Sorted(), actualMatches.Sorted(), opts...); d != \"\" {\n\t\t\t\tt.Errorf(\"FindMatches() matches mismatch [ha!] (-want +got):\\n%s\", d)\n\t\t\t}\n\n\t\t\tif d := cmp.Diff(tt.wantIgnoredMatches, actualIgnoreMatches, opts...); d != \"\" {\n\t\t\t\tt.Errorf(\"FindMatches() ignored matches mismatch [ha!] (-want +got):\\n%s\", d)\n\t\t\t}\n\n\t\t\t// validate the bus-reported ignored counts are accurate\n\t\t\trequire.Equal(t, int64(len(tt.wantIgnoredMatches)), listener.matching.Ignored.Current())\n\t\t})\n\t}\n}\n\nfunc Test_fatalErrors(t *testing.T) {\n\ttests := []struct {\n\t\tname        string\n\t\tmatcherFunc matcherMock.MatchFunc\n\t\tassertErr   assert.ErrorAssertionFunc\n\t}{\n\t\t{\n\t\t\tname: \"no error\",\n\t\t\tmatcherFunc: func(_ vulnerability.Provider, _ pkg.Package) ([]match.Match, []match.IgnoreFilter, error) {\n\t\t\t\treturn nil, nil, nil\n\t\t\t},\n\t\t\tassertErr: assert.NoError,\n\t\t},\n\t\t{\n\t\t\tname: \"non-fatal error\",\n\t\t\tmatcherFunc: func(_ vulnerability.Provider, _ pkg.Package) ([]match.Match, []match.IgnoreFilter, error) {\n\t\t\t\treturn nil, nil, errors.New(\"some error\")\n\t\t\t},\n\t\t\tassertErr: assert.NoError,\n\t\t},\n\t\t{\n\t\t\tname: \"fatal error\",\n\t\t\tmatcherFunc: func(_ vulnerability.Provider, _ pkg.Package) ([]match.Match, []match.IgnoreFilter, error) {\n\t\t\t\treturn nil, nil, match.NewFatalError(match.UnknownMatcherType, errors.New(\"some error\"))\n\t\t\t},\n\t\t\tassertErr: assert.Error,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tm := &VulnerabilityMatcher{\n\t\t\t\tMatchers: []match.Matcher{matcherMock.New(syftPkg.JavaPkg, tt.matcherFunc)},\n\t\t\t}\n\n\t\t\t_, _, err := m.FindMatches([]pkg.Package{\n\t\t\t\t{\n\t\t\t\t\tName:    \"foo\",\n\t\t\t\t\tVersion: \"1.2.3\",\n\t\t\t\t\tType:    syftPkg.JavaPkg,\n\t\t\t\t},\n\t\t\t},\n\t\t\t\tpkg.Context{},\n\t\t\t)\n\n\t\t\ttt.assertErr(t, err)\n\t\t})\n\t}\n}\n\nfunc Test_matchIgnoreFiltering(t *testing.T) {\n\t// one commonly used filter uses APK NAK data to exclude false positives on language packages at the same locations\n\t// based on packages in the APK DB. for example:\n\t//   APK package pkg1 is not vulnerable, but another python package is found with a slightly different name\n\t//   by the python cataloger when it scans the same files at the same locations that were associated with\n\t//   the APK package. the python package is checked by a separate matcher, so we surface ignore rules\n\t//   based on location and vuln id to exclude these false positives\n\tignoreByLocationAndVuln := func(locationToVulnIDs map[string][]string) []match.IgnoreFilter {\n\t\tvar out []match.IgnoreFilter\n\t\tfor path, vulnIDs := range locationToVulnIDs {\n\t\t\tfor _, vulnID := range vulnIDs {\n\t\t\t\tout = append(out, match.IgnoreRule{\n\t\t\t\t\tVulnerability:  vulnID,\n\t\t\t\t\tIncludeAliases: true,\n\t\t\t\t\tPackage: match.IgnoreRulePackage{\n\t\t\t\t\t\tLocation: path,\n\t\t\t\t\t},\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\t\treturn out\n\t}\n\n\t// with the addition of unaffected packages, ignore rules are returned by package language searches to account for\n\t// searches for subsequent CPE searches based on the specific package\n\tignoreByPackageNameAndVuln := func(pkgNameToVulnIDs map[string][]string) []match.IgnoreFilter {\n\t\tvar out []match.IgnoreFilter\n\t\tfor packageName, vulnIDs := range pkgNameToVulnIDs {\n\t\t\tfor _, vulnID := range vulnIDs {\n\t\t\t\tout = append(out, match.IgnoreRule{\n\t\t\t\t\tVulnerability:  vulnID,\n\t\t\t\t\tIncludeAliases: true,\n\t\t\t\t\tPackage: match.IgnoreRulePackage{\n\t\t\t\t\t\tName: packageName,\n\t\t\t\t\t},\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\t\treturn out\n\t}\n\n\t// construct matches here so we can make test cases more readable\n\n\tloc1 := \"/usr/bin/pkg1\"\n\tloc2 := \"/other/pkg1\"\n\n\tvuln1 := \"vuln1\"\n\tvuln2 := \"vuln2\"\n\n\tpkg1_vuln1_loc1 := match.Match{\n\t\tPackage: pkg.Package{\n\t\t\tType:      syftPkg.PythonPkg,\n\t\t\tName:      \"pkg1\",\n\t\t\tLocations: file.NewLocationSet(file.NewLocation(loc1)),\n\t\t},\n\t\tVulnerability: vulnerability.Vulnerability{Reference: vulnerability.Reference{ID: vuln1}},\n\t}\n\tpkg1_vuln2_loc1 := match.Match{\n\t\tPackage: pkg.Package{\n\t\t\tName:      \"pkg1\",\n\t\t\tLocations: file.NewLocationSet(file.NewLocation(loc1)),\n\t\t},\n\t\tVulnerability: vulnerability.Vulnerability{Reference: vulnerability.Reference{ID: vuln2}},\n\t}\n\tpkg1_vuln1_loc2 := match.Match{\n\t\tPackage: pkg.Package{\n\t\t\tName:      \"pkg1\",\n\t\t\tLocations: file.NewLocationSet(file.NewLocation(loc2)),\n\t\t},\n\t\tVulnerability: vulnerability.Vulnerability{Reference: vulnerability.Reference{ID: vuln1}},\n\t}\n\tpkg2_vuln1_loc1 := match.Match{\n\t\tPackage: pkg.Package{\n\t\t\tType:      syftPkg.PythonPkg,\n\t\t\tName:      \"pkg2\",\n\t\t\tLocations: file.NewLocationSet(file.NewLocation(loc1)),\n\t\t},\n\t\tVulnerability: vulnerability.Vulnerability{Reference: vulnerability.Reference{ID: vuln1}},\n\t}\n\tpkg2_vuln2_loc1 := match.Match{\n\t\tPackage: pkg.Package{\n\t\t\tName:      \"pkg2\",\n\t\t\tLocations: file.NewLocationSet(file.NewLocation(loc1)),\n\t\t},\n\t\tVulnerability: vulnerability.Vulnerability{Reference: vulnerability.Reference{ID: vuln2}},\n\t}\n\n\tcases := []struct {\n\t\tname          string\n\t\tinputMatches  []match.Match\n\t\tignoreFilters []match.IgnoreFilter\n\t\texpected      []match.Match\n\t}{\n\t\t{\n\t\t\tname:         \"no input matches\",\n\t\t\tinputMatches: nil,\n\t\t\tignoreFilters: ignoreByLocationAndVuln(map[string][]string{\n\t\t\t\tloc1: {vuln1},\n\t\t\t}),\n\t\t\texpected: nil,\n\t\t},\n\t\t{\n\t\t\tname: \"no ignore rules\",\n\t\t\tinputMatches: []match.Match{\n\t\t\t\tpkg1_vuln1_loc1,\n\t\t\t\tpkg1_vuln1_loc2,\n\t\t\t},\n\t\t\tignoreFilters: nil,\n\t\t\texpected: []match.Match{\n\t\t\t\tpkg1_vuln1_loc1,\n\t\t\t\tpkg1_vuln1_loc2,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"happy path filtering\",\n\t\t\tinputMatches: []match.Match{\n\t\t\t\tpkg1_vuln1_loc1,\n\t\t\t},\n\t\t\tignoreFilters: ignoreByLocationAndVuln(map[string][]string{\n\t\t\t\tloc1: {vuln1},\n\t\t\t}),\n\t\t\texpected: nil,\n\t\t},\n\t\t{\n\t\t\tname: \"location match different vuln\",\n\t\t\tinputMatches: []match.Match{\n\t\t\t\tpkg1_vuln1_loc1,\n\t\t\t},\n\t\t\tignoreFilters: ignoreByLocationAndVuln(map[string][]string{\n\t\t\t\tloc1: {vuln2},\n\t\t\t}),\n\t\t\texpected: []match.Match{\n\t\t\t\tpkg1_vuln1_loc1,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"location across packages\",\n\t\t\tinputMatches: []match.Match{\n\t\t\t\tpkg1_vuln1_loc1,\n\t\t\t\tpkg1_vuln2_loc1,\n\t\t\t\tpkg2_vuln1_loc1,\n\t\t\t\tpkg2_vuln2_loc1,\n\t\t\t},\n\t\t\tignoreFilters: ignoreByLocationAndVuln(map[string][]string{\n\t\t\t\tloc1: {vuln1},\n\t\t\t}),\n\t\t\texpected: []match.Match{\n\t\t\t\tpkg1_vuln2_loc1,\n\t\t\t\tpkg2_vuln2_loc1,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"package name\",\n\t\t\tinputMatches: []match.Match{\n\t\t\t\tpkg1_vuln1_loc1,\n\t\t\t\tpkg1_vuln2_loc1,\n\t\t\t\tpkg2_vuln1_loc1,\n\t\t\t},\n\t\t\tignoreFilters: ignoreByPackageNameAndVuln(map[string][]string{\n\t\t\t\tpkg1_vuln1_loc1.Package.Name: {vuln1},\n\t\t\t}),\n\t\t\texpected: []match.Match{\n\t\t\t\tpkg1_vuln2_loc1,\n\t\t\t\tpkg2_vuln1_loc1,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"not indexed rule\",\n\t\t\tinputMatches: []match.Match{\n\t\t\t\tpkg1_vuln1_loc1,\n\t\t\t\tpkg1_vuln2_loc1, // not python package\n\t\t\t\tpkg2_vuln1_loc1,\n\t\t\t},\n\t\t\tignoreFilters: []match.IgnoreFilter{\n\t\t\t\tmatch.IgnoreRule{\n\t\t\t\t\tPackage: match.IgnoreRulePackage{\n\t\t\t\t\t\tType: string(syftPkg.PythonPkg), // no indexed properties\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: []match.Match{\n\t\t\t\tpkg1_vuln2_loc1,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"not indexed filter\",\n\t\t\tinputMatches: []match.Match{\n\t\t\t\tpkg1_vuln1_loc1,\n\t\t\t\tpkg1_vuln1_loc2,\n\t\t\t\tpkg2_vuln1_loc1,\n\t\t\t\tpkg1_vuln2_loc1,\n\t\t\t},\n\t\t\tignoreFilters: []match.IgnoreFilter{\n\t\t\t\ttestIgnoreFilter{func(m match.Match) bool {\n\t\t\t\t\treturn m.Vulnerability.ID == vuln1\n\t\t\t\t}},\n\t\t\t},\n\t\t\texpected: []match.Match{\n\t\t\t\tpkg1_vuln2_loc1,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"multiple rules mixed\",\n\t\t\tinputMatches: []match.Match{\n\t\t\t\tpkg1_vuln1_loc1, // removed by custom filter\n\t\t\t\tpkg1_vuln1_loc2,\n\t\t\t\tpkg2_vuln1_loc1, // removed by name + vuln\n\t\t\t\tpkg1_vuln2_loc1, // removed by location + vuln\n\t\t\t},\n\t\t\tignoreFilters: append(append([]match.IgnoreFilter{\n\t\t\t\ttestIgnoreFilter{func(m match.Match) bool {\n\t\t\t\t\treturn m.Vulnerability.ID == vuln1\n\t\t\t\t}},\n\t\t\t}, ignoreByLocationAndVuln(map[string][]string{\n\t\t\t\tloc2: {vuln2},\n\t\t\t})...), ignoreByPackageNameAndVuln(map[string][]string{\n\t\t\t\tpkg2_vuln1_loc1.Package.Name: {vuln1},\n\t\t\t})...),\n\t\t\texpected: []match.Match{\n\t\t\t\tpkg1_vuln2_loc1,\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range cases {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tfilter := ignoredMatchFilter(tt.ignoreFilters)\n\n\t\t\tactual, _ := match.ApplyIgnoreFilters(tt.inputMatches, filter)\n\n\t\t\tassert.Equal(t, tt.expected, actual)\n\t\t})\n\t}\n}\n\nfunc Test_ignoredMatchFilterReasons(t *testing.T) {\n\tmatches := []match.Match{\n\t\t{\n\t\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\tID: \"CVE-123\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tPackage: pkg.Package{\n\t\t\t\tLocations: file.NewLocationSet(file.NewLocation(\"/usr/bin/thing\")),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\tID: \"CVE-456\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\tID: \"CVE-789\",\n\t\t\t\t},\n\t\t\t\tStatus: \"filter-me\",\n\t\t\t},\n\t\t},\n\t}\n\n\tignores := []match.IgnoreFilter{\n\t\tmatch.IgnoreRule{\n\t\t\tReason: \"test-location-ignore-rule\",\n\t\t\tPackage: match.IgnoreRulePackage{\n\t\t\t\tLocation: \"/usr/bin/thing\",\n\t\t\t},\n\t\t},\n\t\ttestIgnoreFilter{\n\t\t\tf: func(m match.Match) bool {\n\t\t\t\treturn m.Vulnerability.Status == \"filter-me\"\n\t\t\t},\n\t\t},\n\t}\n\n\tf := ignoredMatchFilter(ignores)\n\n\tvar ignoredReasons []string\n\tfor _, m := range matches {\n\t\tgot := f.IgnoreMatch(m)\n\t\tfor _, r := range got {\n\t\t\tignoredReasons = append(ignoredReasons, r.Reason)\n\t\t}\n\t}\n\n\trequire.ElementsMatch(t, []string{\"test-location-ignore-rule\", \"test-filtered\"}, ignoredReasons)\n}\n\ntype testIgnoreFilter struct {\n\tf func(match.Match) bool\n}\n\nfunc (t testIgnoreFilter) IgnoreMatch(m match.Match) []match.IgnoreRule {\n\tif t.f(m) {\n\t\treturn []match.IgnoreRule{\n\t\t\t{\n\t\t\t\tReason: \"test-filtered\",\n\t\t\t},\n\t\t}\n\t}\n\treturn nil\n}\n\ntype panicyMatcher struct {\n\tmatcherType match.MatcherType\n}\n\nfunc (m *panicyMatcher) PackageTypes() []syftPkg.Type {\n\treturn nil\n}\n\nfunc (m *panicyMatcher) Type() match.MatcherType {\n\treturn m.matcherType\n}\n\nfunc (m *panicyMatcher) Match(_ vulnerability.Provider, _ pkg.Package) ([]match.Match, []match.IgnoreFilter, error) {\n\tpanic(\"test panic message\")\n}\n\nfunc TestCallMatcherSafely_RecoverFromPanic(t *testing.T) {\n\tmatcher := &panicyMatcher{\n\t\tmatcherType: \"test-matcher\",\n\t}\n\t_, _, err := callMatcherSafely(matcher, nil, pkg.Package{})\n\n\trequire.Error(t, err)\n\tassert.True(t, match.IsFatalError(err))\n\trequire.Contains(t, err.Error(), \"test panic message\", \"missing message\")\n\trequire.Contains(t, err.Error(), \"test-matcher\", \"missing matcher name\")\n}\n\ntype busListener struct {\n\tmatching monitor.Matching\n}\n\nfunc (b *busListener) Publish(e partybus.Event) {\n\tif e.Type == event.VulnerabilityScanningStarted {\n\t\tif m, ok := e.Value.(monitor.Matching); ok {\n\t\t\tb.matching = m\n\t\t}\n\t}\n}\n\nvar _ partybus.Publisher = (*busListener)(nil)\n\n// mockEOLProvider is a mock vulnerability provider that also implements EOLChecker\ntype mockEOLProvider struct {\n\tvulnerability.Provider\n\teolDates map[string]*time.Time // distro key -> EOL date\n}\n\nfunc (m *mockEOLProvider) GetOperatingSystemEOL(d *distro.Distro) (*time.Time, *time.Time, error) {\n\tif m.eolDates == nil {\n\t\treturn nil, nil, nil\n\t}\n\teolDate := m.eolDates[d.String()]\n\treturn eolDate, nil, nil\n}\n\nvar _ vulnerability.EOLChecker = (*mockEOLProvider)(nil)\n\nfunc TestEOLTracker(t *testing.T) {\n\tpastDate := time.Now().Add(-24 * time.Hour)\n\tfutureDate := time.Now().Add(24 * time.Hour * 365)\n\n\tdebian8 := &distro.Distro{\n\t\tType:    \"debian\",\n\t\tVersion: \"8\",\n\t}\n\tubuntu2204 := &distro.Distro{\n\t\tType:    \"ubuntu\",\n\t\tVersion: \"22.04\",\n\t}\n\n\ttests := []struct {\n\t\tname          string\n\t\tenabled       bool\n\t\tprovider      vulnerability.Provider\n\t\tpkg           pkg.Package\n\t\texpectedIsEOL bool\n\t}{\n\t\t{\n\t\t\tname:          \"tracking disabled - returns false\",\n\t\t\tenabled:       false,\n\t\t\tprovider:      mock.VulnerabilityProvider(),\n\t\t\tpkg:           pkg.Package{Distro: debian8},\n\t\t\texpectedIsEOL: false,\n\t\t},\n\t\t{\n\t\t\tname:          \"provider does not implement EOLChecker - returns false\",\n\t\t\tenabled:       true,\n\t\t\tprovider:      mock.VulnerabilityProvider(), // does not implement EOLChecker\n\t\t\tpkg:           pkg.Package{Distro: debian8},\n\t\t\texpectedIsEOL: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"package has no distro - returns false\",\n\t\t\tenabled: true,\n\t\t\tprovider: &mockEOLProvider{\n\t\t\t\tProvider: mock.VulnerabilityProvider(),\n\t\t\t\teolDates: map[string]*time.Time{\n\t\t\t\t\tdebian8.String(): &pastDate,\n\t\t\t\t},\n\t\t\t},\n\t\t\tpkg:           pkg.Package{Distro: nil},\n\t\t\texpectedIsEOL: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"distro is EOL - returns true\",\n\t\t\tenabled: true,\n\t\t\tprovider: &mockEOLProvider{\n\t\t\t\tProvider: mock.VulnerabilityProvider(),\n\t\t\t\teolDates: map[string]*time.Time{\n\t\t\t\t\tdebian8.String(): &pastDate,\n\t\t\t\t},\n\t\t\t},\n\t\t\tpkg:           pkg.Package{Distro: debian8},\n\t\t\texpectedIsEOL: true,\n\t\t},\n\t\t{\n\t\t\tname:    \"distro is not EOL - returns false\",\n\t\t\tenabled: true,\n\t\t\tprovider: &mockEOLProvider{\n\t\t\t\tProvider: mock.VulnerabilityProvider(),\n\t\t\t\teolDates: map[string]*time.Time{\n\t\t\t\t\tubuntu2204.String(): &futureDate,\n\t\t\t\t},\n\t\t\t},\n\t\t\tpkg:           pkg.Package{Distro: ubuntu2204},\n\t\t\texpectedIsEOL: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"distro has no EOL data - returns false\",\n\t\t\tenabled: true,\n\t\t\tprovider: &mockEOLProvider{\n\t\t\t\tProvider: mock.VulnerabilityProvider(),\n\t\t\t\teolDates: map[string]*time.Time{}, // no data for any distro\n\t\t\t},\n\t\t\tpkg:           pkg.Package{Distro: debian8},\n\t\t\texpectedIsEOL: false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ttracker := newEOLTracker(tt.enabled, tt.provider)\n\t\t\tresult := tracker.checkAndTrack(tt.pkg)\n\t\t\tassert.Equal(t, tt.expectedIsEOL, result)\n\t\t})\n\t}\n}\n\nfunc TestEOLTrackerCaching(t *testing.T) {\n\tpastDate := time.Now().Add(-24 * time.Hour)\n\n\tdebian8 := &distro.Distro{\n\t\tType:    \"debian\",\n\t\tVersion: \"8\",\n\t}\n\n\tcallCount := 0\n\tprovider := &countingEOLProvider{\n\t\tProvider: mock.VulnerabilityProvider(),\n\t\teolDates: map[string]*time.Time{\n\t\t\tdebian8.String(): &pastDate,\n\t\t},\n\t\tcallCount: &callCount,\n\t}\n\n\ttracker := newEOLTracker(true, provider)\n\n\t// First call should query the provider\n\tresult1 := tracker.checkAndTrack(pkg.Package{Distro: debian8, Name: \"pkg1\"})\n\tassert.True(t, result1)\n\tassert.Equal(t, 1, callCount, \"expected one call to GetOperatingSystemEOL\")\n\n\t// Second call with same distro should use cache\n\tresult2 := tracker.checkAndTrack(pkg.Package{Distro: debian8, Name: \"pkg2\"})\n\tassert.True(t, result2)\n\tassert.Equal(t, 1, callCount, \"expected no additional call - should use cache\")\n\n\t// Third call with same distro should still use cache\n\tresult3 := tracker.checkAndTrack(pkg.Package{Distro: debian8, Name: \"pkg3\"})\n\tassert.True(t, result3)\n\tassert.Equal(t, 1, callCount, \"expected no additional call - should use cache\")\n}\n\ntype countingEOLProvider struct {\n\tvulnerability.Provider\n\teolDates  map[string]*time.Time\n\tcallCount *int\n}\n\nfunc (c *countingEOLProvider) GetOperatingSystemEOL(d *distro.Distro) (*time.Time, *time.Time, error) {\n\t*c.callCount++\n\tif c.eolDates == nil {\n\t\treturn nil, nil, nil\n\t}\n\teolDate := c.eolDates[d.String()]\n\treturn eolDate, nil, nil\n}\n\nvar _ vulnerability.EOLChecker = (*countingEOLProvider)(nil)\n\nfunc TestVulnerabilityMatcher_EOLDistroPackages(t *testing.T) {\n\tpastDate := time.Now().Add(-24 * time.Hour)\n\tfutureDate := time.Now().Add(24 * time.Hour * 365)\n\n\tdebian8 := &distro.Distro{\n\t\tType:    \"debian\",\n\t\tVersion: \"8\",\n\t}\n\tubuntu2204 := &distro.Distro{\n\t\tType:    \"ubuntu\",\n\t\tVersion: \"22.04\",\n\t}\n\n\teolPkg := pkg.Package{\n\t\tID:      pkg.ID(uuid.NewString()),\n\t\tName:    \"eol-pkg\",\n\t\tVersion: \"1.0.0\",\n\t\tType:    syftPkg.DebPkg,\n\t\tDistro:  debian8,\n\t}\n\n\tnonEOLPkg := pkg.Package{\n\t\tID:      pkg.ID(uuid.NewString()),\n\t\tName:    \"non-eol-pkg\",\n\t\tVersion: \"1.0.0\",\n\t\tType:    syftPkg.DebPkg,\n\t\tDistro:  ubuntu2204,\n\t}\n\n\tnoDistroPkg := pkg.Package{\n\t\tID:      pkg.ID(uuid.NewString()),\n\t\tName:    \"no-distro-pkg\",\n\t\tVersion: \"1.0.0\",\n\t\tType:    syftPkg.NpmPkg,\n\t}\n\n\tprovider := &mockEOLProvider{\n\t\tProvider: mock.VulnerabilityProvider(),\n\t\teolDates: map[string]*time.Time{\n\t\t\tdebian8.String():    &pastDate,\n\t\t\tubuntu2204.String(): &futureDate,\n\t\t},\n\t}\n\n\ttests := []struct {\n\t\tname                string\n\t\talertsConfig        AlertsConfig\n\t\tpackages            []pkg.Package\n\t\texpectedEOLPackages []string // package names expected to be tracked as EOL\n\t}{\n\t\t{\n\t\t\tname: \"EOL tracking enabled - tracks EOL packages\",\n\t\t\talertsConfig: AlertsConfig{\n\t\t\t\tEnableEOLDistroWarnings: true,\n\t\t\t},\n\t\t\tpackages:            []pkg.Package{eolPkg, nonEOLPkg, noDistroPkg},\n\t\t\texpectedEOLPackages: []string{\"eol-pkg\"},\n\t\t},\n\t\t{\n\t\t\tname: \"EOL tracking disabled - no packages tracked\",\n\t\t\talertsConfig: AlertsConfig{\n\t\t\t\tEnableEOLDistroWarnings: false,\n\t\t\t},\n\t\t\tpackages:            []pkg.Package{eolPkg, nonEOLPkg, noDistroPkg},\n\t\t\texpectedEOLPackages: nil,\n\t\t},\n\t\t{\n\t\t\tname: \"all packages from EOL distro\",\n\t\t\talertsConfig: AlertsConfig{\n\t\t\t\tEnableEOLDistroWarnings: true,\n\t\t\t},\n\t\t\tpackages:            []pkg.Package{eolPkg},\n\t\t\texpectedEOLPackages: []string{\"eol-pkg\"},\n\t\t},\n\t\t{\n\t\t\tname: \"no packages from EOL distro\",\n\t\t\talertsConfig: AlertsConfig{\n\t\t\t\tEnableEOLDistroWarnings: true,\n\t\t\t},\n\t\t\tpackages:            []pkg.Package{nonEOLPkg, noDistroPkg},\n\t\t\texpectedEOLPackages: nil,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tm := &VulnerabilityMatcher{\n\t\t\t\tVulnerabilityProvider: provider,\n\t\t\t\tMatchers:              matcher.NewDefaultMatchers(matcher.Config{}),\n\t\t\t\tAlerts:                tt.alertsConfig,\n\t\t\t}\n\n\t\t\tlistener := &busListener{}\n\t\t\tbus.Set(listener)\n\t\t\tdefer bus.Set(nil)\n\n\t\t\t_, _, err := m.FindMatches(tt.packages, pkg.Context{})\n\t\t\trequire.NoError(t, err)\n\n\t\t\teolPackages := m.EOLDistroPackages()\n\n\t\t\tvar actualNames []string\n\t\t\tfor _, p := range eolPackages {\n\t\t\t\tactualNames = append(actualNames, p.Name)\n\t\t\t}\n\n\t\t\tif tt.expectedEOLPackages == nil {\n\t\t\t\tassert.Empty(t, actualNames)\n\t\t\t} else {\n\t\t\t\tassert.ElementsMatch(t, tt.expectedEOLPackages, actualNames)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "install.sh",
    "content": "#!/bin/sh\n# note: we require errors to propagate (don't set -e)\nset -u\n\nPROJECT_NAME=grype\nOWNER=anchore\nREPO=\"${PROJECT_NAME}\"\nGITHUB_DOWNLOAD_PREFIX=https://github.com/${OWNER}/${REPO}/releases/download\nINSTALL_SH_BASE_URL=https://get.anchore.io/${PROJECT_NAME}\nLEGACY_INSTALL_SH_BASE_URL=https://raw.githubusercontent.com/${OWNER}/${PROJECT_NAME}\nPROGRAM_ARGS=$@\n\n# signature verification options\n\n# the location to the cosign binary (allowed to be overridden by the user)\nCOSIGN_BINARY=${COSIGN_BINARY:-cosign}\nVERIFY_SIGN=false\n# this is the earliest tag in the repo where cosign sign-blob was introduced in the release process (see the goreleaser config)\nVERIFY_SIGN_SUPPORTED_VERSION=v0.72.0\n# this is the earliest tag in the repo where the -v flag was introduced to this install.sh script\nVERIFY_SIGN_FLAG_VERSION=v0.79.0\n\n# do not change the name of this parameter (this must always be backwards compatible)\nDOWNLOAD_TAG_INSTALL_SCRIPT=${DOWNLOAD_TAG_INSTALL_SCRIPT:-true}\n\n# ------------------------------------------------------------------------\n# https://github.com/client9/shlib - portable posix shell functions\n# Public domain - http://unlicense.org\n# https://github.com/client9/shlib/blob/master/LICENSE.md\n# but credit (and pull requests) appreciated.\n# ------------------------------------------------------------------------\n\nis_command() (\n  command -v \"$1\" >/dev/null\n)\n\necho_stderr() (\n  echo \"$@\" 1>&2\n)\n\n_logp=2\nlog_set_priority() {\n  _logp=\"$1\"\n}\n\nlog_priority() (\n  if test -z \"$1\"; then\n    echo \"$_logp\"\n    return\n  fi\n  [ \"$1\" -le \"$_logp\" ]\n)\n\ninit_colors() {\n  RED=''\n  BLUE=''\n  PURPLE=''\n  BOLD=''\n  RESET=''\n  # check if stdout is a terminal\n  if test -t 1 && is_command tput; then\n      # see if it supports colors\n      ncolors=$(tput colors)\n      if test -n \"$ncolors\" && test $ncolors -ge 8; then\n        RED='\\033[0;31m'\n        BLUE='\\033[0;34m'\n        PURPLE='\\033[0;35m'\n        BOLD='\\033[1m'\n        RESET='\\033[0m'\n      fi\n  fi\n}\n\ninit_colors\n\nlog_tag() (\n  case $1 in\n    0) echo \"${RED}${BOLD}[error]${RESET}\" ;;\n    1) echo \"${RED}[warn]${RESET}\" ;;\n    2) echo \"[info]${RESET}\" ;;\n    3) echo \"${BLUE}[debug]${RESET}\" ;;\n    4) echo \"${PURPLE}[trace]${RESET}\" ;;\n    *) echo \"[$1]\" ;;\n  esac\n)\n\n\nlog_trace_priority=4\nlog_trace() (\n  priority=$log_trace_priority\n  log_priority \"$priority\" || return 0\n  echo_stderr \"$(log_tag $priority)\" \"${@}\" \"${RESET}\"\n)\n\nlog_debug_priority=3\nlog_debug() (\n  priority=$log_debug_priority\n  log_priority \"$priority\" || return 0\n  echo_stderr \"$(log_tag $priority)\" \"${@}\" \"${RESET}\"\n)\n\nlog_info_priority=2\nlog_info() (\n  priority=$log_info_priority\n  log_priority \"$priority\" || return 0\n  echo_stderr \"$(log_tag $priority)\" \"${@}\" \"${RESET}\"\n)\n\nlog_warn_priority=1\nlog_warn() (\n  priority=$log_warn_priority\n  log_priority \"$priority\" || return 0\n  echo_stderr \"$(log_tag $priority)\" \"${@}\" \"${RESET}\"\n)\n\nlog_err_priority=0\nlog_err() (\n  priority=$log_err_priority\n  log_priority \"$priority\" || return 0\n  echo_stderr \"$(log_tag $priority)\" \"${@}\" \"${RESET}\"\n)\n\nuname_os_check() (\n  os=$1\n  case \"$os\" in\n    darwin) return 0 ;;\n    dragonfly) return 0 ;;\n    freebsd) return 0 ;;\n    linux) return 0 ;;\n    android) return 0 ;;\n    nacl) return 0 ;;\n    netbsd) return 0 ;;\n    openbsd) return 0 ;;\n    plan9) return 0 ;;\n    solaris) return 0 ;;\n    windows) return 0 ;;\n  esac\n  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\"\n  return 1\n)\n\nuname_arch_check() (\n  arch=$1\n  case \"$arch\" in\n    386) return 0 ;;\n    amd64) return 0 ;;\n    arm64) return 0 ;;\n    armv5) return 0 ;;\n    armv6) return 0 ;;\n    armv7) return 0 ;;\n    ppc64) return 0 ;;\n    ppc64le) return 0 ;;\n    mips) return 0 ;;\n    mipsle) return 0 ;;\n    mips64) return 0 ;;\n    mips64le) return 0 ;;\n    s390x) return 0 ;;\n    amd64p32) return 0 ;;\n  esac\n  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\"\n  return 1\n)\n\nunpack() (\n  archive=$1\n\n  log_trace \"unpack(archive=${archive})\"\n\n  case \"${archive}\" in\n    *.tar.gz | *.tgz) tar --no-same-owner -xzf \"${archive}\" ;;\n    *.tar) tar --no-same-owner -xf \"${archive}\" ;;\n    *.zip) unzip -q \"${archive}\" ;;\n    *.dmg) extract_from_dmg \"${archive}\" ;;\n    *)\n      log_err \"unpack unknown archive format for ${archive}\"\n      return 1\n      ;;\n  esac\n)\n\nextract_from_dmg() (\n  dmg_file=$1\n\n  mount_point=\"/Volumes/tmp-dmg\"\n  hdiutil attach -quiet -nobrowse -mountpoint \"${mount_point}\" \"${dmg_file}\"\n  cp -fR \"${mount_point}/.\" ./\n  hdiutil detach -quiet -force \"${mount_point}\"\n)\n\nhttp_download_curl() (\n  local_file=$1\n  source_url=$2\n  header=$3\n\n  log_trace \"http_download_curl(local_file=$local_file, source_url=$source_url, header=$header)\"\n\n  if [ -z \"$header\" ]; then\n    code=$(curl -w '%{http_code}' -sL -o \"$local_file\" \"$source_url\")\n  else\n    code=$(curl -w '%{http_code}' -sL -H \"$header\" -o \"$local_file\" \"$source_url\")\n  fi\n\n  if [ \"$code\" != \"200\" ]; then\n    log_err \"received HTTP status=$code for url='$source_url'\"\n    return 1\n  fi\n  return 0\n)\n\nhttp_download_wget() (\n  local_file=$1\n  source_url=$2\n  header=$3\n\n  log_trace \"http_download_wget(local_file=$local_file, source_url=$source_url, header=$header)\"\n\n  if [ -z \"$header\" ]; then\n    wget -q -O \"$local_file\" \"$source_url\"\n  else\n    wget -q --header \"$header\" -O \"$local_file\" \"$source_url\"\n  fi\n)\n\nhttp_download() (\n  log_debug \"http_download(url=$2)\"\n  if is_command curl; then\n    http_download_curl \"$@\"\n    return\n  elif is_command wget; then\n    http_download_wget \"$@\"\n    return\n  fi\n  log_err \"http_download unable to find wget or curl\"\n  return 1\n)\n\nhttp_copy() (\n  tmp=$(mktemp)\n  http_download \"${tmp}\" \"$1\" \"$2\" || return 1\n  body=$(cat \"$tmp\")\n  rm -f \"${tmp}\"\n  echo \"$body\"\n)\n\nhash_sha256() (\n  TARGET=${1:-/dev/stdin}\n  if is_command gsha256sum; then\n    hash=$(gsha256sum \"$TARGET\") || return 1\n    echo \"$hash\" | cut -d ' ' -f 1\n  elif is_command sha256sum; then\n    hash=$(sha256sum \"$TARGET\") || return 1\n    echo \"$hash\" | cut -d ' ' -f 1\n  elif is_command shasum; then\n    hash=$(shasum -a 256 \"$TARGET\" 2>/dev/null) || return 1\n    echo \"$hash\" | cut -d ' ' -f 1\n  elif is_command openssl; then\n    hash=$(openssl -dst openssl dgst -sha256 \"$TARGET\") || return 1\n    echo \"$hash\" | cut -d ' ' -f a\n  else\n    log_err \"hash_sha256 unable to find command to compute sha-256 hash\"\n    return 1\n  fi\n)\n\nhash_sha256_verify() (\n  target=$1\n  checksums=$2\n  if [ -z \"$checksums\" ]; then\n    log_err \"hash_sha256_verify checksum file not specified as argument\"\n    return 1\n  fi\n  target_basename=${target##*/}\n  want=$(grep \"${target_basename}\" \"${checksums}\" 2>/dev/null | tr '\\t' ' ' | cut -d ' ' -f 1)\n  if [ -z \"$want\" ]; then\n    log_err \"hash_sha256_verify unable to find checksum for '${target}' in '${checksums}'\"\n    return 1\n  fi\n  got=$(hash_sha256 \"$target\")\n  if [ \"$want\" != \"$got\" ]; then\n    log_err \"hash_sha256_verify checksum for '$target' did not verify ${want} vs $got\"\n    return 1\n  fi\n)\n\n# ------------------------------------------------------------------------\n# End of functions from https://github.com/client9/shlib\n# ------------------------------------------------------------------------\n\n# asset_file_exists [path]\n#\n# returns 1 if the given file does not exist\n#\nasset_file_exists() (\n  path=\"$1\"\n  if [ ! -f \"${path}\" ]; then\n      return 1\n  fi\n)\n\n\n# github_release_json [owner] [repo] [version]\n#\n# outputs release json string\n#\ngithub_release_json() (\n  owner=$1\n  repo=$2\n  version=$3\n  test -z \"$version\" && version=\"latest\"\n  giturl=\"https://github.com/${owner}/${repo}/releases/${version}\"\n  json=$(http_copy \"$giturl\" \"Accept:application/json\")\n\n  log_trace \"github_release_json(owner=${owner}, repo=${repo}, version=${version}) returned '${json}'\"\n\n  test -z \"$json\" && return 1\n  echo \"${json}\"\n)\n\n# extract_value [key-value-pair]\n#\n# outputs value from a colon delimited key-value pair\n#\nextract_value() (\n  key_value=\"$1\"\n  IFS=':' read -r _ value << EOF\n${key_value}\nEOF\n  echo \"$value\"\n)\n\n# extract_json_value [json] [key]\n#\n# outputs value of the key from the given json string\n#\nextract_json_value() (\n  json=\"$1\"\n  key=\"$2\"\n  key_value=$(echo \"${json}\" | grep  -o \"\\\"$key\\\":[^,]*[,}]\" | tr -d '\",}')\n\n  extract_value \"$key_value\"\n)\n\n# github_release_tag [release-json]\n#\n# outputs release tag string\n#\ngithub_release_tag() (\n  json=\"$1\"\n  tag=$(extract_json_value \"${json}\" \"tag_name\")\n  test -z \"$tag\" && return 1\n  echo \"$tag\"\n)\n\n# github_release_asset_url [release-url-prefix] [name] [version] [output-dir] [filename]\n#\n# outputs the url to the release asset\n#\ngithub_release_asset_url() (\n  download_url=\"$1\"\n  name=\"$2\"\n  version=\"$3\"\n  filename=\"$4\"\n\n  complete_filename=\"${name}_${version}_${filename}\"\n  complete_url=\"${download_url}/${complete_filename}\"\n\n  echo \"${complete_url}\"\n)\n\n# download_github_release_checksums_files [release-url-prefix] [name] [version] [output-dir] [filename]\n#\n# outputs path to the downloaded checksums related file\n#\ndownload_github_release_checksums_files() (\n  download_url=\"$1\"\n  name=\"$2\"\n  version=\"$3\"\n  output_dir=\"$4\"\n  filename=\"$5\"\n\n  log_trace \"download_github_release_checksums_files(url=${download_url}, name=${name}, version=${version}, output_dir=${output_dir}, filename=${filename})\"\n\n  complete_filename=\"${name}_${version}_${filename}\"\n  complete_url=$(github_release_asset_url \"${download_url}\" \"${name}\" \"${version}\" \"${filename}\")\n  output_path=\"${output_dir}/${complete_filename}\"\n\n  http_download \"${output_path}\" \"${complete_url}\" \"\"\n  asset_file_exists \"${output_path}\"\n\n  log_trace \"download_github_release_checksums_files() returned '${output_path}' for file '${complete_filename}'\"\n\n  echo \"${output_path}\"\n)\n\n# download_github_release_checksums [release-url-prefix] [name] [version] [output-dir]\n#\n# outputs path to the downloaded checksums file\n#\ndownload_github_release_checksums() (\n  download_github_release_checksums_files \"$@\" \"checksums.txt\"\n)\n\n# github_release_checksums_sig_url [release-url-prefix] [name] [version]\n#\n# outputs the url to the release checksums signature file\n#\ngithub_release_checksums_sig_url() (\n  github_release_asset_url \"$@\" \"checksums.txt.sig\"\n)\n\n# github_release_checksums_cert_url [release-url-prefix] [name] [version]\n#\n# outputs the url to the release checksums certificate file\n#\ngithub_release_checksums_cert_url() (\n  github_release_asset_url \"$@\" \"checksums.txt.pem\"\n)\n\n# search_for_asset [checksums-file-path] [name] [os] [arch] [format]\n#\n# outputs name of the asset to download\n#\nsearch_for_asset() (\n  checksum_path=\"$1\"\n  name=\"$2\"\n  os=\"$3\"\n  arch=\"$4\"\n  format=\"$5\"\n\n  log_trace \"search_for_asset(checksum-path=${checksum_path}, name=${name}, os=${os}, arch=${arch}, format=${format})\"\n\n  asset_glob=\"${name}_.*_${os}_${arch}.${format}\"\n  output_path=$(grep -o \"${asset_glob}\" \"${checksum_path}\" || true)\n\n  log_trace \"search_for_asset() returned '${output_path}'\"\n\n  echo \"${output_path}\"\n)\n\n# uname_os\n#\n# outputs an adjusted os value\n#\nuname_os() (\n  os=$(uname -s | tr '[:upper:]' '[:lower:]')\n  case \"$os\" in\n    cygwin_nt*) os=\"windows\" ;;\n    mingw*) os=\"windows\" ;;\n    msys_nt*) os=\"windows\" ;;\n  esac\n\n  uname_os_check \"$os\"\n\n  log_trace \"uname_os() returned '${os}'\"\n\n  echo \"$os\"\n)\n\n# uname_arch\n#\n# outputs an adjusted architecture value\n#\nuname_arch() (\n  arch=$(uname -m)\n  case $arch in\n    x86_64) arch=\"amd64\" ;;\n    x86) arch=\"386\" ;;\n    i686) arch=\"386\" ;;\n    i386) arch=\"386\" ;;\n    aarch64) arch=\"arm64\" ;;\n    armv5*) arch=\"armv5\" ;;\n    armv6*) arch=\"armv6\" ;;\n    armv7*) arch=\"armv7\" ;;\n  esac\n\n  uname_arch_check \"${arch}\"\n\n  log_trace \"uname_arch() returned '${arch}'\"\n\n  echo \"${arch}\"\n)\n\n# get_release_tag [owner] [repo] [tag]\n#\n# outputs tag string\n#\nget_release_tag() (\n  owner=\"$1\"\n  repo=\"$2\"\n  tag=\"$3\"\n\n  log_trace \"get_release_tag(owner=${owner}, repo=${repo}, tag=${tag})\"\n\n  json=$(github_release_json \"${owner}\" \"${repo}\" \"${tag}\")\n  real_tag=$(github_release_tag \"${json}\")\n  if test -z \"${real_tag}\"; then\n    return 1\n  fi\n\n  log_trace \"get_release_tag() returned '${real_tag}'\"\n\n  echo \"${real_tag}\"\n)\n\n# tag_to_version [tag]\n#\n# outputs version string\n#\ntag_to_version() (\n  tag=\"$1\"\n  value=\"${tag#v}\"\n\n  log_trace \"tag_to_version(tag=${tag}) returned '${value}'\"\n\n  echo \"$value\"\n)\n\n# get_binary_name [os] [arch] [default-name]\n#\n# outputs a the binary string name\n#\nget_binary_name() (\n  os=\"$1\"\n  arch=\"$2\"\n  binary=\"$3\"\n  original_binary=\"${binary}\"\n\n  case \"${os}\" in\n    windows) binary=\"${binary}.exe\" ;;\n  esac\n\n  log_trace \"get_binary_name(os=${os}, arch=${arch}, binary=${original_binary}) returned '${binary}'\"\n\n  echo \"${binary}\"\n)\n\n\n# get_format_name [os] [arch] [default-format]\n#\n# outputs an adjusted file format\n#\nget_format_name() (\n  os=\"$1\"\n  arch=\"$2\"\n  format=\"$3\"\n  original_format=\"${format}\"\n\n  case ${os} in\n    windows) format=zip ;;\n  esac\n\n  log_trace \"get_format_name(os=${os}, arch=${arch}, format=${original_format}) returned '${format}'\"\n\n  echo \"${format}\"\n)\n\n# download_and_install_asset [release-url-prefix] [download-path] [install-path] [name] [os] [arch] [version] [format] [binary]\n#\n# attempts to download the archive and install it to the given path.\n#\ndownload_and_install_asset() (\n  download_url=\"$1\"\n  download_path=\"$2\"\n  install_path=$3\n  name=\"$4\"\n  os=\"$5\"\n  arch=\"$6\"\n  version=\"$7\"\n  format=\"$8\"\n  binary=\"$9\"\n\n  if ! asset_filepath=$(download_asset \"${download_url}\" \"${download_path}\" \"${name}\" \"${os}\" \"${arch}\" \"${version}\" \"${format}\"); then\n      log_err \"could not download asset for os='${os}' arch='${arch}' format='${format}'\"\n      return 1\n  fi\n\n  # don't continue if we couldn't download an asset\n  if [ -z \"${asset_filepath}\" ]; then\n      log_err \"could not find release asset for os='${os}' arch='${arch}' format='${format}' \"\n      return 1\n  fi\n\n  install_asset \"${asset_filepath}\" \"${install_path}\" \"${binary}\"\n)\n\n# verify_sign [checksums-file-path] [certificate-reference] [signature-reference] [version]\n#\n# attempts verify the signature of the checksums file from the release workflow in Github Actions run against the main branch.\n#\nverify_sign() {\n  checksums_file=$1\n  cert_reference=$2\n  sig_reference=$3\n\n  log_trace \"verifying artifact $1\"\n\n  log_file=$(mktemp)\n\n  ${COSIGN_BINARY} \\\n    verify-blob \"$checksums_file\" \\\n      --certificate \"$cert_reference\" \\\n      --signature \"$sig_reference\" \\\n      --certificate-identity \"https://github.com/${OWNER}/${REPO}/.github/workflows/release.yaml@refs/heads/main\" \\\n      --certificate-oidc-issuer \"https://token.actions.githubusercontent.com\" > \"${log_file}\" 2>&1\n\n  if [ $? -ne 0 ]; then\n    log_err \"$(cat \"${log_file}\")\"\n    rm -f \"${log_file}\"\n    return 1\n  fi\n\n  rm -f \"${log_file}\"\n}\n\n\n# download_asset [release-url-prefix] [download-path] [name] [os] [arch] [version] [format] [binary]\n#\n# outputs the path to the downloaded asset asset_filepath\n#\ndownload_asset() (\n  download_url=\"$1\"\n  destination=\"$2\"\n  name=\"$3\"\n  os=\"$4\"\n  arch=\"$5\"\n  version=\"$6\"\n  format=\"$7\"\n\n  log_trace \"download_asset(url=${download_url}, destination=${destination}, name=${name}, os=${os}, arch=${arch}, version=${version}, format=${format})\"\n\n  checksums_filepath=$(download_github_release_checksums \"${download_url}\" \"${name}\" \"${version}\" \"${destination}\")\n\n  log_trace \"checksums content:\\n$(cat ${checksums_filepath})\"\n\n  asset_filename=$(search_for_asset \"${checksums_filepath}\" \"${name}\" \"${os}\" \"${arch}\" \"${format}\")\n\n  # don't continue if we couldn't find a matching asset from the checksums file\n  if [ -z \"${asset_filename}\" ]; then\n      return 1\n  fi\n\n  if [ \"$VERIFY_SIGN\" = true ]; then\n    checksum_sig_file_url=$(github_release_checksums_sig_url \"${download_url}\" \"${name}\" \"${version}\")\n    log_trace \"checksums signature url: ${checksum_sig_file_url}\"\n\n    checksums_cert_file_url=$(github_release_checksums_cert_url \"${download_url}\" \"${name}\" \"${version}\")\n    log_trace \"checksums certificate url: ${checksums_cert_file_url}\"\n\n    if ! verify_sign \"${checksums_filepath}\" \"${checksums_cert_file_url}\" \"${checksum_sig_file_url}\"; then\n      log_err \"signature verification failed\"\n      return 1\n    fi\n    log_info \"signature verification succeeded\"\n  fi\n\n  asset_url=\"${download_url}/${asset_filename}\"\n  asset_filepath=\"${destination}/${asset_filename}\"\n  http_download \"${asset_filepath}\" \"${asset_url}\" \"\"\n\n  hash_sha256_verify \"${asset_filepath}\" \"${checksums_filepath}\"\n\n  log_trace \"download_asset_by_checksums_file() returned '${asset_filepath}'\"\n\n  echo \"${asset_filepath}\"\n)\n\n# install_asset [asset-path] [destination-path] [binary]\n#\ninstall_asset() (\n  asset_filepath=\"$1\"\n  destination=\"$2\"\n  binary=\"$3\"\n\n  log_trace \"install_asset(asset=${asset_filepath}, destination=${destination}, binary=${binary})\"\n\n  # don't continue if we don't have anything to install\n  if [ -z \"${asset_filepath}\" ]; then\n      return\n  fi\n\n  archive_dir=$(dirname \"${asset_filepath}\")\n\n  # unarchive the downloaded archive to the temp dir\n  (cd \"${archive_dir}\" && unpack \"${asset_filepath}\")\n\n  # create the destination dir\n  test ! -d \"${destination}\" && install -d \"${destination}\"\n\n  # install the binary to the destination dir\n  install \"${archive_dir}/${binary}\" \"${destination}/\"\n)\n\n# compare two semver strings. Returns 0 if version1 >= version2, 1 otherwise.\n# Note: pre-release (-) and metadata (+) are not supported.\ncompare_semver() {\n    # remove leading 'v' if present\n    version1=${1#v}\n    version2=${2#v}\n\n    IFS=. read -r major1 minor1 patch1 <<EOF\n$version1\nEOF\n    IFS=. read -r major2 minor2 patch2 <<EOF\n$version2\nEOF\n\n    if [ \"$major1\" -gt \"$major2\" ]; then\n        return 0\n    elif [ \"$major1\" -lt \"$major2\" ]; then\n        return 1\n    fi\n\n    if [ \"$minor1\" -gt \"$minor2\" ]; then\n        return 0\n    elif [ \"$minor1\" -lt \"$minor2\" ]; then\n        return 1\n    fi\n\n    if [ \"$patch1\" -gt \"$patch2\" ]; then\n        return 0\n    elif [ \"$patch1\" -lt \"$patch2\" ]; then\n        return 1\n    fi\n\n    # versions are equal\n    return 0\n}\n\nprep_signature_verification() {\n  version=\"$1\"\n\n  if [ \"$VERIFY_SIGN\" != true ]; then\n    return 0\n  fi\n\n  # is there any cryptographic material produced at release that we can use for signature verification?\n  if ! compare_semver \"$version\" \"$VERIFY_SIGN_SUPPORTED_VERSION\"; then\n    log_err \"${PROJECT_NAME} release '$version' does not support signature verification\"\n    log_err \"you can still install ${PROJECT_NAME} by removing the -v flag or using a release that supports signature verification (>= '$VERIFY_SIGN_SUPPORTED_VERSION')\"\n    log_err \"aborting installation\"\n    return 1\n  else\n    log_trace \"${PROJECT_NAME} release '$version' supports signature verification (>= '$VERIFY_SIGN_SUPPORTED_VERSION')\"\n  fi\n\n  # will invoking an earlier version of this script work (considering the -v flag)?\n  if ! compare_semver \"$version\" \"$VERIFY_SIGN_FLAG_VERSION\"; then\n    # the -v argument did not always exist, so we cannot be guaranteed that invoking an earlier version of this script\n    # will work (error with \"illegal option -v\"). However, the user requested signature verification, so we will\n    # attempt to install the application with this version of the script (keeping signature verification).\n    DOWNLOAD_TAG_INSTALL_SCRIPT=false\n    log_debug \"provided version install script does not support -v flag (>= '$VERIFY_SIGN_FLAG_VERSION'), using current script for installation\"\n  else\n    log_trace \"provided version install script supports -v flag (>= '$VERIFY_SIGN_FLAG_VERSION')\"\n  fi\n\n  # check to see if the cosign binary is installed\n  if is_command \"${COSIGN_BINARY}\"; then\n    log_trace \"${COSIGN_BINARY} binary is installed\"\n  else\n    log_err \"signature verification is requested but ${COSIGN_BINARY} binary is not installed (see https://docs.sigstore.dev/cosign/system_config/installation/ to install it)\"\n    return 1\n  fi\n}\n\nmain() (\n  # parse arguments\n\n  # note: never change default install directory (this must always be backwards compatible)\n  install_dir=${install_dir:-./bin}\n\n  # note: never change the program flags or arguments (this must always be backwards compatible)\n  while getopts \"b:dvh?x\" arg; do\n    case \"$arg\" in\n      b) install_dir=\"$OPTARG\" ;;\n      d)\n        if [ \"$_logp\" = \"$log_info_priority\" ]; then\n          # -d == debug\n          log_set_priority $log_debug_priority\n        else\n          # -dd (or -ddd...) == trace\n          log_set_priority $log_trace_priority\n        fi\n        ;;\n      v) VERIFY_SIGN=true;;\n      h | \\?)\n        cat <<EOF\nDownload and install a released binary for ${OWNER}/${REPO} from the github releases page\n\nUsage: $0 [-v] [-b DIR] [-d] [TAG]\n  -b DIR  the installation directory (defaults to ./bin)\n  -d      turns on debug logging\n  -dd     turns on trace logging\n  -v      verify checksum signature (requires cosign binary to be installed).\n  TAG     the specific release to use (if missing, then the latest will be used)\nEOF\n        exit 0\n      ;;\n      x) set -x ;;\n    esac\n  done\n  shift $((OPTIND - 1))\n\n  set +u\n  tag=$1\n\n  if [ -z \"${tag}\" ]; then\n    log_info \"checking github for the current release tag\"\n    tag=\"\"\n  else\n    log_info \"checking github for release tag='${tag}'\"\n  fi\n  set -u\n\n  if ! tag=$(get_release_tag \"${OWNER}\" \"${REPO}\" \"${tag}\"); then\n      log_err \"unable to find tag='${tag}'\"\n      log_err \"do not specify a version or select a valid version from https://github.com/${OWNER}/${REPO}/releases\"\n      return 1\n  fi\n\n  # run the application\n\n  version=$(tag_to_version \"${tag}\")\n  os=$(uname_os)\n  arch=$(uname_arch)\n  format=$(get_format_name \"${os}\" \"${arch}\" \"tar.gz\")\n  binary=$(get_binary_name \"${os}\" \"${arch}\" \"${PROJECT_NAME}\")\n  download_url=\"${GITHUB_DOWNLOAD_PREFIX}/${tag}\"\n\n  if ! prep_signature_verification \"$version\"; then\n      return 1\n  fi\n\n  # we always use the install.sh script that is associated with the tagged release. Why? the latest install.sh is not\n  # guaranteed to be able to install every version of the application. We use the DOWNLOAD_TAG_INSTALL_SCRIPT env var\n  # to indicate if we should continue processing with the existing script or to download the script from the given tag.\n  if [ \"${DOWNLOAD_TAG_INSTALL_SCRIPT}\" = \"true\" ]; then\n      export DOWNLOAD_TAG_INSTALL_SCRIPT=false\n      log_info \"fetching release script for tag='${tag}'\"\n      if ! install_script=$(http_copy \"${INSTALL_SH_BASE_URL}/${tag}/install.sh\" \"\"); then\n          log_warn \"failed to fetch from ${INSTALL_SH_BASE_URL}, trying fallback URL\"\n          install_script=$(http_copy \"${LEGACY_INSTALL_SH_BASE_URL}/${tag}/install.sh\" \"\")\n      fi\n      echo \"${install_script}\" | sh -s -- ${PROGRAM_ARGS}\n      exit $?\n  fi\n\n  log_info \"using release tag='${tag}' version='${version}' os='${os}' arch='${arch}'\"\n\n  download_dir=$(mktemp -d)\n  trap 'rm -rf -- \"$download_dir\"' EXIT\n\n  log_debug \"downloading files into ${download_dir}\"\n\n  # don't continue if we couldn't install the asset\n  if ! download_and_install_asset \"${download_url}\" \"${download_dir}\" \"${install_dir}\" \"${PROJECT_NAME}\" \"${os}\" \"${arch}\" \"${version}\" \"${format}\" \"${binary}\"; then\n      log_err \"failed to install ${PROJECT_NAME}\"\n      return 1\n  fi\n\n  log_info \"installed ${install_dir}/${binary}\"\n)\n\n# entrypoint\n\nset +u\nif [ -z \"${TEST_INSTALL_SH}\" ]; then\n  set -u\n  main \"$@\"\n  exit $?\nfi\nset -u\n"
  },
  {
    "path": "internal/bus/bus.go",
    "content": "package bus\n\nimport \"github.com/wagoodman/go-partybus\"\n\nvar publisher partybus.Publisher\n\nfunc Set(p partybus.Publisher) {\n\tpublisher = p\n}\n\nfunc Publish(event partybus.Event) {\n\tif publisher != nil {\n\t\tpublisher.Publish(event)\n\t}\n}\n"
  },
  {
    "path": "internal/bus/helpers.go",
    "content": "package bus\n\nimport (\n\t\"github.com/wagoodman/go-partybus\"\n\n\t\"github.com/anchore/clio\"\n\t\"github.com/anchore/grype/grype/event\"\n\t\"github.com/anchore/grype/internal/redact\"\n)\n\nfunc Exit() {\n\tPublish(clio.ExitEvent(false))\n}\n\nfunc ExitWithInterrupt() {\n\tPublish(clio.ExitEvent(true))\n}\n\nfunc Report(report string) {\n\tPublish(partybus.Event{\n\t\tType:  event.CLIReport,\n\t\tValue: redact.Apply(report),\n\t})\n}\n\nfunc Notify(message string) {\n\tPublish(partybus.Event{\n\t\tType:  event.CLINotification,\n\t\tValue: message,\n\t})\n}\n"
  },
  {
    "path": "internal/cvss/metrics.go",
    "content": "package cvss\n\nimport (\n\t\"fmt\"\n\t\"math\"\n\t\"strings\"\n\n\tgocvss20 \"github.com/pandatix/go-cvss/20\"\n\tgocvss30 \"github.com/pandatix/go-cvss/30\"\n\tgocvss31 \"github.com/pandatix/go-cvss/31\"\n\tgocvss40 \"github.com/pandatix/go-cvss/40\"\n\n\t\"github.com/anchore/grype/grype/vulnerability\"\n)\n\nfunc ParseMetricsFromVector(vector string) (*vulnerability.CvssMetrics, error) {\n\tswitch {\n\tcase strings.HasPrefix(vector, \"CVSS:3.0\"):\n\t\tcvss, err := gocvss30.ParseVector(vector)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"unable to parse CVSS v3 vector: %w\", err)\n\t\t}\n\t\tex := roundScore(cvss.Exploitability())\n\t\tim := roundScore(cvss.Impact())\n\t\treturn &vulnerability.CvssMetrics{\n\t\t\tBaseScore:           roundScore(cvss.BaseScore()),\n\t\t\tExploitabilityScore: &ex,\n\t\t\tImpactScore:         &im,\n\t\t}, nil\n\tcase strings.HasPrefix(vector, \"CVSS:3.1\"):\n\t\tcvss, err := gocvss31.ParseVector(vector)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"unable to parse CVSS v3.1 vector: %w\", err)\n\t\t}\n\t\tex := roundScore(cvss.Exploitability())\n\t\tim := roundScore(cvss.Impact())\n\t\treturn &vulnerability.CvssMetrics{\n\t\t\tBaseScore:           roundScore(cvss.BaseScore()),\n\t\t\tExploitabilityScore: &ex,\n\t\t\tImpactScore:         &im,\n\t\t}, nil\n\tcase strings.HasPrefix(vector, \"CVSS:4.0\"):\n\t\tcvss, err := gocvss40.ParseVector(vector)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"unable to parse CVSS v4.0 vector: %w\", err)\n\t\t}\n\t\t// there are no exploitability and impact scores in CVSS v4.0\n\t\treturn &vulnerability.CvssMetrics{\n\t\t\tBaseScore: roundScore(cvss.Score()),\n\t\t}, nil\n\tdefault:\n\t\t// should be CVSS v2.0 or is invalid\n\t\tcvss, err := gocvss20.ParseVector(vector)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"unable to parse CVSS v2 vector: %w\", err)\n\t\t}\n\t\tex := roundScore(cvss.Exploitability())\n\t\tim := roundScore(cvss.Impact())\n\t\treturn &vulnerability.CvssMetrics{\n\t\t\tBaseScore:           roundScore(cvss.BaseScore()),\n\t\t\tExploitabilityScore: &ex,\n\t\t\tImpactScore:         &im,\n\t\t}, nil\n\t}\n}\n\nfunc SeverityFromBaseScore(bs float64) vulnerability.Severity {\n\tswitch {\n\tcase bs >= 10.0:\n\t\treturn vulnerability.UnknownSeverity\n\tcase bs >= 9.0:\n\t\treturn vulnerability.CriticalSeverity\n\tcase bs >= 7.0:\n\t\treturn vulnerability.HighSeverity\n\tcase bs >= 4.0:\n\t\treturn vulnerability.MediumSeverity\n\tcase bs >= 0.1:\n\t\treturn vulnerability.LowSeverity\n\tcase bs > 0:\n\t\treturn vulnerability.NegligibleSeverity\n\t}\n\treturn vulnerability.UnknownSeverity\n}\n\n// roundScore rounds the score to the nearest tenth based on first.org rounding rules\n// see https://www.first.org/cvss/v3.1/specification-document#Appendix-A---Floating-Point-Rounding\nfunc roundScore(score float64) float64 {\n\tintInput := int(math.Round(score * 100000))\n\tif intInput%10000 == 0 {\n\t\treturn float64(intInput) / 100000.0\n\t}\n\treturn (math.Floor(float64(intInput)/10000.0) + 1) / 10.0\n}\n"
  },
  {
    "path": "internal/cvss/metrics_test.go",
    "content": "package cvss\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/grype/grype/vulnerability\"\n)\n\nfunc TestParseMetricsFromVector(t *testing.T) {\n\ttests := []struct {\n\t\tname            string\n\t\tvector          string\n\t\texpectedMetrics *vulnerability.CvssMetrics\n\t\twantErr         require.ErrorAssertionFunc\n\t}{\n\t\t{\n\t\t\tname:   \"valid CVSS 2.0\",\n\t\t\tvector: \"AV:N/AC:L/Au:N/C:P/I:P/A:P\",\n\t\t\texpectedMetrics: &vulnerability.CvssMetrics{\n\t\t\t\tBaseScore:           7.5,\n\t\t\t\tExploitabilityScore: ptr(10.0),\n\t\t\t\tImpactScore:         ptr(6.5),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:   \"valid CVSS 3.0\",\n\t\t\tvector: \"CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H\",\n\t\t\texpectedMetrics: &vulnerability.CvssMetrics{\n\t\t\t\tBaseScore:           9.8,\n\t\t\t\tExploitabilityScore: ptr(3.9),\n\t\t\t\tImpactScore:         ptr(5.9),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:   \"valid CVSS 3.1\",\n\t\t\tvector: \"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H\",\n\t\t\texpectedMetrics: &vulnerability.CvssMetrics{\n\t\t\t\tBaseScore:           9.8,\n\t\t\t\tExploitabilityScore: ptr(3.9),\n\t\t\t\tImpactScore:         ptr(5.9),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:   \"valid CVSS 4.0\",\n\t\t\tvector: \"CVSS:4.0/AV:N/AC:H/AT:P/PR:L/UI:N/VC:N/VI:H/VA:L/SC:L/SI:H/SA:L/MAC:L/MAT:P/MPR:N/S:N/R:A/RE:L/U:Clear\",\n\t\t\texpectedMetrics: &vulnerability.CvssMetrics{\n\t\t\t\tBaseScore: 9.1,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:    \"invalid CVSS 2.0\",\n\t\t\tvector:  \"AV:N/AC:INVALID\",\n\t\t\twantErr: require.Error,\n\t\t},\n\t\t{\n\t\t\tname:    \"invalid CVSS 3.0\",\n\t\t\tvector:  \"CVSS:3.0/AV:INVALID\",\n\t\t\twantErr: require.Error,\n\t\t},\n\t\t{\n\t\t\tname:    \"invalid CVSS 3.1\",\n\t\t\tvector:  \"CVSS:3.1/AV:INVALID\",\n\t\t\twantErr: require.Error,\n\t\t},\n\t\t{\n\t\t\tname:    \"invalid CVSS 4.0\",\n\t\t\tvector:  \"CVSS:4.0/AV:INVALID\",\n\t\t\twantErr: require.Error,\n\t\t},\n\t\t{\n\t\t\tname:    \"empty vector\",\n\t\t\tvector:  \"\",\n\t\t\twantErr: require.Error,\n\t\t},\n\t\t{\n\t\t\tname:    \"malformed vector\",\n\t\t\tvector:  \"INVALID:VECTOR\",\n\t\t\twantErr: require.Error,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif tt.wantErr == nil {\n\t\t\t\ttt.wantErr = require.NoError\n\t\t\t}\n\t\t\tresult, err := ParseMetricsFromVector(tt.vector)\n\t\t\ttt.wantErr(t, err)\n\t\t\tif err != nil {\n\t\t\t\tassert.Nil(t, result)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\trequire.NotNil(t, result)\n\t\t\tassert.Equal(t, tt.expectedMetrics.BaseScore, result.BaseScore, \"given vector: %s\", tt.vector)\n\n\t\t\tif tt.expectedMetrics.ExploitabilityScore != nil {\n\t\t\t\trequire.NotNil(t, result.ExploitabilityScore)\n\t\t\t\tassert.Equal(t, *tt.expectedMetrics.ExploitabilityScore, *result.ExploitabilityScore, \"given vector: %s\", tt.vector)\n\t\t\t}\n\n\t\t\tif tt.expectedMetrics.ImpactScore != nil {\n\t\t\t\trequire.NotNil(t, result.ImpactScore)\n\t\t\t\tassert.Equal(t, *tt.expectedMetrics.ImpactScore, *result.ImpactScore, \"given vector: %s\", tt.vector)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSeverityFromBaseScore(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tscore    float64\n\t\texpected vulnerability.Severity\n\t}{\n\t\t{\n\t\t\tname:     \"unknown severity (exactly 10.0)\",\n\t\t\tscore:    10.0,\n\t\t\texpected: vulnerability.UnknownSeverity,\n\t\t},\n\t\t{\n\t\t\tname:     \"unknown severity (greater than 10.0)\",\n\t\t\tscore:    10.1,\n\t\t\texpected: vulnerability.UnknownSeverity,\n\t\t},\n\t\t{\n\t\t\tname:     \"critical severity (lower bound)\",\n\t\t\tscore:    9.0,\n\t\t\texpected: vulnerability.CriticalSeverity,\n\t\t},\n\t\t{\n\t\t\tname:     \"critical severity (upper bound)\",\n\t\t\tscore:    9.9,\n\t\t\texpected: vulnerability.CriticalSeverity,\n\t\t},\n\t\t{\n\t\t\tname:     \"high severity (lower bound)\",\n\t\t\tscore:    7.0,\n\t\t\texpected: vulnerability.HighSeverity,\n\t\t},\n\t\t{\n\t\t\tname:     \"high severity (upper bound)\",\n\t\t\tscore:    8.9,\n\t\t\texpected: vulnerability.HighSeverity,\n\t\t},\n\t\t{\n\t\t\tname:     \"medium severity (lower bound)\",\n\t\t\tscore:    4.0,\n\t\t\texpected: vulnerability.MediumSeverity,\n\t\t},\n\t\t{\n\t\t\tname:     \"medium severity (upper bound)\",\n\t\t\tscore:    6.9,\n\t\t\texpected: vulnerability.MediumSeverity,\n\t\t},\n\t\t{\n\t\t\tname:     \"low severity (lower bound)\",\n\t\t\tscore:    0.1,\n\t\t\texpected: vulnerability.LowSeverity,\n\t\t},\n\t\t{\n\t\t\tname:     \"low severity (upper bound)\",\n\t\t\tscore:    3.9,\n\t\t\texpected: vulnerability.LowSeverity,\n\t\t},\n\t\t{\n\t\t\tname:     \"negligible severity (between 0 and 0.1)\",\n\t\t\tscore:    0.05,\n\t\t\texpected: vulnerability.NegligibleSeverity,\n\t\t},\n\t\t{\n\t\t\tname:     \"unknown severity (exactly zero)\",\n\t\t\tscore:    0.0,\n\t\t\texpected: vulnerability.UnknownSeverity,\n\t\t},\n\t\t{\n\t\t\tname:     \"unknown severity (negative)\",\n\t\t\tscore:    -1.0,\n\t\t\texpected: vulnerability.UnknownSeverity,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tassert.Equal(t, tt.expected, SeverityFromBaseScore(tt.score))\n\t\t})\n\t}\n}\n\nfunc ptr(f float64) *float64 {\n\treturn &f\n}\n"
  },
  {
    "path": "internal/dbtest/default_vulnerabilities.go",
    "content": "package dbtest\n\nimport (\n\t\"github.com/anchore/grype/grype/version\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n\t\"github.com/anchore/syft/syft/cpe\"\n)\n\nfunc DefaultVulnerabilities() []vulnerability.Vulnerability {\n\treturn []vulnerability.Vulnerability{\n\t\t{\n\t\t\tReference: vulnerability.Reference{\n\t\t\t\tID:        \"CVE-2024-1234\",\n\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t},\n\t\t\tPackageName:       \"asdf\",\n\t\t\tConstraint:        version.MustGetConstraint(\"< 1.4\", version.ApkFormat),\n\t\t\tPackageQualifiers: nil,\n\t\t\tCPEs: []cpe.CPE{\n\t\t\t\tcpe.Must(\"cpe:2.3:*:stuff:asdf:*:*:*:*:*:*:*:*\", cpe.DeclaredSource),\n\t\t\t},\n\t\t\tFix: vulnerability.Fix{\n\t\t\t\tVersions: []string{\"1.4.0\"},\n\t\t\t\tState:    vulnerability.FixStateFixed,\n\t\t\t},\n\t\t\tAdvisories:             []vulnerability.Advisory{},\n\t\t\tRelatedVulnerabilities: nil,\n\t\t},\n\t}\n}\n"
  },
  {
    "path": "internal/dbtest/server.go",
    "content": "package dbtest\n\nimport (\n\t\"archive/tar\"\n\t\"bytes\"\n\t\"crypto/sha256\"\n\t\"database/sql\"\n\t\"encoding/json\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/mholt/archives\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/grype/grype/db/v5/namespace\"\n\tdistroNs \"github.com/anchore/grype/grype/db/v5/namespace/distro\"\n\t\"github.com/anchore/grype/grype/db/v5/namespace/language\"\n\tv6 \"github.com/anchore/grype/grype/db/v6\"\n\t\"github.com/anchore/grype/grype/db/v6/distribution\"\n\t\"github.com/anchore/grype/grype/version\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n\t\"github.com/anchore/grype/internal/file\"\n\t\"github.com/anchore/grype/internal/schemaver\"\n)\n\ntype ServerBuilder struct {\n\tt               *testing.T\n\tdbContents      []byte\n\tDBFormat        string\n\tDBBuildTime     time.Time\n\tDBVersion       schemaver.SchemaVer\n\tVulnerabilities []vulnerability.Vulnerability\n\tLatestDoc       *distribution.LatestDocument\n\tServerSubdir    string\n\tLatestDocFile   string\n\tRequestHandler  http.HandlerFunc\n}\n\nfunc (s *ServerBuilder) SetDBBuilt(t time.Time) *ServerBuilder {\n\ts.DBBuildTime = t\n\treturn s\n}\n\nfunc (s *ServerBuilder) SetDBVersion(major, minor, patch int) *ServerBuilder {\n\ts.DBVersion = schemaver.New(major, minor, patch)\n\treturn s\n}\n\nfunc (s *ServerBuilder) WithHandler(handler http.HandlerFunc) *ServerBuilder {\n\ts.RequestHandler = handler\n\treturn s\n}\n\n// NewServer creates a new test db server building a single database from the provided\n// vulnerabilities, along with a latest.json pointing to it, optionally with any properties\n// specified in the provided latest parameter\nfunc NewServer(t *testing.T) *ServerBuilder {\n\tt.Helper()\n\treturn &ServerBuilder{\n\t\tt:               t,\n\t\tDBFormat:        \"tar.zst\",\n\t\tDBBuildTime:     time.Now(),\n\t\tDBVersion:       schemaver.New(6, 0, 0),\n\t\tServerSubdir:    \"databases/v6\",\n\t\tLatestDocFile:   \"latest.json\",\n\t\tVulnerabilities: DefaultVulnerabilities(),\n\t\tLatestDoc: &distribution.LatestDocument{\n\t\t\tStatus: \"active\",\n\t\t\tArchive: distribution.Archive{\n\t\t\t\tDescription: v6.Description{},\n\t\t\t},\n\t\t},\n\t}\n}\n\n// Start starts builds a database and starts a server with the current settings\n// if you need to rebuild a DB or modify the behavior, you can either set\n// a custom RequestHandler func or modify the settings and call Start() again.\n// Returns a URL to the latest.json file, e.g. http://127.0.0.1:5678/v6/latest.json\nfunc (s *ServerBuilder) Start() (url string) {\n\ts.t.Helper()\n\n\tserverSubdir := s.ServerSubdir\n\tif serverSubdir != \"\" {\n\t\tserverSubdir += \"/\"\n\t}\n\n\tcontents := s.buildDB()\n\ts.dbContents = pack(s.t, s.DBFormat, contents)\n\n\thandler := http.NewServeMux()\n\thandler.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) {\n\t\tif s.RequestHandler != nil {\n\t\t\trw := wrappedWriter{writer: w}\n\t\t\ts.RequestHandler(&rw, r)\n\t\t\tif rw.handled {\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\n\t\tdbName := \"vulnerability-db_v\" + s.DBVersion.String()\n\t\tarchivePath := dbName + \".\" + s.DBFormat\n\t\tswitch r.RequestURI[1:] {\n\t\tcase serverSubdir + s.LatestDocFile:\n\t\t\tlatestDoc := *s.LatestDoc\n\t\t\tlatestDoc.Built.Time = s.DBBuildTime\n\t\t\tlatestDoc.SchemaVersion = s.DBVersion\n\t\t\tlatestDoc.Built.Time = s.DBBuildTime\n\t\t\tlatestDoc.Path = archivePath\n\t\t\tlatestDoc.Checksum = sha(s.dbContents)\n\t\t\tw.WriteHeader(http.StatusOK)\n\t\t\t_ = json.NewEncoder(w).Encode(latestDoc)\n\t\tcase serverSubdir + archivePath:\n\t\t\tw.WriteHeader(http.StatusOK)\n\t\t\t_, _ = w.Write(s.dbContents)\n\t\tdefault:\n\t\t\thttp.NotFound(w, r)\n\t\t\treturn\n\t\t}\n\t})\n\tmockSrv := httptest.NewServer(handler)\n\ts.t.Cleanup(func() {\n\t\tmockSrv.Close()\n\t})\n\treturn mockSrv.URL + \"/\" + serverSubdir + s.LatestDocFile\n}\n\nfunc sha(contents []byte) string {\n\tdigest, err := file.HashReader(bytes.NewReader(contents), sha256.New())\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn \"sha256:\" + digest\n}\n\n//nolint:funlen\nfunc (s *ServerBuilder) buildDB() []byte {\n\ts.t.Helper()\n\n\ttmp := s.t.TempDir()\n\tw, err := v6.NewWriter(v6.Config{\n\t\tDBDirPath: tmp,\n\t})\n\trequire.NoError(s.t, err)\n\n\taWeekAgo := time.Now().Add(-7 * 24 * time.Hour)\n\ttwoWeeksAgo := time.Now().Add(-14 * 24 * time.Hour)\n\n\tfor _, v := range s.Vulnerabilities {\n\t\tprov := &v6.Provider{\n\t\t\tID:           \"nvd\",\n\t\t\tVersion:      \"1\",\n\t\t\tDateCaptured: &s.DBBuildTime,\n\t\t}\n\n\t\tvar operatingSystem *v6.OperatingSystem\n\t\tpackageType := \"\"\n\n\t\tns, err := namespace.FromString(v.Namespace)\n\t\trequire.NoError(s.t, err)\n\n\t\td, _ := ns.(*distroNs.Namespace)\n\t\tif d != nil {\n\t\t\tpackageType = string(d.DistroType())\n\t\t\toperatingSystem = &v6.OperatingSystem{\n\t\t\t\tName:         d.Provider(),\n\t\t\t\tMajorVersion: strings.Split(d.Version(), \".\")[0],\n\t\t\t}\n\t\t\tprov.ID = d.Provider()\n\t\t}\n\t\tlang, _ := ns.(*language.Namespace)\n\t\tif lang != nil {\n\t\t\tpackageType = string(lang.Language())\n\t\t}\n\n\t\tprov.Processor = prov.ID + \"-processor\"\n\t\tprov.InputDigest = sha([]byte(prov.ID))\n\n\t\tvuln := &v6.VulnerabilityHandle{\n\t\t\tID:            0,\n\t\t\tName:          v.ID,\n\t\t\tStatus:        \"\",\n\t\t\tPublishedDate: &twoWeeksAgo,\n\t\t\tModifiedDate:  &aWeekAgo,\n\t\t\tWithdrawnDate: nil,\n\t\t\tProviderID:    prov.ID,\n\t\t\tProvider:      prov,\n\t\t\tBlobID:        0,\n\t\t\tBlobValue: &v6.VulnerabilityBlob{\n\t\t\t\tID:          v.ID,\n\t\t\t\tAssigners:   []string{v.ID + \"-assigner-1\", v.ID + \"-assigner-2\"},\n\t\t\t\tDescription: v.ID + \"-description\",\n\t\t\t\tReferences: []v6.Reference{\n\t\t\t\t\t{\n\t\t\t\t\t\tURL:  \"http://somewhere/\" + v.ID,\n\t\t\t\t\t\tTags: []string{v.ID + \"-tag-1\", v.ID + \"-tag-2\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t//Aliases: []string{\"GHSA-\" + v.ID},\n\t\t\t\tSeverities: []v6.Severity{\n\t\t\t\t\t{\n\t\t\t\t\t\tScheme: v6.SeveritySchemeCVSS,\n\t\t\t\t\t\tValue:  \"high\",\n\t\t\t\t\t\tSource: \"\",\n\t\t\t\t\t\tRank:   0,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\n\t\terr = w.AddVulnerabilities(vuln)\n\t\trequire.NoError(s.t, err)\n\n\t\tvar cpes []v6.Cpe\n\t\tfor _, cp := range v.CPEs {\n\t\t\trequire.NoError(s.t, err)\n\t\t\tcpes = append(cpes, v6.Cpe{\n\t\t\t\tPart:            cp.Attributes.Part,\n\t\t\t\tVendor:          cp.Attributes.Vendor,\n\t\t\t\tProduct:         cp.Attributes.Product,\n\t\t\t\tEdition:         cp.Attributes.Edition,\n\t\t\t\tLanguage:        cp.Attributes.Language,\n\t\t\t\tSoftwareEdition: cp.Attributes.SWEdition,\n\t\t\t\tTargetHardware:  cp.Attributes.TargetHW,\n\t\t\t\tTargetSoftware:  cp.Attributes.TargetSW,\n\t\t\t\tOther:           cp.Attributes.Other,\n\t\t\t})\n\t\t}\n\n\t\tpkg := &v6.Package{\n\t\t\tID:        0,\n\t\t\tEcosystem: packageType,\n\t\t\tName:      v.PackageName,\n\t\t}\n\n\t\tif prov.ID != \"nvd\" {\n\t\t\tpkg.CPEs = cpes\n\t\t} else {\n\t\t\tfor _, c := range cpes {\n\t\t\t\tac := &v6.AffectedCPEHandle{\n\t\t\t\t\tVulnerability: vuln,\n\t\t\t\t\tCPE:           &c,\n\t\t\t\t\tBlobValue: &v6.PackageBlob{\n\t\t\t\t\t\tRanges: []v6.Range{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tVersion: toAffectedVersion(v.Constraint),\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t}\n\n\t\t\t\terr = w.AddAffectedCPEs(ac)\n\t\t\t\trequire.NoError(s.t, err)\n\t\t\t}\n\t\t}\n\n\t\tap := &v6.AffectedPackageHandle{\n\t\t\tID:                0,\n\t\t\tVulnerabilityID:   0,\n\t\t\tVulnerability:     vuln,\n\t\t\tOperatingSystemID: nil,\n\t\t\tOperatingSystem:   operatingSystem,\n\t\t\tPackageID:         0,\n\t\t\tPackage:           pkg,\n\t\t\tBlobID:            0,\n\t\t\tBlobValue: &v6.PackageBlob{\n\t\t\t\tCVEs:       nil,\n\t\t\t\tQualifiers: nil,\n\t\t\t\tRanges: []v6.Range{\n\t\t\t\t\t{\n\t\t\t\t\t\tFix:     nil,\n\t\t\t\t\t\tVersion: toAffectedVersion(v.Constraint),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\n\t\terr = w.AddAffectedPackages(ap)\n\t\trequire.NoError(s.t, err)\n\t}\n\n\terr = w.SetDBMetadata()\n\trequire.NoError(s.t, err)\n\n\terr = w.Close()\n\trequire.NoError(s.t, err)\n\n\tdbFile := filepath.Join(tmp, \"vulnerability.db\")\n\n\tdb, err := sql.Open(\"sqlite\", dbFile)\n\trequire.NoError(s.t, err)\n\n\tmodel := s.DBVersion.Model\n\trevision := s.DBVersion.Revision\n\taddition := s.DBVersion.Addition\n\t_, err = db.Exec(\"update db_metadata set build_timestamp = ?, model = ?, revision = ?, addition = ?\",\n\t\ts.DBBuildTime, model, revision, addition)\n\trequire.NoError(s.t, err)\n\n\terr = db.Close()\n\trequire.NoError(s.t, err)\n\n\tcontents, err := os.ReadFile(dbFile)\n\trequire.NoError(s.t, err)\n\n\treturn contents\n}\n\nfunc pack(t *testing.T, typ string, contents []byte) []byte {\n\tif typ == \"tar.zst\" {\n\t\tnow := time.Now()\n\t\ttarContents := bytes.Buffer{}\n\t\ttw := tar.NewWriter(&tarContents)\n\t\terr := tw.WriteHeader(&tar.Header{\n\t\t\tTypeflag: tar.TypeReg,\n\t\t\tName:     \"vulnerability.db\",\n\t\t\tSize:     int64(len(contents)),\n\t\t\tMode:     0777,\n\t\t\tModTime:  now,\n\t\t})\n\t\trequire.NoError(t, err)\n\t\t_, err = tw.Write(contents)\n\t\trequire.NoError(t, err)\n\t\terr = tw.Close()\n\t\trequire.NoError(t, err)\n\n\t\ttarZstd := bytes.Buffer{}\n\t\tcompressor, err := archives.Zstd{}.OpenWriter(&tarZstd)\n\t\trequire.NoError(t, err)\n\n\t\t_, err = io.Copy(compressor, &tarContents)\n\t\trequire.NoError(t, err)\n\t\terr = compressor.Close()\n\t\trequire.NoError(t, err)\n\n\t\treturn tarZstd.Bytes()\n\t}\n\n\tpanic(\"unsupported type: \" + typ)\n}\n\nfunc toAffectedVersion(c version.Constraint) v6.Version {\n\tparts := strings.SplitN(c.String(), \"(\", 2)\n\tif len(parts) < 2 {\n\t\treturn v6.Version{\n\t\t\tConstraint: strings.TrimSpace(parts[0]),\n\t\t}\n\t}\n\treturn v6.Version{\n\t\tType:       strings.TrimSpace(strings.Split(parts[1], \")\")[0]),\n\t\tConstraint: strings.TrimSpace(parts[0]),\n\t}\n}\n\ntype wrappedWriter struct {\n\twriter  http.ResponseWriter\n\thandled bool\n}\n\nfunc (w *wrappedWriter) Header() http.Header {\n\tw.handled = true\n\treturn w.writer.Header()\n}\n\nfunc (w *wrappedWriter) Write(contents []byte) (int, error) {\n\tw.handled = true\n\treturn w.writer.Write(contents)\n}\n\nfunc (w *wrappedWriter) WriteHeader(statusCode int) {\n\tw.handled = true\n\tw.writer.WriteHeader(statusCode)\n}\n"
  },
  {
    "path": "internal/dbtest/server_test.go",
    "content": "package dbtest_test\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"io\"\n\t\"net/http\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/grype/grype/db/v6/distribution\"\n\t\"github.com/anchore/grype/internal/dbtest\"\n)\n\nfunc Test_NewServer(t *testing.T) {\n\ttests := []struct {\n\t\tname         string\n\t\tuseDefault   bool\n\t\tserverSubdir string\n\t}{\n\t\t{\n\t\t\tname:       \"default path\",\n\t\t\tuseDefault: true,\n\t\t},\n\t\t{\n\t\t\tname:         \"v6 path\",\n\t\t\tserverSubdir: \"v6\",\n\t\t},\n\t\t{\n\t\t\tname:         \"root path\",\n\t\t\tserverSubdir: \"\",\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tsrv := dbtest.NewServer(t).SetDBBuilt(time.Now().Add(-24 * time.Hour))\n\t\t\tif !test.useDefault {\n\t\t\t\tsrv.ServerSubdir = test.serverSubdir\n\t\t\t}\n\n\t\t\turl := srv.Start() // one day ago\n\t\t\tparts := strings.Split(url, \"/\")\n\t\t\turlPrefix := strings.Join(parts[:len(parts)-1], \"/\")\n\n\t\t\tget := func(url string) (status int, contents []byte, readError error) {\n\t\t\t\tresp, err := http.Get(url)\n\t\t\t\tif resp.Body != nil {\n\t\t\t\t\tdefer func() { require.NoError(t, resp.Body.Close()) }()\n\t\t\t\t}\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tbuf := bytes.Buffer{}\n\t\t\t\t_, err = io.Copy(&buf, resp.Body)\n\t\t\t\treturn resp.StatusCode, buf.Bytes(), err\n\t\t\t}\n\n\t\t\tstatus, content, err := get(urlPrefix + \"/latest.json\")\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, http.StatusOK, status)\n\n\t\t\t// should have a latest document at the given URL\n\t\t\tvar latest distribution.LatestDocument\n\t\t\trequire.NoError(t, json.Unmarshal(content, &latest))\n\n\t\t\trelativeDb := latest.Archive.Path\n\t\t\trequire.NotEmpty(t, relativeDb)\n\n\t\t\t// should have a db at the relative url in the latest doc\n\t\t\tstatus, content, err = get(urlPrefix + \"/\" + relativeDb)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, http.StatusOK, status)\n\t\t\trequire.NotEmpty(t, content)\n\n\t\t\t// should have 404 at wrong URL\n\t\t\tstatus, _, _ = get(urlPrefix + \"/asdf\")\n\t\t\trequire.Equal(t, http.StatusNotFound, status)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/file/copy.go",
    "content": "package file\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"path\"\n\n\t\"github.com/spf13/afero\"\n)\n\nfunc CopyDir(fs afero.Fs, src string, dst string) error {\n\tvar err error\n\tvar fds []os.FileInfo // <-- afero.ReadDir returns []os.FileInfo\n\tvar srcinfo os.FileInfo\n\n\tif srcinfo, err = fs.Stat(src); err != nil {\n\t\treturn err\n\t}\n\n\tif err = fs.MkdirAll(dst, srcinfo.Mode()); err != nil {\n\t\treturn err\n\t}\n\n\tif fds, err = afero.ReadDir(fs, src); err != nil {\n\t\treturn err\n\t}\n\tfor _, fd := range fds {\n\t\tsrcPath := path.Join(src, fd.Name())\n\t\tdstPath := path.Join(dst, fd.Name())\n\n\t\tif fd.IsDir() {\n\t\t\tif err = CopyDir(fs, srcPath, dstPath); err != nil {\n\t\t\t\treturn fmt.Errorf(\"could not copy dir (%s -> %s): %w\", srcPath, dstPath, err)\n\t\t\t}\n\t\t} else {\n\t\t\tif err = CopyFile(fs, srcPath, dstPath); err != nil {\n\t\t\t\treturn fmt.Errorf(\"could not copy file (%s -> %s): %w\", srcPath, dstPath, err)\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc CopyFile(fs afero.Fs, src, dst string) error {\n\tvar err error\n\tvar srcFd afero.File\n\tvar dstFd afero.File\n\tvar srcinfo os.FileInfo\n\n\tif srcFd, err = fs.Open(src); err != nil {\n\t\treturn err\n\t}\n\tdefer srcFd.Close()\n\n\tif dstFd, err = fs.Create(dst); err != nil {\n\t\treturn err\n\t}\n\tdefer dstFd.Close()\n\n\tif _, err = io.Copy(dstFd, srcFd); err != nil {\n\t\treturn err\n\t}\n\tif srcinfo, err = fs.Stat(src); err != nil {\n\t\treturn err\n\t}\n\treturn fs.Chmod(dst, srcinfo.Mode())\n}\n"
  },
  {
    "path": "internal/file/exists.go",
    "content": "package file\n\nimport (\n\t\"os\"\n\n\t\"github.com/spf13/afero\"\n)\n\nfunc Exists(fs afero.Fs, path string) (bool, error) {\n\tinfo, err := fs.Stat(path)\n\tif os.IsNotExist(err) {\n\t\treturn false, nil\n\t} else if err != nil {\n\t\treturn false, err\n\t}\n\n\treturn !info.IsDir(), nil\n}\n"
  },
  {
    "path": "internal/file/getter.go",
    "content": "package file\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\n\t\"github.com/hashicorp/go-getter\"\n\t\"github.com/hashicorp/go-getter/helper/url\"\n\t\"github.com/spf13/afero\"\n\t\"github.com/wagoodman/go-progress\"\n\n\t\"github.com/anchore/clio\"\n\t\"github.com/anchore/grype/internal/stringutil\"\n\t\"github.com/anchore/stereoscope/pkg/file\"\n)\n\nvar (\n\tarchiveExtensions   = getterDecompressorNames()\n\tErrNonArchiveSource = fmt.Errorf(\"non-archive sources are not supported for directory destinations\")\n)\n\ntype Getter interface {\n\t// GetFile downloads the give URL into the given path. The URL must reference a single file.\n\tGetFile(dst, src string, monitor ...*progress.Manual) error\n\n\t// GetToDir downloads the resource found at the `src` URL into the given `dst` directory.\n\t// The directory must already exist, and the remote resource MUST BE AN ARCHIVE (e.g. `.tar.gz`).\n\tGetToDir(dst, src string, monitor ...*progress.Manual) error\n}\n\ntype HashiGoGetter struct {\n\thttpGetter getter.HttpGetter\n}\n\n// NewGetter creates and returns a new Getter. Providing an http.Client is optional. If one is provided,\n// it will be used for all HTTP(S) getting; otherwise, go-getter's default getters will be used.\nfunc NewGetter(id clio.Identification, httpClient *http.Client) *HashiGoGetter {\n\treturn &HashiGoGetter{\n\t\thttpGetter: getter.HttpGetter{\n\t\t\tClient: httpClient,\n\t\t\tHeader: http.Header{\n\t\t\t\t\"User-Agent\": []string{fmt.Sprintf(\"%v %v\", id.Name, id.Version)},\n\t\t\t},\n\t\t},\n\t}\n}\n\nfunc (g HashiGoGetter) GetFile(dst, src string, monitors ...*progress.Manual) error {\n\tif len(monitors) > 1 {\n\t\treturn fmt.Errorf(\"multiple monitors provided, which is not allowed\")\n\t}\n\n\treturn getterClient(dst, src, false, g.httpGetter, monitors).Get()\n}\n\nfunc (g HashiGoGetter) GetToDir(dst, src string, monitors ...*progress.Manual) error {\n\t// though there are multiple getters, only the http/https getter requires extra validation\n\tif err := validateHTTPSource(src); err != nil {\n\t\treturn err\n\t}\n\tif len(monitors) > 1 {\n\t\treturn fmt.Errorf(\"multiple monitors provided, which is not allowed\")\n\t}\n\n\treturn getterClient(dst, src, true, g.httpGetter, monitors).Get()\n}\n\nfunc validateHTTPSource(src string) error {\n\t// we are ignoring any sources that are not destined to use the http getter object\n\tif !stringutil.HasAnyOfPrefixes(src, \"http://\", \"https://\") {\n\t\treturn nil\n\t}\n\n\tu, err := url.Parse(src)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"bad URL provided %q: %w\", src, err)\n\t}\n\t// only allow for sources with archive extensions\n\tif !stringutil.HasAnyOfSuffixes(u.Path, archiveExtensions...) {\n\t\treturn ErrNonArchiveSource\n\t}\n\treturn nil\n}\n\nfunc getterClient(dst, src string, dir bool, httpGetter getter.HttpGetter, monitors []*progress.Manual) *getter.Client {\n\tclient := &getter.Client{\n\t\tSrc: src,\n\t\tDst: dst,\n\t\tDir: dir,\n\t\tGetters: map[string]getter.Getter{\n\t\t\t\"http\":  &httpGetter,\n\t\t\t\"https\": &httpGetter,\n\t\t\t// note: these are the default getters from https://github.com/hashicorp/go-getter/blob/v1.5.9/get.go#L68-L74\n\t\t\t// it is possible that other implementations need to account for custom httpclient injection, however,\n\t\t\t// that has not been accounted for at this time.\n\t\t\t\"file\": new(getter.FileGetter),\n\t\t\t\"git\":  new(getter.GitGetter),\n\t\t\t\"gcs\":  new(getter.GCSGetter),\n\t\t\t\"hg\":   new(getter.HgGetter),\n\t\t\t\"s3\":   new(getter.S3Getter),\n\t\t},\n\t\tOptions: mapToGetterClientOptions(monitors),\n\t}\n\n\treturn client\n}\n\nfunc withProgress(monitor *progress.Manual) func(client *getter.Client) error {\n\treturn getter.WithProgress(\n\t\t&progressAdapter{monitor: monitor},\n\t)\n}\n\nfunc mapToGetterClientOptions(monitors []*progress.Manual) []getter.ClientOption {\n\tvar result []getter.ClientOption\n\n\tfor _, monitor := range monitors {\n\t\tresult = append(result, withProgress(monitor))\n\t}\n\n\t// derived from https://github.com/hashicorp/go-getter/blob/v2.2.3/decompress.go#L23-L63\n\tfileSizeLimit := int64(5 * file.GB)\n\n\tdec := getter.LimitedDecompressors(0, fileSizeLimit)\n\tfs := afero.NewOsFs()\n\txzd := &xzDecompressor{\n\t\tFileSizeLimit: fileSizeLimit,\n\t\tFs:            fs,\n\t}\n\ttxzd := &tarXzDecompressor{\n\t\tFilesLimit:    0,\n\t\tFileSizeLimit: fileSizeLimit,\n\t\tFs:            fs,\n\t}\n\n\tdec[\"xz\"] = xzd\n\tdec[\"tar.xz\"] = txzd\n\tdec[\"txz\"] = txzd\n\n\tresult = append(result, getter.WithDecompressors(dec))\n\n\treturn result\n}\n\ntype readCloser struct {\n\tprogress.Reader\n}\n\nfunc (c *readCloser) Close() error { return nil }\n\ntype progressAdapter struct {\n\tmonitor *progress.Manual\n}\n\nfunc (a *progressAdapter) TrackProgress(_ string, currentSize, totalSize int64, stream io.ReadCloser) io.ReadCloser {\n\ta.monitor.Set(currentSize)\n\ta.monitor.SetTotal(totalSize)\n\treturn &readCloser{\n\t\tReader: *progress.NewProxyReader(stream, a.monitor),\n\t}\n}\n\nfunc getterDecompressorNames() (names []string) {\n\tfor name := range getter.Decompressors {\n\t\tnames = append(names, name)\n\t}\n\treturn names\n}\n"
  },
  {
    "path": "internal/file/getter_test.go",
    "content": "package file\n\nimport (\n\t\"archive/tar\"\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/x509\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"path\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\n\t\"github.com/anchore/clio\"\n)\n\nfunc TestGetter_GetFile(t *testing.T) {\n\ttestCases := []struct {\n\t\tname          string\n\t\tprepareClient func(*http.Client)\n\t\tassert        assert.ErrorAssertionFunc\n\t}{\n\t\t{\n\t\t\tname:   \"client trusts server's CA\",\n\t\t\tassert: assert.NoError,\n\t\t},\n\t\t{\n\t\t\tname:          \"client doesn't trust server's CA\",\n\t\t\tprepareClient: removeTrustedCAs,\n\t\t\tassert:        assertUnknownAuthorityError,\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\trequestPath := \"/foo\"\n\n\t\t\tserver := newTestServer(t, withResponseForPath(t, requestPath, testFileContent))\n\t\t\tt.Cleanup(server.Close)\n\n\t\t\thttpClient := getClient(t, server)\n\t\t\tif tc.prepareClient != nil {\n\t\t\t\ttc.prepareClient(httpClient)\n\t\t\t}\n\n\t\t\tgetter := NewGetter(testID, httpClient)\n\t\t\trequestURL := createRequestURL(t, server, requestPath)\n\n\t\t\ttempDir := t.TempDir()\n\t\t\ttempFile := path.Join(tempDir, \"some-destination-file\")\n\n\t\t\terr := getter.GetFile(tempFile, requestURL)\n\t\t\ttc.assert(t, err)\n\t\t})\n\t}\n}\n\nfunc TestGetter_GetToDir_FilterNonArchivesWired(t *testing.T) {\n\ttestCases := []struct {\n\t\tname   string\n\t\tsource string\n\t\tassert assert.ErrorAssertionFunc\n\t}{\n\t\t{\n\t\t\tname:   \"error out on non-archive sources\",\n\t\t\tsource: \"http://localhost/something.txt\",\n\t\t\tassert: assertErrNonArchiveSource,\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\ttest.assert(t, NewGetter(testID, nil).GetToDir(t.TempDir(), test.source))\n\t\t})\n\t}\n}\n\nfunc TestGetter_validateHttpSource(t *testing.T) {\n\ttestCases := []struct {\n\t\tname   string\n\t\tsource string\n\t\tassert assert.ErrorAssertionFunc\n\t}{\n\t\t{\n\t\t\tname:   \"error out on non-archive sources\",\n\t\t\tsource: \"http://localhost/something.txt\",\n\t\t\tassert: assertErrNonArchiveSource,\n\t\t},\n\t\t{\n\t\t\tname:   \"filter out non-archive sources with get param\",\n\t\t\tsource: \"https://localhost/vulnerability-db_v3_2021-11-21T08:15:44Z.txt?checksum=sha256%3Ac402d01fa909a3fa85a5c6733ef27a3a51a9105b6c62b9152adbd24c08358911\",\n\t\t\tassert: assertErrNonArchiveSource,\n\t\t},\n\t\t{\n\t\t\tname:   \"ignore non http-https input\",\n\t\t\tsource: \"s3://bucket/something.txt\",\n\t\t\tassert: assert.NoError,\n\t\t},\n\t}\n\n\tfor _, test := range testCases {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\ttest.assert(t, validateHTTPSource(test.source))\n\t\t})\n\t}\n}\n\nfunc TestGetter_GetToDir_CertConcerns(t *testing.T) {\n\ttestCases := []struct {\n\t\tname          string\n\t\tprepareClient func(*http.Client)\n\t\tassert        assert.ErrorAssertionFunc\n\t}{\n\n\t\t{\n\t\t\tname:   \"client trusts server's CA\",\n\t\t\tassert: assert.NoError,\n\t\t},\n\t\t{\n\t\t\tname:          \"client doesn't trust server's CA\",\n\t\t\tprepareClient: removeTrustedCAs,\n\t\t\tassert:        assertUnknownAuthorityError,\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\trequestPath := \"/foo.tar\"\n\t\t\ttarball := createTarball(\"foo\", testFileContent)\n\n\t\t\tserver := newTestServer(t, withResponseForPath(t, requestPath, tarball))\n\t\t\tt.Cleanup(server.Close)\n\n\t\t\thttpClient := getClient(t, server)\n\t\t\tif tc.prepareClient != nil {\n\t\t\t\ttc.prepareClient(httpClient)\n\t\t\t}\n\n\t\t\tgetter := NewGetter(testID, httpClient)\n\t\t\trequestURL := createRequestURL(t, server, requestPath)\n\n\t\t\ttempDir := t.TempDir()\n\n\t\t\terr := getter.GetToDir(tempDir, requestURL)\n\t\t\ttc.assert(t, err)\n\t\t})\n\t}\n}\n\nfunc assertUnknownAuthorityError(t assert.TestingT, err error, _ ...interface{}) bool {\n\treturn assert.ErrorAs(t, err, &x509.UnknownAuthorityError{})\n}\n\nfunc assertErrNonArchiveSource(t assert.TestingT, err error, _ ...interface{}) bool {\n\treturn assert.ErrorIs(t, err, ErrNonArchiveSource)\n}\n\nfunc removeTrustedCAs(client *http.Client) {\n\tclient.Transport.(*http.Transport).TLSClientConfig.RootCAs = x509.NewCertPool()\n}\n\n// createTarball makes a single-file tarball and returns it as a byte slice.\nfunc createTarball(filename string, content []byte) []byte {\n\ttarBuffer := new(bytes.Buffer)\n\ttarWriter := tar.NewWriter(tarBuffer)\n\ttarWriter.WriteHeader(&tar.Header{\n\t\tName: filename,\n\t\tSize: int64(len(content)),\n\t\tMode: 0600,\n\t})\n\ttarWriter.Write(content)\n\ttarWriter.Close()\n\n\treturn tarBuffer.Bytes()\n}\n\ntype muxOption func(mux *http.ServeMux)\n\nfunc withResponseForPath(t *testing.T, path string, response []byte) muxOption {\n\tt.Helper()\n\n\treturn func(mux *http.ServeMux) {\n\t\tmux.HandleFunc(path, func(w http.ResponseWriter, req *http.Request) {\n\t\t\tt.Logf(\"server handling request: %s %s\", req.Method, req.URL)\n\n\t\t\t_, err := w.Write(response)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nvar testID = clio.Identification{\n\tName:    \"test-app\",\n\tVersion: \"v0.5.3\",\n}\n\nfunc newTestServer(t *testing.T, muxOptions ...muxOption) *httptest.Server {\n\tt.Helper()\n\n\tmux := http.NewServeMux()\n\tfor _, option := range muxOptions {\n\t\toption(mux)\n\t}\n\n\tserver := httptest.NewTLSServer(mux)\n\tt.Logf(\"new TLS server listening at %s\", getHost(t, server))\n\n\treturn server\n}\n\nfunc createRequestURL(t *testing.T, server *httptest.Server, path string) string {\n\tt.Helper()\n\n\t// TODO: Figure out how to get this value from the server without hardcoding it here\n\tconst testServerCertificateName = \"example.com\"\n\n\tserverURL, err := url.Parse(server.URL)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\t// Set URL hostname to value from TLS certificate\n\tserverURL.Host = fmt.Sprintf(\"%s:%s\", testServerCertificateName, serverURL.Port())\n\n\tserverURL.Path = path\n\n\treturn serverURL.String()\n}\n\n// getClient returns an http.Client that can be used to contact the test TLS server.\nfunc getClient(t *testing.T, server *httptest.Server) *http.Client {\n\tt.Helper()\n\n\thttpClient := server.Client()\n\ttransport := httpClient.Transport.(*http.Transport)\n\n\tserverHost := getHost(t, server)\n\n\ttransport.DialContext = func(_ context.Context, _, addr string) (net.Conn, error) {\n\t\tt.Logf(\"client dialing %q for host %q\", serverHost, addr)\n\n\t\t// Ensure the client dials our test server\n\t\treturn net.Dial(\"tcp\", serverHost)\n\t}\n\n\treturn httpClient\n}\n\n// getHost extracts the host value from a server URL string.\n// e.g. given a server with URL \"http://1.2.3.4:5000/foo\", getHost returns \"1.2.3.4:5000\"\nfunc getHost(t *testing.T, server *httptest.Server) string {\n\tt.Helper()\n\n\tu, err := url.Parse(server.URL)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\treturn u.Hostname() + \":\" + u.Port()\n}\n\nvar testFileContent = []byte(\"This is the content of a test file!\\n\")\n"
  },
  {
    "path": "internal/file/hasher.go",
    "content": "package file\n\nimport (\n\t\"crypto/sha256\"\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"hash\"\n\t\"io\"\n\t\"strings\"\n\n\t\"github.com/OneOfOne/xxhash\"\n\t\"github.com/spf13/afero\"\n)\n\nfunc ValidateByHash(fs afero.Fs, path, hashStr string) (bool, string, error) {\n\tvar hasher hash.Hash\n\tvar hashFn string\n\tswitch {\n\tcase strings.HasPrefix(hashStr, \"sha256:\"):\n\t\thashFn = \"sha256\"\n\t\thasher = sha256.New()\n\tcase strings.HasPrefix(hashStr, \"xxh64:\"):\n\t\thashFn = \"xxh64\"\n\t\thasher = xxhash.New64()\n\tdefault:\n\t\treturn false, \"\", fmt.Errorf(\"hasher not supported or specified (given: %s)\", hashStr)\n\t}\n\n\thashNoPrefix := strings.Split(hashStr, \":\")[1]\n\n\tactualHash, err := HashFile(fs, path, hasher)\n\tif err != nil {\n\t\treturn false, \"\", err\n\t}\n\n\treturn actualHash == hashNoPrefix, hashFn + \":\" + actualHash, nil\n}\n\nfunc HashFile(fs afero.Fs, path string, hasher hash.Hash) (string, error) {\n\tf, err := fs.Open(path)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to open file '%s': %w\", path, err)\n\t}\n\tdefer f.Close()\n\n\treturn HashReader(f, hasher)\n}\n\nfunc HashReader(reader io.Reader, hasher hash.Hash) (string, error) {\n\tif _, err := io.Copy(hasher, reader); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to hash reader: %w\", err)\n\t}\n\n\treturn hex.EncodeToString(hasher.Sum(nil)), nil\n}\n"
  },
  {
    "path": "internal/file/hasher_test.go",
    "content": "package file\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/spf13/afero\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestValidateByHash(t *testing.T) {\n\ttestsCases := []struct {\n\t\tname, path, hashStr, actualHash string\n\t\tsetup                           func(fs afero.Fs)\n\t\tvalid                           bool\n\t\terr                             bool\n\t\terrMsg                          error\n\t}{\n\t\t{\n\t\t\tname:    \"Valid SHA256 hash\",\n\t\t\tpath:    \"test.txt\",\n\t\t\thashStr: \"sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08\",\n\t\t\tsetup: func(fs afero.Fs) {\n\t\t\t\tafero.WriteFile(fs, \"test.txt\", []byte(\"test\"), 0644)\n\t\t\t},\n\t\t\tactualHash: \"sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08\",\n\t\t\tvalid:      true,\n\t\t\terr:        false,\n\t\t},\n\t\t{\n\t\t\tname:    \"Invalid SHA256 hash\",\n\t\t\tpath:    \"test.txt\",\n\t\t\thashStr: \"sha256:deadbeef\",\n\t\t\tsetup: func(fs afero.Fs) {\n\t\t\t\tafero.WriteFile(fs, \"test.txt\", []byte(\"test\"), 0644)\n\t\t\t},\n\t\t\tactualHash: \"sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08\",\n\t\t\tvalid:      false,\n\t\t\terr:        false,\n\t\t},\n\t\t{\n\t\t\tname:    \"Unsupported hash function\",\n\t\t\tpath:    \"test.txt\",\n\t\t\thashStr: \"md5:deadbeef\",\n\t\t\tsetup: func(fs afero.Fs) {\n\t\t\t\tafero.WriteFile(fs, \"test.txt\", []byte(\"test\"), 0644)\n\t\t\t},\n\t\t\tactualHash: \"\",\n\t\t\tvalid:      false,\n\t\t\terr:        true,\n\t\t\terrMsg:     fmt.Errorf(\"hasher not supported or specified (given: md5:deadbeef)\"),\n\t\t},\n\t\t{\n\t\t\tname:       \"File does not exist\",\n\t\t\tpath:       \"nonexistent.txt\",\n\t\t\thashStr:    \"sha256:deadbeef\",\n\t\t\tsetup:      func(fs afero.Fs) {},\n\t\t\tvalid:      false,\n\t\t\tactualHash: \"\",\n\t\t\terr:        true,\n\t\t},\n\t}\n\n\tfor _, tc := range testsCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tfs := afero.NewMemMapFs()\n\t\t\ttc.setup(fs)\n\n\t\t\tvalid, actualHash, err := ValidateByHash(fs, tc.path, tc.hashStr)\n\n\t\t\tassert.Equal(t, tc.valid, valid)\n\t\t\tassert.Equal(t, tc.actualHash, actualHash)\n\n\t\t\tif tc.err {\n\t\t\t\tassert.Error(t, err)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\n\t\t\tif tc.errMsg != nil {\n\t\t\t\tassert.Equal(t, tc.errMsg, err)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/file/tar_xz_decompressor.go",
    "content": "package file\n\nimport (\n\t\"archive/tar\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/spf13/afero\"\n\t\"github.com/xi2/xz\"\n)\n\n// Note: this is a copy of the TarXzDecompressor from https://github.com/hashicorp/go-getter/blob/v2.2.3/decompress_txz.go\n// with the xz lib swapped out (for performance). A few adjustments were made:\n// - refactored to use afero filesystem abstraction\n// - fixed some linting issues\n\n// TarXzDecompressor is an implementation of Decompressor that can\n// decompress tar.xz files.\ntype tarXzDecompressor struct {\n\t// FileSizeLimit limits the total size of all\n\t// decompressed files.\n\t//\n\t// The zero value means no limit.\n\tFileSizeLimit int64\n\n\t// FilesLimit limits the number of files that are\n\t// allowed to be decompressed.\n\t//\n\t// The zero value means no limit.\n\tFilesLimit int\n\n\tFs afero.Fs\n}\n\nfunc (d *tarXzDecompressor) Decompress(dst, src string, dir bool, umask os.FileMode) error {\n\t// If we're going into a directory we should make that first\n\tmkdir := dst\n\tif !dir {\n\t\tmkdir = filepath.Dir(dst)\n\t}\n\tif err := d.Fs.MkdirAll(mkdir, mode(0755, umask)); err != nil {\n\t\treturn err\n\t}\n\n\t// File first\n\tf, err := d.Fs.Open(src)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer f.Close()\n\n\t// xz compression is second\n\ttxzR, err := xz.NewReader(f, 0)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error opening an xz reader for %s: %s\", src, err)\n\t}\n\n\treturn untar(d.Fs, txzR, dst, src, dir, umask, d.FileSizeLimit, d.FilesLimit)\n}\n\n// untar is a shared helper for untarring an archive. The reader should provide\n// an uncompressed view of the tar archive.\nfunc untar(fs afero.Fs, input io.Reader, dst, src string, dir bool, umask os.FileMode, fileSizeLimit int64, filesLimit int) error { // nolint:funlen,gocognit\n\ttarR := tar.NewReader(input)\n\tdone := false\n\tdirHdrs := []*tar.Header{}\n\tnow := time.Now()\n\n\tvar (\n\t\tfileSize   int64\n\t\tfilesCount int\n\t)\n\n\tfor {\n\t\tif filesLimit > 0 {\n\t\t\tfilesCount++\n\t\t\tif filesCount > filesLimit {\n\t\t\t\treturn fmt.Errorf(\"tar archive contains too many files: %d > %d\", filesCount, filesLimit)\n\t\t\t}\n\t\t}\n\n\t\thdr, err := tarR.Next()\n\t\tif err == io.EOF {\n\t\t\tif !done {\n\t\t\t\t// Empty archive\n\t\t\t\treturn fmt.Errorf(\"empty archive: %s\", src)\n\t\t\t}\n\n\t\t\tbreak\n\t\t}\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tswitch hdr.Typeflag {\n\t\tcase tar.TypeSymlink, tar.TypeLink:\n\t\t\t// to prevent any potential indirect traversal attacks\n\t\t\tcontinue\n\t\tcase tar.TypeXGlobalHeader, tar.TypeXHeader:\n\t\t\t// don't unpack extended headers as files\n\t\t\tcontinue\n\t\t}\n\n\t\tpath := dst\n\t\tif dir {\n\t\t\t// Disallow parent traversal\n\t\t\tif containsDotDot(hdr.Name) {\n\t\t\t\treturn fmt.Errorf(\"entry contains '..': %s\", hdr.Name)\n\t\t\t}\n\n\t\t\tpath = filepath.Join(path, hdr.Name) // nolint:gosec // hdr.Name is checked above\n\t\t}\n\n\t\tfileInfo := hdr.FileInfo()\n\n\t\tfileSize += fileInfo.Size()\n\n\t\tif fileSizeLimit > 0 && fileSize > fileSizeLimit {\n\t\t\treturn fmt.Errorf(\"tar archive larger than limit: %d\", fileSizeLimit)\n\t\t}\n\n\t\tif fileInfo.IsDir() {\n\t\t\tif !dir {\n\t\t\t\treturn fmt.Errorf(\"expected a single file: %s\", src)\n\t\t\t}\n\n\t\t\t// A directory, just make the directory and continue unarchiving...\n\t\t\tif err := fs.MkdirAll(path, mode(0755, umask)); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\t// Record the directory information so that we may set its attributes\n\t\t\t// after all files have been extracted\n\t\t\tdirHdrs = append(dirHdrs, hdr)\n\n\t\t\tcontinue\n\t\t}\n\t\t// There is no ordering guarantee that a file in a directory is\n\t\t// listed before the directory\n\t\tdstPath := filepath.Dir(path)\n\n\t\t// Check that the directory exists, otherwise create it\n\t\tif _, err := fs.Stat(dstPath); os.IsNotExist(err) {\n\t\t\tif err := fs.MkdirAll(dstPath, mode(0755, umask)); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\t// We have a file. If we already decoded, then it is an error\n\t\tif !dir && done {\n\t\t\treturn fmt.Errorf(\"expected a single file, got multiple: %s\", src)\n\t\t}\n\n\t\t// Mark that we're done so future in single file mode errors\n\t\tdone = true\n\n\t\t// Size limit is tracked using the returned file info.\n\t\terr = copyReader(fs, path, tarR, hdr.FileInfo().Mode(), umask, 0)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t// Set the access and modification time if valid, otherwise default to current time\n\t\taTime := now\n\t\tmTime := now\n\t\tif hdr.AccessTime.Unix() > 0 {\n\t\t\taTime = hdr.AccessTime\n\t\t}\n\t\tif hdr.ModTime.Unix() > 0 {\n\t\t\tmTime = hdr.ModTime\n\t\t}\n\t\tif err := fs.Chtimes(path, aTime, mTime); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\t// Perform a final pass over extracted directories to update metadata\n\tfor _, dirHdr := range dirHdrs {\n\t\tpath := filepath.Join(dst, dirHdr.Name) // nolint:gosec // hdr.Name is checked above\n\t\t// Chmod the directory since they might be created before we know the mode flags\n\t\tif err := fs.Chmod(path, mode(dirHdr.FileInfo().Mode(), umask)); err != nil {\n\t\t\treturn err\n\t\t}\n\t\t// Set the mtime/atime attributes since they would have been changed during extraction\n\t\taTime := now\n\t\tmTime := now\n\t\tif dirHdr.AccessTime.Unix() > 0 {\n\t\t\taTime = dirHdr.AccessTime\n\t\t}\n\t\tif dirHdr.ModTime.Unix() > 0 {\n\t\t\tmTime = dirHdr.ModTime\n\t\t}\n\t\tif err := fs.Chtimes(path, aTime, mTime); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// containsDotDot checks if the filepath value v contains a \"..\" entry.\n// This will check filepath components by splitting along / or \\. This\n// function is copied directly from the Go net/http implementation.\nfunc containsDotDot(v string) bool {\n\tif !strings.Contains(v, \"..\") {\n\t\treturn false\n\t}\n\tfor _, ent := range strings.FieldsFunc(v, isSlashRune) {\n\t\tif ent == \"..\" {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc isSlashRune(r rune) bool { return r == '/' || r == '\\\\' }\n"
  },
  {
    "path": "internal/file/tar_xz_decompressor_test.go",
    "content": "package file\n\nimport (\n\t\"archive/tar\"\n\t\"bytes\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/spf13/afero\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/ulikunitz/xz\"\n)\n\nfunc TestTarXzDecompressor_Decompress(t *testing.T) {\n\tfiles := map[string]string{\n\t\t\"file1.txt\": \"This is file 1.\",\n\t\t\"file2.txt\": \"This is file 2.\",\n\t}\n\n\tfs := afero.NewMemMapFs()\n\tsrcFile, tmpDir := createTarXzFromFiles(t, fs, files)\n\tdstDir := filepath.Join(tmpDir, \"decompressed\")\n\n\tdecompressor := &tarXzDecompressor{\n\t\tFs: fs,\n\t}\n\n\terr := decompressor.Decompress(dstDir, srcFile, true, 0000)\n\trequire.NoError(t, err)\n\n\tfor name, content := range files {\n\t\tdata, err := afero.ReadFile(fs, filepath.Join(dstDir, name))\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, content, string(data))\n\t}\n}\n\nfunc TestTarXzDecompressor_DecompressWithNestedDirs(t *testing.T) {\n\tfiles := map[string]string{\n\t\t\"file1.txt\":                \"This is file 1.\",\n\t\t\"dir1/file2.txt\":           \"This is file 2 in dir1.\",\n\t\t\"dir1/dir2/file3.txt\":      \"This is file 3 in dir1/dir2.\",\n\t\t\"dir1/dir2/dir3/file4.txt\": \"This is file 4 in dir1/dir2/dir3.\",\n\t}\n\n\tfs := afero.NewMemMapFs()\n\tsrcFile, tmpDir := createTarXzFromFiles(t, fs, files)\n\tdstDir := filepath.Join(tmpDir, \"decompressed\")\n\n\tdecompressor := &tarXzDecompressor{\n\t\tFs: fs,\n\t}\n\n\terr := decompressor.Decompress(dstDir, srcFile, true, 0000)\n\trequire.NoError(t, err)\n\n\tfor name, content := range files {\n\t\tdata, err := afero.ReadFile(fs, filepath.Join(dstDir, name))\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, content, string(data))\n\t}\n}\n\nfunc TestTarXzDecompressor_FileSizeLimit(t *testing.T) {\n\tfiles := map[string]string{\n\t\t\"file1.txt\": \"This is file 1.\",\n\t\t\"file2.txt\": \"This is file 2.\",\n\t}\n\n\tfs := afero.NewMemMapFs()\n\tsrcFile, tmpDir := createTarXzFromFiles(t, fs, files)\n\tdstDir := filepath.Join(tmpDir, \"decompressed\")\n\n\tdecompressor := &tarXzDecompressor{\n\t\tFileSizeLimit: int64(10), // setting a small file size limit\n\t\tFs:            fs,\n\t}\n\n\terr := decompressor.Decompress(dstDir, srcFile, true, 0000)\n\trequire.Error(t, err)\n\tassert.Contains(t, err.Error(), \"tar archive larger than limit\")\n}\n\nfunc TestTarXzDecompressor_FilesLimit(t *testing.T) {\n\tfiles := map[string]string{\n\t\t\"file1.txt\": \"This is file 1.\",\n\t\t\"file2.txt\": \"This is file 2.\",\n\t}\n\n\tfs := afero.NewMemMapFs()\n\tsrcFile, tmpDir := createTarXzFromFiles(t, fs, files)\n\tdstDir := filepath.Join(tmpDir, \"decompressed\")\n\n\tdecompressor := &tarXzDecompressor{\n\t\tFilesLimit: 1, // setting a limit of 1 file\n\t\tFs:         fs,\n\t}\n\n\terr := decompressor.Decompress(dstDir, srcFile, true, 0000)\n\trequire.Error(t, err)\n\tassert.Contains(t, err.Error(), \"tar archive contains too many files\")\n}\n\nfunc TestTarXzDecompressor_DecompressSingleFile(t *testing.T) {\n\tfiles := map[string]string{\n\t\t\"file1.txt\": \"This is file 1.\",\n\t}\n\n\tfs := afero.NewMemMapFs()\n\tsrcFile, tmpDir := createTarXzFromFiles(t, fs, files)\n\tdstFile := filepath.Join(tmpDir, \"single_file.txt\")\n\n\tdecompressor := &tarXzDecompressor{\n\t\tFs: fs,\n\t}\n\n\terr := decompressor.Decompress(dstFile, srcFile, false, 0000)\n\trequire.NoError(t, err)\n\n\tdata, err := afero.ReadFile(fs, dstFile)\n\trequire.NoError(t, err)\n\tassert.Equal(t, files[\"file1.txt\"], string(data))\n}\n\nfunc TestTarXzDecompressor_EmptyArchive(t *testing.T) {\n\tfiles := map[string]string{}\n\n\tfs := afero.NewMemMapFs()\n\tsrcFile, tmpDir := createTarXzFromFiles(t, fs, files)\n\tdstDir := filepath.Join(tmpDir, \"decompressed\")\n\n\tdecompressor := &tarXzDecompressor{\n\t\tFs: fs,\n\t}\n\n\terr := decompressor.Decompress(dstDir, srcFile, true, 0000)\n\trequire.Error(t, err)\n\tassert.Contains(t, err.Error(), \"empty archive\")\n}\n\nfunc TestTarXzDecompressor_PathTraversal(t *testing.T) {\n\tfiles := map[string]string{\n\t\t\"../traversal_file.txt\": \"This file should not be extracted.\",\n\t}\n\n\tfs := afero.NewMemMapFs()\n\tsrcFile, tmpDir := createTarXzFromFiles(t, fs, files)\n\tdstDir := filepath.Join(tmpDir, \"decompressed\")\n\n\tdecompressor := &tarXzDecompressor{\n\t\tFs: fs,\n\t}\n\n\terr := decompressor.Decompress(dstDir, srcFile, true, 0000)\n\trequire.Error(t, err)\n\tassert.Contains(t, err.Error(), \"entry contains '..'\")\n}\n\nfunc createTarXzFromFiles(t *testing.T, fs afero.Fs, files map[string]string) (string, string) {\n\tt.Helper()\n\n\ttmpDir, err := afero.TempDir(fs, \"\", \"tar_xz_decompressor_test\")\n\trequire.NoError(t, err)\n\tsrcFile := filepath.Join(tmpDir, \"src_file.tar.xz\")\n\n\tvar buf bytes.Buffer\n\txzWriter, err := xz.NewWriter(&buf)\n\trequire.NoError(t, err)\n\n\ttarWriter := tar.NewWriter(xzWriter)\n\n\tfor name, content := range files {\n\t\tdir := filepath.Dir(name)\n\t\tif dir != \".\" {\n\t\t\thdr := &tar.Header{\n\t\t\t\tName:     dir + \"/\",\n\t\t\t\tMode:     0755,\n\t\t\t\tTypeflag: tar.TypeDir,\n\t\t\t}\n\t\t\terr := tarWriter.WriteHeader(hdr)\n\t\t\trequire.NoError(t, err)\n\t\t}\n\n\t\thdr := &tar.Header{\n\t\t\tName: name,\n\t\t\tMode: 0600,\n\t\t\tSize: int64(len(content)),\n\t\t}\n\t\terr := tarWriter.WriteHeader(hdr)\n\t\trequire.NoError(t, err)\n\n\t\t_, err = tarWriter.Write([]byte(content))\n\t\trequire.NoError(t, err)\n\t}\n\n\terr = tarWriter.Close()\n\trequire.NoError(t, err)\n\n\terr = xzWriter.Close()\n\trequire.NoError(t, err)\n\n\terr = afero.WriteFile(fs, srcFile, buf.Bytes(), 0644)\n\trequire.NoError(t, err)\n\n\treturn srcFile, tmpDir\n}\n"
  },
  {
    "path": "internal/file/xz_decompressor.go",
    "content": "package file\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/spf13/afero\"\n\t\"github.com/xi2/xz\"\n)\n\n// Note: this is a copy of the XzDecompressor from https://github.com/hashicorp/go-getter/blob/v2.2.3/decompress_xz.go\n// with the xz lib swapped out (for performance). A few adjustments were made:\n// - refactored to use afero filesystem abstraction\n// - fixed some linting issues\n\n// xzDecompressor is an implementation of Decompressor that can decompress xz files.\ntype xzDecompressor struct {\n\t// FileSizeLimit limits the size of a decompressed file.\n\t//\n\t// The zero value means no limit.\n\tFileSizeLimit int64\n\n\tFs afero.Fs\n}\n\nfunc (d *xzDecompressor) Decompress(dst, src string, dir bool, umask os.FileMode) error {\n\t// Directory isn't supported at all\n\tif dir {\n\t\treturn fmt.Errorf(\"xz-compressed files can only unarchive to a single file\")\n\t}\n\n\t// If we're going into a directory we should make that first\n\tif err := d.Fs.MkdirAll(filepath.Dir(dst), mode(0755, umask)); err != nil {\n\t\treturn err\n\t}\n\n\t// File first\n\tf, err := d.Fs.Open(src)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer f.Close()\n\n\t// xz compression is second\n\txzR, err := xz.NewReader(f, 0)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Copy it out, potentially using a file size limit.\n\treturn copyReader(d.Fs, dst, xzR, 0622, umask, d.FileSizeLimit)\n}\n\n// copyReader copies from an io.Reader into a file, using umask to create the dst file\nfunc copyReader(fs afero.Fs, dst string, src io.Reader, fmode, umask os.FileMode, fileSizeLimit int64) error {\n\tdstF, err := fs.OpenFile(dst, os.O_RDWR|os.O_CREATE|os.O_TRUNC, fmode)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer dstF.Close()\n\n\tif fileSizeLimit > 0 {\n\t\tsrc = io.LimitReader(src, fileSizeLimit)\n\t}\n\n\t_, err = io.Copy(dstF, src)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Explicitly chmod; the process umask is unconditionally applied otherwise.\n\t// We'll mask the mode with our own umask, but that may be different than\n\t// the process umask\n\treturn fs.Chmod(dst, mode(fmode, umask))\n}\n\n// mode returns the file mode masked by the umask\nfunc mode(mode, umask os.FileMode) os.FileMode {\n\treturn mode & ^umask\n}\n"
  },
  {
    "path": "internal/file/xz_decompressor_test.go",
    "content": "package file\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/spf13/afero\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/ulikunitz/xz\"\n)\n\nfunc TestXzDecompressor_Decompress(t *testing.T) {\n\tcontent := \"This is a test for xz decompression.\"\n\n\tfs := afero.NewMemMapFs()\n\tsrcFile, tmpDir := createXZFromString(t, fs, content)\n\tdstFile := filepath.Join(tmpDir, \"dst_file.txt\")\n\n\tdecompressor := &xzDecompressor{\n\t\tFs: fs,\n\t}\n\n\terr := decompressor.Decompress(dstFile, srcFile, false, 0000)\n\trequire.NoError(t, err)\n\n\tdata, err := afero.ReadFile(fs, dstFile)\n\trequire.NoError(t, err)\n\tassert.Equal(t, content, string(data))\n}\n\nfunc TestXzDecompressor_FileSizeLimit(t *testing.T) {\n\tcontent := \"This is a test for xz decompression with file size limit.\"\n\n\tfs := afero.NewMemMapFs()\n\tsrcFile, tmpDir := createXZFromString(t, fs, content)\n\tdstFile := filepath.Join(tmpDir, \"dst_file.txt\")\n\n\tfileSizeLimit := int64(10)\n\n\tdecompressor := &xzDecompressor{\n\t\tFileSizeLimit: fileSizeLimit,\n\t\tFs:            fs,\n\t}\n\n\terr := decompressor.Decompress(dstFile, srcFile, false, 0000)\n\trequire.NoError(t, err)\n\n\tdata, err := afero.ReadFile(fs, dstFile)\n\trequire.NoError(t, err)\n\tassert.Equal(t, content[:fileSizeLimit], string(data))\n}\n\nfunc TestCopyReader(t *testing.T) {\n\tcontent := \"This is the content for testing copyReader.\"\n\n\tfs := afero.NewMemMapFs()\n\n\ttmpDir := t.TempDir()\n\tsrcFile := filepath.Join(tmpDir, \"src_file.txt\")\n\terr := afero.WriteFile(fs, srcFile, []byte(content), 0644)\n\trequire.NoError(t, err)\n\n\tsrcF, err := fs.Open(srcFile)\n\trequire.NoError(t, err)\n\tdefer srcF.Close()\n\n\tdstFile := filepath.Join(tmpDir, \"dst_file.txt\")\n\n\terr = copyReader(fs, dstFile, srcF, 0644, 0000, 0)\n\trequire.NoError(t, err)\n\n\tinfo, err := fs.Stat(dstFile)\n\trequire.NoError(t, err)\n\tassert.Equal(t, os.FileMode(0644), info.Mode().Perm())\n\n\tdata, err := afero.ReadFile(fs, dstFile)\n\tassert.NoError(t, err)\n\tassert.Equal(t, content, string(data))\n}\n\nfunc createXZFromString(t *testing.T, fs afero.Fs, content string) (string, string) {\n\tt.Helper()\n\n\ttmpDir, err := afero.TempDir(fs, \"\", \"xz_decompressor_test\")\n\trequire.NoError(t, err)\n\tsrcFile := filepath.Join(tmpDir, \"src_file.xz\")\n\n\tf, err := fs.Create(srcFile)\n\trequire.NoError(t, err)\n\tdefer f.Close()\n\n\txzW, err := xz.NewWriter(f)\n\trequire.NoError(t, err)\n\tdefer xzW.Close()\n\n\t_, err = xzW.Write([]byte(content))\n\tassert.NoError(t, err)\n\n\treturn srcFile, tmpDir\n}\n"
  },
  {
    "path": "internal/format/format.go",
    "content": "package format\n\nimport (\n\t\"strings\"\n)\n\nconst (\n\tUnknownFormat   Format = \"unknown\"\n\tJSONFormat      Format = \"json\"\n\tTableFormat     Format = \"table\"\n\tCycloneDXFormat Format = \"cyclonedx\"\n\tCycloneDXJSON   Format = \"cyclonedx-json\"\n\tCycloneDXXML    Format = \"cyclonedx-xml\"\n\tSarifFormat     Format = \"sarif\"\n\tTemplateFormat  Format = \"template\"\n\n\t// DEPRECATED <-- TODO: remove in v1.0\n\tEmbeddedVEXJSON Format = \"embedded-cyclonedx-vex-json\"\n\tEmbeddedVEXXML  Format = \"embedded-cyclonedx-vex-xml\"\n)\n\n// Format is a dedicated type to represent a specific kind of presenter output format.\ntype Format string\n\nfunc (f Format) String() string {\n\treturn string(f)\n}\n\n// Parse returns the presenter.format specified by the given user input.\nfunc Parse(userInput string) Format {\n\tswitch strings.ToLower(userInput) {\n\tcase \"\":\n\t\treturn TableFormat\n\tcase strings.ToLower(JSONFormat.String()):\n\t\treturn JSONFormat\n\tcase strings.ToLower(TableFormat.String()):\n\t\treturn TableFormat\n\tcase strings.ToLower(SarifFormat.String()):\n\t\treturn SarifFormat\n\tcase strings.ToLower(TemplateFormat.String()):\n\t\treturn TemplateFormat\n\tcase strings.ToLower(CycloneDXFormat.String()):\n\t\treturn CycloneDXFormat\n\tcase strings.ToLower(CycloneDXJSON.String()):\n\t\treturn CycloneDXJSON\n\tcase strings.ToLower(CycloneDXXML.String()):\n\t\treturn CycloneDXXML\n\tcase strings.ToLower(EmbeddedVEXJSON.String()):\n\t\treturn CycloneDXJSON\n\tcase strings.ToLower(EmbeddedVEXXML.String()):\n\t\treturn CycloneDXFormat\n\tdefault:\n\t\treturn UnknownFormat\n\t}\n}\n\n// AvailableFormats is a list of presenter format options available to users.\nvar AvailableFormats = []Format{\n\tJSONFormat,\n\tTableFormat,\n\tCycloneDXFormat,\n\tCycloneDXJSON,\n\tSarifFormat,\n\tTemplateFormat,\n}\n\n// DeprecatedFormats TODO: remove in v1.0\nvar DeprecatedFormats = []Format{\n\tEmbeddedVEXJSON,\n\tEmbeddedVEXXML,\n}\n"
  },
  {
    "path": "internal/format/format_test.go",
    "content": "package format\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestParse(t *testing.T) {\n\tcases := []struct {\n\t\tinput    string\n\t\texpected Format\n\t}{\n\t\t{\n\t\t\t\"\",\n\t\t\tTableFormat,\n\t\t},\n\t\t{\n\t\t\t\"table\",\n\t\t\tTableFormat,\n\t\t},\n\t\t{\n\t\t\t\"jSOn\",\n\t\t\tJSONFormat,\n\t\t},\n\t\t{\n\t\t\t\"booboodepoopoo\",\n\t\t\tUnknownFormat,\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.input, func(t *testing.T) {\n\t\t\tactual := Parse(tc.input)\n\t\t\tassert.Equal(t, tc.expected, actual, \"unexpected result for input %q\", tc.input)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/format/presenter.go",
    "content": "package format\n\nimport (\n\t\"github.com/wagoodman/go-presenter\"\n\n\t\"github.com/anchore/grype/grype/presenter/cyclonedx\"\n\t\"github.com/anchore/grype/grype/presenter/json\"\n\t\"github.com/anchore/grype/grype/presenter/models\"\n\t\"github.com/anchore/grype/grype/presenter/sarif\"\n\t\"github.com/anchore/grype/grype/presenter/table\"\n\t\"github.com/anchore/grype/grype/presenter/template\"\n\t\"github.com/anchore/grype/internal/log\"\n)\n\ntype PresentationConfig struct {\n\tTemplateFilePath string\n\tShowSuppressed   bool\n\tPretty           bool\n}\n\n// GetPresenter retrieves a Presenter that matches a CLI option\nfunc GetPresenter(format Format, c PresentationConfig, pb models.PresenterConfig) presenter.Presenter {\n\tswitch format {\n\tcase JSONFormat:\n\t\treturn json.NewPresenter(pb)\n\tcase TableFormat:\n\t\treturn table.NewPresenter(pb, c.ShowSuppressed)\n\n\t// NOTE: cyclonedx is identical to EmbeddedVEXJSON\n\t// The cyclonedx library only provides two BOM formats: JSON and XML\n\t// These embedded formats will be removed in v1.0\n\tcase CycloneDXFormat:\n\t\treturn cyclonedx.NewXMLPresenter(pb)\n\tcase CycloneDXJSON:\n\t\treturn cyclonedx.NewJSONPresenter(pb)\n\tcase CycloneDXXML:\n\t\treturn cyclonedx.NewXMLPresenter(pb)\n\tcase SarifFormat:\n\t\treturn sarif.NewPresenter(pb)\n\tcase TemplateFormat:\n\t\treturn template.NewPresenter(pb, c.TemplateFilePath)\n\t// DEPRECATED TODO: remove in v1.0\n\tcase EmbeddedVEXJSON:\n\t\tlog.Warn(\"embedded-cyclonedx-vex-json format is deprecated and will be removed in v1.0\")\n\t\treturn cyclonedx.NewJSONPresenter(pb)\n\tcase EmbeddedVEXXML:\n\t\tlog.Warn(\"embedded-cyclonedx-vex-xml format is deprecated and will be removed in v1.0\")\n\t\treturn cyclonedx.NewXMLPresenter(pb)\n\tdefault:\n\t\treturn nil\n\t}\n}\n"
  },
  {
    "path": "internal/format/writer.go",
    "content": "package format\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/hashicorp/go-multierror\"\n\n\t\"github.com/anchore/go-homedir\"\n\t\"github.com/anchore/grype/grype/presenter/models\"\n\t\"github.com/anchore/grype/internal/bus\"\n\t\"github.com/anchore/grype/internal/log\"\n)\n\ntype ScanResultWriter interface {\n\tWrite(result models.PresenterConfig) error\n}\n\nvar _ ScanResultWriter = (*scanResultMultiWriter)(nil)\n\nvar _ interface {\n\tio.Closer\n\tScanResultWriter\n} = (*scanResultStreamWriter)(nil)\n\n// MakeScanResultWriter creates a ScanResultWriter for output or returns an error. this will either return a valid writer\n// or an error but neither both and if there is no error, ScanResultWriter.Close() should be called\nfunc MakeScanResultWriter(outputs []string, defaultFile string, cfg PresentationConfig) (ScanResultWriter, error) {\n\toutputOptions, err := parseOutputFlags(outputs, defaultFile, cfg)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\twriter, err := newMultiWriter(outputOptions...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn writer, nil\n}\n\n// MakeScanResultWriterForFormat creates a ScanResultWriter for the given format or returns an error.\nfunc MakeScanResultWriterForFormat(f string, path string, cfg PresentationConfig) (ScanResultWriter, error) {\n\tformat := Parse(f)\n\n\tif format == UnknownFormat {\n\t\treturn nil, fmt.Errorf(`unsupported output format \"%s\", supported formats are: %+v`, f, AvailableFormats)\n\t}\n\n\twriter, err := newMultiWriter(newWriterDescription(format, path, cfg))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn writer, nil\n}\n\n// parseOutputFlags utility to parse command-line option strings and retain the existing behavior of default format and file\nfunc parseOutputFlags(outputs []string, defaultFile string, cfg PresentationConfig) (out []scanResultWriterDescription, errs error) {\n\t// always should have one option -- we generally get the default of \"table\", but just make sure\n\tif len(outputs) == 0 {\n\t\toutputs = append(outputs, TableFormat.String())\n\t}\n\n\tfor _, name := range outputs {\n\t\tname = strings.TrimSpace(name)\n\n\t\t// split to at most two parts for <format>=<file>\n\t\tparts := strings.SplitN(name, \"=\", 2)\n\n\t\t// the format name is the first part\n\t\tname = parts[0]\n\n\t\t// default to the --file or empty string if not specified\n\t\tfile := defaultFile\n\n\t\t// If a file is specified as part of the output formatName, use that\n\t\tif len(parts) > 1 {\n\t\t\tfile = parts[1]\n\t\t}\n\n\t\tformat := Parse(name)\n\n\t\tif format == UnknownFormat {\n\t\t\terrs = multierror.Append(errs, fmt.Errorf(`unsupported output format \"%s\", supported formats are: %+v`, name, AvailableFormats))\n\t\t\tcontinue\n\t\t}\n\n\t\tout = append(out, newWriterDescription(format, file, cfg))\n\t}\n\treturn out, errs\n}\n\n// scanResultWriterDescription Format and path strings used to create ScanResultWriter\ntype scanResultWriterDescription struct {\n\tFormat Format\n\tPath   string\n\tCfg    PresentationConfig\n}\n\nfunc newWriterDescription(f Format, p string, cfg PresentationConfig) scanResultWriterDescription {\n\texpandedPath, err := homedir.Expand(p)\n\tif err != nil {\n\t\tlog.Warnf(\"could not expand given writer output path=%q: %w\", p, err)\n\t\t// ignore errors\n\t\texpandedPath = p\n\t}\n\treturn scanResultWriterDescription{\n\t\tFormat: f,\n\t\tPath:   expandedPath,\n\t\tCfg:    cfg,\n\t}\n}\n\n// scanResultMultiWriter holds a list of child ScanResultWriters to apply all Write and Close operations to\ntype scanResultMultiWriter struct {\n\twriters []ScanResultWriter\n}\n\n// newMultiWriter create all report writers from input options; if a file is not specified the given defaultWriter is used\nfunc newMultiWriter(options ...scanResultWriterDescription) (_ *scanResultMultiWriter, err error) {\n\tif len(options) == 0 {\n\t\treturn nil, fmt.Errorf(\"no output options provided\")\n\t}\n\n\tout := &scanResultMultiWriter{}\n\n\tfor _, option := range options {\n\t\tswitch len(option.Path) {\n\t\tcase 0:\n\t\t\tout.writers = append(out.writers, &scanResultPublisher{\n\t\t\t\tformat: option.Format,\n\t\t\t\tcfg:    option.Cfg,\n\t\t\t})\n\t\tdefault:\n\t\t\t// create any missing subdirectories\n\t\t\tdir := filepath.Dir(option.Path)\n\t\t\tif dir != \"\" {\n\t\t\t\ts, err := os.Stat(dir)\n\t\t\t\tif err != nil {\n\t\t\t\t\terr = os.MkdirAll(dir, 0755) // maybe should be os.ModePerm ?\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn nil, err\n\t\t\t\t\t}\n\t\t\t\t} else if !s.IsDir() {\n\t\t\t\t\treturn nil, fmt.Errorf(\"output path does not contain a valid directory: %s\", option.Path)\n\t\t\t\t}\n\t\t\t}\n\t\t\tfileOut, err := os.OpenFile(option.Path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"unable to create report file: %w\", err)\n\t\t\t}\n\t\t\tout.writers = append(out.writers, &scanResultStreamWriter{\n\t\t\t\tformat: option.Format,\n\t\t\t\tout:    fileOut,\n\t\t\t\tcfg:    option.Cfg,\n\t\t\t})\n\t\t}\n\t}\n\n\treturn out, nil\n}\n\n// Write writes the result to all writers\nfunc (m *scanResultMultiWriter) Write(s models.PresenterConfig) (errs error) {\n\tfor _, w := range m.writers {\n\t\terr := w.Write(s)\n\t\tif err != nil {\n\t\t\terrs = multierror.Append(errs, fmt.Errorf(\"unable to write result: %w\", err))\n\t\t}\n\t}\n\treturn errs\n}\n\n// scanResultStreamWriter implements ScanResultWriter for a given format and io.Writer, also providing a close function for cleanup\ntype scanResultStreamWriter struct {\n\tformat Format\n\tcfg    PresentationConfig\n\tout    io.Writer\n}\n\n// Write the provided result to the data stream\nfunc (w *scanResultStreamWriter) Write(s models.PresenterConfig) error {\n\tpres := GetPresenter(w.format, w.cfg, s)\n\tif err := pres.Present(w.out); err != nil {\n\t\treturn fmt.Errorf(\"unable to encode result: %w\", err)\n\t}\n\treturn nil\n}\n\n// Close any resources, such as open files\nfunc (w *scanResultStreamWriter) Close() error {\n\tif closer, ok := w.out.(io.Closer); ok {\n\t\treturn closer.Close()\n\t}\n\treturn nil\n}\n\n// scanResultPublisher implements ScanResultWriter that publishes results to the event bus\ntype scanResultPublisher struct {\n\tformat Format\n\tcfg    PresentationConfig\n}\n\n// Write the provided result to the data stream\nfunc (w *scanResultPublisher) Write(s models.PresenterConfig) error {\n\tpres := GetPresenter(w.format, w.cfg, s)\n\tbuf := &bytes.Buffer{}\n\tif err := pres.Present(buf); err != nil {\n\t\treturn fmt.Errorf(\"unable to encode result: %w\", err)\n\t}\n\n\tbus.Report(buf.String())\n\treturn nil\n}\n"
  },
  {
    "path": "internal/format/writer_test.go",
    "content": "package format\n\nimport (\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/docker/docker/pkg/homedir\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc Test_MakeScanResultWriter(t *testing.T) {\n\ttests := []struct {\n\t\toutputs []string\n\t\twantErr assert.ErrorAssertionFunc\n\t}{\n\t\t{\n\t\t\toutputs: []string{\"json\"},\n\t\t\twantErr: assert.NoError,\n\t\t},\n\t\t{\n\t\t\toutputs: []string{\"table\", \"json\"},\n\t\t\twantErr: assert.NoError,\n\t\t},\n\t\t{\n\t\t\toutputs: []string{\"unknown\"},\n\t\t\twantErr: func(t assert.TestingT, err error, bla ...interface{}) bool {\n\t\t\t\treturn assert.ErrorContains(t, err, `unsupported output format \"unknown\", supported formats are: [`)\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\t_, err := MakeScanResultWriter(tt.outputs, \"\", PresentationConfig{})\n\t\ttt.wantErr(t, err)\n\t}\n}\n\nfunc Test_newSBOMMultiWriter(t *testing.T) {\n\ttype writerConfig struct {\n\t\tformat string\n\t\tfile   string\n\t}\n\n\ttmp := t.TempDir()\n\n\ttestName := func(options []scanResultWriterDescription, err bool) string {\n\t\tvar out []string\n\t\tfor _, opt := range options {\n\t\t\tout = append(out, string(opt.Format)+\"=\"+opt.Path)\n\t\t}\n\t\terrs := \"\"\n\t\tif err {\n\t\t\terrs = \"(err)\"\n\t\t}\n\t\treturn strings.Join(out, \", \") + errs\n\t}\n\n\ttests := []struct {\n\t\toutputs  []scanResultWriterDescription\n\t\terr      bool\n\t\texpected []writerConfig\n\t}{\n\t\t{\n\t\t\toutputs: []scanResultWriterDescription{},\n\t\t\terr:     true,\n\t\t},\n\t\t{\n\t\t\toutputs: []scanResultWriterDescription{\n\t\t\t\t{\n\t\t\t\t\tFormat: \"table\",\n\t\t\t\t\tPath:   \"\",\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: []writerConfig{\n\t\t\t\t{\n\t\t\t\t\tformat: \"table\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\toutputs: []scanResultWriterDescription{\n\t\t\t\t{\n\t\t\t\t\tFormat: \"json\",\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: []writerConfig{\n\t\t\t\t{\n\t\t\t\t\tformat: \"json\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\toutputs: []scanResultWriterDescription{\n\t\t\t\t{\n\t\t\t\t\tFormat: \"json\",\n\t\t\t\t\tPath:   \"test-2.json\",\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: []writerConfig{\n\t\t\t\t{\n\t\t\t\t\tformat: \"json\",\n\t\t\t\t\tfile:   \"test-2.json\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\toutputs: []scanResultWriterDescription{\n\t\t\t\t{\n\t\t\t\t\tFormat: \"json\",\n\t\t\t\t\tPath:   \"test-3/1.json\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tFormat: \"spdx-json\",\n\t\t\t\t\tPath:   \"test-3/2.json\",\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: []writerConfig{\n\t\t\t\t{\n\t\t\t\t\tformat: \"json\",\n\t\t\t\t\tfile:   \"test-3/1.json\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tformat: \"spdx-json\",\n\t\t\t\t\tfile:   \"test-3/2.json\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\toutputs: []scanResultWriterDescription{\n\t\t\t\t{\n\t\t\t\t\tFormat: \"text\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tFormat: \"spdx-json\",\n\t\t\t\t\tPath:   \"test-4.json\",\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: []writerConfig{\n\t\t\t\t{\n\t\t\t\t\tformat: \"text\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tformat: \"spdx-json\",\n\t\t\t\t\tfile:   \"test-4.json\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(testName(test.outputs, test.err), func(t *testing.T) {\n\t\t\toutputs := test.outputs\n\t\t\tfor i := range outputs {\n\t\t\t\tif outputs[i].Path != \"\" {\n\t\t\t\t\toutputs[i].Path = tmp + outputs[i].Path\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmw, err := newMultiWriter(outputs...)\n\n\t\t\tif test.err {\n\t\t\t\tassert.Error(t, err)\n\t\t\t\treturn\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\n\t\t\tassert.Len(t, mw.writers, len(test.expected))\n\n\t\t\tfor i, e := range test.expected {\n\t\t\t\tswitch w := mw.writers[i].(type) {\n\t\t\t\tcase *scanResultStreamWriter:\n\t\t\t\t\tassert.Equal(t, string(w.format), e.format)\n\t\t\t\t\tassert.NotNil(t, w.out)\n\t\t\t\t\tif e.file != \"\" {\n\t\t\t\t\t\tassert.FileExists(t, tmp+e.file)\n\t\t\t\t\t}\n\t\t\t\tcase *scanResultPublisher:\n\t\t\t\t\tassert.Equal(t, string(w.format), e.format)\n\t\t\t\tdefault:\n\t\t\t\t\tt.Fatalf(\"unknown writer type: %T\", w)\n\t\t\t\t}\n\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_newSBOMWriterDescription(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tpath     string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:     \"expand home dir\",\n\t\t\tpath:     \"~/place.txt\",\n\t\t\texpected: filepath.Join(homedir.Get(), \"place.txt\"),\n\t\t},\n\t\t{\n\t\t\tname:     \"passthrough other paths\",\n\t\t\tpath:     \"/other/place.txt\",\n\t\t\texpected: \"/other/place.txt\",\n\t\t},\n\t\t{\n\t\t\tname:     \"no path\",\n\t\t\tpath:     \"\",\n\t\t\texpected: \"\",\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\to := newWriterDescription(\"table\", tt.path, PresentationConfig{})\n\t\t\tassert.Equal(t, tt.expected, o.Path)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/input.go",
    "content": "package internal\n\nimport (\n\t\"fmt\"\n\t\"os\"\n)\n\n// IsStdinPipeOrRedirect returns true if stdin is provided via pipe or redirect\nfunc IsStdinPipeOrRedirect() (bool, error) {\n\tfi, err := os.Stdin.Stat()\n\tif err != nil {\n\t\treturn false, fmt.Errorf(\"unable to determine if there is piped input: %w\", err)\n\t}\n\n\t// note: we should NOT use the absence of a character device here as the hint that there may be input expected\n\t// on stdin, as running grype as a subprocess you would expect no character device to be present but input can\n\t// be from either stdin or indicated by the CLI. Checking if stdin is a pipe is the most direct way to determine\n\t// if there *may* be bytes that will show up on stdin that should be used for the analysis source.\n\treturn fi.Mode()&os.ModeNamedPipe != 0 || fi.Size() > 0, nil\n}\n"
  },
  {
    "path": "internal/log/errors.go",
    "content": "package log\n\nimport \"io\"\n\nfunc CloseAndLogError(closer io.Closer, location string) {\n\tif closer == nil {\n\t\tDebug(\"no closer provided when attempting to close: %v\", location)\n\t\treturn\n\t}\n\terr := closer.Close()\n\tif err != nil {\n\t\tDebug(\"failed to close file: %v due to: %v\", location, err)\n\t}\n}\n"
  },
  {
    "path": "internal/log/log.go",
    "content": "/*\nPackage log contains the singleton object and helper functions for facilitating logging within the syft library.\n*/\npackage log\n\nimport (\n\t\"github.com/anchore/go-logger\"\n\t\"github.com/anchore/go-logger/adapter/discard\"\n\t\"github.com/anchore/go-logger/adapter/redact\"\n\tred \"github.com/anchore/grype/internal/redact\"\n)\n\n// log is the singleton used to facilitate logging internally within\nvar log = discard.New()\n\nfunc Set(l logger.Logger) {\n\t// though the application will automatically have a redaction logger, library consumers may not be doing this.\n\t// for this reason we additionally ensure there is a redaction logger configured for any logger passed. The\n\t// source of truth for redaction values is still in the internal redact package. If the passed logger is already\n\t// redacted, then this is a no-op.\n\tstore := red.Get()\n\tif store != nil {\n\t\tl = redact.New(l, store)\n\t}\n\tlog = l\n}\n\nfunc Get() logger.Logger {\n\treturn log\n}\n\n// Errorf takes a formatted template string and template arguments for the error logging level.\nfunc Errorf(format string, args ...interface{}) {\n\tlog.Errorf(format, args...)\n}\n\n// Error logs the given arguments at the error logging level.\nfunc Error(args ...interface{}) {\n\tlog.Error(args...)\n}\n\n// Warnf takes a formatted template string and template arguments for the warning logging level.\nfunc Warnf(format string, args ...interface{}) {\n\tlog.Warnf(format, args...)\n}\n\n// Warn logs the given arguments at the warning logging level.\nfunc Warn(args ...interface{}) {\n\tlog.Warn(args...)\n}\n\n// Infof takes a formatted template string and template arguments for the info logging level.\nfunc Infof(format string, args ...interface{}) {\n\tlog.Infof(format, args...)\n}\n\n// Info logs the given arguments at the info logging level.\nfunc Info(args ...interface{}) {\n\tlog.Info(args...)\n}\n\n// Debugf takes a formatted template string and template arguments for the debug logging level.\nfunc Debugf(format string, args ...interface{}) {\n\tlog.Debugf(format, args...)\n}\n\n// Debug logs the given arguments at the debug logging level.\nfunc Debug(args ...interface{}) {\n\tlog.Debug(args...)\n}\n\n// Tracef takes a formatted template string and template arguments for the trace logging level.\nfunc Tracef(format string, args ...interface{}) {\n\tlog.Tracef(format, args...)\n}\n\n// Trace logs the given arguments at the trace logging level.\nfunc Trace(args ...interface{}) {\n\tlog.Trace(args...)\n}\n\n// WithFields returns a message logger with multiple key-value fields.\nfunc WithFields(fields ...interface{}) logger.MessageLogger {\n\treturn log.WithFields(fields...)\n}\n\n// Nested returns a new logger with hard coded key-value pairs\nfunc Nested(fields ...interface{}) logger.Logger {\n\treturn log.Nested(fields...)\n}\n"
  },
  {
    "path": "internal/redact/redact.go",
    "content": "package redact\n\nimport \"github.com/anchore/go-logger/adapter/redact\"\n\nvar store redact.Store\n\nfunc Set(s redact.Store) {\n\tif store != nil {\n\t\t// if someone is trying to set a redaction store and we already have one then something is wrong. The store\n\t\t// that we're replacing might already have values in it, so we should never replace it.\n\t\tpanic(\"replace existing redaction store (probably unintentional)\")\n\t}\n\tstore = s\n}\n\nfunc Get() redact.Store {\n\treturn store\n}\n\nfunc Add(vs ...string) {\n\tif store == nil {\n\t\t// if someone is trying to add values that should never be output and we don't have a store then something is wrong.\n\t\t// we should never accidentally output values that should be redacted, thus we panic here.\n\t\tpanic(\"cannot add redactions without a store\")\n\t}\n\tstore.Add(vs...)\n}\n\nfunc Apply(value string) string {\n\tif store == nil {\n\t\t// if someone is trying to add values that should never be output and we don't have a store then something is wrong.\n\t\t// we should never accidentally output values that should be redacted, thus we panic here.\n\t\tpanic(\"cannot apply redactions without a store\")\n\t}\n\treturn store.RedactString(value)\n}\n"
  },
  {
    "path": "internal/regex_helpers.go",
    "content": "package internal\n\nimport \"regexp\"\n\n// MatchNamedCaptureGroups takes a regular expression and string and returns all of the named capture group results in a map.\n// This is only for the first match in the regex. Callers shouldn't be providing regexes with multiple capture groups with the same name.\nfunc MatchNamedCaptureGroups(regEx *regexp.Regexp, content string) map[string]string {\n\t// note: we are looking across all matches and stopping on the first non-empty match. Why? Take the following example:\n\t// input: \"cool something to match against\" pattern: `((?P<name>match) (?P<version>against))?`. Since the pattern is\n\t// encapsulated in an optional capture group, there will be results for each character, but the results will match\n\t// on nothing. The only \"true\" match will be at the end (\"match against\").\n\tallMatches := regEx.FindAllStringSubmatch(content, -1)\n\tvar results map[string]string\n\tfor _, match := range allMatches {\n\t\t// fill a candidate results map with named capture group results, accepting empty values, but not groups with\n\t\t// no names\n\t\tfor nameIdx, name := range regEx.SubexpNames() {\n\t\t\tif nameIdx > len(match) || len(name) == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif results == nil {\n\t\t\t\tresults = make(map[string]string)\n\t\t\t}\n\t\t\tresults[name] = match[nameIdx]\n\t\t}\n\t\t// note: since we are looking for the first best potential match we should stop when we find the first one\n\t\t// with non-empty results.\n\t\tif !isEmptyMap(results) {\n\t\t\tbreak\n\t\t}\n\t}\n\treturn results\n}\n\nfunc isEmptyMap(m map[string]string) bool {\n\tif len(m) == 0 {\n\t\treturn true\n\t}\n\tfor _, value := range m {\n\t\tif value != \"\" {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n"
  },
  {
    "path": "internal/regex_helpers_test.go",
    "content": "package internal\n\nimport (\n\t\"regexp\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestMatchCaptureGroups(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    string\n\t\tpattern  string\n\t\texpected map[string]string\n\t}{\n\t\t{\n\t\t\tname:    \"go-case\",\n\t\t\tinput:   \"match this thing\",\n\t\t\tpattern: `(?P<name>match).*(?P<version>thing)`,\n\t\t\texpected: map[string]string{\n\t\t\t\t\"name\":    \"match\",\n\t\t\t\t\"version\": \"thing\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:    \"only matches the first instance\",\n\t\t\tinput:   \"match this thing batch another think\",\n\t\t\tpattern: `(?P<name>[mb]atch).*?(?P<version>thin[gk])`,\n\t\t\texpected: map[string]string{\n\t\t\t\t\"name\":    \"match\",\n\t\t\t\t\"version\": \"thing\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:    \"nested capture groups\",\n\t\t\tinput:   \"cool something to match against\",\n\t\t\tpattern: `((?P<name>match) (?P<version>against))`,\n\t\t\texpected: map[string]string{\n\t\t\t\t\"name\":    \"match\",\n\t\t\t\t\"version\": \"against\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:    \"nested optional capture groups\",\n\t\t\tinput:   \"cool something to match against\",\n\t\t\tpattern: `((?P<name>match) (?P<version>against))?`,\n\t\t\texpected: map[string]string{\n\t\t\t\t\"name\":    \"match\",\n\t\t\t\t\"version\": \"against\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:    \"nested optional capture groups with larger match\",\n\t\t\tinput:   \"cool something to match against match never\",\n\t\t\tpattern: `.*?((?P<name>match) (?P<version>(against|never)))?`,\n\t\t\texpected: map[string]string{\n\t\t\t\t\"name\":    \"match\",\n\t\t\t\t\"version\": \"against\",\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tactual := MatchNamedCaptureGroups(regexp.MustCompile(test.pattern), test.input)\n\t\t\tassert.Equal(t, test.expected, actual)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/schemaver/schema_ver.go",
    "content": "package schemaver\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"strconv\"\n\t\"strings\"\n)\n\ntype SchemaVer struct {\n\tModel    int // breaking changes\n\tRevision int // potentially-breaking changes\n\tAddition int // additions only\n}\n\nfunc New(model, revision, addition int) SchemaVer {\n\treturn SchemaVer{\n\t\tModel:    model,\n\t\tRevision: revision,\n\t\tAddition: addition,\n\t}\n}\n\nfunc Parse(s string) (SchemaVer, error) {\n\t// must provide model.revision.addition\n\tparts := strings.Split(strings.TrimSpace(s), \".\")\n\tif len(parts) != 3 {\n\t\treturn SchemaVer{}, fmt.Errorf(\"invalid schema version format: %s\", s)\n\t}\n\t// check that all parts are integers\n\tmodel, err := strconv.Atoi(strings.TrimPrefix(parts[0], \"v\"))\n\tif err != nil || model < 1 {\n\t\treturn SchemaVer{}, fmt.Errorf(\"invalid schema version format: %s\", s)\n\t}\n\trevision, err := strconv.Atoi(parts[1])\n\tif err != nil || revision < 0 {\n\t\treturn SchemaVer{}, fmt.Errorf(\"invalid schema version format: %s\", s)\n\t}\n\taddition, err := strconv.Atoi(parts[2])\n\tif err != nil || addition < 0 {\n\t\treturn SchemaVer{}, fmt.Errorf(\"invalid schema version format: %s\", s)\n\t}\n\treturn New(model, revision, addition), nil\n}\n\nfunc (s SchemaVer) Valid() bool {\n\treturn s.Model > 0 && s.Revision >= 0 && s.Addition >= 0\n}\n\nfunc (s SchemaVer) MarshalJSON() ([]byte, error) {\n\treturn []byte(fmt.Sprintf(`\"%s\"`, s.String())), nil\n}\n\nfunc (s *SchemaVer) UnmarshalJSON(data []byte) error {\n\tvar str string\n\tif err := json.Unmarshal(data, &str); err != nil {\n\t\treturn fmt.Errorf(\"failed to unmarshal schema version as string: %w\", err)\n\t}\n\n\tparsed, err := Parse(str)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to parse schema version: %w\", err)\n\t}\n\t*s = parsed\n\treturn nil\n}\n\nfunc (s SchemaVer) String() string {\n\treturn fmt.Sprintf(\"v%d.%d.%d\", s.Model, s.Revision, s.Addition)\n}\n\nfunc (s SchemaVer) LessThan(other SchemaVer) bool {\n\tif s.Model != other.Model {\n\t\treturn s.Model < other.Model\n\t}\n\n\tif s.Revision != other.Revision {\n\t\treturn s.Revision < other.Revision\n\t}\n\n\treturn s.Addition < other.Addition\n}\n\nfunc (s SchemaVer) LessThanOrEqualTo(other SchemaVer) bool {\n\treturn s.LessThan(other) || s.Equal(other)\n}\n\nfunc (s SchemaVer) Equal(other SchemaVer) bool {\n\treturn s.Model == other.Model && s.Revision == other.Revision && s.Addition == other.Addition\n}\n\nfunc (s SchemaVer) GreaterThan(other SchemaVer) bool {\n\tif s.Model != other.Model {\n\t\treturn s.Model > other.Model\n\t}\n\n\tif s.Revision != other.Revision {\n\t\treturn s.Revision > other.Revision\n\t}\n\n\treturn s.Addition > other.Addition\n}\n\nfunc (s SchemaVer) GreaterOrEqualTo(other SchemaVer) bool {\n\treturn s.GreaterThan(other) || s.Equal(other)\n}\n"
  },
  {
    "path": "internal/schemaver/schema_ver_test.go",
    "content": "package schemaver\n\nimport (\n\t\"encoding/json\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestSchemaVer_LessThan(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tv1   SchemaVer\n\t\tv2   SchemaVer\n\t\twant bool\n\t}{\n\t\t{\n\t\t\tname: \"equal versions\",\n\t\t\tv1:   New(1, 0, 0),\n\t\t\tv2:   New(1, 0, 0),\n\t\t\twant: false,\n\t\t},\n\t\t{\n\t\t\tname: \"different model versions\",\n\t\t\tv1:   New(1, 0, 0),\n\t\t\tv2:   New(2, 0, 0),\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"different revision versions\",\n\t\t\tv1:   New(1, 1, 0),\n\t\t\tv2:   New(1, 2, 0),\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"different addition versions\",\n\t\t\tv1:   New(1, 0, 1),\n\t\t\tv2:   New(1, 0, 2),\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"inverted addition versions\",\n\t\t\tv1:   New(1, 0, 2),\n\t\t\tv2:   New(1, 0, 1),\n\t\t\twant: false,\n\t\t},\n\t\t{\n\t\t\tname: \"greater model overrides lower revision\",\n\t\t\tv1:   New(2, 0, 0),\n\t\t\tv2:   New(1, 9, 9),\n\t\t\twant: false,\n\t\t},\n\t\t{\n\t\t\tname: \"greater revision overrides lower addition\",\n\t\t\tv1:   New(1, 2, 0),\n\t\t\tv2:   New(1, 1, 9),\n\t\t\twant: false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tassert.Equal(t, tt.want, tt.v1.LessThan(tt.v2))\n\t\t})\n\t}\n}\n\nfunc TestSchemaVer_GreaterOrEqualTo(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tv1   SchemaVer\n\t\tv2   SchemaVer\n\t\twant bool\n\t}{\n\t\t{\n\t\t\tname: \"equal versions\",\n\t\t\tv1:   New(1, 0, 0),\n\t\t\tv2:   New(1, 0, 0),\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"different model versions\",\n\t\t\tv1:   New(1, 0, 0),\n\t\t\tv2:   New(2, 0, 0),\n\t\t\twant: false,\n\t\t},\n\t\t{\n\t\t\tname: \"different revision versions\",\n\t\t\tv1:   New(1, 1, 0),\n\t\t\tv2:   New(1, 2, 0),\n\t\t\twant: false,\n\t\t},\n\t\t{\n\t\t\tname: \"different addition versions\",\n\t\t\tv1:   New(1, 0, 1),\n\t\t\tv2:   New(1, 0, 2),\n\t\t\twant: false,\n\t\t},\n\t\t{\n\t\t\tname: \"inverted addition versions\",\n\t\t\tv1:   New(1, 0, 2),\n\t\t\tv2:   New(1, 0, 1),\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"greater model overrides lower revision\",\n\t\t\tv1:   New(2, 0, 0),\n\t\t\tv2:   New(1, 9, 9),\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"greater revision overrides lower addition\",\n\t\t\tv1:   New(1, 2, 0),\n\t\t\tv2:   New(1, 1, 9),\n\t\t\twant: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tassert.Equal(t, tt.want, tt.v1.GreaterOrEqualTo(tt.v2))\n\t\t})\n\t}\n}\n\nfunc TestSchemaVer_LessThanOrEqualTo(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tv1   SchemaVer\n\t\tv2   SchemaVer\n\t\twant bool\n\t}{\n\t\t{\n\t\t\tname: \"equal versions\",\n\t\t\tv1:   New(1, 2, 3),\n\t\t\tv2:   New(1, 2, 3),\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"less than version\",\n\t\t\tv1:   New(1, 2, 3),\n\t\t\tv2:   New(1, 2, 4),\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"greater than version\",\n\t\t\tv1:   New(1, 2, 4),\n\t\t\tv2:   New(1, 2, 3),\n\t\t\twant: false,\n\t\t},\n\t\t{\n\t\t\tname: \"different model - less\",\n\t\t\tv1:   New(1, 9, 9),\n\t\t\tv2:   New(2, 0, 0),\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"different model - greater\",\n\t\t\tv1:   New(2, 0, 0),\n\t\t\tv2:   New(1, 9, 9),\n\t\t\twant: false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tassert.Equal(t, tt.want, tt.v1.LessThanOrEqualTo(tt.v2))\n\t\t})\n\t}\n}\n\nfunc TestSchemaVer_Equal(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tv1   SchemaVer\n\t\tv2   SchemaVer\n\t\twant bool\n\t}{\n\t\t{\n\t\t\tname: \"equal versions\",\n\t\t\tv1:   New(1, 2, 3),\n\t\t\tv2:   New(1, 2, 3),\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"different addition\",\n\t\t\tv1:   New(1, 2, 3),\n\t\t\tv2:   New(1, 2, 4),\n\t\t\twant: false,\n\t\t},\n\t\t{\n\t\t\tname: \"different revision\",\n\t\t\tv1:   New(1, 2, 3),\n\t\t\tv2:   New(1, 3, 3),\n\t\t\twant: false,\n\t\t},\n\t\t{\n\t\t\tname: \"different model\",\n\t\t\tv1:   New(1, 2, 3),\n\t\t\tv2:   New(2, 2, 3),\n\t\t\twant: false,\n\t\t},\n\t\t{\n\t\t\tname: \"zero values equal\",\n\t\t\tv1:   New(1, 0, 0),\n\t\t\tv2:   New(1, 0, 0),\n\t\t\twant: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tassert.Equal(t, tt.want, tt.v1.Equal(tt.v2))\n\t\t})\n\t}\n}\n\nfunc TestSchemaVer_GreaterThan(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tv1   SchemaVer\n\t\tv2   SchemaVer\n\t\twant bool\n\t}{\n\t\t{\n\t\t\tname: \"equal versions\",\n\t\t\tv1:   New(1, 2, 3),\n\t\t\tv2:   New(1, 2, 3),\n\t\t\twant: false,\n\t\t},\n\t\t{\n\t\t\tname: \"greater addition\",\n\t\t\tv1:   New(1, 2, 4),\n\t\t\tv2:   New(1, 2, 3),\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"greater revision\",\n\t\t\tv1:   New(1, 3, 0),\n\t\t\tv2:   New(1, 2, 9),\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"greater model\",\n\t\t\tv1:   New(2, 0, 0),\n\t\t\tv2:   New(1, 9, 9),\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"less than\",\n\t\t\tv1:   New(1, 2, 3),\n\t\t\tv2:   New(1, 2, 4),\n\t\t\twant: false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tassert.Equal(t, tt.want, tt.v1.GreaterThan(tt.v2))\n\t\t})\n\t}\n}\n\nfunc TestParse(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\tinput   string\n\t\twant    SchemaVer\n\t\twantErr bool\n\t}{\n\t\t{\n\t\t\tname:    \"valid version\",\n\t\t\tinput:   \"1.2.3\",\n\t\t\twant:    New(1, 2, 3),\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"valid version with v prefix\",\n\t\t\tinput:   \"v1.2.3\",\n\t\t\twant:    New(1, 2, 3),\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"valid version with v prefix and zeros\",\n\t\t\tinput:   \"v1.0.0\",\n\t\t\twant:    New(1, 0, 0),\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"valid large numbers\",\n\t\t\tinput:   \"999.888.777\",\n\t\t\twant:    New(999, 888, 777),\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"valid with whitespace\",\n\t\t\tinput:   \"  1.2.3  \",\n\t\t\twant:    New(1, 2, 3),\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"invalid version with zeros\",\n\t\t\tinput:   \"0.0.0\",\n\t\t\twant:    New(0, 0, 0),\n\t\t\twantErr: true,\n\t\t},\n\t\t{\n\t\t\tname:    \"invalid version with v prefix and zero model\",\n\t\t\tinput:   \"v0.0.0\",\n\t\t\twant:    New(0, 0, 0),\n\t\t\twantErr: true,\n\t\t},\n\t\t{\n\t\t\tname:    \"invalid empty string\",\n\t\t\tinput:   \"\",\n\t\t\twantErr: true,\n\t\t},\n\t\t{\n\t\t\tname:    \"invalid too few parts\",\n\t\t\tinput:   \"1.2\",\n\t\t\twantErr: true,\n\t\t},\n\t\t{\n\t\t\tname:    \"invalid too many parts\",\n\t\t\tinput:   \"1.2.3.4\",\n\t\t\twantErr: true,\n\t\t},\n\t\t{\n\t\t\tname:    \"invalid non-numeric model\",\n\t\t\tinput:   \"a.2.3\",\n\t\t\twantErr: true,\n\t\t},\n\t\t{\n\t\t\tname:    \"invalid non-numeric revision\",\n\t\t\tinput:   \"1.b.3\",\n\t\t\twantErr: true,\n\t\t},\n\t\t{\n\t\t\tname:    \"invalid non-numeric addition\",\n\t\t\tinput:   \"1.2.c\",\n\t\t\twantErr: true,\n\t\t},\n\t\t{\n\t\t\tname:    \"invalid negative number\",\n\t\t\tinput:   \"-1.2.3\",\n\t\t\twantErr: true,\n\t\t},\n\t\t{\n\t\t\tname:    \"invalid format with spaces\",\n\t\t\tinput:   \"1 . 2 . 3\",\n\t\t\twantErr: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := Parse(tt.input)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"Parse() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !tt.wantErr && (got.Model != tt.want.Model ||\n\t\t\t\tgot.Revision != tt.want.Revision ||\n\t\t\t\tgot.Addition != tt.want.Addition) {\n\t\t\t\tt.Errorf(\"Parse() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSchemaVer_Valid(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tschema   SchemaVer\n\t\texpected bool\n\t}{\n\t\t{\n\t\t\tname: \"valid schema version - all positive\",\n\t\t\tschema: SchemaVer{\n\t\t\t\tModel:    1,\n\t\t\t\tRevision: 1,\n\t\t\t\tAddition: 1,\n\t\t\t},\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname: \"valid schema version - zero revision and addition\",\n\t\t\tschema: SchemaVer{\n\t\t\t\tModel:    1,\n\t\t\t\tRevision: 0,\n\t\t\t\tAddition: 0,\n\t\t\t},\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname: \"invalid - zero model\",\n\t\t\tschema: SchemaVer{\n\t\t\t\tModel:    0,\n\t\t\t\tRevision: 1,\n\t\t\t\tAddition: 1,\n\t\t\t},\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname: \"invalid - negative model\",\n\t\t\tschema: SchemaVer{\n\t\t\t\tModel:    -1,\n\t\t\t\tRevision: 1,\n\t\t\t\tAddition: 1,\n\t\t\t},\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname: \"invalid - negative revision\",\n\t\t\tschema: SchemaVer{\n\t\t\t\tModel:    1,\n\t\t\t\tRevision: -1,\n\t\t\t\tAddition: 1,\n\t\t\t},\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname: \"invalid - negative addition\",\n\t\t\tschema: SchemaVer{\n\t\t\t\tModel:    1,\n\t\t\t\tRevision: 1,\n\t\t\t\tAddition: -1,\n\t\t\t},\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname: \"invalid - all negative\",\n\t\t\tschema: SchemaVer{\n\t\t\t\tModel:    -1,\n\t\t\t\tRevision: -1,\n\t\t\t\tAddition: -1,\n\t\t\t},\n\t\t\texpected: false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tassert.Equal(t, tt.expected, tt.schema.Valid())\n\t\t})\n\t}\n}\n\nfunc TestSchemaVer_String(t *testing.T) {\n\ttests := []struct {\n\t\tname   string\n\t\tschema SchemaVer\n\t\twant   string\n\t}{\n\t\t{\n\t\t\tname:   \"basic version\",\n\t\t\tschema: New(1, 2, 3),\n\t\t\twant:   \"v1.2.3\",\n\t\t},\n\t\t{\n\t\t\tname:   \"version with zeros\",\n\t\t\tschema: New(1, 0, 0),\n\t\t\twant:   \"v1.0.0\",\n\t\t},\n\t\t{\n\t\t\tname:   \"large numbers\",\n\t\t\tschema: New(999, 888, 777),\n\t\t\twant:   \"v999.888.777\",\n\t\t},\n\t\t{\n\t\t\tname:   \"single digits\",\n\t\t\tschema: New(5, 4, 3),\n\t\t\twant:   \"v5.4.3\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tassert.Equal(t, tt.want, tt.schema.String())\n\t\t})\n\t}\n}\n\nfunc TestSchemaVer_MarshalJSON(t *testing.T) {\n\ttests := []struct {\n\t\tname   string\n\t\tschema SchemaVer\n\t\twant   string\n\t}{\n\t\t{\n\t\t\tname:   \"basic version\",\n\t\t\tschema: New(1, 2, 3),\n\t\t\twant:   `\"v1.2.3\"`,\n\t\t},\n\t\t{\n\t\t\tname:   \"version with zeros\",\n\t\t\tschema: New(1, 0, 0),\n\t\t\twant:   `\"v1.0.0\"`,\n\t\t},\n\t\t{\n\t\t\tname:   \"large numbers\",\n\t\t\tschema: New(999, 888, 777),\n\t\t\twant:   `\"v999.888.777\"`,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := tt.schema.MarshalJSON()\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, tt.want, string(got))\n\t\t})\n\t}\n}\n\nfunc TestSchemaVer_UnmarshalJSON(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\tinput   string\n\t\twant    SchemaVer\n\t\twantErr require.ErrorAssertionFunc\n\t}{\n\t\t{\n\t\t\tname:    \"valid version\",\n\t\t\tinput:   `\"v1.2.3\"`,\n\t\t\twant:    New(1, 2, 3),\n\t\t\twantErr: require.NoError,\n\t\t},\n\t\t{\n\t\t\tname:    \"valid version without v prefix\",\n\t\t\tinput:   `\"1.2.3\"`,\n\t\t\twant:    New(1, 2, 3),\n\t\t\twantErr: require.NoError,\n\t\t},\n\t\t{\n\t\t\tname:    \"valid version with zeros\",\n\t\t\tinput:   `\"v1.0.0\"`,\n\t\t\twant:    New(1, 0, 0),\n\t\t\twantErr: require.NoError,\n\t\t},\n\t\t{\n\t\t\tname:    \"invalid JSON format\",\n\t\t\tinput:   `{\"version\": \"v1.2.3\"}`,\n\t\t\twantErr: require.Error,\n\t\t},\n\t\t{\n\t\t\tname:    \"invalid version format\",\n\t\t\tinput:   `\"invalid\"`,\n\t\t\twantErr: require.Error,\n\t\t},\n\t\t{\n\t\t\tname:    \"invalid zero model\",\n\t\t\tinput:   `\"v0.1.2\"`,\n\t\t\twantErr: require.Error,\n\t\t},\n\t\t{\n\t\t\tname:    \"malformed JSON\",\n\t\t\tinput:   `\"v1.2.3`,\n\t\t\twantErr: require.Error,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvar got SchemaVer\n\t\t\terr := json.Unmarshal([]byte(tt.input), &got)\n\t\t\ttt.wantErr(t, err)\n\t\t\tif err == nil {\n\t\t\t\tassert.Equal(t, tt.want, got)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSchemaVer_JSONRoundTrip(t *testing.T) {\n\ttests := []struct {\n\t\tname   string\n\t\tschema SchemaVer\n\t}{\n\t\t{\n\t\t\tname:   \"basic version\",\n\t\t\tschema: New(1, 2, 3),\n\t\t},\n\t\t{\n\t\t\tname:   \"version with zeros\",\n\t\t\tschema: New(1, 0, 0),\n\t\t},\n\t\t{\n\t\t\tname:   \"large numbers\",\n\t\t\tschema: New(999, 888, 777),\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\t// marshal\n\t\t\tdata, err := json.Marshal(tt.schema)\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// unmarshal\n\t\t\tvar got SchemaVer\n\t\t\terr = json.Unmarshal(data, &got)\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// should be equal\n\t\t\tassert.Equal(t, tt.schema, got)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/stringutil/color.go",
    "content": "package stringutil\n\nimport \"fmt\"\n\nconst (\n\tDefaultColor Color = iota + 30\n\tRed\n\tGreen\n\tYellow\n\tBlue\n\tMagenta\n\tCyan\n\tWhite\n)\n\ntype Color uint8\n\n// TODO: not cross platform (windows...)\nfunc (c Color) Format(s string) string {\n\treturn fmt.Sprintf(\"\\x1b[%dm%s\\x1b[0m\", c, s)\n}\n"
  },
  {
    "path": "internal/stringutil/parse.go",
    "content": "package stringutil\n\nimport \"regexp\"\n\n// MatchCaptureGroups takes a regular expression and string and returns all of the named capture group results in a map.\nfunc MatchCaptureGroups(regEx *regexp.Regexp, str string) map[string]string {\n\tmatch := regEx.FindStringSubmatch(str)\n\tresults := make(map[string]string)\n\tfor i, name := range regEx.SubexpNames() {\n\t\tif i > 0 && i <= len(match) {\n\t\t\tresults[name] = match[i]\n\t\t}\n\t}\n\treturn results\n}\n"
  },
  {
    "path": "internal/stringutil/string_helpers.go",
    "content": "package stringutil\n\nimport (\n\t\"sort\"\n\t\"strings\"\n)\n\n// HasAnyOfSuffixes returns an indication if the given string has any of the given suffixes.\nfunc HasAnyOfSuffixes(input string, suffixes ...string) bool {\n\tfor _, suffix := range suffixes {\n\t\tif strings.HasSuffix(input, suffix) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// HasAnyOfPrefixes returns an indication if the given string has any of the given prefixes.\nfunc HasAnyOfPrefixes(input string, prefixes ...string) bool {\n\tfor _, prefix := range prefixes {\n\t\tif strings.HasPrefix(input, prefix) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// SplitCommaSeparatedString returns a slice of strings separated from the input string by commas\nfunc SplitCommaSeparatedString(input string) []string {\n\toutput := make([]string, 0)\n\tfor _, inputItem := range strings.Split(input, \",\") {\n\t\tif len(inputItem) > 0 {\n\t\t\toutput = append(output, inputItem)\n\t\t}\n\t}\n\treturn output\n}\n\n// SplitOnFirstString splits the input string on the first occurrence of any of the provided separators.\nfunc SplitOnFirstString(s string, separators ...string) (before, after string) {\n\tminIdx := len(s)\n\tfoundSep := \"\"\n\n\tfor _, sep := range separators {\n\t\tif idx := strings.Index(s, sep); idx != -1 && idx < minIdx {\n\t\t\tminIdx = idx\n\t\t\tfoundSep = sep\n\t\t}\n\t}\n\n\tif foundSep == \"\" {\n\t\treturn s, \"\"\n\t}\n\n\treturn s[:minIdx], s[minIdx+len(foundSep):]\n}\n\nfunc SplitOnAny(s string, separators ...string) []string {\n\tif s == \"\" {\n\t\treturn nil\n\t}\n\tparts := []string{s}\n\n\t// sort separators by length in descending order to ensure longer separators are processed first.\n\t// This isn't foolproof, but it helps with common cases where longer separators should take precedence.\n\tseparators = append([]string{}, separators...)\n\tsort.Slice(separators, func(i, j int) bool {\n\t\treturn len(separators[i]) > len(separators[j])\n\t})\n\n\tfor _, sep := range separators {\n\t\tvar newParts []string\n\t\tfor _, part := range parts {\n\t\t\tnewParts = append(newParts, strings.Split(part, sep)...)\n\t\t}\n\t\tparts = newParts\n\t}\n\treturn parts\n}\n"
  },
  {
    "path": "internal/stringutil/string_helpers_test.go",
    "content": "package stringutil\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestHasAnyOfSuffixes(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    string\n\t\tsuffixes []string\n\t\texpected bool\n\t}{\n\t\t{\n\t\t\tname:  \"go case\",\n\t\t\tinput: \"this has something\",\n\t\t\tsuffixes: []string{\n\t\t\t\t\"has something\",\n\t\t\t\t\"has NOT something\",\n\t\t\t},\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:  \"no match\",\n\t\t\tinput: \"this has something\",\n\t\t\tsuffixes: []string{\n\t\t\t\t\"has NOT something\",\n\t\t\t},\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"empty\",\n\t\t\tinput:    \"this has something\",\n\t\t\tsuffixes: []string{},\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:  \"positive match last\",\n\t\t\tinput: \"this has something\",\n\t\t\tsuffixes: []string{\n\t\t\t\t\"that does not have\",\n\t\t\t\t\"something\",\n\t\t\t},\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:  \"empty input\",\n\t\t\tinput: \"\",\n\t\t\tsuffixes: []string{\n\t\t\t\t\"that does not have\",\n\t\t\t\t\"this has\",\n\t\t\t},\n\t\t\texpected: false,\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tassert.Equal(t, test.expected, HasAnyOfSuffixes(test.input, test.suffixes...))\n\t\t})\n\t}\n}\n\nfunc TestHasAnyOfPrefixes(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    string\n\t\tprefixes []string\n\t\texpected bool\n\t}{\n\t\t{\n\t\t\tname:  \"go case\",\n\t\t\tinput: \"this has something\",\n\t\t\tprefixes: []string{\n\t\t\t\t\"this has\",\n\t\t\t\t\"that does not have\",\n\t\t\t},\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:  \"no match\",\n\t\t\tinput: \"this has something\",\n\t\t\tprefixes: []string{\n\t\t\t\t\"this DOES NOT has\",\n\t\t\t\t\"that does not have\",\n\t\t\t},\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"empty\",\n\t\t\tinput:    \"this has something\",\n\t\t\tprefixes: []string{},\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:  \"positive match last\",\n\t\t\tinput: \"this has something\",\n\t\t\tprefixes: []string{\n\t\t\t\t\"that does not have\",\n\t\t\t\t\"this has\",\n\t\t\t},\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:  \"empty input\",\n\t\t\tinput: \"\",\n\t\t\tprefixes: []string{\n\t\t\t\t\"that does not have\",\n\t\t\t\t\"this has\",\n\t\t\t},\n\t\t\texpected: false,\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tassert.Equal(t, test.expected, HasAnyOfPrefixes(test.input, test.prefixes...))\n\t\t})\n\t}\n}\n\nfunc TestSplitCommaSeparatedString(t *testing.T) {\n\ttests := []struct {\n\t\tinput    string\n\t\texpected []string\n\t}{\n\t\t{\n\t\t\tinput:    \"testing\",\n\t\t\texpected: []string{\"testing\"},\n\t\t},\n\t\t{\n\t\t\tinput:    \"\",\n\t\t\texpected: []string{},\n\t\t},\n\t\t{\n\t\t\tinput:    \"testing1,testing2\",\n\t\t\texpected: []string{\"testing1\", \"testing2\"},\n\t\t},\n\t\t{\n\t\t\tinput:    \"testing1,,testing2,testing3\",\n\t\t\texpected: []string{\"testing1\", \"testing2\", \"testing3\"},\n\t\t},\n\t\t{\n\t\t\tinput:    \"testing1,testing2,,\",\n\t\t\texpected: []string{\"testing1\", \"testing2\"},\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.input, func(t *testing.T) {\n\t\t\tassert.Equal(t, test.expected, SplitCommaSeparatedString(test.input))\n\t\t})\n\t}\n}\n\nfunc TestSplitOnFirstString(t *testing.T) {\n\ttests := []struct {\n\t\tname       string\n\t\tinput      string\n\t\tseparators []string\n\t\twantBefore string\n\t\twantAfter  string\n\t}{\n\t\t// go cases\n\t\t{\n\t\t\tname:       \"single separator found\",\n\t\t\tinput:      \"key=value\",\n\t\t\tseparators: []string{\"=\"},\n\t\t\twantBefore: \"key\",\n\t\t\twantAfter:  \"value\",\n\t\t},\n\t\t{\n\t\t\tname:       \"multiple separators, first one wins\",\n\t\t\tinput:      \"protocol://host:port\",\n\t\t\tseparators: []string{\"://\", \":\"},\n\t\t\twantBefore: \"protocol\",\n\t\t\twantAfter:  \"host:port\",\n\t\t},\n\t\t{\n\t\t\tname:       \"multiple separators, earlier position wins\",\n\t\t\tinput:      \"name:value=data\",\n\t\t\tseparators: []string{\"=\", \":\"},\n\t\t\twantBefore: \"name\",\n\t\t\twantAfter:  \"value=data\",\n\t\t},\n\t\t// edge cases\n\t\t{\n\t\t\tname:       \"no separator found\",\n\t\t\tinput:      \"noseparator\",\n\t\t\tseparators: []string{\"=\", \":\"},\n\t\t\twantBefore: \"noseparator\",\n\t\t\twantAfter:  \"\",\n\t\t},\n\t\t{\n\t\t\tname:       \"empty input\",\n\t\t\tinput:      \"\",\n\t\t\tseparators: []string{\"=\"},\n\t\t\twantBefore: \"\",\n\t\t\twantAfter:  \"\",\n\t\t},\n\t\t{\n\t\t\tname:       \"separator at beginning\",\n\t\t\tinput:      \"=value\",\n\t\t\tseparators: []string{\"=\"},\n\t\t\twantBefore: \"\",\n\t\t\twantAfter:  \"value\",\n\t\t},\n\t\t{\n\t\t\tname:       \"separator at end\",\n\t\t\tinput:      \"key=\",\n\t\t\tseparators: []string{\"=\"},\n\t\t\twantBefore: \"key\",\n\t\t\twantAfter:  \"\",\n\t\t},\n\t\t{\n\t\t\tname:       \"only separator\",\n\t\t\tinput:      \"=\",\n\t\t\tseparators: []string{\"=\"},\n\t\t\twantBefore: \"\",\n\t\t\twantAfter:  \"\",\n\t\t},\n\t\t// multiple occurrences\n\t\t{\n\t\t\tname:       \"multiple occurrences of same separator\",\n\t\t\tinput:      \"a=b=c=d\",\n\t\t\tseparators: []string{\"=\"},\n\t\t\twantBefore: \"a\",\n\t\t\twantAfter:  \"b=c=d\",\n\t\t},\n\t\t{\n\t\t\tname:       \"multiple different separators, choose earliest\",\n\t\t\tinput:      \"a:b=c:d\",\n\t\t\tseparators: []string{\"=\", \":\"},\n\t\t\twantBefore: \"a\",\n\t\t\twantAfter:  \"b=c:d\",\n\t\t},\n\n\t\t// multi-character separators\n\t\t{\n\t\t\tname:       \"multi-character separator\",\n\t\t\tinput:      \"before::after\",\n\t\t\tseparators: []string{\"::\"},\n\t\t\twantBefore: \"before\",\n\t\t\twantAfter:  \"after\",\n\t\t},\n\t\t{\n\t\t\tname:       \"overlapping separators\",\n\t\t\tinput:      \"test:::data\",\n\t\t\tseparators: []string{\"::\", \":::\"},\n\t\t\twantBefore: \"test\",\n\t\t\twantAfter:  \":data\",\n\t\t},\n\t\t{\n\t\t\tname:       \"longer separator wins when at same position\",\n\t\t\tinput:      \"test:::data\",\n\t\t\tseparators: []string{\":::\", \"::\"},\n\t\t\twantBefore: \"test\",\n\t\t\twantAfter:  \"data\",\n\t\t},\n\t\t// more realistic cases\n\t\t{\n\t\t\tname:       \"URL parsing\",\n\t\t\tinput:      \"https://user:pass@host:8080/path?query=value\",\n\t\t\tseparators: []string{\"://\", \"@\", \":\", \"/\", \"?\", \"=\"},\n\t\t\twantBefore: \"https\",\n\t\t\twantAfter:  \"user:pass@host:8080/path?query=value\",\n\t\t},\n\t\t{\n\t\t\tname:       \"environment variable\",\n\t\t\tinput:      \"PATH=/usr/bin:/bin\",\n\t\t\tseparators: []string{\"=\", \":\"},\n\t\t\twantBefore: \"PATH\",\n\t\t\twantAfter:  \"/usr/bin:/bin\",\n\t\t},\n\t\t{\n\t\t\tname:       \"docker image tag\",\n\t\t\tinput:      \"registry.example.com/namespace/image:v1.0\",\n\t\t\tseparators: []string{\":\", \"/\"},\n\t\t\twantBefore: \"registry.example.com\",\n\t\t\twantAfter:  \"namespace/image:v1.0\",\n\t\t},\n\n\t\t// special characters\n\t\t{\n\t\t\tname:       \"unicode separators\",\n\t\t\tinput:      \"hello→world\",\n\t\t\tseparators: []string{\"→\"},\n\t\t\twantBefore: \"hello\",\n\t\t\twantAfter:  \"world\",\n\t\t},\n\t\t{\n\t\t\tname:       \"whitespace separators\",\n\t\t\tinput:      \"word1 word2\\tword3\",\n\t\t\tseparators: []string{\" \", \"\\t\"},\n\t\t\twantBefore: \"word1\",\n\t\t\twantAfter:  \"word2\\tword3\",\n\t\t},\n\t\t// co separators provided\n\t\t{\n\t\t\tname:       \"no separators provided\",\n\t\t\tinput:      \"test=data\",\n\t\t\tseparators: []string{},\n\t\t\twantBefore: \"test=data\",\n\t\t\twantAfter:  \"\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgotBefore, gotAfter := SplitOnFirstString(tt.input, tt.separators...)\n\n\t\t\tif gotBefore != tt.wantBefore {\n\t\t\t\tt.Errorf(\"SplitOnFirstString() gotBefore = %q, want %q\", gotBefore, tt.wantBefore)\n\t\t\t}\n\t\t\tif gotAfter != tt.wantAfter {\n\t\t\t\tt.Errorf(\"SplitOnFirstString() gotAfter = %q, want %q\", gotAfter, tt.wantAfter)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSplitOnAny(t *testing.T) {\n\ttests := []struct {\n\t\tname       string\n\t\tinput      string\n\t\tseparators []string\n\t\texpected   []string\n\t}{\n\t\t{\n\t\t\tname:       \"empty string\",\n\t\t\tinput:      \"\",\n\t\t\tseparators: []string{\",\"},\n\t\t\texpected:   nil,\n\t\t},\n\t\t{\n\t\t\tname:       \"single separator\",\n\t\t\tinput:      \"a,b,c\",\n\t\t\tseparators: []string{\",\"},\n\t\t\texpected:   []string{\"a\", \"b\", \"c\"},\n\t\t},\n\t\t{\n\t\t\tname:       \"multiple separators\",\n\t\t\tinput:      \"a,b;c:d\",\n\t\t\tseparators: []string{\",\", \";\", \":\"},\n\t\t\texpected:   []string{\"a\", \"b\", \"c\", \"d\"},\n\t\t},\n\t\t{\n\t\t\tname:       \"no separators found\",\n\t\t\tinput:      \"hello\",\n\t\t\tseparators: []string{\",\", \";\"},\n\t\t\texpected:   []string{\"hello\"},\n\t\t},\n\t\t{\n\t\t\tname:       \"consecutive separators\",\n\t\t\tinput:      \"a,,b\",\n\t\t\tseparators: []string{\",\"},\n\t\t\texpected:   []string{\"a\", \"\", \"b\"},\n\t\t},\n\t\t{\n\t\t\tname:       \"separator at beginning\",\n\t\t\tinput:      \",a,b\",\n\t\t\tseparators: []string{\",\"},\n\t\t\texpected:   []string{\"\", \"a\", \"b\"},\n\t\t},\n\t\t{\n\t\t\tname:       \"separator at end\",\n\t\t\tinput:      \"a,b,\",\n\t\t\tseparators: []string{\",\"},\n\t\t\texpected:   []string{\"a\", \"b\", \"\"},\n\t\t},\n\t\t{\n\t\t\tname:       \"only separators\",\n\t\t\tinput:      \",,\",\n\t\t\tseparators: []string{\",\"},\n\t\t\texpected:   []string{\"\", \"\", \"\"},\n\t\t},\n\t\t{\n\t\t\tname:       \"overlapping separators\",\n\t\t\tinput:      \"a,b;c,d\",\n\t\t\tseparators: []string{\",\", \";\"},\n\t\t\texpected:   []string{\"a\", \"b\", \"c\", \"d\"},\n\t\t},\n\t\t{\n\t\t\tname:       \"separator is substring of another\",\n\t\t\tinput:      \"a::b:c\",\n\t\t\tseparators: []string{\"::\", \":\"},\n\t\t\texpected:   []string{\"a\", \"b\", \"c\"},\n\t\t},\n\t\t{\n\t\t\tname:       \"order does not matter for overlapping\",\n\t\t\tinput:      \"a::b:c\",\n\t\t\tseparators: []string{\":\", \"::\"},\n\t\t\texpected:   []string{\"a\", \"b\", \"c\"},\n\t\t},\n\t\t{\n\t\t\tname:       \"no separators provided\",\n\t\t\tinput:      \"hello\",\n\t\t\tseparators: []string{},\n\t\t\texpected:   []string{\"hello\"},\n\t\t},\n\t\t{\n\t\t\tname:       \"multi-character separator\",\n\t\t\tinput:      \"a<->b<->c\",\n\t\t\tseparators: []string{\"<->\"},\n\t\t\texpected:   []string{\"a\", \"b\", \"c\"},\n\t\t},\n\t\t{\n\t\t\tname:       \"mixed single and multi-character separators\",\n\t\t\tinput:      \"a,b<->c;d\",\n\t\t\tseparators: []string{\",\", \"<->\", \";\"},\n\t\t\texpected:   []string{\"a\", \"b\", \"c\", \"d\"},\n\t\t},\n\t\t{\n\t\t\tname:       \"space separator\",\n\t\t\tinput:      \"hello world test\",\n\t\t\tseparators: []string{\" \"},\n\t\t\texpected:   []string{\"hello\", \"world\", \"test\"},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := SplitOnAny(tt.input, tt.separators...)\n\t\t\tassert.Equal(t, tt.expected, result)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/stringutil/stringset.go",
    "content": "package stringutil\n\ntype StringSet map[string]struct{}\n\nfunc NewStringSet() StringSet {\n\treturn make(StringSet)\n}\n\nfunc NewStringSetFromSlice(start []string) StringSet {\n\tret := make(StringSet)\n\tfor _, s := range start {\n\t\tret.Add(s)\n\t}\n\treturn ret\n}\n\nfunc (s StringSet) Add(i string) {\n\ts[i] = struct{}{}\n}\n\nfunc (s StringSet) Remove(i string) {\n\tdelete(s, i)\n}\n\nfunc (s StringSet) Contains(i string) bool {\n\t_, ok := s[i]\n\treturn ok\n}\n\nfunc (s StringSet) ToSlice() []string {\n\tret := make([]string, len(s))\n\tidx := 0\n\tfor v := range s {\n\t\tret[idx] = v\n\t\tidx++\n\t}\n\treturn ret\n}\n"
  },
  {
    "path": "internal/stringutil/tprint.go",
    "content": "package stringutil\n\nimport (\n\t\"bytes\"\n\t\"text/template\"\n)\n\n// Tprintf renders a string from a given template string and field values\nfunc Tprintf(tmpl string, data map[string]interface{}) string {\n\tt := template.Must(template.New(\"\").Parse(tmpl))\n\tbuf := &bytes.Buffer{}\n\tif err := t.Execute(buf, data); err != nil {\n\t\treturn \"\"\n\t}\n\treturn buf.String()\n}\n"
  },
  {
    "path": "internal/testutils/golden_file.go",
    "content": "package testutils\n\nimport (\n\t\"io\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n)\n\nconst (\n\tTestDataDir       = \"testdata\"\n\tGoldenFileDirName = \"snapshot\"\n\tGoldenFileExt     = \".golden\"\n\tGoldenFileDirPath = TestDataDir + string(filepath.Separator) + GoldenFileDirName\n)\n\n// dangerText wraps text in ANSI escape codes for reverse red to make it highly visible.\nfunc dangerText(s string) string {\n\treturn \"\\033[7;31m\" + s + \"\\033[0m\"\n}\n\nfunc GetGoldenFilePath(t *testing.T) string {\n\tt.Helper()\n\t// when using table-driven-tests, the `t.Name()` results in a string with slashes\n\t// which makes it impossible to reference in a filesystem, producing a \"No such file or directory\"\n\tfilename := strings.ReplaceAll(t.Name(), \"/\", \"_\")\n\treturn path.Join(GoldenFileDirPath, filename+GoldenFileExt)\n}\n\nfunc UpdateGoldenFileContents(t *testing.T, contents []byte) {\n\tt.Helper()\n\n\tgoldenFilePath := GetGoldenFilePath(t)\n\n\tt.Log(dangerText(\"!!! UPDATING GOLDEN FILE !!!\"), goldenFilePath)\n\n\terr := os.WriteFile(goldenFilePath, contents, 0600)\n\tif err != nil {\n\t\tt.Fatalf(\"could not update golden file (%s): %+v\", goldenFilePath, err)\n\t}\n}\n\nfunc GetGoldenFileContents(t *testing.T) []byte {\n\tt.Helper()\n\n\tgoldenPath := GetGoldenFilePath(t)\n\tif !fileOrDirExists(t, goldenPath) {\n\t\tt.Fatalf(\"golden file does not exist: %s\", goldenPath)\n\t}\n\tf, err := os.Open(goldenPath)\n\tif err != nil {\n\t\tt.Fatalf(\"could not open file (%s): %+v\", goldenPath, err)\n\t}\n\tdefer f.Close()\n\n\tbytes, err := io.ReadAll(f)\n\tif err != nil {\n\t\tt.Fatalf(\"could not read file (%s): %+v\", goldenPath, err)\n\t}\n\treturn bytes\n}\n\nfunc fileOrDirExists(t *testing.T, filename string) bool {\n\tt.Helper()\n\t_, err := os.Stat(filename)\n\treturn !os.IsNotExist(err)\n}\n"
  },
  {
    "path": "llms.txt",
    "content": "# Grype\n\nGrype is a vulnerability scanner for container images and filesystems developed by Anchore. It easily finds vulnerabilities for major operating system packages and language-specific packages.\n\n## Key Features\n\n- Scans container images, filesystems, and SBOMs for known vulnerabilities\n- Supports major Linux distributions (Alpine, Ubuntu, Debian, RHEL, CentOS, etc.)\n- Language support for Java, JavaScript, Python, Go, Ruby, Rust, .NET, PHP, and more\n- Works with Docker, OCI, and Singularity image formats\n- Integrates with Syft for SBOM generation\n- Supports VEX (Vulnerability Exploitability Exchange) for filtering results\n- Risk scoring with EPSS (Exploit Prediction Scoring System) and CVSS metrics\n\n## Architecture\n\n- Written in Go\n- Uses SQLite for vulnerability database storage\n- Modular matcher system for different package types and ecosystems\n- Automatic database updates from multiple vulnerability sources\n- CLI-first design with multiple output formats (table, JSON, SARIF, CycloneDX)\n\n## Main Components\n\n- `cmd/grype/` - CLI application entry point\n- `grype/` - Core library with matchers, database, and scanning logic\n- `grype/matcher/` - Package-specific vulnerability matchers\n- `grype/db/` - Database management and vulnerability storage\n- `grype/pkg/` - Package identification and metadata\n- `grype/presenter/` - Output formatting (JSON, table, SARIF, etc.)\n\n## Usage\n\nBasic vulnerability scan:\n```bash\ngrype <image>\n```\n\nScan with SBOM:\n```bash\ngrype sbom:./sbom.json\n```\n\nThe tool automatically manages its vulnerability database and provides configurable output formats and filtering options."
  },
  {
    "path": "schema/grype/db/README.md",
    "content": "# Grype v6 Database Schemas\n\nThis directory contains the schemas for the Grype v6 vulnerability database. These schemas are automatically generated from the Go code definitions and are used to track schema evolution over time.\n\n## What Are These Schemas?\n\nThe Grype database has two types of schemas that are tracked:\n\n### 1. SQL Schema (`sql/`)\nThe SQLite table definitions (CREATE TABLE and CREATE INDEX statements) generated from GORM models in `grype/db/v6/models.go`. This captures:\n- Database tables and columns\n- Foreign key relationships\n- Indexes for query performance\n- All structural aspects of the database\n\n**Example tables:** `vulnerability_handles`, `packages`, `operating_systems`, `cpes`, `blobs`, etc.\n\n### 2. Blob JSON Schema (`blob/json/`)\nA unified JSON schema for all blob types stored in the `blobs` table. The blobs are JSON data referenced by various handle tables and include:\n- `VulnerabilityBlob`: Core vulnerability advisory data\n- `PackageBlob`: Package version ranges and fix information\n- `KnownExploitedVulnerabilityBlob`: CISA KEV catalog data\n\n## Schema Files\n\nEach schema type has two files per version:\n\n- **`schema-X.Y.Z.{sql|json}`**: The versioned schema file that should never be modified after creation\n- **`schema-latest.{sql|json}`**: A copy of the most recent version to show diffs in PR reviews\n\nThe `-latest` files exist to make PR reviews easier. When you increment the schema version and regenerate, Git shows the new `schema-X.Y.Z` file as entirely new (just additions), which makes it hard to see what actually changed. The `-latest` file, however, is already tracked by Git, so it shows as a **diff** - making it easy to review exactly what changed in the schema.\n\n## How to Regenerate Schemas\n\nWhen you make changes to:\n- GORM models in `grype/db/v6/models.go`\n- Blob types in `grype/db/v6/blobs.go`\n\nYou need to regenerate the schemas:\n\n```bash\ntask generate-db-schema\n```\n\nThis will:\n1. Create an in-memory SQLite database with all GORM models\n2. Extract the SQL schema from the database\n3. Generate a unified JSON schema for all blob types\n4. Write the schemas to versioned files\n\n## What to Do When Schema Changes\n\n### If You're Adding Compatible Changes (Addition)\nExamples: Adding a new optional field to a blob, adding a new index\n\n1. Increment `Addition` in `grype/db/v6/db.go`:\n   ```go\n   Addition = 2  // was 1\n   ```\n\n2. Regenerate schemas:\n   ```bash\n   task generate-db-schema\n   ```\n\n3. Commit the new schema files along with your code changes\n\n### If You're Making Potentially Breaking Changes (Revision)\nExamples: Changing field types, removing optional fields, altering table structure\n\n1. Increment `Revision` in `grype/db/v6/db.go` and reset `Addition`:\n   ```go\n   Revision = 2  // was 1\n   Addition = 0  // reset\n   ```\n\n2. Regenerate and commit as above\n\n### If You're Making Definitely Breaking Changes (Model)\n\nPlease meet with the team about this - it requires careful planning and\nshould be rare.\n\n## Versioning Rules (SchemaVer)\n\nThis project uses [SchemaVer](https://docs.snowplowanalytics.com/docs/pipeline-components-and-applications/iglu/common-architecture/schemaver/) for schema versioning: `MODEL.REVISION.ADDITION`\n\n- **MODEL**: Increment for breaking changes that prevent interaction with ALL historical data\n- **REVISION**: Increment for changes that may prevent interaction with SOME historical data\n- **ADDITION**: Increment for changes that are compatible with ALL historical data\n\n**Important:** Never delete or modify existing versioned schema files! Only add new versions.\n\n## CI Drift Detection\n\nThe static analysis CI check runs:\n\n```bash\ntask check-db-schema-drift\n```\n\nThis:\n1. Checks that working directory is clean\n2. Runs `task generate-db-schema`\n3. Checks if any schema files changed\n\nIf schemas changed but weren't committed, the check fails. This ensures:\n- Schema changes are always tracked\n- Version numbers are incremented appropriately\n- Code changes and schema changes stay in sync\n\nThis catches cases where you modified models or blob types but forgot to regenerate and commit the schemas.\n\n## Common Errors\n\n### \"Cowardly refusing to overwrite existing schema\"\n\nThis means:\n- The schema has changed (code differs from committed schema)\n- But the version number hasn't been incremented\n\n**Solution:** Increment the appropriate version constant in `grype/db/v6/db.go`\n\n### \"Database blob schemas have uncommitted changes\"\n\nThis means:\n- You made schema changes\n- Regenerated the schemas\n- But haven't committed the new schema files\n\n**Solution:** Add and commit the schema files in `schema/grype/db/`\n\n## More Information\n\n- **Generator Code**: `grype/db/v6/schema/main.go` - The Go program that generates these schemas\n- **Version Constants**: `grype/db/v6/db.go` - Where `ModelVersion`, `Revision`, and `Addition` are defined\n- **GORM Models**: `grype/db/v6/models.go` - The source for SQL schema generation\n- **Blob Types**: `grype/db/v6/blobs.go` - The source for blob JSON schema generation\n"
  },
  {
    "path": "schema/grype/db/blob/json/schema-6.1.1.json",
    "content": "{\n  \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n  \"$id\": \"anchore.io/schema/grype/db/blob/json/6.1.1\",\n  \"$defs\": {\n    \"Fix\": {\n      \"$defs\": {\n        \"detail\": {\n          \"description\": \"provides additional fix information, such as commit details.\"\n        },\n        \"state\": {\n          \"description\": \"represents the status of the fix (e.g., 'fixed', 'unaffected').\"\n        },\n        \"version\": {\n          \"description\": \"is the version number of the fix.\"\n        }\n      },\n      \"properties\": {\n        \"version\": {\n          \"type\": \"string\"\n        },\n        \"state\": {\n          \"type\": \"string\"\n        },\n        \"detail\": {\n          \"$ref\": \"#/$defs/FixDetail\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"FixAvailability\": {\n      \"$defs\": {\n        \"date\": {\n          \"description\": \"is the date and time when fix information became available. Note: this might not be when the fix was created, committed or merged.\"\n        },\n        \"kind\": {\n          \"description\": \"describes how this date was obtained (e.g. advisory, release, commit, PR, issue, first-observed-record)\"\n        }\n      },\n      \"properties\": {\n        \"date\": {\n          \"type\": \"string\",\n          \"format\": \"date-time\"\n        },\n        \"kind\": {\n          \"type\": \"string\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"FixDetail\": {\n      \"$defs\": {\n        \"available\": {\n          \"description\": \"indicates when the fix information became available and how it was obtained.\"\n        },\n        \"references\": {\n          \"description\": \"contains URLs or identifiers for additional resources on the fix.\"\n        }\n      },\n      \"properties\": {\n        \"available\": {\n          \"$ref\": \"#/$defs/FixAvailability\"\n        },\n        \"references\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/Reference\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"KnownExploitedVulnerabilityBlob\": {\n      \"properties\": {\n        \"cve\": {\n          \"type\": \"string\"\n        },\n        \"vendor_project\": {\n          \"type\": \"string\"\n        },\n        \"product\": {\n          \"type\": \"string\"\n        },\n        \"date_added\": {\n          \"type\": \"string\",\n          \"format\": \"date-time\"\n        },\n        \"required_action\": {\n          \"type\": \"string\"\n        },\n        \"due_date\": {\n          \"type\": \"string\",\n          \"format\": \"date-time\"\n        },\n        \"known_ransomware_campaign_use\": {\n          \"type\": \"string\"\n        },\n        \"notes\": {\n          \"type\": \"string\"\n        },\n        \"urls\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        },\n        \"cwes\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"cve\"\n      ]\n    },\n    \"PackageBlob\": {\n      \"$defs\": {\n        \"cves\": {\n          \"description\": \"is a list of Common Vulnerabilities and Exposures (CVE) identifiers related to this vulnerability.\"\n        },\n        \"qualifiers\": {\n          \"description\": \"are package attributes that confirm the package is affected by the vulnerability.\"\n        },\n        \"ranges\": {\n          \"description\": \"specifies the affected version ranges and fixes if available.\"\n        }\n      },\n      \"properties\": {\n        \"cves\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        },\n        \"qualifiers\": {\n          \"$ref\": \"#/$defs/PackageQualifiers\"\n        },\n        \"ranges\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/Range\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"PackageQualifiers\": {\n      \"$defs\": {\n        \"platform_cpes\": {\n          \"description\": \"lists Common Platform Enumeration (CPE) identifiers for affected platforms.\"\n        },\n        \"rpm_modularity\": {\n          \"description\": \"indicates if the package follows RPM modularity for versioning.\"\n        }\n      },\n      \"properties\": {\n        \"rpm_modularity\": {\n          \"type\": \"string\"\n        },\n        \"platform_cpes\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"Range\": {\n      \"$defs\": {\n        \"fix\": {\n          \"description\": \"provides details on the fix version and its state if available.\"\n        },\n        \"version\": {\n          \"description\": \"defines the version constraints for affected software.\"\n        }\n      },\n      \"properties\": {\n        \"version\": {\n          \"$ref\": \"#/$defs/Version\"\n        },\n        \"fix\": {\n          \"$ref\": \"#/$defs/Fix\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"Reference\": {\n      \"$defs\": {\n        \"tags\": {\n          \"description\": \"is a free-form organizational field to convey additional information about the reference\"\n        },\n        \"url\": {\n          \"description\": \"is the external resource\"\n        }\n      },\n      \"properties\": {\n        \"url\": {\n          \"type\": \"string\"\n        },\n        \"tags\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"url\"\n      ]\n    },\n    \"Severity\": {\n      \"$defs\": {\n        \"rank\": {\n          \"description\": \"is a free-form organizational field to convey priority over other severities\"\n        },\n        \"scheme\": {\n          \"description\": \"describes the quantitative method used to determine the Score, such as 'CVSS_V3'. Alternatively this makes\\nclaim that Value is qualitative, for example 'HML' (High, Medium, Low), CHMLN (critical-high-medium-low-negligible)\"\n        },\n        \"source\": {\n          \"description\": \"is the name of the source of the severity score (e.g. 'nvd@nist.gov' or 'security-advisories@github.com')\"\n        },\n        \"value\": {\n          \"description\": \"is the severity score (e.g. '7.5', 'CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N',  or 'high' )\"\n        }\n      },\n      \"properties\": {\n        \"scheme\": {\n          \"type\": \"string\"\n        },\n        \"value\": true,\n        \"source\": {\n          \"type\": \"string\"\n        },\n        \"rank\": {\n          \"type\": \"integer\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"scheme\",\n        \"value\",\n        \"rank\"\n      ]\n    },\n    \"Version\": {\n      \"$defs\": {\n        \"constraint\": {\n          \"description\": \"defines the version range constraint for affected versions.\"\n        },\n        \"type\": {\n          \"description\": \"specifies the versioning system used (e.g., 'semver', 'rpm').\"\n        }\n      },\n      \"properties\": {\n        \"type\": {\n          \"type\": \"string\"\n        },\n        \"constraint\": {\n          \"type\": \"string\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"VulnerabilityBlob\": {\n      \"$defs\": {\n        \"aliases\": {\n          \"description\": \"is a list of IDs of the same vulnerability in other databases, in the form of the ID field. This allows one database to claim that its own entry describes the same vulnerability as one or more entries in other databases.\"\n        },\n        \"assigner\": {\n          \"description\": \"is a list of names, email, or organizations who submitted the vulnerability\"\n        },\n        \"description\": {\n          \"description\": \"of the vulnerability as provided by the source\"\n        },\n        \"id\": {\n          \"description\": \"is the lowercase unique string identifier for the vulnerability relative to the provider\"\n        },\n        \"refs\": {\n          \"description\": \"are URLs to external resources that provide more information about the vulnerability\"\n        },\n        \"severities\": {\n          \"description\": \"is a list of severity indications (quantitative or qualitative) for the vulnerability\"\n        }\n      },\n      \"properties\": {\n        \"id\": {\n          \"type\": \"string\"\n        },\n        \"assigner\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        },\n        \"description\": {\n          \"type\": \"string\"\n        },\n        \"refs\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/Reference\"\n          },\n          \"type\": \"array\"\n        },\n        \"aliases\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        },\n        \"severities\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/Severity\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"id\"\n      ]\n    }\n  },\n  \"oneOf\": [\n    {\n      \"$ref\": \"#/$defs/VulnerabilityBlob\"\n    },\n    {\n      \"$ref\": \"#/$defs/PackageBlob\"\n    },\n    {\n      \"$ref\": \"#/$defs/KnownExploitedVulnerabilityBlob\"\n    }\n  ],\n  \"description\": \"Unified schema for all blob types stored in the Grype v6 database\"\n}\n"
  },
  {
    "path": "schema/grype/db/blob/json/schema-6.1.2.json",
    "content": "{\n  \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n  \"$id\": \"anchore.io/schema/grype/db/blob/json/6.1.2\",\n  \"$defs\": {\n    \"Fix\": {\n      \"$defs\": {\n        \"detail\": {\n          \"description\": \"provides additional fix information, such as commit details.\"\n        },\n        \"state\": {\n          \"description\": \"represents the status of the fix (e.g., 'fixed', 'unaffected').\"\n        },\n        \"version\": {\n          \"description\": \"is the version number of the fix.\"\n        }\n      },\n      \"properties\": {\n        \"version\": {\n          \"type\": \"string\"\n        },\n        \"state\": {\n          \"type\": \"string\"\n        },\n        \"detail\": {\n          \"$ref\": \"#/$defs/FixDetail\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"FixAvailability\": {\n      \"$defs\": {\n        \"date\": {\n          \"description\": \"is the date and time when fix information became available. Note: this might not be when the fix was created, committed or merged.\"\n        },\n        \"kind\": {\n          \"description\": \"describes how this date was obtained (e.g. advisory, release, commit, PR, issue, first-observed-record)\"\n        }\n      },\n      \"properties\": {\n        \"date\": {\n          \"type\": \"string\",\n          \"format\": \"date-time\"\n        },\n        \"kind\": {\n          \"type\": \"string\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"FixDetail\": {\n      \"$defs\": {\n        \"available\": {\n          \"description\": \"indicates when the fix information became available and how it was obtained.\"\n        },\n        \"references\": {\n          \"description\": \"contains URLs or identifiers for additional resources on the fix.\"\n        }\n      },\n      \"properties\": {\n        \"available\": {\n          \"$ref\": \"#/$defs/FixAvailability\"\n        },\n        \"references\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/Reference\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"KnownExploitedVulnerabilityBlob\": {\n      \"properties\": {\n        \"cve\": {\n          \"type\": \"string\"\n        },\n        \"vendor_project\": {\n          \"type\": \"string\"\n        },\n        \"product\": {\n          \"type\": \"string\"\n        },\n        \"date_added\": {\n          \"type\": \"string\",\n          \"format\": \"date-time\"\n        },\n        \"required_action\": {\n          \"type\": \"string\"\n        },\n        \"due_date\": {\n          \"type\": \"string\",\n          \"format\": \"date-time\"\n        },\n        \"known_ransomware_campaign_use\": {\n          \"type\": \"string\"\n        },\n        \"notes\": {\n          \"type\": \"string\"\n        },\n        \"urls\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        },\n        \"cwes\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"cve\"\n      ]\n    },\n    \"PackageBlob\": {\n      \"$defs\": {\n        \"cves\": {\n          \"description\": \"is a list of Common Vulnerabilities and Exposures (CVE) identifiers related to this vulnerability.\"\n        },\n        \"qualifiers\": {\n          \"description\": \"are package attributes that confirm the package is affected by the vulnerability.\"\n        },\n        \"ranges\": {\n          \"description\": \"specifies the affected version ranges and fixes if available.\"\n        }\n      },\n      \"properties\": {\n        \"cves\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        },\n        \"qualifiers\": {\n          \"$ref\": \"#/$defs/PackageQualifiers\"\n        },\n        \"ranges\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/Range\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"PackageQualifiers\": {\n      \"$defs\": {\n        \"platform_cpes\": {\n          \"description\": \"lists Common Platform Enumeration (CPE) identifiers for affected platforms.\"\n        },\n        \"rpm_modularity\": {\n          \"description\": \"indicates if the package follows RPM modularity for versioning.\"\n        }\n      },\n      \"properties\": {\n        \"rpm_modularity\": {\n          \"type\": \"string\"\n        },\n        \"platform_cpes\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"Range\": {\n      \"$defs\": {\n        \"fix\": {\n          \"description\": \"provides details on the fix version and its state if available.\"\n        },\n        \"version\": {\n          \"description\": \"defines the version constraints for affected software.\"\n        }\n      },\n      \"properties\": {\n        \"version\": {\n          \"$ref\": \"#/$defs/Version\"\n        },\n        \"fix\": {\n          \"$ref\": \"#/$defs/Fix\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"Reference\": {\n      \"$defs\": {\n        \"tags\": {\n          \"description\": \"is a free-form organizational field to convey additional information about the reference\"\n        },\n        \"url\": {\n          \"description\": \"is the external resource\"\n        }\n      },\n      \"properties\": {\n        \"url\": {\n          \"type\": \"string\"\n        },\n        \"tags\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"url\"\n      ]\n    },\n    \"Severity\": {\n      \"$defs\": {\n        \"rank\": {\n          \"description\": \"is a free-form organizational field to convey priority over other severities\"\n        },\n        \"scheme\": {\n          \"description\": \"describes the quantitative method used to determine the Score, such as 'CVSS_V3'. Alternatively this makes\\nclaim that Value is qualitative, for example 'HML' (High, Medium, Low), CHMLN (critical-high-medium-low-negligible)\"\n        },\n        \"source\": {\n          \"description\": \"is the name of the source of the severity score (e.g. 'nvd@nist.gov' or 'security-advisories@github.com')\"\n        },\n        \"value\": {\n          \"description\": \"is the severity score (e.g. '7.5', 'CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N',  or 'high' )\"\n        }\n      },\n      \"properties\": {\n        \"scheme\": {\n          \"type\": \"string\"\n        },\n        \"value\": true,\n        \"source\": {\n          \"type\": \"string\"\n        },\n        \"rank\": {\n          \"type\": \"integer\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"scheme\",\n        \"value\",\n        \"rank\"\n      ]\n    },\n    \"Version\": {\n      \"$defs\": {\n        \"constraint\": {\n          \"description\": \"defines the version range constraint for affected versions.\"\n        },\n        \"type\": {\n          \"description\": \"specifies the versioning system used (e.g., 'semver', 'rpm').\"\n        }\n      },\n      \"properties\": {\n        \"type\": {\n          \"type\": \"string\"\n        },\n        \"constraint\": {\n          \"type\": \"string\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"VulnerabilityBlob\": {\n      \"$defs\": {\n        \"aliases\": {\n          \"description\": \"is a list of IDs of the same vulnerability in other databases, in the form of the ID field. This allows one database to claim that its own entry describes the same vulnerability as one or more entries in other databases.\"\n        },\n        \"assigner\": {\n          \"description\": \"is a list of names, email, or organizations who submitted the vulnerability\"\n        },\n        \"description\": {\n          \"description\": \"of the vulnerability as provided by the source\"\n        },\n        \"id\": {\n          \"description\": \"is the lowercase unique string identifier for the vulnerability relative to the provider\"\n        },\n        \"refs\": {\n          \"description\": \"are URLs to external resources that provide more information about the vulnerability\"\n        },\n        \"severities\": {\n          \"description\": \"is a list of severity indications (quantitative or qualitative) for the vulnerability\"\n        }\n      },\n      \"properties\": {\n        \"id\": {\n          \"type\": \"string\"\n        },\n        \"assigner\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        },\n        \"description\": {\n          \"type\": \"string\"\n        },\n        \"refs\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/Reference\"\n          },\n          \"type\": \"array\"\n        },\n        \"aliases\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        },\n        \"severities\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/Severity\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"id\"\n      ]\n    }\n  },\n  \"oneOf\": [\n    {\n      \"$ref\": \"#/$defs/VulnerabilityBlob\"\n    },\n    {\n      \"$ref\": \"#/$defs/PackageBlob\"\n    },\n    {\n      \"$ref\": \"#/$defs/KnownExploitedVulnerabilityBlob\"\n    }\n  ],\n  \"description\": \"Unified schema for all blob types stored in the Grype v6 database\"\n}\n"
  },
  {
    "path": "schema/grype/db/blob/json/schema-6.1.3.json",
    "content": "{\n  \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n  \"$id\": \"anchore.io/schema/grype/db/blob/json/6.1.3\",\n  \"$defs\": {\n    \"Fix\": {\n      \"$defs\": {\n        \"detail\": {\n          \"description\": \"provides additional fix information, such as commit details.\"\n        },\n        \"state\": {\n          \"description\": \"represents the status of the fix (e.g., 'fixed', 'unaffected').\"\n        },\n        \"version\": {\n          \"description\": \"is the version number of the fix.\"\n        }\n      },\n      \"properties\": {\n        \"version\": {\n          \"type\": \"string\"\n        },\n        \"state\": {\n          \"type\": \"string\"\n        },\n        \"detail\": {\n          \"$ref\": \"#/$defs/FixDetail\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"FixAvailability\": {\n      \"$defs\": {\n        \"date\": {\n          \"description\": \"is the date and time when fix information became available. Note: this might not be when the fix was created, committed or merged.\"\n        },\n        \"kind\": {\n          \"description\": \"describes how this date was obtained (e.g. advisory, release, commit, PR, issue, first-observed-record)\"\n        }\n      },\n      \"properties\": {\n        \"date\": {\n          \"type\": \"string\",\n          \"format\": \"date-time\"\n        },\n        \"kind\": {\n          \"type\": \"string\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"FixDetail\": {\n      \"$defs\": {\n        \"available\": {\n          \"description\": \"indicates when the fix information became available and how it was obtained.\"\n        },\n        \"references\": {\n          \"description\": \"contains URLs or identifiers for additional resources on the fix.\"\n        }\n      },\n      \"properties\": {\n        \"available\": {\n          \"$ref\": \"#/$defs/FixAvailability\"\n        },\n        \"references\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/Reference\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"KnownExploitedVulnerabilityBlob\": {\n      \"properties\": {\n        \"cve\": {\n          \"type\": \"string\"\n        },\n        \"vendor_project\": {\n          \"type\": \"string\"\n        },\n        \"product\": {\n          \"type\": \"string\"\n        },\n        \"date_added\": {\n          \"type\": \"string\",\n          \"format\": \"date-time\"\n        },\n        \"required_action\": {\n          \"type\": \"string\"\n        },\n        \"due_date\": {\n          \"type\": \"string\",\n          \"format\": \"date-time\"\n        },\n        \"known_ransomware_campaign_use\": {\n          \"type\": \"string\"\n        },\n        \"notes\": {\n          \"type\": \"string\"\n        },\n        \"urls\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        },\n        \"cwes\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"cve\"\n      ]\n    },\n    \"PackageBlob\": {\n      \"$defs\": {\n        \"cves\": {\n          \"description\": \"is a list of Common Vulnerabilities and Exposures (CVE) identifiers related to this vulnerability.\"\n        },\n        \"qualifiers\": {\n          \"description\": \"are package attributes that confirm the package is affected by the vulnerability.\"\n        },\n        \"ranges\": {\n          \"description\": \"specifies the affected version ranges and fixes if available.\"\n        }\n      },\n      \"properties\": {\n        \"cves\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        },\n        \"qualifiers\": {\n          \"$ref\": \"#/$defs/PackageQualifiers\"\n        },\n        \"ranges\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/Range\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"PackageQualifiers\": {\n      \"$defs\": {\n        \"platform_cpes\": {\n          \"description\": \"lists Common Platform Enumeration (CPE) identifiers for affected platforms.\"\n        },\n        \"rpm_modularity\": {\n          \"description\": \"indicates if the package follows RPM modularity for versioning.\"\n        }\n      },\n      \"properties\": {\n        \"rpm_modularity\": {\n          \"type\": \"string\"\n        },\n        \"platform_cpes\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"Range\": {\n      \"$defs\": {\n        \"fix\": {\n          \"description\": \"provides details on the fix version and its state if available.\"\n        },\n        \"version\": {\n          \"description\": \"defines the version constraints for affected software.\"\n        }\n      },\n      \"properties\": {\n        \"version\": {\n          \"$ref\": \"#/$defs/Version\"\n        },\n        \"fix\": {\n          \"$ref\": \"#/$defs/Fix\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"Reference\": {\n      \"$defs\": {\n        \"id\": {\n          \"description\": \"is an optional identifier for the reference (e.g., advisory ID like 'RHSA-2023:5455')\"\n        },\n        \"tags\": {\n          \"description\": \"is a free-form organizational field to convey additional information about the reference\"\n        },\n        \"url\": {\n          \"description\": \"is the external resource\"\n        }\n      },\n      \"properties\": {\n        \"url\": {\n          \"type\": \"string\"\n        },\n        \"id\": {\n          \"type\": \"string\"\n        },\n        \"tags\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"url\"\n      ]\n    },\n    \"Severity\": {\n      \"$defs\": {\n        \"rank\": {\n          \"description\": \"is a free-form organizational field to convey priority over other severities\"\n        },\n        \"scheme\": {\n          \"description\": \"describes the quantitative method used to determine the Score, such as 'CVSS_V3'. Alternatively this makes\\nclaim that Value is qualitative, for example 'HML' (High, Medium, Low), CHMLN (critical-high-medium-low-negligible)\"\n        },\n        \"source\": {\n          \"description\": \"is the name of the source of the severity score (e.g. 'nvd@nist.gov' or 'security-advisories@github.com')\"\n        },\n        \"value\": {\n          \"description\": \"is the severity score (e.g. '7.5', 'CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N',  or 'high' )\"\n        }\n      },\n      \"properties\": {\n        \"scheme\": {\n          \"type\": \"string\"\n        },\n        \"value\": true,\n        \"source\": {\n          \"type\": \"string\"\n        },\n        \"rank\": {\n          \"type\": \"integer\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"scheme\",\n        \"value\",\n        \"rank\"\n      ]\n    },\n    \"Version\": {\n      \"$defs\": {\n        \"constraint\": {\n          \"description\": \"defines the version range constraint for affected versions.\"\n        },\n        \"type\": {\n          \"description\": \"specifies the versioning system used (e.g., 'semver', 'rpm').\"\n        }\n      },\n      \"properties\": {\n        \"type\": {\n          \"type\": \"string\"\n        },\n        \"constraint\": {\n          \"type\": \"string\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"VulnerabilityBlob\": {\n      \"$defs\": {\n        \"aliases\": {\n          \"description\": \"is a list of IDs of the same vulnerability in other databases, in the form of the ID field. This allows one database to claim that its own entry describes the same vulnerability as one or more entries in other databases.\"\n        },\n        \"assigner\": {\n          \"description\": \"is a list of names, email, or organizations who submitted the vulnerability\"\n        },\n        \"description\": {\n          \"description\": \"of the vulnerability as provided by the source\"\n        },\n        \"id\": {\n          \"description\": \"is the lowercase unique string identifier for the vulnerability relative to the provider\"\n        },\n        \"refs\": {\n          \"description\": \"are URLs to external resources that provide more information about the vulnerability\"\n        },\n        \"severities\": {\n          \"description\": \"is a list of severity indications (quantitative or qualitative) for the vulnerability\"\n        }\n      },\n      \"properties\": {\n        \"id\": {\n          \"type\": \"string\"\n        },\n        \"assigner\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        },\n        \"description\": {\n          \"type\": \"string\"\n        },\n        \"refs\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/Reference\"\n          },\n          \"type\": \"array\"\n        },\n        \"aliases\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        },\n        \"severities\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/Severity\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"id\"\n      ]\n    }\n  },\n  \"oneOf\": [\n    {\n      \"$ref\": \"#/$defs/VulnerabilityBlob\"\n    },\n    {\n      \"$ref\": \"#/$defs/PackageBlob\"\n    },\n    {\n      \"$ref\": \"#/$defs/KnownExploitedVulnerabilityBlob\"\n    }\n  ],\n  \"description\": \"Unified schema for all blob types stored in the Grype v6 database\"\n}\n"
  },
  {
    "path": "schema/grype/db/blob/json/schema-6.1.4.json",
    "content": "{\n  \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n  \"$id\": \"anchore.io/schema/grype/db/blob/json/6.1.4\",\n  \"$defs\": {\n    \"Fix\": {\n      \"$defs\": {\n        \"detail\": {\n          \"description\": \"provides additional fix information, such as commit details.\"\n        },\n        \"state\": {\n          \"description\": \"represents the status of the fix (e.g., 'fixed', 'unaffected').\"\n        },\n        \"version\": {\n          \"description\": \"is the version number of the fix.\"\n        }\n      },\n      \"properties\": {\n        \"version\": {\n          \"type\": \"string\"\n        },\n        \"state\": {\n          \"type\": \"string\"\n        },\n        \"detail\": {\n          \"$ref\": \"#/$defs/FixDetail\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"FixAvailability\": {\n      \"$defs\": {\n        \"date\": {\n          \"description\": \"is the date and time when fix information became available. Note: this might not be when the fix was created, committed or merged.\"\n        },\n        \"kind\": {\n          \"description\": \"describes how this date was obtained (e.g. advisory, release, commit, PR, issue, first-observed-record)\"\n        }\n      },\n      \"properties\": {\n        \"date\": {\n          \"type\": \"string\",\n          \"format\": \"date-time\"\n        },\n        \"kind\": {\n          \"type\": \"string\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"FixDetail\": {\n      \"$defs\": {\n        \"available\": {\n          \"description\": \"indicates when the fix information became available and how it was obtained.\"\n        },\n        \"references\": {\n          \"description\": \"contains URLs or identifiers for additional resources on the fix.\"\n        }\n      },\n      \"properties\": {\n        \"available\": {\n          \"$ref\": \"#/$defs/FixAvailability\"\n        },\n        \"references\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/Reference\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"KnownExploitedVulnerabilityBlob\": {\n      \"properties\": {\n        \"cve\": {\n          \"type\": \"string\"\n        },\n        \"vendor_project\": {\n          \"type\": \"string\"\n        },\n        \"product\": {\n          \"type\": \"string\"\n        },\n        \"date_added\": {\n          \"type\": \"string\",\n          \"format\": \"date-time\"\n        },\n        \"required_action\": {\n          \"type\": \"string\"\n        },\n        \"due_date\": {\n          \"type\": \"string\",\n          \"format\": \"date-time\"\n        },\n        \"known_ransomware_campaign_use\": {\n          \"type\": \"string\"\n        },\n        \"notes\": {\n          \"type\": \"string\"\n        },\n        \"urls\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        },\n        \"cwes\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"cve\"\n      ]\n    },\n    \"PackageBlob\": {\n      \"$defs\": {\n        \"cves\": {\n          \"description\": \"is a list of Common Vulnerabilities and Exposures (CVE) identifiers related to this vulnerability.\"\n        },\n        \"qualifiers\": {\n          \"description\": \"are package attributes that confirm the package is affected by the vulnerability.\"\n        },\n        \"ranges\": {\n          \"description\": \"specifies the affected version ranges and fixes if available.\"\n        }\n      },\n      \"properties\": {\n        \"cves\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        },\n        \"qualifiers\": {\n          \"$ref\": \"#/$defs/PackageQualifiers\"\n        },\n        \"ranges\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/Range\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"PackageQualifiers\": {\n      \"$defs\": {\n        \"platform_cpes\": {\n          \"description\": \"lists Common Platform Enumeration (CPE) identifiers for affected platforms.\"\n        },\n        \"rpm_modularity\": {\n          \"description\": \"indicates if the package follows RPM modularity for versioning.\"\n        }\n      },\n      \"properties\": {\n        \"rpm_modularity\": {\n          \"type\": \"string\"\n        },\n        \"platform_cpes\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"Range\": {\n      \"$defs\": {\n        \"fix\": {\n          \"description\": \"provides details on the fix version and its state if available.\"\n        },\n        \"version\": {\n          \"description\": \"defines the version constraints for affected software.\"\n        }\n      },\n      \"properties\": {\n        \"version\": {\n          \"$ref\": \"#/$defs/Version\"\n        },\n        \"fix\": {\n          \"$ref\": \"#/$defs/Fix\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"Reference\": {\n      \"$defs\": {\n        \"id\": {\n          \"description\": \"is an optional identifier for the reference (e.g., advisory ID like 'RHSA-2023:5455')\"\n        },\n        \"tags\": {\n          \"description\": \"is a free-form organizational field to convey additional information about the reference\"\n        },\n        \"url\": {\n          \"description\": \"is the external resource\"\n        }\n      },\n      \"properties\": {\n        \"url\": {\n          \"type\": \"string\"\n        },\n        \"id\": {\n          \"type\": \"string\"\n        },\n        \"tags\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"url\"\n      ]\n    },\n    \"Severity\": {\n      \"$defs\": {\n        \"rank\": {\n          \"description\": \"is a free-form organizational field to convey priority over other severities\"\n        },\n        \"scheme\": {\n          \"description\": \"describes the quantitative method used to determine the Score, such as 'CVSS_V3'. Alternatively this makes\\nclaim that Value is qualitative, for example 'HML' (High, Medium, Low), CHMLN (critical-high-medium-low-negligible)\"\n        },\n        \"source\": {\n          \"description\": \"is the name of the source of the severity score (e.g. 'nvd@nist.gov' or 'security-advisories@github.com')\"\n        },\n        \"value\": {\n          \"description\": \"is the severity score (e.g. '7.5', 'CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N',  or 'high' )\"\n        }\n      },\n      \"properties\": {\n        \"scheme\": {\n          \"type\": \"string\"\n        },\n        \"value\": true,\n        \"source\": {\n          \"type\": \"string\"\n        },\n        \"rank\": {\n          \"type\": \"integer\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"scheme\",\n        \"value\",\n        \"rank\"\n      ]\n    },\n    \"Version\": {\n      \"$defs\": {\n        \"constraint\": {\n          \"description\": \"defines the version range constraint for affected versions.\"\n        },\n        \"type\": {\n          \"description\": \"specifies the versioning system used (e.g., 'semver', 'rpm').\"\n        }\n      },\n      \"properties\": {\n        \"type\": {\n          \"type\": \"string\"\n        },\n        \"constraint\": {\n          \"type\": \"string\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"VulnerabilityBlob\": {\n      \"$defs\": {\n        \"aliases\": {\n          \"description\": \"is a list of IDs of the same vulnerability in other databases, in the form of the ID field. This allows one database to claim that its own entry describes the same vulnerability as one or more entries in other databases.\"\n        },\n        \"assigner\": {\n          \"description\": \"is a list of names, email, or organizations who submitted the vulnerability\"\n        },\n        \"description\": {\n          \"description\": \"of the vulnerability as provided by the source\"\n        },\n        \"id\": {\n          \"description\": \"is the lowercase unique string identifier for the vulnerability relative to the provider\"\n        },\n        \"refs\": {\n          \"description\": \"are URLs to external resources that provide more information about the vulnerability\"\n        },\n        \"severities\": {\n          \"description\": \"is a list of severity indications (quantitative or qualitative) for the vulnerability\"\n        }\n      },\n      \"properties\": {\n        \"id\": {\n          \"type\": \"string\"\n        },\n        \"assigner\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        },\n        \"description\": {\n          \"type\": \"string\"\n        },\n        \"refs\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/Reference\"\n          },\n          \"type\": \"array\"\n        },\n        \"aliases\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        },\n        \"severities\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/Severity\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"id\"\n      ]\n    }\n  },\n  \"oneOf\": [\n    {\n      \"$ref\": \"#/$defs/VulnerabilityBlob\"\n    },\n    {\n      \"$ref\": \"#/$defs/PackageBlob\"\n    },\n    {\n      \"$ref\": \"#/$defs/KnownExploitedVulnerabilityBlob\"\n    }\n  ],\n  \"description\": \"Unified schema for all blob types stored in the Grype v6 database\"\n}\n"
  },
  {
    "path": "schema/grype/db/blob/json/schema-latest.json",
    "content": "{\n  \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n  \"$id\": \"anchore.io/schema/grype/db/blob/json/6.1.4\",\n  \"$defs\": {\n    \"Fix\": {\n      \"$defs\": {\n        \"detail\": {\n          \"description\": \"provides additional fix information, such as commit details.\"\n        },\n        \"state\": {\n          \"description\": \"represents the status of the fix (e.g., 'fixed', 'unaffected').\"\n        },\n        \"version\": {\n          \"description\": \"is the version number of the fix.\"\n        }\n      },\n      \"properties\": {\n        \"version\": {\n          \"type\": \"string\"\n        },\n        \"state\": {\n          \"type\": \"string\"\n        },\n        \"detail\": {\n          \"$ref\": \"#/$defs/FixDetail\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"FixAvailability\": {\n      \"$defs\": {\n        \"date\": {\n          \"description\": \"is the date and time when fix information became available. Note: this might not be when the fix was created, committed or merged.\"\n        },\n        \"kind\": {\n          \"description\": \"describes how this date was obtained (e.g. advisory, release, commit, PR, issue, first-observed-record)\"\n        }\n      },\n      \"properties\": {\n        \"date\": {\n          \"type\": \"string\",\n          \"format\": \"date-time\"\n        },\n        \"kind\": {\n          \"type\": \"string\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"FixDetail\": {\n      \"$defs\": {\n        \"available\": {\n          \"description\": \"indicates when the fix information became available and how it was obtained.\"\n        },\n        \"references\": {\n          \"description\": \"contains URLs or identifiers for additional resources on the fix.\"\n        }\n      },\n      \"properties\": {\n        \"available\": {\n          \"$ref\": \"#/$defs/FixAvailability\"\n        },\n        \"references\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/Reference\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"KnownExploitedVulnerabilityBlob\": {\n      \"properties\": {\n        \"cve\": {\n          \"type\": \"string\"\n        },\n        \"vendor_project\": {\n          \"type\": \"string\"\n        },\n        \"product\": {\n          \"type\": \"string\"\n        },\n        \"date_added\": {\n          \"type\": \"string\",\n          \"format\": \"date-time\"\n        },\n        \"required_action\": {\n          \"type\": \"string\"\n        },\n        \"due_date\": {\n          \"type\": \"string\",\n          \"format\": \"date-time\"\n        },\n        \"known_ransomware_campaign_use\": {\n          \"type\": \"string\"\n        },\n        \"notes\": {\n          \"type\": \"string\"\n        },\n        \"urls\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        },\n        \"cwes\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"cve\"\n      ]\n    },\n    \"PackageBlob\": {\n      \"$defs\": {\n        \"cves\": {\n          \"description\": \"is a list of Common Vulnerabilities and Exposures (CVE) identifiers related to this vulnerability.\"\n        },\n        \"qualifiers\": {\n          \"description\": \"are package attributes that confirm the package is affected by the vulnerability.\"\n        },\n        \"ranges\": {\n          \"description\": \"specifies the affected version ranges and fixes if available.\"\n        }\n      },\n      \"properties\": {\n        \"cves\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        },\n        \"qualifiers\": {\n          \"$ref\": \"#/$defs/PackageQualifiers\"\n        },\n        \"ranges\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/Range\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"PackageQualifiers\": {\n      \"$defs\": {\n        \"platform_cpes\": {\n          \"description\": \"lists Common Platform Enumeration (CPE) identifiers for affected platforms.\"\n        },\n        \"rpm_modularity\": {\n          \"description\": \"indicates if the package follows RPM modularity for versioning.\"\n        }\n      },\n      \"properties\": {\n        \"rpm_modularity\": {\n          \"type\": \"string\"\n        },\n        \"platform_cpes\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"Range\": {\n      \"$defs\": {\n        \"fix\": {\n          \"description\": \"provides details on the fix version and its state if available.\"\n        },\n        \"version\": {\n          \"description\": \"defines the version constraints for affected software.\"\n        }\n      },\n      \"properties\": {\n        \"version\": {\n          \"$ref\": \"#/$defs/Version\"\n        },\n        \"fix\": {\n          \"$ref\": \"#/$defs/Fix\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"Reference\": {\n      \"$defs\": {\n        \"id\": {\n          \"description\": \"is an optional identifier for the reference (e.g., advisory ID like 'RHSA-2023:5455')\"\n        },\n        \"tags\": {\n          \"description\": \"is a free-form organizational field to convey additional information about the reference\"\n        },\n        \"url\": {\n          \"description\": \"is the external resource\"\n        }\n      },\n      \"properties\": {\n        \"url\": {\n          \"type\": \"string\"\n        },\n        \"id\": {\n          \"type\": \"string\"\n        },\n        \"tags\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"url\"\n      ]\n    },\n    \"Severity\": {\n      \"$defs\": {\n        \"rank\": {\n          \"description\": \"is a free-form organizational field to convey priority over other severities\"\n        },\n        \"scheme\": {\n          \"description\": \"describes the quantitative method used to determine the Score, such as 'CVSS_V3'. Alternatively this makes\\nclaim that Value is qualitative, for example 'HML' (High, Medium, Low), CHMLN (critical-high-medium-low-negligible)\"\n        },\n        \"source\": {\n          \"description\": \"is the name of the source of the severity score (e.g. 'nvd@nist.gov' or 'security-advisories@github.com')\"\n        },\n        \"value\": {\n          \"description\": \"is the severity score (e.g. '7.5', 'CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N',  or 'high' )\"\n        }\n      },\n      \"properties\": {\n        \"scheme\": {\n          \"type\": \"string\"\n        },\n        \"value\": true,\n        \"source\": {\n          \"type\": \"string\"\n        },\n        \"rank\": {\n          \"type\": \"integer\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"scheme\",\n        \"value\",\n        \"rank\"\n      ]\n    },\n    \"Version\": {\n      \"$defs\": {\n        \"constraint\": {\n          \"description\": \"defines the version range constraint for affected versions.\"\n        },\n        \"type\": {\n          \"description\": \"specifies the versioning system used (e.g., 'semver', 'rpm').\"\n        }\n      },\n      \"properties\": {\n        \"type\": {\n          \"type\": \"string\"\n        },\n        \"constraint\": {\n          \"type\": \"string\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"VulnerabilityBlob\": {\n      \"$defs\": {\n        \"aliases\": {\n          \"description\": \"is a list of IDs of the same vulnerability in other databases, in the form of the ID field. This allows one database to claim that its own entry describes the same vulnerability as one or more entries in other databases.\"\n        },\n        \"assigner\": {\n          \"description\": \"is a list of names, email, or organizations who submitted the vulnerability\"\n        },\n        \"description\": {\n          \"description\": \"of the vulnerability as provided by the source\"\n        },\n        \"id\": {\n          \"description\": \"is the lowercase unique string identifier for the vulnerability relative to the provider\"\n        },\n        \"refs\": {\n          \"description\": \"are URLs to external resources that provide more information about the vulnerability\"\n        },\n        \"severities\": {\n          \"description\": \"is a list of severity indications (quantitative or qualitative) for the vulnerability\"\n        }\n      },\n      \"properties\": {\n        \"id\": {\n          \"type\": \"string\"\n        },\n        \"assigner\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        },\n        \"description\": {\n          \"type\": \"string\"\n        },\n        \"refs\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/Reference\"\n          },\n          \"type\": \"array\"\n        },\n        \"aliases\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        },\n        \"severities\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/Severity\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"id\"\n      ]\n    }\n  },\n  \"oneOf\": [\n    {\n      \"$ref\": \"#/$defs/VulnerabilityBlob\"\n    },\n    {\n      \"$ref\": \"#/$defs/PackageBlob\"\n    },\n    {\n      \"$ref\": \"#/$defs/KnownExploitedVulnerabilityBlob\"\n    }\n  ],\n  \"description\": \"Unified schema for all blob types stored in the Grype v6 database\"\n}\n"
  },
  {
    "path": "schema/grype/db/sql/schema-6.1.1.sql",
    "content": "-- Generated by grype/db/v6/schema\n-- DO NOT EDIT: This file is auto-generated. Run 'task generate-db-schema' to update.\n-- Schema version: 6.1.1\n\nCREATE TABLE `affected_cpe_handles` (`id` integer PRIMARY KEY AUTOINCREMENT,`vulnerability_id` integer NOT NULL,`cpe_id` integer,`blob_id` integer,CONSTRAINT `fk_affected_cpe_handles_cpe` FOREIGN KEY (`cpe_id`) REFERENCES `cpes`(`id`,CONSTRAINT `fk_affected_cpe_handles_vulnerability` FOREIGN KEY (`vulnerability_id`) REFERENCES `vulnerability_handles`(`id`);\n\nCREATE TABLE `affected_package_handles` (`id` integer PRIMARY KEY AUTOINCREMENT,`vulnerability_id` integer NOT NULL,`operating_system_id` integer,`package_id` integer,`blob_id` integer,CONSTRAINT `fk_affected_package_handles_operating_system` FOREIGN KEY (`operating_system_id`) REFERENCES `operating_systems`(`id`,CONSTRAINT `fk_affected_package_handles_package` FOREIGN KEY (`package_id`) REFERENCES `packages`(`id`,CONSTRAINT `fk_affected_package_handles_vulnerability` FOREIGN KEY (`vulnerability_id`) REFERENCES `vulnerability_handles`(`id`);\n\nCREATE TABLE `blobs` (`id` integer PRIMARY KEY AUTOINCREMENT,`value` text NOT NULL);\n\nCREATE TABLE `cpes` (`id` integer PRIMARY KEY AUTOINCREMENT,`part` text NOT NULL,`vendor` text,`product` text NOT NULL,`edition` text,`language` text,`software_edition` text,`target_hardware` text,`target_software` text,`other` text);\n\nCREATE TABLE `db_metadata` (`build_timestamp` datetime NOT NULL,`model` integer NOT NULL,`revision` integer NOT NULL,`addition` integer NOT NULL);\n\nCREATE TABLE `epss_handles` (`id` integer PRIMARY KEY AUTOINCREMENT,`cve` text NOT NULL,`epss` real NOT NULL,`percentile` real NOT NULL);\n\nCREATE TABLE `epss_metadata` (`date` datetime NOT NULL);\n\nCREATE TABLE `known_exploited_vulnerability_handles` (`id` integer PRIMARY KEY AUTOINCREMENT,`cve` text NOT NULL,`blob_id` integer);\n\nCREATE TABLE `operating_system_specifier_overrides` (`alias` text,`version` text,`version_pattern` text,`codename` text,`channel` text,`replacement` text,`replacement_major_version` text,`replacement_minor_version` text,`replacement_label_version` text,`replacement_channel` text,`rolling` numeric,`applicable_client_db_schemas` text,PRIMARY KEY (`alias`,`version`,`version_pattern`,`replacement`,`replacement_major_version`,`replacement_minor_version`,`replacement_label_version`,`replacement_channel`,`rolling`));\n\nCREATE TABLE `operating_systems` (`id` integer PRIMARY KEY AUTOINCREMENT,`name` text,`release_id` text,`major_version` text,`minor_version` text,`label_version` text,`codename` text,`channel` text);\n\nCREATE TABLE `package_cpes` (`cpe_id` integer,`package_id` integer,PRIMARY KEY (`cpe_id`,`package_id`),CONSTRAINT `fk_package_cpes_cpe` FOREIGN KEY (`cpe_id`) REFERENCES `cpes`(`id`,CONSTRAINT `fk_package_cpes_package` FOREIGN KEY (`package_id`) REFERENCES `packages`(`id`);\n\nCREATE TABLE `package_specifier_overrides` (`ecosystem` text,`replacement_ecosystem` text,PRIMARY KEY (`ecosystem`,`replacement_ecosystem`));\n\nCREATE TABLE `packages` (`id` integer PRIMARY KEY AUTOINCREMENT,`ecosystem` text,`name` text);\n\nCREATE TABLE `providers` (`id` text,`version` text,`processor` text,`date_captured` datetime,`input_digest` text,PRIMARY KEY (`id`));\n\nCREATE TABLE `unaffected_cpe_handles` (`id` integer PRIMARY KEY AUTOINCREMENT,`vulnerability_id` integer NOT NULL,`cpe_id` integer,`blob_id` integer,CONSTRAINT `fk_unaffected_cpe_handles_cpe` FOREIGN KEY (`cpe_id`) REFERENCES `cpes`(`id`,CONSTRAINT `fk_unaffected_cpe_handles_vulnerability` FOREIGN KEY (`vulnerability_id`) REFERENCES `vulnerability_handles`(`id`);\n\nCREATE TABLE `unaffected_package_handles` (`id` integer PRIMARY KEY AUTOINCREMENT,`vulnerability_id` integer NOT NULL,`operating_system_id` integer,`package_id` integer,`blob_id` integer,CONSTRAINT `fk_unaffected_package_handles_operating_system` FOREIGN KEY (`operating_system_id`) REFERENCES `operating_systems`(`id`,CONSTRAINT `fk_unaffected_package_handles_package` FOREIGN KEY (`package_id`) REFERENCES `packages`(`id`,CONSTRAINT `fk_unaffected_package_handles_vulnerability` FOREIGN KEY (`vulnerability_id`) REFERENCES `vulnerability_handles`(`id`);\n\nCREATE TABLE `vulnerability_aliases` (`name` text,`alias` text NOT NULL,PRIMARY KEY (`name`,`alias`));\n\nCREATE TABLE `vulnerability_handles` (`id` integer PRIMARY KEY AUTOINCREMENT,`name` text NOT NULL,`status` text NOT NULL,`published_date` datetime,`modified_date` datetime,`withdrawn_date` datetime,`provider_id` text NOT NULL,`blob_id` integer,CONSTRAINT `fk_vulnerability_handles_provider` FOREIGN KEY (`provider_id`) REFERENCES `providers`(`id`);\n\n-- Indexes\nCREATE INDEX `epss_cve_idx` ON `epss_handles`(`cve` COLLATE NOCASE);\n\nCREATE INDEX `idx_affected_cpe_handles_cpe_id` ON `affected_cpe_handles`(`cpe_id`);\n\nCREATE INDEX `idx_affected_package_handles_operating_system_id` ON `affected_package_handles`(`operating_system_id`);\n\nCREATE INDEX `idx_affected_package_handles_package_id` ON `affected_package_handles`(`package_id`);\n\nCREATE INDEX `idx_affected_package_handles_vulnerability_id` ON `affected_package_handles`(`vulnerability_id`);\n\nCREATE INDEX `idx_cpe_product` ON `cpes`(`product` COLLATE NOCASE);\n\nCREATE INDEX `idx_cpe_vendor` ON `cpes`(`vendor` COLLATE NOCASE);\n\nCREATE INDEX `idx_operating_systems_major_version` ON `operating_systems`(`major_version`);\n\nCREATE INDEX `idx_operating_systems_minor_version` ON `operating_systems`(`minor_version`);\n\nCREATE INDEX `idx_package_name` ON `packages`(`name` COLLATE NOCASE);\n\nCREATE INDEX `idx_unaffected_cpe_handles_cpe_id` ON `unaffected_cpe_handles`(`cpe_id`);\n\nCREATE INDEX `idx_unaffected_package_handles_operating_system_id` ON `unaffected_package_handles`(`operating_system_id`);\n\nCREATE INDEX `idx_unaffected_package_handles_package_id` ON `unaffected_package_handles`(`package_id`);\n\nCREATE INDEX `idx_unaffected_package_handles_vulnerability_id` ON `unaffected_package_handles`(`vulnerability_id`);\n\nCREATE INDEX `idx_vuln_provider_id` ON `vulnerability_handles`(`name` COLLATE NOCASE,`provider_id` COLLATE NOCASE);\n\nCREATE INDEX `idx_vulnerability_handles_modified_date` ON `vulnerability_handles`(`modified_date`);\n\nCREATE INDEX `idx_vulnerability_handles_provider_id` ON `vulnerability_handles`(`provider_id`);\n\nCREATE INDEX `idx_vulnerability_handles_published_date` ON `vulnerability_handles`(`published_date`);\n\nCREATE INDEX `idx_vulnerability_handles_withdrawn_date` ON `vulnerability_handles`(`withdrawn_date`);\n\nCREATE INDEX `kev_cve_idx` ON `known_exploited_vulnerability_handles`(`cve` COLLATE NOCASE);\n\nCREATE INDEX `os_alias_idx` ON `operating_system_specifier_overrides`(`alias` COLLATE NOCASE);\n\nCREATE INDEX `pkg_ecosystem_idx` ON `package_specifier_overrides`(`ecosystem` COLLATE NOCASE);\n\nCREATE UNIQUE INDEX `idx_cpe` ON `cpes`(`part` COLLATE NOCASE,`vendor` COLLATE NOCASE,`product` COLLATE NOCASE,`edition` COLLATE NOCASE,`language` COLLATE NOCASE,`software_edition` COLLATE NOCASE,`target_hardware` COLLATE NOCASE,`target_software` COLLATE NOCASE,`other` COLLATE NOCASE);\n\nCREATE UNIQUE INDEX `idx_package` ON `packages`(`ecosystem` COLLATE NOCASE,`name` COLLATE NOCASE);\n\nCREATE UNIQUE INDEX `os_idx` ON `operating_systems`(`name`,`release_id`,`major_version`,`minor_version`,`label_version`,`channel`);\n\n"
  },
  {
    "path": "schema/grype/db/sql/schema-6.1.2.sql",
    "content": "-- Generated by grype/db/v6/schema\n-- DO NOT EDIT: This file is auto-generated. Run 'task generate-db-schema' to update.\n-- Schema version: 6.1.2\n\nCREATE TABLE `affected_cpe_handles` (`id` integer PRIMARY KEY AUTOINCREMENT,`vulnerability_id` integer NOT NULL,`cpe_id` integer,`blob_id` integer,CONSTRAINT `fk_affected_cpe_handles_cpe` FOREIGN KEY (`cpe_id`) REFERENCES `cpes`(`id`,CONSTRAINT `fk_affected_cpe_handles_vulnerability` FOREIGN KEY (`vulnerability_id`) REFERENCES `vulnerability_handles`(`id`);\n\nCREATE TABLE `affected_package_handles` (`id` integer PRIMARY KEY AUTOINCREMENT,`vulnerability_id` integer NOT NULL,`operating_system_id` integer,`package_id` integer,`blob_id` integer,CONSTRAINT `fk_affected_package_handles_operating_system` FOREIGN KEY (`operating_system_id`) REFERENCES `operating_systems`(`id`,CONSTRAINT `fk_affected_package_handles_package` FOREIGN KEY (`package_id`) REFERENCES `packages`(`id`,CONSTRAINT `fk_affected_package_handles_vulnerability` FOREIGN KEY (`vulnerability_id`) REFERENCES `vulnerability_handles`(`id`);\n\nCREATE TABLE `blobs` (`id` integer PRIMARY KEY AUTOINCREMENT,`value` text NOT NULL);\n\nCREATE TABLE `cpes` (`id` integer PRIMARY KEY AUTOINCREMENT,`part` text NOT NULL,`vendor` text,`product` text NOT NULL,`edition` text,`language` text,`software_edition` text,`target_hardware` text,`target_software` text,`other` text);\n\nCREATE TABLE `cwe_handles` (`id` integer PRIMARY KEY AUTOINCREMENT,`cve` text NOT NULL,`cwe` text NOT NULL,`source` text,`type` text);\n\nCREATE TABLE `db_metadata` (`build_timestamp` datetime NOT NULL,`model` integer NOT NULL,`revision` integer NOT NULL,`addition` integer NOT NULL);\n\nCREATE TABLE `epss_handles` (`id` integer PRIMARY KEY AUTOINCREMENT,`cve` text NOT NULL,`epss` real NOT NULL,`percentile` real NOT NULL);\n\nCREATE TABLE `epss_metadata` (`date` datetime NOT NULL);\n\nCREATE TABLE `known_exploited_vulnerability_handles` (`id` integer PRIMARY KEY AUTOINCREMENT,`cve` text NOT NULL,`blob_id` integer);\n\nCREATE TABLE `operating_system_specifier_overrides` (`alias` text,`version` text,`version_pattern` text,`codename` text,`channel` text,`replacement` text,`replacement_major_version` text,`replacement_minor_version` text,`replacement_label_version` text,`replacement_channel` text,`rolling` numeric,`applicable_client_db_schemas` text,PRIMARY KEY (`alias`,`version`,`version_pattern`,`replacement`,`replacement_major_version`,`replacement_minor_version`,`replacement_label_version`,`replacement_channel`,`rolling`));\n\nCREATE TABLE `operating_systems` (`id` integer PRIMARY KEY AUTOINCREMENT,`name` text,`release_id` text,`major_version` text,`minor_version` text,`label_version` text,`codename` text,`channel` text);\n\nCREATE TABLE `package_cpes` (`cpe_id` integer,`package_id` integer,PRIMARY KEY (`cpe_id`,`package_id`),CONSTRAINT `fk_package_cpes_cpe` FOREIGN KEY (`cpe_id`) REFERENCES `cpes`(`id`,CONSTRAINT `fk_package_cpes_package` FOREIGN KEY (`package_id`) REFERENCES `packages`(`id`);\n\nCREATE TABLE `package_specifier_overrides` (`ecosystem` text,`replacement_ecosystem` text,PRIMARY KEY (`ecosystem`,`replacement_ecosystem`));\n\nCREATE TABLE `packages` (`id` integer PRIMARY KEY AUTOINCREMENT,`ecosystem` text,`name` text);\n\nCREATE TABLE `providers` (`id` text,`version` text,`processor` text,`date_captured` datetime,`input_digest` text,PRIMARY KEY (`id`));\n\nCREATE TABLE `unaffected_cpe_handles` (`id` integer PRIMARY KEY AUTOINCREMENT,`vulnerability_id` integer NOT NULL,`cpe_id` integer,`blob_id` integer,CONSTRAINT `fk_unaffected_cpe_handles_cpe` FOREIGN KEY (`cpe_id`) REFERENCES `cpes`(`id`,CONSTRAINT `fk_unaffected_cpe_handles_vulnerability` FOREIGN KEY (`vulnerability_id`) REFERENCES `vulnerability_handles`(`id`);\n\nCREATE TABLE `unaffected_package_handles` (`id` integer PRIMARY KEY AUTOINCREMENT,`vulnerability_id` integer NOT NULL,`operating_system_id` integer,`package_id` integer,`blob_id` integer,CONSTRAINT `fk_unaffected_package_handles_operating_system` FOREIGN KEY (`operating_system_id`) REFERENCES `operating_systems`(`id`,CONSTRAINT `fk_unaffected_package_handles_package` FOREIGN KEY (`package_id`) REFERENCES `packages`(`id`,CONSTRAINT `fk_unaffected_package_handles_vulnerability` FOREIGN KEY (`vulnerability_id`) REFERENCES `vulnerability_handles`(`id`);\n\nCREATE TABLE `vulnerability_aliases` (`name` text,`alias` text NOT NULL,PRIMARY KEY (`name`,`alias`));\n\nCREATE TABLE `vulnerability_handles` (`id` integer PRIMARY KEY AUTOINCREMENT,`name` text NOT NULL,`status` text NOT NULL,`published_date` datetime,`modified_date` datetime,`withdrawn_date` datetime,`provider_id` text NOT NULL,`blob_id` integer,CONSTRAINT `fk_vulnerability_handles_provider` FOREIGN KEY (`provider_id`) REFERENCES `providers`(`id`);\n\n-- Indexes\nCREATE INDEX `cwes_cve_idx` ON `cwe_handles`(`cve` COLLATE NOCASE);\n\nCREATE INDEX `epss_cve_idx` ON `epss_handles`(`cve` COLLATE NOCASE);\n\nCREATE INDEX `idx_affected_cpe_handles_cpe_id` ON `affected_cpe_handles`(`cpe_id`);\n\nCREATE INDEX `idx_affected_package_handles_operating_system_id` ON `affected_package_handles`(`operating_system_id`);\n\nCREATE INDEX `idx_affected_package_handles_package_id` ON `affected_package_handles`(`package_id`);\n\nCREATE INDEX `idx_affected_package_handles_vulnerability_id` ON `affected_package_handles`(`vulnerability_id`);\n\nCREATE INDEX `idx_cpe_product` ON `cpes`(`product` COLLATE NOCASE);\n\nCREATE INDEX `idx_cpe_vendor` ON `cpes`(`vendor` COLLATE NOCASE);\n\nCREATE INDEX `idx_operating_systems_major_version` ON `operating_systems`(`major_version`);\n\nCREATE INDEX `idx_operating_systems_minor_version` ON `operating_systems`(`minor_version`);\n\nCREATE INDEX `idx_package_name` ON `packages`(`name` COLLATE NOCASE);\n\nCREATE INDEX `idx_unaffected_cpe_handles_cpe_id` ON `unaffected_cpe_handles`(`cpe_id`);\n\nCREATE INDEX `idx_unaffected_package_handles_operating_system_id` ON `unaffected_package_handles`(`operating_system_id`);\n\nCREATE INDEX `idx_unaffected_package_handles_package_id` ON `unaffected_package_handles`(`package_id`);\n\nCREATE INDEX `idx_unaffected_package_handles_vulnerability_id` ON `unaffected_package_handles`(`vulnerability_id`);\n\nCREATE INDEX `idx_vuln_provider_id` ON `vulnerability_handles`(`name` COLLATE NOCASE,`provider_id` COLLATE NOCASE);\n\nCREATE INDEX `idx_vulnerability_handles_modified_date` ON `vulnerability_handles`(`modified_date`);\n\nCREATE INDEX `idx_vulnerability_handles_provider_id` ON `vulnerability_handles`(`provider_id`);\n\nCREATE INDEX `idx_vulnerability_handles_published_date` ON `vulnerability_handles`(`published_date`);\n\nCREATE INDEX `idx_vulnerability_handles_withdrawn_date` ON `vulnerability_handles`(`withdrawn_date`);\n\nCREATE INDEX `kev_cve_idx` ON `known_exploited_vulnerability_handles`(`cve` COLLATE NOCASE);\n\nCREATE INDEX `os_alias_idx` ON `operating_system_specifier_overrides`(`alias` COLLATE NOCASE);\n\nCREATE INDEX `pkg_ecosystem_idx` ON `package_specifier_overrides`(`ecosystem` COLLATE NOCASE);\n\nCREATE UNIQUE INDEX `idx_cpe` ON `cpes`(`part` COLLATE NOCASE,`vendor` COLLATE NOCASE,`product` COLLATE NOCASE,`edition` COLLATE NOCASE,`language` COLLATE NOCASE,`software_edition` COLLATE NOCASE,`target_hardware` COLLATE NOCASE,`target_software` COLLATE NOCASE,`other` COLLATE NOCASE);\n\nCREATE UNIQUE INDEX `idx_package` ON `packages`(`ecosystem` COLLATE NOCASE,`name` COLLATE NOCASE);\n\nCREATE UNIQUE INDEX `os_idx` ON `operating_systems`(`name`,`release_id`,`major_version`,`minor_version`,`label_version`,`channel`);\n\n"
  },
  {
    "path": "schema/grype/db/sql/schema-6.1.3.sql",
    "content": "-- Generated by grype/db/v6/schema\n-- DO NOT EDIT: This file is auto-generated. Run 'task generate-db-schema' to update.\n-- Schema version: 6.1.3\n\nCREATE TABLE `affected_cpe_handles` (`id` integer PRIMARY KEY AUTOINCREMENT,`vulnerability_id` integer NOT NULL,`cpe_id` integer,`blob_id` integer,CONSTRAINT `fk_affected_cpe_handles_cpe` FOREIGN KEY (`cpe_id`) REFERENCES `cpes`(`id`,CONSTRAINT `fk_affected_cpe_handles_vulnerability` FOREIGN KEY (`vulnerability_id`) REFERENCES `vulnerability_handles`(`id`);\n\nCREATE TABLE `affected_package_handles` (`id` integer PRIMARY KEY AUTOINCREMENT,`vulnerability_id` integer NOT NULL,`operating_system_id` integer,`package_id` integer,`blob_id` integer,CONSTRAINT `fk_affected_package_handles_operating_system` FOREIGN KEY (`operating_system_id`) REFERENCES `operating_systems`(`id`,CONSTRAINT `fk_affected_package_handles_package` FOREIGN KEY (`package_id`) REFERENCES `packages`(`id`,CONSTRAINT `fk_affected_package_handles_vulnerability` FOREIGN KEY (`vulnerability_id`) REFERENCES `vulnerability_handles`(`id`);\n\nCREATE TABLE `blobs` (`id` integer PRIMARY KEY AUTOINCREMENT,`value` text NOT NULL);\n\nCREATE TABLE `cpes` (`id` integer PRIMARY KEY AUTOINCREMENT,`part` text NOT NULL,`vendor` text,`product` text NOT NULL,`edition` text,`language` text,`software_edition` text,`target_hardware` text,`target_software` text,`other` text);\n\nCREATE TABLE `cwe_handles` (`id` integer PRIMARY KEY AUTOINCREMENT,`cve` text NOT NULL,`cwe` text NOT NULL,`source` text,`type` text);\n\nCREATE TABLE `db_metadata` (`build_timestamp` datetime NOT NULL,`model` integer NOT NULL,`revision` integer NOT NULL,`addition` integer NOT NULL);\n\nCREATE TABLE `epss_handles` (`id` integer PRIMARY KEY AUTOINCREMENT,`cve` text NOT NULL,`epss` real NOT NULL,`percentile` real NOT NULL);\n\nCREATE TABLE `epss_metadata` (`date` datetime NOT NULL);\n\nCREATE TABLE `known_exploited_vulnerability_handles` (`id` integer PRIMARY KEY AUTOINCREMENT,`cve` text NOT NULL,`blob_id` integer);\n\nCREATE TABLE `operating_system_specifier_overrides` (`alias` text,`version` text,`version_pattern` text,`codename` text,`channel` text,`replacement` text,`replacement_major_version` text,`replacement_minor_version` text,`replacement_label_version` text,`replacement_channel` text,`rolling` numeric,`applicable_client_db_schemas` text,PRIMARY KEY (`alias`,`version`,`version_pattern`,`replacement`,`replacement_major_version`,`replacement_minor_version`,`replacement_label_version`,`replacement_channel`,`rolling`));\n\nCREATE TABLE `operating_systems` (`id` integer PRIMARY KEY AUTOINCREMENT,`name` text,`release_id` text,`major_version` text,`minor_version` text,`label_version` text,`codename` text,`channel` text);\n\nCREATE TABLE `package_cpes` (`cpe_id` integer,`package_id` integer,PRIMARY KEY (`cpe_id`,`package_id`),CONSTRAINT `fk_package_cpes_cpe` FOREIGN KEY (`cpe_id`) REFERENCES `cpes`(`id`,CONSTRAINT `fk_package_cpes_package` FOREIGN KEY (`package_id`) REFERENCES `packages`(`id`);\n\nCREATE TABLE `package_specifier_overrides` (`ecosystem` text,`replacement_ecosystem` text,PRIMARY KEY (`ecosystem`,`replacement_ecosystem`));\n\nCREATE TABLE `packages` (`id` integer PRIMARY KEY AUTOINCREMENT,`ecosystem` text,`name` text);\n\nCREATE TABLE `providers` (`id` text,`version` text,`processor` text,`date_captured` datetime,`input_digest` text,PRIMARY KEY (`id`));\n\nCREATE TABLE `unaffected_cpe_handles` (`id` integer PRIMARY KEY AUTOINCREMENT,`vulnerability_id` integer NOT NULL,`cpe_id` integer,`blob_id` integer,CONSTRAINT `fk_unaffected_cpe_handles_cpe` FOREIGN KEY (`cpe_id`) REFERENCES `cpes`(`id`,CONSTRAINT `fk_unaffected_cpe_handles_vulnerability` FOREIGN KEY (`vulnerability_id`) REFERENCES `vulnerability_handles`(`id`);\n\nCREATE TABLE `unaffected_package_handles` (`id` integer PRIMARY KEY AUTOINCREMENT,`vulnerability_id` integer NOT NULL,`operating_system_id` integer,`package_id` integer,`blob_id` integer,CONSTRAINT `fk_unaffected_package_handles_operating_system` FOREIGN KEY (`operating_system_id`) REFERENCES `operating_systems`(`id`,CONSTRAINT `fk_unaffected_package_handles_package` FOREIGN KEY (`package_id`) REFERENCES `packages`(`id`,CONSTRAINT `fk_unaffected_package_handles_vulnerability` FOREIGN KEY (`vulnerability_id`) REFERENCES `vulnerability_handles`(`id`);\n\nCREATE TABLE `vulnerability_aliases` (`name` text,`alias` text NOT NULL,PRIMARY KEY (`name`,`alias`));\n\nCREATE TABLE `vulnerability_handles` (`id` integer PRIMARY KEY AUTOINCREMENT,`name` text NOT NULL,`status` text NOT NULL,`published_date` datetime,`modified_date` datetime,`withdrawn_date` datetime,`provider_id` text NOT NULL,`blob_id` integer,CONSTRAINT `fk_vulnerability_handles_provider` FOREIGN KEY (`provider_id`) REFERENCES `providers`(`id`);\n\n-- Indexes\nCREATE INDEX `cwes_cve_idx` ON `cwe_handles`(`cve` COLLATE NOCASE);\n\nCREATE INDEX `epss_cve_idx` ON `epss_handles`(`cve` COLLATE NOCASE);\n\nCREATE INDEX `idx_affected_cpe_handles_cpe_id` ON `affected_cpe_handles`(`cpe_id`);\n\nCREATE INDEX `idx_affected_package_handles_operating_system_id` ON `affected_package_handles`(`operating_system_id`);\n\nCREATE INDEX `idx_affected_package_handles_package_id` ON `affected_package_handles`(`package_id`);\n\nCREATE INDEX `idx_affected_package_handles_vulnerability_id` ON `affected_package_handles`(`vulnerability_id`);\n\nCREATE INDEX `idx_cpe_product` ON `cpes`(`product` COLLATE NOCASE);\n\nCREATE INDEX `idx_cpe_vendor` ON `cpes`(`vendor` COLLATE NOCASE);\n\nCREATE INDEX `idx_operating_systems_major_version` ON `operating_systems`(`major_version`);\n\nCREATE INDEX `idx_operating_systems_minor_version` ON `operating_systems`(`minor_version`);\n\nCREATE INDEX `idx_package_name` ON `packages`(`name` COLLATE NOCASE);\n\nCREATE INDEX `idx_unaffected_cpe_handles_cpe_id` ON `unaffected_cpe_handles`(`cpe_id`);\n\nCREATE INDEX `idx_unaffected_package_handles_operating_system_id` ON `unaffected_package_handles`(`operating_system_id`);\n\nCREATE INDEX `idx_unaffected_package_handles_package_id` ON `unaffected_package_handles`(`package_id`);\n\nCREATE INDEX `idx_unaffected_package_handles_vulnerability_id` ON `unaffected_package_handles`(`vulnerability_id`);\n\nCREATE INDEX `idx_vuln_provider_id` ON `vulnerability_handles`(`name` COLLATE NOCASE,`provider_id` COLLATE NOCASE);\n\nCREATE INDEX `idx_vulnerability_handles_modified_date` ON `vulnerability_handles`(`modified_date`);\n\nCREATE INDEX `idx_vulnerability_handles_provider_id` ON `vulnerability_handles`(`provider_id`);\n\nCREATE INDEX `idx_vulnerability_handles_published_date` ON `vulnerability_handles`(`published_date`);\n\nCREATE INDEX `idx_vulnerability_handles_withdrawn_date` ON `vulnerability_handles`(`withdrawn_date`);\n\nCREATE INDEX `kev_cve_idx` ON `known_exploited_vulnerability_handles`(`cve` COLLATE NOCASE);\n\nCREATE INDEX `os_alias_idx` ON `operating_system_specifier_overrides`(`alias` COLLATE NOCASE);\n\nCREATE INDEX `pkg_ecosystem_idx` ON `package_specifier_overrides`(`ecosystem` COLLATE NOCASE);\n\nCREATE UNIQUE INDEX `idx_cpe` ON `cpes`(`part` COLLATE NOCASE,`vendor` COLLATE NOCASE,`product` COLLATE NOCASE,`edition` COLLATE NOCASE,`language` COLLATE NOCASE,`software_edition` COLLATE NOCASE,`target_hardware` COLLATE NOCASE,`target_software` COLLATE NOCASE,`other` COLLATE NOCASE);\n\nCREATE UNIQUE INDEX `idx_package` ON `packages`(`ecosystem` COLLATE NOCASE,`name` COLLATE NOCASE);\n\nCREATE UNIQUE INDEX `os_idx` ON `operating_systems`(`name`,`release_id`,`major_version`,`minor_version`,`label_version`,`channel`);\n\n"
  },
  {
    "path": "schema/grype/db/sql/schema-6.1.4.sql",
    "content": "-- Generated by grype/db/v6/schema\n-- DO NOT EDIT: This file is auto-generated. Run 'task generate-db-schema' to update.\n-- Schema version: 6.1.4\n\nCREATE TABLE `affected_cpe_handles` (`id` integer PRIMARY KEY AUTOINCREMENT,`vulnerability_id` integer NOT NULL,`cpe_id` integer,`blob_id` integer,CONSTRAINT `fk_affected_cpe_handles_cpe` FOREIGN KEY (`cpe_id`) REFERENCES `cpes`(`id`,CONSTRAINT `fk_affected_cpe_handles_vulnerability` FOREIGN KEY (`vulnerability_id`) REFERENCES `vulnerability_handles`(`id`);\n\nCREATE TABLE `affected_package_handles` (`id` integer PRIMARY KEY AUTOINCREMENT,`vulnerability_id` integer NOT NULL,`operating_system_id` integer,`package_id` integer,`blob_id` integer,CONSTRAINT `fk_affected_package_handles_operating_system` FOREIGN KEY (`operating_system_id`) REFERENCES `operating_systems`(`id`,CONSTRAINT `fk_affected_package_handles_package` FOREIGN KEY (`package_id`) REFERENCES `packages`(`id`,CONSTRAINT `fk_affected_package_handles_vulnerability` FOREIGN KEY (`vulnerability_id`) REFERENCES `vulnerability_handles`(`id`);\n\nCREATE TABLE `blobs` (`id` integer PRIMARY KEY AUTOINCREMENT,`value` text NOT NULL);\n\nCREATE TABLE `cpes` (`id` integer PRIMARY KEY AUTOINCREMENT,`part` text NOT NULL,`vendor` text,`product` text NOT NULL,`edition` text,`language` text,`software_edition` text,`target_hardware` text,`target_software` text,`other` text);\n\nCREATE TABLE `cwe_handles` (`id` integer PRIMARY KEY AUTOINCREMENT,`cve` text NOT NULL,`cwe` text NOT NULL,`source` text,`type` text);\n\nCREATE TABLE `db_metadata` (`build_timestamp` datetime NOT NULL,`model` integer NOT NULL,`revision` integer NOT NULL,`addition` integer NOT NULL);\n\nCREATE TABLE `epss_handles` (`id` integer PRIMARY KEY AUTOINCREMENT,`cve` text NOT NULL,`epss` real NOT NULL,`percentile` real NOT NULL);\n\nCREATE TABLE `epss_metadata` (`date` datetime NOT NULL);\n\nCREATE TABLE `known_exploited_vulnerability_handles` (`id` integer PRIMARY KEY AUTOINCREMENT,`cve` text NOT NULL,`blob_id` integer);\n\nCREATE TABLE `operating_system_specifier_overrides` (`alias` text,`version` text,`version_pattern` text,`codename` text,`channel` text,`replacement` text,`replacement_major_version` text,`replacement_minor_version` text,`replacement_label_version` text,`replacement_channel` text,`rolling` numeric,`applicable_client_db_schemas` text,PRIMARY KEY (`alias`,`version`,`version_pattern`,`replacement`,`replacement_major_version`,`replacement_minor_version`,`replacement_label_version`,`replacement_channel`,`rolling`));\n\nCREATE TABLE `operating_systems` (`id` integer PRIMARY KEY AUTOINCREMENT,`name` text,`release_id` text,`major_version` text,`minor_version` text,`label_version` text,`codename` text,`channel` text,`eol_date` datetime,`eoas_date` datetime);\n\nCREATE TABLE `package_cpes` (`cpe_id` integer,`package_id` integer,PRIMARY KEY (`cpe_id`,`package_id`),CONSTRAINT `fk_package_cpes_cpe` FOREIGN KEY (`cpe_id`) REFERENCES `cpes`(`id`,CONSTRAINT `fk_package_cpes_package` FOREIGN KEY (`package_id`) REFERENCES `packages`(`id`);\n\nCREATE TABLE `package_specifier_overrides` (`ecosystem` text,`replacement_ecosystem` text,PRIMARY KEY (`ecosystem`,`replacement_ecosystem`));\n\nCREATE TABLE `packages` (`id` integer PRIMARY KEY AUTOINCREMENT,`ecosystem` text,`name` text);\n\nCREATE TABLE `providers` (`id` text,`version` text,`processor` text,`date_captured` datetime,`input_digest` text,PRIMARY KEY (`id`));\n\nCREATE TABLE `unaffected_cpe_handles` (`id` integer PRIMARY KEY AUTOINCREMENT,`vulnerability_id` integer NOT NULL,`cpe_id` integer,`blob_id` integer,CONSTRAINT `fk_unaffected_cpe_handles_cpe` FOREIGN KEY (`cpe_id`) REFERENCES `cpes`(`id`,CONSTRAINT `fk_unaffected_cpe_handles_vulnerability` FOREIGN KEY (`vulnerability_id`) REFERENCES `vulnerability_handles`(`id`);\n\nCREATE TABLE `unaffected_package_handles` (`id` integer PRIMARY KEY AUTOINCREMENT,`vulnerability_id` integer NOT NULL,`operating_system_id` integer,`package_id` integer,`blob_id` integer,CONSTRAINT `fk_unaffected_package_handles_operating_system` FOREIGN KEY (`operating_system_id`) REFERENCES `operating_systems`(`id`,CONSTRAINT `fk_unaffected_package_handles_package` FOREIGN KEY (`package_id`) REFERENCES `packages`(`id`,CONSTRAINT `fk_unaffected_package_handles_vulnerability` FOREIGN KEY (`vulnerability_id`) REFERENCES `vulnerability_handles`(`id`);\n\nCREATE TABLE `vulnerability_aliases` (`name` text,`alias` text NOT NULL,PRIMARY KEY (`name`,`alias`));\n\nCREATE TABLE `vulnerability_handles` (`id` integer PRIMARY KEY AUTOINCREMENT,`name` text NOT NULL,`status` text NOT NULL,`published_date` datetime,`modified_date` datetime,`withdrawn_date` datetime,`provider_id` text NOT NULL,`blob_id` integer,CONSTRAINT `fk_vulnerability_handles_provider` FOREIGN KEY (`provider_id`) REFERENCES `providers`(`id`);\n\n-- Indexes\nCREATE INDEX `cwes_cve_idx` ON `cwe_handles`(`cve` COLLATE NOCASE);\n\nCREATE INDEX `epss_cve_idx` ON `epss_handles`(`cve` COLLATE NOCASE);\n\nCREATE INDEX `idx_affected_cpe_handles_cpe_id` ON `affected_cpe_handles`(`cpe_id`);\n\nCREATE INDEX `idx_affected_package_handles_operating_system_id` ON `affected_package_handles`(`operating_system_id`);\n\nCREATE INDEX `idx_affected_package_handles_package_id` ON `affected_package_handles`(`package_id`);\n\nCREATE INDEX `idx_affected_package_handles_vulnerability_id` ON `affected_package_handles`(`vulnerability_id`);\n\nCREATE INDEX `idx_cpe_product` ON `cpes`(`product` COLLATE NOCASE);\n\nCREATE INDEX `idx_cpe_vendor` ON `cpes`(`vendor` COLLATE NOCASE);\n\nCREATE INDEX `idx_operating_systems_eol_date` ON `operating_systems`(`eol_date`);\n\nCREATE INDEX `idx_operating_systems_major_version` ON `operating_systems`(`major_version`);\n\nCREATE INDEX `idx_operating_systems_minor_version` ON `operating_systems`(`minor_version`);\n\nCREATE INDEX `idx_package_name` ON `packages`(`name` COLLATE NOCASE);\n\nCREATE INDEX `idx_unaffected_cpe_handles_cpe_id` ON `unaffected_cpe_handles`(`cpe_id`);\n\nCREATE INDEX `idx_unaffected_package_handles_operating_system_id` ON `unaffected_package_handles`(`operating_system_id`);\n\nCREATE INDEX `idx_unaffected_package_handles_package_id` ON `unaffected_package_handles`(`package_id`);\n\nCREATE INDEX `idx_unaffected_package_handles_vulnerability_id` ON `unaffected_package_handles`(`vulnerability_id`);\n\nCREATE INDEX `idx_vuln_provider_id` ON `vulnerability_handles`(`name` COLLATE NOCASE,`provider_id` COLLATE NOCASE);\n\nCREATE INDEX `idx_vulnerability_handles_modified_date` ON `vulnerability_handles`(`modified_date`);\n\nCREATE INDEX `idx_vulnerability_handles_provider_id` ON `vulnerability_handles`(`provider_id`);\n\nCREATE INDEX `idx_vulnerability_handles_published_date` ON `vulnerability_handles`(`published_date`);\n\nCREATE INDEX `idx_vulnerability_handles_withdrawn_date` ON `vulnerability_handles`(`withdrawn_date`);\n\nCREATE INDEX `kev_cve_idx` ON `known_exploited_vulnerability_handles`(`cve` COLLATE NOCASE);\n\nCREATE INDEX `os_alias_idx` ON `operating_system_specifier_overrides`(`alias` COLLATE NOCASE);\n\nCREATE INDEX `pkg_ecosystem_idx` ON `package_specifier_overrides`(`ecosystem` COLLATE NOCASE);\n\nCREATE UNIQUE INDEX `idx_cpe` ON `cpes`(`part` COLLATE NOCASE,`vendor` COLLATE NOCASE,`product` COLLATE NOCASE,`edition` COLLATE NOCASE,`language` COLLATE NOCASE,`software_edition` COLLATE NOCASE,`target_hardware` COLLATE NOCASE,`target_software` COLLATE NOCASE,`other` COLLATE NOCASE);\n\nCREATE UNIQUE INDEX `idx_package` ON `packages`(`ecosystem` COLLATE NOCASE,`name` COLLATE NOCASE);\n\nCREATE UNIQUE INDEX `os_idx` ON `operating_systems`(`name`,`release_id`,`major_version`,`minor_version`,`label_version`,`channel`);\n\n"
  },
  {
    "path": "schema/grype/db/sql/schema-latest.sql",
    "content": "-- Generated by grype/db/v6/schema\n-- DO NOT EDIT: This file is auto-generated. Run 'task generate-db-schema' to update.\n-- Schema version: 6.1.4\n\nCREATE TABLE `affected_cpe_handles` (`id` integer PRIMARY KEY AUTOINCREMENT,`vulnerability_id` integer NOT NULL,`cpe_id` integer,`blob_id` integer,CONSTRAINT `fk_affected_cpe_handles_cpe` FOREIGN KEY (`cpe_id`) REFERENCES `cpes`(`id`,CONSTRAINT `fk_affected_cpe_handles_vulnerability` FOREIGN KEY (`vulnerability_id`) REFERENCES `vulnerability_handles`(`id`);\n\nCREATE TABLE `affected_package_handles` (`id` integer PRIMARY KEY AUTOINCREMENT,`vulnerability_id` integer NOT NULL,`operating_system_id` integer,`package_id` integer,`blob_id` integer,CONSTRAINT `fk_affected_package_handles_operating_system` FOREIGN KEY (`operating_system_id`) REFERENCES `operating_systems`(`id`,CONSTRAINT `fk_affected_package_handles_package` FOREIGN KEY (`package_id`) REFERENCES `packages`(`id`,CONSTRAINT `fk_affected_package_handles_vulnerability` FOREIGN KEY (`vulnerability_id`) REFERENCES `vulnerability_handles`(`id`);\n\nCREATE TABLE `blobs` (`id` integer PRIMARY KEY AUTOINCREMENT,`value` text NOT NULL);\n\nCREATE TABLE `cpes` (`id` integer PRIMARY KEY AUTOINCREMENT,`part` text NOT NULL,`vendor` text,`product` text NOT NULL,`edition` text,`language` text,`software_edition` text,`target_hardware` text,`target_software` text,`other` text);\n\nCREATE TABLE `cwe_handles` (`id` integer PRIMARY KEY AUTOINCREMENT,`cve` text NOT NULL,`cwe` text NOT NULL,`source` text,`type` text);\n\nCREATE TABLE `db_metadata` (`build_timestamp` datetime NOT NULL,`model` integer NOT NULL,`revision` integer NOT NULL,`addition` integer NOT NULL);\n\nCREATE TABLE `epss_handles` (`id` integer PRIMARY KEY AUTOINCREMENT,`cve` text NOT NULL,`epss` real NOT NULL,`percentile` real NOT NULL);\n\nCREATE TABLE `epss_metadata` (`date` datetime NOT NULL);\n\nCREATE TABLE `known_exploited_vulnerability_handles` (`id` integer PRIMARY KEY AUTOINCREMENT,`cve` text NOT NULL,`blob_id` integer);\n\nCREATE TABLE `operating_system_specifier_overrides` (`alias` text,`version` text,`version_pattern` text,`codename` text,`channel` text,`replacement` text,`replacement_major_version` text,`replacement_minor_version` text,`replacement_label_version` text,`replacement_channel` text,`rolling` numeric,`applicable_client_db_schemas` text,PRIMARY KEY (`alias`,`version`,`version_pattern`,`replacement`,`replacement_major_version`,`replacement_minor_version`,`replacement_label_version`,`replacement_channel`,`rolling`));\n\nCREATE TABLE `operating_systems` (`id` integer PRIMARY KEY AUTOINCREMENT,`name` text,`release_id` text,`major_version` text,`minor_version` text,`label_version` text,`codename` text,`channel` text,`eol_date` datetime,`eoas_date` datetime);\n\nCREATE TABLE `package_cpes` (`cpe_id` integer,`package_id` integer,PRIMARY KEY (`cpe_id`,`package_id`),CONSTRAINT `fk_package_cpes_cpe` FOREIGN KEY (`cpe_id`) REFERENCES `cpes`(`id`,CONSTRAINT `fk_package_cpes_package` FOREIGN KEY (`package_id`) REFERENCES `packages`(`id`);\n\nCREATE TABLE `package_specifier_overrides` (`ecosystem` text,`replacement_ecosystem` text,PRIMARY KEY (`ecosystem`,`replacement_ecosystem`));\n\nCREATE TABLE `packages` (`id` integer PRIMARY KEY AUTOINCREMENT,`ecosystem` text,`name` text);\n\nCREATE TABLE `providers` (`id` text,`version` text,`processor` text,`date_captured` datetime,`input_digest` text,PRIMARY KEY (`id`));\n\nCREATE TABLE `unaffected_cpe_handles` (`id` integer PRIMARY KEY AUTOINCREMENT,`vulnerability_id` integer NOT NULL,`cpe_id` integer,`blob_id` integer,CONSTRAINT `fk_unaffected_cpe_handles_cpe` FOREIGN KEY (`cpe_id`) REFERENCES `cpes`(`id`,CONSTRAINT `fk_unaffected_cpe_handles_vulnerability` FOREIGN KEY (`vulnerability_id`) REFERENCES `vulnerability_handles`(`id`);\n\nCREATE TABLE `unaffected_package_handles` (`id` integer PRIMARY KEY AUTOINCREMENT,`vulnerability_id` integer NOT NULL,`operating_system_id` integer,`package_id` integer,`blob_id` integer,CONSTRAINT `fk_unaffected_package_handles_operating_system` FOREIGN KEY (`operating_system_id`) REFERENCES `operating_systems`(`id`,CONSTRAINT `fk_unaffected_package_handles_package` FOREIGN KEY (`package_id`) REFERENCES `packages`(`id`,CONSTRAINT `fk_unaffected_package_handles_vulnerability` FOREIGN KEY (`vulnerability_id`) REFERENCES `vulnerability_handles`(`id`);\n\nCREATE TABLE `vulnerability_aliases` (`name` text,`alias` text NOT NULL,PRIMARY KEY (`name`,`alias`));\n\nCREATE TABLE `vulnerability_handles` (`id` integer PRIMARY KEY AUTOINCREMENT,`name` text NOT NULL,`status` text NOT NULL,`published_date` datetime,`modified_date` datetime,`withdrawn_date` datetime,`provider_id` text NOT NULL,`blob_id` integer,CONSTRAINT `fk_vulnerability_handles_provider` FOREIGN KEY (`provider_id`) REFERENCES `providers`(`id`);\n\n-- Indexes\nCREATE INDEX `cwes_cve_idx` ON `cwe_handles`(`cve` COLLATE NOCASE);\n\nCREATE INDEX `epss_cve_idx` ON `epss_handles`(`cve` COLLATE NOCASE);\n\nCREATE INDEX `idx_affected_cpe_handles_cpe_id` ON `affected_cpe_handles`(`cpe_id`);\n\nCREATE INDEX `idx_affected_package_handles_operating_system_id` ON `affected_package_handles`(`operating_system_id`);\n\nCREATE INDEX `idx_affected_package_handles_package_id` ON `affected_package_handles`(`package_id`);\n\nCREATE INDEX `idx_affected_package_handles_vulnerability_id` ON `affected_package_handles`(`vulnerability_id`);\n\nCREATE INDEX `idx_cpe_product` ON `cpes`(`product` COLLATE NOCASE);\n\nCREATE INDEX `idx_cpe_vendor` ON `cpes`(`vendor` COLLATE NOCASE);\n\nCREATE INDEX `idx_operating_systems_eol_date` ON `operating_systems`(`eol_date`);\n\nCREATE INDEX `idx_operating_systems_major_version` ON `operating_systems`(`major_version`);\n\nCREATE INDEX `idx_operating_systems_minor_version` ON `operating_systems`(`minor_version`);\n\nCREATE INDEX `idx_package_name` ON `packages`(`name` COLLATE NOCASE);\n\nCREATE INDEX `idx_unaffected_cpe_handles_cpe_id` ON `unaffected_cpe_handles`(`cpe_id`);\n\nCREATE INDEX `idx_unaffected_package_handles_operating_system_id` ON `unaffected_package_handles`(`operating_system_id`);\n\nCREATE INDEX `idx_unaffected_package_handles_package_id` ON `unaffected_package_handles`(`package_id`);\n\nCREATE INDEX `idx_unaffected_package_handles_vulnerability_id` ON `unaffected_package_handles`(`vulnerability_id`);\n\nCREATE INDEX `idx_vuln_provider_id` ON `vulnerability_handles`(`name` COLLATE NOCASE,`provider_id` COLLATE NOCASE);\n\nCREATE INDEX `idx_vulnerability_handles_modified_date` ON `vulnerability_handles`(`modified_date`);\n\nCREATE INDEX `idx_vulnerability_handles_provider_id` ON `vulnerability_handles`(`provider_id`);\n\nCREATE INDEX `idx_vulnerability_handles_published_date` ON `vulnerability_handles`(`published_date`);\n\nCREATE INDEX `idx_vulnerability_handles_withdrawn_date` ON `vulnerability_handles`(`withdrawn_date`);\n\nCREATE INDEX `kev_cve_idx` ON `known_exploited_vulnerability_handles`(`cve` COLLATE NOCASE);\n\nCREATE INDEX `os_alias_idx` ON `operating_system_specifier_overrides`(`alias` COLLATE NOCASE);\n\nCREATE INDEX `pkg_ecosystem_idx` ON `package_specifier_overrides`(`ecosystem` COLLATE NOCASE);\n\nCREATE UNIQUE INDEX `idx_cpe` ON `cpes`(`part` COLLATE NOCASE,`vendor` COLLATE NOCASE,`product` COLLATE NOCASE,`edition` COLLATE NOCASE,`language` COLLATE NOCASE,`software_edition` COLLATE NOCASE,`target_hardware` COLLATE NOCASE,`target_software` COLLATE NOCASE,`other` COLLATE NOCASE);\n\nCREATE UNIQUE INDEX `idx_package` ON `packages`(`ecosystem` COLLATE NOCASE,`name` COLLATE NOCASE);\n\nCREATE UNIQUE INDEX `os_idx` ON `operating_systems`(`name`,`release_id`,`major_version`,`minor_version`,`label_version`,`channel`);\n\n"
  },
  {
    "path": "schema/grype/db-search/json/README.md",
    "content": "# `db-search` JSON Schema\n\nThis is the JSON schema for output from the `grype db search` command. The required inputs for defining the JSON schema are as follows:\n\n- the value of `cmd/grype/cli/commands/internal/dbsearch.MatchesSchemaVersion` that governs the schema version\n- the `Matches` type definition within `github.com/anchore/grype/cmd/grype/cli/commands/internal/dbsearch/matches.go` that governs the overall document shape\n\n## Versioning\n\nVersioning the JSON schema must be done manually by changing the `MatchesSchemaVersion` constant within `cmd/grype/cli/commands/internal/dbsearch/versions.go`.\n\nThis schema is being versioned based off of the \"SchemaVer\" guidelines, which slightly diverges from Semantic Versioning to tailor for the purposes of data models.\n\nGiven a version number format `MODEL.REVISION.ADDITION`:\n\n- `MODEL`: increment when you make a breaking schema change which will prevent interaction with any historical data\n- `REVISION`: increment when you make a schema change which may prevent interaction with some historical data\n- `ADDITION`: increment when you make a schema change that is compatible with all historical data\n\n## Generating a New Schema\n\nCreate the new schema by running `make generate-json-schema` from the root of the repo:\n\n- If there is **not** an existing schema for the given version, then the new schema file will be written to `schema/grype/db-search/json/schema-$VERSION.json`\n- If there is an existing schema for the given version and the new schema matches the existing schema, no action is taken\n- If there is an existing schema for the given version and the new schema **does not** match the existing schema, an error is shown indicating to increment the version appropriately (see the \"Versioning\" section)\n\n***Note: never delete a JSON schema and never change an existing JSON schema once it has been published in a release!*** Only add new schemas with a newly incremented version.\n"
  },
  {
    "path": "schema/grype/db-search/json/schema-1.0.0.json",
    "content": "{\n  \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n  \"$id\": \"anchore.io/schema/grype/db-search/json/1.0.0/matches\",\n  \"$ref\": \"#/$defs/Matches\",\n  \"$defs\": {\n    \"AffectedPackageBlob\": {\n      \"$defs\": {\n        \"cves\": {\n          \"description\": \"is a list of Common Vulnerabilities and Exposures (CVE) identifiers related to this vulnerability.\"\n        },\n        \"qualifiers\": {\n          \"description\": \"are package attributes that confirm the package is affected by the vulnerability.\"\n        },\n        \"ranges\": {\n          \"description\": \"specifies the affected version ranges and fixes if available.\"\n        }\n      },\n      \"properties\": {\n        \"cves\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        },\n        \"qualifiers\": {\n          \"$ref\": \"#/$defs/AffectedPackageQualifiers\"\n        },\n        \"ranges\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/AffectedRange\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"AffectedPackageInfo\": {\n      \"$defs\": {\n        \"cpe\": {\n          \"description\": \"is a Common Platform Enumeration that is affected by the vulnerability\"\n        },\n        \"detail\": {\n          \"description\": \"is the detailed information about the affected package\"\n        },\n        \"os\": {\n          \"description\": \"identifies the operating system release that the affected package is released for\"\n        },\n        \"package\": {\n          \"description\": \"identifies the name of the package in a specific ecosystem affected by the vulnerability\"\n        }\n      },\n      \"properties\": {\n        \"os\": {\n          \"$ref\": \"#/$defs/OperatingSystem\"\n        },\n        \"package\": {\n          \"$ref\": \"#/$defs/Package\"\n        },\n        \"cpe\": {\n          \"$ref\": \"#/$defs/CPE\"\n        },\n        \"detail\": {\n          \"$ref\": \"#/$defs/AffectedPackageBlob\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"detail\"\n      ]\n    },\n    \"AffectedPackageQualifiers\": {\n      \"$defs\": {\n        \"platform_cpes\": {\n          \"description\": \"lists Common Platform Enumeration (CPE) identifiers for affected platforms.\"\n        },\n        \"rpm_modularity\": {\n          \"description\": \"indicates if the package follows RPM modularity for versioning.\"\n        }\n      },\n      \"properties\": {\n        \"rpm_modularity\": {\n          \"type\": \"string\"\n        },\n        \"platform_cpes\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"AffectedRange\": {\n      \"$defs\": {\n        \"fix\": {\n          \"description\": \"provides details on the fix version and its state if available.\"\n        },\n        \"version\": {\n          \"description\": \"defines the version constraints for affected software.\"\n        }\n      },\n      \"properties\": {\n        \"version\": {\n          \"$ref\": \"#/$defs/AffectedVersion\"\n        },\n        \"fix\": {\n          \"$ref\": \"#/$defs/Fix\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"AffectedVersion\": {\n      \"$defs\": {\n        \"constraint\": {\n          \"description\": \"defines the version range constraint for affected versions.\"\n        },\n        \"type\": {\n          \"description\": \"specifies the versioning system used (e.g., 'semver', 'rpm').\"\n        }\n      },\n      \"properties\": {\n        \"type\": {\n          \"type\": \"string\"\n        },\n        \"constraint\": {\n          \"type\": \"string\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"CPE\": {\n      \"properties\": {\n        \"ID\": {\n          \"type\": \"integer\"\n        },\n        \"Part\": {\n          \"type\": \"string\"\n        },\n        \"Vendor\": {\n          \"type\": \"string\"\n        },\n        \"Product\": {\n          \"type\": \"string\"\n        },\n        \"Edition\": {\n          \"type\": \"string\"\n        },\n        \"Language\": {\n          \"type\": \"string\"\n        },\n        \"SoftwareEdition\": {\n          \"type\": \"string\"\n        },\n        \"TargetHardware\": {\n          \"type\": \"string\"\n        },\n        \"TargetSoftware\": {\n          \"type\": \"string\"\n        },\n        \"Other\": {\n          \"type\": \"string\"\n        },\n        \"Packages\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/Package\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"ID\",\n        \"Part\",\n        \"Vendor\",\n        \"Product\",\n        \"Edition\",\n        \"Language\",\n        \"SoftwareEdition\",\n        \"TargetHardware\",\n        \"TargetSoftware\",\n        \"Other\",\n        \"Packages\"\n      ]\n    },\n    \"Fix\": {\n      \"$defs\": {\n        \"detail\": {\n          \"description\": \"provides additional fix information, such as commit details.\"\n        },\n        \"state\": {\n          \"description\": \"represents the status of the fix (e.g., 'fixed', 'unaffected').\"\n        },\n        \"version\": {\n          \"description\": \"is the version number of the fix.\"\n        }\n      },\n      \"properties\": {\n        \"version\": {\n          \"type\": \"string\"\n        },\n        \"state\": {\n          \"type\": \"string\"\n        },\n        \"detail\": {\n          \"$ref\": \"#/$defs/FixDetail\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"FixDetail\": {\n      \"$defs\": {\n        \"git_commit\": {\n          \"description\": \"is the identifier for the Git commit associated with the fix.\"\n        },\n        \"references\": {\n          \"description\": \"contains URLs or identifiers for additional resources on the fix.\"\n        },\n        \"timestamp\": {\n          \"description\": \"is the date and time when the fix was committed.\"\n        }\n      },\n      \"properties\": {\n        \"git_commit\": {\n          \"type\": \"string\"\n        },\n        \"timestamp\": {\n          \"type\": \"string\",\n          \"format\": \"date-time\"\n        },\n        \"references\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/Reference\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"Match\": {\n      \"$defs\": {\n        \"packages\": {\n          \"description\": \"is the list of packages affected by the vulnerability.\"\n        },\n        \"vulnerability\": {\n          \"description\": \"is the core advisory record for a single known vulnerability from a specific provider.\"\n        }\n      },\n      \"properties\": {\n        \"vulnerability\": {\n          \"$ref\": \"#/$defs/VulnerabilityInfo\"\n        },\n        \"packages\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/AffectedPackageInfo\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"vulnerability\",\n        \"packages\"\n      ]\n    },\n    \"Matches\": {\n      \"items\": {\n        \"$ref\": \"#/$defs/Match\"\n      },\n      \"type\": \"array\"\n    },\n    \"OperatingSystem\": {\n      \"properties\": {\n        \"name\": {\n          \"type\": \"string\"\n        },\n        \"version\": {\n          \"type\": \"string\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"name\",\n        \"version\"\n      ]\n    },\n    \"Package\": {\n      \"properties\": {\n        \"name\": {\n          \"type\": \"string\"\n        },\n        \"ecosystem\": {\n          \"type\": \"string\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"name\",\n        \"ecosystem\"\n      ]\n    },\n    \"Reference\": {\n      \"$defs\": {\n        \"tags\": {\n          \"description\": \"is a free-form organizational field to convey additional information about the reference\"\n        },\n        \"url\": {\n          \"description\": \"is the external resource\"\n        }\n      },\n      \"properties\": {\n        \"url\": {\n          \"type\": \"string\"\n        },\n        \"tags\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"url\"\n      ]\n    },\n    \"Severity\": {\n      \"$defs\": {\n        \"rank\": {\n          \"description\": \"is a free-form organizational field to convey priority over other severities\"\n        },\n        \"scheme\": {\n          \"description\": \"describes the quantitative method used to determine the Score, such as 'CVSS_V3'. Alternatively this makes\\nclaim that Value is qualitative, for example 'HML' (High, Medium, Low), CHMLN (critical-high-medium-low-negligible)\"\n        },\n        \"source\": {\n          \"description\": \"is the name of the source of the severity score (e.g. 'nvd@nist.gov' or 'security-advisories@github.com')\"\n        },\n        \"value\": {\n          \"description\": \"is the severity score (e.g. '7.5', 'CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N',  or 'high' )\"\n        }\n      },\n      \"properties\": {\n        \"scheme\": {\n          \"type\": \"string\"\n        },\n        \"value\": true,\n        \"source\": {\n          \"type\": \"string\"\n        },\n        \"rank\": {\n          \"type\": \"integer\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"scheme\",\n        \"value\",\n        \"rank\"\n      ]\n    },\n    \"VulnerabilityInfo\": {\n      \"$defs\": {\n        \"modified_date\": {\n          \"description\": \"is the date the vulnerability record was last modified\"\n        },\n        \"provider\": {\n          \"description\": \"is the upstream data processor (usually Vunnel) that is responsible for vulnerability records. Each provider\\nshould be scoped to a specific vulnerability dataset, for instance, the 'ubuntu' provider for all records from\\nCanonicals' Ubuntu Security Notices (for all Ubuntu distro versions).\"\n        },\n        \"published_date\": {\n          \"description\": \"is the date the vulnerability record was first published\"\n        },\n        \"status\": {\n          \"description\": \"conveys the actionability of the current record (one of 'active', 'analyzing', 'rejected', 'disputed')\"\n        },\n        \"withdrawn_date\": {\n          \"description\": \"is the date the vulnerability record was withdrawn\"\n        }\n      },\n      \"properties\": {\n        \"id\": {\n          \"type\": \"string\"\n        },\n        \"assigner\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        },\n        \"description\": {\n          \"type\": \"string\"\n        },\n        \"refs\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/Reference\"\n          },\n          \"type\": \"array\"\n        },\n        \"aliases\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        },\n        \"severities\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/Severity\"\n          },\n          \"type\": \"array\"\n        },\n        \"provider\": {\n          \"type\": \"string\"\n        },\n        \"status\": {\n          \"type\": \"string\"\n        },\n        \"published_date\": {\n          \"type\": \"string\",\n          \"format\": \"date-time\"\n        },\n        \"modified_date\": {\n          \"type\": \"string\",\n          \"format\": \"date-time\"\n        },\n        \"withdrawn_date\": {\n          \"type\": \"string\",\n          \"format\": \"date-time\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"id\",\n        \"provider\",\n        \"status\"\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "schema/grype/db-search/json/schema-1.0.1.json",
    "content": "{\n  \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n  \"$id\": \"anchore.io/schema/grype/db-search/json/1.0.1/matches\",\n  \"$ref\": \"#/$defs/Matches\",\n  \"$defs\": {\n    \"AffectedPackageBlob\": {\n      \"$defs\": {\n        \"cves\": {\n          \"description\": \"is a list of Common Vulnerabilities and Exposures (CVE) identifiers related to this vulnerability.\"\n        },\n        \"qualifiers\": {\n          \"description\": \"are package attributes that confirm the package is affected by the vulnerability.\"\n        },\n        \"ranges\": {\n          \"description\": \"specifies the affected version ranges and fixes if available.\"\n        }\n      },\n      \"properties\": {\n        \"cves\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        },\n        \"qualifiers\": {\n          \"$ref\": \"#/$defs/AffectedPackageQualifiers\"\n        },\n        \"ranges\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/AffectedRange\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"AffectedPackageInfo\": {\n      \"$defs\": {\n        \"cpe\": {\n          \"description\": \"is a Common Platform Enumeration that is affected by the vulnerability\"\n        },\n        \"detail\": {\n          \"description\": \"is the detailed information about the affected package\"\n        },\n        \"os\": {\n          \"description\": \"identifies the operating system release that the affected package is released for\"\n        },\n        \"package\": {\n          \"description\": \"identifies the name of the package in a specific ecosystem affected by the vulnerability\"\n        }\n      },\n      \"properties\": {\n        \"os\": {\n          \"$ref\": \"#/$defs/OperatingSystem\"\n        },\n        \"package\": {\n          \"$ref\": \"#/$defs/Package\"\n        },\n        \"cpe\": {\n          \"$ref\": \"#/$defs/CPE\"\n        },\n        \"detail\": {\n          \"$ref\": \"#/$defs/AffectedPackageBlob\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"detail\"\n      ]\n    },\n    \"AffectedPackageQualifiers\": {\n      \"$defs\": {\n        \"platform_cpes\": {\n          \"description\": \"lists Common Platform Enumeration (CPE) identifiers for affected platforms.\"\n        },\n        \"rpm_modularity\": {\n          \"description\": \"indicates if the package follows RPM modularity for versioning.\"\n        }\n      },\n      \"properties\": {\n        \"rpm_modularity\": {\n          \"type\": \"string\"\n        },\n        \"platform_cpes\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"AffectedRange\": {\n      \"$defs\": {\n        \"fix\": {\n          \"description\": \"provides details on the fix version and its state if available.\"\n        },\n        \"version\": {\n          \"description\": \"defines the version constraints for affected software.\"\n        }\n      },\n      \"properties\": {\n        \"version\": {\n          \"$ref\": \"#/$defs/AffectedVersion\"\n        },\n        \"fix\": {\n          \"$ref\": \"#/$defs/Fix\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"AffectedVersion\": {\n      \"$defs\": {\n        \"constraint\": {\n          \"description\": \"defines the version range constraint for affected versions.\"\n        },\n        \"type\": {\n          \"description\": \"specifies the versioning system used (e.g., 'semver', 'rpm').\"\n        }\n      },\n      \"properties\": {\n        \"type\": {\n          \"type\": \"string\"\n        },\n        \"constraint\": {\n          \"type\": \"string\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"CPE\": {\n      \"properties\": {\n        \"ID\": {\n          \"type\": \"integer\"\n        },\n        \"Part\": {\n          \"type\": \"string\"\n        },\n        \"Vendor\": {\n          \"type\": \"string\"\n        },\n        \"Product\": {\n          \"type\": \"string\"\n        },\n        \"Edition\": {\n          \"type\": \"string\"\n        },\n        \"Language\": {\n          \"type\": \"string\"\n        },\n        \"SoftwareEdition\": {\n          \"type\": \"string\"\n        },\n        \"TargetHardware\": {\n          \"type\": \"string\"\n        },\n        \"TargetSoftware\": {\n          \"type\": \"string\"\n        },\n        \"Other\": {\n          \"type\": \"string\"\n        },\n        \"Packages\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/Package\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"ID\",\n        \"Part\",\n        \"Vendor\",\n        \"Product\",\n        \"Edition\",\n        \"Language\",\n        \"SoftwareEdition\",\n        \"TargetHardware\",\n        \"TargetSoftware\",\n        \"Other\",\n        \"Packages\"\n      ]\n    },\n    \"EPSS\": {\n      \"properties\": {\n        \"cve\": {\n          \"type\": \"string\"\n        },\n        \"epss\": {\n          \"type\": \"number\"\n        },\n        \"percentile\": {\n          \"type\": \"number\"\n        },\n        \"date\": {\n          \"type\": \"string\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"cve\",\n        \"epss\",\n        \"percentile\",\n        \"date\"\n      ]\n    },\n    \"Fix\": {\n      \"$defs\": {\n        \"detail\": {\n          \"description\": \"provides additional fix information, such as commit details.\"\n        },\n        \"state\": {\n          \"description\": \"represents the status of the fix (e.g., 'fixed', 'unaffected').\"\n        },\n        \"version\": {\n          \"description\": \"is the version number of the fix.\"\n        }\n      },\n      \"properties\": {\n        \"version\": {\n          \"type\": \"string\"\n        },\n        \"state\": {\n          \"type\": \"string\"\n        },\n        \"detail\": {\n          \"$ref\": \"#/$defs/FixDetail\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"FixDetail\": {\n      \"$defs\": {\n        \"git_commit\": {\n          \"description\": \"is the identifier for the Git commit associated with the fix.\"\n        },\n        \"references\": {\n          \"description\": \"contains URLs or identifiers for additional resources on the fix.\"\n        },\n        \"timestamp\": {\n          \"description\": \"is the date and time when the fix was committed.\"\n        }\n      },\n      \"properties\": {\n        \"git_commit\": {\n          \"type\": \"string\"\n        },\n        \"timestamp\": {\n          \"type\": \"string\",\n          \"format\": \"date-time\"\n        },\n        \"references\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/Reference\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"KnownExploited\": {\n      \"properties\": {\n        \"cve\": {\n          \"type\": \"string\"\n        },\n        \"vendor_project\": {\n          \"type\": \"string\"\n        },\n        \"product\": {\n          \"type\": \"string\"\n        },\n        \"date_added\": {\n          \"type\": \"string\"\n        },\n        \"required_action\": {\n          \"type\": \"string\"\n        },\n        \"due_date\": {\n          \"type\": \"string\"\n        },\n        \"known_ransomware_campaign_use\": {\n          \"type\": \"string\"\n        },\n        \"notes\": {\n          \"type\": \"string\"\n        },\n        \"urls\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        },\n        \"cwes\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"cve\",\n        \"known_ransomware_campaign_use\"\n      ]\n    },\n    \"Match\": {\n      \"$defs\": {\n        \"packages\": {\n          \"description\": \"is the list of packages affected by the vulnerability.\"\n        },\n        \"vulnerability\": {\n          \"description\": \"is the core advisory record for a single known vulnerability from a specific provider.\"\n        }\n      },\n      \"properties\": {\n        \"vulnerability\": {\n          \"$ref\": \"#/$defs/VulnerabilityInfo\"\n        },\n        \"packages\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/AffectedPackageInfo\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"vulnerability\",\n        \"packages\"\n      ]\n    },\n    \"Matches\": {\n      \"items\": {\n        \"$ref\": \"#/$defs/Match\"\n      },\n      \"type\": \"array\"\n    },\n    \"OperatingSystem\": {\n      \"properties\": {\n        \"name\": {\n          \"type\": \"string\"\n        },\n        \"version\": {\n          \"type\": \"string\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"name\",\n        \"version\"\n      ]\n    },\n    \"Package\": {\n      \"properties\": {\n        \"name\": {\n          \"type\": \"string\"\n        },\n        \"ecosystem\": {\n          \"type\": \"string\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"name\",\n        \"ecosystem\"\n      ]\n    },\n    \"Reference\": {\n      \"$defs\": {\n        \"tags\": {\n          \"description\": \"is a free-form organizational field to convey additional information about the reference\"\n        },\n        \"url\": {\n          \"description\": \"is the external resource\"\n        }\n      },\n      \"properties\": {\n        \"url\": {\n          \"type\": \"string\"\n        },\n        \"tags\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"url\"\n      ]\n    },\n    \"Severity\": {\n      \"$defs\": {\n        \"rank\": {\n          \"description\": \"is a free-form organizational field to convey priority over other severities\"\n        },\n        \"scheme\": {\n          \"description\": \"describes the quantitative method used to determine the Score, such as 'CVSS_V3'. Alternatively this makes\\nclaim that Value is qualitative, for example 'HML' (High, Medium, Low), CHMLN (critical-high-medium-low-negligible)\"\n        },\n        \"source\": {\n          \"description\": \"is the name of the source of the severity score (e.g. 'nvd@nist.gov' or 'security-advisories@github.com')\"\n        },\n        \"value\": {\n          \"description\": \"is the severity score (e.g. '7.5', 'CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N',  or 'high' )\"\n        }\n      },\n      \"properties\": {\n        \"scheme\": {\n          \"type\": \"string\"\n        },\n        \"value\": true,\n        \"source\": {\n          \"type\": \"string\"\n        },\n        \"rank\": {\n          \"type\": \"integer\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"scheme\",\n        \"value\",\n        \"rank\"\n      ]\n    },\n    \"VulnerabilityInfo\": {\n      \"$defs\": {\n        \"epss\": {\n          \"description\": \"is a list of Exploit Prediction Scoring System (EPSS) scores for the vulnerability\"\n        },\n        \"known_exploited\": {\n          \"description\": \"is a list of known exploited vulnerabilities from the CISA KEV dataset\"\n        },\n        \"modified_date\": {\n          \"description\": \"is the date the vulnerability record was last modified\"\n        },\n        \"provider\": {\n          \"description\": \"is the upstream data processor (usually Vunnel) that is responsible for vulnerability records. Each provider\\nshould be scoped to a specific vulnerability dataset, for instance, the 'ubuntu' provider for all records from\\nCanonicals' Ubuntu Security Notices (for all Ubuntu distro versions).\"\n        },\n        \"published_date\": {\n          \"description\": \"is the date the vulnerability record was first published\"\n        },\n        \"status\": {\n          \"description\": \"conveys the actionability of the current record (one of 'active', 'analyzing', 'rejected', 'disputed')\"\n        },\n        \"withdrawn_date\": {\n          \"description\": \"is the date the vulnerability record was withdrawn\"\n        }\n      },\n      \"properties\": {\n        \"id\": {\n          \"type\": \"string\"\n        },\n        \"assigner\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        },\n        \"description\": {\n          \"type\": \"string\"\n        },\n        \"refs\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/Reference\"\n          },\n          \"type\": \"array\"\n        },\n        \"aliases\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        },\n        \"severities\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/Severity\"\n          },\n          \"type\": \"array\"\n        },\n        \"provider\": {\n          \"type\": \"string\"\n        },\n        \"status\": {\n          \"type\": \"string\"\n        },\n        \"published_date\": {\n          \"type\": \"string\",\n          \"format\": \"date-time\"\n        },\n        \"modified_date\": {\n          \"type\": \"string\",\n          \"format\": \"date-time\"\n        },\n        \"withdrawn_date\": {\n          \"type\": \"string\",\n          \"format\": \"date-time\"\n        },\n        \"known_exploited\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/KnownExploited\"\n          },\n          \"type\": \"array\"\n        },\n        \"epss\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/EPSS\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"id\",\n        \"provider\",\n        \"status\"\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "schema/grype/db-search/json/schema-1.0.2.json",
    "content": "{\n  \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n  \"$id\": \"anchore.io/schema/grype/db-search/json/1.0.2/matches\",\n  \"$ref\": \"#/$defs/Matches\",\n  \"$defs\": {\n    \"AffectedPackageBlob\": {\n      \"$defs\": {\n        \"cves\": {\n          \"description\": \"is a list of Common Vulnerabilities and Exposures (CVE) identifiers related to this vulnerability.\"\n        },\n        \"qualifiers\": {\n          \"description\": \"are package attributes that confirm the package is affected by the vulnerability.\"\n        },\n        \"ranges\": {\n          \"description\": \"specifies the affected version ranges and fixes if available.\"\n        }\n      },\n      \"properties\": {\n        \"cves\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        },\n        \"qualifiers\": {\n          \"$ref\": \"#/$defs/AffectedPackageQualifiers\"\n        },\n        \"ranges\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/AffectedRange\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"AffectedPackageInfo\": {\n      \"$defs\": {\n        \"cpe\": {\n          \"description\": \"is a Common Platform Enumeration that is affected by the vulnerability\"\n        },\n        \"detail\": {\n          \"description\": \"is the detailed information about the affected package\"\n        },\n        \"namespace\": {\n          \"description\": \"is a holdover value from the v5 DB schema that combines provider and search methods into a single value\\nDeprecated: this field will be removed in a later version of the search schema\"\n        },\n        \"os\": {\n          \"description\": \"identifies the operating system release that the affected package is released for\"\n        },\n        \"package\": {\n          \"description\": \"identifies the name of the package in a specific ecosystem affected by the vulnerability\"\n        }\n      },\n      \"properties\": {\n        \"os\": {\n          \"$ref\": \"#/$defs/OperatingSystem\"\n        },\n        \"package\": {\n          \"$ref\": \"#/$defs/Package\"\n        },\n        \"cpe\": {\n          \"$ref\": \"#/$defs/CPE\"\n        },\n        \"namespace\": {\n          \"type\": \"string\"\n        },\n        \"detail\": {\n          \"$ref\": \"#/$defs/AffectedPackageBlob\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"namespace\",\n        \"detail\"\n      ]\n    },\n    \"AffectedPackageQualifiers\": {\n      \"$defs\": {\n        \"platform_cpes\": {\n          \"description\": \"lists Common Platform Enumeration (CPE) identifiers for affected platforms.\"\n        },\n        \"rpm_modularity\": {\n          \"description\": \"indicates if the package follows RPM modularity for versioning.\"\n        }\n      },\n      \"properties\": {\n        \"rpm_modularity\": {\n          \"type\": \"string\"\n        },\n        \"platform_cpes\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"AffectedRange\": {\n      \"$defs\": {\n        \"fix\": {\n          \"description\": \"provides details on the fix version and its state if available.\"\n        },\n        \"version\": {\n          \"description\": \"defines the version constraints for affected software.\"\n        }\n      },\n      \"properties\": {\n        \"version\": {\n          \"$ref\": \"#/$defs/AffectedVersion\"\n        },\n        \"fix\": {\n          \"$ref\": \"#/$defs/Fix\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"AffectedVersion\": {\n      \"$defs\": {\n        \"constraint\": {\n          \"description\": \"defines the version range constraint for affected versions.\"\n        },\n        \"type\": {\n          \"description\": \"specifies the versioning system used (e.g., 'semver', 'rpm').\"\n        }\n      },\n      \"properties\": {\n        \"type\": {\n          \"type\": \"string\"\n        },\n        \"constraint\": {\n          \"type\": \"string\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"CPE\": {\n      \"properties\": {\n        \"ID\": {\n          \"type\": \"integer\"\n        },\n        \"Part\": {\n          \"type\": \"string\"\n        },\n        \"Vendor\": {\n          \"type\": \"string\"\n        },\n        \"Product\": {\n          \"type\": \"string\"\n        },\n        \"Edition\": {\n          \"type\": \"string\"\n        },\n        \"Language\": {\n          \"type\": \"string\"\n        },\n        \"SoftwareEdition\": {\n          \"type\": \"string\"\n        },\n        \"TargetHardware\": {\n          \"type\": \"string\"\n        },\n        \"TargetSoftware\": {\n          \"type\": \"string\"\n        },\n        \"Other\": {\n          \"type\": \"string\"\n        },\n        \"Packages\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/Package\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"ID\",\n        \"Part\",\n        \"Vendor\",\n        \"Product\",\n        \"Edition\",\n        \"Language\",\n        \"SoftwareEdition\",\n        \"TargetHardware\",\n        \"TargetSoftware\",\n        \"Other\",\n        \"Packages\"\n      ]\n    },\n    \"EPSS\": {\n      \"properties\": {\n        \"cve\": {\n          \"type\": \"string\"\n        },\n        \"epss\": {\n          \"type\": \"number\"\n        },\n        \"percentile\": {\n          \"type\": \"number\"\n        },\n        \"date\": {\n          \"type\": \"string\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"cve\",\n        \"epss\",\n        \"percentile\",\n        \"date\"\n      ]\n    },\n    \"Fix\": {\n      \"$defs\": {\n        \"detail\": {\n          \"description\": \"provides additional fix information, such as commit details.\"\n        },\n        \"state\": {\n          \"description\": \"represents the status of the fix (e.g., 'fixed', 'unaffected').\"\n        },\n        \"version\": {\n          \"description\": \"is the version number of the fix.\"\n        }\n      },\n      \"properties\": {\n        \"version\": {\n          \"type\": \"string\"\n        },\n        \"state\": {\n          \"type\": \"string\"\n        },\n        \"detail\": {\n          \"$ref\": \"#/$defs/FixDetail\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"FixDetail\": {\n      \"$defs\": {\n        \"git_commit\": {\n          \"description\": \"is the identifier for the Git commit associated with the fix.\"\n        },\n        \"references\": {\n          \"description\": \"contains URLs or identifiers for additional resources on the fix.\"\n        },\n        \"timestamp\": {\n          \"description\": \"is the date and time when the fix was committed.\"\n        }\n      },\n      \"properties\": {\n        \"git_commit\": {\n          \"type\": \"string\"\n        },\n        \"timestamp\": {\n          \"type\": \"string\",\n          \"format\": \"date-time\"\n        },\n        \"references\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/Reference\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"KnownExploited\": {\n      \"properties\": {\n        \"cve\": {\n          \"type\": \"string\"\n        },\n        \"vendor_project\": {\n          \"type\": \"string\"\n        },\n        \"product\": {\n          \"type\": \"string\"\n        },\n        \"date_added\": {\n          \"type\": \"string\"\n        },\n        \"required_action\": {\n          \"type\": \"string\"\n        },\n        \"due_date\": {\n          \"type\": \"string\"\n        },\n        \"known_ransomware_campaign_use\": {\n          \"type\": \"string\"\n        },\n        \"notes\": {\n          \"type\": \"string\"\n        },\n        \"urls\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        },\n        \"cwes\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"cve\",\n        \"known_ransomware_campaign_use\"\n      ]\n    },\n    \"Match\": {\n      \"$defs\": {\n        \"packages\": {\n          \"description\": \"is the list of packages affected by the vulnerability.\"\n        },\n        \"vulnerability\": {\n          \"description\": \"is the core advisory record for a single known vulnerability from a specific provider.\"\n        }\n      },\n      \"properties\": {\n        \"vulnerability\": {\n          \"$ref\": \"#/$defs/VulnerabilityInfo\"\n        },\n        \"packages\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/AffectedPackageInfo\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"vulnerability\",\n        \"packages\"\n      ]\n    },\n    \"Matches\": {\n      \"items\": {\n        \"$ref\": \"#/$defs/Match\"\n      },\n      \"type\": \"array\"\n    },\n    \"OperatingSystem\": {\n      \"properties\": {\n        \"name\": {\n          \"type\": \"string\"\n        },\n        \"version\": {\n          \"type\": \"string\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"name\",\n        \"version\"\n      ]\n    },\n    \"Package\": {\n      \"properties\": {\n        \"name\": {\n          \"type\": \"string\"\n        },\n        \"ecosystem\": {\n          \"type\": \"string\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"name\",\n        \"ecosystem\"\n      ]\n    },\n    \"Reference\": {\n      \"$defs\": {\n        \"tags\": {\n          \"description\": \"is a free-form organizational field to convey additional information about the reference\"\n        },\n        \"url\": {\n          \"description\": \"is the external resource\"\n        }\n      },\n      \"properties\": {\n        \"url\": {\n          \"type\": \"string\"\n        },\n        \"tags\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"url\"\n      ]\n    },\n    \"Severity\": {\n      \"$defs\": {\n        \"rank\": {\n          \"description\": \"is a free-form organizational field to convey priority over other severities\"\n        },\n        \"scheme\": {\n          \"description\": \"describes the quantitative method used to determine the Score, such as 'CVSS_V3'. Alternatively this makes\\nclaim that Value is qualitative, for example 'HML' (High, Medium, Low), CHMLN (critical-high-medium-low-negligible)\"\n        },\n        \"source\": {\n          \"description\": \"is the name of the source of the severity score (e.g. 'nvd@nist.gov' or 'security-advisories@github.com')\"\n        },\n        \"value\": {\n          \"description\": \"is the severity score (e.g. '7.5', 'CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N',  or 'high' )\"\n        }\n      },\n      \"properties\": {\n        \"scheme\": {\n          \"type\": \"string\"\n        },\n        \"value\": true,\n        \"source\": {\n          \"type\": \"string\"\n        },\n        \"rank\": {\n          \"type\": \"integer\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"scheme\",\n        \"value\",\n        \"rank\"\n      ]\n    },\n    \"VulnerabilityInfo\": {\n      \"$defs\": {\n        \"epss\": {\n          \"description\": \"is a list of Exploit Prediction Scoring System (EPSS) scores for the vulnerability\"\n        },\n        \"known_exploited\": {\n          \"description\": \"is a list of known exploited vulnerabilities from the CISA KEV dataset\"\n        },\n        \"modified_date\": {\n          \"description\": \"is the date the vulnerability record was last modified\"\n        },\n        \"provider\": {\n          \"description\": \"is the upstream data processor (usually Vunnel) that is responsible for vulnerability records. Each provider\\nshould be scoped to a specific vulnerability dataset, for instance, the 'ubuntu' provider for all records from\\nCanonicals' Ubuntu Security Notices (for all Ubuntu distro versions).\"\n        },\n        \"published_date\": {\n          \"description\": \"is the date the vulnerability record was first published\"\n        },\n        \"status\": {\n          \"description\": \"conveys the actionability of the current record (one of 'active', 'analyzing', 'rejected', 'disputed')\"\n        },\n        \"withdrawn_date\": {\n          \"description\": \"is the date the vulnerability record was withdrawn\"\n        }\n      },\n      \"properties\": {\n        \"id\": {\n          \"type\": \"string\"\n        },\n        \"assigner\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        },\n        \"description\": {\n          \"type\": \"string\"\n        },\n        \"refs\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/Reference\"\n          },\n          \"type\": \"array\"\n        },\n        \"aliases\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        },\n        \"severities\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/Severity\"\n          },\n          \"type\": \"array\"\n        },\n        \"provider\": {\n          \"type\": \"string\"\n        },\n        \"status\": {\n          \"type\": \"string\"\n        },\n        \"published_date\": {\n          \"type\": \"string\",\n          \"format\": \"date-time\"\n        },\n        \"modified_date\": {\n          \"type\": \"string\",\n          \"format\": \"date-time\"\n        },\n        \"withdrawn_date\": {\n          \"type\": \"string\",\n          \"format\": \"date-time\"\n        },\n        \"known_exploited\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/KnownExploited\"\n          },\n          \"type\": \"array\"\n        },\n        \"epss\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/EPSS\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"id\",\n        \"provider\",\n        \"status\"\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "schema/grype/db-search/json/schema-1.0.3.json",
    "content": "{\n  \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n  \"$id\": \"anchore.io/schema/grype/db-search/json/1.0.3/matches\",\n  \"$ref\": \"#/$defs/Matches\",\n  \"$defs\": {\n    \"AffectedPackageBlob\": {\n      \"$defs\": {\n        \"cves\": {\n          \"description\": \"is a list of Common Vulnerabilities and Exposures (CVE) identifiers related to this vulnerability.\"\n        },\n        \"qualifiers\": {\n          \"description\": \"are package attributes that confirm the package is affected by the vulnerability.\"\n        },\n        \"ranges\": {\n          \"description\": \"specifies the affected version ranges and fixes if available.\"\n        }\n      },\n      \"properties\": {\n        \"cves\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        },\n        \"qualifiers\": {\n          \"$ref\": \"#/$defs/AffectedPackageQualifiers\"\n        },\n        \"ranges\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/AffectedRange\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"AffectedPackageInfo\": {\n      \"$defs\": {\n        \"cpe\": {\n          \"description\": \"is a Common Platform Enumeration that is affected by the vulnerability\"\n        },\n        \"detail\": {\n          \"description\": \"is the detailed information about the affected package\"\n        },\n        \"namespace\": {\n          \"description\": \"is a holdover value from the v5 DB schema that combines provider and search methods into a single value\\nDeprecated: this field will be removed in a later version of the search schema\"\n        },\n        \"os\": {\n          \"description\": \"identifies the operating system release that the affected package is released for\"\n        },\n        \"package\": {\n          \"description\": \"identifies the name of the package in a specific ecosystem affected by the vulnerability\"\n        }\n      },\n      \"properties\": {\n        \"os\": {\n          \"$ref\": \"#/$defs/OperatingSystem\"\n        },\n        \"package\": {\n          \"$ref\": \"#/$defs/Package\"\n        },\n        \"cpe\": {\n          \"$ref\": \"#/$defs/CPE\"\n        },\n        \"namespace\": {\n          \"type\": \"string\"\n        },\n        \"detail\": {\n          \"$ref\": \"#/$defs/AffectedPackageBlob\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"namespace\",\n        \"detail\"\n      ]\n    },\n    \"AffectedPackageQualifiers\": {\n      \"$defs\": {\n        \"platform_cpes\": {\n          \"description\": \"lists Common Platform Enumeration (CPE) identifiers for affected platforms.\"\n        },\n        \"rpm_modularity\": {\n          \"description\": \"indicates if the package follows RPM modularity for versioning.\"\n        }\n      },\n      \"properties\": {\n        \"rpm_modularity\": {\n          \"type\": \"string\"\n        },\n        \"platform_cpes\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"AffectedRange\": {\n      \"$defs\": {\n        \"fix\": {\n          \"description\": \"provides details on the fix version and its state if available.\"\n        },\n        \"version\": {\n          \"description\": \"defines the version constraints for affected software.\"\n        }\n      },\n      \"properties\": {\n        \"version\": {\n          \"$ref\": \"#/$defs/AffectedVersion\"\n        },\n        \"fix\": {\n          \"$ref\": \"#/$defs/Fix\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"AffectedVersion\": {\n      \"$defs\": {\n        \"constraint\": {\n          \"description\": \"defines the version range constraint for affected versions.\"\n        },\n        \"type\": {\n          \"description\": \"specifies the versioning system used (e.g., 'semver', 'rpm').\"\n        }\n      },\n      \"properties\": {\n        \"type\": {\n          \"type\": \"string\"\n        },\n        \"constraint\": {\n          \"type\": \"string\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"CPE\": {\n      \"properties\": {\n        \"ID\": {\n          \"type\": \"integer\"\n        },\n        \"Part\": {\n          \"type\": \"string\"\n        },\n        \"Vendor\": {\n          \"type\": \"string\"\n        },\n        \"Product\": {\n          \"type\": \"string\"\n        },\n        \"Edition\": {\n          \"type\": \"string\"\n        },\n        \"Language\": {\n          \"type\": \"string\"\n        },\n        \"SoftwareEdition\": {\n          \"type\": \"string\"\n        },\n        \"TargetHardware\": {\n          \"type\": \"string\"\n        },\n        \"TargetSoftware\": {\n          \"type\": \"string\"\n        },\n        \"Other\": {\n          \"type\": \"string\"\n        },\n        \"Packages\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/Package\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"ID\",\n        \"Part\",\n        \"Vendor\",\n        \"Product\",\n        \"Edition\",\n        \"Language\",\n        \"SoftwareEdition\",\n        \"TargetHardware\",\n        \"TargetSoftware\",\n        \"Other\",\n        \"Packages\"\n      ]\n    },\n    \"EPSS\": {\n      \"properties\": {\n        \"cve\": {\n          \"type\": \"string\"\n        },\n        \"epss\": {\n          \"type\": \"number\"\n        },\n        \"percentile\": {\n          \"type\": \"number\"\n        },\n        \"date\": {\n          \"type\": \"string\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"cve\",\n        \"epss\",\n        \"percentile\",\n        \"date\"\n      ]\n    },\n    \"Fix\": {\n      \"$defs\": {\n        \"detail\": {\n          \"description\": \"provides additional fix information, such as commit details.\"\n        },\n        \"state\": {\n          \"description\": \"represents the status of the fix (e.g., 'fixed', 'unaffected').\"\n        },\n        \"version\": {\n          \"description\": \"is the version number of the fix.\"\n        }\n      },\n      \"properties\": {\n        \"version\": {\n          \"type\": \"string\"\n        },\n        \"state\": {\n          \"type\": \"string\"\n        },\n        \"detail\": {\n          \"$ref\": \"#/$defs/FixDetail\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"FixDetail\": {\n      \"$defs\": {\n        \"git_commit\": {\n          \"description\": \"is the identifier for the Git commit associated with the fix.\"\n        },\n        \"references\": {\n          \"description\": \"contains URLs or identifiers for additional resources on the fix.\"\n        },\n        \"timestamp\": {\n          \"description\": \"is the date and time when the fix was committed.\"\n        }\n      },\n      \"properties\": {\n        \"git_commit\": {\n          \"type\": \"string\"\n        },\n        \"timestamp\": {\n          \"type\": \"string\",\n          \"format\": \"date-time\"\n        },\n        \"references\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/Reference\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"KnownExploited\": {\n      \"properties\": {\n        \"cve\": {\n          \"type\": \"string\"\n        },\n        \"vendor_project\": {\n          \"type\": \"string\"\n        },\n        \"product\": {\n          \"type\": \"string\"\n        },\n        \"date_added\": {\n          \"type\": \"string\"\n        },\n        \"required_action\": {\n          \"type\": \"string\"\n        },\n        \"due_date\": {\n          \"type\": \"string\"\n        },\n        \"known_ransomware_campaign_use\": {\n          \"type\": \"string\"\n        },\n        \"notes\": {\n          \"type\": \"string\"\n        },\n        \"urls\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        },\n        \"cwes\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"cve\",\n        \"known_ransomware_campaign_use\"\n      ]\n    },\n    \"Match\": {\n      \"$defs\": {\n        \"packages\": {\n          \"description\": \"is the list of packages affected by the vulnerability.\"\n        },\n        \"vulnerability\": {\n          \"description\": \"is the core advisory record for a single known vulnerability from a specific provider.\"\n        }\n      },\n      \"properties\": {\n        \"vulnerability\": {\n          \"$ref\": \"#/$defs/VulnerabilityInfo\"\n        },\n        \"packages\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/AffectedPackageInfo\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"vulnerability\",\n        \"packages\"\n      ]\n    },\n    \"Matches\": {\n      \"items\": {\n        \"$ref\": \"#/$defs/Match\"\n      },\n      \"type\": \"array\"\n    },\n    \"OperatingSystem\": {\n      \"properties\": {\n        \"name\": {\n          \"type\": \"string\"\n        },\n        \"version\": {\n          \"type\": \"string\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"name\",\n        \"version\"\n      ]\n    },\n    \"Package\": {\n      \"properties\": {\n        \"name\": {\n          \"type\": \"string\"\n        },\n        \"ecosystem\": {\n          \"type\": \"string\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"name\",\n        \"ecosystem\"\n      ]\n    },\n    \"Reference\": {\n      \"$defs\": {\n        \"tags\": {\n          \"description\": \"is a free-form organizational field to convey additional information about the reference\"\n        },\n        \"url\": {\n          \"description\": \"is the external resource\"\n        }\n      },\n      \"properties\": {\n        \"url\": {\n          \"type\": \"string\"\n        },\n        \"tags\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"url\"\n      ]\n    },\n    \"Severity\": {\n      \"$defs\": {\n        \"rank\": {\n          \"description\": \"is a free-form organizational field to convey priority over other severities\"\n        },\n        \"scheme\": {\n          \"description\": \"describes the quantitative method used to determine the Score, such as 'CVSS_V3'. Alternatively this makes\\nclaim that Value is qualitative, for example 'HML' (High, Medium, Low), CHMLN (critical-high-medium-low-negligible)\"\n        },\n        \"source\": {\n          \"description\": \"is the name of the source of the severity score (e.g. 'nvd@nist.gov' or 'security-advisories@github.com')\"\n        },\n        \"value\": {\n          \"description\": \"is the severity score (e.g. '7.5', 'CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N',  or 'high' )\"\n        }\n      },\n      \"properties\": {\n        \"scheme\": {\n          \"type\": \"string\"\n        },\n        \"value\": true,\n        \"source\": {\n          \"type\": \"string\"\n        },\n        \"rank\": {\n          \"type\": \"integer\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"scheme\",\n        \"value\",\n        \"rank\"\n      ]\n    },\n    \"VulnerabilityInfo\": {\n      \"$defs\": {\n        \"epss\": {\n          \"description\": \"is a list of Exploit Prediction Scoring System (EPSS) scores for the vulnerability\"\n        },\n        \"known_exploited\": {\n          \"description\": \"is a list of known exploited vulnerabilities from the CISA KEV dataset\"\n        },\n        \"modified_date\": {\n          \"description\": \"is the date the vulnerability record was last modified\"\n        },\n        \"provider\": {\n          \"description\": \"is the upstream data processor (usually Vunnel) that is responsible for vulnerability records. Each provider\\nshould be scoped to a specific vulnerability dataset, for instance, the 'ubuntu' provider for all records from\\nCanonicals' Ubuntu Security Notices (for all Ubuntu distro versions).\"\n        },\n        \"published_date\": {\n          \"description\": \"is the date the vulnerability record was first published\"\n        },\n        \"severity\": {\n          \"description\": \"is the single string representation of the vulnerability's severity based on the set of available severity values\"\n        },\n        \"status\": {\n          \"description\": \"conveys the actionability of the current record (one of 'active', 'analyzing', 'rejected', 'disputed')\"\n        },\n        \"withdrawn_date\": {\n          \"description\": \"is the date the vulnerability record was withdrawn\"\n        }\n      },\n      \"properties\": {\n        \"id\": {\n          \"type\": \"string\"\n        },\n        \"assigner\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        },\n        \"description\": {\n          \"type\": \"string\"\n        },\n        \"refs\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/Reference\"\n          },\n          \"type\": \"array\"\n        },\n        \"aliases\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        },\n        \"severities\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/Severity\"\n          },\n          \"type\": \"array\"\n        },\n        \"severity\": {\n          \"type\": \"string\"\n        },\n        \"provider\": {\n          \"type\": \"string\"\n        },\n        \"status\": {\n          \"type\": \"string\"\n        },\n        \"published_date\": {\n          \"type\": \"string\",\n          \"format\": \"date-time\"\n        },\n        \"modified_date\": {\n          \"type\": \"string\",\n          \"format\": \"date-time\"\n        },\n        \"withdrawn_date\": {\n          \"type\": \"string\",\n          \"format\": \"date-time\"\n        },\n        \"known_exploited\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/KnownExploited\"\n          },\n          \"type\": \"array\"\n        },\n        \"epss\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/EPSS\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"id\",\n        \"provider\",\n        \"status\"\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "schema/grype/db-search/json/schema-1.1.0.json",
    "content": "{\n  \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n  \"$id\": \"anchore.io/schema/grype/db-search/json/1.1.0/matches\",\n  \"$ref\": \"#/$defs/Matches\",\n  \"$defs\": {\n    \"AffectedPackageBlob\": {\n      \"$defs\": {\n        \"cves\": {\n          \"description\": \"is a list of Common Vulnerabilities and Exposures (CVE) identifiers related to this vulnerability.\"\n        },\n        \"qualifiers\": {\n          \"description\": \"are package attributes that confirm the package is affected by the vulnerability.\"\n        },\n        \"ranges\": {\n          \"description\": \"specifies the affected version ranges and fixes if available.\"\n        }\n      },\n      \"properties\": {\n        \"cves\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        },\n        \"qualifiers\": {\n          \"$ref\": \"#/$defs/AffectedPackageQualifiers\"\n        },\n        \"ranges\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/AffectedRange\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"AffectedPackageInfo\": {\n      \"$defs\": {\n        \"cpe\": {\n          \"description\": \"is a Common Platform Enumeration that is affected by the vulnerability\"\n        },\n        \"detail\": {\n          \"description\": \"is the detailed information about the affected package\"\n        },\n        \"namespace\": {\n          \"description\": \"is a holdover value from the v5 DB schema that combines provider and search methods into a single value\\nDeprecated: this field will be removed in a later version of the search schema\"\n        },\n        \"os\": {\n          \"description\": \"identifies the operating system release that the affected package is released for\"\n        },\n        \"package\": {\n          \"description\": \"identifies the name of the package in a specific ecosystem affected by the vulnerability\"\n        }\n      },\n      \"properties\": {\n        \"os\": {\n          \"$ref\": \"#/$defs/OperatingSystem\"\n        },\n        \"package\": {\n          \"$ref\": \"#/$defs/Package\"\n        },\n        \"cpe\": {\n          \"$ref\": \"#/$defs/CPE\"\n        },\n        \"namespace\": {\n          \"type\": \"string\"\n        },\n        \"detail\": {\n          \"$ref\": \"#/$defs/AffectedPackageBlob\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"namespace\",\n        \"detail\"\n      ]\n    },\n    \"AffectedPackageQualifiers\": {\n      \"$defs\": {\n        \"platform_cpes\": {\n          \"description\": \"lists Common Platform Enumeration (CPE) identifiers for affected platforms.\"\n        },\n        \"rpm_modularity\": {\n          \"description\": \"indicates if the package follows RPM modularity for versioning.\"\n        }\n      },\n      \"properties\": {\n        \"rpm_modularity\": {\n          \"type\": \"string\"\n        },\n        \"platform_cpes\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"AffectedRange\": {\n      \"$defs\": {\n        \"fix\": {\n          \"description\": \"provides details on the fix version and its state if available.\"\n        },\n        \"version\": {\n          \"description\": \"defines the version constraints for affected software.\"\n        }\n      },\n      \"properties\": {\n        \"version\": {\n          \"$ref\": \"#/$defs/AffectedVersion\"\n        },\n        \"fix\": {\n          \"$ref\": \"#/$defs/Fix\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"AffectedVersion\": {\n      \"$defs\": {\n        \"constraint\": {\n          \"description\": \"defines the version range constraint for affected versions.\"\n        },\n        \"type\": {\n          \"description\": \"specifies the versioning system used (e.g., 'semver', 'rpm').\"\n        }\n      },\n      \"properties\": {\n        \"type\": {\n          \"type\": \"string\"\n        },\n        \"constraint\": {\n          \"type\": \"string\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"CPE\": {\n      \"properties\": {\n        \"ID\": {\n          \"type\": \"integer\"\n        },\n        \"Part\": {\n          \"type\": \"string\"\n        },\n        \"Vendor\": {\n          \"type\": \"string\"\n        },\n        \"Product\": {\n          \"type\": \"string\"\n        },\n        \"Edition\": {\n          \"type\": \"string\"\n        },\n        \"Language\": {\n          \"type\": \"string\"\n        },\n        \"SoftwareEdition\": {\n          \"type\": \"string\"\n        },\n        \"TargetHardware\": {\n          \"type\": \"string\"\n        },\n        \"TargetSoftware\": {\n          \"type\": \"string\"\n        },\n        \"Other\": {\n          \"type\": \"string\"\n        },\n        \"Packages\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/Package\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"ID\",\n        \"Part\",\n        \"Vendor\",\n        \"Product\",\n        \"Edition\",\n        \"Language\",\n        \"SoftwareEdition\",\n        \"TargetHardware\",\n        \"TargetSoftware\",\n        \"Other\",\n        \"Packages\"\n      ]\n    },\n    \"EPSS\": {\n      \"properties\": {\n        \"cve\": {\n          \"type\": \"string\"\n        },\n        \"epss\": {\n          \"type\": \"number\"\n        },\n        \"percentile\": {\n          \"type\": \"number\"\n        },\n        \"date\": {\n          \"type\": \"string\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"cve\",\n        \"epss\",\n        \"percentile\",\n        \"date\"\n      ]\n    },\n    \"Fix\": {\n      \"$defs\": {\n        \"detail\": {\n          \"description\": \"provides additional fix information, such as commit details.\"\n        },\n        \"state\": {\n          \"description\": \"represents the status of the fix (e.g., 'fixed', 'unaffected').\"\n        },\n        \"version\": {\n          \"description\": \"is the version number of the fix.\"\n        }\n      },\n      \"properties\": {\n        \"version\": {\n          \"type\": \"string\"\n        },\n        \"state\": {\n          \"type\": \"string\"\n        },\n        \"detail\": {\n          \"$ref\": \"#/$defs/FixDetail\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"FixAvailability\": {\n      \"$defs\": {\n        \"date\": {\n          \"description\": \"is the date and time when fix information became available. Note: this might not be when the fix was created, committed or merged.\"\n        },\n        \"kind\": {\n          \"description\": \"describes how this date was obtained (e.g. advisory, release, commit, PR, issue, first-observed-record)\"\n        }\n      },\n      \"properties\": {\n        \"date\": {\n          \"type\": \"string\",\n          \"format\": \"date-time\"\n        },\n        \"kind\": {\n          \"type\": \"string\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"FixDetail\": {\n      \"$defs\": {\n        \"available\": {\n          \"description\": \"indicates when the fix information became available and how it was obtained.\"\n        },\n        \"references\": {\n          \"description\": \"contains URLs or identifiers for additional resources on the fix.\"\n        }\n      },\n      \"properties\": {\n        \"available\": {\n          \"$ref\": \"#/$defs/FixAvailability\"\n        },\n        \"references\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/Reference\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"KnownExploited\": {\n      \"properties\": {\n        \"cve\": {\n          \"type\": \"string\"\n        },\n        \"vendor_project\": {\n          \"type\": \"string\"\n        },\n        \"product\": {\n          \"type\": \"string\"\n        },\n        \"date_added\": {\n          \"type\": \"string\"\n        },\n        \"required_action\": {\n          \"type\": \"string\"\n        },\n        \"due_date\": {\n          \"type\": \"string\"\n        },\n        \"known_ransomware_campaign_use\": {\n          \"type\": \"string\"\n        },\n        \"notes\": {\n          \"type\": \"string\"\n        },\n        \"urls\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        },\n        \"cwes\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"cve\",\n        \"known_ransomware_campaign_use\"\n      ]\n    },\n    \"Match\": {\n      \"$defs\": {\n        \"packages\": {\n          \"description\": \"is the list of packages affected by the vulnerability.\"\n        },\n        \"vulnerability\": {\n          \"description\": \"is the core advisory record for a single known vulnerability from a specific provider.\"\n        }\n      },\n      \"properties\": {\n        \"vulnerability\": {\n          \"$ref\": \"#/$defs/VulnerabilityInfo\"\n        },\n        \"packages\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/AffectedPackageInfo\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"vulnerability\",\n        \"packages\"\n      ]\n    },\n    \"Matches\": {\n      \"items\": {\n        \"$ref\": \"#/$defs/Match\"\n      },\n      \"type\": \"array\"\n    },\n    \"OperatingSystem\": {\n      \"properties\": {\n        \"name\": {\n          \"type\": \"string\"\n        },\n        \"version\": {\n          \"type\": \"string\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"name\",\n        \"version\"\n      ]\n    },\n    \"Package\": {\n      \"properties\": {\n        \"name\": {\n          \"type\": \"string\"\n        },\n        \"ecosystem\": {\n          \"type\": \"string\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"name\",\n        \"ecosystem\"\n      ]\n    },\n    \"Reference\": {\n      \"$defs\": {\n        \"tags\": {\n          \"description\": \"is a free-form organizational field to convey additional information about the reference\"\n        },\n        \"url\": {\n          \"description\": \"is the external resource\"\n        }\n      },\n      \"properties\": {\n        \"url\": {\n          \"type\": \"string\"\n        },\n        \"tags\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"url\"\n      ]\n    },\n    \"Severity\": {\n      \"$defs\": {\n        \"rank\": {\n          \"description\": \"is a free-form organizational field to convey priority over other severities\"\n        },\n        \"scheme\": {\n          \"description\": \"describes the quantitative method used to determine the Score, such as 'CVSS_V3'. Alternatively this makes\\nclaim that Value is qualitative, for example 'HML' (High, Medium, Low), CHMLN (critical-high-medium-low-negligible)\"\n        },\n        \"source\": {\n          \"description\": \"is the name of the source of the severity score (e.g. 'nvd@nist.gov' or 'security-advisories@github.com')\"\n        },\n        \"value\": {\n          \"description\": \"is the severity score (e.g. '7.5', 'CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N',  or 'high' )\"\n        }\n      },\n      \"properties\": {\n        \"scheme\": {\n          \"type\": \"string\"\n        },\n        \"value\": true,\n        \"source\": {\n          \"type\": \"string\"\n        },\n        \"rank\": {\n          \"type\": \"integer\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"scheme\",\n        \"value\",\n        \"rank\"\n      ]\n    },\n    \"VulnerabilityInfo\": {\n      \"$defs\": {\n        \"epss\": {\n          \"description\": \"is a list of Exploit Prediction Scoring System (EPSS) scores for the vulnerability\"\n        },\n        \"known_exploited\": {\n          \"description\": \"is a list of known exploited vulnerabilities from the CISA KEV dataset\"\n        },\n        \"modified_date\": {\n          \"description\": \"is the date the vulnerability record was last modified\"\n        },\n        \"provider\": {\n          \"description\": \"is the upstream data processor (usually Vunnel) that is responsible for vulnerability records. Each provider\\nshould be scoped to a specific vulnerability dataset, for instance, the 'ubuntu' provider for all records from\\nCanonicals' Ubuntu Security Notices (for all Ubuntu distro versions).\"\n        },\n        \"published_date\": {\n          \"description\": \"is the date the vulnerability record was first published\"\n        },\n        \"severity\": {\n          \"description\": \"is the single string representation of the vulnerability's severity based on the set of available severity values\"\n        },\n        \"status\": {\n          \"description\": \"conveys the actionability of the current record (one of 'active', 'analyzing', 'rejected', 'disputed')\"\n        },\n        \"withdrawn_date\": {\n          \"description\": \"is the date the vulnerability record was withdrawn\"\n        }\n      },\n      \"properties\": {\n        \"id\": {\n          \"type\": \"string\"\n        },\n        \"assigner\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        },\n        \"description\": {\n          \"type\": \"string\"\n        },\n        \"refs\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/Reference\"\n          },\n          \"type\": \"array\"\n        },\n        \"aliases\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        },\n        \"severities\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/Severity\"\n          },\n          \"type\": \"array\"\n        },\n        \"severity\": {\n          \"type\": \"string\"\n        },\n        \"provider\": {\n          \"type\": \"string\"\n        },\n        \"status\": {\n          \"type\": \"string\"\n        },\n        \"published_date\": {\n          \"type\": \"string\",\n          \"format\": \"date-time\"\n        },\n        \"modified_date\": {\n          \"type\": \"string\",\n          \"format\": \"date-time\"\n        },\n        \"withdrawn_date\": {\n          \"type\": \"string\",\n          \"format\": \"date-time\"\n        },\n        \"known_exploited\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/KnownExploited\"\n          },\n          \"type\": \"array\"\n        },\n        \"epss\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/EPSS\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"id\",\n        \"provider\",\n        \"status\"\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "schema/grype/db-search/json/schema-1.1.1.json",
    "content": "{\n  \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n  \"$id\": \"anchore.io/schema/grype/db-search/json/1.1.1/matches\",\n  \"$ref\": \"#/$defs/Matches\",\n  \"$defs\": {\n    \"AffectedPackageInfo\": {\n      \"$defs\": {\n        \"cpe\": {\n          \"description\": \"is a Common Platform Enumeration that is affected by the vulnerability\"\n        },\n        \"detail\": {\n          \"description\": \"is the detailed information about the affected package\"\n        },\n        \"namespace\": {\n          \"description\": \"is a holdover value from the v5 DB schema that combines provider and search methods into a single value\\nDeprecated: this field will be removed in a later version of the search schema\"\n        },\n        \"os\": {\n          \"description\": \"identifies the operating system release that the affected package is released for\"\n        },\n        \"package\": {\n          \"description\": \"identifies the name of the package in a specific ecosystem affected by the vulnerability\"\n        }\n      },\n      \"properties\": {\n        \"os\": {\n          \"$ref\": \"#/$defs/OperatingSystem\"\n        },\n        \"package\": {\n          \"$ref\": \"#/$defs/Package\"\n        },\n        \"cpe\": {\n          \"$ref\": \"#/$defs/CPE\"\n        },\n        \"namespace\": {\n          \"type\": \"string\"\n        },\n        \"detail\": {\n          \"$ref\": \"#/$defs/PackageBlob\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"namespace\",\n        \"detail\"\n      ]\n    },\n    \"CPE\": {\n      \"properties\": {\n        \"ID\": {\n          \"type\": \"integer\"\n        },\n        \"Part\": {\n          \"type\": \"string\"\n        },\n        \"Vendor\": {\n          \"type\": \"string\"\n        },\n        \"Product\": {\n          \"type\": \"string\"\n        },\n        \"Edition\": {\n          \"type\": \"string\"\n        },\n        \"Language\": {\n          \"type\": \"string\"\n        },\n        \"SoftwareEdition\": {\n          \"type\": \"string\"\n        },\n        \"TargetHardware\": {\n          \"type\": \"string\"\n        },\n        \"TargetSoftware\": {\n          \"type\": \"string\"\n        },\n        \"Other\": {\n          \"type\": \"string\"\n        },\n        \"Packages\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/Package\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"ID\",\n        \"Part\",\n        \"Vendor\",\n        \"Product\",\n        \"Edition\",\n        \"Language\",\n        \"SoftwareEdition\",\n        \"TargetHardware\",\n        \"TargetSoftware\",\n        \"Other\",\n        \"Packages\"\n      ]\n    },\n    \"EPSS\": {\n      \"properties\": {\n        \"cve\": {\n          \"type\": \"string\"\n        },\n        \"epss\": {\n          \"type\": \"number\"\n        },\n        \"percentile\": {\n          \"type\": \"number\"\n        },\n        \"date\": {\n          \"type\": \"string\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"cve\",\n        \"epss\",\n        \"percentile\",\n        \"date\"\n      ]\n    },\n    \"Fix\": {\n      \"$defs\": {\n        \"detail\": {\n          \"description\": \"provides additional fix information, such as commit details.\"\n        },\n        \"state\": {\n          \"description\": \"represents the status of the fix (e.g., 'fixed', 'unaffected').\"\n        },\n        \"version\": {\n          \"description\": \"is the version number of the fix.\"\n        }\n      },\n      \"properties\": {\n        \"version\": {\n          \"type\": \"string\"\n        },\n        \"state\": {\n          \"type\": \"string\"\n        },\n        \"detail\": {\n          \"$ref\": \"#/$defs/FixDetail\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"FixAvailability\": {\n      \"$defs\": {\n        \"date\": {\n          \"description\": \"is the date and time when fix information became available. Note: this might not be when the fix was created, committed or merged.\"\n        },\n        \"kind\": {\n          \"description\": \"describes how this date was obtained (e.g. advisory, release, commit, PR, issue, first-observed-record)\"\n        }\n      },\n      \"properties\": {\n        \"date\": {\n          \"type\": \"string\",\n          \"format\": \"date-time\"\n        },\n        \"kind\": {\n          \"type\": \"string\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"FixDetail\": {\n      \"$defs\": {\n        \"available\": {\n          \"description\": \"indicates when the fix information became available and how it was obtained.\"\n        },\n        \"references\": {\n          \"description\": \"contains URLs or identifiers for additional resources on the fix.\"\n        }\n      },\n      \"properties\": {\n        \"available\": {\n          \"$ref\": \"#/$defs/FixAvailability\"\n        },\n        \"references\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/Reference\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"KnownExploited\": {\n      \"properties\": {\n        \"cve\": {\n          \"type\": \"string\"\n        },\n        \"vendor_project\": {\n          \"type\": \"string\"\n        },\n        \"product\": {\n          \"type\": \"string\"\n        },\n        \"date_added\": {\n          \"type\": \"string\"\n        },\n        \"required_action\": {\n          \"type\": \"string\"\n        },\n        \"due_date\": {\n          \"type\": \"string\"\n        },\n        \"known_ransomware_campaign_use\": {\n          \"type\": \"string\"\n        },\n        \"notes\": {\n          \"type\": \"string\"\n        },\n        \"urls\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        },\n        \"cwes\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"cve\",\n        \"known_ransomware_campaign_use\"\n      ]\n    },\n    \"Match\": {\n      \"$defs\": {\n        \"packages\": {\n          \"description\": \"is the list of packages affected by the vulnerability.\"\n        },\n        \"vulnerability\": {\n          \"description\": \"is the core advisory record for a single known vulnerability from a specific provider.\"\n        }\n      },\n      \"properties\": {\n        \"vulnerability\": {\n          \"$ref\": \"#/$defs/VulnerabilityInfo\"\n        },\n        \"packages\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/AffectedPackageInfo\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"vulnerability\",\n        \"packages\"\n      ]\n    },\n    \"Matches\": {\n      \"items\": {\n        \"$ref\": \"#/$defs/Match\"\n      },\n      \"type\": \"array\"\n    },\n    \"OperatingSystem\": {\n      \"properties\": {\n        \"name\": {\n          \"type\": \"string\"\n        },\n        \"version\": {\n          \"type\": \"string\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"name\",\n        \"version\"\n      ]\n    },\n    \"Package\": {\n      \"properties\": {\n        \"name\": {\n          \"type\": \"string\"\n        },\n        \"ecosystem\": {\n          \"type\": \"string\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"name\",\n        \"ecosystem\"\n      ]\n    },\n    \"PackageBlob\": {\n      \"$defs\": {\n        \"cves\": {\n          \"description\": \"is a list of Common Vulnerabilities and Exposures (CVE) identifiers related to this vulnerability.\"\n        },\n        \"qualifiers\": {\n          \"description\": \"are package attributes that confirm the package is affected by the vulnerability.\"\n        },\n        \"ranges\": {\n          \"description\": \"specifies the affected version ranges and fixes if available.\"\n        }\n      },\n      \"properties\": {\n        \"cves\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        },\n        \"qualifiers\": {\n          \"$ref\": \"#/$defs/PackageQualifiers\"\n        },\n        \"ranges\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/Range\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"PackageQualifiers\": {\n      \"$defs\": {\n        \"platform_cpes\": {\n          \"description\": \"lists Common Platform Enumeration (CPE) identifiers for affected platforms.\"\n        },\n        \"rpm_modularity\": {\n          \"description\": \"indicates if the package follows RPM modularity for versioning.\"\n        }\n      },\n      \"properties\": {\n        \"rpm_modularity\": {\n          \"type\": \"string\"\n        },\n        \"platform_cpes\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"Range\": {\n      \"$defs\": {\n        \"fix\": {\n          \"description\": \"provides details on the fix version and its state if available.\"\n        },\n        \"version\": {\n          \"description\": \"defines the version constraints for affected software.\"\n        }\n      },\n      \"properties\": {\n        \"version\": {\n          \"$ref\": \"#/$defs/Version\"\n        },\n        \"fix\": {\n          \"$ref\": \"#/$defs/Fix\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"Reference\": {\n      \"$defs\": {\n        \"tags\": {\n          \"description\": \"is a free-form organizational field to convey additional information about the reference\"\n        },\n        \"url\": {\n          \"description\": \"is the external resource\"\n        }\n      },\n      \"properties\": {\n        \"url\": {\n          \"type\": \"string\"\n        },\n        \"tags\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"url\"\n      ]\n    },\n    \"Severity\": {\n      \"$defs\": {\n        \"rank\": {\n          \"description\": \"is a free-form organizational field to convey priority over other severities\"\n        },\n        \"scheme\": {\n          \"description\": \"describes the quantitative method used to determine the Score, such as 'CVSS_V3'. Alternatively this makes\\nclaim that Value is qualitative, for example 'HML' (High, Medium, Low), CHMLN (critical-high-medium-low-negligible)\"\n        },\n        \"source\": {\n          \"description\": \"is the name of the source of the severity score (e.g. 'nvd@nist.gov' or 'security-advisories@github.com')\"\n        },\n        \"value\": {\n          \"description\": \"is the severity score (e.g. '7.5', 'CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N',  or 'high' )\"\n        }\n      },\n      \"properties\": {\n        \"scheme\": {\n          \"type\": \"string\"\n        },\n        \"value\": true,\n        \"source\": {\n          \"type\": \"string\"\n        },\n        \"rank\": {\n          \"type\": \"integer\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"scheme\",\n        \"value\",\n        \"rank\"\n      ]\n    },\n    \"Version\": {\n      \"$defs\": {\n        \"constraint\": {\n          \"description\": \"defines the version range constraint for affected versions.\"\n        },\n        \"type\": {\n          \"description\": \"specifies the versioning system used (e.g., 'semver', 'rpm').\"\n        }\n      },\n      \"properties\": {\n        \"type\": {\n          \"type\": \"string\"\n        },\n        \"constraint\": {\n          \"type\": \"string\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"VulnerabilityInfo\": {\n      \"$defs\": {\n        \"epss\": {\n          \"description\": \"is a list of Exploit Prediction Scoring System (EPSS) scores for the vulnerability\"\n        },\n        \"known_exploited\": {\n          \"description\": \"is a list of known exploited vulnerabilities from the CISA KEV dataset\"\n        },\n        \"modified_date\": {\n          \"description\": \"is the date the vulnerability record was last modified\"\n        },\n        \"provider\": {\n          \"description\": \"is the upstream data processor (usually Vunnel) that is responsible for vulnerability records. Each provider\\nshould be scoped to a specific vulnerability dataset, for instance, the 'ubuntu' provider for all records from\\nCanonicals' Ubuntu Security Notices (for all Ubuntu distro versions).\"\n        },\n        \"published_date\": {\n          \"description\": \"is the date the vulnerability record was first published\"\n        },\n        \"severity\": {\n          \"description\": \"is the single string representation of the vulnerability's severity based on the set of available severity values\"\n        },\n        \"status\": {\n          \"description\": \"conveys the actionability of the current record (one of 'active', 'analyzing', 'rejected', 'disputed')\"\n        },\n        \"withdrawn_date\": {\n          \"description\": \"is the date the vulnerability record was withdrawn\"\n        }\n      },\n      \"properties\": {\n        \"id\": {\n          \"type\": \"string\"\n        },\n        \"assigner\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        },\n        \"description\": {\n          \"type\": \"string\"\n        },\n        \"refs\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/Reference\"\n          },\n          \"type\": \"array\"\n        },\n        \"aliases\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        },\n        \"severities\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/Severity\"\n          },\n          \"type\": \"array\"\n        },\n        \"severity\": {\n          \"type\": \"string\"\n        },\n        \"provider\": {\n          \"type\": \"string\"\n        },\n        \"status\": {\n          \"type\": \"string\"\n        },\n        \"published_date\": {\n          \"type\": \"string\",\n          \"format\": \"date-time\"\n        },\n        \"modified_date\": {\n          \"type\": \"string\",\n          \"format\": \"date-time\"\n        },\n        \"withdrawn_date\": {\n          \"type\": \"string\",\n          \"format\": \"date-time\"\n        },\n        \"known_exploited\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/KnownExploited\"\n          },\n          \"type\": \"array\"\n        },\n        \"epss\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/EPSS\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"id\",\n        \"provider\",\n        \"status\"\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "schema/grype/db-search/json/schema-1.1.2.json",
    "content": "{\n  \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n  \"$id\": \"anchore.io/schema/grype/db-search/json/1.1.2/matches\",\n  \"$ref\": \"#/$defs/Matches\",\n  \"$defs\": {\n    \"AffectedPackageInfo\": {\n      \"$defs\": {\n        \"cpe\": {\n          \"description\": \"is a Common Platform Enumeration that is affected by the vulnerability\"\n        },\n        \"detail\": {\n          \"description\": \"is the detailed information about the affected package\"\n        },\n        \"namespace\": {\n          \"description\": \"is a holdover value from the v5 DB schema that combines provider and search methods into a single value\\nDeprecated: this field will be removed in a later version of the search schema\"\n        },\n        \"os\": {\n          \"description\": \"identifies the operating system release that the affected package is released for\"\n        },\n        \"package\": {\n          \"description\": \"identifies the name of the package in a specific ecosystem affected by the vulnerability\"\n        }\n      },\n      \"properties\": {\n        \"os\": {\n          \"$ref\": \"#/$defs/OperatingSystem\"\n        },\n        \"package\": {\n          \"$ref\": \"#/$defs/Package\"\n        },\n        \"cpe\": {\n          \"$ref\": \"#/$defs/CPE\"\n        },\n        \"namespace\": {\n          \"type\": \"string\"\n        },\n        \"detail\": {\n          \"$ref\": \"#/$defs/PackageBlob\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"namespace\",\n        \"detail\"\n      ]\n    },\n    \"CPE\": {\n      \"properties\": {\n        \"ID\": {\n          \"type\": \"integer\"\n        },\n        \"Part\": {\n          \"type\": \"string\"\n        },\n        \"Vendor\": {\n          \"type\": \"string\"\n        },\n        \"Product\": {\n          \"type\": \"string\"\n        },\n        \"Edition\": {\n          \"type\": \"string\"\n        },\n        \"Language\": {\n          \"type\": \"string\"\n        },\n        \"SoftwareEdition\": {\n          \"type\": \"string\"\n        },\n        \"TargetHardware\": {\n          \"type\": \"string\"\n        },\n        \"TargetSoftware\": {\n          \"type\": \"string\"\n        },\n        \"Other\": {\n          \"type\": \"string\"\n        },\n        \"Packages\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/Package\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"ID\",\n        \"Part\",\n        \"Vendor\",\n        \"Product\",\n        \"Edition\",\n        \"Language\",\n        \"SoftwareEdition\",\n        \"TargetHardware\",\n        \"TargetSoftware\",\n        \"Other\",\n        \"Packages\"\n      ]\n    },\n    \"CWE\": {\n      \"properties\": {\n        \"cve\": {\n          \"type\": \"string\"\n        },\n        \"cwe\": {\n          \"type\": \"string\"\n        },\n        \"source\": {\n          \"type\": \"string\"\n        },\n        \"type\": {\n          \"type\": \"string\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"cve\",\n        \"cwe\",\n        \"source\",\n        \"type\"\n      ]\n    },\n    \"EPSS\": {\n      \"properties\": {\n        \"cve\": {\n          \"type\": \"string\"\n        },\n        \"epss\": {\n          \"type\": \"number\"\n        },\n        \"percentile\": {\n          \"type\": \"number\"\n        },\n        \"date\": {\n          \"type\": \"string\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"cve\",\n        \"epss\",\n        \"percentile\",\n        \"date\"\n      ]\n    },\n    \"Fix\": {\n      \"$defs\": {\n        \"detail\": {\n          \"description\": \"provides additional fix information, such as commit details.\"\n        },\n        \"state\": {\n          \"description\": \"represents the status of the fix (e.g., 'fixed', 'unaffected').\"\n        },\n        \"version\": {\n          \"description\": \"is the version number of the fix.\"\n        }\n      },\n      \"properties\": {\n        \"version\": {\n          \"type\": \"string\"\n        },\n        \"state\": {\n          \"type\": \"string\"\n        },\n        \"detail\": {\n          \"$ref\": \"#/$defs/FixDetail\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"FixAvailability\": {\n      \"$defs\": {\n        \"date\": {\n          \"description\": \"is the date and time when fix information became available. Note: this might not be when the fix was created, committed or merged.\"\n        },\n        \"kind\": {\n          \"description\": \"describes how this date was obtained (e.g. advisory, release, commit, PR, issue, first-observed-record)\"\n        }\n      },\n      \"properties\": {\n        \"date\": {\n          \"type\": \"string\",\n          \"format\": \"date-time\"\n        },\n        \"kind\": {\n          \"type\": \"string\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"FixDetail\": {\n      \"$defs\": {\n        \"available\": {\n          \"description\": \"indicates when the fix information became available and how it was obtained.\"\n        },\n        \"references\": {\n          \"description\": \"contains URLs or identifiers for additional resources on the fix.\"\n        }\n      },\n      \"properties\": {\n        \"available\": {\n          \"$ref\": \"#/$defs/FixAvailability\"\n        },\n        \"references\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/Reference\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"KnownExploited\": {\n      \"properties\": {\n        \"cve\": {\n          \"type\": \"string\"\n        },\n        \"vendor_project\": {\n          \"type\": \"string\"\n        },\n        \"product\": {\n          \"type\": \"string\"\n        },\n        \"date_added\": {\n          \"type\": \"string\"\n        },\n        \"required_action\": {\n          \"type\": \"string\"\n        },\n        \"due_date\": {\n          \"type\": \"string\"\n        },\n        \"known_ransomware_campaign_use\": {\n          \"type\": \"string\"\n        },\n        \"notes\": {\n          \"type\": \"string\"\n        },\n        \"urls\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        },\n        \"cwes\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"cve\",\n        \"known_ransomware_campaign_use\"\n      ]\n    },\n    \"Match\": {\n      \"$defs\": {\n        \"packages\": {\n          \"description\": \"is the list of packages affected by the vulnerability.\"\n        },\n        \"vulnerability\": {\n          \"description\": \"is the core advisory record for a single known vulnerability from a specific provider.\"\n        }\n      },\n      \"properties\": {\n        \"vulnerability\": {\n          \"$ref\": \"#/$defs/VulnerabilityInfo\"\n        },\n        \"packages\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/AffectedPackageInfo\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"vulnerability\",\n        \"packages\"\n      ]\n    },\n    \"Matches\": {\n      \"items\": {\n        \"$ref\": \"#/$defs/Match\"\n      },\n      \"type\": \"array\"\n    },\n    \"OperatingSystem\": {\n      \"properties\": {\n        \"name\": {\n          \"type\": \"string\"\n        },\n        \"version\": {\n          \"type\": \"string\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"name\",\n        \"version\"\n      ]\n    },\n    \"Package\": {\n      \"properties\": {\n        \"name\": {\n          \"type\": \"string\"\n        },\n        \"ecosystem\": {\n          \"type\": \"string\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"name\",\n        \"ecosystem\"\n      ]\n    },\n    \"PackageBlob\": {\n      \"$defs\": {\n        \"cves\": {\n          \"description\": \"is a list of Common Vulnerabilities and Exposures (CVE) identifiers related to this vulnerability.\"\n        },\n        \"qualifiers\": {\n          \"description\": \"are package attributes that confirm the package is affected by the vulnerability.\"\n        },\n        \"ranges\": {\n          \"description\": \"specifies the affected version ranges and fixes if available.\"\n        }\n      },\n      \"properties\": {\n        \"cves\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        },\n        \"qualifiers\": {\n          \"$ref\": \"#/$defs/PackageQualifiers\"\n        },\n        \"ranges\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/Range\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"PackageQualifiers\": {\n      \"$defs\": {\n        \"platform_cpes\": {\n          \"description\": \"lists Common Platform Enumeration (CPE) identifiers for affected platforms.\"\n        },\n        \"rpm_modularity\": {\n          \"description\": \"indicates if the package follows RPM modularity for versioning.\"\n        }\n      },\n      \"properties\": {\n        \"rpm_modularity\": {\n          \"type\": \"string\"\n        },\n        \"platform_cpes\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"Range\": {\n      \"$defs\": {\n        \"fix\": {\n          \"description\": \"provides details on the fix version and its state if available.\"\n        },\n        \"version\": {\n          \"description\": \"defines the version constraints for affected software.\"\n        }\n      },\n      \"properties\": {\n        \"version\": {\n          \"$ref\": \"#/$defs/Version\"\n        },\n        \"fix\": {\n          \"$ref\": \"#/$defs/Fix\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"Reference\": {\n      \"$defs\": {\n        \"tags\": {\n          \"description\": \"is a free-form organizational field to convey additional information about the reference\"\n        },\n        \"url\": {\n          \"description\": \"is the external resource\"\n        }\n      },\n      \"properties\": {\n        \"url\": {\n          \"type\": \"string\"\n        },\n        \"tags\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"url\"\n      ]\n    },\n    \"Severity\": {\n      \"$defs\": {\n        \"rank\": {\n          \"description\": \"is a free-form organizational field to convey priority over other severities\"\n        },\n        \"scheme\": {\n          \"description\": \"describes the quantitative method used to determine the Score, such as 'CVSS_V3'. Alternatively this makes\\nclaim that Value is qualitative, for example 'HML' (High, Medium, Low), CHMLN (critical-high-medium-low-negligible)\"\n        },\n        \"source\": {\n          \"description\": \"is the name of the source of the severity score (e.g. 'nvd@nist.gov' or 'security-advisories@github.com')\"\n        },\n        \"value\": {\n          \"description\": \"is the severity score (e.g. '7.5', 'CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N',  or 'high' )\"\n        }\n      },\n      \"properties\": {\n        \"scheme\": {\n          \"type\": \"string\"\n        },\n        \"value\": true,\n        \"source\": {\n          \"type\": \"string\"\n        },\n        \"rank\": {\n          \"type\": \"integer\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"scheme\",\n        \"value\",\n        \"rank\"\n      ]\n    },\n    \"Version\": {\n      \"$defs\": {\n        \"constraint\": {\n          \"description\": \"defines the version range constraint for affected versions.\"\n        },\n        \"type\": {\n          \"description\": \"specifies the versioning system used (e.g., 'semver', 'rpm').\"\n        }\n      },\n      \"properties\": {\n        \"type\": {\n          \"type\": \"string\"\n        },\n        \"constraint\": {\n          \"type\": \"string\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"VulnerabilityInfo\": {\n      \"$defs\": {\n        \"cwes\": {\n          \"description\": \"is a list of Common Weakness Enumeration (CWE) identifiers for the vulnerability\"\n        },\n        \"epss\": {\n          \"description\": \"is a list of Exploit Prediction Scoring System (EPSS) scores for the vulnerability\"\n        },\n        \"known_exploited\": {\n          \"description\": \"is a list of known exploited vulnerabilities from the CISA KEV dataset\"\n        },\n        \"modified_date\": {\n          \"description\": \"is the date the vulnerability record was last modified\"\n        },\n        \"provider\": {\n          \"description\": \"is the upstream data processor (usually Vunnel) that is responsible for vulnerability records. Each provider\\nshould be scoped to a specific vulnerability dataset, for instance, the 'ubuntu' provider for all records from\\nCanonicals' Ubuntu Security Notices (for all Ubuntu distro versions).\"\n        },\n        \"published_date\": {\n          \"description\": \"is the date the vulnerability record was first published\"\n        },\n        \"severity\": {\n          \"description\": \"is the single string representation of the vulnerability's severity based on the set of available severity values\"\n        },\n        \"status\": {\n          \"description\": \"conveys the actionability of the current record (one of 'active', 'analyzing', 'rejected', 'disputed')\"\n        },\n        \"withdrawn_date\": {\n          \"description\": \"is the date the vulnerability record was withdrawn\"\n        }\n      },\n      \"properties\": {\n        \"id\": {\n          \"type\": \"string\"\n        },\n        \"assigner\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        },\n        \"description\": {\n          \"type\": \"string\"\n        },\n        \"refs\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/Reference\"\n          },\n          \"type\": \"array\"\n        },\n        \"aliases\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        },\n        \"severities\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/Severity\"\n          },\n          \"type\": \"array\"\n        },\n        \"severity\": {\n          \"type\": \"string\"\n        },\n        \"provider\": {\n          \"type\": \"string\"\n        },\n        \"status\": {\n          \"type\": \"string\"\n        },\n        \"published_date\": {\n          \"type\": \"string\",\n          \"format\": \"date-time\"\n        },\n        \"modified_date\": {\n          \"type\": \"string\",\n          \"format\": \"date-time\"\n        },\n        \"withdrawn_date\": {\n          \"type\": \"string\",\n          \"format\": \"date-time\"\n        },\n        \"known_exploited\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/KnownExploited\"\n          },\n          \"type\": \"array\"\n        },\n        \"epss\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/EPSS\"\n          },\n          \"type\": \"array\"\n        },\n        \"cwes\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/CWE\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"id\",\n        \"provider\",\n        \"status\"\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "schema/grype/db-search/json/schema-1.1.3.json",
    "content": "{\n  \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n  \"$id\": \"anchore.io/schema/grype/db-search/json/1.1.3/matches\",\n  \"$ref\": \"#/$defs/Matches\",\n  \"$defs\": {\n    \"AffectedPackageInfo\": {\n      \"$defs\": {\n        \"cpe\": {\n          \"description\": \"is a Common Platform Enumeration that is affected by the vulnerability\"\n        },\n        \"detail\": {\n          \"description\": \"is the detailed information about the affected package\"\n        },\n        \"namespace\": {\n          \"description\": \"is a holdover value from the v5 DB schema that combines provider and search methods into a single value\\n\\nDeprecated: this field will be removed in a later version of the search schema\"\n        },\n        \"os\": {\n          \"description\": \"identifies the operating system release that the affected package is released for\"\n        },\n        \"package\": {\n          \"description\": \"identifies the name of the package in a specific ecosystem affected by the vulnerability\"\n        }\n      },\n      \"properties\": {\n        \"os\": {\n          \"$ref\": \"#/$defs/OperatingSystem\"\n        },\n        \"package\": {\n          \"$ref\": \"#/$defs/Package\"\n        },\n        \"cpe\": {\n          \"$ref\": \"#/$defs/CPE\"\n        },\n        \"namespace\": {\n          \"type\": \"string\"\n        },\n        \"detail\": {\n          \"$ref\": \"#/$defs/PackageBlob\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"namespace\",\n        \"detail\"\n      ]\n    },\n    \"CPE\": {\n      \"properties\": {\n        \"ID\": {\n          \"type\": \"integer\"\n        },\n        \"Part\": {\n          \"type\": \"string\"\n        },\n        \"Vendor\": {\n          \"type\": \"string\"\n        },\n        \"Product\": {\n          \"type\": \"string\"\n        },\n        \"Edition\": {\n          \"type\": \"string\"\n        },\n        \"Language\": {\n          \"type\": \"string\"\n        },\n        \"SoftwareEdition\": {\n          \"type\": \"string\"\n        },\n        \"TargetHardware\": {\n          \"type\": \"string\"\n        },\n        \"TargetSoftware\": {\n          \"type\": \"string\"\n        },\n        \"Other\": {\n          \"type\": \"string\"\n        },\n        \"Packages\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/Package\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"ID\",\n        \"Part\",\n        \"Vendor\",\n        \"Product\",\n        \"Edition\",\n        \"Language\",\n        \"SoftwareEdition\",\n        \"TargetHardware\",\n        \"TargetSoftware\",\n        \"Other\",\n        \"Packages\"\n      ]\n    },\n    \"CWE\": {\n      \"properties\": {\n        \"cve\": {\n          \"type\": \"string\"\n        },\n        \"cwe\": {\n          \"type\": \"string\"\n        },\n        \"source\": {\n          \"type\": \"string\"\n        },\n        \"type\": {\n          \"type\": \"string\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"cve\",\n        \"cwe\",\n        \"source\",\n        \"type\"\n      ]\n    },\n    \"EPSS\": {\n      \"properties\": {\n        \"cve\": {\n          \"type\": \"string\"\n        },\n        \"epss\": {\n          \"type\": \"number\"\n        },\n        \"percentile\": {\n          \"type\": \"number\"\n        },\n        \"date\": {\n          \"type\": \"string\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"cve\",\n        \"epss\",\n        \"percentile\",\n        \"date\"\n      ]\n    },\n    \"Fix\": {\n      \"$defs\": {\n        \"detail\": {\n          \"description\": \"provides additional fix information, such as commit details.\"\n        },\n        \"state\": {\n          \"description\": \"represents the status of the fix (e.g., 'fixed', 'unaffected').\"\n        },\n        \"version\": {\n          \"description\": \"is the version number of the fix.\"\n        }\n      },\n      \"properties\": {\n        \"version\": {\n          \"type\": \"string\"\n        },\n        \"state\": {\n          \"type\": \"string\"\n        },\n        \"detail\": {\n          \"$ref\": \"#/$defs/FixDetail\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"FixAvailability\": {\n      \"$defs\": {\n        \"date\": {\n          \"description\": \"is the date and time when fix information became available. Note: this might not be when the fix was created, committed or merged.\"\n        },\n        \"kind\": {\n          \"description\": \"describes how this date was obtained (e.g. advisory, release, commit, PR, issue, first-observed-record)\"\n        }\n      },\n      \"properties\": {\n        \"date\": {\n          \"type\": \"string\",\n          \"format\": \"date-time\"\n        },\n        \"kind\": {\n          \"type\": \"string\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"FixDetail\": {\n      \"$defs\": {\n        \"available\": {\n          \"description\": \"indicates when the fix information became available and how it was obtained.\"\n        },\n        \"references\": {\n          \"description\": \"contains URLs or identifiers for additional resources on the fix.\"\n        }\n      },\n      \"properties\": {\n        \"available\": {\n          \"$ref\": \"#/$defs/FixAvailability\"\n        },\n        \"references\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/Reference\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"KnownExploited\": {\n      \"properties\": {\n        \"cve\": {\n          \"type\": \"string\"\n        },\n        \"vendor_project\": {\n          \"type\": \"string\"\n        },\n        \"product\": {\n          \"type\": \"string\"\n        },\n        \"date_added\": {\n          \"type\": \"string\"\n        },\n        \"required_action\": {\n          \"type\": \"string\"\n        },\n        \"due_date\": {\n          \"type\": \"string\"\n        },\n        \"known_ransomware_campaign_use\": {\n          \"type\": \"string\"\n        },\n        \"notes\": {\n          \"type\": \"string\"\n        },\n        \"urls\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        },\n        \"cwes\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"cve\",\n        \"known_ransomware_campaign_use\"\n      ]\n    },\n    \"Match\": {\n      \"$defs\": {\n        \"packages\": {\n          \"description\": \"is the list of packages affected by the vulnerability.\"\n        },\n        \"vulnerability\": {\n          \"description\": \"is the core advisory record for a single known vulnerability from a specific provider.\"\n        }\n      },\n      \"properties\": {\n        \"vulnerability\": {\n          \"$ref\": \"#/$defs/VulnerabilityInfo\"\n        },\n        \"packages\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/AffectedPackageInfo\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"vulnerability\",\n        \"packages\"\n      ]\n    },\n    \"Matches\": {\n      \"items\": {\n        \"$ref\": \"#/$defs/Match\"\n      },\n      \"type\": \"array\"\n    },\n    \"OperatingSystem\": {\n      \"properties\": {\n        \"name\": {\n          \"type\": \"string\"\n        },\n        \"version\": {\n          \"type\": \"string\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"name\",\n        \"version\"\n      ]\n    },\n    \"Package\": {\n      \"properties\": {\n        \"name\": {\n          \"type\": \"string\"\n        },\n        \"ecosystem\": {\n          \"type\": \"string\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"name\",\n        \"ecosystem\"\n      ]\n    },\n    \"PackageBlob\": {\n      \"$defs\": {\n        \"cves\": {\n          \"description\": \"is a list of Common Vulnerabilities and Exposures (CVE) identifiers related to this vulnerability.\"\n        },\n        \"qualifiers\": {\n          \"description\": \"are package attributes that confirm the package is affected by the vulnerability.\"\n        },\n        \"ranges\": {\n          \"description\": \"specifies the affected version ranges and fixes if available.\"\n        }\n      },\n      \"properties\": {\n        \"cves\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        },\n        \"qualifiers\": {\n          \"$ref\": \"#/$defs/PackageQualifiers\"\n        },\n        \"ranges\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/Range\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"PackageQualifiers\": {\n      \"$defs\": {\n        \"platform_cpes\": {\n          \"description\": \"lists Common Platform Enumeration (CPE) identifiers for affected platforms.\"\n        },\n        \"rpm_modularity\": {\n          \"description\": \"indicates if the package follows RPM modularity for versioning.\"\n        }\n      },\n      \"properties\": {\n        \"rpm_modularity\": {\n          \"type\": \"string\"\n        },\n        \"platform_cpes\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"Range\": {\n      \"$defs\": {\n        \"fix\": {\n          \"description\": \"provides details on the fix version and its state if available.\"\n        },\n        \"version\": {\n          \"description\": \"defines the version constraints for affected software.\"\n        }\n      },\n      \"properties\": {\n        \"version\": {\n          \"$ref\": \"#/$defs/Version\"\n        },\n        \"fix\": {\n          \"$ref\": \"#/$defs/Fix\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"Reference\": {\n      \"$defs\": {\n        \"id\": {\n          \"description\": \"is an optional identifier for the reference (e.g., advisory ID like 'RHSA-2023:5455')\"\n        },\n        \"tags\": {\n          \"description\": \"is a free-form organizational field to convey additional information about the reference\"\n        },\n        \"url\": {\n          \"description\": \"is the external resource\"\n        }\n      },\n      \"properties\": {\n        \"url\": {\n          \"type\": \"string\"\n        },\n        \"id\": {\n          \"type\": \"string\"\n        },\n        \"tags\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"url\"\n      ]\n    },\n    \"Severity\": {\n      \"$defs\": {\n        \"rank\": {\n          \"description\": \"is a free-form organizational field to convey priority over other severities\"\n        },\n        \"scheme\": {\n          \"description\": \"describes the quantitative method used to determine the Score, such as 'CVSS_V3'. Alternatively this makes\\nclaim that Value is qualitative, for example 'HML' (High, Medium, Low), CHMLN (critical-high-medium-low-negligible)\"\n        },\n        \"source\": {\n          \"description\": \"is the name of the source of the severity score (e.g. 'nvd@nist.gov' or 'security-advisories@github.com')\"\n        },\n        \"value\": {\n          \"description\": \"is the severity score (e.g. '7.5', 'CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N',  or 'high' )\"\n        }\n      },\n      \"properties\": {\n        \"scheme\": {\n          \"type\": \"string\"\n        },\n        \"value\": true,\n        \"source\": {\n          \"type\": \"string\"\n        },\n        \"rank\": {\n          \"type\": \"integer\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"scheme\",\n        \"value\",\n        \"rank\"\n      ]\n    },\n    \"Version\": {\n      \"$defs\": {\n        \"constraint\": {\n          \"description\": \"defines the version range constraint for affected versions.\"\n        },\n        \"type\": {\n          \"description\": \"specifies the versioning system used (e.g., 'semver', 'rpm').\"\n        }\n      },\n      \"properties\": {\n        \"type\": {\n          \"type\": \"string\"\n        },\n        \"constraint\": {\n          \"type\": \"string\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"VulnerabilityInfo\": {\n      \"$defs\": {\n        \"cwes\": {\n          \"description\": \"is a list of Common Weakness Enumeration (CWE) identifiers for the vulnerability\"\n        },\n        \"epss\": {\n          \"description\": \"is a list of Exploit Prediction Scoring System (EPSS) scores for the vulnerability\"\n        },\n        \"known_exploited\": {\n          \"description\": \"is a list of known exploited vulnerabilities from the CISA KEV dataset\"\n        },\n        \"modified_date\": {\n          \"description\": \"is the date the vulnerability record was last modified\"\n        },\n        \"provider\": {\n          \"description\": \"is the upstream data processor (usually Vunnel) that is responsible for vulnerability records. Each provider\\nshould be scoped to a specific vulnerability dataset, for instance, the 'ubuntu' provider for all records from\\nCanonicals' Ubuntu Security Notices (for all Ubuntu distro versions).\"\n        },\n        \"published_date\": {\n          \"description\": \"is the date the vulnerability record was first published\"\n        },\n        \"severity\": {\n          \"description\": \"is the single string representation of the vulnerability's severity based on the set of available severity values\"\n        },\n        \"status\": {\n          \"description\": \"conveys the actionability of the current record (one of 'active', 'analyzing', 'rejected', 'disputed')\"\n        },\n        \"withdrawn_date\": {\n          \"description\": \"is the date the vulnerability record was withdrawn\"\n        }\n      },\n      \"properties\": {\n        \"id\": {\n          \"type\": \"string\"\n        },\n        \"assigner\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        },\n        \"description\": {\n          \"type\": \"string\"\n        },\n        \"refs\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/Reference\"\n          },\n          \"type\": \"array\"\n        },\n        \"aliases\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        },\n        \"severities\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/Severity\"\n          },\n          \"type\": \"array\"\n        },\n        \"severity\": {\n          \"type\": \"string\"\n        },\n        \"provider\": {\n          \"type\": \"string\"\n        },\n        \"status\": {\n          \"type\": \"string\"\n        },\n        \"published_date\": {\n          \"type\": \"string\",\n          \"format\": \"date-time\"\n        },\n        \"modified_date\": {\n          \"type\": \"string\",\n          \"format\": \"date-time\"\n        },\n        \"withdrawn_date\": {\n          \"type\": \"string\",\n          \"format\": \"date-time\"\n        },\n        \"known_exploited\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/KnownExploited\"\n          },\n          \"type\": \"array\"\n        },\n        \"epss\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/EPSS\"\n          },\n          \"type\": \"array\"\n        },\n        \"cwes\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/CWE\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"id\",\n        \"provider\",\n        \"status\"\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "schema/grype/db-search/json/schema-latest.json",
    "content": "{\n  \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n  \"$id\": \"anchore.io/schema/grype/db-search/json/1.1.3/matches\",\n  \"$ref\": \"#/$defs/Matches\",\n  \"$defs\": {\n    \"AffectedPackageInfo\": {\n      \"$defs\": {\n        \"cpe\": {\n          \"description\": \"is a Common Platform Enumeration that is affected by the vulnerability\"\n        },\n        \"detail\": {\n          \"description\": \"is the detailed information about the affected package\"\n        },\n        \"namespace\": {\n          \"description\": \"is a holdover value from the v5 DB schema that combines provider and search methods into a single value\\n\\nDeprecated: this field will be removed in a later version of the search schema\"\n        },\n        \"os\": {\n          \"description\": \"identifies the operating system release that the affected package is released for\"\n        },\n        \"package\": {\n          \"description\": \"identifies the name of the package in a specific ecosystem affected by the vulnerability\"\n        }\n      },\n      \"properties\": {\n        \"os\": {\n          \"$ref\": \"#/$defs/OperatingSystem\"\n        },\n        \"package\": {\n          \"$ref\": \"#/$defs/Package\"\n        },\n        \"cpe\": {\n          \"$ref\": \"#/$defs/CPE\"\n        },\n        \"namespace\": {\n          \"type\": \"string\"\n        },\n        \"detail\": {\n          \"$ref\": \"#/$defs/PackageBlob\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"namespace\",\n        \"detail\"\n      ]\n    },\n    \"CPE\": {\n      \"properties\": {\n        \"ID\": {\n          \"type\": \"integer\"\n        },\n        \"Part\": {\n          \"type\": \"string\"\n        },\n        \"Vendor\": {\n          \"type\": \"string\"\n        },\n        \"Product\": {\n          \"type\": \"string\"\n        },\n        \"Edition\": {\n          \"type\": \"string\"\n        },\n        \"Language\": {\n          \"type\": \"string\"\n        },\n        \"SoftwareEdition\": {\n          \"type\": \"string\"\n        },\n        \"TargetHardware\": {\n          \"type\": \"string\"\n        },\n        \"TargetSoftware\": {\n          \"type\": \"string\"\n        },\n        \"Other\": {\n          \"type\": \"string\"\n        },\n        \"Packages\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/Package\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"ID\",\n        \"Part\",\n        \"Vendor\",\n        \"Product\",\n        \"Edition\",\n        \"Language\",\n        \"SoftwareEdition\",\n        \"TargetHardware\",\n        \"TargetSoftware\",\n        \"Other\",\n        \"Packages\"\n      ]\n    },\n    \"CWE\": {\n      \"properties\": {\n        \"cve\": {\n          \"type\": \"string\"\n        },\n        \"cwe\": {\n          \"type\": \"string\"\n        },\n        \"source\": {\n          \"type\": \"string\"\n        },\n        \"type\": {\n          \"type\": \"string\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"cve\",\n        \"cwe\",\n        \"source\",\n        \"type\"\n      ]\n    },\n    \"EPSS\": {\n      \"properties\": {\n        \"cve\": {\n          \"type\": \"string\"\n        },\n        \"epss\": {\n          \"type\": \"number\"\n        },\n        \"percentile\": {\n          \"type\": \"number\"\n        },\n        \"date\": {\n          \"type\": \"string\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"cve\",\n        \"epss\",\n        \"percentile\",\n        \"date\"\n      ]\n    },\n    \"Fix\": {\n      \"$defs\": {\n        \"detail\": {\n          \"description\": \"provides additional fix information, such as commit details.\"\n        },\n        \"state\": {\n          \"description\": \"represents the status of the fix (e.g., 'fixed', 'unaffected').\"\n        },\n        \"version\": {\n          \"description\": \"is the version number of the fix.\"\n        }\n      },\n      \"properties\": {\n        \"version\": {\n          \"type\": \"string\"\n        },\n        \"state\": {\n          \"type\": \"string\"\n        },\n        \"detail\": {\n          \"$ref\": \"#/$defs/FixDetail\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"FixAvailability\": {\n      \"$defs\": {\n        \"date\": {\n          \"description\": \"is the date and time when fix information became available. Note: this might not be when the fix was created, committed or merged.\"\n        },\n        \"kind\": {\n          \"description\": \"describes how this date was obtained (e.g. advisory, release, commit, PR, issue, first-observed-record)\"\n        }\n      },\n      \"properties\": {\n        \"date\": {\n          \"type\": \"string\",\n          \"format\": \"date-time\"\n        },\n        \"kind\": {\n          \"type\": \"string\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"FixDetail\": {\n      \"$defs\": {\n        \"available\": {\n          \"description\": \"indicates when the fix information became available and how it was obtained.\"\n        },\n        \"references\": {\n          \"description\": \"contains URLs or identifiers for additional resources on the fix.\"\n        }\n      },\n      \"properties\": {\n        \"available\": {\n          \"$ref\": \"#/$defs/FixAvailability\"\n        },\n        \"references\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/Reference\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"KnownExploited\": {\n      \"properties\": {\n        \"cve\": {\n          \"type\": \"string\"\n        },\n        \"vendor_project\": {\n          \"type\": \"string\"\n        },\n        \"product\": {\n          \"type\": \"string\"\n        },\n        \"date_added\": {\n          \"type\": \"string\"\n        },\n        \"required_action\": {\n          \"type\": \"string\"\n        },\n        \"due_date\": {\n          \"type\": \"string\"\n        },\n        \"known_ransomware_campaign_use\": {\n          \"type\": \"string\"\n        },\n        \"notes\": {\n          \"type\": \"string\"\n        },\n        \"urls\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        },\n        \"cwes\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"cve\",\n        \"known_ransomware_campaign_use\"\n      ]\n    },\n    \"Match\": {\n      \"$defs\": {\n        \"packages\": {\n          \"description\": \"is the list of packages affected by the vulnerability.\"\n        },\n        \"vulnerability\": {\n          \"description\": \"is the core advisory record for a single known vulnerability from a specific provider.\"\n        }\n      },\n      \"properties\": {\n        \"vulnerability\": {\n          \"$ref\": \"#/$defs/VulnerabilityInfo\"\n        },\n        \"packages\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/AffectedPackageInfo\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"vulnerability\",\n        \"packages\"\n      ]\n    },\n    \"Matches\": {\n      \"items\": {\n        \"$ref\": \"#/$defs/Match\"\n      },\n      \"type\": \"array\"\n    },\n    \"OperatingSystem\": {\n      \"properties\": {\n        \"name\": {\n          \"type\": \"string\"\n        },\n        \"version\": {\n          \"type\": \"string\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"name\",\n        \"version\"\n      ]\n    },\n    \"Package\": {\n      \"properties\": {\n        \"name\": {\n          \"type\": \"string\"\n        },\n        \"ecosystem\": {\n          \"type\": \"string\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"name\",\n        \"ecosystem\"\n      ]\n    },\n    \"PackageBlob\": {\n      \"$defs\": {\n        \"cves\": {\n          \"description\": \"is a list of Common Vulnerabilities and Exposures (CVE) identifiers related to this vulnerability.\"\n        },\n        \"qualifiers\": {\n          \"description\": \"are package attributes that confirm the package is affected by the vulnerability.\"\n        },\n        \"ranges\": {\n          \"description\": \"specifies the affected version ranges and fixes if available.\"\n        }\n      },\n      \"properties\": {\n        \"cves\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        },\n        \"qualifiers\": {\n          \"$ref\": \"#/$defs/PackageQualifiers\"\n        },\n        \"ranges\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/Range\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"PackageQualifiers\": {\n      \"$defs\": {\n        \"platform_cpes\": {\n          \"description\": \"lists Common Platform Enumeration (CPE) identifiers for affected platforms.\"\n        },\n        \"rpm_modularity\": {\n          \"description\": \"indicates if the package follows RPM modularity for versioning.\"\n        }\n      },\n      \"properties\": {\n        \"rpm_modularity\": {\n          \"type\": \"string\"\n        },\n        \"platform_cpes\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"Range\": {\n      \"$defs\": {\n        \"fix\": {\n          \"description\": \"provides details on the fix version and its state if available.\"\n        },\n        \"version\": {\n          \"description\": \"defines the version constraints for affected software.\"\n        }\n      },\n      \"properties\": {\n        \"version\": {\n          \"$ref\": \"#/$defs/Version\"\n        },\n        \"fix\": {\n          \"$ref\": \"#/$defs/Fix\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"Reference\": {\n      \"$defs\": {\n        \"id\": {\n          \"description\": \"is an optional identifier for the reference (e.g., advisory ID like 'RHSA-2023:5455')\"\n        },\n        \"tags\": {\n          \"description\": \"is a free-form organizational field to convey additional information about the reference\"\n        },\n        \"url\": {\n          \"description\": \"is the external resource\"\n        }\n      },\n      \"properties\": {\n        \"url\": {\n          \"type\": \"string\"\n        },\n        \"id\": {\n          \"type\": \"string\"\n        },\n        \"tags\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"url\"\n      ]\n    },\n    \"Severity\": {\n      \"$defs\": {\n        \"rank\": {\n          \"description\": \"is a free-form organizational field to convey priority over other severities\"\n        },\n        \"scheme\": {\n          \"description\": \"describes the quantitative method used to determine the Score, such as 'CVSS_V3'. Alternatively this makes\\nclaim that Value is qualitative, for example 'HML' (High, Medium, Low), CHMLN (critical-high-medium-low-negligible)\"\n        },\n        \"source\": {\n          \"description\": \"is the name of the source of the severity score (e.g. 'nvd@nist.gov' or 'security-advisories@github.com')\"\n        },\n        \"value\": {\n          \"description\": \"is the severity score (e.g. '7.5', 'CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N',  or 'high' )\"\n        }\n      },\n      \"properties\": {\n        \"scheme\": {\n          \"type\": \"string\"\n        },\n        \"value\": true,\n        \"source\": {\n          \"type\": \"string\"\n        },\n        \"rank\": {\n          \"type\": \"integer\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"scheme\",\n        \"value\",\n        \"rank\"\n      ]\n    },\n    \"Version\": {\n      \"$defs\": {\n        \"constraint\": {\n          \"description\": \"defines the version range constraint for affected versions.\"\n        },\n        \"type\": {\n          \"description\": \"specifies the versioning system used (e.g., 'semver', 'rpm').\"\n        }\n      },\n      \"properties\": {\n        \"type\": {\n          \"type\": \"string\"\n        },\n        \"constraint\": {\n          \"type\": \"string\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"VulnerabilityInfo\": {\n      \"$defs\": {\n        \"cwes\": {\n          \"description\": \"is a list of Common Weakness Enumeration (CWE) identifiers for the vulnerability\"\n        },\n        \"epss\": {\n          \"description\": \"is a list of Exploit Prediction Scoring System (EPSS) scores for the vulnerability\"\n        },\n        \"known_exploited\": {\n          \"description\": \"is a list of known exploited vulnerabilities from the CISA KEV dataset\"\n        },\n        \"modified_date\": {\n          \"description\": \"is the date the vulnerability record was last modified\"\n        },\n        \"provider\": {\n          \"description\": \"is the upstream data processor (usually Vunnel) that is responsible for vulnerability records. Each provider\\nshould be scoped to a specific vulnerability dataset, for instance, the 'ubuntu' provider for all records from\\nCanonicals' Ubuntu Security Notices (for all Ubuntu distro versions).\"\n        },\n        \"published_date\": {\n          \"description\": \"is the date the vulnerability record was first published\"\n        },\n        \"severity\": {\n          \"description\": \"is the single string representation of the vulnerability's severity based on the set of available severity values\"\n        },\n        \"status\": {\n          \"description\": \"conveys the actionability of the current record (one of 'active', 'analyzing', 'rejected', 'disputed')\"\n        },\n        \"withdrawn_date\": {\n          \"description\": \"is the date the vulnerability record was withdrawn\"\n        }\n      },\n      \"properties\": {\n        \"id\": {\n          \"type\": \"string\"\n        },\n        \"assigner\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        },\n        \"description\": {\n          \"type\": \"string\"\n        },\n        \"refs\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/Reference\"\n          },\n          \"type\": \"array\"\n        },\n        \"aliases\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        },\n        \"severities\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/Severity\"\n          },\n          \"type\": \"array\"\n        },\n        \"severity\": {\n          \"type\": \"string\"\n        },\n        \"provider\": {\n          \"type\": \"string\"\n        },\n        \"status\": {\n          \"type\": \"string\"\n        },\n        \"published_date\": {\n          \"type\": \"string\",\n          \"format\": \"date-time\"\n        },\n        \"modified_date\": {\n          \"type\": \"string\",\n          \"format\": \"date-time\"\n        },\n        \"withdrawn_date\": {\n          \"type\": \"string\",\n          \"format\": \"date-time\"\n        },\n        \"known_exploited\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/KnownExploited\"\n          },\n          \"type\": \"array\"\n        },\n        \"epss\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/EPSS\"\n          },\n          \"type\": \"array\"\n        },\n        \"cwes\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/CWE\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"id\",\n        \"provider\",\n        \"status\"\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "schema/grype/db-search-vuln/json/README.md",
    "content": "# `db-search vuln` JSON Schema\n\nThis is the JSON schema for output from the `grype db search vuln` command. The required inputs for defining the JSON schema are as follows:\n\n- the value of `cmd/grype/cli/commands/internal/dbsearch.VulnerabilitiesSchemaVersion` that governs the schema version\n- the `Vulnerabilities` type definition within `github.com/anchore/grype/cmd/grype/cli/commands/internal/dbsearch/vulnerabilities.go` that governs the overall document shape\n\n## Versioning\n\nVersioning the JSON schema must be done manually by changing the `VulnerabilitiesSchemaVersion` constant within `cmd/grype/cli/commands/internal/dbsearch/versions.go`.\n\nThis schema is being versioned based off of the \"SchemaVer\" guidelines, which slightly diverges from Semantic Versioning to tailor for the purposes of data models.\n\nGiven a version number format `MODEL.REVISION.ADDITION`:\n\n- `MODEL`: increment when you make a breaking schema change which will prevent interaction with any historical data\n- `REVISION`: increment when you make a schema change which may prevent interaction with some historical data\n- `ADDITION`: increment when you make a schema change that is compatible with all historical data\n\n## Generating a New Schema\n\nCreate the new schema by running `make generate-json-schema` from the root of the repo:\n\n- If there is **not** an existing schema for the given version, then the new schema file will be written to `schema/grype/db-search-vuln/json/schema-$VERSION.json`\n- If there is an existing schema for the given version and the new schema matches the existing schema, no action is taken\n- If there is an existing schema for the given version and the new schema **does not** match the existing schema, an error is shown indicating to increment the version appropriately (see the \"Versioning\" section)\n\n***Note: never delete a JSON schema and never change an existing JSON schema once it has been published in a release!*** Only add new schemas with a newly incremented version.\n"
  },
  {
    "path": "schema/grype/db-search-vuln/json/schema-1.0.0.json",
    "content": "{\n  \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n  \"$id\": \"anchore.io/schema/grype/db-search-vuln/json/1.0.0/vulnerabilities\",\n  \"$ref\": \"#/$defs/Vulnerabilities\",\n  \"$defs\": {\n    \"OperatingSystem\": {\n      \"properties\": {\n        \"name\": {\n          \"type\": \"string\"\n        },\n        \"version\": {\n          \"type\": \"string\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"name\",\n        \"version\"\n      ]\n    },\n    \"Reference\": {\n      \"$defs\": {\n        \"tags\": {\n          \"description\": \"is a free-form organizational field to convey additional information about the reference\"\n        },\n        \"url\": {\n          \"description\": \"is the external resource\"\n        }\n      },\n      \"properties\": {\n        \"url\": {\n          \"type\": \"string\"\n        },\n        \"tags\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"url\"\n      ]\n    },\n    \"Severity\": {\n      \"$defs\": {\n        \"rank\": {\n          \"description\": \"is a free-form organizational field to convey priority over other severities\"\n        },\n        \"scheme\": {\n          \"description\": \"describes the quantitative method used to determine the Score, such as 'CVSS_V3'. Alternatively this makes\\nclaim that Value is qualitative, for example 'HML' (High, Medium, Low), CHMLN (critical-high-medium-low-negligible)\"\n        },\n        \"source\": {\n          \"description\": \"is the name of the source of the severity score (e.g. 'nvd@nist.gov' or 'security-advisories@github.com')\"\n        },\n        \"value\": {\n          \"description\": \"is the severity score (e.g. '7.5', 'CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N',  or 'high' )\"\n        }\n      },\n      \"properties\": {\n        \"scheme\": {\n          \"type\": \"string\"\n        },\n        \"value\": true,\n        \"source\": {\n          \"type\": \"string\"\n        },\n        \"rank\": {\n          \"type\": \"integer\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"scheme\",\n        \"value\",\n        \"rank\"\n      ]\n    },\n    \"Vulnerabilities\": {\n      \"items\": {\n        \"$ref\": \"#/$defs/Vulnerability\"\n      },\n      \"type\": \"array\"\n    },\n    \"Vulnerability\": {\n      \"$defs\": {\n        \"affected_packages\": {\n          \"description\": \"is the number of packages affected by the vulnerability\"\n        },\n        \"operating_systems\": {\n          \"description\": \"is a list of operating systems affected by the vulnerability\"\n        }\n      },\n      \"properties\": {\n        \"id\": {\n          \"type\": \"string\"\n        },\n        \"assigner\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        },\n        \"description\": {\n          \"type\": \"string\"\n        },\n        \"refs\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/Reference\"\n          },\n          \"type\": \"array\"\n        },\n        \"aliases\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        },\n        \"severities\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/Severity\"\n          },\n          \"type\": \"array\"\n        },\n        \"provider\": {\n          \"type\": \"string\"\n        },\n        \"status\": {\n          \"type\": \"string\"\n        },\n        \"published_date\": {\n          \"type\": \"string\",\n          \"format\": \"date-time\"\n        },\n        \"modified_date\": {\n          \"type\": \"string\",\n          \"format\": \"date-time\"\n        },\n        \"withdrawn_date\": {\n          \"type\": \"string\",\n          \"format\": \"date-time\"\n        },\n        \"operating_systems\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/OperatingSystem\"\n          },\n          \"type\": \"array\"\n        },\n        \"affected_packages\": {\n          \"type\": \"integer\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"id\",\n        \"provider\",\n        \"status\",\n        \"operating_systems\",\n        \"affected_packages\"\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "schema/grype/db-search-vuln/json/schema-1.0.1.json",
    "content": "{\n  \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n  \"$id\": \"anchore.io/schema/grype/db-search-vuln/json/1.0.1/vulnerabilities\",\n  \"$ref\": \"#/$defs/Vulnerabilities\",\n  \"$defs\": {\n    \"EPSS\": {\n      \"properties\": {\n        \"cve\": {\n          \"type\": \"string\"\n        },\n        \"epss\": {\n          \"type\": \"number\"\n        },\n        \"percentile\": {\n          \"type\": \"number\"\n        },\n        \"date\": {\n          \"type\": \"string\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"cve\",\n        \"epss\",\n        \"percentile\",\n        \"date\"\n      ]\n    },\n    \"KnownExploited\": {\n      \"properties\": {\n        \"cve\": {\n          \"type\": \"string\"\n        },\n        \"vendor_project\": {\n          \"type\": \"string\"\n        },\n        \"product\": {\n          \"type\": \"string\"\n        },\n        \"date_added\": {\n          \"type\": \"string\"\n        },\n        \"required_action\": {\n          \"type\": \"string\"\n        },\n        \"due_date\": {\n          \"type\": \"string\"\n        },\n        \"known_ransomware_campaign_use\": {\n          \"type\": \"string\"\n        },\n        \"notes\": {\n          \"type\": \"string\"\n        },\n        \"urls\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        },\n        \"cwes\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"cve\",\n        \"known_ransomware_campaign_use\"\n      ]\n    },\n    \"OperatingSystem\": {\n      \"properties\": {\n        \"name\": {\n          \"type\": \"string\"\n        },\n        \"version\": {\n          \"type\": \"string\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"name\",\n        \"version\"\n      ]\n    },\n    \"Reference\": {\n      \"$defs\": {\n        \"tags\": {\n          \"description\": \"is a free-form organizational field to convey additional information about the reference\"\n        },\n        \"url\": {\n          \"description\": \"is the external resource\"\n        }\n      },\n      \"properties\": {\n        \"url\": {\n          \"type\": \"string\"\n        },\n        \"tags\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"url\"\n      ]\n    },\n    \"Severity\": {\n      \"$defs\": {\n        \"rank\": {\n          \"description\": \"is a free-form organizational field to convey priority over other severities\"\n        },\n        \"scheme\": {\n          \"description\": \"describes the quantitative method used to determine the Score, such as 'CVSS_V3'. Alternatively this makes\\nclaim that Value is qualitative, for example 'HML' (High, Medium, Low), CHMLN (critical-high-medium-low-negligible)\"\n        },\n        \"source\": {\n          \"description\": \"is the name of the source of the severity score (e.g. 'nvd@nist.gov' or 'security-advisories@github.com')\"\n        },\n        \"value\": {\n          \"description\": \"is the severity score (e.g. '7.5', 'CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N',  or 'high' )\"\n        }\n      },\n      \"properties\": {\n        \"scheme\": {\n          \"type\": \"string\"\n        },\n        \"value\": true,\n        \"source\": {\n          \"type\": \"string\"\n        },\n        \"rank\": {\n          \"type\": \"integer\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"scheme\",\n        \"value\",\n        \"rank\"\n      ]\n    },\n    \"Vulnerabilities\": {\n      \"items\": {\n        \"$ref\": \"#/$defs/Vulnerability\"\n      },\n      \"type\": \"array\"\n    },\n    \"Vulnerability\": {\n      \"$defs\": {\n        \"affected_packages\": {\n          \"description\": \"is the number of packages affected by the vulnerability\"\n        },\n        \"operating_systems\": {\n          \"description\": \"is a list of operating systems affected by the vulnerability\"\n        }\n      },\n      \"properties\": {\n        \"id\": {\n          \"type\": \"string\"\n        },\n        \"assigner\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        },\n        \"description\": {\n          \"type\": \"string\"\n        },\n        \"refs\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/Reference\"\n          },\n          \"type\": \"array\"\n        },\n        \"aliases\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        },\n        \"severities\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/Severity\"\n          },\n          \"type\": \"array\"\n        },\n        \"provider\": {\n          \"type\": \"string\"\n        },\n        \"status\": {\n          \"type\": \"string\"\n        },\n        \"published_date\": {\n          \"type\": \"string\",\n          \"format\": \"date-time\"\n        },\n        \"modified_date\": {\n          \"type\": \"string\",\n          \"format\": \"date-time\"\n        },\n        \"withdrawn_date\": {\n          \"type\": \"string\",\n          \"format\": \"date-time\"\n        },\n        \"known_exploited\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/KnownExploited\"\n          },\n          \"type\": \"array\"\n        },\n        \"epss\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/EPSS\"\n          },\n          \"type\": \"array\"\n        },\n        \"operating_systems\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/OperatingSystem\"\n          },\n          \"type\": \"array\"\n        },\n        \"affected_packages\": {\n          \"type\": \"integer\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"id\",\n        \"provider\",\n        \"status\",\n        \"operating_systems\",\n        \"affected_packages\"\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "schema/grype/db-search-vuln/json/schema-1.0.3.json",
    "content": "{\n  \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n  \"$id\": \"anchore.io/schema/grype/db-search-vuln/json/1.0.3/vulnerabilities\",\n  \"$ref\": \"#/$defs/Vulnerabilities\",\n  \"$defs\": {\n    \"EPSS\": {\n      \"properties\": {\n        \"cve\": {\n          \"type\": \"string\"\n        },\n        \"epss\": {\n          \"type\": \"number\"\n        },\n        \"percentile\": {\n          \"type\": \"number\"\n        },\n        \"date\": {\n          \"type\": \"string\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"cve\",\n        \"epss\",\n        \"percentile\",\n        \"date\"\n      ]\n    },\n    \"KnownExploited\": {\n      \"properties\": {\n        \"cve\": {\n          \"type\": \"string\"\n        },\n        \"vendor_project\": {\n          \"type\": \"string\"\n        },\n        \"product\": {\n          \"type\": \"string\"\n        },\n        \"date_added\": {\n          \"type\": \"string\"\n        },\n        \"required_action\": {\n          \"type\": \"string\"\n        },\n        \"due_date\": {\n          \"type\": \"string\"\n        },\n        \"known_ransomware_campaign_use\": {\n          \"type\": \"string\"\n        },\n        \"notes\": {\n          \"type\": \"string\"\n        },\n        \"urls\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        },\n        \"cwes\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"cve\",\n        \"known_ransomware_campaign_use\"\n      ]\n    },\n    \"OperatingSystem\": {\n      \"properties\": {\n        \"name\": {\n          \"type\": \"string\"\n        },\n        \"version\": {\n          \"type\": \"string\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"name\",\n        \"version\"\n      ]\n    },\n    \"Reference\": {\n      \"$defs\": {\n        \"tags\": {\n          \"description\": \"is a free-form organizational field to convey additional information about the reference\"\n        },\n        \"url\": {\n          \"description\": \"is the external resource\"\n        }\n      },\n      \"properties\": {\n        \"url\": {\n          \"type\": \"string\"\n        },\n        \"tags\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"url\"\n      ]\n    },\n    \"Severity\": {\n      \"$defs\": {\n        \"rank\": {\n          \"description\": \"is a free-form organizational field to convey priority over other severities\"\n        },\n        \"scheme\": {\n          \"description\": \"describes the quantitative method used to determine the Score, such as 'CVSS_V3'. Alternatively this makes\\nclaim that Value is qualitative, for example 'HML' (High, Medium, Low), CHMLN (critical-high-medium-low-negligible)\"\n        },\n        \"source\": {\n          \"description\": \"is the name of the source of the severity score (e.g. 'nvd@nist.gov' or 'security-advisories@github.com')\"\n        },\n        \"value\": {\n          \"description\": \"is the severity score (e.g. '7.5', 'CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N',  or 'high' )\"\n        }\n      },\n      \"properties\": {\n        \"scheme\": {\n          \"type\": \"string\"\n        },\n        \"value\": true,\n        \"source\": {\n          \"type\": \"string\"\n        },\n        \"rank\": {\n          \"type\": \"integer\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"scheme\",\n        \"value\",\n        \"rank\"\n      ]\n    },\n    \"Vulnerabilities\": {\n      \"items\": {\n        \"$ref\": \"#/$defs/Vulnerability\"\n      },\n      \"type\": \"array\"\n    },\n    \"Vulnerability\": {\n      \"$defs\": {\n        \"affected_packages\": {\n          \"description\": \"is the number of packages affected by the vulnerability\"\n        },\n        \"operating_systems\": {\n          \"description\": \"is a list of operating systems affected by the vulnerability\"\n        }\n      },\n      \"properties\": {\n        \"id\": {\n          \"type\": \"string\"\n        },\n        \"assigner\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        },\n        \"description\": {\n          \"type\": \"string\"\n        },\n        \"refs\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/Reference\"\n          },\n          \"type\": \"array\"\n        },\n        \"aliases\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        },\n        \"severities\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/Severity\"\n          },\n          \"type\": \"array\"\n        },\n        \"severity\": {\n          \"type\": \"string\"\n        },\n        \"provider\": {\n          \"type\": \"string\"\n        },\n        \"status\": {\n          \"type\": \"string\"\n        },\n        \"published_date\": {\n          \"type\": \"string\",\n          \"format\": \"date-time\"\n        },\n        \"modified_date\": {\n          \"type\": \"string\",\n          \"format\": \"date-time\"\n        },\n        \"withdrawn_date\": {\n          \"type\": \"string\",\n          \"format\": \"date-time\"\n        },\n        \"known_exploited\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/KnownExploited\"\n          },\n          \"type\": \"array\"\n        },\n        \"epss\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/EPSS\"\n          },\n          \"type\": \"array\"\n        },\n        \"operating_systems\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/OperatingSystem\"\n          },\n          \"type\": \"array\"\n        },\n        \"affected_packages\": {\n          \"type\": \"integer\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"id\",\n        \"provider\",\n        \"status\",\n        \"operating_systems\",\n        \"affected_packages\"\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "schema/grype/db-search-vuln/json/schema-1.0.4.json",
    "content": "{\n  \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n  \"$id\": \"anchore.io/schema/grype/db-search-vuln/json/1.0.4/vulnerabilities\",\n  \"$ref\": \"#/$defs/Vulnerabilities\",\n  \"$defs\": {\n    \"CWE\": {\n      \"properties\": {\n        \"cve\": {\n          \"type\": \"string\"\n        },\n        \"cwe\": {\n          \"type\": \"string\"\n        },\n        \"source\": {\n          \"type\": \"string\"\n        },\n        \"type\": {\n          \"type\": \"string\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"cve\",\n        \"cwe\",\n        \"source\",\n        \"type\"\n      ]\n    },\n    \"EPSS\": {\n      \"properties\": {\n        \"cve\": {\n          \"type\": \"string\"\n        },\n        \"epss\": {\n          \"type\": \"number\"\n        },\n        \"percentile\": {\n          \"type\": \"number\"\n        },\n        \"date\": {\n          \"type\": \"string\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"cve\",\n        \"epss\",\n        \"percentile\",\n        \"date\"\n      ]\n    },\n    \"KnownExploited\": {\n      \"properties\": {\n        \"cve\": {\n          \"type\": \"string\"\n        },\n        \"vendor_project\": {\n          \"type\": \"string\"\n        },\n        \"product\": {\n          \"type\": \"string\"\n        },\n        \"date_added\": {\n          \"type\": \"string\"\n        },\n        \"required_action\": {\n          \"type\": \"string\"\n        },\n        \"due_date\": {\n          \"type\": \"string\"\n        },\n        \"known_ransomware_campaign_use\": {\n          \"type\": \"string\"\n        },\n        \"notes\": {\n          \"type\": \"string\"\n        },\n        \"urls\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        },\n        \"cwes\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"cve\",\n        \"known_ransomware_campaign_use\"\n      ]\n    },\n    \"OperatingSystem\": {\n      \"properties\": {\n        \"name\": {\n          \"type\": \"string\"\n        },\n        \"version\": {\n          \"type\": \"string\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"name\",\n        \"version\"\n      ]\n    },\n    \"Reference\": {\n      \"$defs\": {\n        \"tags\": {\n          \"description\": \"is a free-form organizational field to convey additional information about the reference\"\n        },\n        \"url\": {\n          \"description\": \"is the external resource\"\n        }\n      },\n      \"properties\": {\n        \"url\": {\n          \"type\": \"string\"\n        },\n        \"tags\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"url\"\n      ]\n    },\n    \"Severity\": {\n      \"$defs\": {\n        \"rank\": {\n          \"description\": \"is a free-form organizational field to convey priority over other severities\"\n        },\n        \"scheme\": {\n          \"description\": \"describes the quantitative method used to determine the Score, such as 'CVSS_V3'. Alternatively this makes\\nclaim that Value is qualitative, for example 'HML' (High, Medium, Low), CHMLN (critical-high-medium-low-negligible)\"\n        },\n        \"source\": {\n          \"description\": \"is the name of the source of the severity score (e.g. 'nvd@nist.gov' or 'security-advisories@github.com')\"\n        },\n        \"value\": {\n          \"description\": \"is the severity score (e.g. '7.5', 'CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N',  or 'high' )\"\n        }\n      },\n      \"properties\": {\n        \"scheme\": {\n          \"type\": \"string\"\n        },\n        \"value\": true,\n        \"source\": {\n          \"type\": \"string\"\n        },\n        \"rank\": {\n          \"type\": \"integer\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"scheme\",\n        \"value\",\n        \"rank\"\n      ]\n    },\n    \"Vulnerabilities\": {\n      \"items\": {\n        \"$ref\": \"#/$defs/Vulnerability\"\n      },\n      \"type\": \"array\"\n    },\n    \"Vulnerability\": {\n      \"$defs\": {\n        \"affected_packages\": {\n          \"description\": \"is the number of packages affected by the vulnerability\"\n        },\n        \"operating_systems\": {\n          \"description\": \"is a list of operating systems affected by the vulnerability\"\n        }\n      },\n      \"properties\": {\n        \"id\": {\n          \"type\": \"string\"\n        },\n        \"assigner\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        },\n        \"description\": {\n          \"type\": \"string\"\n        },\n        \"refs\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/Reference\"\n          },\n          \"type\": \"array\"\n        },\n        \"aliases\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        },\n        \"severities\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/Severity\"\n          },\n          \"type\": \"array\"\n        },\n        \"severity\": {\n          \"type\": \"string\"\n        },\n        \"provider\": {\n          \"type\": \"string\"\n        },\n        \"status\": {\n          \"type\": \"string\"\n        },\n        \"published_date\": {\n          \"type\": \"string\",\n          \"format\": \"date-time\"\n        },\n        \"modified_date\": {\n          \"type\": \"string\",\n          \"format\": \"date-time\"\n        },\n        \"withdrawn_date\": {\n          \"type\": \"string\",\n          \"format\": \"date-time\"\n        },\n        \"known_exploited\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/KnownExploited\"\n          },\n          \"type\": \"array\"\n        },\n        \"epss\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/EPSS\"\n          },\n          \"type\": \"array\"\n        },\n        \"cwes\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/CWE\"\n          },\n          \"type\": \"array\"\n        },\n        \"operating_systems\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/OperatingSystem\"\n          },\n          \"type\": \"array\"\n        },\n        \"affected_packages\": {\n          \"type\": \"integer\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"id\",\n        \"provider\",\n        \"status\",\n        \"operating_systems\",\n        \"affected_packages\"\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "schema/grype/db-search-vuln/json/schema-1.0.5.json",
    "content": "{\n  \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n  \"$id\": \"anchore.io/schema/grype/db-search-vuln/json/1.0.5/vulnerabilities\",\n  \"$ref\": \"#/$defs/Vulnerabilities\",\n  \"$defs\": {\n    \"CWE\": {\n      \"properties\": {\n        \"cve\": {\n          \"type\": \"string\"\n        },\n        \"cwe\": {\n          \"type\": \"string\"\n        },\n        \"source\": {\n          \"type\": \"string\"\n        },\n        \"type\": {\n          \"type\": \"string\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"cve\",\n        \"cwe\",\n        \"source\",\n        \"type\"\n      ]\n    },\n    \"EPSS\": {\n      \"properties\": {\n        \"cve\": {\n          \"type\": \"string\"\n        },\n        \"epss\": {\n          \"type\": \"number\"\n        },\n        \"percentile\": {\n          \"type\": \"number\"\n        },\n        \"date\": {\n          \"type\": \"string\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"cve\",\n        \"epss\",\n        \"percentile\",\n        \"date\"\n      ]\n    },\n    \"KnownExploited\": {\n      \"properties\": {\n        \"cve\": {\n          \"type\": \"string\"\n        },\n        \"vendor_project\": {\n          \"type\": \"string\"\n        },\n        \"product\": {\n          \"type\": \"string\"\n        },\n        \"date_added\": {\n          \"type\": \"string\"\n        },\n        \"required_action\": {\n          \"type\": \"string\"\n        },\n        \"due_date\": {\n          \"type\": \"string\"\n        },\n        \"known_ransomware_campaign_use\": {\n          \"type\": \"string\"\n        },\n        \"notes\": {\n          \"type\": \"string\"\n        },\n        \"urls\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        },\n        \"cwes\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"cve\",\n        \"known_ransomware_campaign_use\"\n      ]\n    },\n    \"OperatingSystem\": {\n      \"properties\": {\n        \"name\": {\n          \"type\": \"string\"\n        },\n        \"version\": {\n          \"type\": \"string\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"name\",\n        \"version\"\n      ]\n    },\n    \"Reference\": {\n      \"$defs\": {\n        \"id\": {\n          \"description\": \"is an optional identifier for the reference (e.g., advisory ID like 'RHSA-2023:5455')\"\n        },\n        \"tags\": {\n          \"description\": \"is a free-form organizational field to convey additional information about the reference\"\n        },\n        \"url\": {\n          \"description\": \"is the external resource\"\n        }\n      },\n      \"properties\": {\n        \"url\": {\n          \"type\": \"string\"\n        },\n        \"id\": {\n          \"type\": \"string\"\n        },\n        \"tags\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"url\"\n      ]\n    },\n    \"Severity\": {\n      \"$defs\": {\n        \"rank\": {\n          \"description\": \"is a free-form organizational field to convey priority over other severities\"\n        },\n        \"scheme\": {\n          \"description\": \"describes the quantitative method used to determine the Score, such as 'CVSS_V3'. Alternatively this makes\\nclaim that Value is qualitative, for example 'HML' (High, Medium, Low), CHMLN (critical-high-medium-low-negligible)\"\n        },\n        \"source\": {\n          \"description\": \"is the name of the source of the severity score (e.g. 'nvd@nist.gov' or 'security-advisories@github.com')\"\n        },\n        \"value\": {\n          \"description\": \"is the severity score (e.g. '7.5', 'CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N',  or 'high' )\"\n        }\n      },\n      \"properties\": {\n        \"scheme\": {\n          \"type\": \"string\"\n        },\n        \"value\": true,\n        \"source\": {\n          \"type\": \"string\"\n        },\n        \"rank\": {\n          \"type\": \"integer\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"scheme\",\n        \"value\",\n        \"rank\"\n      ]\n    },\n    \"Vulnerabilities\": {\n      \"items\": {\n        \"$ref\": \"#/$defs/Vulnerability\"\n      },\n      \"type\": \"array\"\n    },\n    \"Vulnerability\": {\n      \"$defs\": {\n        \"affected_packages\": {\n          \"description\": \"is the number of packages affected by the vulnerability\"\n        },\n        \"operating_systems\": {\n          \"description\": \"is a list of operating systems affected by the vulnerability\"\n        }\n      },\n      \"properties\": {\n        \"id\": {\n          \"type\": \"string\"\n        },\n        \"assigner\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        },\n        \"description\": {\n          \"type\": \"string\"\n        },\n        \"refs\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/Reference\"\n          },\n          \"type\": \"array\"\n        },\n        \"aliases\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        },\n        \"severities\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/Severity\"\n          },\n          \"type\": \"array\"\n        },\n        \"severity\": {\n          \"type\": \"string\"\n        },\n        \"provider\": {\n          \"type\": \"string\"\n        },\n        \"status\": {\n          \"type\": \"string\"\n        },\n        \"published_date\": {\n          \"type\": \"string\",\n          \"format\": \"date-time\"\n        },\n        \"modified_date\": {\n          \"type\": \"string\",\n          \"format\": \"date-time\"\n        },\n        \"withdrawn_date\": {\n          \"type\": \"string\",\n          \"format\": \"date-time\"\n        },\n        \"known_exploited\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/KnownExploited\"\n          },\n          \"type\": \"array\"\n        },\n        \"epss\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/EPSS\"\n          },\n          \"type\": \"array\"\n        },\n        \"cwes\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/CWE\"\n          },\n          \"type\": \"array\"\n        },\n        \"operating_systems\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/OperatingSystem\"\n          },\n          \"type\": \"array\"\n        },\n        \"affected_packages\": {\n          \"type\": \"integer\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"id\",\n        \"provider\",\n        \"status\",\n        \"operating_systems\",\n        \"affected_packages\"\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "schema/grype/db-search-vuln/json/schema-latest.json",
    "content": "{\n  \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n  \"$id\": \"anchore.io/schema/grype/db-search-vuln/json/1.0.5/vulnerabilities\",\n  \"$ref\": \"#/$defs/Vulnerabilities\",\n  \"$defs\": {\n    \"CWE\": {\n      \"properties\": {\n        \"cve\": {\n          \"type\": \"string\"\n        },\n        \"cwe\": {\n          \"type\": \"string\"\n        },\n        \"source\": {\n          \"type\": \"string\"\n        },\n        \"type\": {\n          \"type\": \"string\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"cve\",\n        \"cwe\",\n        \"source\",\n        \"type\"\n      ]\n    },\n    \"EPSS\": {\n      \"properties\": {\n        \"cve\": {\n          \"type\": \"string\"\n        },\n        \"epss\": {\n          \"type\": \"number\"\n        },\n        \"percentile\": {\n          \"type\": \"number\"\n        },\n        \"date\": {\n          \"type\": \"string\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"cve\",\n        \"epss\",\n        \"percentile\",\n        \"date\"\n      ]\n    },\n    \"KnownExploited\": {\n      \"properties\": {\n        \"cve\": {\n          \"type\": \"string\"\n        },\n        \"vendor_project\": {\n          \"type\": \"string\"\n        },\n        \"product\": {\n          \"type\": \"string\"\n        },\n        \"date_added\": {\n          \"type\": \"string\"\n        },\n        \"required_action\": {\n          \"type\": \"string\"\n        },\n        \"due_date\": {\n          \"type\": \"string\"\n        },\n        \"known_ransomware_campaign_use\": {\n          \"type\": \"string\"\n        },\n        \"notes\": {\n          \"type\": \"string\"\n        },\n        \"urls\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        },\n        \"cwes\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"cve\",\n        \"known_ransomware_campaign_use\"\n      ]\n    },\n    \"OperatingSystem\": {\n      \"properties\": {\n        \"name\": {\n          \"type\": \"string\"\n        },\n        \"version\": {\n          \"type\": \"string\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"name\",\n        \"version\"\n      ]\n    },\n    \"Reference\": {\n      \"$defs\": {\n        \"id\": {\n          \"description\": \"is an optional identifier for the reference (e.g., advisory ID like 'RHSA-2023:5455')\"\n        },\n        \"tags\": {\n          \"description\": \"is a free-form organizational field to convey additional information about the reference\"\n        },\n        \"url\": {\n          \"description\": \"is the external resource\"\n        }\n      },\n      \"properties\": {\n        \"url\": {\n          \"type\": \"string\"\n        },\n        \"id\": {\n          \"type\": \"string\"\n        },\n        \"tags\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"url\"\n      ]\n    },\n    \"Severity\": {\n      \"$defs\": {\n        \"rank\": {\n          \"description\": \"is a free-form organizational field to convey priority over other severities\"\n        },\n        \"scheme\": {\n          \"description\": \"describes the quantitative method used to determine the Score, such as 'CVSS_V3'. Alternatively this makes\\nclaim that Value is qualitative, for example 'HML' (High, Medium, Low), CHMLN (critical-high-medium-low-negligible)\"\n        },\n        \"source\": {\n          \"description\": \"is the name of the source of the severity score (e.g. 'nvd@nist.gov' or 'security-advisories@github.com')\"\n        },\n        \"value\": {\n          \"description\": \"is the severity score (e.g. '7.5', 'CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N',  or 'high' )\"\n        }\n      },\n      \"properties\": {\n        \"scheme\": {\n          \"type\": \"string\"\n        },\n        \"value\": true,\n        \"source\": {\n          \"type\": \"string\"\n        },\n        \"rank\": {\n          \"type\": \"integer\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"scheme\",\n        \"value\",\n        \"rank\"\n      ]\n    },\n    \"Vulnerabilities\": {\n      \"items\": {\n        \"$ref\": \"#/$defs/Vulnerability\"\n      },\n      \"type\": \"array\"\n    },\n    \"Vulnerability\": {\n      \"$defs\": {\n        \"affected_packages\": {\n          \"description\": \"is the number of packages affected by the vulnerability\"\n        },\n        \"operating_systems\": {\n          \"description\": \"is a list of operating systems affected by the vulnerability\"\n        }\n      },\n      \"properties\": {\n        \"id\": {\n          \"type\": \"string\"\n        },\n        \"assigner\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        },\n        \"description\": {\n          \"type\": \"string\"\n        },\n        \"refs\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/Reference\"\n          },\n          \"type\": \"array\"\n        },\n        \"aliases\": {\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"type\": \"array\"\n        },\n        \"severities\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/Severity\"\n          },\n          \"type\": \"array\"\n        },\n        \"severity\": {\n          \"type\": \"string\"\n        },\n        \"provider\": {\n          \"type\": \"string\"\n        },\n        \"status\": {\n          \"type\": \"string\"\n        },\n        \"published_date\": {\n          \"type\": \"string\",\n          \"format\": \"date-time\"\n        },\n        \"modified_date\": {\n          \"type\": \"string\",\n          \"format\": \"date-time\"\n        },\n        \"withdrawn_date\": {\n          \"type\": \"string\",\n          \"format\": \"date-time\"\n        },\n        \"known_exploited\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/KnownExploited\"\n          },\n          \"type\": \"array\"\n        },\n        \"epss\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/EPSS\"\n          },\n          \"type\": \"array\"\n        },\n        \"cwes\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/CWE\"\n          },\n          \"type\": \"array\"\n        },\n        \"operating_systems\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/OperatingSystem\"\n          },\n          \"type\": \"array\"\n        },\n        \"affected_packages\": {\n          \"type\": \"integer\"\n        }\n      },\n      \"type\": \"object\",\n      \"required\": [\n        \"id\",\n        \"provider\",\n        \"status\",\n        \"operating_systems\",\n        \"affected_packages\"\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "templates/README.md",
    "content": "# Grype Templates\n\nThis folder contains a set of \"helper\" go templates you can use for your own reports.\n\nPlease feel free to extend and/or update the templates for your needs, be sure to contribute back into this folder any new templates!\n\nCurrent templates:\n\n<pre>\n.\n├── README.md\n├── html.tmpl\n├── junit.tmpl\n├── csv.tmpl\n└── table.tmpl\n</pre>\n\n## Table\n\nThis template mimics the \"default\" table output of Grype, there are some drawbacks to using the template vs the native output such as:\n\n- unsorted\n- duplicate rows\n- no (wont-fix) logic\n\nAs you can see from the above list, it's not perfect but it's a start.\n\n## HTML\n\nProduces a nice html template with a dynamic table using datatables.js.\n\nYou can also modify the templating filter to limit the output to a subset.\n\nDefault includes all\n\n```\n    {{- if or (eq $vuln.Vulnerability.Severity \"Critical\") (eq $vuln.Vulnerability.Severity \"High\") (eq $vuln.Vulnerability.Severity \"Medium\") (eq $vuln.Vulnerability.Severity \"Low\") (eq $vuln.Vulnerability.Severity \"Unknown\") }}\n```\n\nWe can limit it to only Critical, High, and Medium by editing the filter as follows\n\n```\n    {{- if or (eq $vuln.Vulnerability.Severity \"Critical\") (eq $vuln.Vulnerability.Severity \"High\") (eq $vuln.Vulnerability.Severity \"Medium\") }}\n```\n\n"
  },
  {
    "path": "templates/csv.tmpl",
    "content": "\"Package\",\"Version Installed\",\"Vulnerability ID\",\"Severity\"\n{{- range .Matches}}\n\"{{.Artifact.Name}}\",\"{{.Artifact.Version}}\",\"{{.Vulnerability.ID}}\",\"{{.Vulnerability.Severity}}\"\n{{- end}}\n"
  },
  {
    "path": "templates/html.tmpl",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n    <meta charset=\"UTF-8\">\n    <title>Vulnerability Report</title>\n\n    <!-- Template Metadata-->\n    <meta name=\"author\" content=\"grype\">\n    <meta name=\"version\" content=\"1.0.0\">\n\n    <!-- Source DataTables.js and its dependencies -->\n    <!-- Styling: DataTables | Packages: Jquery | Core: DataTables | Extensions: Buttons, HTML5 Export, JSZip, PDFmake, Responsive -->\n    <link href=\"https://cdn.datatables.net/v/dt/jq-3.7.0/jszip-3.10.1/dt-2.3.0/b-3.2.3/b-html5-3.2.3/r-3.0.4/datatables.min.css\" rel=\"stylesheet\" integrity=\"sha384-i3FrIG8iE4wl9Hmwo+yL2xgtj0L+/QgCSGfIAJbuoowqsGIdhIpg9ax2eizUyCZw\" crossorigin=\"anonymous\">\n    <script src=\"https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.2.7/pdfmake.min.js\" integrity=\"sha384-VFQrHzqBh5qiJIU0uGU5CIW3+OWpdGGJM9LBnGbuIH2mkICcFZ7lPd/AAtI7SNf7\" crossorigin=\"anonymous\"></script>\n    <script src=\"https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.2.7/vfs_fonts.js\" integrity=\"sha384-/RlQG9uf0M2vcTw3CX7fbqgbj/h8wKxw7C3zu9/GxcBPRKOEcESxaxufwRXqzq6n\" crossorigin=\"anonymous\"></script>\n    <script src=\"https://cdn.datatables.net/v/dt/jq-3.7.0/jszip-3.10.1/dt-2.3.0/b-3.2.3/b-html5-3.2.3/r-3.0.4/datatables.min.js\" integrity=\"sha384-uD0xNCd/C2vKjG5NDZ8BdHFubelfW0p6XH+6n7crJnbQcPP6aUw5MJ3WfEZFz3bA\" crossorigin=\"anonymous\"></script>\n    \n    <!-- Include Devicon (for specific tech icons) - Using @latest -->\n    <link rel=\"stylesheet\" type='text/css' href=\"https://cdn.jsdelivr.net/gh/devicons/devicon@2.16.0/devicon.min.css\" /> \n    \n    <!-- Preload SVG icons used in CSS -->\n    <link rel=\"preload\" href=\"https://api.iconify.design/noto:package.svg\" as=\"image\">\n    <link rel=\"preload\" href=\"https://api.iconify.design/file-icons:alpine-linux.svg\" as=\"image\">\n    <link rel=\"preload\" href=\"https://api.iconify.design/vscode-icons:file-type-excel.svg\" as=\"image\">\n    <link rel=\"preload\" href=\"https://api.iconify.design/vscode-icons:file-type-pdf2.svg\" as=\"image\">\n\n    <!-- Font Awesome for search icon -->\n    <link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css\" integrity=\"sha512-Evv84Mr4kqVGRNSgIGL/F/aIDqQb7xQ2vcrdIwxfjThSH8CSR7PBEakCr51Ck+w+/U6swU2Im1vVX0SVk9ABhg==\" crossorigin=\"anonymous\" referrerpolicy=\"no-referrer\" />\n    \n    <!-- CSS Variables for Theming -->\n    <style>\n        /* Root variables for light theme */\n        :root {\n            --bg-color: #f4f4f4;\n            --text-color: #333;\n            --container-bg: #fff;\n            --container-shadow: rgba(0, 0, 0, 0.1);\n            --header-bg: #e8f0fe;\n            --header-text: #495057;\n            --severity-box-text: rgba(255, 255, 255, 0.9);\n            --severity-box-shadow-hover: rgba(0, 0, 0, 0.2);\n            --input-border-color: #ccc;\n            --input-bg-color: #fff;\n            --input-text-color: #333;\n            --button-bg: #007bff;\n            --button-hover-bg: #0056b3;\n            --button-text: white;\n            --table-border: #ddd;\n            --table-header-bg: #d1dff0;\n            --table-header-text: #333;\n            --table-row-hover-bg: #f5f5f5;\n            --link-color: #007bff;\n            --link-visited-color: #551a8b;\n            --pill-text: white;\n        }\n\n        /* Dark theme variables */\n        @media (prefers-color-scheme: dark) {\n            :root {\n                --bg-color: #1a1a1a;\n                --text-color: #e0e0e0;\n                --container-bg: #2c2c2c;\n                --container-shadow: rgba(255, 255, 255, 0.1);\n                --header-bg: #3a3a3a;\n                --header-text: #c0c0c0;\n                --severity-box-text: rgba(255, 255, 255, 0.9);\n                --severity-box-shadow-hover: rgba(255, 255, 255, 0.2);\n                --input-border-color: #555;\n                --input-bg-color: #3a3a3a;\n                --input-text-color: #e0e0e0;\n                --button-bg: #0d6efd;\n                --button-hover-bg: #0b5ed7;\n                --button-text: white;\n                --table-border: #444;\n                --table-header-bg: #4a4a4a;\n                --table-header-text: #e0e0e0;\n                --table-row-hover-bg: #3f3f3f;\n                --link-color: #58a6ff;\n                --link-visited-color: #9d8eee;\n                --pill-text: white;\n            }\n\n            /* DataTables dark mode adjustments */\n            .dataTables_wrapper .dataTables_length select,\n            .dataTables_wrapper .dt-search input,\n            .dataTables_wrapper .dataTables_info,\n            .dataTables_wrapper .dataTables_paginate .paginate_button {\n                color: var(--text-color) !important;\n            }\n\n            .dataTables_wrapper .dataTables_paginate .paginate_button.disabled {\n                color: #666 !important;\n            }\n\n            .dataTables_wrapper .dataTables_paginate .paginate_button:hover {\n                 background-color: var(--table-row-hover-bg);\n                 border-color: var(--table-border);\n                 color: var(--text-color) !important;\n            }\n\n            /* Dark mode table link colors */\n            #vulnerabilityTable a {\n                color: var(--link-color);\n            }\n            #vulnerabilityTable a:visited {\n                 color: var(--link-visited-color);\n             }\n        }\n    </style>\n    <!-- Page & Main Container -->\n    <style>\n        /* Base page styling */\n        body {\n            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;\n            margin: 0;\n            padding: 0;\n            display: flex;\n            justify-content: center;\n            align-items: flex-start;\n            background-color: var(--bg-color);\n            color: var(--text-color);\n        }\n\n        /* Main container styling */\n        .main-container {\n            width: 90%;\n            max-width: 1200px;\n            background-color: var(--container-bg);\n            padding: 20px;\n            box-shadow: 0 4px 8px var(--container-shadow);\n            border-radius: 10px;\n            margin: 20px;\n        }\n\n        /* Global link styling */\n        a {\n            color: var(--link-color);\n            text-decoration: none;\n        }\n        a:hover {\n            text-decoration: underline;\n        }\n        a:visited {\n            color: var(--link-visited-color);\n        }\n    </style>\n    <!-- Header Container -->\n    <style>\n        /* Header container styling */\n        .heading {\n            display: flex;\n            justify-content: space-between;\n            align-items: center;\n            padding: 10px;\n            margin-bottom: 20px;\n            border-radius: 5px;\n            background-color: var(--header-bg);\n            color: var(--header-text);\n        }\n\n        /* Left and right sections of header */\n        .heading-left,\n        .heading-right {\n            display: flex;\n            flex-direction: column;\n            align-items: flex-start;\n        }\n\n        /* Right section specific styling */\n        .heading-right {\n            align-items: flex-end;\n            padding-right: 20px;\n            color: var(--header-text);\n        }\n\n        /* Logo image styling */\n        .heading-right img {\n            width: 70px;\n            height: auto;\n        }\n\n        /* Left section text styling */\n        .heading-left h1,\n        .heading-left p {\n            padding-left: 20px;\n            margin: 4px 0;\n        }\n\n        /* Report details grid layout */\n        .heading-left dl.report-details {\n            padding-left: 20px;\n            margin: 4px 0;\n            display: grid;\n            grid-template-columns: auto 1fr;\n            gap: 2px 10px;\n            align-items: baseline;\n        }\n\n        /* Definition term styling */\n        .heading-left dt {\n            grid-column: 1;\n            font-weight: bold;\n            text-align: left;\n        }\n\n        /* Definition description styling */\n        .heading-left dd {\n            grid-column: 2;\n            margin: 0;\n            text-align: left;\n            word-break: break-all;\n        }\n    </style>\n    <!-- Severity Information Container -->\n    <style>\n        /* Severity info container styling */\n        .severity-info {\n            display: flex;\n            justify-content: space-around;\n            align-items: center;\n            padding: 10px;\n            background-color: var(--header-bg);\n            color: var(--header-text);\n            margin-bottom: 20px;\n            border-radius: 5px;\n        }\n\n        /* Individual severity box styling */\n        .severity-box {\n            flex-grow: 1;\n            display: flex;\n            flex-direction: column;\n            justify-content: space-between;\n            align-items: center;\n            text-align: center;\n            padding: 20px;\n            margin: 5px;\n            border-radius: 5px;\n            cursor: pointer;\n            position: relative;\n            transition: transform 0.3s ease, box-shadow 0.3s ease;\n        }\n\n        /* Severity title styling */\n        .severity-title {\n            font-size: 0.8em;\n            position: absolute;\n            bottom: 10px;\n            width: 100%;\n            color: var(--severity-box-text);\n        }\n\n        /* Severity count styling */\n        .severity-count {\n            font-size: 3em;\n            margin-bottom: 20px;\n            color: #fff;\n        }\n\n        /* Active severity box styling */\n        .severity-box.active {\n            border: 2px solid #007bff;\n        }\n\n        /* Severity color definitions */\n        #critical {\n            background-color: #d9534f;\n        }\n\n        #high {\n            background-color: #f0ad4e;\n        }\n\n        #medium {\n            background-color: #5bc0de;\n        }\n\n        #low {\n            background-color: #5cb85c;\n        }\n\n        #unknown {\n            background-color: #777;\n        }\n\n        /* Severity box hover effects */\n        .severity-box:hover {\n            transform: translateY(-5px);\n            box-shadow: 0 10px 20px var(--severity-box-shadow-hover);\n        }\n    </style>\n    <!-- Table Control & Datatables Wrapper -->\n    <style>\n        /* DataTables wrapper styling */\n        .dataTables_wrapper {\n            display: flex;\n            justify-content: space-between;\n            align-items: center;\n            background-color: var(--header-bg);\n            color: var(--header-text);\n            margin-bottom: 10px;\n            border-radius: 5px;\n            transition: background-color 0.3s ease, color 0.3s ease;\n        }\n\n        /* Button group styling */\n        .dt-buttons {\n            order: 2;\n            flex-grow: 1;\n            display: flex;\n            justify-content: flex-end;\n            gap: 10px;\n            padding-right: 10px;\n        }\n\n        /* DataTables button styling */\n        .dt-button {\n            background-color: var(--button-bg) !important;\n            color: white !important;\n            border: none !important;\n            border-radius: 5px !important;\n            padding: 0 !important;\n            width: 36px !important;\n            height: 36px !important;\n            font-size: 16px !important;\n            display: flex !important;\n            align-items: center !important;\n            justify-content: center !important;\n            text-align: center;\n            cursor: pointer;\n            margin-left: 5px !important;\n            box-shadow: 0 2px 5px rgba(0,0,0,0.2);\n            transition: background-color 0.2s ease;\n            overflow: hidden;\n        }\n\n        /* Button hover state */\n        .dt-button:hover {\n            background-color: var(--button-hover-bg) !important;\n            box-shadow: 0 4px 8px rgba(0,0,0,0.3);\n        }\n\n        /* Icon-only button styling */\n        .dt-button.buttons-pdf,\n        .dt-button.buttons-excel {\n            background-color: transparent !important;\n            border: none !important;\n            box-shadow: none !important;\n        }\n\n        /* Icon-only button hover state */\n        .dt-button.buttons-pdf:hover,\n        .dt-button.buttons-excel:hover {\n            background-color: rgba(0, 0, 0, 0.1) !important;\n            box-shadow: none !important;\n        }\n\n        /* Icon sizing */\n        .dt-button.buttons-pdf .vscode-icons--file-type-pdf2,\n        .dt-button.buttons-excel .vscode-icons--file-type-excel {\n            width: 32px;\n            height: 32px;\n        }\n\n        /* Child row details styling */\n        .child-row-details {\n            padding: 10px;\n            margin-left: 25px;\n            background-color: var(--container-bg);\n            color: var(--text-color);\n            border-top: 1px solid var(--table-border);\n            margin-top: -1px;\n            white-space: normal !important;\n        }\n        .child-row-details b {\n            font-weight: bold;\n        }\n        .child-row-details ul {\n            list-style-type: disc;\n            padding-left: 20px;\n            margin-top: 5px;\n            margin-bottom: 10px;\n        }\n\n        /* === Search Bar === */\n\n        /* Search box container */\n        .dataTables_wrapper .dt-search {\n            position: relative;\n        }\n\n        /* Search input styling */\n        .dataTables_wrapper .dt-search .dt-input {\n            height: 36px;\n            width: 36px;\n            border-style: none;\n            padding: 5px 10px;\n            font-size: 14px;\n            letter-spacing: 1px;\n            outline: none;\n            border-radius: 18px;\n            transition: all 0.5s ease-in-out;\n            background-color: var(--button-bg);\n            padding-left: 10px;\n            padding-right: 30px;\n            color: var(--button-text);\n            vertical-align: middle;\n            cursor: pointer;\n        }\n\n        /* Search input placeholder styling */\n        .dataTables_wrapper .dt-search .dt-input::placeholder {\n            color: var(--text-color);\n            opacity: 0.7;\n        }\n\n        /* Dark mode placeholder adjustments */\n        @media (prefers-color-scheme: dark) {\n            .dataTables_wrapper .dt-search .dt-input::placeholder {\n                color: var(--text-color);\n                opacity: 0.7;\n            }\n            .dataTables_wrapper .dt-search .dt-input:focus {\n                background-color: var(--input-bg-color);\n                color: var(--input-text-color);\n            }\n        }\n\n        /* Search input focus state */\n        .dataTables_wrapper .dt-search .dt-input:focus {\n            width: 250px;\n            border-radius: 5px;\n            background-color: var(--input-bg-color);\n            border: 1px solid var(--input-border-color);\n            color: var(--input-text-color);\n            cursor: text;\n        }\n\n        /* Search icon container */\n        .dataTables_wrapper .dt-search > label {\n            position: absolute;\n            right: 0.1em;\n            top: 0;\n            height: 36px;\n            width: 36px;\n            display: flex;\n            align-items: center;\n            justify-content: center;\n            color: var(--button-text);\n            font-size: 14px;\n            cursor: pointer;\n            pointer-events: none;\n            z-index: 2;\n            transition: color 0.3s ease-in-out;\n        }\n\n        /* Search icon styling */\n        .dataTables_wrapper .dt-search > label::before {\n                font-family: \"Font Awesome 6 Free\";\n                font-weight: 900;\n                content: \"\\f002\";\n        }\n\n        /* Search icon focus state */\n        .dataTables_wrapper .dt-search .dt-input:focus + label {\n            color: var(--text-color);\n        }\n\n        /* Hide label text */\n        .dataTables_wrapper .dt-search > label span {\n            display: none;\n        }\n    </style>\n    <!-- Table Container -->\n    <style>\n        /* Table container styling */\n        .data-table {\n            background-color: var(--header-bg);\n            color: var(--header-text);\n            padding: 20px;\n            border-radius: 5px;\n            overflow-x: auto;\n            transition: background-color 0.3s ease, color 0.3s ease;\n        }\n\n        /* Table base styling */\n        table.display {\n            width: 100%;\n            border-collapse: collapse;\n            color: var(--text-color);\n        }\n\n        /* Table cell styling */\n        th,\n        td {\n            padding: 12px 15px;\n            text-align: left;\n            border-bottom: 1px solid var(--table-border);\n        }\n\n        /* Table header styling */\n        th {\n            background-color: var(--table-header-bg);\n            font-weight: bold;\n            color: var(--table-header-text);\n        }\n\n        /* Table row hover state */\n        tr:hover {\n            background-color: var(--table-row-hover-bg);\n        }\n\n        /* Table link styling */\n        table.display a {\n             color: var(--link-color);\n        }\n\n        table.display a:visited {\n             color: var(--link-visited-color);\n        }\n\n        /* Fixed-in column styling */\n        .fixed-in ul {\n            list-style-type: none;\n            padding: 0;\n            margin: 0;\n        }\n\n        .fixed-in li {\n            padding: 0;\n            margin: 0;\n        }\n    </style>\n    <!-- Severity Coloring -->\n    <style>\n        /* Severity pill base styling */\n        .severity-pill,\n        .child-row-details .severity-pill {\n            display: inline-block; \n            padding: 5px 10px;\n            border-radius: 15px;\n            color: var(--pill-text);\n            text-align: center;\n        }\n\n        /* Severity color definitions */\n        .critical,\n        .child-row-details .critical {\n            background-color: #d9534f;\n        }\n\n        .high,\n        .child-row-details .high {\n            background-color: #f0ad4e;\n        }\n\n        .medium,\n        .child-row-details .medium {\n            background-color: #5bc0de;\n        }\n\n        .low,\n        .child-row-details .low {\n            background-color: #5cb85c;\n        }\n\n        .unknown,\n        .child-row-details .unknown {\n            background-color: #777;\n        }\n    </style>\n    <!-- State Coloring -->\n    <style>\n        /* State pill base styling */\n        .state-pill,\n        .child-row-details .state-pill {\n            display: inline-block;\n            padding: 5px 10px;\n            border-radius: 15px;\n            color: var(--pill-text);\n            text-align: center;\n            white-space: nowrap;\n        }\n\n        /* State color definitions */\n        .fixed,\n        .child-row-details .fixed {\n            background-color: #d9534f;\n        }\n\n        .not-fixed,\n        .child-row-details .not-fixed {\n            background-color: #f0ad4e;\n        }\n\n        .unknown,\n        .child-row-details .unknown {\n            background-color: #6c757d;\n        }\n\n        .wont-fix,\n        .child-row-details .wont-fix {\n            background-color: #5cb85c;\n        }\n    </style>\n\n    <!-- Package Type Icon Styling -->\n    <style>\n        /* Package type cell container */\n        .pkg-type-cell {\n            display: flex;\n            align-items: center;\n            gap: 6px;\n        }\n\n        /* Icon base styling */\n        .pkg-type-cell i,\n        .pkg-type-cell .icon-span {\n            font-size: 1.2em;\n            width: 1.2em;\n            text-align: center;\n            color: #555;\n            flex-shrink: 0;\n        }\n\n        /* Package icon fallback */\n        .iconify-noto--package {\n            display: inline-block;\n            width: 1.2em;\n            height: 1.2em;\n            vertical-align: middle;\n            background-color: transparent;\n            background-repeat: no-repeat;\n            background-size: contain;\n            background-position: center;\n            background-image: url('https://api.iconify.design/noto:package.svg');\n        }\n\n        /* Alpine Linux icon */\n        .iconify-file-icons--alpine-linux {\n            display: inline-block;\n            width: 1.2em;\n            height: 1.2em;\n            background-color: #0d597f;\n            mask-image: url('https://api.iconify.design/file-icons:alpine-linux.svg');\n            mask-size: contain;\n            mask-repeat: no-repeat;\n            mask-position: center;\n        }\n\n        /* Excel icon */\n        .vscode-icons--file-type-excel {\n            display: inline-block;\n            width: 20px;\n            height: 20px;\n            vertical-align: middle;\n            background-color: transparent;\n            background-repeat: no-repeat;\n            background-size: contain;\n            background-position: center;\n            background-image: url('https://api.iconify.design/vscode-icons:file-type-excel.svg');\n        }\n\n        /* PDF icon */\n        .vscode-icons--file-type-pdf2 {\n            display: inline-block;\n            width: 1em;\n            height: 1em;\n            vertical-align: middle;\n            background-color: transparent;\n            background-repeat: no-repeat;\n            background-size: contain;\n            background-position: center;\n            background-image: url('https://api.iconify.design/vscode-icons:file-type-pdf2.svg');\n        }\n    </style>\n\n</head>\n{{/* Initialize counters */}}\n{{- $CountCritical := 0 }}\n{{- $CountHigh := 0 }}\n{{- $CountMedium := 0 }}\n{{- $CountLow := 0}}\n{{- $CountUnknown := 0 }}\n\n{{/* Create a list */}}\n{{- $FilteredMatches := list }}\n\n{{/* Loop through all vulns limit output and set count*/}}\n{{- range $vuln := .Matches }}\n    {{/* Use this filter to exclude severity if needed */}}\n    {{- if or (eq $vuln.Vulnerability.Severity \"Critical\") (eq $vuln.Vulnerability.Severity \"High\") (eq $vuln.Vulnerability.Severity \"Medium\") (eq $vuln.Vulnerability.Severity \"Low\") (eq $vuln.Vulnerability.Severity \"Unknown\") }}\n        {{- $FilteredMatches = append $FilteredMatches $vuln }}\n        {{- if eq $vuln.Vulnerability.Severity \"Critical\" }}\n            {{- $CountCritical = add $CountCritical 1 }}\n        {{- else if eq $vuln.Vulnerability.Severity \"High\" }}\n            {{- $CountHigh = add $CountHigh 1 }}\n        {{- else if eq $vuln.Vulnerability.Severity \"Medium\" }}\n            {{- $CountMedium = add $CountMedium 1 }}\n        {{- else if eq $vuln.Vulnerability.Severity \"Low\" }}\n            {{- $CountLow = add $CountLow 1 }}\n        {{- else }}\n            {{- $CountUnknown = add $CountUnknown 1 }}\n        {{- end }}\n    {{- end }}\n{{- end }}\n\n<body>\n    <div class=\"main-container\">\n        <div class=\"heading\">\n            <div class=\"heading-left\">\n                <h1 class=\"report-title\">Vulnerability Report</h1>\n                <dl class=\"report-details\">\n                    <dt>Name:</dt>\n                    <dd id=\"nameValue\"> {{- if eq (.Source.Type) \"image\" -}} {{.Source.Target.UserInput}}\n                        {{- else if eq (.Source.Type) \"directory\" -}} {{.Source.Target}}\n                        {{- else if eq (.Source.Type) \"file\" -}} {{.Source.Target}}\n                        {{- else -}} unknown\n                        {{- end -}}</dd>\n\n                    <dt>Type:</dt>\n                    <dd id=\"typeValue\">{{ .Source.Type }}</dd>\n\n                    {{- /* Conditionally add ImageID (Checksum) for images */ -}}\n                    {{- if eq .Source.Type \"image\" -}}\n                    {{- with .Source.Target.ID -}}\n                    <dt>Checksum:</dt>\n                    <dd id=\"checksumValue\">{{ . }}</dd>\n                    {{- end -}}\n                    {{- end -}}\n\n                    <dt>Date:</dt>\n                    <dd>\n                        <span id=\"dateElement\">{{.Descriptor.Timestamp}}</span>\n                        <span id=\"prettyDateElement\" style=\"display: none;\"></span>\n                    </dd>\n                </dl>\n            </div>\n            <div class=\"heading-right\">\n                <img src=\"https://user-images.githubusercontent.com/5199289/136855393-d0a9eef9-ccf1-4e2b-9d7c-7aad16a567e5.png\"\n                    alt=\"Grype Logo\">\n            </div>\n        </div>\n        <div class=\"severity-info\">\n            <div class=\"severity-box\" id=\"critical\">\n                <div class=\"severity-title\">Critical</div>\n                <div class=\"severity-count\" id=\"criticalCount\">{{ $CountCritical }}</div>\n            </div>\n            <div class=\"severity-box\" id=\"high\">\n                <div class=\"severity-title\">High</div>\n                <div class=\"severity-count\" id=\"highCount\">{{ $CountHigh }}</div>\n            </div>\n            <div class=\"severity-box\" id=\"medium\">\n                <div class=\"severity-title\">Medium</div>\n                <div class=\"severity-count\" id=\"mediumCount\">{{ $CountMedium }}</div>\n            </div>\n            <div class=\"severity-box\" id=\"low\">\n                <div class=\"severity-title\">Low</div>\n                <div class=\"severity-count\" id=\"lowCount\">{{ $CountLow }}</div>\n            </div>\n            <div class=\"severity-box\" id=\"unknown\">\n                <div class=\"severity-title\">Unknown</div>\n                <div class=\"severity-count\" id=\"unknownCount\">{{ $CountUnknown }}</div>\n            </div>\n        </div>\n        <div class=\"data-table\">\n            <table id=\"vulnerabilityTable\" class=\"display nowrap\" style=\"width:100%\">\n                <thead>\n                    <tr>\n                        <th>Name</th>\n                        <th>Version</th>\n                        <th>Type</th>\n                        <th>Vulnerability</th>\n                        <th>Severity</th>\n                        <th>State</th>\n                        <th>Fixed In</th>\n                        <th>Description</th>\n                        <th>Related URLs</th>\n                        <th>PURL</th>\n                    </tr>\n                </thead>\n                <tbody>\n                    {{- range $FilteredMatches }}\n                    <tr>\n                        <td>{{.Artifact.Name}}</td>\n                        <td>{{.Artifact.Version}}</td>\n                        <td>{{.Artifact.Type}}</td>\n                        <td>\n                            <a href=\"{{.Vulnerability.DataSource}}\">{{.Vulnerability.ID}}</a>\n                        </td>\n                        <td>{{.Vulnerability.Severity}}</td>\n                        <td>{{.Vulnerability.Fix.State}}</td>\n                        <td>\n                            {{- if .Vulnerability.Fix.Versions }}\n                            <ul>\n                                {{- range .Vulnerability.Fix.Versions }}\n                                <li>{{ . }}</li>\n                                {{- end }}\n                            </ul>\n                            {{- else }}\n                            N/A\n                            {{- end }}\n                        </td>\n                        <td>{{html .Vulnerability.Description}}</td>\n                        <td>{{ toJson .Vulnerability.URLs }}</td>\n                        <td>{{ .Artifact.PURL }}</td>\n                    </tr>\n                    {{- end }}\n                </tbody>\n            </table>\n        </div>\n    </div>\n    <script>\n\n    // ================================================\n    // DataTables Core Logic & Callbacks\n    // ================================================\n\n    /**\n     * Initializes the main DataTable instance (#vulnerabilityTable) with configuration\n     * for responsiveness, buttons (PDF, Excel), dynamic page length calculation,\n     * column definitions, severity filtering, and other display settings.\n     */\n    function initDataTable() {\n        // Target the table element\n        const tableElement = $('#vulnerabilityTable');\n        \n        // Destroy existing DataTable instance if it exists, to prevent reinitialization errors\n        if ($.fn.dataTable.isDataTable(tableElement)) {\n            tableElement.DataTable().destroy();\n        }\n\n        const table = tableElement.DataTable({\n            // Responsive Extension Configuration\n            responsive: {\n                details: {\n                    type: 'inline',\n                    // Use the custom renderer function for child row content\n                    renderer: renderChildRowDetailsDt\n                }\n            },\n            // DOM Structure Definition (Controls placement)\n            // B: Buttons, f: Filter input, t: Table, i: Info, p: Pagination\n            // Custom wrappers for styling/control: dataTables_wrapper, dataTables_control\n            dom: '<\"dataTables_wrapper\"Bf>t<\"dataTables_control\"ip>',\n\n            // Language Configuration\n            language: {\n                search: \"\", // Hide the default \"Search:\" label\n                searchPlaceholder: \"Search vulnerabilities...\" // Set custom placeholder\n            },\n\n            // Initial Sorting Order (Sort by Name column, ascending)\n            order: [[0, 'asc']],\n\n            // Column Definitions (retrieved from helper function)\n            columnDefs: getColumnDefsDt(),\n\n            // Callback after table initialization is complete\n            initComplete: function(settings, json) {\n                // Calculate and set initial page length based on viewport height\n                setDynamicPageLengthDt(this.api());\n            },\n\n            // Callback after each table draw (including pagination, filtering, sorting)\n            drawCallback: function (settings) {\n                const api = this.api();\n                const pageInfo = api.page.info();\n\n                // Target the specific wrapper for pagination controls\n                const controlsWrapper = $(api.table().container()).find('.dataTables_control');\n\n                // Hide pagination controls if there's only one page\n                if (controlsWrapper.length) { // Ensure wrapper exists\n                     controlsWrapper.toggle(pageInfo.pages > 1);\n                }\n            },\n\n            // Buttons Configuration (Export functionality)\n            buttons: [\n                {\n                    extend: 'pdfHtml5',\n                    text: '<span class=\"vscode-icons--file-type-pdf2\"></span>', // Use Iconify span\n                    titleAttr: 'Export to PDF',                   // Tooltip\n                    title: function () { return getReportMetadata().title; },\n                    messageTop: function () { return getReportMetadata().message; },\n                    filename: function () {\n                        return `vulnerability-report-${getReportMetadata().safeName}`;\n                    },\n                    exportOptions: {\n                        // Specify columns to include by name (more robust than index)\n                        // Excludes control, Description, PURL, Related URLs from PDF\n                        columns: [\n                            'Name:name',\n                            'Version:name',\n                            'Type:name',\n                            'Vulnerability:name',\n                            'Severity:name',\n                            'State:name',\n                            'Fixed In:name'\n                        ]\n                    },\n                    customize: function (doc) {\n                        // PDF customization: left-align content, adjust widths\n                        doc.styles.tableHeader.alignment = 'left';\n                        doc.defaultStyle.alignment = 'left';\n                        // Relative widths: Name, Vuln, FixedIn get more space\n                        doc.content[doc.content.length - 1].table.widths = ['*', 'auto', 'auto', '*', 'auto', 'auto', '*'];\n                    }\n                },\n                {\n                    extend: 'excelHtml5',\n                    text: '<span class=\"vscode-icons--file-type-excel\"></span>', // Use Iconify span\n                    titleAttr: 'Export to Excel',                    // Tooltip\n                    title: function () { return getReportMetadata().title; },\n                    messageTop: function () { return getReportMetadata().message; },\n                    filename: function () {\n                        return `vulnerability-report-${getReportMetadata().safeName}`;\n                    },\n                    exportOptions: {\n                        // Specify columns to include by name for Excel\n                        // Includes PURL and Related URLs in Excel export\n                         columns: [\n                            'Name:name',\n                            'Version:name',\n                            'Type:name',\n                            'Vulnerability:name',\n                            'Severity:name', \n                            'State:name',\n                            'Fixed In:name',\n                            'Related URLs:name',\n                            'PURL:name'\n                        ]\n                    }\n                }\n            ]\n        });\n\n        // Setup the click handlers for the severity filter boxes\n        initSeverityFilteringDt(table);\n\n        // --- Resize Listener for Dynamic Page Length ---\n        // Create a debounced version of the page length calculation\n         const dtInstance = table; // Reference the table instance\n         const debouncedPageLengthHandler = debounce(function() {\n             // Ensure the DataTable instance still exists before calculating\n             if ($.fn.dataTable.isDataTable(tableElement)) {\n                setDynamicPageLengthDt(dtInstance);\n             }\n        }, 250); // Delay execution by 250ms after the last resize event\n\n        // Attach the debounced handler to the window resize event\n        window.addEventListener('resize', debouncedPageLengthHandler);\n    }\n\n    /**\n     * Defines the column configurations for the DataTables instance.\n     * Specifies properties like name, target index, visibility, searchability,\n     * rendering functions, cell creation callbacks, and responsive priority.\n     *\n     * @returns {Array<object>} An array of DataTables column definition objects.\n     */\n    function getColumnDefsDt() {\n        return [\n            // Column 0: Name\n            {\n                name: 'Name',\n                targets: 0,\n                searchable: true,\n                responsivePriority: 1\n            },\n            // Column 1: Version\n            {\n                name: 'Version',\n                targets: 1,\n                searchable: true,\n                responsivePriority: 5\n            },\n            // Column 2: Type\n            {\n                name: 'Type',\n                targets: 2,\n                render: function (data, type, row) { return data; },\n                createdCell: function (td, cellData, rowData, row, col) {\n                    $(td).html(formatTypeCell(cellData));\n                },\n                searchable: false,\n                responsivePriority: 6\n            },\n            // Column 3: Vulnerability ID\n            {\n                name: 'Vulnerability',\n                targets: 3,\n                searchable: true,\n                responsivePriority: 2\n            },\n            // Column 4: Severity\n            {\n                name: 'Severity',\n                targets: 4,\n                // Render raw data; formatting is handled by createdCell and renderChildRowDetails\n                render: function (data, type, row) { return data; },\n                // Apply pill formatting using helper function after cell is created\n                createdCell: function (td, cellData, rowData, row, col) {\n                    $(td).html(formatPill(cellData, 'severity'));\n                },\n                searchable: true,\n                responsivePriority: 3\n            },\n            // Column 5: State\n            {\n                name: 'State',\n                targets: 5,\n                 // Render raw data\n                render: function (data, type, row) { return data; },\n                 // Apply pill formatting using helper function after cell is created\n                createdCell: function (td, cellData, rowData, row, col) {\n                    $(td).html(formatPill(cellData, 'state'));\n                },\n                searchable: true,\n                responsivePriority: 4\n            },\n            // Column 7: Fixed In Version(s)\n            {\n                name: 'Fixed In',\n                targets: 6,\n                 // Add class to apply styling\n                createdCell: function (td, cellData, rowData, row, col) {\n                    $(td).addClass('fixed-in');\n                },\n                searchable: true,\n                responsivePriority: 7\n            },\n             // --- Columns initially hidden, shown in child row by default ---\n             // They are excluded from responsivity using className: 'none'\n            {\n                name: 'Description',\n                targets: 7,\n                searchable: true,\n                className: 'none', // Tell Responsive to hide this column initially\n            },\n            {\n                name: 'Related URLs',\n                targets: 8,\n                searchable: false,\n                className: 'none', // Tell Responsive to hide this column initially\n            },\n            {\n                name: 'PURL',\n                targets: 9,\n                searchable: false,\n                className: 'none', // Tell Responsive to hide this column initially\n            },\n        ];\n    }\n\n    /**\n     * Custom renderer function for DataTables Responsive extension.\n     * Generates the HTML content for the child row displayed when a row collapses.\n     * It iterates through the columns marked as 'hidden' by Responsive for the\n     * current row and formats their data appropriately (using helpers like\n     * formatPill and formatUrlList) before combining them into a single HTML block.\n     *\n     * @param {DataTables.Api} api - The DataTables API instance.\n     * @param {number} rowIndex - The index of the row being rendered.\n     * @param {Array<object>} columns - An array of column objects provided by Responsive,\n     *                                  indicating which columns are hidden ({columnIndex, title, hidden}).\n     * @returns {string|false} The HTML string for the child row content, or false if no content is generated.\n     */\n    function renderChildRowDetailsDt( api, rowIndex, columns ) {\n        // We will get data cell-by-cell using the correct API.\n        let finalHtml = '';\n\n        // Iterate through the column information provided by the Responsive extension\n        columns.forEach(function(col) {\n            // Only process columns that are currently hidden by Responsive for this specific row\n            if (col.hidden) {\n                const index = col.columnIndex; // Original index of the column (0-based)\n                const title = col.title;       // Column title (from <thead> or columnDefs name)\n                \n                // --- Use api.cell(rowIndex, columnIndex).data() for robust data retrieval ---\n                const data = api.cell(rowIndex, index).data();\n                \n                let contentHtml = ''; // HTML for this specific column's content\n\n                // Apply specific formatting based on the original column index\n                switch (index) {\n                    case 2: // Type\n                        contentHtml = formatTypeCell(data);\n                        break;\n                    case 4: // Severity\n                        contentHtml = formatPill(data, 'severity');\n                        break;\n                    case 5: // State\n                        contentHtml = formatPill(data, 'state');\n                        break;\n                     case 7: // Description\n                         // Ensure data exists before trying to format\n                         contentHtml = data ? String(data) : ''; \n                         break;\n                    case 8: // Related URLs\n                        contentHtml = formatUrlList(data); // formatUrlList handles empty/invalid data\n                        break;\n                    default:\n                         // Check if data exists and is not null/undefined before displaying\n                         if (data !== null && data !== undefined) {\n                             contentHtml = String(data); // Convert to string just in case\n                         }\n                         break;\n                }\n\n                // Add the formatted content to the final HTML, wrapped in a div with a title\n                // Only add if contentHtml was actually generated (e.g., empty URLs or null data won't add a section)\n                if (contentHtml) {\n                    finalHtml += `<div class=\"child-row-details\"><b>${title}</b><br>${contentHtml}</div>`;\n                }\n            }\n        });\n\n        // Return the generated HTML (if any content was added), otherwise false\n        // Returning false tells Responsive not to show a child row if there's nothing to display\n        return finalHtml ? finalHtml : false;\n    }\n\n    /**\n     * Attaches click handlers to the severity overview boxes.\n     * Clicking a box adds/removes its severity from the active filter set.\n     * Updates the DataTable to show rows matching *any* of the selected severities.\n     * Highlights the active severity boxes.\n     *\n     * @param {DataTables.Api} table - The DataTables API instance.\n     */\n    function initSeverityFilteringDt(table) {\n        let activeSeverityFilters = []; // Use an array to track multiple selections\n        const severityBoxes = $('.severity-box'); // Cache selector\n\n        severityBoxes.on('click', function () {\n            const clickedBox = $(this);\n            const severity = clickedBox.find('.severity-title').text().trim();\n            const severityIndex = activeSeverityFilters.indexOf(severity);\n\n            // Toggle severity in the active filter list\n            if (severityIndex > -1) {\n                // Severity is currently active, remove it\n                activeSeverityFilters.splice(severityIndex, 1);\n                clickedBox.removeClass('active');\n            } else {\n                // Severity is not active, add it\n                activeSeverityFilters.push(severity);\n                clickedBox.addClass('active');\n            }\n\n            // Clear any global search and previous column searches\n            table.search('').columns().search('');\n\n            // Apply the combined severity filter to column 5 (Severity)\n            if (activeSeverityFilters.length > 0) {\n                // Create a regex string like \"^Critical$|^High$|^Medium$\"\n                // This ensures exact matches for each selected severity\n                const filterRegex = activeSeverityFilters.map(sev => `^${sev}$`).join('|');\n                table.column(4).search(filterRegex, true, false); // Enable regex, disable smart search\n            } else {\n                // If no filters are active, clear the specific column search\n                table.column(4).search('');\n            }\n            \n            // Redraw the table to apply the filter changes\n            table.draw();\n        });\n    }\n\n    /**\n     * Calculates the optimal number of rows to display in the DataTable based on\n     * available viewport height and sets the table's page length accordingly.\n     * It subtracts the height of the table header, pagination controls, and a\n     * fixed buffer from the total viewport height to determine the space available\n     * for table rows. Ensures at least one row is shown if space permits.\n     *\n     * @param {DataTables.Api} table - The DataTables API instance for the table.\n     */\n    function setDynamicPageLengthDt(table) {\n        try {\n            const tableWrapper = table.table().container();\n            const tableBody = table.table().body();\n            const controls = $(tableWrapper).find('.dataTables_control')[0];\n\n            // If the table is not visible, don't calculate the page length\n            if (!tableWrapper || !tableBody || !tableBody.rows.length) {\n                return;\n            }\n\n            const firstRow = tableBody.rows[0];\n            const rowHeight = firstRow.offsetHeight;\n\n            // If the row height is not valid, don't calculate the page length\n            if (!rowHeight || rowHeight <= 0) {\n                return;\n            }\n\n            // Measure heights and positions\n            const controlsHeight = controls ? $(controls).outerHeight(true) : 0; // Height of pagination controls\n            const tableBodyTopOffset = tableBody.getBoundingClientRect().top; // Vertical position of the tbody relative to viewport\n            const verticalBuffer = 60; // Pixels to leave empty below the table for aesthetics\n\n            // Calculate available height for rows\n            const availableHeight = window.innerHeight - tableBodyTopOffset - controlsHeight - verticalBuffer;\n\n            // If there's not enough available height, don't calculate the page length\n            if (availableHeight <= 0) {\n                return;\n            }\n\n            // Calculate how many rows fit and ensure at least 1\n            const calculatedNumberOfRows = Math.floor(availableHeight / rowHeight);\n            const numberOfRowsToShow = Math.max(1, calculatedNumberOfRows);\n\n            // Apply the new page length only if it has changed\n            const currentPageLength = table.page.len();\n            if (currentPageLength !== numberOfRowsToShow) {\n                table.page.len(numberOfRowsToShow).draw(false);\n            }\n\n        } catch (error) {\n            console.error(\"Error calculating page length:\", error);\n        }\n    }\n\n    // ================================================\n    // UI Formatting & Utility Helpers\n    // ================================================\n\n    /**\n     * Formats the original ISO timestamp from the #dateElement into a human-readable\n     * format (e.g., \"Monday, January 1, 2024 at 12:00 PM\") and displays it in\n     * the #prettyDateElement, hiding the original element.\n     */\n    function renderFormattedTimestamp() {\n        const originalDateElement = document.getElementById(\"dateElement\");\n        const prettyDateElement = document.getElementById(\"prettyDateElement\");\n\n        // Ensure both elements exist\n        if (!originalDateElement || !prettyDateElement) {\n            console.error(\"prettyTimestamp: Date elements not found.\");\n            return;\n        }\n\n        const dateString = originalDateElement.textContent;\n        if (!dateString) {\n            console.warn(\"prettyTimestamp: Original date string is empty.\");\n            // Optionally hide the pretty date element or show 'N/A'\n            prettyDateElement.textContent = 'N/A';\n            prettyDateElement.style.display = '';\n            originalDateElement.style.display = 'none';\n            return;\n        }\n\n        try {\n            const date = new Date(dateString);\n\n            // Check for invalid date\n            if (isNaN(date.getTime())) {\n                console.warn(\"prettyTimestamp: Invalid date string:\", dateString);\n                prettyDateElement.textContent = 'Invalid Date';\n                prettyDateElement.style.display = '';\n                originalDateElement.style.display = 'none';\n                return;\n            }\n\n            // Format date to a more human-readable form\n            const formattedDate = date.toLocaleDateString(\"en-US\", {\n                weekday: \"long\",\n                year: \"numeric\",\n                month: \"long\",\n                day: \"numeric\",\n            });\n\n            const formattedTime = date.toLocaleTimeString(\"en-US\", {\n                hour: \"2-digit\",\n                minute: \"2-digit\",\n                hour12: true,\n            });\n\n            // Update the content of the pretty date element\n            prettyDateElement.textContent = `${formattedDate} at ${formattedTime}`;\n\n            // Hide the original and show the pretty one\n            originalDateElement.style.display = 'none';\n            prettyDateElement.style.display = ''; // Reset to default (inline)\n\n        } catch (error) {\n             console.error(\"prettyTimestamp: Error formatting date:\", error);\n             prettyDateElement.textContent = 'Error'; // Indicate an error occurred\n             prettyDateElement.style.display = '';\n             originalDateElement.style.display = 'none';\n        }\n    }\n\n    /**\n     * Extracts report metadata (title, name, type, checksum, date) from DOM elements.\n     * Formats the data for use in report exports (PDF, Excel messageTop) and\n     * generates a sanitized version of the name for use in filenames.\n     *\n     * @returns {object} An object containing report metadata:\n     *                   { title: string, message: string, name: string, safeName: string }\n     */\n    function getReportMetadata() {\n        // Query elements safely\n        const titleElem = document.querySelector('.report-title');\n        const nameElem = document.getElementById('nameValue');\n        const typeElem = document.getElementById('typeValue');\n        const checksumElem = document.getElementById('checksumValue');\n        const dateElem = document.getElementById('prettyDateElement');\n\n        // Extract text content with fallbacks\n        const title = titleElem ? titleElem.textContent.trim() : 'Vulnerability Report';\n        const name = nameElem ? nameElem.textContent.trim() : 'N/A';\n        const type = typeElem ? typeElem.textContent.trim() : 'N/A';\n        const checksum = checksumElem ? checksumElem.textContent.trim() : 'N/A';\n        const dateText = dateElem ? dateElem.textContent.trim() : 'N/A';\n\n        // Sanitize the name for use as a filename (allow letters, numbers, ., _, -)\n        const safeName = name.replace(/[^a-z0-9._-]+/gi, '_').replace(/_+/g, '_');\n\n        // Construct the message string for export headers\n        let message = `Name: ${name}\\n`;\n        message += `Type: ${type}\\n`;\n        if (checksumElem && checksum !== 'N/A') {\n            message += `Checksum: ${checksum}\\n`;\n        }\n        message += `Date: ${dateText}\\n\\n`;\n\n        return {\n            title,    // Report title\n            message,  // Formatted multi-line message for export header\n            name,     // Original name\n            safeName  // Sanitized name for filename\n        };\n    }\n\n    /**\n     * Generates HTML for a styled \"pill\" element used to display Severity or State.\n     * Applies appropriate CSS classes based on the data value and type.\n     *\n     * @param {string} data - The data value (e.g., \"Critical\", \"fixed\").\n     * @param {string} type - The type of pill ('severity' or 'state').\n     * @returns {string} HTML string for the styled span element.\n     */\n    function formatPill(data, type) {\n        // Define mappings from data values to CSS classes\n        const severityClasses = {'Critical': 'critical','High': 'high','Medium': 'medium','Low': 'low','Unknown': 'unknown'};\n        const stateClasses = {'fixed': 'fixed','not-fixed': 'not-fixed','unknown': 'unknown','wont-fix': 'wont-fix'};\n\n        let baseClass = '';\n        let specificClass = '';\n        let text = data || 'Unknown'; // Use 'Unknown' as default text if data is falsy\n\n        // Determine base class and specific class based on type\n        if (type === 'severity') {\n            baseClass = 'severity-pill';\n            // Use provided data, fallback to 'Unknown' class if data is not in map\n            specificClass = severityClasses[data] || 'unknown';\n        } else if (type === 'state') {\n            baseClass = 'state-pill';\n             // Normalize state data to lower case for matching, fallback to 'unknown' class\n            specificClass = stateClasses[String(data).toLowerCase()] || 'unknown';\n        } else {\n            // If type is unknown, return the raw text without formatting\n             console.warn(`formatPill: Unknown type \"${type}\" for data \"${data}\"`);\n            return text;\n        }\n\n        // Construct and return the HTML span element\n        // Use text content that was determined (might be 'Unknown')\n        return `<span class=\"${baseClass} ${specificClass}\">${text}</span>`;\n    }\n\n    /**\n     * Parses a JSON string containing an array of URLs and formats them\n     * into an HTML unordered list (<ul>). Handles potential JSON parsing errors.\n     * URLs starting with 'http' are made into clickable links. Other strings\n     * are displayed as plain text list items.\n     *\n     * @param {string} data - A JSON string representing an array of URLs/strings, or null/undefined.\n     * @returns {string} An HTML string containing the <ul> list, or an empty string if no valid URLs are found or parsing fails.\n     */\n    function formatUrlList(data) {\n        let urls = [];\n        // Safely parse the JSON data\n        try {\n            // Only attempt parsing if data is a non-empty string\n            if (data && typeof data === 'string') {\n                urls = JSON.parse(data);\n            }\n        } catch (error) {\n            console.error(\"Failed to parse Related URLs JSON:\", data, error);\n            urls = []; // Ensure urls is an array on error\n        }\n\n        let listHtml = '';\n        // Ensure urls is an array before proceeding\n        if (Array.isArray(urls) && urls.length > 0) {\n            listHtml += '<ul>';\n            urls.forEach(url => {\n                // Check if the item is a valid, non-empty string\n                if (url && typeof url === 'string') {\n                     // Check if it looks like a clickable URL\n                    if (url.startsWith('http://') || url.startsWith('https://')) {\n                        // Create a link, escaping URL for safety (though usually fine here)\n                        // Add rel=\"noopener noreferrer\" for security when using target=\"_blank\"\n                        listHtml += `<li><a href=\"${url}\" target=\"_blank\" rel=\"noopener noreferrer\">${url}</a></li>`;\n                    } else {\n                        // Display other strings as plain text list items\n                        listHtml += `<li>${url}</li>`;\n                    }\n                }\n            });\n            listHtml += '</ul>';\n        }\n        return listHtml;\n    }\n\n    /**\n     * Returns a function, that, as long as it continues to be invoked, will not\n     * be triggered. The function will be called after it stops being called for\n     * N milliseconds. If `immediate` is passed, trigger the function on the\n     * leading edge, instead of the trailing.\n     *\n     * @param {Function} func - The function to debounce.\n     * @param {number} wait - The number of milliseconds to delay.\n     * @param {boolean} [immediate=false] - Trigger the function on the leading edge.\n     * @returns {Function} The debounced function.\n     */\n    function debounce(func, wait, immediate = false) {\n        let timeout;\n\n        return function executedFunction() {\n            const context = this;\n            const args = arguments;\n\n            const later = function() {\n                timeout = null;\n                if (!immediate) {\n                    func.apply(context, args);\n                }\n            };\n\n            const callNow = immediate && !timeout;\n\n            clearTimeout(timeout);\n            timeout = setTimeout(later, wait);\n\n            if (callNow) {\n                func.apply(context, args);\n            }\n        };\n    };\n\n    /**\n    * Icon mapping for different package types.\n    * Uses Devicon classes where available, otherwise a fallback span.\n    */\n    const packageTypeIcons = {\n        'alpm': { type: 'icon', class: 'devicon-archlinux-plain colored' },\n        'apk': { type: 'fallback', class: 'iconify-file-icons--alpine-linux' },\n        'cocoapods': { type: 'icon', class: 'devicon-apple-original colored' },\n        'composer': { type: 'icon', class: 'devicon-composer-line colored' },\n        'conan': { type: 'icon', class: 'devicon-cplusplus-plain' },\n        'deb': { type: 'icon', class: 'devicon-debian-plain colored' },\n        'dotnet': { type: 'icon', class: 'devicon-dotnetcore-plain colored' },\n        'gem': { type: 'icon', class: 'devicon-ruby-plain colored' },\n        'go-module': { type: 'icon', class: 'devicon-go-original-wordmark colored' },\n        'haskell': { type: 'icon', class: 'devicon-haskell-plain colored' },\n        'hex': { type: 'icon', class: 'devicon-elixir-plain colored' },\n        'java': { type: 'icon', class: 'devicon-java-plain colored' },\n        'jenkins-plugin': { type: 'icon', class: 'devicon-jenkins-line' },\n        'linux-kernel-module': { type: 'icon', class: 'devicon-linux-plain colored' },\n        'lua': { type: 'icon', class: 'devicon-lua-plain colored' },\n        'npm': { type: 'icon', class: 'devicon-npm-original-wordmark colored' },\n        'nix': { type: 'icon', class: 'devicon-nixos-plain colored' },\n        'portage': { type: 'icon', class: 'devicon-gentoo-plain colored' },\n        'pub': { type: 'icon', class: 'devicon-dart-plain colored' },\n        'python': { type: 'icon', class: 'devicon-python-plain colored' },\n        'rpm': { type: 'icon', class: 'devicon-redhat-plain colored' },\n        'rust-crate': { type: 'icon', class: 'devicon-rust-plain colored' },\n        'swift': { type: 'icon', class: 'devicon-swift-plain colored' },\n        'wordpress-plugin': { type: 'icon', class: 'devicon-wordpress-plain colored' },\n        'github-action': { type: 'icon', class: 'devicon-github-original colored' },\n        'binary': { type: 'fallback', class: 'iconify-noto--package' },\n        'buildroot': { type: 'fallback', class: 'iconify-noto--package' },\n        'graalvm-native-image': { type: 'fallback', class: 'iconify-noto--package' },\n        'kb': { type: 'icon', class: 'devicon-windows11-original colored' },\n        // Fallback for unknown types will be handled in formatTypeCell\n    };\n\n    /**\n     * Generates HTML for a package type cell, including an icon.\n     *\n     * @param {string} data - The package type string (e.g., \"java\", \"npm\").\n     * @returns {string} HTML string for the formatted cell content.\n     */\n    function formatTypeCell(data) {\n        const typeKey = data ? String(data).toLowerCase() : 'unknown'; // Normalize key\n        const iconInfo = packageTypeIcons[typeKey];\n        const fallbackInfo = { type: 'fallback', class: 'noto--package' };\n        const chosenInfo = iconInfo || fallbackInfo;\n\n        let iconHtml = '';\n        if (chosenInfo.type === 'icon') {\n            iconHtml = `<i class=\"${chosenInfo.class}\"></i>`;\n        } else { // fallback\n            iconHtml = `<span class=\"icon-span ${chosenInfo.class}\"></span>`;\n        }\n\n        return `<span class=\"pkg-type-cell\">${iconHtml}<span>${data}</span></span>`;\n    }\n\n    // ================================================\n    // Main Execution\n    // ================================================\n\n    document.addEventListener(\"DOMContentLoaded\", function () {\n        // 1. Update the browser page title dynamically using report name\n        const headerInfo = getReportMetadata();\n        if (headerInfo && headerInfo.safeName && headerInfo.safeName !== 'N_A') {\n            document.title = `Vulnerability Report (${headerInfo.safeName})`;\n        } else {\n            document.title = `Vulnerability Report`;\n        }\n\n        // 2. Format the timestamp in the header\n        renderFormattedTimestamp();\n\n        // 3. Initialize the main DataTable\n        initDataTable();\n        \n    });\n\n\n\n    </script>\n\n</body>\n\n</html>"
  },
  {
    "path": "templates/junit.tmpl",
    "content": "<?xml version=\"1.0\" ?>\n<testsuites name=\"grype-junit\">\n{{- $failures := len $.Matches }}\n    <testsuite tests=\"{{ $failures }}\" failures=\"{{ $failures }}\" name=\"{{ $.Distro.Name }}:{{ $.Distro.Version }}\" errors=\"0\" skipped=\"0\">\n        <properties>\n            <property name=\"type\" value=\"{{ $.Distro.Name }}\"></property>\n        </properties>\n        {{- range .Matches }}\n        <testcase classname=\"{{ .Artifact.Name }}-{{ .Artifact.Version }} ({{ .Artifact.Type }})\" name=\"[{{ .Vulnerability.Severity }}] {{ .Vulnerability.ID }}\">\n            <failure message=\"{{ .Artifact.Name }}: {{ .Vulnerability.ID }}\" type=\"description\">\n<![CDATA[\n{{ html .Vulnerability.Description }}\n\nCPEs:\n{{- range .Artifact.CPEs }}\n{{ . }}\n{{- end }}\n\nDataSource:\n{{ html .Vulnerability.DataSource }}\n\n]]></failure>\n        </testcase>\n        {{- end }}\n    </testsuite>\n</testsuites>\n"
  },
  {
    "path": "templates/markdown.tmpl",
    "content": "# Vulnerability Report\n\n- Name: {{- if eq (.Source.Type) \"image\" }} {{ .Source.Target.UserInput }}\n{{ else if eq (.Source.Type) \"directory\" }} {{ .Source.Target }}\n{{ else if eq (.Source.Type) \"file\" }} {{ .Source.Target }}\n{{ else if eq (.Source.Type) \"sbom-file\" }} {{ .Source.Target }}\n{{ else }} unknown\n{{ end -}}\n- Type: {{ .Source.Type }}\n{{- if eq .Source.Type \"image\" }}\n{{ with .Source.Target.ID }}\n- Checksum: {{ . }}\n{{- end -}}\n{{- end}}\n- Date: {{ .Descriptor.Timestamp }}\n\n| Package | Version Installed | Vulnerability ID | Severity |\n|---------|-------------------|------------------|----------|\n{{- range .Matches }}\n| {{ .Artifact.Name }} | {{ .Artifact.Version }} | {{ .Vulnerability.ID }} | {{ .Vulnerability.Severity }} |\n{{- end }}\n"
  },
  {
    "path": "templates/table.tmpl",
    "content": "{{- $name_length := 4}}\n{{- $installed_length := 9}}\n{{- $fixed_in_length := 8}}\n{{- $type_length := 4}}\n{{- $vulnerability_length := 13}}\n{{- $severity_length := 8}}\n{{- range .Matches}}\n{{- $temp_name_length := (len .Artifact.Name)}}\n{{- $temp_installed_length := (len .Artifact.Version)}}\n{{- $temp_fixed_in_length := (len (.Vulnerability.Fix.Versions | join \" \"))}}\n{{- $temp_type_length := (len .Artifact.Type)}}\n{{- $temp_vulnerability_length := (len .Vulnerability.ID)}}\n{{- $temp_severity_length := (len .Vulnerability.Severity)}}\n{{- if (lt $name_length $temp_name_length) }}\n{{- $name_length = $temp_name_length}}\n{{- end}}\n{{- if (lt $installed_length $temp_installed_length) }}\n{{- $installed_length = $temp_installed_length}}\n{{- end}}\n{{- if (lt $fixed_in_length $temp_fixed_in_length) }}\n{{- $fixed_in_length = $temp_fixed_in_length}}\n{{- end}}\n{{- if (lt $type_length $temp_type_length) }}\n{{- $type_length = $temp_type_length}}\n{{- end}}\n{{- if (lt $vulnerability_length $temp_vulnerability_length) }}\n{{- $vulnerability_length = $temp_vulnerability_length}}\n{{- end}}\n{{- if (lt $severity_length $temp_severity_length) }}\n{{- $severity_length = $temp_severity_length}}\n{{- end}}\n{{- end}}\n{{- $name_length = add $name_length 2}}\n{{- $pad_name := repeat (int $name_length) \" \"}}\n{{- $installed_length = add $installed_length 2}}\n{{- $pad_installed := repeat (int $installed_length) \" \"}}\n{{- $fixed_in_length = add $fixed_in_length 2}}\n{{- $pad_fixed_in := repeat (int $fixed_in_length) \" \"}}\n{{- $type_length = add $type_length 2}}\n{{- $pad_type := repeat (int $type_length) \" \"}}\n{{- $vulnerability_length = add $vulnerability_length 2}}\n{{- $pad_vulnerability := repeat (int $vulnerability_length) \" \"}}\n{{- $severity_length = add $severity_length 2}}\n{{- $pad_severity := repeat (int $severity_length) \" \"}}\n{{cat \"NAME\" (substr 5 (int $name_length) $pad_name)}}{{cat \"INSTALLED\" (substr 10 (int $installed_length) $pad_installed)}}{{cat \"FIXED-IN\" (substr 9 (int $fixed_in_length) $pad_fixed_in)}}{{cat \"TYPE\" (substr 5 (int $type_length) $pad_type)}}{{cat \"VULNERABILITY\" (substr 14 (int $vulnerability_length) $pad_vulnerability)}}{{cat \"SEVERITY\" (substr 9 (int $severity_length) $pad_severity)}}\n{{- range .Matches}}\n{{cat .Artifact.Name (substr (int (add (len .Artifact.Name) 1)) (int $name_length) $pad_name)}}{{cat .Artifact.Version (substr (int (add (len .Artifact.Version) 1)) (int $installed_length) $pad_installed)}}{{cat (.Vulnerability.Fix.Versions | join \" \") (substr (int (add (len (.Vulnerability.Fix.Versions | join \" \")) 1)) (int $fixed_in_length) $pad_fixed_in)}}{{cat .Artifact.Type (substr (int (add (len .Artifact.Type) 1)) (int $type_length) $pad_type)}}{{cat .Vulnerability.ID (substr (int (add (len .Vulnerability.ID) 1)) (int $vulnerability_length) $pad_vulnerability)}}{{cat .Vulnerability.Severity (substr (int (add (len .Vulnerability.Severity) 1)) (int $severity_length) $pad_severity)}}\n{{- end}}"
  },
  {
    "path": "test/cli/cmd_test.go",
    "content": "package cli\n\nimport (\n\t\"encoding/json\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestCmd(t *testing.T) {\n\ttests := []struct {\n\t\tname       string\n\t\targs       []string\n\t\tenv        map[string]string\n\t\tassertions []traitAssertion\n\t}{\n\t\t{\n\t\t\tname: \"no-args-shows-help\",\n\t\t\targs: []string{},\n\t\t\tassertions: []traitAssertion{\n\t\t\t\tassertInOutput(\"an image/directory argument is required\"),                              // specific error that should be shown\n\t\t\t\tassertInOutput(\"A vulnerability scanner for container images, filesystems, and SBOMs\"), // excerpt from help description\n\t\t\t\tassertFailingReturnCode,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"empty-string-arg-shows-help\",\n\t\t\targs: []string{\"\"},\n\t\t\tassertions: []traitAssertion{\n\t\t\t\tassertInOutput(\"an image/directory argument is required\"),                              // specific error that should be shown\n\t\t\t\tassertInOutput(\"A vulnerability scanner for container images, filesystems, and SBOMs\"), // excerpt from help description\n\t\t\t\tassertFailingReturnCode,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"ensure valid descriptor\",\n\t\t\targs: []string{getFixtureImage(t, \"image-bare\"), \"-o\", \"json\"},\n\t\t\tassertions: []traitAssertion{\n\t\t\t\tassertInOutput(`\"check-for-app-update\":`), // assert existence of the app config block\n\t\t\t\tassertInOutput(`\"db\":`),                   // assert existence of the db status block\n\t\t\t\tassertInOutput(`\"built\":`),                // assert existence of the db status block\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"platform-option-wired-up\",\n\t\t\targs: []string{\"--platform\", \"arm64\", \"-o\", \"json\", \"registry:busybox:1.31\"},\n\t\t\tassertions: []traitAssertion{\n\t\t\t\tassertInOutput(\"sha256:1ee006886991ad4689838d3a288e0dd3fd29b70e276622f16b67a8922831a853\"), // linux/arm64 image digest\n\t\t\t},\n\t\t},\n\t\t// TODO: uncomment this test when we can use `grype config`\n\t\t//{\n\t\t//\tname: \"responds-to-search-options\",\n\t\t//\targs: []string{\"--help\"},\n\t\t//\tenv: map[string]string{\n\t\t//\t\t\"GRYPE_SEARCH_UNINDEXED_ARCHIVES\": \"true\",\n\t\t//\t\t\"GRYPE_SEARCH_INDEXED_ARCHIVES\":   \"false\",\n\t\t//\t\t\"GRYPE_SEARCH_SCOPE\":              \"all-layers\",\n\t\t//\t},\n\t\t//\tassertions: []traitAssertion{\n\t\t//\t\t// the application config in the log matches that of what we expect to have been configured. Note:\n\t\t//\t\t// we are not testing further wiring of this option, only that the config responds to\n\t\t//\t\t// package-cataloger-level options.\n\t\t//\t\tassertInOutput(\"unindexed-archives: true\"),\n\t\t//\t\tassertInOutput(\"indexed-archives: false\"),\n\t\t//\t\tassertInOutput(\"scope: 'all-layers'\"),\n\t\t//\t},\n\t\t//},\n\t\t{\n\t\t\tname: \"vulnerabilities in output on -f with failure\",\n\t\t\targs: []string{\"registry:busybox:1.31\", \"-f\", \"high\", \"--platform\", \"linux/amd64\"},\n\t\t\tassertions: []traitAssertion{\n\t\t\t\tassertInOutput(\"CVE-2021-42379\"),\n\t\t\t\tassertFailingReturnCode,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"reason for ignored vulnerabilities is available in the template\",\n\t\t\targs: []string{\n\t\t\t\t\"sbom:\" + filepath.Join(\"testdata\", \"test-ignore-reason\", \"sbom.json\"),\n\t\t\t\t\"-c\", filepath.Join(\"testdata\", \"test-ignore-reason\", \"config-with-ignore.yaml\"),\n\t\t\t\t\"-o\", \"template\",\n\t\t\t\t\"-t\", filepath.Join(\"testdata\", \"test-ignore-reason\", \"template-with-ignore-reasons\"),\n\t\t\t},\n\t\t\tassertions: []traitAssertion{\n\t\t\t\tassertInOutput(\"CVE-2021-42385 (test reason for vulnerability being ignored)\"),\n\t\t\t\tassertSucceedingReturnCode,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"ignore-states wired up\",\n\t\t\targs: []string{\"./testdata/sbom-grype-source.json\", \"--ignore-states\", \"unknown\"},\n\t\t\tassertions: []traitAssertion{\n\t\t\t\tassertSucceedingReturnCode,\n\t\t\t\tassertRowInStdOut([]string{\"Pygments\", \"2.6.1\", \"2.7.4\", \"python\", \"GHSA-pq64-v7f5-gqh8\", \"High\"}),\n\t\t\t\tassertNotInOutput(\"CVE-2014-6052\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"ignore-states wired up - ignore fixed\",\n\t\t\targs: []string{\"./testdata/sbom-grype-source.json\", \"--ignore-states\", \"fixed\"},\n\t\t\tassertions: []traitAssertion{\n\t\t\t\tassertSucceedingReturnCode,\n\t\t\t\tassertRowInStdOut([]string{\"libvncserver\", \"0.9.9\", \"apk\", \"CVE-2014-6052\", \"High\"}),\n\t\t\t\tassertNotInOutput(\"GHSA-pq64-v7f5-gqh8\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"ignore-states wired up - ignore fixed, show suppressed\",\n\t\t\targs: []string{\"./testdata/sbom-grype-source.json\", \"--ignore-states\", \"fixed\", \"--show-suppressed\"},\n\t\t\tassertions: []traitAssertion{\n\t\t\t\tassertSucceedingReturnCode,\n\t\t\t\tassertRowInStdOut([]string{\"Pygments\", \"2.6.1\", \"2.7.4\", \"python\", \"GHSA-pq64-v7f5-gqh8\", \"High\", \"(suppressed)\"}),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t// from: https://github.com/anchore/grype/issues/2412 we need to ensure that explicit ignores in code don't break\n\t\t\tname: \"explicit ignores wired up\",\n\t\t\targs: []string{getFixtureImage(t, \"image-java-subprocess\")},\n\t\t\tassertions: []traitAssertion{\n\t\t\t\tassertSucceedingReturnCode,\n\t\t\t\tassertNotInOutput(\"CVE-2023-45853\"),\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tcmd, stdout, stderr := runGrype(t, test.env, test.args...)\n\t\t\tfor _, traitFn := range test.assertions {\n\t\t\t\ttraitFn(t, stdout, stderr, cmd.ProcessState.ExitCode())\n\t\t\t}\n\t\t\tif t.Failed() {\n\t\t\t\tt.Log(\"STDOUT:\\n\", stdout)\n\t\t\t\tt.Log(\"STDERR:\\n\", stderr)\n\t\t\t\tt.Log(\"COMMAND:\", strings.Join(cmd.Args, \" \"))\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_descriptorNameAndVersionSet(t *testing.T) {\n\t_, output, _ := runGrype(t, nil, \"-o\", \"json\", getFixtureImage(t, \"image-bare\"))\n\n\tparsed := map[string]any{}\n\terr := json.Unmarshal([]byte(output), &parsed)\n\trequire.NoError(t, err)\n\n\tdesc, _ := parsed[\"descriptor\"].(map[string]any)\n\trequire.NotNil(t, desc)\n\n\tname := desc[\"name\"]\n\trequire.Equal(t, \"grype\", name)\n\n\tversion := desc[\"version\"]\n\trequire.NotEmpty(t, version)\n}\n"
  },
  {
    "path": "test/cli/config_test.go",
    "content": "package cli\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"gopkg.in/yaml.v3\"\n)\n\nfunc Test_configLoading(t *testing.T) {\n\tcwd, err := os.Getwd()\n\trequire.NoError(t, err)\n\n\tconfigsDir := filepath.Join(cwd, \"testdata\", \"configs\")\n\tpath := func(path string) string {\n\t\treturn filepath.Join(configsDir, filepath.Join(strings.Split(path, \"/\")...))\n\t}\n\n\ttype ignore struct {\n\t\tVuln string `yaml:\"vulnerability\"`\n\t}\n\n\ttype config struct {\n\t\tIgnores []ignore `yaml:\"ignore\"`\n\t}\n\n\ttests := []struct {\n\t\tname     string\n\t\thome     string\n\t\tcwd      string\n\t\targs     []string\n\t\texpected []ignore\n\t\terr      string\n\t}{\n\t\t{\n\t\t\tname: \"single explicit config\",\n\t\t\thome: configsDir,\n\t\t\tcwd:  cwd,\n\t\t\targs: []string{\n\t\t\t\t\"-c\",\n\t\t\t\tpath(\"dir1/.grype.yaml\"),\n\t\t\t},\n\t\t\texpected: []ignore{\n\t\t\t\t{\n\t\t\t\t\tVuln: \"dir1-vuln1\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tVuln: \"dir1-vuln2\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"multiple explicit config\",\n\t\t\thome: configsDir,\n\t\t\tcwd:  cwd,\n\t\t\targs: []string{\n\t\t\t\t\"-c\",\n\t\t\t\tpath(\"dir1/.grype.yaml\"),\n\t\t\t\t\"-c\",\n\t\t\t\tpath(\"dir2/.grype.yaml\"),\n\t\t\t},\n\t\t\texpected: []ignore{\n\t\t\t\t{\n\t\t\t\t\tVuln: \"dir1-vuln1\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tVuln: \"dir1-vuln2\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tVuln: \"dir2-vuln1\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tVuln: \"dir2-vuln2\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"empty profile override\",\n\t\t\thome: configsDir,\n\t\t\tcwd:  cwd,\n\t\t\targs: []string{\n\t\t\t\t\"-c\",\n\t\t\t\tpath(\"dir1/.grype.yaml\"),\n\t\t\t\t\"-c\",\n\t\t\t\tpath(\"dir2/.grype.yaml\"),\n\t\t\t\t\"--profile\",\n\t\t\t\t\"no-ignore\",\n\t\t\t},\n\t\t\texpected: []ignore{},\n\t\t},\n\t\t{\n\t\t\tname: \"no profiles defined\",\n\t\t\thome: configsDir,\n\t\t\tcwd:  configsDir,\n\t\t\targs: []string{\n\t\t\t\t\"-c\",\n\t\t\t\tpath(\"dir3/.grype.yaml\"),\n\t\t\t\t\"--profile\",\n\t\t\t\t\"invalid\",\n\t\t\t},\n\t\t\terr: \"not found in any configuration files\",\n\t\t},\n\t\t{\n\t\t\tname: \"invalid profile name\",\n\t\t\thome: configsDir,\n\t\t\tcwd:  cwd,\n\t\t\targs: []string{\n\t\t\t\t\"-c\",\n\t\t\t\tpath(\"dir1/.grype.yaml\"),\n\t\t\t\t\"-c\",\n\t\t\t\tpath(\"dir2/.grype.yaml\"),\n\t\t\t\t\"--profile\",\n\t\t\t\t\"alt\",\n\t\t\t},\n\t\t\terr: \"profile not found\",\n\t\t},\n\t\t{\n\t\t\tname: \"explicit with profile override\",\n\t\t\thome: configsDir,\n\t\t\tcwd:  cwd,\n\t\t\targs: []string{\n\t\t\t\t\"-c\",\n\t\t\t\tpath(\"dir1/.grype.yaml\"),\n\t\t\t\t\"-c\",\n\t\t\t\tpath(\"dir2/.grype.yaml\"),\n\t\t\t\t\"--profile\",\n\t\t\t\t\"alt-ignore\",\n\t\t\t},\n\t\t\texpected: []ignore{\n\t\t\t\t{\n\t\t\t\t\tVuln: \"dir1-alt-vuln1\", // dir1 is still first\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tVuln: \"dir1-alt-vuln2\", // dir1 is still first\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tVuln: \"dir2-alt-vuln1\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tVuln: \"dir2-alt-vuln2\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tt.Chdir(test.cwd)\n\t\t\tenv := map[string]string{\n\t\t\t\t\"HOME\":            test.home,\n\t\t\t\t\"XDG_CONFIG_HOME\": test.home,\n\t\t\t}\n\t\t\t_, stdout, stderr := runGrype(t, env, append([]string{\"config\", \"--load\"}, test.args...)...)\n\t\t\tif test.err != \"\" {\n\t\t\t\trequire.Contains(t, stderr, test.err)\n\t\t\t\treturn\n\t\t\t} else {\n\t\t\t\trequire.Empty(t, stderr)\n\t\t\t}\n\t\t\tgot := config{}\n\t\t\terr = yaml.NewDecoder(strings.NewReader(stdout)).Decode(&got)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, test.expected, got.Ignores)\n\t\t})\n\t}\n}\n\nfunc Test_dpkgUseCPEsForEOLEnvVar(t *testing.T) {\n\t// Test that GRYPE_MATCH_DPKG_USE_CPES_FOR_EOL env var is properly wired up\n\ttype matchConfig struct {\n\t\tDpkg struct {\n\t\t\tUseCPEsForEOL bool `yaml:\"use-cpes-for-eol\"`\n\t\t} `yaml:\"dpkg\"`\n\t}\n\ttype testConfig struct {\n\t\tMatch matchConfig `yaml:\"match\"`\n\t}\n\n\ttests := []struct {\n\t\tname     string\n\t\tenvValue string\n\t\texpected bool\n\t}{\n\t\t{\n\t\t\tname:     \"env var true enables CPE matching for EOL\",\n\t\t\tenvValue: \"true\",\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"env var false disables CPE matching for EOL\",\n\t\t\tenvValue: \"false\",\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"default is false\",\n\t\t\tenvValue: \"\",\n\t\t\texpected: false,\n\t\t},\n\t}\n\n\t// Create a minimal config file for testing\n\ttmpDir := t.TempDir()\n\tcfgPath := filepath.Join(tmpDir, \".grype.yaml\")\n\terr := os.WriteFile(cfgPath, []byte(\"check-for-app-update: false\\n\"), 0644)\n\trequire.NoError(t, err)\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tenv := map[string]string{\n\t\t\t\t\"HOME\":            tmpDir,\n\t\t\t\t\"XDG_CONFIG_HOME\": tmpDir,\n\t\t\t}\n\t\t\tif test.envValue != \"\" {\n\t\t\t\tenv[\"GRYPE_MATCH_DPKG_USE_CPES_FOR_EOL\"] = test.envValue\n\t\t\t}\n\n\t\t\t_, stdout, stderr := runGrype(t, env, \"-c\", cfgPath, \"config\", \"--load\")\n\t\t\trequire.Empty(t, stderr)\n\n\t\t\tgot := testConfig{}\n\t\t\terr := yaml.NewDecoder(strings.NewReader(stdout)).Decode(&got)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, test.expected, got.Match.Dpkg.UseCPEsForEOL,\n\t\t\t\t\"expected match.dpkg.use-cpes-for-eol to be %v\", test.expected)\n\t\t})\n\t}\n}\n\nfunc Test_rpmUseCPEsForEOLEnvVar(t *testing.T) {\n\t// Test that GRYPE_MATCH_RPM_USE_CPES_FOR_EOL env var is properly wired up\n\ttype matchConfig struct {\n\t\tRpm struct {\n\t\t\tUseCPEsForEOL bool `yaml:\"use-cpes-for-eol\"`\n\t\t} `yaml:\"rpm\"`\n\t}\n\ttype testConfig struct {\n\t\tMatch matchConfig `yaml:\"match\"`\n\t}\n\n\ttests := []struct {\n\t\tname     string\n\t\tenvValue string\n\t\texpected bool\n\t}{\n\t\t{\n\t\t\tname:     \"env var true enables CPE matching for EOL\",\n\t\t\tenvValue: \"true\",\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"env var false disables CPE matching for EOL\",\n\t\t\tenvValue: \"false\",\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"default is false\",\n\t\t\tenvValue: \"\",\n\t\t\texpected: false,\n\t\t},\n\t}\n\n\t// Create a minimal config file for testing\n\ttmpDir := t.TempDir()\n\tcfgPath := filepath.Join(tmpDir, \".grype.yaml\")\n\terr := os.WriteFile(cfgPath, []byte(\"check-for-app-update: false\\n\"), 0644)\n\trequire.NoError(t, err)\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tenv := map[string]string{\n\t\t\t\t\"HOME\":            tmpDir,\n\t\t\t\t\"XDG_CONFIG_HOME\": tmpDir,\n\t\t\t}\n\t\t\tif test.envValue != \"\" {\n\t\t\t\tenv[\"GRYPE_MATCH_RPM_USE_CPES_FOR_EOL\"] = test.envValue\n\t\t\t}\n\n\t\t\t_, stdout, stderr := runGrype(t, env, \"-c\", cfgPath, \"config\", \"--load\")\n\t\t\trequire.Empty(t, stderr)\n\n\t\t\tgot := testConfig{}\n\t\t\terr := yaml.NewDecoder(strings.NewReader(stdout)).Decode(&got)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, test.expected, got.Match.Rpm.UseCPEsForEOL,\n\t\t\t\t\"expected match.rpm.use-cpes-for-eol to be %v\", test.expected)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "test/cli/db_providers_test.go",
    "content": "package cli\n\nimport (\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestDBProviders(t *testing.T) {\n\ttests := []struct {\n\t\tname       string\n\t\targs       []string\n\t\tenv        map[string]string\n\t\tassertions []traitAssertion\n\t}{\n\t\t{\n\t\t\tname: \"db providers command\",\n\t\t\targs: []string{\"db\", \"providers\"},\n\t\t\tassertions: []traitAssertion{\n\t\t\t\tassertNoStderr,\n\t\t\t\tassertDbProvidersTableReport,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"db providers command help\",\n\t\t\targs: []string{\"db\", \"providers\", \"-h\"},\n\t\t\tassertions: []traitAssertion{\n\t\t\t\tassertInOutput(\"List vulnerability providers that are in the database\"),\n\t\t\t\tassertNoStderr,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"db providers command with table output flag\",\n\t\t\targs: []string{\"db\", \"providers\", \"-o\", \"table\"},\n\t\t\tassertions: []traitAssertion{\n\t\t\t\tassertNoStderr,\n\t\t\t\tassertDbProvidersTableReport,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"db providers command with json output flag\",\n\t\t\targs: []string{\"db\", \"providers\", \"-o\", \"json\"},\n\t\t\tassertions: []traitAssertion{\n\t\t\t\tassertInOutput(\"processor\"),\n\t\t\t\tassertNoStderr,\n\t\t\t\tassertJsonReport,\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tcmd, stdout, stderr := runGrype(t, test.env, test.args...)\n\t\t\tfor _, traitAssertionFn := range test.assertions {\n\t\t\t\ttraitAssertionFn(t, stdout, stderr, cmd.ProcessState.ExitCode())\n\t\t\t}\n\t\t\tif t.Failed() {\n\t\t\t\tt.Log(\"STDOUT:\\n\", stdout)\n\t\t\t\tt.Log(\"STDERR:\\n\", stderr)\n\t\t\t\tt.Log(\"COMMAND:\", strings.Join(cmd.Args, \" \"))\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "test/cli/db_validations_test.go",
    "content": "package cli\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/require\"\n\n\tv6 \"github.com/anchore/grype/grype/db/v6\"\n\t\"github.com/anchore/grype/internal/dbtest\"\n\t\"github.com/anchore/grype/internal/schemaver\"\n)\n\nfunc TestDBValidations(t *testing.T) {\n\tinvalidUpdateURL := fmt.Sprintf(\"https://localhost:%v\", availablePort())\n\texpiredDbURL := dbtest.NewServer(t).SetDBBuilt(time.Now().Add(-24*24*time.Hour)).SetDBVersion(6, 0, 0).Start() // 24 days old\n\tyesterdayDbURL := dbtest.NewServer(t).SetDBBuilt(time.Now().Add(-24*time.Hour)).SetDBVersion(6, 0, 0).Start()  // 24 hours old\n\ttodayDbURL := dbtest.NewServer(t).SetDBBuilt(time.Now()).SetDBVersion(6, 0, 0).Start()                         // just built\n\ttodayDbNewerVersionURL := dbtest.NewServer(t).SetDBBuilt(time.Now()).SetDBVersion(6, 0, 1).Start()             // just built\n\ttodayDbOlderVersionURL := dbtest.NewServer(t).SetDBBuilt(time.Now()).SetDBVersion(5, 9, 9).Start()             // just built\n\tnotFoundDbURL := dbtest.NewServer(t).SetDBBuilt(time.Now().Add(3 * time.Hour)).WithHandler(http.NotFound).Start()\n\n\t// common setup functions\n\ttype setupFunc = func(t *testing.T, dir string)\n\tsetup := func(funcs ...setupFunc) setupFunc {\n\t\treturn func(t *testing.T, dir string) {\n\t\t\tfor _, f := range funcs {\n\t\t\t\tf(t, dir)\n\t\t\t}\n\t\t}\n\t}\n\n\tsetupDb := func(url string) setupFunc {\n\t\treturn func(t *testing.T, dir string) {\n\t\t\tcmd, stdout, stderr := runGrype(t, map[string]string{\n\t\t\t\t\"GRYPE_DB_CACHE_DIR\":  dir,\n\t\t\t\t\"GRYPE_DB_UPDATE_URL\": url,\n\t\t\t}, \"db\", \"update\", \"-vvv\")\n\t\t\tassertInOutput(\"downloading new vulnerability DB\")(t, stdout, stderr, cmd.ProcessState.ExitCode())\n\t\t\tassertSucceedingReturnCode(t, stdout, stderr, cmd.ProcessState.ExitCode())\n\t\t}\n\t}\n\tsetupExpiredDb := setupDb(expiredDbURL)\n\tsetupYesterdayDb := setupDb(yesterdayDbURL)\n\tsetupTodayDb := setupDb(todayDbURL)\n\n\tdbFilePath := func(dir string) string {\n\t\treturn filepath.Join(dir, strconv.Itoa(v6.ModelVersion), \"vulnerability.db\")\n\t}\n\n\tcorruptDb := func(t *testing.T, dir string) {\n\t\terr := os.Truncate(dbFilePath(dir), 20)\n\t\trequire.NoError(t, err)\n\t}\n\n\tmoveDbToBackup := func(t *testing.T, dir string) {\n\t\terr := os.Rename(dbFilePath(dir), filepath.Join(dir, \"db.old\"))\n\t\trequire.NoError(t, err)\n\t}\n\n\trestoreDbFromBackup := func(t *testing.T, dir string) {\n\t\t// replace with valid db, which doesn't match the hash\n\t\terr := os.Rename(filepath.Join(dir, \"db.old\"), dbFilePath(dir))\n\t\trequire.NoError(t, err)\n\t}\n\n\tdeleteDb := func(t *testing.T, dir string) {\n\t\terr := os.Remove(dbFilePath(dir))\n\t\trequire.NoError(t, err)\n\t}\n\n\t// common asserts\n\tassertDbDownloaded := assertInOutput(\"downloading new vulnerability DB\")\n\tassertDbNotDownloaded := assertNotInOutput(\"downloading new vulnerability DB\")\n\tassertScanRan := assertInOutput(\"No vulnerabilities found\")\n\tassertDbLoadFailed := assertInOutput(\"failed to load vulnerability db\")\n\tassertDbLoadNotAtempted := assertNotInOutput(\"failed to load vulnerability db\")\n\tassertDbNotFound := assertInOutput(\"No installed DB version found\")\n\tassertCheckedForDbUpdate := assertInOutput(\"checking for available database updates\")\n\tassertDbHashed := assertInOutput(\"captured DB digest\")\n\tassertUpdateMessageDisplayed := assertInOutput(\"update to the latest db\")\n\tcmdAliases := map[string]string{\"scan\": \"pkg:no/thing@0\"} // scan: matching a purl with no vulnerabilities\n\n\t// ensure we have grype built and ready\n\trunGrype(t, map[string]string{}, \"config\")\n\n\ttests := []struct {\n\t\tname                      string    // the portion of the name before `:` is the command to run from cmdAliases above or the literal value\n\t\tsetup                     setupFunc // setup to run before test cmd\n\t\tdbUpdateURL               string    // update url to use, e.g. todayDbURL\n\t\tdbRequireUpdate           bool      // whether an update check is required\n\t\tdbMaxUpdateCheckFrequency string    // max update check frequency, defaults to 0 to always check\n\t\tdbValidateHash            bool      // whether to validate existing db by hash\n\t\tdbValidateAge             bool      // whether to validate existing db age\n\t\tdbCaCert                  string    // ca cert file, if set\n\t\tassertions                []traitAssertion\n\t}{\n\t\t{\n\t\t\tname:        \"scan: new install downloads successfully\",\n\t\t\tsetup:       nil,\n\t\t\tdbUpdateURL: yesterdayDbURL,\n\t\t\tassertions: []traitAssertion{\n\t\t\t\tassertDbDownloaded,\n\t\t\t\tassertScanRan,\n\t\t\t\tassertSucceedingReturnCode,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:        \"scan: existing db updates successfully\",\n\t\t\tsetup:       setupYesterdayDb,\n\t\t\tdbUpdateURL: todayDbURL,\n\t\t\tassertions: []traitAssertion{\n\t\t\t\tassertDbHashed,\n\t\t\t\tassertDbDownloaded,\n\t\t\t\tassertScanRan,\n\t\t\t\tassertSucceedingReturnCode,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:        \"scan: existing db skips update when same\",\n\t\t\tsetup:       setupYesterdayDb,\n\t\t\tdbUpdateURL: yesterdayDbURL,\n\t\t\tassertions: []traitAssertion{\n\t\t\t\tassertDbNotDownloaded,\n\t\t\t\tassertScanRan,\n\t\t\t\tassertSucceedingReturnCode,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:        \"scan: existing db skips update when newer\",\n\t\t\tsetup:       setupTodayDb,\n\t\t\tdbUpdateURL: yesterdayDbURL,\n\t\t\tassertions: []traitAssertion{\n\t\t\t\tassertDbNotDownloaded,\n\t\t\t\tassertScanRan,\n\t\t\t\tassertSucceedingReturnCode,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:        \"scan: continues on corrupt db no update\",\n\t\t\tsetup:       setup(setupYesterdayDb, corruptDb),\n\t\t\tdbUpdateURL: yesterdayDbURL,\n\t\t\tassertions: []traitAssertion{\n\t\t\t\tassertDbDownloaded,\n\t\t\t\tassertScanRan,\n\t\t\t\tassertSucceedingReturnCode,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:        \"db check: continues on corrupt db no update\",\n\t\t\tsetup:       setup(setupYesterdayDb, corruptDb),\n\t\t\tdbUpdateURL: yesterdayDbURL,\n\t\t\tassertions: []traitAssertion{\n\t\t\t\tassertDbNotFound,\n\t\t\t\tassertFailingReturnCode,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:        \"db check: continues on corrupt db with update\",\n\t\t\tsetup:       setup(setupYesterdayDb, corruptDb),\n\t\t\tdbUpdateURL: todayDbURL,\n\t\t\tassertions: []traitAssertion{\n\t\t\t\tassertDbNotFound,\n\t\t\t\tassertFailingReturnCode,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:        \"db status: fails with corrupt db no update\",\n\t\t\tsetup:       setup(setupYesterdayDb, corruptDb),\n\t\t\tdbUpdateURL: yesterdayDbURL,\n\t\t\tassertions: []traitAssertion{\n\t\t\t\tassertDbNotDownloaded,\n\t\t\t\tassertInOutput(\"failed to read DB description\"),\n\t\t\t\tassertFailingReturnCode,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:        \"db status: fails with corrupt db with update\",\n\t\t\tsetup:       setup(setupYesterdayDb, corruptDb),\n\t\t\tdbUpdateURL: todayDbURL,\n\t\t\tassertions: []traitAssertion{\n\t\t\t\tassertDbNotDownloaded,\n\t\t\t\tassertInOutput(\"failed to read DB description\"),\n\t\t\t\tassertFailingReturnCode,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:        \"scan: missing db downloads a new one\",\n\t\t\tsetup:       setup(setupYesterdayDb, deleteDb),\n\t\t\tdbUpdateURL: todayDbURL,\n\t\t\tassertions: []traitAssertion{\n\t\t\t\tassertDbDownloaded,\n\t\t\t\tassertScanRan,\n\t\t\t\tassertSucceedingReturnCode,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:        \"db check: missing db does not affect no update\",\n\t\t\tsetup:       setup(setupYesterdayDb, deleteDb),\n\t\t\tdbUpdateURL: yesterdayDbURL,\n\t\t\tassertions: []traitAssertion{\n\t\t\t\tassertDbNotFound,\n\t\t\t\tassertFailingReturnCode,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:        \"db check: missing db does not affect with update\",\n\t\t\tsetup:       setup(setupYesterdayDb, deleteDb),\n\t\t\tdbUpdateURL: todayDbURL,\n\t\t\tassertions: []traitAssertion{\n\t\t\t\tassertDbNotFound,\n\t\t\t\tassertFailingReturnCode,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:        \"db status: missing db returns error\",\n\t\t\tsetup:       setup(setupYesterdayDb, deleteDb),\n\t\t\tdbUpdateURL: todayDbURL,\n\t\t\tassertions: []traitAssertion{\n\t\t\t\tassertInOutput(\"database does not exist\"),\n\t\t\t\tassertFailingReturnCode,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:           \"db status: valid db fails with hash mismatch\",\n\t\t\tsetup:          setup(setupYesterdayDb, moveDbToBackup, setupTodayDb, deleteDb, restoreDbFromBackup),\n\t\t\tdbUpdateURL:    invalidUpdateURL,\n\t\t\tdbValidateHash: true,\n\t\t\tassertions: []traitAssertion{\n\t\t\t\tassertInOutput(\"bad db checksum\"),\n\t\t\t\tassertFailingReturnCode,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:           \"db check: valid db with hash mismatch\",\n\t\t\tsetup:          setup(setupYesterdayDb, moveDbToBackup, setupTodayDb, deleteDb, restoreDbFromBackup),\n\t\t\tdbUpdateURL:    invalidUpdateURL,\n\t\t\tdbValidateHash: true,\n\t\t\tassertions: []traitAssertion{\n\t\t\t\tassertDbLoadNotAtempted,\n\t\t\t\tassertFailingReturnCode,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:           \"scan: valid db fails with hash mismatch\",\n\t\t\tsetup:          setup(setupYesterdayDb, moveDbToBackup, setupTodayDb, deleteDb, restoreDbFromBackup),\n\t\t\tdbUpdateURL:    invalidUpdateURL,\n\t\t\tdbValidateHash: true,\n\t\t\tassertions: []traitAssertion{\n\t\t\t\tassertInOutput(\"bad db checksum\"),\n\t\t\t\tassertDbLoadFailed,\n\t\t\t\tassertDbNotDownloaded,\n\t\t\t\t// notification mentions grype db delete and grype db update\n\t\t\t\tassertInOutput(\"grype db delete\"),\n\t\t\t\tassertInOutput(\"grype db update\"),\n\t\t\t\tassertFailingReturnCode,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"scan: missing import.json\",\n\t\t\tsetup: setup(setupYesterdayDb, func(t *testing.T, dir string) {\n\t\t\t\trequire.NoError(t, os.Remove(filepath.Join(filepath.Dir(dbFilePath(dir)), \"import.json\")))\n\t\t\t}),\n\t\t\tdbUpdateURL:    invalidUpdateURL,\n\t\t\tdbValidateHash: true,\n\t\t\tassertions: []traitAssertion{\n\t\t\t\tassertInOutput(\"no import metadata\"),\n\t\t\t\tassertDbLoadFailed,\n\t\t\t\tassertDbNotDownloaded,\n\t\t\t\t// notification mentions grype db delete and grype db update\n\t\t\t\tassertInOutput(\"grype db delete\"),\n\t\t\t\tassertInOutput(\"grype db update\"),\n\t\t\t\tassertFailingReturnCode,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:            \"scan: update check error with valid db continues\",\n\t\t\tsetup:           setupYesterdayDb,\n\t\t\tdbUpdateURL:     notFoundDbURL,\n\t\t\tdbRequireUpdate: false,\n\t\t\tassertions: []traitAssertion{\n\t\t\t\tassertInOutput(\"error updating db\"),\n\t\t\t\tassertSucceedingReturnCode,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:            \"scan: update check error with valid db fails when require update\",\n\t\t\tsetup:           setupYesterdayDb,\n\t\t\tdbUpdateURL:     notFoundDbURL,\n\t\t\tdbRequireUpdate: true,\n\t\t\tassertions: []traitAssertion{\n\t\t\t\tassertInOutput(\"unable to update db\"),\n\t\t\t\tassertFailingReturnCode,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:            \"db check: update check error with valid db fails\",\n\t\t\tsetup:           setupYesterdayDb,\n\t\t\tdbUpdateURL:     notFoundDbURL,\n\t\t\tdbRequireUpdate: false,\n\t\t\tassertions: []traitAssertion{\n\t\t\t\tassertInOutput(\"unable to check for vulnerability database update\"),\n\t\t\t\tassertFailingReturnCode,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:          \"scan: database older than max age fails when unable to update\",\n\t\t\tsetup:         setupExpiredDb,\n\t\t\tdbUpdateURL:   notFoundDbURL,\n\t\t\tdbValidateAge: true,\n\t\t\tassertions: []traitAssertion{\n\t\t\t\tassertInOutput(\"the vulnerability database was built\"),\n\t\t\t\tassertFailingReturnCode,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:          \"scan: database older than max age succeeds with update\",\n\t\t\tsetup:         setupExpiredDb,\n\t\t\tdbUpdateURL:   todayDbURL,\n\t\t\tdbValidateAge: true,\n\t\t\tassertions: []traitAssertion{\n\t\t\t\tassertDbDownloaded,\n\t\t\t\tassertScanRan,\n\t\t\t\tassertSucceedingReturnCode,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:     \"scan: no panic on bad cert configuration\",\n\t\t\tdbCaCert: \"./does-not-exist.crt\",\n\t\t\tassertions: []traitAssertion{\n\t\t\t\tassertInOutput(\"failed to load vulnerability db\"),\n\t\t\t\tassertFailingReturnCode,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:                      \"db check: always check for updates regardless of frequency\",\n\t\t\tsetup:                     setupYesterdayDb,\n\t\t\tdbUpdateURL:               todayDbURL,\n\t\t\tdbMaxUpdateCheckFrequency: \"10h\",\n\t\t\tassertions: []traitAssertion{\n\t\t\t\tassertCheckedForDbUpdate,\n\t\t\t\tassertUpdateMessageDisplayed,\n\t\t\t\tfunc(tb testing.TB, stdout, stderr string, rc int) {\n\t\t\t\t\trequire.Equal(t, 100, rc)\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:                      \"db update: always update regardless of frequency\",\n\t\t\tsetup:                     setupYesterdayDb,\n\t\t\tdbUpdateURL:               todayDbURL,\n\t\t\tdbMaxUpdateCheckFrequency: \"10h\",\n\t\t\tassertions: []traitAssertion{\n\t\t\t\tassertCheckedForDbUpdate,\n\t\t\t\tassertDbDownloaded,\n\t\t\t\tassertSucceedingReturnCode,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:                      \"scan: ensure db update frequency config is respected\",\n\t\t\tsetup:                     setupYesterdayDb,\n\t\t\tdbUpdateURL:               todayDbURL,\n\t\t\tdbMaxUpdateCheckFrequency: \"10h\", // last check was during setup, much more recently than 10h ago\n\t\t\tassertions: []traitAssertion{\n\t\t\t\tassertNotInOutput(\"no max-frequency set for update check\"),\n\t\t\t\tassertNotInOutput(\"checking for available database updates\"),\n\t\t\t\tassertDbNotDownloaded,\n\t\t\t\tassertInOutput(\"max-update-check-frequency: 10h\"),\n\t\t\t\tassertSucceedingReturnCode,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"scan: ensure newer db version with older grype is not hydrated\",\n\t\t\tsetup: setup(setupDb(todayDbNewerVersionURL), func(t *testing.T, dir string) {\n\t\t\t\t// change the hydration version to a newer version that this grype\n\t\t\t\tmetaFile := filepath.Join(filepath.Dir(dbFilePath(dir)), v6.ImportMetadataFileName)\n\t\t\t\tcontents, err := os.ReadFile(metaFile)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tmeta := v6.ImportMetadata{}\n\t\t\t\terr = json.Unmarshal(contents, &meta)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tmeta.ClientVersion = schemaver.New(v6.ModelVersion, v6.Revision, v6.Addition+1).String()\n\t\t\t\tcontents, err = json.Marshal(meta)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\terr = os.WriteFile(metaFile, contents, 0x777)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}),\n\t\t\tdbUpdateURL: todayDbNewerVersionURL,\n\t\t\tassertions: []traitAssertion{\n\t\t\t\tassertInOutput(\"DB rehydration not needed\"),\n\t\t\t\tassertDbNotDownloaded,\n\t\t\t\tassertSucceedingReturnCode,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"scan: ensure older db version with newer db version hydrated\",\n\t\t\tsetup: setup(setupDb(todayDbNewerVersionURL), func(t *testing.T, dir string) {\n\t\t\t\t// change the hydration version to a older version that this grype\n\t\t\t\tmetaFile := filepath.Join(filepath.Dir(dbFilePath(dir)), v6.ImportMetadataFileName)\n\t\t\t\tcontents, err := os.ReadFile(metaFile)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tmeta := v6.ImportMetadata{}\n\t\t\t\terr = json.Unmarshal(contents, &meta)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tmeta.ClientVersion = schemaver.New(v6.ModelVersion-1, v6.Revision, v6.Addition).String()\n\t\t\t\tcontents, err = json.Marshal(meta)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\terr = os.WriteFile(metaFile, contents, 0x777)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}),\n\t\t\tdbUpdateURL: todayDbOlderVersionURL,\n\t\t\tassertions: []traitAssertion{\n\t\t\t\tassertInOutput(\"rehydrating DB\"),\n\t\t\t\tassertDbNotDownloaded,\n\t\t\t\tassertSucceedingReturnCode,\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\ttest := test\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tdbDir := t.TempDir()\n\t\t\tif test.setup != nil {\n\t\t\t\ttest.setup(t, dbDir)\n\t\t\t}\n\n\t\t\t// set up values\n\t\t\tenv := map[string]string{\n\t\t\t\t\"GRYPE_DB_CACHE_DIR\":                  dbDir,\n\t\t\t\t\"GRYPE_DB_UPDATE_URL\":                 defaultValue(test.dbUpdateURL, invalidUpdateURL),\n\t\t\t\t\"GRYPE_DB_VALIDATE_BY_HASH_ON_START\":  fmt.Sprintf(\"%v\", defaultValue(test.dbValidateHash, false)),\n\t\t\t\t\"GRYPE_DB_VALIDATE_AGE\":               fmt.Sprintf(\"%v\", defaultValue(test.dbValidateAge, false)),\n\t\t\t\t\"GRYPE_DB_MAX_UPDATE_CHECK_FREQUENCY\": defaultValue(test.dbMaxUpdateCheckFrequency, \"0\"),\n\t\t\t}\n\t\t\tif test.dbValidateAge {\n\t\t\t\tenv[\"GRYPE_DB_MAX_ALLOWED_BUILT_AGE\"] = \"48h\" // expired db is 24 days old\n\t\t\t}\n\t\t\tif test.dbCaCert != \"\" {\n\t\t\t\tenv[\"GRYPE_DB_CA_CERT\"] = test.dbCaCert\n\t\t\t}\n\t\t\tif test.dbRequireUpdate {\n\t\t\t\tenv[\"GRYPE_DB_REQUIRE_UPDATE_CHECK\"] = \"true\"\n\t\t\t}\n\n\t\t\t// test name before : is command args\n\t\t\targs := strings.Split(test.name, \":\")\n\t\t\targs = strings.Split(args[0], \" \")\n\t\t\tif cmd := cmdAliases[args[0]]; cmd != \"\" {\n\t\t\t\targs[0] = cmd\n\t\t\t}\n\t\t\tcmd, stdout, stderr := runGrype(t, env, append(args, \"-vvv\")...)\n\t\t\tfor _, traitAssertionFn := range test.assertions {\n\t\t\t\ttraitAssertionFn(t, stdout, stderr, cmd.ProcessState.ExitCode())\n\t\t\t}\n\t\t\tif t.Failed() {\n\t\t\t\tt.Log(\"STDOUT:\\n\", stdout)\n\t\t\t\tt.Log(\"STDERR:\\n\", stderr)\n\t\t\t\tt.Log(\"COMMAND:\", strings.Join(cmd.Args, \" \"))\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc defaultValue[T comparable](value T, defaultValue T) T {\n\tvar empty T\n\tif value == empty {\n\t\treturn defaultValue\n\t}\n\treturn value\n}\n\nfunc availablePort() int {\n\tif a, err := net.ResolveTCPAddr(\"tcp\", \"127.0.0.1:0\"); err == nil {\n\t\tvar l *net.TCPListener\n\t\tif l, err = net.ListenTCP(\"tcp\", a); err == nil {\n\t\t\tdefer func() { _ = l.Close() }()\n\t\t\treturn l.Addr().(*net.TCPAddr).Port\n\t\t}\n\t}\n\tpanic(\"unable to get port\")\n}\n"
  },
  {
    "path": "test/cli/registry_auth_test.go",
    "content": "package cli\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestRegistryAuth(t *testing.T) {\n\ttests := []struct {\n\t\tname       string\n\t\targs       []string\n\t\tenv        map[string]string\n\t\tassertions []traitAssertion\n\t}{\n\t\t{\n\t\t\tname: \"fallback to keychain\",\n\t\t\targs: []string{\"-vv\", \"registry:localhost:5000/something:latest\"},\n\t\t\tassertions: []traitAssertion{\n\t\t\t\tassertInOutput(\"from registry\"),\n\t\t\t\tassertInOutput(\"localhost:5000/something:latest\"),\n\t\t\t\tassertInOutput(`no registry credentials configured for \"localhost:5000\", using the default keychain`),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"use creds\",\n\t\t\targs: []string{\"-vv\", \"registry:localhost:5000/something:latest\"},\n\t\t\tenv: map[string]string{\n\t\t\t\t\"GRYPE_REGISTRY_AUTH_AUTHORITY\": \"localhost:5000\",\n\t\t\t\t\"GRYPE_REGISTRY_AUTH_USERNAME\":  \"username\",\n\t\t\t\t\"GRYPE_REGISTRY_AUTH_PASSWORD\":  \"password\",\n\t\t\t},\n\t\t\tassertions: []traitAssertion{\n\t\t\t\tassertInOutput(\"from registry\"),\n\t\t\t\tassertInOutput(\"localhost:5000/something:latest\"),\n\t\t\t\tassertInOutput(`using basic auth for registry \"localhost:5000\"`),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"use token\",\n\t\t\targs: []string{\"-vv\", \"registry:localhost:5000/something:latest\"},\n\t\t\tenv: map[string]string{\n\t\t\t\t\"GRYPE_REGISTRY_AUTH_AUTHORITY\": \"localhost:5000\",\n\t\t\t\t\"GRYPE_REGISTRY_AUTH_TOKEN\":     \"my-token\",\n\t\t\t},\n\t\t\tassertions: []traitAssertion{\n\t\t\t\tassertInOutput(\"from registry\"),\n\t\t\t\tassertInOutput(\"localhost:5000/something:latest\"),\n\t\t\t\tassertInOutput(`using token for registry \"localhost:5000\"`),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"not enough info fallsback to keychain\",\n\t\t\targs: []string{\"-vv\", \"registry:localhost:5000/something:latest\"},\n\t\t\tenv: map[string]string{\n\t\t\t\t\"GRYPE_REGISTRY_AUTH_AUTHORITY\": \"localhost:5000\",\n\t\t\t},\n\t\t\tassertions: []traitAssertion{\n\t\t\t\tassertInOutput(\"from registry\"),\n\t\t\t\tassertInOutput(\"localhost:5000/something:latest\"),\n\t\t\t\tassertInOutput(`no registry credentials configured for \"localhost:5000\", using the default keychain`),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"allows insecure http flag\",\n\t\t\targs: []string{\"-vv\", \"registry:localhost:5000/something:latest\"},\n\t\t\tenv: map[string]string{\n\t\t\t\t\"GRYPE_REGISTRY_INSECURE_USE_HTTP\": \"true\",\n\t\t\t},\n\t\t\tassertions: []traitAssertion{\n\t\t\t\tassertInOutput(\"insecure-use-http: true\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"use tls configuration\",\n\t\t\targs: []string{\"-vvv\", \"registry:localhost:5000/something:latest\"},\n\t\t\tenv: map[string]string{\n\t\t\t\t\"GRYPE_REGISTRY_AUTH_TLS_CERT\": \"place.crt\",\n\t\t\t\t\"GRYPE_REGISTRY_AUTH_TLS_KEY\":  \"place.key\",\n\t\t\t},\n\t\t\tassertions: []traitAssertion{\n\t\t\t\tassertInOutput(\"using custom TLS credentials from\"),\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tcmd, stdout, stderr := runGrype(t, test.env, test.args...)\n\t\t\tfor _, traitAssertionFn := range test.assertions {\n\t\t\t\ttraitAssertionFn(t, stdout, stderr, cmd.ProcessState.ExitCode())\n\t\t\t}\n\t\t\tif t.Failed() {\n\t\t\t\tt.Log(\"STDOUT:\\n\", stdout)\n\t\t\t\tt.Log(\"STDERR:\\n\", stderr)\n\t\t\t\tt.Log(\"COMMAND:\", strings.Join(cmd.Args, \" \"))\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestRegistryAuthRedactions(t *testing.T) {\n\ttmp := filepath.Join(t.TempDir(), \"output.json\")\n\n\tassertNotInFile := func(text string) traitAssertion {\n\t\treturn func(tb testing.TB, stdout, stderr string, rc int) {\n\t\t\tcontents, err := os.ReadFile(tmp)\n\t\t\trequire.NoError(tb, err)\n\t\t\trequire.NotEmpty(tb, contents)\n\t\t\trequire.NotContains(tb, string(contents), text)\n\t\t}\n\t}\n\n\ttests := []struct {\n\t\tname       string\n\t\targs       []string\n\t\tenv        map[string]string\n\t\tassertions []traitAssertion\n\t}{\n\t\t{\n\t\t\tname: \"use creds\",\n\t\t\targs: []string{\"-vv\", \"sbom:testdata/sbom-grype-source.json\", \"-o\", \"json\"},\n\t\t\tenv: map[string]string{\n\t\t\t\t\"GRYPE_REGISTRY_AUTH_USERNAME\": \"foobar-username\",\n\t\t\t\t\"GRYPE_REGISTRY_AUTH_PASSWORD\": \"foobar-password\",\n\t\t\t},\n\t\t\tassertions: []traitAssertion{\n\t\t\t\tassertSucceedingReturnCode,\n\t\t\t\tassertNotInOutput(\"foobar-username\"),\n\t\t\t\tassertNotInOutput(\"foobar-password\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"use token\",\n\t\t\targs: []string{\"-vv\", \"sbom:testdata/sbom-grype-source.json\", \"-o\", \"json\"},\n\t\t\tenv: map[string]string{\n\t\t\t\t\"GRYPE_REGISTRY_AUTH_TOKEN\": \"foobar-token\",\n\t\t\t},\n\t\t\tassertions: []traitAssertion{\n\t\t\t\tassertSucceedingReturnCode,\n\t\t\t\tassertNotInOutput(\"foobar-token\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"use creds file\",\n\t\t\targs: []string{\"-vv\", \"sbom:testdata/sbom-grype-source.json\", \"-o\", \"json\", \"--file\", tmp},\n\t\t\tenv: map[string]string{\n\t\t\t\t\"GRYPE_REGISTRY_AUTH_USERNAME\": \"foobar-username\",\n\t\t\t\t\"GRYPE_REGISTRY_AUTH_PASSWORD\": \"foobar-password\",\n\t\t\t},\n\t\t\tassertions: []traitAssertion{\n\t\t\t\tassertSucceedingReturnCode,\n\t\t\t\tassertNotInFile(\"foobar-username\"),\n\t\t\t\tassertNotInFile(\"foobar-password\"),\n\t\t\t\tassertNotInOutput(\"foobar-username\"),\n\t\t\t\tassertNotInOutput(\"foobar-password\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"use token file\",\n\t\t\targs: []string{\"-vv\", \"sbom:testdata/sbom-grype-source.json\", \"-o\", \"json\", \"--file\", tmp},\n\t\t\tenv: map[string]string{\n\t\t\t\t\"GRYPE_REGISTRY_AUTH_TOKEN\": \"foobar-token\",\n\t\t\t},\n\t\t\tassertions: []traitAssertion{\n\t\t\t\tassertSucceedingReturnCode,\n\t\t\t\tassertNotInFile(\"foobar-token\"),\n\t\t\t\tassertNotInOutput(\"foobar-token\"),\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\t_ = os.Remove(tmp) // ok to fail\n\t\t\tcmd, stdout, stderr := runGrype(t, test.env, test.args...)\n\t\t\tfor _, traitAssertionFn := range test.assertions {\n\t\t\t\ttraitAssertionFn(t, stdout, stderr, cmd.ProcessState.ExitCode())\n\t\t\t}\n\t\t\tif t.Failed() {\n\t\t\t\tfileContents, _ := os.ReadFile(tmp)\n\t\t\t\tt.Log(\"FILE:\\n\", string(fileContents))\n\t\t\t\tt.Log(\"STDOUT:\\n\", stdout)\n\t\t\t\tt.Log(\"STDERR:\\n\", stderr)\n\t\t\t\tt.Log(\"COMMAND:\", strings.Join(cmd.Args, \" \"))\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "test/cli/sbom_input_test.go",
    "content": "package cli\n\nimport (\n\t\"os\"\n\t\"os/exec\"\n\t\"path\"\n\t\"runtime\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestSBOMInput_AsArgument(t *testing.T) {\n\tworkingDirectory, err := os.Getwd()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tcases := []struct {\n\t\tname string\n\t\tpath string\n\t}{\n\t\t{\n\t\t\t\"absolute path - image scan\",\n\t\t\tpath.Join(workingDirectory, \"./testdata/sbom-ubuntu-20.04--pruned.json\"),\n\t\t},\n\t\t{\n\t\t\t\"relative path - image scan\",\n\t\t\t\"./testdata/sbom-ubuntu-20.04--pruned.json\",\n\t\t},\n\t\t{\n\t\t\t\"directory scan\",\n\t\t\t\"./testdata/sbom-grype-source.json\",\n\t\t},\n\t}\n\n\tt.Run(\"explicit\", func(t *testing.T) {\n\t\tfor _, tc := range cases {\n\t\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\t\tsbomArg := \"sbom:\" + tc.path\n\t\t\t\tcmd := getGrypeCommand(t, sbomArg)\n\n\t\t\t\tassertCommandExecutionSuccess(t, cmd)\n\t\t\t})\n\t\t}\n\t})\n\n\tt.Run(\"implicit\", func(t *testing.T) {\n\t\tfor _, tc := range cases {\n\t\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\t\tsbomArg := tc.path\n\t\t\t\tcmd := getGrypeCommand(t, sbomArg)\n\n\t\t\t\tassertCommandExecutionSuccess(t, cmd)\n\t\t\t})\n\t\t}\n\t})\n}\n\nfunc TestSBOMInput_FromStdin(t *testing.T) {\n\ttests := []struct {\n\t\tname       string\n\t\tinput      string\n\t\targs       []string\n\t\twantErr    require.ErrorAssertionFunc\n\t\twantOutput string\n\t}{\n\t\t{\n\t\t\tname:       \"empty file\",\n\t\t\tinput:      \"./testdata/empty.json\",\n\t\t\targs:       []string{\"-c\", \"../grype-test-config.yaml\"},\n\t\t\twantErr:    require.Error,\n\t\t\twantOutput: \"unable to decode sbom: sbom format not recognized\",\n\t\t},\n\t\t{\n\t\t\tname:    \"sbom\",\n\t\t\tinput:   \"./testdata/sbom-ubuntu-20.04--pruned.json\",\n\t\t\targs:    []string{\"-c\", \"../grype-test-config.yaml\"},\n\t\t\twantErr: require.NoError,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tcmd := exec.Command(getGrypeSnapshotLocation(t, runtime.GOOS), tt.args...)\n\n\t\t\tinput, err := os.Open(tt.input)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tattachFileToCommandStdin(t, input, cmd)\n\t\t\terr = input.Close()\n\t\t\trequire.NoError(t, err)\n\n\t\t\toutput, err := cmd.CombinedOutput()\n\t\t\ttt.wantErr(t, err, \"output: %s\", output)\n\t\t\tif tt.wantOutput != \"\" {\n\t\t\t\trequire.Contains(t, string(output), tt.wantOutput)\n\t\t\t}\n\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "test/cli/subprocess_test.go",
    "content": "package cli\n\nimport (\n\t\"fmt\"\n\t\"path\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/anchore/stereoscope/pkg/imagetest\"\n)\n\nfunc TestSubprocessStdin(t *testing.T) {\n\tbinDir := path.Dir(getGrypeSnapshotLocation(t, \"linux\"))\n\ttests := []struct {\n\t\tname       string\n\t\targs       []string\n\t\tenv        map[string]string\n\t\tassertions []traitAssertion\n\t}{\n\t\t{\n\t\t\t// regression\n\t\t\tname: \"ensure can be used by node subprocess (without hanging)\",\n\t\t\targs: []string{\"-v\", fmt.Sprintf(\"%s:%s:ro\", binDir, \"/app/bin\"), imagetest.LoadFixtureImageIntoDocker(t, \"image-node-subprocess\"), \"node\", \"/app.js\"},\n\t\t\tenv: map[string]string{\n\t\t\t\t\"GRYPE_CHECK_FOR_APP_UPDATE\": \"false\",\n\t\t\t},\n\t\t\tassertions: []traitAssertion{\n\t\t\t\tassertSucceedingReturnCode,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t// regression: https://github.com/anchore/grype/issues/267\n\t\t\tname: \"ensure can be used by java subprocess (without hanging)\",\n\t\t\targs: []string{\"-v\", fmt.Sprintf(\"%s:%s:ro\", binDir, \"/app/bin\"), imagetest.LoadFixtureImageIntoDocker(t, \"image-java-subprocess\"), \"java\", \"/app.java\"},\n\t\t\tenv: map[string]string{\n\t\t\t\t\"GRYPE_CHECK_FOR_APP_UPDATE\": \"false\",\n\t\t\t},\n\t\t\tassertions: []traitAssertion{\n\t\t\t\tassertSucceedingReturnCode,\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\ttestFn := func(t *testing.T) {\n\t\t\tcmd := getDockerRunCommand(t, test.args...)\n\t\t\tstdout, stderr := runCommand(cmd, test.env)\n\t\t\tfor _, traitAssertionFn := range test.assertions {\n\t\t\t\ttraitAssertionFn(t, stdout, stderr, cmd.ProcessState.ExitCode())\n\t\t\t}\n\t\t\tif t.Failed() {\n\t\t\t\tt.Log(\"STDOUT:\\n\", stdout)\n\t\t\t\tt.Log(\"STDERR:\\n\", stderr)\n\t\t\t\tt.Log(\"COMMAND:\", strings.Join(cmd.Args, \" \"))\n\t\t\t}\n\t\t}\n\n\t\ttestWithTimeout(t, test.name, 60*time.Second, testFn)\n\t}\n}\n"
  },
  {
    "path": "test/cli/testdata/Makefile",
    "content": "# change these if you want CI to not use previous stored cache\nCLI_CACHE_BUSTER := \"e5cdfd8\"\n\n.PHONY: cache.fingerprint\ncache.fingerprint:\n\tfind image-* -type f -exec md5sum {} + | awk '{print $1}' | sort | md5sum | tee cache.fingerprint && echo \"$(CLI_CACHE_BUSTER)\" >> cache.fingerprint\n"
  },
  {
    "path": "test/cli/testdata/another_cosign.pub",
    "content": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEFkdSiEHXuVIQMJWLeRbj+xuAIdzB\nYNPLm67ahl7GBcPYWirZfssklnwDldY8TLbK4igxT7YisGPMLGyiJsvvhg==\n-----END PUBLIC KEY-----\n"
  },
  {
    "path": "test/cli/testdata/configs/dir1/.grype.yaml",
    "content": "ignore:\n  - vulnerability: 'dir1-vuln1'\n  - vulnerability: 'dir1-vuln2'\n\nprofiles:\n  no-ignore:\n    ignore: []\n\n  alt-ignore:\n    ignore:\n      - vulnerability: 'dir1-alt-vuln1'\n      - vulnerability: 'dir1-alt-vuln2'\n"
  },
  {
    "path": "test/cli/testdata/configs/dir2/.grype.yaml",
    "content": "ignore:\n  - vulnerability: 'dir2-vuln1'\n  - vulnerability: 'dir2-vuln2'\n\nprofiles:\n  alt-ignore:\n    ignore:\n      - vulnerability: 'dir2-alt-vuln1'\n      - vulnerability: 'dir2-alt-vuln2'\n"
  },
  {
    "path": "test/cli/testdata/configs/dir3/.grype.yaml",
    "content": ""
  },
  {
    "path": "test/cli/testdata/cosign.pub",
    "content": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEFRk//8DKJlhLGay1c2sB5ApHblJB\nZXCNSffjHFH+f061ZuBTuFPQwsh/Hhypo8zj7X0VjdV4+t32neAWeYQBrg==\n-----END PUBLIC KEY-----\n"
  },
  {
    "path": "test/cli/testdata/cosign_broken.pub",
    "content": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEFRk//8DKJlhLGay1c2sB5ApHblJB\nZXCNSffjHFH+f061ZuBTuFPQwsh/Hhypo8zj7X0VjdV4+t32neAWeYQBrg=1\n-----END PUBLIC KEY-----\n"
  },
  {
    "path": "test/cli/testdata/empty.json",
    "content": ""
  },
  {
    "path": "test/cli/testdata/image-bare/Dockerfile",
    "content": "FROM scratch\nADD file-1.txt .\n"
  },
  {
    "path": "test/cli/testdata/image-bare/file-1.txt",
    "content": "this file has contents"
  },
  {
    "path": "test/cli/testdata/image-java-subprocess/Dockerfile",
    "content": "FROM openjdk:15-slim-buster@sha256:1e069bf1c5c23adde58b29b82281b862e473d698ce7cc4e164194a0a2a1c044a\nCOPY app.java /\nENV PATH=\"/app/bin:${PATH}\"\nWORKDIR /\n"
  },
  {
    "path": "test/cli/testdata/image-java-subprocess/app.java",
    "content": "import java.io.IOException;\n\npublic class GrypeExecutionTest {\n\n  public static void main(String[] args) {\n    try {\n      ProcessBuilder builder = new ProcessBuilder(\"grype\", \"registry:busybox:latest\", \"-vv\");\n\n      builder.inheritIO();\n      Process process = builder.start();\n\n      process.waitFor();\n\n    } catch (IOException | InterruptedException e) {\n      e.printStackTrace();\n    }\n  }\n}"
  },
  {
    "path": "test/cli/testdata/image-node-subprocess/Dockerfile",
    "content": "FROM node:16-stretch@sha256:5810de52349af302a2c5dddf0a3f31174ef65d0eed8985959a5e83bb1084b79b\nCOPY app.js /\nENV PATH=\"/app/bin:${PATH}\"\nWORKDIR /\n"
  },
  {
    "path": "test/cli/testdata/image-node-subprocess/app.js",
    "content": "require(\"child_process\").spawn(\"grype\", [\n    \"-vv\",\n    \"registry:busybox:latest\",\n], {\n  // we want to see any output from stdout/stderr which is why they are inherited from the parent process.\n  // The real test is to make certain that piped input will not hang forever when nothing is provided on stdin\n  // and there is input from the user to not use stdin. That is --make certain that we don't use \"stdin is a pipe\"\n  // as the only indicator to expect analysis input from stdin.\n  stdio: [\"pipe\", \"inherit\", \"inherit\"]\n});\n"
  },
  {
    "path": "test/cli/testdata/sbom-grype-source.json",
    "content": "{\n \"artifacts\": [\n  {\n   \"id\": \"bef1ce7f-cce6-4049-9da4-53882a612bb3\",\n   \"name\": \"Pygments\",\n   \"version\": \"2.6.1\",\n   \"type\": \"python\",\n   \"foundBy\": \"python-package-cataloger\",\n   \"locations\": [\n    {\n     \"path\": \"test/integration/testdata/image-debian-match-coverage/python/dist-info/METADATA\"\n    },\n    {\n     \"path\": \"test/integration/testdata/image-debian-match-coverage/python/dist-info/top_level.txt\"\n    }\n   ],\n   \"licenses\": [\n    \"BSD License\"\n   ],\n   \"language\": \"python\",\n   \"cpes\": [\n    \"cpe:2.3:a:python-Pygments:python-Pygments:2.6.1:*:*:*:*:*:*:*\",\n    \"cpe:2.3:a:python_Pygments:python-Pygments:2.6.1:*:*:*:*:*:*:*\",\n    \"cpe:2.3:a:python-Pygments:python_Pygments:2.6.1:*:*:*:*:*:*:*\",\n    \"cpe:2.3:a:python_Pygments:python_Pygments:2.6.1:*:*:*:*:*:*:*\",\n    \"cpe:2.3:a:georg_brandl:python_Pygments:2.6.1:*:*:*:*:*:*:*\",\n    \"cpe:2.3:a:georg_brandl:python-Pygments:2.6.1:*:*:*:*:*:*:*\",\n    \"cpe:2.3:a:Pygments:python_Pygments:2.6.1:*:*:*:*:*:*:*\",\n    \"cpe:2.3:a:Pygments:python-Pygments:2.6.1:*:*:*:*:*:*:*\",\n    \"cpe:2.3:a:python_Pygments:Pygments:2.6.1:*:*:*:*:*:*:*\",\n    \"cpe:2.3:a:python-Pygments:Pygments:2.6.1:*:*:*:*:*:*:*\",\n    \"cpe:2.3:a:python:python_Pygments:2.6.1:*:*:*:*:*:*:*\",\n    \"cpe:2.3:a:python:python-Pygments:2.6.1:*:*:*:*:*:*:*\",\n    \"cpe:2.3:a:georg_brandl:Pygments:2.6.1:*:*:*:*:*:*:*\",\n    \"cpe:2.3:a:georg:python-Pygments:2.6.1:*:*:*:*:*:*:*\",\n    \"cpe:2.3:a:georg:python_Pygments:2.6.1:*:*:*:*:*:*:*\",\n    \"cpe:2.3:a:Pygments:Pygments:2.6.1:*:*:*:*:*:*:*\",\n    \"cpe:2.3:a:python:Pygments:2.6.1:*:*:*:*:*:*:*\",\n    \"cpe:2.3:a:georg:Pygments:2.6.1:*:*:*:*:*:*:*\"\n   ],\n   \"purl\": \"pkg:pypi/Pygments@2.6.1\",\n   \"metadataType\": \"PythonPackageMetadata\",\n   \"metadata\": {\n    \"name\": \"Pygments\",\n    \"version\": \"2.6.1\",\n    \"license\": \"BSD License\",\n    \"author\": \"Georg Brandl\",\n    \"authorEmail\": \"georg@python.org\",\n    \"platform\": \"any\",\n    \"sitePackagesRootPath\": \"test/integration/testdata/image-debian-match-coverage/python\",\n    \"topLevelPackages\": [\n     \"pygments\"\n    ]\n   }\n  },\n  {\n   \"id\": \"651f06ac-d509-4b33-92f1-88bf006beaf7\",\n   \"name\": \"apt\",\n   \"version\": \"1.8.2\",\n   \"type\": \"deb\",\n   \"foundBy\": \"dpkgdb-cataloger\",\n   \"locations\": [\n    {\n     \"path\": \"test/integration/testdata/image-debian-match-coverage/var/lib/dpkg/status\"\n    }\n   ],\n   \"licenses\": [],\n   \"language\": \"\",\n   \"cpes\": [\n    \"cpe:2.3:a:apt:apt:1.8.2:*:*:*:*:*:*:*\"\n   ],\n   \"purl\": \"\",\n   \"metadataType\": \"DpkgMetadata\",\n   \"metadata\": {\n    \"package\": \"apt\",\n    \"source\": \"apt-dev\",\n    \"version\": \"1.8.2\",\n    \"sourceVersion\": \"\",\n    \"architecture\": \"amd64\",\n    \"maintainer\": \"APT Development Team <deity@lists.debian.org>\",\n    \"installedSize\": 4064,\n    \"files\": [\n     {\n      \"path\": \"/etc/apt/apt.conf.d/01autoremove\",\n      \"digest\": {\n       \"algorithm\": \"md5\",\n       \"value\": \"76120d358bc9037bb6358e737b3050b5\"\n      },\n      \"isConfigFile\": true\n     },\n     {\n      \"path\": \"/etc/cron.daily/apt-compat\",\n      \"digest\": {\n       \"algorithm\": \"md5\",\n       \"value\": \"49e9b2cfa17849700d4db735d04244f3\"\n      },\n      \"isConfigFile\": true\n     },\n     {\n      \"path\": \"/etc/kernel/postinst.d/apt-auto-removal\",\n      \"digest\": {\n       \"algorithm\": \"md5\",\n       \"value\": \"4ad976a68f045517cf4696cec7b8aa3a\"\n      },\n      \"isConfigFile\": true\n     },\n     {\n      \"path\": \"/etc/logrotate.d/apt\",\n      \"digest\": {\n       \"algorithm\": \"md5\",\n       \"value\": \"179f2ed4f85cbaca12fa3d69c2a4a1c3\"\n      },\n      \"isConfigFile\": true\n     }\n    ]\n   }\n  },\n  {\n   \"id\": \"e1d0474e-2a82-420c-ad82-a9de40d866c7\",\n   \"name\": \"dive\",\n   \"version\": \"0.9.2-1\",\n   \"type\": \"rpm\",\n   \"foundBy\": \"rpm-db-cataloger\",\n   \"locations\": [\n    {\n     \"path\": \"test/integration/testdata/image-sles-match-coverage/var/lib/rpm/Packages\"\n    }\n   ],\n   \"licenses\": [],\n   \"language\": \"\",\n   \"cpes\": [\n    \"cpe:2.3:a:dive:dive:0.9.2-1:*:*:*:*:*:*:*\"\n   ],\n   \"purl\": \"\",\n   \"metadataType\": \"RpmMetadata\",\n   \"metadata\": {\n    \"name\": \"dive\",\n    \"version\": \"0.9.2\",\n    \"epoch\": null,\n    \"architecture\": \"x86_64\",\n    \"release\": \"1\",\n    \"sourceRpm\": \"dive-0.9.2-1.src.rpm\",\n    \"size\": 12406784,\n    \"license\": \"MIT\",\n    \"vendor\": \"\",\n    \"files\": []\n   }\n  },\n  {\n   \"id\": \"4f86c82e-2e7a-4f61-97cc-2058938257be\",\n   \"name\": \"example-java-app-maven\",\n   \"version\": \"0.1.0\",\n   \"type\": \"java-archive\",\n   \"foundBy\": \"java-cataloger\",\n   \"locations\": [\n    {\n     \"path\": \"test/integration/testdata/image-debian-match-coverage/java/example-java-app-maven-0.1.0.jar\"\n    }\n   ],\n   \"licenses\": [],\n   \"language\": \"java\",\n   \"cpes\": [\n    \"cpe:2.3:a:example_java_app_maven:example-java-app-maven:0.1.0:*:*:*:*:*:*:*\",\n    \"cpe:2.3:a:example_java_app_maven:example_java_app_maven:0.1.0:*:*:*:*:*:*:*\",\n    \"cpe:2.3:a:example-java-app-maven:example_java_app_maven:0.1.0:*:*:*:*:*:*:*\",\n    \"cpe:2.3:a:example-java-app-maven:example-java-app-maven:0.1.0:*:*:*:*:*:*:*\",\n    \"cpe:2.3:a:example_java_app:example-java-app-maven:0.1.0:*:*:*:*:*:*:*\",\n    \"cpe:2.3:a:example_java_app:example_java_app_maven:0.1.0:*:*:*:*:*:*:*\",\n    \"cpe:2.3:a:example-java-app:example-java-app-maven:0.1.0:*:*:*:*:*:*:*\",\n    \"cpe:2.3:a:example-java-app:example_java_app_maven:0.1.0:*:*:*:*:*:*:*\",\n    \"cpe:2.3:a:example_java:example-java-app-maven:0.1.0:*:*:*:*:*:*:*\",\n    \"cpe:2.3:a:example-java:example-java-app-maven:0.1.0:*:*:*:*:*:*:*\",\n    \"cpe:2.3:a:example-java:example_java_app_maven:0.1.0:*:*:*:*:*:*:*\",\n    \"cpe:2.3:a:example_java:example_java_app_maven:0.1.0:*:*:*:*:*:*:*\",\n    \"cpe:2.3:a:example:example-java-app-maven:0.1.0:*:*:*:*:*:*:*\",\n    \"cpe:2.3:a:example:example_java_app_maven:0.1.0:*:*:*:*:*:*:*\",\n    \"cpe:2.3:a:anchore:example-java-app-maven:0.1.0:*:*:*:*:*:*:*\",\n    \"cpe:2.3:a:anchore:example_java_app_maven:0.1.0:*:*:*:*:*:*:*\"\n   ],\n   \"purl\": \"pkg:maven/org.anchore/example-java-app-maven@0.1.0\",\n   \"metadataType\": \"JavaMetadata\",\n   \"metadata\": {\n    \"virtualPath\": \"test/integration/testdata/image-debian-match-coverage/java/example-java-app-maven-0.1.0.jar\",\n    \"manifest\": {\n     \"main\": {\n      \"Archiver-Version\": \"Plexus Archiver\",\n      \"Build-Jdk\": \"14.0.1\",\n      \"Built-By\": \"?\",\n      \"Created-By\": \"Apache Maven 3.6.3\",\n      \"Main-Class\": \"hello.HelloWorld\",\n      \"Manifest-Version\": \"1.0\"\n     }\n    },\n    \"pomProperties\": {\n     \"path\": \"META-INF/maven/org.anchore/example-java-app-maven/pom.properties\",\n     \"name\": \"\",\n     \"groupId\": \"org.anchore\",\n     \"artifactId\": \"example-java-app-maven\",\n     \"version\": \"0.1.0\",\n     \"extraFields\": {}\n    }\n   }\n  },\n  {\n   \"id\": \"26f1dd60-f006-480f-8ad2-3c92a8d19f42\",\n   \"name\": \"github.com/acarl005/stripansi\",\n   \"version\": \"v0.0.0-20180116102854-5a71ef0e047d\",\n   \"type\": \"go-module\",\n   \"foundBy\": \"go-cataloger\",\n   \"locations\": [\n    {\n     \"path\": \"go.mod\"\n    }\n   ],\n   \"licenses\": [],\n   \"language\": \"go\",\n   \"cpes\": [\n    \"cpe:2.3:a:acarl005:stripansi:v0.0.0-20180116102854-5a71ef0e047d:*:*:*:*:*:*:*\"\n   ],\n   \"purl\": \"pkg:golang/github.com/acarl005/stripansi@v0.0.0-20180116102854-5a71ef0e047d\",\n   \"metadataType\": \"\",\n   \"metadata\": null\n  },\n  {\n   \"id\": \"b296ad62-b21d-487e-9914-6a5f5358f53a\",\n   \"name\": \"github.com/adrg/xdg\",\n   \"version\": \"v0.2.1\",\n   \"type\": \"go-module\",\n   \"foundBy\": \"go-cataloger\",\n   \"locations\": [\n    {\n     \"path\": \"go.mod\"\n    }\n   ],\n   \"licenses\": [],\n   \"language\": \"go\",\n   \"cpes\": [\n    \"cpe:2.3:a:adrg:xdg:v0.2.1:*:*:*:*:*:*:*\"\n   ],\n   \"purl\": \"pkg:golang/github.com/adrg/xdg@v0.2.1\",\n   \"metadataType\": \"\",\n   \"metadata\": null\n  },\n  {\n   \"id\": \"e1a7a167-c7c1-41b8-938e-3b17de0907fd\",\n   \"name\": \"github.com/anchore/go-testutils\",\n   \"version\": \"v0.0.0-20200925183923-d5f45b0d3c04\",\n   \"type\": \"go-module\",\n   \"foundBy\": \"go-cataloger\",\n   \"locations\": [\n    {\n     \"path\": \"go.mod\"\n    }\n   ],\n   \"licenses\": [],\n   \"language\": \"go\",\n   \"cpes\": [\n    \"cpe:2.3:a:anchore:go_testutils:v0.0.0-20200925183923-d5f45b0d3c04:*:*:*:*:*:*:*\",\n    \"cpe:2.3:a:anchore:go-testutils:v0.0.0-20200925183923-d5f45b0d3c04:*:*:*:*:*:*:*\"\n   ],\n   \"purl\": \"pkg:golang/github.com/anchore/go-testutils@v0.0.0-20200925183923-d5f45b0d3c04\",\n   \"metadataType\": \"\",\n   \"metadata\": null\n  },\n  {\n   \"id\": \"9ae8c8cb-0513-46c4-9a10-91720a0cf3c0\",\n   \"name\": \"github.com/anchore/go-version\",\n   \"version\": \"v1.2.2-0.20210903204242-51efa5b487c4\",\n   \"type\": \"go-module\",\n   \"foundBy\": \"go-cataloger\",\n   \"locations\": [\n    {\n     \"path\": \"go.mod\"\n    }\n   ],\n   \"licenses\": [],\n   \"language\": \"go\",\n   \"cpes\": [\n    \"cpe:2.3:a:anchore:go-version:v1.2.2-0.20210903204242-51efa5b487c4:*:*:*:*:*:*:*\",\n    \"cpe:2.3:a:anchore:go_version:v1.2.2-0.20210903204242-51efa5b487c4:*:*:*:*:*:*:*\"\n   ],\n   \"purl\": \"pkg:golang/github.com/anchore/go-version@v1.2.2-0.20210903204242-51efa5b487c4\",\n   \"metadataType\": \"\",\n   \"metadata\": null\n  },\n  {\n   \"id\": \"2268071f-cb45-4f99-82d3-87ac2ad0f1cc\",\n   \"name\": \"github.com/anchore/grype-db\",\n   \"version\": \"v0.0.0-20210928194208-f146397d6cd0\",\n   \"type\": \"go-module\",\n   \"foundBy\": \"go-cataloger\",\n   \"locations\": [\n    {\n     \"path\": \"go.mod\"\n    }\n   ],\n   \"licenses\": [],\n   \"language\": \"go\",\n   \"cpes\": [\n    \"cpe:2.3:a:anchore:grype-db:v0.0.0-20210928194208-f146397d6cd0:*:*:*:*:*:*:*\",\n    \"cpe:2.3:a:anchore:grype_db:v0.0.0-20210928194208-f146397d6cd0:*:*:*:*:*:*:*\"\n   ],\n   \"purl\": \"pkg:golang/github.com/anchore/grype-db@v0.0.0-20210928194208-f146397d6cd0\",\n   \"metadataType\": \"\",\n   \"metadata\": null\n  },\n  {\n   \"id\": \"65c50f92-183e-4ff5-ba28-15c7eb5838dc\",\n   \"name\": \"github.com/anchore/stereoscope\",\n   \"version\": \"v0.0.0-20210817160504-0f4abc2a5a5a\",\n   \"type\": \"go-module\",\n   \"foundBy\": \"go-cataloger\",\n   \"locations\": [\n    {\n     \"path\": \"go.mod\"\n    }\n   ],\n   \"licenses\": [],\n   \"language\": \"go\",\n   \"cpes\": [\n    \"cpe:2.3:a:anchore:stereoscope:v0.0.0-20210817160504-0f4abc2a5a5a:*:*:*:*:*:*:*\"\n   ],\n   \"purl\": \"pkg:golang/github.com/anchore/stereoscope@v0.0.0-20210817160504-0f4abc2a5a5a\",\n   \"metadataType\": \"\",\n   \"metadata\": null\n  },\n  {\n   \"id\": \"42c879dc-c9d8-4c08-afde-2623f0ce6749\",\n   \"name\": \"github.com/anchore/syft\",\n   \"version\": \"v0.24.1\",\n   \"type\": \"go-module\",\n   \"foundBy\": \"go-cataloger\",\n   \"locations\": [\n    {\n     \"path\": \"go.mod\"\n    }\n   ],\n   \"licenses\": [],\n   \"language\": \"go\",\n   \"cpes\": [\n    \"cpe:2.3:a:anchore:syft:v0.24.1:*:*:*:*:*:*:*\"\n   ],\n   \"purl\": \"pkg:golang/github.com/anchore/syft@v0.24.1\",\n   \"metadataType\": \"\",\n   \"metadata\": null\n  },\n  {\n   \"id\": \"70be86c2-9e8b-43b3-90d7-2f0e61100b54\",\n   \"name\": \"github.com/bmatcuk/doublestar/v2\",\n   \"version\": \"v2.0.4\",\n   \"type\": \"go-module\",\n   \"foundBy\": \"go-cataloger\",\n   \"locations\": [\n    {\n     \"path\": \"go.mod\"\n    }\n   ],\n   \"licenses\": [],\n   \"language\": \"go\",\n   \"cpes\": [\n    \"cpe:2.3:a:bmatcuk:doublestar:v2.0.4:*:*:*:*:*:*:*\"\n   ],\n   \"purl\": \"pkg:golang/github.com/bmatcuk/doublestar/v2@v2.0.4\",\n   \"metadataType\": \"\",\n   \"metadata\": null\n  },\n  {\n   \"id\": \"814fe4dd-afad-4db8-bb2a-09d9d8fccbb1\",\n   \"name\": \"github.com/docker/docker\",\n   \"version\": \"v17.12.0-ce-rc1.0.20200309214505-aa6a9891b09c+incompatible\",\n   \"type\": \"go-module\",\n   \"foundBy\": \"go-cataloger\",\n   \"locations\": [\n    {\n     \"path\": \"go.mod\"\n    }\n   ],\n   \"licenses\": [],\n   \"language\": \"go\",\n   \"cpes\": [\n    \"cpe:2.3:a:docker:docker:v17.12.0-ce-rc1.0.20200309214505-aa6a9891b09c+incompatible:*:*:*:*:*:*:*\"\n   ],\n   \"purl\": \"pkg:golang/github.com/docker/docker@v17.12.0-ce-rc1.0.20200309214505-aa6a9891b09c+incompatible\",\n   \"metadataType\": \"\",\n   \"metadata\": null\n  },\n  {\n   \"id\": \"8fda3ee5-90c5-4fbc-9b74-ac53f75e95a7\",\n   \"name\": \"github.com/dustin/go-humanize\",\n   \"version\": \"v1.0.0\",\n   \"type\": \"go-module\",\n   \"foundBy\": \"go-cataloger\",\n   \"locations\": [\n    {\n     \"path\": \"go.mod\"\n    }\n   ],\n   \"licenses\": [],\n   \"language\": \"go\",\n   \"cpes\": [\n    \"cpe:2.3:a:dustin:go-humanize:v1.0.0:*:*:*:*:*:*:*\",\n    \"cpe:2.3:a:dustin:go_humanize:v1.0.0:*:*:*:*:*:*:*\"\n   ],\n   \"purl\": \"pkg:golang/github.com/dustin/go-humanize@v1.0.0\",\n   \"metadataType\": \"\",\n   \"metadata\": null\n  },\n  {\n   \"id\": \"8b49fe37-0618-4dad-8379-692443ddc5e8\",\n   \"name\": \"github.com/facebookincubator/nvdtools\",\n   \"version\": \"v0.1.4\",\n   \"type\": \"go-module\",\n   \"foundBy\": \"go-cataloger\",\n   \"locations\": [\n    {\n     \"path\": \"go.mod\"\n    }\n   ],\n   \"licenses\": [],\n   \"language\": \"go\",\n   \"cpes\": [\n    \"cpe:2.3:a:facebookincubator:nvdtools:v0.1.4:*:*:*:*:*:*:*\"\n   ],\n   \"purl\": \"pkg:golang/github.com/facebookincubator/nvdtools@v0.1.4\",\n   \"metadataType\": \"\",\n   \"metadata\": null\n  },\n  {\n   \"id\": \"04e5343e-1023-48a1-9423-79972c4e4214\",\n   \"name\": \"github.com/go-test/deep\",\n   \"version\": \"v1.0.7\",\n   \"type\": \"go-module\",\n   \"foundBy\": \"go-cataloger\",\n   \"locations\": [\n    {\n     \"path\": \"go.mod\"\n    }\n   ],\n   \"licenses\": [],\n   \"language\": \"go\",\n   \"cpes\": [\n    \"cpe:2.3:a:go-test:deep:v1.0.7:*:*:*:*:*:*:*\",\n    \"cpe:2.3:a:go_test:deep:v1.0.7:*:*:*:*:*:*:*\",\n    \"cpe:2.3:a:go:deep:v1.0.7:*:*:*:*:*:*:*\"\n   ],\n   \"purl\": \"pkg:golang/github.com/go-test/deep@v1.0.7\",\n   \"metadataType\": \"\",\n   \"metadata\": null\n  },\n  {\n   \"id\": \"1ad992ea-1296-47e8-a468-4eb0c362a7e3\",\n   \"name\": \"github.com/google/go-cmp\",\n   \"version\": \"v0.4.1\",\n   \"type\": \"go-module\",\n   \"foundBy\": \"go-cataloger\",\n   \"locations\": [\n    {\n     \"path\": \"go.mod\"\n    }\n   ],\n   \"licenses\": [],\n   \"language\": \"go\",\n   \"cpes\": [\n    \"cpe:2.3:a:google:go-cmp:v0.4.1:*:*:*:*:*:*:*\",\n    \"cpe:2.3:a:google:go_cmp:v0.4.1:*:*:*:*:*:*:*\"\n   ],\n   \"purl\": \"pkg:golang/github.com/google/go-cmp@v0.4.1\",\n   \"metadataType\": \"\",\n   \"metadata\": null\n  },\n  {\n   \"id\": \"6d6cff34-9f29-450f-8cb8-ffe70775c1dd\",\n   \"name\": \"github.com/google/uuid\",\n   \"version\": \"v1.1.1\",\n   \"type\": \"go-module\",\n   \"foundBy\": \"go-cataloger\",\n   \"locations\": [\n    {\n     \"path\": \"go.mod\"\n    }\n   ],\n   \"licenses\": [],\n   \"language\": \"go\",\n   \"cpes\": [\n    \"cpe:2.3:a:google:uuid:v1.1.1:*:*:*:*:*:*:*\"\n   ],\n   \"purl\": \"pkg:golang/github.com/google/uuid@v1.1.1\",\n   \"metadataType\": \"\",\n   \"metadata\": null\n  },\n  {\n   \"id\": \"b4202bbd-9fb5-4d6f-83bd-83a4aa6d1608\",\n   \"name\": \"github.com/gookit/color\",\n   \"version\": \"v1.4.2\",\n   \"type\": \"go-module\",\n   \"foundBy\": \"go-cataloger\",\n   \"locations\": [\n    {\n     \"path\": \"go.mod\"\n    }\n   ],\n   \"licenses\": [],\n   \"language\": \"go\",\n   \"cpes\": [\n    \"cpe:2.3:a:gookit:color:v1.4.2:*:*:*:*:*:*:*\"\n   ],\n   \"purl\": \"pkg:golang/github.com/gookit/color@v1.4.2\",\n   \"metadataType\": \"\",\n   \"metadata\": null\n  },\n  {\n   \"id\": \"4ef230a8-22ef-4a45-92bb-fda1a17785bd\",\n   \"name\": \"github.com/hashicorp/go-getter\",\n   \"version\": \"v1.4.1\",\n   \"type\": \"go-module\",\n   \"foundBy\": \"go-cataloger\",\n   \"locations\": [\n    {\n     \"path\": \"go.mod\"\n    }\n   ],\n   \"licenses\": [],\n   \"language\": \"go\",\n   \"cpes\": [\n    \"cpe:2.3:a:hashicorp:go_getter:v1.4.1:*:*:*:*:*:*:*\",\n    \"cpe:2.3:a:hashicorp:go-getter:v1.4.1:*:*:*:*:*:*:*\"\n   ],\n   \"purl\": \"pkg:golang/github.com/hashicorp/go-getter@v1.4.1\",\n   \"metadataType\": \"\",\n   \"metadata\": null\n  },\n  {\n   \"id\": \"53de4a0f-f65c-4df4-90e7-9274471ae45e\",\n   \"name\": \"github.com/hashicorp/go-multierror\",\n   \"version\": \"v1.1.0\",\n   \"type\": \"go-module\",\n   \"foundBy\": \"go-cataloger\",\n   \"locations\": [\n    {\n     \"path\": \"go.mod\"\n    }\n   ],\n   \"licenses\": [],\n   \"language\": \"go\",\n   \"cpes\": [\n    \"cpe:2.3:a:hashicorp:go-multierror:v1.1.0:*:*:*:*:*:*:*\",\n    \"cpe:2.3:a:hashicorp:go_multierror:v1.1.0:*:*:*:*:*:*:*\"\n   ],\n   \"purl\": \"pkg:golang/github.com/hashicorp/go-multierror@v1.1.0\",\n   \"metadataType\": \"\",\n   \"metadata\": null\n  },\n  {\n   \"id\": \"3eacac5b-f362-4d47-a509-3dfaa0f65bde\",\n   \"name\": \"github.com/jinzhu/copier\",\n   \"version\": \"v0.0.0-20190924061706-b57f9002281a\",\n   \"type\": \"go-module\",\n   \"foundBy\": \"go-cataloger\",\n   \"locations\": [\n    {\n     \"path\": \"go.mod\"\n    }\n   ],\n   \"licenses\": [],\n   \"language\": \"go\",\n   \"cpes\": [\n    \"cpe:2.3:a:jinzhu:copier:v0.0.0-20190924061706-b57f9002281a:*:*:*:*:*:*:*\"\n   ],\n   \"purl\": \"pkg:golang/github.com/jinzhu/copier@v0.0.0-20190924061706-b57f9002281a\",\n   \"metadataType\": \"\",\n   \"metadata\": null\n  },\n  {\n   \"id\": \"ee1ac005-9c0d-45f4-a9b8-de9c805c8540\",\n   \"name\": \"github.com/knqyf263/go-deb-version\",\n   \"version\": \"v0.0.0-20190517075300-09fca494f03d\",\n   \"type\": \"go-module\",\n   \"foundBy\": \"go-cataloger\",\n   \"locations\": [\n    {\n     \"path\": \"go.mod\"\n    }\n   ],\n   \"licenses\": [],\n   \"language\": \"go\",\n   \"cpes\": [\n    \"cpe:2.3:a:knqyf263:go-deb-version:v0.0.0-20190517075300-09fca494f03d:*:*:*:*:*:*:*\",\n    \"cpe:2.3:a:knqyf263:go_deb_version:v0.0.0-20190517075300-09fca494f03d:*:*:*:*:*:*:*\"\n   ],\n   \"purl\": \"pkg:golang/github.com/knqyf263/go-deb-version@v0.0.0-20190517075300-09fca494f03d\",\n   \"metadataType\": \"\",\n   \"metadata\": null\n  },\n  {\n   \"id\": \"71970349-8c0d-41d9-b3f9-7dd5b50153ce\",\n   \"name\": \"github.com/mitchellh/go-homedir\",\n   \"version\": \"v1.1.0\",\n   \"type\": \"go-module\",\n   \"foundBy\": \"go-cataloger\",\n   \"locations\": [\n    {\n     \"path\": \"go.mod\"\n    }\n   ],\n   \"licenses\": [],\n   \"language\": \"go\",\n   \"cpes\": [\n    \"cpe:2.3:a:mitchellh:go-homedir:v1.1.0:*:*:*:*:*:*:*\",\n    \"cpe:2.3:a:mitchellh:go_homedir:v1.1.0:*:*:*:*:*:*:*\"\n   ],\n   \"purl\": \"pkg:golang/github.com/mitchellh/go-homedir@v1.1.0\",\n   \"metadataType\": \"\",\n   \"metadata\": null\n  },\n  {\n   \"id\": \"fdf5f063-2178-454b-8e84-1412a65ee968\",\n   \"name\": \"github.com/olekukonko/tablewriter\",\n   \"version\": \"v0.0.4\",\n   \"type\": \"go-module\",\n   \"foundBy\": \"go-cataloger\",\n   \"locations\": [\n    {\n     \"path\": \"go.mod\"\n    }\n   ],\n   \"licenses\": [],\n   \"language\": \"go\",\n   \"cpes\": [\n    \"cpe:2.3:a:olekukonko:tablewriter:v0.0.4:*:*:*:*:*:*:*\"\n   ],\n   \"purl\": \"pkg:golang/github.com/olekukonko/tablewriter@v0.0.4\",\n   \"metadataType\": \"\",\n   \"metadata\": null\n  },\n  {\n   \"id\": \"ed4372e1-d33e-4d18-9b5c-5c4b59e80662\",\n   \"name\": \"github.com/pkg/profile\",\n   \"version\": \"v1.6.0\",\n   \"type\": \"go-module\",\n   \"foundBy\": \"go-cataloger\",\n   \"locations\": [\n    {\n     \"path\": \"go.mod\"\n    }\n   ],\n   \"licenses\": [],\n   \"language\": \"go\",\n   \"cpes\": [\n    \"cpe:2.3:a:pkg:profile:v1.6.0:*:*:*:*:*:*:*\"\n   ],\n   \"purl\": \"pkg:golang/github.com/pkg/profile@v1.6.0\",\n   \"metadataType\": \"\",\n   \"metadata\": null\n  },\n  {\n   \"id\": \"8870327f-8eef-4e2d-af6e-3b57e11d50a7\",\n   \"name\": \"github.com/scylladb/go-set\",\n   \"version\": \"v1.0.2\",\n   \"type\": \"go-module\",\n   \"foundBy\": \"go-cataloger\",\n   \"locations\": [\n    {\n     \"path\": \"go.mod\"\n    }\n   ],\n   \"licenses\": [],\n   \"language\": \"go\",\n   \"cpes\": [\n    \"cpe:2.3:a:scylladb:go-set:v1.0.2:*:*:*:*:*:*:*\",\n    \"cpe:2.3:a:scylladb:go_set:v1.0.2:*:*:*:*:*:*:*\"\n   ],\n   \"purl\": \"pkg:golang/github.com/scylladb/go-set@v1.0.2\",\n   \"metadataType\": \"\",\n   \"metadata\": null\n  },\n  {\n   \"id\": \"b1a92a31-3aaf-45ec-9876-37bd1de81f8d\",\n   \"name\": \"github.com/sergi/go-diff\",\n   \"version\": \"v1.1.0\",\n   \"type\": \"go-module\",\n   \"foundBy\": \"go-cataloger\",\n   \"locations\": [\n    {\n     \"path\": \"go.mod\"\n    }\n   ],\n   \"licenses\": [],\n   \"language\": \"go\",\n   \"cpes\": [\n    \"cpe:2.3:a:sergi:go_diff:v1.1.0:*:*:*:*:*:*:*\",\n    \"cpe:2.3:a:sergi:go-diff:v1.1.0:*:*:*:*:*:*:*\"\n   ],\n   \"purl\": \"pkg:golang/github.com/sergi/go-diff@v1.1.0\",\n   \"metadataType\": \"\",\n   \"metadata\": null\n  },\n  {\n   \"id\": \"432db78d-4f4e-4df3-a1d9-22a19a5cecd3\",\n   \"name\": \"github.com/sirupsen/logrus\",\n   \"version\": \"v1.6.0\",\n   \"type\": \"go-module\",\n   \"foundBy\": \"go-cataloger\",\n   \"locations\": [\n    {\n     \"path\": \"go.mod\"\n    }\n   ],\n   \"licenses\": [],\n   \"language\": \"go\",\n   \"cpes\": [\n    \"cpe:2.3:a:sirupsen:logrus:v1.6.0:*:*:*:*:*:*:*\"\n   ],\n   \"purl\": \"pkg:golang/github.com/sirupsen/logrus@v1.6.0\",\n   \"metadataType\": \"\",\n   \"metadata\": null\n  },\n  {\n   \"id\": \"6cbdf0e8-95d6-4be6-b6db-ae1aed8495b3\",\n   \"name\": \"github.com/spf13/afero\",\n   \"version\": \"v1.3.2\",\n   \"type\": \"go-module\",\n   \"foundBy\": \"go-cataloger\",\n   \"locations\": [\n    {\n     \"path\": \"go.mod\"\n    }\n   ],\n   \"licenses\": [],\n   \"language\": \"go\",\n   \"cpes\": [\n    \"cpe:2.3:a:spf13:afero:v1.3.2:*:*:*:*:*:*:*\"\n   ],\n   \"purl\": \"pkg:golang/github.com/spf13/afero@v1.3.2\",\n   \"metadataType\": \"\",\n   \"metadata\": null\n  },\n  {\n   \"id\": \"999d42fa-0df6-4fb3-b12a-4943ce613034\",\n   \"name\": \"github.com/spf13/cobra\",\n   \"version\": \"v1.0.1-0.20200909172742-8a63648dd905\",\n   \"type\": \"go-module\",\n   \"foundBy\": \"go-cataloger\",\n   \"locations\": [\n    {\n     \"path\": \"go.mod\"\n    }\n   ],\n   \"licenses\": [],\n   \"language\": \"go\",\n   \"cpes\": [\n    \"cpe:2.3:a:spf13:cobra:v1.0.1-0.20200909172742-8a63648dd905:*:*:*:*:*:*:*\"\n   ],\n   \"purl\": \"pkg:golang/github.com/spf13/cobra@v1.0.1-0.20200909172742-8a63648dd905\",\n   \"metadataType\": \"\",\n   \"metadata\": null\n  },\n  {\n   \"id\": \"dccd69f3-390f-4480-a645-262e0d01452f\",\n   \"name\": \"github.com/spf13/pflag\",\n   \"version\": \"v1.0.5\",\n   \"type\": \"go-module\",\n   \"foundBy\": \"go-cataloger\",\n   \"locations\": [\n    {\n     \"path\": \"go.mod\"\n    }\n   ],\n   \"licenses\": [],\n   \"language\": \"go\",\n   \"cpes\": [\n    \"cpe:2.3:a:spf13:pflag:v1.0.5:*:*:*:*:*:*:*\"\n   ],\n   \"purl\": \"pkg:golang/github.com/spf13/pflag@v1.0.5\",\n   \"metadataType\": \"\",\n   \"metadata\": null\n  },\n  {\n   \"id\": \"ca7a9196-0bc3-4044-a09f-03b90208d4ca\",\n   \"name\": \"github.com/spf13/viper\",\n   \"version\": \"v1.7.0\",\n   \"type\": \"go-module\",\n   \"foundBy\": \"go-cataloger\",\n   \"locations\": [\n    {\n     \"path\": \"go.mod\"\n    }\n   ],\n   \"licenses\": [],\n   \"language\": \"go\",\n   \"cpes\": [\n    \"cpe:2.3:a:spf13:viper:v1.7.0:*:*:*:*:*:*:*\"\n   ],\n   \"purl\": \"pkg:golang/github.com/spf13/viper@v1.7.0\",\n   \"metadataType\": \"\",\n   \"metadata\": null\n  },\n  {\n   \"id\": \"203ee2fd-d494-4bd3-b563-edf0a217a625\",\n   \"name\": \"github.com/stretchr/testify\",\n   \"version\": \"v1.7.0\",\n   \"type\": \"go-module\",\n   \"foundBy\": \"go-cataloger\",\n   \"locations\": [\n    {\n     \"path\": \"go.mod\"\n    }\n   ],\n   \"licenses\": [],\n   \"language\": \"go\",\n   \"cpes\": [\n    \"cpe:2.3:a:stretchr:testify:v1.7.0:*:*:*:*:*:*:*\"\n   ],\n   \"purl\": \"pkg:golang/github.com/stretchr/testify@v1.7.0\",\n   \"metadataType\": \"\",\n   \"metadata\": null\n  },\n  {\n   \"id\": \"a9b26b3c-b7fa-4896-a1d3-4d57b237e0e2\",\n   \"name\": \"github.com/wagoodman/go-partybus\",\n   \"version\": \"v0.0.0-20210627031916-db1f5573bbc5\",\n   \"type\": \"go-module\",\n   \"foundBy\": \"go-cataloger\",\n   \"locations\": [\n    {\n     \"path\": \"go.mod\"\n    }\n   ],\n   \"licenses\": [],\n   \"language\": \"go\",\n   \"cpes\": [\n    \"cpe:2.3:a:wagoodman:go-partybus:v0.0.0-20210627031916-db1f5573bbc5:*:*:*:*:*:*:*\",\n    \"cpe:2.3:a:wagoodman:go_partybus:v0.0.0-20210627031916-db1f5573bbc5:*:*:*:*:*:*:*\"\n   ],\n   \"purl\": \"pkg:golang/github.com/wagoodman/go-partybus@v0.0.0-20210627031916-db1f5573bbc5\",\n   \"metadataType\": \"\",\n   \"metadata\": null\n  },\n  {\n   \"id\": \"0fc965b2-dae1-48c2-b444-aa06c0444e3a\",\n   \"name\": \"github.com/wagoodman/go-progress\",\n   \"version\": \"v0.0.0-20200807221327-51d465df1451\",\n   \"type\": \"go-module\",\n   \"foundBy\": \"go-cataloger\",\n   \"locations\": [\n    {\n     \"path\": \"go.mod\"\n    }\n   ],\n   \"licenses\": [],\n   \"language\": \"go\",\n   \"cpes\": [\n    \"cpe:2.3:a:wagoodman:go-progress:v0.0.0-20200807221327-51d465df1451:*:*:*:*:*:*:*\",\n    \"cpe:2.3:a:wagoodman:go_progress:v0.0.0-20200807221327-51d465df1451:*:*:*:*:*:*:*\"\n   ],\n   \"purl\": \"pkg:golang/github.com/wagoodman/go-progress@v0.0.0-20200807221327-51d465df1451\",\n   \"metadataType\": \"\",\n   \"metadata\": null\n  },\n  {\n   \"id\": \"7d006c48-72c5-442d-b485-c69eae9c2bd8\",\n   \"name\": \"github.com/wagoodman/jotframe\",\n   \"version\": \"v0.0.0-20200730190914-3517092dd163\",\n   \"type\": \"go-module\",\n   \"foundBy\": \"go-cataloger\",\n   \"locations\": [\n    {\n     \"path\": \"go.mod\"\n    }\n   ],\n   \"licenses\": [],\n   \"language\": \"go\",\n   \"cpes\": [\n    \"cpe:2.3:a:wagoodman:jotframe:v0.0.0-20200730190914-3517092dd163:*:*:*:*:*:*:*\"\n   ],\n   \"purl\": \"pkg:golang/github.com/wagoodman/jotframe@v0.0.0-20200730190914-3517092dd163\",\n   \"metadataType\": \"\",\n   \"metadata\": null\n  },\n  {\n   \"id\": \"179ada5b-cac6-49fe-a786-865874ab04ca\",\n   \"name\": \"github.com/x-cray/logrus-prefixed-formatter\",\n   \"version\": \"v0.5.2\",\n   \"type\": \"go-module\",\n   \"foundBy\": \"go-cataloger\",\n   \"locations\": [\n    {\n     \"path\": \"go.mod\"\n    }\n   ],\n   \"licenses\": [],\n   \"language\": \"go\",\n   \"cpes\": [\n    \"cpe:2.3:a:x_cray:logrus_prefixed_formatter:v0.5.2:*:*:*:*:*:*:*\",\n    \"cpe:2.3:a:x-cray:logrus_prefixed_formatter:v0.5.2:*:*:*:*:*:*:*\",\n    \"cpe:2.3:a:x_cray:logrus-prefixed-formatter:v0.5.2:*:*:*:*:*:*:*\",\n    \"cpe:2.3:a:x-cray:logrus-prefixed-formatter:v0.5.2:*:*:*:*:*:*:*\",\n    \"cpe:2.3:a:x:logrus_prefixed_formatter:v0.5.2:*:*:*:*:*:*:*\",\n    \"cpe:2.3:a:x:logrus-prefixed-formatter:v0.5.2:*:*:*:*:*:*:*\"\n   ],\n   \"purl\": \"pkg:golang/github.com/x-cray/logrus-prefixed-formatter@v0.5.2\",\n   \"metadataType\": \"\",\n   \"metadata\": null\n  },\n  {\n   \"id\": \"278908c5-bf45-4a37-9ef5-6ada37b2935a\",\n   \"name\": \"golang.org/x/crypto\",\n   \"version\": \"v0.0.0-20200622213623-75b288015ac9\",\n   \"type\": \"go-module\",\n   \"foundBy\": \"go-cataloger\",\n   \"locations\": [\n    {\n     \"path\": \"go.mod\"\n    }\n   ],\n   \"licenses\": [],\n   \"language\": \"go\",\n   \"cpes\": [\n    \"cpe:2.3:a:golang:x/crypto:v0.0.0-20200622213623-75b288015ac9:*:*:*:*:*:*:*\"\n   ],\n   \"purl\": \"pkg:golang/golang.org/x/crypto@v0.0.0-20200622213623-75b288015ac9\",\n   \"metadataType\": \"\",\n   \"metadata\": null\n  },\n  {\n   \"id\": \"ccd900cd-78b1-4377-8265-2b1f2dff34a6\",\n   \"name\": \"gopkg.in/yaml.v2\",\n   \"version\": \"v2.3.0\",\n   \"type\": \"go-module\",\n   \"foundBy\": \"go-cataloger\",\n   \"locations\": [\n    {\n     \"path\": \"go.mod\"\n    }\n   ],\n   \"licenses\": [],\n   \"language\": \"go\",\n   \"cpes\": [],\n   \"purl\": \"pkg:golang/gopkg.in/yaml.v2@v2.3.0\",\n   \"metadataType\": \"\",\n   \"metadata\": null\n  },\n  {\n   \"id\": \"5582f267-e031-4687-99dc-3ee0d57cb724\",\n   \"name\": \"joda-time\",\n   \"version\": \"2.9.2\",\n   \"type\": \"java-archive\",\n   \"foundBy\": \"java-cataloger\",\n   \"locations\": [\n    {\n     \"path\": \"test/integration/testdata/image-debian-match-coverage/java/example-java-app-maven-0.1.0.jar\"\n    }\n   ],\n   \"licenses\": [],\n   \"language\": \"java\",\n   \"cpes\": [\n    \"cpe:2.3:a:joda-time:joda-time:2.9.2:*:*:*:*:*:*:*\",\n    \"cpe:2.3:a:joda_time:joda-time:2.9.2:*:*:*:*:*:*:*\",\n    \"cpe:2.3:a:joda-time:joda_time:2.9.2:*:*:*:*:*:*:*\",\n    \"cpe:2.3:a:joda_time:joda_time:2.9.2:*:*:*:*:*:*:*\",\n    \"cpe:2.3:a:joda:joda-time:2.9.2:*:*:*:*:*:*:*\",\n    \"cpe:2.3:a:joda:joda_time:2.9.2:*:*:*:*:*:*:*\"\n   ],\n   \"purl\": \"pkg:maven/joda-time/joda-time@2.9.2\",\n   \"metadataType\": \"JavaMetadata\",\n   \"metadata\": {\n    \"virtualPath\": \"test/integration/testdata/image-debian-match-coverage/java/example-java-app-maven-0.1.0.jar:joda-time\",\n    \"pomProperties\": {\n     \"path\": \"META-INF/maven/joda-time/joda-time/pom.properties\",\n     \"name\": \"\",\n     \"groupId\": \"joda-time\",\n     \"artifactId\": \"joda-time\",\n     \"version\": \"2.9.2\",\n     \"extraFields\": {}\n    },\n    \"pomProject\": {\n     \"path\": \"META-INF/maven/joda-time/joda-time/pom.xml\",\n     \"groupId\": \"joda-time\",\n     \"artifactId\": \"joda-time\",\n     \"version\": \"2.9.2\",\n     \"name\": \"Joda-Time\",\n     \"description\": \"Date and time library to replace JDK date handling\",\n     \"url\": \"http://www.joda.org/joda-time/\"\n    }\n   }\n  },\n  {\n   \"id\": \"f727dab3-3fe8-4082-b12d-0fed59452c89\",\n   \"name\": \"libvncserver\",\n   \"version\": \"0.9.9\",\n   \"type\": \"apk\",\n   \"foundBy\": \"apkdb-cataloger\",\n   \"locations\": [\n    {\n     \"path\": \"test/integration/testdata/image-alpine-match-coverage/lib/apk/db/installed\"\n    }\n   ],\n   \"licenses\": [\n    \"GPL-2.0-or-later\"\n   ],\n   \"language\": \"\",\n   \"cpes\": [\n    \"cpe:2.3:a:libvncserver:libvncserver:0.9.9:*:*:*:*:*:*:*\"\n   ],\n   \"purl\": \"pkg:alpine/libvncserver@0.9.9?arch=x86_64\",\n   \"metadataType\": \"ApkMetadata\",\n   \"metadata\": {\n    \"package\": \"libvncserver\",\n    \"originPackage\": \"libvncserver\",\n    \"maintainer\": \"A. Wilcox <awilfox@adelielinux.org>\",\n    \"version\": \"0.9.9\",\n    \"license\": \"GPL-2.0-or-later\",\n    \"architecture\": \"x86_64\",\n    \"url\": \"http://libvncserver.sourceforge.net/\",\n    \"description\": \"Library to make writing a vnc server easy\",\n    \"size\": 166239,\n    \"installedSize\": 389120,\n    \"pullDependencies\": \"so:libc.musl-x86_64.so.1 so:libgcrypt.so.20 so:libgnutls.so.30 so:libjpeg.so.8 so:libpng16.so.16 so:libz.so.1\",\n    \"pullChecksum\": \"Q1z0MwWQKfva+S+q7XmOBYFfQgW/k=\",\n    \"gitCommitOfApkPort\": \"bf1ec813f662f128fc6b70f37ef1c0474bb24488\",\n    \"files\": [\n     {\n      \"path\": \"/usr\",\n      \"digest\": {\n       \"algorithm\": \"\",\n       \"value\": \"\"\n      }\n     },\n     {\n      \"path\": \"/usr/lib\",\n      \"digest\": {\n       \"algorithm\": \"\",\n       \"value\": \"\"\n      }\n     },\n     {\n      \"path\": \"/usr/lib/libvncclient.so.1\",\n      \"ownerUid\": \"0\",\n      \"ownerGid\": \"0\",\n      \"permissions\": \"777\",\n      \"digest\": {\n       \"algorithm\": \"sha1\",\n       \"value\": \"Q1quyp/JcSPFQhtQFjMUYdMwRvAWM=\"\n      }\n     },\n     {\n      \"path\": \"/usr/lib/libvncserver.so.1.0.0\",\n      \"ownerUid\": \"0\",\n      \"ownerGid\": \"0\",\n      \"permissions\": \"755\",\n      \"digest\": {\n       \"algorithm\": \"sha1\",\n       \"value\": \"Q16Pd1AqyqQRMwiFfbUt9XkYnkapw=\"\n      }\n     },\n     {\n      \"path\": \"/usr/lib/libvncserver.so.1\",\n      \"ownerUid\": \"0\",\n      \"ownerGid\": \"0\",\n      \"permissions\": \"777\",\n      \"digest\": {\n       \"algorithm\": \"sha1\",\n       \"value\": \"Q184HrHsxEBqnsH4QNxeU5w8alhKI=\"\n      }\n     },\n     {\n      \"path\": \"/usr/lib/libvncclient.so.1.0.0\",\n      \"ownerUid\": \"0\",\n      \"ownerGid\": \"0\",\n      \"permissions\": \"755\",\n      \"digest\": {\n       \"algorithm\": \"sha1\",\n       \"value\": \"Q1IEjCrEwVlQt2GjIsb3o39vcgqMg=\"\n      }\n     }\n    ]\n   }\n  },\n  {\n   \"id\": \"731abc1d-e0ed-4c6d-9554-1df437e4c540\",\n   \"name\": \"rails\",\n   \"version\": \"4.1.1\",\n   \"type\": \"gem\",\n   \"foundBy\": \"ruby-gemfile-cataloger\",\n   \"locations\": [\n    {\n     \"path\": \"test/integration/testdata/image-debian-match-coverage/ruby/Gemfile.lock\"\n    }\n   ],\n   \"licenses\": [],\n   \"language\": \"ruby\",\n   \"cpes\": [\n    \"cpe:2.3:a:ruby_lang:rails:4.1.1:*:*:*:*:*:*:*\",\n    \"cpe:2.3:a:ruby-lang:rails:4.1.1:*:*:*:*:*:*:*\",\n    \"cpe:2.3:a:rails:rails:4.1.1:*:*:*:*:*:*:*\",\n    \"cpe:2.3:a:ruby:rails:4.1.1:*:*:*:*:*:*:*\",\n    \"cpe:2.3:a:*:rails:4.1.1:*:*:*:*:*:*:*\"\n   ],\n   \"purl\": \"pkg:gem/rails@4.1.1\",\n   \"metadataType\": \"\",\n   \"metadata\": null\n  }\n ],\n \"artifactRelationships\": [],\n \"source\": {\n  \"type\": \"directory\",\n  \"target\": \"./\"\n },\n \"distro\": {\n  \"name\": \"\",\n  \"version\": \"\",\n  \"idLike\": \"\"\n },\n \"descriptor\": {\n  \"name\": \"syft\",\n  \"version\": \"[not provided]\"\n },\n \"schema\": {\n  \"version\": \"1.1.0\",\n  \"url\": \"https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-1.1.0.json\"\n }\n}\n"
  },
  {
    "path": "test/cli/testdata/sbom-ubuntu-20.04--pruned.json",
    "content": "{\n  \"artifacts\": [\n    {\n      \"name\": \"gcc-10-base\",\n      \"version\": \"10.2.0-5ubuntu1~20.04\",\n      \"type\": \"deb\",\n      \"foundBy\": \"dpkgdb-cataloger\",\n      \"locations\": [\n        {\n          \"path\": \"/var/lib/dpkg/status\",\n          \"layerID\": \"sha256:9f32931c9d28f10104a8eb1330954ba90e76d92b02c5256521ba864feec14009\"\n        },\n        {\n          \"path\": \"/var/lib/dpkg/info/gcc-10-base:amd64.md5sums\",\n          \"layerID\": \"sha256:9f32931c9d28f10104a8eb1330954ba90e76d92b02c5256521ba864feec14009\"\n        },\n        {\n          \"path\": \"/usr/share/doc/gcc-10-base/copyright\",\n          \"layerID\": \"sha256:9f32931c9d28f10104a8eb1330954ba90e76d92b02c5256521ba864feec14009\"\n        }\n      ],\n      \"licenses\": [],\n      \"language\": \"\",\n      \"cpes\": [\n        \"cpe:2.3:a:gcc-10-base:gcc-10-base:10.2.0-5ubuntu1~20.04:*:*:*:*:*:*:*\",\n        \"cpe:2.3:a:*:gcc-10-base:10.2.0-5ubuntu1~20.04:*:*:*:*:*:*:*\"\n      ],\n      \"purl\": \"pkg:deb/ubuntu/gcc-10-base@10.2.0-5ubuntu1~20.04?arch=amd64\",\n      \"metadataType\": \"DpkgMetadata\",\n      \"metadata\": {\n        \"package\": \"gcc-10-base\",\n        \"source\": \"gcc-10\",\n        \"version\": \"10.2.0-5ubuntu1~20.04\",\n        \"sourceVersion\": \"\",\n        \"architecture\": \"amd64\",\n        \"maintainer\": \"Ubuntu Core developers <ubuntu-devel-discuss@lists.ubuntu.com>\",\n        \"installedSize\": 260,\n        \"files\": [\n          {\n            \"path\": \"/usr/share/doc/gcc-10-base/README.Debian.amd64.gz\",\n            \"md5\": \"3c03902e06eef5dcfe3005376c23a120\"\n          },\n          {\n            \"path\": \"/usr/share/doc/gcc-10-base/TODO.Debian\",\n            \"md5\": \"8afe308ec72834f3c24b209fbc4d149e\"\n          },\n          {\n            \"path\": \"/usr/share/doc/gcc-10-base/changelog.Debian.gz\",\n            \"md5\": \"0e3cbc1152a18bddf7c24fe3913866c6\"\n          },\n          {\n            \"path\": \"/usr/share/doc/gcc-10-base/copyright\",\n            \"md5\": \"a80ca2e181b9eecc3e4d373fd7ca59f2\"\n          }\n        ]\n      }\n    },\n    {\n      \"name\": \"hostname\",\n      \"version\": \"3.23\",\n      \"type\": \"deb\",\n      \"foundBy\": \"dpkgdb-cataloger\",\n      \"locations\": [\n        {\n          \"path\": \"/var/lib/dpkg/status\",\n          \"layerID\": \"sha256:9f32931c9d28f10104a8eb1330954ba90e76d92b02c5256521ba864feec14009\"\n        },\n        {\n          \"path\": \"/var/lib/dpkg/info/hostname.md5sums\",\n          \"layerID\": \"sha256:9f32931c9d28f10104a8eb1330954ba90e76d92b02c5256521ba864feec14009\"\n        },\n        {\n          \"path\": \"/usr/share/doc/hostname/copyright\",\n          \"layerID\": \"sha256:9f32931c9d28f10104a8eb1330954ba90e76d92b02c5256521ba864feec14009\"\n        }\n      ],\n      \"licenses\": [],\n      \"language\": \"\",\n      \"cpes\": [\n        \"cpe:2.3:a:hostname:hostname:3.23:*:*:*:*:*:*:*\",\n        \"cpe:2.3:a:*:hostname:3.23:*:*:*:*:*:*:*\"\n      ],\n      \"purl\": \"pkg:deb/ubuntu/hostname@3.23?arch=amd64\",\n      \"metadataType\": \"DpkgMetadata\",\n      \"metadata\": {\n        \"package\": \"hostname\",\n        \"source\": \"\",\n        \"version\": \"3.23\",\n        \"sourceVersion\": \"\",\n        \"architecture\": \"amd64\",\n        \"maintainer\": \"Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>\",\n        \"installedSize\": 54,\n        \"files\": [\n          {\n            \"path\": \"/bin/hostname\",\n            \"md5\": \"1ce73d718e3dccc1aaa7bce6ae2ef0a7\"\n          },\n          {\n            \"path\": \"/usr/share/doc/hostname/changelog.gz\",\n            \"md5\": \"087a3eabd7427692c216a5d7a4341127\"\n          },\n          {\n            \"path\": \"/usr/share/doc/hostname/copyright\",\n            \"md5\": \"460b6a1df2db2b5e80f05a44ec21c62f\"\n          },\n          {\n            \"path\": \"/usr/share/man/man1/hostname.1.gz\",\n            \"md5\": \"62e6be6a928b4b9f2a985778fee171fd\"\n          }\n        ]\n      }\n    },\n    {\n      \"name\": \"libacl1\",\n      \"version\": \"2.2.53-6\",\n      \"type\": \"deb\",\n      \"foundBy\": \"dpkgdb-cataloger\",\n      \"locations\": [\n        {\n          \"path\": \"/var/lib/dpkg/status\",\n          \"layerID\": \"sha256:9f32931c9d28f10104a8eb1330954ba90e76d92b02c5256521ba864feec14009\"\n        },\n        {\n          \"path\": \"/var/lib/dpkg/info/libacl1:amd64.md5sums\",\n          \"layerID\": \"sha256:9f32931c9d28f10104a8eb1330954ba90e76d92b02c5256521ba864feec14009\"\n        },\n        {\n          \"path\": \"/usr/share/doc/libacl1/copyright\",\n          \"layerID\": \"sha256:9f32931c9d28f10104a8eb1330954ba90e76d92b02c5256521ba864feec14009\"\n        }\n      ],\n      \"licenses\": [\n        \"GPL-2+\",\n        \"LGPL-2+\"\n      ],\n      \"language\": \"\",\n      \"cpes\": [\n        \"cpe:2.3:a:libacl1:libacl1:2.2.53-6:*:*:*:*:*:*:*\",\n        \"cpe:2.3:a:*:libacl1:2.2.53-6:*:*:*:*:*:*:*\"\n      ],\n      \"purl\": \"pkg:deb/ubuntu/libacl1@2.2.53-6?arch=amd64\",\n      \"metadataType\": \"DpkgMetadata\",\n      \"metadata\": {\n        \"package\": \"libacl1\",\n        \"source\": \"acl\",\n        \"version\": \"2.2.53-6\",\n        \"sourceVersion\": \"\",\n        \"architecture\": \"amd64\",\n        \"maintainer\": \"Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>\",\n        \"installedSize\": 70,\n        \"files\": [\n          {\n            \"path\": \"/usr/lib/x86_64-linux-gnu/libacl.so.1.1.2253\",\n            \"md5\": \"e77bf61a72656a594ef49768a7d6097b\"\n          },\n          {\n            \"path\": \"/usr/share/doc/libacl1/changelog.Debian.gz\",\n            \"md5\": \"65de3b787d67d4755ad3ae0584aee9f2\"\n          },\n          {\n            \"path\": \"/usr/share/doc/libacl1/copyright\",\n            \"md5\": \"40822d07cf4c0fb9ab13c2bebf51d981\"\n          }\n        ]\n      }\n    },\n    {\n      \"name\": \"libattr1\",\n      \"version\": \"1:2.4.48-5\",\n      \"type\": \"deb\",\n      \"foundBy\": \"dpkgdb-cataloger\",\n      \"locations\": [\n        {\n          \"path\": \"/var/lib/dpkg/status\",\n          \"layerID\": \"sha256:9f32931c9d28f10104a8eb1330954ba90e76d92b02c5256521ba864feec14009\"\n        },\n        {\n          \"path\": \"/var/lib/dpkg/info/libattr1:amd64.md5sums\",\n          \"layerID\": \"sha256:9f32931c9d28f10104a8eb1330954ba90e76d92b02c5256521ba864feec14009\"\n        },\n        {\n          \"path\": \"/usr/share/doc/libattr1/copyright\",\n          \"layerID\": \"sha256:9f32931c9d28f10104a8eb1330954ba90e76d92b02c5256521ba864feec14009\"\n        }\n      ],\n      \"licenses\": [\n        \"GPL-2+\",\n        \"LGPL-2+\"\n      ],\n      \"language\": \"\",\n      \"cpes\": [\n        \"cpe:2.3:a:libattr1:libattr1:1:2.4.48-5:*:*:*:*:*:*:*\",\n        \"cpe:2.3:a:*:libattr1:1:2.4.48-5:*:*:*:*:*:*:*\"\n      ],\n      \"purl\": \"pkg:deb/ubuntu/libattr1@1:2.4.48-5?arch=amd64\",\n      \"metadataType\": \"DpkgMetadata\",\n      \"metadata\": {\n        \"package\": \"libattr1\",\n        \"source\": \"attr\",\n        \"version\": \"1:2.4.48-5\",\n        \"sourceVersion\": \"\",\n        \"architecture\": \"amd64\",\n        \"maintainer\": \"Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>\",\n        \"installedSize\": 57,\n        \"files\": [\n          {\n            \"path\": \"/usr/lib/x86_64-linux-gnu/libattr.so.1.1.2448\",\n            \"md5\": \"708453da8ebde1aaca2ca69c04d4c0a8\"\n          },\n          {\n            \"path\": \"/usr/share/doc/libattr1/changelog.Debian.gz\",\n            \"md5\": \"6465a4cda28287d4ea9979b530648ee3\"\n          },\n          {\n            \"path\": \"/usr/share/doc/libattr1/copyright\",\n            \"md5\": \"1e0c5c8b55170890f960aad90336aaed\"\n          }\n        ]\n      }\n    }\n  ],\n  \"source\": {\n    \"type\": \"image\",\n    \"target\": {\n      \"userInput\": \"ubuntu:20.04\",\n      \"imageID\": \"sha256:f63181f19b2fe819156dcb068b3b5bc036820bec7014c5f77277cfa341d4cb5e\",\n      \"manifestDigest\": \"sha256:5146935f9248826d44dfc2489abfd5f4bdfbc319a738c04dfe1ef071f228a1ac\",\n      \"mediaType\": \"application/vnd.docker.distribution.manifest.v2+json\",\n      \"tags\": [\n        \"ubuntu:20.04\"\n      ],\n      \"imageSize\": 72898411,\n      \"scope\": \"Squashed\",\n      \"layers\": [\n        {\n          \"mediaType\": \"application/vnd.docker.image.rootfs.diff.tar.gzip\",\n          \"digest\": \"sha256:9f32931c9d28f10104a8eb1330954ba90e76d92b02c5256521ba864feec14009\",\n          \"size\": 72897593\n        },\n        {\n          \"mediaType\": \"application/vnd.docker.image.rootfs.diff.tar.gzip\",\n          \"digest\": \"sha256:dbf2c0f42a39b60301f6d3936f7f8adb59bb97d31ec11cc4a049ce81155fef89\",\n          \"size\": 811\n        },\n        {\n          \"mediaType\": \"application/vnd.docker.image.rootfs.diff.tar.gzip\",\n          \"digest\": \"sha256:02473afd360bd5391fa51b6e7849ce88732ae29f50f3630c3551f528eba66d1e\",\n          \"size\": 7\n        }\n      ]\n    }\n  },\n  \"distro\": {\n    \"name\": \"ubuntu\",\n    \"version\": \"20.04\",\n    \"idLike\": \"debian\"\n  },\n  \"descriptor\": {\n    \"name\": \"syft\",\n    \"version\": \"0.12.7\"\n  },\n  \"schema\": {\n    \"version\": \"1.0.1\",\n    \"url\": \"https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-1.0.1.json\"\n  }\n}\n"
  },
  {
    "path": "test/cli/testdata/test-ignore-reason/config-with-ignore.yaml",
    "content": "check-for-app-update: false\nignore:\n  - vulnerability: CVE-2021-42385\n    reason: test reason for vulnerability being ignored\n"
  },
  {
    "path": "test/cli/testdata/test-ignore-reason/sbom.json",
    "content": "{\n  \"artifacts\": [\n    {\n      \"id\": \"20a169629a73fdbd\",\n      \"name\": \"busybox\",\n      \"version\": \"1.31.1\",\n      \"type\": \"binary\",\n      \"foundBy\": \"binary-cataloger\",\n      \"locations\": [\n        {\n          \"path\": \"/bin/[\",\n          \"layerID\": \"sha256:7ce37844ca75600dbcbe085858845c5b92b6109db3c8c1ae6eb887aab91ad04f\",\n          \"annotations\": {\n            \"evidence\": \"primary\"\n          }\n        }\n      ],\n      \"licenses\": [],\n      \"language\": \"\",\n      \"cpes\": [\n        \"cpe:2.3:a:busybox:busybox:1.31.1:*:*:*:*:*:*:*\",\n        \"cpe:2.3:a:busybox:busybox:1.31.1:*:*:*:*:*:*:*\"\n      ],\n      \"purl\": \"\",\n      \"metadataType\": \"BinaryMetadata\",\n      \"metadata\": {\n        \"matches\": [\n          {\n            \"classifier\": \"busybox-binary\",\n            \"location\": {\n              \"path\": \"/bin/[\",\n              \"layerID\": \"sha256:7ce37844ca75600dbcbe085858845c5b92b6109db3c8c1ae6eb887aab91ad04f\",\n              \"annotations\": {\n                \"evidence\": \"primary\"\n              }\n            }\n          }\n        ]\n      }\n    }\n  ],\n  \"artifactRelationships\": [\n    {\n      \"parent\": \"1ee006886991ad4689838d3a288e0dd3fd29b70e276622f16b67a8922831a853\",\n      \"child\": \"20a169629a73fdbd\",\n      \"type\": \"contains\"\n    },\n    {\n      \"parent\": \"20a169629a73fdbd\",\n      \"child\": \"1c3ded193f8808da\",\n      \"type\": \"evident-by\"\n    }\n  ],\n  \"files\": [\n    {\n      \"id\": \"1c3ded193f8808da\",\n      \"location\": {\n        \"path\": \"/bin/[\",\n        \"layerID\": \"sha256:7ce37844ca75600dbcbe085858845c5b92b6109db3c8c1ae6eb887aab91ad04f\"\n      }\n    }\n  ],\n  \"source\": {\n    \"id\": \"1ee006886991ad4689838d3a288e0dd3fd29b70e276622f16b67a8922831a853\",\n    \"name\": \"busybox\",\n    \"version\": \"1.31\",\n    \"type\": \"image\",\n    \"metadata\": {\n      \"userInput\": \"busybox:1.31\",\n      \"imageID\": \"sha256:19d689bc58fd64da6a46d46512ea965a12b6bfb5b030400e21bc0a04c4ff155e\",\n      \"manifestDigest\": \"sha256:1ee006886991ad4689838d3a288e0dd3fd29b70e276622f16b67a8922831a853\",\n      \"mediaType\": \"application/vnd.docker.distribution.manifest.v2+json\",\n      \"tags\": [],\n      \"imageSize\": 1384134,\n      \"layers\": [\n        {\n          \"mediaType\": \"application/vnd.docker.image.rootfs.diff.tar.gzip\",\n          \"digest\": \"sha256:7ce37844ca75600dbcbe085858845c5b92b6109db3c8c1ae6eb887aab91ad04f\",\n          \"size\": 1384134\n        }\n      ],\n      \"manifest\": \"ewogICAic2NoZW1hVmVyc2lvbiI6IDIsCiAgICJtZWRpYVR5cGUiOiAiYXBwbGljYXRpb24vdm5kLmRvY2tlci5kaXN0cmlidXRpb24ubWFuaWZlc3QudjIranNvbiIsCiAgICJjb25maWciOiB7CiAgICAgICJtZWRpYVR5cGUiOiAiYXBwbGljYXRpb24vdm5kLmRvY2tlci5jb250YWluZXIuaW1hZ2UudjEranNvbiIsCiAgICAgICJzaXplIjogMTQ5NCwKICAgICAgImRpZ2VzdCI6ICJzaGEyNTY6MTlkNjg5YmM1OGZkNjRkYTZhNDZkNDY1MTJlYTk2NWExMmI2YmZiNWIwMzA0MDBlMjFiYzBhMDRjNGZmMTU1ZSIKICAgfSwKICAgImxheWVycyI6IFsKICAgICAgewogICAgICAgICAibWVkaWFUeXBlIjogImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuaW1hZ2Uucm9vdGZzLmRpZmYudGFyLmd6aXAiLAogICAgICAgICAic2l6ZSI6IDgxNTQwMCwKICAgICAgICAgImRpZ2VzdCI6ICJzaGEyNTY6ZmQ0NDAxNmUzZDNlZGI4ZDFkOGIxYTMzNTdmNzcwOGE4Mzg4ZTQ2MjI1MDBkMzZmZGUzYThjZGZiMjNmYmFjOCIKICAgICAgfQogICBdCn0=\",\n      \"config\": \"eyJhcmNoaXRlY3R1cmUiOiJhcm02NCIsImNvbmZpZyI6eyJIb3N0bmFtZSI6IiIsIkRvbWFpbm5hbWUiOiIiLCJVc2VyIjoiIiwiQXR0YWNoU3RkaW4iOmZhbHNlLCJBdHRhY2hTdGRvdXQiOmZhbHNlLCJBdHRhY2hTdGRlcnIiOmZhbHNlLCJUdHkiOmZhbHNlLCJPcGVuU3RkaW4iOmZhbHNlLCJTdGRpbk9uY2UiOmZhbHNlLCJFbnYiOlsiUEFUSD0vdXNyL2xvY2FsL3NiaW46L3Vzci9sb2NhbC9iaW46L3Vzci9zYmluOi91c3IvYmluOi9zYmluOi9iaW4iXSwiQ21kIjpbInNoIl0sIkFyZ3NFc2NhcGVkIjp0cnVlLCJJbWFnZSI6InNoYTI1NjpmNDFmZmIxYWI1MWNhMDg3YzdkN2E3MWM5NDUzMGNmOWM4MjFkZDM3Zjc2ZGU0NjY2NTZmNjM1NDE0NGQzYmQyIiwiVm9sdW1lcyI6bnVsbCwiV29ya2luZ0RpciI6IiIsIkVudHJ5cG9pbnQiOm51bGwsIk9uQnVpbGQiOm51bGwsIkxhYmVscyI6bnVsbH0sImNvbnRhaW5lciI6IjBlY2U0NzFlM2MyMGFmYTQyYWM2ZTRhNzBlYTc0MDFmM2ViNGNiNzUzZmQ0MzYyZDA3ZDNmNWI1ZjFlODhhZDEiLCJjb250YWluZXJfY29uZmlnIjp7Ikhvc3RuYW1lIjoiMGVjZTQ3MWUzYzIwIiwiRG9tYWlubmFtZSI6IiIsIlVzZXIiOiIiLCJBdHRhY2hTdGRpbiI6ZmFsc2UsIkF0dGFjaFN0ZG91dCI6ZmFsc2UsIkF0dGFjaFN0ZGVyciI6ZmFsc2UsIlR0eSI6ZmFsc2UsIk9wZW5TdGRpbiI6ZmFsc2UsIlN0ZGluT25jZSI6ZmFsc2UsIkVudiI6WyJQQVRIPS91c3IvbG9jYWwvc2JpbjovdXNyL2xvY2FsL2JpbjovdXNyL3NiaW46L3Vzci9iaW46L3NiaW46L2JpbiJdLCJDbWQiOlsiL2Jpbi9zaCIsIi1jIiwiIyhub3ApICIsIkNNRCBbXCJzaFwiXSJdLCJBcmdzRXNjYXBlZCI6dHJ1ZSwiSW1hZ2UiOiJzaGEyNTY6ZjQxZmZiMWFiNTFjYTA4N2M3ZDdhNzFjOTQ1MzBjZjljODIxZGQzN2Y3NmRlNDY2NjU2ZjYzNTQxNDRkM2JkMiIsIlZvbHVtZXMiOm51bGwsIldvcmtpbmdEaXIiOiIiLCJFbnRyeXBvaW50IjpudWxsLCJPbkJ1aWxkIjpudWxsLCJMYWJlbHMiOnt9fSwiY3JlYXRlZCI6IjIwMjAtMDYtMDJUMjE6Mzk6NDUuMzc0Mjg5MDQxWiIsImRvY2tlcl92ZXJzaW9uIjoiMTguMDkuNyIsImhpc3RvcnkiOlt7ImNyZWF0ZWQiOiIyMDIwLTA2LTAyVDIxOjM5OjQ0LjUzMDIwOTg5MVoiLCJjcmVhdGVkX2J5IjoiL2Jpbi9zaCAtYyAjKG5vcCkgQUREIGZpbGU6MDdkOTQ2NmVkMWExMDkxNmY0ODIzZGI2OWY0YzU4NDg0Y2U3MDIyMWMzYzk0YTBmMmE4MDRmNTM3MWE2Mjc2NSBpbiAvICJ9LHsiY3JlYXRlZCI6IjIwMjAtMDYtMDJUMjE6Mzk6NDUuMzc0Mjg5MDQxWiIsImNyZWF0ZWRfYnkiOiIvYmluL3NoIC1jICMobm9wKSAgQ01EIFtcInNoXCJdIiwiZW1wdHlfbGF5ZXIiOnRydWV9XSwib3MiOiJsaW51eCIsInJvb3RmcyI6eyJ0eXBlIjoibGF5ZXJzIiwiZGlmZl9pZHMiOlsic2hhMjU2OjdjZTM3ODQ0Y2E3NTYwMGRiY2JlMDg1ODU4ODQ1YzViOTJiNjEwOWRiM2M4YzFhZTZlYjg4N2FhYjkxYWQwNGYiXX19\",\n      \"repoDigests\": [\n        \"index.docker.io/library/busybox@sha256:95cf004f559831017cdf4628aaf1bb30133677be8702a8c5f2994629f637a209\"\n      ],\n      \"architecture\": \"arm64\",\n      \"os\": \"linux\"\n    }\n  },\n  \"distro\": {\n    \"prettyName\": \"BusyBox v1.31.1\",\n    \"name\": \"busybox\",\n    \"id\": \"busybox\",\n    \"idLike\": [\n      \"busybox\"\n    ],\n    \"version\": \"1.31.1\",\n    \"versionID\": \"1.31.1\"\n  },\n  \"descriptor\": {\n    \"name\": \"syft\",\n    \"version\": \"0.94.0\",\n    \"configuration\": {\n      \"catalogers\": null,\n      \"package\": {\n        \"cataloger\": {\n          \"enabled\": true,\n          \"scope\": \"Squashed\"\n        },\n        \"search-unindexed-archives\": false,\n        \"search-indexed-archives\": true\n      },\n      \"golang\": {\n        \"search-local-mod-cache-licenses\": false,\n        \"local-mod-cache-dir\": \"\",\n        \"search-remote-licenses\": false,\n        \"proxy\": \"\",\n        \"no-proxy\": \"\"\n      },\n      \"linux-kernel\": {\n        \"catalog-modules\": true\n      },\n      \"python\": {\n        \"guess-unpinned-requirements\": false\n      },\n      \"file-metadata\": {\n        \"cataloger\": {\n          \"enabled\": false,\n          \"scope\": \"Squashed\"\n        },\n        \"digests\": [\n          \"sha256\"\n        ]\n      },\n      \"file-classification\": {\n        \"cataloger\": {\n          \"enabled\": false,\n          \"scope\": \"Squashed\"\n        }\n      },\n      \"file-contents\": {\n        \"cataloger\": {\n          \"enabled\": false,\n          \"scope\": \"Squashed\"\n        },\n        \"skip-files-above-size\": 1048576,\n        \"globs\": null\n      },\n      \"secrets\": {\n        \"cataloger\": {\n          \"enabled\": false,\n          \"scope\": \"AllLayers\"\n        },\n        \"additional-patterns\": null,\n        \"exclude-pattern-names\": null,\n        \"reveal-values\": false,\n        \"skip-files-above-size\": 1048576\n      },\n      \"registry\": {\n        \"insecure-skip-tls-verify\": false,\n        \"insecure-use-http\": false,\n        \"auth\": null,\n        \"ca-cert\": \"\"\n      },\n      \"exclude\": [],\n      \"platform\": \"\",\n      \"name\": \"\",\n      \"source\": {\n        \"name\": \"\",\n        \"version\": \"\",\n        \"file\": {\n          \"digests\": [\n            \"sha256\"\n          ]\n        }\n      },\n      \"parallelism\": 1,\n      \"default-image-pull-source\": \"\",\n      \"base-path\": \"\",\n      \"exclude-binary-overlap-by-ownership\": true\n    }\n  },\n  \"schema\": {\n    \"version\": \"11.0.1\",\n    \"url\": \"https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-11.0.1.json\"\n  }\n}\n"
  },
  {
    "path": "test/cli/testdata/test-ignore-reason/template-with-ignore-reasons",
    "content": "The following vulnerabilities are considered irrelevant:\n{{- range .IgnoredMatches}}\n  {{.Vulnerability.ID}} ({{ range $air := .AppliedIgnoreRules }}{{ $air.Reason }}{{ end }})\n{{- end}}\n"
  },
  {
    "path": "test/cli/trait_assertions_test.go",
    "content": "package cli\n\nimport (\n\t\"encoding/json\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/acarl005/stripansi\"\n)\n\ntype traitAssertion func(tb testing.TB, stdout, stderr string, rc int)\n\nfunc assertNoStderr(tb testing.TB, _, stderr string, _ int) {\n\ttb.Helper()\n\tif len(stderr) > 0 {\n\t\ttb.Errorf(\"expected stderr to be empty, but wasn't\")\n\t}\n}\n\nfunc assertInOutput(data string) traitAssertion {\n\treturn func(tb testing.TB, stdout, stderr string, _ int) {\n\t\ttb.Helper()\n\n\t\tif !strings.Contains(stripansi.Strip(stderr), data) && !strings.Contains(stripansi.Strip(stdout), data) {\n\t\t\ttb.Errorf(\"data=%q was NOT found in any output, but should have been there\", data)\n\t\t}\n\t}\n}\n\nfunc assertFailingReturnCode(tb testing.TB, _, _ string, rc int) {\n\ttb.Helper()\n\tif rc == 0 {\n\t\ttb.Errorf(\"expected a failure but got rc=%d\", rc)\n\t}\n}\n\nfunc assertSucceedingReturnCode(tb testing.TB, _, _ string, rc int) {\n\ttb.Helper()\n\tif rc != 0 {\n\t\ttb.Errorf(\"expected to succeed but got rc=%d\", rc)\n\t}\n}\n\nfunc assertRowInStdOut(row []string) traitAssertion {\n\treturn func(tb testing.TB, stdout, stderr string, _ int) {\n\t\ttb.Helper()\n\n\t\tfor _, line := range strings.Split(stdout, \"\\n\") {\n\t\t\tlineMatched := false\n\t\t\tfor _, column := range row {\n\t\t\t\tif !strings.Contains(line, column) {\n\t\t\t\t\t// it wasn't this line\n\t\t\t\t\tlineMatched = false\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tlineMatched = true\n\t\t\t}\n\t\t\tif lineMatched {\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t\t// none of the lines matched\n\t\ttb.Errorf(\"expected stdout to contain %s, but it did not\", strings.Join(row, \" \"))\n\t}\n}\n\nfunc assertNotInOutput(notWanted string) traitAssertion {\n\treturn func(tb testing.TB, stdout, stderr string, _ int) {\n\t\tif strings.Contains(stdout, notWanted) {\n\t\t\ttb.Errorf(\"got unwanted %s in stdout %s\", notWanted, stdout)\n\t\t}\n\t}\n}\n\nfunc assertJsonReport(tb testing.TB, stdout, _ string, _ int) {\n\ttb.Helper()\n\tvar data interface{}\n\n\tif err := json.Unmarshal([]byte(stdout), &data); err != nil {\n\t\ttb.Errorf(\"expected to find a JSON report, but was unmarshalable: %+v\", err)\n\t}\n}\n\nfunc assertDbProvidersTableReport(tb testing.TB, stdout, _ string, _ int) {\n\ttb.Helper()\n\tif !strings.Contains(stdout, \"NAME\") || !strings.Contains(stdout, \"DATE CAPTURED\") {\n\t\ttb.Errorf(\"expected to find a table report, but did not\")\n\t}\n}\n"
  },
  {
    "path": "test/cli/utils_test.go",
    "content": "package cli\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/stereoscope/pkg/imagetest\"\n)\n\nfunc getFixtureImage(tb testing.TB, fixtureImageName string) string {\n\ttb.Helper()\n\n\timagetest.GetFixtureImage(tb, \"docker-archive\", fixtureImageName)\n\treturn imagetest.GetFixtureImageTarPath(tb, fixtureImageName)\n}\n\nfunc getGrypeCommand(tb testing.TB, args ...string) *exec.Cmd {\n\ttb.Helper()\n\targsWithConfig := args\n\tif !grypeCommandHasConfigArg(argsWithConfig...) {\n\t\targsWithConfig = append(\n\t\t\t[]string{\"-c\", \"../grype-test-config.yaml\"},\n\t\t\targs...,\n\t\t)\n\t}\n\n\treturn exec.Command(\n\t\tgetGrypeSnapshotLocation(tb, runtime.GOOS),\n\t\targsWithConfig...,\n\t)\n}\n\nfunc grypeCommandHasConfigArg(args ...string) bool {\n\tfor _, arg := range args {\n\t\tif arg == \"-c\" || arg == \"--config\" {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc getGrypeSnapshotLocation(t testing.TB, goOS string) string {\n\t// GRYPE_BINARY_LOCATION is the absolute path to the snapshot binary\n\tconst envKey = \"GRYPE_BINARY_LOCATION\"\n\tif os.Getenv(envKey) != \"\" {\n\t\treturn os.Getenv(envKey)\n\t}\n\tloc := getGrypeBinaryLocationByOS(t, goOS)\n\tbuildBinary(t, loc)\n\t_ = os.Setenv(envKey, loc)\n\treturn loc\n}\n\nfunc getGrypeBinaryLocationByOS(t testing.TB, goOS string) string {\n\t// note: for amd64 we need to update the snapshot location with the v1 suffix\n\t// see : https://goreleaser.com/customization/build/#why-is-there-a-_v1-suffix-on-amd64-builds\n\tarchPath := runtime.GOARCH\n\tif runtime.GOARCH == \"amd64\" {\n\t\tarchPath = fmt.Sprintf(\"%s_v1\", archPath)\n\t}\n\texecutable := \"grype\"\n\t// note: there is a subtle - vs _ difference between these versions\n\tswitch goOS {\n\tcase \"windows\":\n\t\texecutable += \".exe\"\n\t\tfallthrough\n\tcase \"darwin\", \"linux\":\n\t\treturn filepath.Join(repoRoot(t), \"snapshot\", fmt.Sprintf(\"%s-build_%s_%s\", goOS, goOS, archPath), executable)\n\tdefault:\n\t\tt.Fatalf(\"unsupported OS: %s\", runtime.GOOS)\n\t}\n\treturn \"\"\n}\n\nfunc buildBinary(t testing.TB, loc string) {\n\tt.Chdir(repoRoot(t))\n\tt.Log(\"Building grype...\")\n\tc := exec.Command(\"go\", \"build\", \"-o\", loc, \"./cmd/grype\")\n\tc.Stdout = os.Stdout\n\tc.Stderr = os.Stderr\n\tc.Stdin = os.Stdin\n\trequire.NoError(t, c.Run())\n}\n\nfunc getDockerRunCommand(tb testing.TB, args ...string) *exec.Cmd {\n\ttb.Helper()\n\n\treturn exec.Command(\n\t\t\"docker\",\n\t\tappend(\n\t\t\t[]string{\"run\"},\n\t\t\targs...,\n\t\t)...,\n\t)\n}\n\nfunc runGrype(tb testing.TB, env map[string]string, args ...string) (*exec.Cmd, string, string) {\n\ttb.Helper()\n\n\tcmd := getGrypeCommand(tb, args...)\n\tif env == nil {\n\t\tenv = make(map[string]string)\n\t}\n\n\t// we should not have tests reaching out for app update checks\n\tenv[\"GRYPE_CHECK_FOR_APP_UPDATE\"] = \"false\"\n\n\tstdout, stderr := runCommand(cmd, env)\n\treturn cmd, stdout, stderr\n}\n\nfunc runCommand(cmd *exec.Cmd, env map[string]string) (string, string) {\n\tif env != nil {\n\t\tcmd.Env = append(os.Environ(), envMapToSlice(env)...)\n\t}\n\tvar stdout, stderr bytes.Buffer\n\tcmd.Stdout = &stdout\n\tcmd.Stderr = &stderr\n\n\t// ignore errors since this may be what the test expects\n\tcmd.Run()\n\n\treturn stdout.String(), stderr.String()\n}\n\nfunc envMapToSlice(env map[string]string) (envList []string) {\n\tfor key, val := range env {\n\t\tif key == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tenvList = append(envList, fmt.Sprintf(\"%s=%s\", key, val))\n\t}\n\treturn\n}\n\nfunc repoRoot(tb testing.TB) string {\n\ttb.Helper()\n\troot, err := exec.Command(\"git\", \"rev-parse\", \"--show-toplevel\").Output()\n\tif err != nil {\n\t\ttb.Fatalf(\"unable to find repo root dir: %+v\", err)\n\t}\n\tabsRepoRoot, err := filepath.Abs(strings.TrimSpace(string(root)))\n\tif err != nil {\n\t\ttb.Fatal(\"unable to get abs path to repo root:\", err)\n\t}\n\treturn absRepoRoot\n}\n\nfunc attachFileToCommandStdin(tb testing.TB, file io.Reader, command *exec.Cmd) {\n\ttb.Helper()\n\n\tb, err := io.ReadAll(file)\n\trequire.NoError(tb, err)\n\tcommand.Stdin = bytes.NewReader(b)\n}\n\nfunc assertCommandExecutionSuccess(t testing.TB, cmd *exec.Cmd) {\n\t_, err := cmd.CombinedOutput()\n\tif err != nil {\n\t\tif exitErr, ok := err.(*exec.ExitError); ok {\n\t\t\tt.Fatal(exitErr)\n\t\t}\n\n\t\tt.Fatalf(\"unable to run command %q: %v\", cmd, err)\n\t}\n}\n\nfunc testWithTimeout(t *testing.T, name string, timeout time.Duration, test func(*testing.T)) {\n\tdone := make(chan bool)\n\tgo func() {\n\t\tt.Run(name, test)\n\t\tdone <- true\n\t}()\n\n\tselect {\n\tcase <-time.After(timeout):\n\t\tt.Fatal(\"test timed out\")\n\tcase <-done:\n\t}\n}\n"
  },
  {
    "path": "test/cli/version_cmd_test.go",
    "content": "package cli\n\nimport (\n\t\"testing\"\n)\n\nfunc TestVersionCmdPrintsToStdout(t *testing.T) {\n\ttests := []struct {\n\t\tname       string\n\t\tenv        map[string]string\n\t\tassertions []traitAssertion\n\t}{\n\t\t{\n\t\t\tname: \"version command prints to stdout\",\n\t\t\tassertions: []traitAssertion{\n\t\t\t\tassertInOutput(\"Version:\"),\n\t\t\t\tassertNoStderr,\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tpkgCmd, pkgsStdout, pkgsStderr := runGrype(t, test.env, \"version\")\n\t\t\tfor _, traitFn := range test.assertions {\n\t\t\t\ttraitFn(t, pkgsStdout, pkgsStderr, pkgCmd.ProcessState.ExitCode())\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "test/grype-test-config.yaml",
    "content": "check-for-app-update: false\n\n"
  },
  {
    "path": "test/ignore-att-signature.yaml",
    "content": "check-for-app-update: false"
  },
  {
    "path": "test/install/.dockerignore",
    "content": "**"
  },
  {
    "path": "test/install/.gitignore",
    "content": "cache/"
  },
  {
    "path": "test/install/0_checksums_test.sh",
    "content": ". test_harness.sh\n\n# search for an asset in a release checksums file\ntest_search_for_asset_release() {\n  fixture=./testdata/grype_0.32.0_checksums.txt\n\n  # search_for_asset [checksums-file-path] [name] [os] [arch] [format]\n\n  # positive case\n  actual=$(search_for_asset \"${fixture}\" \"grype\" \"linux\" \"amd64\" \"tar.gz\")\n  assertEquals \"grype_0.32.0_linux_amd64.tar.gz\" \"${actual}\" \"unable to find release asset\"\n\n  # negative cases\n  actual=$(search_for_asset \"${fixture}\" \"grype\" \"Linux\" \"amd64\" \"tar.gz\")\n  assertEquals \"\" \"${actual}\" \"found a release asset but did not expect to (os)\"\n\n  actual=$(search_for_asset \"${fixture}\" \"grype\" \"darwin\" \"amd64\" \"rpm\")\n  assertEquals \"\" \"${actual}\" \"found a release asset but did not expect to (format)\"\n\n}\n\nrun_test_case test_search_for_asset_release\n\n\n# search for an asset in a snapshot checksums file\ntest_search_for_asset_snapshot() {\n  fixture=./testdata/grype_0.32.0-SNAPSHOT-d461f63_checksums.txt\n\n  # search_for_asset [checksums-file-path] [name] [os] [arch] [format]\n\n  # positive case\n  actual=$(search_for_asset \"${fixture}\" \"grype\" \"linux\" \"amd64\" \"rpm\")\n  assertEquals \"grype_0.32.0-SNAPSHOT-d461f63_linux_amd64.rpm\" \"${actual}\" \"unable to find snapshot asset\"\n\n  # negative case\n  actual=$(search_for_asset \"${fixture}\" \"grype\" \"linux\" \"amd64\" \"zip\")\n  assertEquals \"\" \"${actual}\" \"found a snapshot asset but did not expect to (format)\"\n}\n\nrun_test_case test_search_for_asset_snapshot\n\n# verify 256 digest of a file\ntest_hash_sha256() {\n  target=./testdata/assets/valid/grype_0.78.0_linux_arm64.tar.gz\n\n  # hash_sha256 [target]\n\n  # positive case\n  actual=$(hash_sha256 \"${target}\")\n  assertEquals \"8d57abb57a0dae3ff23c8f0df1f51951b7772822e0d560e860d6f68c24ef6d3d\" \"${actual}\" \"mismatched checksum\"\n}\n\nrun_test_case test_hash_sha256\n\n# verify 256 digest of a file relative to the checksums file\ntest_hash_sha256_verify() {\n\n  # hash_sha256_verify [target] [checksums]\n\n\n  # positive case\n\n  checksums=./testdata/assets/valid/checksums.txt\n  target=./testdata/assets/valid/grype_0.78.0_linux_arm64.tar.gz\n\n  hash_sha256_verify \"${target}\" \"${checksums}\"\n  assertEquals \"0\" \"$?\" \"mismatched checksum\"\n\n\n  # negative case\n\n  # we are expecting error messages, which is confusing to look at in passing tests... disable logging for now\n  log_set_priority -1\n\n  checksums=./testdata/assets/invalid/checksums.txt\n  target=./testdata/assets/invalid/grype_0.78.0_linux_arm64.tar.gz\n\n  hash_sha256_verify \"${target}\" \"${checksums}\"\n  assertEquals \"1\" \"$?\" \"verification did not catch mismatched checksum\"\n\n  # restore logging...\n  log_set_priority 0\n}\n\nrun_test_case test_hash_sha256_verify\n"
  },
  {
    "path": "test/install/1_download_snapshot_asset_test.sh",
    "content": ". test_harness.sh\n\nDOWNLOAD_SNAPSHOT_POSITIVE_CASES=0\n\n# helper for asserting test_positive_snapshot_download_asset positive cases\ntest_positive_snapshot_download_asset() {\n  os=\"$1\"\n  arch=\"$2\"\n  format=\"$3\"\n\n  # for troubleshooting\n  # log_set_priority 10\n\n  name=${PROJECT_NAME}\n  github_download=$(snapshot_download_url)\n  version=$(snapshot_version)\n\n  tmpdir=$(mktemp -d)\n\n  actual_filepath=$(download_asset \"${github_download}\" \"${tmpdir}\" \"${name}\" \"${os}\" \"${arch}\" \"${version}\" \"${format}\" )\n\n  assertFileExists \"${actual_filepath}\" \"download_asset os=${os} arch=${arch} format=${format}\"\n\n  assertFilesEqual \\\n    \"$(snapshot_dir)/${name}_${version}_${os}_${arch}.${format}\" \\\n    \"${actual_filepath}\" \\\n    \"unable to download os=${os} arch=${arch} format=${format}\"\n\n  ((DOWNLOAD_SNAPSHOT_POSITIVE_CASES++))\n\n  rm -rf -- \"$tmpdir\"\n}\n\n\ntest_download_snapshot_asset_exercised_all_assets() {\n  expected=$(snapshot_assets_count)\n\n  assertEquals \"${expected}\" \"${DOWNLOAD_SNAPSHOT_POSITIVE_CASES}\" \"did not download all possible assets (missing an os/arch/format variant?)\"\n}\n\n# helper for asserting download_asset negative cases\ntest_negative_snapshot_download_asset() {\n  os=\"$1\"\n  arch=\"$2\"\n  format=\"$3\"\n\n  # for troubleshooting\n  # log_set_priority 10\n\n  name=${PROJECT_NAME}\n  github_download=$(snapshot_download_url)\n  version=$(snapshot_version)\n\n  tmpdir=$(mktemp -d)\n\n  actual_filepath=$(download_asset \"${github_download}\" \"${tmpdir}\" \"${name}\" \"${os}\" \"${arch}\" \"${version}\" \"${format}\")\n\n  assertEquals \"\"  \"${actual_filepath}\" \"unable to download os=${os} arch=${arch} format=${format}\"\n\n  rm -rf -- \"$tmpdir\"\n}\n\n\nworker_pid=$(setup_snapshot_server)\ntrap 'teardown_snapshot_server ${worker_pid}' EXIT\n\n# exercise all possible assets\nrun_test_case test_positive_snapshot_download_asset \"linux\" \"amd64\" \"tar.gz\"\nrun_test_case test_positive_snapshot_download_asset \"linux\" \"amd64\" \"rpm\"\nrun_test_case test_positive_snapshot_download_asset \"linux\" \"amd64\" \"deb\"\nrun_test_case test_positive_snapshot_download_asset \"linux\" \"arm64\" \"tar.gz\"\nrun_test_case test_positive_snapshot_download_asset \"linux\" \"arm64\" \"rpm\"\nrun_test_case test_positive_snapshot_download_asset \"linux\" \"arm64\" \"deb\"\nrun_test_case test_positive_snapshot_download_asset \"linux\" \"s390x\" \"tar.gz\"\nrun_test_case test_positive_snapshot_download_asset \"linux\" \"s390x\" \"rpm\"\nrun_test_case test_positive_snapshot_download_asset \"linux\" \"s390x\" \"deb\"\nrun_test_case test_positive_snapshot_download_asset \"linux\" \"ppc64le\" \"tar.gz\"\nrun_test_case test_positive_snapshot_download_asset \"linux\" \"ppc64le\" \"rpm\"\nrun_test_case test_positive_snapshot_download_asset \"linux\" \"ppc64le\" \"deb\"\nrun_test_case test_positive_snapshot_download_asset \"darwin\" \"amd64\" \"tar.gz\"\nrun_test_case test_positive_snapshot_download_asset \"darwin\" \"arm64\" \"tar.gz\"\nrun_test_case test_positive_snapshot_download_asset \"windows\" \"amd64\" \"zip\"\n# note: the mac signing process produces a dmg which is not part of the snapshot process (thus is not exercised here)\n\n# let's make certain we covered all assets that were expected\nrun_test_case test_download_snapshot_asset_exercised_all_assets\n\n# make certain we handle missing assets alright\nrun_test_case test_negative_snapshot_download_asset \"bogus\" \"amd64\" \"zip\"\n\ntrap - EXIT\nteardown_snapshot_server \"${worker_pid}\"\n"
  },
  {
    "path": "test/install/2_download_release_asset_test.sh",
    "content": ". test_harness.sh\n\ntest_download_release_asset() {\n  release=\"$1\"\n  os=\"$2\"\n  arch=\"$3\"\n  format=\"$4\"\n  expected_mime_type=\"$5\"\n\n  # for troubleshooting\n  # log_set_priority 10\n\n  name=${PROJECT_NAME}\n  version=$(tag_to_version ${release})\n  github_download=\"https://github.com/${OWNER}/${REPO}/releases/download/${release}\"\n\n  tmpdir=$(mktemp -d)\n\n  actual_filepath=$(download_asset \"${github_download}\" \"${tmpdir}\" \"${name}\" \"${os}\" \"${arch}\" \"${version}\" \"${format}\" )\n\n  assertFileExists \"${actual_filepath}\" \"download_asset os=${os} arch=${arch} format=${format}\"\n\n  actual_mime_type=$(file -b --mime-type ${actual_filepath})\n\n  assertEquals \"${expected_mime_type}\" \"${actual_mime_type}\" \"unexpected mimetype for os=${os} arch=${arch} format=${format}\"\n\n  rm -rf -- \"$tmpdir\"\n}\n\n# always test against the latest release\nrelease=$(get_release_tag \"${OWNER}\" \"${REPO}\" \"latest\" )\n\n# exercise all possible assets against a real github release (based on asset listing from https://github.com/anchore/grype/releases/tag/v0.32.0)\n\n# verify all downloads against the checksums file + checksums file signature\nVERIFY_SIGN=true\n\nrun_test_case test_download_release_asset \"${release}\" \"darwin\" \"amd64\" \"tar.gz\" \"application/gzip\"\nrun_test_case test_download_release_asset \"${release}\" \"darwin\" \"arm64\" \"tar.gz\" \"application/gzip\"\nrun_test_case test_download_release_asset \"${release}\" \"linux\" \"amd64\" \"tar.gz\" \"application/gzip\"\nrun_test_case test_download_release_asset \"${release}\" \"linux\" \"amd64\" \"rpm\" \"application/x-rpm\"\nrun_test_case test_download_release_asset \"${release}\" \"linux\" \"amd64\" \"deb\" \"application/vnd.debian.binary-package\"\nrun_test_case test_download_release_asset \"${release}\" \"linux\" \"arm64\" \"tar.gz\" \"application/gzip\"\nrun_test_case test_download_release_asset \"${release}\" \"linux\" \"arm64\" \"rpm\" \"application/x-rpm\"\nrun_test_case test_download_release_asset \"${release}\" \"linux\" \"arm64\" \"deb\" \"application/vnd.debian.binary-package\"\n"
  },
  {
    "path": "test/install/3_install_asset_test.sh",
    "content": ". test_harness.sh\n\nINSTALL_ARCHIVE_POSITIVE_CASES=0\n\n# helper for asserting install_asset positive cases\ntest_positive_snapshot_install_asset() {\n  os=\"$1\"\n  arch=\"$2\"\n  format=\"$3\"\n\n  # for troubleshooting\n  # log_set_priority 10\n\n  name=${PROJECT_NAME}\n  binary=$(get_binary_name \"${os}\" \"${arch}\" \"${PROJECT_NAME}\")\n  github_download=$(snapshot_download_url)\n  version=$(snapshot_version)\n\n  download_dir=$(mktemp -d)\n  install_dir=$(mktemp -d)\n\n  download_and_install_asset \"${github_download}\" \"${download_dir}\" \"${install_dir}\" \"${name}\" \"${os}\" \"${arch}\" \"${version}\" \"${format}\" \"${binary}\"\n\n  assertEquals \"0\" \"$?\" \"download/install did not succeed\"\n\n  expected_path=\"${install_dir}/${binary}\"\n  assertFileExists \"${expected_path}\" \"install_asset os=${os} arch=${arch} format=${format}\"\n\n  # directory structure for arch has been updated as of go 1.18\n  # https://goreleaser.com/customization/build/#why-is-there-a-_v1-suffix-on-amd64-buildsjk\n  if [ $arch == \"amd64\" ]; then\n\t  arch=\"amd64_v1\"\n  fi\n\n  local_suffix=\"\"\n  if [ \"${arch}\" == \"arm64\" ]; then\n    local_suffix=\"_v8.0\"\n  fi\n\n  if [ \"${arch}\" == \"ppc64le\" ]; then\n    local_suffix=\"_power8\"\n  fi\n\n\n  assertFilesEqual \\\n    \"$(snapshot_dir)/${os}-build_${os}_${arch}${local_suffix}/${binary}\" \\\n    \"${expected_path}\" \\\n    \"unable to verify installation of os=${os} arch=${arch} format=${format}\"\n\n ((INSTALL_ARCHIVE_POSITIVE_CASES++))\n\n  rm -rf -- \"$download_dir\"\n  rm -rf -- \"$install_dir\"\n}\n\n# helper for asserting install_asset negative cases\ntest_negative_snapshot_install_asset() {\n  os=\"$1\"\n  arch=\"$2\"\n  format=\"$3\"\n\n  # for troubleshooting\n  # log_set_priority 10\n\n  name=${PROJECT_NAME}\n  binary=$(get_binary_name \"${os}\" \"${arch}\" \"${PROJECT_NAME}\")\n  github_download=$(snapshot_download_url)\n  version=$(snapshot_version)\n\n  download_dir=$(mktemp -d)\n  install_dir=$(mktemp -d)\n\n  download_and_install_asset \"${github_download}\" \"${download_dir}\" \"${install_dir}\" \"${name}\" \"${os}\" \"${arch}\" \"${version}\" \"${format}\" \"${binary}\"\n\n  assertNotEquals \"0\" \"$?\" \"download/install should have failed but did not\"\n\n  rm -rf -- \"$download_dir\"\n  rm -rf -- \"$install_dir\"\n}\n\n\ntest_install_asset_exercised_all_archive_assets() {\n  expected=$(snapshot_assets_archive_count)\n\n  assertEquals \"${expected}\" \"${INSTALL_ARCHIVE_POSITIVE_CASES}\" \"did not download all possible archive assets (missing an os/arch/format variant?)\"\n}\n\n\nworker_pid=$(setup_snapshot_server)\ntrap 'teardown_snapshot_server ${worker_pid}' EXIT\n\n# exercise all possible archive assets (not rpm/deb/dmg) against a snapshot build\nrun_test_case test_positive_snapshot_install_asset \"linux\" \"amd64\" \"tar.gz\"\nrun_test_case test_positive_snapshot_install_asset \"linux\" \"arm64\" \"tar.gz\"\nrun_test_case test_positive_snapshot_install_asset \"linux\" \"s390x\" \"tar.gz\"\nrun_test_case test_positive_snapshot_install_asset \"linux\" \"ppc64le\" \"tar.gz\"\nrun_test_case test_positive_snapshot_install_asset \"darwin\" \"amd64\" \"tar.gz\"\nrun_test_case test_positive_snapshot_install_asset \"darwin\" \"arm64\" \"tar.gz\"\nrun_test_case test_positive_snapshot_install_asset \"windows\" \"amd64\" \"zip\"\n\n# let's make certain we covered all assets that were expected\nrun_test_case test_install_asset_exercised_all_archive_assets\n\n# make certain we handle missing assets alright\nrun_test_case test_negative_snapshot_install_asset \"bogus\" \"amd64\" \"zip\"\n\ntrap - EXIT\nteardown_snapshot_server \"${worker_pid}\"\n"
  },
  {
    "path": "test/install/4_prep_signature_verification_test.sh",
    "content": ". test_harness.sh\n\ntest_compare_semver() {\n  # compare_semver [version1] [version2]\n\n  # positive cases (version1 >= version2)\n  compare_semver \"0.32.0\" \"0.32.0\"\n  assertEquals \"0\" \"$?\" \"+ versions should equal\"\n\n  compare_semver \"0.32.1\" \"0.32.0\"\n  assertEquals \"0\" \"$?\" \"+ patch version should be greater\"\n\n  compare_semver \"0.33.0\" \"0.32.0\"\n  assertEquals \"0\" \"$?\" \"+ minor version should be greater\"\n\n  compare_semver \"0.333.0\" \"0.32.0\"\n  assertEquals \"0\" \"$?\" \"+ minor version should be greater (different length)\"\n\n  compare_semver \"00.33.00\" \"0.032.0\"\n  assertEquals \"0\" \"$?\" \"+ minor version should be greater (different length reversed)\"\n\n  compare_semver \"1.0.0\" \"0.9.9\"\n  assertEquals \"0\" \"$?\" \"+ major version should be greater\"\n\n  compare_semver \"v1.0.0\" \"1.0.0\"\n  assertEquals \"0\" \"$?\" \"+ can remove leading 'v' from version\"\n\n  # negative cases (version1 < version2)\n  compare_semver \"0.32.0\" \"0.32.1\"\n  assertEquals \"1\" \"$?\" \"- patch version should be less\"\n\n  compare_semver \"0.32.7\" \"0.33.0\"\n  assertEquals \"1\" \"$?\" \"- minor version should be less\"\n\n  compare_semver \"00.00032.070\" \"0.33.0\"\n  assertEquals \"1\" \"$?\" \"- minor version should be less (different length)\"\n\n  compare_semver \"0.32.7\" \"00.0033.000\"\n  assertEquals \"1\" \"$?\" \"- minor version should be less (different length reversed)\"\n\n  compare_semver \"1.9.9\" \"2.0.1\"\n  assertEquals \"1\" \"$?\" \"- major version should be less\"\n\n  compare_semver \"1.0.0\" \"v2.0.0\"\n  assertEquals \"1\" \"$?\" \"- can remove leading 'v' from version\"\n}\n\nrun_test_case test_compare_semver\n\n# ensure that various signature verification pre-requisites are correctly checked for\ntest_prep_signature_verification() {\n  # prep_sign_verification [version]\n\n  # we are expecting error messages, which is confusing to look at in passing tests... disable logging for now\n  log_set_priority -1\n\n  # backup original values...\n  OG_COSIGN_BINARY=${COSIGN_BINARY}\n\n  # check the verification path...\n  VERIFY_SIGN=true\n\n  # release does not support signature verification\n  prep_signature_verification \"0.71.0\"\n  assertEquals \"1\" \"$?\" \"release does not support signature verification\"\n\n  # check that the COSIGN binary exists\n  COSIGN_BINARY=fake-cosign-that-doesnt-exist\n  prep_signature_verification \"0.80.0\"\n  assertEquals \"1\" \"$?\" \"cosign binary verification failed\"\n  # restore original values...\n  COSIGN_BINARY=${OG_COSIGN_BINARY}\n\n  # ignore any failing conditions since we are not verifying the signature\n  VERIFY_SIGN=false\n  prep_signature_verification \"0.71.0\"\n  assertEquals \"0\" \"$?\" \"release support verification should not have been triggered\"\n\n  COSIGN_BINARY=fake-cosign-that-doesnt-exist\n  prep_signature_verification \"0.80.0\"\n  assertEquals \"0\" \"$?\" \"cosign binary verification should not have been triggered\"\n  # restore original values...\n  COSIGN_BINARY=${OG_COSIGN_BINARY}\n\n  # restore logging...\n  log_set_priority 0\n}\n\nrun_test_case test_prep_signature_verification\n"
  },
  {
    "path": "test/install/Makefile",
    "content": "NAME=grype\n\n# for local testing (not testing within containers) use the binny-managed version of cosign.\n# this also means that the user does not need to install cosign on their system to run tests.\nCOSIGN_BINARY=../../.tool/cosign\n\nIMAGE_NAME=$(NAME)-install.sh-env\nUBUNTU_IMAGE=$(IMAGE_NAME):ubuntu-20.04\nALPINE_IMAGE=$(IMAGE_NAME):alpine-3.6\nBUSYBOX_IMAGE=$(IMAGE_NAME):busybox-1.36\n\nENVS=./environments\nDOCKER_RUN=docker run --rm -t -w /project/test/install -v $(shell pwd)/../../:/project\nUNIT=make unit-run\n\n# acceptance testing is running the current install.sh against the latest release. Note: this could be a problem down\n# the line if there are breaking changes made that don't align with the latest release (but will be OK with the next\n# release). This tests both installing with signature verification and without.\nACCEPTANCE_CMD=sh -c '../../install.sh -v -b /usr/local/bin && grype version && rm /usr/local/bin/grype && ../../install.sh -b /usr/local/bin && grype version'\n# we also want to test against a previous release to ensure that install.sh defers execution to a former install.sh\n# this version should be at least as recent as when grype was publishing for darwin arm64 as that is what the github runner uses for osx validation\nPREVIOUS_RELEASE=v0.60.0\nACCEPTANCE_PREVIOUS_RELEASE_CMD=sh -c \"../../install.sh -b /usr/local/bin $(PREVIOUS_RELEASE) && grype version\"\n\n# CI cache busting values; change these if you want CI to not use previous stored cache\nINSTALL_TEST_CACHE_BUSTER=894d8ca\n\ndefine title\n    @printf '\\n≡≡≡[ $(1) ]≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡\\n'\nendef\n\n.PHONY: test\ntest: unit acceptance\n\n.PHONY: ci-test-mac\nci-test-mac: unit-run acceptance-local\n\n# note: do not add acceptance-local to this list\n.PHONY: acceptance\nacceptance: acceptance-ubuntu-20.04 acceptance-alpine-3.6 acceptance-busybox-1.36\n\n.PHONY: unit\nunit: unit-ubuntu-20.04\n\n.PHONY: unit-local\nunit-local:\n\t$(call title,unit tests)\n\t@for f in $(shell ls *_test.sh); do echo \"Running unit test suite '$${f}'\"; bash -c \"COSIGN_BINARY=$(COSIGN_BINARY) ./$${f}\" || exit 1; done\n\n.PHONY: unit-run\nunit-run:\n\t$(call title,unit tests)\n\t@for f in $(shell ls *_test.sh); do echo \"Running unit test suite '$${f}'\"; bash $${f} || exit 1; done\n\n.PHONY: acceptance-local\nacceptance-local: acceptance-current-release-local acceptance-previous-release-local\n\n.PHONY: acceptance-current-release-local\nacceptance-current-release-local:\n\t$(ACCEPTANCE_CMD)\n\n.PHONY: acceptance-previous-release-local\nacceptance-previous-release-local:\n\t$(ACCEPTANCE_PREVIOUS_RELEASE_CMD)\n\tgrype version | grep $(shell echo $(PREVIOUS_RELEASE)| tr -d \"v\")\n\n.PHONY: save\nsave: ubuntu-20.04 alpine-3.6 busybox-1.36\n\t@mkdir cache || true\n\tdocker image save -o cache/ubuntu-env.tar $(UBUNTU_IMAGE)\n\tdocker image save -o cache/alpine-env.tar $(ALPINE_IMAGE)\n\tdocker image save -o cache/busybox-env.tar $(BUSYBOX_IMAGE)\n\n.PHONY: load\nload:\n\tdocker image load -i cache/ubuntu-env.tar\n\tdocker image load -i cache/alpine-env.tar\n\tdocker image load -i cache/busybox-env.tar\n\n## UBUNTU #######################################################\n\n.PHONY: acceptance-ubuntu-20.04\nacceptance-ubuntu-20.04: ubuntu-20.04\n\t$(call title,ubuntu:20.04 - acceptance)\n\t$(DOCKER_RUN) $(UBUNTU_IMAGE) \\\n\t\t$(ACCEPTANCE_CMD)\n\n.PHONY: unit-ubuntu-20.04\nunit-ubuntu-20.04: ubuntu-20.04\n\t$(call title,ubuntu:20.04 - unit)\n\t$(DOCKER_RUN) $(UBUNTU_IMAGE) \\\n\t\t$(UNIT)\n\n.PHONY: ubuntu-20.04\nubuntu-20.04:\n\t$(call title,ubuntu:20.04 - build environment)\n\tdocker build -t $(UBUNTU_IMAGE) -f $(ENVS)/Dockerfile-ubuntu-20.04 .\n\n## ALPINE #######################################################\n\n# note: unit tests cannot be run with sh (alpine dosn't have bash by default)\n\n.PHONY: acceptance-alpine-3.6\nacceptance-alpine-3.6: alpine-3.6\n\t$(call title,alpine:3.6 - acceptance)\n\t$(DOCKER_RUN) $(ALPINE_IMAGE) \\\n\t\t$(ACCEPTANCE_CMD)\n\n.PHONY: alpine-3.6\nalpine-3.6:\n\t$(call title,alpine:3.6 - build environment)\n\tdocker build -t $(ALPINE_IMAGE) -f $(ENVS)/Dockerfile-alpine-3.6 .\n\n## BUSYBOX #######################################################\n\n# note: unit tests cannot be run with sh (busybox dosn't have bash by default)\n\n# note: busybox by default will not have cacerts, so you will get TLS warnings (we want to test under these conditions)\n\n.PHONY: acceptance-busybox-1.36\nacceptance-busybox-1.36: busybox-1.36\n\t$(call title,busybox-1.36 - acceptance)\n\t$(DOCKER_RUN) $(BUSYBOX_IMAGE) \\\n\t\t$(ACCEPTANCE_CMD)\n\t@echo \"\\n*** test note: you should see grype spit out a 'x509: certificate signed by unknown authority' error --this is expected ***\"\n\n.PHONY: busybox-1.36\nbusybox-1.36:\n\t$(call title,busybox-1.36 - build environment)\n\tdocker build -t $(BUSYBOX_IMAGE) -f $(ENVS)/Dockerfile-busybox-1.36 .\n\n## For CI ########################################################\n\n.PHONY: cache.fingerprint\ncache.fingerprint:\n\t$(call title,Install test fixture fingerprint)\n\t@find ./environments/* -type f -exec md5sum {} + | awk '{print $1}' | sort | tee /dev/stderr | md5sum | tee cache.fingerprint && echo \"$(INSTALL_TEST_CACHE_BUSTER)\" >> cache.fingerprint\n"
  },
  {
    "path": "test/install/environments/Dockerfile-alpine-3.6",
    "content": "FROM alpine:3.6\nRUN apk update && apk add python3 wget curl unzip make ca-certificates\nRUN curl -O -L \"https://github.com/sigstore/cosign/releases/latest/download/cosign-linux-amd64\" && \\\n    mv cosign-linux-amd64 /usr/local/bin/cosign && \\\n    chmod +x /usr/local/bin/cosign\n"
  },
  {
    "path": "test/install/environments/Dockerfile-busybox-1.36",
    "content": "FROM alpine as certs\nRUN apk update && apk add ca-certificates\n\n# note: using qemu with a multi-arch image results in redirects not working with wget\n# so let docker pull the image that matches the hosts architecture first and then pull the correct asset\nFROM busybox:1.36.1-musl\n\nRUN ARCH=$(uname -m) && \\\n    if [ \"$ARCH\" = \"x86_64\" ]; then \\\n        COSIGN_ARCH=\"amd64\"; \\\n    elif [ \"$ARCH\" = \"aarch64\" ]; then \\\n        COSIGN_ARCH=\"arm64\"; \\\n    else \\\n        echo \"Unsupported architecture: $ARCH\" && exit 1; \\\n    fi && \\\n    echo \"Downloading cosign for $COSIGN_ARCH\" && \\\n    wget https://github.com/sigstore/cosign/releases/latest/download/cosign-linux-${COSIGN_ARCH} && \\\n    mv cosign-linux-${COSIGN_ARCH} /bin/cosign && \\\n    chmod +x /bin/cosign\n\nCOPY --from=certs /etc/ssl/certs /etc/ssl/certs\n"
  },
  {
    "path": "test/install/environments/Dockerfile-ubuntu-20.04",
    "content": "FROM --platform=linux/amd64 ubuntu:20.04@sha256:33a5cc25d22c45900796a1aca487ad7a7cb09f09ea00b779e3b2026b4fc2faba\nRUN apt update -y && apt install make python3 curl unzip -y\nRUN LATEST_VERSION=$(curl https://api.github.com/repos/sigstore/cosign/releases/latest | grep tag_name | cut -d : -f2 | tr -d \"v\\\", \") && \\\n    curl -O -L \"https://github.com/sigstore/cosign/releases/latest/download/cosign_${LATEST_VERSION}_amd64.deb\" && \\\n    dpkg -i cosign_${LATEST_VERSION}_amd64.deb"
  },
  {
    "path": "test/install/github_test.sh",
    "content": ". test_harness.sh\n\n# check that we can extract single json values\ntest_extract_json_value() {\n  fixture=./testdata/github-api-grype-v0.32.0-release.json\n  content=$(cat ${fixture})\n\n  actual=$(extract_json_value \"${content}\" \"tag_name\")\n  assertEquals \"v0.32.0\" \"${actual}\" \"unable to find tag_name\"\n\n  actual=$(extract_json_value \"${content}\" \"id\")\n  assertEquals \"57501596\" \"${actual}\" \"unable to find tag_name\"\n}\n\nrun_test_case test_extract_json_value\n\n\n# check that we can extract github release tag from github api json\ntest_github_release_tag() {\n  fixture=./testdata/github-api-grype-v0.32.0-release.json\n  content=$(cat ${fixture})\n\n  actual=$(github_release_tag \"${content}\")\n  assertEquals \"v0.32.0\" \"${actual}\" \"unable to find release tag\"\n}\n\nrun_test_case test_github_release_tag\n\n\n# download a known good github release checksums and compare against a test-fixture\ntest_download_github_release_checksums() {\n  tmpdir=$(mktemp -d)\n\n  tag=v0.32.0\n  github_download=\"https://github.com/anchore/grype/releases/download/${tag}\"\n  name=${PROJECT_NAME}\n  version=$(tag_to_version \"${tag}\")\n\n  actual_filepath=$(download_github_release_checksums \"${github_download}\" \"${name}\" \"${version}\" \"${tmpdir}\")\n  assertFilesEqual \\\n    \"./testdata/grype_0.32.0_checksums.txt\" \\\n    \"${actual_filepath}\" \\\n    \"unable to find release tag\"\n\n  rm -rf -- \"$tmpdir\"\n}\n\nrun_test_case test_download_github_release_checksums\n\n\n# download a checksums file from a locally served-up snapshot directory and compare against the file in the snapshot dir\ntest_download_github_release_checksums_snapshot() {\n  tmpdir=$(mktemp -d)\n\n  github_download=$(snapshot_download_url)\n  name=${PROJECT_NAME}\n  version=$(snapshot_version)\n\n  actual_filepath=$(download_github_release_checksums \"${github_download}\" \"${name}\" \"${version}\" \"${tmpdir}\")\n  assertFilesEqual \\\n    \"$(snapshot_checksums_path)\" \\\n    \"${actual_filepath}\" \\\n    \"unable to find release tag\"\n\n  rm -rf -- \"$tmpdir\"\n}\n\nrun_test_case_with_snapshot_release test_download_github_release_checksums_snapshot"
  },
  {
    "path": "test/install/test_harness.sh",
    "content": "# disable using the install.sh entrypoint such that we can unit test\n# script functions without invoking main()\nTEST_INSTALL_SH=true\n\n. ../../install.sh\nset -u\n\nechoerr() {\n  echo \"$@\" 1>&2\n}\n\nprintferr() {\n  printf \"%s\" \"$*\" >&2\n}\n\nassertTrue() {\n  if eval \"$1\"; then\n    echo \"assertTrue failed: $2\"\n    exit 2\n  fi\n}\n\nassertFalse() {\n  if eval \"$1\"; then\n    echo \"assertFalse failed: $2\"\n    exit 2\n  fi\n}\n\nassertEquals() {\n  want=$1\n  got=$2\n  msg=$3\n  if [ \"$want\" != \"$got\" ]; then\n    echo \"assertEquals failed: want='$want' got='$got' $msg\"\n    exit 2\n  fi\n}\n\nassertFilesDoesNotExist() {\n  path=\"$1\"\n  msg=$2\n  if [ -f \"${path}\" ]; then\n    echo \"assertFilesDoesNotExist failed: path exists '$path': $msg\"\n    exit 2\n  fi\n}\n\nassertFileExists() {\n  path=\"$1\"\n  msg=$2\n  if [ ! -f \"${path}\" ]; then\n    echo \"assertFileExists failed: path does not exist '$path': $msg\"\n    exit 2\n  fi\n}\n\nassertFilesEqual() {\n  want=$1\n  got=$2\n  msg=$3\n\n  diff \"$1\" \"$2\"\n  if [ $? -ne 0 ]; then\n    echo \"assertFilesEqual failed: $msg\"\n    exit 2\n  fi\n}\n\nassertNotEquals() {\n  want=$1\n  got=$2\n  msg=$3\n  if [ \"$want\" = \"$got\" ]; then\n    echo \"assertNotEquals failed: want='$want' got='$got' $msg\"\n    exit 2\n  fi\n}\n\nlog_test_case() {\n  echo \"  running $@\"\n}\n\nrun_test_case_with_snapshot_release() {\n  log_test_case ${@:1}\n\n  worker_pid=$(setup_snapshot_server)\n  trap \"teardown_snapshot_server $worker_pid\" EXIT\n\n  # run test function with all arguments\n  ${@:1}\n\n  trap - EXIT\n  teardown_snapshot_server \"${worker_pid}\"\n}\n\nserve_port=8000\n\nsetup_snapshot_server() {\n  # if you want to see proof in the logs, feel free to adjust the redirection\n  python3 -m http.server --directory \"$(snapshot_dir)\" $serve_port &> /dev/null &\n  worker_pid=$!\n\n  echoerr \"serving up $(snapshot_dir) on port $serve_port\"\n\n  echoerr \"$(ls -1 $(snapshot_dir) | sed 's/^/  ▕―― /')\"\n\n  check_snapshots_server_ready\n\n  echoerr \"snapshot server ready! (worker=${worker_pid})\"\n\n  echo \"$worker_pid\"\n}\n\ncheck_snapshots_server_ready() {\n  i=0\n  until $(curl -m 3 --output /dev/null --silent --head --fail localhost:$serve_port/); do\n    sleep 1\n    ((i=i+1))\n    if [ \"$i\" -gt \"30\" ]; then\n      echoerr \"could not connect to local snapshot server! bailing...\"\n      exit 1\n    fi\n    printferr '.'\n  done\n}\n\nteardown_snapshot_server() {\n  worker_pid=\"$1\"\n  echoerr \"stopping worker=${worker_pid}\"\n  kill \"$worker_pid\"\n}\n\nsnapshot_version() {\n  partial=$(ls ../../snapshot/*_checksums.txt | grep -o \"_.*_checksums.txt\")\n  partial=\"${partial%_checksums.txt}\"\n  echo \"${partial#_}\"\n}\n\nsnapshot_download_url() {\n  echo \"localhost:${serve_port}\"\n}\n\nsnapshot_dir() {\n  echo \"../../snapshot\"\n}\n\nsnapshot_checksums_path() {\n  echo \"$(ls $(snapshot_dir)/*_checksums.txt)\"\n}\n\nsnapshot_assets_count() {\n  # example output before wc -l:\n\n  #  ../../snapshot/grype_0.32.0-SNAPSHOT-e5e847a_linux_arm64.deb\n  #  ../../snapshot/grype_0.32.0-SNAPSHOT-e5e847a_linux_arm64.tar.gz\n  #  ../../snapshot/grype_0.32.0-SNAPSHOT-e5e847a_linux_amd64.rpm\n  #  ../../snapshot/grype_0.32.0-SNAPSHOT-e5e847a_darwin_arm64.tar.gz\n  #  ../../snapshot/grype_0.32.0-SNAPSHOT-e5e847a_linux_amd64.deb\n  #  ../../snapshot/grype_0.32.0-SNAPSHOT-e5e847a_linux_arm64.rpm\n  #  ../../snapshot/grype_0.32.0-SNAPSHOT-e5e847a_darwin_amd64.zip\n  #  ../../snapshot/grype_0.32.0-SNAPSHOT-e5e847a_windows_amd64.zip\n  #  ../../snapshot/grype_0.32.0-SNAPSHOT-e5e847a_darwin_arm64.zip\n  #  ../../snapshot/grype_0.32.0-SNAPSHOT-e5e847a_linux_amd64.tar.gz\n  #  ../../snapshot/grype_0.32.0-SNAPSHOT-e5e847a_darwin_amd64.tar.gz\n\n  echo \"$(find ../../snapshot -maxdepth 1 -type f | grep 'grype_' | grep -v checksums | wc -l | tr -d '[:space:]')\"\n}\n\n\nsnapshot_assets_archive_count() {\n  # example output before wc -l:\n\n  #  ../../snapshot/grype_0.32.0-SNAPSHOT-e5e847a_linux_arm64.tar.gz\n  #  ../../snapshot/grype_0.32.0-SNAPSHOT-e5e847a_darwin_arm64.tar.gz\n  #  ../../snapshot/grype_0.32.0-SNAPSHOT-e5e847a_darwin_amd64.zip\n  #  ../../snapshot/grype_0.32.0-SNAPSHOT-e5e847a_windows_amd64.zip\n  #  ../../snapshot/grype_0.32.0-SNAPSHOT-e5e847a_darwin_arm64.zip\n  #  ../../snapshot/grype_0.32.0-SNAPSHOT-e5e847a_linux_amd64.tar.gz\n  #  ../../snapshot/grype_0.32.0-SNAPSHOT-e5e847a_darwin_amd64.tar.gz\n\n  echo \"$(find ../../snapshot -maxdepth 1  -type f | grep 'grype_' | grep 'tar\\|zip' | wc -l | tr -d '[:space:]')\"\n}\n\n\nrun_test_case() {\n  log_test_case ${@:1}\n  ${@:1}\n}\n"
  },
  {
    "path": "test/install/testdata/assets/invalid/.gitignore",
    "content": "!grype_0.78.0_linux_arm64.tar.gz"
  },
  {
    "path": "test/install/testdata/assets/invalid/checksums.txt",
    "content": "cb4f335e106532b927dac14d4857b7be2333ec1b8bd2aea82be3f9112bb2728f  grype_0.78.0_darwin_amd64.tar.gz\n51249ee801b41272218252af2c72a644a7ef037b0b27d7b0eae3b55361e82cf6  grype_0.78.0_darwin_arm64.tar.gz\ncc3cf4fcc856898fcd05ba2b8590de06e380b958fea5957b0a3e4eff5e8aeeaf  grype_0.78.0_linux_amd64.deb\n3a9af0f08d1aaf15853f8292be0aa896639e09328416a50d5deaefef894bab61  grype_0.78.0_linux_amd64.rpm\n6037fd3763b6112302b98db559bb5390fbb06f0011c0585a4be03ca851daa838  grype_0.78.0_linux_amd64.tar.gz\n0f2e3e07be5b5eb08637ac9071f4b0f95f8b4c7c7ea66592852ca82fea4adb93  grype_0.78.0_linux_arm64.deb\n89a7f68676a18eb9dc0b706036dacbfb8b78833ed0950b8c6fa63ac159b93781  grype_0.78.0_linux_arm64.rpm\n0d560e860d6f68cf23c8f0df1f5124ef6d3d8d57abb57a0dae3f951b7772822e  grype_0.78.0_linux_arm64.tar.gz\n7b22795114e27c3d147998edc9e803988d7c987cad2623d7fb1d7bf730b4e176  grype_0.78.0_linux_ppc64le.deb\n91813ac66ad2ef761ce9629eb4213988de594abd4cab9148a85d71bfa80f6699  grype_0.78.0_linux_ppc64le.rpm\ncb923a08fb9f367410190675f187b6aa5a04c1d538f055700c89c8350b826dcb  grype_0.78.0_linux_ppc64le.tar.gz\n4beb9d31d61df6212c3f996fc8f33239520eeea083dbe70b0969f23739d44dd1  grype_0.78.0_linux_s390x.deb\n28f723777b1a136d2fadbdca0ae5e7e9b26f9bd08114095dbd2898def7e8b0b6  grype_0.78.0_linux_s390x.rpm\n6c3e7e54ce40aa33ca5fc774c3be664fb910a99aa77b1e5e3cee77156e8399f4  grype_0.78.0_linux_s390x.tar.gz\n31ca5d02a75dbb8f3361ac9836a2384013a67a7d9e2e437cb80e4ddfbd4c7812  grype_0.78.0_windows_amd64.zip\n"
  },
  {
    "path": "test/install/testdata/assets/valid/.gitignore",
    "content": "!grype_0.78.0_linux_arm64.tar.gz"
  },
  {
    "path": "test/install/testdata/assets/valid/checksums.txt",
    "content": "cb4f335e106532b927dac14d4857b7be2333ec1b8bd2aea82be3f9112bb2728f  grype_0.78.0_darwin_amd64.tar.gz\n51249ee801b41272218252af2c72a644a7ef037b0b27d7b0eae3b55361e82cf6  grype_0.78.0_darwin_arm64.tar.gz\ncc3cf4fcc856898fcd05ba2b8590de06e380b958fea5957b0a3e4eff5e8aeeaf  grype_0.78.0_linux_amd64.deb\n3a9af0f08d1aaf15853f8292be0aa896639e09328416a50d5deaefef894bab61  grype_0.78.0_linux_amd64.rpm\n6037fd3763b6112302b98db559bb5390fbb06f0011c0585a4be03ca851daa838  grype_0.78.0_linux_amd64.tar.gz\n0f2e3e07be5b5eb08637ac9071f4b0f95f8b4c7c7ea66592852ca82fea4adb93  grype_0.78.0_linux_arm64.deb\n89a7f68676a18eb9dc0b706036dacbfb8b78833ed0950b8c6fa63ac159b93781  grype_0.78.0_linux_arm64.rpm\n8d57abb57a0dae3ff23c8f0df1f51951b7772822e0d560e860d6f68c24ef6d3d  grype_0.78.0_linux_arm64.tar.gz\n7b22795114e27c3d147998edc9e803988d7c987cad2623d7fb1d7bf730b4e176  grype_0.78.0_linux_ppc64le.deb\n91813ac66ad2ef761ce9629eb4213988de594abd4cab9148a85d71bfa80f6699  grype_0.78.0_linux_ppc64le.rpm\ncb923a08fb9f367410190675f187b6aa5a04c1d538f055700c89c8350b826dcb  grype_0.78.0_linux_ppc64le.tar.gz\n4beb9d31d61df6212c3f996fc8f33239520eeea083dbe70b0969f23739d44dd1  grype_0.78.0_linux_s390x.deb\n28f723777b1a136d2fadbdca0ae5e7e9b26f9bd08114095dbd2898def7e8b0b6  grype_0.78.0_linux_s390x.rpm\n6c3e7e54ce40aa33ca5fc774c3be664fb910a99aa77b1e5e3cee77156e8399f4  grype_0.78.0_linux_s390x.tar.gz\n31ca5d02a75dbb8f3361ac9836a2384013a67a7d9e2e437cb80e4ddfbd4c7812  grype_0.78.0_windows_amd64.zip\n"
  },
  {
    "path": "test/install/testdata/github-api-grype-v0.32.0-release.json",
    "content": "{\"id\":57501596,\"tag_name\":\"v0.32.0\",\"update_url\":\"/anchore/grype/releases/tag/v0.32.0\",\"update_authenticity_token\":\"7XbNZgRHpbHegdv-xRlbe84Y983YgyXa3YKWwv_e0ocqTHagsHq5dxCTQUQnuX3vbsgdWQU3A3__hkVNhKGHSg\",\"delete_url\":\"/anchore/grype/releases/tag/v0.32.0\",\"delete_authenticity_token\":\"6tLaRtXKUc-zz4tHIwCbbD7CksxIHK5imZE1gnA39oVCe6fYux5a8cPD9J52kGUzM1Hs9JPBjceG7yyszBk_2A\",\"edit_url\":\"/anchore/grype/releases/edit/v0.32.0\"}\n"
  },
  {
    "path": "test/install/testdata/grype_0.32.0-SNAPSHOT-d461f63_checksums.txt",
    "content": "250dddf3338d34012b55b4167b72f8bc87944e61aee35879342206a115a0f64b  grype_0.32.0-SNAPSHOT-d461f63_darwin_amd64.tar.gz\n4b2973604085c14bc4c452f5354110384d371f0d5c3f93c0e3a44498f54283d7  grype_0.32.0-SNAPSHOT-d461f63_linux_amd64.rpm\n569b040bde6d369b9e3b96fb3d9d7ee5aa11267f3aa91fad3d8f4095f1cee150  grype_0.32.0-SNAPSHOT-d461f63_darwin_arm64.tar.gz\n5c666286bca9d8c84f7355d5afe720186b0a06bed23ac0518a35a79ff905de28  grype_0.32.0-SNAPSHOT-d461f63_linux_arm64.tar.gz\ndd1d7492e7a7db9a765a02927b0d019d8f9facb1173ae7c245cd06fefedddfd0  grype_0.32.0-SNAPSHOT-d461f63_windows_amd64.zip\ndd4e5857856b4655511a75911fd7b53a3ebb9d2f584ae3c7ff7f52ad0dd93745  grype_0.32.0-SNAPSHOT-d461f63_linux_amd64.tar.gz\ndfe9d8212def2eb3685bacf3c77f664830680a475eb6356e67c96abe4af00e74  grype_0.32.0-SNAPSHOT-d461f63_linux_arm64.rpm\ne1efed13fa93c207b773cbc2a9252b87049e1a826bacb77b756a20a13a29e465  grype_0.32.0-SNAPSHOT-d461f63_linux_arm64.deb\nef2725de0e154059fb59c6268e68fd0ba3a7ce5b23e604166140f284b54ef9b4  grype_0.32.0-SNAPSHOT-d461f63_linux_amd64.deb\n"
  },
  {
    "path": "test/install/testdata/grype_0.32.0_checksums.txt",
    "content": "250dddf3338d34012b55b4167b72f8bc87944e61aee35879342206a115a0f64b  grype_0.32.0_darwin_amd64.tar.gz\n4b2973604085c14bc4c452f5354110384d371f0d5c3f93c0e3a44498f54283d7  grype_0.32.0_linux_amd64.rpm\n569b040bde6d369b9e3b96fb3d9d7ee5aa11267f3aa91fad3d8f4095f1cee150  grype_0.32.0_darwin_arm64.tar.gz\n5c666286bca9d8c84f7355d5afe720186b0a06bed23ac0518a35a79ff905de28  grype_0.32.0_linux_arm64.tar.gz\ndd1d7492e7a7db9a765a02927b0d019d8f9facb1173ae7c245cd06fefedddfd0  grype_0.32.0_windows_amd64.zip\ndd4e5857856b4655511a75911fd7b53a3ebb9d2f584ae3c7ff7f52ad0dd93745  grype_0.32.0_linux_amd64.tar.gz\ndfe9d8212def2eb3685bacf3c77f664830680a475eb6356e67c96abe4af00e74  grype_0.32.0_linux_arm64.rpm\ne1efed13fa93c207b773cbc2a9252b87049e1a826bacb77b756a20a13a29e465  grype_0.32.0_linux_arm64.deb\nef2725de0e154059fb59c6268e68fd0ba3a7ce5b23e604166140f284b54ef9b4  grype_0.32.0_linux_amd64.deb\n"
  },
  {
    "path": "test/integration/compare_sbom_input_vs_lib_test.go",
    "content": "package integration\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/scylladb/go-set/strset\"\n\t\"github.com/stretchr/testify/assert\"\n\n\t\"github.com/anchore/grype/grype\"\n\t\"github.com/anchore/grype/grype/db/v6/distribution\"\n\t\"github.com/anchore/grype/grype/db/v6/installation\"\n\t\"github.com/anchore/grype/internal/log\"\n\t\"github.com/anchore/syft/syft/format/spdxjson\"\n\t\"github.com/anchore/syft/syft/format/spdxtagvalue\"\n\t\"github.com/anchore/syft/syft/format/syftjson\"\n\tsyftPkg \"github.com/anchore/syft/syft/pkg\"\n\t\"github.com/anchore/syft/syft/sbom\"\n\t\"github.com/anchore/syft/syft/source\"\n)\n\nfunc getLatestURL() string {\n\tif value, ok := os.LookupEnv(\"GRYPE_DB_UPDATE_URL\"); ok {\n\t\treturn value\n\t}\n\treturn distribution.DefaultConfig().LatestURL\n}\n\nfunc must(e sbom.FormatEncoder, err error) sbom.FormatEncoder {\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn e\n}\n\nfunc TestCompareSBOMInputToLibResults(t *testing.T) {\n\t// get a grype DB\n\tstore, status, err := grype.LoadVulnerabilityDB(distribution.Config{\n\t\tLatestURL: getLatestURL(),\n\t}, installation.Config{\n\t\tDBRootDir:        \"testdata/grype-db\",\n\t\tValidateChecksum: false,\n\t}, true)\n\tassert.NoError(t, err)\n\tdefer log.CloseAndLogError(store, status.Path)\n\n\tdefinedPkgTypes := strset.New()\n\tfor _, p := range syftPkg.AllPkgs {\n\t\tdefinedPkgTypes.Add(string(p))\n\t}\n\t// exceptions: rust, php, dart, msrc (kb), etc. are not under test\n\tdefinedPkgTypes.Remove(\n\t\tstring(syftPkg.BinaryPkg), // these are removed due to overlap-by-file-ownership\n\t\tstring(syftPkg.BitnamiPkg),\n\t\tstring(syftPkg.PhpPeclPkg),\n\t\tstring(syftPkg.PhpPearPkg),\n\t\tstring(syftPkg.RustPkg),\n\t\tstring(syftPkg.KbPkg),\n\t\tstring(syftPkg.DartPubPkg),\n\t\tstring(syftPkg.DotnetPkg),\n\t\tstring(syftPkg.PhpComposerPkg),\n\t\tstring(syftPkg.ConanPkg),\n\t\tstring(syftPkg.CondaPkg),\n\t\tstring(syftPkg.HexPkg),\n\t\tstring(syftPkg.PortagePkg),\n\t\tstring(syftPkg.HomebrewPkg),\n\t\tstring(syftPkg.CocoapodsPkg),\n\t\tstring(syftPkg.HackagePkg),\n\t\tstring(syftPkg.NixPkg),\n\t\tstring(syftPkg.JenkinsPluginPkg), // package type cannot be inferred for all formats\n\t\tstring(syftPkg.LinuxKernelPkg),\n\t\tstring(syftPkg.LinuxKernelModulePkg),\n\t\tstring(syftPkg.ModelPkg),\n\t\tstring(syftPkg.OpamPkg),\n\t\tstring(syftPkg.Rpkg),\n\t\tstring(syftPkg.SwiplPackPkg),\n\t\tstring(syftPkg.SwiftPkg),\n\t\tstring(syftPkg.GithubActionPkg),\n\t\tstring(syftPkg.GithubActionWorkflowPkg),\n\t\tstring(syftPkg.GraalVMNativeImagePkg),\n\t\tstring(syftPkg.ErlangOTPPkg),\n\t\tstring(syftPkg.WordpressPluginPkg), // TODO: remove me when there is a matcher for this merged in https://github.com/anchore/grype/pull/1553\n\t\tstring(syftPkg.LuaRocksPkg),\n\t\tstring(syftPkg.TerraformPkg),\n\t)\n\tobservedPkgTypes := strset.New()\n\ttestCases := []struct {\n\t\tname   string\n\t\timage  string\n\t\tformat sbom.FormatEncoder\n\t\t// knownFormatLossMatches lists match keys (vuln-pkg-version) that are expected to\n\t\t// appear only in results from this SBOM format (and not from an image scan) due to\n\t\t// metadata loss during format encoding. For example, SPDX does not preserve APK file\n\t\t// ownership metadata, so distro-package-fixed ignore rules cannot be scoped to owned\n\t\t// paths and are not emitted — causing matches that the image scan correctly suppresses\n\t\t// to remain visible. This does NOT apply to syft-json, which preserves all metadata.\n\t\tknownFormatLossMatches []string\n\t}{\n\t\t{\n\t\t\timage:  \"anchore/test_images:vulnerabilities-alpine\",\n\t\t\tformat: syftjson.NewFormatEncoder(),\n\t\t\tname:   \"alpine-syft-json\",\n\t\t},\n\n\t\t{\n\t\t\timage:  \"anchore/test_images:vulnerabilities-alpine\",\n\t\t\tformat: must(spdxjson.NewFormatEncoderWithConfig(spdxjson.DefaultEncoderConfig())),\n\t\t\tname:   \"alpine-spdx-json\",\n\t\t},\n\n\t\t{\n\t\t\timage:  \"anchore/test_images:vulnerabilities-alpine\",\n\t\t\tformat: must(spdxtagvalue.NewFormatEncoderWithConfig(spdxtagvalue.DefaultEncoderConfig())),\n\t\t\tname:   \"alpine-spdx-tag-value\",\n\t\t},\n\n\t\t{\n\t\t\timage:  \"anchore/test_images:gems\",\n\t\t\tformat: syftjson.NewFormatEncoder(),\n\t\t\tname:   \"gems-syft-json\",\n\t\t},\n\n\t\t{\n\t\t\timage:  \"anchore/test_images:gems\",\n\t\t\tformat: must(spdxjson.NewFormatEncoderWithConfig(spdxjson.DefaultEncoderConfig())),\n\t\t\tname:   \"gems-spdx-json\",\n\t\t\t// SPDX does not preserve APK file ownership metadata, so distro-package-fixed\n\t\t\t// ignore rules cannot be scoped to owned paths and are not emitted from the SBOM.\n\t\t\tknownFormatLossMatches: []string{\"GHSA-8cr8-4vfw-mr7h-rexml-3.2.3.1\"},\n\t\t},\n\n\t\t{\n\t\t\timage:  \"anchore/test_images:gems\",\n\t\t\tformat: must(spdxtagvalue.NewFormatEncoderWithConfig(spdxtagvalue.DefaultEncoderConfig())),\n\t\t\tname:   \"gems-spdx-tag-value\",\n\t\t\t// SPDX does not preserve APK file ownership metadata, so distro-package-fixed\n\t\t\t// ignore rules cannot be scoped to owned paths and are not emitted from the SBOM.\n\t\t\tknownFormatLossMatches: []string{\"GHSA-8cr8-4vfw-mr7h-rexml-3.2.3.1\"},\n\t\t},\n\n\t\t{\n\t\t\timage:  \"anchore/test_images:vulnerabilities-debian\",\n\t\t\tformat: syftjson.NewFormatEncoder(),\n\t\t\tname:   \"debian-syft-json\",\n\t\t},\n\n\t\t{\n\t\t\timage:  \"anchore/test_images:vulnerabilities-debian\",\n\t\t\tformat: must(spdxjson.NewFormatEncoderWithConfig(spdxjson.DefaultEncoderConfig())),\n\t\t\tname:   \"debian-spdx-json\",\n\t\t},\n\n\t\t{\n\t\t\timage:  \"anchore/test_images:vulnerabilities-debian\",\n\t\t\tformat: must(spdxtagvalue.NewFormatEncoderWithConfig(spdxtagvalue.DefaultEncoderConfig())),\n\t\t\tname:   \"debian-spdx-tag-value\",\n\t\t},\n\n\t\t{\n\t\t\timage:  \"anchore/test_images:vulnerabilities-centos\",\n\t\t\tformat: syftjson.NewFormatEncoder(),\n\t\t\tname:   \"centos-syft-json\",\n\t\t},\n\n\t\t{\n\t\t\timage:  \"anchore/test_images:vulnerabilities-centos\",\n\t\t\tformat: must(spdxjson.NewFormatEncoderWithConfig(spdxjson.DefaultEncoderConfig())),\n\t\t\tname:   \"centos-spdx-json\",\n\t\t},\n\n\t\t{\n\t\t\timage:  \"anchore/test_images:vulnerabilities-centos\",\n\t\t\tformat: must(spdxtagvalue.NewFormatEncoderWithConfig(spdxtagvalue.DefaultEncoderConfig())),\n\t\t\tname:   \"centos-spdx-tag-value\",\n\t\t},\n\n\t\t{\n\t\t\timage:  \"anchore/test_images:npm\",\n\t\t\tformat: syftjson.NewFormatEncoder(),\n\t\t\tname:   \"npm-syft-json\",\n\t\t},\n\n\t\t{\n\t\t\timage:  \"anchore/test_images:npm\",\n\t\t\tformat: must(spdxjson.NewFormatEncoderWithConfig(spdxjson.DefaultEncoderConfig())),\n\t\t\tname:   \"npm-spdx-json\",\n\t\t},\n\n\t\t{\n\t\t\timage:  \"anchore/test_images:npm\",\n\t\t\tformat: must(spdxtagvalue.NewFormatEncoderWithConfig(spdxtagvalue.DefaultEncoderConfig())),\n\t\t\tname:   \"npm-spdx-tag-value\",\n\t\t},\n\n\t\t{\n\t\t\timage:  \"anchore/test_images:java\",\n\t\t\tformat: syftjson.NewFormatEncoder(),\n\t\t\tname:   \"java-syft-json\",\n\t\t},\n\n\t\t{\n\t\t\timage:  \"anchore/test_images:java\",\n\t\t\tformat: must(spdxjson.NewFormatEncoderWithConfig(spdxjson.DefaultEncoderConfig())),\n\t\t\tname:   \"java-spdx-json\",\n\t\t},\n\n\t\t{\n\t\t\timage:  \"anchore/test_images:java\",\n\t\t\tformat: must(spdxtagvalue.NewFormatEncoderWithConfig(spdxtagvalue.DefaultEncoderConfig())),\n\t\t\tname:   \"java-spdx-tag-value\",\n\t\t},\n\n\t\t{\n\t\t\timage:  \"anchore/test_images:golang-56d52bc\",\n\t\t\tformat: syftjson.NewFormatEncoder(),\n\t\t\tname:   \"go-syft-json\",\n\t\t},\n\n\t\t{\n\t\t\timage:  \"anchore/test_images:golang-56d52bc\",\n\t\t\tformat: must(spdxjson.NewFormatEncoderWithConfig(spdxjson.DefaultEncoderConfig())),\n\t\t\tname:   \"go-spdx-json\",\n\t\t},\n\n\t\t{\n\t\t\timage:  \"anchore/test_images:golang-56d52bc\",\n\t\t\tformat: must(spdxtagvalue.NewFormatEncoderWithConfig(spdxtagvalue.DefaultEncoderConfig())),\n\t\t\tname:   \"go-spdx-tag-value\",\n\t\t},\n\n\t\t{\n\t\t\timage:  \"anchore/test_images:arch\",\n\t\t\tformat: syftjson.NewFormatEncoder(),\n\t\t\tname:   \"arch-syft-json\",\n\t\t},\n\n\t\t{\n\t\t\timage:  \"anchore/test_images:arch\",\n\t\t\tformat: must(spdxjson.NewFormatEncoderWithConfig(spdxjson.DefaultEncoderConfig())),\n\t\t\tname:   \"arch-spdx-json\",\n\t\t},\n\n\t\t{\n\t\t\timage:  \"anchore/test_images:arch\",\n\t\t\tformat: must(spdxtagvalue.NewFormatEncoderWithConfig(spdxtagvalue.DefaultEncoderConfig())),\n\t\t\tname:   \"arch-spdx-tag-value\",\n\t\t},\n\t}\n\tfor _, tc := range testCases {\n\t\timageArchive := PullThroughImageCache(t, tc.image)\n\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\t// get SBOM from syft, write to temp file\n\t\t\tsbomBytes := getSyftSBOM(t, imageArchive, \"docker-archive\", tc.format)\n\t\t\tsbomFile, err := os.CreateTemp(\"\", \"\")\n\t\t\tassert.NoError(t, err)\n\t\t\tt.Cleanup(func() {\n\t\t\t\tassert.NoError(t, os.Remove(sbomFile.Name()))\n\t\t\t})\n\t\t\t_, err = sbomFile.WriteString(sbomBytes)\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.NoError(t, sbomFile.Close())\n\n\t\t\t// get vulns (sbom)\n\t\t\tmatchesFromSbom, _, pkgsFromSbom, err := grype.FindVulnerabilities(store, fmt.Sprintf(\"sbom:%s\", sbomFile.Name()), source.SquashedScope, nil)\n\t\t\tassert.NoError(t, err)\n\n\t\t\t// get vulns (image)\n\t\t\timageSource := fmt.Sprintf(\"docker-archive:%s\", imageArchive)\n\t\t\tmatchesFromImage, _, _, err := grype.FindVulnerabilities(store, imageSource, source.SquashedScope, nil)\n\t\t\tassert.NoError(t, err)\n\n\t\t\t// compare packages (shallow)\n\t\t\tmatchSetFromSbom := getMatchSet(matchesFromSbom)\n\t\t\tmatchSetFromImage := getMatchSet(matchesFromImage)\n\n\t\t\tsbomOnly := strset.Difference(matchSetFromSbom, matchSetFromImage)\n\t\t\tif len(tc.knownFormatLossMatches) > 0 {\n\t\t\t\tsbomOnly.Remove(tc.knownFormatLossMatches...)\n\t\t\t}\n\t\t\tassert.Empty(t, sbomOnly.List(), \"vulnerabilities present only in results when using sbom as input\")\n\t\t\tassert.Empty(t, strset.Difference(matchSetFromImage, matchSetFromSbom).List(), \"vulnerabilities present only in results when using image as input\")\n\n\t\t\t// track all covered package types (for use after the test)\n\t\t\tfor _, p := range pkgsFromSbom {\n\t\t\t\tobservedPkgTypes.Add(string(p.Type))\n\t\t\t}\n\n\t\t})\n\t}\n\n\t// ensure we've covered all package types (-rust, -kb)\n\tunobservedPackageTypes := strset.Difference(definedPkgTypes, observedPkgTypes)\n\tassert.Empty(t, unobservedPackageTypes.List(), \"not all package type were covered in testing\")\n}\n"
  },
  {
    "path": "test/integration/db_mock_test.go",
    "content": "package integration\n\nimport (\n\t\"github.com/anchore/grype/grype/version\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n\t\"github.com/anchore/grype/grype/vulnerability/mock\"\n\t\"github.com/anchore/syft/syft/cpe\"\n)\n\nfunc newMockDbProvider() vulnerability.Provider {\n\treturn mock.VulnerabilityProvider([]vulnerability.Vulnerability{\n\t\t{\n\t\t\tReference: vulnerability.Reference{\n\t\t\t\tID:        \"CVE-jdk\",\n\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t},\n\t\t\tPackageName: \"jdk\",\n\t\t\tConstraint:  version.MustGetConstraint(\"< 1.8.0_401\", version.JVMFormat),\n\t\t\tCPEs:        []cpe.CPE{cpe.Must(\"cpe:2.3:a:oracle:jdk:*:*:*:*:*:*:*:*\", \"\")},\n\t\t},\n\t\t{\n\t\t\tReference: vulnerability.Reference{\n\t\t\t\tID:        \"CVE-2024-0000\",\n\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t},\n\t\t\tPackageName: \"libvncserver\",\n\t\t\tConstraint:  version.MustGetConstraint(\"< 0.9.10\", version.UnknownFormat),\n\t\t\tCPEs:        []cpe.CPE{cpe.Must(\"cpe:2.3:a:lib_vnc_project-(server):libvncserver:*:*:*:*:*:*:*:*\", \"\")},\n\t\t},\n\t\t{\n\t\t\tReference: vulnerability.Reference{\n\t\t\t\tID:        \"CVE-bogus-my-package-1\",\n\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t},\n\t\t\tPackageName: \"my-package\",\n\t\t\tConstraint:  version.MustGetConstraint(\"< 2.0\", version.UnknownFormat),\n\t\t\tCPEs:        []cpe.CPE{cpe.Must(\"cpe:2.3:a:bogus:my-package:*:*:*:*:*:*:something:*\", \"\")},\n\t\t},\n\t\t{\n\t\t\tReference: vulnerability.Reference{\n\t\t\t\tID:        \"CVE-bogus-my-package-2-never-match\",\n\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t},\n\t\t\tPackageName: \"my-package\",\n\t\t\tConstraint:  version.MustGetConstraint(\"< 2.0\", version.UnknownFormat),\n\t\t\tCPEs:        []cpe.CPE{cpe.Must(\"cpe:2.3:a:something-wrong:my-package:*:*:*:*:*:*:something:*\", \"\")},\n\t\t},\n\t\t{\n\t\t\tReference: vulnerability.Reference{\n\t\t\t\tID:        \"CVE-2024-0000\",\n\t\t\t\tNamespace: \"alpine:distro:alpine:3.12\",\n\t\t\t},\n\t\t\tPackageName: \"libvncserver\",\n\t\t\tConstraint:  version.MustGetConstraint(\"< 0.9.10\", version.UnknownFormat),\n\t\t},\n\t\t{\n\t\t\tReference: vulnerability.Reference{\n\t\t\t\tID:        \"CVE-azure-autorest-vuln-false-positive\",\n\t\t\t\tNamespace: \"alpine:distro:alpine:3.12\",\n\t\t\t},\n\t\t\tPackageName: \"ko\",\n\t\t\tConstraint:  version.MustGetConstraint(\"< 0\", version.ApkFormat),\n\t\t},\n\t\t{\n\t\t\tReference: vulnerability.Reference{\n\t\t\t\tID:        \"CVE-npm-false-positive-in-apk-subpackage\",\n\t\t\t\tNamespace: \"alpine:distro:alpine:3.12\",\n\t\t\t},\n\t\t\tPackageName: \"npm-apk-package-with-false-positive\",\n\t\t\tConstraint:  version.MustGetConstraint(\"< 0\", version.ApkFormat),\n\t\t},\n\t\t{\n\t\t\tReference: vulnerability.Reference{\n\t\t\t\tID:        \"CVE-gentoo-skopeo\",\n\t\t\t\tNamespace: \"gentoo:distro:gentoo:2.8\",\n\t\t\t},\n\t\t\tPackageName: \"app-containers/skopeo\",\n\t\t\tConstraint:  version.MustGetConstraint(\"< 1.6.0\", version.UnknownFormat),\n\t\t},\n\t\t{\n\t\t\tReference: vulnerability.Reference{\n\t\t\t\tID:        \"CVE-coverage-main-module-vuln\",\n\t\t\t\tNamespace: \"github:language:go\",\n\t\t\t},\n\t\t\tPackageName: \"github.com/anchore/coverage\",\n\t\t\tConstraint:  version.MustGetConstraint(\"< 1.4.0\", version.UnknownFormat),\n\t\t},\n\t\t{\n\t\t\tReference: vulnerability.Reference{\n\t\t\t\tID:        \"CVE-uuid-vuln\",\n\t\t\t\tNamespace: \"github:language:go\",\n\t\t\t},\n\t\t\tPackageName: \"github.com/google/uuid\",\n\t\t\tConstraint:  version.MustGetConstraint(\"< 1.4.0\", version.UnknownFormat),\n\t\t},\n\t\t{\n\t\t\tReference: vulnerability.Reference{\n\t\t\t\tID:        \"CVE-azure-autorest-vuln-false-positive\",\n\t\t\t\tNamespace: \"github:language:go\",\n\t\t\t},\n\t\t\tPackageName: \"github.com/azure/go-autorest/autorest\",\n\t\t\tConstraint:  version.MustGetConstraint(\"< 0.11.30\", version.UnknownFormat),\n\t\t},\n\t\t{\n\t\t\tReference: vulnerability.Reference{\n\t\t\t\tID:        \"CVE-bogus-my-package-2-idris\",\n\t\t\t\tNamespace: \"github:language:idris\",\n\t\t\t},\n\t\t\tPackageName: \"my-package\",\n\t\t\tConstraint:  version.MustGetConstraint(\"< 2.0\", version.UnknownFormat),\n\t\t},\n\t\t{\n\t\t\tReference: vulnerability.Reference{\n\t\t\t\tID:        \"CVE-javascript-validator\",\n\t\t\t\tNamespace: \"github:language:javascript\",\n\t\t\t},\n\t\t\tPackageName: \"npm\",\n\t\t\tConstraint:  version.MustGetConstraint(\"> 5, < 7.2.1\", version.UnknownFormat),\n\t\t},\n\t\t{\n\t\t\tReference: vulnerability.Reference{\n\t\t\t\tID:        \"CVE-npm-false-positive-in-apk-subpackage\",\n\t\t\t\tNamespace: \"github:language:javascript\",\n\t\t\t},\n\t\t\tPackageName: \"npm-apk-subpackage-with-false-positive\",\n\t\t\tConstraint:  version.MustGetConstraint(\"< 2.0.0\", version.UnknownFormat),\n\t\t},\n\t\t{\n\t\t\tReference: vulnerability.Reference{\n\t\t\t\tID:        \"CVE-python-pygments\",\n\t\t\t\tNamespace: \"github:language:python\",\n\t\t\t},\n\t\t\tPackageName: \"pygments\",\n\t\t\tConstraint:  version.MustGetConstraint(\"< 2.6.2\", version.PythonFormat),\n\t\t},\n\t\t//{\n\t\t//\tReference: vulnerability.Reference{\n\t\t//\t\tID:        \"CVE-my-package-python\",\n\t\t//\t\tNamespace: \"github:language:python\",\n\t\t//\t},\n\t\t//\tPackageName: \"my-package\",\n\t\t//},\n\t\t{\n\t\t\tReference: vulnerability.Reference{\n\t\t\t\tID:        \"CVE-ruby-bundler\",\n\t\t\t\tNamespace: \"github:language:ruby\", // github:language:gem ??\n\t\t\t},\n\t\t\tPackageName: \"bundler\",\n\t\t\tConstraint:  version.MustGetConstraint(\"> 2.0.0, <= 2.1.4\", version.UnknownFormat), //version.GemFormat),\n\t\t},\n\t\t{\n\t\t\tReference: vulnerability.Reference{\n\t\t\t\tID:        \"CVE-java-example-java-app\",\n\t\t\t\tNamespace: \"github:language:java\",\n\t\t\t},\n\t\t\tPackageName: \"org.anchore:example-java-app-maven\",\n\t\t\tConstraint:  version.MustGetConstraint(\">= 0.0.1, < 1.2.0\", version.UnknownFormat),\n\t\t},\n\t\t{\n\t\t\tReference: vulnerability.Reference{\n\t\t\t\tID:        \"CVE-dotnet-sample\",\n\t\t\t\tNamespace: \"github:language:dotnet\",\n\t\t\t},\n\t\t\tPackageName: \"awssdk.core\",\n\t\t\tConstraint:  version.MustGetConstraint(\">= 3.7.0.0, < 3.7.12.0\", version.UnknownFormat), // was: \"dotnet\"\n\t\t},\n\t\t{\n\t\t\tReference: vulnerability.Reference{\n\t\t\t\tID:        \"CVE-haskell-sample\",\n\t\t\t\tNamespace: \"github:language:haskell\",\n\t\t\t},\n\t\t\tPackageName: \"shellcheck\",\n\t\t\tConstraint:  version.MustGetConstraint(\"< 0.9.0\", version.UnknownFormat), // was: \"haskell\"\n\t\t},\n\t\t{\n\t\t\tReference: vulnerability.Reference{\n\t\t\t\tID:        \"CVE-hex-plug\",\n\t\t\t\tNamespace: \"github:language:elixir\",\n\t\t\t},\n\t\t\tPackageName: \"plug\",\n\t\t\tConstraint:  version.MustGetConstraint(\"< 1.12.0\", version.UnknownFormat),\n\t\t},\n\t\t{\n\t\t\tReference: vulnerability.Reference{\n\t\t\t\tID:        \"CVE-rust-sample-1\",\n\t\t\t\tNamespace: \"github:language:rust\",\n\t\t\t},\n\t\t\tPackageName: \"hello-auditable\",\n\t\t\tConstraint:  version.MustGetConstraint(\"< 0.2.0\", version.UnknownFormat),\n\t\t},\n\t\t{\n\t\t\tReference: vulnerability.Reference{\n\t\t\t\tID:        \"CVE-rust-sample-2\",\n\t\t\t\tNamespace: \"github:language:rust\",\n\t\t\t},\n\t\t\tPackageName: \"auditable\",\n\t\t\tConstraint:  version.MustGetConstraint(\"< 0.2.0\", version.UnknownFormat),\n\t\t},\n\t\t{\n\t\t\tReference: vulnerability.Reference{\n\t\t\t\tID:        \"CVE-dpkg-apt\",\n\t\t\t\tNamespace: \"debian:distro:debian:8\",\n\t\t\t},\n\t\t\tPackageName: \"apt-dev\",\n\t\t\tConstraint:  version.MustGetConstraint(\"<= 1.8.2\", version.DebFormat), // was: \"dpkg\"\n\t\t},\n\t\t{\n\t\t\tReference: vulnerability.Reference{\n\t\t\t\tID:        \"CVE-rpmdb-dive\",\n\t\t\t\tNamespace: \"redhat:distro:redhat:8\",\n\t\t\t},\n\t\t\tPackageName: \"dive\",\n\t\t\tConstraint:  version.MustGetConstraint(\"<= 1.0.42\", version.RpmFormat),\n\t\t},\n\t\t{\n\t\t\tReference: vulnerability.Reference{\n\t\t\t\tID:        \"CVE-2016-3333\",\n\t\t\t\tNamespace: \"msrc:distro:windows:10816\",\n\t\t\t},\n\t\t\tPackageName: \"10816\",\n\t\t\tConstraint:  version.MustGetConstraint(\"3200970 || 878787 || base\", version.KBFormat),\n\t\t},\n\t\t{\n\t\t\tReference: vulnerability.Reference{\n\t\t\t\tID:        \"CVE-rpmdb-dive\",\n\t\t\t\tNamespace: \"sles:distro:sles:12.5\",\n\t\t\t},\n\t\t\tPackageName: \"dive\",\n\t\t\tConstraint:  version.MustGetConstraint(\"<= 1.0.42\", version.RpmFormat),\n\t\t},\n\t\t{\n\t\t\tReference: vulnerability.Reference{\n\t\t\t\tID:        \"CVE-arch-xz-backdoor\",\n\t\t\t\tNamespace: \"arch:distro:arch:rolling\",\n\t\t\t},\n\t\t\tPackageName: \"xz\",\n\t\t\tConstraint:  version.MustGetConstraint(\"< 5.6.1-2\", version.PacmanFormat),\n\t\t},\n\t}...)\n}\n"
  },
  {
    "path": "test/integration/match_by_image_test.go",
    "content": "package integration\n\nimport (\n\t\"context\"\n\t\"sort\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/google/go-cmp/cmp/cmpopts\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/grype/grype\"\n\t\"github.com/anchore/grype/grype/distro\"\n\t\"github.com/anchore/grype/grype/match\"\n\t\"github.com/anchore/grype/grype/matcher\"\n\t\"github.com/anchore/grype/grype/matcher/dotnet\"\n\t\"github.com/anchore/grype/grype/matcher/golang\"\n\t\"github.com/anchore/grype/grype/matcher/java\"\n\t\"github.com/anchore/grype/grype/matcher/javascript\"\n\t\"github.com/anchore/grype/grype/matcher/python\"\n\t\"github.com/anchore/grype/grype/matcher/ruby\"\n\t\"github.com/anchore/grype/grype/matcher/rust\"\n\t\"github.com/anchore/grype/grype/matcher/stock\"\n\t\"github.com/anchore/grype/grype/pkg\"\n\t\"github.com/anchore/grype/grype/search\"\n\t\"github.com/anchore/grype/grype/vex\"\n\tvexStatus \"github.com/anchore/grype/grype/vex/status\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n\t\"github.com/anchore/grype/internal/stringutil\"\n\t\"github.com/anchore/stereoscope/pkg/imagetest\"\n\t\"github.com/anchore/syft/syft\"\n\t\"github.com/anchore/syft/syft/cataloging/pkgcataloging\"\n\t\"github.com/anchore/syft/syft/cpe\"\n\tsyftPkg \"github.com/anchore/syft/syft/pkg\"\n\t\"github.com/anchore/syft/syft/source\"\n)\n\nfunc addAlpineMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, provider vulnerability.Provider, theResult *match.Matches) {\n\tpackages := catalog.PackagesByPath(\"/lib/apk/db/installed\")\n\tif len(packages) != 3 {\n\t\tt.Logf(\"Alpine Packages: %+v\", packages)\n\t\tt.Fatalf(\"problem with upstream syft cataloger (alpine)\")\n\t}\n\tthePkg := pkg.New(packages[0])\n\tvulns, err := provider.FindVulnerabilities(byNamespace(\"alpine:distro:alpine:3.12\"), search.ByPackageName(thePkg.Name))\n\trequire.NoError(t, err)\n\trequire.NotEmpty(t, vulns)\n\tvulnObj := vulns[0]\n\n\ttheResult.Add(match.Match{\n\t\t// note: we are matching on the secdb record, not NVD primarily\n\n\t\tVulnerability: vulnObj,\n\t\tPackage:       thePkg,\n\t\tDetails: []match.Detail{\n\t\t\t{\n\t\t\t\tType:       match.ExactDirectMatch,\n\t\t\t\tConfidence: 1.0,\n\t\t\t\tSearchedBy: match.DistroParameters{\n\t\t\t\t\tDistro: match.DistroIdentification{\n\t\t\t\t\t\tType:    \"alpine\",\n\t\t\t\t\t\tVersion: \"3.12.0\",\n\t\t\t\t\t},\n\t\t\t\t\tNamespace: \"alpine:distro:alpine:3.12\",\n\t\t\t\t\tPackage: match.PackageParameter{\n\t\t\t\t\t\tName:    \"libvncserver\",\n\t\t\t\t\t\tVersion: \"0.9.9\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tFound: match.DistroResult{\n\t\t\t\t\tVersionConstraint: \"< 0.9.10 (unknown)\",\n\t\t\t\t\tVulnerabilityID:   vulnObj.ID,\n\t\t\t\t},\n\t\t\t\tMatcher: match.ApkMatcher,\n\t\t\t},\n\t\t\t{\n\t\t\t\t// note: the input pURL has an upstream reference (redundant)\n\t\t\t\tType: match.ExactIndirectMatch,\n\t\t\t\tSearchedBy: match.DistroParameters{\n\t\t\t\t\tDistro: match.DistroIdentification{\n\t\t\t\t\t\tType:    \"alpine\",\n\t\t\t\t\t\tVersion: \"3.12.0\",\n\t\t\t\t\t},\n\t\t\t\t\tNamespace: \"alpine:distro:alpine:3.12\",\n\t\t\t\t\tPackage: match.PackageParameter{\n\t\t\t\t\t\tName:    \"libvncserver\",\n\t\t\t\t\t\tVersion: \"0.9.9\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tFound: match.DistroResult{\n\t\t\t\t\tVersionConstraint: \"< 0.9.10 (unknown)\",\n\t\t\t\t\tVulnerabilityID:   \"CVE-2024-0000\",\n\t\t\t\t},\n\t\t\t\tMatcher:    match.ApkMatcher,\n\t\t\t\tConfidence: 1,\n\t\t\t},\n\t\t},\n\t})\n}\n\nfunc addJavascriptMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, provider vulnerability.Provider, theResult *match.Matches) {\n\tpackages := catalog.PackagesByPath(\"/javascript/pkg-json/package.json\")\n\tif len(packages) != 1 {\n\t\tt.Logf(\"Javascript Packages: %+v\", packages)\n\t\tt.Fatalf(\"problem with upstream syft cataloger (javascript)\")\n\t}\n\tthePkg := pkg.New(packages[0])\n\tvulns, err := provider.FindVulnerabilities(byNamespace(\"github:language:javascript\"), search.ByPackageName(thePkg.Name))\n\trequire.NoError(t, err)\n\trequire.NotEmpty(t, vulns)\n\tvulnObj := vulns[0]\n\n\ttheResult.Add(match.Match{\n\t\tVulnerability: vulnObj,\n\t\tPackage:       thePkg,\n\t\tDetails: []match.Detail{\n\t\t\t{\n\t\t\t\tType:       match.ExactDirectMatch,\n\t\t\t\tConfidence: 1.0,\n\t\t\t\tSearchedBy: match.EcosystemParameters{\n\t\t\t\t\tLanguage:  \"javascript\",\n\t\t\t\t\tNamespace: \"github:language:javascript\",\n\t\t\t\t\tPackage: match.PackageParameter{\n\t\t\t\t\t\tName:    thePkg.Name,\n\t\t\t\t\t\tVersion: thePkg.Version,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tFound: match.EcosystemResult{\n\t\t\t\t\tVersionConstraint: \"> 5, < 7.2.1 (unknown)\",\n\t\t\t\t\tVulnerabilityID:   vulnObj.ID,\n\t\t\t\t},\n\t\t\t\tMatcher: match.JavascriptMatcher,\n\t\t\t},\n\t\t},\n\t})\n}\n\nfunc addPythonMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, provider vulnerability.Provider, theResult *match.Matches) {\n\tpackages := catalog.PackagesByPath(\"/python/dist-info/METADATA\")\n\tif len(packages) != 1 {\n\t\tfor _, p := range packages {\n\t\t\tt.Logf(\"Python Package: %s %+v\", p.ID(), p)\n\t\t}\n\n\t\tt.Fatalf(\"problem with upstream syft cataloger (python)\")\n\t}\n\tthePkg := pkg.New(packages[0])\n\tvulns, err := provider.FindVulnerabilities(byNamespace(\"github:language:python\"), search.ByPackageName(strings.ToLower(thePkg.Name)))\n\trequire.NoError(t, err)\n\trequire.NotEmpty(t, vulns)\n\tvulnObj := vulns[0]\n\n\ttheResult.Add(match.Match{\n\t\tVulnerability: vulnObj,\n\t\tPackage:       thePkg,\n\t\tDetails: []match.Detail{\n\t\t\t{\n\t\t\t\tType:       match.ExactDirectMatch,\n\t\t\t\tConfidence: 1.0,\n\t\t\t\tSearchedBy: match.EcosystemParameters{\n\t\t\t\t\tLanguage:  \"python\",\n\t\t\t\t\tNamespace: \"github:language:python\",\n\t\t\t\t\tPackage: match.PackageParameter{\n\t\t\t\t\t\tName:    provider.PackageSearchNames(thePkg)[0], // there is normalization that should be accounted for\n\t\t\t\t\t\tVersion: thePkg.Version,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tFound: match.EcosystemResult{\n\t\t\t\t\tVersionConstraint: \"< 2.6.2 (python)\",\n\t\t\t\t\tVulnerabilityID:   vulnObj.ID,\n\t\t\t\t},\n\t\t\t\tMatcher: match.PythonMatcher,\n\t\t\t},\n\t\t},\n\t})\n}\n\nfunc addDotnetMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, provider vulnerability.Provider, theResult *match.Matches) {\n\tpackages := catalog.PackagesByPath(\"/dotnet/TestLibrary.deps.json\")\n\t// 55caef8df7ac822e Pkg(name=\"TestLibrary\" version=\"1.0.0\" type=\"dotnet\" id=\"55caef8df7ac822e\")\n\t// 0012329cdebba0ea Pkg(name=\"AWSSDK.Core\" version=\"3.7.10.6\" type=\"dotnet\" id=\"0012329cdebba0ea\")\n\t// 07ec6fb2adb2cf8f Pkg(name=\"Microsoft.Extensions.DependencyInjection.Abstractions\" version=\"6.0.0\" type=\"dotnet\" id=\"07ec6fb2adb2cf8f\")\n\t// ff03e77b91acca32 Pkg(name=\"Microsoft.Extensions.DependencyInjection\" version=\"6.0.0\" type=\"dotnet\" id=\"ff03e77b91acca32\")\n\t// a1ea42c8f064083e Pkg(name=\"Microsoft.Extensions.Logging.Abstractions\" version=\"6.0.0\" type=\"dotnet\" id=\"a1ea42c8f064083e\")\n\t// aaef85a2649e5d15 Pkg(name=\"Microsoft.Extensions.Logging\" version=\"6.0.0\" type=\"dotnet\" id=\"aaef85a2649e5d15\")\n\t// 4af0fb6a81ba0423 Pkg(name=\"Microsoft.Extensions.Options\" version=\"6.0.0\" type=\"dotnet\" id=\"4af0fb6a81ba0423\")\n\t// cb41a8aefdf40c3a Pkg(name=\"Microsoft.Extensions.Primitives\" version=\"6.0.0\" type=\"dotnet\" id=\"cb41a8aefdf40c3a\")\n\t// 5ee80fba9caa3ab3 Pkg(name=\"Newtonsoft.Json\" version=\"13.0.1\" type=\"dotnet\" id=\"5ee80fba9caa3ab3\")\n\t// df4b5dc73acd1f36 Pkg(name=\"Serilog.Sinks.Console\" version=\"4.0.1\" type=\"dotnet\" id=\"df4b5dc73acd1f36\")\n\t// 023b9ba74c5c5ef5 Pkg(name=\"Serilog\" version=\"2.10.0\" type=\"dotnet\" id=\"023b9ba74c5c5ef5\")\n\t// 430e4d4304a3ff55 Pkg(name=\"System.Diagnostics.DiagnosticSource\" version=\"6.0.0\" type=\"dotnet\" id=\"430e4d4304a3ff55\")\n\t// 42021023d8f87661 Pkg(name=\"System.Runtime.CompilerServices.Unsafe\" version=\"6.0.0\" type=\"dotnet\" id=\"42021023d8f87661\")\n\t// 2bb01d8c22df1e95 Pkg(name=\"TestCommon\" version=\"1.0.0\" type=\"dotnet\" id=\"2bb01d8c22df1e95\")\n\tif len(packages) != 14 {\n\t\tfor _, p := range packages {\n\t\t\tt.Logf(\"Dotnet Package: %s %+v\", p.ID(), p)\n\t\t}\n\n\t\tt.Fatalf(\"problem with upstream syft cataloger (dotnet)\")\n\t}\n\tthePkg := pkg.New(packages[1])\n\tvulns, err := provider.FindVulnerabilities(byNamespace(\"github:language:dotnet\"), search.ByPackageName(strings.ToLower(thePkg.Name)))\n\trequire.NoError(t, err)\n\trequire.NotEmpty(t, vulns)\n\tvulnObj := vulns[0]\n\n\ttheResult.Add(match.Match{\n\t\tVulnerability: vulnObj,\n\t\tPackage:       thePkg,\n\t\tDetails: []match.Detail{\n\t\t\t{\n\t\t\t\tType:       match.ExactDirectMatch,\n\t\t\t\tConfidence: 1.0,\n\t\t\t\tSearchedBy: match.EcosystemParameters{\n\t\t\t\t\tLanguage:  \"dotnet\",\n\t\t\t\t\tNamespace: \"github:language:dotnet\",\n\t\t\t\t\tPackage: match.PackageParameter{\n\t\t\t\t\t\tName:    thePkg.Name,\n\t\t\t\t\t\tVersion: thePkg.Version,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tFound: match.EcosystemResult{\n\t\t\t\t\tVersionConstraint: \">= 3.7.0.0, < 3.7.12.0 (unknown)\",\n\t\t\t\t\tVulnerabilityID:   vulnObj.ID,\n\t\t\t\t},\n\t\t\t\tMatcher: match.DotnetMatcher,\n\t\t\t},\n\t\t},\n\t})\n}\n\nfunc addRubyMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, provider vulnerability.Provider, theResult *match.Matches) {\n\tpackages := catalog.PackagesByPath(\"/ruby/specifications/bundler.gemspec\")\n\tif len(packages) != 1 {\n\t\tt.Logf(\"Ruby Packages: %+v\", packages)\n\t\tt.Fatalf(\"problem with upstream syft cataloger (ruby)\")\n\t}\n\tthePkg := pkg.New(packages[0])\n\tvulns, err := provider.FindVulnerabilities(byNamespace(\"github:language:ruby\"), search.ByPackageName(thePkg.Name))\n\trequire.NoError(t, err)\n\trequire.NotEmpty(t, vulns)\n\tvulnObj := vulns[0]\n\n\ttheResult.Add(match.Match{\n\t\tVulnerability: vulnObj,\n\t\tPackage:       thePkg,\n\t\tDetails: []match.Detail{\n\t\t\t{\n\t\t\t\tType:       match.ExactDirectMatch,\n\t\t\t\tConfidence: 1.0,\n\t\t\t\tSearchedBy: match.EcosystemParameters{\n\t\t\t\t\tLanguage:  \"ruby\",\n\t\t\t\t\tNamespace: \"github:language:ruby\",\n\t\t\t\t\tPackage: match.PackageParameter{\n\t\t\t\t\t\tName:    thePkg.Name,\n\t\t\t\t\t\tVersion: thePkg.Version,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tFound: match.EcosystemResult{\n\t\t\t\t\tVersionConstraint: \"> 2.0.0, <= 2.1.4 (unknown)\",\n\t\t\t\t\tVulnerabilityID:   vulnObj.ID,\n\t\t\t\t},\n\t\t\t\tMatcher: match.RubyGemMatcher,\n\t\t\t},\n\t\t},\n\t})\n}\n\nfunc addGolangMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, provider vulnerability.Provider, theResult *match.Matches) {\n\tmodPackages := catalog.PackagesByPath(\"/golang/go.mod\")\n\tif len(modPackages) != 1 {\n\t\tt.Logf(\"Golang Mod Packages: %+v\", modPackages)\n\t\tt.Fatalf(\"problem with upstream syft cataloger (golang)\")\n\t}\n\n\tbinPackages := catalog.PackagesByPath(\"/go-app\")\n\t// contains 2 package + a single stdlib package\n\tif len(binPackages) != 3 {\n\t\tt.Logf(\"Golang Bin Packages: %+v\", binPackages)\n\t\tt.Fatalf(\"problem with upstream syft cataloger (golang)\")\n\t}\n\n\tvar packages []syftPkg.Package\n\tpackages = append(packages, modPackages...)\n\tpackages = append(packages, binPackages...)\n\n\tfor _, p := range packages {\n\t\t// no vuln match supported for main module\n\t\tif p.Name == \"github.com/anchore/coverage\" {\n\t\t\tcontinue\n\t\t}\n\n\t\tif p.Name == \"stdlib\" {\n\t\t\tcontinue\n\t\t}\n\n\t\tthePkg := pkg.New(p)\n\t\tvulns, err := provider.FindVulnerabilities(byNamespace(\"github:language:go\"), search.ByPackageName(thePkg.Name))\n\t\trequire.NoError(t, err)\n\t\trequire.NotEmpty(t, vulns)\n\t\tvulnObj := vulns[0]\n\n\t\ttheResult.Add(match.Match{\n\t\t\tVulnerability: vulnObj,\n\t\t\tPackage:       thePkg,\n\t\t\tDetails: []match.Detail{\n\t\t\t\t{\n\t\t\t\t\tType:       match.ExactDirectMatch,\n\t\t\t\t\tConfidence: 1.0,\n\t\t\t\t\tSearchedBy: match.EcosystemParameters{\n\t\t\t\t\t\tLanguage:  \"go\",\n\t\t\t\t\t\tNamespace: \"github:language:go\",\n\t\t\t\t\t\tPackage: match.PackageParameter{\n\t\t\t\t\t\t\tName:    thePkg.Name,\n\t\t\t\t\t\t\tVersion: thePkg.Version,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tFound: match.EcosystemResult{\n\t\t\t\t\t\tVersionConstraint: \"< 1.4.0 (unknown)\",\n\t\t\t\t\t\tVulnerabilityID:   vulnObj.ID,\n\t\t\t\t\t},\n\t\t\t\t\tMatcher: match.GoModuleMatcher,\n\t\t\t\t},\n\t\t\t},\n\t\t})\n\n\t}\n}\n\nfunc addJavaMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, provider vulnerability.Provider, theResult *match.Matches) {\n\tpackages := make([]syftPkg.Package, 0)\n\tfor p := range catalog.Enumerate(syftPkg.JavaPkg) {\n\t\tpackages = append(packages, p)\n\t}\n\tif len(packages) != 2 { // 2, because there's a nested JAR inside the test fixture JAR\n\t\tt.Logf(\"Java Packages: %+v\", packages)\n\t\tt.Fatalf(\"problem with upstream syft cataloger (java)\")\n\t}\n\ttheSyftPkg := packages[0]\n\n\tgroupId := theSyftPkg.Metadata.(syftPkg.JavaArchive).PomProperties.GroupID\n\tlookup := groupId + \":\" + theSyftPkg.Name\n\n\tthePkg := pkg.New(theSyftPkg)\n\tvulns, err := provider.FindVulnerabilities(byNamespace(\"github:language:java\"), search.ByPackageName(lookup))\n\trequire.NoError(t, err)\n\trequire.NotEmpty(t, vulns)\n\tvulnObj := vulns[0]\n\n\ttheResult.Add(match.Match{\n\t\tVulnerability: vulnObj,\n\t\tPackage:       thePkg,\n\t\tDetails: []match.Detail{\n\t\t\t{\n\t\t\t\tType:       match.ExactDirectMatch,\n\t\t\t\tConfidence: 1.0,\n\t\t\t\tSearchedBy: match.EcosystemParameters{\n\t\t\t\t\tLanguage:  \"java\",\n\t\t\t\t\tNamespace: \"github:language:java\",\n\t\t\t\t\tPackage: match.PackageParameter{\n\t\t\t\t\t\tName:    provider.PackageSearchNames(thePkg)[0], // there is normalization that should be accounted for\n\t\t\t\t\t\tVersion: thePkg.Version,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tFound: match.EcosystemResult{\n\t\t\t\t\tVersionConstraint: \">= 0.0.1, < 1.2.0 (unknown)\",\n\t\t\t\t\tVulnerabilityID:   vulnObj.ID,\n\t\t\t\t},\n\t\t\t\tMatcher: match.JavaMatcher,\n\t\t\t},\n\t\t},\n\t})\n}\n\nfunc addDpkgMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, provider vulnerability.Provider, theResult *match.Matches) {\n\tpackages := catalog.PackagesByPath(\"/var/lib/dpkg/status\")\n\tif len(packages) != 1 {\n\t\tt.Logf(\"Dpkg Packages: %+v\", packages)\n\t\tt.Fatalf(\"problem with upstream syft cataloger (dpkg)\")\n\t}\n\tthePkg := pkg.New(packages[0])\n\t// NOTE: this is an indirect match, in typical debian style\n\tvulns, err := provider.FindVulnerabilities(byNamespace(\"debian:distro:debian:8\"), search.ByPackageName(thePkg.Name+\"-dev\"))\n\trequire.NoError(t, err)\n\trequire.NotEmpty(t, vulns)\n\tvulnObj := vulns[0]\n\n\ttheResult.Add(match.Match{\n\t\tVulnerability: vulnObj,\n\t\tPackage:       thePkg,\n\t\tDetails: []match.Detail{\n\t\t\t{\n\t\t\t\tType:       match.ExactIndirectMatch,\n\t\t\t\tConfidence: 1.0,\n\t\t\t\tSearchedBy: match.DistroParameters{\n\t\t\t\t\tDistro: match.DistroIdentification{\n\t\t\t\t\t\tType:    \"debian\",\n\t\t\t\t\t\tVersion: \"8\",\n\t\t\t\t\t},\n\t\t\t\t\tNamespace: \"debian:distro:debian:8\",\n\t\t\t\t\tPackage: match.PackageParameter{\n\t\t\t\t\t\tName:    \"apt-dev\",\n\t\t\t\t\t\tVersion: \"1.8.2\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tFound: match.DistroResult{\n\t\t\t\t\tVersionConstraint: \"<= 1.8.2 (deb)\",\n\t\t\t\t\tVulnerabilityID:   vulnObj.ID,\n\t\t\t\t},\n\t\t\t\tMatcher: match.DpkgMatcher,\n\t\t\t},\n\t\t},\n\t})\n}\n\nfunc addPortageMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, provider vulnerability.Provider, theResult *match.Matches) {\n\tpackages := catalog.PackagesByPath(\"/var/db/pkg/app-containers/skopeo-1.5.1/CONTENTS\")\n\tif len(packages) != 1 {\n\t\tt.Logf(\"Portage Packages: %+v\", packages)\n\t\tt.Fatalf(\"problem with upstream syft cataloger (portage)\")\n\t}\n\tthePkg := pkg.New(packages[0])\n\tvulns, err := provider.FindVulnerabilities(byNamespace(\"gentoo:distro:gentoo:2.8\"), search.ByPackageName(thePkg.Name))\n\trequire.NoError(t, err)\n\trequire.NotEmpty(t, vulns)\n\tvulnObj := vulns[0]\n\n\ttheResult.Add(match.Match{\n\t\tVulnerability: vulnObj,\n\t\tPackage:       thePkg,\n\t\tDetails: []match.Detail{\n\t\t\t{\n\t\t\t\tType:       match.ExactDirectMatch,\n\t\t\t\tConfidence: 1.0,\n\t\t\t\tSearchedBy: match.DistroParameters{\n\t\t\t\t\tDistro: match.DistroIdentification{\n\t\t\t\t\t\tType:    \"gentoo\",\n\t\t\t\t\t\tVersion: \"2.8\",\n\t\t\t\t\t},\n\t\t\t\t\tNamespace: \"gentoo:distro:gentoo:2.8\",\n\t\t\t\t\tPackage: match.PackageParameter{\n\t\t\t\t\t\tName:    \"app-containers/skopeo\",\n\t\t\t\t\t\tVersion: \"1.5.1\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tFound: match.DistroResult{\n\t\t\t\t\tVersionConstraint: \"< 1.6.0 (unknown)\",\n\t\t\t\t\tVulnerabilityID:   vulnObj.ID,\n\t\t\t\t},\n\t\t\t\tMatcher: match.PortageMatcher,\n\t\t\t},\n\t\t},\n\t})\n}\n\nfunc addRhelMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, provider vulnerability.Provider, theResult *match.Matches) {\n\tpackages := catalog.PackagesByPath(\"/var/lib/rpm/Packages\")\n\tif len(packages) != 1 {\n\t\tt.Logf(\"RPMDB Packages: %+v\", packages)\n\t\tt.Fatalf(\"problem with upstream syft cataloger (RPMDB)\")\n\t}\n\tthePkg := pkg.New(packages[0])\n\tvulns, err := provider.FindVulnerabilities(byNamespace(\"redhat:distro:redhat:8\"), search.ByPackageName(thePkg.Name))\n\trequire.NoError(t, err)\n\trequire.NotEmpty(t, vulns)\n\tvulnObj := vulns[0]\n\n\ttheResult.Add(match.Match{\n\t\tVulnerability: vulnObj,\n\t\tPackage:       thePkg,\n\t\tDetails: []match.Detail{\n\t\t\t{\n\t\t\t\tType:       match.ExactDirectMatch,\n\t\t\t\tConfidence: 1.0,\n\t\t\t\tSearchedBy: match.DistroParameters{\n\t\t\t\t\tDistro: match.DistroIdentification{\n\t\t\t\t\t\tType:    \"centos\",\n\t\t\t\t\t\tVersion: \"8\",\n\t\t\t\t\t},\n\t\t\t\t\tNamespace: \"redhat:distro:redhat:8\",\n\t\t\t\t\tPackage: match.PackageParameter{\n\t\t\t\t\t\tName:    \"dive\",\n\t\t\t\t\t\tVersion: \"0:0.9.2-1\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tFound: match.DistroResult{\n\t\t\t\t\tVersionConstraint: \"<= 1.0.42 (rpm)\",\n\t\t\t\t\tVulnerabilityID:   vulnObj.ID,\n\t\t\t\t},\n\t\t\t\tMatcher: match.RpmMatcher,\n\t\t\t},\n\t\t},\n\t})\n}\n\nfunc addSlesMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, provider vulnerability.Provider, theResult *match.Matches) {\n\tpackages := catalog.PackagesByPath(\"/var/lib/rpm/Packages\")\n\tif len(packages) != 1 {\n\t\tt.Logf(\"Sles Packages: %+v\", packages)\n\t\tt.Fatalf(\"problem with upstream syft cataloger (RPMDB)\")\n\t}\n\tthePkg := pkg.New(packages[0])\n\n\tvulns, err := provider.FindVulnerabilities(byNamespace(\"redhat:distro:redhat:8\"), search.ByPackageName(thePkg.Name))\n\trequire.NoError(t, err)\n\trequire.NotEmpty(t, vulns)\n\tvulnObj := vulns[0]\n\n\tvulnObj.Namespace = \"sles:distro:sles:12.5\"\n\ttheResult.Add(match.Match{\n\t\tVulnerability: vulnObj,\n\t\tPackage:       thePkg,\n\t\tDetails: []match.Detail{\n\t\t\t{\n\t\t\t\tType:       match.ExactDirectMatch,\n\t\t\t\tConfidence: 1.0,\n\t\t\t\tSearchedBy: match.DistroParameters{\n\t\t\t\t\tDistro: match.DistroIdentification{\n\t\t\t\t\t\tType:    \"sles\",\n\t\t\t\t\t\tVersion: \"12.5\",\n\t\t\t\t\t},\n\t\t\t\t\tNamespace: \"sles:distro:sles:12.5\",\n\t\t\t\t\tPackage: match.PackageParameter{\n\t\t\t\t\t\tName:    \"dive\",\n\t\t\t\t\t\tVersion: \"0:0.9.2-1\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tFound: match.DistroResult{\n\t\t\t\t\tVersionConstraint: \"<= 1.0.42 (rpm)\",\n\t\t\t\t\tVulnerabilityID:   vulnObj.ID,\n\t\t\t\t},\n\t\t\t\tMatcher: match.RpmMatcher,\n\t\t\t},\n\t\t},\n\t})\n}\n\nfunc addArchMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, provider vulnerability.Provider, theResult *match.Matches) {\n\tpackages := catalog.PackagesByPath(\"/var/lib/pacman/local/xz-5.2.4-1/desc\")\n\tif len(packages) != 1 {\n\t\tt.Logf(\"Arch Packages: %+v\", packages)\n\t\tt.Fatalf(\"problem with upstream syft cataloger (pacman)\")\n\t}\n\tthePkg := pkg.New(packages[0])\n\tvulns, err := provider.FindVulnerabilities(byNamespace(\"arch:distro:arch:rolling\"), search.ByPackageName(thePkg.Name))\n\trequire.NoError(t, err)\n\trequire.NotEmpty(t, vulns)\n\tvulnObj := vulns[0]\n\n\ttheResult.Add(match.Match{\n\t\tVulnerability: vulnObj,\n\t\tPackage:       thePkg,\n\t\tDetails: []match.Detail{\n\t\t\t{\n\t\t\t\tType:       match.ExactDirectMatch,\n\t\t\t\tConfidence: 1.0,\n\t\t\t\tSearchedBy: match.DistroParameters{\n\t\t\t\t\tDistro: match.DistroIdentification{\n\t\t\t\t\t\tType:    \"archlinux\",\n\t\t\t\t\t\tVersion: \"\",\n\t\t\t\t\t},\n\t\t\t\t\tNamespace: \"arch:distro:arch:rolling\",\n\t\t\t\t\tPackage: match.PackageParameter{\n\t\t\t\t\t\tName:    \"xz\",\n\t\t\t\t\t\tVersion: \"5.2.4-1\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tFound: match.DistroResult{\n\t\t\t\t\tVersionConstraint: \"< 5.6.1-2 (pacman)\",\n\t\t\t\t\tVulnerabilityID:   vulnObj.ID,\n\t\t\t\t},\n\t\t\t\tMatcher: match.PacmanMatcher,\n\t\t\t},\n\t\t},\n\t})\n}\n\nfunc addHaskellMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, provider vulnerability.Provider, theResult *match.Matches) {\n\tpackages := catalog.PackagesByPath(\"/haskell/stack.yaml\")\n\tif len(packages) < 1 {\n\t\tt.Logf(\"Haskell Packages: %+v\", packages)\n\t\tt.Fatalf(\"problem with upstream syft cataloger (haskell)\")\n\t}\n\tthePkg := pkg.New(packages[0])\n\tvulns, err := provider.FindVulnerabilities(byNamespace(\"github:language:haskell\"), search.ByPackageName(strings.ToLower(thePkg.Name)))\n\trequire.NoError(t, err)\n\trequire.NotEmpty(t, vulns)\n\tvulnObj := vulns[0]\n\n\ttheResult.Add(match.Match{\n\t\tVulnerability: vulnObj,\n\t\tPackage:       thePkg,\n\t\tDetails: []match.Detail{\n\t\t\t{\n\t\t\t\tType:       match.ExactDirectMatch,\n\t\t\t\tConfidence: 1.0,\n\t\t\t\tSearchedBy: match.EcosystemParameters{\n\t\t\t\t\tLanguage:  \"haskell\",\n\t\t\t\t\tNamespace: \"github:language:haskell\",\n\t\t\t\t\tPackage: match.PackageParameter{\n\t\t\t\t\t\tName:    thePkg.Name,\n\t\t\t\t\t\tVersion: thePkg.Version,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tFound: match.EcosystemResult{\n\t\t\t\t\tVersionConstraint: \"< 0.9.0 (unknown)\",\n\t\t\t\t\tVulnerabilityID:   \"CVE-haskell-sample\",\n\t\t\t\t},\n\t\t\t\tMatcher: match.StockMatcher,\n\t\t\t},\n\t\t},\n\t})\n}\n\nfunc addHexMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, provider vulnerability.Provider, theResult *match.Matches) {\n\tpackages := catalog.PackagesByPath(\"/hex/mix.lock\")\n\tif len(packages) < 1 {\n\t\tt.Logf(\"Hex Packages: %+v\", packages)\n\t\tt.Fatalf(\"problem with upstream syft cataloger (elixir-mix-lock)\")\n\t}\n\tthePkg := pkg.New(packages[0])\n\tvulns, err := provider.FindVulnerabilities(byNamespace(\"github:language:elixir\"), search.ByPackageName(thePkg.Name))\n\trequire.NoError(t, err)\n\trequire.NotEmpty(t, vulns)\n\tvulnObj := vulns[0]\n\n\ttheResult.Add(match.Match{\n\t\tVulnerability: vulnObj,\n\t\tPackage:       thePkg,\n\t\tDetails: []match.Detail{\n\t\t\t{\n\t\t\t\tType:       match.ExactDirectMatch,\n\t\t\t\tConfidence: 1.0,\n\t\t\t\tSearchedBy: match.EcosystemParameters{\n\t\t\t\t\tLanguage:  \"elixir\",\n\t\t\t\t\tNamespace: \"github:language:elixir\",\n\t\t\t\t\tPackage: match.PackageParameter{\n\t\t\t\t\t\tName:    thePkg.Name,\n\t\t\t\t\t\tVersion: thePkg.Version,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tFound: match.EcosystemResult{\n\t\t\t\t\tVersionConstraint: \"< 1.12.0 (unknown)\",\n\t\t\t\t\tVulnerabilityID:   \"CVE-hex-plug\",\n\t\t\t\t},\n\t\t\t\tMatcher: match.HexMatcher,\n\t\t\t},\n\t\t},\n\t})\n}\n\nfunc addJvmMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, provider vulnerability.Provider, theResult *match.Matches) {\n\tpackages := catalog.PackagesByPath(\"/opt/java/openjdk/release\")\n\tif len(packages) < 1 {\n\t\tt.Logf(\"JVM Packages: %+v\", packages)\n\t\tt.Fatalf(\"problem with upstream syft cataloger (java-jvm-cataloger)\")\n\t}\n\n\tfor _, p := range packages {\n\t\tthePkg := pkg.New(p)\n\t\tvulns, err := provider.FindVulnerabilities(byNamespace(\"nvd:cpe\"), search.ByPackageName(thePkg.Name))\n\t\trequire.NoError(t, err)\n\t\trequire.NotEmpty(t, vulns)\n\t\tvulnObj := vulns[0]\n\n\t\t// why is this being set?\n\t\tvulnObj.CPEs = []cpe.CPE{\n\t\t\tcpe.Must(\"cpe:2.3:a:oracle:jdk:*:*:*:*:*:*:*:*\", \"\"),\n\t\t}\n\n\t\ttheResult.Add(match.Match{\n\t\t\tVulnerability: vulnObj,\n\t\t\tPackage:       thePkg,\n\t\t\tDetails: []match.Detail{\n\t\t\t\t{\n\t\t\t\t\tType:       match.CPEMatch,\n\t\t\t\t\tConfidence: 0.9,\n\t\t\t\t\tSearchedBy: match.CPEParameters{\n\t\t\t\t\t\tNamespace: \"nvd:cpe\",\n\t\t\t\t\t\tCPEs: []string{\n\t\t\t\t\t\t\t\"cpe:2.3:a:oracle:jdk:1.8.0:update400:*:*:*:*:*:*\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tPackage: match.PackageParameter{Name: \"jdk\", Version: \"1.8.0_400-b07\"},\n\t\t\t\t\t},\n\t\t\t\t\tFound: match.CPEResult{\n\t\t\t\t\t\tVulnerabilityID:   \"CVE-jdk\",\n\t\t\t\t\t\tVersionConstraint: \"< 1.8.0_401 (jvm)\",\n\t\t\t\t\t\tCPEs: []string{\n\t\t\t\t\t\t\t\"cpe:2.3:a:oracle:jdk:*:*:*:*:*:*:*:*\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tMatcher: match.StockMatcher,\n\t\t\t\t},\n\t\t\t},\n\t\t})\n\t}\n}\n\nfunc addRustMatches(t *testing.T, theSource source.Source, catalog *syftPkg.Collection, provider vulnerability.Provider, theResult *match.Matches) {\n\tpackages := catalog.PackagesByPath(\"/hello-auditable\")\n\tif len(packages) < 1 {\n\t\tt.Logf(\"Rust Packages: %+v\", packages)\n\t\tt.Fatalf(\"problem with upstream syft cataloger (cargo-auditable-binary-cataloger)\")\n\t}\n\n\tfor _, p := range packages {\n\t\tthePkg := pkg.New(p)\n\t\tvulns, err := provider.FindVulnerabilities(byNamespace(\"github:language:rust\"), search.ByPackageName(thePkg.Name))\n\t\trequire.NoError(t, err)\n\t\trequire.NotEmpty(t, vulns)\n\t\tvulnObj := vulns[0]\n\n\t\ttheResult.Add(match.Match{\n\t\t\tVulnerability: vulnObj,\n\t\t\tPackage:       thePkg,\n\t\t\tDetails: []match.Detail{\n\t\t\t\t{\n\t\t\t\t\tType:       match.ExactDirectMatch,\n\t\t\t\t\tConfidence: 1.0,\n\t\t\t\t\tSearchedBy: match.EcosystemParameters{\n\t\t\t\t\t\tLanguage:  \"rust\",\n\t\t\t\t\t\tNamespace: \"github:language:rust\",\n\t\t\t\t\t\tPackage: match.PackageParameter{\n\t\t\t\t\t\t\tName:    thePkg.Name,\n\t\t\t\t\t\t\tVersion: thePkg.Version,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tFound: match.EcosystemResult{\n\t\t\t\t\t\tVersionConstraint: vulnObj.Constraint.String(),\n\t\t\t\t\t\tVulnerabilityID:   vulnObj.ID,\n\t\t\t\t\t},\n\t\t\t\t\tMatcher: match.RustMatcher,\n\t\t\t\t},\n\t\t\t},\n\t\t})\n\t}\n}\n\nfunc TestMatchByImage(t *testing.T) {\n\tobservedMatchers := stringutil.NewStringSet()\n\tdefinedMatchers := stringutil.NewStringSet()\n\tfor _, l := range match.AllMatcherTypes {\n\t\tdefinedMatchers.Add(string(l))\n\t}\n\n\ttests := []struct {\n\t\tname       string\n\t\texpectedFn func(source.Source, *syftPkg.Collection, vulnerability.Provider) match.Matches\n\t}{\n\t\t{\n\t\t\tname: \"image-debian-match-coverage\",\n\t\t\texpectedFn: func(theSource source.Source, catalog *syftPkg.Collection, provider vulnerability.Provider) match.Matches {\n\t\t\t\texpectedMatches := match.NewMatches()\n\t\t\t\taddPythonMatches(t, theSource, catalog, provider, &expectedMatches)\n\t\t\t\taddRubyMatches(t, theSource, catalog, provider, &expectedMatches)\n\t\t\t\taddJavaMatches(t, theSource, catalog, provider, &expectedMatches)\n\t\t\t\taddDpkgMatches(t, theSource, catalog, provider, &expectedMatches)\n\t\t\t\taddJavascriptMatches(t, theSource, catalog, provider, &expectedMatches)\n\t\t\t\taddDotnetMatches(t, theSource, catalog, provider, &expectedMatches)\n\t\t\t\taddGolangMatches(t, theSource, catalog, provider, &expectedMatches)\n\t\t\t\taddHaskellMatches(t, theSource, catalog, provider, &expectedMatches)\n\t\t\t\taddHexMatches(t, theSource, catalog, provider, &expectedMatches)\n\t\t\t\treturn expectedMatches\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"image-centos-match-coverage\",\n\t\t\texpectedFn: func(theSource source.Source, catalog *syftPkg.Collection, provider vulnerability.Provider) match.Matches {\n\t\t\t\texpectedMatches := match.NewMatches()\n\t\t\t\taddRhelMatches(t, theSource, catalog, provider, &expectedMatches)\n\t\t\t\treturn expectedMatches\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"image-alpine-match-coverage\",\n\t\t\texpectedFn: func(theSource source.Source, catalog *syftPkg.Collection, provider vulnerability.Provider) match.Matches {\n\t\t\t\texpectedMatches := match.NewMatches()\n\t\t\t\taddAlpineMatches(t, theSource, catalog, provider, &expectedMatches)\n\t\t\t\treturn expectedMatches\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"image-sles-match-coverage\",\n\t\t\texpectedFn: func(theSource source.Source, catalog *syftPkg.Collection, provider vulnerability.Provider) match.Matches {\n\t\t\t\texpectedMatches := match.NewMatches()\n\t\t\t\taddSlesMatches(t, theSource, catalog, provider, &expectedMatches)\n\t\t\t\treturn expectedMatches\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"image-arch-match-coverage\",\n\t\t\texpectedFn: func(theSource source.Source, catalog *syftPkg.Collection, provider vulnerability.Provider) match.Matches {\n\t\t\t\texpectedMatches := match.NewMatches()\n\t\t\t\taddArchMatches(t, theSource, catalog, provider, &expectedMatches)\n\t\t\t\treturn expectedMatches\n\t\t\t},\n\t\t},\n\t\t// TODO: add this back in when #744 is fully implemented (see https://github.com/anchore/grype/issues/744#issuecomment-2448163737)\n\t\t//{\n\t\t//\tname: \"image-portage-match-coverage\",\n\t\t//\texpectedFn: func(theSource source.Source, catalog *syftPkg.Collection, provider vulnerability.Provider) match.Matches {\n\t\t//\t\texpectedMatches := match.NewMatches()\n\t\t//\t\taddPortageMatches(t, theSource, catalog, provider, &expectedMatches)\n\t\t//\t\treturn expectedMatches\n\t\t//\t},\n\t\t//},\n\t\t{\n\t\t\tname: \"image-rust-auditable-match-coverage\",\n\t\t\texpectedFn: func(theSource source.Source, catalog *syftPkg.Collection, provider vulnerability.Provider) match.Matches {\n\t\t\t\texpectedMatches := match.NewMatches()\n\t\t\t\taddRustMatches(t, theSource, catalog, provider, &expectedMatches)\n\t\t\t\treturn expectedMatches\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"image-jvm-match-coverage\",\n\t\t\texpectedFn: func(theSource source.Source, catalog *syftPkg.Collection, provider vulnerability.Provider) match.Matches {\n\t\t\t\texpectedMatches := match.NewMatches()\n\t\t\t\taddJvmMatches(t, theSource, catalog, provider, &expectedMatches)\n\t\t\t\treturn expectedMatches\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\ttheProvider := newMockDbProvider()\n\n\t\t\timagetest.GetFixtureImage(t, \"docker-archive\", test.name)\n\t\t\ttarPath := imagetest.GetFixtureImageTarPath(t, test.name)\n\n\t\t\t// this is purely done to help setup mocks\n\t\t\ttheSource, err := syft.GetSource(context.Background(), tarPath, syft.DefaultGetSourceConfig().WithSources(\"docker-archive\"))\n\t\t\trequire.NoError(t, err)\n\t\t\tt.Cleanup(func() {\n\t\t\t\trequire.NoError(t, theSource.Close())\n\t\t\t})\n\n\t\t\t// TODO: relationships are not verified at this time\n\t\t\t// enable all catalogers to cover non default cases\n\t\t\tconfig := syft.DefaultCreateSBOMConfig().WithCatalogerSelection(pkgcataloging.NewSelectionRequest().WithDefaults(\"all\"))\n\t\t\tconfig.Search.Scope = source.SquashedScope\n\n\t\t\ts, err := syft.CreateSBOM(context.Background(), theSource, config)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.NotNil(t, s)\n\n\t\t\t// TODO: we need to use the API default configuration, not something hard coded here\n\t\t\tmatchers := matcher.NewDefaultMatchers(matcher.Config{\n\t\t\t\tJava: java.MatcherConfig{\n\t\t\t\t\tUseCPEs: true,\n\t\t\t\t},\n\t\t\t\tRuby: ruby.MatcherConfig{\n\t\t\t\t\tUseCPEs: true,\n\t\t\t\t},\n\t\t\t\tPython: python.MatcherConfig{\n\t\t\t\t\tUseCPEs: true,\n\t\t\t\t},\n\t\t\t\tDotnet: dotnet.MatcherConfig{\n\t\t\t\t\tUseCPEs: true,\n\t\t\t\t},\n\t\t\t\tJavascript: javascript.MatcherConfig{\n\t\t\t\t\tUseCPEs: true,\n\t\t\t\t},\n\t\t\t\tGolang: golang.MatcherConfig{\n\t\t\t\t\tUseCPEs: true,\n\t\t\t\t},\n\t\t\t\tRust: rust.MatcherConfig{\n\t\t\t\t\tUseCPEs: true,\n\t\t\t\t},\n\t\t\t\tStock: stock.MatcherConfig{\n\t\t\t\t\tUseCPEs: true,\n\t\t\t\t},\n\t\t\t})\n\n\t\t\tactualResults := grype.FindVulnerabilitiesForPackage(theProvider, matchers, pkg.FromCollection(s.Artifacts.Packages, pkg.SynthesisConfig{\n\t\t\t\tDistro: pkg.DistroConfig{\n\t\t\t\t\tOverride: distro.FromRelease(s.Artifacts.LinuxDistribution, distro.DefaultFixChannels()),\n\t\t\t\t},\n\t\t\t}))\n\t\t\tfor _, m := range actualResults.Sorted() {\n\t\t\t\tfor _, d := range m.Details {\n\t\t\t\t\tobservedMatchers.Add(string(d.Matcher))\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// build expected matches from what's discovered from the catalog\n\t\t\texpectedMatches := test.expectedFn(theSource, s.Artifacts.Packages, theProvider)\n\n\t\t\tassertMatches(t, expectedMatches.Sorted(), actualResults.Sorted())\n\t\t})\n\t}\n\n\t// Test that VEX matchers produce matches when fed documents with \"affected\"\n\t// or \"under investigation\" statuses.\n\tfor n, tc := range map[string]struct {\n\t\tvexStatus    vexStatus.Status\n\t\tvexDocuments []string\n\t}{\n\t\t\"csaf-affected\":               {vexStatus.Affected, []string{\"testdata/vex/csaf/affected.csaf.json\"}},\n\t\t\"csaf-under_investigation\":    {vexStatus.UnderInvestigation, []string{\"testdata/vex/csaf/under_investigation.csaf.json\"}},\n\t\t\"openvex-affected\":            {vexStatus.Affected, []string{\"testdata/vex/openvex/affected.openvex.json\"}},\n\t\t\"openvex-under_investigation\": {vexStatus.UnderInvestigation, []string{\"testdata/vex/openvex/under_investigation.openvex.json\"}},\n\t} {\n\t\tt.Run(n, func(t *testing.T) {\n\t\t\tignoredMatches := testIgnoredMatches()\n\t\t\tvexedResults := vexMatches(t, ignoredMatches, tc.vexStatus, tc.vexDocuments)\n\t\t\tif len(vexedResults.Sorted()) != 1 {\n\t\t\t\tt.Errorf(\"expected one vexed result, got none\")\n\t\t\t}\n\n\t\t\texpectedMatches := match.NewMatches()\n\t\t\t// The single match in the actual results is the same in ignoredMatched\n\t\t\t// but must the details of the VEX matcher appended\n\t\t\tif len(vexedResults.Sorted()) < 1 {\n\t\t\t\tt.Errorf(\n\t\t\t\t\t\"Expected VEXed Results to produce an array of vexMatches but got none; len(vexedResults)=%d\",\n\t\t\t\t\tlen(vexedResults.Sorted()),\n\t\t\t\t)\n\t\t\t}\n\t\t\tresult := vexedResults.Sorted()[0]\n\t\t\tif len(result.Details) != len(ignoredMatches[0].Match.Details)+1 {\n\t\t\t\tt.Errorf(\n\t\t\t\t\t\"Details in VEXed results don't match (expected %d, got %d)\",\n\t\t\t\t\tlen(ignoredMatches[0].Match.Details)+1, len(result.Details),\n\t\t\t\t)\n\t\t\t}\n\n\t\t\tresult.Details = result.Details[:len(result.Details)-1]\n\t\t\tactualResults := match.NewMatches()\n\t\t\tactualResults.Add(result)\n\n\t\t\texpectedMatches.Add(ignoredMatches[0].Match)\n\t\t\tassertMatches(t, expectedMatches.Sorted(), actualResults.Sorted())\n\n\t\t\tfor _, m := range vexedResults.Sorted() {\n\t\t\t\tfor _, d := range m.Details {\n\t\t\t\t\tobservedMatchers.Add(string(d.Matcher))\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n\n\t// ensure that integration test cases stay in sync with the implemented matchers\n\tobservedMatchers.Remove(string(match.StockMatcher))\n\tdefinedMatchers.Remove(string(match.StockMatcher))\n\tdefinedMatchers.Remove(string(match.MsrcMatcher))\n\tdefinedMatchers.Remove(string(match.PortageMatcher)) // TODO: add this back in when #744 is complete\n\tdefinedMatchers.Remove(string(match.BitnamiMatcher)) // bitnami will be tested via quality gate\n\n\tif len(observedMatchers) != len(definedMatchers) {\n\t\tt.Errorf(\"matcher coverage incomplete (matchers=%d, coverage=%d)\", len(definedMatchers), len(observedMatchers))\n\t\tdefs := definedMatchers.ToSlice()\n\t\tsort.Strings(defs)\n\t\tobs := observedMatchers.ToSlice()\n\t\tsort.Strings(obs)\n\n\t\tt.Log(cmp.Diff(defs, obs))\n\t}\n}\n\n// testIgnoredMatches returns an list of ignored matches to test the vex\n// matchers\nfunc testIgnoredMatches() []match.IgnoredMatch {\n\treturn []match.IgnoredMatch{\n\t\t{\n\t\t\tMatch: match.Match{\n\t\t\t\tVulnerability: vulnerability.Vulnerability{\n\t\t\t\t\tReference: vulnerability.Reference{\n\t\t\t\t\t\tID:        \"CVE-2024-0000\",\n\t\t\t\t\t\tNamespace: \"alpine:distro:alpine:3.12\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tPackage: pkg.Package{\n\t\t\t\t\tID:       \"44fa3691ae360cac\",\n\t\t\t\t\tName:     \"libvncserver\",\n\t\t\t\t\tVersion:  \"0.9.9\",\n\t\t\t\t\tLicenses: []string{\"GPL-2.0-or-later\"},\n\t\t\t\t\tType:     \"apk\",\n\t\t\t\t\tCPEs: []cpe.CPE{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tAttributes: cpe.Attributes{\n\t\t\t\t\t\t\t\tPart:    \"a\",\n\t\t\t\t\t\t\t\tVendor:  \"libvncserver\",\n\t\t\t\t\t\t\t\tProduct: \"libvncserver\",\n\t\t\t\t\t\t\t\tVersion: \"0.9.9\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tPURL:      \"pkg:apk/alpine/libvncserver@0.9.9?arch=x86_64&distro=alpine-3.12.0\",\n\t\t\t\t\tUpstreams: []pkg.UpstreamPackage{{Name: \"libvncserver\"}},\n\t\t\t\t},\n\t\t\t\tDetails: []match.Detail{\n\t\t\t\t\t{\n\t\t\t\t\t\tType: match.ExactIndirectMatch,\n\t\t\t\t\t\tSearchedBy: match.DistroParameters{\n\t\t\t\t\t\t\tDistro: match.DistroIdentification{\n\t\t\t\t\t\t\t\tType:    \"alpine\",\n\t\t\t\t\t\t\t\tVersion: \"3.12.0\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tNamespace: \"alpine:distro:alpine:3.12\",\n\t\t\t\t\t\t\tPackage: match.PackageParameter{\n\t\t\t\t\t\t\t\tName:    \"libvncserver\",\n\t\t\t\t\t\t\t\tVersion: \"0.9.9\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tFound: match.DistroResult{\n\t\t\t\t\t\t\tVersionConstraint: \"< 0.9.10 (unknown)\",\n\t\t\t\t\t\t\tVulnerabilityID:   \"CVE-2024-0000\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tMatcher:    match.ApkMatcher,\n\t\t\t\t\t\tConfidence: 1,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tAppliedIgnoreRules: []match.IgnoreRule{},\n\t\t},\n\t}\n}\n\n// vexMatches moves the first match of a matches list to an ignore list and\n// applies a VEX \"affected\" document to it to move it to the matches list.\nfunc vexMatches(t *testing.T, ignoredMatches []match.IgnoredMatch, vexStatus vexStatus.Status, vexDocuments []string) match.Matches {\n\tmatches := match.NewMatches()\n\tvexMatcher, err := vex.NewProcessor(vex.ProcessorOptions{\n\t\tDocuments: vexDocuments,\n\t\tIgnoreRules: []match.IgnoreRule{\n\t\t\t{VexStatus: string(vexStatus)},\n\t\t},\n\t})\n\tif err != nil {\n\t\tt.Errorf(\"creating VEX processor: %s\", err)\n\t}\n\n\tpctx := &pkg.Context{\n\t\tSource: &source.Description{\n\t\t\tMetadata: source.ImageMetadata{\n\t\t\t\tRepoDigests: []string{\n\t\t\t\t\t\"alpine@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tvexedMatches, ignoredMatches, err := vexMatcher.ApplyVEX(pctx, &matches, ignoredMatches)\n\tif err != nil {\n\t\tt.Errorf(\"applying VEX data: %s\", err)\n\t}\n\n\tif len(ignoredMatches) != 0 {\n\t\tt.Errorf(\"VEX text fixture %s must affect all ignored matches (%d left)\", vexDocuments, len(ignoredMatches))\n\t}\n\n\treturn *vexedMatches\n}\n\nfunc assertMatches(t *testing.T, expected, actual []match.Match) {\n\tt.Helper()\n\topts := []cmp.Option{\n\t\tcmpopts.EquateEmpty(),\n\t\tcmpopts.IgnoreFields(vulnerability.Vulnerability{}, \"Constraint\"),\n\t\tcmpopts.IgnoreFields(pkg.Package{}, \"Locations\", \"Distro\"),\n\t\tcmpopts.SortSlices(func(a, b match.Match) bool {\n\t\t\treturn a.Package.ID < b.Package.ID\n\t\t}),\n\t}\n\n\tif diff := cmp.Diff(expected, actual, opts...); diff != \"\" {\n\t\tt.Errorf(\"mismatch (-want +got):\\n%s\", diff)\n\t}\n}\n\nfunc byNamespace(ns string) vulnerability.Criteria {\n\treturn search.ByFunc(func(v vulnerability.Vulnerability) (bool, string, error) {\n\t\treturn v.Reference.Namespace == ns, \"\", nil\n\t})\n}\n"
  },
  {
    "path": "test/integration/match_by_sbom_document_test.go",
    "content": "package integration\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/google/go-cmp/cmp/cmpopts\"\n\t\"github.com/scylladb/go-set/strset\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/grype/grype\"\n\t\"github.com/anchore/grype/grype/match\"\n\t\"github.com/anchore/grype/grype/pkg\"\n\t\"github.com/anchore/syft/syft/source\"\n)\n\nfunc TestMatchBySBOMDocument(t *testing.T) {\n\ttests := []struct {\n\t\tname            string\n\t\tfixture         string\n\t\texpectedIDs     []string\n\t\texpectedDetails []match.Detail\n\t}{\n\t\t{\n\t\t\tname:        \"unknown package type\",\n\t\t\tfixture:     \"testdata/sbom/syft-sbom-with-unknown-packages.json\",\n\t\t\texpectedIDs: []string{\"CVE-bogus-my-package-2-idris\"},\n\t\t\texpectedDetails: []match.Detail{\n\t\t\t\t{\n\t\t\t\t\tType: match.ExactDirectMatch,\n\t\t\t\t\tSearchedBy: match.EcosystemParameters{\n\t\t\t\t\t\tLanguage:  \"idris\",\n\t\t\t\t\t\tNamespace: \"github:language:idris\",\n\t\t\t\t\t\tPackage:   match.PackageParameter{Name: \"my-package\", Version: \"1.0.5\"},\n\t\t\t\t\t},\n\t\t\t\t\tFound: match.EcosystemResult{\n\t\t\t\t\t\tVersionConstraint: \"< 2.0 (unknown)\",\n\t\t\t\t\t\tVulnerabilityID:   \"CVE-bogus-my-package-2-idris\",\n\t\t\t\t\t},\n\t\t\t\t\tMatcher:    match.StockMatcher,\n\t\t\t\t\tConfidence: 1,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tvp := newMockDbProvider()\n\t\t\tmatches, _, _, err := grype.FindVulnerabilities(vp, fmt.Sprintf(\"sbom:%s\", test.fixture), source.SquashedScope, nil)\n\t\t\tassert.NoError(t, err)\n\t\t\tdetails := make([]match.Detail, 0)\n\t\t\tids := strset.New()\n\t\t\tfor _, m := range matches.Sorted() {\n\t\t\t\tdetails = append(details, m.Details...)\n\t\t\t\tids.Add(m.Vulnerability.ID)\n\t\t\t}\n\n\t\t\trequire.Len(t, details, len(test.expectedDetails))\n\n\t\t\tcmpOpts := []cmp.Option{\n\t\t\t\tcmpopts.IgnoreFields(pkg.Package{}, \"Locations\"),\n\t\t\t}\n\n\t\t\tfor i := range test.expectedDetails {\n\t\t\t\tif d := cmp.Diff(test.expectedDetails[i], details[i], cmpOpts...); d != \"\" {\n\t\t\t\t\tt.Errorf(\"unexpected match details (-want +got):\\n%s\", d)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tassert.ElementsMatch(t, test.expectedIDs, ids.List())\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "test/integration/testdata/.gitignore",
    "content": "!**/image-*/Dockerfile\ngrype-db\ngrype-db-download*"
  },
  {
    "path": "test/integration/testdata/Makefile",
    "content": "# change these if you want CI to not use previous stored cache\nINTEGRATION_CACHE_BUSTER := \"894d8ca\"\n\n.PHONY: cache.fingerprint\ncache.fingerprint:\n\tfind image-* -type f -exec md5sum {} + | awk '{print $1}' | sort | tee /dev/stderr | md5sum | tee cache.fingerprint && echo \"$(INTEGRATION_CACHE_BUSTER)\" >> cache.fingerprint\n"
  },
  {
    "path": "test/integration/testdata/image-alpine-match-coverage/Dockerfile",
    "content": "FROM cgr.dev/chainguard/go AS builder\n\nFROM scratch\nCOPY . .\n"
  },
  {
    "path": "test/integration/testdata/image-alpine-match-coverage/etc/os-release",
    "content": "NAME=\"Alpine Linux\"\r\nID=alpine\r\nVERSION_ID=3.12.0\r\nPRETTY_NAME=\"Alpine Linux v3.12\"\r\nHOME_URL=\"https://alpinelinux.org/\"\r\nBUG_REPORT_URL=\"https://bugs.alpinelinux.org/\"\r\n"
  },
  {
    "path": "test/integration/testdata/image-alpine-match-coverage/lib/apk/db/installed",
    "content": "C:Q1z0MwWQKfva+S+q7XmOBYFfQgW/k=\nP:libvncserver\nV:0.9.9\nA:x86_64\nS:166239\nI:389120\nT:Library to make writing a vnc server easy\nU:http://libvncserver.sourceforge.net/\nL:GPL-2.0-or-later\no:libvncserver\nm:A. Wilcox <awilfox@adelielinux.org>\nt:1572818861\nc:bf1ec813f662f128fc6b70f37ef1c0474bb24488\nD:so:libc.musl-x86_64.so.1 so:libgcrypt.so.20 so:libgnutls.so.30 so:libjpeg.so.8 so:libpng16.so.16 so:libz.so.1\np:so:libvncclient.so.1=1.0.0 so:libvncserver.so.1=1.0.0\nF:usr\nF:usr/lib\nR:libvncclient.so.1\na:0:0:777\nZ:Q1quyp/JcSPFQhtQFjMUYdMwRvAWM=\nR:libvncserver.so.1.0.0\na:0:0:755\nZ:Q16Pd1AqyqQRMwiFfbUt9XkYnkapw=\nR:libvncserver.so.1\na:0:0:777\nZ:Q184HrHsxEBqnsH4QNxeU5w8alhKI=\nR:libvncclient.so.1.0.0\na:0:0:755\nZ:Q1IEjCrEwVlQt2GjIsb3o39vcgqMg=\n\nC:Q1z0MwWQKfva+S+q7XmOBYFfQgW/k=\nP:ko\nV:0.15.1\nA:x86_64\nS:166239\nI:389120\nT:Build and deploy Go applications\no:ko\nt:1572818861\nR:ko\na:0:0:755\nZ:Q16Pd1AqyqQRMwiFfbUt9XkYnkapw=\n\nC:Q1z0MwWQKfva+S+q7XmOBYFfQgW/k=\nP:npm-apk-subpackage-with-false-positive\nV:7.0.0\nA:x86_64\nS:166239\nI:389120\nT:NPM package, in an APK subpackage, that has a false positive\no:npm-apk-package-with-false-positive\nt:1572818861\nF:lib\nF:lib/node_modules\nF:lib/node_modules/npm-apk-subpackage-with-false-positive\nR:package.json\na:0:0:755\nZ:Q16Pd1AqyqQRMwiFfbUt9XkYnkapw=\n"
  },
  {
    "path": "test/integration/testdata/image-arch-match-coverage/Dockerfile",
    "content": "FROM docker.io/archlinux:20191105@sha256:1097437745db73ba839d60b9b9b96e6648e62751519a1319bfccc849f6a3f74c\n"
  },
  {
    "path": "test/integration/testdata/image-centos-match-coverage/Dockerfile",
    "content": "FROM scratch\nCOPY . ."
  },
  {
    "path": "test/integration/testdata/image-centos-match-coverage/etc/os-release",
    "content": "NAME=\"CentOS Linux\"\nVERSION=\"8 (Core)\"\nID=\"centos\"\nID_LIKE=\"rhel fedora\"\nVERSION_ID=\"8\"\nPLATFORM_ID=\"platform:el8\"\nPRETTY_NAME=\"CentOS Linux 8 (Core)\"\nANSI_COLOR=\"0;31\"\nCPE_NAME=\"cpe:/o:centos:centos:8\"\nHOME_URL=\"https://www.centos.org/\"\nBUG_REPORT_URL=\"https://bugs.centos.org/\""
  },
  {
    "path": "test/integration/testdata/image-centos-match-coverage/var/lib/rpm/generate-fixture.sh",
    "content": "#!/usr/bin/env bash\nset -eux\n\ndocker create --name generate-rpmdb-fixture centos:latest sh -c 'tail -f /dev/null'\n\nfunction cleanup {\n  docker kill generate-rpmdb-fixture\n  docker rm generate-rpmdb-fixture\n}\ntrap cleanup EXIT\n\ndocker start generate-rpmdb-fixture\ndocker exec -i --tty=false generate-rpmdb-fixture bash <<-EOF\n  mkdir -p /scratch\n  cd /scratch\n  rpm --initdb --dbpath /scratch\n  curl -sSLO https://github.com/wagoodman/dive/releases/download/v0.9.2/dive_0.9.2_linux_amd64.rpm\n  rpm --dbpath /scratch -ivh dive_0.9.2_linux_amd64.rpm\n  rm dive_0.9.2_linux_amd64.rpm\n  rpm --dbpath /scratch -qa\nEOF\n\ndocker cp generate-rpmdb-fixture:/scratch/Packages .\n"
  },
  {
    "path": "test/integration/testdata/image-debian-match-coverage/Dockerfile",
    "content": "FROM docker.io/golang:1.16@sha256:92ccbb6513249c08e582ca3eafc5c9176dbc5cbfe73af245542c5c78250e9b49\nWORKDIR /go/src/github.com/anchore/test/\nCOPY golang/ ./\nRUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o go-app .\n\nFROM scratch\nCOPY --from=0 /go/src/github.com/anchore/test/go-app ./\nCOPY . .\n"
  },
  {
    "path": "test/integration/testdata/image-debian-match-coverage/dotnet/TestLibrary.deps.json",
    "content": "{\n  \"runtimeTarget\": {\n    \"name\": \".NETCoreApp,Version=v6.0\",\n    \"signature\": \"\"\n  },\n  \"compilationOptions\": {},\n  \"targets\": {\n    \".NETCoreApp,Version=v6.0\": {\n      \"TestLibrary/1.0.0\": {\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection\": \"6.0.0\",\n          \"Microsoft.Extensions.Logging\": \"6.0.0\",\n          \"Newtonsoft.Json\": \"13.0.1\",\n          \"Serilog\": \"2.10.0\",\n          \"Serilog.Sinks.Console\": \"4.0.1\",\n          \"TestCommon\": \"1.0.0\"\n        },\n        \"runtime\": {\n          \"TestLibrary.dll\": {}\n        }\n      },\n      \"AWSSDK.Core/3.7.10.6\": {\n        \"runtime\": {\n          \"lib/netcoreapp3.1/AWSSDK.Core.dll\": {\n            \"assemblyVersion\": \"3.3.0.0\",\n            \"fileVersion\": \"3.7.10.6\"\n          }\n        }\n      },\n      \"Microsoft.Extensions.DependencyInjection/6.0.0\": {\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"6.0.0\",\n          \"System.Runtime.CompilerServices.Unsafe\": \"6.0.0\"\n        },\n        \"runtime\": {\n          \"lib/net6.0/Microsoft.Extensions.DependencyInjection.dll\": {\n            \"assemblyVersion\": \"6.0.0.0\",\n            \"fileVersion\": \"6.0.21.52210\"\n          }\n        }\n      },\n      \"Microsoft.Extensions.DependencyInjection.Abstractions/6.0.0\": {\n        \"runtime\": {\n          \"lib/net6.0/Microsoft.Extensions.DependencyInjection.Abstractions.dll\": {\n            \"assemblyVersion\": \"6.0.0.0\",\n            \"fileVersion\": \"6.0.21.52210\"\n          }\n        }\n      },\n      \"Microsoft.Extensions.Logging/6.0.0\": {\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection\": \"6.0.0\",\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"6.0.0\",\n          \"Microsoft.Extensions.Logging.Abstractions\": \"6.0.0\",\n          \"Microsoft.Extensions.Options\": \"6.0.0\",\n          \"System.Diagnostics.DiagnosticSource\": \"6.0.0\"\n        },\n        \"runtime\": {\n          \"lib/netstandard2.1/Microsoft.Extensions.Logging.dll\": {\n            \"assemblyVersion\": \"6.0.0.0\",\n            \"fileVersion\": \"6.0.21.52210\"\n          }\n        }\n      },\n      \"Microsoft.Extensions.Logging.Abstractions/6.0.0\": {\n        \"runtime\": {\n          \"lib/net6.0/Microsoft.Extensions.Logging.Abstractions.dll\": {\n            \"assemblyVersion\": \"6.0.0.0\",\n            \"fileVersion\": \"6.0.21.52210\"\n          }\n        }\n      },\n      \"Microsoft.Extensions.Options/6.0.0\": {\n        \"dependencies\": {\n          \"Microsoft.Extensions.DependencyInjection.Abstractions\": \"6.0.0\",\n          \"Microsoft.Extensions.Primitives\": \"6.0.0\"\n        },\n        \"runtime\": {\n          \"lib/netstandard2.1/Microsoft.Extensions.Options.dll\": {\n            \"assemblyVersion\": \"6.0.0.0\",\n            \"fileVersion\": \"6.0.21.52210\"\n          }\n        }\n      },\n      \"Microsoft.Extensions.Primitives/6.0.0\": {\n        \"dependencies\": {\n          \"System.Runtime.CompilerServices.Unsafe\": \"6.0.0\"\n        },\n        \"runtime\": {\n          \"lib/net6.0/Microsoft.Extensions.Primitives.dll\": {\n            \"assemblyVersion\": \"6.0.0.0\",\n            \"fileVersion\": \"6.0.21.52210\"\n          }\n        }\n      },\n      \"Newtonsoft.Json/13.0.1\": {\n        \"runtime\": {\n          \"lib/netstandard2.0/Newtonsoft.Json.dll\": {\n            \"assemblyVersion\": \"13.0.0.0\",\n            \"fileVersion\": \"13.0.1.25517\"\n          }\n        }\n      },\n      \"Serilog/2.10.0\": {\n        \"runtime\": {\n          \"lib/netstandard2.1/Serilog.dll\": {\n            \"assemblyVersion\": \"2.0.0.0\",\n            \"fileVersion\": \"2.10.0.0\"\n          }\n        }\n      },\n      \"Serilog.Sinks.Console/4.0.1\": {\n        \"dependencies\": {\n          \"Serilog\": \"2.10.0\"\n        },\n        \"runtime\": {\n          \"lib/net5.0/Serilog.Sinks.Console.dll\": {\n            \"assemblyVersion\": \"4.0.1.0\",\n            \"fileVersion\": \"4.0.1.0\"\n          }\n        }\n      },\n      \"System.Diagnostics.DiagnosticSource/6.0.0\": {\n        \"dependencies\": {\n          \"System.Runtime.CompilerServices.Unsafe\": \"6.0.0\"\n        }\n      },\n      \"System.Runtime.CompilerServices.Unsafe/6.0.0\": {},\n      \"TestCommon/1.0.0\": {\n        \"dependencies\": {\n          \"AWSSDK.Core\": \"3.7.10.6\"\n        },\n        \"runtime\": {\n          \"TestCommon.dll\": {}\n        }\n      }\n    }\n  },\n  \"libraries\": {\n    \"TestLibrary/1.0.0\": {\n      \"type\": \"project\",\n      \"serviceable\": false,\n      \"sha512\": \"\"\n    },\n    \"AWSSDK.Core/3.7.10.6\": {\n      \"type\": \"package\",\n      \"serviceable\": true,\n      \"sha512\": \"sha512-kHBB+QmosVaG6DpngXQ8OlLVVNMzltNITfsRr68Z90qO7dSqJ2EHNd8dtBU1u3AQQLqqFHOY0lfmbpexeH6Pew==\",\n      \"path\": \"awssdk.core/3.7.10.6\",\n      \"hashPath\": \"awssdk.core.3.7.10.6.nupkg.sha512\"\n    }\n  }\n}"
  },
  {
    "path": "test/integration/testdata/image-debian-match-coverage/golang/go.mod",
    "content": "module github.com/anchore/coverage\n\ngo 1.18\n\nrequire github.com/google/uuid v1.3.0\n"
  },
  {
    "path": "test/integration/testdata/image-debian-match-coverage/golang/go.sum",
    "content": "github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=\ngithub.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\n"
  },
  {
    "path": "test/integration/testdata/image-debian-match-coverage/golang/main.go",
    "content": "package main\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/google/uuid\"\n)\n\nfunc main() {\n\tfmt.Println(uuid.New())\n}\n"
  },
  {
    "path": "test/integration/testdata/image-debian-match-coverage/haskell/cabal.project.freeze",
    "content": "active-repositories: hackage.haskell.org:merge\nconstraints: any.Cabal ==3.2.1.0,\n             any.Diff ==0.4.1,\n             any.HTTP ==4000.3.16,\n             any.HUnit ==1.6.2.0,\n             any.OneTuple ==0.3.1,\n             tls +compat -hans +network,\n             any.Only ==0.1,\n             any.PyF ==0.10.2.0,\n             any.QuickCheck ==2.14.2,\n             semigroups +binary +bytestring -bytestring-builder +containers +deepseq +hashable +tagged +template-haskell +text +transformers +unordered-containers,\n             any.RSA ==2.4.1,\n             any.SHA ==1.6.4.4,\n             void -safe,\n             any.Spock ==0.14.0.0,\n\nindex-state: hackage.haskell.org 2022-07-07T01:01:53Z\n"
  },
  {
    "path": "test/integration/testdata/image-debian-match-coverage/haskell/stack.yaml",
    "content": "flags: {}\nextra-package-dbs: []\npackages:\n  - .\nresolver: lts-18.28\nextra-deps:\n  - ShellCheck-0.8.0@sha256:353c9322847b661e4c6f7c83c2acf8e5c08b682fbe516c7d46c29605937543df,3297\n  - colourista-0.1.0.1@sha256:98353ee0e2f5d97d2148513f084c1cd37dfda03e48aa9dd7a017c9d9c0ba710e,3307\n  - language-docker-11.0.0@sha256:3406ff0c1d592490f53ead8cf2cd22bdf3d79fd125ccaf3add683f6d71c24d55,3883\n  - spdx-1.0.0.2@sha256:7dfac9b454ff2da0abb7560f0ffbe00ae442dd5cb76e8be469f77e6988a70fed,2008\n  - hspec-2.9.4@sha256:658a6a74d5a70c040edd6df2a12228c6d9e63082adaad1ed4d0438ad082a0ef3,1709\n  - hspec-core-2.9.4@sha256:a126e9087409fef8dcafcd2f8656456527ac7bb163ed4d9cb3a57589042a5fe8,6498\n  - hspec-discover-2.9.4@sha256:fbcf49ecfc3d4da53e797fd0275264cba776ffa324ee223e2a3f4ec2d2c9c4a6,2165\n  - stm-2.5.0.2@sha256:e4dc6473faaa75fbd7eccab4e3ee1d651d75bb0e49946ef0b8b751ccde771a55,2314\nghc-options:\n  \"$everything\": -haddock\n"
  },
  {
    "path": "test/integration/testdata/image-debian-match-coverage/java/generate-fixtures.md",
    "content": "See the syft/cataloger/java/testdata/java-builds dir to generate test fixtures and copy to here manually."
  },
  {
    "path": "test/integration/testdata/image-debian-match-coverage/javascript/pkg-json/package.json",
    "content": "{\n  \"version\": \"6.14.6\",\n  \"name\": \"npm\",\n  \"description\": \"a package manager for JavaScript\",\n  \"keywords\": [\n    \"install\",\n    \"modules\",\n    \"package manager\",\n    \"package.json\"\n  ],\n  \"preferGlobal\": true,\n  \"config\": {\n    \"publishtest\": false\n  },\n  \"homepage\": \"https://docs.npmjs.com/\",\n  \"author\": \"Isaac Z. Schlueter <i@izs.me> (http://blog.izs.me)\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/npm/cli\"\n  },\n  \"bugs\": {\n    \"url\": \"https://npm.community/c/bugs\"\n  },\n  \"directories\": {\n    \"bin\": \"./bin\",\n    \"doc\": \"./doc\",\n    \"lib\": \"./lib\",\n    \"man\": \"./man\"\n  },\n  \"main\": \"./lib/npm.js\",\n  \"bin\": {\n    \"npm\": \"./bin/npm-cli.js\",\n    \"npx\": \"./bin/npx-cli.js\"\n  },\n  \"dependencies\": {\n    \"JSONStream\": \"^1.3.5\",\n    \"abbrev\": \"~1.1.1\",\n    \"ansicolors\": \"~0.3.2\",\n    \"write-file-atomic\": \"^2.4.3\"\n  },\n  \"bundleDependencies\": [\n    \"abbrev\",\n    \"ansicolors\",\n    \"ansistyles\",\n    \"write-file-atomic\"\n  ],\n  \"devDependencies\": {\n    \"deep-equal\": \"^1.0.1\",\n    \"get-stream\": \"^4.1.0\",\n    \"licensee\": \"^7.0.3\",\n    \"marked\": \"^0.6.3\",\n    \"marked-man\": \"^0.6.0\",\n    \"npm-registry-couchapp\": \"^2.7.4\",\n    \"npm-registry-mock\": \"^1.3.1\",\n    \"require-inject\": \"^1.4.4\",\n    \"sprintf-js\": \"^1.1.2\",\n    \"standard\": \"^11.0.1\",\n    \"tacks\": \"^1.3.0\",\n    \"tap\": \"^12.7.0\",\n    \"tar-stream\": \"^2.1.0\"\n  },\n  \"scripts\": {\n    \"dumpconf\": \"env | grep npm | sort | uniq\",\n    \"prepare\": \"node bin/npm-cli.js rebuild && node bin/npm-cli.js --no-audit --no-timing prune --prefix=. --no-global && rimraf test/*/*/node_modules && make -j4 mandocs\",\n    \"preversion\": \"bash scripts/update-authors.sh && git add AUTHORS && git commit -m \\\"update AUTHORS\\\" || true\",\n    \"licenses\": \"licensee --production --errors-only\",\n    \"tap\": \"tap -J --timeout 300 --no-esm\",\n    \"tap-cover\": \"tap -J --nyc-arg=--cache --coverage --timeout 600 --no-esm\",\n    \"lint\": \"standard\",\n    \"pretest\": \"npm run lint\",\n    \"test\": \"npm run test-tap --\",\n    \"test:nocleanup\": \"NO_TEST_CLEANUP=1 npm run test --\",\n    \"sudotest\": \"sudo npm run tap -- \\\"test/tap/*.js\\\"\",\n    \"sudotest:nocleanup\": \"sudo NO_TEST_CLEANUP=1 npm run tap -- \\\"test/tap/*.js\\\"\",\n    \"posttest\": \"rimraf test/npm_cache*\",\n    \"test-coverage\": \"npm run tap-cover -- \\\"test/tap/*.js\\\" \\\"test/network/*.js\\\"\",\n    \"test-tap\": \"npm run tap -- \\\"test/tap/*.js\\\" \\\"test/network/*.js\\\"\",\n    \"test-node\": \"tap --timeout 240 \\\"test/tap/*.js\\\" \\\"test/network/*.js\\\"\"\n  },\n  \"license\": \"Artistic-2.0\",\n  \"engines\": {\n    \"node\": \"6 >=6.2.0 || 8 || >=9.3.0\"\n  }\n}"
  },
  {
    "path": "test/integration/testdata/image-debian-match-coverage/python/dist-info/METADATA",
    "content": "Metadata-Version: 2.1\nName: Pygments\nVersion: 2.6.1\nSummary: Pygments is a syntax highlighting package written in Python.\nHome-page: https://pygments.org/\nAuthor: Georg Brandl\nAuthor-email: georg@python.org\nLicense: BSD License\nKeywords: syntax highlighting\nPlatform: any\nClassifier: License :: OSI Approved :: BSD License\nClassifier: Intended Audience :: Developers\nClassifier: Intended Audience :: End Users/Desktop\nClassifier: Intended Audience :: System Administrators\nClassifier: Development Status :: 6 - Mature\nClassifier: Programming Language :: Python\nClassifier: Programming Language :: Python :: 3\nClassifier: Programming Language :: Python :: 3.5\nClassifier: Programming Language :: Python :: 3.6\nClassifier: Programming Language :: Python :: 3.7\nClassifier: Programming Language :: Python :: 3.8\nClassifier: Programming Language :: Python :: Implementation :: CPython\nClassifier: Programming Language :: Python :: Implementation :: PyPy\nClassifier: Operating System :: OS Independent\nClassifier: Topic :: Text Processing :: Filters\nClassifier: Topic :: Utilities\nRequires-Python: >=3.5\n\n\nPygments\n~~~~~~~~\n\nPygments is a syntax highlighting package written in Python.\n\nIt is a generic syntax highlighter suitable for use in code hosting, forums,\nwikis or other applications that need to prettify source code.  Highlights\nare:\n\n* a wide range of over 500 languages and other text formats is supported\n* special attention is paid to details, increasing quality by a fair amount\n* support for new languages and formats are added easily\n* a number of output formats, presently HTML, LaTeX, RTF, SVG, all image     formats that PIL supports and ANSI sequences\n* it is usable as a command-line tool and as a library\n\n:copyright: Copyright 2006-2019 by the Pygments team, see AUTHORS.\n:license: BSD, see LICENSE for details.\n\n"
  },
  {
    "path": "test/integration/testdata/image-debian-match-coverage/python/dist-info/top_level.txt",
    "content": "pygments"
  },
  {
    "path": "test/integration/testdata/image-debian-match-coverage/ruby/specifications/bundler.gemspec",
    "content": "# frozen_string_literal: true\n# -*- encoding: utf-8 -*-\n# stub: bundler 2.1.4 ruby lib\n\nGem::Specification.new do |s|\n  s.name = \"bundler\".freeze\n  s.version = \"2.1.4\"\n\n  s.required_rubygems_version = Gem::Requirement.new(\">= 2.5.2\".freeze) if s.respond_to? :required_rubygems_version=\n  s.metadata = { \"bug_tracker_uri\" => \"https://github.com/bundler/bundler/issues\", \"changelog_uri\" => \"https://github.com/bundler/bundler/blob/master/CHANGELOG.md\", \"homepage_uri\" => \"https://bundler.io/\", \"source_code_uri\" => \"https://github.com/bundler/bundler/\" } if s.respond_to? :metadata=\n  s.require_paths = [\"lib\".freeze]\n  s.authors = [\"Andr\\u00E9 Arko\".freeze, \"Samuel Giddins\".freeze, \"Colby Swandale\".freeze, \"Hiroshi Shibata\".freeze, \"David Rodr\\u00EDguez\".freeze, \"Grey Baker\".freeze, \"Stephanie Morillo\".freeze, \"Chris Morris\".freeze, \"James Wen\".freeze, \"Tim Moore\".freeze, \"Andr\\u00E9 Medeiros\".freeze, \"Jessica Lynn Suttles\".freeze, \"Terence Lee\".freeze, \"Carl Lerche\".freeze, \"Yehuda Katz\".freeze]\n  s.bindir = \"exe\".freeze\n  s.date = \"2020-01-05\"\n  s.description = \"Bundler manages an application's dependencies through its entire life, across many machines, systematically and repeatably\".freeze\n  s.email = [\"team@bundler.io\".freeze]\n  s.executables = [\"bundle\".freeze, \"bundler\".freeze]\n  s.files = [\"exe/bundle\".freeze, \"exe/bundler\".freeze]\n  s.homepage = \"https://bundler.io\".freeze\n  s.licenses = [\"MIT\".freeze]\n  s.required_ruby_version = Gem::Requirement.new(\">= 2.3.0\".freeze)\n  s.rubygems_version = \"3.1.2\".freeze\n  s.summary = \"The best way to manage your application's dependencies\".freeze\n\n  s.installed_by_version = \"3.1.2\" if s.respond_to? :installed_by_version\nend"
  },
  {
    "path": "test/integration/testdata/image-debian-match-coverage/usr/lib/os-release",
    "content": "PRETTY_NAME=\"Debian GNU/Linux 8 (jessie)\"\nNAME=\"Debian GNU/Linux\"\nVERSION_ID=\"8\"\nVERSION=\"8 (jessie)\"\nID=debian\nHOME_URL=\"http://www.debian.org/\"\nSUPPORT_URL=\"http://www.debian.org/support\"\nBUG_REPORT_URL=\"https://bugs.debian.org/\"\n"
  },
  {
    "path": "test/integration/testdata/image-debian-match-coverage/var/lib/dpkg/status",
    "content": "Package: apt\nStatus: install ok installed\nPriority: required\nSection: admin\nInstalled-Size: 4064\nMaintainer: APT Development Team <deity@lists.debian.org>\nArchitecture: amd64\nVersion: 1.8.2\nSource: apt-dev\nReplaces: apt-transport-https (<< 1.5~alpha4~), apt-utils (<< 1.3~exp2~)\nProvides: apt-transport-https (= 1.8.2)\nDepends: adduser, gpgv | gpgv2 | gpgv1, debian-archive-keyring, libapt-pkg5.0 (>= 1.7.0~alpha3~), libc6 (>= 2.15), libgcc1 (>= 1:3.0), libgnutls30 (>= 3.6.6), libseccomp2 (>= 1.0.1), libstdc++6 (>= 5.2)\nRecommends: ca-certificates\nSuggests: apt-doc, aptitude | synaptic | wajig, dpkg-dev (>= 1.17.2), gnupg | gnupg2 | gnupg1, powermgmt-base\nBreaks: apt-transport-https (<< 1.5~alpha4~), apt-utils (<< 1.3~exp2~), aptitude (<< 0.8.10)\nConffiles:\n /etc/apt/apt.conf.d/01autoremove 76120d358bc9037bb6358e737b3050b5\n /etc/cron.daily/apt-compat 49e9b2cfa17849700d4db735d04244f3\n /etc/kernel/postinst.d/apt-auto-removal 4ad976a68f045517cf4696cec7b8aa3a\n /etc/logrotate.d/apt 179f2ed4f85cbaca12fa3d69c2a4a1c3\nDescription: commandline package manager\n This package provides commandline tools for searching and\n managing as well as querying information about packages\n as a low-level access to all features of the libapt-pkg library.\n .\n These include:\n  * apt-get for retrieval of packages and information about them\n    from authenticated sources and for installation, upgrade and\n    removal of packages together with their dependencies\n  * apt-cache for querying available information about installed\n    as well as installable packages\n  * apt-cdrom to use removable media as a source for packages\n  * apt-config as an interface to the configuration settings\n  * apt-key as an interface to manage authentication keys\n\n"
  },
  {
    "path": "test/integration/testdata/image-jvm-match-coverage/Dockerfile",
    "content": "FROM scratch\nCOPY . .\n"
  },
  {
    "path": "test/integration/testdata/image-jvm-match-coverage/opt/java/openjdk/release",
    "content": "JAVA_VERSION=\"1.8.0_400\"\nFULL_VERSION=\"1.8.0_400-b07\"\nNOPE_SEMANTIC_VERSION=\"8.0.400+7\"\nIMPLEMENTOR=\"Oracle\"\nIMAGE_TYPE=\"JDK\"\n"
  },
  {
    "path": "test/integration/testdata/image-portage-match-coverage/Dockerfile",
    "content": "FROM scratch\nCOPY . ."
  },
  {
    "path": "test/integration/testdata/image-portage-match-coverage/etc/os-release",
    "content": "NAME=Gentoo\nID=gentoo\nPRETTY_NAME=\"Gentoo Linux\"\nANSI_COLOR=\"1;32\"\nHOME_URL=\"https://www.gentoo.org/\"\nSUPPORT_URL=\"https://www.gentoo.org/support/\"\nBUG_REPORT_URL=\"https://bugs.gentoo.org/\"\nVERSION_ID=\"2.8\"\n"
  },
  {
    "path": "test/integration/testdata/image-portage-match-coverage/var/db/pkg/app-containers/skopeo-1.5.1/CONTENTS",
    "content": "dir /usr\ndir /usr/bin\nobj /usr/bin/skopeo 376c02bd3b22804df8fdfdc895e7dbfb 1649284374\ndir /etc\ndir /etc/containers\nobj /etc/containers/policy.json c01eb6950f03419e09d4fc88cb42ff6f 1649284375\ndir /etc/containers/registries.d\nobj /etc/containers/registries.d/default.yaml e6e66cd3c24623e0667f26542e0e08f6 1649284375\ndir /var\ndir /var/lib\ndir /var/lib/atomic\ndir /var/lib/atomic/sigstore\nobj /var/lib/atomic/sigstore/.keep_app-containers_skopeo-0 d41d8cd98f00b204e9800998ecf8427e 1649284375\n"
  },
  {
    "path": "test/integration/testdata/image-portage-match-coverage/var/db/pkg/app-containers/skopeo-1.5.1/LICENSE",
    "content": "Apache-2.0 BSD BSD-2 CC-BY-SA-4.0 ISC MIT\n"
  },
  {
    "path": "test/integration/testdata/image-portage-match-coverage/var/db/pkg/app-containers/skopeo-1.5.1/SIZE",
    "content": "27937835\n"
  },
  {
    "path": "test/integration/testdata/image-portage-match-coverage/var/db/repos/gentoo/skel.ebuild",
    "content": "# Copyright 1999-2022 Gentoo Authors\n# Distributed under the terms of the GNU General Public License v2\n\n# NOTE: The comments in this file are for instruction and documentation.\n# They're not meant to appear with your final, production ebuild.  Please\n# remember to remove them before submitting or committing your ebuild.  That\n# doesn't mean you can't add your own comments though.\n\n# The EAPI variable tells the ebuild format in use.\n# It is suggested that you use the latest EAPI approved by the Council.\n# The PMS contains specifications for all EAPIs. Eclasses will test for this\n# variable if they need to use features that are not universal in all EAPIs.\n# If an eclass doesn't support latest EAPI, use the previous EAPI instead.\nEAPI=7\n\n\n# inherit lists eclasses to inherit functions from. For example, an ebuild\n# that needs the eautoreconf function from autotools.eclass won't work\n# without the following line:\n#inherit autotools\n#\n# Eclasses tend to list descriptions of how to use their functions properly.\n# Take a look at the eclass/ directory for more examples.\n\n# Short one-line description of this package.\nDESCRIPTION=\"This is a sample skeleton ebuild file\""
  },
  {
    "path": "test/integration/testdata/image-rust-auditable-match-coverage/Dockerfile",
    "content": "# An image containing the example hello-auditable binary from https://github.com/Shnatsel/rust-audit/tree/master/hello-auditable\nFROM docker.io/tofay/hello-rust-auditable:latest"
  },
  {
    "path": "test/integration/testdata/image-sles-match-coverage/Dockerfile",
    "content": "FROM scratch\nCOPY . ."
  },
  {
    "path": "test/integration/testdata/image-sles-match-coverage/etc/os-release",
    "content": "NAME=\"SLES\"\nVERSION=\"12-SP5\"\nVERSION_ID=\"12.5\"\nPRETTY_NAME=\"SUSE Linux Enterprise Server 12 SP5\"\nID=\"sles\"\nID_LIKE=\"suse\"\nANSI_COLOR=\"0;32\"\nCPE_NAME=\"cpe:/o:suse:sles:12:sp5\"\nDOCUMENTATION_URL=\"https://documentation.suse.com/\""
  },
  {
    "path": "test/integration/testdata/image-sles-match-coverage/var/lib/rpm/generate-fixture.sh",
    "content": "#!/usr/bin/env bash\nset -eux\n\ndocker create --name generate-rpmdb-fixture sles12sp5:latest sh -c 'tail -f /dev/null'\n\nfunction cleanup {\n  docker kill generate-rpmdb-fixture\n  docker rm generate-rpmdb-fixture\n}\ntrap cleanup EXIT\n\ndocker start generate-rpmdb-fixture\ndocker exec -i --tty=false generate-rpmdb-fixture bash <<-EOF\n  mkdir -p /scratch\n  cd /scratch\n  rpm --initdb --dbpath /scratch\n  curl -sSLO https://github.com/wagoodman/dive/releases/download/v0.9.2/dive_0.9.2_linux_amd64.rpm\n  rpm --dbpath /scratch -ivh dive_0.9.2_linux_amd64.rpm\n  rm dive_0.9.2_linux_amd64.rpm\n  rpm --dbpath /scratch -qa\nEOF\n\ndocker cp generate-rpmdb-fixture:/scratch/Packages .\n"
  },
  {
    "path": "test/integration/testdata/sbom/syft-sbom-with-kb-packages.json",
    "content": "{\n  \"artifacts\": [\n    {\n      \"id\": \"eeb36c1c-c03a-425b-901f-df918cc3757e\",\n      \"name\": \"10816\",\n      \"version\": \"3200970\",\n      \"type\": \"msrc-kb\"\n    }\n  ],\n  \"artifactRelationships\": [],\n  \"source\": {\n    \"type\": \"image\",\n    \"target\": {\n      \"userInput\": \"my-awesome-image:latest\",\n      \"scope\": \"Squashed\"\n    }\n  },\n  \"distro\": {\n    \"name\": \"windows\",\n    \"version\": \"10816\",\n    \"idLike\": \"\"\n  },\n  \"descriptor\": {\n    \"name\": \"syft\",\n    \"version\": \"v0.19.1\"\n  },\n  \"schema\": {\n    \"version\": \"1.1.0\",\n    \"url\": \"https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-1.1.0.json\"\n  }\n}\n"
  },
  {
    "path": "test/integration/testdata/sbom/syft-sbom-with-unknown-packages.json",
    "content": "{\n  \"artifacts\": [\n    {\n      \"id\": \"eeb36c1c-c03a-425b-901f-df918cc3757e\",\n      \"name\": \"my-package\",\n      \"version\": \"1.0.5\",\n      \"type\": \"idris\",\n      \"language\": \"idris\",\n      \"cpes\": [\n        \"cpe:2.3:a:my-package:my-package:1.0.5:*:*:*:*:*:*:*\",\n        \"cpe:2.3:a:bogus:my-package:1.0.5:*:*:*:*:*:*:*\"\n      ]\n    }\n  ],\n  \"artifactRelationships\": [],\n  \"source\": {\n    \"type\": \"image\",\n    \"target\": {\n      \"userInput\": \"my-awesome-image:latest\",\n      \"scope\": \"Squashed\"\n    }\n  },\n  \"distro\": {\n    \"name\": \"ubuntu\",\n    \"version\": \"20.04\",\n    \"idLike\": \"\"\n  },\n  \"descriptor\": {\n    \"name\": \"syft\",\n    \"version\": \"v0.19.1\"\n  },\n  \"schema\": {\n    \"version\": \"1.1.0\",\n    \"url\": \"https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-1.1.0.json\"\n  }\n}\n"
  },
  {
    "path": "test/integration/testdata/skopeo-policy.json",
    "content": "{\n  \"default\": [\n    {\n      \"type\": \"insecureAcceptAnything\"\n    }\n  ],\n  \"transports\": {\n    \"docker-daemon\": {\n      \"\": [\n        {\n          \"type\": \"insecureAcceptAnything\"\n        }\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "test/integration/testdata/snapshot/TestDatabaseDiff.golden",
    "content": "[\n {\n  \"reason\": \"added\",\n  \"id\": \"ALAS-2022-145\",\n  \"namespace\": \"amazon:distro:amazonlinux:2022\",\n  \"packages\": [\n   \"curl\",\n   \"curl-debuginfo\",\n   \"curl-debugsource\",\n   \"curl-minimal\",\n   \"curl-minimal-debuginfo\",\n   \"libcurl\",\n   \"libcurl-debuginfo\",\n   \"libcurl-devel\",\n   \"libcurl-minimal\",\n   \"libcurl-minimal-debuginfo\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"ALAS-2022-146\",\n  \"namespace\": \"amazon:distro:amazonlinux:2022\",\n  \"packages\": [\n   \"lua\",\n   \"lua-debuginfo\",\n   \"lua-debugsource\",\n   \"lua-devel\",\n   \"lua-libs\",\n   \"lua-libs-debuginfo\",\n   \"lua-static\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"ALAS-2022-147\",\n  \"namespace\": \"amazon:distro:amazonlinux:2022\",\n  \"packages\": [\n   \"openssl\",\n   \"openssl-debuginfo\",\n   \"openssl-debugsource\",\n   \"openssl-devel\",\n   \"openssl-libs\",\n   \"openssl-libs-debuginfo\",\n   \"openssl-perl\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"ALAS-2022-148\",\n  \"namespace\": \"amazon:distro:amazonlinux:2022\",\n  \"packages\": [\n   \"rsync\",\n   \"rsync-daemon\",\n   \"rsync-debuginfo\",\n   \"rsync-debugsource\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"ALAS-2022-149\",\n  \"namespace\": \"amazon:distro:amazonlinux:2022\",\n  \"packages\": [\n   \"python3-subversion\",\n   \"python3-subversion-debuginfo\",\n   \"subversion\",\n   \"subversion-debuginfo\",\n   \"subversion-debugsource\",\n   \"subversion-devel\",\n   \"subversion-devel-debuginfo\",\n   \"subversion-javahl\",\n   \"subversion-libs\",\n   \"subversion-libs-debuginfo\",\n   \"subversion-perl\",\n   \"subversion-perl-debuginfo\",\n   \"subversion-tools\",\n   \"subversion-tools-debuginfo\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"ALAS-2022-150\",\n  \"namespace\": \"amazon:distro:amazonlinux:2022\",\n  \"packages\": [\n   \"bpftool\",\n   \"bpftool-debuginfo\",\n   \"kernel\",\n   \"kernel-debuginfo\",\n   \"kernel-debuginfo-common-x86_64\",\n   \"kernel-devel\",\n   \"kernel-headers\",\n   \"kernel-libbpf\",\n   \"kernel-libbpf-devel\",\n   \"kernel-libbpf-static\",\n   \"kernel-livepatch-5.15.72-43.134\",\n   \"kernel-tools\",\n   \"kernel-tools-debuginfo\",\n   \"kernel-tools-devel\",\n   \"perf\",\n   \"perf-debuginfo\",\n   \"python3-perf\",\n   \"python3-perf-debuginfo\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"ALASDOCKER-2022-020\",\n  \"namespace\": \"amazon:distro:amazonlinux:2\",\n  \"packages\": [\n   \"runc\",\n   \"runc-debuginfo\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"ALASDOCKER-2022-021\",\n  \"namespace\": \"amazon:distro:amazonlinux:2\",\n  \"packages\": [\n   \"containerd\",\n   \"containerd-debuginfo\",\n   \"containerd-stress\",\n   \"docker\",\n   \"docker-debuginfo\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"ALASKERNEL-5.10-2022-020\",\n  \"namespace\": \"amazon:distro:amazonlinux:2\",\n  \"packages\": [\n   \"bpftool\",\n   \"bpftool-debuginfo\",\n   \"kernel\",\n   \"kernel-debuginfo\",\n   \"kernel-debuginfo-common-x86_64\",\n   \"kernel-devel\",\n   \"kernel-headers\",\n   \"kernel-livepatch-5.10.144-127.601\",\n   \"kernel-tools\",\n   \"kernel-tools-debuginfo\",\n   \"kernel-tools-devel\",\n   \"perf\",\n   \"perf-debuginfo\",\n   \"python-perf\",\n   \"python-perf-debuginfo\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"ALASKERNEL-5.15-2022-008\",\n  \"namespace\": \"amazon:distro:amazonlinux:2\",\n  \"packages\": [\n   \"bpftool\",\n   \"bpftool-debuginfo\",\n   \"kernel\",\n   \"kernel-debuginfo\",\n   \"kernel-debuginfo-common-x86_64\",\n   \"kernel-devel\",\n   \"kernel-headers\",\n   \"kernel-livepatch-5.15.69-37.134\",\n   \"kernel-tools\",\n   \"kernel-tools-debuginfo\",\n   \"kernel-tools-devel\",\n   \"perf\",\n   \"perf-debuginfo\",\n   \"python-perf\",\n   \"python-perf-debuginfo\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"ALASKERNEL-5.4-2022-036\",\n  \"namespace\": \"amazon:distro:amazonlinux:2\",\n  \"packages\": [\n   \"bpftool\",\n   \"bpftool-debuginfo\",\n   \"kernel\",\n   \"kernel-debuginfo\",\n   \"kernel-debuginfo-common-x86_64\",\n   \"kernel-devel\",\n   \"kernel-headers\",\n   \"kernel-tools\",\n   \"kernel-tools-debuginfo\",\n   \"kernel-tools-devel\",\n   \"perf\",\n   \"perf-debuginfo\",\n   \"python-perf\",\n   \"python-perf-debuginfo\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2013-0350\",\n  \"namespace\": \"debian:distro:debian:12\",\n  \"packages\": [\n   \"pktstat\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2014-125002\",\n  \"namespace\": \"ubuntu:distro:ubuntu:16.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2014-125003\",\n  \"namespace\": \"ubuntu:distro:ubuntu:16.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2014-125004\",\n  \"namespace\": \"ubuntu:distro:ubuntu:16.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2014-125005\",\n  \"namespace\": \"ubuntu:distro:ubuntu:16.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2014-125006\",\n  \"namespace\": \"ubuntu:distro:ubuntu:16.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2014-125007\",\n  \"namespace\": \"ubuntu:distro:ubuntu:16.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2014-125008\",\n  \"namespace\": \"ubuntu:distro:ubuntu:16.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2014-125009\",\n  \"namespace\": \"ubuntu:distro:ubuntu:16.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2014-125010\",\n  \"namespace\": \"ubuntu:distro:ubuntu:16.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2014-125011\",\n  \"namespace\": \"ubuntu:distro:ubuntu:16.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2014-125012\",\n  \"namespace\": \"ubuntu:distro:ubuntu:16.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2014-125013\",\n  \"namespace\": \"ubuntu:distro:ubuntu:16.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2014-125014\",\n  \"namespace\": \"ubuntu:distro:ubuntu:16.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2014-125015\",\n  \"namespace\": \"ubuntu:distro:ubuntu:16.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2014-125016\",\n  \"namespace\": \"ubuntu:distro:ubuntu:16.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2014-125017\",\n  \"namespace\": \"ubuntu:distro:ubuntu:16.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2014-125018\",\n  \"namespace\": \"ubuntu:distro:ubuntu:16.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2014-125019\",\n  \"namespace\": \"ubuntu:distro:ubuntu:16.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2014-125020\",\n  \"namespace\": \"ubuntu:distro:ubuntu:16.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2014-125021\",\n  \"namespace\": \"ubuntu:distro:ubuntu:16.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2014-125022\",\n  \"namespace\": \"ubuntu:distro:ubuntu:16.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2014-125023\",\n  \"namespace\": \"ubuntu:distro:ubuntu:16.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2014-125024\",\n  \"namespace\": \"ubuntu:distro:ubuntu:16.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2014-125025\",\n  \"namespace\": \"ubuntu:distro:ubuntu:16.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2014-6274\",\n  \"namespace\": \"debian:distro:debian:12\",\n  \"packages\": [\n   \"git-annex\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2015-0283\",\n  \"namespace\": \"redhat:distro:redhat:7\",\n  \"packages\": [\n   \"ipa\",\n   \"slapi-nis\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2015-1827\",\n  \"namespace\": \"redhat:distro:redhat:7\",\n  \"packages\": [\n   \"ipa\",\n   \"slapi-nis\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2015-2779\",\n  \"namespace\": \"ubuntu:distro:ubuntu:14.04\",\n  \"packages\": [\n   \"quassel\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2017-12976\",\n  \"namespace\": \"debian:distro:debian:12\",\n  \"packages\": [\n   \"git-annex\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2017-20149\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2018-10857\",\n  \"namespace\": \"debian:distro:debian:12\",\n  \"packages\": [\n   \"git-annex\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2018-10859\",\n  \"namespace\": \"debian:distro:debian:12\",\n  \"packages\": [\n   \"git-annex\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2018-16860\",\n  \"namespace\": \"ubuntu:distro:ubuntu:18.04\",\n  \"packages\": [\n   \"heimdal\",\n   \"samba\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2018-17954\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"openstack_cloud\",\n   \"openstack_cloud_crowbar\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2018-18446\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"paint.net\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2018-18447\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"paint.net\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-12098\",\n  \"namespace\": \"ubuntu:distro:ubuntu:18.04\",\n  \"packages\": [\n   \"heimdal\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-13699\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"backports_sle\",\n   \"chrome\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-13700\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"backports_sle\",\n   \"chrome\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-13701\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"backports_sle\",\n   \"chrome\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-13702\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"backports_sle\",\n   \"chrome\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-13703\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"backports_sle\",\n   \"chrome\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-13704\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"backports_sle\",\n   \"chrome\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-13706\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"backports_sle\",\n   \"chrome\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-13708\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"backports_sle\",\n   \"chrome\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-13709\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"backports_sle\",\n   \"chrome\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-13710\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"backports_sle\",\n   \"chrome\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-13714\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"backports_sle\",\n   \"chrome\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-13715\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"backports_sle\",\n   \"chrome\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-13716\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"backports_sle\",\n   \"chrome\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-13717\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"backports_sle\",\n   \"chrome\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-13718\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"backports_sle\",\n   \"chrome\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-13719\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"backports_sle\",\n   \"chrome\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-18906\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"cryptctl\"\n  ]\n },\n {\n  \"reason\": \"removed\",\n  \"id\": \"CVE-2019-19274\",\n  \"namespace\": \"debian:distro:debian:12\",\n  \"packages\": [\n   \"python3-typed-ast\"\n  ]\n },\n {\n  \"reason\": \"removed\",\n  \"id\": \"CVE-2019-19275\",\n  \"namespace\": \"debian:distro:debian:12\",\n  \"packages\": [\n   \"python3-typed-ast\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-20326\",\n  \"namespace\": \"ubuntu:distro:ubuntu:20.04\",\n  \"packages\": [\n   \"gthumb\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-5477\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"nokogiri\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-5888\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"geocall\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-5889\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"geocall\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-5890\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"geocall\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-5891\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"geocall\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-5924\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"smart_forms\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-6002\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"central_dogma\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-6166\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"service_bridge\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-6167\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"service_bridge\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-6168\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"service_bridge\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-6169\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"service_bridge\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-6177\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"solution_center\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-6178\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-6179\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"xclarity_administrator\",\n   \"xclarity_integrator\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-6180\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"xclarity_administrator\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-6181\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"xclarity_administrator\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-6182\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"xclarity_administrator\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-6727\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"phantompdf\",\n   \"reader\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-6728\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"phantompdf\",\n   \"reader\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-6730\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"phantompdf\",\n   \"reader\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-6733\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"phantompdf\",\n   \"reader\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-6734\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"phantompdf\",\n   \"reader\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-6735\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"phantompdf\",\n   \"reader\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-6737\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"safepay\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-6741\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-6743\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"mi6_browser\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-6746\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"foxit_studio_photo\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-6747\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"foxit_studio_photo\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-6748\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"foxit_studio_photo\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-6749\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"foxit_studio_photo\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-6750\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"foxit_studio_photo\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-6751\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"foxit_studio_photo\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-6753\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"foxit_reader\",\n   \"phantompdf\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-6754\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"foxit_reader\",\n   \"phantompdf\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-6755\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"foxit_reader\",\n   \"phantompdf\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-6756\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"foxit_reader\",\n   \"phantompdf\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-6757\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"foxit_reader\",\n   \"phantompdf\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-6758\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"foxit_reader\",\n   \"phantompdf\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-6759\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"foxit_reader\",\n   \"phantompdf\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-6760\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"foxit_reader\",\n   \"phantompdf\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-6761\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"foxit_reader\",\n   \"phantompdf\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-6762\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"foxit_reader\",\n   \"phantompdf\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-6763\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"foxit_reader\",\n   \"phantompdf\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-6764\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"foxit_reader\",\n   \"phantompdf\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-6765\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"foxit_reader\",\n   \"phantompdf\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-6766\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"foxit_reader\",\n   \"phantompdf\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-6767\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"foxit_reader\",\n   \"phantompdf\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-6768\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"foxit_reader\",\n   \"phantompdf\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-6769\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"foxit_reader\",\n   \"phantompdf\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-6770\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"foxit_reader\",\n   \"phantompdf\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-6771\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"foxit_reader\",\n   \"phantompdf\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-6772\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"foxit_reader\",\n   \"phantompdf\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-6773\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"foxit_reader\",\n   \"phantompdf\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-6812\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-6822\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"zelio_soft_2\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-6823\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"proclima\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-6824\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"proclima\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-6827\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"interactive_graphical_scada_system\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-7061\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"acrobat_dc\",\n   \"acrobat_reader_dc\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-7088\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"acrobat_dc\",\n   \"acrobat_reader_dc\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-7096\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"flash_player\",\n   \"flash_player_desktop_runtime\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-7107\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"indesign\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-7108\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"flash_player\",\n   \"flash_player_desktop_runtime\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-7255\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-7256\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-7257\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-7258\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-7259\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-7261\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-7262\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-7265\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-7266\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-7267\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-7268\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-7269\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-7270\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-7273\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"enterprise\",\n   \"proton\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-7274\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"enterprise\",\n   \"proton\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-7275\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"enterprise\",\n   \"proton\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-7654\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"streaming_engine\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-7655\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"streaming_engine\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-7672\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"flexair\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-8292\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"online_store_system\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-8625\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"icloud\",\n   \"itunes\",\n   \"webkitgtk+\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-8674\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"safari\",\n   \"webkitgtk\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-8719\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"icloud\",\n   \"itunes\",\n   \"webkitgtk+\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2019-8764\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"webkitgtk+\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-8813\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"icloud\",\n   \"itunes\",\n   \"safari\",\n   \"webkitgtk+\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-8987\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"data_science_for_aws\",\n   \"spotfire_data_science\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-8988\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"data_science_for_aws\",\n   \"spotfire_data_science\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-8990\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"activematrix_businessworks\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-8991\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"activematrix_bpm\",\n   \"activematrix_policy_director\",\n   \"activematrix_service_bus\",\n   \"activematrix_service_grid\",\n   \"silver_fabric_enabler\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-8992\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"activematrix_bpm\",\n   \"activematrix_policy_director\",\n   \"activematrix_service_bus\",\n   \"activematrix_service_grid\",\n   \"silver_fabric_enabler\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-8993\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"activematrix_bpm\",\n   \"activematrix_policy_director\",\n   \"activematrix_service_bus\",\n   \"activematrix_service_grid\",\n   \"silver_fabric_enabler\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-8995\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"activematrix_bpm\",\n   \"silver_fabric_enabler\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-9213\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-9445\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-9850\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"libreoffice\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-9851\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"libreoffice\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2019-9855\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"libreoffice\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2020-0093\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"libexif\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2020-0181\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"libexif\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2020-0198\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"libexif\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2020-10735\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"python\",\n   \"quay\",\n   \"software_collections\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2020-10735\",\n  \"namespace\": \"ubuntu:distro:ubuntu:14.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2020-10735\",\n  \"namespace\": \"ubuntu:distro:ubuntu:16.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2020-10735\",\n  \"namespace\": \"ubuntu:distro:ubuntu:18.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2020-10735\",\n  \"namespace\": \"ubuntu:distro:ubuntu:20.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2020-10735\",\n  \"namespace\": \"ubuntu:distro:ubuntu:22.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"removed\",\n  \"id\": \"CVE-2020-12802\",\n  \"namespace\": \"ubuntu:distro:ubuntu:18.04\",\n  \"packages\": [\n   \"libreoffice\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2020-14129\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"xiaomi\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2020-14131\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"xiaomi\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2020-14305\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"cloud_backup\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2020-14314\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"starwind_virtual_san\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2020-14409\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"simple_directmedia_layer\",\n   \"starwind_virtual_san\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2020-16593\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"binutils\",\n   \"cloud_backup\",\n   \"ontap_select_deploy_administration_utility\",\n   \"solidfire_&_hci_management_node\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2020-17531\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"tapestry\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2020-24394\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"sd-wan_edge\",\n   \"starwind_virtual_san\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2020-25656\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"starwind_virtual_san\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2020-25692\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"cloud_backup\",\n   \"openldap\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2020-26247\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"nokogiri\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2020-26839\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2020-26840\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2020-26841\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2020-26842\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2020-26843\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2020-26844\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2020-26845\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2020-26846\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2020-26847\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2020-26848\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2020-26849\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2020-26850\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2020-26851\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2020-26852\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2020-26853\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2020-26854\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2020-26855\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2020-26856\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2020-26857\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2020-26858\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2020-26859\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2020-26860\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2020-26861\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2020-26862\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2020-26863\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2020-26864\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2020-26865\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2020-26866\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2020-2778\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"7-mode_transition_tool\",\n   \"active_iq_unified_manager\",\n   \"cloud_backup\",\n   \"cloud_secure_agent\",\n   \"e-series_performance_analyzer\",\n   \"e-series_santricity_os_controller\",\n   \"e-series_santricity_web_services\",\n   \"jdk\",\n   \"jre\",\n   \"oncommand_insight\",\n   \"oncommand_workflow_automation\",\n   \"openjdk\",\n   \"plug-in_for_symantec_netbackup\",\n   \"santricity_unified_manager\",\n   \"snapmanager\",\n   \"steelstore_cloud_integrated_storage\",\n   \"storagegrid\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2020-2783\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"outside_in_technology\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2020-2785\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"outside_in_technology\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2020-2786\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"outside_in_technology\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2020-2787\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"outside_in_technology\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2020-27918\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"icloud\",\n   \"itunes\",\n   \"safari\",\n   \"webkitgtk+\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2020-28383\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"jt2go\",\n   \"solid_edge\",\n   \"teamcenter_visualization\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2020-28851\",\n  \"namespace\": \"redhat:distro:redhat:8\",\n  \"packages\": [\n   \"container-tools:1.0/buildah\",\n   \"container-tools:1.0/podman\",\n   \"container-tools:2.0/buildah\",\n   \"container-tools:2.0/podman\",\n   \"container-tools:rhel8/buildah\",\n   \"container-tools:rhel8/podman\",\n   \"git-lfs\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2020-28852\",\n  \"namespace\": \"redhat:distro:redhat:8\",\n  \"packages\": [\n   \"container-tools:1.0/buildah\",\n   \"container-tools:1.0/podman\",\n   \"container-tools:2.0/buildah\",\n   \"container-tools:2.0/podman\",\n   \"container-tools:rhel8/buildah\",\n   \"container-tools:rhel8/podman\",\n   \"git-lfs\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2020-29651\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"py\",\n   \"zfs_storage_appliance_kit\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2020-35538\",\n  \"namespace\": \"redhat:distro:redhat:8\",\n  \"packages\": [\n   \"libjpeg-turbo\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2020-35538\",\n  \"namespace\": \"redhat:distro:redhat:9\",\n  \"packages\": [\n   \"libjpeg-turbo\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2020-36322\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"starwind_virtual_san\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2020-36427\",\n  \"namespace\": \"ubuntu:distro:ubuntu:18.04\",\n  \"packages\": [\n   \"gthumb\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2020-36427\",\n  \"namespace\": \"ubuntu:distro:ubuntu:20.04\",\n  \"packages\": [\n   \"gthumb\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2020-4301\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"cognos_analytics\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2020-6624\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"jhead\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2020-6625\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"jhead\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2020-7774\",\n  \"namespace\": \"debian:distro:debian:10\",\n  \"packages\": [\n   \"node-y18n\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2020-7774\",\n  \"namespace\": \"debian:distro:debian:11\",\n  \"packages\": [\n   \"node-y18n\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2020-7774\",\n  \"namespace\": \"debian:distro:debian:12\",\n  \"packages\": [\n   \"node-y18n\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2020-7774\",\n  \"namespace\": \"debian:distro:debian:unstable\",\n  \"packages\": [\n   \"node-y18n\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2020-7774\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"graalvm\",\n   \"sinec_infrastructure_network_services\",\n   \"y18n\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2020-9876\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"icloud\",\n   \"itunes\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2021-0696\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2021-0699\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2021-0951\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2021-20030\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"global_management_system\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2021-20468\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"cognos_analytics\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2021-20594\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2021-20597\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2021-20599\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2021-22543\",\n  \"namespace\": \"redhat:distro:redhat:7\",\n  \"packages\": [\n   \"kernel\",\n   \"kernel-alt\",\n   \"kernel-rt\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2021-22685\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"access_controller\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2021-27406\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2021-27853\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"ieee_802.2\",\n   \"ios_xe\",\n   \"p802.1q\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2021-27854\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"ieee_802.2\",\n   \"p802.1q\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2021-27861\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"ieee_802.2\",\n   \"p802.1q\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2021-27862\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"ieee_802.2\",\n   \"p802.1q\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2021-29823\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"cognos_analytics\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2021-30496\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"telegram\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2021-31439\",\n  \"namespace\": \"alpine:distro:alpine:3.16\",\n  \"packages\": [\n   \"netatalk\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2021-31439\",\n  \"namespace\": \"alpine:distro:alpine:edge\",\n  \"packages\": [\n   \"netatalk\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2021-31997\",\n  \"namespace\": \"debian:distro:debian:10\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2021-31997\",\n  \"namespace\": \"debian:distro:debian:11\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2021-31997\",\n  \"namespace\": \"debian:distro:debian:unstable\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2021-31997\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"python-postorius\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2021-33655\",\n  \"namespace\": \"ubuntu:distro:ubuntu:22.04\",\n  \"packages\": [\n   \"linux\",\n   \"linux-aws\",\n   \"linux-azure\",\n   \"linux-azure-fde\",\n   \"linux-gcp\",\n   \"linux-gke\",\n   \"linux-gkeop\",\n   \"linux-ibm\",\n   \"linux-intel-iotg\",\n   \"linux-kvm\",\n   \"linux-lowlatency\",\n   \"linux-oem-5.17\",\n   \"linux-oracle\",\n   \"linux-raspi\",\n   \"linux-riscv\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2021-35452\",\n  \"namespace\": \"debian:distro:debian:unstable\",\n  \"packages\": [\n   \"libde265\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2021-36201\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2021-36369\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"dropbear_ssh\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2021-36369\",\n  \"namespace\": \"ubuntu:distro:ubuntu:14.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2021-36369\",\n  \"namespace\": \"ubuntu:distro:ubuntu:16.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2021-36369\",\n  \"namespace\": \"ubuntu:distro:ubuntu:18.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2021-36369\",\n  \"namespace\": \"ubuntu:distro:ubuntu:20.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2021-36369\",\n  \"namespace\": \"ubuntu:distro:ubuntu:22.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2021-36408\",\n  \"namespace\": \"debian:distro:debian:unstable\",\n  \"packages\": [\n   \"libde265\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2021-36409\",\n  \"namespace\": \"debian:distro:debian:unstable\",\n  \"packages\": [\n   \"libde265\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2021-36410\",\n  \"namespace\": \"debian:distro:debian:unstable\",\n  \"packages\": [\n   \"libde265\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2021-36411\",\n  \"namespace\": \"debian:distro:debian:unstable\",\n  \"packages\": [\n   \"libde265\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2021-3671\",\n  \"namespace\": \"ubuntu:distro:ubuntu:18.04\",\n  \"packages\": [\n   \"heimdal\",\n   \"samba\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2021-3671\",\n  \"namespace\": \"ubuntu:distro:ubuntu:20.04\",\n  \"packages\": [\n   \"heimdal\",\n   \"samba\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2021-36778\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"rancher\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2021-36913\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"redirection_for_contact_form_7\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2021-36915\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"profile_builder\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2021-3807\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"ansi-regex\",\n   \"communications_cloud_native_core_policy\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2021-39009\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"cognos_analytics\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2021-39045\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"cognos_analytics\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2021-40017\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2021-40345\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"nagios_xi\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2021-40394\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"gerbv\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2021-4159\",\n  \"namespace\": \"ubuntu:distro:ubuntu:18.04\",\n  \"packages\": [\n   \"linux\",\n   \"linux-aws\",\n   \"linux-aws-5.4\",\n   \"linux-azure-4.15\",\n   \"linux-azure-5.4\",\n   \"linux-dell300x\",\n   \"linux-gcp-4.15\",\n   \"linux-gcp-5.4\",\n   \"linux-gke-5.4\",\n   \"linux-gkeop-5.4\",\n   \"linux-hwe-5.4\",\n   \"linux-ibm-5.4\",\n   \"linux-kvm\",\n   \"linux-oracle\",\n   \"linux-oracle-5.4\",\n   \"linux-raspi-5.4\",\n   \"linux-raspi2\",\n   \"linux-snapdragon\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2021-4159\",\n  \"namespace\": \"ubuntu:distro:ubuntu:20.04\",\n  \"packages\": [\n   \"linux\",\n   \"linux-aws\",\n   \"linux-azure\",\n   \"linux-azure-fde\",\n   \"linux-bluefield\",\n   \"linux-gcp\",\n   \"linux-gke\",\n   \"linux-gkeop\",\n   \"linux-ibm\",\n   \"linux-kvm\",\n   \"linux-oracle\",\n   \"linux-raspi\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2021-4217\",\n  \"namespace\": \"alpine:distro:alpine:edge\",\n  \"packages\": [\n   \"unzip\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2021-4217\",\n  \"namespace\": \"ubuntu:distro:ubuntu:18.04\",\n  \"packages\": [\n   \"unzip\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2021-4217\",\n  \"namespace\": \"ubuntu:distro:ubuntu:20.04\",\n  \"packages\": [\n   \"unzip\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2021-4217\",\n  \"namespace\": \"ubuntu:distro:ubuntu:22.04\",\n  \"packages\": [\n   \"unzip\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2021-43466\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"thymeleaf\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2021-43618\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"gmp\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2021-43766\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"odyssey\",\n   \"postgresql\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2021-43980\",\n  \"namespace\": \"redhat:distro:redhat:8\",\n  \"packages\": [\n   \"pki-deps:10.6/pki-servlet-engine\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2021-43980\",\n  \"namespace\": \"redhat:distro:redhat:9\",\n  \"packages\": [\n   \"pki-servlet-engine\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2021-44171\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2021-46839\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2021-46840\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-0030\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-0194\",\n  \"namespace\": \"alpine:distro:alpine:3.16\",\n  \"packages\": [\n   \"netatalk\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-0194\",\n  \"namespace\": \"alpine:distro:alpine:edge\",\n  \"packages\": [\n   \"netatalk\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-0529\",\n  \"namespace\": \"alpine:distro:alpine:edge\",\n  \"packages\": [\n   \"unzip\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-0529\",\n  \"namespace\": \"ubuntu:distro:ubuntu:18.04\",\n  \"packages\": [\n   \"unzip\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-0529\",\n  \"namespace\": \"ubuntu:distro:ubuntu:20.04\",\n  \"packages\": [\n   \"unzip\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-0529\",\n  \"namespace\": \"ubuntu:distro:ubuntu:22.04\",\n  \"packages\": [\n   \"unzip\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-0530\",\n  \"namespace\": \"alpine:distro:alpine:edge\",\n  \"packages\": [\n   \"unzip\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-0530\",\n  \"namespace\": \"ubuntu:distro:ubuntu:18.04\",\n  \"packages\": [\n   \"unzip\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-0530\",\n  \"namespace\": \"ubuntu:distro:ubuntu:20.04\",\n  \"packages\": [\n   \"unzip\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-0530\",\n  \"namespace\": \"ubuntu:distro:ubuntu:22.04\",\n  \"packages\": [\n   \"unzip\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-0669\",\n  \"namespace\": \"redhat:distro:redhat:7\",\n  \"packages\": [\n   \"dpdk\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-0669\",\n  \"namespace\": \"redhat:distro:redhat:8\",\n  \"packages\": [\n   \"dpdk\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-0812\",\n  \"namespace\": \"ubuntu:distro:ubuntu:18.04\",\n  \"packages\": [\n   \"linux\",\n   \"linux-aws\",\n   \"linux-aws-5.4\",\n   \"linux-azure-4.15\",\n   \"linux-azure-5.4\",\n   \"linux-dell300x\",\n   \"linux-gcp-4.15\",\n   \"linux-gcp-5.4\",\n   \"linux-hwe-5.4\",\n   \"linux-kvm\",\n   \"linux-oracle\",\n   \"linux-oracle-5.4\",\n   \"linux-raspi-5.4\",\n   \"linux-raspi2\",\n   \"linux-snapdragon\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-0836\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"sema_api\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-1011\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"build_of_quarkus\",\n   \"codeready_linux_builder\",\n   \"communications_cloud_native_core_binding_support_function\",\n   \"developer_tools\",\n   \"hci_baseboard_management_controller\",\n   \"virtualization_host\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-1012\",\n  \"namespace\": \"ubuntu:distro:ubuntu:18.04\",\n  \"packages\": [\n   \"linux\",\n   \"linux-aws\",\n   \"linux-aws-5.4\",\n   \"linux-azure-4.15\",\n   \"linux-azure-5.4\",\n   \"linux-dell300x\",\n   \"linux-gcp-4.15\",\n   \"linux-gcp-5.4\",\n   \"linux-gke-5.4\",\n   \"linux-gkeop-5.4\",\n   \"linux-hwe-5.4\",\n   \"linux-ibm-5.4\",\n   \"linux-kvm\",\n   \"linux-oracle\",\n   \"linux-oracle-5.4\",\n   \"linux-raspi-5.4\",\n   \"linux-raspi2\",\n   \"linux-snapdragon\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-1097\",\n  \"namespace\": \"redhat:distro:redhat:7\",\n  \"packages\": [\n   \"firefox\",\n   \"thunderbird\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-1097\",\n  \"namespace\": \"redhat:distro:redhat:8\",\n  \"packages\": [\n   \"firefox\",\n   \"thunderbird\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-1253\",\n  \"namespace\": \"debian:distro:debian:unstable\",\n  \"packages\": [\n   \"libde265\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-1259\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"build_of_quarkus\",\n   \"integration_camel_k\",\n   \"jboss_enterprise_application_platform\",\n   \"openshift_application_runtimes\",\n   \"single_sign-on\",\n   \"undertow\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-1319\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"openshift_application_runtimes\",\n   \"single_sign-on\",\n   \"undertow\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-1325\",\n  \"namespace\": \"debian:distro:debian:unstable\",\n  \"packages\": [\n   \"cimg\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-1354\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"libtiff\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-1355\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"libtiff\"\n  ]\n },\n {\n  \"reason\": \"removed\",\n  \"id\": \"CVE-2022-1480\",\n  \"namespace\": \"ubuntu:distro:ubuntu:18.04\",\n  \"packages\": [\n   \"chromium-browser\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-1560\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"amministrazione_aperta\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-1882\",\n  \"namespace\": \"ubuntu:distro:ubuntu:22.04\",\n  \"packages\": [\n   \"linux\",\n   \"linux-aws\",\n   \"linux-azure\",\n   \"linux-azure-fde\",\n   \"linux-gcp\",\n   \"linux-gke\",\n   \"linux-gkeop\",\n   \"linux-ibm\",\n   \"linux-intel-iotg\",\n   \"linux-kvm\",\n   \"linux-lowlatency\",\n   \"linux-oem-5.17\",\n   \"linux-oracle\",\n   \"linux-raspi\",\n   \"linux-riscv\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-20231\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-20351\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-20364\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-20369\",\n  \"namespace\": \"ubuntu:distro:ubuntu:18.04\",\n  \"packages\": [\n   \"linux\",\n   \"linux-aws\",\n   \"linux-aws-5.4\",\n   \"linux-azure-4.15\",\n   \"linux-azure-5.4\",\n   \"linux-dell300x\",\n   \"linux-gcp-4.15\",\n   \"linux-gcp-5.4\",\n   \"linux-hwe-5.4\",\n   \"linux-ibm-5.4\",\n   \"linux-kvm\",\n   \"linux-oracle\",\n   \"linux-oracle-5.4\",\n   \"linux-raspi-5.4\",\n   \"linux-raspi2\",\n   \"linux-snapdragon\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-20369\",\n  \"namespace\": \"ubuntu:distro:ubuntu:20.04\",\n  \"packages\": [\n   \"linux\",\n   \"linux-aws\",\n   \"linux-azure\",\n   \"linux-azure-5.15\",\n   \"linux-azure-fde\",\n   \"linux-bluefield\",\n   \"linux-gcp\",\n   \"linux-gke\",\n   \"linux-gkeop\",\n   \"linux-hwe-5.15\",\n   \"linux-ibm\",\n   \"linux-intel-iotg-5.15\",\n   \"linux-kvm\",\n   \"linux-lowlatency-hwe-5.15\",\n   \"linux-oem-5.14\",\n   \"linux-oracle\",\n   \"linux-raspi\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-20394\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-20397\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-20409\",\n  \"namespace\": \"debian:distro:debian:11\",\n  \"packages\": [\n   \"linux\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-20409\",\n  \"namespace\": \"debian:distro:debian:12\",\n  \"packages\": [\n   \"linux\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-20409\",\n  \"namespace\": \"debian:distro:debian:unstable\",\n  \"packages\": [\n   \"linux\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-20409\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-20410\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-20412\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-20413\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-20415\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-20416\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-20417\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-20418\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-20419\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-20420\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-20421\",\n  \"namespace\": \"debian:distro:debian:10\",\n  \"packages\": [\n   \"linux\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-20421\",\n  \"namespace\": \"debian:distro:debian:11\",\n  \"packages\": [\n   \"linux\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-20421\",\n  \"namespace\": \"debian:distro:debian:12\",\n  \"packages\": [\n   \"linux\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-20421\",\n  \"namespace\": \"debian:distro:debian:unstable\",\n  \"packages\": [\n   \"linux\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-20421\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-20422\",\n  \"namespace\": \"debian:distro:debian:10\",\n  \"packages\": [\n   \"linux\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-20422\",\n  \"namespace\": \"debian:distro:debian:11\",\n  \"packages\": [\n   \"linux\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-20422\",\n  \"namespace\": \"debian:distro:debian:12\",\n  \"packages\": [\n   \"linux\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-20422\",\n  \"namespace\": \"debian:distro:debian:unstable\",\n  \"packages\": [\n   \"linux\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-20422\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-20423\",\n  \"namespace\": \"debian:distro:debian:10\",\n  \"packages\": [\n   \"linux\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-20423\",\n  \"namespace\": \"debian:distro:debian:11\",\n  \"packages\": [\n   \"linux\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-20423\",\n  \"namespace\": \"debian:distro:debian:12\",\n  \"packages\": [\n   \"linux\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-20423\",\n  \"namespace\": \"debian:distro:debian:unstable\",\n  \"packages\": [\n   \"linux\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-20423\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-20425\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-20429\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-20430\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-20431\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-20432\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-20433\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-20434\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-20435\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-20436\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-20437\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-20438\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-20439\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-20440\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-20464\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-20830\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"sd-wan_vmanage\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-20837\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-20864\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-20870\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-20915\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-20920\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-20944\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-21797\",\n  \"namespace\": \"debian:distro:debian:12\",\n  \"packages\": [\n   \"joblib\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-21936\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"metasys_extended_application_and_data_server\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-2249\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"aura_communication_manager\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-22818\",\n  \"namespace\": \"debian:distro:debian:11\",\n  \"packages\": [\n   \"python-django\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-22818\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"django\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-2308\",\n  \"namespace\": \"debian:distro:debian:unstable\",\n  \"packages\": [\n   \"linux\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-23121\",\n  \"namespace\": \"alpine:distro:alpine:3.16\",\n  \"packages\": [\n   \"netatalk\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-23121\",\n  \"namespace\": \"alpine:distro:alpine:edge\",\n  \"packages\": [\n   \"netatalk\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-23122\",\n  \"namespace\": \"alpine:distro:alpine:3.16\",\n  \"packages\": [\n   \"netatalk\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-23122\",\n  \"namespace\": \"alpine:distro:alpine:edge\",\n  \"packages\": [\n   \"netatalk\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-23123\",\n  \"namespace\": \"alpine:distro:alpine:3.16\",\n  \"packages\": [\n   \"netatalk\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-23123\",\n  \"namespace\": \"alpine:distro:alpine:edge\",\n  \"packages\": [\n   \"netatalk\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-23124\",\n  \"namespace\": \"alpine:distro:alpine:3.16\",\n  \"packages\": [\n   \"netatalk\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-23124\",\n  \"namespace\": \"alpine:distro:alpine:edge\",\n  \"packages\": [\n   \"netatalk\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-23125\",\n  \"namespace\": \"alpine:distro:alpine:3.16\",\n  \"packages\": [\n   \"netatalk\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-23125\",\n  \"namespace\": \"alpine:distro:alpine:edge\",\n  \"packages\": [\n   \"netatalk\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-2318\",\n  \"namespace\": \"ubuntu:distro:ubuntu:18.04\",\n  \"packages\": [\n   \"linux\",\n   \"linux-aws\",\n   \"linux-aws-5.4\",\n   \"linux-azure-4.15\",\n   \"linux-azure-5.4\",\n   \"linux-dell300x\",\n   \"linux-gcp-4.15\",\n   \"linux-gcp-5.4\",\n   \"linux-hwe-5.4\",\n   \"linux-ibm-5.4\",\n   \"linux-kvm\",\n   \"linux-oracle\",\n   \"linux-oracle-5.4\",\n   \"linux-raspi-5.4\",\n   \"linux-raspi2\",\n   \"linux-snapdragon\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-2318\",\n  \"namespace\": \"ubuntu:distro:ubuntu:20.04\",\n  \"packages\": [\n   \"linux\",\n   \"linux-aws\",\n   \"linux-aws-5.15\",\n   \"linux-azure\",\n   \"linux-azure-5.15\",\n   \"linux-azure-fde\",\n   \"linux-bluefield\",\n   \"linux-gcp\",\n   \"linux-gcp-5.15\",\n   \"linux-gke\",\n   \"linux-gke-5.15\",\n   \"linux-gkeop\",\n   \"linux-hwe-5.15\",\n   \"linux-ibm\",\n   \"linux-intel-iotg-5.15\",\n   \"linux-kvm\",\n   \"linux-lowlatency-hwe-5.15\",\n   \"linux-oem-5.14\",\n   \"linux-oracle\",\n   \"linux-raspi\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-2318\",\n  \"namespace\": \"ubuntu:distro:ubuntu:22.04\",\n  \"packages\": [\n   \"linux\",\n   \"linux-aws\",\n   \"linux-azure\",\n   \"linux-azure-fde\",\n   \"linux-gcp\",\n   \"linux-gke\",\n   \"linux-gkeop\",\n   \"linux-ibm\",\n   \"linux-intel-iotg\",\n   \"linux-kvm\",\n   \"linux-lowlatency\",\n   \"linux-oem-5.17\",\n   \"linux-oracle\",\n   \"linux-raspi\",\n   \"linux-riscv\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-23833\",\n  \"namespace\": \"debian:distro:debian:11\",\n  \"packages\": [\n   \"python-django\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-23833\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"django\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-24106\",\n  \"namespace\": \"debian:distro:debian:10\",\n  \"packages\": [\n   \"poppler\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-24106\",\n  \"namespace\": \"debian:distro:debian:11\",\n  \"packages\": [\n   \"poppler\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-24106\",\n  \"namespace\": \"debian:distro:debian:12\",\n  \"packages\": [\n   \"poppler\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-24106\",\n  \"namespace\": \"debian:distro:debian:unstable\",\n  \"packages\": [\n   \"poppler\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-2447\",\n  \"namespace\": \"debian:distro:debian:12\",\n  \"packages\": [\n   \"python-keystonemiddleware\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-24697\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-2476\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"wavpack\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-24795\",\n  \"namespace\": \"debian:distro:debian:12\",\n  \"packages\": [\n   \"ruby-yajl\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-24795\",\n  \"namespace\": \"debian:distro:debian:unstable\",\n  \"packages\": [\n   \"ruby-yajl\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-24836\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"nokogiri\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-25235\",\n  \"namespace\": \"redhat:distro:redhat:8\",\n  \"packages\": [\n   \"expat\",\n   \"firefox\",\n   \"firefox:flatpak/firefox\",\n   \"thunderbird\",\n   \"thunderbird:flatpak/thunderbird\",\n   \"xmlrpc-c\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-25236\",\n  \"namespace\": \"redhat:distro:redhat:8\",\n  \"packages\": [\n   \"expat\",\n   \"firefox\",\n   \"firefox:flatpak/firefox\",\n   \"thunderbird\",\n   \"thunderbird:flatpak/thunderbird\",\n   \"xmlrpc-c\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-25315\",\n  \"namespace\": \"redhat:distro:redhat:8\",\n  \"packages\": [\n   \"expat\",\n   \"firefox\",\n   \"firefox:flatpak/firefox\",\n   \"thunderbird\",\n   \"thunderbird:flatpak/thunderbird\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-25648\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"extra_packages_for_enterprise_linux\",\n   \"git\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-26121\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"fortianalyzer\",\n   \"fortimanager\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-2625\",\n  \"namespace\": \"redhat:distro:redhat:8\",\n  \"packages\": [\n   \"postgresql\",\n   \"postgresql:10/postgresql\",\n   \"postgresql:12/postgresql\",\n   \"postgresql:13/postgresql\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-26305\",\n  \"namespace\": \"ubuntu:distro:ubuntu:18.04\",\n  \"packages\": [\n   \"libreoffice\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-26306\",\n  \"namespace\": \"ubuntu:distro:ubuntu:18.04\",\n  \"packages\": [\n   \"libreoffice\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-26307\",\n  \"namespace\": \"ubuntu:distro:ubuntu:18.04\",\n  \"packages\": [\n   \"libreoffice\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-26365\",\n  \"namespace\": \"ubuntu:distro:ubuntu:18.04\",\n  \"packages\": [\n   \"linux\",\n   \"linux-aws\",\n   \"linux-aws-5.4\",\n   \"linux-azure-4.15\",\n   \"linux-azure-5.4\",\n   \"linux-dell300x\",\n   \"linux-gcp-4.15\",\n   \"linux-gcp-5.4\",\n   \"linux-hwe-5.4\",\n   \"linux-ibm-5.4\",\n   \"linux-kvm\",\n   \"linux-oracle\",\n   \"linux-oracle-5.4\",\n   \"linux-raspi-5.4\",\n   \"linux-raspi2\",\n   \"linux-snapdragon\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-26365\",\n  \"namespace\": \"ubuntu:distro:ubuntu:20.04\",\n  \"packages\": [\n   \"linux\",\n   \"linux-aws\",\n   \"linux-aws-5.15\",\n   \"linux-azure\",\n   \"linux-azure-5.15\",\n   \"linux-azure-fde\",\n   \"linux-bluefield\",\n   \"linux-gcp\",\n   \"linux-gcp-5.15\",\n   \"linux-gke\",\n   \"linux-gke-5.15\",\n   \"linux-gkeop\",\n   \"linux-hwe-5.15\",\n   \"linux-ibm\",\n   \"linux-intel-iotg-5.15\",\n   \"linux-kvm\",\n   \"linux-lowlatency-hwe-5.15\",\n   \"linux-oem-5.14\",\n   \"linux-oracle\",\n   \"linux-raspi\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-26365\",\n  \"namespace\": \"ubuntu:distro:ubuntu:22.04\",\n  \"packages\": [\n   \"linux\",\n   \"linux-aws\",\n   \"linux-azure\",\n   \"linux-azure-fde\",\n   \"linux-gcp\",\n   \"linux-gke\",\n   \"linux-gkeop\",\n   \"linux-ibm\",\n   \"linux-intel-iotg\",\n   \"linux-kvm\",\n   \"linux-lowlatency\",\n   \"linux-oem-5.17\",\n   \"linux-oracle\",\n   \"linux-raspi\",\n   \"linux-riscv\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-26373\",\n  \"namespace\": \"ubuntu:distro:ubuntu:18.04\",\n  \"packages\": [\n   \"linux\",\n   \"linux-aws\",\n   \"linux-aws-5.4\",\n   \"linux-azure-4.15\",\n   \"linux-azure-5.4\",\n   \"linux-dell300x\",\n   \"linux-gcp-4.15\",\n   \"linux-gcp-5.4\",\n   \"linux-hwe-5.4\",\n   \"linux-ibm-5.4\",\n   \"linux-kvm\",\n   \"linux-oracle\",\n   \"linux-oracle-5.4\",\n   \"linux-raspi-5.4\",\n   \"linux-raspi2\",\n   \"linux-snapdragon\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-26373\",\n  \"namespace\": \"ubuntu:distro:ubuntu:20.04\",\n  \"packages\": [\n   \"linux\",\n   \"linux-aws\",\n   \"linux-aws-5.15\",\n   \"linux-azure\",\n   \"linux-azure-5.15\",\n   \"linux-azure-fde\",\n   \"linux-bluefield\",\n   \"linux-gcp\",\n   \"linux-gcp-5.15\",\n   \"linux-gke\",\n   \"linux-gke-5.15\",\n   \"linux-gkeop\",\n   \"linux-hwe-5.15\",\n   \"linux-ibm\",\n   \"linux-intel-iotg-5.15\",\n   \"linux-kvm\",\n   \"linux-lowlatency-hwe-5.15\",\n   \"linux-oem-5.14\",\n   \"linux-oracle\",\n   \"linux-raspi\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-26373\",\n  \"namespace\": \"ubuntu:distro:ubuntu:22.04\",\n  \"packages\": [\n   \"linux\",\n   \"linux-aws\",\n   \"linux-azure\",\n   \"linux-azure-fde\",\n   \"linux-gcp\",\n   \"linux-gke\",\n   \"linux-gkeop\",\n   \"linux-ibm\",\n   \"linux-intel-iotg\",\n   \"linux-kvm\",\n   \"linux-lowlatency\",\n   \"linux-oem-5.17\",\n   \"linux-oracle\",\n   \"linux-raspi\",\n   \"linux-riscv\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-26505\",\n  \"namespace\": \"alpine:distro:alpine:edge\",\n  \"packages\": [\n   \"minidlna\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-2663\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-2720\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"octopus_server\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-2764\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"integration_camel_k\",\n   \"jboss_enterprise_application_platform\",\n   \"jboss_fuse\",\n   \"single_sign-on\",\n   \"undertow\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-27664\",\n  \"namespace\": \"redhat:distro:redhat:8\",\n  \"packages\": [\n   \"container-tools:3.0/buildah\",\n   \"container-tools:3.0/containernetworking-plugins\",\n   \"container-tools:3.0/podman\",\n   \"container-tools:3.0/skopeo\",\n   \"container-tools:3.0/toolbox\",\n   \"container-tools:4.0/buildah\",\n   \"container-tools:4.0/conmon\",\n   \"container-tools:4.0/containernetworking-plugins\",\n   \"container-tools:4.0/podman\",\n   \"container-tools:4.0/skopeo\",\n   \"container-tools:4.0/toolbox\",\n   \"container-tools:rhel8/buildah\",\n   \"container-tools:rhel8/conmon\",\n   \"container-tools:rhel8/containernetworking-plugins\",\n   \"container-tools:rhel8/podman\",\n   \"container-tools:rhel8/skopeo\",\n   \"container-tools:rhel8/toolbox\",\n   \"git-lfs\",\n   \"go-toolset:rhel8/golang\",\n   \"grafana\",\n   \"grafana-pcp\",\n   \"osbuild-composer\",\n   \"weldr-client\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-27664\",\n  \"namespace\": \"redhat:distro:redhat:9\",\n  \"packages\": [\n   \"buildah\",\n   \"butane\",\n   \"containernetworking-plugins\",\n   \"git-lfs\",\n   \"golang\",\n   \"grafana\",\n   \"grafana-pcp\",\n   \"ignition\",\n   \"osbuild-composer\",\n   \"podman\",\n   \"skopeo\",\n   \"toolbox\",\n   \"weldr-client\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-2780\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-28193\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-28194\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-28195\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-28197\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-2828\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"octopus_server\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-28327\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"extra_packages_for_enterprise_linux\",\n   \"go\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-28346\",\n  \"namespace\": \"debian:distro:debian:11\",\n  \"packages\": [\n   \"python-django\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-28346\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"django\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-28347\",\n  \"namespace\": \"debian:distro:debian:11\",\n  \"packages\": [\n   \"python-django\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-28347\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"django\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-2850\",\n  \"namespace\": \"debian:distro:debian:10\",\n  \"packages\": [\n   \"389-ds-base\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-2850\",\n  \"namespace\": \"debian:distro:debian:11\",\n  \"packages\": [\n   \"389-ds-base\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-2850\",\n  \"namespace\": \"debian:distro:debian:12\",\n  \"packages\": [\n   \"389-ds-base\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-2850\",\n  \"namespace\": \"debian:distro:debian:unstable\",\n  \"packages\": [\n   \"389-ds-base\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-2850\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-28697\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"active_management_technology\",\n   \"standard_manageability\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-28759\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-28760\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-28761\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-28762\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-2879\",\n  \"namespace\": \"debian:distro:debian:10\",\n  \"packages\": [\n   \"golang-1.11\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-2879\",\n  \"namespace\": \"debian:distro:debian:11\",\n  \"packages\": [\n   \"golang-1.15\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-2879\",\n  \"namespace\": \"debian:distro:debian:12\",\n  \"packages\": [\n   \"golang-1.18\",\n   \"golang-1.19\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-2879\",\n  \"namespace\": \"debian:distro:debian:unstable\",\n  \"packages\": [\n   \"golang-1.18\",\n   \"golang-1.19\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-2879\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-2879\",\n  \"namespace\": \"redhat:distro:redhat:8\",\n  \"packages\": [\n   \"container-tools:3.0/buildah\",\n   \"container-tools:3.0/podman\",\n   \"container-tools:3.0/skopeo\",\n   \"container-tools:4.0/buildah\",\n   \"container-tools:4.0/podman\",\n   \"container-tools:4.0/skopeo\",\n   \"container-tools:rhel8/buildah\",\n   \"container-tools:rhel8/podman\",\n   \"container-tools:rhel8/skopeo\",\n   \"go-toolset:rhel8/golang\",\n   \"osbuild-composer\",\n   \"weldr-client\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-2879\",\n  \"namespace\": \"redhat:distro:redhat:9\",\n  \"packages\": [\n   \"buildah\",\n   \"golang\",\n   \"osbuild-composer\",\n   \"podman\",\n   \"skopeo\",\n   \"weldr-client\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-2880\",\n  \"namespace\": \"debian:distro:debian:10\",\n  \"packages\": [\n   \"golang-1.11\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-2880\",\n  \"namespace\": \"debian:distro:debian:11\",\n  \"packages\": [\n   \"golang-1.15\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-2880\",\n  \"namespace\": \"debian:distro:debian:12\",\n  \"packages\": [\n   \"golang-1.18\",\n   \"golang-1.19\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-2880\",\n  \"namespace\": \"debian:distro:debian:unstable\",\n  \"packages\": [\n   \"golang-1.18\",\n   \"golang-1.19\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-2880\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-2880\",\n  \"namespace\": \"redhat:distro:redhat:8\",\n  \"packages\": [\n   \"container-tools:3.0/buildah\",\n   \"container-tools:3.0/podman\",\n   \"container-tools:3.0/skopeo\",\n   \"container-tools:4.0/buildah\",\n   \"container-tools:4.0/podman\",\n   \"container-tools:4.0/skopeo\",\n   \"container-tools:rhel8/buildah\",\n   \"container-tools:rhel8/podman\",\n   \"container-tools:rhel8/skopeo\",\n   \"git-lfs\",\n   \"go-toolset:rhel8/golang\",\n   \"grafana\",\n   \"grafana-pcp\",\n   \"osbuild-composer\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-2880\",\n  \"namespace\": \"redhat:distro:redhat:9\",\n  \"packages\": [\n   \"buildah\",\n   \"git-lfs\",\n   \"golang\",\n   \"grafana\",\n   \"grafana-pcp\",\n   \"ignition\",\n   \"osbuild-composer\",\n   \"podman\",\n   \"skopeo\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-28866\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-28887\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"atlant\",\n   \"elements_endpoint_detection_and_response\",\n   \"elements_endpoint_protection\",\n   \"internet_gatekeeper\",\n   \"linux_security\",\n   \"linux_security_64\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-2928\",\n  \"namespace\": \"alpine:distro:alpine:3.13\",\n  \"packages\": [\n   \"dhcp\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-2928\",\n  \"namespace\": \"alpine:distro:alpine:3.14\",\n  \"packages\": [\n   \"dhcp\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-2928\",\n  \"namespace\": \"alpine:distro:alpine:3.15\",\n  \"packages\": [\n   \"dhcp\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-2928\",\n  \"namespace\": \"alpine:distro:alpine:3.16\",\n  \"packages\": [\n   \"dhcp\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-2928\",\n  \"namespace\": \"alpine:distro:alpine:edge\",\n  \"packages\": [\n   \"dhcp\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-2928\",\n  \"namespace\": \"debian:distro:debian:10\",\n  \"packages\": [\n   \"isc-dhcp\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-2928\",\n  \"namespace\": \"debian:distro:debian:11\",\n  \"packages\": [\n   \"isc-dhcp\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-2928\",\n  \"namespace\": \"debian:distro:debian:12\",\n  \"packages\": [\n   \"isc-dhcp\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-2928\",\n  \"namespace\": \"debian:distro:debian:unstable\",\n  \"packages\": [\n   \"isc-dhcp\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-2928\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"dhcp\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-2929\",\n  \"namespace\": \"alpine:distro:alpine:3.13\",\n  \"packages\": [\n   \"dhcp\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-2929\",\n  \"namespace\": \"alpine:distro:alpine:3.14\",\n  \"packages\": [\n   \"dhcp\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-2929\",\n  \"namespace\": \"alpine:distro:alpine:3.15\",\n  \"packages\": [\n   \"dhcp\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-2929\",\n  \"namespace\": \"alpine:distro:alpine:3.16\",\n  \"packages\": [\n   \"dhcp\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-2929\",\n  \"namespace\": \"alpine:distro:alpine:edge\",\n  \"packages\": [\n   \"dhcp\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-2929\",\n  \"namespace\": \"debian:distro:debian:10\",\n  \"packages\": [\n   \"isc-dhcp\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-2929\",\n  \"namespace\": \"debian:distro:debian:11\",\n  \"packages\": [\n   \"isc-dhcp\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-2929\",\n  \"namespace\": \"debian:distro:debian:12\",\n  \"packages\": [\n   \"isc-dhcp\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-2929\",\n  \"namespace\": \"debian:distro:debian:unstable\",\n  \"packages\": [\n   \"isc-dhcp\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-2929\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"dhcp\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-2953\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"libtiff\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-2953\",\n  \"namespace\": \"redhat:distro:redhat:6\",\n  \"packages\": [\n   \"libtiff\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-2953\",\n  \"namespace\": \"redhat:distro:redhat:7\",\n  \"packages\": [\n   \"compat-libtiff3\",\n   \"libtiff\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-2953\",\n  \"namespace\": \"redhat:distro:redhat:8\",\n  \"packages\": [\n   \"compat-libtiff3\",\n   \"libtiff\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-2953\",\n  \"namespace\": \"redhat:distro:redhat:9\",\n  \"packages\": [\n   \"libtiff\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-2962\",\n  \"namespace\": \"debian:distro:debian:10\",\n  \"packages\": [\n   \"qemu\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-2963\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-2963\",\n  \"namespace\": \"redhat:distro:redhat:8\",\n  \"packages\": [\n   \"jasper\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-2963\",\n  \"namespace\": \"redhat:distro:redhat:9\",\n  \"packages\": [\n   \"jasper\"\n  ]\n },\n {\n  \"reason\": \"removed\",\n  \"id\": \"CVE-2022-2964\",\n  \"namespace\": \"redhat:distro:redhat:6\",\n  \"packages\": [\n   \"kernel\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-2981\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"download_monitor\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-2984\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-2985\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-30601\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"active_management_technology\",\n   \"standard_manageability\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-30614\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"cognos_analytics\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-30763\",\n  \"namespace\": \"alpine:distro:alpine:edge\",\n  \"packages\": [\n   \"janet\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-30763\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"janet\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-30944\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"active_management_technology\",\n   \"standard_manageability\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-31123\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"grafana\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-31123\",\n  \"namespace\": \"redhat:distro:redhat:8\",\n  \"packages\": [\n   \"grafana\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-31123\",\n  \"namespace\": \"redhat:distro:redhat:9\",\n  \"packages\": [\n   \"grafana\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-31123\",\n  \"namespace\": \"ubuntu:distro:ubuntu:14.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-31123\",\n  \"namespace\": \"ubuntu:distro:ubuntu:16.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-31123\",\n  \"namespace\": \"ubuntu:distro:ubuntu:18.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-31123\",\n  \"namespace\": \"ubuntu:distro:ubuntu:20.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-31123\",\n  \"namespace\": \"ubuntu:distro:ubuntu:22.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-31129\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"moment\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-31130\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-31130\",\n  \"namespace\": \"redhat:distro:redhat:8\",\n  \"packages\": [\n   \"grafana\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-31130\",\n  \"namespace\": \"redhat:distro:redhat:9\",\n  \"packages\": [\n   \"grafana\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-31130\",\n  \"namespace\": \"ubuntu:distro:ubuntu:14.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-31130\",\n  \"namespace\": \"ubuntu:distro:ubuntu:16.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-31130\",\n  \"namespace\": \"ubuntu:distro:ubuntu:18.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-31130\",\n  \"namespace\": \"ubuntu:distro:ubuntu:20.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-31130\",\n  \"namespace\": \"ubuntu:distro:ubuntu:22.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-3116\",\n  \"namespace\": \"ubuntu:distro:ubuntu:18.04\",\n  \"packages\": [\n   \"heimdal\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-3116\",\n  \"namespace\": \"ubuntu:distro:ubuntu:20.04\",\n  \"packages\": [\n   \"heimdal\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-31228\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"xtremio_management_server\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-3136\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"social_rocket\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-3137\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"taskbuilder\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-3140\",\n  \"namespace\": \"debian:distro:debian:10\",\n  \"packages\": [\n   \"libreoffice\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-3140\",\n  \"namespace\": \"debian:distro:debian:11\",\n  \"packages\": [\n   \"libreoffice\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-3140\",\n  \"namespace\": \"debian:distro:debian:12\",\n  \"packages\": [\n   \"libreoffice\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-3140\",\n  \"namespace\": \"debian:distro:debian:unstable\",\n  \"packages\": [\n   \"libreoffice\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-3140\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"libreoffice\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-3140\",\n  \"namespace\": \"redhat:distro:redhat:7\",\n  \"packages\": [\n   \"libreoffice\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-3140\",\n  \"namespace\": \"redhat:distro:redhat:8\",\n  \"packages\": [\n   \"libreoffice\",\n   \"libreoffice:flatpak/libreoffice\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-3140\",\n  \"namespace\": \"redhat:distro:redhat:9\",\n  \"packages\": [\n   \"libreoffice\",\n   \"libreoffice:flatpak/libreoffice\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-3140\",\n  \"namespace\": \"ubuntu:distro:ubuntu:14.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-3140\",\n  \"namespace\": \"ubuntu:distro:ubuntu:16.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-3140\",\n  \"namespace\": \"ubuntu:distro:ubuntu:18.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-3140\",\n  \"namespace\": \"ubuntu:distro:ubuntu:20.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-3140\",\n  \"namespace\": \"ubuntu:distro:ubuntu:22.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-3154\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"integration_for_billingo_&_gravity_forms\",\n   \"integration_for_szamlazz.hu_&_gravity_forms\",\n   \"woo_billingo_plus\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-31682\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"vrealize_operations\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-3171\",\n  \"namespace\": \"debian:distro:debian:10\",\n  \"packages\": [\n   \"protobuf\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-3171\",\n  \"namespace\": \"debian:distro:debian:11\",\n  \"packages\": [\n   \"protobuf\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-3171\",\n  \"namespace\": \"debian:distro:debian:12\",\n  \"packages\": [\n   \"protobuf\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-3171\",\n  \"namespace\": \"debian:distro:debian:unstable\",\n  \"packages\": [\n   \"protobuf\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-3171\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"google-protobuf\",\n   \"protobuf-java\",\n   \"protobuf-javalite\",\n   \"protobuf-kotlin\",\n   \"protobuf-kotlin-lite\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-31765\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-31766\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-3176\",\n  \"namespace\": \"ubuntu:distro:ubuntu:18.04\",\n  \"packages\": [\n   \"linux-aws-5.4\",\n   \"linux-azure-5.4\",\n   \"linux-gcp-5.4\",\n   \"linux-hwe-5.4\",\n   \"linux-ibm-5.4\",\n   \"linux-oracle-5.4\",\n   \"linux-raspi-5.4\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-3176\",\n  \"namespace\": \"ubuntu:distro:ubuntu:20.04\",\n  \"packages\": [\n   \"linux\",\n   \"linux-aws\",\n   \"linux-aws-5.15\",\n   \"linux-azure\",\n   \"linux-azure-5.15\",\n   \"linux-azure-fde\",\n   \"linux-bluefield\",\n   \"linux-gcp\",\n   \"linux-gcp-5.15\",\n   \"linux-gke\",\n   \"linux-gke-5.15\",\n   \"linux-gkeop\",\n   \"linux-hwe-5.15\",\n   \"linux-ibm\",\n   \"linux-intel-iotg-5.15\",\n   \"linux-kvm\",\n   \"linux-lowlatency-hwe-5.15\",\n   \"linux-oem-5.14\",\n   \"linux-oracle\",\n   \"linux-raspi\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-3176\",\n  \"namespace\": \"ubuntu:distro:ubuntu:22.04\",\n  \"packages\": [\n   \"linux\",\n   \"linux-aws\",\n   \"linux-azure\",\n   \"linux-azure-fde\",\n   \"linux-gcp\",\n   \"linux-gke\",\n   \"linux-gkeop\",\n   \"linux-ibm\",\n   \"linux-intel-iotg\",\n   \"linux-kvm\",\n   \"linux-lowlatency\",\n   \"linux-oracle\",\n   \"linux-raspi\",\n   \"linux-riscv\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-3207\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"simple-file-list\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-3208\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"simple-file-list\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-3209\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"soledad\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-32149\",\n  \"namespace\": \"debian:distro:debian:11\",\n  \"packages\": [\n   \"golang-golang-x-text\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-32149\",\n  \"namespace\": \"debian:distro:debian:12\",\n  \"packages\": [\n   \"golang-golang-x-text\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-32149\",\n  \"namespace\": \"debian:distro:debian:unstable\",\n  \"packages\": [\n   \"golang-golang-x-text\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-32149\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-32175\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"adguardhome\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-32177\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"gin-vue-admin\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-3220\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"advanced_comment_form\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-32296\",\n  \"namespace\": \"ubuntu:distro:ubuntu:18.04\",\n  \"packages\": [\n   \"linux\",\n   \"linux-aws\",\n   \"linux-aws-5.4\",\n   \"linux-azure-4.15\",\n   \"linux-azure-5.4\",\n   \"linux-dell300x\",\n   \"linux-gcp-4.15\",\n   \"linux-gcp-5.4\",\n   \"linux-gke-5.4\",\n   \"linux-gkeop-5.4\",\n   \"linux-hwe-5.4\",\n   \"linux-ibm-5.4\",\n   \"linux-kvm\",\n   \"linux-oracle\",\n   \"linux-oracle-5.4\",\n   \"linux-raspi-5.4\",\n   \"linux-raspi2\",\n   \"linux-snapdragon\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-3234\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"vim\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-3235\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"vim\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-32483\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-32484\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-32485\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-32486\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-32487\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-32488\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-32489\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-32491\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-32492\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-32493\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-3256\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"vim\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-32589\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"yocto\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-32590\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"yocto\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-32591\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-32592\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"yocto\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-32593\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-3278\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"vim\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-3296\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"vim\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-3297\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"vim\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-33106\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-3324\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"vim\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-3352\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"vim\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-3358\",\n  \"namespace\": \"alpine:distro:alpine:3.15\",\n  \"packages\": [\n   \"openssl3\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-3358\",\n  \"namespace\": \"alpine:distro:alpine:3.16\",\n  \"packages\": [\n   \"openssl3\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-3358\",\n  \"namespace\": \"alpine:distro:alpine:edge\",\n  \"packages\": [\n   \"openssl\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-3358\",\n  \"namespace\": \"debian:distro:debian:12\",\n  \"packages\": [\n   \"openssl\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-3358\",\n  \"namespace\": \"debian:distro:debian:unstable\",\n  \"packages\": [\n   \"openssl\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-3358\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"openssl\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-3358\",\n  \"namespace\": \"redhat:distro:redhat:6\",\n  \"packages\": [\n   \"openssl\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-3358\",\n  \"namespace\": \"redhat:distro:redhat:7\",\n  \"packages\": [\n   \"openssl\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-3358\",\n  \"namespace\": \"redhat:distro:redhat:9\",\n  \"packages\": [\n   \"openssl\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-3358\",\n  \"namespace\": \"ubuntu:distro:ubuntu:14.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-3358\",\n  \"namespace\": \"ubuntu:distro:ubuntu:16.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-3358\",\n  \"namespace\": \"ubuntu:distro:ubuntu:18.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-3358\",\n  \"namespace\": \"ubuntu:distro:ubuntu:20.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-3358\",\n  \"namespace\": \"ubuntu:distro:ubuntu:22.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-33639\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"edge_chromium\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-33740\",\n  \"namespace\": \"ubuntu:distro:ubuntu:18.04\",\n  \"packages\": [\n   \"linux\",\n   \"linux-aws\",\n   \"linux-aws-5.4\",\n   \"linux-azure-4.15\",\n   \"linux-azure-5.4\",\n   \"linux-dell300x\",\n   \"linux-gcp-4.15\",\n   \"linux-gcp-5.4\",\n   \"linux-hwe-5.4\",\n   \"linux-ibm-5.4\",\n   \"linux-kvm\",\n   \"linux-oracle\",\n   \"linux-oracle-5.4\",\n   \"linux-raspi-5.4\",\n   \"linux-raspi2\",\n   \"linux-snapdragon\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-33740\",\n  \"namespace\": \"ubuntu:distro:ubuntu:20.04\",\n  \"packages\": [\n   \"linux\",\n   \"linux-aws\",\n   \"linux-aws-5.15\",\n   \"linux-azure\",\n   \"linux-azure-5.15\",\n   \"linux-azure-fde\",\n   \"linux-bluefield\",\n   \"linux-gcp\",\n   \"linux-gcp-5.15\",\n   \"linux-gke\",\n   \"linux-gke-5.15\",\n   \"linux-gkeop\",\n   \"linux-hwe-5.15\",\n   \"linux-ibm\",\n   \"linux-intel-iotg-5.15\",\n   \"linux-kvm\",\n   \"linux-lowlatency-hwe-5.15\",\n   \"linux-oem-5.14\",\n   \"linux-oracle\",\n   \"linux-raspi\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-33740\",\n  \"namespace\": \"ubuntu:distro:ubuntu:22.04\",\n  \"packages\": [\n   \"linux\",\n   \"linux-aws\",\n   \"linux-azure\",\n   \"linux-azure-fde\",\n   \"linux-gcp\",\n   \"linux-gke\",\n   \"linux-gkeop\",\n   \"linux-ibm\",\n   \"linux-intel-iotg\",\n   \"linux-kvm\",\n   \"linux-lowlatency\",\n   \"linux-oem-5.17\",\n   \"linux-oracle\",\n   \"linux-raspi\",\n   \"linux-riscv\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-33741\",\n  \"namespace\": \"ubuntu:distro:ubuntu:18.04\",\n  \"packages\": [\n   \"linux\",\n   \"linux-aws\",\n   \"linux-aws-5.4\",\n   \"linux-azure-4.15\",\n   \"linux-azure-5.4\",\n   \"linux-dell300x\",\n   \"linux-gcp-4.15\",\n   \"linux-gcp-5.4\",\n   \"linux-hwe-5.4\",\n   \"linux-ibm-5.4\",\n   \"linux-kvm\",\n   \"linux-oracle\",\n   \"linux-oracle-5.4\",\n   \"linux-raspi-5.4\",\n   \"linux-raspi2\",\n   \"linux-snapdragon\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-33741\",\n  \"namespace\": \"ubuntu:distro:ubuntu:20.04\",\n  \"packages\": [\n   \"linux\",\n   \"linux-aws\",\n   \"linux-aws-5.15\",\n   \"linux-azure\",\n   \"linux-azure-5.15\",\n   \"linux-azure-fde\",\n   \"linux-bluefield\",\n   \"linux-gcp\",\n   \"linux-gcp-5.15\",\n   \"linux-gke\",\n   \"linux-gke-5.15\",\n   \"linux-gkeop\",\n   \"linux-hwe-5.15\",\n   \"linux-ibm\",\n   \"linux-intel-iotg-5.15\",\n   \"linux-kvm\",\n   \"linux-lowlatency-hwe-5.15\",\n   \"linux-oem-5.14\",\n   \"linux-oracle\",\n   \"linux-raspi\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-33741\",\n  \"namespace\": \"ubuntu:distro:ubuntu:22.04\",\n  \"packages\": [\n   \"linux\",\n   \"linux-aws\",\n   \"linux-azure\",\n   \"linux-azure-fde\",\n   \"linux-gcp\",\n   \"linux-gke\",\n   \"linux-gkeop\",\n   \"linux-ibm\",\n   \"linux-intel-iotg\",\n   \"linux-kvm\",\n   \"linux-lowlatency\",\n   \"linux-oem-5.17\",\n   \"linux-oracle\",\n   \"linux-raspi\",\n   \"linux-riscv\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-33742\",\n  \"namespace\": \"ubuntu:distro:ubuntu:18.04\",\n  \"packages\": [\n   \"linux\",\n   \"linux-aws\",\n   \"linux-aws-5.4\",\n   \"linux-azure-4.15\",\n   \"linux-azure-5.4\",\n   \"linux-dell300x\",\n   \"linux-gcp-4.15\",\n   \"linux-gcp-5.4\",\n   \"linux-hwe-5.4\",\n   \"linux-ibm-5.4\",\n   \"linux-kvm\",\n   \"linux-oracle\",\n   \"linux-oracle-5.4\",\n   \"linux-raspi-5.4\",\n   \"linux-raspi2\",\n   \"linux-snapdragon\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-33742\",\n  \"namespace\": \"ubuntu:distro:ubuntu:20.04\",\n  \"packages\": [\n   \"linux\",\n   \"linux-aws\",\n   \"linux-aws-5.15\",\n   \"linux-azure\",\n   \"linux-azure-5.15\",\n   \"linux-azure-fde\",\n   \"linux-bluefield\",\n   \"linux-gcp\",\n   \"linux-gcp-5.15\",\n   \"linux-gke\",\n   \"linux-gke-5.15\",\n   \"linux-gkeop\",\n   \"linux-hwe-5.15\",\n   \"linux-ibm\",\n   \"linux-intel-iotg-5.15\",\n   \"linux-kvm\",\n   \"linux-lowlatency-hwe-5.15\",\n   \"linux-oem-5.14\",\n   \"linux-oracle\",\n   \"linux-raspi\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-33742\",\n  \"namespace\": \"ubuntu:distro:ubuntu:22.04\",\n  \"packages\": [\n   \"linux\",\n   \"linux-aws\",\n   \"linux-azure\",\n   \"linux-azure-fde\",\n   \"linux-gcp\",\n   \"linux-gke\",\n   \"linux-gkeop\",\n   \"linux-ibm\",\n   \"linux-intel-iotg\",\n   \"linux-kvm\",\n   \"linux-lowlatency\",\n   \"linux-oem-5.17\",\n   \"linux-oracle\",\n   \"linux-raspi\",\n   \"linux-riscv\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-33743\",\n  \"namespace\": \"ubuntu:distro:ubuntu:22.04\",\n  \"packages\": [\n   \"linux\",\n   \"linux-aws\",\n   \"linux-azure\",\n   \"linux-azure-fde\",\n   \"linux-gcp\",\n   \"linux-gke\",\n   \"linux-gkeop\",\n   \"linux-ibm\",\n   \"linux-intel-iotg\",\n   \"linux-kvm\",\n   \"linux-lowlatency\",\n   \"linux-oem-5.17\",\n   \"linux-oracle\",\n   \"linux-raspi\",\n   \"linux-riscv\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-33744\",\n  \"namespace\": \"ubuntu:distro:ubuntu:18.04\",\n  \"packages\": [\n   \"linux\",\n   \"linux-aws\",\n   \"linux-aws-5.4\",\n   \"linux-azure-4.15\",\n   \"linux-azure-5.4\",\n   \"linux-dell300x\",\n   \"linux-gcp-4.15\",\n   \"linux-gcp-5.4\",\n   \"linux-hwe-5.4\",\n   \"linux-ibm-5.4\",\n   \"linux-kvm\",\n   \"linux-oracle\",\n   \"linux-oracle-5.4\",\n   \"linux-raspi-5.4\",\n   \"linux-raspi2\",\n   \"linux-snapdragon\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-33744\",\n  \"namespace\": \"ubuntu:distro:ubuntu:20.04\",\n  \"packages\": [\n   \"linux\",\n   \"linux-aws\",\n   \"linux-aws-5.15\",\n   \"linux-azure\",\n   \"linux-azure-5.15\",\n   \"linux-azure-fde\",\n   \"linux-bluefield\",\n   \"linux-gcp\",\n   \"linux-gcp-5.15\",\n   \"linux-gke\",\n   \"linux-gke-5.15\",\n   \"linux-gkeop\",\n   \"linux-hwe-5.15\",\n   \"linux-ibm\",\n   \"linux-intel-iotg-5.15\",\n   \"linux-kvm\",\n   \"linux-lowlatency-hwe-5.15\",\n   \"linux-oem-5.14\",\n   \"linux-oracle\",\n   \"linux-raspi\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-33744\",\n  \"namespace\": \"ubuntu:distro:ubuntu:22.04\",\n  \"packages\": [\n   \"linux\",\n   \"linux-aws\",\n   \"linux-azure\",\n   \"linux-azure-fde\",\n   \"linux-gcp\",\n   \"linux-gke\",\n   \"linux-gkeop\",\n   \"linux-ibm\",\n   \"linux-intel-iotg\",\n   \"linux-kvm\",\n   \"linux-lowlatency\",\n   \"linux-oem-5.17\",\n   \"linux-oracle\",\n   \"linux-raspi\",\n   \"linux-riscv\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-33746\",\n  \"namespace\": \"debian:distro:debian:11\",\n  \"packages\": [\n   \"xen\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-33746\",\n  \"namespace\": \"debian:distro:debian:12\",\n  \"packages\": [\n   \"xen\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-33746\",\n  \"namespace\": \"debian:distro:debian:unstable\",\n  \"packages\": [\n   \"xen\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-33746\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-33746\",\n  \"namespace\": \"ubuntu:distro:ubuntu:14.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-33746\",\n  \"namespace\": \"ubuntu:distro:ubuntu:16.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-33746\",\n  \"namespace\": \"ubuntu:distro:ubuntu:18.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-33746\",\n  \"namespace\": \"ubuntu:distro:ubuntu:20.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-33746\",\n  \"namespace\": \"ubuntu:distro:ubuntu:22.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-33747\",\n  \"namespace\": \"debian:distro:debian:11\",\n  \"packages\": [\n   \"xen\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-33747\",\n  \"namespace\": \"debian:distro:debian:12\",\n  \"packages\": [\n   \"xen\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-33747\",\n  \"namespace\": \"debian:distro:debian:unstable\",\n  \"packages\": [\n   \"xen\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-33747\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-33747\",\n  \"namespace\": \"ubuntu:distro:ubuntu:14.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-33747\",\n  \"namespace\": \"ubuntu:distro:ubuntu:16.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-33747\",\n  \"namespace\": \"ubuntu:distro:ubuntu:18.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-33747\",\n  \"namespace\": \"ubuntu:distro:ubuntu:20.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-33747\",\n  \"namespace\": \"ubuntu:distro:ubuntu:22.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-33748\",\n  \"namespace\": \"debian:distro:debian:11\",\n  \"packages\": [\n   \"xen\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-33748\",\n  \"namespace\": \"debian:distro:debian:12\",\n  \"packages\": [\n   \"xen\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-33748\",\n  \"namespace\": \"debian:distro:debian:unstable\",\n  \"packages\": [\n   \"xen\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-33748\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-33748\",\n  \"namespace\": \"ubuntu:distro:ubuntu:14.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-33748\",\n  \"namespace\": \"ubuntu:distro:ubuntu:16.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-33748\",\n  \"namespace\": \"ubuntu:distro:ubuntu:18.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-33748\",\n  \"namespace\": \"ubuntu:distro:ubuntu:20.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-33748\",\n  \"namespace\": \"ubuntu:distro:ubuntu:22.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-33749\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-33890\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"autocad\",\n   \"autocad_advance_steel\",\n   \"autocad_architecture\",\n   \"autocad_civil_3d\",\n   \"autocad_electrical\",\n   \"autocad_lt\",\n   \"autocad_map_3d\",\n   \"autocad_mechanical\",\n   \"autocad_mep\",\n   \"autocad_plant_3d\",\n   \"design_review\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-33918\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"geodrive\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-33919\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"geodrive\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-33920\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"geodrive\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-33921\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"geodrive\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-33922\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"geodrive\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-33937\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"geodrive\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-33978\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"fontmeister\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-34020\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"iot_platform_and_lorawan_network_server\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-34021\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"iot_platform_and_lorawan_network_server\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-34022\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-34265\",\n  \"namespace\": \"debian:distro:debian:11\",\n  \"packages\": [\n   \"python-django\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-34265\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"django\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-34326\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-34334\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"sterling_partner_engagement_manager\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-3435\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-34390\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-34391\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-3439\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"rdiffweb\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-34402\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-34425\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-34426\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"container_storage_modules\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-34427\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"container_storage_modules\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-34430\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"hybrid_client\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-34431\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"hybrid_client\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-34432\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"hybrid_client\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-34434\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"cloud_mobility_for_dell_emc_storage\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-3445\",\n  \"namespace\": \"debian:distro:debian:11\",\n  \"packages\": [\n   \"chromium\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-3445\",\n  \"namespace\": \"debian:distro:debian:12\",\n  \"packages\": [\n   \"chromium\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-3445\",\n  \"namespace\": \"debian:distro:debian:unstable\",\n  \"packages\": [\n   \"chromium\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-3446\",\n  \"namespace\": \"debian:distro:debian:11\",\n  \"packages\": [\n   \"chromium\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-3446\",\n  \"namespace\": \"debian:distro:debian:12\",\n  \"packages\": [\n   \"chromium\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-3446\",\n  \"namespace\": \"debian:distro:debian:unstable\",\n  \"packages\": [\n   \"chromium\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-3447\",\n  \"namespace\": \"debian:distro:debian:11\",\n  \"packages\": [\n   \"chromium\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-3447\",\n  \"namespace\": \"debian:distro:debian:12\",\n  \"packages\": [\n   \"chromium\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-3447\",\n  \"namespace\": \"debian:distro:debian:unstable\",\n  \"packages\": [\n   \"chromium\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-3448\",\n  \"namespace\": \"debian:distro:debian:11\",\n  \"packages\": [\n   \"chromium\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-3448\",\n  \"namespace\": \"debian:distro:debian:12\",\n  \"packages\": [\n   \"chromium\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-3448\",\n  \"namespace\": \"debian:distro:debian:unstable\",\n  \"packages\": [\n   \"chromium\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-34494\",\n  \"namespace\": \"ubuntu:distro:ubuntu:22.04\",\n  \"packages\": [\n   \"linux\",\n   \"linux-aws\",\n   \"linux-azure\",\n   \"linux-azure-fde\",\n   \"linux-gcp\",\n   \"linux-gke\",\n   \"linux-gkeop\",\n   \"linux-ibm\",\n   \"linux-intel-iotg\",\n   \"linux-kvm\",\n   \"linux-lowlatency\",\n   \"linux-oem-5.17\",\n   \"linux-oracle\",\n   \"linux-raspi\",\n   \"linux-riscv\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-34495\",\n  \"namespace\": \"ubuntu:distro:ubuntu:22.04\",\n  \"packages\": [\n   \"linux\",\n   \"linux-aws\",\n   \"linux-azure\",\n   \"linux-azure-fde\",\n   \"linux-gcp\",\n   \"linux-gke\",\n   \"linux-gkeop\",\n   \"linux-ibm\",\n   \"linux-intel-iotg\",\n   \"linux-kvm\",\n   \"linux-lowlatency\",\n   \"linux-oem-5.17\",\n   \"linux-oracle\",\n   \"linux-raspi\",\n   \"linux-riscv\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-3449\",\n  \"namespace\": \"debian:distro:debian:11\",\n  \"packages\": [\n   \"chromium\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-3449\",\n  \"namespace\": \"debian:distro:debian:12\",\n  \"packages\": [\n   \"chromium\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-3449\",\n  \"namespace\": \"debian:distro:debian:unstable\",\n  \"packages\": [\n   \"chromium\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-3450\",\n  \"namespace\": \"debian:distro:debian:11\",\n  \"packages\": [\n   \"chromium\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-3450\",\n  \"namespace\": \"debian:distro:debian:12\",\n  \"packages\": [\n   \"chromium\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-3450\",\n  \"namespace\": \"debian:distro:debian:unstable\",\n  \"packages\": [\n   \"chromium\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-3456\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"rdiffweb\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-3457\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"rdiffweb\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-3458\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"human_resource_management_system\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-3464\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"puppycms\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-3465\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-3467\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"jiusi_oa\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-3470\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"human_resource_management_system\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-3471\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"human_resource_management_system\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-3472\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"human_resource_management_system\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-3473\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"human_resource_management_system\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-3479\",\n  \"namespace\": \"debian:distro:debian:10\",\n  \"packages\": [\n   \"nss\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-3479\",\n  \"namespace\": \"debian:distro:debian:11\",\n  \"packages\": [\n   \"nss\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-3479\",\n  \"namespace\": \"debian:distro:debian:12\",\n  \"packages\": [\n   \"nss\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-3479\",\n  \"namespace\": \"debian:distro:debian:unstable\",\n  \"packages\": [\n   \"nss\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-3479\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-3479\",\n  \"namespace\": \"ubuntu:distro:ubuntu:14.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-3479\",\n  \"namespace\": \"ubuntu:distro:ubuntu:16.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-3479\",\n  \"namespace\": \"ubuntu:distro:ubuntu:18.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-3479\",\n  \"namespace\": \"ubuntu:distro:ubuntu:20.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-3479\",\n  \"namespace\": \"ubuntu:distro:ubuntu:22.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-3492\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"human_resource_management_system\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-3493\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"human_resource_management_system\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-3495\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"simple_online_public_access_catalog\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-3496\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"human_resource_management_system\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-3497\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"human_resource_management_system\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-3502\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"human_resource_management_system\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-3503\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"purchase_order_management_system\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35040\",\n  \"namespace\": \"debian:distro:debian:10\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35040\",\n  \"namespace\": \"debian:distro:debian:11\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35040\",\n  \"namespace\": \"debian:distro:debian:12\",\n  \"packages\": [\n   \"texlive-bin\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35040\",\n  \"namespace\": \"debian:distro:debian:unstable\",\n  \"packages\": [\n   \"texlive-bin\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35040\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"otfcc\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35041\",\n  \"namespace\": \"debian:distro:debian:10\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35041\",\n  \"namespace\": \"debian:distro:debian:11\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35041\",\n  \"namespace\": \"debian:distro:debian:12\",\n  \"packages\": [\n   \"texlive-bin\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35041\",\n  \"namespace\": \"debian:distro:debian:unstable\",\n  \"packages\": [\n   \"texlive-bin\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35041\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"otfcc\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35042\",\n  \"namespace\": \"debian:distro:debian:10\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35042\",\n  \"namespace\": \"debian:distro:debian:11\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35042\",\n  \"namespace\": \"debian:distro:debian:12\",\n  \"packages\": [\n   \"texlive-bin\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35042\",\n  \"namespace\": \"debian:distro:debian:unstable\",\n  \"packages\": [\n   \"texlive-bin\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35042\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"otfcc\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35043\",\n  \"namespace\": \"debian:distro:debian:10\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35043\",\n  \"namespace\": \"debian:distro:debian:11\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35043\",\n  \"namespace\": \"debian:distro:debian:12\",\n  \"packages\": [\n   \"texlive-bin\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35043\",\n  \"namespace\": \"debian:distro:debian:unstable\",\n  \"packages\": [\n   \"texlive-bin\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35043\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"otfcc\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35044\",\n  \"namespace\": \"debian:distro:debian:10\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35044\",\n  \"namespace\": \"debian:distro:debian:11\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35044\",\n  \"namespace\": \"debian:distro:debian:12\",\n  \"packages\": [\n   \"texlive-bin\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35044\",\n  \"namespace\": \"debian:distro:debian:unstable\",\n  \"packages\": [\n   \"texlive-bin\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35044\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"otfcc\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35045\",\n  \"namespace\": \"debian:distro:debian:10\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35045\",\n  \"namespace\": \"debian:distro:debian:11\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35045\",\n  \"namespace\": \"debian:distro:debian:12\",\n  \"packages\": [\n   \"texlive-bin\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35045\",\n  \"namespace\": \"debian:distro:debian:unstable\",\n  \"packages\": [\n   \"texlive-bin\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35045\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"otfcc\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35046\",\n  \"namespace\": \"debian:distro:debian:10\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35046\",\n  \"namespace\": \"debian:distro:debian:11\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35046\",\n  \"namespace\": \"debian:distro:debian:12\",\n  \"packages\": [\n   \"texlive-bin\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35046\",\n  \"namespace\": \"debian:distro:debian:unstable\",\n  \"packages\": [\n   \"texlive-bin\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35046\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"otfcc\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35047\",\n  \"namespace\": \"debian:distro:debian:10\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35047\",\n  \"namespace\": \"debian:distro:debian:11\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35047\",\n  \"namespace\": \"debian:distro:debian:12\",\n  \"packages\": [\n   \"texlive-bin\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35047\",\n  \"namespace\": \"debian:distro:debian:unstable\",\n  \"packages\": [\n   \"texlive-bin\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35047\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"otfcc\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35048\",\n  \"namespace\": \"debian:distro:debian:10\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35048\",\n  \"namespace\": \"debian:distro:debian:11\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35048\",\n  \"namespace\": \"debian:distro:debian:12\",\n  \"packages\": [\n   \"texlive-bin\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35048\",\n  \"namespace\": \"debian:distro:debian:unstable\",\n  \"packages\": [\n   \"texlive-bin\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35048\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"otfcc\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35049\",\n  \"namespace\": \"debian:distro:debian:10\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35049\",\n  \"namespace\": \"debian:distro:debian:11\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35049\",\n  \"namespace\": \"debian:distro:debian:12\",\n  \"packages\": [\n   \"texlive-bin\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35049\",\n  \"namespace\": \"debian:distro:debian:unstable\",\n  \"packages\": [\n   \"texlive-bin\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35049\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"otfcc\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-3504\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35050\",\n  \"namespace\": \"debian:distro:debian:10\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35050\",\n  \"namespace\": \"debian:distro:debian:11\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35050\",\n  \"namespace\": \"debian:distro:debian:12\",\n  \"packages\": [\n   \"texlive-bin\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35050\",\n  \"namespace\": \"debian:distro:debian:unstable\",\n  \"packages\": [\n   \"texlive-bin\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35050\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"otfcc\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35051\",\n  \"namespace\": \"debian:distro:debian:10\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35051\",\n  \"namespace\": \"debian:distro:debian:11\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35051\",\n  \"namespace\": \"debian:distro:debian:12\",\n  \"packages\": [\n   \"texlive-bin\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35051\",\n  \"namespace\": \"debian:distro:debian:unstable\",\n  \"packages\": [\n   \"texlive-bin\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35051\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"otfcc\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35052\",\n  \"namespace\": \"debian:distro:debian:10\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35052\",\n  \"namespace\": \"debian:distro:debian:11\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35052\",\n  \"namespace\": \"debian:distro:debian:12\",\n  \"packages\": [\n   \"texlive-bin\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35052\",\n  \"namespace\": \"debian:distro:debian:unstable\",\n  \"packages\": [\n   \"texlive-bin\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35052\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"otfcc\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35053\",\n  \"namespace\": \"debian:distro:debian:10\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35053\",\n  \"namespace\": \"debian:distro:debian:11\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35053\",\n  \"namespace\": \"debian:distro:debian:12\",\n  \"packages\": [\n   \"texlive-bin\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35053\",\n  \"namespace\": \"debian:distro:debian:unstable\",\n  \"packages\": [\n   \"texlive-bin\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35053\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"otfcc\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35054\",\n  \"namespace\": \"debian:distro:debian:10\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35054\",\n  \"namespace\": \"debian:distro:debian:11\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35054\",\n  \"namespace\": \"debian:distro:debian:12\",\n  \"packages\": [\n   \"texlive-bin\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35054\",\n  \"namespace\": \"debian:distro:debian:unstable\",\n  \"packages\": [\n   \"texlive-bin\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35054\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"otfcc\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35055\",\n  \"namespace\": \"debian:distro:debian:10\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35055\",\n  \"namespace\": \"debian:distro:debian:11\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35055\",\n  \"namespace\": \"debian:distro:debian:12\",\n  \"packages\": [\n   \"texlive-bin\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35055\",\n  \"namespace\": \"debian:distro:debian:unstable\",\n  \"packages\": [\n   \"texlive-bin\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35055\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"otfcc\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35056\",\n  \"namespace\": \"debian:distro:debian:10\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35056\",\n  \"namespace\": \"debian:distro:debian:11\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35056\",\n  \"namespace\": \"debian:distro:debian:12\",\n  \"packages\": [\n   \"texlive-bin\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35056\",\n  \"namespace\": \"debian:distro:debian:unstable\",\n  \"packages\": [\n   \"texlive-bin\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35056\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"otfcc\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35058\",\n  \"namespace\": \"debian:distro:debian:10\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35058\",\n  \"namespace\": \"debian:distro:debian:11\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35058\",\n  \"namespace\": \"debian:distro:debian:12\",\n  \"packages\": [\n   \"texlive-bin\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35058\",\n  \"namespace\": \"debian:distro:debian:unstable\",\n  \"packages\": [\n   \"texlive-bin\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35058\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"otfcc\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35059\",\n  \"namespace\": \"debian:distro:debian:10\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35059\",\n  \"namespace\": \"debian:distro:debian:11\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35059\",\n  \"namespace\": \"debian:distro:debian:12\",\n  \"packages\": [\n   \"texlive-bin\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35059\",\n  \"namespace\": \"debian:distro:debian:unstable\",\n  \"packages\": [\n   \"texlive-bin\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35059\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"otfcc\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-3505\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-3506\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35080\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"swftools\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35080\",\n  \"namespace\": \"ubuntu:distro:ubuntu:14.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35080\",\n  \"namespace\": \"ubuntu:distro:ubuntu:16.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35080\",\n  \"namespace\": \"ubuntu:distro:ubuntu:18.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35080\",\n  \"namespace\": \"ubuntu:distro:ubuntu:20.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35080\",\n  \"namespace\": \"ubuntu:distro:ubuntu:22.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35081\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"swftools\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35081\",\n  \"namespace\": \"ubuntu:distro:ubuntu:14.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35081\",\n  \"namespace\": \"ubuntu:distro:ubuntu:16.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35081\",\n  \"namespace\": \"ubuntu:distro:ubuntu:18.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35081\",\n  \"namespace\": \"ubuntu:distro:ubuntu:20.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35081\",\n  \"namespace\": \"ubuntu:distro:ubuntu:22.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35134\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"iot_platform\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35135\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"iot_platform\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35136\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"iot_platform\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-3517\",\n  \"namespace\": \"redhat:distro:redhat:8\",\n  \"packages\": [\n   \"389-ds:1.4/389-ds-base\",\n   \"cockpit\",\n   \"cockpit-appstream\",\n   \"grafana\",\n   \"mozjs60\",\n   \"nodejs:14/nodejs\",\n   \"nodejs:14/nodejs-nodemon\",\n   \"nodejs:16/nodejs\",\n   \"nodejs:16/nodejs-nodemon\",\n   \"nodejs:18/nodejs\",\n   \"nodejs:18/nodejs-nodemon\",\n   \"pcs\",\n   \"ruby:3.1/ruby\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-3517\",\n  \"namespace\": \"redhat:distro:redhat:9\",\n  \"packages\": [\n   \"389-ds-base\",\n   \"gjs\",\n   \"grafana\",\n   \"nodejs\",\n   \"nodejs-nodemon\",\n   \"nodejs:18/nodejs\",\n   \"nodejs:18/nodejs-nodemon\",\n   \"polkit\",\n   \"rust\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-3518\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-3519\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-3521\",\n  \"namespace\": \"debian:distro:debian:10\",\n  \"packages\": [\n   \"linux\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-3521\",\n  \"namespace\": \"debian:distro:debian:11\",\n  \"packages\": [\n   \"linux\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-3521\",\n  \"namespace\": \"debian:distro:debian:12\",\n  \"packages\": [\n   \"linux\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-3521\",\n  \"namespace\": \"debian:distro:debian:unstable\",\n  \"packages\": [\n   \"linux\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-35226\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"data_services\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-3522\",\n  \"namespace\": \"debian:distro:debian:10\",\n  \"packages\": [\n   \"linux\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-3522\",\n  \"namespace\": \"debian:distro:debian:11\",\n  \"packages\": [\n   \"linux\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-3522\",\n  \"namespace\": \"debian:distro:debian:12\",\n  \"packages\": [\n   \"linux\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-3522\",\n  \"namespace\": \"debian:distro:debian:unstable\",\n  \"packages\": [\n   \"linux\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-3523\",\n  \"namespace\": \"debian:distro:debian:10\",\n  \"packages\": [\n   \"linux\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-3523\",\n  \"namespace\": \"debian:distro:debian:11\",\n  \"packages\": [\n   \"linux\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-3523\",\n  \"namespace\": \"debian:distro:debian:12\",\n  \"packages\": [\n   \"linux\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-3523\",\n  \"namespace\": \"debian:distro:debian:unstable\",\n  \"packages\": [\n   \"linux\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-3524\",\n  \"namespace\": \"debian:distro:debian:10\",\n  \"packages\": [\n   \"linux\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-3524\",\n  \"namespace\": \"debian:distro:debian:11\",\n  \"packages\": [\n   \"linux\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-3524\",\n  \"namespace\": \"debian:distro:debian:12\",\n  \"packages\": [\n   \"linux\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-3524\",\n  \"namespace\": \"debian:distro:debian:unstable\",\n  \"packages\": [\n   \"linux\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-35255\",\n  \"namespace\": \"debian:distro:debian:12\",\n  \"packages\": [\n   \"nodejs\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-35255\",\n  \"namespace\": \"redhat:distro:redhat:9\",\n  \"packages\": [\n   \"nodejs\",\n   \"nodejs:18/nodejs\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-35256\",\n  \"namespace\": \"debian:distro:debian:12\",\n  \"packages\": [\n   \"nodejs\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-3526\",\n  \"namespace\": \"debian:distro:debian:10\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-3526\",\n  \"namespace\": \"debian:distro:debian:11\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-3526\",\n  \"namespace\": \"debian:distro:debian:12\",\n  \"packages\": [\n   \"linux\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-3526\",\n  \"namespace\": \"debian:distro:debian:unstable\",\n  \"packages\": [\n   \"linux\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-3527\",\n  \"namespace\": \"debian:distro:debian:10\",\n  \"packages\": [\n   \"iproute2\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-3527\",\n  \"namespace\": \"debian:distro:debian:11\",\n  \"packages\": [\n   \"iproute2\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-3527\",\n  \"namespace\": \"debian:distro:debian:12\",\n  \"packages\": [\n   \"iproute2\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-3527\",\n  \"namespace\": \"debian:distro:debian:unstable\",\n  \"packages\": [\n   \"iproute2\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-3528\",\n  \"namespace\": \"debian:distro:debian:10\",\n  \"packages\": [\n   \"iproute2\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-3528\",\n  \"namespace\": \"debian:distro:debian:11\",\n  \"packages\": [\n   \"iproute2\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-3528\",\n  \"namespace\": \"debian:distro:debian:12\",\n  \"packages\": [\n   \"iproute2\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-3528\",\n  \"namespace\": \"debian:distro:debian:unstable\",\n  \"packages\": [\n   \"iproute2\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-35296\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"businessobjects_business_intelligence\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-35297\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"enable_now\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-35299\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"sap_iq\",\n   \"sql_anywhere\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-3529\",\n  \"namespace\": \"debian:distro:debian:10\",\n  \"packages\": [\n   \"iproute2\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-3529\",\n  \"namespace\": \"debian:distro:debian:11\",\n  \"packages\": [\n   \"iproute2\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-3529\",\n  \"namespace\": \"debian:distro:debian:12\",\n  \"packages\": [\n   \"iproute2\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-3529\",\n  \"namespace\": \"debian:distro:debian:unstable\",\n  \"packages\": [\n   \"iproute2\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-3530\",\n  \"namespace\": \"debian:distro:debian:10\",\n  \"packages\": [\n   \"iproute2\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-3530\",\n  \"namespace\": \"debian:distro:debian:11\",\n  \"packages\": [\n   \"iproute2\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-3530\",\n  \"namespace\": \"debian:distro:debian:12\",\n  \"packages\": [\n   \"iproute2\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-3530\",\n  \"namespace\": \"debian:distro:debian:unstable\",\n  \"packages\": [\n   \"iproute2\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35611\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"mqttroute\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35612\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"mqttroute\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35689\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35690\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35691\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35698\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35710\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35711\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35712\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-35829\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"azure_service_fabric\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-35944\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-36063\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-36067\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"vm2\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-36109\",\n  \"namespace\": \"debian:distro:debian:unstable\",\n  \"packages\": [\n   \"docker.io\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-36280\",\n  \"namespace\": \"redhat:distro:redhat:6\",\n  \"packages\": [\n   \"kernel\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-36280\",\n  \"namespace\": \"redhat:distro:redhat:7\",\n  \"packages\": [\n   \"kernel\",\n   \"kernel-rt\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-36280\",\n  \"namespace\": \"redhat:distro:redhat:8\",\n  \"packages\": [\n   \"kernel\",\n   \"kernel-rt\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-36280\",\n  \"namespace\": \"redhat:distro:redhat:9\",\n  \"packages\": [\n   \"kernel\",\n   \"kernel-rt\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-36359\",\n  \"namespace\": \"debian:distro:debian:11\",\n  \"packages\": [\n   \"python-django\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-36359\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"django\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-36360\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-36361\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-36362\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-36363\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-36402\",\n  \"namespace\": \"redhat:distro:redhat:6\",\n  \"packages\": [\n   \"kernel\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-36402\",\n  \"namespace\": \"redhat:distro:redhat:7\",\n  \"packages\": [\n   \"kernel\",\n   \"kernel-rt\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-36402\",\n  \"namespace\": \"redhat:distro:redhat:8\",\n  \"packages\": [\n   \"kernel\",\n   \"kernel-rt\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-36402\",\n  \"namespace\": \"redhat:distro:redhat:9\",\n  \"packages\": [\n   \"kernel\",\n   \"kernel-rt\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-36773\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"cognos_analytics\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-36802\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-36803\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-36879\",\n  \"namespace\": \"ubuntu:distro:ubuntu:18.04\",\n  \"packages\": [\n   \"linux\",\n   \"linux-aws\",\n   \"linux-aws-5.4\",\n   \"linux-azure-4.15\",\n   \"linux-azure-5.4\",\n   \"linux-dell300x\",\n   \"linux-gcp-4.15\",\n   \"linux-gcp-5.4\",\n   \"linux-hwe-5.4\",\n   \"linux-ibm-5.4\",\n   \"linux-kvm\",\n   \"linux-oracle\",\n   \"linux-oracle-5.4\",\n   \"linux-raspi-5.4\",\n   \"linux-raspi2\",\n   \"linux-snapdragon\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-36879\",\n  \"namespace\": \"ubuntu:distro:ubuntu:20.04\",\n  \"packages\": [\n   \"linux\",\n   \"linux-aws\",\n   \"linux-aws-5.15\",\n   \"linux-azure\",\n   \"linux-azure-5.15\",\n   \"linux-azure-fde\",\n   \"linux-bluefield\",\n   \"linux-gcp\",\n   \"linux-gke\",\n   \"linux-gkeop\",\n   \"linux-hwe-5.15\",\n   \"linux-ibm\",\n   \"linux-intel-iotg-5.15\",\n   \"linux-kvm\",\n   \"linux-lowlatency-hwe-5.15\",\n   \"linux-oem-5.14\",\n   \"linux-oracle\",\n   \"linux-raspi\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-36879\",\n  \"namespace\": \"ubuntu:distro:ubuntu:22.04\",\n  \"packages\": [\n   \"linux\",\n   \"linux-aws\",\n   \"linux-azure\",\n   \"linux-azure-fde\",\n   \"linux-gcp\",\n   \"linux-gke\",\n   \"linux-gkeop\",\n   \"linux-ibm\",\n   \"linux-intel-iotg\",\n   \"linux-kvm\",\n   \"linux-lowlatency\",\n   \"linux-oem-5.17\",\n   \"linux-oracle\",\n   \"linux-raspi\",\n   \"linux-riscv\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-36944\",\n  \"namespace\": \"debian:distro:debian:10\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-36944\",\n  \"namespace\": \"debian:distro:debian:11\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-36944\",\n  \"namespace\": \"debian:distro:debian:12\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-36944\",\n  \"namespace\": \"debian:distro:debian:unstable\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-36944\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"scala\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-36946\",\n  \"namespace\": \"ubuntu:distro:ubuntu:22.04\",\n  \"packages\": [\n   \"linux\",\n   \"linux-aws\",\n   \"linux-azure\",\n   \"linux-azure-fde\",\n   \"linux-gcp\",\n   \"linux-gke\",\n   \"linux-gkeop\",\n   \"linux-ibm\",\n   \"linux-intel-iotg\",\n   \"linux-kvm\",\n   \"linux-lowlatency\",\n   \"linux-oem-5.17\",\n   \"linux-oracle\",\n   \"linux-raspi\",\n   \"linux-riscv\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-37026\",\n  \"namespace\": \"debian:distro:debian:10\",\n  \"packages\": [\n   \"erlang\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-37208\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"jfinal_cms\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-37599\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"loader-utils\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-37599\",\n  \"namespace\": \"ubuntu:distro:ubuntu:14.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-37599\",\n  \"namespace\": \"ubuntu:distro:ubuntu:16.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-37599\",\n  \"namespace\": \"ubuntu:distro:ubuntu:18.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-37599\",\n  \"namespace\": \"ubuntu:distro:ubuntu:20.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-37599\",\n  \"namespace\": \"ubuntu:distro:ubuntu:22.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-37601\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"loader-utils\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-37601\",\n  \"namespace\": \"ubuntu:distro:ubuntu:14.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-37601\",\n  \"namespace\": \"ubuntu:distro:ubuntu:16.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-37601\",\n  \"namespace\": \"ubuntu:distro:ubuntu:18.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-37601\",\n  \"namespace\": \"ubuntu:distro:ubuntu:20.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-37601\",\n  \"namespace\": \"ubuntu:distro:ubuntu:22.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-37602\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"grunt-karma\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-37603\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-37609\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"js-beautify\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-37609\",\n  \"namespace\": \"ubuntu:distro:ubuntu:14.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-37609\",\n  \"namespace\": \"ubuntu:distro:ubuntu:16.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-37609\",\n  \"namespace\": \"ubuntu:distro:ubuntu:18.04\",\n  \"packages\": [\n   \"thunderbird\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-37609\",\n  \"namespace\": \"ubuntu:distro:ubuntu:20.04\",\n  \"packages\": [\n   \"thunderbird\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-37609\",\n  \"namespace\": \"ubuntu:distro:ubuntu:22.04\",\n  \"packages\": [\n   \"thunderbird\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-37611\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"gh-pages\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-37614\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"mockery\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-37616\",\n  \"namespace\": \"debian:distro:debian:12\",\n  \"packages\": [\n   \"node-xmldom\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-37616\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"xmldom\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-37617\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"browserify-shim\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-37864\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"solid_edge\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-37968\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"azure_arc-enabled_kubernetes\",\n   \"azure_stack_edge\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-37971\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"malware_protection_engine\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-37973\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-37975\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-38001\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"365_apps\",\n   \"office\",\n   \"office_long_term_servicing_channel\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-38022\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-38032\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-38034\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-38042\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-38043\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-38045\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-38046\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-38048\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"365_apps\",\n   \"office\",\n   \"office_long_term_servicing_channel\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-38049\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"365_apps\",\n   \"office\",\n   \"office_long_term_servicing_channel\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-38053\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"sharepoint_enterprise_server\",\n   \"sharepoint_foundation\",\n   \"sharepoint_server\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-38086\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"shortcodes_ultimate\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-38096\",\n  \"namespace\": \"redhat:distro:redhat:6\",\n  \"packages\": [\n   \"kernel\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-38096\",\n  \"namespace\": \"redhat:distro:redhat:7\",\n  \"packages\": [\n   \"kernel\",\n   \"kernel-rt\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-38096\",\n  \"namespace\": \"redhat:distro:redhat:8\",\n  \"packages\": [\n   \"kernel\",\n   \"kernel-rt\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-38096\",\n  \"namespace\": \"redhat:distro:redhat:9\",\n  \"packages\": [\n   \"kernel\",\n   \"kernel-rt\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-38177\",\n  \"namespace\": \"redhat:distro:redhat:9\",\n  \"packages\": [\n   \"bind\",\n   \"dhcp\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-38339\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"fme_server\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-38340\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"fme_server\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-38341\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"fme_server\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-38342\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"fme_server\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-38371\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"nucleus_net\",\n   \"nucleus_readystart_v3\",\n   \"nucleus_source_code\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-38388\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"navigator_mobile\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-38418\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-38419\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-38420\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-38421\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-38422\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-38423\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-38424\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-38437\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-38440\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-38441\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-38442\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-38443\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-38444\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-38445\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-38446\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-38447\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-38448\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-38449\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-38450\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-38457\",\n  \"namespace\": \"redhat:distro:redhat:6\",\n  \"packages\": [\n   \"kernel\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-38457\",\n  \"namespace\": \"redhat:distro:redhat:7\",\n  \"packages\": [\n   \"kernel\",\n   \"kernel-rt\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-38457\",\n  \"namespace\": \"redhat:distro:redhat:8\",\n  \"packages\": [\n   \"kernel\",\n   \"kernel-rt\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-38457\",\n  \"namespace\": \"redhat:distro:redhat:9\",\n  \"packages\": [\n   \"kernel\",\n   \"kernel-rt\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-38465\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"simatic_s7-1500_software_controller\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-38537\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"archery\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-38541\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"archery\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-38669\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-38670\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-38671\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-38672\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-38673\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-38676\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-38677\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-38679\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-38687\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-38688\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-38689\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-38690\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-38697\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-38698\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-38902\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-38977\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-38980\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-38981\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-38982\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-38983\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-38984\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-38985\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-38986\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-38998\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-39002\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-39011\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-39013\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"business_objects_business_intelligence_platform\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-39015\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"business_objects_business_intelligence_platform\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-39064\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-39065\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-39080\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-39103\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-39105\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-39107\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-39108\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-39109\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-39110\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-39111\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-39112\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-39113\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-39114\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-39115\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-39117\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-39120\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-39121\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-39122\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-39123\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-39124\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-39125\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-39126\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-39127\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-39128\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-39189\",\n  \"namespace\": \"ubuntu:distro:ubuntu:22.04\",\n  \"packages\": [\n   \"linux\",\n   \"linux-aws\",\n   \"linux-azure\",\n   \"linux-azure-fde\",\n   \"linux-gcp\",\n   \"linux-gke\",\n   \"linux-gkeop\",\n   \"linux-ibm\",\n   \"linux-intel-iotg\",\n   \"linux-kvm\",\n   \"linux-lowlatency\",\n   \"linux-oem-5.17\",\n   \"linux-oracle\",\n   \"linux-raspi\",\n   \"linux-riscv\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-39201\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-39201\",\n  \"namespace\": \"redhat:distro:redhat:8\",\n  \"packages\": [\n   \"grafana\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-39201\",\n  \"namespace\": \"redhat:distro:redhat:9\",\n  \"packages\": [\n   \"grafana\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-39201\",\n  \"namespace\": \"ubuntu:distro:ubuntu:14.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-39201\",\n  \"namespace\": \"ubuntu:distro:ubuntu:16.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-39201\",\n  \"namespace\": \"ubuntu:distro:ubuntu:18.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-39201\",\n  \"namespace\": \"ubuntu:distro:ubuntu:20.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-39201\",\n  \"namespace\": \"ubuntu:distro:ubuntu:22.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-39229\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-39229\",\n  \"namespace\": \"redhat:distro:redhat:8\",\n  \"packages\": [\n   \"grafana\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-39229\",\n  \"namespace\": \"redhat:distro:redhat:9\",\n  \"packages\": [\n   \"grafana\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-39229\",\n  \"namespace\": \"ubuntu:distro:ubuntu:14.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-39229\",\n  \"namespace\": \"ubuntu:distro:ubuntu:16.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-39229\",\n  \"namespace\": \"ubuntu:distro:ubuntu:18.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-39229\",\n  \"namespace\": \"ubuntu:distro:ubuntu:20.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-39229\",\n  \"namespace\": \"ubuntu:distro:ubuntu:22.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-39271\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"traefik\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-39278\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-39282\",\n  \"namespace\": \"debian:distro:debian:10\",\n  \"packages\": [\n   \"freerdp2\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-39282\",\n  \"namespace\": \"debian:distro:debian:11\",\n  \"packages\": [\n   \"freerdp2\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-39282\",\n  \"namespace\": \"debian:distro:debian:12\",\n  \"packages\": [\n   \"freerdp2\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-39282\",\n  \"namespace\": \"debian:distro:debian:unstable\",\n  \"packages\": [\n   \"freerdp2\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-39282\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"freerdp\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-39282\",\n  \"namespace\": \"ubuntu:distro:ubuntu:14.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-39282\",\n  \"namespace\": \"ubuntu:distro:ubuntu:16.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-39282\",\n  \"namespace\": \"ubuntu:distro:ubuntu:18.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-39282\",\n  \"namespace\": \"ubuntu:distro:ubuntu:20.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-39282\",\n  \"namespace\": \"ubuntu:distro:ubuntu:22.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-39283\",\n  \"namespace\": \"debian:distro:debian:10\",\n  \"packages\": [\n   \"freerdp2\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-39283\",\n  \"namespace\": \"debian:distro:debian:11\",\n  \"packages\": [\n   \"freerdp2\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-39283\",\n  \"namespace\": \"debian:distro:debian:12\",\n  \"packages\": [\n   \"freerdp2\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-39283\",\n  \"namespace\": \"debian:distro:debian:unstable\",\n  \"packages\": [\n   \"freerdp2\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-39283\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"freerdp\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-39283\",\n  \"namespace\": \"ubuntu:distro:ubuntu:14.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-39283\",\n  \"namespace\": \"ubuntu:distro:ubuntu:16.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-39283\",\n  \"namespace\": \"ubuntu:distro:ubuntu:18.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-39283\",\n  \"namespace\": \"ubuntu:distro:ubuntu:20.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-39283\",\n  \"namespace\": \"ubuntu:distro:ubuntu:22.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-39288\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"fastify\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-39293\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-39295\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-39296\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"melis-asset-manager\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-39297\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"meliscms\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-39298\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"meliscms\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-39299\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"passport-saml\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-39300\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"node_saml\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-39302\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-39303\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-39308\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-39309\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-39310\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-39311\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-39800\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"businessobjects_business_intelligence\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-39802\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"manufacturing_execution\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-39803\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"3d_visual_enterprise_author\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-39804\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"3d_visual_enterprise_author\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-39805\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"3d_visual_enterprise_author\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-39806\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"3d_visual_enterprise_author\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-39807\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"3d_visual_enterprise_author\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-39808\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"3d_visual_enterprise_author\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-39955\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"owasp_modsecurity_core_rule_set\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-39956\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"owasp_modsecurity_core_rule_set\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-39957\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"owasp_modsecurity_core_rule_set\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-39958\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"owasp_modsecurity_core_rule_set\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-40023\",\n  \"namespace\": \"redhat:distro:redhat:6\",\n  \"packages\": [\n   \"python-mako\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-40023\",\n  \"namespace\": \"redhat:distro:redhat:7\",\n  \"packages\": [\n   \"python-mako\",\n   \"resource-agents\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-40023\",\n  \"namespace\": \"redhat:distro:redhat:8\",\n  \"packages\": [\n   \"python-mako\",\n   \"resource-agents\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-40023\",\n  \"namespace\": \"redhat:distro:redhat:9\",\n  \"packages\": [\n   \"python-mako\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-40047\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"flatpress\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-40133\",\n  \"namespace\": \"redhat:distro:redhat:6\",\n  \"packages\": [\n   \"kernel\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-40133\",\n  \"namespace\": \"redhat:distro:redhat:7\",\n  \"packages\": [\n   \"kernel\",\n   \"kernel-rt\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-40133\",\n  \"namespace\": \"redhat:distro:redhat:8\",\n  \"packages\": [\n   \"kernel\",\n   \"kernel-rt\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-40133\",\n  \"namespace\": \"redhat:distro:redhat:9\",\n  \"packages\": [\n   \"kernel\",\n   \"kernel-rt\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-40147\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"industrial_edge_management\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-40176\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-40177\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-40178\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-40179\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-40180\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-40181\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-40182\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-40187\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-40226\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-40227\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-40303\",\n  \"namespace\": \"alpine:distro:alpine:3.13\",\n  \"packages\": [\n   \"libxml2\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-40303\",\n  \"namespace\": \"alpine:distro:alpine:3.14\",\n  \"packages\": [\n   \"libxml2\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-40303\",\n  \"namespace\": \"alpine:distro:alpine:3.15\",\n  \"packages\": [\n   \"libxml2\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-40303\",\n  \"namespace\": \"alpine:distro:alpine:3.16\",\n  \"packages\": [\n   \"libxml2\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-40303\",\n  \"namespace\": \"alpine:distro:alpine:edge\",\n  \"packages\": [\n   \"libxml2\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-40304\",\n  \"namespace\": \"alpine:distro:alpine:3.13\",\n  \"packages\": [\n   \"libxml2\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-40304\",\n  \"namespace\": \"alpine:distro:alpine:3.14\",\n  \"packages\": [\n   \"libxml2\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-40304\",\n  \"namespace\": \"alpine:distro:alpine:3.15\",\n  \"packages\": [\n   \"libxml2\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-40304\",\n  \"namespace\": \"alpine:distro:alpine:3.16\",\n  \"packages\": [\n   \"libxml2\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-40304\",\n  \"namespace\": \"alpine:distro:alpine:edge\",\n  \"packages\": [\n   \"libxml2\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-40440\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"mxgraph\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-40468\",\n  \"namespace\": \"debian:distro:debian:10\",\n  \"packages\": [\n   \"tinyproxy\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-40469\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-40494\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"nps\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-40631\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-40664\",\n  \"namespace\": \"debian:distro:debian:10\",\n  \"packages\": [\n   \"shiro\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-40664\",\n  \"namespace\": \"debian:distro:debian:11\",\n  \"packages\": [\n   \"shiro\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-40664\",\n  \"namespace\": \"debian:distro:debian:12\",\n  \"packages\": [\n   \"shiro\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-40664\",\n  \"namespace\": \"debian:distro:debian:unstable\",\n  \"packages\": [\n   \"shiro\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-40664\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"shiro\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-40664\",\n  \"namespace\": \"ubuntu:distro:ubuntu:14.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-40664\",\n  \"namespace\": \"ubuntu:distro:ubuntu:16.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-40664\",\n  \"namespace\": \"ubuntu:distro:ubuntu:18.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-40664\",\n  \"namespace\": \"ubuntu:distro:ubuntu:20.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-40664\",\n  \"namespace\": \"ubuntu:distro:ubuntu:22.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-40674\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"libexpat\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-40674\",\n  \"namespace\": \"redhat:distro:redhat:8\",\n  \"packages\": [\n   \"expat\",\n   \"firefox\",\n   \"firefox:flatpak/firefox\",\n   \"thunderbird\",\n   \"thunderbird:flatpak/thunderbird\",\n   \"xmlrpc-c\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-40768\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-40777\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"email_marketer\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-40871\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"dolibarr_erp/crm\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-40871\",\n  \"namespace\": \"ubuntu:distro:ubuntu:14.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-40871\",\n  \"namespace\": \"ubuntu:distro:ubuntu:16.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-40871\",\n  \"namespace\": \"ubuntu:distro:ubuntu:18.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-40871\",\n  \"namespace\": \"ubuntu:distro:ubuntu:20.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-40871\",\n  \"namespace\": \"ubuntu:distro:ubuntu:22.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-40921\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"dedecms\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41031\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"365_apps\",\n   \"office\",\n   \"office_long_term_servicing_channel\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41032\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \".net\",\n   \".net_core\",\n   \"visual_studio_2019\",\n   \"visual_studio_2022\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41032\",\n  \"namespace\": \"redhat:distro:redhat:8\",\n  \"packages\": [\n   \"dotnet3.1\",\n   \"dotnet6.0\",\n   \"dotnet7.0\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41032\",\n  \"namespace\": \"redhat:distro:redhat:9\",\n  \"packages\": [\n   \"dotnet6.0\",\n   \"dotnet7.0\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41034\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"visual_studio_code\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-41035\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"edge_chromium\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41036\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"sharepoint_foundation\",\n   \"sharepoint_server\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41037\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"sharepoint_foundation\",\n   \"sharepoint_server\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41038\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"sharepoint_foundation\",\n   \"sharepoint_server\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41042\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"visual_studio_code\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-41043\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"office\",\n   \"office_long_term_servicing_channel\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41083\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"jupyter\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-41166\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"3d_visual_enterprise_author\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-41167\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"3d_visual_enterprise_author\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-41168\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"3d_visual_enterprise_author\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-41169\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"3d_visual_enterprise_author\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-41170\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"3d_visual_enterprise_author\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-41171\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"3d_visual_enterprise_author\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-41172\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"3d_visual_enterprise_author\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-41173\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"3d_visual_enterprise_author\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-41174\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"3d_visual_enterprise_author\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-41175\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"3d_visual_enterprise_author\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-41176\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"3d_visual_enterprise_author\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-41177\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"3d_visual_enterprise_author\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-41178\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"3d_visual_enterprise_author\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-41179\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"3d_visual_enterprise_author\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-41180\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"3d_visual_enterprise_author\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-41181\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"3d_visual_enterprise_author\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-41182\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"3d_visual_enterprise_author\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-41183\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"3d_visual_enterprise_author\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-41184\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"3d_visual_enterprise_author\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-41185\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"3d_visual_enterprise_author\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-41186\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"3d_visual_enterprise_viewer\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-41187\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"3d_visual_enterprise_viewer\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-41188\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"3d_visual_enterprise_viewer\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-41189\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"3d_visual_enterprise_viewer\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-41190\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"3d_visual_enterprise_viewer\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-41191\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"3d_visual_enterprise_viewer\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-41192\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"3d_visual_enterprise_viewer\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-41193\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"3d_visual_enterprise_viewer\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-41194\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"3d_visual_enterprise_viewer\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-41195\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"3d_visual_enterprise_viewer\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-41196\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"3d_visual_enterprise_viewer\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-41197\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"3d_visual_enterprise_viewer\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-41198\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"3d_visual_enterprise_viewer\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-41199\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"3d_visual_enterprise_viewer\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-41200\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"3d_visual_enterprise_viewer\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-41201\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"3d_visual_enterprise_viewer\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-41202\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"3d_visual_enterprise_viewer\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-41204\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"commerce\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-41206\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"businessobjects_business_intelligence\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-41209\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"customer_data_cloud\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-41210\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"customer_data_cloud\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-41301\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"subassembly_composer\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41302\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41303\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41304\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41305\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41306\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41307\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41308\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41316\",\n  \"namespace\": \"alpine:distro:alpine:3.16\",\n  \"packages\": [\n   \"vault\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41316\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"vault\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-41323\",\n  \"namespace\": \"debian:distro:debian:10\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-41323\",\n  \"namespace\": \"debian:distro:debian:11\",\n  \"packages\": [\n   \"python-django\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-41323\",\n  \"namespace\": \"debian:distro:debian:12\",\n  \"packages\": [\n   \"python-django\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-41323\",\n  \"namespace\": \"debian:distro:debian:unstable\",\n  \"packages\": [\n   \"python-django\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41323\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41348\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"collaboration\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41349\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"collaboration\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41350\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"collaboration\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41351\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"collaboration\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-41380\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"d8s-yaml\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-41381\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"d8s-utility\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-41382\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"d8s-json\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-41383\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"d8s-archives\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-41384\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"d8s-domains\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-41385\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"d8s-html\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-41386\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"d8s-utility\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-41387\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"d8s-pdfs\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41390\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41391\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-41392\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"total.js\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41403\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"newsletter_subscribe_(popup_+_regular_module)\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-41404\",\n  \"namespace\": \"debian:distro:debian:10\",\n  \"packages\": [\n   \"ini4j\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-41404\",\n  \"namespace\": \"debian:distro:debian:11\",\n  \"packages\": [\n   \"ini4j\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-41404\",\n  \"namespace\": \"debian:distro:debian:12\",\n  \"packages\": [\n   \"ini4j\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-41404\",\n  \"namespace\": \"debian:distro:debian:unstable\",\n  \"packages\": [\n   \"ini4j\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-41404\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"ini4j\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41404\",\n  \"namespace\": \"ubuntu:distro:ubuntu:14.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41404\",\n  \"namespace\": \"ubuntu:distro:ubuntu:16.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41404\",\n  \"namespace\": \"ubuntu:distro:ubuntu:18.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41404\",\n  \"namespace\": \"ubuntu:distro:ubuntu:20.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41404\",\n  \"namespace\": \"ubuntu:distro:ubuntu:22.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-41406\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"church_management_system\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-41407\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"online_pet_shop_we_app\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-41408\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"online_pet_shop_we_app\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41416\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41436\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41473\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"rpcms\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41474\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"rpcms\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41475\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"rpcms\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41477\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41480\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41481\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41482\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41483\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41484\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41485\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41489\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41495\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"clippercms\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41496\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"icms\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41497\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"clippercms\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-41530\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"open_source_sacco_management_system\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-41532\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"open_source_sacco_management_system\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41533\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"online_diagnostic_lab_management_system\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41534\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"online_diagnostic_lab_management_system\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41535\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41536\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41538\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41539\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-41550\",\n  \"namespace\": \"debian:distro:debian:10\",\n  \"packages\": [\n   \"libosip2\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-41550\",\n  \"namespace\": \"debian:distro:debian:11\",\n  \"packages\": [\n   \"libosip2\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-41550\",\n  \"namespace\": \"debian:distro:debian:12\",\n  \"packages\": [\n   \"libosip2\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-41550\",\n  \"namespace\": \"debian:distro:debian:unstable\",\n  \"packages\": [\n   \"libosip2\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-41550\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"osip\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41550\",\n  \"namespace\": \"ubuntu:distro:ubuntu:14.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41550\",\n  \"namespace\": \"ubuntu:distro:ubuntu:16.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41550\",\n  \"namespace\": \"ubuntu:distro:ubuntu:18.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41550\",\n  \"namespace\": \"ubuntu:distro:ubuntu:20.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41550\",\n  \"namespace\": \"ubuntu:distro:ubuntu:22.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41576\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41577\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41578\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41580\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41581\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41582\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41583\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41584\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41585\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41586\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41587\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41588\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41589\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41592\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41593\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41594\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41595\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41597\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41598\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41600\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41601\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41602\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41603\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-41606\",\n  \"namespace\": \"debian:distro:debian:11\",\n  \"packages\": [\n   \"nomad\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-41606\",\n  \"namespace\": \"debian:distro:debian:unstable\",\n  \"packages\": [\n   \"nomad\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-41606\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"nomad\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41606\",\n  \"namespace\": \"ubuntu:distro:ubuntu:14.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41606\",\n  \"namespace\": \"ubuntu:distro:ubuntu:16.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41606\",\n  \"namespace\": \"ubuntu:distro:ubuntu:18.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41606\",\n  \"namespace\": \"ubuntu:distro:ubuntu:20.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41606\",\n  \"namespace\": \"ubuntu:distro:ubuntu:22.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41623\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-41665\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41674\",\n  \"namespace\": \"alpine:distro:alpine:3.15\",\n  \"packages\": [\n   \"linux-lts\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41674\",\n  \"namespace\": \"alpine:distro:alpine:3.16\",\n  \"packages\": [\n   \"linux-lts\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41674\",\n  \"namespace\": \"alpine:distro:alpine:edge\",\n  \"packages\": [\n   \"linux-lts\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41674\",\n  \"namespace\": \"debian:distro:debian:10\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41674\",\n  \"namespace\": \"debian:distro:debian:11\",\n  \"packages\": [\n   \"linux\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41674\",\n  \"namespace\": \"debian:distro:debian:12\",\n  \"packages\": [\n   \"linux\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41674\",\n  \"namespace\": \"debian:distro:debian:unstable\",\n  \"packages\": [\n   \"linux\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41674\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41674\",\n  \"namespace\": \"redhat:distro:redhat:8\",\n  \"packages\": [\n   \"kernel\",\n   \"kernel-rt\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41674\",\n  \"namespace\": \"redhat:distro:redhat:9\",\n  \"packages\": [\n   \"kernel\",\n   \"kernel-rt\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41674\",\n  \"namespace\": \"ubuntu:distro:ubuntu:14.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41674\",\n  \"namespace\": \"ubuntu:distro:ubuntu:16.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41674\",\n  \"namespace\": \"ubuntu:distro:ubuntu:18.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41674\",\n  \"namespace\": \"ubuntu:distro:ubuntu:20.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41674\",\n  \"namespace\": \"ubuntu:distro:ubuntu:22.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41686\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-41715\",\n  \"namespace\": \"debian:distro:debian:10\",\n  \"packages\": [\n   \"golang-1.11\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-41715\",\n  \"namespace\": \"debian:distro:debian:11\",\n  \"packages\": [\n   \"golang-1.15\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-41715\",\n  \"namespace\": \"debian:distro:debian:12\",\n  \"packages\": [\n   \"golang-1.18\",\n   \"golang-1.19\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-41715\",\n  \"namespace\": \"debian:distro:debian:unstable\",\n  \"packages\": [\n   \"golang-1.18\",\n   \"golang-1.19\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41715\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41715\",\n  \"namespace\": \"redhat:distro:redhat:8\",\n  \"packages\": [\n   \"container-tools:3.0/buildah\",\n   \"container-tools:3.0/containernetworking-plugins\",\n   \"container-tools:3.0/oci-seccomp-bpf-hook\",\n   \"container-tools:3.0/podman\",\n   \"container-tools:3.0/runc\",\n   \"container-tools:3.0/skopeo\",\n   \"container-tools:3.0/toolbox\",\n   \"container-tools:4.0/buildah\",\n   \"container-tools:4.0/containernetworking-plugins\",\n   \"container-tools:4.0/oci-seccomp-bpf-hook\",\n   \"container-tools:4.0/podman\",\n   \"container-tools:4.0/runc\",\n   \"container-tools:4.0/skopeo\",\n   \"container-tools:4.0/toolbox\",\n   \"container-tools:rhel8/buildah\",\n   \"container-tools:rhel8/containernetworking-plugins\",\n   \"container-tools:rhel8/oci-seccomp-bpf-hook\",\n   \"container-tools:rhel8/podman\",\n   \"container-tools:rhel8/runc\",\n   \"container-tools:rhel8/skopeo\",\n   \"container-tools:rhel8/toolbox\",\n   \"git-lfs\",\n   \"go-toolset:rhel8/golang\",\n   \"grafana\",\n   \"grafana-pcp\",\n   \"osbuild-composer\",\n   \"rsyslog\",\n   \"weldr-client\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-41715\",\n  \"namespace\": \"redhat:distro:redhat:9\",\n  \"packages\": [\n   \"buildah\",\n   \"butane\",\n   \"containernetworking-plugins\",\n   \"git-lfs\",\n   \"golang\",\n   \"grafana\",\n   \"grafana-pcp\",\n   \"ignition\",\n   \"oci-seccomp-bpf-hook\",\n   \"osbuild-composer\",\n   \"podman\",\n   \"rsyslog\",\n   \"runc\",\n   \"skopeo\",\n   \"weldr-client\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-41828\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"amazon_web_services_redshift_java_database_connectivity_driver\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-41851\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"jt_open_toolkit\",\n   \"simcenter_femap\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-42003\",\n  \"namespace\": \"debian:distro:debian:10\",\n  \"packages\": [\n   \"jackson-databind\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-42003\",\n  \"namespace\": \"debian:distro:debian:11\",\n  \"packages\": [\n   \"jackson-databind\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-42003\",\n  \"namespace\": \"debian:distro:debian:12\",\n  \"packages\": [\n   \"jackson-databind\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-42003\",\n  \"namespace\": \"debian:distro:debian:unstable\",\n  \"packages\": [\n   \"jackson-databind\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-42003\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"jackson-databind\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-42010\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"d-bus\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42010\",\n  \"namespace\": \"redhat:distro:redhat:6\",\n  \"packages\": [\n   \"dbus\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42010\",\n  \"namespace\": \"redhat:distro:redhat:7\",\n  \"packages\": [\n   \"dbus\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42010\",\n  \"namespace\": \"redhat:distro:redhat:8\",\n  \"packages\": [\n   \"dbus\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42010\",\n  \"namespace\": \"redhat:distro:redhat:9\",\n  \"packages\": [\n   \"dbus\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-42011\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"d-bus\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42011\",\n  \"namespace\": \"redhat:distro:redhat:6\",\n  \"packages\": [\n   \"dbus\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42011\",\n  \"namespace\": \"redhat:distro:redhat:7\",\n  \"packages\": [\n   \"dbus\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42011\",\n  \"namespace\": \"redhat:distro:redhat:8\",\n  \"packages\": [\n   \"dbus\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42011\",\n  \"namespace\": \"redhat:distro:redhat:9\",\n  \"packages\": [\n   \"dbus\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-42012\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"d-bus\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42012\",\n  \"namespace\": \"redhat:distro:redhat:6\",\n  \"packages\": [\n   \"dbus\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42012\",\n  \"namespace\": \"redhat:distro:redhat:7\",\n  \"packages\": [\n   \"dbus\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42012\",\n  \"namespace\": \"redhat:distro:redhat:8\",\n  \"packages\": [\n   \"dbus\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42012\",\n  \"namespace\": \"redhat:distro:redhat:9\",\n  \"packages\": [\n   \"dbus\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-42036\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"d8s-urls\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-42037\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"d8s-asns\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-42038\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"d8s-ip-addresses\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-42039\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"d8s-lists\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-42040\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"d8s-algorithms\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-42041\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"d8s-file-system\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-42042\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"d8s-networking\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-42043\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"d8s-xml\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-42044\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"d8s-asns\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42064\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"online_diagnostic_lab_management_system\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42066\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"online_examination_system\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42067\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42069\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"online_birth_certificate_management_system\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42070\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42071\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42077\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42078\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42079\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42080\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42081\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42086\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42087\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42156\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42159\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42160\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42161\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42232\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42234\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42339\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42340\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42341\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42342\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42463\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42464\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42488\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-42711\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"whatsup_gold\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42715\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"redcap\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"CVE-2022-42717\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"packer\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42717\",\n  \"namespace\": \"ubuntu:distro:ubuntu:14.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42717\",\n  \"namespace\": \"ubuntu:distro:ubuntu:16.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42717\",\n  \"namespace\": \"ubuntu:distro:ubuntu:18.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42717\",\n  \"namespace\": \"ubuntu:distro:ubuntu:20.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42717\",\n  \"namespace\": \"ubuntu:distro:ubuntu:22.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42719\",\n  \"namespace\": \"alpine:distro:alpine:3.15\",\n  \"packages\": [\n   \"linux-lts\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42719\",\n  \"namespace\": \"alpine:distro:alpine:3.16\",\n  \"packages\": [\n   \"linux-lts\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42719\",\n  \"namespace\": \"alpine:distro:alpine:edge\",\n  \"packages\": [\n   \"linux-lts\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42719\",\n  \"namespace\": \"debian:distro:debian:10\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42719\",\n  \"namespace\": \"debian:distro:debian:11\",\n  \"packages\": [\n   \"linux\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42719\",\n  \"namespace\": \"debian:distro:debian:12\",\n  \"packages\": [\n   \"linux\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42719\",\n  \"namespace\": \"debian:distro:debian:unstable\",\n  \"packages\": [\n   \"linux\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42719\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42719\",\n  \"namespace\": \"ubuntu:distro:ubuntu:14.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42719\",\n  \"namespace\": \"ubuntu:distro:ubuntu:16.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42719\",\n  \"namespace\": \"ubuntu:distro:ubuntu:18.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42719\",\n  \"namespace\": \"ubuntu:distro:ubuntu:20.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42719\",\n  \"namespace\": \"ubuntu:distro:ubuntu:22.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42720\",\n  \"namespace\": \"alpine:distro:alpine:3.15\",\n  \"packages\": [\n   \"linux-lts\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42720\",\n  \"namespace\": \"alpine:distro:alpine:3.16\",\n  \"packages\": [\n   \"linux-lts\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42720\",\n  \"namespace\": \"alpine:distro:alpine:edge\",\n  \"packages\": [\n   \"linux-lts\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42720\",\n  \"namespace\": \"debian:distro:debian:10\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42720\",\n  \"namespace\": \"debian:distro:debian:11\",\n  \"packages\": [\n   \"linux\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42720\",\n  \"namespace\": \"debian:distro:debian:12\",\n  \"packages\": [\n   \"linux\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42720\",\n  \"namespace\": \"debian:distro:debian:unstable\",\n  \"packages\": [\n   \"linux\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42720\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42720\",\n  \"namespace\": \"redhat:distro:redhat:6\",\n  \"packages\": [\n   \"kernel\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42720\",\n  \"namespace\": \"redhat:distro:redhat:7\",\n  \"packages\": [\n   \"kernel\",\n   \"kernel-rt\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42720\",\n  \"namespace\": \"redhat:distro:redhat:8\",\n  \"packages\": [\n   \"kernel\",\n   \"kernel-rt\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42720\",\n  \"namespace\": \"redhat:distro:redhat:9\",\n  \"packages\": [\n   \"kernel\",\n   \"kernel-rt\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42720\",\n  \"namespace\": \"ubuntu:distro:ubuntu:14.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42720\",\n  \"namespace\": \"ubuntu:distro:ubuntu:16.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42720\",\n  \"namespace\": \"ubuntu:distro:ubuntu:18.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42720\",\n  \"namespace\": \"ubuntu:distro:ubuntu:20.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42720\",\n  \"namespace\": \"ubuntu:distro:ubuntu:22.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42721\",\n  \"namespace\": \"alpine:distro:alpine:3.15\",\n  \"packages\": [\n   \"linux-lts\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42721\",\n  \"namespace\": \"alpine:distro:alpine:3.16\",\n  \"packages\": [\n   \"linux-lts\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42721\",\n  \"namespace\": \"alpine:distro:alpine:edge\",\n  \"packages\": [\n   \"linux-lts\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42721\",\n  \"namespace\": \"debian:distro:debian:10\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42721\",\n  \"namespace\": \"debian:distro:debian:11\",\n  \"packages\": [\n   \"linux\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42721\",\n  \"namespace\": \"debian:distro:debian:12\",\n  \"packages\": [\n   \"linux\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42721\",\n  \"namespace\": \"debian:distro:debian:unstable\",\n  \"packages\": [\n   \"linux\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42721\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42721\",\n  \"namespace\": \"redhat:distro:redhat:8\",\n  \"packages\": [\n   \"kernel\",\n   \"kernel-rt\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42721\",\n  \"namespace\": \"redhat:distro:redhat:9\",\n  \"packages\": [\n   \"kernel\",\n   \"kernel-rt\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42721\",\n  \"namespace\": \"ubuntu:distro:ubuntu:14.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42721\",\n  \"namespace\": \"ubuntu:distro:ubuntu:16.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42721\",\n  \"namespace\": \"ubuntu:distro:ubuntu:18.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42721\",\n  \"namespace\": \"ubuntu:distro:ubuntu:20.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42721\",\n  \"namespace\": \"ubuntu:distro:ubuntu:22.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42722\",\n  \"namespace\": \"alpine:distro:alpine:3.15\",\n  \"packages\": [\n   \"linux-lts\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42722\",\n  \"namespace\": \"alpine:distro:alpine:3.16\",\n  \"packages\": [\n   \"linux-lts\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42722\",\n  \"namespace\": \"alpine:distro:alpine:edge\",\n  \"packages\": [\n   \"linux-lts\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42722\",\n  \"namespace\": \"debian:distro:debian:10\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42722\",\n  \"namespace\": \"debian:distro:debian:11\",\n  \"packages\": [\n   \"linux\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42722\",\n  \"namespace\": \"debian:distro:debian:12\",\n  \"packages\": [\n   \"linux\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42722\",\n  \"namespace\": \"debian:distro:debian:unstable\",\n  \"packages\": [\n   \"linux\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42722\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42722\",\n  \"namespace\": \"redhat:distro:redhat:6\",\n  \"packages\": [\n   \"kernel\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42722\",\n  \"namespace\": \"redhat:distro:redhat:7\",\n  \"packages\": [\n   \"kernel\",\n   \"kernel-rt\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42722\",\n  \"namespace\": \"redhat:distro:redhat:8\",\n  \"packages\": [\n   \"kernel\",\n   \"kernel-rt\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42722\",\n  \"namespace\": \"redhat:distro:redhat:9\",\n  \"packages\": [\n   \"kernel\",\n   \"kernel-rt\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42722\",\n  \"namespace\": \"ubuntu:distro:ubuntu:14.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42722\",\n  \"namespace\": \"ubuntu:distro:ubuntu:16.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42722\",\n  \"namespace\": \"ubuntu:distro:ubuntu:18.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42722\",\n  \"namespace\": \"ubuntu:distro:ubuntu:20.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42722\",\n  \"namespace\": \"ubuntu:distro:ubuntu:22.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42889\",\n  \"namespace\": \"debian:distro:debian:11\",\n  \"packages\": [\n   \"commons-text\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42889\",\n  \"namespace\": \"debian:distro:debian:12\",\n  \"packages\": [\n   \"commons-text\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42889\",\n  \"namespace\": \"debian:distro:debian:unstable\",\n  \"packages\": [\n   \"commons-text\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42889\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42889\",\n  \"namespace\": \"ubuntu:distro:ubuntu:14.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42889\",\n  \"namespace\": \"ubuntu:distro:ubuntu:16.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42889\",\n  \"namespace\": \"ubuntu:distro:ubuntu:18.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42889\",\n  \"namespace\": \"ubuntu:distro:ubuntu:20.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42889\",\n  \"namespace\": \"ubuntu:distro:ubuntu:22.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42897\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42899\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"microstation\",\n   \"view\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42900\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"microstation\",\n   \"view\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42901\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"microstation\",\n   \"view\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42902\",\n  \"namespace\": \"debian:distro:debian:10\",\n  \"packages\": [\n   \"lava\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42902\",\n  \"namespace\": \"debian:distro:debian:11\",\n  \"packages\": [\n   \"lava\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42902\",\n  \"namespace\": \"debian:distro:debian:unstable\",\n  \"packages\": [\n   \"lava\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42902\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": [\n   \"lava\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42906\",\n  \"namespace\": \"debian:distro:debian:10\",\n  \"packages\": [\n   \"powerline-gitstatus\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42906\",\n  \"namespace\": \"debian:distro:debian:11\",\n  \"packages\": [\n   \"powerline-gitstatus\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42906\",\n  \"namespace\": \"debian:distro:debian:12\",\n  \"packages\": [\n   \"powerline-gitstatus\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42906\",\n  \"namespace\": \"debian:distro:debian:unstable\",\n  \"packages\": [\n   \"powerline-gitstatus\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42906\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42906\",\n  \"namespace\": \"ubuntu:distro:ubuntu:14.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42906\",\n  \"namespace\": \"ubuntu:distro:ubuntu:16.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42906\",\n  \"namespace\": \"ubuntu:distro:ubuntu:18.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42906\",\n  \"namespace\": \"ubuntu:distro:ubuntu:20.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42906\",\n  \"namespace\": \"ubuntu:distro:ubuntu:22.04\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42961\",\n  \"namespace\": \"debian:distro:debian:11\",\n  \"packages\": [\n   \"wolfssl\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42961\",\n  \"namespace\": \"debian:distro:debian:12\",\n  \"packages\": [\n   \"wolfssl\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42961\",\n  \"namespace\": \"debian:distro:debian:unstable\",\n  \"packages\": [\n   \"wolfssl\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42961\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42968\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"CVE-2022-42969\",\n  \"namespace\": \"nvd:cpe\",\n  \"packages\": []\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"ELSA-2022-6911\",\n  \"namespace\": \"oracle:distro:oraclelinux:8\",\n  \"packages\": [\n   \"aspnetcore-runtime-6.0\",\n   \"aspnetcore-targeting-pack-6.0\",\n   \"dotnet\",\n   \"dotnet-apphost-pack-6.0\",\n   \"dotnet-host\",\n   \"dotnet-hostfxr-6.0\",\n   \"dotnet-runtime-6.0\",\n   \"dotnet-sdk-6.0\",\n   \"dotnet-sdk-6.0-source-built-artifacts\",\n   \"dotnet-targeting-pack-6.0\",\n   \"dotnet-templates-6.0\",\n   \"netstandard-targeting-pack-2.1\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"ELSA-2022-9862\",\n  \"namespace\": \"oracle:distro:oraclelinux:8\",\n  \"packages\": [\n   \"hivex\",\n   \"hivex-devel\",\n   \"libguestfs\",\n   \"libguestfs-appliance\",\n   \"libguestfs-bash-completion\",\n   \"libguestfs-devel\",\n   \"libguestfs-gfs2\",\n   \"libguestfs-gobject\",\n   \"libguestfs-gobject-devel\",\n   \"libguestfs-inspect-icons\",\n   \"libguestfs-java\",\n   \"libguestfs-java-devel\",\n   \"libguestfs-javadoc\",\n   \"libguestfs-man-pages-ja\",\n   \"libguestfs-man-pages-uk\",\n   \"libguestfs-rescue\",\n   \"libguestfs-rsync\",\n   \"libguestfs-tools\",\n   \"libguestfs-tools-c\",\n   \"libguestfs-winsupport\",\n   \"libguestfs-xfs\",\n   \"libiscsi\",\n   \"libiscsi-devel\",\n   \"libiscsi-utils\",\n   \"libnbd\",\n   \"libnbd-bash-completion\",\n   \"libnbd-devel\",\n   \"libtpms\",\n   \"libtpms-devel\",\n   \"libvirt\",\n   \"libvirt-client\",\n   \"libvirt-daemon\",\n   \"libvirt-daemon-config-network\",\n   \"libvirt-daemon-config-nwfilter\",\n   \"libvirt-daemon-driver-interface\",\n   \"libvirt-daemon-driver-network\",\n   \"libvirt-daemon-driver-nodedev\",\n   \"libvirt-daemon-driver-nwfilter\",\n   \"libvirt-daemon-driver-qemu\",\n   \"libvirt-daemon-driver-secret\",\n   \"libvirt-daemon-driver-storage\",\n   \"libvirt-daemon-driver-storage-core\",\n   \"libvirt-daemon-driver-storage-disk\",\n   \"libvirt-daemon-driver-storage-gluster\",\n   \"libvirt-daemon-driver-storage-iscsi\",\n   \"libvirt-daemon-driver-storage-iscsi-direct\",\n   \"libvirt-daemon-driver-storage-logical\",\n   \"libvirt-daemon-driver-storage-mpath\",\n   \"libvirt-daemon-driver-storage-rbd\",\n   \"libvirt-daemon-driver-storage-scsi\",\n   \"libvirt-daemon-kvm\",\n   \"libvirt-dbus\",\n   \"libvirt-devel\",\n   \"libvirt-docs\",\n   \"libvirt-libs\",\n   \"libvirt-lock-sanlock\",\n   \"libvirt-nss\",\n   \"libvirt-wireshark\",\n   \"lua-guestfs\",\n   \"nbdfuse\",\n   \"nbdkit\",\n   \"nbdkit-bash-completion\",\n   \"nbdkit-basic-filters\",\n   \"nbdkit-basic-plugins\",\n   \"nbdkit-curl-plugin\",\n   \"nbdkit-devel\",\n   \"nbdkit-example-plugins\",\n   \"nbdkit-gzip-filter\",\n   \"nbdkit-gzip-plugin\",\n   \"nbdkit-linuxdisk-plugin\",\n   \"nbdkit-nbd-plugin\",\n   \"nbdkit-python-plugin\",\n   \"nbdkit-server\",\n   \"nbdkit-ssh-plugin\",\n   \"nbdkit-tar-filter\",\n   \"nbdkit-tar-plugin\",\n   \"nbdkit-tmpdisk-plugin\",\n   \"nbdkit-vddk-plugin\",\n   \"nbdkit-xz-filter\",\n   \"netcf\",\n   \"netcf-devel\",\n   \"netcf-libs\",\n   \"perl-hivex\",\n   \"perl-sys-guestfs\",\n   \"perl-sys-virt\",\n   \"python3-hivex\",\n   \"python3-libguestfs\",\n   \"python3-libnbd\",\n   \"python3-libvirt\",\n   \"qemu-guest-agent\",\n   \"qemu-img\",\n   \"qemu-kvm\",\n   \"qemu-kvm-block-curl\",\n   \"qemu-kvm-block-gluster\",\n   \"qemu-kvm-block-iscsi\",\n   \"qemu-kvm-block-rbd\",\n   \"qemu-kvm-block-ssh\",\n   \"qemu-kvm-common\",\n   \"qemu-kvm-core\",\n   \"qemu-virtiofsd\",\n   \"ruby-hivex\",\n   \"ruby-libguestfs\",\n   \"seabios\",\n   \"seabios-bin\",\n   \"seavgabios-bin\",\n   \"sgabios\",\n   \"sgabios-bin\",\n   \"supermin\",\n   \"supermin-devel\",\n   \"swtpm\",\n   \"swtpm-devel\",\n   \"swtpm-libs\",\n   \"swtpm-tools\",\n   \"swtpm-tools-pkcs11\",\n   \"virt-dib\",\n   \"virt-v2v\",\n   \"virt-v2v-bash-completion\",\n   \"virt-v2v-man-pages-ja\",\n   \"virt-v2v-man-pages-uk\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"GHSA-3fhf-6939-qg8p\",\n  \"namespace\": \"github:language:ruby\",\n  \"packages\": [\n   \"rest-client\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"GHSA-45x9-q6vj-cqgq\",\n  \"namespace\": \"github:language:java\",\n  \"packages\": [\n   \"org.apache.shiro:shiro-core\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"GHSA-4f63-89w9-3jjv\",\n  \"namespace\": \"github:language:rust\",\n  \"packages\": [\n   \"openssl-src\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"GHSA-599f-7c49-w659\",\n  \"namespace\": \"github:language:java\",\n  \"packages\": [\n   \"org.apache.commons:commons-text\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"GHSA-5c6q-f783-h888\",\n  \"namespace\": \"github:language:java\",\n  \"packages\": [\n   \"com.amazon.redshift:redshift-jdbc42\"\n  ]\n },\n {\n  \"reason\": \"changed\",\n  \"id\": \"GHSA-7v3g-4878-5qrf\",\n  \"namespace\": \"github:language:go\",\n  \"packages\": [\n   \"github.com/hashicorp/nomad\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"GHSA-824x-jcxf-hpfg\",\n  \"namespace\": \"github:language:python\",\n  \"packages\": [\n   \"rdiffweb\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"GHSA-92gf-p376-6r9r\",\n  \"namespace\": \"github:language:python\",\n  \"packages\": [\n   \"rdiffweb\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"GHSA-qj6r-fhrc-jj5r\",\n  \"namespace\": \"github:language:go\",\n  \"packages\": [\n   \"github.com/hyperledger/fabric\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"GHSA-w67g-6gjv-c599\",\n  \"namespace\": \"github:language:python\",\n  \"packages\": [\n   \"powerline-gitstatus\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"GHSA-x4q7-m6fp-4v9v\",\n  \"namespace\": \"github:language:php\",\n  \"packages\": [\n   \"october/system\"\n  ]\n },\n {\n  \"reason\": \"added\",\n  \"id\": \"GHSA-x8x2-wc2h-wc48\",\n  \"namespace\": \"github:language:python\",\n  \"packages\": [\n   \"rdiffweb\"\n  ]\n }\n]\n"
  },
  {
    "path": "test/integration/testdata/vex/csaf/affected.csaf.json",
    "content": "{\n  \"document\": {\n    \"category\": \"csaf_vex\",\n    \"csaf_version\": \"2.0\",\n    \"notes\": [\n      {\n        \"category\": \"summary\",\n        \"text\": \"Example Company VEX document. Unofficial content for demonstration purposes only.\",\n        \"title\": \"Author comment\"\n      }\n    ],\n    \"publisher\": {\n      \"category\": \"vendor\",\n      \"name\": \"Example Company ProductCERT\",\n      \"namespace\": \"https://psirt.example.com\"\n    },\n    \"title\": \"Example VEX Document\",\n    \"tracking\": {\n      \"current_release_date\": \"2024-04-25T11:00:00.000Z\",\n      \"generator\": {\n        \"date\": \"2024-04-25T11:00:00.000Z\",\n        \"engine\": {\n          \"name\": \"Secvisogram\",\n          \"version\": \"1.11.0\"\n        }\n      },\n      \"id\": \"2022-EVD-UC-01-A-001\",\n      \"initial_release_date\": \"2024-04-25T11:00:00.000Z\",\n      \"revision_history\": [\n        {\n          \"date\": \"2024-04-25T11:00:00.000Z\",\n          \"number\": \"1\",\n          \"summary\": \"Initial version.\"\n        }\n      ],\n      \"status\": \"final\",\n      \"version\": \"1\"\n    }\n  },\n  \"product_tree\": {\n    \"branches\": [\n      {\n        \"branches\": [\n          {\n            \"branches\": [\n              {\n                \"category\": \"product_version\",\n                \"name\": \"0.9.9\",\n                \"product\": {\n                  \"name\": \"LibVNCServer 0.9.9\",\n                  \"product_id\": \"CSAFPID-0001\",\n                  \"product_identification_helper\": {\n                    \"purl\": \"pkg:apk/alpine/libvncserver@0.9.9?arch=x86_64&distro=alpine-3.12.0\"\n                  }\n                }\n              }\n            ],\n            \"category\": \"product_name\",\n            \"name\": \"LibVNCServer\"\n          }\n        ],\n        \"category\": \"vendor\",\n        \"name\": \"Example Company\"\n      }\n    ]\n  },\n  \"vulnerabilities\": [\n    {\n      \"cve\": \"CVE-2024-0000\",\n      \"notes\": [\n        {\n          \"category\": \"description\",\n          \"text\": \"A CVE affecting libvncserver.\",\n          \"title\": \"CVE description\"\n        }\n      ],\n      \"product_status\": {\n        \"known_affected\": [\n          \"CSAFPID-0001\"\n        ]\n      },\n      \"remediations\": [\n        {\n          \"category\": \"vendor_fix\",\n          \"details\": \"Customers should update to version 1.1 of product DEF which fixes the issue.\",\n          \"product_ids\": [\n            \"CSAFPID-0001\"\n          ]\n        }\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": "test/integration/testdata/vex/csaf/under_investigation.csaf.json",
    "content": "{\n    \"document\": {\n      \"category\": \"csaf_vex\",\n      \"csaf_version\": \"2.0\",\n      \"notes\": [\n        {\n          \"category\": \"summary\",\n          \"text\": \"Example Company VEX document. Unofficial content for demonstration purposes only.\",\n          \"title\": \"Author comment\"\n        }\n      ],\n      \"publisher\": {\n        \"category\": \"vendor\",\n        \"name\": \"Example Company ProductCERT\",\n        \"namespace\": \"https://psirt.example.com\"\n      },\n      \"title\": \"Example VEX Document\",\n      \"tracking\": {\n        \"current_release_date\": \"2024-04-25T11:00:00.000Z\",\n        \"generator\": {\n          \"date\": \"2024-04-25T11:00:00.000Z\",\n          \"engine\": {\n            \"name\": \"Secvisogram\",\n            \"version\": \"1.11.0\"\n          }\n        },\n        \"id\": \"2022-EVD-UC-01-A-001\",\n        \"initial_release_date\": \"2024-04-25T11:00:00.000Z\",\n        \"revision_history\": [\n          {\n            \"date\": \"2024-04-25T11:00:00.000Z\",\n            \"number\": \"1\",\n            \"summary\": \"Initial version.\"\n          }\n        ],\n        \"status\": \"final\",\n        \"version\": \"1\"\n      }\n    },\n    \"product_tree\": {\n      \"branches\": [\n        {\n          \"branches\": [\n            {\n              \"branches\": [\n                {\n                  \"category\": \"product_version\",\n                  \"name\": \"0.9.9\",\n                  \"product\": {\n                    \"name\": \"LibVNCServer 0.9.9\",\n                    \"product_id\": \"CSAFPID-0001\",\n                    \"product_identification_helper\": {\n                        \"purl\": \"pkg:apk/alpine/libvncserver@0.9.9?arch=x86_64&distro=alpine-3.12.0\"\n                    }\n                  }\n                }\n              ],\n              \"category\": \"product_name\",\n              \"name\": \"LibVNCServer\"\n            }\n          ],\n          \"category\": \"vendor\",\n          \"name\": \"Example Company\"\n        }\n      ]\n    },\n    \"vulnerabilities\": [\n      {\n        \"cve\": \"CVE-2024-0000\",\n        \"notes\": [\n          {\n            \"category\": \"description\",\n            \"text\": \"A CVE affecting libvncserver.\",\n            \"title\": \"CVE description\"\n          }\n        ],\n        \"product_status\": {\n          \"under_investigation\": [\n            \"CSAFPID-0001\"\n          ]\n        }\n      }\n    ]\n  }\n"
  },
  {
    "path": "test/integration/testdata/vex/openvex/affected.openvex.json",
    "content": "{\n  \"@context\": \"https://openvex.dev/ns/v0.2.0\",\n  \"@id\": \"https://openvex.dev/docs/public/vex-d4e9020b6d0d26f131d535e055902dd6ccf3e2088bce3079a8cd3588a4b14c78\",\n  \"author\": \"The OpenVEX Project <openvex@openssf.org>\",\n  \"timestamp\": \"2023-07-17T18:28:47.696004345-06:00\",\n  \"version\": 1,\n  \"statements\": [\n    {\n      \"vulnerability\": {\n        \"name\": \"CVE-2024-0000\"\n      },\n      \"products\": [\n        {\n          \"@id\": \"pkg:oci/alpine@sha256%3Affffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\",\n          \"subcomponents\": [\n            { \"@id\": \"pkg:apk/alpine/libvncserver@0.9.9?arch=x86_64&distro=alpine-3.12.0\" }\n          ]\n        }\n      ],\n      \"status\": \"affected\"\n    }\n  ]\n}\n"
  },
  {
    "path": "test/integration/testdata/vex/openvex/under_investigation.openvex.json",
    "content": "{\n  \"@context\": \"https://openvex.dev/ns/v0.2.0\",\n  \"@id\": \"https://openvex.dev/docs/public/vex-d4e9020b6d0d26f131d535e055902dd6ccf3e2088bce3079a8cd3588a4b14c78\",\n  \"author\": \"The OpenVEX Project <openvex@openssf.org>\",\n  \"timestamp\": \"2023-07-17T18:28:47.696004345-06:00\",\n  \"version\": 1,\n  \"statements\": [\n    {\n      \"timestamp\": \"2023-07-16T18:28:47.696004345-06:00\",\n      \"vulnerability\": {\n        \"name\": \"CVE-2024-0000\"\n      },\n      \"products\": [\n        {\n          \"@id\": \"pkg:oci/alpine@sha256%3Affffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\",\n          \"subcomponents\": [\n            { \"@id\": \"pkg:apk/alpine/libvncserver@0.9.9?arch=x86_64&distro=alpine-3.12.0\" }\n          ]\n        }\n      ],\n      \"status\": \"under_investigation\"\n    }\n  ]\n}\n"
  },
  {
    "path": "test/integration/utils_test.go",
    "content": "package integration\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/scylladb/go-set/strset\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/anchore/grype/grype/match\"\n\t\"github.com/anchore/syft/syft\"\n\t\"github.com/anchore/syft/syft/sbom\"\n\t\"github.com/anchore/syft/syft/source\"\n)\n\nconst cacheDirRelativePath string = \"./testdata/cache\"\n\nfunc PullThroughImageCache(t testing.TB, imageName string) string {\n\tcacheDirectory, absErr := filepath.Abs(cacheDirRelativePath)\n\tif absErr != nil {\n\t\tt.Fatalf(\"could not get absolute path of cache directory %s; %v\", cacheDirRelativePath, absErr)\n\t}\n\n\tmkdirError := os.MkdirAll(cacheDirectory, 0755)\n\tif mkdirError != nil {\n\t\tt.Fatalf(\"could not create cache directory %s; %v\", cacheDirRelativePath, absErr)\n\t}\n\n\tre := regexp.MustCompile(\"[/:]\")\n\tarchiveFileName := fmt.Sprintf(\"%s.tar\", re.ReplaceAllString(imageName, \"-\"))\n\timageArchivePath := filepath.Join(cacheDirectory, archiveFileName)\n\n\tif _, err := os.Stat(imageArchivePath); os.IsNotExist(err) {\n\t\tt.Logf(\"Cache miss for image %s; copying to archive at %s\", imageName, imageArchivePath)\n\t\tsaveImage(t, imageName, imageArchivePath)\n\t}\n\n\treturn imageArchivePath\n}\n\nfunc saveImage(t testing.TB, imageName string, destPath string) {\n\tsourceImage := fmt.Sprintf(\"docker://docker.io/%s\", imageName)\n\tdestinationString := fmt.Sprintf(\"docker-archive:%s\", destPath)\n\tskopeoPath := filepath.Join(repoRoot(t), \".tool\", \"skopeo\")\n\tpolicyPath := filepath.Join(repoRoot(t), \"test\", \"integration\", \"testdata\", \"skopeo-policy.json\")\n\n\tskopeoCommand := []string{\n\t\t\"--policy\", policyPath,\n\t\t\"copy\", \"--override-os\", \"linux\", sourceImage, destinationString,\n\t}\n\n\tcmd := exec.Command(skopeoPath, skopeoCommand...)\n\n\tout, err := cmd.Output()\n\tif err != nil {\n\t\tvar exitError *exec.ExitError\n\t\tif errors.As(err, &exitError) {\n\t\t\tt.Logf(\"Stderr: %s\", exitError.Stderr)\n\t\t}\n\t\tt.Fatal(err)\n\t}\n\n\tt.Logf(\"Stdout: %s\\n\", out)\n}\n\nfunc getSyftSBOM(t testing.TB, image, from string, encoder sbom.FormatEncoder) string {\n\tsrc, err := syft.GetSource(context.Background(), image, syft.DefaultGetSourceConfig().WithSources(from))\n\trequire.NoError(t, err)\n\n\tt.Cleanup(func() {\n\t\trequire.NoError(t, src.Close())\n\t})\n\n\tconfig := syft.DefaultCreateSBOMConfig()\n\n\tconfig.Search.Scope = source.SquashedScope\n\t// TODO: relationships are not verified at this time\n\ts, err := syft.CreateSBOM(context.Background(), src, config)\n\trequire.NoError(t, err)\n\trequire.NotNil(t, s)\n\n\tvar buf bytes.Buffer\n\n\terr = encoder.Encode(&buf, *s)\n\trequire.NoError(t, err)\n\n\treturn buf.String()\n}\n\nfunc getMatchSet(matches match.Matches) *strset.Set {\n\ts := strset.New()\n\tfor _, m := range matches.Sorted() {\n\t\ts.Add(fmt.Sprintf(\"%s-%s-%s\", m.Vulnerability.ID, m.Package.Name, m.Package.Version))\n\t}\n\treturn s\n}\n\nfunc repoRoot(tb testing.TB) string {\n\ttb.Helper()\n\troot, err := exec.Command(\"git\", \"rev-parse\", \"--show-toplevel\").Output()\n\tif err != nil {\n\t\ttb.Fatalf(\"unable to find repo root dir: %+v\", err)\n\t}\n\tabsRepoRoot, err := filepath.Abs(strings.TrimSpace(string(root)))\n\tif err != nil {\n\t\ttb.Fatal(\"unable to get abs path to repo root:\", err)\n\t}\n\treturn absRepoRoot\n}\n"
  },
  {
    "path": "test/quality/.gitignore",
    "content": "venv\n.yardstick/tools\n.yardstick/result\nstage\npull\nmigrate.py\n.oras-cache\n*.tar.gz\n*.tar.zst"
  },
  {
    "path": "test/quality/.grype.yaml",
    "content": "by-cve: true\n\n# we want to be able to validate CPE findings with the broadest lens possible (not just the default configuration)\n# to aid in validating CPE related changes (whereas the default configuration is more focused on non CPE matching)\nmatch:\n  java:\n    using-cpes: true\n  jvm:\n    using-cpes: true\n  dotnet:\n    using-cpes: true\n  golang:\n    using-cpes: true\n    always-use-cpe-for-stdlib: true\n  javascript:\n    using-cpes: true\n  python:\n    using-cpes: true\n  ruby:\n    using-cpes: true\n  rust:\n    using-cpes: true\n  stock:\n    using-cpes: true\n\n"
  },
  {
    "path": "test/quality/.python-version",
    "content": "3.10.7\n"
  },
  {
    "path": "test/quality/.yardstick.yaml",
    "content": "x-ref:\n  # note: always reference images with BOTH a tag and a digest\n  images: &images\n    - docker.io/cloudbees/cloudbees-core-agent:2.289.2.2@sha256:d48f0546b4cf5ef4626136242ce302f94a42751156b7be42f4b1b75a66608880\n    - docker.io/cloudbees/cloudbees-core-mm:2.277.3.1@sha256:4c564f473d38f23da1caa48c4ef53b958ef03d279232007ad3319b1f38584bdb\n    - docker.io/cloudbees/cloudbees-core-oc:2.289.2.2@sha256:9cd85ee84e401dc27e3a8268aae67b594a651b2f4c7fc056ca14c7b0a0a6b82d\n    - docker.io/anchore/test_images:grype-quality-dotnet-69f15d2@sha256:e25a9a175433c2bfe9c04e6482e6c5eca0491629144c78061763f7f604fdea80\n    - docker.io/anchore/test_images:grype-quality-node-d89207b@sha256:f56164678054e5eb59ab838367373a49df723b324617b1ba6de775749d7f91d4\n    - docker.io/anchore/test_images:grype-quality-python-d89207b@sha256:b2b58a55c0b03c1626d2aaae2add9832208b02124dda7b7b41811e14f0fb272c\n    - docker.io/anchore/test_images:grype-quality-java-d89207b@sha256:b3534fc2e37943136d5b54e3a58b55d4ccd4363d926cf7aa5bf55a524cf8275b\n    - docker.io/anchore/test_images:grype-quality-golang-d89207b@sha256:7536ee345532f674ec9e448e3768db4e546c48220ba2b6ec9bc9cfbfb3b7b74a\n    - docker.io/anchore/test_images:grype-quality-ruby-d89207b@sha256:1a5a5f870924e88a6f0f2b8089cf276ef0a79b5244a052cdfe4a47bb9e5a2c10\n    - docker.io/anchore/test_images:vulnerabilities-package-name-normalization@sha256:92f1981518e92bf3712ff95cf342f7f4d5fc83fb93a30a36d7d1204e64342199\n    - docker.io/anchore/test_images:appstreams-centos-stream-8-1a287dd@sha256:808f6cf3cf4473eb39ff9bb47ead639d2ed71255b75b9b140162b58c6102bcc9\n    - docker.io/anchore/test_images:java-56d52bc@sha256:10008791acbc5866de04108746a02a0c4029ce3a4400a9b3dad45d7f2245f9da\n    - docker.io/anchore/test_images:npm-56d52bc@sha256:ba42ded8613fc643d407a050faf5ab48cfb405ad3ef2015bf6feeb5dff44738d\n    - docker.io/anchore/test_images:gems-56d52bc@sha256:5763c8a225f950961bf01ddec68e36f18e236130e182f2b9290a6e03b9777bfe\n    - docker.io/anchore/test_images:golang-56d52bc@sha256:d1819e59e89e8ea90073460acb4ebb2ee18ccead9fa880dae91e8fc61b19ca1c\n    - docker.io/anchore/test_images:ubuntu-content-56d52bc@sha256:f8e72da9f67caf90714926e7b21f0da93ca1e528b37a97dffe71e2ec38872a8b\n    - docker.io/anchore/test_images:vulnerabilities-alpine-3.11-d5be50d@sha256:01c78cee3fe398bf1f77566177770b07f1d2af01753c2434cb0735bd43a078b6\n    - docker.io/anchore/test_images:vulnerabilities-alpine-3.12-d5be50d@sha256:55c9ba4e24e15c0467a071d93fead0990b8f04bb60b359b4056a997598aa56a1\n    - docker.io/anchore/test_images:vulnerabilities-alpine-3.13-d5be50d@sha256:6749b1509fc4dd3f2b4e8688325fc5d447751bc9ae3be10c0f1fb92ec062b798\n    - docker.io/anchore/test_images:vulnerabilities-alpine-3.14-d5be50d@sha256:fe242a3a63699425317fba0a749253bceb700fb3d63e7a0f6497f53a587e38c5\n    - docker.io/anchore/test_images:vulnerabilities-alpine-3.15-d5be50d@sha256:7790691e5efae8bfe9cf4a4447312318d8daaf05ffd5f265ae913edf660f4653\n    - docker.io/anchore/test_images:vulnerabilities-alpine-3.6-d5be50d@sha256:58637f273108e3e9eb4df4d73f7b6b1da303cbbf64f65e65fb7762482f2de63d\n    - docker.io/anchore/test_images:vulnerabilities-alpine-3.8-d5be50d@sha256:a287a0ff98ac343aa710f4f4258d7198e240e9d416d5c7274663564202f832fb\n    - docker.io/anchore/test_images:vulnerabilities-amazonlinux-2-5c26ce9@sha256:cf742eca189b02902a0a7926ac3fbb423e799937bf4358b0d2acc6cc36ab82aa\n    - docker.io/anchore/test_images:vulnerabilities-centos@sha256:746d31247006cc06434ce91ccf3523b2c230ff6c378ffed7ca1c60bbb48ea86f\n    - docker.io/anchore/test_images:vulnerabilities-no-distro-6bde59e@sha256:347fba6fbfa15d4e11217f9d49bf70a5a6eef35c6c642dc8c5db89115912d0c1\n    - docker.io/anchore/test_images:syft_bin-cf22714@sha256:c27b02c6322180fd8a7a3097d2b430bfdf9ea52ecf136edf258458e82f2c6f21\n    - docker.io/anchore/test_images:alpine-package-cpe-vuln-match-bd0aaef@sha256:0825acea611c7c5cc792bc7cc20de44d7413fd287dc5afc4aab9c1891d037b4f\n    - docker.io/alpine:3.2@sha256:ddac200f3ebc9902fb8cfcd599f41feb2151f1118929da21bcef57dc276975f9\n    - docker.io/centos:6@sha256:3688aa867eb84332460e172b9250c9c198fdfd8d987605fd53f246f498c60bcf\n    - docker.io/almalinux:8@sha256:cd49d7250ed7bb194d502d8a3e50bd775055ca275d1d9c2785aea72b890afe6a\n    - docker.io/rockylinux:8@sha256:72afc2e1a20c9ddf56a81c51148ebcbe927c0a879849efe813bee77d69df1dd8\n    - docker.io/oraclelinux:6@sha256:a06327c0f1d18d753f2a60bb17864c84a850bb6dcbcf5946dd1a8123f6e75495\n    - docker.io/debian:7@sha256:81e88820a7759038ffa61cff59dfcc12d3772c3a2e75b7cfe963c952da2ad264\n    - docker.io/busybox:1.28.1@sha256:2107a35b58593c58ec5f4e8f2c4a70d195321078aebfadfbfb223a2ff4a4ed21\n    - docker.io/amazonlinux:2@sha256:1301cc9f889f21dc45733df9e58034ac1c318202b4b0f0a08d88b3fdc03004de\n    - registry.access.redhat.com/ubi8@sha256:68fecea0d255ee253acbf0c860eaebb7017ef5ef007c25bee9eeffd29ce85b29\n    - docker.io/python:3.8.0-slim@sha256:5e96e03a493a54904aa8be573fc0414431afb4f47ac58fbffd03b2a725005364\n    - docker.io/ghost:5.2.4@sha256:42137b9bd1faf4cdea5933279c48a912d010ef614551aeb0e44308600aa3e69f\n# commented out lines in this list are Docker v1 images, which no longer work after docker daemon dropped support\n#    - #docker.io/node:4.2.1-slim@sha256:af31633b87d0dc58c306b04ad9f6ca88104626363c5c085e9962832628eb09ce\n    - docker.io/elastic/kibana:8.5.0@sha256:b9e3e52f61e0a347e38eabe80ba0859f859023bc0cc8836410320aa7eb5d3e02\n    - docker.io/jenkins/jenkins:2.361.4-lts-jdk11@sha256:6fd5699ab182b5d23d0e3936de6047edc30955a3a92e01c392d5a2fd583efac0\n    - docker.io/neo4j:4.4.14-community@sha256:fcfcbb026e0e538bf66f5fe5c4b2db3dd4931c3aae07f13a5a8c10e979596256\n    - docker.io/sonatype/nexus3:3.30.0@sha256:e8fea6b4279f2b5b24b36170459cb7aa3d6afe999f9d3e3713541be28bae8ec4\n    - cgr.dev/chainguard/wolfi-base:latest-20221001@sha256:be3834598c3c4b76ace6a866edcbbe1fa18086f9ee238b57769e4d230cd7d507\n    - docker.io/gitlab/gitlab-ce:15.6.1-ce.0@sha256:04d4219d5dfb3acccc9997e50477c8d24b371387a95857e1ea8fc779e17a716c\n    - docker.io/postgres:13.2@sha256:1a67ab960138c479d66834cd6bcb5b5582c53869e6052dbf4ff48d4a94c13da3\n    - ghcr.io/chainguard-images/scanner-test@sha256:59bddc101fba0c45d5c093575c6bc5bfee7f0e46ff127e6bb4e5acaaafb525f9\n    - docker.io/keycloak/keycloak:21.0.2@sha256:347a0d748d05a050dc64b92de2246d2240db6eb38afbc17c3c08d0acb0db1b50\n    - docker.io/datawire/aes:3.6.0@sha256:86a072278135462b6cbef70e89894df8f9b20f428b361fda2132fbb442ef257b\n    - ghcr.io/anchore/test-images/bitnami/spark:3.2.4-debian-11-r8@sha256:267d5a6345636710b4b57b7fe981c9760203e7e092c705416310ea30a9806d74\n    - docker.io/grafana/grafana:9.2.4@sha256:a11c6829cdfe7fd791e48ba5b511f3562384361fb4c568ec2d8a5041ac52babe\n    - docker.io/hashicorp/vault:1.12.0@sha256:09354ca0891f7cee8fbfe8db08c62d2d757fad8ae6c91f2b6cce7a34440e3fae\n    - docker.io/ubuntu:12.04@sha256:18305429afa14ea462f810146ba44d4363ae76e4c8dfc38288cf73aa07485005\n#    - #docker.io/ubuntu:12.10@sha256:002fba3e3255af10be97ea26e476692a7ebed0bb074a9ab960b2e7a1526b15d7\n#    - #docker.io/ubuntu:13.04@sha256:bc48dd7075ce920ebbaa4581d3200e9fb3aaec31591061d7e3a280a04ef0248c\n    - docker.io/ubuntu:14.04@sha256:881afbae521c910f764f7187dbfbca3cc10c26f8bafa458c76dda009a901c29d\n#    - #docker.io/ubuntu:14.10@sha256:6341c688b4b0b82ec735389b3c97df8cf2831b8cb8bd1856779130a86574ac5c\n#    - #docker.io/ubuntu:15.04@sha256:2fb27e433b3ecccea2a14e794875b086711f5d49953ef173d8a03e8707f1510f\n    - docker.io/ubuntu:15.10@sha256:02521a2d079595241c6793b2044f02eecf294034f31d6e235ac4b2b54ffc41f3\n    - docker.io/ubuntu:16.10@sha256:8dc9652808dc091400d7d5983949043a9f9c7132b15c14814275d25f94bca18a\n    - docker.io/ubuntu:17.04@sha256:213e05583a7cb8756a3f998e6dd65204ddb6b4c128e2175dcdf174cdf1877459\n    - docker.io/ubuntu:17.10@sha256:9c4bf7dbb981591d4a1169138471afe4bf5ff5418841d00e30a7ba372e38d6c1\n    - docker.io/ubuntu:18.04@sha256:971a12d7e92a23183dead8bfc415aa650e7deb1cc5fed11a3d21f759a891fde9\n    - docker.io/ubuntu:18.10@sha256:c95b7b93ccd48c3bfd97f8cac6d5ca8053ced584c9e8e6431861ca30b0d73114\n    - docker.io/ubuntu:19.04@sha256:3db17bfc30b41cc18552578f4a66d7010050eb9fdc42bf6c3d82bb0dcdf88d58\n    - docker.io/ubuntu:19.10@sha256:6852f9e05c5bce8aa77173fa83ce611f69f271ee3a16503c5f80c199969fd1eb\n    - docker.io/ubuntu:20.04@sha256:9d42d0e3e57bc067d10a75ee33bdd1a5298e95e5fc3c5d1fce98b455cb879249\n    - docker.io/ubuntu:20.10@sha256:754eb641a1ba98a8b483c3595a14164fa4ed7f4b457e1aa05f13ce06f8151723\n    - docker.io/ubuntu:21.04@sha256:cb92f03e258f965442b883f5402b310dd7a5ea0a661a865ad02a42bc21234bf7\n    - docker.io/ubuntu:21.10@sha256:253908b2844746ab3f3a08fc8a44b9b9fc1efc408d5969b093ab9ffa11eb1894\n    - docker.io/ubuntu:22.04@sha256:aa6c2c047467afc828e77e306041b7fa4a65734fe3449a54aa9c280822b0d87d\n    - docker.io/ubuntu:22.10@sha256:80fb4ea0c0a384a3072a6be1879c342bb636b0d105209535ba893ba75ab38ede\n    - docker.io/ubuntu:23.04@sha256:09f035f46361d193ded647342903b413d57d05cc06acff8285f9dda9f2d269d5\n    - gcr.io/distroless/python3-debian11@sha256:69ae7f133d33faab720af28e78fb45707b623bcbc94ae02a07c633bf053f4b40\n    - registry.suse.com/suse/sles12sp4:26.380@sha256:94b537f5b312e7397b5d0bbb3d892f961acdd9454950fc233d77f771e25335fb\n    - registry.suse.com/suse/sle15:15.1.6.2.461@sha256:6e613c994c3b33224e439ef8ee9003fb69416f77f7a6b1da0b18981d5aa3bb75\n\n# new vulnerabilities are added all of the time, instead of keeping up it's easier to ignore newer entries.\n# This approach helps tremendously with keeping the analysis relatively stable.\ndefault_max_year: 2021\n\nresult-sets:\n  pr_vs_latest_via_sbom:\n    description: \"latest released grype vs grype from the current build (via SBOM ingestion)\"\n    validations:\n      - max-f1-regression: 0.0\n        max-new-false-negatives: 00\n        max-unlabeled-percent: 10\n        max_year: 2021\n    matrix:\n      images: *images\n\n      tools:\n\n        - name: syft\n          # note: we want to use a fixed version of syft for capturing all results (NOT \"latest\")\n          version: v1.14.0\n          produces: SBOM\n          refresh: false\n\n        - name: grype\n          # note: we import a static (pinned) DB as to prevent changes in the DB from affecting the results. The\n          # point of this test is to ensure the correctness of the logic in grype itself with real production data.\n          # By pinning the DB the grype code itself becomes the independent variable under test (and not the\n          # every-changing DB). That being said, we should be updating this DB periodically to ensure what we\n          # are testing with is not too stale.\n          # version: git:current-commit+import-db=db.tar.zst\n          # for local build of grype, use for example:\n          version: path:../../+import-db=db.tar.zst\n          takes: SBOM\n          label: candidate\n\n        - name: grype\n          # note: we import a static (pinned) DB as to prevent changes in the DB from affecting the results. The\n          # point of this test is to ensure the correctness of the logic in grype itself with real production data.\n          # By pinning the DB the grype code itself becomes the independent variable under test (and not the\n          # every-changing DB). That being said, we should be updating this DB periodically to ensure what we\n          # are testing with is not too stale.\n          version: latest+import-db=db.tar.zst\n          takes: SBOM\n          label: reference\n  pr_vs_latest_via_sbom_2022:\n    description: \"same as 'pr_vs_latest_via_sbom', but includes vulnerabilities from 2022 and before, instead of 2021 and before\"\n    max_year: 2022\n    validations:\n      - max-f1-regression: 0.0\n        max-new-false-negatives: 00\n        max-unlabeled-percent: 10\n        max_year: 2022\n        fail_on_empty_match_set: false\n    matrix:\n      images:\n        - mcr.microsoft.com/cbl-mariner/base/core:2.0.20220731-arm64@sha256:51101e635f56032d5afd3fb56d66c7b93b34d5a39ddac01695d62b94473cc34e\n        - docker.io/anchore/test_images:azurelinux3-63671fe@sha256:2d761ba36575ddd4e07d446f4f2a05448298c20e5bdcd3dedfbbc00f9865240d\n        - docker.io/anchore/test_images:appstreams-oraclelinux-8-1a287dd@sha256:c8d664b0e728d52f57eeb98ed1899c16d3b265f02ddfb41303d7a16c31e0b0f1\n        - docker.io/anchore/test_images:appstreams-rhel-8-1a287dd@sha256:524ff8a75f21fd886ec7ed82387766df386671e8b77e898d05786118d5b7880b\n      tools:\n        - name: syft\n          # note: we want to use a fixed version of syft for capturing all results (NOT \"latest\")\n          version: v1.14.0\n          produces: SBOM\n          refresh: false\n\n        - name: grype\n          # note: we import a static (pinned) DB as to prevent changes in the DB from affecting the results. The\n          # point of this test is to ensure the correctness of the logic in grype itself with real production data.\n          # By pinning the DB the grype code itself becomes the independent variable under test (and not the\n          # every-changing DB). That being said, we should be updating this DB periodically to ensure what we\n          # are testing with is not too stale.\n          # version: git:current-commit+import-db=db.tar.zst\n          # for local build of grype, use for example:\n          version: path:../../+import-db=db.tar.zst\n          takes: SBOM\n          label: candidate # is candidate better than the current baseline?\n\n        - name: grype\n          # note: we import a static (pinned) DB as to prevent changes in the DB from affecting the results. The\n          # point of this test is to ensure the correctness of the logic in grype itself with real production data.\n          # By pinning the DB the grype code itself becomes the independent variable under test (and not the\n          # every-changing DB). That being said, we should be updating this DB periodically to ensure what we\n          # are testing with is not too stale.\n          version: latest+import-db=db.tar.zst\n          takes: SBOM\n          label: reference # this run is the current baseline\n\n  pr_vs_latest_via_sbom_2023:\n    description: \"same as 'pr_vs_latest_via_sbom', but includes vulnerabilities from 2023 and before, instead of 2021 and before\"\n    max_year: 2023\n    validations:\n      - max-f1-regression: 0.0\n        max-new-false-negatives: 00\n        max-unlabeled-percent: 10\n        max_year: 2023\n        fail_on_empty_match_set: false\n    matrix:\n      images:\n        - docker.io/anchore/test_images:archlinux-28cca4e@sha256:a933b27534e5c911e2c660f7090aa497dee763fbbcb214a37207c2320cfedd98\n        - docker.io/anchore/test_images:almalinux8-271722c@sha256:6485db654df0452bd15ea71ec43e808bc8eb05b91f1c2754669a5573479a6c19\n      tools:\n        - name: syft\n          # note: we want to use a fixed version of syft for capturing all results (NOT \"latest\")\n          version: v1.14.0\n          produces: SBOM\n          refresh: false\n\n        - name: grype\n          # note: we import a static (pinned) DB as to prevent changes in the DB from affecting the results. The\n          # point of this test is to ensure the correctness of the logic in grype itself with real production data.\n          # By pinning the DB the grype code itself becomes the independent variable under test (and not the\n          # every-changing DB). That being said, we should be updating this DB periodically to ensure what we\n          # are testing with is not too stale.\n          # version: git:current-commit+import-db=db.tar.zst\n          # for local build of grype, use for example:\n          version: path:../../+import-db=db.tar.zst\n          takes: SBOM\n          label: candidate # is candidate better than the current baseline?\n\n        - name: grype\n          # note: we import a static (pinned) DB as to prevent changes in the DB from affecting the results. The\n          # point of this test is to ensure the correctness of the logic in grype itself with real production data.\n          # By pinning the DB the grype code itself becomes the independent variable under test (and not the\n          # every-changing DB). That being said, we should be updating this DB periodically to ensure what we\n          # are testing with is not too stale.\n          version: latest+import-db=db.tar.zst\n          takes: SBOM\n          label: reference # this run is the current baseline\n\n  pr_vs_latest_via_sbom_2024:\n    description: \"same as 'pr_vs_latest_via_sbom', but includes vulnerabilities from 2024 and before, instead of 2021 and before\"\n    max_year: 2024\n    validations:\n      - max-f1-regression: 0.0\n        max-new-false-negatives: 00\n        max-unlabeled-percent: 10\n        max_year: 2024\n        fail_on_empty_match_set: false\n    matrix:\n      images:\n        - ghcr.io/anchore/test-images/bitnami/redis:7.4.0@sha256:4bad45268adfdbb0b456d6bf74ded449ef79f3706cb4e473516a0a5b393968c0\n        # echo\n        - ghcr.io/buildecho/scanner-test:latest@sha256:60557350ad6976dad3b88d891de8f090b20b3271c660272d30d44b5d07b23edc\n        # minimos\n        - docker.io/dimastopelmini/forgrype:3.1.6@sha256:ebe0c6ca122deef072c29be2f915130e5c8b4c277ad5ef551385f6496dae4dfa\n        - docker.io/dimastopelmini/forgrype:3.1.7@sha256:653c8980c63a9ac403a3b9f56a08f43f929432ece69894423c165b4d61d3dcdb\n        # postmarketos\n        - ghcr.io/anchore/test-images/postmarketos:edge@sha256:2bdab220693cecfe3474055076bcbfe9ec8faf466867a5db3e0b76afaa9f4b89\n        - ghcr.io/anchore/test-images/postmarketos:24.06@sha256:05b42fdb332f8a5794c9d1e6ab83cd32030bf0cd3ef797ada5546419e9ad293d\n        # Anchore test images\n        - docker.io/anchore/test_images:appstreams-nodejs-18-rhel-9-1b0b1b4@sha256:08dbfad2d6af9afe47f7647b0b8f38fd29fc9e89306cfc39c9509981f9388b7f\n        - docker.io/anchore/test_images:appstreams-nodejs-base-rhel-9-1b0b1b4@sha256:fc6f7a37d7e320f6ff3643d4ec9a208adb1462cd16027f045b56563e12bb0461\n      tools:\n        - name: syft\n          # note: we want to use a fixed version of syft for capturing all results (NOT \"latest\")\n          version: v1.14.0\n          produces: SBOM\n          refresh: false\n\n        - name: grype\n          # note: we import a static (pinned) DB as to prevent changes in the DB from affecting the results. The\n          # point of this test is to ensure the correctness of the logic in grype itself with real production data.\n          # By pinning the DB the grype code itself becomes the independent variable under test (and not the\n          # every-changing DB). That being said, we should be updating this DB periodically to ensure what we\n          # are testing with is not too stale.\n          # version: git:current-commit+import-db=db.tar.zst\n          # for local build of grype, use for example:\n          version: path:../../+import-db=db.tar.zst\n          takes: SBOM\n          label: candidate # is candidate better than the current baseline?\n\n        - name: grype\n          # note: we import a static (pinned) DB as to prevent changes in the DB from affecting the results. The\n          # point of this test is to ensure the correctness of the logic in grype itself with real production data.\n          # By pinning the DB the grype code itself becomes the independent variable under test (and not the\n          # every-changing DB). That being said, we should be updating this DB periodically to ensure what we\n          # are testing with is not too stale.\n          version: latest+import-db=db.tar.zst\n          takes: SBOM\n          label: reference # this run is the current baseline\n\n  pr_vs_latest_via_sbom_2025:\n    description: \"same as 'pr_vs_latest_via_sbom', but includes vulnerabilities from 2025 and before, instead of 2021 and before\"\n    max_year: 2025\n    validations:\n      - max-f1-regression: 0.0\n        max-new-false-negatives: 00\n        max-unlabeled-percent: 10\n        max_year: 2025\n        fail_on_empty_match_set: false\n    matrix:\n      images:\n        - ghcr.io/chainguard-images/scanner-test:python-library-aiohttp-chainguard@sha256:046b2c1b2df1e496eb3abf9e21224ffd879d92abc2bd57401456764d9aa8ddf1\n        # secureos\n        - registry.replicated.com/library/grype-test:20250106@sha256:3339bcd874d21fa3ca5bd20636e793c0c33bd71ace3a18a9a3b3d147b91dd000\n\n      tools:\n        - name: syft\n          # note: we want to use a fixed version of syft for capturing all results (NOT \"latest\")\n          version: v1.14.0\n          produces: SBOM\n          refresh: false\n\n        - name: grype\n          # note: we import a static (pinned) DB as to prevent changes in the DB from affecting the results. The\n          # point of this test is to ensure the correctness of the logic in grype itself with real production data.\n          # By pinning the DB the grype code itself becomes the independent variable under test (and not the\n          # every-changing DB). That being said, we should be updating this DB periodically to ensure what we\n          # are testing with is not too stale.\n          # version: git:current-commit+import-db=db.tar.zst\n          # for local build of grype, use for example:\n          version: path:../../+import-db=db.tar.zst\n          takes: SBOM\n          label: candidate # is candidate better than the current baseline?\n\n        - name: grype\n          # note: we import a static (pinned) DB as to prevent changes in the DB from affecting the results. The\n          # point of this test is to ensure the correctness of the logic in grype itself with real production data.\n          # By pinning the DB the grype code itself becomes the independent variable under test (and not the\n          # every-changing DB). That being said, we should be updating this DB periodically to ensure what we\n          # are testing with is not too stale.\n          version: latest+import-db=db.tar.zst\n          takes: SBOM\n          label: reference # this run is the current baseline\n"
  },
  {
    "path": "test/quality/Makefile",
    "content": "SBOM_STORE_TAG = md5-$(shell md5sum .yardstick.yaml | cut -d' ' -f1)\nSBOM_STORE_IMAGE = ghcr.io/anchore/grype/quality-test-sbom-store:$(SBOM_STORE_TAG)\nACTIVATE_VENV = . venv/bin/activate &&\nYARDSTICK = $(ACTIVATE_VENV) yardstick -v\nYARDSTICK_RESULT_DIR = .yardstick/result\nYARDSTICK_LABELS_DIR = .yardstick/labels\nVULNERABILITY_LABELS = ./vulnerability-labels\nRESULT_SET = pr_vs_latest_via_sbom\n\n# update periodically with values from \"grype db list\"\nTEST_DB_URL_FILE = ./test-db\nTEST_DB_URL = \"$(shell cat $(TEST_DB_URL_FILE))\"\nTEST_DB = db.tar.zst\n\n# formatting variables\nBOLD := $(shell tput -T linux bold)\nPURPLE := $(shell tput -T linux setaf 5)\nGREEN := $(shell tput -T linux setaf 2)\nCYAN := $(shell tput -T linux setaf 6)\nRED := $(shell tput -T linux setaf 1)\nRESET := $(shell tput -T linux sgr0)\nTITLE := $(BOLD)$(PURPLE)\nSUCCESS := $(BOLD)$(GREEN)\n\n.PHONY: all\nall: capture validate ## Fetch or capture all data and run all quality checks\n\n.PHONY: validate\nvalidate: venv $(VULNERABILITY_LABELS)/Makefile ## Run all quality checks against already collected data\n\t$(YARDSTICK) validate -r $(RESULT_SET) -r $(RESULT_SET)_2022 -r $(RESULT_SET)_2024\n\n.PHONY: capture\ncapture: sboms vulns ## Collect and store all syft and grype results\n\n.PHONY: vulns\nvulns: venv $(TEST_DB) ## Collect and store all grype results\n\t$(YARDSTICK) -v result capture -r $(RESULT_SET)\n\t$(YARDSTICK) -v result capture -r $(RESULT_SET)_2022\n\t$(YARDSTICK) -v result capture -r $(RESULT_SET)_2024\n\n$(TEST_DB):\n\t@curl -o $(TEST_DB) -SsL $(TEST_DB_URL)\n\n.PHONY: sboms\nsboms: $(YARDSTICK_RESULT_DIR) venv clear-results ## Collect and store all syft results (deletes all existing results)\n\tbash -c \"make download-sboms || ($(YARDSTICK) -v result capture -r $(RESULT_SET) --only-producers && $(YARDSTICK) -v result capture -r $(RESULT_SET)_2022 -r $(RESULT_SET)_2024 --only-producers)\"\n\n.PHONY: download-sboms\ndownload-sboms: $(VULNERABILITY_LABELS)/Makefile\n\tcd vulnerability-match-labels && make venv\n\tbash -c \"export ORAS_CACHE=$(shell pwd)/.oras-cache && make venv && . vulnerability-match-labels/venv/bin/activate && ./vulnerability-match-labels/sboms.py download -r $(RESULT_SET) && ./vulnerability-match-labels/sboms.py download -r $(RESULT_SET)_2022 && ./vulnerability-match-labels/sboms.py download -r $(RESULT_SET)_2024\"\n\nvenv: venv/touchfile\n\nvenv/touchfile: requirements.txt\n\ttest -d venv || python3 -m venv venv\n\t$(ACTIVATE_VENV) pip install -Ur requirements.txt\n\ttouch venv/touchfile\n\n$(YARDSTICK_RESULT_DIR):\n\tmkdir -p $(YARDSTICK_RESULT_DIR)\n\n$(VULNERABILITY_LABELS)/Makefile:\n\tgit submodule update vulnerability-match-labels\n\n.PHONY: clear-results\nclear-results: venv ## Clear all existing yardstick results\n\t$(YARDSTICK) result clear\n\n.PHONY: clean\nclean: clear-results ## Clear all existing yardstick results and delete python environment\n\trm -rf venv\n\tfind -iname \"*.pyc\" -delete\n\nhelp:\n\t@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = \":.*?## \"}; {printf \"$(BOLD)$(CYAN)%-25s$(RESET)%s\\n\", $$1, $$2}'\n\n"
  },
  {
    "path": "test/quality/README.md",
    "content": "# Match quality testing\n\nThis form of testing compares the results from various releases of grype using a\nstatic set of reference container images. The kinds of comparisons made are:\n\n1) \"relative\": find the vulnerability matching differences between both tools\n   for a given image. This helps identify when a change has occurred in matching\n   behavior and where the changes are.\n\n2) \"against labels\": pair each tool results for an image with ground truth. This\n   helps identify how well the matching behavior is performing (did it get\n   better or worse).\n\n\n## Getting started\n\nFor information about required setup see: [Required setup](#required-setup).\n\nTo capture raw tool output and store into the local `.yardstick` directory for\nfurther analysis:\n```\nmake capture\n```\n\nTo analyze the tool output and evaluate a pass/fail result:\n```\nmake validate\n```\n\nA pass/fail result is shown in the output with reasons for the failure being\nlisted explicitly.\n\n\n## What is the quality gate criteria\n\nThe label comparison results are used to determine a pass/fail result,\nspecifically with the following criteria:\n\n - fail when current grype F1 score drops below last grype release F1 score (or\n   F1 score is indeterminate)\n - fail when the indeterminate matches % > 10% in the current grype results\n - fail when there is a rise in FNs relative to the results from the last grype\n   release\n - otherwise, pass\n\nF1 score is the primary way that tool matching performance is characterized. F1\nscore combines the TP, FP, and FN counts into a single metric between 0 and 1.\nIdeally the F1 score for an image-tool pair should be 1. F1 score is a good way\nto summarize the matching performance but does not explain why the matching\nperformance is what it is.\n\nIndeterminate matches are matches from results that could not be paired with a\nlabel (TP or FP). This could also mean that multiple conflicting labels were\nfound for a single match. The more indeterminate matches there are the less\nconfident you can be about the F1 score. Ideally there should be 0 indeterminate\nmatches, but this is difficult to achieve since vulnerability data is constantly\nchanging. \n\nFalse negatives represent matches that should have been made by the tool but\nwere missed. We should always make certain that this value does not increase\nbetween releases of grype.\n\n## Assumptions\n\n1. **Comparing vulnerability results taken at different times is invalid**.\n   We leverage the yardstick result-set feature to capture all vulnerability\n   results at one time for a specific image and tool set. Why? If we use grype\n   at version `a` on monday and grype at version `b` on tuesday and attempt to\n   compare the results, if differences are found it will not be immediately\n   clear why the results are different. That is, it is entirely possible that\n   the vulnerability databases from the run of `b` simply had more up to date\n   information, and if `grype@a` were run at the same time (on tuesday) this\n   reason can be almost entirely eliminated.\n\n2. **Comparing vulnerability results across images with different digests is invalid**.\n   It may be very tempting to compare vulnerability results for\n   `alpine:3.2` from monday and `alpine:3.2` from tuesday to see if there are\n   any changes. However, this is potentially inaccurate as the image references\n   are for the same tag, but the publisher may have pushed a new image with\n   differing content. Any change could lead to different vulnerability matching\n   results but we are only interested in vulnerability match differences that\n   are due to actionable reasons (grype matcher logic problems or [SBOM] input\n   data into matchers).\n\n## Approach\n\nVulnerability matching has essentially two inputs:\n\n- the packages that were found in the scanned artifact\n\n- the vulnerability data from upstream providers (e.g. NVD, GHSA, etc.)\n\n\nThese are both moving targets!\n\n\nWe may implement more catalogers in syft that raise up more packages discovered\nover time (for the same artifact scanned). Also the world is continually finding\nand reporting new vulnerabilities. The more moving parts there are in this form\nof testing the harder it is to come to a conclusion about the actual quality of\nthe output over time.\n\n\nTo reduce the eroding value over time we've decided to change as many moving\ntargets into fixed targets as possible:\n\n- Vulnerability results beyond a particular year are ignored (the current config\n  allows for <= 2020). Though there are still retroactive CVEs created, this\n  helps a lot in terms of keeping vulnerability results relatively stable.\n\n- SBOMs are used as input into grype instead of the raw container images. This\n  allows the artifacts under test to remain truly fixed and saves a lot of time\n  when capturing grype results (as the container image is no longer needed\n  during analysis).\n\n- For the captured SBOMs, container images referenced must be with a digest, not\n  just a tag. In case we update a tool version (say syft) we want to make\n  certain that we are scanning the exact same artifact later when we re-run the\n  analysis.\n\n- Versions of tools used are fixed to a specific `major.minor.patch` release used.\n  This allows us to account for capability differences between tool runs.\n\n\nTo reduce maintenance effort of this comparison over time there are a few things \nto keep in mind:\n\n- Once an image is labeled (at a specific digest) the image digest should be\n  considered immutable (never updated). Why? It takes a lot of effort to label\n  images and there are no \"clearly safe\" assumptions that can be made when it\n  comes to migrating labels from one image to another no matter how \"similar\"\n  the images may be. There is also no value in updating the image; these images\n  are not being executed and their only purpose is to survey the matching\n  performance of grype. In the philosophy of \"maximizing fixed points\" it\n  doesn't make sense to change these assets. Over time it may be that we remove\n  assets that are no longer useful for comparison, but this should rarely be\n  done.\n\n- Consider not changing the CVE year max-ceiling (currently set to 2020).\n  Pushing this ceiling will likely raise the number of unlabled matches\n  significantly for all images. Only bump this ceiling if all possible matches\n  are labeled.\n\n## Workflow\n\nOne way of working is to simply run `yardstick` and `gate.py` in the `test/quality` directory.\nYou will need to make sure the `vulnerability-match-labels` submodule has been initialized. This happens automatically\nfor some `make` commands, but you can ensure this by `git submodule update --init`. After the submodule has been\ninitialized, the match data from `vulnerability-match-labels` will be available locally.\n\n**TIP**: when dealing with submodules, it may be convenient to set the git config option `submodule.recurse` to `true`\nso `git checkout` will automatically update submodules to the correct commit:\n```shell\ngit config submodule.recurse true\n```\n\nTo do this we need some results to begin with. As noted above, start with (this does ensure the submodule is initialized):\n```shell\nmake capture\n```\n\nThis will download prebuilt SBOMs for the configured images and generate match results for configured tools (here:\nthe previous Grype version as well as the local version).\n\nAfter `make capture` has finished, we should have results and can now start inspecting and\nmodifying the comparison labels.\n\nTo get started, let's assume we see some quality gate failure in like this (something found in CI\nor after running `./gate.py`):\n```\nRunning comparison against labels... \n   Results used:\n    ├── f4fb4e6e-c911-41b6-9a10-f90b3954a41a : grype@v0.53.1-19-g8900767 against docker.io/anchore/test_images@sha256:808f6cf3cf4473eb39ff9bb47ead639d2ed71255b75b9b140162b58c6102bcc9\n    └── fcebdd0b-d80a-4fe2-b81a-802c7b98d83b : grype@v0.53.1 against docker.io/anchore/test_images@sha256:808f6cf3cf4473eb39ff9bb47ead639d2ed71255b75b9b140162b58c6102bcc9\n\nMatch differences between tooling (with labels):\n   TOOL PARTITION      PACKAGE       VULNERABILITY   LABEL         COMMENTARY\n   grype@v0.53.1 ONLY  node@14.18.2  CVE-2021-44531  TruePositive  (this is a new FN 😱)\n   grype@v0.53.1 ONLY  node@14.18.2  CVE-2021-44532  TruePositive  (this is a new FN 😱)\n   grype@v0.53.1 ONLY  node@14.18.2  CVE-2021-44533  TruePositive  (this is a new FN 😱)\n\nFailed quality gate\n   - current F1 score is lower than the latest release F1 score: current=0.80 latest=0.80 image=docker.io/anchore/test_images@sha256:808f6cf3cf4473eb39ff9bb47ead639d2ed71255b75b9b140162b58c6102bcc9\n   - current indeterminate matches % is greater than 10%: current=13.60% image=docker.io/anchore/test_images@sha256:808f6cf3cf4473eb39ff9bb47ead639d2ed71255b75b9b140162b58c6102bcc9\n   - current false negatives is greater than the latest release false negatives: current=6 latest=3 image=docker.io/anchore/test_images@sha256:808f6cf3cf4473eb39ff9bb47ead639d2ed71255b75b9b140162b58c6102bcc9\n```\n\nThis tells us some important information: which package, version, and vulnerability had a difference;\nhow it was previously labeled, and most importantly: the image we need to focus on (`docker.io/anchore/test_images@sha256:808f6cf3cf4473eb39ff9bb47ead639d2ed71255b75b9b140162b58c6102bcc9`).\n\nUsing the SHA above, we can run `yardstick` to see which results are available:\n```shell\n$ yardstick result list --result-set pr_vs_latest_via_sbom | grep 808f6cf3cf4473eb39ff9bb47ead639d2ed71255b75b9b140162b58c6102bcc9\n\n5bf0611b-183f-4525-a1ab-f268f62f48b6  docker.io/anchore/test_images:appstreams-centos-stream-8-1a287dd@sha256:808f6cf3cf4473eb39ff9bb47ead639d2ed71255b75b9b140162b58c6102bcc9          grype@v0.53.1              2022-12-09 20:49:56+00:00\n43a9650a-d5de-4687-b3ba-459105e32cb8  docker.io/anchore/test_images:appstreams-centos-stream-8-1a287dd@sha256:808f6cf3cf4473eb39ff9bb47ead639d2ed71255b75b9b140162b58c6102bcc9          grype@v0.53.1-15-gf29a32b  2022-12-09 20:49:53+00:00\n67913f57-690f-4f35-a2d9-ffccd2a0b2a1  docker.io/anchore/test_images:appstreams-centos-stream-8-1a287dd@sha256:808f6cf3cf4473eb39ff9bb47ead639d2ed71255b75b9b140162b58c6102bcc9          syft@v0.60.1               2022-11-01 20:30:52+00:00\n```\n\nWe'll need to use the UUIDs to explore the labels, so copy the first UUID, which we can see was run against the last Grype release (`grype@v0.53.1`). Use the UUID to explore and edit the results with\n`yardstick label explore`:\n```shell\nyardstick label explore 5bf0611b-183f-4525-a1ab-f268f62f48b6\n```\n\nAt this point we can use the TUI to explore and modify the match data, by deleting things or labeling as\ntrue positives, false positives, etc.. **After making changes make sure to save the results** (`Ctrl-S`)!\n\nAt this point you can run the quality gate using updated label data. The quality gate can run against \njust one image, for example the image we first found in the failure, so run the quality gate and see\nhow changes to the label data have affected the result:\n```shell\n./gate.py --image docker.io/anchore/test_images@sha256:808f6cf3cf4473eb39ff9bb47ead639d2ed71255b75b9b140162b58c6102bcc9\n```\n\nAfter iterating on all the changes we need using `yardstick label explore`, we're now ready to commit the changes. Since\nwe're using `git submodules`, we need to complete two steps:\n1. get the changes merged to the `vulnerability-match-labels` repository `main` branch\n2. update the submodule in this repository\n\nTo create a pull request for the `vulnerability-match-labels`, make sure you are in the `vulnerability-match-labels`\nsubdirectory and create a branch -- something like:\n```shell\ngit checkout --no-track -b my-branch-name\n```\n\nCommit the changes to this branch, push, create a pull request like normal. NOTE: you may need to add a fork\n(`git remote add ...`) and push to the fork if you don't have push permissions against the main\n`vulnerability-match-labels` repo. After the PR is approved and merged to `vulnerability-match-labels` repo's `main`\nbranch, update the submodule locally using:\n```shell\ngit submodule update --remote\n```\n\nNext, _commit the submodule change_ as part of any other changes \nto the Grype pull request and push as part of the in-progress PR\nagainst Grype. The PR will now use the updated match labels when running\nthe quality check.\n\n## Required setup\n\nIn order to manage Python versions, [pyenv](https://github.com/pyenv/pyenv) can be used. (e.g. `brew install pyenv`)\n\nBoth this project and `yardstick` require Python 3.10.\n\nUsing `pyenv`, see which python versions are available, for example:\n```shell\n$ pyenv install --list|grep 3.10\n  3.10.0\n  ...\n  3.10.7\n  ...\n```\n\nIn this case, we see `3.10.7` is the latest version, so we'll use that for the rest of the setup:\n\nInstall this version using `pyenv`:\n```shell\npyenv install 3.10.7\n```\n\nNOTE: to view the specific Python versions installed use `pyenv versions`:\n```shell\n$ pyenv versions\n  system\n* 3.8.13 (set by /Users/usr/.pyenv/version)\n  3.10.7\n```\n\nTo select the `3.10` version use the exact version number:\n```shell\npyenv shell 3.10.7\n```\n\n(or maybe just: `pyenv shell $(pyenv versions | grep 3.10 | tail -1)`)\n\nVerify this has worked properly by running:\n```shell\npython --version\n```\n\n**Important:** it is also required to have `oras` installed (e.g. `brew install oras`)\n\n**After** setting the working Python version to 3.10, in the `test/quality` directory,\nyou need to set up a virtual environment using:\n```shell\nmake venv\n```\n\n**After** creating the virtual environment, you can now activate it to set up a\nworking shell using:\n```shell\n. venv/bin/activate\n```\n\nYou should now have a shell running in the correct virtual environment, it might look something\nlike this:\n```shell\n(venv) user@HOST quality %\n```\n\nNow you should be able to run both `yardstick` and `./gate.py`.\n\n## Troubleshooting\n\nAs noted above, yardstick requires Python 3.10. If you try to run with an older version, such as\nthe default macOS 3.8 version, you will likely see an error similar to:\n\n```\nTraceback (most recent call last):\n  File \"./vulnerability-match-labels/sboms.py\", line 12, in <module>\n    import yardstick\n  File \"/grype/test/quality/vulnerability-match-labels/venv/lib/python3.8/site-packages/yardstick/__init__.py\", line 4, in <module>\n    from . import arrange, artifact, capture, cli, comparison, label, store, tool, utils\n  File \"/grype/test/quality/vulnerability-match-labels/venv/lib/python3.8/site-packages/yardstick/arrange.py\", line 4, in <module>\n    from yardstick import artifact\n  File \"/grype/test/quality/vulnerability-match-labels/venv/lib/python3.8/site-packages/yardstick/artifact.py\", line 482, in <module>\n    class ResultSet:\n  File \"/grype/test/quality/vulnerability-match-labels/venv/lib/python3.8/site-packages/yardstick/artifact.py\", line 484, in ResultSet\n    state: list[ResultState] = field(default_factory=list)\nTypeError: 'type' object is not subscriptable\n```\n"
  },
  {
    "path": "test/quality/requirements.txt",
    "content": "yardstick==v0.15.0\n# ../../../yardstick\ntabulate==0.9.0\ndataclass-wizard==0.36.2\n"
  },
  {
    "path": "test/quality/test-db",
    "content": "https://grype.anchore.io/databases/v6/vulnerability-db_v6.1.4_2026-03-01T00:32:26Z_1772346262.tar.zst\n"
  },
  {
    "path": "test/validate-grype-db-schema.py",
    "content": "#!/usr/bin/env python\nimport re\nimport os\nimport sys\nimport collections\n\ndir_pattern = r'grype/db/v(?P<version>\\d+)'\ndb_dir_regex = re.compile(dir_pattern)\nimport_regex = re.compile(rf'github.com/anchore/grype/{dir_pattern}')\n\n\ndef report_schema_versions_found(title, schema_to_locations):\n    for schema, locations in sorted(schema_to_locations.items()):\n        print(f\"{title} schema: {schema}\")\n        for location in locations:\n            print(f\"  {location}\")\n    print()\n\n\ndef assert_single_schema_version(schema_to_locations):\n    schema_versions_found = list(schema_to_locations.keys())\n    try:\n        for x in schema_versions_found:\n            int(x)\n    except ValueError:\n        sys.exit(\"Non-numeric schema found: %s\" % \", \".join(schema_versions_found))\n\n    if len(schema_to_locations) > 1:\n        sys.exit(\"Found multiple schemas: %s\" % \", \".join(schema_versions_found))\n    elif len(schema_to_locations) == 0:\n        sys.exit(\"No schemas found!\")\n\n\ndef find_db_schema_usages(filter_out_regexes=None, keep_regexes=None):\n    schema_to_locations = collections.defaultdict(list)\n\n    for root, dirs, files in os.walk(\".\"):\n        for file in files:\n            if not file.endswith(\".go\"):\n                continue\n            location = os.path.join(root, file)\n\n            if filter_out_regexes:\n                do_filter = False\n                for regex in filter_out_regexes:\n                    if regex.findall(location):\n                        do_filter = True\n                        break\n                if do_filter:\n                    continue\n\n            if keep_regexes:\n                do_keep = False\n                for regex in keep_regexes:\n                    if regex.findall(location):\n                        do_keep = True\n                        break\n                if not do_keep:\n                    continue\n\n            # keep track of all of the imports (from this point on, this is only possible consumers of db/v# code\n            with open(location) as f:\n                for match in import_regex.findall(f.read(), re.MULTILINE):\n                    schema_to_locations[match].append(location)\n\n    return schema_to_locations\n\n\ndef assert_schema_version_prefix(schema, locations):\n    for location in locations:\n        if f\"/grype/db/v{schema}\" not in location:\n            sys.exit(f\"found cross-schema reference: {location}\")\n\n\ndef validate_schema_consumers():\n    schema_to_locations = find_db_schema_usages(filter_out_regexes=[db_dir_regex])\n    report_schema_versions_found(\"Consumers of\", schema_to_locations)\n    assert_single_schema_version(schema_to_locations)\n    print(\"Consuming schema versions found: %s\" % list(schema_to_locations.keys())[0])\n\n\ndef validate_schema_definitions():\n    schema_to_locations = find_db_schema_usages(keep_regexes=[db_dir_regex])\n    report_schema_versions_found(\"Definitions of\", schema_to_locations)\n    # make certain that each definition keeps out of other schema definitions\n    for schema, locations in schema_to_locations.items():\n        assert_schema_version_prefix(schema, locations)\n    print(\"Verified that schema definitions don't cross-import\")\n\n\ndef main():\n    validate_schema_definitions()\n    print()\n    validate_schema_consumers()\n\n\nif __name__ == \"__main__\":\n    main()\n"
  }
]