[
  {
    "path": ".chglog/CHANGELOG.tpl.md",
    "content": "{{ if .Versions -}}\n<a name=\"unreleased\"></a>\n## [Unreleased]\n\n{{ if .Unreleased.CommitGroups -}}\n{{ range .Unreleased.CommitGroups -}}\n### {{ .Title }}\n{{ range .Commits -}}\n- [{{.Hash.Short}}]({{ $.Info.RepositoryURL  }}/commit/{{ .Hash.Long }}): {{ .Subject }}\n{{ if .Refs -}}{{ range .Refs }} -{{if .Action}}{{ .Action }} {{ end }} [#{{ .Ref }}]({{ $.Info.RepositoryURL  }}/issues/{{ .Ref }}){{ end -}}\n{{ end -}}\n{{ end -}}\n{{ end -}}\n{{ end -}}\n\n{{ range .Versions }}\n<a name=\"{{ .Tag.Name }}\"></a>\n## {{ if .Tag.Previous }}[{{ .Tag.Name }}]{{ else }}{{ .Tag.Name }}{{ end }} - {{ datetime \"2006-01-02\" .Tag.Date }}\n{{ range .CommitGroups -}}\n### {{ .Title }}\n{{ range .Commits -}}\n- [{{.Hash.Short}}]({{ $.Info.RepositoryURL  }}/commit/{{ .Hash.Long }}): {{ .Subject }}\n{{ if .Refs -}}{{ range .Refs }} - {{if .Action}}{{ .Action }}{{ end }} [#{{ .Ref }}]({{ $.Info.RepositoryURL  }}/issues/{{ .Ref }}){{ end -}}\n{{ end -}}\n{{ end -}}\n{{ end -}}\n\n{{- if .RevertCommits -}}\n### Reverts\n{{ range .RevertCommits -}}\n- {{ .Revert.Header }}\n{{ end }}\n{{ end -}}\n\n{{- if .MergeCommits -}}\n### Pull Requests\n{{ range .MergeCommits -}}\n- {{ .Header }}\n{{ end }}\n{{ end -}}\n\n{{- if .NoteGroups -}}\n{{ range .NoteGroups -}}\n### {{ .Title }}\n{{ range .Notes }}\n{{ .Body }}\n{{ end }}\n{{ end -}}\n{{ end -}}\n{{ end -}}\n\n{{- if .Versions }}\n[Unreleased]: {{ .Info.RepositoryURL }}/compare/{{ $latest := index .Versions 0 }}{{ $latest.Tag.Name }}...HEAD\n{{ range .Versions -}}\n{{ if .Tag.Previous -}}\n[{{ .Tag.Name }}]: {{ $.Info.RepositoryURL }}/compare/{{ .Tag.Previous.Name }}...{{ .Tag.Name }}\n{{ end -}}\n{{ end -}}\n{{ end -}}\n{{ end -}}\n"
  },
  {
    "path": ".chglog/config.yml",
    "content": "style: github\ntemplate: CHANGELOG.tpl.md\ninfo:\n  title: CHANGELOG\n  repository_url: https://github.com/quay/clair\noptions:\n  tag_filter_pattern: '^v'\n  sort: semver\n  commits:\n    sort_by: Scope\n  commit_groups:\n    group_by: Scope\n  header:\n    pattern: '^(.*?):\\s*(.*)$'\n    pattern_maps:\n      - Scope\n      - Subject\n  issues:\n    prefix: \n      - \"#\"\n\n  refs:\n    actions:\n      - Closes\n      - Fixes\n      - PullRequest\n\n  notes:\n    keywords:\n      - BREAKING CHANGE\n      - NOTE\n"
  },
  {
    "path": ".clang-format",
    "content": "---\nLanguage:        Proto\nBasedOnStyle:    Google\n...\n\n"
  },
  {
    "path": ".dockerignore",
    "content": "# Files\nDCO\n.dockerignore\nLICENSE\n*.md\nNOTICE\n*.oci\n*.tar*\n/clairctl-*\n!testdata/**\n# Directories\n/book\n/contrib\n!/contrib/cmd\n/Documenatation\n/etc\n/local-dev\n/.github\n# Allow `.git` to get sent for build vcs stamping.\n"
  },
  {
    "path": ".gitattributes",
    "content": "cmd/build.go export-subst\n*.go diff=golang\nDocumentation/reference/api.md linguist-generated\n"
  },
  {
    "path": ".github/CONTRIBUTING.md",
    "content": "# How to Contribute\n\nClair is [Apache 2.0 licensed](LICENSE) and accepts contributions via GitHub pull requests.\nThis document outlines some of the conventions on development workflow, commit message formatting, contact points and other resources to make it easier to get your contribution accepted.\n\n# Certificate of Origin\n\nBy contributing to this project you agree to the Developer Certificate of Origin [DCO](../DCO).\nThis document was created by the Linux Kernel community and is a simple statement that you, as a contributor, have the legal right to make the contribution.\nSee the [DCO](../DCO) file for details.\n\n# Email and Chat\n\nThe project currently uses a mailing list and IRC channel:\n\n- Email: [clair-dev@googlegroups.com](https://groups.google.com/forum/#!forum/clair-dev)\n- IRC: #[clair](irc://irc.freenode.org:6667/#clair) IRC channel on freenode.org\n\nPlease avoid emailing maintainers directly.\nThey are very busy and read the mailing lists.\n\n## Getting Started\n\n- Fork the repository on GitHub\n- Read the [README](../README.md) for build and test instructions\n- Play with the project, submit bugs, submit patches!\n\n## Contribution Flow\n\nThis is a rough outline of what a contributor's workflow looks like:\n\n- Create a topic branch from where you want to base your work (usually main).\n- Make commits of logical units.\n- Make sure your commit messages are in the proper format (see below).\n- Push your changes to a topic branch in your fork of the repository.\n- Make sure the tests pass, and add any new tests as appropriate.\n- Submit a pull request to the original repository.\n\nThanks for your contributions!\n\n### Format of the Commit Message\n\nWe follow a rough convention for commit messages that is designed to answer two questions: what changed and why.\nThe subject line should feature the what and the body of the commit should describe the why.\n\n```\nscripts: add the test-cluster command\n\nthis uses tmux to setup a test cluster that you can easily kill and\nstart for debugging.\n\nFixes #38\n```\n\nThe format can be described more formally as follows:\n\n```\n<subsystem>: <what changed>\n<BLANK LINE>\n<why this change was made>\n<BLANK LINE>\n<footer>\n```\n\nThe first line is the subject and should be no longer than 70 characters, the second line is always blank, and other lines should be wrapped at 80 characters.\nThis allows the message to be easier to read on GitHub as well as in various git tools.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE.md",
    "content": "<!--\n\nProject discussion and other meta topics should be discussed on the mailing\nlist or in the discussions section.\n\nGitHub issues are ONLY for bugs and features for the Clair API.\nPlease first create an issue on your client's repository before opening one here.\n\nAre you using a development build of Clair?\nYour problem might be solved by switching to a stable release.\n\n-->\n\n### Description of Problem / Feature Request\n\n<!--- your content here --->\n\n### Expected Outcome\n\n<!--- your content here --->\n\n### Actual Outcome\n\n<!--- your content here --->\n<!-- Please attach or paste logs from both clair and clairctl. -->\n\n### Environment\n\n<!--\n\nIssues that do not contain the Environment section are AUTOMATICALLY CLOSED.\nIf you're making a feature request, please specify \"N/A\" under the environment section.\n\nPlease note that issues against Clair v2 will be closed automatically unless\nsomeone has volunteered to work on it.\n-->\n\n- Clair version/image: \n- Clair client name/version: \n- Host OS: \n- Kernel (e.g. `uname -a`): \n- Kubernetes version (use `kubectl version`): \n- Network/Firewall setup: \n"
  },
  {
    "path": ".github/actions/documentation/action.yml",
    "content": "name: 'Documentation'\ndescription: 'build with mdBook and optionally push'\ninputs:\n  publish:\n    description: 'push resulting files to gh-pages'\n    default: 'true'\n  token:\n    description: 'github token'\n    default: ${{ github.token }}\nruns:\n  using: 'composite'\n  steps:\n    - uses: peaceiris/actions-mdbook@v1\n      with:\n        mdbook-version: 'latest'\n    - shell: sh\n      run: |\n        d=\"$(echo \"${GITHUB_REF#refs/tags/}\" | sed '/^refs\\/heads\\//d')\"\n        if test -z \"$d\"; then\n          exec mdbook build\n        else\n          exec mdbook build --dest-dir \"./book/${d}\"\n        fi\n    - if: ${{ inputs.publish == 'true' }}\n      uses: peaceiris/actions-gh-pages@v3\n      with:\n        github_token: ${{ inputs.token }}\n        publish_dir: ./book\n        keep_files: true\n"
  },
  {
    "path": ".github/actions/go-cache/action.yml",
    "content": "name: 'Go cache'\ndescription: 'cache go modules, and build artifacts'\ninputs:\n  go:\n    description: 'Go version to use'\nruns:\n  using: 'composite'\n  steps:\n    - uses: actions/cache@v4\n      with:\n        path: |\n          ~/.cache/go-build\n          ~/go/pkg/mod\n        key: ${{ runner.os }}-go${{ inputs.go }}-${{ hashFiles('**/go.sum') }}\n        restore-keys: |\n          ${{ runner.os }}-go${{ inputs.go }}\n    - shell: bash\n      run: |\n        find . -name go.mod -type f -printf '%h\\n' | while read dir; do\n          cd \"$dir\"\n          go mod download\n        done\n"
  },
  {
    "path": ".github/actions/go-tidy/action.yml",
    "content": "name: 'Go tidy'\ndescription: 'check that all modules are tidy-clean'\ninputs:\n  go:\n    description: 'Go version to use'\n  dir:\n    description: 'module root directory'\n    default: '.'\nruns:\n  using: 'composite'\n  steps:\n    - uses: ./.github/actions/go-cache\n      with:\n        go: ${{ inputs.go }}\n    - run: |\n        cd \"${{ inputs.dir }}\"\n        go mod tidy\n        git diff --exit-code\n      shell: bash\n"
  },
  {
    "path": ".github/actions/set-image-expiration/action.yml",
    "content": "name: 'Set Image Expiration'\ndescription: 'Use the Quay API to set an expiration time on an image'\ninputs:\n  quay:\n    description: 'Quay instance to issue calls to.'\n    required: false\n    default: 'quay.io'\n  duration:\n    description: 'Duration (in seconds) into the future to expire the image.'\n    required: false\n    default: '1209600'\n  repo:\n    description: 'Namespace & repository'\n    required: true\n  tag:\n    description: 'image tag'\n    required: true\n  token:\n    description: 'API token'\n    required: true\nruns:\n  using: 'composite'\n  steps:\n    - id: add-mask\n      name: Add Mask\n      shell: sh\n      run: |\n        printf '::add-mask::%s\\n' \"${{ inputs.token }}\"\n    - id: write-script\n      name: Prepare Request\n      shell: sh\n      run: |\n        jq -n -c --argjson e \"$(($(date -u +%s) + ${{ inputs.duration }}))\" '{expiration: $e}' > \"${RUNNER_TEMP}/expiration.json\"\n        cat <<. >\"${RUNNER_TEMP}/run\"\n        #!/usr/bin/env -S curl -K\n        silent\n        show-error\n        fail-with-body\n        data-binary=\"@${RUNNER_TEMP}/expiration.json\"\n        header=\"Authorization: Bearer ${{ inputs.token }}\"\n        header=\"Content-Type: application/json\"\n        header=\"Accept: application/json\"\n        request=PUT\n        url=\"https://${{ inputs.quay }}/api/v1/repository/${{ inputs.repo }}/tag/${{ inputs.tag }}\"\n        .\n        chmod +x \"${RUNNER_TEMP}/run\"\n    - id: call\n      name: Execute Request\n      shell: sh\n      run: '${RUNNER_TEMP}/run'\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n  - package-ecosystem: \"github-actions\"\n    directory: \"/\"\n    schedule:\n      interval: \"weekly\"\n  - package-ecosystem: \"gomod\"\n    directory: \"/\"\n    allow:\n      - dependency-type: \"direct\"\n    schedule:\n      interval: \"weekly\"\n    commit-message:\n      prefix: \"chore\"\n      include: \"scope\"\n    groups:\n      otel:\n        patterns:\n          - \"go.opentelemetry.io/otel/*\"\n      golang-x:\n        patterns:\n          - \"golang.org/x/*\"\n  - package-ecosystem: \"gomod\"\n    directory: \"/config\"\n    allow:\n      - dependency-type: \"direct\"\n    schedule:\n      interval: \"weekly\"\n    commit-message:\n      prefix: \"chore\"\n      include: \"scope\"\n    groups:\n      otel:\n        patterns:\n          - \"go.opentelemetry.io/otel/*\"\n      golang-x:\n        patterns:\n          - \"golang.org/x/*\"\n"
  },
  {
    "path": ".github/issue-close-app.yml",
    "content": "comment: \"This issue is closed because it does not meet our issue template. Please read it.\"\nissueConfigs:\n- content:\n  - \"### Environment\"\n"
  },
  {
    "path": ".github/script/nightly-module.sh",
    "content": "#!/bin/sh\nset -e\n: \"${CLAIRCORE_BRANCH:=main}\"\ncd \"$(git rev-parse --show-toplevel)\"\ntest -d vendor && rm -rf vendor\n\necho \"::group::Edits\"\ngo mod edit \\\n\t\"-replace=github.com/quay/claircore=github.com/quay/claircore@${CLAIRCORE_BRANCH}\"\ngo mod tidy\ngo mod download # Shouldn't be needed, but just to be safe...\necho \"::endgroup::\"\n\nclair_version=\"$(git describe --tags --always --dirty --match 'v4.*')\"\necho \"clair_version=${clair_version}\" >> \"$GITHUB_OUTPUT\"\n\ncat <<. >>\"$GITHUB_STEP_SUMMARY\"\n### Changes\n\n- **Go version:** $(go version)\n- **Clair version:** ${clair_version}\n.\n{\n\techo '```patch'\n\tgit diff\n\techo '```' \n} >>\"$GITHUB_STEP_SUMMARY\"\n"
  },
  {
    "path": ".github/workflows/.gitignore",
    "content": "yq\nyajsv\n*.json-schema\n"
  },
  {
    "path": ".github/workflows/.yamllint",
    "content": "---\nextends: default\nrules:\n  line-length: false\n  truthy:\n    check-keys: false\n"
  },
  {
    "path": ".github/workflows/Makefile",
    "content": "check: github-workflow.json-schema yajsv yq\n\tfor f in *.yml; do ./yq -o json \"$$f\" > \"$${f%yml}json\"; done\n\t./yajsv -s $< *.json\n\trm *.json\n\tcommand -v yamllint >/dev/null 2>&1 && yamllint .\n\nclean:\n\trm -rf yq yajsv github-workflow.json-schema\n\n.PHONY: check clean\n\nyq:\n\tcd /tmp && GOBIN=$(PWD) go install github.com/mikefarah/yq/v4@latest\n\nyajsv:\n\tcd /tmp && GOBIN=$(PWD) go install github.com/neilpa/yajsv@latest\n\ngithub-workflow.json-schema:\n\tcurl -sSLf https://github.com/SchemaStore/schemastore/raw/master/src/schemas/json/github-workflow.json > $@\n"
  },
  {
    "path": ".github/workflows/check-fast-forward.yml",
    "content": "---\nname: Check Fast Forward\non:\n  pull_request:\n    types: [opened, reopened, synchronize]\njobs:\n  check-fast-forward:\n    runs-on: ubuntu-latest\n\n    permissions:\n      contents: read\n      # We appear to need write permission for both pull-requests and\n      # issues in order to post a comment to a pull request.\n      pull-requests: write\n      issues: write\n\n    steps:\n      - name: Checking if fast forwarding is possible\n        uses: sequoia-pgp/fast-forward@v1\n        with:\n          merge: false\n          comment: on-error\n"
  },
  {
    "path": ".github/workflows/config-ci.yml",
    "content": "---\nname: Config module CI\n\non:\n  push:\n    paths:\n      - config/**\n    branches:\n      - main\n      - release-4.*\n  pull_request:\n    paths:\n      - config/**\n    branches:\n      - main\n      - release-4.*\n\njobs:\n  commit-check:\n    name: Commit Check\n    runs-on: ubuntu-latest\n    steps:\n      - name: Commit Check\n        uses: gsactions/commit-message-checker@v2\n        with:\n          pattern: |\n            ^[^:!]+: .+\\n\\n.*$\n          error: 'Commit must begin with <scope>: <subject>'\n          flags: 'gm'\n          excludeTitle: true\n          excludeDescription: true\n          checkAllCommitMessages: true\n          accessToken: ${{ secrets.GITHUB_TOKEN }}\n\n  tidy:\n    name: Tidy\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        id: checkout\n        if: ${{ !cancelled() }}\n        uses: actions/checkout@v6\n      - name: Setup Go\n        id: setupgo\n        if: ${{ !cancelled() && steps.checkout.conclusion == 'success' }}\n        uses: actions/setup-go@v6\n        with:\n          cache: false\n          go-version-file: ./config/go.mod\n      - name: Go Tidy\n        if: ${{ !cancelled() && steps.checkout.conclusion == 'success' && steps.setupgo.conclusion == 'success' }}\n        working-directory: ./config\n        run: |\n          trap 'echo \"::error file=go.mod,title=Tidy Check::Commit would leave go.mod untidy\"' ERR\n          go mod tidy\n          git diff --exit-code\n\n  tests:\n    name: Tests\n    if: ${{ !cancelled() }}\n    uses: ./.github/workflows/tests.yml\n    with:\n      cd: config\n      package_expr: ./...\n      qemu: false\n"
  },
  {
    "path": ".github/workflows/cut-release.yml",
    "content": "---\nname: Release\n\non:\n  push:\n    tags:\n      - v4.*\n  workflow_dispatch: {}\n\njobs:\n  config:\n    name: Config\n    runs-on: 'ubuntu-latest'\n    strategy:\n      matrix:\n        image: ['quay.io/projectquay/golang:1.25']\n    container:\n      image: ${{ matrix.image }}\n    outputs:\n      version: ${{ steps.setup.outputs.version }}\n      tar_prefix: ${{ steps.setup.outputs.tar_prefix }}\n      is_prerelease: ${{ startsWith(github.ref, 'refs/tags/') && (contains(github.ref, 'alpha') || contains(github.ref, 'beta') || contains(github.ref, 'rc')) }}\n      image_tag: ${{ steps.setup.outputs.image_tag }}\n      image_repo: ${{ steps.setup.outputs.image_repo }}\n      build_image: ${{ steps.setup.outputs.build_image }}\n      build_go_version: ${{ steps.setup.outputs.build_go_version }}\n      build_cache_key: ${{ steps.setup.outputs.cache_key }}\n      chglog_version: ${{ '0.15.1' }}\n    steps:\n      - name: Setup\n        id: setup\n        run: |\n          : \"${tag:=\"$(basename \"${GITHUB_REF}\")\"}\"\n          : \"${repo:=$GITHUB_REPOSITORY}\"\n          test \"${GITHUB_REPOSITORY_OWNER}\" = quay && repo=\"projectquay/${GITHUB_REPOSITORY##*/}\" ||:\n          cat <<. >>\"$GITHUB_OUTPUT\"\n          version=$tag\n          tar_prefix=clair-${tag}/\n          image_tag=${tag#v}\n          image_repo=${repo}\n          build_image=${{ matrix.image }}\n          build_go_version=$(go version | cut -f 3 -d ' ' | sed 's/^go//;s/\\.[0-9]\\+$//')\n          cache_key=$(go version | md5sum - | cut -f 1 -d ' ')\n          .\n\n  release-archive:\n    name: Create Release Archive\n    runs-on: 'ubuntu-latest'\n    needs: [config]\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6\n        with:\n          fetch-depth: 0\n      - uses: ./.github/actions/go-cache\n        with:\n          go: ${{ needs.config.outputs.build_go_version }}\n      - name: Create Release Archive\n        run: |\n          # Fix the checkout action overwriting the tag: (see https://github.com/actions/checkout/issues/882)\n          git fetch origin \"+${GITHUB_REF}:${GITHUB_REF}\"\n          git archive --prefix '${{ needs.config.outputs.tar_prefix }}' -o clair.tar \"${GITHUB_REF}\"\n          go mod vendor\n          tar -rf clair.tar --transform 's,^,${{ needs.config.outputs.tar_prefix }},' vendor\n          gzip clair.tar\n          mv clair.tar.gz clair-${{ needs.config.outputs.version }}.tar.gz\n      - name: Cache Changelog\n        uses: actions/cache@v5\n        id: chglog-cache\n        if: github.event_name != 'workflow_dispatch'\n        with:\n          path: /usr/local/bin/git-chglog\n          key: changelog-${{ needs.config.outputs.chglog_version }}\n      - name: Fetch Changelog\n        if: steps.chglog-cache.outputs.cache-hit != 'true' && github.event_name != 'workflow_dispatch'\n        run: |\n          cd \"$RUNNER_TEMP\"\n          v=\"${{ needs.config.outputs.chglog_version }}\"\n          f=\"git-chglog_${v}_linux_amd64.tar.gz\"\n          curl -fsOSL \"https://github.com/git-chglog/git-chglog/releases/download/v${v}/${f}\"\n          tar xvf \"${f}\"\n          install git-chglog /usr/local/bin\n      - name: Generate changelog\n        shell: bash\n        if: github.event_name != 'workflow_dispatch'\n        run: |\n          v=\"${{ needs.config.outputs.version }}\"\n          echo \"creating change log for tag: ${v}\"\n          git-chglog \"${v}\" > changelog\n      - name: Fake changelog\n        if: github.event_name == 'workflow_dispatch'\n        run: touch changelog\n      - name: Upload Release Archive\n        uses: actions/upload-artifact@v7\n        with:\n          name: clair-release\n          path: |\n            clair-${{ needs.config.outputs.version }}.tar.gz\n            changelog\n          if-no-files-found: error\n\n  release-binaries:\n    name: Create Release Binaries\n    runs-on: 'ubuntu-latest'\n    container: ${{ needs.config.outputs.build_image }}\n    needs: [config, release-archive]\n    strategy:\n      matrix:\n        goarch: ['arm64', 'amd64', '386', 'ppc64le', 's390x']\n        goos: ['linux', 'windows', 'darwin']\n        exclude:\n          - goos: darwin\n            goarch: '386'\n          - goos: windows\n            goarch: '386'\n          - goos: windows\n            goarch: 'ppc64le'\n          - goos: darwin\n            goarch: 'ppc64le'\n          - goos: windows\n            goarch: 's390x'\n          - goos: darwin\n            goarch: 's390x'\n    env:\n      GOOS: ${{matrix.goos}}\n      GOARCH: ${{matrix.goarch}}\n    steps:\n      - name: Fetch Artifacts\n        uses: actions/download-artifact@v7\n        id: download\n        with:\n          name: clair-release\n      - name: Unpack\n        run: |\n          tar -xz -f ${{steps.download.outputs.download-path}}/clair-${{ needs.config.outputs.version }}.tar.gz --strip-components=1\n      - uses: actions/setup-go@v6\n        with:\n          go-version: ${{ needs.config.outputs.build_go_version }}\n      - name: Build\n        # Build with path trimming, ELF debug stripping, and no VCS injection (should be done by the `git archive` process).\n        run: |\n          go build\\\n            -trimpath -ldflags=\"-s -w\" -buildvcs=false\\\n            -o \"clairctl-${{matrix.goos}}-${{matrix.goarch}}\"\\\n            ./cmd/clairctl\n      - name: Upload\n        uses: actions/upload-artifact@v7\n        with:\n          name: clairctl-${{matrix.goos}}-${{matrix.goarch}}\n          path: clairctl-${{matrix.goos}}-${{matrix.goarch}}\n          if-no-files-found: error\n\n  release:\n    name: Release\n    runs-on: 'ubuntu-latest'\n    if: github.event_name == 'push'\n    needs: [config, release-archive, release-binaries]\n    outputs:\n      upload_url: ${{ steps.create_release.outputs.upload_url }}\n    steps:\n      - name: Fetch Artifacts\n        uses: actions/download-artifact@v7\n        id: download\n        with:\n          name: clair-release\n      - name: Create Release\n        uses: ncipollo/release-action@v1\n        id: create_release\n        with:\n          name: ${{ needs.config.outputs.version }} Release\n          bodyFile: ${{steps.download.outputs.download-path}}/changelog\n          prerelease: ${{ needs.config.outputs.is_prerelease }}\n          artifacts: '${{steps.download.outputs.download-path}}/clair-*'\n\n  publish-container:\n    name: Publish Container\n    runs-on: 'ubuntu-latest'\n    needs: [config, release-archive, release]\n    steps:\n      - name: Fetch Artifacts\n        uses: actions/download-artifact@v7\n        id: download\n        with:\n          name: clair-release\n      - name: Set up QEMU\n        uses: docker/setup-qemu-action@v3\n        with:\n          platforms: all\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v3\n      - name: Login\n        uses: docker/login-action@v3\n        with:\n          registry: quay.io\n          username: ${{ secrets.QUAY_USER }}\n          password: ${{ secrets.QUAY_TOKEN }}\n      - name: Extract Release\n        run: |\n          mkdir \"${{ runner.temp }}/build\"\n          tar -xz -f ${{steps.download.outputs.download-path}}/clair-${{ needs.config.outputs.version }}.tar.gz --strip-components=1 -C \"${{ runner.temp }}/build\"\n      - name: Build Container\n        uses: docker/build-push-action@v6\n        with:\n          cache-from: type=gha\n          cache-to: type=gha,mode=max\n          context: ${{ runner.temp }}/build\n          platforms: linux/amd64,linux/arm64,linux/ppc64le,linux/s390x\n          push: true\n          tags: |\n            quay.io/${{ needs.config.outputs.image_repo }}:${{ needs.config.outputs.image_tag }}\n      - name: Checkout\n        if: needs.config.outputs.is_prerelease == 'true'\n        uses: actions/checkout@v6\n      - name: Set Expiration\n        if: needs.config.outputs.is_prerelease == 'true'\n        uses: ./.github/actions/set-image-expiration\n        with:\n          repo: ${{ needs.config.outputs.image_repo }}\n          tag: ${{ needs.config.outputs.image_tag }}\n          token: ${{ secrets.QUAY_API_TOKEN }}\n\n  publish-binaries:\n    name: Publish Binaries\n    runs-on: 'ubuntu-latest'\n    needs: [release-archive, release]\n    strategy:\n      matrix:\n        goarch: ['arm64', 'amd64', '386', 'ppc64le', 's390x']\n        goos: ['linux', 'windows', 'darwin']\n        exclude:\n          - goos: darwin\n            goarch: '386'\n          - goos: windows\n            goarch: '386'\n          - goos: darwin\n            goarch: ppc64le\n          - goos: windows\n            goarch: ppc64le\n          - goos: windows\n            goarch: 's390x'\n          - goos: darwin\n            goarch: 's390x'\n    steps:\n      - name: Fetch Artifacts\n        uses: actions/download-artifact@v7\n        id: download\n        with:\n          pattern: clairctl-*\n          merge-multiple: true\n      - name: Publish clairctl-${{matrix.goos}}-${{matrix.goarch}}\n        uses: actions/upload-release-asset@v1\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        with:\n          upload_url: ${{ needs.release.outputs.upload_url }}\n          asset_path: ${{steps.download.outputs.download-path}}/clairctl-${{matrix.goos}}-${{matrix.goarch}}\n          asset_name: clairctl-${{matrix.goos}}-${{matrix.goarch}}\n          asset_content_type: application/octet-stream\n\n  deploy-documentation:\n    name: Deploy Documentation\n    runs-on: ubuntu-latest\n    needs: [release]\n    steps:\n      - uses: actions/checkout@v6\n      - uses: ./.github/actions/documentation\n"
  },
  {
    "path": ".github/workflows/documentation.yml",
    "content": "---\nname: Deploy Main Documentation\n\non:\n  push:\n    branches:\n      - main\n    paths:\n      - 'Documentation/**'\n\njobs:\n  deploy-documentation:\n    name: Deploy Documentation\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v6\n      - uses: ./.github/actions/documentation\n"
  },
  {
    "path": ".github/workflows/fast-forward.yml",
    "content": "---\nname: Fast Forward\non:\n  issue_comment:\n    types: [created, edited]\n  pull_request_review:\n    types: [submitted]\njobs:\n  fast-forward:\n    # Only run if the comment or approval contains the /fast-forward command,\n    # or if it was a dependabot PR.\n    if: >-\n      ${{\n      ( github.event.issue.pull_request && contains(github.event.comment.body, '/fast-forward')) ||\n      (\n        github.event.review && github.event.review.state == 'approved' && (\n          github.event.pull_request.user.url == 'https://api.github.com/users/dependabot%5Bbot%5D' ||\n          contains(github.event.review.body, '/fast-forward')\n        ))\n      }}\n    runs-on: ubuntu-latest\n\n    permissions:\n      contents: write\n      pull-requests: write\n      issues: write\n\n    steps:\n      - name: Fast forwarding\n        uses: sequoia-pgp/fast-forward@v1\n        with:\n          merge: true\n          comment: on-error\n"
  },
  {
    "path": ".github/workflows/main.yml",
    "content": "---\nname: CI\n\non:\n  push:\n    branches:\n      - main\n      - release-4.*\n  pull_request:\n    branches:\n      - main\n      - release-4.*\n\njobs:\n  lints:\n    name: Lints\n    runs-on: ubuntu-latest\n    steps:\n      - name: Commit Check\n        uses: gsactions/commit-message-checker@v2\n        with:\n          pattern: |\n            ^[^:!]+: .+\\n\\n.*$\n          error: 'Commit must begin with <scope>: <subject>'\n          flags: 'gm'\n          excludeTitle: true\n          excludeDescription: true\n          checkAllCommitMessages: true\n          accessToken: ${{ secrets.GITHUB_TOKEN }}\n      - name: Checkout\n        id: checkout\n        if: ${{ !cancelled() }}\n        uses: actions/checkout@v6\n      - name: Check Filenames\n        if: ${{ !cancelled() && steps.checkout.conclusion == 'success' }}\n        run: | # Check for all the characters Windows hates.\n          git ls-files -- ':/:*[<>:\"|?*]*' | while read -r file; do\n            printf '::error file=%s,title=Bad Filename::Disallowed character in file name\\n' \"$file\"\n          done\n          exit $(git ls-files -- ':/:*[<>:\"|?*]*' | wc -l)\n      - name: Check Container Versions\n        if: ${{ !cancelled() && steps.checkout.conclusion == 'success' }}\n        run: |\n          # awk ...\n          version=$(sed -n '/^go /{s/go \\(1\\.[0-9]\\+\\)\\.[0-9]\\+/\\1/;p;q}' go.mod)\n          {\n            find . -name Dockerfile |\n              xargs awk -v \"want=$version\" '/^ARG GO_VERSION/{split($2,ver,/=/);if(ver[2]!=want) printf \"%s\\t%d\\n\", FILENAME, FNR}'\n            awk -v \"want=$version\" '/&go-image/{split($3,ref,/:/);if(ref[2]!=want) printf \"%s\\t%d\\n\", FILENAME, FNR}' docker-compose.yaml\n          } |\n            awk -v \"want=$version\" '{printf \"::error file=%s,line=%d,title=Go Version Skew::Go version does not match `go.mod`: want %s\\n\", $1, $2, want}'\n      - name: Setup Go\n        id: 'setupgo'\n        if: ${{ !cancelled() && steps.checkout.conclusion == 'success' }}\n        uses: actions/setup-go@v6\n        with:\n          cache: false\n          go-version-file: ./go.mod\n      - name: Go Tidy\n        if: ${{ !cancelled() && steps.checkout.conclusion == 'success' && steps.setupgo.conclusion == 'success' }}\n        run: |\n          # go mod tidy\n          trap 'echo \"::error file=go.mod,title=Tidy Check::Commit would leave go.mod untidy\"' ERR\n          go mod tidy\n          git diff --exit-code\n\n  documentation:\n    name: Documentation\n    needs: ['lints']\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v6\n      - uses: ./.github/actions/documentation\n        with:\n          publish: false\n\n  tests:\n    name: Tests\n    needs: ['lints']\n    uses: ./.github/workflows/tests.yml\n    with:\n      package_expr: ./...\n      qemu: false\n"
  },
  {
    "path": ".github/workflows/nightly-ci.yml",
    "content": "---\nname: Nightly CI\n\non:\n  schedule:\n    - cron: '30 4 * * *'\n  workflow_dispatch:\n    inputs:\n      package_expr:\n        required: false\n        type: string\n        description: 'Package expression(s) passed to `go test`'\n\njobs:\n  defaults:\n    name: Check Input\n    runs-on: ubuntu-latest\n    outputs:\n      package_expr: ${{ steps.config.outputs.package_expr }}\n    steps:\n      - name: Checkout\n        id: checkout\n        uses: actions/checkout@v6\n      - name: Make package expressions\n        id: config\n        if: ${{ !cancelled() && steps.checkout.conclusion == 'success' }}\n        run: |\n          if test -n \"${{ inputs.package_expr }}\"; then\n            printf 'package_expr=%s\\n' '${{ inputs.package_expr }}' >> \"$GITHUB_OUTPUT\"\n          else\n            printf 'package_expr=%s\\n' \"$(go list -m github.com/quay/clair{core,}/... | awk '{printf(\"%s/... \",$1)}')\" >> \"$GITHUB_OUTPUT\"\n          fi\n\n  tests:\n    name: Tests\n    needs: ['defaults']\n    uses: ./.github/workflows/tests.yml\n    with:\n      package_expr: ${{ needs.defaults.outputs.package_expr }}\n      qemu: true\n"
  },
  {
    "path": ".github/workflows/nightly.yml",
    "content": "---\nname: Nightly\n\non:\n  workflow_dispatch:\n    inputs:\n      branch:\n        description: 'Claircore branch to reference'\n        required: false\n      tag:\n        description: 'Tag to push resulting image to'\n        required: false\n  schedule:\n    - cron: '30 5 * * *'\n\njobs:\n  build:\n    name: Build and Push container\n    runs-on: 'ubuntu-latest'\n    steps:\n      - name: Setup\n        id: setup\n        env:\n          QUAY_TOKEN: ${{ secrets.QUAY_TOKEN }}\n          QUAY_API_TOKEN: ${{ secrets.QUAY_API_TOKEN }}\n        # This step uses defaults written in the shell script instead of the\n        # nicer workflow inputs so that the cron trigger works.\n        run: |\n          br=$(test -n \"${{github.event.inputs.branch}}\" && echo \"${{github.event.inputs.branch}}\" || echo main)\n          : \"${repo:=$GITHUB_REPOSITORY}\"\n          test \"${repo%%/*}\" = quay && repo=\"projectquay/${repo##*/}\" ||:\n          cat <<. >>$GITHUB_OUTPUT\n          push=${{ env.QUAY_TOKEN != '' }}\n          api=${{ env.QUAY_API_TOKEN != '' }}\n          date=$(date -u '+%Y-%m-%d')\n          tag=$(test -n \"${{github.event.inputs.tag}}\" && echo \"${{github.event.inputs.tag}}\" || echo nightly)\n          claircore_branch=${br}\n          repo=${repo}\n          .\n          # Environment variables\n          printf 'CLAIRCORE_BRANCH=%s\\n' \"${br}\" >> $GITHUB_ENV\n      - uses: docker/setup-qemu-action@v3\n        with:\n          platforms: all\n      - uses: docker/setup-buildx-action@v3\n      - uses: actions/checkout@v6\n        with:\n          fetch-depth: 0\n      - id: setup-go\n        uses: actions/setup-go@v6\n        with:\n          go-version-file: go.mod\n          check-latest: true\n      - name: Warm cache\n        if: steps.setup-go.outputs.cache-hit != 'true'\n        run: |\n          # go mod download\n          find . -name go.mod -type f -execdir go mod download \\;\n      - id: mod\n        run: ./.github/script/nightly-module.sh\n      - id: novelty\n        uses: actions/cache@v5\n        with:\n          path: go.sum\n          key: novelty-${{ github.sha }}-${{ hashFiles('./go.*') }}\n      - uses: docker/login-action@v3\n        if: steps.setup.outputs.push && steps.novelty.outputs.cache-hit != 'true'\n        with:\n          registry: quay.io\n          username: ${{ secrets.QUAY_USER }}\n          password: ${{ secrets.QUAY_TOKEN }}\n      - name: Export\n        if: steps.novelty.outputs.cache-hit != 'true'\n        # This exports the current state of the main branch, and appends our modified go module files.\n        run: |\n          mkdir \"${{ runner.temp }}/build\"\n          git archive --add-file=go.mod --add-file=go.sum origin/main |\n            tar -x -C \"${{ runner.temp }}/build\"\n          (\n            cd \"${{ runner.temp }}/build\"\n            go mod vendor\n          )\n      - uses: docker/build-push-action@v6\n        if: steps.novelty.outputs.cache-hit != 'true'\n        with:\n          cache-from: type=gha\n          cache-to: type=gha,mode=max\n          context: ${{ runner.temp }}/build\n          platforms: linux/amd64,linux/arm64,linux/s390x,linux/ppc64le\n          push: ${{ steps.setup.outputs.push && steps.novelty.outputs.cache-hit != 'true' }}\n          tags: |\n            quay.io/${{ steps.setup.outputs.repo }}:${{ steps.setup.outputs.tag }}\n            quay.io/${{ steps.setup.outputs.repo }}:${{ steps.setup.outputs.tag }}-${{ steps.setup.outputs.date }}\n      - uses: ./.github/actions/set-image-expiration\n        if: steps.setup.outputs.push && steps.setup.outputs.api && steps.novelty.outputs.cache-hit != 'true'\n        with:\n          repo: ${{ steps.setup.outputs.repo }}\n          tag: ${{ steps.setup.outputs.tag }}-${{ steps.setup.outputs.date }}\n          token: ${{ secrets.QUAY_API_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/prepare-release.yml",
    "content": "---\nname: Prepare Release\n\non:\n  workflow_dispatch:\n    inputs:\n      branch:\n        description: 'the branch to prepare the release against'\n        required: true\n        default: 'main'\n      tag:\n        description: 'the tag to be released'\n        required: true\n\njobs:\n  prepare:\n    name: Prepare Release\n    runs-on: 'ubuntu-latest'\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6\n        with:\n          fetch-depth: 0\n          ref: ${{ github.event.inputs.branch }}\n      - name: Changelog\n        shell: bash\n        run: |\n          curl -o /tmp/git-chglog.tar.gz -fsSL\\\n            https://github.com/git-chglog/git-chglog/releases/download/v0.14.0/git-chglog_0.14.0_linux_amd64.tar.gz\n          tar xvf /tmp/git-chglog.tar.gz --directory /tmp\n          chmod u+x /tmp/git-chglog\n          echo \"creating change log for tag: ${{ github.event.inputs.tag }}\"\n\n          # if this is a release branch filter our change\n          # log to only include logs with the same minor\n          # versions\n          # otherwise just filter v4 for full v4 history\n          # on main branch\n          filter_tag=\"--tag-filter-pattern v4\"\n          branch=${{ github.event.inputs.branch }}\n          echo \"discovered branch $branch\"\n          if [[ ${branch%-*} == \"release\" ]]; then\n            filter_tag=\"--tag-filter-pattern v${branch#release-}\"\n          fi\n\n          /tmp/git-chglog $filter_tag --next-tag \"${{ github.event.inputs.tag }}\" -o CHANGELOG.md v4.0.0-alpha.2..\n      - name: Create Pull Request\n        uses: peter-evans/create-pull-request@v8\n        with:\n          title: \"${{ github.event.inputs.tag }} Changelog Bump\"\n          body: \"This is an automated changelog commit.\"\n          commit-message: \"chore: ${{ github.event.inputs.tag }} changelog bump\"\n          branch: \"ready-${{ github.event.inputs.tag }}\"\n          signoff: true\n          delete-branch: true\n"
  },
  {
    "path": ".github/workflows/tests.yml",
    "content": "---\nname: Tests\n\non:\n  workflow_call:\n    inputs:\n      package_expr:\n        required: true\n        type: string\n        description: 'Package expression(s) passed to `go test`'\n      qemu:\n        required: false\n        type: boolean\n        default: false\n        description: 'Run tests for additional architectures under qemu-static'\n      cd:\n        required: false\n        type: string\n        default: \"\"\n        description: 'Change to this directory before running tests'\n\njobs:\n  setup:\n    name: Setup\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        go: ['oldstable', 'stable']\n    outputs:\n      go-cache: ${{ steps.check.outputs.cache-hit == 'true' || steps.setup-go.outputs.cache-hit == 'true' || steps.warm.conclusion == 'success' }}\n      runner-arch: ${{ steps.arch.outputs.result }}\n    steps:\n      - name: Map Arch\n        id: arch\n        uses: actions/github-script@v8\n        with:\n          result-encoding: string\n          script: |\n            switch (process.env.RUNNER_ARCH) {\n            case \"X64\":\n              return \"amd64\";\n            case \"ARM64\":\n              return \"arm64\";\n            default:\n              core.setFailed(`unknown/unsupported architecture: ${process.env.RUNNER_ARCH}`);\n            }\n            return \"\"\n      - name: Checkout\n        if: ${{ inputs.cd == '' }}\n        id: checkout\n        uses: actions/checkout@v6\n      - name: Check Go Version\n        if: ${{ inputs.cd == '' && steps.checkout.conclusion == 'success' }}\n        id: checkversion\n        uses: actions/setup-go@v6\n        with:\n          go-version: ${{ matrix.go }}\n          cache: false\n      - name: Get ImageOS\n        # There's no way around this, because \"ImageOS\" is only available to\n        # processes, but the setup-go action uses it in its key.\n        if: ${{ inputs.cd == '' && steps.checkout.conclusion == 'success' }}\n        id: imageos\n        uses: actions/github-script@v8\n        with:\n          result-encoding: string\n          script: |\n            return process.env.ImageOS\n      - name: Check Cache\n        if: ${{ inputs.cd == '' && steps.checkout.conclusion == 'success' }}\n        id: check\n        uses: actions/cache/restore@v5\n        with:\n          key: >-\n            setup-go-${{ runner.os }}-${{ steps.imageos.outputs.result }}-go-${{ steps.checkversion.outputs.go-version }}-${{ hashFiles('go.sum') }}\n          lookup-only: true\n          path: |\n            ~/go/pkg/mod\n            ~/.cache/go-build\n      - name: Setup Go\n        if: ${{ inputs.cd == '' && steps.check.outputs.cache-hit != 'true' }}\n        id: setup-go\n        uses: actions/setup-go@v6\n        with:\n          go-version: ${{ matrix.go }}\n      - name: Warm Cache on Miss\n        id: warm\n        if: ${{ inputs.cd == '' && steps.check.outputs.cache-hit != 'true' && steps.setup-go.outputs.cache-hit != 'true' }}\n        run: |\n          # Warm module+build cache\n          for GOARCH in amd64 arm64 ppc64le s390x; do\n            export GOARCH\n            for mod in '' github.com/quay/clair/config github.com/quay/claircore github.com/quay/claircore/toolkit; do\n              echo Downloading modules for \"${mod-main}/$GOARCH\"\n              go mod download $mod\n            done\n            echo Building '\"std\"' for \"$GOARCH\"\n            go build std\n          done\n\n  tests:\n    name: Tests\n    runs-on: ubuntu-latest\n    needs: ['setup']\n    strategy:\n      matrix:\n        go: ['oldstable', 'stable']\n        platform: ${{ inputs.qemu && fromJSON('[\"amd64\",\"arm64\",\"ppc64le\",\"s390x\"]') || fromJSON('[\"amd64\"]')}}\n      fail-fast: false\n    services:\n      postgres:\n        image: docker.io/library/postgres:15\n        env:\n          POSTGRES_DB: \"clair\"\n          POSTGRES_INITDB_ARGS: \"--no-sync\"\n          POSTGRES_PASSWORD: password\n          POSTGRES_USER: \"clair\"\n        options: >-\n          --health-cmd \"pg_isready -U clair\"\n          --health-interval 10s\n          --health-timeout 5s\n          --health-retries 5\n        ports:\n          - 5432\n      rabbitmq:\n        image: docker.io/library/rabbitmq:3\n        env:\n          RABBITMQ_SERVER_ADDITIONAL_ERL_ARGS: '-rabbit vm_memory_high_watermark 0.85'\n        ports:\n          - 5672\n          - 61613\n\n    steps:\n      - name: Configure RabbitMQ\n        run: |\n          docker exec ${{ job.services.rabbitmq.id }} rabbitmqctl await_startup\n          docker exec ${{ job.services.rabbitmq.id }} rabbitmq-plugins enable rabbitmq_stomp\n          docker exec ${{ job.services.rabbitmq.id }} rabbitmq-plugins disable rabbitmq_management_agent rabbitmq_prometheus rabbitmq_web_dispatch\n          docker exec ${{ job.services.rabbitmq.id }} rabbitmqctl add_vhost localhost\n          docker exec ${{ job.services.rabbitmq.id }} rabbitmqctl set_permissions -p localhost guest '.*' '.*' '.*'\n          docker exec ${{ job.services.rabbitmq.id }} rabbitmqctl add_user clair password\n          docker exec ${{ job.services.rabbitmq.id }} rabbitmqctl set_permissions -p '/' clair '.*' '.*' '.*'\n          docker exec ${{ job.services.rabbitmq.id }} rabbitmqctl set_permissions -p localhost clair '.*' '.*' '.*'\n\n      - name: Checkout\n        uses: actions/checkout@v6\n      - name: Setup Go\n        id: setup-go\n        uses: actions/setup-go@v6\n        with:\n          go-version: ${{ matrix.go }}\n          cache: ${{ needs.setup.outputs.go-cache }}\n      - name: Assets Cache\n        id: assets\n        uses: actions/cache/restore@v5\n        with:\n          key: integration-assets-${{ hashFiles('go.sum') }}\n          restore-keys: |\n            integration-assets-\n          path: |\n            ~/.cache/clair-testing\n      - name: Set up QEMU\n        uses: docker/setup-qemu-action@v3\n        if: ${{ matrix.platform != needs.setup.outputs.runner-arch }}\n        with:\n          platforms: linux/${{ matrix.platform }}\n\n      - name: Tests\n        # NB If Clair gains any C dependencies, this will need additional setup:\n        #  - `-extldflags=-static`\n        #  - CGO_ENABLED=1\n        #  - relevant architecture's libc, linker, etc.\n        env:\n          POSTGRES_CONNECTION_STRING: >-\n            host=localhost\n            port=${{ job.services.postgres.ports[5432] }}\n            user=clair\n            dbname=clair\n            password=password\n            sslmode=disable\n          RABBITMQ_CONNECTION_STRING: \"amqp://clair:password@localhost:${{ job.services.rabbitmq.ports[5672] }}/\"\n          STOMP_CONNECTION_STRING: \"stomp://clair:password@localhost:${{ job.services.rabbitmq.ports[61613] }}/\"\n          GOARCH: ${{ matrix.platform }}\n          CGO_ENABLED: '0'\n          GOFLAGS: '-mod=mod'\n        working-directory: ./${{ inputs.cd }}\n        run: |\n          # Go Tests\n          for expr in ${{ inputs.package_expr }}; do\n            printf '::group::go test %s\\n' \"$expr\"\n            go test -tags integration \"$expr\"\n            printf '::endgroup::\\n'\n          done\n"
  },
  {
    "path": ".github/workflows/v2-issues.yml",
    "content": "---\nname: 'Manage v2 Issues'\non:\n  workflow_dispatch:\n    inputs:\n      debug:\n        description: 'Run in debug mode (i.e. dry-run)'\n        required: false\n        default: false\n  schedule:\n    - cron: '30 1 * * *'\n\njobs:\n  stale:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/stale@v10\n        with:\n          only-labels: 'V2'\n          exempt-issue-labels: 'V2/active'\n          days-before-stale: 14\n          days-before-issue-stale: 0\n          days-before-close: 14\n          remove-stale-when-updated: false\n          ignore-updates: true\n          debug-only: ${{ github.event.inputs.debug || false }}\n          enable-statistics: true\n          operations-per-run: 1000\n"
  },
  {
    "path": ".gitignore",
    "content": "vendor/\nbook/\nclairctl\n!clairctl/\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "<a name=\"unreleased\"></a>\n## [Unreleased]\n\n\n\n<a name=\"v4.9.0\"></a>\n## [v4.9.0] - 2025-12-08\n### All\n- [1aca06b8](https://github.com/quay/clair/commit/1aca06b8155b038d4d2f480cab58e938cf156337): fix formatted print calls\n### Amqp\n- [1a9f8769](https://github.com/quay/clair/commit/1a9f8769746a484fba6acdd2d4a88da661f1eb28): add deprecation notice\n### Build(Deps)\n- [e4feca46](https://github.com/quay/clair/commit/e4feca46d5cea624c2764b427f3e9f1b07deaeeb): bump golang.org/x/time from 0.7.0 to 0.8.0\n- [f54011b5](https://github.com/quay/clair/commit/f54011b57c4fb44d153a3ab15f9dc1a80bb182a9): bump golang.org/x/sync from 0.8.0 to 0.9.0\n- [ee5524b8](https://github.com/quay/clair/commit/ee5524b897ae19af977f8b5fc7de19c30e925fce): bump go.opentelemetry.io/otel/sdk from 1.31.0 to 1.32.0\n- [757b649c](https://github.com/quay/clair/commit/757b649c3286a01948e0342985d4c09915f6c392): bump go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp\n- [20c0040f](https://github.com/quay/clair/commit/20c0040f8982f2fed944fb4182bac5aa3c56ada4): bump github.com/go-stomp/stomp/v3 from 3.1.2 to 3.1.3\n- [1607766c](https://github.com/quay/clair/commit/1607766cc5691378c70a6113a57d3a2086c65e63): bump github.com/prometheus/client_golang\n- [0a3a4611](https://github.com/quay/clair/commit/0a3a4611964014c0c9846d8c79acdd062af9c17d): bump go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace\n- [12ea7bf9](https://github.com/quay/clair/commit/12ea7bf97273f2a22d72dddd76bd7fb7e717838f): bump go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp\n- [146d4a67](https://github.com/quay/clair/commit/146d4a670c8d24774206bc5b30deb506901f3c7d): bump github.com/urfave/cli/v2 from 2.27.3 to 2.27.5\n- [50003694](https://github.com/quay/clair/commit/5000369402f550817eaa1364ba6eccce3691aa2e): bump github.com/klauspost/compress from 1.17.10 to 1.17.11\n- [6069bb24](https://github.com/quay/clair/commit/6069bb249b9228210a988b3ecb883f1adcd77e7a): bump go.opentelemetry.io/otel/exporters/stdout/stdouttrace\n### Chore\n- [cbfd97b6](https://github.com/quay/clair/commit/cbfd97b655b59c192bda4aefde5dc0c8cd3ae8bb): fix typos in config.yaml.sample\n- [7c9c079b](https://github.com/quay/clair/commit/7c9c079bc643e8f1383ad476f624331938d30d61): update claircore to v1.5.48\n- [8e9a6d46](https://github.com/quay/clair/commit/8e9a6d4629288f02a24368105598d02ef6ec0583): update claircore to v1.5.47\n- [804ef6a4](https://github.com/quay/clair/commit/804ef6a4890f6aa3a96f9910b6be14876b7623a5): update claircore to v1.5.46\n- [a50727a3](https://github.com/quay/clair/commit/a50727a3f281079fffa90b0172f99c8c0f6a027c): add DVO ignore annotations\n- [8d991938](https://github.com/quay/clair/commit/8d991938a30759da699ea7dd696a4b9bb79ff9dc): update claircore to v1.5.45\n- [ff2059cf](https://github.com/quay/clair/commit/ff2059cf2cbaa422967cc920edf6bbd82d77ed15): update claircore to v1.5.44\n- [db51ed82](https://github.com/quay/clair/commit/db51ed82dc8d62bf1051c8286f98233e728b3dab): update claircore to v1.5.42\n- [c2dc1766](https://github.com/quay/clair/commit/c2dc1766fffcb7126c3d63299f1702a7be4e2ce9): update claircore to v1.5.41\n- [8aa9e1e2](https://github.com/quay/clair/commit/8aa9e1e205961e855159a698e5a8f97089e91a3c): update claircore to v1.5.40\n- [eca299b7](https://github.com/quay/clair/commit/eca299b706af03bb4517bd6f4717744708bced16): update go references to go1.24\n- [1660b66b](https://github.com/quay/clair/commit/1660b66bd59630b9513a1a0f16cbdd4e552ed43f): upgrade from pgx v4 to v5\n- [68d03bae](https://github.com/quay/clair/commit/68d03bae216bbfe785b2fd52c66ac3481c21792d): remove reviews from dependabot config\n- [0c5292e7](https://github.com/quay/clair/commit/0c5292e7b19a6271d8c7ea5da72dc5ef66fba3e7): upgrade config module to v1.4.2\n- [e5d4c19c](https://github.com/quay/clair/commit/e5d4c19cb3f448f1f057124a916f8056abb0325b): update minimum go version to 1.23\n- [e45fbf0e](https://github.com/quay/clair/commit/e45fbf0e762de8fad56200cdbc22547df7c112d3): update claircore to v1.5.35\n- [708bf2f5](https://github.com/quay/clair/commit/708bf2f54c7715fd77fa874030740995c60a1c6a): update local-dev tracing configs to fix errors\n- [216ca2f1](https://github.com/quay/clair/commit/216ca2f1df0677e4ac0e42758d1fc16010e344ed): update claircore to v1.5.34\n- [dde57fc1](https://github.com/quay/clair/commit/dde57fc11d5328f3bb2d561e2df2a312fb812a01): update openAPI spec to remove SourcePackage\n- [e5149fd3](https://github.com/quay/clair/commit/e5149fd3ebb1e730ee549af3098a5fc60ccbf0f5): group some dependencies to avoid excessive PRs\n- [60ebea73](https://github.com/quay/clair/commit/60ebea73e6abd1c69005e86edf26aefd7a6df23f): update claircore to v1.5.33\n### Chore(Deps)\n- [f598d3ec](https://github.com/quay/clair/commit/f598d3ec6c2c091f9ec15da2988e0f579dd6bddc): bump go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp\n- [a952e3c6](https://github.com/quay/clair/commit/a952e3c66d3482aa7285b58836beaf195bc4b88f): bump the otel group with 11 updates\n- [878fbceb](https://github.com/quay/clair/commit/878fbceb42510407fece6b4f46ea66335491af21): bump github.com/google/go-containerregistry\n- [468e409c](https://github.com/quay/clair/commit/468e409ce4ddfb459e48023a5c898753fb1fd68f): bump actions/upload-artifact from 4 to 5\n- [c87bc8f0](https://github.com/quay/clair/commit/c87bc8f075780714db8f2251285d86f649212f05): bump github.com/klauspost/compress from 1.18.1 to 1.18.2\n- [2a5c11fd](https://github.com/quay/clair/commit/2a5c11fd121e9c40cf7e601bce67f119a380def8): bump actions/checkout from 5 to 6\n- [b12439f4](https://github.com/quay/clair/commit/b12439f4fadd467b198b4f226c4d2fb6a466fe26): bump golang.org/x/crypto from 0.44.0 to 0.45.0\n- [e169a50a](https://github.com/quay/clair/commit/e169a50a7971142e77e0f9194676424bbbe3493f): bump google.golang.org/grpc from 1.76.0 to 1.77.0\n- [3e778f2c](https://github.com/quay/clair/commit/3e778f2c2c3a4d0674aa1da2e540dc2a32abaecd): bump golang.org/x/net in the golang-x group\n- [4563ccbd](https://github.com/quay/clair/commit/4563ccbd8481f0f651b09e8f6e07e2d561850c31): bump github.com/go-stomp/stomp/v3 from 3.1.3 to 3.1.5\n- [195cdb06](https://github.com/quay/clair/commit/195cdb06b3e76ceb2e565178898af2d45d416e5c): bump golang.org/x/sync in the golang-x group\n- [b50044f4](https://github.com/quay/clair/commit/b50044f42e2780fab6db3147138c4c52193ade84): bump actions/download-artifact from 5 to 6\n- [1b429595](https://github.com/quay/clair/commit/1b42959514e932319d63e27dbb98545b622728f7): bump github.com/klauspost/compress from 1.18.0 to 1.18.1\n- [e439e4df](https://github.com/quay/clair/commit/e439e4df7a2db9922b7043d09071809d5ce8f6d8): bump the golang-x group with 2 updates\n- [fe37c68b](https://github.com/quay/clair/commit/fe37c68b97f59e0fe526ca97bad05210a74ef004): bump google.golang.org/grpc from 1.75.1 to 1.76.0\n- [ee6ea1c8](https://github.com/quay/clair/commit/ee6ea1c8816bc1f6541f4aa5dabc08f5b704ff2e): bump github.com/quay/claircore from 1.5.42 to 1.5.43\n- [afcfd7f0](https://github.com/quay/clair/commit/afcfd7f0b7013ecfd38f71883b511f5bda6646f8): bump google.golang.org/grpc from 1.75.0 to 1.75.1\n- [6a4937e4](https://github.com/quay/clair/commit/6a4937e45475a9c910dd0b3bef3803efb3d14166): bump the golang-x group across 1 directory with 3 updates\n- [53cf68e9](https://github.com/quay/clair/commit/53cf68e9b024baf58d99b5a1ed90e57ea9861942): bump github.com/jackc/pgx/v5 from 5.7.5 to 5.7.6\n- [e9850949](https://github.com/quay/clair/commit/e9850949db1eddca2de7c0c1fc568aaa1cd2f83b): bump github.com/prometheus/client_golang\n- [290969cd](https://github.com/quay/clair/commit/290969cd1ef8f9e87cf12a658b8223b0ac4a5635): bump actions/stale from 9 to 10\n- [5b5519b5](https://github.com/quay/clair/commit/5b5519b5e264c4107166b3f26239aac0ce696bac): bump actions/github-script from 7 to 8\n- [b78c76b1](https://github.com/quay/clair/commit/b78c76b152890837cb2961039af982d404a35ff0): bump actions/setup-go from 5 to 6\n- [b1f4716b](https://github.com/quay/clair/commit/b1f4716b279fdddb143b0cf5c65a5a5b4f62c43d): bump go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace\n- [93174450](https://github.com/quay/clair/commit/93174450e69ef63b1f2b0368a13aac0a818aba70): bump github.com/grafana/pyroscope-go/godeltaprof\n- [0f1fde39](https://github.com/quay/clair/commit/0f1fde397494c03329574edbaf2b9e4f66a56103): bump the otel group with 11 updates\n- [8dbb0f48](https://github.com/quay/clair/commit/8dbb0f4868eea306a5c9ae1576c2fd4041446b6f): bump golang.org/x/net in the golang-x group\n- [a35a1281](https://github.com/quay/clair/commit/a35a12819da47d1ac10519788be4adce24f6c33a): bump github.com/ulikunitz/xz from 0.5.11 to 0.5.14\n- [1fa9a753](https://github.com/quay/clair/commit/1fa9a75352b7304dc25a42808ae8c51b12d344b2): bump actions/checkout from 4 to 5\n- [f0b0949c](https://github.com/quay/clair/commit/f0b0949cf7eb3168f4b471414c99ae846a95a7e7): bump actions/download-artifact from 4 to 5\n- [890f4a1b](https://github.com/quay/clair/commit/890f4a1bc06b6817db91e38a9c6211019a171bcf): bump github.com/prometheus/client_golang\n- [80add42b](https://github.com/quay/clair/commit/80add42bbc040210aecc60cbde3a760ad71b5cb6): bump google.golang.org/grpc from 1.73.0 to 1.75.0\n- [e4746794](https://github.com/quay/clair/commit/e4746794de3720b6f49b8ec67afc84b8df4ab0d4): bump github.com/jackc/pgx/v5 from 5.7.4 to 5.7.5\n- [ba6fe31c](https://github.com/quay/clair/commit/ba6fe31c743674b828c5cee5ca269fe2545f1871): bump go.opentelemetry.io/otel/exporters/prometheus\n- [40b0402e](https://github.com/quay/clair/commit/40b0402e4945d6e28076b0bf8345550fb64cba35): bump the golang-x group with 2 updates\n- [f9635886](https://github.com/quay/clair/commit/f96358865c699367dd3fa197c7b579c73780ec3d): bump github.com/quay/zlog from 1.1.8 to 1.1.9\n- [4415106e](https://github.com/quay/clair/commit/4415106e79b9ef686133f7f9dc218c5c64e16da2): bump go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace\n- [b7325ada](https://github.com/quay/clair/commit/b7325adaaea397ceda18b86e75295d43781b0bd7): bump go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp\n- [78b92595](https://github.com/quay/clair/commit/78b92595494b4cc2a221dd1d8f8f19235ee594e9): bump the otel group with 11 updates\n- [62956271](https://github.com/quay/clair/commit/629562717ccdd276f0d23ea2d2a814f817fa9b5b): bump github.com/urfave/cli/v2 from 2.27.6 to 2.27.7\n- [440eee8e](https://github.com/quay/clair/commit/440eee8e1dccfd24f55cf97b23a52e3666c346cc): bump github.com/google/go-containerregistry\n- [e75e2e2b](https://github.com/quay/clair/commit/e75e2e2b43d2990f656c7505f0dcc61c4a3219eb): bump the golang-x group with 3 updates\n- [cf20adbd](https://github.com/quay/clair/commit/cf20adbddc9dd63917e6321cd5c0d1f0467d51cf): bump google.golang.org/grpc from 1.72.2 to 1.73.0\n- [d9c211b4](https://github.com/quay/clair/commit/d9c211b4eedfc4254005fc99ff02b9e6d730e9ac): bump github.com/quay/claircore from 1.5.37 to 1.5.38\n- [6338de8b](https://github.com/quay/clair/commit/6338de8b2318c30973eba180969e49242e42f50c): bump github.com/ugorji/go/codec from 1.2.12 to 1.2.14\n- [566271a1](https://github.com/quay/clair/commit/566271a163f8be8817e9c2d6e199aa5cf229b3b8): bump go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace\n- [3e3a2d33](https://github.com/quay/clair/commit/3e3a2d336195c8a64ece2ae387c0f556c76d165a): bump github.com/google/go-containerregistry\n- [81b725ba](https://github.com/quay/clair/commit/81b725ba9ee96443e9013dc3817edd4677beee38): bump google.golang.org/grpc from 1.72.1 to 1.72.2\n- [faad36e2](https://github.com/quay/clair/commit/faad36e2c040e7588c6be9cc2dc91f39b037ae68): bump the otel group with 11 updates\n- [7979e036](https://github.com/quay/clair/commit/7979e036f09e90e12a61aa7152bbc8d780cdbc08): bump google.golang.org/grpc from 1.72.0 to 1.72.1\n- [99ab2c1a](https://github.com/quay/clair/commit/99ab2c1ab17bb58e9e2e7ff10fc5a524d0e8d60d): bump the golang-x group with 2 updates\n- [a166f610](https://github.com/quay/clair/commit/a166f6103a50b3374797f7825270fc74d95ca51c): bump github.com/quay/claircore from 1.5.36 to 1.5.37\n- [d8e9dcf4](https://github.com/quay/clair/commit/d8e9dcf43083d7c5812f85190ffaeebe2b4e8db9): bump google.golang.org/grpc from 1.71.1 to 1.72.0\n- [bfa8f11d](https://github.com/quay/clair/commit/bfa8f11de0d6ff542c23c5146248473b932b9b4b): bump github.com/quay/claircore from 1.5.35 to 1.5.36\n- [f8a41628](https://github.com/quay/clair/commit/f8a4162819ca57e13e073c7e52ab78093347cd84): bump github.com/prometheus/client_golang\n- [7ce22abe](https://github.com/quay/clair/commit/7ce22abec64cc1ffa0b96522f9c1903340539ff4): bump google.golang.org/grpc from 1.71.0 to 1.71.1\n- [c53cf2ba](https://github.com/quay/clair/commit/c53cf2bafc79dcd13b8a27532da5e2118dc0d1a3): bump the golang-x group with 2 updates\n- [a5833a44](https://github.com/quay/clair/commit/a5833a4405c11add7666e1532551a5d71e3036e5): bump golang.org/x/net in the golang-x group\n- [cc6fb14a](https://github.com/quay/clair/commit/cc6fb14ada0f9af9411fef5a247dad04a77e269b): bump github.com/rs/zerolog from 1.33.0 to 1.34.0\n- [851e4a36](https://github.com/quay/clair/commit/851e4a36dfd093bda1a81ff555a13c6d8fae38bb): bump github.com/urfave/cli/v2 from 2.27.5 to 2.27.6\n- [e9997624](https://github.com/quay/clair/commit/e9997624ee9ae9e030052f764f9b2e428a9021f7): bump go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace\n- [a73e832b](https://github.com/quay/clair/commit/a73e832bb478f34ec5cc74f3e63d79be4a0b8867): bump github.com/prometheus/client_golang\n- [35110e9e](https://github.com/quay/clair/commit/35110e9e3b285dfefa2f8e4e858fb24fda84d245): bump go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp\n- [0a9866e3](https://github.com/quay/clair/commit/0a9866e39c2398ecf80b289c1b90227f0c1ba966): bump the golang-x group with 3 updates\n- [1ce14606](https://github.com/quay/clair/commit/1ce146061c87336eb3f41d394fbab4e15b237922): bump the otel group with 11 updates\n- [919d5287](https://github.com/quay/clair/commit/919d5287ee85489e211f0f909078a7ff204af886): bump github.com/google/go-cmp in /config\n- [2673e4f4](https://github.com/quay/clair/commit/2673e4f4a9adf3b836d0ba9d6b5dd28823f540ce): bump github.com/rogpeppe/go-internal from 1.13.1 to 1.14.1\n- [cf7af98a](https://github.com/quay/clair/commit/cf7af98a4571e2a05ca0839000be2ca275a647c6): bump github.com/go-jose/go-jose/v3 from 3.0.3 to 3.0.4\n- [6c9fae1e](https://github.com/quay/clair/commit/6c9fae1e29f50dc11871f28cbe51d609120a065b): bump github.com/google/go-cmp from 0.6.0 to 0.7.0\n- [707d8049](https://github.com/quay/clair/commit/707d80495e98786757f31f7028bc54f98e524276): bump github.com/prometheus/client_golang\n- [136a618f](https://github.com/quay/clair/commit/136a618f241f48c00a32b0386e9225bb5d1dfa7d): bump github.com/klauspost/compress from 1.17.11 to 1.18.0\n- [3e7c6e74](https://github.com/quay/clair/commit/3e7c6e7463307ebef02b708e85d3092c41771e44): bump the golang-x group with 3 updates\n- [73db520d](https://github.com/quay/clair/commit/73db520d4c8d0a55e8aa2e4f507412443f12226a): bump github.com/evanphx/json-patch/v5 from 5.9.10 to 5.9.11\n- [a3a60f10](https://github.com/quay/clair/commit/a3a60f1017a482909f9451c04501d9c3c179855f): bump google.golang.org/grpc from 1.69.4 to 1.70.0\n- [cc29705c](https://github.com/quay/clair/commit/cc29705c7cb02f77566db47dc57c4cac78032420): bump github.com/evanphx/json-patch/v5 from 5.9.0 to 5.9.10\n- [d05b4049](https://github.com/quay/clair/commit/d05b4049c7cceda76416b35795ef9290f82c5d95): bump go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace\n- [8b99d320](https://github.com/quay/clair/commit/8b99d32021936f2eadda01c38b2bb788272cb7bb): bump the otel group with 11 updates\n- [b2c66991](https://github.com/quay/clair/commit/b2c669913bbd4ef1a7df5c2dfad6459ee8fcb117): bump google.golang.org/grpc from 1.69.2 to 1.69.4\n- [ef4a1f11](https://github.com/quay/clair/commit/ef4a1f113ae4c192feeadaf50571ff8f11ddb6ed): bump the golang-x group with 2 updates\n- [38b77499](https://github.com/quay/clair/commit/38b774994da0481efb4b852d86b96332167baef8): bump golang.org/x/net in the golang-x group\n- [80c0381a](https://github.com/quay/clair/commit/80c0381a0d6de6ab79b04294980c1d7e59deb72a): bump the otel group across 1 directory with 2 updates\n- [3eff1ef1](https://github.com/quay/clair/commit/3eff1ef131d17b383fdf84172818f6e8d5d307e2): bump go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace\n- [5bf85313](https://github.com/quay/clair/commit/5bf853139ea1a3cdcc5ee73fe21b18c7fef4fe35): bump go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp\n- [9ebb61d9](https://github.com/quay/clair/commit/9ebb61d945cc8b61d507adee56e020788bcfb931): bump golang.org/x/crypto from 0.30.0 to 0.31.0\n- [0881e079](https://github.com/quay/clair/commit/0881e07960cd3799d78707760876cf713e7de29a): bump the golang-x group with 2 updates\n- [f556ef16](https://github.com/quay/clair/commit/f556ef162483b3e39605d7073c0ab02108323da1): bump go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace\n- [bf8737a1](https://github.com/quay/clair/commit/bf8737a14ae5ab91e1747238f327feade8d9084b): bump golang.org/x/net in the golang-x group\n- [f1d9aae4](https://github.com/quay/clair/commit/f1d9aae4eb4952687c02f7ba1f9237e2d8510fd2): bump go.opentelemetry.io/otel/exporters/stdout/stdouttrace\n### Chore(Manifests)\n- [48b75fe4](https://github.com/quay/clair/commit/48b75fe45d534081c65d3a55e8206b8ca405f276): add anti-affinity rules\n### Ci\n- [a0a35fd7](https://github.com/quay/clair/commit/a0a35fd7b6f2aed63adf9feb2c5d5af76a93278e): Allow go test to access un-vendored dependencies\n### Cicd\n- [ab791a2e](https://github.com/quay/clair/commit/ab791a2ee21107beb49cd516807f1b15c21b085e): run multiarch tests without a full container\n- [935a61f3](https://github.com/quay/clair/commit/935a61f3142baf1d426d52cb20dddc74a259cc99): vendor modules into nightly source\n### Clairctl\n- [4c93f8ea](https://github.com/quay/clair/commit/4c93f8ea2611855503e9c0ca18e0d5e7f9aa55e7): Print a friendly error on panic\n -  [#2221](https://github.com/quay/clair/issues/2221)### Config\n- [0db9beaf](https://github.com/quay/clair/commit/0db9beaf1c5fa7da76d03a06672000666f62b591): add ability to disable enrichment\n- [7ab81b38](https://github.com/quay/clair/commit/7ab81b3866677804f19dffdf29422d6213a4ba2c): clean environment in example\n### Dev\n- [503215f5](https://github.com/quay/clair/commit/503215f5100fff89ea1656281394a151fd9cb8b5): rename dashboard.json file to clair.json\n- [65cd4244](https://github.com/quay/clair/commit/65cd4244e3b322e7a9c75482cdd6696b6ab24ec3): add a grafana dashboard for postgres stats\n### Docker\n- [10485679](https://github.com/quay/clair/commit/1048567920dd7aa56442c55c3b928a09f5f9899d): remove version line from docker-compose.yaml\n### Docker-Compose\n- [8c71b46e](https://github.com/quay/clair/commit/8c71b46e83770234c08e86dea1fa53f01c6a8ad8): update containers\n### Enrichments\n- [6527a9ec](https://github.com/quay/clair/commit/6527a9ec3e9ab360cb2ed18646041e628fd2dcda): disable enrichers if config option is set\n### Fix\n- [0a8c3864](https://github.com/quay/clair/commit/0a8c3864648d43fa2650742dba351efdd8c035fe): typo in variable name\n### Go.Mod\n- [6db583f7](https://github.com/quay/clair/commit/6db583f7dcffaa3d94ba7566df23b79dc430057e): Update Go version to 1.24.9 for CVE-2025-47907\n### Health\n- [b57b9fa6](https://github.com/quay/clair/commit/b57b9fa642ab2cb561fb205f5349fd81adb56c82): using atomic.Uint32\n### Introspection\n- [797c2f45](https://github.com/quay/clair/commit/797c2f45fbd8b3c6660a5ef5979e62ffa0b26107): implement OTLP support for metrics and traces\n### Misc\n- [5891f64b](https://github.com/quay/clair/commit/5891f64bd2451ddf62ad01176416aa8eccba9b79): remove API doc make target, CI check\n### Notifier\n- [a9a68e18](https://github.com/quay/clair/commit/a9a68e18dd8e4fec4d05a2e82dda92e14b7542e3): increase default durations to be more reasonable\n### Openapi\n- [8c540b96](https://github.com/quay/clair/commit/8c540b964f6b8afc99f6a82f60fede028fc739f1): rebuild OpenAPI spec\n### Signer\n- [1c6d0496](https://github.com/quay/clair/commit/1c6d0496ea19a72b0d0dcaff959b4554d19b7b9e): initialize before checking for PSK\n - Fixes [#2214](https://github.com/quay/clair/issues/2214) -  [#2221](https://github.com/quay/clair/issues/2221)### Stomp\n- [b2501ba3](https://github.com/quay/clair/commit/b2501ba3f277034283d1ecfb30b6fb808ab8ffc9): ignore Unsubscribe error in test\n- [0b8e3507](https://github.com/quay/clair/commit/0b8e3507a0425fde64932627245a66458cb853da): add deprecation notice\n- [684be8d0](https://github.com/quay/clair/commit/684be8d043d43b66915fbf0d8976c1230a15f9b3): catch test-specific error\n### Types/V1\n- [50d0164b](https://github.com/quay/clair/commit/50d0164b0cf9d305f4cc5af1f0a9787e67623ca9): add JSON API v1 types and schemas\n### Reverts\n- cicd: exclude darwin/arm64\n\n\n<a name=\"v4.8.0\"></a>\n## [v4.8.0] - 2024-10-09\n### 'Chore\n- [ab3a754e](https://github.com/quay/clair/commit/ab3a754e1408d8fed0160ce999536c8fd2f452f7): update claircore to v1.5.19\n- [f783b356](https://github.com/quay/clair/commit/f783b356ce3c7903641913ee25d0a384c19602db): update claircore to v1.5.18\n- [9286ab86](https://github.com/quay/clair/commit/9286ab86517a6738bd82150e695badde814f8bab): update claircore to v1.5.17\n### Admin\n- [d3467bad](https://github.com/quay/clair/commit/d3467bad8aab1f463b51ed22ad377b78ef687920): add pre v4.8.0 admin command to delete OVAL vulns\n- [d53780b6](https://github.com/quay/clair/commit/d53780b6ada05c763576457cc7d0a8f98147f29e): add a check for compatible migration version\n- [87c24a9c](https://github.com/quay/clair/commit/87c24a9c63b59ebfeae76f198ddace78ff095c54): add command to update go packages with norm_version\n- [02e6c925](https://github.com/quay/clair/commit/02e6c9256ea5514088b7817df4df13c62b45fc73): add pre v4.7.3 admin command to create index\n### All\n- [55294aaf](https://github.com/quay/clair/commit/55294aafae6d943edf09328ed52c4e6d92676966): fix incorrect API paths\n- [daa78ec2](https://github.com/quay/clair/commit/daa78ec25bf087e65a1b5ac387880666515cc359): fix some typos\n### Amqp\n- [8fcd294c](https://github.com/quay/clair/commit/8fcd294c6609be94d6d56b14ff2b39046e8f134c): migrate to maintained package\n -  [#1793](https://github.com/quay/clair/issues/1793)### Auto\n- [07b0ea7b](https://github.com/quay/clair/commit/07b0ea7b47e7b15867bb6c5209eca995faf119f9): improve log messages\n -  [#2092](https://github.com/quay/clair/issues/2092)### Build(Deps)\n- [5092198b](https://github.com/quay/clair/commit/5092198b0459e12ec2472dd74c6c7d60b33e847d): bump golang.org/x/time from 0.6.0 to 0.7.0\n- [e7b6deac](https://github.com/quay/clair/commit/e7b6deacd205a9e2138f91115c7d19c87ffb2365): bump golang.org/x/net from 0.29.0 to 0.30.0\n- [55fb7735](https://github.com/quay/clair/commit/55fb773530fe243935f3ceb59606d500db27820e): bump github.com/klauspost/compress from 1.17.9 to 1.17.10\n- [7a2e7186](https://github.com/quay/clair/commit/7a2e7186b5d33cbbb2b66fbfbcbaaad3fb60c210): bump github.com/prometheus/client_golang\n- [698d9170](https://github.com/quay/clair/commit/698d9170d5d88e79b80ee3a5aa5f4f8b2a929e99): bump github.com/rogpeppe/go-internal from 1.12.0 to 1.13.1\n- [7ec7e04f](https://github.com/quay/clair/commit/7ec7e04f6eaff5cf4cacc11839d4b167f685a4b1): bump go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace\n- [96ee336f](https://github.com/quay/clair/commit/96ee336fa33329d69819f00400a79491eca8952e): bump go.opentelemetry.io/otel/exporters/stdout/stdouttrace\n- [5fb41ed8](https://github.com/quay/clair/commit/5fb41ed8436ac629aef16d78658c3b7b6ce90232): bump golang.org/x/net from 0.28.0 to 0.29.0\n- [2a13e7b7](https://github.com/quay/clair/commit/2a13e7b751f934cfde15e64cebd8001686e4d839): bump peter-evans/create-pull-request from 6 to 7\n- [061b1e09](https://github.com/quay/clair/commit/061b1e091394f5ac49d18dab9727cf70f72dc0bc): bump github.com/prometheus/client_golang\n- [a2c920f4](https://github.com/quay/clair/commit/a2c920f4e8b82a753459fc772d763b3891d9f30c): bump go.opentelemetry.io/otel/exporters/stdout/stdouttrace\n- [bbaece4e](https://github.com/quay/clair/commit/bbaece4ef05e6cc1b5fc3f4de9ece173df2d908f): bump go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace\n- [24aff4e4](https://github.com/quay/clair/commit/24aff4e4362e5eef9cf77aba231c11da8d7b6c3d): bump go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp\n- [b203913a](https://github.com/quay/clair/commit/b203913af17ed8a5ba0a19514d52ea177618aafe): bump github.com/prometheus/client_golang\n- [96937294](https://github.com/quay/clair/commit/96937294d27187b80cd632b018c8e4aabdaf5d1a): bump github.com/grafana/pyroscope-go/godeltaprof\n- [01b57db6](https://github.com/quay/clair/commit/01b57db6fa2188a9630cec86239406d3ae706d2d): bump github.com/google/go-containerregistry\n- [7ceeaaa2](https://github.com/quay/clair/commit/7ceeaaa2e589e3a89414d2ede08799a359926b1d): bump github.com/go-stomp/stomp/v3 from 3.1.1 to 3.1.2\n- [c3ce1982](https://github.com/quay/clair/commit/c3ce198288a56c2d011cf114cae09704eee0e2bf): bump github.com/urfave/cli/v2 from 2.27.2 to 2.27.3\n- [95f5a5f2](https://github.com/quay/clair/commit/95f5a5f2afce16af5f7013025198d10294f5eb05): bump github.com/google/go-containerregistry\n- [1a5f342c](https://github.com/quay/clair/commit/1a5f342c73dc841a97e8f043e1f4fd7e310dea35): bump github.com/go-stomp/stomp/v3 from 3.1.0 to 3.1.1\n- [5821a5bf](https://github.com/quay/clair/commit/5821a5bfd0f4defe0f4cbb27bfc90102215a914d): bump golang.org/x/net from 0.26.0 to 0.27.0\n- [08587861](https://github.com/quay/clair/commit/0858786146e0b1971b4417b0c7744e035f1fef73): bump github.com/google/go-containerregistry\n- [74914938](https://github.com/quay/clair/commit/749149387eda7b4ecc564d9df24c6eb011fb0f94): bump go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace\n- [67bdbbbe](https://github.com/quay/clair/commit/67bdbbbef3b521a7cc94545a7428cc3f46be27d0): bump go.opentelemetry.io/otel/exporters/stdout/stdouttrace\n- [dd9d6760](https://github.com/quay/clair/commit/dd9d676048b92ed497ad8612da8ef175cca9675f): bump go.opentelemetry.io/otel from 1.27.0 to 1.28.0\n- [fcee4364](https://github.com/quay/clair/commit/fcee436402c57cfb1555b2380d972b2301415e65): bump github.com/klauspost/compress from 1.17.8 to 1.17.9\n- [3f229e99](https://github.com/quay/clair/commit/3f229e99b279bebd371c5b7fba26d610110751a9): bump github.com/google/go-containerregistry\n- [c5ae5021](https://github.com/quay/clair/commit/c5ae5021f3fd28ab25caf00bd6629e81015efef2): bump docker/build-push-action from 5 to 6\n- [7400db24](https://github.com/quay/clair/commit/7400db249415f5597f634c85979ae20b5c64f308): bump golang.org/x/net from 0.25.0 to 0.26.0\n- [74b377b8](https://github.com/quay/clair/commit/74b377b8f9bf340c427025de0dfe9b02cfa094f4): bump github.com/rs/zerolog from 1.32.0 to 1.33.0\n- [1fff0726](https://github.com/quay/clair/commit/1fff07260778fb19820ad3fe038be41d39e9a44a): bump go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace\n- [f2533fbf](https://github.com/quay/clair/commit/f2533fbf053c50c35548078091fb0596a8d5bdc4): bump go.opentelemetry.io/otel/exporters/stdout/stdouttrace\n- [5376a756](https://github.com/quay/clair/commit/5376a7563dd4fc7f2565d6e270f0a0cc6c22bad9): bump github.com/rabbitmq/amqp091-go from 1.9.0 to 1.10.0\n- [d82ab343](https://github.com/quay/clair/commit/d82ab34331da273e75ab63e27d92c3c5a9d91f94): bump golang.org/x/net from 0.24.0 to 0.25.0\n- [453d2c60](https://github.com/quay/clair/commit/453d2c60547c493ae8968ed45e72b26ad139e72c): bump github.com/urfave/cli/v2 from 2.27.1 to 2.27.2\n- [5323fa31](https://github.com/quay/clair/commit/5323fa3186a2695903c2737831f66a31dcb58dfa): bump go.opentelemetry.io/otel/exporters/stdout/stdouttrace\n- [3e1f5c15](https://github.com/quay/clair/commit/3e1f5c15a0713859ba51d5eab54e98b881b1ecb9): bump go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace\n- [71078832](https://github.com/quay/clair/commit/710788327dc2870e4e277b77dff338adb030c73a): bump go.opentelemetry.io/otel from 1.25.0 to 1.26.0\n- [1006287a](https://github.com/quay/clair/commit/1006287a5e9568c3deb30c13674f3baf24cdab2a): bump go.opentelemetry.io/otel/exporters/stdout/stdouttrace\n- [43f3a3e4](https://github.com/quay/clair/commit/43f3a3e4c426c9a160926aeb5008783a903307aa): bump go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace\n- [343515af](https://github.com/quay/clair/commit/343515af8838b521c61663fe846bb32662363f01): bump github.com/klauspost/compress from 1.17.7 to 1.17.8\n- [c3db2e4d](https://github.com/quay/clair/commit/c3db2e4dc61b8f9ba8039e655f159efd7c1d2d5b): bump github.com/quay/claircore from 1.5.25 to 1.5.26\n- [4cf0febf](https://github.com/quay/clair/commit/4cf0febfff33b734ffb021bfd2b35a8082320d4e): bump golang.org/x/sync from 0.6.0 to 0.7.0\n- [36d21edd](https://github.com/quay/clair/commit/36d21edd3d1471094f0b4737fcadcad8b41ee53b): bump golang.org/x/net from 0.22.0 to 0.24.0\n- [93a70b35](https://github.com/quay/clair/commit/93a70b35ee2c51544e87d7a1109e301be3baf821): bump go.opentelemetry.io/otel/sdk from 1.24.0 to 1.25.0\n- [da30be8b](https://github.com/quay/clair/commit/da30be8bad23248b514903fddbb6864953a7b636): bump github.com/google/go-containerregistry\n- [5a5e1776](https://github.com/quay/clair/commit/5a5e177685daaf45b38640a48658b6cbfc5ca36f): bump golang.org/x/net from 0.21.0 to 0.22.0\n- [d4ceeea2](https://github.com/quay/clair/commit/d4ceeea24f518abc409e0d60361a575f3b395cbe): bump github.com/go-jose/go-jose/v3 from 3.0.2 to 3.0.3\n- [d64064ce](https://github.com/quay/clair/commit/d64064ce59c2c0fbb1b0ea1f9e1ab27dfa39329d): bump github.com/prometheus/client_golang\n- [06c9ddab](https://github.com/quay/clair/commit/06c9ddab732512b03f9edc5f69d1551823154f7c): bump github.com/jackc/pgx/v4 from 4.18.1 to 4.18.3\n- [e4d79110](https://github.com/quay/clair/commit/e4d7911000add800242efd6fdaba557c69774052): bump github.com/go-stomp/stomp/v3 from 3.0.6 to 3.1.0\n- [d7c5821f](https://github.com/quay/clair/commit/d7c5821f888b01545247cb7a80e79b9d70caac36): bump go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace\n- [523ebf7f](https://github.com/quay/clair/commit/523ebf7fb0204171656b62d69c5ea3a8ba275772): bump go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp\n- [0803380f](https://github.com/quay/clair/commit/0803380f040f699cb08564899fe2a5d6a674d504): bump github.com/go-jose/go-jose/v3 from 3.0.1 to 3.0.2\n- [a3e0786c](https://github.com/quay/clair/commit/a3e0786c5984447725ab7cd7826cae108314dc57): bump go.opentelemetry.io/otel/exporters/stdout/stdouttrace\n- [684c3ac3](https://github.com/quay/clair/commit/684c3ac3310861186ff5a61b56c98b1c5facd4b0): bump peter-evans/create-pull-request from 6.0.0 to 6.0.1\n- [3fb2c921](https://github.com/quay/clair/commit/3fb2c921c83dfbe89ca92b7e46cb9bf0d98c84a7): bump go.opentelemetry.io/otel/exporters/stdout/stdouttrace\n- [51981290](https://github.com/quay/clair/commit/519812903d6048748ea5b29c5dd33889ae9afa50): bump go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace\n- [115cbb22](https://github.com/quay/clair/commit/115cbb2203768ade1a861358230767deab0fbd42): bump github.com/go-stomp/stomp/v3 from 3.0.5 to 3.0.6\n- [43b164e7](https://github.com/quay/clair/commit/43b164e78cfaf48ff317bd491712b2ceac9d2668): bump golang.org/x/net from 0.20.0 to 0.21.0\n- [acf2cdf6](https://github.com/quay/clair/commit/acf2cdf6305ea0b23021ddb55346d6dfabcc0fab): bump go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp\n- [0c7fe4dd](https://github.com/quay/clair/commit/0c7fe4dd821aff8c569d332938c1dd53b43f1aaa): bump go.opentelemetry.io/otel/sdk from 1.22.0 to 1.23.1\n- [16a1504a](https://github.com/quay/clair/commit/16a1504accbe3e33b01f639ac2038890eeef66fe): bump go.opentelemetry.io/otel from 1.22.0 to 1.23.1\n- [1f98abe7](https://github.com/quay/clair/commit/1f98abe77ca9ab3fb0cf8a2b68a692f3ebfb6c63): bump peter-evans/create-pull-request from 5.0.2 to 6.0.0\n- [fb5efb51](https://github.com/quay/clair/commit/fb5efb51ec338f5c8b1a57a0e6b73ec6c8867e84): bump github.com/klauspost/compress from 1.17.5 to 1.17.6\n- [8dbacd3c](https://github.com/quay/clair/commit/8dbacd3c0fd7f16bc6fdbc0cc3c3caf9536b0cd3): bump github.com/rs/zerolog from 1.31.0 to 1.32.0\n- [96d34f64](https://github.com/quay/clair/commit/96d34f64849f2acfe785b88f2e674e540d605f1a): bump github.com/google/go-containerregistry\n- [3bcf9aac](https://github.com/quay/clair/commit/3bcf9aacf00f574b32f7baee77466852e3d17dfe): bump github.com/klauspost/compress from 1.17.4 to 1.17.5\n- [19afbbbe](https://github.com/quay/clair/commit/19afbbbe284b0af634043a82947b2a6ed10f5a41): bump github.com/evanphx/json-patch/v5 from 5.8.0 to 5.9.0\n- [50eb4b52](https://github.com/quay/clair/commit/50eb4b5281f9bd8410c8595c17f373f2ccd014d7): bump github.com/google/uuid from 1.5.0 to 1.6.0\n- [4ed100ec](https://github.com/quay/clair/commit/4ed100ec4b4d33ac136b0394cc7c4fb06fd9a4bc): bump go.opentelemetry.io/otel/exporters/stdout/stdouttrace\n- [1d338051](https://github.com/quay/clair/commit/1d338051f374f78b3989ef92d8255be0ce7089b0): bump actions/cache from 3 to 4\n- [a0e1ba8b](https://github.com/quay/clair/commit/a0e1ba8bcb0a027e340511b36c29593eb5a11488): bump github.com/grafana/pyroscope-go/godeltaprof\n- [1ab0557b](https://github.com/quay/clair/commit/1ab0557b60d3e8843dc37132184c435f0c2965da): bump go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace\n- [fcf0ccdd](https://github.com/quay/clair/commit/fcf0ccdde5b06410c1e758535c35271ba324f25b): bump go.opentelemetry.io/otel/sdk from 1.21.0 to 1.22.0\n- [6fe56438](https://github.com/quay/clair/commit/6fe564389c53b74e692f06afb7414927740f4e3d): bump github.com/evanphx/json-patch/v5 from 5.7.0 to 5.8.0\n- [6ef2554e](https://github.com/quay/clair/commit/6ef2554ef2f98aeefc22a9bc6836394e8b0523e0): bump golang.org/x/net from 0.19.0 to 0.20.0\n- [7b48e897](https://github.com/quay/clair/commit/7b48e897374e5e67e7d59753ee28175361d59b3a): bump golang.org/x/sync from 0.5.0 to 0.6.0\n- [c25d841a](https://github.com/quay/clair/commit/c25d841aba4e3cc0996c62d5a5a8bd6eb6c921a5): bump github.com/quay/zlog from 1.1.7 to 1.1.8\n- [94b57fa0](https://github.com/quay/clair/commit/94b57fa0c8c465b579e357a30e09fbebb2e52629): bump github.com/prometheus/client_golang\n- [ad2c872c](https://github.com/quay/clair/commit/ad2c872c05584a73f70f214db52a9bbfce72c530): bump github.com/urfave/cli/v2 from 2.26.0 to 2.27.1\n- [2159bfb5](https://github.com/quay/clair/commit/2159bfb5f10819d6f66e1f8b17c4df9ed8d50153): bump github.com/google/uuid from 1.4.0 to 1.5.0\n- [aaa335b3](https://github.com/quay/clair/commit/aaa335b3e8dcd73b0881ab2d50a8f0a1fecda177): bump golang.org/x/crypto from 0.16.0 to 0.17.0\n- [9c588cf5](https://github.com/quay/clair/commit/9c588cf5a7dfacb37f7e781870e3c884b562cb5a): bump github.com/google/go-containerregistry\n- [cbc166d6](https://github.com/quay/clair/commit/cbc166d61c9e0c427cd78077feb5ab3635ca82ee): bump actions/upload-artifact from 3 to 4\n- [355cab98](https://github.com/quay/clair/commit/355cab9896e5ccd892e41698fdc3599b4aa3f11e): bump actions/download-artifact from 3 to 4\n- [7b7ff298](https://github.com/quay/clair/commit/7b7ff298a524bee4582b475dcc162c01fc2e8e47): bump github.com/ugorji/go/codec from 1.2.11 to 1.2.12\n- [45625c51](https://github.com/quay/clair/commit/45625c51e82f0045c441e0f55d7869a978bd97f4): bump github.com/urfave/cli/v2 from 2.25.7 to 2.26.0\n- [b6b39706](https://github.com/quay/clair/commit/b6b397064846253c7229d27ad90919fcc025a334): bump actions/setup-go from 4 to 5\n- [913a5114](https://github.com/quay/clair/commit/913a51146ccee020ff19895f3ffe96fdc629a3ad): bump go.opentelemetry.io/otel/exporters/stdout/stdouttrace\n- [71c66638](https://github.com/quay/clair/commit/71c66638229e9670e1bf5da3d8e77446795c386d): bump github.com/klauspost/compress from 1.17.2 to 1.17.4\n- [825dddc1](https://github.com/quay/clair/commit/825dddc106931ccbb4ee1008ba647b15cfe595c3): bump golang.org/x/net from 0.17.0 to 0.19.0\n- [e7314325](https://github.com/quay/clair/commit/e73143254426f46a75bc178c8aaddf2da097735a): bump actions/stale from 8 to 9\n- [99291347](https://github.com/quay/clair/commit/99291347704de0f22afe657b9111e23669dc18b1): bump github.com/quay/zlog from 1.1.5 to 1.1.7\n- [d75c2c40](https://github.com/quay/clair/commit/d75c2c40ef3d4f8bae6f38a6dd13d2f7d1f10668): bump go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace\n- [83a935dd](https://github.com/quay/clair/commit/83a935dd39b92b90a62b7f376d7f9be7da1dacd3): bump go.opentelemetry.io/otel/sdk from 1.20.0 to 1.21.0\n- [4db3b77e](https://github.com/quay/clair/commit/4db3b77e1c2630a8b6c0014fbd6517efda0872e7): bump github.com/go-jose/go-jose/v3\n- [1b2248b9](https://github.com/quay/clair/commit/1b2248b9ac524b0c4e83c4290f26ccb94da78b9f): update opentelemetry modules\n -  [#1909](https://github.com/quay/clair/issues/1909) -  [#1911](https://github.com/quay/clair/issues/1911) -  [#1912](https://github.com/quay/clair/issues/1912) -  [#1913](https://github.com/quay/clair/issues/1913)- [4a84b949](https://github.com/quay/clair/commit/4a84b9491c0309af8ec4e0dce6b5c5daf0089d2c): bump github.com/google/uuid from 1.3.1 to 1.4.0\n- [efc1ab07](https://github.com/quay/clair/commit/efc1ab07d1fbd101d584bbfe8a883fbf6cf8552d): bump golang.org/x/time from 0.3.0 to 0.4.0\n- [61aa3ebd](https://github.com/quay/clair/commit/61aa3ebd9671f7e23d152971328a268482fe095d): bump golang.org/x/sync from 0.4.0 to 0.5.0\n- [54eb2e85](https://github.com/quay/clair/commit/54eb2e857c033483ea5acddd8254f6ee080ab77b): bump github.com/google/go-cmp from 0.5.9 to 0.6.0\n- [b0497e58](https://github.com/quay/clair/commit/b0497e58b2c61a6462bb7a7fabf5302230b3ea28): bump github.com/klauspost/compress from 1.17.0 to 1.17.2\n- [a90ecc45](https://github.com/quay/clair/commit/a90ecc4514385e011e971b97adc157062cddecf0): bump go.opentelemetry.io/otel/sdk from 1.17.0 to 1.19.0\n- [55dc551f](https://github.com/quay/clair/commit/55dc551f39550ff7857acf500a7f80666b213bf6): bump go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace\n- [5a8c21a0](https://github.com/quay/clair/commit/5a8c21a0271a5dac4e83e100fe4f291552f0a429): bump github.com/google/go-cmp in /config\n- [f3072d19](https://github.com/quay/clair/commit/f3072d198700320c2144d2b1409d59440886084b): bump go.opentelemetry.io/otel from 1.18.0 to 1.19.0\n- [8468d861](https://github.com/quay/clair/commit/8468d861204350005c9eff1469a67cd4b409a915): bump golang.org/x/net from 0.16.0 to 0.17.0\n- [afafe835](https://github.com/quay/clair/commit/afafe8357a948d9b6bcd8f82a7b8fad86512a66e): bump golang.org/x/net from 0.15.0 to 0.16.0\n- [f162e1ce](https://github.com/quay/clair/commit/f162e1ce4b4444d69227b90f8bfc440c8514bcb9): bump github.com/rs/zerolog from 1.30.0 to 1.31.0\n- [e6f72bc4](https://github.com/quay/clair/commit/e6f72bc47903135bd1978988f040b14577e075b2): bump go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp\n- [c0eef84b](https://github.com/quay/clair/commit/c0eef84b28488258f0a5e42233240c04f52ffb2f): bump go.opentelemetry.io/otel/exporters/stdout/stdouttrace\n- [7129bacf](https://github.com/quay/clair/commit/7129bacf79302b91edf0482b3ca271d85dd66642): bump github.com/evanphx/json-patch/v5 from 5.6.0 to 5.7.0\n- [6969e003](https://github.com/quay/clair/commit/6969e003d05dfb7777afa0d5a628f8d445dfa2d3): bump docker/setup-buildx-action from 2 to 3\n- [606c5c9b](https://github.com/quay/clair/commit/606c5c9b36bdf18fa243024b69e002798e6146f7): bump docker/login-action from 2 to 3\n- [24eb3f71](https://github.com/quay/clair/commit/24eb3f7113929b65b9257b60ba9cba923c575f09): bump docker/build-push-action from 4 to 5\n- [dbaebb58](https://github.com/quay/clair/commit/dbaebb5821b567255e50315c60a8ee00b9b541fb): bump docker/setup-qemu-action from 2 to 3\n- [a31be2e2](https://github.com/quay/clair/commit/a31be2e2cae12d3ca09fc19f5f97ab2ceb5aaa85): bump actions/checkout from 3 to 4\n- [480996b1](https://github.com/quay/clair/commit/480996b1f386a117feff025d56b5d6c0eab7f77c): bump go.opentelemetry.io/otel/exporters/jaeger\n- [bc21afa0](https://github.com/quay/clair/commit/bc21afa0171521104b21c6175099cb3c5cb48175): bump github.com/google/uuid from 1.3.0 to 1.3.1\n- [5ae4f0fa](https://github.com/quay/clair/commit/5ae4f0fa3db4e5154d178bccaa2a652f11576000): bump github.com/google/go-containerregistry\n- [56cd1851](https://github.com/quay/clair/commit/56cd1851f463d31e6c1f552d42ea35a591186a60): bump github.com/rs/zerolog from 1.29.1 to 1.30.0\n- [67b92e71](https://github.com/quay/clair/commit/67b92e71ca3ae3017add43f846e9c4b83f9fd29f): bump golang.org/x/net from 0.12.0 to 0.15.0\n- [a478ce91](https://github.com/quay/clair/commit/a478ce918566144d58b50ffb892cb33a11992036): bump github.com/pyroscope-io/godeltaprof\n### Chore\n- [05680a2b](https://github.com/quay/clair/commit/05680a2bd328410c3aa19de39f3226acc3c234d6): v4.8.0 changelog bump\n- [94113d95](https://github.com/quay/clair/commit/94113d9539b83a25829a540afdb965a579d96c6e): update claircore to v1.5.32\n- [e77deb98](https://github.com/quay/clair/commit/e77deb9820c6d8896c377d228f5741ea57b7bc2f): update config module to v1.4.1\n- [e5fca953](https://github.com/quay/clair/commit/e5fca9539437286efbbbe5ead42972607ac149a5): update references to rhel updater to rhel-vex updater\n- [64b66ff9](https://github.com/quay/clair/commit/64b66ff96e05f20c4410a78292e293a9bfa744c8): update go version to specific patch\n- [89ebd521](https://github.com/quay/clair/commit/89ebd521d32d76eb4a37d150fa5e59d193542ff5): update go version to 1.22\n- [9333770e](https://github.com/quay/clair/commit/9333770e3e6f2434d291ef3619c314cd3d4d213f): update claircore to v1.5.31\n- [93fa883d](https://github.com/quay/clair/commit/93fa883df4a24f3fd681295012fd84821e986680): update claircore to v1.5.30\n- [1209772d](https://github.com/quay/clair/commit/1209772de458082e25aee042aa44febb2ec7fa46): update claircore to v1.5.29\n- [3c623553](https://github.com/quay/clair/commit/3c6235532215d96a54210ca88f6eae692cdbda17): run the go formatting over the repo\n- [7703b4a2](https://github.com/quay/clair/commit/7703b4a23b4e1a4fcdf391a09ce1d7252bdc0722): fix some comments\n- [7d3f12e3](https://github.com/quay/clair/commit/7d3f12e34ef637d7f4a7682ebc0aba7e1ffe4210): use the merge-multiple directive when downloading binaries\n- [b5a0d8a6](https://github.com/quay/clair/commit/b5a0d8a60c643c0867c850634145f6192d694667): update claircore to v1.5.28\n- [ac255112](https://github.com/quay/clair/commit/ac25511280ca75fd6b7b32dcdadfdd01f5fdc9db): Add merge step when creating release binaries\n- [5dc73b16](https://github.com/quay/clair/commit/5dc73b16ffffe7a5245355a5c56a726a4e1801e4): update go version for release\n- [ea990567](https://github.com/quay/clair/commit/ea990567b95d61f0f7438ba6b8ad2519dda3288a): update claircore to v1.5.27\n- [0bf9286e](https://github.com/quay/clair/commit/0bf9286e2c0e6c1591616729aa5b35386471efd3): update production manifest with new tmp dir\n- [6a3ce17f](https://github.com/quay/clair/commit/6a3ce17f0a45f962f1d1d25eccfd99d235bbfeff): update go version\n- [3e5740e0](https://github.com/quay/clair/commit/3e5740e038742aa29fe193d2dee70616eca17eaf): remove repetitive word\n- [222f2273](https://github.com/quay/clair/commit/222f2273813baec3078404e2a74c1227784db262): update claircore to v1.5.25\n- [7ac4609b](https://github.com/quay/clair/commit/7ac4609b00cbe6093af07ab0320fe3275fbbb313): update claircore to v1.5.24\n- [bad8abe5](https://github.com/quay/clair/commit/bad8abe5786b17c8291870864f04c5bec70cf758): update claircore to v1.5.23\n- [c81b3b9a](https://github.com/quay/clair/commit/c81b3b9a57ccd5a549ed8f29d1b646be752893c6): update claircore to v1.5.22\n- [a9b5e91d](https://github.com/quay/clair/commit/a9b5e91d7c09bea896d55575b2c88a97bc919271): update claircore to v1.5.21\n- [6de0d807](https://github.com/quay/clair/commit/6de0d807a5d63d5b10d63c274c297fcf095a308b): Add Go 1.22 support via moved godeltaprof dependancy bump\n- [b65445ce](https://github.com/quay/clair/commit/b65445ce7c0874a61fbccdd5df33419a62268d49): clean up sample config\n- [a359eb01](https://github.com/quay/clair/commit/a359eb01e0b9aaa4eb3e6433af8812a2de9908b5): migrate go-jose to maintained version\n- [5cf5fb8d](https://github.com/quay/clair/commit/5cf5fb8dba87b03ca37ed5a60856f5b4c3e72404): update claircore to v1.5.20\n- [180fa4f4](https://github.com/quay/clair/commit/180fa4f444ff0affcc6ee9dd4436f97ea75c2dab): bump claircore to v1.5.16\n- [696b266e](https://github.com/quay/clair/commit/696b266e18205b216a2a4e4cdb95fd814892ef57): bump claircore to v1.5.15\n- [2829eacf](https://github.com/quay/clair/commit/2829eacfbe5eb89cb8e5261d136354da50300a82): bump claircore to v1.5.14\n### Cicd\n- [dbcfe30d](https://github.com/quay/clair/commit/dbcfe30d9225b65e3f5d4bd117dfe437ec8e057a): tweak login behavior\n- [6861b804](https://github.com/quay/clair/commit/6861b8047fd8e64ee285d48319cd40939be86367): remove second go-caching action\n- [c42bee62](https://github.com/quay/clair/commit/c42bee62e2fcad39743be02191055d967d5a15a6): improve nightly script output\n- [08581d82](https://github.com/quay/clair/commit/08581d82726f60d26c4508400883e936d270909c): tweaks to the set-image-expiration action\n- [3b650c56](https://github.com/quay/clair/commit/3b650c56a5ecd4242ecb4926a0b4e62a7a1ef691): fix nightly build\n- [6884969b](https://github.com/quay/clair/commit/6884969b37239a30e0966e71adcafb5ce51f74dc): add /var/tmp mount to make sure it's on a real filesystem\n- [139aed21](https://github.com/quay/clair/commit/139aed21d72f91d007302fa4df690ff17a574c5d): reorganize the docker test so that it's less error-prone\n- [b48682a4](https://github.com/quay/clair/commit/b48682a4260353ed6be503913020baaa4d028924): remove comment that the linter complained about\n- [bf7005f0](https://github.com/quay/clair/commit/bf7005f091ae65ac6ecb481b88f0cece30bc0493): add `/fast-forward` command\n- [d11a2602](https://github.com/quay/clair/commit/d11a26026e871d42c6dd8c9b4f97d53b4272d029): add container version skew check\n- [fd153765](https://github.com/quay/clair/commit/fd1537652df13da6a988e1eb8822e416dae57866): update testing workflow\n- [23a8c33d](https://github.com/quay/clair/commit/23a8c33d7f062a61d5fe96b1821dd7bcea80c24c): don't upload workspace on failure\n- [6f3b1347](https://github.com/quay/clair/commit/6f3b13479a40b3285fbb2cfd633fc8815db98a1e): update actions/cache version\n- [0604f1e6](https://github.com/quay/clair/commit/0604f1e6004e10d92f06c67552b7604dcd98f77f): change version specifiers to be major-version only\n- [718ef948](https://github.com/quay/clair/commit/718ef948653b85198caaafb50db1ca64fe5706e4): make nightly script shellcheck-clean\n### Clair\n- [ba6fc371](https://github.com/quay/clair/commit/ba6fc37173c2f0fae701482a3d1e758c1988ce04): add platform-specific signals\n- [76a5d50b](https://github.com/quay/clair/commit/76a5d50b0c2fedc4a426988955436a47fb480ac8): break cancellation chain for request contexts\n- [b0086d80](https://github.com/quay/clair/commit/b0086d800b113d038f8a4b66fcf134a5b41105b3): redo shutdown structure\n -  [#1946](https://github.com/quay/clair/issues/1946)### Clairctl\n- [13acc582](https://github.com/quay/clair/commit/13acc5828603177ab9b0cf5b871c65dbbdaa6ed7): warn when range requests are not honored\n### Cmd\n- [7de8cea7](https://github.com/quay/clair/commit/7de8cea790f5caed8621a576f20ccae20078bb40): add exported source date\n- [7121ceaa](https://github.com/quay/clair/commit/7121ceaad9dd92c3b6229182f06db0ea3a1424e0): annotate fake key for gitleaks\n### Compress\n- [c90a55fd](https://github.com/quay/clair/commit/c90a55fd056d18b18b0f64f02320a226ad1c34e7): update compression middleware\n### Config\n- [33a77438](https://github.com/quay/clair/commit/33a7743867d99a1ad96c146ff618a6d27c5f9b0b): update minimum TLS version for server\n- [e0a1f235](https://github.com/quay/clair/commit/e0a1f235b6bf26010f932dc732043d79d1b56f3f): Update comment to describe currently supported updaters\n- [36210370](https://github.com/quay/clair/commit/36210370be51136471d12968751f32367b2e9871): add Sentry config\n- [33cc3e5c](https://github.com/quay/clair/commit/33cc3e5c0199841c108a95e88c4d764739f21e80): add OTLP configuration types\n- [f503d670](https://github.com/quay/clair/commit/f503d6703d60e66c3186d727fbc670988ea342e3): fix typo\n### Contrib\n- [74974320](https://github.com/quay/clair/commit/74974320f4a1358911fdc263d2060839ca25cb2d): correct position of startupProbe spec\n- [5ad0d6be](https://github.com/quay/clair/commit/5ad0d6be2d06cfe9e6805a565f5f5a569583a5a0): update `build_and_deploy.sh` script\n- [accee22f](https://github.com/quay/clair/commit/accee22ffd15d8ad8da957b3946b5ab03120999b): account for different container engine clients\n- [1160febe](https://github.com/quay/clair/commit/1160febe28589c4eb85bef8bc5e51e4c7db82907): update build script to use podman\n- [f19b59bd](https://github.com/quay/clair/commit/f19b59bd3efad37aa199c62c5bc5e5914d8d4143): remove rms that were needed for previous fetcher\n- [b60d8266](https://github.com/quay/clair/commit/b60d8266700903ad06c2ec65842e0f2383d352ab): update dashboard regex\n- [4405fdad](https://github.com/quay/clair/commit/4405fdad0274fdfebb2cb0a7e1214cdb36c74ede): simplify openshift/pr_check.sh\n- [16bd3666](https://github.com/quay/clair/commit/16bd36669a0274815f6766f78dbb37d05d250646): add grafana dashboards for deletion metrics\n### Contrib/Openshfit\n- [89af3db1](https://github.com/quay/clair/commit/89af3db111a317abeb1b4c409840aa8f7660dcbf): only start buildkitd container if needed\n### Contrib/Openshift\n- [ab6e9e07](https://github.com/quay/clair/commit/ab6e9e07dd04130f7bf0b6733071f530cea7769b): login shenanigans\n- [002df72b](https://github.com/quay/clair/commit/002df72bfe3ef5f0b89c24f8e6a640a330de0bf5): avoid patching when using upstream images\n### Doc\n- [244183ee](https://github.com/quay/clair/commit/244183ee2072d48b6bd2eeadd3acebd0f6eb7ffe): fix typo\n### Dockerfile\n- [f7abfe50](https://github.com/quay/clair/commit/f7abfe50c2ea47a60601d05d9137833b2336b8bd): update with new syntax and features\n- [e2fbf199](https://github.com/quay/clair/commit/e2fbf1998bd45c99bc44382798f240d08651dd6c): add `GOTOOLCHAIN`\n- [e871998f](https://github.com/quay/clair/commit/e871998fb9cb01153fd307f446c5c71561fafad8): tweak ignores\n- [d78d3beb](https://github.com/quay/clair/commit/d78d3beb15fae2204eb7ea86e51474e015ac936f): remove sh loop\n### Docs\n- [038966e2](https://github.com/quay/clair/commit/038966e2ba65cd88ad85d1828de71b8fc61e7549): add building and Makefile usage sections\n- [137b6c50](https://github.com/quay/clair/commit/137b6c50640688b196106e64452620e28f56c8f2): add mention of disk space path and usage\n- [1e78f45a](https://github.com/quay/clair/commit/1e78f45a4e213c1d73f5a745b08e30aa94d7f7c8): add OTLP configuration to prose documentation\n- [eb54b889](https://github.com/quay/clair/commit/eb54b8896f6fd93bd67d6b6c0875cfff426698d7): add dropins to prose documentation\n -  [#1783](https://github.com/quay/clair/issues/1783)### Documentation\n- [80482345](https://github.com/quay/clair/commit/804823453f154b6fb3afa602320443df4621779f): add more information on how to test and get started\n### Documentation\n- [38b72352](https://github.com/quay/clair/commit/38b723529f11e02d595c3f04dccbc86d600c2932): correct stale configuration options\n### Httptransport\n- [20582315](https://github.com/quay/clair/commit/205823151a20ab7e2f167042b3a3f6344b3077c0): fix test flake\n- [df348dc9](https://github.com/quay/clair/commit/df348dc9b7b2deea92083fb6462e76c12db41493): GET vuln report returns 404 when indexing in-progress\n- [e84883f7](https://github.com/quay/clair/commit/e84883f7f6c2c962cbd70e0affa8d229aff58c9d): change api error handling to panic internally\n- [c7920962](https://github.com/quay/clair/commit/c79209620b424729ca317631e809b2f1eb6a9448): add metrics test\n- [15732398](https://github.com/quay/clair/commit/15732398f24adf20984f187c71ba8edabe4fd1af): add unauthenticated \"/robots.txt\" endpoint\n- [201ed2be](https://github.com/quay/clair/commit/201ed2be9d487773d8878ef894d99f221c6161db): add \"robots.txt\" endpoint\n- [5262f773](https://github.com/quay/clair/commit/5262f773924d56f418175fee5e7fde188bf6fb83): add client-close detection\n- [e97f6b3c](https://github.com/quay/clair/commit/e97f6b3cbaf291b45c78f6534833f0d05108954a): use compression middleware\n- [0d2bf7e6](https://github.com/quay/clair/commit/0d2bf7e693ccc61043a23c8cc2ff0bfad4bb926d): lints\n- [d4b9d30f](https://github.com/quay/clair/commit/d4b9d30fc1083db5ed00f812946bde3d56461db0): rework constructor\n- [067bf861](https://github.com/quay/clair/commit/067bf861976632afca16d941f12202b6e71066a5): update DiscoveryHandler to new style\n- [7a1186e3](https://github.com/quay/clair/commit/7a1186e31a89cd1bbf7fc086b2dc47bd2a3f7b00): re-instrument handlers with new primitives\n- [bddbc57b](https://github.com/quay/clair/commit/bddbc57bad5ed1e933b3fd74e55fa080802dfdee): exit goroutine in error helper\n### Httputil\n- [1fd77f0d](https://github.com/quay/clair/commit/1fd77f0dcb5cee865e9656365fb9660439a2f28f): add test for non-OK statuses\n- [0cde61bf](https://github.com/quay/clair/commit/0cde61bfd5557e90a6e7e59d66fd24db9cc608e8): add response recorder\n### Initialize\n- [4686fb46](https://github.com/quay/clair/commit/4686fb46c2f7c4050dd80e3d99124201e69b11a3): use defaults for NewRemoteFetcher\n### Introspection\n- [c31e40e3](https://github.com/quay/clair/commit/c31e40e349827a5d963ca5361cc961cdcf4dc567): lints\n- [8e1a7bd8](https://github.com/quay/clair/commit/8e1a7bd8a60179677a96579470d46f3f5934efd2): allow trace shutdown hook full timeout\n### Makefile\n- [95a765f4](https://github.com/quay/clair/commit/95a765f46b7d31efab121b555bb5d57f2beb00b7): fix direct `go` command\n- [a9a8ec98](https://github.com/quay/clair/commit/a9a8ec9896e3c77ce602c3253d8872210cc30618): make `buildctl` usage more convenient\n- [2c093d9c](https://github.com/quay/clair/commit/2c093d9c113f7fd51529d60cee8f2a21de648f7e): force line endings for `git archive`\n- [f7bfacf1](https://github.com/quay/clair/commit/f7bfacf17a91ec245b1238c959242b17f2d41274): rebuild the make setup\n- [7cc2107b](https://github.com/quay/clair/commit/7cc2107b4b2b820c108f793f2a19084fd440f929): updates\n### Openshift\n- [6bb55a21](https://github.com/quay/clair/commit/6bb55a2128a5184164af4be37b73210f279d9bb3): add backstop cron manifest\n- [3615748d](https://github.com/quay/clair/commit/3615748dad2effa94a8def781a2305375354176c): handle multiple Dockerfiles in build script\n- [5f36fc12](https://github.com/quay/clair/commit/5f36fc125ebccc6465b445b9b83ec19dbbffdc87): have the pr_check script \"dry run\" a build\n- [3d3c03ce](https://github.com/quay/clair/commit/3d3c03ce5a008b9253553c2c2b7d1746e7632171): add \"dry run\" flag\n- [135af0e0](https://github.com/quay/clair/commit/135af0e02df0f3a3d2a2d1364a0b929e062732a7): make build_and_deploy script shellcheck-clean\n### Quaybackstop\n- [e5e7ba5a](https://github.com/quay/clair/commit/e5e7ba5a9ed18d984d89b9814e5aa517ae2c3b6f): add backstop GC command\n### README\n- [abd13784](https://github.com/quay/clair/commit/abd137848bc9fa8300d8a454b20082020c890d47): format nit\n### Stomp\n- [3de24d71](https://github.com/quay/clair/commit/3de24d7191005449dcc5de9a6c2ba19a6f5b26b0): guard against race in test\n### Webhook\n- [41cda1fb](https://github.com/quay/clair/commit/41cda1fbb963dfe8f7474dd09f5727dfae6e11ac): move+update debug server\n\n<a name=\"v4.7.4\"></a>\n## [v4.7.4] - 2024-05-01\n### Build(Deps)\n- [3ebd889c](https://github.com/quay/clair/commit/3ebd889cb37603e071ba0d8fb8ba631702b13414): bump peter-evans/create-pull-request from 6.0.0 to 6.0.1\n- [b7566a0f](https://github.com/quay/clair/commit/b7566a0feedc5de0227a5bfd6e2921014ec68704): bump peter-evans/create-pull-request from 5.0.2 to 6.0.0\n- [4db2f09b](https://github.com/quay/clair/commit/4db2f09be2330e4dc9061dc2e9d44d8979b8c34d): bump actions/cache from 3 to 4\n- [6cef8311](https://github.com/quay/clair/commit/6cef8311255b024dd3a1fc0a70f7edf38969906c): bump actions/upload-artifact from 3 to 4\n- [5ed80215](https://github.com/quay/clair/commit/5ed80215fa17adb7e72ba3d9267368474afd411e): bump actions/download-artifact from 3 to 4\n- [c9e1f56b](https://github.com/quay/clair/commit/c9e1f56b224f25aacd5df861563006520e63297b): bump actions/setup-go from 4 to 5\n- [3ab3de55](https://github.com/quay/clair/commit/3ab3de558380fff38b8503de914c1b9c0611c4c8): bump actions/stale from 8 to 9\n- [591188f0](https://github.com/quay/clair/commit/591188f0a877b81878209be4b9185d4cb7c404f3): bump docker/setup-buildx-action from 2 to 3\n- [7ef6ef6b](https://github.com/quay/clair/commit/7ef6ef6b7587386b80cef9fbe7269b0083c8f039): bump docker/login-action from 2 to 3\n- [5597e7cc](https://github.com/quay/clair/commit/5597e7ccb94c113cc8344e8289be886049b23f63): bump docker/build-push-action from 4 to 5\n- [14d7f2b4](https://github.com/quay/clair/commit/14d7f2b4927fa9e4debc9005fb6dc620a6a47833): bump docker/setup-qemu-action from 2 to 3\n- [1204db98](https://github.com/quay/clair/commit/1204db98853cdfdce7790f3d3276d4617e4e3000): bump actions/checkout from 3 to 4\n### Chore\n- [4170798b](https://github.com/quay/clair/commit/4170798b6d464be0b8f74b1979785a17ad71dbd0): 4.7.4 changelog bump\n- [96dc6074](https://github.com/quay/clair/commit/96dc60748b492df1cb4af3761c9c44c10266ed09): Add merge step when creating release binaries\n- [a1c7eb7c](https://github.com/quay/clair/commit/a1c7eb7c8cce687e12fcc056be1acacbdc608a31): update go version for release\n- [6eeb9393](https://github.com/quay/clair/commit/6eeb9393b539cf49a10c42e247b833ce7e040607): update claircore to v1.5.27\n- [809dd5ab](https://github.com/quay/clair/commit/809dd5ab474994eb9a32599f4257437fde995064): update go version\n### Cicd\n- [e6378d03](https://github.com/quay/clair/commit/e6378d0333085a072cf73bfa32228af24b710b05): add container version skew check\n- [2ba3ecc0](https://github.com/quay/clair/commit/2ba3ecc0a66679dcd82f7015695db5b2a3f0c02a): update testing workflow\n- [ae135c49](https://github.com/quay/clair/commit/ae135c4956358d6c18109438b6e59170100787ea): don't upload workspace on failure\n- [7222dc88](https://github.com/quay/clair/commit/7222dc88d4fe919cc2b88e2fe7587061a881b794): change version specifiers to be major-version only\n### Clairctl\n- [2a2ba37f](https://github.com/quay/clair/commit/2a2ba37f6404e377d702e50f78bccd172b08b03f): warn when range requests are not honored\n### Dockerfile\n- [5547b96a](https://github.com/quay/clair/commit/5547b96aeff795da43fbc6e2dc8ec7b5dda7691d): remove sh loop\n### Docs\n- [3753415b](https://github.com/quay/clair/commit/3753415b4e2b6bc9b63a940b8b2101d82ab523ef): add mention of disk space path and usage\n### Httptransport\n- [c6df986f](https://github.com/quay/clair/commit/c6df986fddfe5121b6fd9dbfff0ccca35c55cb71): GET vuln report returns 404 when indexing in-progress\n### Initialize\n- [9828576a](https://github.com/quay/clair/commit/9828576af20c966e0dbe99bbac68d3e80b07baa1): use defaults for NewRemoteFetcher\n\n<a name=\"v4.7.3\"></a>\n## [v4.7.3] - 2024-02-26\n### Admin\n- [9517c7be](https://github.com/quay/clair/commit/9517c7bed060d575869a2cbaaa5a255d3714a0eb): add a check for compatible migration version\n -  [#1915](https://github.com/quay/clair/issues/1915)- [5d689efb](https://github.com/quay/clair/commit/5d689efb908aaa01290140a0f8b4e022588226dd): add command to update go packages with norm_version\n -  [#1915](https://github.com/quay/clair/issues/1915)### Chore\n- [e5a896c9](https://github.com/quay/clair/commit/e5a896c9b939e7e59d92be4f17805f0ed70ea89e): v4.7.3 changelog bump\n- [d17ee97b](https://github.com/quay/clair/commit/d17ee97bdccdc46ba25c8c7de151bb84f84236ef): update claircore to v1.5.25\n -  [#1990](https://github.com/quay/clair/issues/1990) -  [#1957](https://github.com/quay/clair/issues/1957) -  [#1942](https://github.com/quay/clair/issues/1942)### Config\n- [6ba32131](https://github.com/quay/clair/commit/6ba32131be6a79c4d5d070e666e81bfedcc09798): update minimum TLS version for server\n -  [#1945](https://github.com/quay/clair/issues/1945)\n<a name=\"v4.7.2\"></a>\n## [v4.7.2] - 2023-10-09\n### 'Chore\n- [9a3cde3b](https://github.com/quay/clair/commit/9a3cde3b16bb0c0e02fb7e128ff86e255ec6112f): update claircore to v1.5.19\n### Admin\n- [5a825a07](https://github.com/quay/clair/commit/5a825a07191e62f3214700df519202806a39d6c9): add pre v4.7.3 admin command to create index\n### Chore\n- [0729ad2a](https://github.com/quay/clair/commit/0729ad2a36721f9fe99196370a9f3cd31fdbd4b7): bump claircore to v1.5.16\n### Contrib\n- [04f36991](https://github.com/quay/clair/commit/04f36991e19cfb44423cbb2e96ccfcd786dd85c5): add grafana dashboards for deletion metrics\n### Docs\n- [8a2d99f4](https://github.com/quay/clair/commit/8a2d99f4da70de6bdf14858c6163a2e3b8042782): add dropins to prose documentation\n -  [#1783](https://github.com/quay/clair/issues/1783) -  [#1806](https://github.com/quay/clair/issues/1806)\n<a name=\"v4.7.1\"></a>\n## [v4.7.1] - 2023-08-10\n### Build(Deps)\n- [bd4bdbf6](https://github.com/quay/clair/commit/bd4bdbf68c62eb9982e028cafcabd58eb4e91c6c): bump github.com/pyroscope-io/godeltaprof\n### Chore\n- [25ab0f4e](https://github.com/quay/clair/commit/25ab0f4e01ac08870b6e8fe9cccc134a011a4f4f): bump claircore to v1.5.15\n- [4bf37a11](https://github.com/quay/clair/commit/4bf37a11861ca89c56dc94db9b1e002ef3d44265): bump claircore to v1.5.14\n\n<a name=\"v4.7.0\"></a>\n## [v4.7.0] - 2023-07-27\n### Auto\n- [1e574c25](https://github.com/quay/clair/commit/1e574c25a3830b1d2f1b420c9ea2deaaba13a238): enable mutex, blocking profiles by default\n### Build(Deps)\n- [adee21df](https://github.com/quay/clair/commit/adee21df77903f159ca1b6dc631700d096c42f0f): bump golang.org/x/net from 0.11.0 to 0.12.0\n- [32c9ae2e](https://github.com/quay/clair/commit/32c9ae2e81c4ab8a443f50926697a40a6cf9af56): bump github.com/klauspost/compress from 1.16.6 to 1.16.7\n### Chore\n- [1bfbfa1b](https://github.com/quay/clair/commit/1bfbfa1bcf5489bcc91ad34aef0d4517cc6bb2e4): bump claircore to v1.5.13\n- [31cf5570](https://github.com/quay/clair/commit/31cf5570673f8e34a9fd40dfe3e6710cf517da17): Bump claircore to v1.5.12\n- [2d2d16a1](https://github.com/quay/clair/commit/2d2d16a15ad90928174d1549d7a17bf24aa285aa): Bump claircore to v1.5.11\n- [048ad2f1](https://github.com/quay/clair/commit/048ad2f1dd03197afd3434ef0bec83d1d3dedd1a): Bump claircore to v1.5.10\n- [5550b27a](https://github.com/quay/clair/commit/5550b27a89e1004e93d85d6194dbe5132a9c2659): bump Claircore to v1.5.9\n- [7df2b863](https://github.com/quay/clair/commit/7df2b86372b18ba9eeb07ffe87f17e80a04e26d4): add pyroscope to compose setup\n- [c28648e5](https://github.com/quay/clair/commit/c28648e5ea7336bf6e2a70cb1d3d9dbb6c706b95): Update outdated docs and comment with default update period.\n- [a02a0f2f](https://github.com/quay/clair/commit/a02a0f2f0d6315250a083351a584ce11e8a55dcf): remove refs to deprecated io/ioutil\n- [44638edf](https://github.com/quay/clair/commit/44638edf5b99ddcc7c73b8313557f200469228d4): Remove dogstatsd variable and references\n### Clairctl\n- [bccabff1](https://github.com/quay/clair/commit/bccabff1a003a37a35b7976eb1ff3e9fce35e97e): Add post 4.7 admin command to delete pyupio vulns\n- [d2b3d826](https://github.com/quay/clair/commit/d2b3d826bba6522e69d214023d16070287b9da15): Scan the pointer to the pointer of the bool\n- [05bd8fa0](https://github.com/quay/clair/commit/05bd8fa0382c6527d49627aff70cbfc9dd8ad9e1): Add log line signifying admin is done\n- [c636e207](https://github.com/quay/clair/commit/c636e207b3ad3a07512035dc228a5af9c75a12cd): Remove DSN logging\n- [89cae779](https://github.com/quay/clair/commit/89cae7796e63749f7b0dd123135d96af6cf91d48): `admin` subcommand\n### Cmd\n- [8231b438](https://github.com/quay/clair/commit/8231b438bedf60bbfeb3b8ac8bcd384a28f63d5e): version for old gits\n -  [#882](https://github.com/quay/clair/issues/882)### Config\n- [83ee24af](https://github.com/quay/clair/commit/83ee24afd2262353dfe41c0f6bec8d64e289c27b): pick a real versioning scheme\n### Contrib\n- [70d878eb](https://github.com/quay/clair/commit/70d878eb268f17f4f92b1833a634c367e3bacc1b): Add manifest for a Job to run DB jobs\n### Docs\n- [394efe15](https://github.com/quay/clair/commit/394efe1557b8edd9f01d3d2d7a129bbc91b657ed): Fix up debug tools table\n- [a4ec17f6](https://github.com/quay/clair/commit/a4ec17f618708480613c94bedfced910ae892d76): Add description of debugging services available during local-dev\n### Httptransport\n- [86f7a86a](https://github.com/quay/clair/commit/86f7a86aaa45f6bb6b9f8422678996a69b2f64ef): add request ID to profiler labels\n### Introspection\n- [caba76e1](https://github.com/quay/clair/commit/caba76e1329660c1c136130e5619626c308e514f): add delta pprof endpoints\n\n<a name=\"v4.7.0-rc.1\"></a>\n## [v4.7.0-rc.1] - 2023-06-26\n### Airgap\n- [94757c7d](https://github.com/quay/clair/commit/94757c7d8cda907902d1020a5c2fe74b2e5ccba9): Remove libindex Airgap option\n### All\n- [5d30ed66](https://github.com/quay/clair/commit/5d30ed66f5d80dcfa47a850b69f01ed281074271): update to new config module\n### Build(Deps)\n- [00a4279d](https://github.com/quay/clair/commit/00a4279d3796097a3ac0474836c35a5dea94efd9): bump github.com/prometheus/client_golang\n- [f4f22e33](https://github.com/quay/clair/commit/f4f22e33a9da565d89d37888c87247529d65d08d): bump golang.org/x/net from 0.10.0 to 0.11.0\n- [36a7c88c](https://github.com/quay/clair/commit/36a7c88c9a777de33b08c61168827a0a1f4c5241): bump github.com/klauspost/compress from 1.16.5 to 1.16.6\n- [17cdc922](https://github.com/quay/clair/commit/17cdc92270670c2b29d2df22c7f504b6d374254c): bump peter-evans/create-pull-request from 5.0.1 to 5.0.2\n- [b95be229](https://github.com/quay/clair/commit/b95be2296ef34e657b458a6a621a4206e31c033e): bump github.com/streadway/amqp from 1.0.0 to 1.1.0\n- [45f808da](https://github.com/quay/clair/commit/45f808dac09fd1ccd8e899de131dcf884554a0a5): bump github.com/urfave/cli/v2 from 2.25.5 to 2.25.7\n- [b75a00c3](https://github.com/quay/clair/commit/b75a00c30ad255f3d04c59233d03c4a81133e842): bump github.com/urfave/cli/v2 from 2.25.3 to 2.25.5\n- [22a75603](https://github.com/quay/clair/commit/22a756036c818026a82f25b9598c61c9802694c9): bump github.com/google/go-containerregistry\n- [300b1374](https://github.com/quay/clair/commit/300b13743f868e6578a3a55634e090ee60f7d53a): bump go.opentelemetry.io/otel/exporters/jaeger\n- [b2d7a091](https://github.com/quay/clair/commit/b2d7a091bde14b7b7f43a2ab073210ee284889c0): bump github.com/urfave/cli/v2 from 2.3.0 to 2.25.3\n- [a21fb21d](https://github.com/quay/clair/commit/a21fb21d39dbdc727324e68a7b4a7afa63199278): bump go.opentelemetry.io/otel/exporters/stdout/stdouttrace\n- [b188cba7](https://github.com/quay/clair/commit/b188cba75dbef66fcd5994aaf6e49fc4955228f6): bump github.com/quay/claircore from 1.5.2 to 1.5.3\n- [eb9d1225](https://github.com/quay/clair/commit/eb9d12256205d230a0c6f4c249eba459a4249c1d): bump golang.org/x/sync from 0.1.0 to 0.2.0\n- [f35c832f](https://github.com/quay/clair/commit/f35c832ff59f0090e6e9c01c34594f2e8acef86d): bump golang.org/x/net from 0.9.0 to 0.10.0\n- [3dbbaf7b](https://github.com/quay/clair/commit/3dbbaf7bcd87a75fb65c2d9b59f1ba34d7ed14a7): bump github.com/rs/zerolog from 1.29.0 to 1.29.1\n- [1ee7cb8a](https://github.com/quay/clair/commit/1ee7cb8aadb4f898bb190543e6d05cc27d3f8097): bump go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace\n- [dcb7a05a](https://github.com/quay/clair/commit/dcb7a05a245990b4c61dcb2df9d60c5437493e8e): bump go.opentelemetry.io/otel/exporters/jaeger\n- [fca257d7](https://github.com/quay/clair/commit/fca257d7d55d91d4798f078f94e86460ce95c7cf): bump go.opentelemetry.io/otel/exporters/stdout/stdouttrace\n- [933cc5c7](https://github.com/quay/clair/commit/933cc5c788782971b1841753b619c330ccd449b8): bump github.com/ugorji/go/codec from 1.2.9 to 1.2.11\n- [4f39b319](https://github.com/quay/clair/commit/4f39b319ccc910ee78aae5a8a7818621dfa4bfc4): bump github.com/klauspost/compress from 1.16.4 to 1.16.5\n- [3643f9d2](https://github.com/quay/clair/commit/3643f9d27c6a10a09760383956c8e1c7d3eab4c4): bump go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp\n- [c13eaecc](https://github.com/quay/clair/commit/c13eaecc738240c1df5d77767ee6eab71c8d9c21): bump go.opentelemetry.io/otel/trace from 1.11.0 to 1.15.1\n- [43e3daea](https://github.com/quay/clair/commit/43e3daea1d0473d2263f42ba88ef3144a30307e8): bump github.com/jackc/pgx/v4 from 4.18.0 to 4.18.1\n- [2180bc40](https://github.com/quay/clair/commit/2180bc4071915f9c5f1f52c4036063a7d0ec297b): bump gopkg.in/square/go-jose.v2 from 2.5.1 to 2.6.0\n- [f669244a](https://github.com/quay/clair/commit/f669244a80a1743b82f565281b96fd211d8241e8): bump peter-evans/create-pull-request from 5.0.0 to 5.0.1\n- [74bc404f](https://github.com/quay/clair/commit/74bc404fbc2dc6f0dac55780e8df75fc4ce8bd6f): bump peter-evans/create-pull-request from 4.2.4 to 5.0.0\n- [912c6e47](https://github.com/quay/clair/commit/912c6e47e3e09197ee8f962fc1e25b18851b14ac): bump actions/stale from 7 to 8\n- [ddec3b43](https://github.com/quay/clair/commit/ddec3b437fc08f7144ddeee272cd24d99af789dd): bump peter-evans/create-pull-request from 4.2.3 to 4.2.4\n- [f35a3611](https://github.com/quay/clair/commit/f35a361187b5ce9b9e5e63b311343d7847183847): bump actions/setup-go from 3 to 4\n- [d3655eef](https://github.com/quay/clair/commit/d3655eefb8332a02503d534d34258fcb1188433a): bump golang.org/x/net from 0.5.0 to 0.7.0\n- [854a2fbf](https://github.com/quay/clair/commit/854a2fbf120a2bcc14a0a2c0072a033ff90873eb): bump docker/build-push-action from 3 to 4\n### Chore\n- [9d58dba8](https://github.com/quay/clair/commit/9d58dba8890b49cae279274d3bd11ab1bc83b55f): v4.7.0-rc.1 changelog bump\n- [31823df2](https://github.com/quay/clair/commit/31823df20228404dea72417262709905308d3314): bump Claircore to v1.5.8\n- [836c0579](https://github.com/quay/clair/commit/836c0579450d1648c74b69de4005e91dcfa2cbe1): bump Claircore to v1.5.7\n- [e688e88b](https://github.com/quay/clair/commit/e688e88b6dd64549a5b93da242bc13235ab2236a): bump Claircore to v1.5.6\n- [3d61485d](https://github.com/quay/clair/commit/3d61485d00aa5c5d4eb172f3e0db40403096d7e4): bump Claircore to v1.5.5\n- [ddc4cc24](https://github.com/quay/clair/commit/ddc4cc24d9d5751c58e27c4b16aa50b7405c05cd): bump Claircore to v1.5.4\n- [76686650](https://github.com/quay/clair/commit/7668665029a5f9e030b17abee0862194b905be1f): Add the osv updater to the local-dev config\n- [56e63e8b](https://github.com/quay/clair/commit/56e63e8b7566b56b5a5c85847a190ec2ea5570e8): Update opentelemetry to v1.16.0\n- [5df81b19](https://github.com/quay/clair/commit/5df81b1953121cca97e1afec2527f35c84021632): bump Claircore to v1.5.2\n- [cc0d9df4](https://github.com/quay/clair/commit/cc0d9df4038a0d4f59312f6649e75fb9b0377dd7): bump Claircore to v1.5.1\n- [35971dc9](https://github.com/quay/clair/commit/35971dc9b33d873950c9e95055f6b0ffd84650a4): produce nightly for ppc64le\n- [471da4ee](https://github.com/quay/clair/commit/471da4eeb96492a113b7c6f3c48ab47cb21b2a26): Only ask dependabot to care about direct dependencies\n- [62119209](https://github.com/quay/clair/commit/62119209db607dce8456782067d885d1e7af43fd): updated nightly for s390x support\n- [57774bd9](https://github.com/quay/clair/commit/57774bd943cd2ec3ce564325e1de2277a1722999): added s390x support\n- [248a4733](https://github.com/quay/clair/commit/248a4733789c2bf2b0e51b81cc9b3ec00fd9b052): move emulator tests to a nightly run\n- [bd0488ee](https://github.com/quay/clair/commit/bd0488eed8da465cc0fbca8b1c04f5a992dd07ea): add gomod ecosystems to dependabot\n- [8174e950](https://github.com/quay/clair/commit/8174e950186c03bee10a9174643bca0f173710c2): Remove 1.19\n- [efe27892](https://github.com/quay/clair/commit/efe27892afe6519f2676813d18f3a3d662e52009): Bump Claircore to v1.4.22\n- [1b857d13](https://github.com/quay/clair/commit/1b857d139305ebf91dd2f53865e5e97ba5e346eb): Update go version in go.mod\n- [5faf0fc9](https://github.com/quay/clair/commit/5faf0fc9edba86cef87bff4e9941fd2a93a2889a): Bump Claircore to v1.4.21\n- [a433c93c](https://github.com/quay/clair/commit/a433c93c349f63e7b8cc6f4d5a95a2394fe1dd31): Bump Claircore to v1.4.20\n- [d565775c](https://github.com/quay/clair/commit/d565775c190a4262ce049cb06a9c1842c42e00b8): Add back GIT_HASH as needed for image name\n- [12f38e45](https://github.com/quay/clair/commit/12f38e45cec579f92438059702884fa4284bb93c): Update go-image version in docker-compose manifest\n- [02f311d5](https://github.com/quay/clair/commit/02f311d56de5fb482742f3708cdae0d0e08cbf2c): Use our dedicated metric for the go version\n- [896b2dfb](https://github.com/quay/clair/commit/896b2dfb77cdd4c06b4edd038c4833b8cfacd092): Update go version in Dockerfile\n- [d10c06e0](https://github.com/quay/clair/commit/d10c06e086f9c69075d5faef14e2eba021d201a9): Bump claircore to v1.4.18\n### Cicd\n- [58c26f4a](https://github.com/quay/clair/commit/58c26f4a2d8f4df9efebfade7f785b4613a15683): don't checkout source on clairctl builds\n- [2eb10895](https://github.com/quay/clair/commit/2eb10895a4946c68528db6bd1e54ff8004b16426): use common workflow in main module CI\n- [83d9b2f5](https://github.com/quay/clair/commit/83d9b2f50c8157506a3440ae51c063f4ebd5e3ed): use common workflow in config module CI\n- [e2f264f4](https://github.com/quay/clair/commit/e2f264f4aac85df33cb2c0a9db88a25ad65072ad): fix nightly connection strings\n- [1ea95d83](https://github.com/quay/clair/commit/1ea95d838b028dd8c22a6a6487a5f5961e231fd3): rename yamllint config\n- [7e2ae8fc](https://github.com/quay/clair/commit/7e2ae8fc43709eb09220ea9085f9ff7a6f4fe40e): fix nightly-ci error\n- [1267335e](https://github.com/quay/clair/commit/1267335e515cddfcc07af49955c97bb05a3b042e): use rabbitmq as STOMP broker in nightly CI\n- [2edb4915](https://github.com/quay/clair/commit/2edb491524f701c3d15c752bac3bf5bbac574272): use rabbitmq as STOMP broker in tests\n- [74c34c0c](https://github.com/quay/clair/commit/74c34c0cc16f60b8b20d213d14dc3e37f846756c): update nightly job to work\n- [30a98697](https://github.com/quay/clair/commit/30a98697e42069c5faa2f115464226f6575b456b): update go versions\n### Clair\n- [5226d2a3](https://github.com/quay/clair/commit/5226d2a310145ce86ade6e805d280e6b058dbe03): use new `cmd.LoadConfig`\n### Clairctl\n- [06f5bc05](https://github.com/quay/clair/commit/06f5bc0515ce49096b8236291bbfa2a0143a293f): use new `cmd.LoadConfig`\n### Cmd\n- [3ff924ad](https://github.com/quay/clair/commit/3ff924ad2f6e0461ed5bfe5440887c1f53790f3d): implement modular configs\n- [d3e88775](https://github.com/quay/clair/commit/d3e887750a0617b0f8387ed5c15f39ffe175bedb): better version information\n### Config\n- [cee776b3](https://github.com/quay/clair/commit/cee776b3830e60d3ec8ccd6703363e9c1f2ae56d): add newtype for Durations\n- [1ebbbf24](https://github.com/quay/clair/commit/1ebbbf24c573ddb241b22fae6f5ea6c45482b015): add some omitempty tags\n- [3b6047ca](https://github.com/quay/clair/commit/3b6047ca80de3be3b71d89d4471b1ea2382ef76f): update module to remove x/sys dependency\n### Contrib\n- [bb3a4be5](https://github.com/quay/clair/commit/bb3a4be513bf7fcc390a0fa7baaba9bcd8bbe5bc): Better versioning when building the service image\n- [8566c525](https://github.com/quay/clair/commit/8566c525cccda9eb8aebb5d4980754fd649fca1f): Add a dashboard panel to surface running versions\n### Docker-Compose\n- [bb777399](https://github.com/quay/clair/commit/bb77739996f7df185dd3b5da70bef6227cf6cf7e): use rabbitmq instead of activemq\n### Dockerfile\n- [497ab2d2](https://github.com/quay/clair/commit/497ab2d2cae0beea8ef41aeb0878742e8a69d4f1): remove init process\n### Docs\n- [45e6f5c0](https://github.com/quay/clair/commit/45e6f5c0b6d535a134c07a18154528f3dcf00e9c): update old `go get` command\n- [d2d9f385](https://github.com/quay/clair/commit/d2d9f38548ea5d3b2477d095a4d11a2a773200bc): fix host flag order\n -  [#1754](https://github.com/quay/clair/issues/1754)- [d726e157](https://github.com/quay/clair/commit/d726e15796882e6f2adba6a84a0aef419bc59849): remove reference to \"filters\"\n -  [#1690](https://github.com/quay/clair/issues/1690)### Go.Mod\n- [670376a2](https://github.com/quay/clair/commit/670376a29dad524e8ebea8f2acd22220053e6ec9): update json (de)serializer\n### Httptransport\n- [72417962](https://github.com/quay/clair/commit/72417962880862b986872709317a39fa0582f143): debug log calls to apiError\n- [378a4b5f](https://github.com/quay/clair/commit/378a4b5f35ceb452654e7d9dca12fc455ac1697c): fix request_id logging\n### Httputil\n- [b18f989c](https://github.com/quay/clair/commit/b18f989c7869a480b7bbfc8181d515227e701a39): fix ParseIP usage\n -  [#1689](https://github.com/quay/clair/issues/1689)### Notifier\n- [5446e49f](https://github.com/quay/clair/commit/5446e49ff9de13a0d95ce5937f1ce722e59304f5): Avoid double reference\n### Stomp\n- [5b876935](https://github.com/quay/clair/commit/5b87693500fcd9222426e62f6bc86bd7736159e1): override default behavior for \"host\" header\n- [643bd1c9](https://github.com/quay/clair/commit/643bd1c957d6755b7fe0f4fb762d31071274dd2a): rework tests\n- [f84e3491](https://github.com/quay/clair/commit/f84e3491966e88bdc337f98029c0c8a1de0267d5): plumb Context into Dialer\n- [7d476ebd](https://github.com/quay/clair/commit/7d476ebd90bd05a52f4ab2a64711a312972d2abe): remove apparent ActiveMQ-ism\n- [aa441b3c](https://github.com/quay/clair/commit/aa441b3cffb3109fcb2a717caec059027042dd76): switch to module release for stomp client\n -  [#1739](https://github.com/quay/clair/issues/1739)### Updater\n- [95970e28](https://github.com/quay/clair/commit/95970e283b0fc37f56cf2f93e27a801c0b03b809): Extend default updater time to 6 hours\n\n<a name=\"v4.6.1\"></a>\n## [v4.6.1] - 2023-04-13\n### Airgap\n- [e02aba27](https://github.com/quay/clair/commit/e02aba27de01cb461f79bee9644aac80c2f9bd65): Remove libindex Airgap option\n### Chore\n- [36990912](https://github.com/quay/clair/commit/36990912450fba2ccbef260a4829f1d9f69f45c6): v4.6.1 changelog bump\n- [e676671c](https://github.com/quay/clair/commit/e676671c17d2612470cd8de05aa668312fbb3036): Bump Claircore to v1.4.21\n### Go.Mod\n- [36de97cc](https://github.com/quay/clair/commit/36de97ccf619113b1ef4dff6bfd0e0c692252544): update json (de)serializer\n### Httptransport\n- [922f33d1](https://github.com/quay/clair/commit/922f33d18919578049fbf2ccb756e6990b66f280): fix request_id logging\n### Httputil\n- [9e8eacf5](https://github.com/quay/clair/commit/9e8eacf51b2a45f967036396b3dc14a52edc480d): fix ParseIP usage\n -  [#1689](https://github.com/quay/clair/issues/1689)### Notifier\n- [ffa4556d](https://github.com/quay/clair/commit/ffa4556d0f251cc984ed34594356625b9b747744): Avoid double reference\n\n<a name=\"v4.6.0\"></a>\n## [v4.6.0] - 2023-01-20\n### All\n- [577a55d4](https://github.com/quay/clair/commit/577a55d44d0ec337178680ec1ad6f0862a0c2482): use httputil to construct requests\n### Auto\n- [1f1010fe](https://github.com/quay/clair/commit/1f1010fe4ff81ed954ce9e680a7228742d41f533): add automatic memory limit discovery\n### Build(Deps)\n- [ef896eb6](https://github.com/quay/clair/commit/ef896eb62d4e5fd286213d8208105de92b28dadc): bump actions/stale from 6 to 7\n- [5a212ffe](https://github.com/quay/clair/commit/5a212ffed49b0f652d5f742400eb040b71dde16f): bump peter-evans/create-pull-request from 4.1.4 to 4.2.3\n- [b883bc2b](https://github.com/quay/clair/commit/b883bc2b9174618abd156e61037517a9f379020f): bump gsactions/commit-message-checker from 1 to 2\n### Chore\n- [5fd26563](https://github.com/quay/clair/commit/5fd265634d162dc0acba6c28e36d35dd0a90aec0): v4.6.0 changelog bump\n- [33f4fcbd](https://github.com/quay/clair/commit/33f4fcbdd0d80c9aa6878a0bdb6b1bd3332db823): Bump claircore to v1.4.17\n- [54d44908](https://github.com/quay/clair/commit/54d449081e29a456ed533bd2d1f189b7f4bc1b39): Bump Claircore to v1.4.16\n- [430e6087](https://github.com/quay/clair/commit/430e6087b6f245dc2cc95ef36836bedc9e458748): Bump Claircore to v1.4.15\n- [652d8ce6](https://github.com/quay/clair/commit/652d8ce6d5d627ba63e2aaf8f22991e1cc2fc5b4): Bump Claircore to v1.4.14\n- [9f6828cd](https://github.com/quay/clair/commit/9f6828cd36dce5033a68c641585c2f0b93edec87): Update to Go 1.18 for local-dev\n- [1c002bcd](https://github.com/quay/clair/commit/1c002bcda7e604e083de6f576d1a8801dceac44a): added ppc64le support\n- [4b37dcdf](https://github.com/quay/clair/commit/4b37dcdfcd0c89ff0be26ffd3e9dfbb9d15229df): Bump Claircore to v1.4.13\n- [9b273420](https://github.com/quay/clair/commit/9b273420c9aa0bd1975d925e5d38f8d852a58851): Bump claircore to v1.4.12\n### Cicd\n- [1dfb42a0](https://github.com/quay/clair/commit/1dfb42a09a25d7298ee2c343893dcc2c25d3f830): use extracted git archive\n- [aff17a4a](https://github.com/quay/clair/commit/aff17a4a3e2c9288baa69a1b9bee0b2e6318d276): update usage of `set-output`\n- [a8a97f80](https://github.com/quay/clair/commit/a8a97f80632c1c88711b7729a2d3bb726cdf9cbf): update cache action\n- [7de63a9c](https://github.com/quay/clair/commit/7de63a9c01f1f3990b5c8d91ee3bf486418f0ff0): add tests for odd architectures\n- [e923360c](https://github.com/quay/clair/commit/e923360cb2124e20da6646d02e94b5e9541b6653): omit Dockerfile build args\n- [14b8f690](https://github.com/quay/clair/commit/14b8f6906a08529e93fa563c5567311154e58b1b): enable go1.19\n- [5a8128c1](https://github.com/quay/clair/commit/5a8128c1241a9e48a8b84c290405993e12e6d776): inject version into built `clairctl` binaries\n -  [#1649](https://github.com/quay/clair/issues/1649)### Clairctl\n- [a367a7ae](https://github.com/quay/clair/commit/a367a7ae9b59fee5a5b102f4da89a1f5bc732e0a): use a better user-agent\n- [3b9ff6de](https://github.com/quay/clair/commit/3b9ff6de75b27d6a3ce593212850f294942b8be0): update with new signer\n### Client\n- [ddea858f](https://github.com/quay/clair/commit/ddea858f16e990eca838edbb8fe59560cb63bcdc): Add the passed host to the signer\n- [adbaa567](https://github.com/quay/clair/commit/adbaa567fb9e9d271c406f47c76218d301e1cdc9): use signer\n- [d8ad1ba4](https://github.com/quay/clair/commit/d8ad1ba475fd0a40f592c3b3618a9b1d16295171): update for httputil changes\n### Cmd\n- [8b899803](https://github.com/quay/clair/commit/8b8998034ceded096a761b99ff0a1f5a79f0a7b6): use git-archive for version information\n### Documentation\n- [9d1a7aab](https://github.com/quay/clair/commit/9d1a7aab465664bb70ea3672aed70e8193c6e4d0): fix typo in link\n### Httptransport\n- [25ac033f](https://github.com/quay/clair/commit/25ac033f67e2e1d39edd8ebf85b3bf61f751e433): use new signer scheme in test\n- [a9228d40](https://github.com/quay/clair/commit/a9228d40cb4bfc210148b2f931f7f287515bfa5e): add a `request_id` to logs\n -  [#1547](https://github.com/quay/clair/issues/1547)### Httputil\n- [e746ff05](https://github.com/quay/clair/commit/e746ff056193606b8a20a240a098ef1309311e2d): rework request signing and request restriction\n### Service\n- [e08f3972](https://github.com/quay/clair/commit/e08f3972393d8d9a23b9fec79e9beee11fc5933a): add signer option\n### Webhook\n- [d99f7005](https://github.com/quay/clair/commit/d99f7005bc48724d1da804a47a4099e7eedce252): add explicit signer argument\n\n<a name=\"v4.5.1\"></a>\n## [v4.5.1] - 2022-11-22\n### Chore\n- [0a0aa1cc](https://github.com/quay/clair/commit/0a0aa1cca3937cec42649bf171d2e1436c9bd792): Bump claircore to v1.4.12\n -  [#1646](https://github.com/quay/clair/issues/1646)\n<a name=\"v4.5.0\"></a>\n## [v4.5.0] - 2022-11-04\n### Build(Deps)\n- [df77d75a](https://github.com/quay/clair/commit/df77d75a9850ebc2120f3c0a6162d246a7847ce0): bump peter-evans/create-pull-request from 4.1.3 to 4.1.4\n### Chore\n- [e0aec666](https://github.com/quay/clair/commit/e0aec666625ab4b5f1b3ddc35ff0fc75aa578e8c): Remove windows 386 as a binary target for releases\n- [0772b85f](https://github.com/quay/clair/commit/0772b85feaab5928e3f1d5352c6c7c17cef3e782): v4.5.0 changelog bump\n- [070a611a](https://github.com/quay/clair/commit/070a611a2dadcb8cf16c57c40479db4d028c0d03): Bump Claircore to v1.4.11\n- [5ff4805a](https://github.com/quay/clair/commit/5ff4805a3294313e38e540664ba7a7f9732876b4): Bump Claircore to v1.4.10\n- [08ad0697](https://github.com/quay/clair/commit/08ad06979051308d3d7cbc07d751977cf48d6a9e): Bump Claircore to v1.4.9\n- [731c16f7](https://github.com/quay/clair/commit/731c16f70b7938df46b0f3ca6e431377982da4e9): bump Claircore to v1.4.8\n### Clairctl\n- [e431960e](https://github.com/quay/clair/commit/e431960e87fc6a8f2c257b1bda860c242a6c713b): Add delete command\n- [66325b12](https://github.com/quay/clair/commit/66325b12a329a0517e453aa0a2658b9c263335d1): don't use internal client\n### Cmd\n- [9b0f1a96](https://github.com/quay/clair/commit/9b0f1a962585761065b7adb0d18191f911c93ed3): unify version information\n\n<a name=\"v4.5.0-rc.0\"></a>\n## [v4.5.0-rc.0] - 2022-10-10\n### All\n- [1a1d5662](https://github.com/quay/clair/commit/1a1d566249aa4be93c60e1af773ced0a8d227fb2): remove Quay keyserver support\n### Build(Deps)\n- [224d0698](https://github.com/quay/clair/commit/224d06988b6196ef6617ae807abb652a0580c0dc): bump actions/stale from 5 to 6\n- [180b887c](https://github.com/quay/clair/commit/180b887c8748e3367e962fa78551d526b79de378): bump peter-evans/create-pull-request from 4.1.2 to 4.1.3\n- [0537bbc0](https://github.com/quay/clair/commit/0537bbc06eeb1a050530a62ac263a4a60bd298c4): bump peter-evans/create-pull-request from 4.1.1 to 4.1.2\n- [47a9c1cb](https://github.com/quay/clair/commit/47a9c1cb108872a6d30fe36328b2bf9126f9b13d): bump peter-evans/create-pull-request from 4.0.4 to 4.1.1\n- [3cad3319](https://github.com/quay/clair/commit/3cad331970a3d1a9c8093f62087e63d48859a7b3): bump peter-evans/create-pull-request from 4.0.3 to 4.0.4\n- [c5975257](https://github.com/quay/clair/commit/c597525708138d390041a4d72ed9de9f43ec28c5): bump peter-evans/create-pull-request from 4.0.2 to 4.0.3\n- [57dc2378](https://github.com/quay/clair/commit/57dc23781da8328410be551090a92aec99dbd41e): bump docker/setup-qemu-action from 1 to 2\n- [c4e2031b](https://github.com/quay/clair/commit/c4e2031b36dea93fbe8078bff093c0525d32da6e): bump docker/login-action from 1 to 2\n- [a9823a91](https://github.com/quay/clair/commit/a9823a9107234e7c51f2daf0824ed0e35bd4939b): bump docker/setup-buildx-action from 1 to 2\n- [7c8bafbe](https://github.com/quay/clair/commit/7c8bafbe9b9b021da81055d98f95106649cf6d48): bump docker/build-push-action from 2 to 3\n- [4408b1bb](https://github.com/quay/clair/commit/4408b1bb39d8c443509872dd54dd959bc5a11ac5): bump actions/download-artifact from 2 to 3\n- [4c91a714](https://github.com/quay/clair/commit/4c91a714cd1fa86a20e8d40fcbe8a344e3b94e72): bump actions/setup-go from 2 to 3\n- [64389db0](https://github.com/quay/clair/commit/64389db059ed0a3dbba6d0599272961ab88adb7a): bump actions/upload-artifact from 2 to 3\n- [1db22a62](https://github.com/quay/clair/commit/1db22a62b0569c96772a3b6785ca007454e063fd): bump peter-evans/create-pull-request from 4.0.1 to 4.0.2\n- [c0953e6f](https://github.com/quay/clair/commit/c0953e6f15ef83d2af317ae7a1cd40d37336446e): bump actions/stale from 4 to 5\n- [53e944f9](https://github.com/quay/clair/commit/53e944f9f2321330d1ed8172365892bf461b0eb3): bump peter-evans/create-pull-request from 3.14.0 to 4.0.1\n- [c76efaee](https://github.com/quay/clair/commit/c76efaee8d97a5820d2ba0b3668ad3ccd10fbe02): bump actions/cache from 2 to 3\n### CRDA\n- [4bb2d332](https://github.com/quay/clair/commit/4bb2d33291f652961a4f4d2da8e5e297df4d19ee): replace API key request form URL\n### Chore\n- [4d4c425b](https://github.com/quay/clair/commit/4d4c425b25996a7d0834307fc908090ecbbe805b): Bump claircore to v1.4.4\n### Chore\n- [aae2d839](https://github.com/quay/clair/commit/aae2d839fbc36996e97ab1d93fca00c70d2278c6): v4.5.0-rc.0 changelog bump\n- [95073d0b](https://github.com/quay/clair/commit/95073d0bdff80f74a055bb8cbc4ebf01d6c800a0): Bump claircore to v1.4.7\n- [415b2a17](https://github.com/quay/clair/commit/415b2a17bc71064a4db47d622b378c345ca5a4ed): Add back Publish Binaries to upload clairctl versions\n- [c9041efa](https://github.com/quay/clair/commit/c9041efaf9aa0b8082bc06e400d31767285e5c20): bump Claircore to v1.4.6\n- [039d2073](https://github.com/quay/clair/commit/039d2073b21fd8d1ba52d26f14884c56d620df30): bump Claircore to v1.4.5\n- [4e44f7ef](https://github.com/quay/clair/commit/4e44f7efd1ae880b6edf0b7145c271619d10cb03): bump claircore v1.4.2 -> v1.4.3\n- [e2b8e101](https://github.com/quay/clair/commit/e2b8e10152744ff245469be83c75f1a794648584): Bump claircore v1.4.1 -> 1.4.2\n- [3273a969](https://github.com/quay/clair/commit/3273a96981b007d8c4e271aec0371cb7e4f45baf): bump claircore to v1.3.2\n### Ci\n- [45443c8e](https://github.com/quay/clair/commit/45443c8ead5ad369987b33ec8a0b28ec0544d9c3): fix prerelease conditional\n- [eea6fea1](https://github.com/quay/clair/commit/eea6fea1966b4d599e8eb900150c2b90bff47e37): fix config tidy check\n- [4180d787](https://github.com/quay/clair/commit/4180d78769579b28534a45a78a96a7b7bb09eaa1): update workflows and machinery for go1.18\n### Clair\n- [b8882f9d](https://github.com/quay/clair/commit/b8882f9ddb1d3801fdbb47705a68640cfd819aab): better argument error messages\n -  [#1605](https://github.com/quay/clair/issues/1605)### Clairctl\n- [f0d6a357](https://github.com/quay/clair/commit/f0d6a35763d5589f17c1a40dca5e155188a79b1e): fix error reporting for streaming responses\n### Config\n- [677a3137](https://github.com/quay/clair/commit/677a3137e27c55f802a8d7d6fd6c7a7ce7587f9b): Don't use flag default combo\n- [f0e077e0](https://github.com/quay/clair/commit/f0e077e0cd79d7a71314a4b87e5cb1d14549efa8): add \"omitempty\" tags everywhere\n- [f1bb53ea](https://github.com/quay/clair/commit/f1bb53ea2b5e03385cf59c941b185baf6df16f8b): implement TextMarshaler for LogLevel\n- [6a99b61a](https://github.com/quay/clair/commit/6a99b61ab5fed89f018ab3c707c52a6aca15add0): add top-level docs\n### Contrib\n- [9612ee67](https://github.com/quay/clair/commit/9612ee675ce9ce5db35c596c82a27057e322099e): remove rpmscanner files on startup\n- [a6609638](https://github.com/quay/clair/commit/a660963897dbf9cf7097acfade26e45fd8276154): First wipe anything that might be left before starting clair indexers\n- [6a6fd901](https://github.com/quay/clair/commit/6a6fd90151d454b7288d0144dc855974969b2c26): fix DB connection charts\n- [6b60eef6](https://github.com/quay/clair/commit/6b60eef6bcf91d9f809ad00928196bfd802db897): Only count index report creation latency for successful requests\n- [17862ae3](https://github.com/quay/clair/commit/17862ae3775cfa8b085856455614d3799bef36f7): Add DB connections to Grafana dashboard\n- [37ca1ab0](https://github.com/quay/clair/commit/37ca1ab04958c4c474c05b24b62d0172747ad9d7): Add dedicated serviceAccount\n- [1d89c032](https://github.com/quay/clair/commit/1d89c032aac54789241a82491c97496be403ec30): Wipe all the temporary files in the process of being fetched\n- [187764a3](https://github.com/quay/clair/commit/187764a3c7859d86906b34023a9f8658d25390ea): Wipe all the contents of /tmp on container start\n- [ae7675af](https://github.com/quay/clair/commit/ae7675af1e68c4e5cf2301480b7a3b99ae6faf89): Use the readyz endpoint in startup probes\n - Fixes [#1488](https://github.com/quay/clair/issues/1488)### Docker-Compose\n- [dfd68db8](https://github.com/quay/clair/commit/dfd68db8df6ebe61b20888858fadf3b2fab27e5b): remove -mod=vendor flag\n### Dockerfile\n- [e689241b](https://github.com/quay/clair/commit/e689241b16f07e79257a2b3264ff5cb730388d8d): strip binaries to reduce size\n- [2af2a7f6](https://github.com/quay/clair/commit/2af2a7f617cf49ba2cd2588d5583479743f3d8c5): fix build with newer ubi8/ubi-minimal image\n- [f2e209c6](https://github.com/quay/clair/commit/f2e209c62acd6d2a728ac59dbbcc3ce734962310): update for 1.18, add trimpath\n### Docs\n- [369319cd](https://github.com/quay/clair/commit/369319cd1ebabaf86fa5e029a0f23724434faed5): note tested `docker-compose` version\n### Documentation\n- [9258a313](https://github.com/quay/clair/commit/9258a313a95086148e5c0efe293ed7c5a686dbec): add config reference cross-checking\n- [9a74ac8f](https://github.com/quay/clair/commit/9a74ac8f4f1e0606e1830e881b52ae2ab4331d92): add notes on metrics\n- [d09b3192](https://github.com/quay/clair/commit/d09b3192a4a8fec2a54c6d89594f17d3694c9b33): add link checker\n### Go.Mod\n- [d583395e](https://github.com/quay/clair/commit/d583395ec2a03655f44c8eea7e1052e04d6ff889): update claircore version\n- [12b676d4](https://github.com/quay/clair/commit/12b676d4862a97629acf5bc501189da9728d690d): patch update dependencies\n- [e1833161](https://github.com/quay/clair/commit/e1833161315d45484f433fdb3a3692a9a617f3bd): update claircore version\n- [65dcc39c](https://github.com/quay/clair/commit/65dcc39c688ab14cc8330ef05eef77b445d6ecd9): update minimum go version\n### Httptransport\n- [f34148ad](https://github.com/quay/clair/commit/f34148adb89922c4035840cf81018392953f209b): update discovery endpoint\n- [88149118](https://github.com/quay/clair/commit/88149118a5f0ee212a069938a7a3031aa8725fde): use less-verbose instrumentation construction\n- [51119521](https://github.com/quay/clair/commit/5111952106e614c671171f872dcaf7627adf908b): update notification endpoints\n -  [#1523](https://github.com/quay/clair/issues/1523)- [fa49078d](https://github.com/quay/clair/commit/fa49078db0cabae7fbdab0e725752b8de93bc714): fix test log panic\n- [19fa0aa8](https://github.com/quay/clair/commit/19fa0aa8af90288542b9cf65fea08b91f3c106bb): Refactor Matcher to align with indexer\n- [ce462ea4](https://github.com/quay/clair/commit/ce462ea41a81be2c13f9a4a85847b7f93570db48): handle no notifier in \"combo\" mode\n### Indexer\n- [8e5d76d3](https://github.com/quay/clair/commit/8e5d76d3926c748caaf34e7d722b58502c5a5813): Return 4XX status code when Index() returns tarfs.ErrBadFormat\n### Introspection\n- [f4db2610](https://github.com/quay/clair/commit/f4db2610af02f6b933cddfeb71f271d58e412a8a): allow custom health function\n### Logging\n- [5c5a1ab4](https://github.com/quay/clair/commit/5c5a1ab496ea07c38a885b260dea14aa952f08b9): log when request is rate-limited\n### Matcher\n- [e5cb6a91](https://github.com/quay/clair/commit/e5cb6a91484254ab647989ab7761ae4d0f85a5f4): Update matcher client to match server definition\n### Metrics\n- [e1664659](https://github.com/quay/clair/commit/e1664659909ddfac0fca48b4995d2b772ebd085b): Spread clair_http_indexerv1_request_duration buckets\n### Prometheus\n- [b6ce5043](https://github.com/quay/clair/commit/b6ce504373675f4b75c5e229098d9fd89669b13f): rework indexer buckets\n### Services\n- [668f443f](https://github.com/quay/clair/commit/668f443f1c0829ce3f5977ff158214fa76951d59): update initialization\n### Webhook\n- [472e70b6](https://github.com/quay/clair/commit/472e70b6c4c6a65f7afd5ef4ecd2c4d722578ba5): clone headers on request\n\n<a name=\"v4.4.4\"></a>\n## [v4.4.4] - 2022-06-09\n### Chore\n- [48a3a4ee](https://github.com/quay/clair/commit/48a3a4eef20ce76e25bb57ed9e8444af60998fd4): v4.4.4 changelog bump\n- [6b1f27ed](https://github.com/quay/clair/commit/6b1f27ed682605fa89641cebf4f0f029c604f0d3): Bump claircore v1.4.1 -> 1.4.2\n\n<a name=\"v4.4.3\"></a>\n## [v4.4.3] - 2022-06-06\n### Chore\n- [3682f31e](https://github.com/quay/clair/commit/3682f31ee399e2b14a1d669c1f3d9ee774feefdd): v4.4.3 changelog bump\n### Go.Mod\n- [51c63e32](https://github.com/quay/clair/commit/51c63e323cc27824cdb59b942b66af110400d5b3): update claircore version\n -  [#1580](https://github.com/quay/clair/issues/1580)### Webhook\n- [edc65d66](https://github.com/quay/clair/commit/edc65d667261fbc08d54bbc4057151f47ce6d4b7): clone headers on request\n -  [#1557](https://github.com/quay/clair/issues/1557)\n<a name=\"v4.4.2\"></a>\n## [v4.4.2] - 2022-05-26\n### Chore\n- [2a4694bf](https://github.com/quay/clair/commit/2a4694bff671a9e41c3c5c5c77eb1a53afebf971): v4.4.2 changelog bump\n### Go.Mod\n- [67f32bff](https://github.com/quay/clair/commit/67f32bff3f3ef655ff24313ccc7905d6d2a0a719): update claircore version\n -  [#1571](https://github.com/quay/clair/issues/1571)\n<a name=\"v4.4.1\"></a>\n## [v4.4.1] - 2022-04-04\n### Chore\n- [363dca4d](https://github.com/quay/clair/commit/363dca4d771d7e36e2925552cce102e458193c4f): v4.4.1 changelog bump\n- [cc5a916e](https://github.com/quay/clair/commit/cc5a916ef11f5de53af0b87b9ad75d940a615beb): bump claircore to v1.3.2\n -  [#1537](https://github.com/quay/clair/issues/1537)### Httptransport\n- [d314e412](https://github.com/quay/clair/commit/d314e41234292084dc125c3c9489f3958ca772ae): handle no notifier in \"combo\" mode\n -  [#1531](https://github.com/quay/clair/issues/1531)\n<a name=\"v4.4.0\"></a>\n## [v4.4.0] - 2022-03-16\n### Chore\n- [c7075aa4](https://github.com/quay/clair/commit/c7075aa46dfffbbd9b09393d5db42938cda2a615): v4.4.0 changelog bump\n\n<a name=\"v4.4.0-rc.7\"></a>\n## [v4.4.0-rc.7] - 2022-03-14\n### Chore\n- [94fdf1f8](https://github.com/quay/clair/commit/94fdf1f83102d1ae11f9363f7a41532e2414f18e): v4.4.0-rc.7 changelog bump\n### Ci\n- [87a2421f](https://github.com/quay/clair/commit/87a2421f458591be50f2303de4cf76add5789925): use runner context object\n\n<a name=\"v4.4.0-rc.6\"></a>\n## [v4.4.0-rc.6] - 2022-03-14\n### Build(Deps)\n- [323e83ce](https://github.com/quay/clair/commit/323e83cee577e6df4b41db03f6a90d05480c6443): bump actions/checkout from 2 to 3\n### Chore\n- [0ea0e9d5](https://github.com/quay/clair/commit/0ea0e9d547de3d0ed2d7fe4e400b1fcb8cb16476): v4.4.0-rc.6 changelog bump\n### Ci\n- [d4983fdb](https://github.com/quay/clair/commit/d4983fdb737aa11ce01a952dc4ca6b26af7f4bab): fix extract step\n### Httptransport\n- [5caad7fc](https://github.com/quay/clair/commit/5caad7fc921b64a877e5055171c382a28ba2dbba): remove unused AffectedManifest handler\n\n<a name=\"v4.4.0-rc.5\"></a>\n## [v4.4.0-rc.5] - 2022-03-03\n### Chore\n- [8d2d1593](https://github.com/quay/clair/commit/8d2d159396004f17bfedb755b5f6a32b90ede1c2): v4.4.0-rc.5 changelog bump\n### Ci\n- [58c761e3](https://github.com/quay/clair/commit/58c761e3662524358720ea15789e5121375f7bc6): update nightly version description\n- [bbdd9252](https://github.com/quay/clair/commit/bbdd925292fd426cdaae8172fafe2f333d7b127b): fix tar prefix\n\n<a name=\"v4.4.0-rc.4\"></a>\n## [v4.4.0-rc.4] - 2022-03-03\n### Chore\n- [fa6fad70](https://github.com/quay/clair/commit/fa6fad709018f8f27a7c43d8da6cfe69489587a5): v4.4.0-rc.4 changelog bump\n### Cicd\n- [3ba11c9c](https://github.com/quay/clair/commit/3ba11c9cf57f7a8cb3f17e956dbb379fdb986724): checkout repo where needed\n\n<a name=\"v4.4.0-rc.3\"></a>\n## [v4.4.0-rc.3] - 2022-03-03\n### Build(Deps)\n- [593b868e](https://github.com/quay/clair/commit/593b868eac69f88481d48085a8b15a3004d0ebe3): bump peter-evans/create-pull-request from 3.12.1 to 3.14.0\n### Chore\n- [c7ef8501](https://github.com/quay/clair/commit/c7ef8501a5e76ec71d953307f0ec6964144a11e8): v4.4.0-rc.3 changelog bump\n- [fa2c5380](https://github.com/quay/clair/commit/fa2c5380ad1116ed465d1e37d3d0ad816f6b6f58): Update zlog dep\n### Contrib\n- [6d2dfbea](https://github.com/quay/clair/commit/6d2dfbeaddf5d7a569e5b54ae5ad9a808ac683bf): Update grafana dashboards\n### Metrics\n- [cdb67fb6](https://github.com/quay/clair/commit/cdb67fb652b9025e7db791474b4e1d6d8dc000c9): matcher add status code label to matcher latency metric\n\n<a name=\"v4.4.0-rc.2\"></a>\n## [v4.4.0-rc.2] - 2022-02-23\n### Chore\n- [5454de31](https://github.com/quay/clair/commit/5454de316c55704c508e8e2101ed0090791210b8): v4.4.0-rc.2 changelog bump\n### Workflows\n- [8359de62](https://github.com/quay/clair/commit/8359de622b212f3b1d12839913e2cf1c692d9592): fix \"chglog\" typo\n\n<a name=\"v4.4.0-rc.1\"></a>\n## [v4.4.0-rc.1] - 2022-02-23\n### Chore\n- [6b029ffc](https://github.com/quay/clair/commit/6b029ffc6290bc6034b6f343d78b978d4513e55e): v4.4.0-rc.1 changelog bump\n### Workflows\n- [f2819bd6](https://github.com/quay/clair/commit/f2819bd63b90d93ac0930a83d5de11bd5adbfe4a): rewrite go version check\n- [a40b1849](https://github.com/quay/clair/commit/a40b1849582a95e3e43f40485f6509eb90d3ca93): remove `env` usage\n\n<a name=\"v4.4.0-rc.0\"></a>\n## [v4.4.0-rc.0] - 2022-02-23\n### All\n- [128b27b7](https://github.com/quay/clair/commit/128b27b72563d1d42be426d1a7b144250ec8cb0a): update zlog and corresponding otel packages\n- [35c9c9f2](https://github.com/quay/clair/commit/35c9c9f28947af2e3394c2a866ee07f325809e22): move config package to new module\n### Auto\n- [3b2f4958](https://github.com/quay/clair/commit/3b2f49585d3c444d40fa5bd49d24fe624d0e7778): fall back to root cgroup\n- [00163750](https://github.com/quay/clair/commit/00163750e4929759a0d2f816e9749fe19082cbd7): add automatic runtime configuration\n### Build(Deps)\n- [496f24c9](https://github.com/quay/clair/commit/496f24c9cf25ccde2a3c1164a25fe3ca2518e622): bump peter-evans/create-pull-request from 3.12.0 to 3.12.1\n- [422d6b4a](https://github.com/quay/clair/commit/422d6b4a60fcb3ae7c059b97d89dcd456ed01d28): bump peter-evans/create-pull-request from 3.11.0 to 3.12.0\n- [55dbdd99](https://github.com/quay/clair/commit/55dbdd995748bdef7575165f1010732bd1db19e9): bump peter-evans/create-pull-request from 3.10.1 to 3.11.0\n- [0d2a60b3](https://github.com/quay/clair/commit/0d2a60b3f83aa393f1e4d2547b61561a9c60900c): bump peter-evans/create-pull-request from 3.5.1 to 3.10.1\n### Chore\n- [5997745c](https://github.com/quay/clair/commit/5997745c9765afadd3369abb604b079c376524fb): v4.4.0-rc.0 changelog bump\n- [bd115483](https://github.com/quay/clair/commit/bd1154839eff429bb774abba186477feadbfa9b6): bump claircore to v1.3.1\n- [c1d66d61](https://github.com/quay/clair/commit/c1d66d614fc41594d4538af77cac01ef886cd1f1): update claircore version\n- [953fa97b](https://github.com/quay/clair/commit/953fa97b70878dfbe2ccf0b1183c902b8618119b): update claircore version\n- [74210ca1](https://github.com/quay/clair/commit/74210ca1bea67f0369bc9b26d40354410317627b): update changelog to cope with submodule tags\n- [5c44e70c](https://github.com/quay/clair/commit/5c44e70c92331f4150b8c2474043e6d92f125d31): update claircore version\n- [28647ba1](https://github.com/quay/clair/commit/28647ba13fd38162ebdbc2f878db2691431f197a): update claircore version\n### Cicd\n- [3399a752](https://github.com/quay/clair/commit/3399a75297495a7bcc512ef0366ab886c139665e): update actions to use native conditional\n- [c473c92c](https://github.com/quay/clair/commit/c473c92c31d3e38027b35395fb951524e03f84c7): add CI job for config module\n- [752988a9](https://github.com/quay/clair/commit/752988a99d99daca7acda5b921ec5ebd5e5df63e): fix output/outputs typo\n- [a930239b](https://github.com/quay/clair/commit/a930239bfcb587bf7dc7958a907144ee39ac4258): fix nightly setup\n- [6a6df52d](https://github.com/quay/clair/commit/6a6df52dcc4e435fefd95848aa7098e94af4a9b6): factor out go module and build caching\n- [dc30ee31](https://github.com/quay/clair/commit/dc30ee31dfd438d0427ae28c8d3a0b4f8f3a21ca): add conditional step hack\n- [83f6bcf5](https://github.com/quay/clair/commit/83f6bcf5c3cba6c91d9eff307de0a8e97593b4b0): prevent push on unchanged code\n- [8b173887](https://github.com/quay/clair/commit/8b1738873ac2081f4b9c0b14e0ddccfa54b71858): use common documentation action\n- [f185a9b4](https://github.com/quay/clair/commit/f185a9b42422adcd07d652e261a2044069b3c9a8): use common expiration action\n- [bfc1abd3](https://github.com/quay/clair/commit/bfc1abd354227f2f18c97f5b2a734710cafd6110): add caches to release workflow\n- [29d9153b](https://github.com/quay/clair/commit/29d9153b83a9a959ee5b76a1783db2218c5f9b71): move config into a discrete step\n- [9bb8cc7b](https://github.com/quay/clair/commit/9bb8cc7b1e52ca43e271eef3007f7bbc179c0314): add some composite actions\n- [b9a9d069](https://github.com/quay/clair/commit/b9a9d06973d8bfad87764cb836e8e7872bfac1a7): use local go for mod check\n- [f7188e3d](https://github.com/quay/clair/commit/f7188e3d88896ee1f2bd7b85291d6ce8c5bd7041): add dependabot for GitHub Actions\n### Clair\n- [11cb491f](https://github.com/quay/clair/commit/11cb491fc6efef6c5dedc2d292b628034f4cf3b5): allow TLS for API server\n- [c1d51a66](https://github.com/quay/clair/commit/c1d51a66f1143d60683f0ae682e6dc0d3102cb20): update for config package changes\n### Clairctl\n- [a8c7ebe9](https://github.com/quay/clair/commit/a8c7ebe95819969c10ccc1afcf264e04412dcf56): uniform import/export compression\n- [872ba0b1](https://github.com/quay/clair/commit/872ba0b1801eddaf06774d40c523863a2793ac1c): add additional report flags\n- [ac80e5d8](https://github.com/quay/clair/commit/ac80e5d830c007ab9580592f7c396676592fc728): add retries to manifest fetching\n- [dc3d1148](https://github.com/quay/clair/commit/dc3d114879474e4fa007915d6b146a5023c5d8a7): internal client improvements\n### Client\n- [1972a877](https://github.com/quay/clair/commit/1972a87703ad5a542958537fe70fbe5c9d857760): add DeleteManifests method\n### Config\n- [e0f865ad](https://github.com/quay/clair/commit/e0f865ad8f8c050335ecce946cf06fc0a9cf4a6d): add deprecation notice to `max_conn_pool`\n- [d2152205](https://github.com/quay/clair/commit/d2152205f56f1029c7face5d4f2b486f28677147): MarshalText on value receiver\n- [2cc4af7e](https://github.com/quay/clair/commit/2cc4af7e1cd1b35b3ff0d0f4bae1be3188bfb675): auto-size concurrent request capacity\n- [1b4a736d](https://github.com/quay/clair/commit/1b4a736d2ed78c4c8bcc1c8b6477e06ac462a513): add tls for http API\n- [9d2575d1](https://github.com/quay/clair/commit/9d2575d1cd491d62b02038a7698592995b243a16): update sample config for crda-matcher\n- [69e53bae](https://github.com/quay/clair/commit/69e53baea3b55851a2f892ac5fec1d940d79d95d): add documentation to all exported types\n- [cc7d8a37](https://github.com/quay/clair/commit/cc7d8a37e5315ba8504bd74f0c26cdafd881bbb4): consolidate and document default values\n- [7bcfc206](https://github.com/quay/clair/commit/7bcfc2063df8e5e10d8ca32752295d7bddf6fd68): move to use Mode and LogLevel types\n- [9033bc9f](https://github.com/quay/clair/commit/9033bc9f865db1d6b2e99139bc4eff6ed927e516): implement and use validator interface\n- [63c26ab8](https://github.com/quay/clair/commit/63c26ab8a2b135f3a634ae8103f9ff09cf10f7c0): add linter\n- [6759ce5c](https://github.com/quay/clair/commit/6759ce5c54515a2c1b4461c5f10917b8efdc7de8): add test for struct tags\n- [c6b2d3c5](https://github.com/quay/clair/commit/c6b2d3c5c115dad1ae31391830e2d28ef606e276): swap notifier config structs\n- [1f4ed842](https://github.com/quay/clair/commit/1f4ed8429be91e760669cf510dac4698df1e7cb5): use json for unmarshal test\n- [11509eef](https://github.com/quay/clair/commit/11509eef7718d156458c0af58f7be2e5fb7cad6a): move notifier structs into this package\n- [0439169a](https://github.com/quay/clair/commit/0439169a89605f50851fc54a5106eca9e37ba0ff): remove yaml.Node in API\n- [eb64aa50](https://github.com/quay/clair/commit/eb64aa50a4414ea295eb6c73e1df42e212fb40f2): struct ordering and simplifications\n- [579719a3](https://github.com/quay/clair/commit/579719a3458a4d71e02603b384289b38e9c204cc): remove unused FilterSets method\n- [ae7e5a3b](https://github.com/quay/clair/commit/ae7e5a3bcaf97f01290af4cee25164188ff02493): do base64 encoding smarter\n### Contrib\n- [45fae9fa](https://github.com/quay/clair/commit/45fae9fa2b559606b7f181ee1cae8d4a6e7e77a5): Better visualization of API latency.\n- [56c1fe90](https://github.com/quay/clair/commit/56c1fe9087134ccd99fe71ac7135e067b336768f): Update grafana dashboard to relect new metrics names\n- [c88c406f](https://github.com/quay/clair/commit/c88c406fb3854cfe70bfc0cfaf8158f9c0c565d9): revert headless service\n- [136f8e6b](https://github.com/quay/clair/commit/136f8e6b50bbcaf532cd7bc08a4c193313382c5d): grafana dashboard updates\n- [e2ff9a6b](https://github.com/quay/clair/commit/e2ff9a6b1545dffaed61c939727ddcdc6e3b03ec): make indexer service headless\n- [3af15937](https://github.com/quay/clair/commit/3af15937ca093e91b34a67047d5c218f7895e6c0): adding missing template variables\n- [b49b8f75](https://github.com/quay/clair/commit/b49b8f75bbc9dc5abc41005578f0d6971f28fb45): need to have single braces for strings\n- [e9a0ded5](https://github.com/quay/clair/commit/e9a0ded5669a4fcdd7ecbf1f3428f6c4b0b8c994): add initContainer to wipe VPC on startup\n- [ded05191](https://github.com/quay/clair/commit/ded051917dd730faf1392158761a25ebc175f536): add set podManagementPolicy to Parallel\n- [eddad2e6](https://github.com/quay/clair/commit/eddad2e61984defca5b0e9f842d2df44c1ea11fc): use a real target for generating configMap\n- [f0e9108e](https://github.com/quay/clair/commit/f0e9108ebb5bbdea5197ecd4d1c069af74ef7d56): different directories for template and configmap\n- [6c78d1fd](https://github.com/quay/clair/commit/6c78d1fddebb34916a055c00558a409f5279e133): add indentation\n- [7c258d39](https://github.com/quay/clair/commit/7c258d395ab5474696f50a1785ff51eabb69272e): add grafana configMap\n- [7bfc9f94](https://github.com/quay/clair/commit/7bfc9f94050d77a7028e7f14f7d1172384703bbc): update config secret\n### Deps\n- [30964e6b](https://github.com/quay/clair/commit/30964e6b4ebe0559c799367f790646645581af06): update zlog\n### Docker-Compose\n- [3c2ad90f](https://github.com/quay/clair/commit/3c2ad90ffc1d32f6adb05ca0b93618c035705841): add ActiveMQ container\n- [16950591](https://github.com/quay/clair/commit/169505918bb7cbf8b0693850dbd2aba8e10c3952): add paused skopeo container\n- [f4f243f7](https://github.com/quay/clair/commit/f4f243f799aa50e83705b74b915b43e2e40e7944): use profiles and anchors\n### Docs\n- [8cc4bf29](https://github.com/quay/clair/commit/8cc4bf29d762bee7ec7fd2c3f26608a3b158f90d): update `max_conn_pool`\n- [722c0deb](https://github.com/quay/clair/commit/722c0deb2a70ea21641bcf541c659a98667fc43e): link to upstream go package documentation\n- [7d93d376](https://github.com/quay/clair/commit/7d93d3760fd78c262c5f145892799fec9ebad5fa): add crda remote matcher details\n### Go.Mod\n- [b1fe4db1](https://github.com/quay/clair/commit/b1fe4db1d1b295b5377a0a70dbe5ae977e393eb5): update claircore\n- [bd8160c9](https://github.com/quay/clair/commit/bd8160c908a6ec27420144c5c39397c8e866bd97): update go-containerregistry\n### Grafana\n- [c65579fc](https://github.com/quay/clair/commit/c65579fc0d5e526171966423587e287060ed91c2): Adding GC DB charts to dashboard\n- [9c82a96d](https://github.com/quay/clair/commit/9c82a96d99dcda1d0fb60e48c2a8177ba9c8a29c): Update dashboard with Notifier metrics\n### Httptransport\n- [c8024bbd](https://github.com/quay/clair/commit/c8024bbdafaa7b4065eb4fe213b87ff5295195a4): fix error message\n- [9a21b499](https://github.com/quay/clair/commit/9a21b49913b28a817f80cdf242c07ccc39095a19): use new IndexerV1 handler\n- [c66e2060](https://github.com/quay/clair/commit/c66e20606bc1f615cb596fdf3846a7da63129992): add IndexerV1 type\n- [29ad2ba5](https://github.com/quay/clair/commit/29ad2ba574d7241ad6f447a42ef4515e1545bde5): add concurrency limiter\n- [85b851e9](https://github.com/quay/clair/commit/85b851e92abc4b63e50385e859e88bc220733f58): add instrumentation helper\n- [55198e82](https://github.com/quay/clair/commit/55198e82ca372485443b70a9c6c7a1bd18c5532e): add error helper\n- [d3cbb5ad](https://github.com/quay/clair/commit/d3cbb5ad25753123736d9ca96be35cf5edbe8086): fix content negotiation\n -  [#1441](https://github.com/quay/clair/issues/1441)- [96bad4f4](https://github.com/quay/clair/commit/96bad4f4fd3f63cd07d7c0e79c64e0dc31e88d25): error message correction\n- [88cf06e5](https://github.com/quay/clair/commit/88cf06e577ae5d68ca77af84442e9d317775b576): gofmt simplification\n- [6f9ca6bc](https://github.com/quay/clair/commit/6f9ca6bcc61d4baa0b78a4828f24d62751c6e3a4): simpler codegen for openapi JSON\n### Httputil\n- [9ca1e8bd](https://github.com/quay/clair/commit/9ca1e8bd04d67a42df824e58095aa284cf058798): move signed client creation\n### Indexer\n- [78819116](https://github.com/quay/clair/commit/7881911674a27753214d845e8571f64a2b35d3cd): add DeleteManifests method\n### Initialize\n- [6562b707](https://github.com/quay/clair/commit/6562b7076de65553f9e002a8129eec75febfbb50): update to new interfaces\n### Local-Dev\n- [b2d68b39](https://github.com/quay/clair/commit/b2d68b39a8ae3095d2cbcc471f8efcd30be43ea9): config changes to restore grafana functionality\n- [d0ec89d1](https://github.com/quay/clair/commit/d0ec89d11c150d5b66f404fbee77b052f67fff5b): quay: update for other config changes\n- [2a0943b1](https://github.com/quay/clair/commit/2a0943b14ac09198a51216ebe945a5ab9552cc57): prometheus: update endpoints\n- [a8348361](https://github.com/quay/clair/commit/a834836104231718b6413fa875ffcf3d0575f1ab): pgadmin: update for multiple databases\n- [c92b02c2](https://github.com/quay/clair/commit/c92b02c2439be10e264876357d3a4badb5cbfde6): grafana: update endpoint\n- [3ee44c14](https://github.com/quay/clair/commit/3ee44c148ef0ae3e5b2928b734df1ac128723d8a): traefik: use file configuration\n- [0fd11207](https://github.com/quay/clair/commit/0fd1120745da17dee5ce81a51bf64ae98cedb050): clair: update default dev config\n### Makefile\n- [356a4d7d](https://github.com/quay/clair/commit/356a4d7d526313f0d16549dba37c0d1c4f1c3da1): update targets and docs\n### Migrations\n- [2e8fc1d3](https://github.com/quay/clair/commit/2e8fc1d30ee8c6b22555557712d7edda42b21fae): add cascade constraints\n### Notifier\n- [d1e7791a](https://github.com/quay/clair/commit/d1e7791a7c86dde918eaa7f12f1acd61b9620917): update to new interfaces\n- [fb1c0231](https://github.com/quay/clair/commit/fb1c023112e16a3557b35fd1f2fa6770319965cb): move Service interface here\n- [0733900a](https://github.com/quay/clair/commit/0733900a3c67767ceaf37a544884515c4741ed79): change DeleteNotification to CollectNotifications\n- [4ba6aca0](https://github.com/quay/clair/commit/4ba6aca06c26d298b6e287ffb595ee3f2af42b68): use external concurrency in Delivery, Poller, Processor\n- [df0b2b4a](https://github.com/quay/clair/commit/df0b2b4ab579c1b3b5d808e5abf5fef6156da810): documentation and simplification pass\n### Openapi\n- [93a835ec](https://github.com/quay/clair/commit/93a835ec75959252d0d7eaac4d16a7ffca468a67): update OpenAPI spec with delete operations\n### Postgres\n- [dab3735a](https://github.com/quay/clair/commit/dab3735a0252b1f4da1b00b1079bc6270fbde2f2): update tests\n- [164219e8](https://github.com/quay/clair/commit/164219e8a9ea2d8d819509a185ace12a5ceb46f2): add Init function\n- [e0db5ac2](https://github.com/quay/clair/commit/e0db5ac2d3edfc12f93d55cba2d40b512bc115d9): refactor notification methods\n- [4650b691](https://github.com/quay/clair/commit/4650b691b7a50851522eecbf0fdc99e119375175): refactor receipt methods\n- [be87d387](https://github.com/quay/clair/commit/be87d387d36469f8f3f4969229d74101ac30596f): refactor status getters\n- [149ec354](https://github.com/quay/clair/commit/149ec354bdcb435d3ea2fe9482cbe31167f46100): refactor status setters\n### Service\n- [e86c1fbf](https://github.com/quay/clair/commit/e86c1fbfe45b7b6898ba09ca567e814e5022698e): unify connection string handling\n- [02c0dc21](https://github.com/quay/clair/commit/02c0dc21e7379f1c7cf024c0dbf41df7192c3ec1): update for changes in the config package\n### Webhook\n- [1068f493](https://github.com/quay/clair/commit/1068f4930f255b279c3c13eec6a24b1f5fa8b171): add debug server\n- [7334521f](https://github.com/quay/clair/commit/7334521fb9994d0857f7e176edc7ff48b0edb9b4): fix a code smell\n\n<a name=\"v4.3.6\"></a>\n## [v4.3.6] - 2022-01-14\n### Chore\n- [84801fa2](https://github.com/quay/clair/commit/84801fa23add925ba6b406a9e1bc5d2aaba217fb): v4.3.6 changelog bump\n### Go.Mod\n- [8562653a](https://github.com/quay/clair/commit/8562653ac42933bbf0e73d82abde09c2a337ea22): update claircore\n### Webhook\n- [ca28de41](https://github.com/quay/clair/commit/ca28de4149e5041958b220daa2c8601abb424dda): clone headers out of Config struct\n\n<a name=\"v4.3.5\"></a>\n## [v4.3.5] - 2021-11-19\n### Chore\n- [844bfd24](https://github.com/quay/clair/commit/844bfd2436fb5acb1d08975acad152d78367364a): v4.3.5 changelog bump\n- [bef331e6](https://github.com/quay/clair/commit/bef331e67e1b00a1e6f2139c0c817907de93d7d4): Revert \"chore: v3.4.5 changelog bump\"\n- [c01d88c6](https://github.com/quay/clair/commit/c01d88c6ac2513cb728703f5721ce61ff839f5a3): v3.4.5 changelog bump\n- [8849c613](https://github.com/quay/clair/commit/8849c61360520230b6c987bdc243db03b7340c9f): update claircore version\n -  [#1437](https://github.com/quay/clair/issues/1437)\n<a name=\"v4.3.4\"></a>\n## [v4.3.4] - 2021-11-05\n### Chore\n- [dddb910b](https://github.com/quay/clair/commit/dddb910b6f51e7b69042ba3db98c2a0d6cc1caa2): v4.3.4 changelog bump\n- [41d25933](https://github.com/quay/clair/commit/41d25933ad25b8ae5deb4b6da84336c2a4d400fe): update changelog to cope with submodule tags\n -  [#1421](https://github.com/quay/clair/issues/1421)\n<a name=\"v4.3.3\"></a>\n## [v4.3.3] - 2021-11-05\n### Chore\n- [4aca7b5a](https://github.com/quay/clair/commit/4aca7b5a8bd82826d4c6e41dcafcb52d72696d3a): v4.3.3 changelog bump\n- [5e060135](https://github.com/quay/clair/commit/5e060135111e07fcc3d0c2fc6c6570a98021dde7): update claircore version\n -  [#1418](https://github.com/quay/clair/issues/1418)\n<a name=\"v4.3.2\"></a>\n## [v4.3.2] - 2021-10-29\n### Chore\n- [bfd97186](https://github.com/quay/clair/commit/bfd971861e88bb21e4480ac98b8b0b7e1abf1501): v4.3.2 changelog bump\n### Go.Mod\n- [f8dff8b8](https://github.com/quay/clair/commit/f8dff8b8691ad58508457b2022425a7c533fca3c): update go-containerregistry\n -  [#1407](https://github.com/quay/clair/issues/1407)\n<a name=\"v4.3.1\"></a>\n## [v4.3.1] - 2021-10-28\n### Chore\n- [6ddf8620](https://github.com/quay/clair/commit/6ddf86205e558df705e0f21dd12c582a67566b3d): v4.3.1 changelog bump\n- [ec26f33a](https://github.com/quay/clair/commit/ec26f33a54fb2995a5898f1bd42484bf90da14fd): update claircore version\n -  [#1404](https://github.com/quay/clair/issues/1404)\n<a name=\"v4.3.0\"></a>\n## [v4.3.0] - 2021-10-01\n### Chore\n- [ce63ff26](https://github.com/quay/clair/commit/ce63ff2615cef36804346acc016c625d2fcfd630): v4.3.0 changelog bump\n### Contrib\n- [2cee4e1c](https://github.com/quay/clair/commit/2cee4e1c6b06179f8166b3090526d6d5d592433c): update secrets path\n### Dockerfile\n- [5493bbc0](https://github.com/quay/clair/commit/5493bbc0d5cdd67c52c4d1786ddc3226086403d6): remove dumb-init, tar\n\n<a name=\"v4.3.0-rc.0\"></a>\n## [v4.3.0-rc.0] - 2021-09-28\n### Chore\n- [b5ea27bc](https://github.com/quay/clair/commit/b5ea27bc324308e359adf3084b92301441457001): v4.3.0-rc.0 changelog bump\n- [d65432d3](https://github.com/quay/clair/commit/d65432d3cc7dbe57de1b7be7cfd4745005e44159): update claircore version\n- [5ab6c761](https://github.com/quay/clair/commit/5ab6c76121a03543f8edf748fa511a7d486df54c): automation around v2 issues\n- [b53aeb71](https://github.com/quay/clair/commit/b53aeb714aa3536dd7aa295ea6306a20e69e03cf): update claircore version\n- [f06b0419](https://github.com/quay/clair/commit/f06b0419f3aa38d54a56fda3a07729fab80ca6a1): update go version\n### Cicd\n- [7f192003](https://github.com/quay/clair/commit/7f192003eef5eb6e0d42e2d21264410f71af96ce): create expiring, dated image\n- [673ab1eb](https://github.com/quay/clair/commit/673ab1ebd1efc1cfc778316ca7c4f183687138a1): update go version\n- [ace31fe6](https://github.com/quay/clair/commit/ace31fe6e8737fb4ab4ad66750e7637c8af36e0d): re-vendor after modifying the module\n- [411facfa](https://github.com/quay/clair/commit/411facfaeb292bd74e1865a2b48767ac1560febf): use push flag with build\n- [7d29f694](https://github.com/quay/clair/commit/7d29f694b0de2751ef1d1886ca2b15317a3b33d0): add \"nightly\" workflow\n- [e36bc822](https://github.com/quay/clair/commit/e36bc8226ab762948cc0f855751b5d55eea44afe): add script to edit module for nightly\n### Clairctl\n- [66a3137a](https://github.com/quay/clair/commit/66a3137af6d6b5f1640c32e55aa5a65b71d606a0): update to new API\n### Config\n- [b4ec7aa3](https://github.com/quay/clair/commit/b4ec7aa3e2d4bd272e8f518ff3bd2b3e2434216d): add CacheAge field\n### Contrib\n- [73082541](https://github.com/quay/clair/commit/73082541568b014b89407bee9d30a0142ccd21ea): stop unneeded vendoring\n- [87f8f6cb](https://github.com/quay/clair/commit/87f8f6cb7525ad6e85c49907f17a35523de2a19f): update saas file\n### Documentation\n- [28b78c54](https://github.com/quay/clair/commit/28b78c5415696ab23a1465f5b631f0be2cb97704): fix typo\n### Httptransport\n- [22a25484](https://github.com/quay/clair/commit/22a25484e149bc5665e5e78754552cbda6967b9f): add Cache-Control header to VulnerabilityReport response\n- [1d6ce962](https://github.com/quay/clair/commit/1d6ce962d79c2cad86e8c7f6c5a247bf1f329d92): documentation updates\n- [f7fdc906](https://github.com/quay/clair/commit/f7fdc906c3bd0e779e0480b2444d6068ae939546): fix auth test logging\n### Matcher\n- [b3c3e385](https://github.com/quay/clair/commit/b3c3e38574df1d53c37fbf9c7fe0128b2c76c35e): default garbage collection on\n### Notifier\n- [0f6d0e4a](https://github.com/quay/clair/commit/0f6d0e4a6950c64c74f18f1c4d0dee3fbc2eaf01): move to ctxlock\n### Shutdown\n- [6b7029df](https://github.com/quay/clair/commit/6b7029df2f74a44f57f924545442465874103e0b): introduce the new NotifyContext\n\n<a name=\"v4.2.3\"></a>\n## [v4.2.3] - 2021-09-28\n### Chore\n- [8bc69b0a](https://github.com/quay/clair/commit/8bc69b0a4c83a3275ee8dc17042a58357cd167ec): v4.2.3 changelog bump\n- [7ee97e50](https://github.com/quay/clair/commit/7ee97e506372454963c81a3c5c045ce487056d87): bump claircore version\n\n<a name=\"v4.2.2\"></a>\n## [v4.2.2] - 2021-08-17\n### Chore\n- [3762d9c3](https://github.com/quay/clair/commit/3762d9c3459f5bb1b12fab46453358b79f3952ba): v4.2.2 changelog bump\n- [90f2909e](https://github.com/quay/clair/commit/90f2909ed8d29b90dad205f9ad92bbbf9660f88c): bump claircore version\n\n<a name=\"v4.2.1\"></a>\n## [v4.2.1] - 2021-08-16\n### Chore\n- [1882e1ee](https://github.com/quay/clair/commit/1882e1eed86bfcd1e9a9667bc1900fdb1da081db): v4.2.1 changelog bump\n- [b48814b6](https://github.com/quay/clair/commit/b48814b6b79fc75fb91dce858490e2d4a8f2db3d): bump claircore version\n\n<a name=\"v4.2.0\"></a>\n## [v4.2.0] - 2021-08-10\n### Chore\n- [2c7fd9cb](https://github.com/quay/clair/commit/2c7fd9cbcdc1d37e42ea9338ae72ada0b1da10dc): v4.2.0 changelog bump\n- [1e0a43aa](https://github.com/quay/clair/commit/1e0a43aa1d6714c79a5b9f5b4568c0de16d3e127): bump claircore to v0.5.2\n### Http\n- [4cd09528](https://github.com/quay/clair/commit/4cd0952833b7048ccf8fbf024a7c61a3c4a6c34b): rate limit index report requests\n### Introspection\n- [5b129ad9](https://github.com/quay/clair/commit/5b129ad997fb33b02c6ac1c605fb8444399a5c43): capture rate-limited requests\n\n<a name=\"v4.2.0-rc.2\"></a>\n## [v4.2.0-rc.2] - 2021-07-29\n### Chore\n- [263d6677](https://github.com/quay/clair/commit/263d6677e6cf661c281de6087bb93628b79c3ee0): update claircore\n### Deployment\n- [c888a3f2](https://github.com/quay/clair/commit/c888a3f21b6397355761b6eb056d95487bbf3eca): Fix microdnf install inconsistencies\n\n<a name=\"v4.2.0-rc.1\"></a>\n## [v4.2.0-rc.1] - 2021-07-20\n### All\n- [9ce2af3f](https://github.com/quay/clair/commit/9ce2af3fbc3b7040de37ef47accf37e055e0d730): remove jzelinskie from codeowners\n### Chore\n- [72df3577](https://github.com/quay/clair/commit/72df35774fcb65e772214378b25da870fd5c7adb): update CODEOWNERS\n- [248e7961](https://github.com/quay/clair/commit/248e796184f8da454b21564b3ec6e43020391b92): update responserecorder\n- [5354f107](https://github.com/quay/clair/commit/5354f1073cf073202c7c98388c13abc6b6743d08): bump claircore version\n### Clairctl\n- [45538e0c](https://github.com/quay/clair/commit/45538e0ccee4d4ee12247da11f295ff32dcbe4ff): add support for s3 registries using V4\n - Fixes [#1264](https://github.com/quay/clair/issues/1264)### Config\n- [af6a1f49](https://github.com/quay/clair/commit/af6a1f49b35f10faff1102a5d776050eb74cd0d0): omit Authorization header for empty claims\n - Fixes [#1283](https://github.com/quay/clair/issues/1283)### Docker\n- [22ee21df](https://github.com/quay/clair/commit/22ee21df6f712a7461c9dfd6c48b06c1da5b8670): reflect quay Dockerfile updates\n### Httptransport\n- [fee8bc5a](https://github.com/quay/clair/commit/fee8bc5ae70ee6030f7c2d6ad9b901e6fb7aaaea): remove key management API\n### Initialize\n- [1e26de57](https://github.com/quay/clair/commit/1e26de5737f369b1c55f873471c43a1fa1919fef): use new enrichers\n### Introspection\n- [8d128903](https://github.com/quay/clair/commit/8d128903a1990e33c036edab6f02bd3c28472407): use the response recorder\n### Keymanager\n- [dc0b7079](https://github.com/quay/clair/commit/dc0b70791b4ed37fbb6bb697c40cafb4824adfab): remove package\n### Local-Dev\n- [0285c300](https://github.com/quay/clair/commit/0285c3000abc1bf588b9773dfb225fa74afcf2cb): add grafana to docker-compose\n- [5df0b7b4](https://github.com/quay/clair/commit/5df0b7b4c46a88e9f476e99a4b0c03ede88722ea): remove whitelist env var for quay conf\n### Matcher\n- [49bfd4d7](https://github.com/quay/clair/commit/49bfd4d791c087634df749f9895caebd0e5c4f41): disable updaters creates empty updater sets\n - Fixes [#1273](https://github.com/quay/clair/issues/1273)### Migrations\n- [cef8142a](https://github.com/quay/clair/commit/cef8142a725016c49425b4dd27d61708fec899e3): add future key table removal\n### Notifier\n- [85ac7bb8](https://github.com/quay/clair/commit/85ac7bb877d7cb2154d5a59c6cf13bb690d8179c): remove KeyStore interface\n### Openapi\n- [5d032233](https://github.com/quay/clair/commit/5d032233ad90b10728bb564e92019b8b9b9b1197): fix paths\n -  [#1280](https://github.com/quay/clair/issues/1280)### Postgres\n- [a5ae3426](https://github.com/quay/clair/commit/a5ae34261ee7e4451ed3d6d27b28e0a7d472f44d): update to new test database harness\n- [6184ce33](https://github.com/quay/clair/commit/6184ce332fd1531d11bc27e2150fe16b899d6449): update test harness\n- [0ca77cec](https://github.com/quay/clair/commit/0ca77cec0af74b4304c2ccf805ee3dea642cad37): remove KeyStore implementation and tests\n### Service\n- [d1ca564c](https://github.com/quay/clair/commit/d1ca564c59f844ce27538c41527ddf639aabb3e4): remove KeyManager and KeyStore\n### Services\n- [b3e490db](https://github.com/quay/clair/commit/b3e490dbc1022a5d08f7ceef8565928fdd19be98): disable transport compression in matcher\n### Webhook\n- [79089a44](https://github.com/quay/clair/commit/79089a44122587eea8ceab246b7d34615ebcf9dd): remove keymanager usage\n\n<a name=\"v4.1.6\"></a>\n## [v4.1.6] - 2021-09-28\n### Chore\n- [018a2db2](https://github.com/quay/clair/commit/018a2db20131d0e56c1f52732a9ed1d73cf23d61): v4.1.6 changelog bump\n- [63b5b3d9](https://github.com/quay/clair/commit/63b5b3d97f50fc1c22eb4be55e9cfc2cdb7ed86b): bump claircore version\n\n<a name=\"v4.1.5\"></a>\n## [v4.1.5] - 2021-08-17\n### Chore\n- [7df9b906](https://github.com/quay/clair/commit/7df9b9067c3b7e7a9351bbc73b2dc48d507203af): v4.1.5 changelog bump\n- [f4d8255c](https://github.com/quay/clair/commit/f4d8255c3a7db8b7e59066bc13ff25d39be938ae): bump claircore version\n\n<a name=\"v4.1.4\"></a>\n## [v4.1.4] - 2021-08-16\n### Chore\n- [92eef18d](https://github.com/quay/clair/commit/92eef18de3a6dfc08373598d542e6644beab9d0a): v4.1.4 changelog bump\n- [bee9c642](https://github.com/quay/clair/commit/bee9c642bba0aad96fcc48c7c2b2c7b0692c6825): bump claircore version\n\n<a name=\"v4.1.3\"></a>\n## [v4.1.3] - 2021-08-11\n### Chore\n- [df2624c1](https://github.com/quay/clair/commit/df2624c1c9256d5e578db2047a772d372a561ea0): v4.1.3 changelog bump\n- [7ac3d94f](https://github.com/quay/clair/commit/7ac3d94f12726e3f0f88281ecab47341fb5fbb0a): bump claircore\n\n<a name=\"v4.1.2\"></a>\n## [v4.1.2] - 2021-08-06\n### Chore\n- [e6c9bc28](https://github.com/quay/clair/commit/e6c9bc2890b75b91a7d3a7aa5257a37b25d05dfb): v4.1.2 changelog bump\n- [1e130f28](https://github.com/quay/clair/commit/1e130f2844d26ceac4a26f0548d8bc2bea79a91f): bump claircore version\n### Introspection\n- [804cbedb](https://github.com/quay/clair/commit/804cbedbd6724bc22f9adac02186f45bf77c703f): use the response recorder\n -  [#1318](https://github.com/quay/clair/issues/1318)### Services\n- [bc60dcc2](https://github.com/quay/clair/commit/bc60dcc29d4440636f68911e79b08b188ac8e81e): disable transport compression in matcher\n\n<a name=\"v4.1.1\"></a>\n## [v4.1.1] - 2021-06-15\n### Chore\n- [6528f738](https://github.com/quay/clair/commit/6528f738a2a0cf303625040459d5590050f75294): v4.1.1 changelog bump\n- [a3a8020c](https://github.com/quay/clair/commit/a3a8020c2225de42e248352503b06704d7167839): bump claircore version\n### Clairctl\n- [343e7da0](https://github.com/quay/clair/commit/343e7da0f82f83e40f88a6fb64c5f6441ad2f27a): add support for s3 registries using V4\n - Fixes [#1264](https://github.com/quay/clair/issues/1264)### Config\n- [ad9eccf9](https://github.com/quay/clair/commit/ad9eccf9c93a00bda4300ae783b42753d2c10d35): omit Authorization header for empty claims\n -  [#1284](https://github.com/quay/clair/issues/1284)\n<a name=\"v4.1.0\"></a>\n## [v4.1.0] - 2021-05-13\n### All\n- [66387930](https://github.com/quay/clair/commit/66387930f2b80087a32a1aeddc9b1ef16eec01e1): use RateLimiter where it seems appropriate\n### Chore\n- [8bcbbf1b](https://github.com/quay/clair/commit/8bcbbf1be8b14051a05cc86bc404834b5778a6e8): v4.1.0 changelog bump\n- [04f2cb71](https://github.com/quay/clair/commit/04f2cb71acc8eceac0d1a7766c5ebfcfa01150ee): bump claircore version\n### Cicd\n- [8b0cdb38](https://github.com/quay/clair/commit/8b0cdb38fa8f4d701e0ef804e37728721798f564): use golang major version tag for dev env\n- [c1895c43](https://github.com/quay/clair/commit/c1895c433dfc3a872cce2c1468801ecdddf2e962): use quay.io/projectquay/golang image\n### Claircore\n- [bc2b0591](https://github.com/quay/clair/commit/bc2b0591d3ea3a07498820bc625f7dc9cd5ce934): update to use new libvuln API\n### Clairctl\n- [c80a99d1](https://github.com/quay/clair/commit/c80a99d14ed96e539a79212fb23f608a03ee636c): move to updates.Manager interface\n- [30f86961](https://github.com/quay/clair/commit/30f86961b88b7a590157f28fc6cb8f22f16dfa06): move to zlog\n### Httputil\n- [ed8ffc50](https://github.com/quay/clair/commit/ed8ffc50b56c9b11873f00bb2deb4fba9107ec95): create package and RateLimiter\n### Initialize\n- [5df82e19](https://github.com/quay/clair/commit/5df82e19e971c67ebdecf3f92682d4ae897db53a): update call to Libindex contstructor\n### Introspection\n- [ec59a431](https://github.com/quay/clair/commit/ec59a431032713654e2eb7a29ad7c446dd16a490): enable readiness endpoint\n\n<a name=\"v4.1.0-alpha.3\"></a>\n## [v4.1.0-alpha.3] - 2021-05-04\n### Chore\n- [f3d64ffc](https://github.com/quay/clair/commit/f3d64ffc3f3b8ebcf4d91d60117e8a268d840fd8): v4.1.0-alpha.3 changelog bump\n- [01c44cc3](https://github.com/quay/clair/commit/01c44cc39dd5d5c644d6849dfe204a1ffd02bab8): update claircore revision\n### Cicd\n- [4535b9f4](https://github.com/quay/clair/commit/4535b9f41c310b3d590e2e0d8e3758d0d39d5105): changelog fixups\n### Config\n- [1f9b5657](https://github.com/quay/clair/commit/1f9b56577957ce28044b221af58b160328a671a2): validate based on combo mode or not\n### Httptransport\n- [9e67501d](https://github.com/quay/clair/commit/9e67501d818045749c4f263128b72e7cb6856bd1): fix LatestUpdateOperations method\n### Notifier\n- [6d331530](https://github.com/quay/clair/commit/6d331530c7a8714a16d32ce3ca6e74ec8afc5184): check msg contents in integration tests\n- [cc4a10ff](https://github.com/quay/clair/commit/cc4a10ffedfc2edaae229cd953b3602ca16da2ec): remove direct zerolog use\n### Tests\n- [08734ab2](https://github.com/quay/clair/commit/08734ab233457dc4bba1b071331f0c8024f6b4dd): fix small unit test race\n- [6e50ec2e](https://github.com/quay/clair/commit/6e50ec2eec4eb50711ac48f14181e5a7ca075a70): add testing command\n- [1e92bd24](https://github.com/quay/clair/commit/1e92bd241ba42eec3cca6c8e983ba937caa23bd9): fix small race\n\n<a name=\"v4.1.0-alpha.2\"></a>\n## [v4.1.0-alpha.2] - 2021-04-09\n### Chore\n- [e0eea383](https://github.com/quay/clair/commit/e0eea383b9e791b5b041136b88f1b69b3d4841bb): v4.1.0-alpha.2 changelog bump\n### Codec\n- [d5cac131](https://github.com/quay/clair/commit/d5cac1315481a87f596f395e1c2da2bf57eaf18c): use stdlib time.Time encoding\n - Closes [#1231](https://github.com/quay/clair/issues/1231)### Docs\n- [60f9684a](https://github.com/quay/clair/commit/60f9684accfd7e6b9e1bd585a55874803e1160f5): minor updates\n- [cbdc9caa](https://github.com/quay/clair/commit/cbdc9caab450489377ab1d6bb19429d54df639cc): update configuration file reference\n\n<a name=\"v4.1.0-alpha.1\"></a>\n## [v4.1.0-alpha.1] - 2021-04-05\n### All\n- [a5bfaeb3](https://github.com/quay/clair/commit/a5bfaeb33cc43350234345aba0059a02098f0d67): switch to using codec package\n### Chore\n- [493beb13](https://github.com/quay/clair/commit/493beb13d3a9d0739bcffa74217f7e2107f8438d): v4.1.0-alpha.1 changelog bump\n- [47344357](https://github.com/quay/clair/commit/473443575e0160cdc83574dcd48982d9922ddf4e): v4.1.0-alpha.1 changelog bump\n- [6e8a8383](https://github.com/quay/clair/commit/6e8a838305c6bdc6a71e3b3a9ee5735660ebbd22): bump cc to v0.4.0\n- [5a6f1c3b](https://github.com/quay/clair/commit/5a6f1c3b24f9c178838e905a1435078f9706a7b9): update claircore version for database fix\n- [ea0378d4](https://github.com/quay/clair/commit/ea0378d4d67376ebb924b7fb78d4d4f22ad9e1de): bump cc v0.3.0\n- [6e195c99](https://github.com/quay/clair/commit/6e195c99a14139360c8d09f90c94024eb7d27b67): fix yaml file indentation issue\n### Cicd\n- [b1145e3a](https://github.com/quay/clair/commit/b1145e3a1c5e8faf3d1a64a403de940386b73102): sort changelog by semver\n- [7dc55fa9](https://github.com/quay/clair/commit/7dc55fa9bb0b968ab580c7d6d0ea4ffa053eaba0): bump in go.16, bump out go1.14\n- [d5e57afb](https://github.com/quay/clair/commit/d5e57afb594d58cf817a962d9e282c820ab6577e): enable CI on stable branches\n- [f7737e58](https://github.com/quay/clair/commit/f7737e58cfca3640d4a901a658317becb47ba2af): fix openshift ci/cd script\n- [30c0311a](https://github.com/quay/clair/commit/30c0311a8b1584a40f5b956b3b3d9e9ab7eee18a): update golang container for go-mod in app-sre\n- [cb656dfb](https://github.com/quay/clair/commit/cb656dfbd69ff1ce11976c7de672b50277091ab8): add notifier to app interface\n- [9254ab66](https://github.com/quay/clair/commit/9254ab66ea7f1b9711242026045da35b7ffa2782): use quay.io image in CI and Dockerfile\n### Clair\n- [ecd8999c](https://github.com/quay/clair/commit/ecd8999cbfd6b9140f0aa8aebc11a67cbefcb4d2): fix initialization error logging\n- [dc2f8936](https://github.com/quay/clair/commit/dc2f8936a564fbc234e1b8f00a3eb4778452f2ec): reorganize initialization\n- [391c2f76](https://github.com/quay/clair/commit/391c2f766bcbf9c2392c12dca2bb9f225f1ef424): add Shutdown struct\n### Claircore\n- [f1834212](https://github.com/quay/clair/commit/f1834212272b07f02228b04a67e9339001dc51f8): bump to v0.2.0\n### Clairctl\n- [5740a1b0](https://github.com/quay/clair/commit/5740a1b0427c81ae5f447add372db43a1ec73dbf): Add subpath to clairctl\n### Client\n- [bd50a957](https://github.com/quay/clair/commit/bd50a9570d996578e0209286a66ec3d7f41d6aaf): remove request body buffering\n- [ce11fd70](https://github.com/quay/clair/commit/ce11fd7077c2fb10715b37a8248b42583d930462): fix panic on request failure\n -  [#1186](https://github.com/quay/clair/issues/1186)### Codec\n- [1fb6dcfd](https://github.com/quay/clair/commit/1fb6dcfd32143520aa348b184e865be7a6081134): add package for codec pooling\n### Config\n- [e9390fad](https://github.com/quay/clair/commit/e9390fadc24e53e455360f709e79674f752c4a29): add matchers settings\n- [eb519e07](https://github.com/quay/clair/commit/eb519e0752d3cf7f5f8daeefd4ad9bd29cbfa8c2): allow gc to be disabled\n- [f2d73134](https://github.com/quay/clair/commit/f2d731341722e3d59c9351c10b7e8eedbe74f276): rework into specific validators\n### Docs\n- [0f230f99](https://github.com/quay/clair/commit/0f230f99f22150a00b36654ee8a5a7674e5507f7): add support matrix\n- [102ae88d](https://github.com/quay/clair/commit/102ae88dd84c1f769b8c037226d92b301d887aab): update cli reference\n- [9d0a2b20](https://github.com/quay/clair/commit/9d0a2b20a6808f0e86cbd4f2a6046a6c7abdc2ea): fix psk related config references\n- [44303dcc](https://github.com/quay/clair/commit/44303dccfd26935fd66ff041e22602c709c4a428): install clairctl correctly\n- [a3bb1b6d](https://github.com/quay/clair/commit/a3bb1b6d8caebf228ac39b8793d5326bea0d1b55): use correct clairctl subcommands\n - Closes [#1122](https://github.com/quay/clair/issues/1122)### Documentation\n- [2e659250](https://github.com/quay/clair/commit/2e6592500fbe9c3197782133965de6503b07b6ab): modified testing.md for clarity\n -  [#1180](https://github.com/quay/clair/issues/1180)### Httptransport\n- [21dc720a](https://github.com/quay/clair/commit/21dc720a7f1e63e731eadbf72cf192913bf88c39): add mime type to indexer and matcher handler\n- [8616cc68](https://github.com/quay/clair/commit/8616cc68b030fc417c693b3d2dc7208015ce9f4e): return Accepted when not ready\n- [1ac26daf](https://github.com/quay/clair/commit/1ac26daf5501876495ec09f4e67b50eaca4bd1a5): fix panic in metrics registration\n- [7305b3d7](https://github.com/quay/clair/commit/7305b3d735786e340833e045e2cd5888c8af866b): use correct handler for state endpoint\n- [df5e7f96](https://github.com/quay/clair/commit/df5e7f9658b1fed55d067013656115b062127c23): check for err before deferring resp.Body.Close()\n### Initialize\n- [8a2df099](https://github.com/quay/clair/commit/8a2df099fe2e69a572e8d81b352f688f82de341a): remove New function\n- [2d27ae5c](https://github.com/quay/clair/commit/2d27ae5cd3fe55737c2fa02b46616ec09ade47c5): add standalone initialization functions\n### Instrospection\n- [b78f954d](https://github.com/quay/clair/commit/b78f954dbf3210f7deb87dc371b1d35cba216d78): bump to opentelemetry 0.16.0\n### Introspection\n- [1ece08f4](https://github.com/quay/clair/commit/1ece08f49434828c8c672f08ec45844b99187983): database metrics for notifier\n- [84ba35f2](https://github.com/quay/clair/commit/84ba35f29ee81849cb2f424b3624895f9bd05a79): implement prometheus http\n### Local-Dev\n- [1c85589a](https://github.com/quay/clair/commit/1c85589abdef98b5af8d4f6e2cd9eb5db6a723a0): remove unintented change in config.yaml\n### Logging\n- [9f3d167d](https://github.com/quay/clair/commit/9f3d167d5d85d345c7d0ee666be075a545a553f4): move to zlog throughout\n### Matcher\n- [858c540b](https://github.com/quay/clair/commit/858c540b2ef9b8d7f71d16bbe3ba797f73f654ab): add Initialized method\n### Notifier\n- [e7bf3b17](https://github.com/quay/clair/commit/e7bf3b1730e04ad10ec4baef1643556bf5626090): construct notification objects directly\n- [99622021](https://github.com/quay/clair/commit/99622021c594149a0b0d183b6349e2ee7139e5d2): do AffectedManifests calls in chunks\n### Severity_mapping\n- [8e39fa40](https://github.com/quay/clair/commit/8e39fa40eebca7b50ab29f0001686fa7c5c49e1e): remove defcon1 severity\n### Updaters\n- [8105b033](https://github.com/quay/clair/commit/8105b033fb53f0907373f6af76af954fe95a856d): plumb update retention in\n\n<a name=\"v4.0.6\"></a>\n## [v4.0.6] - 2021-06-15\n### Chore\n- [d1694147](https://github.com/quay/clair/commit/d16941471a9d2e7e5434dab9173b70c1966f75f9): v4.0.6 changelog bump\n- [73adee2f](https://github.com/quay/clair/commit/73adee2f86d17460a41c0b7f08a320442ff18b91): bump claircore to v0.1.26\n### Cicd\n- [cd64d3ff](https://github.com/quay/clair/commit/cd64d3ffebdb4bf3f42ebb8f755f86ad7866d5d5): changelog fixups\n### Clairctl\n- [f745d455](https://github.com/quay/clair/commit/f745d455a26145ab06fca2efa23a9fd9da7cda2d): add support for s3 registries using V4\n - Fixes [#1264](https://github.com/quay/clair/issues/1264)\n<a name=\"v4.0.5\"></a>\n## [v4.0.5] - 2021-04-16\n### Chore\n- [b92ba981](https://github.com/quay/clair/commit/b92ba981540bf13344f5fe48d5683fd2c600e92b): v4.0.5 changelog bump\n- [486ccfb9](https://github.com/quay/clair/commit/486ccfb9d8baac5f468acf0cc0752d7d2d9f8ce4): bump cc stable to v0.1.25\n\n<a name=\"v4.0.4\"></a>\n## [v4.0.4] - 2021-03-25\n### Chore\n- [4bfd7d11](https://github.com/quay/clair/commit/4bfd7d11c3f1290af889e258283f585f5f4abbd4): v4.0.4 changelog bump\n- [4ff4c908](https://github.com/quay/clair/commit/4ff4c9082573cadf8c96b6e4f5e67aa46ac31699): bump cc to stable v0.1.24\n### Cicd\n- [0800ba46](https://github.com/quay/clair/commit/0800ba46b160c30c623f0ad7062fe7882604233e): sort changelog by semver\n### Initialize\n- [7c4787bf](https://github.com/quay/clair/commit/7c4787bfb1585d54f0ef371487228cb4941db5a0): wire up DisableUpdaters option\n\n<a name=\"v4.0.3\"></a>\n## [v4.0.3] - 2021-03-12\n### Chore\n- [a844fb22](https://github.com/quay/clair/commit/a844fb2290bdaeb2d6f99c013e1ea3ab2b17dc6f): v4.0.3 changelog bump\n- [a26eb80d](https://github.com/quay/clair/commit/a26eb80d83bf0e993ccd7df977be6bc456a0de4c): bump cc stable to v0.1.23\n\n<a name=\"v4.0.2\"></a>\n## [v4.0.2] - 2021-02-18\n### Chore\n- [5c236e6d](https://github.com/quay/clair/commit/5c236e6d2afe05c92245f817337341ae6478125d): 4.0.2 changelog bump\n### Client\n- [8b63953e](https://github.com/quay/clair/commit/8b63953e99e0246a9428205cf51c66ec3af65ba3): fix panic on request failure\n -  [#1186](https://github.com/quay/clair/issues/1186) -  [#1188](https://github.com/quay/clair/issues/1188)\n<a name=\"v4.0.1\"></a>\n## [v4.0.1] - 2021-02-15\n### Chore\n- [8a392f1b](https://github.com/quay/clair/commit/8a392f1bd3a381e98ece87e9ccd4842113563bb4): v4.0.1 changelog bump\n- [c47be87d](https://github.com/quay/clair/commit/c47be87d6fbb0a34960001a45246d3936e5f8710): bump cc to v0.1.22 stable\n\n<a name=\"v4.0.0\"></a>\n## [v4.0.0] - 2020-12-15\n### Chore\n- [73cdf7d9](https://github.com/quay/clair/commit/73cdf7d904a1aa6341a27c3ecae11c89d7444e39): v4.0.0 changelog bump\n### Reverts\n- Dockerfile: Get build image from Quay instead of DockerHub\n- cicd: use golang image from quay.io\n\n\n<a name=\"v4.0.0-rc.24\"></a>\n## [v4.0.0-rc.24] - 2020-12-11\n### Chore\n- [d3b3497d](https://github.com/quay/clair/commit/d3b3497d997020a879eca1190150ce73642d90b9): v4.0.0-rc.24 changelog bump\n- [0515f09a](https://github.com/quay/clair/commit/0515f09a2fbc4f5a29fda476f6be1f0e77f5d8fa): bump cc to v0.1.20\n\n<a name=\"v4.0.0-rc.23\"></a>\n## [v4.0.0-rc.23] - 2020-12-07\n### Chore\n- [2080ece3](https://github.com/quay/clair/commit/2080ece3032daf0f28f85dd07749886a451cf71f): v4.0.0-rc.23 changelog bump\n- [289208cf](https://github.com/quay/clair/commit/289208cfab02b587366434372e5295150306abf2): bump cc to v0.1.19\n### Cicd\n- [30444f3b](https://github.com/quay/clair/commit/30444f3b782044373ab174ffa2628aaf9495d832): use golang image from quay.io\n\n<a name=\"v4.0.0-rc.22\"></a>\n## [v4.0.0-rc.22] - 2020-12-02\n### Chore\n- [8ef85097](https://github.com/quay/clair/commit/8ef8509753387e1dcdf709e71fee16cbdd7146f9): v4.0.0-rc.22 changelog bump\n- [bbe1cd8f](https://github.com/quay/clair/commit/bbe1cd8f19f62a0becbd110d27cb20b6bd699f36): claircore v0.1.18 bump\n### Documentation\n- [d962bef8](https://github.com/quay/clair/commit/d962bef8140516c739e322a7406c0068e2164d45): update links in howto/api\n\n<a name=\"v4.0.0-rc.21\"></a>\n## [v4.0.0-rc.21] - 2020-12-01\n### Chore\n- [c6933f0a](https://github.com/quay/clair/commit/c6933f0ab78810eeb8dd3fb66cea0d86e9a7d1de): v4.0.0-rc.21 changelog bump\n- [56484395](https://github.com/quay/clair/commit/5648439518cbf10b44fc7fac8a3710c044487bb8): bump cc to v0.0.17\n### Cidi\n- [a576bf29](https://github.com/quay/clair/commit/a576bf290ba9ccbae5d869a5f12ff2897585a2c0): bump create pull request action\n### Clairctl\n- [835af272](https://github.com/quay/clair/commit/835af272fad49342f51adb4633ff639de3cc14a1): fix and codify import arguments\n- [b9ef1073](https://github.com/quay/clair/commit/b9ef1073ca48ed5ed7caaa3e0fbad03a7d83592c): update import and export online help\n- [9883e80f](https://github.com/quay/clair/commit/9883e80f331190e60de711b0705e9b37017fc5b1): unifiy config, client handling\n### Config\n- [dc8ba891](https://github.com/quay/clair/commit/dc8ba8912fa482378ef393aa51b4e9528d2877f2): expose notification summary toggle\n- [bb3cd669](https://github.com/quay/clair/commit/bb3cd669f66345aaa0fc5df6f502f34922cc069e): add 'omitempty' to 'updaters' config struct for correct marshalling\n### Direct-Delivery\n- [ea564d48](https://github.com/quay/clair/commit/ea564d489f2cb8b43c6ea1c90eb40bbcf39ebc63): Fix slices in direct notifier\n### Dockerfile\n- [c18563d9](https://github.com/quay/clair/commit/c18563d90b5ca9d6185f1e503c54912bcdee7564): Get build image from Quay instead of DockerHub\n### Docs\n- [425fc38a](https://github.com/quay/clair/commit/425fc38af9837527421ebf550259f9d7e8371039): add clairctl's new powers to the reference\n- [f4169c43](https://github.com/quay/clair/commit/f4169c43d283ede2678ab620db5fa4ee9d6b2c37): Add information about AMQP delivery compatibility\n### Local-Dev\n- [550f6b93](https://github.com/quay/clair/commit/550f6b93b178846b243585d912c7d6efdd6abcae): fix pgadmin name\n### Notifier\n- [153f3e36](https://github.com/quay/clair/commit/153f3e3682921b84249950ddfbd186d825377bef): add summary tests\n- [dd2e16db](https://github.com/quay/clair/commit/dd2e16db6e952fd5135dadb99ce8ec4b6ea65361): optionally disable per-manifest summary\n- [77ca6535](https://github.com/quay/clair/commit/77ca6535649c4860b17d345505d44c0511d12bb0): log failed delivery reason\n\n<a name=\"v4.0.0-rc.20\"></a>\n## [v4.0.0-rc.20] - 2020-11-02\n### Chore\n- [ba70ca3e](https://github.com/quay/clair/commit/ba70ca3eda70c4654d0f44348ec89231dfb40f44): v4.0.0-rc.20 changelog bump\n- [e0e1f0db](https://github.com/quay/clair/commit/e0e1f0dbcd8e7ff46b3c34066f9e894ce124e84f): bump claircore to v0.1.15\n\n<a name=\"v4.0.0-rc.19\"></a>\n## [v4.0.0-rc.19] - 2020-10-26\n### Chore\n- [ecdcc8ea](https://github.com/quay/clair/commit/ecdcc8ea104161ecbc36edd7cbf2d6e49e1f836d): v4.0.0-rc.19 changelog bump\n### Config\n- [157628df](https://github.com/quay/clair/commit/157628dfe1c7f1f837dc8df0e622a2d64a31c79a): add custom config marshaling\n### Go.Mod\n- [1d4f6c33](https://github.com/quay/clair/commit/1d4f6c33fa314f9a550ba4b2701ec208aff9c93f): new claircore version\n\n<a name=\"v4.0.0-rc.18\"></a>\n## [v4.0.0-rc.18] - 2020-10-21\n### Chore\n- [f0881e4a](https://github.com/quay/clair/commit/f0881e4a16050a902dc1df3afe77f7ea280d77ef): v4.0.0-rc.18 changelog bump\n### Notifier\n- [40abaa67](https://github.com/quay/clair/commit/40abaa67e5b3e4453d6e6cf2ec3452a0c3570f42): do less work\n\n<a name=\"v4.0.0-rc.17\"></a>\n## [v4.0.0-rc.17] - 2020-10-19\n### Chore\n- [37f77912](https://github.com/quay/clair/commit/37f7791287d28de53828ee35c139dc78d5f3e962): claircore bump v0.1.13\n### Cicd\n- [d2bc2b6c](https://github.com/quay/clair/commit/d2bc2b6cda9ff609e0e9883467095f6e425cdae9): remove deprecated set-env commands\n- [0cfda4dd](https://github.com/quay/clair/commit/0cfda4ddf6a86f01fddd8b8143ce4f64b23a0527): update documentation action\n- [49e01d60](https://github.com/quay/clair/commit/49e01d60feeb9fdce618d82b7b328f80bdb4fa89): fix container build\n### Clairctl\n- [2363778b](https://github.com/quay/clair/commit/2363778b4086a62de55cef1afa8f3c519328ec25): add environment variables for clairctl\n### Docs\n- [dc4bda49](https://github.com/quay/clair/commit/dc4bda499e7aeed655536aa8409b6512113eb7ea): add Makefile target to build docs website\n### Local-Dev\n- [15b607a9](https://github.com/quay/clair/commit/15b607a98e92d87f083d4ca406c7d795fc373cd5): add pgadmin4 container\n### Notifier\n- [673bd0fe](https://github.com/quay/clair/commit/673bd0fe32d5422e8eb3dff3716a2bfce81b891c): fix poller loop\n\n<a name=\"v4.0.0-rc.16\"></a>\n## [v4.0.0-rc.16] - 2020-10-09\n### Chore\n- [88407e25](https://github.com/quay/clair/commit/88407e254d33b7c66e0484ee980daa3f69b1683e): v4.0.0-rc.16 changelog bump\n### Cicd\n- [96909bf3](https://github.com/quay/clair/commit/96909bf3bfca47c8ea1ce0b53d9d5b7e9897b55e): exclude darwin/arm64\n- [7786d7d3](https://github.com/quay/clair/commit/7786d7d34b01b48f9d2d87682faa54936c25a21f): more debugging\n- [89c26ec5](https://github.com/quay/clair/commit/89c26ec55443050d2e3dd425b4e37a15fe96c061): more debugging\n- [7a1eeafd](https://github.com/quay/clair/commit/7a1eeafd0bebc261f356f05244c3a946bfee8c87): make sure the workspace exists\n- [68c0318b](https://github.com/quay/clair/commit/68c0318b1b225387402a3d4e66c1803f64fd1d98): make empty changelog on manual trigger\n- [4e5ee290](https://github.com/quay/clair/commit/4e5ee2908d49191f54951cc276cf95387f90b83c): rig up a workflow_dispatch to help debugging\n\n<a name=\"v4.0.0-rc.15\"></a>\n## [v4.0.0-rc.15] - 2020-10-09\n### Chore\n- [8d87481b](https://github.com/quay/clair/commit/8d87481b80e495c52c8677eae2db1e288576dcb1): v4.0.0-rc.15 changelog bump\n### Cicd\n- [d7582487](https://github.com/quay/clair/commit/d758248755fe18f4658eee93cc5d40dc2b206c06): maybe there's some newline issues\n\n<a name=\"v4.0.0-rc.14\"></a>\n## [v4.0.0-rc.14] - 2020-10-09\n### Chore\n- [e46b4f89](https://github.com/quay/clair/commit/e46b4f89aa2a4a13062c2dd4726b66d476ef8b24): v4.0.0-rc.14 changelog bump\n### Cicd\n- [58a987d6](https://github.com/quay/clair/commit/58a987d6078506357933b378c299576bf15e39c8): invalid goos+goarch pair\n\n<a name=\"v4.0.0-rc.13\"></a>\n## [v4.0.0-rc.13] - 2020-10-09\n### Chore\n- [63263273](https://github.com/quay/clair/commit/63263273a7440cb93fcd044a33ad2d0b5a70f425): v4.0.0-rc.13 changelog bump\n### Cicd\n- [f6a28c2d](https://github.com/quay/clair/commit/f6a28c2dcc252b4e8be08158a227b8fed7b71b27): fix goos/goarch\n\n<a name=\"v4.0.0-rc.12\"></a>\n## [v4.0.0-rc.12] - 2020-10-08\n### Chore\n- [61ce6759](https://github.com/quay/clair/commit/61ce675946bd186f5a0be800ab26cad3f008a0f1): v4.0.0-rc.12 changelog bump\n### Cicd\n- [28dcd944](https://github.com/quay/clair/commit/28dcd9443f95c4243a94896b04ff7b3075a1c21c): parallelize release process, keep test failures\n### Clairctl\n- [b1fee08e](https://github.com/quay/clair/commit/b1fee08e43401fdbe6fd9af222bbe64b6412c773): update some interactive help\n### Go.Mod\n- [af868db1](https://github.com/quay/clair/commit/af868db100705074f718f1a8f7caaafaa8b88220): update dependencies\n### Local-Dev\n- [3b602925](https://github.com/quay/clair/commit/3b60292591900173f4eda02461d3891e48d070c2): make quay container ignore validations\n### Notifier\n- [0c1554e9](https://github.com/quay/clair/commit/0c1554e9aa6cb8d1116376bf303c8af8e5112b23): ensure Content-Type header present in webhook notification\n- [a2d5f9b9](https://github.com/quay/clair/commit/a2d5f9b92371094ade82b8f9bef19d72fb8addcd): copy url struct\n### Pull Requests\n- Merge pull request [#1086](https://github.com/quay/clair/issues/1086) from alecmerdler/webhook-notifier-headers\n\n\n<a name=\"v4.0.0-rc.11\"></a>\n## [v4.0.0-rc.11] - 2020-10-02\n### Chore\n- [f9f86350](https://github.com/quay/clair/commit/f9f86350521ed9c02258c713a992d76976fab9cc): v4.0.0-rc.11 changelog bump\n### Config\n- [a4e04105](https://github.com/quay/clair/commit/a4e04105cb15173ee3b06090de7573540969a89c): allow HTTP client to specify claims\n- [5aba7278](https://github.com/quay/clair/commit/5aba72783bff70fa41769cbe33365a34835bd73f): ensure yaml/json struct tag for auth 'Issuer' field are the same\n### Notifier\n- [57e1ed0a](https://github.com/quay/clair/commit/57e1ed0a1178ca5b980f65e5030a00a82acf52c8): pass configured client into notifier\n### Pull Requests\n- Merge pull request [#1078](https://github.com/quay/clair/issues/1078) from alecmerdler/fix-issuer-struct-tag\n\n\n<a name=\"v4.0.0-rc.10\"></a>\n## [v4.0.0-rc.10] - 2020-10-01\n### Chore\n- [2c54a824](https://github.com/quay/clair/commit/2c54a82467af83db44b9dcccdfb73a320699426b): v4.0.0-rc.10 changelog bump\n### Cicd\n- [f04bc76c](https://github.com/quay/clair/commit/f04bc76c666b3d1c5098dcc14b928a165066549a): api reference check\n### Docs\n- [0d8a2a4a](https://github.com/quay/clair/commit/0d8a2a4aae8358597e3b67cbf4deb8be79e76dec): bump api reference\n### Go.Mod\n- [bd1a3b77](https://github.com/quay/clair/commit/bd1a3b772c2dbd6ae87a94c5608f1cdc2bf0044a): update claircore version\n### Httptransport\n- [2c9762b0](https://github.com/quay/clair/commit/2c9762b0351449b5b09249da513178b3d2757985): remove redundant method check\n### Openapi\n- [015d862d](https://github.com/quay/clair/commit/015d862dce63068951d145c5512ec95977262a03): yamllint and spellcheck\n- [d06dabfe](https://github.com/quay/clair/commit/d06dabfe57fbb90ba376e8f1fa81d18db90ed070): change OperationIDs for notification endpoints\n\n<a name=\"v4.0.0-rc.9\"></a>\n## [v4.0.0-rc.9] - 2020-09-29\n### Cicd\n- [04fab4a7](https://github.com/quay/clair/commit/04fab4a7ef9344ee5e3578c60873a5e56bff64b7): build container with local checkout\n\n<a name=\"v4.0.0-rc.8\"></a>\n## [v4.0.0-rc.8] - 2020-09-29\n### Chore\n- [6181cc6c](https://github.com/quay/clair/commit/6181cc6c7a47203482f420ccd257318a0d478978): v4.0.0-rc.8 changelog bump\n### Cicd\n- [7520b091](https://github.com/quay/clair/commit/7520b0912967a0811660ead8e35f80d85899c000): fix container building\n\n<a name=\"v4.0.0-rc.7\"></a>\n## [v4.0.0-rc.7] - 2020-09-29\n### Chore\n- [9282d299](https://github.com/quay/clair/commit/9282d299fda9f1dee321d0f9b883651e18bf4bf8): v4.0.0-rc.7 changelog bump\n### Cicd\n- [195ce7a5](https://github.com/quay/clair/commit/195ce7a59a5db9d7e1f854e25242434e82635fa7): move container building out of container\n\n<a name=\"v4.0.0-rc.6\"></a>\n## [v4.0.0-rc.6] - 2020-09-29\n### Chore\n- [2f5756de](https://github.com/quay/clair/commit/2f5756de91a4367c4ebda047f2dcbc29bf252d6f): v4.0.0-rc.6 changelog bump\n### Cicd\n- [f6aa6e6e](https://github.com/quay/clair/commit/f6aa6e6e028f73c5ae05e65e2c870972fa04393d): use multiline string for clairctl build command\n\n<a name=\"v4.0.0-rc.5\"></a>\n## [v4.0.0-rc.5] - 2020-09-29\n### Chore\n- [9b9ab323](https://github.com/quay/clair/commit/9b9ab323147b14dbd9fecf4c0620c8f34ae7b23a): v4.0.0-rc.5 changelog bump\n### Cicd\n- [9aa8adc1](https://github.com/quay/clair/commit/9aa8adc10d4e48b01abff666e160b498d982124a): fix clairctl builds\n\n<a name=\"v4.0.0-rc.4\"></a>\n## [v4.0.0-rc.4] - 2020-09-29\n### Chore\n- [31adc6de](https://github.com/quay/clair/commit/31adc6dec374eb2ff1a08cf1154d327cf5f30865): v4.0.0-rc.4 changelog bump\n- [d141c5ca](https://github.com/quay/clair/commit/d141c5cac5d4e4b13f614ebedb89c99ee3ebf8b0): bump claircore to v0.1.9\n- [cd34ea9e](https://github.com/quay/clair/commit/cd34ea9e264a8690bb88866f96a407949b14b0a1): remove unused files\n### Cicd\n- [600c737c](https://github.com/quay/clair/commit/600c737c659ea03a6695cc6c0dda0f1d8cce4497): constrain changelog\n- [c447bcce](https://github.com/quay/clair/commit/c447bcce4ce4546228b91559c85108ec7a3194af): commit check regexp fix\n- [54ee2d25](https://github.com/quay/clair/commit/54ee2d25cd05593edc38a94103bef459f6219c4b): change log generation and releases\n### Docs\n- [d34acaf5](https://github.com/quay/clair/commit/d34acaf54063d04979820a6be6e8c0181fc0fb65): update for v4\n### Httptransport\n- [e1144aaf](https://github.com/quay/clair/commit/e1144aaf0af143d63c59d1cfcc8f06490377c1d8): made discovery endpoint more Accepting\n### Misc\n- [18e4db2c](https://github.com/quay/clair/commit/18e4db2c0298696797975911ff4c7b48f41b54fc): doc and commit check fixes\n### Notifier\n- [7d95067f](https://github.com/quay/clair/commit/7d95067f4762ec1aa79879e23c7956eaef8ca4f7): remove first update constraint\n\n<a name=\"v4.0.0-rc.3\"></a>\n## [v4.0.0-rc.3] - 2020-09-23\n### Auth\n- [f00698ba](https://github.com/quay/clair/commit/f00698ba36ac1b88bb77f21ca4e9d99caf28b0b1): psk fixup\n### Chore\n- [a38501b3](https://github.com/quay/clair/commit/a38501b3aabb92b244f51268e565c1763f62622b): claircore bump to v0.1.8\n### Client\n- [32b327f7](https://github.com/quay/clair/commit/32b327f701f579d595b5b94cb7ca08813e366101): fix nil check\n### Deployment\n- [bc4c3243](https://github.com/quay/clair/commit/bc4c3243c0e0bb952bb2e5d7a29d9e5d08e71962): use service prefix for simplified path routing\n### Docs\n- [4f35fd09](https://github.com/quay/clair/commit/4f35fd0959cbfb2f7c195c45d17d1c90ca1b7390): rework mdbook\n### Logging\n- [15f3755b](https://github.com/quay/clair/commit/15f3755b349064a9093fa199e2a89e89038a1b61): use duration stringer\n- [10b87578](https://github.com/quay/clair/commit/10b87578aa55ecaed27d36964b1de18e7eaffe42): add similar logging to v3\n\n<a name=\"v4.0.0-rc.2\"></a>\n## [v4.0.0-rc.2] - 2020-09-11\n### Chore\n- [f41fba50](https://github.com/quay/clair/commit/f41fba5087f0ff5ebcd3724cb22975a5547fa572): bump cc and golang container\n\n<a name=\"v4.0.0-rc.1\"></a>\n## [v4.0.0-rc.1] - 2020-09-10\n### Auth\n- [29ed5f60](https://github.com/quay/clair/commit/29ed5f60b8dfe882f95aae7d61b1e373e06a2145): use better guesses for \"aud\" claim\n- [6932ad32](https://github.com/quay/clair/commit/6932ad3264c3b1760ef46d094b25c12664cee1cc): add keyserver algorithm allowlist\n- [dc91ec9e](https://github.com/quay/clair/commit/dc91ec9e96db7ab7eee853c89a768ac0414a8f9a): test multiple PSK signing algorithms\n### Clairctl\n- [c0a9c0b7](https://github.com/quay/clair/commit/c0a9c0b7d336de1377eb3cb14fb8a1af97a49b3e): init default updaters\n- [050bc2d1](https://github.com/quay/clair/commit/050bc2d1f44824f2573c6219a21d3a00e1ce7a76): add import and export commands\n### Config\n- [03cf7555](https://github.com/quay/clair/commit/03cf7555ab13856fddd5b71e1374d1f7281a800e): update matcher configurables\n- [daf2e296](https://github.com/quay/clair/commit/daf2e296e9d2bc2b4d40f18ff00937829f469c04): reorganize updater configuration\n### Deployment\n- [0fe5f731](https://github.com/quay/clair/commit/0fe5f7315c90bc5c0e984fa6de72b96c79dec27c): ubi8 based dockerfile\n -  [#198](https://github.com/quay/clair/issues/198)### Go.Mod\n- [28957dce](https://github.com/quay/clair/commit/28957dce0b23c2018bb3a874a4e45651173f7260): update claircore version\n### Httptransport\n- [fb03692e](https://github.com/quay/clair/commit/fb03692ecbacd9b4ada902d9fe4b2e211fced82e): add integration tests\n### Initialize\n- [98c8ffd6](https://github.com/quay/clair/commit/98c8ffd67dee0f5768b4fa28c86f89f114b2af7c): wire through new configuration options\n### Local-Dev\n- [d1b60120](https://github.com/quay/clair/commit/d1b6012093025413e1f3774acd895b679c21c6fc): implement quay local development\n### Notifier\n- [9bd4f4df](https://github.com/quay/clair/commit/9bd4f4dfb1a5acccf6295a03fe71b51d8259b16f): test mode implementation\n- [4b35c887](https://github.com/quay/clair/commit/4b35c88740c93689dd7270079f962a79cc77d27f): log better\n- [717f8a0d](https://github.com/quay/clair/commit/717f8a0dea82aa7e8c8f1c06de5468b06498dd0b): correctly close channels after amqp delivery\n\n<a name=\"v4.0.0-alpha.7\"></a>\n## [v4.0.0-alpha.7] - 2020-06-01\n### Config\n- [3ccc6e03](https://github.com/quay/clair/commit/3ccc6e03be0ce1b6c439d5c0649ee785dc7c559f): add support for per-scanner configuration\n### Dockerfile\n- [5a73cb49](https://github.com/quay/clair/commit/5a73cb49d64e839d7675979b5e3f348d94dd26a5): make -mod=vendor opportunisitic ([#999](https://github.com/quay/clair/issues/999))\n -  [#999](https://github.com/quay/clair/issues/999)- [de32b072](https://github.com/quay/clair/commit/de32b0728ccdbafb85988e2f87618c9d576fc87e): update to alpine:3.11 for newest rpm\n### Go.Mod\n- [badcac44](https://github.com/quay/clair/commit/badcac4420b44d92d1d56d5f9c9a09daf8a5db50): update yaml to v3\n### Httptransport\n- [54c6a6d4](https://github.com/quay/clair/commit/54c6a6d46e6087690287c4b247668e954d6913af): document exposed API\n\n<a name=\"v4.0.0-alpha.6\"></a>\n## [v4.0.0-alpha.6] - 2020-05-01\n### Go.Mod\n- [ef5fbc4d](https://github.com/quay/clair/commit/ef5fbc4d6dcf877a05a5a12b6dd2a7a7c50568cf): bump claircore version for severity fix\n\n<a name=\"v4.0.0-alpha.5\"></a>\n## [v4.0.0-alpha.5] - 2020-04-30\n### Config\n- [a93271b3](https://github.com/quay/clair/commit/a93271b3be48ebe617363751d64e26840678583e): implement base64 -> []byte conversion ([#984](https://github.com/quay/clair/issues/984))\n -  [#984](https://github.com/quay/clair/issues/984)\n<a name=\"v4.0.0-alpha.4\"></a>\n## [v4.0.0-alpha.4] - 2020-04-20\n### Config\n- [2ed3c2c8](https://github.com/quay/clair/commit/2ed3c2c800bb9639618a86f33916625b0a595f49): rework auth config\n### Httptransport\n- [5683018f](https://github.com/quay/clair/commit/5683018f2e7d091897a238aa82e88da56941fee8): serve OpenAPI definition\n\n<a name=\"v4.0.0-alpha.3\"></a>\n## [v4.0.0-alpha.3] - 2020-04-14\n### Clair\n- [fa95f5d8](https://github.com/quay/clair/commit/fa95f5d80c86f3e916661156f99dac6fcc91a3bb): bump claircore version\n### Clairctl\n- [2e681788](https://github.com/quay/clair/commit/2e6817881eed93af469abd7e16839961aa812469): remove log.Lmsgprefix\n- [0282f68b](https://github.com/quay/clair/commit/0282f68bf381a5b0a592079819e38b3d88296f92): report command\n### Client\n- [1ba68911](https://github.com/quay/clair/commit/1ba68911163afb001cd89cf84862506f008edcf4): add differ and refactor client\n### Config\n- [b2666e57](https://github.com/quay/clair/commit/b2666e57202d7c690a40d7c86975c13e0b3db56e): set a canonical default port\n### Dockerfile\n- [33da12a3](https://github.com/quay/clair/commit/33da12a3bb9a28fdbcc6302caa4212d38a2acbbb): run as unprivledged user by default\n### Documentation\n- [fe324a58](https://github.com/quay/clair/commit/fe324a58e6be8c36da74afcd5487d0da4a547d5b): start writing v4-specific docs\n### Httptransport\n- [e783062b](https://github.com/quay/clair/commit/e783062b41af06eed250d289a2dfa43a4b6aeb25): wire in update endpoints\n- [9cd6cabf](https://github.com/quay/clair/commit/9cd6cabf62b60bd47bd2f6546cd5a806f1d79ad3): report write errors via trailer\n### Workflows\n- [f0039247](https://github.com/quay/clair/commit/f0039247e1f4c8a2f97b81896782cb802cdeffd8): add go testing matrix\n- [ea5873bc](https://github.com/quay/clair/commit/ea5873bc8f57eb4d545e0a25a2da868371196926): fix gh-pages argument\n- [cec05a35](https://github.com/quay/clair/commit/cec05a35f71dffb6603a2debb14d5388e80643c7): more workflow automation\n- [a19407e4](https://github.com/quay/clair/commit/a19407e4fd40585b45ffceb507e24c194db78ccc): fix asset name\n### Pull Requests\n- Merge pull request [#955](https://github.com/quay/clair/issues/955) from alecmerdler/openapi-fixes\n\n\n<a name=\"v4.0.0-alpha.2\"></a>\n## v4.0.0-alpha.2 - 2020-03-26\n### *\n- [74efdf6b](https://github.com/quay/clair/commit/74efdf6b51e3e625ca9f122e7aa88e88f4708a68): update roadmap\n - Fixes [#626](https://github.com/quay/clair/issues/626)- [ce15f735](https://github.com/quay/clair/commit/ce15f73501b758b3d24e06753ce62123d0a36920): gofmt -s\n- [5caa821c](https://github.com/quay/clair/commit/5caa821c80a4efa2986728d6f223552b44f6ce15): remove bzr dependency\n- [033cae7d](https://github.com/quay/clair/commit/033cae7d358b2f7b866da7d9be3367d902cdf035): regenerate bill of materials\n- [1f5bc263](https://github.com/quay/clair/commit/1f5bc26320bc58676d88c096404a8503dca7a4d8): rename example config\n- [d0ca4d1f](https://github.com/quay/clair/commit/d0ca4d1fe6a4b28be5f2a82c640f8886551034fb): added bill-of-materials\n- [324ad5f2](https://github.com/quay/clair/commit/324ad5f2435d02a10211e3134fb3353aeb62c55d): move all references in README to HEAD\n- [836d37b2](https://github.com/quay/clair/commit/836d37b2758e5abcce1d85ee680bd5d0d65f0538): use `path/filepath` instead of `path`\n- [51e40862](https://github.com/quay/clair/commit/51e4086298f6f17b5ad92cb6c021f16e80440d46): Create a ROADMAP\n- [16a652fa](https://github.com/quay/clair/commit/16a652fa4726a0e06492ce36eaae281c83ccd774): refresh godeps\n- [f6baac36](https://github.com/quay/clair/commit/f6baac3628d89f00cd6bb4f28163a3d089b429df): refresh godeps\n- [8f7e6585](https://github.com/quay/clair/commit/8f7e6585746e756fac5ca2686e5c730977656f99): remove tests from docker file\n- [1e1eb921](https://github.com/quay/clair/commit/1e1eb9218dc4e04cbe70bd6b4e22186cd0b820e2): add postgres 9.4 to travis\n- [5fdd9d1a](https://github.com/quay/clair/commit/5fdd9d1a07220ede12a7009b54641103fcfe2c24): add metadata support along with NVD CVSS\n- [40a7c8a0](https://github.com/quay/clair/commit/40a7c8a00d580d1a7db4f8bca2152bc5eb491c0a): refresh godeps\n- [4bdbd5e6](https://github.com/quay/clair/commit/4bdbd5e6db2e8e919938c3cb348350ba91966a12): fix several tests\n- [b8b7be3f](https://github.com/quay/clair/commit/b8b7be3f8127e0858c14eda0557ae51f2f897129): remove health checker\n- [82175dcf](https://github.com/quay/clair/commit/82175dcfe9e36766c5e88199d8045e2f0733f483): add missing copyright headers\n- [2c150b01](https://github.com/quay/clair/commit/2c150b015e63d7ee5f45a6a875df8a14a2ac0b24): refactor & do initial work towards PostgreSQL implementation\n- [8c1d3c9a](https://github.com/quay/clair/commit/8c1d3c9a861d17d5b6ef59f5479192eb35b0a02b): Fix `authentification` typo\n### .Github\n- [9b1f2058](https://github.com/quay/clair/commit/9b1f2058338b8aeaa5441091b4920731235f1353): add stale and issue template enforcement\n### API\n- [0151dbae](https://github.com/quay/clair/commit/0151dbaef81cae54aa95dd8abf36d58414de2b26): change api port to api addr, rename RunV2 to Run.\n - Fixes [#446](https://github.com/quay/clair/issues/446)- [a378cb07](https://github.com/quay/clair/commit/a378cb070cb9ec56f363ec08adb8e023bfb3994e): drop v1 api, changed v2 api for Clair v3.\n### All\n- [fbbffcd2](https://github.com/quay/clair/commit/fbbffcd2c2a34d8a6128a06a399234b444c74d09): add opentelemetry hooks\n### Alpine\n- [59e6c628](https://github.com/quay/clair/commit/59e6c628dcb2b4306ed971a609f7a50973ca2b2c): refactor fetcher & git pull on update\n- [9be305d1](https://github.com/quay/clair/commit/9be305d19f5fec286492cd09ed71623f356fcdc0): truncate namespace to \"vMAJOR.MINOR\"\n- [f8457b98](https://github.com/quay/clair/commit/f8457b98e7dfe1c6fde7783c59a1c1143823a0e2): compile alpine into clair binary\n- [3d90cac4](https://github.com/quay/clair/commit/3d90cac427e52a6470f112746fa86a595ffe8717): add support for v3.4 YAML schema\n### Api\n- [69c0c843](https://github.com/quay/clair/commit/69c0c84348c74749cd1d12ee4e4959991621a59d): Rename detector type to DType\n- [48427e9b](https://github.com/quay/clair/commit/48427e9b8808f86929ffb905952395c91644f04e): Add detectors for RPC\n- [dc6be5d1](https://github.com/quay/clair/commit/dc6be5d1b073d87b2405d84d33f5bb5f6ced490e): remove handleShutdown func\n- [30644fcc](https://github.com/quay/clair/commit/30644fcc01df7748d8e2ae15c427f01702dd4e90): remove dependency on graceful\n- [58022d97](https://github.com/quay/clair/commit/58022d97e3ec7194e89522c9adb866a85c704378): renamed V2 API to V3 API for consistency.\n- [c6f0eaa3](https://github.com/quay/clair/commit/c6f0eaa3c82197f15371b4d2c8af686d8a7a569f): fix remote addr shows reverse proxy addr problem\n- [a4edf385](https://github.com/quay/clair/commit/a4edf385663b2e412e1fd64f7d45e1ee01749798): v2 api with gRPC and gRPC-gateway\n - Fixes [#98](https://github.com/quay/clair/issues/98)- [6a50bbb8](https://github.com/quay/clair/commit/6a50bbb8b89cb78e38a9cb13b3cfc3fff277739c): fix 404 error logging\n- [7aa88690](https://github.com/quay/clair/commit/7aa88690af4c85133519747e3633a458e6f44ba0): WriteHeader on health endpoint\n - Fixes [#141](https://github.com/quay/clair/issues/141)- [f14e4de4](https://github.com/quay/clair/commit/f14e4de4d82d51f8dc41d55a782b2c4b535bae7e): fix anchor link in docs\n- [3563cf90](https://github.com/quay/clair/commit/3563cf9061d80c52319d7814e0319c4c3689df95): fix pagination token that's returned to match what has been passed\n- [274a1620](https://github.com/quay/clair/commit/274a1620a50815149671368f1a1feda409830286): log instead of panic when a response could not be marshaled\n- [8d767005](https://github.com/quay/clair/commit/8d767005063ebb285f6890500e35d0bab2174340): add call duration in logs\n- [418ab08c](https://github.com/quay/clair/commit/418ab08c4b248fe119a83179b25b6aef43070014): adjust postLayer error codes\n- [f40f6a5a](https://github.com/quay/clair/commit/f40f6a5ab6ebf4275be58c1b2e3a48c246f9df2e): add missing link field in vulnerability in getLayer\n- [0e9a7e17](https://github.com/quay/clair/commit/0e9a7e174032e1304b367a30b689fbea91c59da4): close gzip writer to flush it\n- [db974ae7](https://github.com/quay/clair/commit/db974ae72205fb4f65ebad8a997ca686df658aef): fix postLayer response headers\n- [6f02119c](https://github.com/quay/clair/commit/6f02119c56182b53bc6d39eb67dff8e3501ebe34): add bad requests to insert layer\n- [ca2b0ccf](https://github.com/quay/clair/commit/ca2b0ccfcb336cb8440cc4d3c4071c30e61f36b0): support gzip responses\n- [c7aa7c4d](https://github.com/quay/clair/commit/c7aa7c4db4259d659e5c866ca3d0a61dd5cc247b): reorder constants and add comments\n- [4516d6fd](https://github.com/quay/clair/commit/4516d6fd73a9d69a4d041dc880f4bd5a00b4ad01): make postLayer returns a Layer\n- [d19a4348](https://github.com/quay/clair/commit/d19a4348dfdd0312d89857e2540e550b7f235fa9): implement fernet encryption of pagination tokens\n- [b8c534cd](https://github.com/quay/clair/commit/b8c534cd0da918626bc590d533874d20545a91a7): fix putVulnerability (fill missing Namespace.Name and Name fields)\n- [c2061dc6](https://github.com/quay/clair/commit/c2061dc69e7202a22affe8ab18513da17adc3f0e): fix negative timestamps in notifications\n- [f68012de](https://github.com/quay/clair/commit/f68012de0031ca9df9292e69bd940147840079fb): fix 404->500 and NPE issues\n- [c504d2ed](https://github.com/quay/clair/commit/c504d2ed0e409ebc1a579259c8f8c80a3ba6e1a6): add FeatureFromDatabaseModel\n- [f351d630](https://github.com/quay/clair/commit/f351d6304e91d5eced2161efdaddf57b662e7395): add \"Content-Type\" and \"Server\" headers\n- [2d8d9ca4](https://github.com/quay/clair/commit/2d8d9ca4010ec237a1dbd8fa56810180280df582): finish initial work on v1 API\n- [b9a6da4a](https://github.com/quay/clair/commit/b9a6da4a57698020d9e361e0b32acbf1b6de4f8c): implement delete notification\n- [96e96d94](https://github.com/quay/clair/commit/96e96d948d226398df8c9e9662afe6ea47d262cf): handle last page for notifications\n- [3eaae478](https://github.com/quay/clair/commit/3eaae478f9e8c2267c208bbbbd0c05029dcc7e53): implement get notification\n- [116ce1a8](https://github.com/quay/clair/commit/116ce1a806ca60aa50fbc1592b2118ab351d6b4d): fix log message when stopping the API server\n- [c05848e3](https://github.com/quay/clair/commit/c05848e32da7e2923d57d63d63fb131e2e611c0b): implement put vulnerability\n- [8209922c](https://github.com/quay/clair/commit/8209922c0c93992d484e9369e80d7981c5d6300c): implement delete vulnerability\n- [dc99d45f](https://github.com/quay/clair/commit/dc99d45f47392f833ad254cbccfb190b5fc5acdc): refactor endpoints and implement get vulnerability\n- [6ac9b5e6](https://github.com/quay/clair/commit/6ac9b5e6451abecb879ed63a446e051d206b6af6): fix graceful stop\n- [9a8d4aa5](https://github.com/quay/clair/commit/9a8d4aa591c6103531fc681ff03c6b1f89d85f4a): implement post vulnerability\n- [38aeed4f](https://github.com/quay/clair/commit/38aeed4f2c629ba79a7c176a74cdba3e0e0573dd): implement get namespaces route\n- [b916fba4](https://github.com/quay/clair/commit/b916fba4c6514d8c92f407c04873cec515b25d7d): implement delete layer route\n- [04c73519](https://github.com/quay/clair/commit/04c7351911feb697712a77a8a06364709b448778): use pointers in models to get proper `omitempty` semantics\n- [1a5aa88b](https://github.com/quay/clair/commit/1a5aa88b18e74973c681b2937b2acb289167ceb8): use only one layer envelope\n- [fa45d516](https://github.com/quay/clair/commit/fa45d516df5bbd46f622cc175f537d7dcee472da): add JSON tags to API models\n- [d130d2fa](https://github.com/quay/clair/commit/d130d2fab477964c0302218b9bb184b91a6056bf): implement getLayer\n- [6b3f95dc](https://github.com/quay/clair/commit/6b3f95dc0313268e36d9bee5d7ca6482049739e9): fix /v1 router and some status codes\n- [be9423b4](https://github.com/quay/clair/commit/be9423b489e4e694c35b187175aba10bb77012c8): add request / response types and rename some fields\n- [822ac7ab](https://github.com/quay/clair/commit/822ac7ab4c10f57463b3d2712f7ebedb26721354): add initial work on the new API\n- [6e20993b](https://github.com/quay/clair/commit/6e20993bac425bd14b86d5198f586ff8fc9a6b9c): simplify getLayer route and JSON output\n- [e8b16175](https://github.com/quay/clair/commit/e8b16175effcff9b9ead13aeadab7897f5331d37): return 400 if we can't extract a layer\n- [99463822](https://github.com/quay/clair/commit/9946382223431179b1133786bc6debfa1e288fee): Extracted client cert & HTTP JSON Render to utils.\n- [9db0e634](https://github.com/quay/clair/commit/9db0e634011d6e805d3a542ab03cf2956b7d9734): Specify what packages cause the layer to have vulnerabilities.\n### Api,Database\n- [a75b8ac7](https://github.com/quay/clair/commit/a75b8ac7ffe3ccd7ff9c4718e547c6c5103e9747): updated version_format documentation.\n - Fixes [#514](https://github.com/quay/clair/issues/514)### Api/Database\n- [6d2eedf1](https://github.com/quay/clair/commit/6d2eedf12131611b7be6c50dec04e9f55f363833): add the layer name that add each feature in getLayer\n- [e444e93c](https://github.com/quay/clair/commit/e444e93c975d977a5178351e41388c9d52d3872a): Add the ability to delete layers\n### Api/Prometheus\n- [83b19b61](https://github.com/quay/clair/commit/83b19b6179c663a11b8e6d0651397a5262a3fc3e): add prometheus metrics to API routes\n### Api/V1\n- [ebd0170f](https://github.com/quay/clair/commit/ebd0170f5b5144d7ab5e4facf44eb99fe147fdc3): fix JSON struct tag misnomer\n- [d4522e9c](https://github.com/quay/clair/commit/d4522e9c6e3a6237863cc83d0f2fd1be74212613): indexed layers for notifications\n- [68250f39](https://github.com/quay/clair/commit/68250f392b7820c30ee042a905ff9eb25860c186): create namespace type\n - Fixes [#99](https://github.com/quay/clair/issues/99)### Api/V3\n- [32b11e54](https://github.com/quay/clair/commit/32b11e54eb287ed0d686ba72fe413b773b748a38): Add feature type to API feature\n- [f550dd16](https://github.com/quay/clair/commit/f550dd16a01edc17de0e3c658c5f7bc25639a0a1): remove dependency on google empty message\n- [d7a751e0](https://github.com/quay/clair/commit/d7a751e0d4298442883fde30ee37c529b2bb3719): prototool format\n### Api/V3/Clairpb\n- [6b9f668e](https://github.com/quay/clair/commit/6b9f668ea0b657526b35008f8efd9c8f0a46df9b): document and regenerate protos\n- [ec5014f8](https://github.com/quay/clair/commit/ec5014f8a13605458faf1894bb905f2123ded0a7): regen protobufs\n- [389b6e99](https://github.com/quay/clair/commit/389b6e992790f6e28b77ca5979c0589e43dbe40a): generate protobufs in docker\n### Api/Worker\n- [53e62577](https://github.com/quay/clair/commit/53e62577bc9adc0f002f97fc87bdf9387e3ee663): s/Authorization/Headers ([#167](https://github.com/quay/clair/issues/167))\n -  [#167](https://github.com/quay/clair/issues/167)- [9b5afc79](https://github.com/quay/clair/commit/9b5afc79cab103721a599d65c45825d1faed766d): introduce optional authorization\n- [e78d076d](https://github.com/quay/clair/commit/e78d076d02343f2d3d167e3e5f96364c3837fec0): adjust error codes in postLayer\n### CODEOWNERS\n- [f20a72c3](https://github.com/quay/clair/commit/f20a72c34ef80b4c1dee7b9984aa713f82e6c342): add Louis\n- [abf6e747](https://github.com/quay/clair/commit/abf6e74790294bb765a68765afa9d8e73c3fab22): init\n### Clair\n- [42b1ba9f](https://github.com/quay/clair/commit/42b1ba9f91f9174397280152eca5a0096342019e): use Etag header to communicate indexer state change\n- [fd5993f9](https://github.com/quay/clair/commit/fd5993f9765cc23355e5895105a15b71e5eb3156): add \"mode\" argument\n- [40913295](https://github.com/quay/clair/commit/409132958e0538046e3481d3197e192316b06d91): change version information\n- [8cbddd18](https://github.com/quay/clair/commit/8cbddd187e7065315417ca2f86a5e261f3d92651): better introspection server defaults\n- [c097454c](https://github.com/quay/clair/commit/c097454c182daa68427918d0ba2fe24bbdf6ed71): logging and introspection setup\n- [a003aa41](https://github.com/quay/clair/commit/a003aa414ead82a32b24a977e301e5697718ec43): add configuration for introspection\n- [d9db7c15](https://github.com/quay/clair/commit/d9db7c153ce80d3d47bbb342bd6ef873bc2954b4): use \"Updaters\" config option\n- [48daeaea](https://github.com/quay/clair/commit/48daeaeacc5a1444a07cc6ddc20b4b800d8b43be): fix header casing\n- [fb28e569](https://github.com/quay/clair/commit/fb28e569da21f847c7bbc2f97807485ea007e698): remove os.Exit call on clean shutdown\n- [8039e1c9](https://github.com/quay/clair/commit/8039e1c95f56353e47aaa5ed66b80244ac2d2cad): add authorization checking\n- [1b413362](https://github.com/quay/clair/commit/1b41336265126c23b152d18c28ea6e0fd3d6baf8): update claircore to 0.0.14\n- [791610f1](https://github.com/quay/clair/commit/791610f1c893fc76d6fcf350a7383a2479aa723a): remove goautoneg\n- [7b6ef7da](https://github.com/quay/clair/commit/7b6ef7da8c125111ec37fe61206dce1ee25408ec): reset writers when pulled from pool\n- [ad73d747](https://github.com/quay/clair/commit/ad73d747fcc6c674752eaf5ae7ccdcb6fa4daead): remove vendor directory\n- [00eff59a](https://github.com/quay/clair/commit/00eff59af580893d3e045333fa095d3507a528f1): rewrite imports\n- [1f2ceeb8](https://github.com/quay/clair/commit/1f2ceeb8f7fcf9e8ce94206f76a8b610b84424ca): create module\n- [c6497dda](https://github.com/quay/clair/commit/c6497dda0a95a3309dc649761243250634a31d40): Fix namespace update logic\n- [465687fa](https://github.com/quay/clair/commit/465687fa94b4e9fe00e0ba1190989d0d454c14ab): Add more logging on ancestry cache hit\n- [5b237649](https://github.com/quay/clair/commit/5b2376498bbc0ea0a893754887defce4daa59daa): Use builder pattern for constructing ancestry\n- [02832401](https://github.com/quay/clair/commit/028324014ba3b7111e4e4533d6a8d4d99bb1fd72): Implement worker detector support\n- [88961527](https://github.com/quay/clair/commit/889615276af2c1d5ac04971a237e09d2e9fa6bda): move worker to top level package\n- [e5c567f3](https://github.com/quay/clair/commit/e5c567f3f98b68d990e2b41c3a7f1f0261dcf060): mv notifier to top level\n- [9c63a639](https://github.com/quay/clair/commit/9c63a639440d5669bbc318f85aa672d4ce9fa10f): mv updater clair and mv severity to db\n- [343e24eb](https://github.com/quay/clair/commit/343e24eb7eb6336dca94df7b43499dfef08ee4fe): remove `types` package\n- [19e9d123](https://github.com/quay/clair/commit/19e9d1234ee2f001c01b688fd9dde84498b23df3): catch both SIGINT and SIGTERM for graceful shutdown\n### Clair Logic, Extensions\n- [fb32dcfa](https://github.com/quay/clair/commit/fb32dcfa58077dadd8bfbf338c4aa342d5e9ef85): updated mock tests, extensions, basic logic\n### Clairctl\n- [f1c4798b](https://github.com/quay/clair/commit/f1c4798bb10292fe1f14d71691ab33d4ea5a2ae9): start on clair cli tool\n### Cmd\n- [0342a2a3](https://github.com/quay/clair/commit/0342a2a3e5d077d50127ee6bbcee21d4260a29be): make pagination key error clearer\n### Cmd/Clair\n- [b20482e0](https://github.com/quay/clair/commit/b20482e0aebcf2cc67f61e8ff821ddcdffc53ac7): document constants\n- [09dda9bf](https://github.com/quay/clair/commit/09dda9bfd72b2c87ecd40578114f0ad452599db4): fix pprof\n### Config\n- [4f232698](https://github.com/quay/clair/commit/4f232698b0178ef9d1a3cde01b6ff40e47659cfa): add updaters and tracing options\n- [162e8cda](https://github.com/quay/clair/commit/162e8cdafc66be28b021f83da736a2b612ddda99): enable suse updater\n- [0609ed96](https://github.com/quay/clair/commit/0609ed964b0673806462a24147e6028da85d8a38): removed worker config\n- [af2c6886](https://github.com/quay/clair/commit/af2c68863482ae9f93a2db1533be260468a6ea2d): not properly loaded error ([#140](https://github.com/quay/clair/issues/140))\n -  [#140](https://github.com/quay/clair/issues/140) - fixes [#134](https://github.com/quay/clair/issues/134)- [30055af0](https://github.com/quay/clair/commit/30055af03e357b44cfbacb3088eab337a94e51e8): fallover correctly to default config\n- [20af7874](https://github.com/quay/clair/commit/20af78743774b18795cbf5210cc97cc172b1880d): fix default fallback\n- [4fc32d22](https://github.com/quay/clair/commit/4fc32d22713a47eabf5b12b81897fdd34d59935b): add top-level YAML namespace 'clair'\n - Fixes [#95](https://github.com/quay/clair/issues/95)- [bb7745f3](https://github.com/quay/clair/commit/bb7745f3fe21e85b5fe37919e11d6d121e08b9a1): better document example\n### Contrib\n- [76b9f8ea](https://github.com/quay/clair/commit/76b9f8ea05b110d1ff659964fc9126824ec28b17): replace old k8s manifests with helm\n- [ac1cdd03](https://github.com/quay/clair/commit/ac1cdd03c9e31ddaea627e076704f38a0d4719fb): move grafana and compose here\n- [5540d02b](https://github.com/quay/clair/commit/5540d02bc225a240ebc1b04cc83c1adae680da39): delete unsupported tools\n- [f3840f30](https://github.com/quay/clair/commit/f3840f30b9228319751435fee5ed9a25202aa4ab): Revert \"Merge pull request [#367](https://github.com/quay/clair/issues/367) from jzelinskie/analyze-layers-v2\"\n -  [#367](https://github.com/quay/clair/issues/367)- [d76c549d](https://github.com/quay/clair/commit/d76c549dfb18267ce72bd4e1e2fcb18f0d3bdc1a): add missing :=\n -  [#367](https://github.com/quay/clair/issues/367) - Fixes [#368](https://github.com/quay/clair/issues/368)- [e772be5f](https://github.com/quay/clair/commit/e772be5f6f75af54bff1c2febd3c863308d53956): only extract layers from history\n- [ff3c6ecc](https://github.com/quay/clair/commit/ff3c6eccc849c7bce27e872e59584e646081b02c): Catch signals to delete tmp folder in local-analyze-images\n- [55e9c0d8](https://github.com/quay/clair/commit/55e9c0d8547e7dbd03da09fd17c1f68f17cac092): Fix dead link from analyze-local-images' README\n- [1040dbbf](https://github.com/quay/clair/commit/1040dbbff9ea395700d1232b4906b73e0de32a8f): Use `return` instead of `os.Exit(1)` in analyze-local-images\n - Fixes [#117](https://github.com/quay/clair/issues/117)- [251df954](https://github.com/quay/clair/commit/251df954ce2aadadd0fb5060bb2458480f3358b4): Add a ability to force colored output in analyze-local-images\n- [f0245762](https://github.com/quay/clair/commit/f024576223c5b4a1a06207f2e756f62e160ea99b): Add vendors to analyze-local-images\n- [80ddc7f9](https://github.com/quay/clair/commit/80ddc7f949f31abcf0130c3886af8ef72bb72127): Pretty up analyze-local-images\n- [e3417102](https://github.com/quay/clair/commit/e34171025d61a6272036e770989167716042256d): Add colors / Modify spacing in the analyze-local-images's output\n- [93ffc5a1](https://github.com/quay/clair/commit/93ffc5a1e5052bba2b7ddeb28b2414e6025c8b3d): Show feature line only if there's a vuln in analyze-local-images\n- [910288fc](https://github.com/quay/clair/commit/910288fc97ff26d8aac2b96ada85a6f79a1069e6): Add minimum severity support to analyze-local-images\n- [001c0a73](https://github.com/quay/clair/commit/001c0a73d3c186feb7932c47fd8e99212319a6b2): adapt analyze-local-images for new API\n- [fee0bb5e](https://github.com/quay/clair/commit/fee0bb5e495df241594419977750e00159d4b460): load image history from 'manifest.json' first due to docker 1.10 changes.\n - Fixes [#69](https://github.com/quay/clair/issues/69)- [75aff038](https://github.com/quay/clair/commit/75aff0382a567a29ef12d5003e8f8d7cbba092bf): check-openvz-mirror-with-clair fix license\n- [8b137e8a](https://github.com/quay/clair/commit/8b137e8a95a1755aacd3e97b707c647f96a9c3ac): add copyright in check-openvz-mirror-with-clair\n- [7df8e7fb](https://github.com/quay/clair/commit/7df8e7fb1a3c1de4fe3998ffa9a34fa607e05e5e): add copyright in analyze-local-images\n- [867279a5](https://github.com/quay/clair/commit/867279a5c9589885743f0157d26a23e44227a69a): Improve analyze-local-images docs and launch command.\n - Fixes [#32](https://github.com/quay/clair/issues/32)- [9391417b](https://github.com/quay/clair/commit/9391417b2d2761918c1a6f6f165fe1a35275cfb7): Wait for extraction to finish before continuing.\n- [8d071e28](https://github.com/quay/clair/commit/8d071e28ffb445030d358b12030a5928477052bd): Don't pass -z to tar in analyze-local-images\n- [46f7645a](https://github.com/quay/clair/commit/46f7645a53772310cbbacab8bd6aba8ae91fe63e): Add a tool to analyze local Docker images\n### Contrib/Analyze-Local-Images\n- [e1035286](https://github.com/quay/clair/commit/e10352864da16a7484fa445c2bba07998123e153): use exit(1) when there are vulnerabilities\n### Contrib/Helm/Clair\n- [13be17a6](https://github.com/quay/clair/commit/13be17a69082d30996d53d3087b7265007bae555): fix the ingress template\n### Convert\n- [f2ce8325](https://github.com/quay/clair/commit/f2ce8325b975a15c977654d3be1084ad1e890bf3): return nil when detector is empty\n### Database\n- [506698a4](https://github.com/quay/clair/commit/506698a4246e24bb3a72bd626d95bd47dc38beb8): add mapping for Ubuntu Eoan (19.10)\n- [1ddc0532](https://github.com/quay/clair/commit/1ddc0532e4be8dac02e171b986da51deaffbb636): Handle FindAncestryAndRollback datastore.Begin() error\n - Fixes [#828](https://github.com/quay/clair/issues/828)- [6617f560](https://github.com/quay/clair/commit/6617f560cc9ce90eece08aca29841827c72ca5c2): Rename affected type to feature type (for Amazon Linux updater)\n- [3fafb73c](https://github.com/quay/clair/commit/3fafb73c4fe0e9fbc03d1c5657b57ba0ca04c000): Split models.go into different files each contains one model\n- [1b9ed996](https://github.com/quay/clair/commit/1b9ed99646e492a27e982ae34dea7c6fc7273c52): Move db logic to dbutil\n- [961c7d46](https://github.com/quay/clair/commit/961c7d4680c58e3b01eedb4361a3fa57a1f9a904): add test for lock expiration\n- [a4e7873d](https://github.com/quay/clair/commit/a4e7873d1432b9b593f2e9dc44a02f2badea9002): make locks SOI & add Extend method\n- [5fa1ac89](https://github.com/quay/clair/commit/5fa1ac89b9946f2e32ac666080b4f78ad1f9bbfa): Add StorageError type\n- [f6167535](https://github.com/quay/clair/commit/f61675355e7a296989e778f37257e6e416e6f208): Update feature model Remove source name/version fields Add Type field to indicate if it's binary package or source package\n- [7dd989c0](https://github.com/quay/clair/commit/7dd989c0f21bc5c4cb390f575dca9973829ef9ce): Rename affected Type to feature type\n- [00eed77b](https://github.com/quay/clair/commit/00eed77b451b8913771feef7a40067dd246d7872): Add feature_type database model\n- [dd91597f](https://github.com/quay/clair/commit/dd91597f19dae90e8b671d2c80004f0a28dc177c): remove FindLock from mock\n- [399deab1](https://github.com/quay/clair/commit/399deab1005b7c3541ad0dacb52bd7961b5167cc): remove FindLock()\n- [300bb526](https://github.com/quay/clair/commit/300bb52696036dce96ee360f4431837e6ee452a2): add FindLock dbutil\n- [4fbeb9ce](https://github.com/quay/clair/commit/4fbeb9ced594b17aeee3e022f87ed7345376f232): add (Acquire|Release)Lock dbutils\n- [6c682da3](https://github.com/quay/clair/commit/6c682da3e138e0a7d09dadae7040d8cebba88e2b): add mapping for Ubuntu Cosmic (18.10)\n- [a3f7387f](https://github.com/quay/clair/commit/a3f7387ff146226f31a03906591cbb0d0e64cb44): Add FindKeyValue function wrapper\n- [00fadfc3](https://github.com/quay/clair/commit/00fadfc3e3da8c25b6c0c3f13d48017173a45a93): Add affected feature type\n- [f759dd54](https://github.com/quay/clair/commit/f759dd54c028e8b39fd1e21c8c70ebda567aa7cd): Replace Parent Feature with source metadata\n- [3fe894c5](https://github.com/quay/clair/commit/3fe894c5ad7b33223be4a6d52bc0d88fc0fd3a18): Add parent feature pointer to Feature struct\n- [a3e9b5b5](https://github.com/quay/clair/commit/a3e9b5b55d13921b61e2f92a1ade9392b6e7d7a0): rename utility functions with commit/rollback\n- [e657d263](https://github.com/quay/clair/commit/e657d26313b1b91fe4dab17298597119dc919cd2): move dbutil and testutil to database from pkg\n- [db2db8bb](https://github.com/quay/clair/commit/db2db8bbe8a17e10c9fb365196f88d552e70e91d): Update database model and interface for detectors\n- [e1606167](https://github.com/quay/clair/commit/e160616723643beff99363b7b385fd4b8ce6802a): Use LayerWithContent as Layer\n- [ff930390](https://github.com/quay/clair/commit/ff9303905beb2e2f28d2a33e3fc232cd846b5963): changed Notification interface name\n- [a5c64000](https://github.com/quay/clair/commit/a5c6400065a873f6ae14d50b73550dc07239d7bf): postgres implementation with tests.\n- [b99e2b50](https://github.com/quay/clair/commit/b99e2b50e27e1b882c7aac4fc0c02e7b561dafce): Add some missing copyright headers\n- [629d2ce6](https://github.com/quay/clair/commit/629d2ce662c5e81628982b1b4f81ef15aa04fae7): Mock Datastore interface\n- [e7b960c0](https://github.com/quay/clair/commit/e7b960c05b19d3bca0e1027071cacbbcf131c1a5): Allow specifying datastore driver by config\n - Fixes [#145](https://github.com/quay/clair/issues/145)- [79ba99bb](https://github.com/quay/clair/commit/79ba99bbea9c8d4b13b96e4a53b37068007e44e7): Fix invalid error message\n- [9b191fb5](https://github.com/quay/clair/commit/9b191fb598ab9b227180202603b5c3182d562686): Find the FeatureVersion we try to insert before doing any lock\n- [84319507](https://github.com/quay/clair/commit/84319507df7ca69aa5d8311fd2d8431041b1c2e4): use constants to store queries\n- [06531e01](https://github.com/quay/clair/commit/06531e01c5d195fdf5a0865fdcd423d03be06fd5): disable hash/merge joins in FindLayer\n- [18f2d7e6](https://github.com/quay/clair/commit/18f2d7e672d99091414a14ff3a80ac97f73e03d0): modify join table in FindLayer to reduce cost by 3.5x\n- [b5d8f995](https://github.com/quay/clair/commit/b5d8f9952e2d89444ec286fab7a077cedc73b8fd): fix notification test (wrong signature)\n- [f0816d2c](https://github.com/quay/clair/commit/f0816d2c4dd92e10fe8e0713fb7890da1e306c0f): add docs about the interface\n- [d3b14106](https://github.com/quay/clair/commit/d3b14106a9c1ba4855b94bcb5cfef37028706d83): ignore insertLayer collisions to make it truly idempotent\n- [e3a25e53](https://github.com/quay/clair/commit/e3a25e53680fe01c8166ef0f1dd0a4ff3fda5f85): ignore min versions during new vulnerability insertions\n- [883be876](https://github.com/quay/clair/commit/883be8769f06ce2eebc70e8c3416376bbc05ce44): fix Ping() method in PostgreSQL's implementation\n- [f8b4a52f](https://github.com/quay/clair/commit/f8b4a52f8a4c02629184d5fe3c12ee4962f5af31): make notification tests more robust (old/new, update/delete vulnerabilities)\n- [ccaaff00](https://github.com/quay/clair/commit/ccaaff000e42adc71149b56dfcd3d4a740d4b830): add created_at field for layers and vulnerabilities\n- [94ece7bf](https://github.com/quay/clair/commit/94ece7bf2b5a26e6b99fd0dbafe5f04580100196): fix notification design and add vulnerability history\n- [99f35524](https://github.com/quay/clair/commit/99f35524709119eced2e482a35f91af052e21916): add Insert/DeleteVulnerabilityFix\n- [03d904c6](https://github.com/quay/clair/commit/03d904c6206da4f868c0c5f52cd96cd6f812e28a): improve PostgreSQL test inits and cleanups\n- [8f9779e2](https://github.com/quay/clair/commit/8f9779e232193787ab60263d67a555cf3a8ab811): cache feature version upon lookup\n- [1e4ded6f](https://github.com/quay/clair/commit/1e4ded6f2b314fe4b61b3b6613b53923bfcacb69): add ability to list namespaces\n- [35df7ca0](https://github.com/quay/clair/commit/35df7ca0eb3529f458aa4c7436149045a0d7df97): fix feature version cache\n- [8be18a0a](https://github.com/quay/clair/commit/8be18a0a01e8733fd7a841645eecc04143949833): write more of the notification system\n- [d3d689a2](https://github.com/quay/clair/commit/d3d689a26ae89a700ff6fcdc1c3fefea345d297d): don't prune locks when we renew one\n- [26908003](https://github.com/quay/clair/commit/26908003314498c899545bf73319f274fb9071b5): create notification during vulnerability insertion\n- [63ebddfd](https://github.com/quay/clair/commit/63ebddfd3662c6a208c5960b64a13ed6e86dd6f6): add vulnerability deletion support\n- [21f152c0](https://github.com/quay/clair/commit/21f152c03e8d5274d60889d2faf0bba77034958a): fix keyvalue/notification tests\n- [563b3825](https://github.com/quay/clair/commit/563b3825d8702b34390223a3a96e86d5ff651c18): let handleErrors deal with the not found case\n- [5759af5b](https://github.com/quay/clair/commit/5759af5bcff60d1163f4995d8182402eff19ba8f): test and fix layer updates\n- [248fc7df](https://github.com/quay/clair/commit/248fc7df7226a02169d32f8d8e5d4709b82377c9): fix cache collision (feature & feature versions)\n- [92b734d0](https://github.com/quay/clair/commit/92b734d0a44fece5657e548a1fb65d7bf93ab7bb): remove an useless query in FindLayer\n- [bd17dfb5](https://github.com/quay/clair/commit/bd17dfb5e11b927e7134998286aff8511e83e954): ensure that concurrent vulnerability/feature versions insertions work fine\n- [74fc5b3e](https://github.com/quay/clair/commit/74fc5b3e66dc81f2079f6d0d730491ff7b30a2c7): add missing transaction commits and close opened statement before inserting feature versions.\n- [c5d1a8e5](https://github.com/quay/clair/commit/c5d1a8e5f78f3774f4cb895aa901d6bc780719df): update vulnerabilities only when necessary\n- [1b53142e](https://github.com/quay/clair/commit/1b53142e3808942d9a871192e464a6bb10dd3ddb): allow removing fixed packages in vulnerabilities\n- [7c70fc1c](https://github.com/quay/clair/commit/7c70fc1c205caa45926ae1435d74d162abf13d54): add initial vulnerability support\n- [3a786ae0](https://github.com/quay/clair/commit/3a786ae020d6e0a07c2b7b1d572070afc242634a): add lock support\n- [6a9cf21f](https://github.com/quay/clair/commit/6a9cf21fd4a8e5e04426e5f0c28b7ccac3e10823): log and mask SQL errors\n- [970756cd](https://github.com/quay/clair/commit/970756cd5a8364b4912f99859c7626d4db7f97b6): do insert/find layers (with their features and vulnerabilities)\n- [32747a5f](https://github.com/quay/clair/commit/32747a5f250bc5bfac41c5754bbe49efde7bb847): Don't ignore empty results in toValue(s)()\n- [3fe3f3a4](https://github.com/quay/clair/commit/3fe3f3a4c74568699d0cbbeb9abdb28d1f249c21): Update cayley and use Triple instead of Quad\n- [9fc29e29](https://github.com/quay/clair/commit/9fc29e291c60a0f244cca91372ba6044bb670838): put missing predicates in consts and un-expose some of them\n - Fixes [#16](https://github.com/quay/clair/issues/16)- [8285c567](https://github.com/quay/clair/commit/8285c567c811de952d30ef0583db42237600b2f0): Improve InsertVulnerabilities.\n- [cfa960d6](https://github.com/quay/clair/commit/cfa960d61903887c203af8d0a3d204a4fe0b7fb0): Update Cayley to fix slow deletions\n- [915903c1](https://github.com/quay/clair/commit/915903c1c151df563204f76038f77df578d64cd4): Fix to a locking issue with PostgreSQL\n- [8aacc8bf](https://github.com/quay/clair/commit/8aacc8bfdcf72bd607bb491ce2533c5c0ef2313e): Ensure that quads in a tx are applied in the desired order.\n- [3a1d0602](https://github.com/quay/clair/commit/3a1d0602fb7006dceb99f4697bbee02ad5ffaa93): Use an estimator in Cayley's Size() w/ PostgreSQL\n- [b0142e19](https://github.com/quay/clair/commit/b0142e1982ebec8226b1f4c8622a49f52b8adba2): reduce pruneLocks/Unlock transaction.\n- [7f1ff8f9](https://github.com/quay/clair/commit/7f1ff8f97908d092bc73388ee910e8c0fdbda096): reduce InsertPackages transaction\n### Database/Api\n- [726bd3c0](https://github.com/quay/clair/commit/726bd3c0c60522fd3fa56b0e5a79494afed2c186): add layer deletion support\n### Database/Models\n- [0305dde9](https://github.com/quay/clair/commit/0305dde964e5f21a84087b55d7c6899107543b4b): MetadataMap decodes from string\n### Database/Pgsql\n- [4491bedf](https://github.com/quay/clair/commit/4491bedf2e284007fa7f527bf264dc98c937d820): move token lib\n- [9e875f74](https://github.com/quay/clair/commit/9e875f748dd218ac0d3bdb4a11bc3830cee5c8be): copy whole namespace\n### Database/Worker\n- [f229083e](https://github.com/quay/clair/commit/f229083e1e52d2fea46cb7c69be05b5e5f32c680): Remove useless log message\n### Datastore\n- [57b146d0](https://github.com/quay/clair/commit/57b146d0d808a29db9f299778fb5527cd0974b06): updated for Clair V3, decoupled interfaces and models\n### Db/Pgsql/Feature\n- [627b98ef](https://github.com/quay/clair/commit/627b98ef3126d517d3e80aef4c2ab9ed3d14b893): fix SQL error reporting\n### Db/Pgsql/Migration\n- [8df8170b](https://github.com/quay/clair/commit/8df8170ba54d945cf2e2ad201bcdb8cc09fddc06): convert to pure SQL\n### Dckerfile\n- [80f150f9](https://github.com/quay/clair/commit/80f150f93bf3b12e6c9bcea2d71ccfa956bd50c9): Add docker-compose.yml\n### Detectors/Feature\n- [fc908e65](https://github.com/quay/clair/commit/fc908e65ba6c0ad3edb344cfb263adff8efe6f4e): add apk feature detector\n- [e4b5930f](https://github.com/quay/clair/commit/e4b5930f7769083767005d8e1730be81bf9eab8f): consistent naming and godoc\n### Detectors/Namespace\n- [1d5a9ddd](https://github.com/quay/clair/commit/1d5a9ddd3c2849ecd0346b55212c73a616b1382d): add alpine-release detector\n- [0b2a9ab1](https://github.com/quay/clair/commit/0b2a9ab12b1f6e1308cad933bb7ad6cf15017011): support pointers in tests\n### Dockerfile\n- [2ca92d00](https://github.com/quay/clair/commit/2ca92d00754b1d1859e9d6f3169d67d6b96d6bee): bump Go to 1.13\n- [c1e0f618](https://github.com/quay/clair/commit/c1e0f618caad4464d90ba20e13baaa1fb1617cb9): add git dependency\n- [8918f405](https://github.com/quay/clair/commit/8918f40599685c5781d5b6b53ec99120bedc65f4): update deps and move to Go 1.6\n- [ea193d3a](https://github.com/quay/clair/commit/ea193d3ae72a3a52e56289dceebf1fbda9949c4c): syntax updates and s/xz/xz-utils\n### Dockerfile\n- [e56b95ac](https://github.com/quay/clair/commit/e56b95aca0085067f91f90e3b32dab9d04e7fb48): use environment variables\n- [33b3224d](https://github.com/quay/clair/commit/33b3224df13b9c2aa8b0281f120997abce82eaf9): update for clair v4\n- [df4f277d](https://github.com/quay/clair/commit/df4f277d0e36405dc2e607730097464dfd45c1f3): use alpine linux 3.5 (bis)\n- [4721e92b](https://github.com/quay/clair/commit/4721e92b17d96f7a229112288f25d2a03c741ef7): use alpine linux 3.5\n- [6b235207](https://github.com/quay/clair/commit/6b23520710396877e941611f62f4e12fa002db99): remove useless volume\n### Docs\n- [49b5621d](https://github.com/quay/clair/commit/49b5621d738978c94e8d311775bba48a1daafc7e): fix typo in running-clair\n- [9ee2ff48](https://github.com/quay/clair/commit/9ee2ff4877db15a5ad8ae24afcb8f02f0e8289cf): add troubleshooting about kernel packages\n- [3f91bd2a](https://github.com/quay/clair/commit/3f91bd2a9bc40bd7b6f4e5a5a8a533de383f3554): turn README into full articles\n- [821a608b](https://github.com/quay/clair/commit/821a608bb1ad7336bc817ef5ef4ded3b8ddb2ed9): add links to contrib tools\n- [6e8e6ad2](https://github.com/quay/clair/commit/6e8e6ad26b0d6ce7a9c34dde7a7c80926aae3a48): fix broken link\n- [107582c9](https://github.com/quay/clair/commit/107582c96e59b67959811ea8b99d17e512fcc2a7): Correct docker-compose command\n- [12c47e40](https://github.com/quay/clair/commit/12c47e406608c72bf481dfe403863f7ae05ffb2b): split http and json code blocks\n- [37a58260](https://github.com/quay/clair/commit/37a58260db3f4b269edb7d0785fb8cce34969b74): improve GET/POST /v1/layers documentation\n- [859b1942](https://github.com/quay/clair/commit/859b1942a5872faed16101277479ea7796033442): fix the docker cli of running clair in README.md\n- [fd6fdbd3](https://github.com/quay/clair/commit/fd6fdbd3f9de119e9528f836f6d67bd951af3586): update config example\n- [93291726](https://github.com/quay/clair/commit/93291726839994651ba981d9d85efef04807602a): provide information to run Clair in README\n- [7b608ced](https://github.com/quay/clair/commit/7b608ceda50be838a801a40c00012e26a32bffc2): Add missing field in API Example\n- [ec0decfc](https://github.com/quay/clair/commit/ec0decfcafd32edc9212ed7c1a94e96df10924d6): fix a typo in the model\n - Fixes [#43](https://github.com/quay/clair/issues/43)### Documentation\n- [3e6896c6](https://github.com/quay/clair/commit/3e6896c6a4e5cdd04d91927d762b332b62e1d4fe): fix links to presentations\n - Closes [#661](https://github.com/quay/clair/issues/661) - Closes [#665](https://github.com/quay/clair/issues/665) - Closes [#560](https://github.com/quay/clair/issues/560)### Documentation\n- [c1a58bf9](https://github.com/quay/clair/commit/c1a58bf9224bbcd7e0f02ea4065650d220654f29): add new 3rd party tool\n### Driver\n- [5c585754](https://github.com/quay/clair/commit/5c5857548d43fa866d46a4c98309b2dfa88be418): Add proxy support\n### Drone\n- [0fd9cd3b](https://github.com/quay/clair/commit/0fd9cd3b59bd42ef0e508f0f415028a0ee8fa44f): remove broken drone CI\n- [352f7383](https://github.com/quay/clair/commit/352f73834e7bdef31dc5e3a715133f5c47947764): init\n### Example Config\n- [8d10d93b](https://github.com/quay/clair/commit/8d10d93b177490139ec1f9ce417a9a8acfb3b1b6): add localhost postgres\n### Ext\n- [25078ac8](https://github.com/quay/clair/commit/25078ac838920e4010ecdbe4546af0d4b502dabd): add CleanAll() utility functions\n- [081ae34a](https://github.com/quay/clair/commit/081ae34af146365146cf4548a8a0afa293e15695): remove duplicate vectorValuesToLetters definition\n- [4f0da12b](https://github.com/quay/clair/commit/4f0da12b123ec543a58936c0f7226254e411cc00): pass through CVSSv3 impact and exploitability score\n- [8efc3e40](https://github.com/quay/clair/commit/8efc3e40382287e88714fdcf634a79e6347b6157): remove unneeded use of init()\n- [699d1143](https://github.com/quay/clair/commit/699d1143e5ab2a673d0f83249f3268cfebaf3e57): fixup incorrect copyright year\n- [b81e4454](https://github.com/quay/clair/commit/b81e4454fbb7f3dcec4a2dd6064820bf0c6321f2): Parse CVSSv3 data from JSON NVD feed\n- [14277a8f](https://github.com/quay/clair/commit/14277a8f5d95799bb651c194785dd04e75a08ee1): Add JSON NVD parsing tests\n- [aab46f56](https://github.com/quay/clair/commit/aab46f5658cf5a75262945033cb41d93af5f2131): Parse NVD JSON feed instead of XML\n- [8d5a0131](https://github.com/quay/clair/commit/8d5a0131c48d0812d1dd53b1af8e24ae4e51c4ba): Use SHA256 instead of SHA1 for fingerprinting\n- [53bf19ae](https://github.com/quay/clair/commit/53bf19aecfcccb367bc359a2dd6d7320fa4e4855): Lister and Detector returns detector info with detected content\n- [cda3d481](https://github.com/quay/clair/commit/cda3d4819c261932ad24196f51f4a4b4fec022bd): feature detector -> featurefmt\n- [71a8b542](https://github.com/quay/clair/commit/71a8b542f95cf34746d1b544a0fe1790a9f6eb09): misc doc comment fixes\n- [fb193e1f](https://github.com/quay/clair/commit/fb193e1fdecad209e17a890e6727d04931015e0b): namespace detector -> featurens\n- [d9be34c3](https://github.com/quay/clair/commit/d9be34c3c4cf7a8d1865abd064c59dd3f24f51bd): data detector -> imagefmt\n- [f9b31908](https://github.com/quay/clair/commit/f9b319089d4d5b0dfd64c6f80fc4117657270b77): lock all drivers\n### Ext/Featurefmt\n- [1c40e7d0](https://github.com/quay/clair/commit/1c40e7d01697f5680408f138e6974266c6530cb1): Refactor featurefmt testing code\n### Ext/Featurefmt/Apk\n- [2cc61f9f](https://github.com/quay/clair/commit/2cc61f9fc0edc42d2c0fda71471208e3faba507d): Extract origin package information from database\n- [b2f2b2c8](https://github.com/quay/clair/commit/b2f2b2c854b4e3e15e53616ca221f7953bdc38eb): handle malformed packages\n### Ext/Featurefmt/Dpkg\n- [4ac04664](https://github.com/quay/clair/commit/4ac046642ffea9fb60af455b9d22d19cd4408f32): Extract source package metadata\n- [590e7e26](https://github.com/quay/clair/commit/590e7e2602526dd5ae1c08436ab98b299e3cd69d): handle malformed packages\n### Ext/Featurefmt/Rpm\n- [a057e4a9](https://github.com/quay/clair/commit/a057e4a943dc1a2dc1898b67435b05417725402e): Extract source package from rpm database\n### Ext/Featurens\n- [34bc7227](https://github.com/quay/clair/commit/34bc722794291da77c9917155fbbc31a7001baf4): add empty filesmap tests for all\n- [03b8cd9a](https://github.com/quay/clair/commit/03b8cd9a4584db0ca18032bf109469ceb2adc3d3): add missing lock\n### Ext/Vulnsrc/Alpine\n- [0891bbac](https://github.com/quay/clair/commit/0891bbac00c9e0bbed159bcdd438edcb42331954): use HTTPS\n### Ext/Vulnsrc/Oracle\n- [09cbfe32](https://github.com/quay/clair/commit/09cbfe325b93f19aa05a946ab90d76296d2bd2f4): ensure flag is largest elsa\n- [bcf47f53](https://github.com/quay/clair/commit/bcf47f53ee704892483f4d3b1c29f306b1eb6dcf): fix ELSA version comparison\n### Ext/Vulnsrc/Rhel\n- [d606d85a](https://github.com/quay/clair/commit/d606d85afeed37df5f2806325fd6f4eae03be5ac): fix logging namespace\n### Ext/Vulnsrc/Ubuntu\n- [300fe980](https://github.com/quay/clair/commit/300fe980ef44134d23b21afd643fa9336210c0f2): add missing version format\n### Feature\n- [90f55920](https://github.com/quay/clair/commit/90f5592095f74e9704193f4362c494571667b326): replace arrays with slices\n### Featurefmt\n- [34c2d96b](https://github.com/quay/clair/commit/34c2d96b3685a927749536017add6538578fb2df): Extract PotentialNamespace\n- [0e0d8b38](https://github.com/quay/clair/commit/0e0d8b38bba4c62552c98ad5b98242ddd2c3464b): Extract source packages and binary packages The featurefmt now extracts both binary packages and source packages from the package manager infos.\n- [9561d623](https://github.com/quay/clair/commit/9561d623c29394dddca0823721d7d3622b3dec65): use namespace's versionfmt to specify listers\n### Featurens\n- [947a8aa0](https://github.com/quay/clair/commit/947a8aa00c6f72a20e7fca63993dafaf3185fdc4): Ensure RHEL is correctly identified\n - Fixes [#436](https://github.com/quay/clair/issues/436)- [50437f32](https://github.com/quay/clair/commit/50437f32a1d7d609cfd5e6eb3f0bbf180099fc05): fix detecting duplicated namespaces problem\n- [75d5d40d](https://github.com/quay/clair/commit/75d5d40d796f4233a58c16443614933c8b326d49): added multiple namespace testing for namespace detector\n### Fetchers/Alpine\n- [f74cd352](https://github.com/quay/clair/commit/f74cd352438a710d51f56b9f3a32b77cc403fe32): add notes for untracked namespaces\n- [3be8dfcf](https://github.com/quay/clair/commit/3be8dfcf99dfde8aaf67c60eb279c17b8a7e83d2): auto detect namespaces\n### Fix\n- [4e49aaf3](https://github.com/quay/clair/commit/4e49aaf34647ab636595c1ba631efa0cea56ceac): lock updater - return correct bool value\n### Github\n- [6a42aba3](https://github.com/quay/clair/commit/6a42aba3aa7c73627fd73da3d57dd233de1184e8): add mailing list!\n- [c7a67edf](https://github.com/quay/clair/commit/c7a67edf5d8957ff05391770d6800e9e83b6b0a9): add issue template stable release notice\n- [f6cac473](https://github.com/quay/clair/commit/f6cac4733a7545736d5875f0b36324481098d471): add issue template\n- [24ca12bd](https://github.com/quay/clair/commit/24ca12bdecfcbc2d7797a01dcde87fea44dad7c8): move CONTRIBUTING to github dir\n### Gitutil\n- [11b67e61](https://github.com/quay/clair/commit/11b67e612c3703af63a4c63364ea60445077a2a7): Fix git pull on non-git repository directory\n - Fixes [#641](https://github.com/quay/clair/issues/641)### Glide\n- [165c397f](https://github.com/quay/clair/commit/165c397f169409dfce9b41459d5845e774c8ef81): add errgroup and regenerate vendor\n- [d846c508](https://github.com/quay/clair/commit/d846c508c3aecf52d4e5aa3d47591614e50aa4e7): refresh dependencies\n### Go.Mod\n- [ad58dd97](https://github.com/quay/clair/commit/ad58dd9758726e488b5c60a47b602f1492de7204): update to latest claircore\n### Godeps\n- [213468a6](https://github.com/quay/clair/commit/213468a6d58787a7c52ecaea97d60412ae02965d): Remove implicit git submodules\n### HELM\n- [81430ffb](https://github.com/quay/clair/commit/81430ffbb252990ebfd74b0bba284c7564b69dae): also add option for nodeSelector\n- [6a94d8cc](https://github.com/quay/clair/commit/6a94d8ccd267cc428dd2161bb1e5b71dd3cd244f): add option for tolerations\n### Helm\n- [690d26ed](https://github.com/quay/clair/commit/690d26edbac2605b19900549b70d74fa47bdfef9): change postgresql connection string format in configmap template\n - Fixes [#561](https://github.com/quay/clair/issues/561)- [7a06a7a2](https://github.com/quay/clair/commit/7a06a7a2b4a68c2567a5bcc41c497fdb9d8d2c15): Fixed a typo in maintainers field.\n### Helm\n- [710c6553](https://github.com/quay/clair/commit/710c65530f4524693e6a863075b4d3760901a3bc): allow for ingress path configuration in values.yml\n### Helm Chart\n- [bc6f37f1](https://github.com/quay/clair/commit/bc6f37f1ae0df5a7c01184ef1483a889e82e86ba): Use Secret for config file. Fix some minor issues\n - Fixes [#581](https://github.com/quay/clair/issues/581)### Imagefmt\n- [891ce169](https://github.com/quay/clair/commit/891ce1697d0e53e253001d0ae7620f31b886618c): Move layer blob download logic to blob.go\n### Indexer\n- [500355b5](https://github.com/quay/clair/commit/500355b53c213193147e653b147afc3036ea2125): add basic latency summary\n- [8953724b](https://github.com/quay/clair/commit/8953724bab392fa3897c2fae62b5df6e9567047c): QoL changes to headers\n- [741fc2c4](https://github.com/quay/clair/commit/741fc2c4bacb7e5651b05b298257a41ec7558858): HTTP correctness changes\n- [10d2f547](https://github.com/quay/clair/commit/10d2f5472efc414846b56edf9d77a69246ea06b2): rename index endpoint\n- [ac0a0d49](https://github.com/quay/clair/commit/ac0a0d49424f1f19b5044ea84a245e3139b5adb3): add Accept-Encoding aware middleware\n- [3a9ca8e5](https://github.com/quay/clair/commit/3a9ca8e57a041bdd78d5e37a904a1ff5942befd8): add State method\n### Integrations\n- [a5b92feb](https://github.com/quay/clair/commit/a5b92feb46dd12244672ed2ddf27350046ae2c1d): add quay enterprise as well\n### Layer\n- [015a79fd](https://github.com/quay/clair/commit/015a79fd5a077a3e8340f8cef8610512f53ef053): replace arrays with slices\n### Main\n- [7ca9127b](https://github.com/quay/clair/commit/7ca9127bbec404b9e2edbb5919a610dc0ac6a4fc): default config to /etc/clair/config.yml\n- [eb7e5d5c](https://github.com/quay/clair/commit/eb7e5d5c742afb26963b6ef2f3fe2712b9d76ce4): Use configuration file instead of flags and simplify app extension.\n### Mapping\n- [07a08a4f](https://github.com/quay/clair/commit/07a08a4f53cab155814eadde44a847e2389b5bcc): add ubuntu mapping\n - Fixes [#552](https://github.com/quay/clair/issues/552)### Matcher\n- [15c098c4](https://github.com/quay/clair/commit/15c098c48cac6e87b82a4af4b5914aef0ab83310): add basic latency summary\n- [00179464](https://github.com/quay/clair/commit/0017946470397c252b1934d1637fe7b1d01fe280): return OK instead of Created\n### Namespace\n- [c28d2b3a](https://github.com/quay/clair/commit/c28d2b3a66cbd468f567ed0b4ddce3157169707d): add debug output\n### New API\n- [a541e964](https://github.com/quay/clair/commit/a541e964e07ea0e9a70f2ebee68897edf852bcba): list vulnerabilities by namespace\n### Notifier\n- [927af43b](https://github.com/quay/clair/commit/927af43be074584546c3ece5d0cf4c91d8389669): Verify that the given webhook endpoint is an absolute URL\n- [2fb815dc](https://github.com/quay/clair/commit/2fb815dc3716f297870f63e3624eb473bfa3ddda): Add proxy parameter to webhook notifier\n- [136b9070](https://github.com/quay/clair/commit/136b907050e0686e60ae8cbcd51ec67ab4627063): add README\n- [904ce600](https://github.com/quay/clair/commit/904ce6004f09f1b8db376b20caff6246e8561e8b): add a timeout on the http client\n- [4478f40e](https://github.com/quay/clair/commit/4478f40ef19e3c3a067ca166ce7ab3e7218de6df): fix notifier error handling and improve web hook error message\n- [f4a4d417](https://github.com/quay/clair/commit/f4a4d417e7fee46add6f32d6284a9a6a8b9ce10d): Rename HTTP to Webhook Notifier\n- [2ea86c53](https://github.com/quay/clair/commit/2ea86c53f3e32b5e2781bd5b087d3123a8e61e6c): fix a bug that prevented graceful shutdown in certain cases\n- [480589a8](https://github.com/quay/clair/commit/480589a83abfa6c9249771d535c2a405c7ce3466): retry upon failure\n- [3ff8bfaa](https://github.com/quay/clair/commit/3ff8bfaa9311ca5923809b89b92299eed558be2c): Allow custom notifiers to be registered.\n- [b3828c9c](https://github.com/quay/clair/commit/b3828c9c4c622891426da8a65f1de471fdd3ecbe): add ServerName configuration for TLS\n- [20a126c8](https://github.com/quay/clair/commit/20a126c84ae8fad5f9a9ee3bc042866407d48308): Refactor and add client certificate authentification support.\n - Fixes [#23](https://github.com/quay/clair/issues/23)### Notifier/Database\n- [ad0531ac](https://github.com/quay/clair/commit/ad0531acc7614cf6fa68d9ce7c66ff293832dfcf): refactor notification system and add initial Prometheus support\n- [c60d0054](https://github.com/quay/clair/commit/c60d0054fa0f11dac76547f30f2bd25410d4bf9f): draft new notification system\n### Nvd\n- [e953a259](https://github.com/quay/clair/commit/e953a259b008042d733a4c0aadc9b85d1bedf251): fix the name of a field\n### Openapi\n- [1949ec3a](https://github.com/quay/clair/commit/1949ec3a22a5d2dd5cc30a5fccb99c49a657677a): lint and update Layer\n### Osrelease-Detector\n- [d88f7978](https://github.com/quay/clair/commit/d88f7978213d1b21ea7c3bd4d1466f35dc2784e4): avoid colliding with other detectors\n### PgSQL\n- [57a4f977](https://github.com/quay/clair/commit/57a4f977803e5eb0d5ddb23e6d54e8490efe89c9): fixed invalidating vulnerability cache query.\n### Pgsql\n- [0731df97](https://github.com/quay/clair/commit/0731df972c5270d2540411cc2ae1b4f3c9b36dc6): Remove unused test code\n- [dfa07f6d](https://github.com/quay/clair/commit/dfa07f6d860c59ba2b2cc4909d38f650e9d3969b): Move notification to its module\n- [921acb26](https://github.com/quay/clair/commit/921acb26fe875ed18c95b2f62a73fa3e1a8aa355): Split vulnerability.go to files in vulnerability module\n- [7cc83ccb](https://github.com/quay/clair/commit/7cc83ccbc5b4e34762d10343c2bc989a14fddebc): Split ancestry.go to files in ancestry module\n- [497b79a2](https://github.com/quay/clair/commit/497b79a293ce9d07f34ffd8ea51264c8ae6bd84c): Add test for migrations\n- [ea418cff](https://github.com/quay/clair/commit/ea418cffd474252d9a59881677daffbdaa507768): Split layer.go to files in layer module\n- [176c69e5](https://github.com/quay/clair/commit/176c69e59dfbd4b39d520005b712858dff502e45): Move namespace to its module\n- [98e81ff5](https://github.com/quay/clair/commit/98e81ff5f1230f67c3a73055f694a423763062a7): Move keyvalue to keyvalue module\n- [ba50d7c6](https://github.com/quay/clair/commit/ba50d7c62648471e6e7cf74afe14e9c3268a3a98): Move lock to lock module\n- [0b32b36c](https://github.com/quay/clair/commit/0b32b36cf7168eef2c005a3d7ec9c3a5996d910b): Move detector to pgsql/detector module\n- [c50a2339](https://github.com/quay/clair/commit/c50a2339b79c2b5af8552ab6ae4d0e9441df57ac): Split feature.go to table based files in feature module\n- [43f3ea87](https://github.com/quay/clair/commit/43f3ea87d86097c81951faf96c000b05445d0947): Move batch queries to corresponding modules\n- [a3305063](https://github.com/quay/clair/commit/a33050637b4b28f947eb8256cd48ee35d2fe5bfe): Move extra logic in pgsql.go to util folder\n- [8bebea36](https://github.com/quay/clair/commit/8bebea3643e294bb11a1766ec450b1e518b0003b): Split testutil.go into multiple files\n- [b03f1bc3](https://github.com/quay/clair/commit/b03f1bc3a671a28f914ecf012df5250ebf20df03): Fix failed tests\n- [ed9c6baf](https://github.com/quay/clair/commit/ed9c6baf4faecad71828dacabc5e804a7f11252b): Fix pgsql test\n- [5bf8365f](https://github.com/quay/clair/commit/5bf8365f7b5bf493ec3a3c119538c58abaa29209): Prevent inserting invalid entry to database\n- [8aae73f1](https://github.com/quay/clair/commit/8aae73f1c8cf4dddb91babde813097789eb876f3): Remove unnecessary logs\n- [79af05e6](https://github.com/quay/clair/commit/79af05e67d6e6f09bd1913dbfe405ebdbd9a9c59): Fix postgres queries for feature_type\n- [073c685c](https://github.com/quay/clair/commit/073c685c5b085813a9ffbec20fa3c49332f7ec66): Add proper tests for database migration\n- [c6c8fce3](https://github.com/quay/clair/commit/c6c8fce39a5c28645b9626bc3774bd6b6aadd427): Add feature_type to initial schema\n- [a57d8067](https://github.com/quay/clair/commit/a57d80671793d48782f8d3777984e99d02dc1fd9): fix unchecked error\n- [0c1b80b2](https://github.com/quay/clair/commit/0c1b80b2ed54dcbe227f7233468a5bdc66d4a17e): Implement database queries for detector relationship\n- [9c49d9dc](https://github.com/quay/clair/commit/9c49d9dc5591d62a86632881af8d7a7f15fbf25e): Move queries to corresponding files\n- [dca2d4e5](https://github.com/quay/clair/commit/dca2d4e597ba837b6f96f3b3e32e23f6b843f9ab): Add detector to database schema\n- [53433090](https://github.com/quay/clair/commit/53433090a39195d9df7c920d2e4d142f89abae31): update the query format\n- [aea74550](https://github.com/quay/clair/commit/aea74550e14a0f0121fb21a2bba6bb6882c2050f): Expand layer, namespace column widths\n- [ca9f340a](https://github.com/quay/clair/commit/ca9f340a91eaca32af143349d14e9a9639801938): only select distinct layers\n- [ea73aa15](https://github.com/quay/clair/commit/ea73aa153d8b266096e37be3c87f3d0b045f31a5): searchNotificationLayerIntroducingVulnerability order by layer ID\n- [7a3dd5c8](https://github.com/quay/clair/commit/7a3dd5c817ae9f53917a1f7a87380f148fda4ec4): Disable hashjoins to get introducing layers for notifications\n- [dc8f7102](https://github.com/quay/clair/commit/dc8f71024f0340db55d4b3dd3675e9bdce0c53f2): Reduce cost of GetNotification by 2.5\n- [ec0aad9b](https://github.com/quay/clair/commit/ec0aad9b7a4e8021b633f983f2e1d9ef496bf49c): Use booleans instead of varchar to return creation status\n- [cd23262e](https://github.com/quay/clair/commit/cd23262e41fc51eefe6aad403f777a21df93bb22): Do not insert entry in Vulnerability_FixedIn_Feature if existing\n - Fixes [#238](https://github.com/quay/clair/issues/238)- [b8865b21](https://github.com/quay/clair/commit/b8865b21061a5801ada688fe06ce8745e94f0093): Replace liamstask/goose by remind101/migrate\n - Fixes [#93](https://github.com/quay/clair/issues/93)- [5d8336ac](https://github.com/quay/clair/commit/5d8336acb342cc022c912f8f37045ace62f3c84a): use subquery to plan GetNotification query ([#182](https://github.com/quay/clair/issues/182))\n -  [#182](https://github.com/quay/clair/issues/182)- [51f9c5dc](https://github.com/quay/clair/commit/51f9c5dcb430dab8907df425521ee650d5a4fb45): remove unnecessary join used in GetNotification ([#179](https://github.com/quay/clair/issues/179))\n -  [#179](https://github.com/quay/clair/issues/179)### Pgsql/Migrations\n- [224ff825](https://github.com/quay/clair/commit/224ff825434ebdb907a2276ef4577b69e3d49808): fix dpkg default versionfmt\n- [eeb13a02](https://github.com/quay/clair/commit/eeb13a02baa5db60b997454af8f059a6381f42da): add index on Vulnerability_Notification.deleted_at\n- [7cff31a0](https://github.com/quay/clair/commit/7cff31a058230a94838b7fd8493380d444c31f5a): add ldfv compound index\n### Pkg\n- [c3904c96](https://github.com/quay/clair/commit/c3904c9696bddc20a27db9b4142ae704350bbe3f): Add fsutil to contian file system utility functions\n- [78cef02f](https://github.com/quay/clair/commit/78cef02fdad4afd12a2a00df51d457c796278bfa): cerrors -> commonerr\n- [03bac0f1](https://github.com/quay/clair/commit/03bac0f1b6f504a416937e309a62fdfc308dc397): utils/tar.go -> pkg/tarutil\n### Pkg/Gitutil\n- [c2d887f9](https://github.com/quay/clair/commit/c2d887f9e99184af502aca7abbe2044d2929e789): init\n### Pkg/Grpcutil\n- [c4a32543](https://github.com/quay/clair/commit/c4a32543e85a46a94012cfd03fc199854ccf3b44): use cockroachdb cipher suite\n- [1ec27595](https://github.com/quay/clair/commit/1ec2759550d6a6bcae7c7252c8718b783426c653): init\n### Pkg/Pagination\n- [05659389](https://github.com/quay/clair/commit/05659389569549f445eefac650df260ab4f4f05b): add token type\n- [d193b464](https://github.com/quay/clair/commit/d193b46449a64a554c3b54dd637a371769bfe195): init\n### Pkg/Stopper\n- [00e4f709](https://github.com/quay/clair/commit/00e4f7097241574277d4ed77d4c017f0c158a4e0): init from utils.Stopper\n### Pkg/Timeutil\n- [45ecf188](https://github.com/quay/clair/commit/45ecf1881521281f09e437c904e1f211dc36e319): init\n### Prometheus\n- [4f0f8136](https://github.com/quay/clair/commit/4f0f8136c0459565ec0fcfb5c6035075f49d28b3): fix grafana's updater notes graph\n- [cf3573cf](https://github.com/quay/clair/commit/cf3573cf671ec0f14344d8ee072e3abdf030bbcc): correct notifier latency metric in grafana\n- [3defe644](https://github.com/quay/clair/commit/3defe6448a9e8cf8cdcdb4294330a1077acf3918): add quantile to grafana\n- [0c5cdab0](https://github.com/quay/clair/commit/0c5cdab0b15f1d67dc755f9b068c50371d4390f9): update grafana\n- [baed60e1](https://github.com/quay/clair/commit/baed60e19ba5e58271cbe30aecb09404716e4a99): add initial Prometheus support\n### Psql\n- [9dc00262](https://github.com/quay/clair/commit/9dc002621abe5283e7f4fa645a6c8b8d823b6a95): add useful indexes\n- [363cde29](https://github.com/quay/clair/commit/363cde29f4139953485a3ec04743025b35271c22): add debug message for duplicate layers\n### Psql/Migrations\n- [9338f28e](https://github.com/quay/clair/commit/9338f28e82bf01f7571f0419e0344a8cd5e1ce6a): fix ordering\n### README\n- [4db72b8c](https://github.com/quay/clair/commit/4db72b8c26a5754d61931c2fd5a6ee1829b9f016): fixed issues address\n- [6c3b3986](https://github.com/quay/clair/commit/6c3b398607f701ac8f016c804f2b2883c0ca1db9): fix IRC copypasta\n- [f36aa120](https://github.com/quay/clair/commit/f36aa12024ad430843110ca2a23140664f6c621d): clean up after README refactor\n- [3b2c4e5e](https://github.com/quay/clair/commit/3b2c4e5e92aaa49d2e90a5c2ab39fb79cbed4117): improve readability\n- [346c22fe](https://github.com/quay/clair/commit/346c22fe28db34f87959e5f7a4931940fb926a08): s/Namespace/Feature Namespace\n- [6c906358](https://github.com/quay/clair/commit/6c90635848da7aa3d5c7ed011773de93cf119775): update to reflect ext directory\n- [67be72b9](https://github.com/quay/clair/commit/67be72b97e581a0d54d2a1f20a9ac54404a04819): rm images from repo\n- [a1bbd7db](https://github.com/quay/clair/commit/a1bbd7dbf0d71e72cf8b2284aff8ca2326162709): add git dependency\n- [805f620b](https://github.com/quay/clair/commit/805f620b4b4e84514ac139b49c0d88cd85a4625e): add alpine data sources\n- [861cba0f](https://github.com/quay/clair/commit/861cba0f49101d117a3d49aed34a9ed11f4f91a4): s/1.2.2/1.2.4\n- [4bc64161](https://github.com/quay/clair/commit/4bc6416132dd5d3586d08a17d358adeeb760269e): include data licenses for data sources ([#219](https://github.com/quay/clair/issues/219))\n -  [#219](https://github.com/quay/clair/issues/219)- [c4281b3a](https://github.com/quay/clair/commit/c4281b3a3c9c90beed7ea723a0a343ad80068e1e): add reference to Klar tool\n- [4246c524](https://github.com/quay/clair/commit/4246c5244bbcffb9d278617a80bbe184bb8e935d): add master branch warning\n- [4ab49ee0](https://github.com/quay/clair/commit/4ab49ee0a0615f631f2f8a150c97c1bdc5527f67): Fix Kubernetes instructions\n- [9ce0956f](https://github.com/quay/clair/commit/9ce0956f1af348b4b0180e30e4b958b335faece4): add instructions for kubernetes\n- [e72f0e69](https://github.com/quay/clair/commit/e72f0e69232529b63cb6be1bd7bc431c3bfe2eb8): Reduce logo size\n- [9573acbc](https://github.com/quay/clair/commit/9573acbc1bea515bd3187d9f7e6dc5f6fde13212): Add logo\n- [f6ba17df](https://github.com/quay/clair/commit/f6ba17dfc7a4c5e516f970d795851b4a8cf0bbbc): Update Docker Compose instructions\n- [20ecc847](https://github.com/quay/clair/commit/20ecc847d99305e984a0d39e5766bb869c1ba556): Add FeatureDetector and NamespaceDetector\n- [440b5d58](https://github.com/quay/clair/commit/440b5d58cdef2ea7c3e3ba77fa3182a2ba25e4ed): fix godoc badge copypasta\n- [ec8cf9fb](https://github.com/quay/clair/commit/ec8cf9fb26efc4a59359e78048ac01a49ac839f9): add documentation with links\n- [fe1e0666](https://github.com/quay/clair/commit/fe1e06669f80d66794341557edd7befd2dc2618b): nitpick\n- [6b8e198e](https://github.com/quay/clair/commit/6b8e198ef917f25794e688f5e5601f6d396e6d7e): fix link\n- [80977f23](https://github.com/quay/clair/commit/80977f233ed5fbc10670638649080e28657c5481): add go report card\n- [c61eebaf](https://github.com/quay/clair/commit/c61eebafdfad2f241e9aca1f1815b17db860f329): move diagram to architecture section\n- [6e196e41](https://github.com/quay/clair/commit/6e196e416da89eb8947cddc295157d7384c7073e): add diagram & custom data sources\n- [ef7ccd37](https://github.com/quay/clair/commit/ef7ccd3773c4e87c738357275a172a16e08c7c6c): minor grammar/spelling tweaks\n- [a10260c8](https://github.com/quay/clair/commit/a10260c80d6ea2ba5ab65d2164b2e625d1eb2a99): add container badge\n### ROADMAP\n- [e9eb761d](https://github.com/quay/clair/commit/e9eb761db6d4093a2cbb907c14a0a99ae4c5982c): refresh with current priorities\n### Readme\n- [a8c58d4e](https://github.com/quay/clair/commit/a8c58d4e3d1e2190c54bcfcbe6a637ce9e946827): add various talks & slides\n- [93f7f10b](https://github.com/quay/clair/commit/93f7f10bf71ddfe353d4751d5f9c337bdb52f420): replace latest by v1.2.2 and add reference to container repositories\n- [49fa75a6](https://github.com/quay/clair/commit/49fa75a64abe50e1be134fd542b6973fa2ac4624): split \"Related Links\" into projects/slides ([#177](https://github.com/quay/clair/issues/177))\n -  [#177](https://github.com/quay/clair/issues/177) - Fixes [#173](https://github.com/quay/clair/issues/173)- [b3837673](https://github.com/quay/clair/commit/b3837673feefba04f7fc08d9fdeda7f0edb08d68): add dependencies to getting started\n- [0979b01a](https://github.com/quay/clair/commit/0979b01a44a1555273ebb6db4b42cbfab93253d5): add terminology and generic customization\n- [d47616a3](https://github.com/quay/clair/commit/d47616a33969b7d880021ca24eca265128cc6ed1): make API description consistence\n- [af0ddcea](https://github.com/quay/clair/commit/af0ddceaa2b8b914d8d7a49a538dc54281172a65): s/notification/notifications\n- [2140995a](https://github.com/quay/clair/commit/2140995a54040496961cc55e31871279a559bc17): clarify \"marked as read\" notifications\n- [f48f94cb](https://github.com/quay/clair/commit/f48f94cbd0f4e9c2205705bddc05f2ad3b3c74eb): continue to nitpick\n- [cadc182c](https://github.com/quay/clair/commit/cadc182cc41ee66cb08a7d8be8fbca08100c3fad): add travis-ci badge\n### Redhatrelease\n- [ce8d31bb](https://github.com/quay/clair/commit/ce8d31bbb323471bf2a69427e4a645b3ce8a25c1): override match for RHEL hosts\n### Refactor\n- [4a990372](https://github.com/quay/clair/commit/4a990372fff35f606184e276976f0279e4ea5a56): move updaters and notifier into ext\n### Style\n- [bd68578b](https://github.com/quay/clair/commit/bd68578b8bdd4488e197ccdf6d9322380c6ae7d0): Fix typo in headline\n### Tarutil\n- [a3a37072](https://github.com/quay/clair/commit/a3a37072b54840aaebde1cd0bba62b8939dafbdc): convert all filename specs to regexps\n- [afd7fe25](https://github.com/quay/clair/commit/afd7fe2554d65040b27291d658af21af8f8ae521): allow file names to be specified by regexp\n - fixes [#456](https://github.com/quay/clair/issues/456)### Travis\n- [52ecf35c](https://github.com/quay/clair/commit/52ecf35ca67558c1bedefb2259e9af9ad9649f9d): fail if not gofmt -s\n- [7492aa31](https://github.com/quay/clair/commit/7492aa31baf5b834088ecb8e8bd6ffd7817e5dd7): fail unformatted protos\n- [4fab3273](https://github.com/quay/clair/commit/4fab327397a1d9484809768fe357428599b510d6): add matrix for postgres\n- [2d0be7cc](https://github.com/quay/clair/commit/2d0be7ccf46c60717fc19049edfd6ded4bb6ee0e): update to use Go 1.7, glide\n- [bed3662e](https://github.com/quay/clair/commit/bed3662e64881f2eb8828937427d9e0ab1893654): allow golang 'tip' failures ([#202](https://github.com/quay/clair/issues/202))\n -  [#202](https://github.com/quay/clair/issues/202)- [0423f976](https://github.com/quay/clair/commit/0423f976b72a73f9708d60e9ef06a0e4acb456c3): test against Go 1.6\n- [02d38843](https://github.com/quay/clair/commit/02d38843cb5b4f0708ab0fd9790b60c50d17c19d): disable install step\n- [1b55d387](https://github.com/quay/clair/commit/1b55d387f6373ecea4ffeec92c0f995024f8d54c): add missing rpm dependency\n- [5873ab89](https://github.com/quay/clair/commit/5873ab892cb83b30478eecad2b41efc769b7a41d): initial travis.yml\n### Travis\n- [870e8123](https://github.com/quay/clair/commit/870e8123769a3dd717bfdcd21473a8e691806653): Drop support for postgres 9.4 postgres 9.4 doesn't support ON CONFLICT, which is required in our implementation.\n### Update Documentation\n- [1105102b](https://github.com/quay/clair/commit/1105102b8449fcf20b8db1b1722eeeeece2f33fa): talk about SUSE support\n### Update The Ingress To Use ApiVersion\n- [435d0539](https://github.com/quay/clair/commit/435d05394a9e7895d8daf2804bbe3668e1666981): networking.k8s.io/v1beta1\n### Updater\n- [7084a226](https://github.com/quay/clair/commit/7084a226ae9c5a3aed1248ad3d653100d610146c): extract deduplicate function\n- [e16d17dd](https://github.com/quay/clair/commit/e16d17dda9d29e8fdc33ef9da6a4a8be0e6b648f): remove original RunUpdate()\n- [0d41968a](https://github.com/quay/clair/commit/0d41968acdeeb2325bf9573a65fd1d05345ba255): reimplement fetch() with errgroup\n- [6c5be7e1](https://github.com/quay/clair/commit/6c5be7e1c6856fbae55e77c0a3411e7fe4d61f82): refactor to use errgroup\n- [2236b0a5](https://github.com/quay/clair/commit/2236b0a5c9a094bde2b7979417b9538cb944e726): Add vulnsrc affected feature type\n- [0d18a629](https://github.com/quay/clair/commit/0d18a629cab15d57fb7b00777f1537039b69401b): sleep before continuing the lock loop\n - Fixes [#415](https://github.com/quay/clair/issues/415)- [edfadc2f](https://github.com/quay/clair/commit/edfadc2f870776a14feaa46da35bceab5b0f9c74): Log fetch completion\n- [b792eb61](https://github.com/quay/clair/commit/b792eb61f69c8e9c100675970ac4d928fd2b5e98): copy whole namespace when deduping vulns\n- [96398465](https://github.com/quay/clair/commit/96398465dea9f86b569cacc7d3677db2f09a763b): Set vulns' Severity from NVD metadata fetcher if unknown\n- [1c3daa23](https://github.com/quay/clair/commit/1c3daa23b9e6fb76a9b0b283d3b2a5e1037b50b6): minimize vulns' lock duration in the NVD metadata fetcher\n- [be97db52](https://github.com/quay/clair/commit/be97db52611b69aab97db3b90e2b371349e137b2): enable fetching of RHEL 5 vulnerabilities ([#217](https://github.com/quay/clair/issues/217))\n -  [#217](https://github.com/quay/clair/issues/217) - Fixes [#215](https://github.com/quay/clair/issues/215)- [34f62ef1](https://github.com/quay/clair/commit/34f62ef1f1a61895ccbb997eabea6d904d0d4cc8): delete Ubuntu's repository upon bzr errors\n - Fixes [#169](https://github.com/quay/clair/issues/169)- [45ed80df](https://github.com/quay/clair/commit/45ed80df1b455b094ad2d6911b47010e228f3760): remove useless error\n- [2126259c](https://github.com/quay/clair/commit/2126259c9974d13eef118b49fbd972b6fbf05b3c): use a better link for Ubuntu vulnerabilities and rename some constants\n- [431c0ccb](https://github.com/quay/clair/commit/431c0ccb03f152ba60d1c465c682c39aa96ab8df): add a clean function to fetchers\n- [3ecb8b69](https://github.com/quay/clair/commit/3ecb8b69cb6823f2672fd5f5e59c9bdb30537894): ignore \"ubuntu-core\" in the Ubuntu fetcher\n- [8e852348](https://github.com/quay/clair/commit/8e852348a12593173e9d06726399a8c8d899b363): ensure that ubuntu's notes are unique\n- [99de7592](https://github.com/quay/clair/commit/99de759224089d70b4934099a38e99cd7641a0e3): namespace and split Ubuntu/RHEL vulnerabilities\n- [847c6492](https://github.com/quay/clair/commit/847c6492886ba85542759265399c0fd1e199dd63): update RHEL fetcher and add not-affected capability\n- [ea59b0e4](https://github.com/quay/clair/commit/ea59b0e45f36c0458876b9481463df748da43eee): update Ubuntu fetcher and add not-affected capability\n- [7e72eb10](https://github.com/quay/clair/commit/7e72eb10b66bfddb865e184e0dc56a839c411e70): ignore Debian's \"temp\" vulnerabilities\n- [77387af2](https://github.com/quay/clair/commit/77387af2ac9a8c9e900b9e362687d0c3a46121b8): port updater and its fetchers\n- [452f7018](https://github.com/quay/clair/commit/452f7018ecf55d5f3770d15f0e740ae332ada30c): move each fetcher to its own package\n- [e91365f4](https://github.com/quay/clair/commit/e91365f4b3ca6e6b7c30cea7d6700679a313b734): fix typos\n- [712aa11b](https://github.com/quay/clair/commit/712aa11b8b566be8a1c471481667c6a6f633f08f): Add support for Ubuntu Vivid Core and ignore Vivid PhoneOverlay\n- [c055c33c](https://github.com/quay/clair/commit/c055c33cf8cef8279c5f3c7d0ea5e0fd7e9a47b3): Fix Ubuntu's partial update bug.\n- [a7b683d4](https://github.com/quay/clair/commit/a7b683d4ba8420a1ad18c5392517774f82ce14d6): Refactor and merge fetcher responses\n - Fixes [#17](https://github.com/quay/clair/issues/17) -  [#19](https://github.com/quay/clair/issues/19)- [2452a8fc](https://github.com/quay/clair/commit/2452a8fc488f7ef6dd245bf0299e931d7453e0d5): Always use `bzr revno` to get Ubuntu db's revision number.\n - Fixes [#7](https://github.com/quay/clair/issues/7)### Updater\n- [a14b3728](https://github.com/quay/clair/commit/a14b372838a72d24110b57c6443d784d6fbe4451): fix stuck updater process\n### Updater,Pkg/Timeutil\n- [f64bd117](https://github.com/quay/clair/commit/f64bd117b2fa946c26a2e3368925f6dae8e4a2d3): minor cleanups\n### Updater/Database\n- [7c11e4eb](https://github.com/quay/clair/commit/7c11e4eb5da948c77a53820398d0ced42dca5601): do not create notifications during the initial update\n### Updater/Fetchers\n- [0cb8fc94](https://github.com/quay/clair/commit/0cb8fc9455905c658732fbc36ee9efe41fb78de5): add alpine secdb fetcher\n### Updater/Worker\n- [85fa3f9a](https://github.com/quay/clair/commit/85fa3f9a38ee625c005c375d0412cf8b7c131ff8): adapt several tests\n### Upgrade To Golang\n- [db5dbbe4](https://github.com/quay/clair/commit/db5dbbe4e983a4ac827f5b6597aac780c03124b3): 1.10-alpine\n### Utils\n- [3e4dc383](https://github.com/quay/clair/commit/3e4dc3834f539d67829d83cf42c4c978611ab83e): remove string.go\n- [c2f4a440](https://github.com/quay/clair/commit/c2f4a4406812f85658615305f52329ec688975e5): rm exec.go\n- [e7f72ef5](https://github.com/quay/clair/commit/e7f72ef5adca478985545dfafd9d0011e098367c): rm prometheus.go\n- [1faf27ba](https://github.com/quay/clair/commit/1faf27ba185bbad2e12558accb65b6460d5ee682): Fix OVAL's log statements\n### Utils/Http\n- [02e2c582](https://github.com/quay/clair/commit/02e2c5823670d9587a2143a231adfa3cd38a87bb): remove unused pkg\n### V1\n- [4fd4049f](https://github.com/quay/clair/commit/4fd4049fee70539bbf2acee7e451cb35ae476c3f): update documented error codes\n- [452c32d7](https://github.com/quay/clair/commit/452c32d7d7ff412a4a9c069f07eef220e16686bd): pagination now deterministic\n- [dc431c22](https://github.com/quay/clair/commit/dc431c22f34660746e7efa19d86cd846b1272b70): add readme\n- [771e35de](https://github.com/quay/clair/commit/771e35def021863d5f6b94536f87a5812718e01f): return object on PUT/POST\n- [c06df1af](https://github.com/quay/clair/commit/c06df1affdf60e35e2be7811be2469e4ee3bf827): 200 on PUT\n### V3\n- [88f50691](https://github.com/quay/clair/commit/88f506918b9cb32ab77e41e0cbbe2f9db6e6b358): Analyze layer content in parallel\n- [dd239762](https://github.com/quay/clair/commit/dd239762f63702c1800895ee9b86bdda316830ef): Move services to top of the file\n- [9f5d1ea4](https://github.com/quay/clair/commit/9f5d1ea4e16793ebd9390673aed34855671b5c24): associate feature and namespace with detector\n### Various\n- [500fc4e4](https://github.com/quay/clair/commit/500fc4e407961419dac87072123a21adc2c6f15b): gofmt -s\n- [8fd0aa16](https://github.com/quay/clair/commit/8fd0aa162bb847c1a81e84920b2e4a04daf41d62): spelling corrections\n### Vendor\n- [41063221](https://github.com/quay/clair/commit/41063221075cea67636f77f58a9d3e112771b835): Update gopkg.in/yaml.v2 package\n- [34d0e516](https://github.com/quay/clair/commit/34d0e516e0792ca2d06299a1262e5676d4145f80): Add golang-set dependency\n- [55ecf1e5](https://github.com/quay/clair/commit/55ecf1e58aa75346ca6c4d702eb31e02ff32ee0e): regenerate after removing graceful\n- [1533dd1d](https://github.com/quay/clair/commit/1533dd1d51d4f89febd857897addb6dfb6c161e4): updated vendor dir for grpc v2 api\n- [35df9d58](https://github.com/quay/clair/commit/35df9d5846d5b69e832d987a87e9ba4d838d4178): regenerate vendor directory with glide\n- [50d07ccf](https://github.com/quay/clair/commit/50d07ccf597e95dad6d0ceff386aa14e3d062d77): rm everything to prep for regeneration\n### Versionfmt\n- [8d29bf86](https://github.com/quay/clair/commit/8d29bf860d363a6ef061a4a4f3c1276e365966b4): convert to using constant over literal\n- [6864a8ef](https://github.com/quay/clair/commit/6864a8efead0337c7af700c60f1ed85b8a15ff9f): init rpm versionfmt\n### Versionfmt/Dpkg\n- [1e9f14ae](https://github.com/quay/clair/commit/1e9f14ae33963d5dea1ec5217ba9069934e2e655): remove leading digit requirement\n### Versionfmt/Rpm\n- [db8a133d](https://github.com/quay/clair/commit/db8a133d2130e8a6b9f598c4bd859b06a5a0a8af): handle a tilde correctly\n### Vulnmdsrc\n- [ce6b0088](https://github.com/quay/clair/commit/ce6b00887b1db3a402b1a02bdebb5bcc23d4add0): update NVD URLs\n - Fixes [#575](https://github.com/quay/clair/issues/575)### Vulnsrc\n- [72674ca8](https://github.com/quay/clair/commit/72674ca871dd2b0a9afdbd9c6a6b50f49a50b20b): Refactor vulnerability sources to use utility functions\n### Vulnsrc Rhel\n- [bd7102d9](https://github.com/quay/clair/commit/bd7102d96304b02ff09077edc16f5f60bd784c8b): handle \"none\" CVE impact\n### Vulnsrc/Alpine\n- [c031f8ea](https://github.com/quay/clair/commit/c031f8ea0c793ba0462f2b8a204c15ab3a65f1a5): s/pull/clone\n- [4c2be528](https://github.com/quay/clair/commit/4c2be5285e1419844377c11484bd684b45948958): avoid shadowing vars\n- [c8622d5f](https://github.com/quay/clair/commit/c8622d5f3472698e872b7b6a6ff817da42bbcf07): unify schema and parse v3.5\n### Vulnsrc/Ubuntu\n- [456af5f4](https://github.com/quay/clair/commit/456af5f48c8da8325266209e58cec90f4a3f1f68): use new git-based ubuntu tracker\n### Vulnsrc_oracle\n- [3503ddb9](https://github.com/quay/clair/commit/3503ddb96fe412242b84ec28f36a7ddd787b823f): one vulnerability per CVE\n -  [#495](https://github.com/quay/clair/issues/495) -  [#499](https://github.com/quay/clair/issues/499)### Vulnsrc_rhel\n- [c4ffa0c3](https://github.com/quay/clair/commit/c4ffa0c370e793546dd51ea25fc98961c2d25970): cve impact\n- [a90db713](https://github.com/quay/clair/commit/a90db713a2722a80db33e47343c4a4d417f48a0e): add test\n- [8b3338ef](https://github.com/quay/clair/commit/8b3338ef56b060e27bc3d81124f52bbded315f1a): minor changes\n- [4e4e98f3](https://github.com/quay/clair/commit/4e4e98f328309d1c0a470388d198fa37c27e47d5): minor changes\n- [ac86a367](https://github.com/quay/clair/commit/ac86a3674094f93b71e8736392b7a4707fa972fe): rhsa_ID by default\n- [4ab98cfe](https://github.com/quay/clair/commit/4ab98cfe54bedcce7880cc03b1c52d5a91811860): one vulnerability by CVE\n - Fixes [#495](https://github.com/quay/clair/issues/495)### Webhook\n- [8c282fdb](https://github.com/quay/clair/commit/8c282fdb5a73e2b6579bbafb367c68d45766f760): add JSON envelope\n### Worker\n- [23ccd9b5](https://github.com/quay/clair/commit/23ccd9b53ba0a8bcf800fecdbd72d5cbefd2ea60): Fix tests for feature_type\n- [f0e21df7](https://github.com/quay/clair/commit/f0e21df7830e3f8d00498936d0d292ae6ff6765b): fixed duplicated ns and ns not inherited bug\n- [ce6eba9f](https://github.com/quay/clair/commit/ce6eba9fcb8f037a644f1790d690ad846559d274): Rewrite unknown namespace warning\n- [8bedd0a3](https://github.com/quay/clair/commit/8bedd0a3670dfe2bc60d3b750c981441f91d32c8): ns detectors now support VersionFormat\n- [de1f09e8](https://github.com/quay/clair/commit/de1f09e8b375ae79a16349dee74d2dd664a606bd): clarify maxFileSize purpose\n - Fixes [#237](https://github.com/quay/clair/issues/237)- [2cb23ced](https://github.com/quay/clair/commit/2cb23ced02b3cdbbb2de62d042562fe2fc68dc2e): bump engine version\n- [8551a0a3](https://github.com/quay/clair/commit/8551a0a3b2a0b93867395dce3efc6156ed642aad): Mock datastore in worker's tests\n- [bae5a5e3](https://github.com/quay/clair/commit/bae5a5e3ad15719f4094d2f766937952d5576fad): remove duplicated tests\n- [c2605e0b](https://github.com/quay/clair/commit/c2605e0bf2db061ad56b70823a33724e2ec606d9): verify download status code\n- [41736e46](https://github.com/quay/clair/commit/41736e4600ae0314c039ec7b931491f132af9999): DetectData should return an error if the supported detector failed\n- [98ed0419](https://github.com/quay/clair/commit/98ed041956b6710b56a645949692c0c950b0e82d): remove double error\n- [9b51f7f4](https://github.com/quay/clair/commit/9b51f7f4fbf4d9e700d7accc8802265de23a99c6): raise worker version number\n- [2f57f0d4](https://github.com/quay/clair/commit/2f57f0d4b1a65e4b426f9da4d27fd405d399e9e5): change worker errors to bad request errors\n- [b3ddfbc3](https://github.com/quay/clair/commit/b3ddfbc3538bae97e7b6875e3bdfc97e31383f33): remove namespace whitelist\n- [90fe137d](https://github.com/quay/clair/commit/90fe137de82f10df644a1671ee12fc91afbeb1ab): move each data detector to their own packages and remove image format whitelist\n- [34842fd8](https://github.com/quay/clair/commit/34842fd8f77a200a77f546dce8728f63fb378675): fix dpkg detector and adapt tests\n- [343ce398](https://github.com/quay/clair/commit/343ce39865dc2994c0bd8c4d9f75c5be476fe1b0): detect the status code when downloading a layer and expect 2XX.\n- [ac0e68ef](https://github.com/quay/clair/commit/ac0e68efe7fee01f01bfdc7b13de513e29e69f91): Add a missing CleanURL\n### Worker/Database\n- [a38fbf6c](https://github.com/quay/clair/commit/a38fbf6cfe3345cd7b684a1d38d8021f9e7c8e2a): Move upgrade detection logic out of database to worker\n### Workflows\n- [e1902d4d](https://github.com/quay/clair/commit/e1902d4d7c1f7d7fdccc6b339736966d2ece0cf6): proper tag name\n- [b2d781c2](https://github.com/quay/clair/commit/b2d781c2ed50262f4882e34b2585bf99d80fb15b): bad tar flag\n### Reverts\n- Merge pull request [#199](https://github.com/quay/clair/issues/199) from openSUSE/feature/opensuse\n- v1: pagination now deterministic\n\n### Pull Requests\n- Merge pull request [#949](https://github.com/quay/clair/issues/949) from alecmerdler/PROJQUAY-494\n- Merge pull request [#936](https://github.com/quay/clair/issues/936) from ldelossa/louis/interface-refactor\n- Merge pull request [#933](https://github.com/quay/clair/issues/933) from ldelossa/louis/config-and-make\n- Merge pull request [#930](https://github.com/quay/clair/issues/930) from ldelossa/louis/middleware-packaging\n- Merge pull request [#929](https://github.com/quay/clair/issues/929) from ldelossa/louis/cc-bump-v0.0.17\n- Merge pull request [#924](https://github.com/quay/clair/issues/924) from ldelossa/louis/severity-mapping\n- Merge pull request [#903](https://github.com/quay/clair/issues/903) from ldelossa/louis/environment-api\n- Merge pull request [#897](https://github.com/quay/clair/issues/897) from ldelossa/louis/state-json\n- Merge pull request [#890](https://github.com/quay/clair/issues/890) from ldelossa/louis/remove-healthhandler\n- Merge pull request [#877](https://github.com/quay/clair/issues/877) from mtougeron/update-ingress-apiversion\n- Merge pull request [#873](https://github.com/quay/clair/issues/873) from coreos/code-owners-update\n- Merge pull request [#867](https://github.com/quay/clair/issues/867) from andrewsharon/ubuntu19.10\n- Merge pull request [#861](https://github.com/quay/clair/issues/861) from thekbb/fix-broken-link-i-missed\n- Merge pull request [#856](https://github.com/quay/clair/issues/856) from thekbb/fix-links\n- Merge pull request [#860](https://github.com/quay/clair/issues/860) from jzelinskie/bump-v2-master\n- Merge pull request [#851](https://github.com/quay/clair/issues/851) from Allda/log-fix\n- Merge pull request [#774](https://github.com/quay/clair/issues/774) from Allda/updater_fix\n- Merge pull request [#839](https://github.com/quay/clair/issues/839) from noahklein/nvd-status-error\n- Merge pull request [#829](https://github.com/quay/clair/issues/829) from peacocb/peacocb-828-dos-on-ancestry-post\n- Merge pull request [#831](https://github.com/quay/clair/issues/831) from MVrachev/patch-1\n- Merge pull request [#818](https://github.com/quay/clair/issues/818) from vsamidurai/master\n- Merge pull request [#822](https://github.com/quay/clair/issues/822) from imlonghao/bullseye\n- Merge pull request [#817](https://github.com/quay/clair/issues/817) from ldelossa/remove-detectors\n- Merge pull request [#755](https://github.com/quay/clair/issues/755) from Allda/openshift_cert\n- Merge pull request [#808](https://github.com/quay/clair/issues/808) from coreos/add-louis\n- Merge pull request [#797](https://github.com/quay/clair/issues/797) from jzelinskie/drone\n- Merge pull request [#805](https://github.com/quay/clair/issues/805) from ldelossa/remove-ancestry-copy\n- Merge pull request [#794](https://github.com/quay/clair/issues/794) from ldelossa/local-dev-readme-update\n- Merge pull request [#793](https://github.com/quay/clair/issues/793) from ldelossa/local-dev-clair-db\n- Merge pull request [#788](https://github.com/quay/clair/issues/788) from ldelossa/helm-local-dev\n- Merge pull request [#780](https://github.com/quay/clair/issues/780) from jzelinskie/CODEOWNERS\n- Merge pull request [#779](https://github.com/quay/clair/issues/779) from jzelinskie/mailing-list\n- Merge pull request [#773](https://github.com/quay/clair/issues/773) from flumm/disco\n- Merge pull request [#671](https://github.com/quay/clair/issues/671) from ericysim/amazon\n- Merge pull request [#766](https://github.com/quay/clair/issues/766) from Allda/lock_timeout\n- Merge pull request [#742](https://github.com/quay/clair/issues/742) from bluelabsio/path-templating\n- Merge pull request [#739](https://github.com/quay/clair/issues/739) from joelee2012/master\n- Merge pull request [#749](https://github.com/quay/clair/issues/749) from cnorthwood/tarutil-glob\n- Merge pull request [#741](https://github.com/quay/clair/issues/741) from KeyboardNerd/parallel_download\n- Merge pull request [#738](https://github.com/quay/clair/issues/738) from Allda/potentialNamespaceAncestry\n- Merge pull request [#721](https://github.com/quay/clair/issues/721) from KeyboardNerd/cache\n- Merge pull request [#735](https://github.com/quay/clair/issues/735) from jzelinskie/fix-sweet32\n- Merge pull request [#722](https://github.com/quay/clair/issues/722) from Allda/feature_ns\n- Merge pull request [#724](https://github.com/quay/clair/issues/724) from KeyboardNerd/ref\n- Merge pull request [#728](https://github.com/quay/clair/issues/728) from KeyboardNerd/fix\n- Merge pull request [#727](https://github.com/quay/clair/issues/727) from KeyboardNerd/master\n- Merge pull request [#725](https://github.com/quay/clair/issues/725) from KeyboardNerd/license_test\n- Merge pull request [#723](https://github.com/quay/clair/issues/723) from jzelinskie/lock-tx\n- Merge pull request [#720](https://github.com/quay/clair/issues/720) from KeyboardNerd/update_ns\n- Merge pull request [#695](https://github.com/quay/clair/issues/695) from saromanov/fix-unchecked-error\n- Merge pull request [#712](https://github.com/quay/clair/issues/712) from KeyboardNerd/builder\n- Merge pull request [#672](https://github.com/quay/clair/issues/672) from KeyboardNerd/source_package/feature_type\n- Merge pull request [#685](https://github.com/quay/clair/issues/685) from jzelinskie/updater-cleanup\n- Merge pull request [#701](https://github.com/quay/clair/issues/701) from dustinspecker/patch-1\n- Merge pull request [#700](https://github.com/quay/clair/issues/700) from traum-ferienwohnungen/master\n- Merge pull request [#680](https://github.com/quay/clair/issues/680) from Allda/slices\n- Merge pull request [#687](https://github.com/quay/clair/issues/687) from jzelinskie/suse-config\n- Merge pull request [#686](https://github.com/quay/clair/issues/686) from jzelinskie/fix-presentations\n- Merge pull request [#679](https://github.com/quay/clair/issues/679) from kubeshield/master\n- Merge pull request [#506](https://github.com/quay/clair/issues/506) from openSUSE/reintroduce-suse-opensuse\n- Merge pull request [#681](https://github.com/quay/clair/issues/681) from Allda/rhel_severity\n- Merge pull request [#667](https://github.com/quay/clair/issues/667) from travelaudience/helm-tolerations\n- Merge pull request [#656](https://github.com/quay/clair/issues/656) from glb/elsa_CVEID\n- Merge pull request [#650](https://github.com/quay/clair/issues/650) from Katee/add-ubuntu-cosmic\n- Merge pull request [#653](https://github.com/quay/clair/issues/653) from brosander/helm-dep\n- Merge pull request [#648](https://github.com/quay/clair/issues/648) from HaraldNordgren/go_versions\n- Merge pull request [#647](https://github.com/quay/clair/issues/647) from KeyboardNerd/spkg/cvrf\n- Merge pull request [#644](https://github.com/quay/clair/issues/644) from KeyboardNerd/bug/git\n- Merge pull request [#645](https://github.com/quay/clair/issues/645) from Katee/include-cvssv3\n- Merge pull request [#646](https://github.com/quay/clair/issues/646) from KeyboardNerd/spkg/model\n- Merge pull request [#640](https://github.com/quay/clair/issues/640) from KeyboardNerd/sourcePackage\n- Merge pull request [#639](https://github.com/quay/clair/issues/639) from Katee/update-sha1-to-sha256\n- Merge pull request [#638](https://github.com/quay/clair/issues/638) from KeyboardNerd/featureTree\n- Merge pull request [#633](https://github.com/quay/clair/issues/633) from coreos/roadmap-1\n- Merge pull request [#620](https://github.com/quay/clair/issues/620) from KeyboardNerd/feature/detector\n- Merge pull request [#627](https://github.com/quay/clair/issues/627) from haydenhughes/master\n- Merge pull request [#624](https://github.com/quay/clair/issues/624) from jzelinskie/probot\n- Merge pull request [#621](https://github.com/quay/clair/issues/621) from jzelinskie/gitutil\n- Merge pull request [#610](https://github.com/quay/clair/issues/610) from MackJM/wip/master_nvd_httputil\n- Merge pull request [#499](https://github.com/quay/clair/issues/499) from yebinama/rhel_CVEID\n- Merge pull request [#619](https://github.com/quay/clair/issues/619) from KeyboardNerd/sidac/rm_layer\n- Merge pull request [#617](https://github.com/quay/clair/issues/617) from jzelinskie/grpc-refactor\n- Merge pull request [#614](https://github.com/quay/clair/issues/614) from KeyboardNerd/sidac/simplify\n- Merge pull request [#613](https://github.com/quay/clair/issues/613) from jzelinskie/pkg-pagination\n- Merge pull request [#611](https://github.com/quay/clair/issues/611) from jzelinskie/drop-graceful\n- Merge pull request [#605](https://github.com/quay/clair/issues/605) from KeyboardNerd/sidchen/feature\n- Merge pull request [#606](https://github.com/quay/clair/issues/606) from MackJM/wip/master_httputil\n- Merge pull request [#607](https://github.com/quay/clair/issues/607) from jzelinskie/gofmt\n- Merge pull request [#604](https://github.com/quay/clair/issues/604) from jzelinskie/nvd-urls\n- Merge pull request [#601](https://github.com/quay/clair/issues/601) from KeyboardNerd/sidchen/status\n- Merge pull request [#594](https://github.com/quay/clair/issues/594) from reasonerjt/fix-alpine-url\n- Merge pull request [#578](https://github.com/quay/clair/issues/578) from naibaf0/fix/helmtemplate/configmap/postgresql\n- Merge pull request [#586](https://github.com/quay/clair/issues/586) from robertomlsoares/update-helm-chart\n- Merge pull request [#582](https://github.com/quay/clair/issues/582) from brosander/helm-alpine-postgres\n- Merge pull request [#571](https://github.com/quay/clair/issues/571) from ErikThoreson/nvdupdates\n- Merge pull request [#574](https://github.com/quay/clair/issues/574) from hongli-my/fix-nvd-path\n- Merge pull request [#572](https://github.com/quay/clair/issues/572) from arno01/multi-stage\n- Merge pull request [#540](https://github.com/quay/clair/issues/540) from jzelinskie/document-proto\n- Merge pull request [#569](https://github.com/quay/clair/issues/569) from jzelinskie/ubuntu-git\n- Merge pull request [#553](https://github.com/quay/clair/issues/553) from qeqar/master\n- Merge pull request [#551](https://github.com/quay/clair/issues/551) from usr42/upgrade_to_1.10-alpine\n- Merge pull request [#538](https://github.com/quay/clair/issues/538) from jzelinskie/dockerize-protogen\n- Merge pull request [#537](https://github.com/quay/clair/issues/537) from tomer-1/patch-1\n- Merge pull request [#532](https://github.com/quay/clair/issues/532) from KeyboardNerd/readme_typo\n- Merge pull request [#508](https://github.com/quay/clair/issues/508) from joerayme/bug/436\n- Merge pull request [#528](https://github.com/quay/clair/issues/528) from KeyboardNerd/helm_typo\n- Merge pull request [#522](https://github.com/quay/clair/issues/522) from vdboor/master\n- Merge pull request [#521](https://github.com/quay/clair/issues/521) from yebinama/paclair\n- Merge pull request [#518](https://github.com/quay/clair/issues/518) from traum-ferienwohnungen/master\n- Merge pull request [#513](https://github.com/quay/clair/issues/513) from leandrocr/patch-1\n- Merge pull request [#517](https://github.com/quay/clair/issues/517) from KeyboardNerd/master\n- Merge pull request [#505](https://github.com/quay/clair/issues/505) from ericchiang/coc\n- Merge pull request [#484](https://github.com/quay/clair/issues/484) from odg0318/master\n- Merge pull request [#498](https://github.com/quay/clair/issues/498) from bkochendorfer/contributing-link\n- Merge pull request [#482](https://github.com/quay/clair/issues/482) from yfoelling/patch-1\n- Merge pull request [#487](https://github.com/quay/clair/issues/487) from ajgreenb/db-connection-backoff\n- Merge pull request [#488](https://github.com/quay/clair/issues/488) from caulagi/patch-1\n- Merge pull request [#485](https://github.com/quay/clair/issues/485) from yebinama/proxy\n- Merge pull request [#481](https://github.com/quay/clair/issues/481) from coreos/stable-release-issue-template\n- Merge pull request [#479](https://github.com/quay/clair/issues/479) from yebinama/nvd_vectors\n- Merge pull request [#477](https://github.com/quay/clair/issues/477) from bseb/master\n- Merge pull request [#469](https://github.com/quay/clair/issues/469) from zamarrowski/master\n- Merge pull request [#475](https://github.com/quay/clair/issues/475) from dctrud/clair-singularity\n- Merge pull request [#467](https://github.com/quay/clair/issues/467) from grebois/master\n- Merge pull request [#465](https://github.com/quay/clair/issues/465) from jzelinskie/github\n- Merge pull request [#463](https://github.com/quay/clair/issues/463) from brunomcustodio/fix-ingress\n- Merge pull request [#459](https://github.com/quay/clair/issues/459) from arthurlm44/patch-1\n- Merge pull request [#458](https://github.com/quay/clair/issues/458) from jzelinskie/linux-vulns\n- Merge pull request [#450](https://github.com/quay/clair/issues/450) from jzelinskie/move-token\n- Merge pull request [#454](https://github.com/quay/clair/issues/454) from InTheCloudDan/helm-tls-option\n- Merge pull request [#455](https://github.com/quay/clair/issues/455) from zmarouf/master\n- Merge pull request [#449](https://github.com/quay/clair/issues/449) from jzelinskie/helm\n- Merge pull request [#447](https://github.com/quay/clair/issues/447) from KeyboardNerd/ancestry_\n- Merge pull request [#448](https://github.com/quay/clair/issues/448) from jzelinskie/woops\n- Merge pull request [#444](https://github.com/quay/clair/issues/444) from jzelinskie/docs-refresh\n- Merge pull request [#432](https://github.com/quay/clair/issues/432) from KeyboardNerd/ancestry_\n- Merge pull request [#442](https://github.com/quay/clair/issues/442) from arminc/add-integration-clari-scanner\n- Merge pull request [#433](https://github.com/quay/clair/issues/433) from mssola/portus-integration\n- Merge pull request [#408](https://github.com/quay/clair/issues/408) from KeyboardNerd/grpc\n- Merge pull request [#423](https://github.com/quay/clair/issues/423) from jzelinskie/sleep-updater\n- Merge pull request [#418](https://github.com/quay/clair/issues/418) from KeyboardNerd/multiplens\n- Merge pull request [#410](https://github.com/quay/clair/issues/410) from KeyboardNerd/xforward\n- Merge pull request [#416](https://github.com/quay/clair/issues/416) from tianon/debian-buster\n- Merge pull request [#413](https://github.com/quay/clair/issues/413) from transcedentalia/master\n- Merge pull request [#403](https://github.com/quay/clair/issues/403) from KeyboardNerd/multiplens\n- Merge pull request [#407](https://github.com/quay/clair/issues/407) from swestcott/kubernetes-config-fix\n- Merge pull request [#394](https://github.com/quay/clair/issues/394) from KeyboardNerd/multiplens\n- Merge pull request [#382](https://github.com/quay/clair/issues/382) from caipre/patch-1\n- Merge pull request [#395](https://github.com/quay/clair/issues/395) from knqyf263/handle_tilde\n- Merge pull request [#392](https://github.com/quay/clair/issues/392) from jzelinskie/https-sec-db\n- Merge pull request [#390](https://github.com/quay/clair/issues/390) from KeyboardNerd/fernet\n- Merge pull request [#389](https://github.com/quay/clair/issues/389) from jzelinskie/revendor\n- Merge pull request [#387](https://github.com/quay/clair/issues/387) from jzelinskie/rm-analyze-local-images\n- Merge pull request [#385](https://github.com/quay/clair/issues/385) from KeyboardNerd/logrus\n- Merge pull request [#381](https://github.com/quay/clair/issues/381) from KeyboardNerd/bill-of-materials\n- Merge pull request [#373](https://github.com/quay/clair/issues/373) from josuesdiaz/fix_analyze_local\n- Merge pull request [#378](https://github.com/quay/clair/issues/378) from jzelinskie/oracle-update-fix\n- Merge pull request [#374](https://github.com/quay/clair/issues/374) from tianon/new-ubuntu-releases\n- Merge pull request [#371](https://github.com/quay/clair/issues/371) from caipre/add-logging\n- Merge pull request [#370](https://github.com/quay/clair/issues/370) from jzelinskie/featurens\n- Merge pull request [#369](https://github.com/quay/clair/issues/369) from jzelinskie/fix-ali\n- Merge pull request [#367](https://github.com/quay/clair/issues/367) from jzelinskie/analyze-layers-v2\n- Merge pull request [#366](https://github.com/quay/clair/issues/366) from jzelinskie/fixoracle\n- Merge pull request [#361](https://github.com/quay/clair/issues/361) from jzelinskie/ROADMAP.md\n- Merge pull request [#363](https://github.com/quay/clair/issues/363) from davidxia/patch-1\n- Merge pull request [#362](https://github.com/quay/clair/issues/362) from jzelinskie/malformedpkg\n- Merge pull request [#360](https://github.com/quay/clair/issues/360) from jzelinskie/cleanup\n- Merge pull request [#359](https://github.com/quay/clair/issues/359) from matslina/patch-1\n- Merge pull request [#357](https://github.com/quay/clair/issues/357) from jzelinskie/readme-reboot\n- Merge pull request [#352](https://github.com/quay/clair/issues/352) from kevinburke/fix-404\n- Merge pull request [#354](https://github.com/quay/clair/issues/354) from kevinburke/change-readme-text\n- Merge pull request [#347](https://github.com/quay/clair/issues/347) from jzelinskie/composeup\n- Merge pull request [#348](https://github.com/quay/clair/issues/348) from supereagle/update-image-spec-url\n- Merge pull request [#341](https://github.com/quay/clair/issues/341) from pizzarabe/Readme_Alpine35\n- Merge pull request [#340](https://github.com/quay/clair/issues/340) from coreos/philips-patch-1\n- Merge pull request [#338](https://github.com/quay/clair/issues/338) from pgburt/paulb-prod-users-integrations\n- Merge pull request [#335](https://github.com/quay/clair/issues/335) from jzelinskie/fixns\n- Merge pull request [#334](https://github.com/quay/clair/issues/334) from supereagle/update-dockerfile\n- Merge pull request [#331](https://github.com/quay/clair/issues/331) from supereagle/insecure-tls\n- Merge pull request [#328](https://github.com/quay/clair/issues/328) from jgsqware/master\n- Merge pull request [#327](https://github.com/quay/clair/issues/327) from jzelinskie/bad-ns-copy\n- Merge pull request [#326](https://github.com/quay/clair/issues/326) from Quentin-M/alpine_dfile\n- Merge pull request [#324](https://github.com/quay/clair/issues/324) from Quentin-M/log_ns\n- Merge pull request [#325](https://github.com/quay/clair/issues/325) from Quentin-M/alpine_dfile\n- Merge pull request [#316](https://github.com/quay/clair/issues/316) from jzelinskie/fix-alpine\n- Merge pull request [#305](https://github.com/quay/clair/issues/305) from jzelinskie/ext\n- Merge pull request [#309](https://github.com/quay/clair/issues/309) from jzelinskie/fixmigration6\n- Merge pull request [#308](https://github.com/quay/clair/issues/308) from jzelinskie/fixpagination\n- Merge pull request [#307](https://github.com/quay/clair/issues/307) from jzelinskie/layeridorder\n- Merge pull request [#302](https://github.com/quay/clair/issues/302) from jzelinskie/rmimage\n- Merge pull request [#301](https://github.com/quay/clair/issues/301) from jzelinskie/readme-git\n- Merge pull request [#298](https://github.com/quay/clair/issues/298) from jzelinskie/versions\n- Merge pull request [#300](https://github.com/quay/clair/issues/300) from miketheman/patch-1\n- Merge pull request [#299](https://github.com/quay/clair/issues/299) from alexei-led/master\n- Merge pull request [#295](https://github.com/quay/clair/issues/295) from jzelinskie/fixmigrationorder\n- Merge pull request [#290](https://github.com/quay/clair/issues/290) from Djelibeybi/oraclelinux-support\n- Merge pull request [#288](https://github.com/quay/clair/issues/288) from jzelinskie/200mb\n- Merge pull request [#289](https://github.com/quay/clair/issues/289) from jzelinskie/revert-suse\n- Merge pull request [#287](https://github.com/quay/clair/issues/287) from jzelinskie/enginebump\n- Merge pull request [#272](https://github.com/quay/clair/issues/272) from jzelinskie/alpine\n- Merge pull request [#282](https://github.com/quay/clair/issues/282) from jzelinskie/layer-sort-id\n- Merge pull request [#280](https://github.com/quay/clair/issues/280) from coreos/add_idx_deleted_at\n- Merge pull request [#281](https://github.com/quay/clair/issues/281) from coreos/dis_hashjoins_introducing\n- Merge pull request [#277](https://github.com/quay/clair/issues/277) from jzelinskie/travispg\n- Merge pull request [#279](https://github.com/quay/clair/issues/279) from coreos/searchintro_optimize\n- Merge pull request [#278](https://github.com/quay/clair/issues/278) from jzelinskie/layerdiffindex\n- Merge pull request [#276](https://github.com/quay/clair/issues/276) from jzelinskie/index\n- Merge pull request [#274](https://github.com/quay/clair/issues/274) from JensPiegsa/patch-1\n- Merge pull request [#271](https://github.com/quay/clair/issues/271) from Quentin-M/nvd_severity\n- Merge pull request [#270](https://github.com/quay/clair/issues/270) from Quentin-M/imp_docs\n- Merge pull request [#263](https://github.com/quay/clair/issues/263) from Quentin-M/rhel_unique_fixedin\n- Merge pull request [#261](https://github.com/quay/clair/issues/261) from Quentin-M/replace_goose\n- Merge pull request [#262](https://github.com/quay/clair/issues/262) from jzelinskie/travis\n- Merge pull request [#257](https://github.com/quay/clair/issues/257) from mattmoor/yakkety\n- Merge pull request [#199](https://github.com/quay/clair/issues/199) from openSUSE/feature/opensuse\n- Merge pull request [#236](https://github.com/quay/clair/issues/236) from robszumski/doc-link\n- Merge pull request [#235](https://github.com/quay/clair/issues/235) from jzelinskie/doc-move\n- Merge pull request [#229](https://github.com/quay/clair/issues/229) from vbatts/redhatrelease_detector\n- Merge pull request [#216](https://github.com/quay/clair/issues/216) from optiopay/doc-klar-ref\n- Merge pull request [#205](https://github.com/quay/clair/issues/205) from Quentin-M/readme_v122\n- Merge pull request [#206](https://github.com/quay/clair/issues/206) from Quentin-M/godeps_implsubmod\n- Merge pull request [#186](https://github.com/quay/clair/issues/186) from Quentin-M/delete_ubuntu_repository\n- Merge pull request [#196](https://github.com/quay/clair/issues/196) from jgsqware/integrate-glide\n- Merge pull request [#188](https://github.com/quay/clair/issues/188) from databus23/patch-1\n- Merge pull request [#165](https://github.com/quay/clair/issues/165) from Quentin-M/db_registration\n- Merge pull request [#166](https://github.com/quay/clair/issues/166) from jzelinskie/authlayer\n- Merge pull request [#158](https://github.com/quay/clair/issues/158) from Quentin-M/contrib_cleanup_signals\n- Merge pull request [#143](https://github.com/quay/clair/issues/143) from jzelinskie/travis\n- Merge pull request [#142](https://github.com/quay/clair/issues/142) from jzelinskie/healthfix\n- Merge pull request [#139](https://github.com/quay/clair/issues/139) from coreos/webhook_proxy\n- Merge pull request [#137](https://github.com/quay/clair/issues/137) from coreos/fix_k8s\n- Merge pull request [#126](https://github.com/quay/clair/issues/126) from harsha-y/master\n- Merge pull request [#118](https://github.com/quay/clair/issues/118) from coreos/cleanup_contrib\n- Merge pull request [#123](https://github.com/quay/clair/issues/123) from coreos/contrib_fix_deadlink\n- Merge pull request [#116](https://github.com/quay/clair/issues/116) from BWITS/master\n- Merge pull request [#110](https://github.com/quay/clair/issues/110) from jzelinskie/config-fixes\n- Merge pull request [#111](https://github.com/quay/clair/issues/111) from jzelinskie/dockerfile-update\n- Merge pull request [#108](https://github.com/quay/clair/issues/108) from philips/add-k8s-contrib\n- Merge pull request [#107](https://github.com/quay/clair/issues/107) from Quentin-M/reduce_logo\n- Merge pull request [#106](https://github.com/quay/clair/issues/106) from Quentin-M/logo\n- Merge pull request [#105](https://github.com/quay/clair/issues/105) from coreos/crtrb_forcecolor\n- Merge pull request [#104](https://github.com/quay/clair/issues/104) from coreos/ctrb_minseverity\n- Merge pull request [#103](https://github.com/quay/clair/issues/103) from jzelinskie/fix-config\n- Merge pull request [#101](https://github.com/quay/clair/issues/101) from Quentin-M/ctrb_minseverity\n- Merge pull request [#100](https://github.com/quay/clair/issues/100) from jzelinskie/namespaces\n- Merge pull request [#96](https://github.com/quay/clair/issues/96) from jzelinskie/rootyamlkey\n- Merge pull request [#85](https://github.com/quay/clair/issues/85) from keloyang/allowHost\n- Merge pull request [#94](https://github.com/quay/clair/issues/94) from unageanu/support-docker-compose\n- Merge pull request [#82](https://github.com/quay/clair/issues/82) from liangchenye/getvulns\n- Merge pull request [#91](https://github.com/quay/clair/issues/91) from Quentin-M/fix_pprof\n- Merge pull request [#90](https://github.com/quay/clair/issues/90) from jzelinskie/README-deps\n- Merge pull request [#89](https://github.com/quay/clair/issues/89) from Quentin-M/fv_find_before_lock\n- Merge pull request [#83](https://github.com/quay/clair/issues/83) from coreos/readme-feature-namespace\n- Merge pull request [#81](https://github.com/quay/clair/issues/81) from coolljt0725/fix_readme\n- Merge pull request [#79](https://github.com/quay/clair/issues/79) from liangchenye/v1doc\n- Merge pull request [#77](https://github.com/quay/clair/issues/77) from coreos/simplify\n- Merge pull request [#76](https://github.com/quay/clair/issues/76) from coreos/sp\n- Merge pull request [#71](https://github.com/quay/clair/issues/71) from Quentin-M/sql\n- Merge pull request [#75](https://github.com/quay/clair/issues/75) from sjourdan/fix_vuln_typo\n- Merge pull request [#73](https://github.com/quay/clair/issues/73) from maxking/doc\n- Merge pull request [#74](https://github.com/quay/clair/issues/74) from mnuessler/causedByPackage\n- Merge pull request [#70](https://github.com/quay/clair/issues/70) from liangchenye/read-manifest\n- Merge pull request [#67](https://github.com/quay/clair/issues/67) from Quentin-M/master\n- Merge pull request [#65](https://github.com/quay/clair/issues/65) from jzelinskie/fixdockerfile\n- Merge pull request [#49](https://github.com/quay/clair/issues/49) from liangchenye/master\n- Merge pull request [#59](https://github.com/quay/clair/issues/59) from davidxia/patch1\n- Merge pull request [#52](https://github.com/quay/clair/issues/52) from Quentin-M/custom_notifiers\n- Merge pull request [#53](https://github.com/quay/clair/issues/53) from coreos/ubdater\n- Merge pull request [#46](https://github.com/quay/clair/issues/46) from coreos/fix_sql_tovalue\n- Merge pull request [#47](https://github.com/quay/clair/issues/47) from coreos/sn\n- Merge pull request [#51](https://github.com/quay/clair/issues/51) from coolljt0725/update_analyze_local_image_doc\n- Merge pull request [#50](https://github.com/quay/clair/issues/50) from coolljt0725/fix_stop\n- Merge pull request [#44](https://github.com/quay/clair/issues/44) from Quentin-M/configfile\n- Merge pull request [#42](https://github.com/quay/clair/issues/42) from Quentin-M/triple\n- Merge pull request [#35](https://github.com/quay/clair/issues/35) from mrqwer88/check_openvz_mirror_with_clair\n- Merge pull request [#29](https://github.com/quay/clair/issues/29) from Quentin-M/notifier_tls\n- Merge pull request [#22](https://github.com/quay/clair/issues/22) from Quentin-M/predcst\n- Merge pull request [#41](https://github.com/quay/clair/issues/41) from coreos/travisfix\n- Merge pull request [#33](https://github.com/quay/clair/issues/33) from Quentin-M/insertvulns\n- Merge pull request [#36](https://github.com/quay/clair/issues/36) from coreos/gc\n- Merge pull request [#39](https://github.com/quay/clair/issues/39) from coreos/travis\n- Merge pull request [#37](https://github.com/quay/clair/issues/37) from Quentin-M/updater_refactor\n- Merge pull request [#38](https://github.com/quay/clair/issues/38) from Quentin-M/causedby\n- Merge pull request [#26](https://github.com/quay/clair/issues/26) from stapelberg/patch-1\n- Merge pull request [#25](https://github.com/quay/clair/issues/25) from fatalbanana/patch-1\n- Merge pull request [#21](https://github.com/quay/clair/issues/21) from coreos/updatefix\n- Merge pull request [#24](https://github.com/quay/clair/issues/24) from coreos/jonboulle-patch-1\n- Merge pull request [#18](https://github.com/quay/clair/issues/18) from Quentin-M/local-analysis\n- Merge pull request [#11](https://github.com/quay/clair/issues/11) from Quentin-M/bzr_parsing\n- Merge pull request [#6](https://github.com/quay/clair/issues/6) from Quentin-M/reduce_tx\n- Merge pull request [#4](https://github.com/quay/clair/issues/4) from Quentin-M/reduce_tx\n\n\n[Unreleased]: https://github.com/quay/clair/compare/v4.9.0...HEAD\n[v4.9.0]: https://github.com/quay/clair/compare/v4.8.0...v4.9.0\n[v4.8.0]: https://github.com/quay/clair/compare/v4.7.4...v4.8.0\n[v4.7.4]: https://github.com/quay/clair/compare/v4.7.3...v4.7.4\n[v4.7.3]: https://github.com/quay/clair/compare/v4.7.2...v4.7.3\n[v4.7.2]: https://github.com/quay/clair/compare/v4.7.1...v4.7.2\n[v4.7.1]: https://github.com/quay/clair/compare/v4.7.0...v4.7.1\n[v4.7.0]: https://github.com/quay/clair/compare/v4.7.0-rc.1...v4.7.0\n[v4.7.0-rc.1]: https://github.com/quay/clair/compare/v4.6.1...v4.7.0-rc.1\n[v4.6.1]: https://github.com/quay/clair/compare/v4.6.0...v4.6.1\n[v4.6.0]: https://github.com/quay/clair/compare/v4.5.1...v4.6.0\n[v4.5.1]: https://github.com/quay/clair/compare/v4.5.0...v4.5.1\n[v4.5.0]: https://github.com/quay/clair/compare/v4.5.0-rc.0...v4.5.0\n[v4.5.0-rc.0]: https://github.com/quay/clair/compare/v4.4.4...v4.5.0-rc.0\n[v4.4.4]: https://github.com/quay/clair/compare/v4.4.3...v4.4.4\n[v4.4.3]: https://github.com/quay/clair/compare/v4.4.2...v4.4.3\n[v4.4.2]: https://github.com/quay/clair/compare/v4.4.1...v4.4.2\n[v4.4.1]: https://github.com/quay/clair/compare/v4.4.0...v4.4.1\n[v4.4.0]: https://github.com/quay/clair/compare/v4.4.0-rc.7...v4.4.0\n[v4.4.0-rc.7]: https://github.com/quay/clair/compare/v4.4.0-rc.6...v4.4.0-rc.7\n[v4.4.0-rc.6]: https://github.com/quay/clair/compare/v4.4.0-rc.5...v4.4.0-rc.6\n[v4.4.0-rc.5]: https://github.com/quay/clair/compare/v4.4.0-rc.4...v4.4.0-rc.5\n[v4.4.0-rc.4]: https://github.com/quay/clair/compare/v4.4.0-rc.3...v4.4.0-rc.4\n[v4.4.0-rc.3]: https://github.com/quay/clair/compare/v4.4.0-rc.2...v4.4.0-rc.3\n[v4.4.0-rc.2]: https://github.com/quay/clair/compare/v4.4.0-rc.1...v4.4.0-rc.2\n[v4.4.0-rc.1]: https://github.com/quay/clair/compare/v4.4.0-rc.0...v4.4.0-rc.1\n[v4.4.0-rc.0]: https://github.com/quay/clair/compare/v4.3.6...v4.4.0-rc.0\n[v4.3.6]: https://github.com/quay/clair/compare/v4.3.5...v4.3.6\n[v4.3.5]: https://github.com/quay/clair/compare/v4.3.4...v4.3.5\n[v4.3.4]: https://github.com/quay/clair/compare/v4.3.3...v4.3.4\n[v4.3.3]: https://github.com/quay/clair/compare/v4.3.2...v4.3.3\n[v4.3.2]: https://github.com/quay/clair/compare/v4.3.1...v4.3.2\n[v4.3.1]: https://github.com/quay/clair/compare/v4.3.0...v4.3.1\n[v4.3.0]: https://github.com/quay/clair/compare/v4.3.0-rc.0...v4.3.0\n[v4.3.0-rc.0]: https://github.com/quay/clair/compare/v4.2.3...v4.3.0-rc.0\n[v4.2.3]: https://github.com/quay/clair/compare/v4.2.2...v4.2.3\n[v4.2.2]: https://github.com/quay/clair/compare/v4.2.1...v4.2.2\n[v4.2.1]: https://github.com/quay/clair/compare/v4.2.0...v4.2.1\n[v4.2.0]: https://github.com/quay/clair/compare/v4.2.0-rc.2...v4.2.0\n[v4.2.0-rc.2]: https://github.com/quay/clair/compare/v4.2.0-rc.1...v4.2.0-rc.2\n[v4.2.0-rc.1]: https://github.com/quay/clair/compare/v4.1.6...v4.2.0-rc.1\n[v4.1.6]: https://github.com/quay/clair/compare/v4.1.5...v4.1.6\n[v4.1.5]: https://github.com/quay/clair/compare/v4.1.4...v4.1.5\n[v4.1.4]: https://github.com/quay/clair/compare/v4.1.3...v4.1.4\n[v4.1.3]: https://github.com/quay/clair/compare/v4.1.2...v4.1.3\n[v4.1.2]: https://github.com/quay/clair/compare/v4.1.1...v4.1.2\n[v4.1.1]: https://github.com/quay/clair/compare/v4.1.0...v4.1.1\n[v4.1.0]: https://github.com/quay/clair/compare/v4.1.0-alpha.3...v4.1.0\n[v4.1.0-alpha.3]: https://github.com/quay/clair/compare/v4.1.0-alpha.2...v4.1.0-alpha.3\n[v4.1.0-alpha.2]: https://github.com/quay/clair/compare/v4.1.0-alpha.1...v4.1.0-alpha.2\n[v4.1.0-alpha.1]: https://github.com/quay/clair/compare/v4.0.6...v4.1.0-alpha.1\n[v4.0.6]: https://github.com/quay/clair/compare/v4.0.5...v4.0.6\n[v4.0.5]: https://github.com/quay/clair/compare/v4.0.4...v4.0.5\n[v4.0.4]: https://github.com/quay/clair/compare/v4.0.3...v4.0.4\n[v4.0.3]: https://github.com/quay/clair/compare/v4.0.2...v4.0.3\n[v4.0.2]: https://github.com/quay/clair/compare/v4.0.1...v4.0.2\n[v4.0.1]: https://github.com/quay/clair/compare/v4.0.0...v4.0.1\n[v4.0.0]: https://github.com/quay/clair/compare/v4.0.0-rc.24...v4.0.0\n[v4.0.0-rc.24]: https://github.com/quay/clair/compare/v4.0.0-rc.23...v4.0.0-rc.24\n[v4.0.0-rc.23]: https://github.com/quay/clair/compare/v4.0.0-rc.22...v4.0.0-rc.23\n[v4.0.0-rc.22]: https://github.com/quay/clair/compare/v4.0.0-rc.21...v4.0.0-rc.22\n[v4.0.0-rc.21]: https://github.com/quay/clair/compare/v4.0.0-rc.20...v4.0.0-rc.21\n[v4.0.0-rc.20]: https://github.com/quay/clair/compare/v4.0.0-rc.19...v4.0.0-rc.20\n[v4.0.0-rc.19]: https://github.com/quay/clair/compare/v4.0.0-rc.18...v4.0.0-rc.19\n[v4.0.0-rc.18]: https://github.com/quay/clair/compare/v4.0.0-rc.17...v4.0.0-rc.18\n[v4.0.0-rc.17]: https://github.com/quay/clair/compare/v4.0.0-rc.16...v4.0.0-rc.17\n[v4.0.0-rc.16]: https://github.com/quay/clair/compare/v4.0.0-rc.15...v4.0.0-rc.16\n[v4.0.0-rc.15]: https://github.com/quay/clair/compare/v4.0.0-rc.14...v4.0.0-rc.15\n[v4.0.0-rc.14]: https://github.com/quay/clair/compare/v4.0.0-rc.13...v4.0.0-rc.14\n[v4.0.0-rc.13]: https://github.com/quay/clair/compare/v4.0.0-rc.12...v4.0.0-rc.13\n[v4.0.0-rc.12]: https://github.com/quay/clair/compare/v4.0.0-rc.11...v4.0.0-rc.12\n[v4.0.0-rc.11]: https://github.com/quay/clair/compare/v4.0.0-rc.10...v4.0.0-rc.11\n[v4.0.0-rc.10]: https://github.com/quay/clair/compare/v4.0.0-rc.9...v4.0.0-rc.10\n[v4.0.0-rc.9]: https://github.com/quay/clair/compare/v4.0.0-rc.8...v4.0.0-rc.9\n[v4.0.0-rc.8]: https://github.com/quay/clair/compare/v4.0.0-rc.7...v4.0.0-rc.8\n[v4.0.0-rc.7]: https://github.com/quay/clair/compare/v4.0.0-rc.6...v4.0.0-rc.7\n[v4.0.0-rc.6]: https://github.com/quay/clair/compare/v4.0.0-rc.5...v4.0.0-rc.6\n[v4.0.0-rc.5]: https://github.com/quay/clair/compare/v4.0.0-rc.4...v4.0.0-rc.5\n[v4.0.0-rc.4]: https://github.com/quay/clair/compare/v4.0.0-rc.3...v4.0.0-rc.4\n[v4.0.0-rc.3]: https://github.com/quay/clair/compare/v4.0.0-rc.2...v4.0.0-rc.3\n[v4.0.0-rc.2]: https://github.com/quay/clair/compare/v4.0.0-rc.1...v4.0.0-rc.2\n[v4.0.0-rc.1]: https://github.com/quay/clair/compare/v4.0.0-alpha.7...v4.0.0-rc.1\n[v4.0.0-alpha.7]: https://github.com/quay/clair/compare/v4.0.0-alpha.6...v4.0.0-alpha.7\n[v4.0.0-alpha.6]: https://github.com/quay/clair/compare/v4.0.0-alpha.5...v4.0.0-alpha.6\n[v4.0.0-alpha.5]: https://github.com/quay/clair/compare/v4.0.0-alpha.4...v4.0.0-alpha.5\n[v4.0.0-alpha.4]: https://github.com/quay/clair/compare/v4.0.0-alpha.3...v4.0.0-alpha.4\n[v4.0.0-alpha.3]: https://github.com/quay/clair/compare/v4.0.0-alpha.2...v4.0.0-alpha.3\n"
  },
  {
    "path": "CODEOWNERS",
    "content": "*\t@quay/clair\n"
  },
  {
    "path": "DCO",
    "content": "Developer Certificate of Origin\nVersion 1.1\n\nCopyright (C) 2004, 2006 The Linux Foundation and its contributors.\n660 York Street, Suite 102,\nSan Francisco, CA 94110 USA\n\nEveryone is permitted to copy and distribute verbatim copies of this\nlicense document, but changing it is not allowed.\n\n\nDeveloper's Certificate of Origin 1.1\n\nBy making a contribution to this project, I certify that:\n\n(a) The contribution was created in whole or in part by me and I\n    have the right to submit it under the open source license\n    indicated in the file; or\n\n(b) The contribution is based upon previous work that, to the best\n    of my knowledge, is covered under an appropriate open source\n    license and I have the right under that license to submit that\n    work with modifications, whether created in whole or in part\n    by me, under the same open source license (unless I am\n    permitted to submit under a different license), as indicated\n    in the file; or\n\n(c) The contribution was provided directly to me by some other\n    person who certified (a), (b) or (c) and I have not modified\n    it.\n\n(d) I understand and agree that this project and the contribution\n    are public and that a record of the contribution (including all\n    personal information I submit with it, including my sign-off) is\n    maintained indefinitely and may be redistributed consistent with\n    this project or the open source license(s) involved.\n"
  },
  {
    "path": "Dockerfile",
    "content": "# syntax=docker.io/docker/dockerfile:1.7\n\n# Copyright 2024 clair authors\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\nARG GOTOOLCHAIN=local\nARG GO_VERSION=1.25\nFROM --platform=$BUILDPLATFORM quay.io/projectquay/golang:${GO_VERSION} AS build\nWORKDIR /build\nRUN --mount=type=cache,target=/root/.cache/go-build \\\n\t--mount=type=cache,target=/go/pkg/mod \\\n\t--mount=type=bind,source=go.mod,target=go.mod \\\n\t--mount=type=bind,source=go.sum,target=go.sum \\\n\tgo mod download\n\nARG TARGETOS\nARG TARGETARCH\nARG TARGETVARIANT\nARG CLAIR_VERSION=\"\"\n# This script needs to use `go build` instead of `go install` to cross-compile.\nRUN --mount=type=bind,target=. \\\n\t--mount=type=cache,target=/root/.cache/go-build \\\n\t--mount=type=cache,target=/go/pkg/mod \\\n\t--network=none \\\n\t<<.\nset -e\nexport GOOS=\"$TARGETOS\" GOARCH=\"$TARGETARCH\" GOBIN=/out/bin\nif [ -n \"$TARGETVARIANT\" ]; then\n\tcase \"$TARGETARCH\" in\n\tamd64)  export GOAMD64=\"$TARGETVARIANT\" ;;\n\tppc64*) export GOPPC64=\"$TARGETVARIANT\" ;;\n\tesac\nfi\nif [ -n \"${CLAIR_VERSION}\" ]; then\n\tvstr=\"${CLAIR_VERSION} (user)\"\n\tcat <<msg >&2\nSetting reported version to \"${vstr}\".\n\nThis hook into the go build process is not needed if building using a prepared\nsource archive and will go away in the future.\n\nPlease open an issue if this would prevent a desired use-case.\nmsg\n\tvarg=\" -X 'github.com/quay/clair/v4/cmd.Version=${vstr}'\"\nfi\ninstall -d \"${GOBIN}\"\ngo build \\\n\t-ldflags=\"-s -w$varg\" -trimpath \\\n\t-o \"${GOBIN}\" \\\n\t./cmd/...\n.\n\nFROM scratch AS ctl\nCOPY --from=build /out/bin/clairctl* /\n\nFROM registry.access.redhat.com/ubi8/ubi-minimal AS final\nENTRYPOINT [\"/usr/bin/clair\"]\nVOLUME /config\nEXPOSE 6060\nENV CLAIR_CONF=/config/config.yaml\\\n\tCLAIR_MODE=combo\\\n\tSSL_CERT_DIR=\"/etc/ssl/certs:/etc/pki/tls/certs:/var/run/certs\"\nUSER nobody:nobody\n# The WORKDIR command creates an empty layer, there's nothing we can do.\nWORKDIR /run\nCOPY --from=build /out/bin/* /usr/bin/\n"
  },
  {
    "path": "Documentation/SUMMARY.md",
    "content": "# Summary\n\n- [What is ClairV4](./whatis.md)\n- [How Tos](./howto.md)\n  - [Getting Started With ClairV4](./howto/getting_started.md)\n  - [Deployment Models](./howto/deployment.md)\n  - [API Definition](./howto/api.md)\n  - [Testing ClairV4](./howto/testing.md)\n- [Concepts](./concepts.md)\n  - [Indexing](./concepts/indexing.md)\n  - [Matching](./concepts/matching.md)\n  - [Internal API](./concepts/api_internal.md)\n  - [Authentication](./concepts/authentication.md)\n  - [Notifications](./concepts/notifications.md)\n  - [Updaters and Airgap](./concepts/updatersandairgap.md)\n  - [Operation](./concepts/operation.md)\n- [Contribution](./contribution.md)\n  - [Building](./contribution/building.md)\n  - [Commit Style](./contribution/commit_style.md)\n  - [Releases](./contribution/releases.md)\n  - [OpenAPI](./contribution/openapi.md)\n- [Reference](./reference.md)\n  - [Api](./reference/api.md)\n  - [Clairctl](./reference/clairctl.md)\n  - [Config](./reference/config.md)\n  - [Indexer](./reference/indexer.md)\n  - [Matcher](./reference/matcher.md)\n  - [Notifier](./reference/notifier.md)\n  - [Metrics](./reference/metrics.md)\n"
  },
  {
    "path": "Documentation/concepts/api_internal.md",
    "content": "# Internal\n\nInternal endpoints are underneath `/api/v1/internal` and are meant for\ncommunication between Clair microservices. If Clair is operating in combo mode,\nthese endpoints may not exist. Any sort of API ingress should disallow clients\nto talk to these endpoints.\n\nWe do not formally expose these APIs in our OpenAPI spec. \nFurther information and usage is an effort left to the reader.\n\n## Updates Diffs\n\nThe `update_diff/` endpoint exposes the api for diffing two update operations. \nThis is used by the notifier to determine the added and removed vulnerabilities on security database update.\n\n## Update Operation\n\nThe `update_operation` endpoint exposes the api for viewing updaters' activity. \nThis is used by the notifier to determine if new updates have occurred and triggers an update diff to see what has changed.\n\n## AffectedManifest\n\nThe `affected_manifest` endpoint exposes the api for retreiving affected manifests given a list of Vulnerabilities.\nThis is used by the notifier to determine the manifests that need to have a notification generated.\n"
  },
  {
    "path": "Documentation/concepts/authentication.md",
    "content": "# Authentication\n\nPrevious versions of Clair used [jwtproxy] to gate authentication. For ease of\nbuilding and deployment, v4 handles authentication itself.\n\nAuthentication is configured by specifying configuration objects underneath the\n`auth` key of the configuration. Multiple authentication configurations may be\npresent, but they will be used preferentially in the order laid out below.\n\n[jwtproxy]: https://github.com/quay/jwtproxy\n\n### PSK\n\nClair implements JWT-based authentication using a pre-shared key.\n\n#### Configuration\n\nThe `auth` stanza of the configuration file requires two parameters: `iss`, which\nis the issuer to validate on all incoming requests; and `key`, which is a base64\nencoded symmetric key for validating the requests.\n\n```yaml\nauth:\n  psk:\n    key: >-\n      MDQ4ODBlNDAtNDc0ZC00MWUxLThhMzAtOTk0MzEwMGQwYTMxCg==\n    iss: 'issuer'\n```\n\n"
  },
  {
    "path": "Documentation/concepts/indexing.md",
    "content": "# Indexing\n\nThe [Indexer](../reference/indexer.md) service is responsble for \"indexing a manifest\".\n\nIndexing involves taking a manifest representing a container image and computing its constituent parts. The indexer is trying to discover what packages exist in the image, what distribution the image is derived from, and what package repositories are used within the image. Once this information is computed it is persisted in an IndexReport.\n\nThe IndexReport is an intermediate data structure describing the contents of a container image. This report can be fed to a [Matcher](../reference/matcher.md) node for vulnerability analysis.\n\n## Content Addressability\n\nClairV4 treats all manifests and layers as [content addressable](https://en.wikipedia.org/wiki/Content-addressable_storage). In the context of ClairV4 this means once we index a specific manifest we will not index it again unless it's required, and likewise with individual layers. This allows a large reduction in work. \n\nFor example, consider how many images in a registry may use \"ubuntu:artful\" as a base layer. It could be a large majority of images if the developers prefer basing their images off ubuntu. Treating the layers and manifests as content addressable means we will only fetch and scan the base layer once.\n\nThere are of course conditions where ClairV4 should re-index a manifest. \n\nWhen an internal component such as a package scanner is updated, Clair will know to perform the scan with the new package scanner. Clair has enough information to determine that a component has changed and the IndexReport may be different this time around. \n\nA client can track ClairV4's `index_state` endpoint to understand when an internal component has changed and subsequently issue re-indexes. See our [api](../howto/api.md) guide to learn how to view our api specification.\n\n## Summary\n\nIn summary, you should understand that Indexing is the process Clair uses to understand the contents of layers.\n\nFor a more indepth look at indexing check out the [ClairCore Documentation](https://quay.github.io/claircore/)\n"
  },
  {
    "path": "Documentation/concepts/matching.md",
    "content": "# Matching\n\nA [Matcher](../reference/matcher.md) node is responsible for matching vulnerabilities to a provided IndexReport. \n\nMatchers by default are also responsible for keeping the database of vulnerabilities up to date. Matchers will typically run a set of Updaters which periodically probe their data sources for new contents, storing new vulnerabilities in the database when discovered.\n\nThe matcher API is designed to be called often and will always provide the most up-to-date VulnerabilityReport when queried. This VulnerabilityReport summaries both a manifest's contents and any vulnerabilities affecting the contents.\n\nSee our [api](../howto/api.md) guide to learn how to view our api specification and work with the Matcher api.\n\n# Remote Matching\n\nA remote matcher behaves similarly to a matcher, except that it uses api calls to fetch vulnerability data for a provided IndexReport.\nRemote matchers are useful when it is not possible to persist data from a given source into the database.\n\nThe `crda` remote matcher is responsible for fetching vulnerabilities from Red Hat Code Ready Dependency Analytics (CRDA).\nBy default, this matcher serves 100 requests per minute.\nThe rate-limiting can be lifted by requesting a dedicated API key, which is done via [this form][CRDA-Request-Form].\n\n[CRDA-Request-Form]: https://developers.redhat.com/content-gateway/link/3872178\n\n## Summary\n\nIn summary you should understand that a Matcher node provides vulnerability reports given the output of an Indexing process. By default it will also run background Updaters keeping the vulnerability database up-to-date.\n\nFor a more indepth look at indexing check out the [ClairCore Documentation](https://quay.github.io/claircore/)\n"
  },
  {
    "path": "Documentation/concepts/notifications.md",
    "content": "# Notifications\n\nClairV4 implements a notification system.\n\nThe notifier service will keep track of new security database updates and inform an interested client if new or removed vulnerabilities affect an indexed manifest.\n\nThe interested client can subscribe to notifications via several mechanisms:\n* Webhook delivery\n* AMQP delivery\n* STOMP delivery\n\nConfiguring the notifier is done via the yaml configuration.\n\nSee the \"Notifier\" object in our [config reference](../reference/config.md)\n\n## A Notification\n\nWhen the notifier becomes aware of new vulnerabilities affecting a previously indexed manifest, it will use the configured method(s) to issue notifications about the new changes. Any given notification expresses the **most severe** vulnerability discovered because of the change. This avoids creating a flurry of notifications for the same security database update.\n\nOnce a client receives a notification, it should issue a new request against the [matcher](../reference/matcher.md) to receive an up-to-date vulnerability report.\n\nThe notification schema is the JSON marshaled form of the following types:\n\n```go\n// Reason indicates the catalyst for a notification\ntype Reason string\nconst (\n\tAdded   Reason = \"added\"\n\tRemoved Reason = \"removed\"\n\tChanged Reason = \"changed\"\n)\ntype Notification struct {\n\tID            uuid.UUID        `json:\"id\"`\n\tManifest      claircore.Digest `json:\"manifest\"`\n\tReason        Reason           `json:\"reason\"`\n\tVulnerability VulnSummary      `json:\"vulnerability\"`\n}\ntype VulnSummary struct {\n\tName           string                  `json:\"name\"`\n\tDescription    string                  `json:\"description\"`\n\tPackage        *claircore.Package      `json:\"package,omitempty\"`\n\tDistribution   *claircore.Distribution `json:\"distribution,omitempty\"`\n\tRepo           *claircore.Repository   `json:\"repo,omitempty\"`\n\tSeverity       string                  `json:\"severity\"`\n\tFixedInVersion string                  `json:\"fixed_in_version\"`\n\tLinks          string                  `json:\"links\"`\n}\n```\n\n## Webhook Delivery\n*See the \"Notifier.Webhook\" object in the [config reference](../reference/config.md) for complete configuration details.*\n\nWhen you configure notifier for webhook delivery you provide the service with the following pieces of information:\n* A target URL where the webhook will fire\n* The callback URL where the notifier may be reached including its API path\n    * e.g. \"http://clair-notifier/notifier/api/v1/notification\"\n\nWhen the notifier has determined an updated security database has changed the affected status of an indexed manifest, it will deliver the following JSON body to the configured target:\n```json\n{\n  \"notification_id\": {uuid_string},\n  \"callback\": {url_to_notifications}\n}\n```\n\nOn receipt, the server can immediately browse to the URL provided in the callback field.\n\n### Pagination\n\nThe URL returned in the callback field brings the client to a paginated result.\n\nThe callback endpoint specification follows:\n\n```go\nGET /notifier/api/v1/notification/{id}?[page_size=N][next=N]\n{\n  page: {\n    size:    int,      // maximum number of notifications in the response\n    next:   string, //  if present, the next id to fetch.\n  }\n  notifications: [ Notification… ] // array of notifications; max len == page.size\n}\n```\nThe GET callback request implements a simple bare-minimum paging mechanism.\n\nThe \"page_size\" url param controls how many notifications are returned in a single page.\nIf not provided a default of 500 is used.\n\nThe \"next\" url param informs Clair the next set of paged notifications to return. If not provided the 0th page is assumed.\n\nA page object accompanying the notification list specifies \"next\" and \"size\" fields.\n\nThe \"next\" field returned in the page must be provided as the subsequent request's \"next\" url parameter to retrieve the next set of notifications.\n\nThe \"size\" field will simply echo back the request page_size parameter.\n\nWhen the final page is served to the client the returned \"page\" data structure will not contain a \"next\" member.\n\nTherefore the following loop is valid for obtaining all notifications for a notification id in pages of a specified size.\n\n```\n{ page, notifications } = http.Get(\"http://clairv4/notifier/api/v1/notification/{id}?page_size=1000\")\n\nwhile (page.Next != None) {\n    { page, notifications } = http.Get(\"http://clairv4/notifier/api/v1/notification/{id}?next={page.Next},page_size=1000\")\n}\n```\n\n*Note: If the client specifies a custom page_size it must specify this page_size on every request for accurate responses.*\n\n### Deleting Notifications\n\nWhile not mandatory, the client may issue a delete of the notification via a DELETE method. See [api](../howto/api.md) to view the delete api.\n\nDeleting a notification ID will clean up resources in the notifier quicker. Otherwise the notifier will wait a predetermined length of time before clearing delivered notifications from its database.\n\n## AMQP Delivery\n*See the \"Notifier.AMQP\" object in our [config reference](../reference/config.md) for complete configuration details.*\n\nThe notifier also supports delivering to an AMQP broker. With AMQP delivery you can control whether a callback is delivered to the broker or whether notifications are directly delivered to the queue.\n\nThis allows the developer of the AMQP consumer to determine the logic of notification processing.\n\nNote that AMQP delivery only supports AMQP 0.x protocol (e.g. RabbitMQ). If you need to publish notifications on AMQP 1.x message queue (e.g. ActiveMQ), you can use STOMP delivery.\n\n### Direct Delivery\n\nIf the notifier's configuration specifies `direct: true` for AMQP, notifications will be delivered directly to the configured exchange.\n\nWhen `direct` is set, the `rollup` property may be set to instruct the notifier to send a max number of notifications in a single AMQP message. This allows a balance between size of the message and number of messages delivered to the queue.\n\n## Testing and Development\n\nThe notifier has a testing mode enabled when it sees the \"NOTIFIER_TEST_MODE\" environment variable set. It can be set to any value as we only check to see if it exists.\n\nWhen this environment variable is set, the notifier will begin sending fake notifications to the configured delivery mechanism every \"poll_interval\" interval. This provides an easy way to implement and test new or existing deliverers.\n\nThe notifier will run in this mode until the environment variable is cleared and the service is restarted.\n"
  },
  {
    "path": "Documentation/concepts/operation.md",
    "content": "# Operation\n"
  },
  {
    "path": "Documentation/concepts/updatersandairgap.md",
    "content": "## Updaters\n\nClair utilizes go packages we call \"updaters\" that encapsulate the logic of\nfetching and parsing different vulnerability databases. Updaters are usually\npared with a matcher to interpret if and how any vulnerability is related to a\npackage.\n\nOperators may wish to update the vulnerability database less frequently or not\nimport vulnerabilities from databases that they know will not be used.\n\n### Configuration\n\nUpdaters can be configured by `updaters` key at the top of the configuration. If\nupdaters are being run automatically within the matcher processes, as is the\ndefault, the period for running updaters is configured under the matcher's\nconfiguration stanza.\n\n#### Choosing Sets\n\nSpecific sets of updaters can be selected by the `sets` list. If not present,\nthe defaults of all upstream updaters will be used.\n\n```yaml\nupdaters:\n  sets:\n    - rhel\n```\n\n#### Specific Updaters\n\nConfiguration for specific updaters can be passed by putting a key underneath\nthe `config` member of the `updaters` object. The name of an updater may be\nconstructed dynamically; users should examine logs to double-check names.\nThe specific object that an updater expects should be covered in the updater's\ndocumentation.\n\nFor example, to have the \"rhel\" updater fetch a manifest from a different\nlocation:\n\n```yaml\nupdaters:\n  config:\n    rhel:\n      url: https://example.com/mirror/oval/PULP_MANIFEST\n```\n\n### Airgap\n\nFor additional flexibility, Clair supports running updaters in a different\nenvironment and importing the results. This is aimed at supporting installations\nthat disallow the Clair cluster from talking to the Internet directly. An update\nprocedure needs to arrange to call the relevant `clairctl` command in an\nenvironment with access to the Internet, move the resulting artifact across the\nairgap according to site policy, and then call the relevant `clairctl` command\nto import the updates.\n\nFor example:\n\n```sh\n# On a workstation, run:\nclairctl export-updaters updates.json.gz\n```\n\n```sh\n# Move the resulting file to a place reachable by the cluster:\nscp updates.json.gz internal-webserver:/var/www/\n```\n\n```sh\n# On a pod inside the cluster, import the file:\nclairctl import-updaters http://web.svc/updates.json.gz\n```\n\nNote that a configuration file is needed to run these commands.\n\n#### Configuration\n\nMatcher processes should have the `disable_updaters` key set to disable\nautomatic updaters running.\n\n```yaml\nmatcher:\n  disable_updaters: true\n```\n\n## Indexers\n\n### Configuration\n\n#### Airgap\n\n```yaml\nindexer:\n  airgap: true\n```\n\n#### Specific Scanners\n\n```yaml\nindexer:\n  scanner:\n    package:\n      name:\n        key: value\n    repo:\n      name:\n        key: value\n    dist:\n      name:\n        key: value\n```\n\n"
  },
  {
    "path": "Documentation/concepts.md",
    "content": "# Concepts\n\nThe following sections give a conceptual overview of how Clair works internally.\n\n- [Internal API](./concepts/api_internal.md)\n- [Authentication](./concepts/authentication.md)\n- [Notifications](./concepts/notifications.md)\n- [Updaters and Airgap](./concepts/updatersandairgap.md)\n"
  },
  {
    "path": "Documentation/contribution/building.md",
    "content": "# Building\n\nThis repo is intended to be built with familiar `go build` or `go install` invocations.\nAll binaries (excepting debugging tools) are underneath the `cmd` directory.\n\n### Cross-compiling\n\nCurrently Clair does not have any cgo dependencies, so there should not be any cross-compilation concerns.\n\n## Container\n\nA `Dockerfile` for the project is in the repo root.\n**The only upstream-supported means of using it is Buildkit via `buildctl`.**\nSee the `container`, `container-build`, `dist-container`, and `dist-clairctl` make targets for example invocations.\nThe `BUILDKIT_HOST` environment variable may need to be set, depending on how `buildkitd` is running in one's environment.\n"
  },
  {
    "path": "Documentation/contribution/commit_style.md",
    "content": "# Commit Style\n\nThe Clair project utilizes well structured commits to keep the history useful and help with release automation.\nWe suggest signing off on your commits as well.\n\nA typical commit will take on the following structure:\n\n```\n<scope>: <subject>\n\n<body>\nFixes #1\nPull Request #2\n\nSigned-Off By: <email>\n```\n\nThe header of the commit is regexp checked before commit and your commit will be kicked back if it does not conform.\n\n## Scope\n\nThis is the section of code this commit influences. \n\nYou will often see scopes such as \"notifier\", \"auth\", \"chore\", \"cicd\".\n\nWe use this field to group commits by scope in our automated changelog generation.\n\nIt would be wise to take a look at our changelog before contributing to get an idea of the common scopes we use.\n\n## Subject\n\nSubject is a short and concise summary of the change the commit is introducing. It should be a sentence fragment without starting capitalization and ending punctuation and limited to about 60 characters, to allow for the scope prefix and decoration in the git log.\n\n## Body\n\nBody should be full of detail.\n\nExplain what this commit is doing and why it is necessary.\n\nYou may include references to issues and pull requests as well. Our automated changelog process will discover references prefixed with \"Fixes\", \"Closed\" and \"Pull Request\"\n\n"
  },
  {
    "path": "Documentation/contribution/openapi.md",
    "content": "# OpenAPI\n\nThe [OpenAPI specification] has moved from `openapi.yaml` to the `openapi.json`\nand `openapi.yaml` files in `httptransport/api/v1`.\n\nThese files are autogenerated from files in `httptransport/api` and\n`httptransport/types` via the `httptransport/api/openapi.zsh` script. This\nscript requires `zsh` to run and `sha256sum`, `git`, `jq`, `yq`, and `npx`\ncommands in `PATH`.\n\n## Modifications\n\nTo modify the OpenAPI spec, edit the relevant `httptransport/api/*/openapi.jq`\nfile (a [`jq`] script) or the `httptransport/api/lib/oapi.jq` library as needed,\nthen run `go generate ./httptransport`.\n\nThe `go generate` command also needs to be run if files in `httptransport/types`\nare modified.\n\n## Script\n\nThe `openapi.zsh` script works by:\n- for each `v*` directory under `httptransport/api`:\n  - for all [JSON Schema] files in the corresponding `httptransport/types/v*` directory:\n    - lint the schema\n    - slip-steam examples from the corresponding `examples` file\n  - amalgamate all the files from the previous step\n  - run the `openapi.jq` script with `null` input\n  - merge the outputs of the previous two steps\n  - ~~validate and lint the OpenAPI spec~~[^note]\n  - generate a `yaml` representation\n  - write out a `sha256` checksum in [Etag] format\n\n[^note]: The OpenAPI spec _should_ also be validated or linted, but there's not a known tool that handles JSON Schema [reference resolution] correctly.\n\n[OpenAPI specification]: https://spec.openapis.org/oas/v3.1.0.html\n[`jq`]: https://jqlang.org/manual/\n[JSON Schema]: https://json-schema.org/\n[Etag]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/ETag\n[reference resolution]: https://www.learnjsonschema.com/2020-12/core/ref/\n"
  },
  {
    "path": "Documentation/contribution/releases.md",
    "content": "# Releases\n\nClair releases are cut roughly every three months and actively maintained for\nsix.\n\nThis means that bugfixes should be landed on `master` (if applicable) and then\nmarked for backporting to a minor version's release branch. The process for\ndoing this is not yet formalized.\n\n## Process\n\n### Minor\n\nWhen cutting a new minor release, two things need to be done: creating a tag and\ncreating a release branch. This can be done like so:\n\n```sh\ngit tag -as v4.x.0 HEAD\ngit push upstream HEAD:release-4.x tag v4.x.0\n```\n\nThen, a \"release\" needs to be created in the Github UI using the created tag.\n\n### Patch\n\nA patch release is just like a minor release with the caveat that minor version\ntags should *only* appear on release branches and a new branch does not need to\nbe created.\n\n```sh\ngit checkout release-4.x\ngit tag -as v4.x.1 HEAD\ngit push upstream tag v4.x.1\n```\n\nThen, a \"release\" needs to be created in the Github UI using the created tag.\n\n### Creating Artifacts\n\nClair's artifact release process is automated and driven off the releases in\nGithub.\n\nPublishing a new release in the Github UI automatically triggers the creation of\na complete source archive and a container. The archive is attached to the\nrelease, and the container is pushed to the\n[`quay.io/projectquay/clair`](https://quay.io/repository/projectquay/clair)\nrepository.\n\nThis is all powered by a Github Action in `.github/workflows/cut-release.yml`.\n\nA complete source archive can be created with `make dist`.\nA corresponding container can be created with `make dist-container`.\nSee `etc/config.mk` for documentation on variables that control these targets.\n"
  },
  {
    "path": "Documentation/contribution.md",
    "content": "# Contribuion\nThe following sections provides information on how to contribute to Clair.\n\n- [Building](./contribution/building.md)\n- [Commit Style](./contribution/commit_style.md)\n- [Releases](./contribution/releases.md)\n- [OpenAPI](./contribution/openapi.md)\n"
  },
  {
    "path": "Documentation/howto/api.md",
    "content": "# API Definition\n\nClair provides its API definition via an OpenAPI specification. You can view our OpenAPI spec [here][openapi_v1].\n\nThe OpenAPI spec can be used in a variety of ways.\n* Generating http clients for your application\n* Validating data returned from Clair\n* Importing into a rest client such as [Postman](https://learning.postman.com/docs/integrations/available-integrations/working-with-openAPI/)\n* API documentation via [Swagger Editor](https://petstore.swagger.io/#/)\n\nSee [Testing Clair](./testing.md) to learn how the local dev tooling starts a local swagger editor. This is handy for making changes to the spec in real time.\n\nSee [API Reference](../reference/api.md) for a markdown rendered API reference.\n\n[openapi_v1]: https://github.com/quay/clair/tree/main/httptransport/api/v1\n"
  },
  {
    "path": "Documentation/howto/deployment.md",
    "content": "# Deploying Clair\n\nClair v4 was designed with flexible deployment architectures in mind.\nAn operator is free to choose a deployment model which scales to their use cases.\n\n## Configuration\n\nBefore jumping directly into the models, its important to note that Clair is designed to use a single configuration file across all node types.\nThis design decision makes it very easy to deploy on systems like Kubernetes and OpenShift.\n\nSee [Config Reference](../reference/config.md)\n\n## Combined Deployment\n\nIn a combined deployment, all the Clair processes run in a single OS process.\nThis is by far the easiest deployment model to configure as it involves the least moving parts. \n\nA load balancer is still recommended if you plan on performing TLS termination.\nTypically this will be a OpenShift route or a Kubernetes ingress.\n\n![combo mode single db deployment diagran](./clairv4_combo_single_db.png)\n\nIn the above diagram, Clair is running in combo mode and talking to a single database.\nTo configure this mode, you will provide all node types the same database and start Clair in **combo** mode.\n\n```\n...\nindexer:\n    connstring: \"host=clairdb user=pqgotest dbname=pqgotest sslmode=verify-full\"\nmatcher:\n    connstring: \"host=clairdb user=pqgotest dbname=pqgotest sslmode=verify-full\"\n    ...\nnotifier:\n    connstring: \"host=clairdb user=pqgotest dbname=pqgotest sslmode=verify-full\"\n    ...\n```\nIn this mode, any configuration informing Clair how to talk to other nodes is ignored;\nit is not needed as all intra-process communication is done directly.\n\nFor added flexibility, it's also supported to split the databases while in combo mode.\n\n![combo mode multiple db deployment diagran](./clairv4_combo_multi_db.png)\n\nIn the above diagram, Clair is running in combo mode but database load is split between multiple databases.\nSince Clair is conceptually a set of micro-services, its processes do not share database tables even when combined into the same OS process.\n\nTo configure this mode, you would provide each process its own \"connstring\" in the configuration. \n```\n...\nindexer:\n    connstring: \"host=indexer-clairdb user=pqgotest dbname=pqgotest sslmode=verify-full\"\nmatcher:\n    connstring: \"host=matcher-clairdb user=pqgotest dbname=pqgotest sslmode=verify-full\"\n    ...\nnotifier:\n    connstring: \"host=notifier-clairdb user=pqgotest dbname=pqgotest sslmode=verify-full\"\n    ...\n```\n\n## Distributed Deployment\n\nIf your application needs to asymmetrically scale or you expect high load you may want to consider a distributed deployment.\n\nIn a distributed deployment, each Clair process runs in its own OS process.\nTypically this will be a Kubernetes or OpenShift Pod.\n\nA load balancer **must** be setup in this deployment model.\nThe load balancer will route traffic between Clair nodes along with routing API requests via [path based routing] to the correct services.\nIn a Kubernetes or OpenShift deployment this is usually handled with the `Service` and `Route` abstractions.\nIf deploying on bare metal, a load balancer will need to be configured appropriately. \n\n\n![distributed mode multiple db deployment diagran](./clairv4_distributed_multi_db.png)\n\nIn the above diagram, a load balancer is configured to route traffic coming from the client to the correct service.\nThis routing is path based and requires a layer 7 load balancer.\nTraefik, Nginx, and HAProxy are all capable of this.\nAs mentioned above, this functionality is native to OpenShift and Kubernetes.\n\nIn this configuration, you'd supply each process with database connection strings and addresses for their dependent services.\nEach OS process will need to have its \"mode\" CLI flag or environment variable set to the appropriate value. \nSee [Config Reference](../reference/config.md)\n\n```\n...\nindexer:\n    connstring: \"host=indexer-clairdb user=pqgotest dbname=pqgotest sslmode=verify-full\"\nmatcher:\n    connstring: \"host=matcher-clairdb user=pqgotest dbname=pqgotest sslmode=verify-full\"\n    indexer_addr: \"indexer-service\"\n    ...\nnotifier:\n    connstring: \"host=notifier-clairdb user=pqgotest dbname=pqgotest sslmode=verify-full\"\n    indexer_addr: \"indexer-service\"\n    matcher_addr: \"matcher-service\"\n    ...\n```\n\nKeep in mind a config file per process is not need.\nProcesses only use the values necessary for their configured mode.\n\n## TLS Termination\n\nIt's recommended to offload TLS termination to the load balancing infrastructure.\nThis design choice is due to the ubiquity of Kubernetes and OpenShift infrastructure already providing this facility.\n\nIf this is not possible for some reason, it is possible to have processes terminate TLS by using the `$.tls` configuration key.\nA load balancer is still required.\n\n## Disk Usage Considerations\n\nBy default, Clair will store container layers in `/var/tmp` while in use.\nThis can be changed by setting the `TMPDIR` environment variable.\nThere's currently no way to change this in the configuration file.\n\nThe disk space needed depends on the precise layers being indexed at any one time,\nbut a good approximation is twice as large as the largest (uncompressed size) layer in the corpus.\n\n## More On Path Routing\n\nIf you are considering a distributed deployment you will need more details on [path based routing]. \n\nLearn how to grab our OpenAPI spec [here](./api.md) and either start up a local dev instance of the swagger editor or load the spec file into the [online editor](https://petstore.swagger.io/#/).\n\nYou will notice particular API paths are grouped by the services which implement them.\nThis is your guide to configure your layer 7 load balancer correctly. \n\nWhen the load balancer encounters a particular path prefix it must send those request to the correct set of Clair nodes. \n\nFor example, this is how we configure Traefik in our local development environment:\n```\n- \"traefik.enable=true\"\n- \"traefik.http.routers.notifier.entrypoints=clair\"\n- \"traefik.http.routers.notifier.rule=PathPrefix(`/notifier`)\"\n- \"traefik.http.routers.notifier.service=notifier\"\n- \"traefik.http.services.notifier.loadbalancer.server.port=6000\"\n```\n\nThis configuration is saying \"take any paths prefixes of /notifier/ and send them to the notifier services on port 6000\".\n\nEvery load balancer will have their own way to perform path routing.\nCheck the documentation for your infrastructure of choice.\n\n[path based routing]: https://devcentral.f5.com/s/articles/the-three-http-routing-patterns-you-should-know-30764\n"
  },
  {
    "path": "Documentation/howto/getting_started.md",
    "content": "# Getting Started With Clair\n\n## Releases\n\nAll of the source code needed to build clair is packaged as an archive and\nattached to the release. Releases are tracked at the [github releases].\n\nThe release artifacts also include the clairctl command line tool.\n\n[github releases]: https://github.com/quay/clair/releases\n\n## Official Containers\n\nClair is officially packaged and released as a container at\n[quay.io/projectquay/clair]. The `latest` tag tracks the git development branch,\nand version tags are built from the corresponding release.\n\n[quay.io/projectquay/clair]: https://quay.io/repository/projectquay/clair\n\n## Running Clair\n\nThe easiest way to get Clair up and running for test purposes is to use our [local dev environment](./testing.md)\n\nIf you're the hands on type who wants to get into the details however, continue reading.\n\n## Modes\n\nClair can run in several modes. [Indexer](../reference/indexer.md), [matcher](../reference/matcher.md), [notifier](../reference/notifier.md) or combo mode. In combo mode, everything runs in a single OS process. \n\nIf you are just starting with Clair you will most likely want to start with combo mode and venture out to a distributed deployment once acquainted. \n\nThis how-to will demonstrate combo mode and introduce some further reading on a distributed deployment.\n\n## Postgres\n\nClair uses PostgreSQL for its data persistence. Migrations are supported so you should only need to point Clair to a fresh database and have it do the setup for you.\n\nWe will assume you have setup a postgres database and it's reachable with the following connection string:\n`host=clair-db port=5432 user=clair dbname=clair sslmode=disable`. Adjust for your environment accordingly. \n\n## Starting Clair In Combo Mode\n\nAt this point, you should either have built Clair from source or have pulled the container. In either case, we will assume that the `clair-db` hostname will resolve to your postgres database. \n\n*You may need to configure [docker](https://docs.docker.com/network/) or [podman](https://podman.io/getting-started/network.html) networking if you are utilizing containers. This is out of scope for this how-to.*\n\nA basic config for combo mode can be found [here](https://github.com/quay/clair/blob/main/config.yaml.sample). Make sure to edit this config with your database settings and set \"migrations\" to `true` for all mode stanzas. In this basic combo mode, all \"connstring\" fields should point to the same database and any *_addr fields are simply ignored. For more details see the [config reference](../reference/config.md) and [deployment models](./deployment.md)\n\nClair has 3 requirements to start:\n* The `mode` flag or `CLAIR_MODE` environment variable specifying what mode this instance will run in.\n* The `conf` flag or `CLAIR_CONF` environment variable specifying where Clair can find its configuration.\n* A yaml document providing Clair's configuration.\n\nIf you are running a container, you can [mount](https://docs.docker.com/storage/volumes/) a Clair config and set the `CLAIR_CONF` environment variable to the corresponding path.\n```\nCLAIR_MODE=combo\nCLAIR_CONF=/path/to/mounted/config.yaml\n```\n\nIf you are running a Clair binary directly, its likely easiest to use the command line.\n```\nclair -conf \"path/to/config.yaml\" -mode \"combo\"\n```\n\n## Submitting A Manifest\n\nThe simplest way to submit a manifest to your running Clair is utilizing [clairctl](../reference/clairctl.md). This is a CLI tool capable of grabbing image manifests from public repositories and submitting them for analysis. \nThe command will be in the Clair container, but can also be installed locally by running the following command:\n```\ngo install github.com/quay/clair/v4/cmd/clairctl@latest\n```\n\nYou can submit a manifest to ClairV4 via the following command.\n```shell\n$ clairctl report --host ${net_address_of_clair} ${image_tag}\n```\nYou will need to add the `config` flag if you are using a PSK authentication (as in the [local dev environment](./testing.md) setup, for example).\n```shell\n$ clairctl report --config local-dev/clair/config.yaml --host ${net_address_of_clair} ${image_tag}\n```\nBy default, `clairctl` will look for Clair at `localhost:6060` or the environment variable `CLAIR_API`, and for a configuration at `config.yaml` or the environment variable `CLAIR_CONF`.\n\nIf everything is configured correctly, you should see some output like the following informing you of vulnerabilities affecting the supplied image.\n\n```shell\n$ clairctl report ubuntu:focal\nubuntu:focal found bash        5.0-6ubuntu1.1         CVE-2019-18276\nubuntu:focal found libpcre3    2:8.39-12build1        CVE-2017-11164\nubuntu:focal found libpcre3    2:8.39-12build1        CVE-2019-20838\nubuntu:focal found libpcre3    2:8.39-12build1        CVE-2020-14155\nubuntu:focal found libsystemd0 245.4-4ubuntu3.2       CVE-2018-20839\nubuntu:focal found libsystemd0 245.4-4ubuntu3.2       CVE-2020-13776\nubuntu:focal found libtasn1-6  4.16.0-2               CVE-2018-1000654\nubuntu:focal found libudev1    245.4-4ubuntu3.2       CVE-2018-20839\nubuntu:focal found libudev1    245.4-4ubuntu3.2       CVE-2020-13776\nubuntu:focal found login       1:4.8.1-1ubuntu5.20.04 CVE-2013-4235\nubuntu:focal found login       1:4.8.1-1ubuntu5.20.04 CVE-2018-7169\nubuntu:focal found coreutils   8.30-3ubuntu2          CVE-2016-2781\nubuntu:focal found passwd      1:4.8.1-1ubuntu5.20.04 CVE-2013-4235\nubuntu:focal found passwd      1:4.8.1-1ubuntu5.20.04 CVE-2018-7169\nubuntu:focal found perl-base   5.30.0-9build1         CVE-2020-10543\nubuntu:focal found perl-base   5.30.0-9build1         CVE-2020-10878\nubuntu:focal found perl-base   5.30.0-9build1         CVE-2020-12723\nubuntu:focal found tar         1.30+dfsg-7            CVE-2019-9923\nubuntu:focal found dpkg        1.19.7ubuntu3          CVE-2017-8283\nubuntu:focal found gpgv        2.2.19-3ubuntu2        CVE-2019-13050\nubuntu:focal found libc-bin    2.31-0ubuntu9          CVE-2016-10228\nubuntu:focal found libc-bin    2.31-0ubuntu9          CVE-2020-6096\nubuntu:focal found libc6       2.31-0ubuntu9          CVE-2016-10228\nubuntu:focal found libc6       2.31-0ubuntu9          CVE-2020-6096\nubuntu:focal found libgcrypt20 1.8.5-5ubuntu1         CVE-2019-12904\n```\n\nTo test locally-built images, you'll need to push them to a registry that is accessible by the Clair service and the `clairctl` command.\nA local registry can be used for this, but the specifics of configuration vary by registry and container runtime.\nConsult the relevant documentation for more information.\n\n## What's Next\n\nNow that you see the basic usage of Clair, you can checkout our [deployment models](./deployment.md) to learn different ways of deploying.\n\nYou may also be curious about how `clairctl` did that work. Check out our [API definition](./api.md) to understand how an application interacts with Clair.\n"
  },
  {
    "path": "Documentation/howto/testing.md",
    "content": "# Testing Clair\n\nWe provide dev tooling in order to quickly get a fully configured Clair and Quay environment stood up locally.\nThis environment can be used to test and develop Clair's Quay integration.\n\n## Requirements\n\n### Make\n\nMake is used to stand up the local dev environment.\nMake is readily available in just about every package manager you can think of.\nIt's very likely your workstation already has make on it.\n\n### Podman/Docker and Docker Compose\n\nCurrently our local dev tooling is supported by docker and docker-compose.\nPodman should work fine since v3.0.\n\nDocker version 19.03.11 and docker-compose version 1.28.6 are confirmed working.\nOur assumption is most recent versions will not have an issue running the local dev tooling.\n\nSee [Get Started with Podman](https://podman.io/get-started).\n\n### Go Toolchain\n\nGo 1.20 or higher is required.\n\nSee [Install Golang](https://golang.org/doc/install).\n\n## Starting a cluster\n\n```\ngit clone git@github.com:quay/clair.git\ncd clair\ndocker-compose up -d\n# or: make local-dev\n# or: make local-dev-debug\n# or: make local-dev-quay\n```\n\nAfter the local development environment successfully starts, the following infrastructure is available to you:\n\n- `localhost:8080`\n\n  Dashboards and debugging services -- See the traefik configs in `local-dev/traefik` for where the various services are served.\n\n- `localhost:6060`\n\n  Clair services.\n\n- Quay (if started)\n\n  Quay will be started in a single node, local storage configuration.\n  A random port will be forwarded from localhost, see `podman port` for the mapping.\n\n- PostgreSQL\n\n  PostgreSQL will have a random port forwarded from localhost to the database server.\n  See `local-dev/clair/init.sql` for credentials and permissions and `podman port` for the mapping.\n\n### Debugging\n\nWith the `local-dev-debug` make target the operator has access to some more useful tools:\n\n| Tool       | Description                     | URL                                                         | Credentials           |\n| ---------- | ------------------------------- | ----------------------------------------------------------- | --------------------- |\n| pgAdmin    | Postgres administration         | [localhost:8080/pgadmin](http://localhost:8080/pgadmin)     | clair@clair.com:clair |\n| jaeger     | Distributed tracing             | [localhost:8080/jaeger](http://localhost:8080/jaeger)       | -                     |\n| prometheus | Metrics collection and querying | [localhost:8080/prom](http://localhost:8080/prom)           | -                     |\n| pyroscope  | Continuous profiling            | [localhost:8080/pyroscope](http://localhost:8080/pyroscope) | -                     |\n| grafana    | Metrics dashboards              | [localhost:8080/grafana](http://localhost:8080/grafana)     | admin:admin           |\n\n## Pushing to the Local Quay\n\nAs mentioned above, Quay is forwarded to a random port on the host.\nYou can connect to the server on that port and create a account.\nCreating an account named `admin` will ensure you are a super user.\nAn email is required, but is not validated.\nYou'll also need to create a namespace.\n\nTo push to Quay, you'll need to exec into the skopeo container:\n\n```shell\ndocker-compose exec -it skopeo /usr/bin/skopeo copy --dest-creds '<user>:<pass>' --dest-tls-verify=false <src> docker://clair-quay:8080/<namespace>/<repo>:<tag>\n```\nNote that skopeo expects its image arguments in [`containers-transports(5)`] format.\n\n[`containers-transports(5)`]: https://github.com/containers/image/blob/main/docs/containers-transports.5.md\n\n## Viewing Results\n\nBy default, Quay displays security scanner results on the Tags page of the given repository.\n\n## Making changes to configuration\n\nYou may want to play with either Clair or Quay's configuration.\nIf so, the configuration files can be found inside the repository at `local-dev/quay/config.yaml` and `local-dev/clair/config.yaml`.\nAny changes to the configs will require a restart of the relevant service.\nThe quay-specific clair config is autogenerated, see the `Makefile`.\n\n## Tearing it down\n\n```\ndocker-compose down\n```\n\nwill rip the entire environment down.\n\n\n## Troubleshooting\n\nThe most common issue encountered when standing up the dev environment is port conflicts.\nMake sure that you do not have any other processes listening on any of the ports outlined above.\n\nThe second issue you may face is your Docker resource settings being too constrained to support the local dev stack.\nThis is typically seen on Docker4Mac since a VM is used with a specific set of resources configured.\nSee [Docker For Mac Manual](https://docs.docker.com/docker-for-mac/) for instructions on how to change these resources.\n\nIf `docker-compose` reports errors like `Unsupported config option for services.activemq: 'profiles'`, the `docker-compose` version is too old and you'll need to upgrade.\nConsult the relevant documentation for your environment for instructions.\n\nLastly, you can view traefik's ui at [`localhost:8080/dashboard/`](http://localhost:8080/dashboard/).\n"
  },
  {
    "path": "Documentation/howto.md",
    "content": "# How Tos\n\nThe following sections provide instructions on accomplish specific goals in Clair.\n\n- [API Definition](./howto/api.md)\n- [Deployment Models](./howto/deployment.md)\n- [Getting Started](./howto/getting_started.md)\n- [Testing ClairV4](./howto/testing.md)\n"
  },
  {
    "path": "Documentation/listing_test.go",
    "content": "package Documentation\n\nimport (\n\t\"bufio\"\n\t\"io/fs\"\n\t\"os\"\n\t\"path\"\n\t\"regexp\"\n\t\"sort\"\n\t\"testing\"\n\n\t\"github.com/google/go-cmp/cmp\"\n)\n\n// TestListing fails if the SUMMARY.md falls out of sync with the markdown files\n// in this directory.\nfunc TestListing(t *testing.T) {\n\t// Check that this is the docs test.\n\t// These files are copied into the \"book\" directory, so when left around in\n\t// a work tree, test will run there as well.\n\tif _, err := os.Stat(\"index.html\"); err == nil {\n\t\tt.Skip(\"skip listing check in compiled docs\")\n\t}\n\n\tlinkline, err := regexp.Compile(`\\s*- \\[.+\\]\\((.+)\\)`)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tf, err := os.Open(\"SUMMARY.md\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer f.Close()\n\n\tlinked := []string{\"SUMMARY.md\"}\n\ts := bufio.NewScanner(f)\n\tfor s.Scan() {\n\t\tms := linkline.FindSubmatch(s.Bytes())\n\t\tswitch {\n\t\tcase ms == nil, len(ms) == 1:\n\t\t\tcontinue\n\t\tcase len(ms) == 2:\n\t\t\tlinked = append(linked, path.Clean(string(ms[1])))\n\t\t}\n\t}\n\tif err := s.Err(); err != nil {\n\t\tt.Error(err)\n\t}\n\tsort.Strings(linked)\n\n\tvar files []string\n\terr = fs.WalkDir(os.DirFS(\".\"), \".\", func(p string, d fs.DirEntry, err error) error {\n\t\tswitch {\n\t\tcase err != nil:\n\t\t\treturn err\n\t\tcase d.IsDir():\n\t\t\treturn nil\n\t\tcase path.Ext(d.Name()) != \".md\":\n\t\t\treturn nil\n\t\t}\n\t\tfiles = append(files, p)\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n\tsort.Strings(files)\n\n\tif !cmp.Equal(linked, files) {\n\t\tt.Error(cmp.Diff(linked, files))\n\t}\n}\n"
  },
  {
    "path": "Documentation/reference/api.md",
    "content": "---\ntitle: Clair Container Analyzer v1.2.0\nlanguage_tabs:\n  - python: Python\n  - go: Golang\n  - javascript: Javascript\nlanguage_clients:\n  - python: \"\"\n  - go: \"\"\n  - javascript: \"\"\ntoc_footers:\n  - <a href=\"https://quay.github.io/clair/\">External documentation</a>\nincludes: []\nsearch: false\nhighlight_theme: darkula\nheadingLevel: 2\n\n---\n\n<!-- Generator: Widdershins v4.0.1 -->\n\n<h1 id=\"clair-container-analyzer\">Clair Container Analyzer v1.2.0</h1>\n\n> Scroll down for code samples, example requests and responses. Select a language for code samples from the tabs above or the mobile navigation menu.\n\nClair is a set of cooperating microservices which can index and match a container image's content with known vulnerabilities.\n\n**Note:** Any endpoints tagged \"internal\" are documented for completeness but are considered exempt from versioning.\n\nEmail: <a href=\"mailto:clair-devel@googlegroups.com\">Clair Team</a> Web: <a href=\"http://github.com/quay/clair\">Clair Team</a> \nLicense: <a href=\"http://www.apache.org/licenses/\">Apache License 2.0</a>\n\n# Authentication\n\n- HTTP Authentication, scheme: bearer Clair's authentication scheme.\n\nThis is a [JWT](https://datatracker.ietf.org/doc/html/rfc7519) signed with a configured pre-shared key containing an allowlisted `iss` claim.\n\n<h1 id=\"clair-container-analyzer-indexer\">indexer</h1>\n\nIndexer service endpoints.\n\nThese are responsible for determining the contents of containers.\n\n## Index a Manifest\n\n<a id=\"opIdIndex\"></a>\n\n> Code samples\n\n```python\nimport requests\nheaders = {\n  'Content-Type': 'application/vnd.clair.manifest.v1+json',\n  'Accept': 'application/vnd.clair.index_report.v1+json'\n}\n\nr = requests.post('/indexer/api/v1/index_report', headers = headers)\n\nprint(r.json())\n\n```\n\n```go\npackage main\n\nimport (\n       \"bytes\"\n       \"net/http\"\n)\n\nfunc main() {\n\n    headers := map[string][]string{\n        \"Content-Type\": []string{\"application/vnd.clair.manifest.v1+json\"},\n        \"Accept\": []string{\"application/vnd.clair.index_report.v1+json\"},\n    }\n\n    data := bytes.NewBuffer([]byte{jsonReq})\n    req, err := http.NewRequest(\"POST\", \"/indexer/api/v1/index_report\", data)\n    req.Header = headers\n\n    client := &http.Client{}\n    resp, err := client.Do(req)\n    // ...\n}\n\n```\n\n```javascript\nconst inputBody = '{\n  \"hash\": \"sha256:fc84b5febd328eccaa913807716887b3eb5ed08bc22cc6933a9ebf82766725e3\",\n  \"layers\": [\n    {\n      \"hash\": \"sha256:2f077db56abccc19f16f140f629ae98e904b4b7d563957a7fc319bd11b82ba36\",\n      \"uri\": \"https://storage.example.com/blob/2f077db56abccc19f16f140f629ae98e904b4b7d563957a7fc319bd11b82ba36\",\n      \"headers\": {\n        \"Authoriztion\": [\n          \"Bearer hunter2\"\n        ]\n      }\n    }\n  ]\n}';\nconst headers = {\n  'Content-Type':'application/vnd.clair.manifest.v1+json',\n  'Accept':'application/vnd.clair.index_report.v1+json'\n};\n\nfetch('/indexer/api/v1/index_report',\n{\n  method: 'POST',\n  body: inputBody,\n  headers: headers\n})\n.then(function(res) {\n    return res.json();\n}).then(function(body) {\n    console.log(body);\n});\n\n```\n\n`POST /indexer/api/v1/index_report`\n\nBy submitting a Manifest object to this endpoint Clair will fetch the layers, scan each layer's contents, and provide an index of discovered packages, repository and distribution information.\n\n> Body parameter\n\n```json\n{\n  \"hash\": \"sha256:fc84b5febd328eccaa913807716887b3eb5ed08bc22cc6933a9ebf82766725e3\",\n  \"layers\": [\n    {\n      \"hash\": \"sha256:2f077db56abccc19f16f140f629ae98e904b4b7d563957a7fc319bd11b82ba36\",\n      \"uri\": \"https://storage.example.com/blob/2f077db56abccc19f16f140f629ae98e904b4b7d563957a7fc319bd11b82ba36\",\n      \"headers\": {\n        \"Authoriztion\": [\n          \"Bearer hunter2\"\n        ]\n      }\n    }\n  ]\n}\n```\n\n<h3 id=\"index-a-manifest-parameters\">Parameters</h3>\n\n|Name|In|Type|Required|Description|\n|---|---|---|---|---|\n|body|body|[manifest](#schemamanifest)|true|Manifest to index.|\n\n> Example responses\n\n> 201 Response\n\n```json\n{\n  \"manifest_hash\": null,\n  \"state\": \"string\",\n  \"err\": \"string\",\n  \"success\": true,\n  \"packages\": {},\n  \"distributions\": {},\n  \"repository\": {},\n  \"environments\": {\n    \"property1\": [],\n    \"property2\": []\n  }\n}\n```\n\n<h3 id=\"index-a-manifest-responses\">Responses</h3>\n\n|Status|Meaning|Description|Schema|\n|---|---|---|---|\n|201|[Created](https://tools.ietf.org/html/rfc7231#section-6.3.2)|IndexReport created.\n\nClients may want to avoid reading the body if simply submitting the manifest for later vulnerability reporting.|[index_report](#schemaindex_report)|\n|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request|[error](#schemaerror)|\n|412|[Precondition Failed](https://tools.ietf.org/html/rfc7232#section-4.2)|Precondition Failed|None|\n|415|[Unsupported Media Type](https://tools.ietf.org/html/rfc7231#section-6.5.13)|Unsupported Media Type|[error](#schemaerror)|\n|default|Default|Internal Server Error|[error](#schemaerror)|\n\n### Response Headers\n\n|Status|Header|Type|Format|Description|\n|---|---|---|---|---|\n|201|Location|string||HTTP [Location header](https://httpwg.org/specs/rfc9110.html#field.location)|\n|201|Link|string||Web Linking [Link header](https://httpwg.org/specs/rfc8288.html#header)|\n\n<aside class=\"warning\">\nTo perform this operation, you must be authenticated by means of one of the following methods:\nNone, PSK\n</aside>\n\n## Delete Indexed Manifests\n\n<a id=\"opIdDeleteManifests\"></a>\n\n> Code samples\n\n```python\nimport requests\nheaders = {\n  'Content-Type': 'application/vnd.clair.bulk_delete.v1+json',\n  'Accept': 'application/vnd.clair.bulk_delete.v1+json'\n}\n\nr = requests.delete('/indexer/api/v1/index_report', headers = headers)\n\nprint(r.json())\n\n```\n\n```go\npackage main\n\nimport (\n       \"bytes\"\n       \"net/http\"\n)\n\nfunc main() {\n\n    headers := map[string][]string{\n        \"Content-Type\": []string{\"application/vnd.clair.bulk_delete.v1+json\"},\n        \"Accept\": []string{\"application/vnd.clair.bulk_delete.v1+json\"},\n    }\n\n    data := bytes.NewBuffer([]byte{jsonReq})\n    req, err := http.NewRequest(\"DELETE\", \"/indexer/api/v1/index_report\", data)\n    req.Header = headers\n\n    client := &http.Client{}\n    resp, err := client.Do(req)\n    // ...\n}\n\n```\n\n```javascript\nconst inputBody = '[\n  \"sha256:fc84b5febd328eccaa913807716887b3eb5ed08bc22cc6933a9ebf82766725e3\"\n]';\nconst headers = {\n  'Content-Type':'application/vnd.clair.bulk_delete.v1+json',\n  'Accept':'application/vnd.clair.bulk_delete.v1+json'\n};\n\nfetch('/indexer/api/v1/index_report',\n{\n  method: 'DELETE',\n  body: inputBody,\n  headers: headers\n})\n.then(function(res) {\n    return res.json();\n}).then(function(body) {\n    console.log(body);\n});\n\n```\n\n`DELETE /indexer/api/v1/index_report`\n\nGiven a Manifest's content addressable hash, any data related to it will be removed if it exists.\n\n> Body parameter\n\n```json\n[\n  \"sha256:fc84b5febd328eccaa913807716887b3eb5ed08bc22cc6933a9ebf82766725e3\"\n]\n```\n\n<h3 id=\"delete-indexed-manifests-parameters\">Parameters</h3>\n\n|Name|In|Type|Required|Description|\n|---|---|---|---|---|\n|body|body|[bulk_delete](#schemabulk_delete)|true|Array of manifest digests to delete.|\n\n> Example responses\n\n> 200 Response\n\n```json\n[\n  \"sha256:fc84b5febd328eccaa913807716887b3eb5ed08bc22cc6933a9ebf82766725e3\"\n]\n```\n\n<h3 id=\"delete-indexed-manifests-responses\">Responses</h3>\n\n|Status|Meaning|Description|Schema|\n|---|---|---|---|\n|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Successfully deleted manifests.|[bulk_delete](#schemabulk_delete)|\n|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request|[error](#schemaerror)|\n|415|[Unsupported Media Type](https://tools.ietf.org/html/rfc7231#section-6.5.13)|Unsupported Media Type|[error](#schemaerror)|\n|default|Default|Internal Server Error|[error](#schemaerror)|\n\n### Response Headers\n\n|Status|Header|Type|Format|Description|\n|---|---|---|---|---|\n|200|Clair-Error|string||This is a trailer containing any errors encountered while writing the response.|\n\n<aside class=\"warning\">\nTo perform this operation, you must be authenticated by means of one of the following methods:\nNone, PSK\n</aside>\n\n## Delete an Indexed Manifest\n\n<a id=\"opIdDeleteManifest\"></a>\n\n> Code samples\n\n```python\nimport requests\nheaders = {\n  'Accept': 'application/vnd.clair.error.v1+json'\n}\n\nr = requests.delete('/indexer/api/v1/index_report/{digest}', headers = headers)\n\nprint(r.json())\n\n```\n\n```go\npackage main\n\nimport (\n       \"bytes\"\n       \"net/http\"\n)\n\nfunc main() {\n\n    headers := map[string][]string{\n        \"Accept\": []string{\"application/vnd.clair.error.v1+json\"},\n    }\n\n    data := bytes.NewBuffer([]byte{jsonReq})\n    req, err := http.NewRequest(\"DELETE\", \"/indexer/api/v1/index_report/{digest}\", data)\n    req.Header = headers\n\n    client := &http.Client{}\n    resp, err := client.Do(req)\n    // ...\n}\n\n```\n\n```javascript\n\nconst headers = {\n  'Accept':'application/vnd.clair.error.v1+json'\n};\n\nfetch('/indexer/api/v1/index_report/{digest}',\n{\n  method: 'DELETE',\n\n  headers: headers\n})\n.then(function(res) {\n    return res.json();\n}).then(function(body) {\n    console.log(body);\n});\n\n```\n\n`DELETE /indexer/api/v1/index_report/{digest}`\n\nGiven a Manifest's content addressable hash, any data related to it will be removed it it exists.\n\n<h3 id=\"delete-an-indexed-manifest-parameters\">Parameters</h3>\n\n|Name|In|Type|Required|Description|\n|---|---|---|---|---|\n|digest|path|[digest](#schemadigest)|true|OCI-compatible digest of a referred object.|\n\n> Example responses\n\n> 400 Response\n\n```json\n{\n  \"code\": \"string\",\n  \"message\": \"string\"\n}\n```\n\n<h3 id=\"delete-an-indexed-manifest-responses\">Responses</h3>\n\n|Status|Meaning|Description|Schema|\n|---|---|---|---|\n|204|[No Content](https://tools.ietf.org/html/rfc7231#section-6.3.5)|Success|None|\n|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request|[error](#schemaerror)|\n|415|[Unsupported Media Type](https://tools.ietf.org/html/rfc7231#section-6.5.13)|Unsupported Media Type|[error](#schemaerror)|\n|default|Default|Internal Server Error|[error](#schemaerror)|\n\n<aside class=\"warning\">\nTo perform this operation, you must be authenticated by means of one of the following methods:\nNone, PSK\n</aside>\n\n## Retrieve the IndexReport for a Manifest\n\n<a id=\"opIdGetIndexReport\"></a>\n\n> Code samples\n\n```python\nimport requests\nheaders = {\n  'Accept': 'application/vnd.clair.index_report.v1+json'\n}\n\nr = requests.get('/indexer/api/v1/index_report/{digest}', headers = headers)\n\nprint(r.json())\n\n```\n\n```go\npackage main\n\nimport (\n       \"bytes\"\n       \"net/http\"\n)\n\nfunc main() {\n\n    headers := map[string][]string{\n        \"Accept\": []string{\"application/vnd.clair.index_report.v1+json\"},\n    }\n\n    data := bytes.NewBuffer([]byte{jsonReq})\n    req, err := http.NewRequest(\"GET\", \"/indexer/api/v1/index_report/{digest}\", data)\n    req.Header = headers\n\n    client := &http.Client{}\n    resp, err := client.Do(req)\n    // ...\n}\n\n```\n\n```javascript\n\nconst headers = {\n  'Accept':'application/vnd.clair.index_report.v1+json'\n};\n\nfetch('/indexer/api/v1/index_report/{digest}',\n{\n  method: 'GET',\n\n  headers: headers\n})\n.then(function(res) {\n    return res.json();\n}).then(function(body) {\n    console.log(body);\n});\n\n```\n\n`GET /indexer/api/v1/index_report/{digest}`\n\nGiven a Manifest's content addressable hash, an IndexReport will be retrieved if it exists.\n\n<h3 id=\"retrieve-the-indexreport-for-a-manifest-parameters\">Parameters</h3>\n\n|Name|In|Type|Required|Description|\n|---|---|---|---|---|\n|digest|path|[digest](#schemadigest)|true|OCI-compatible digest of a referred object.|\n\n> Example responses\n\n> 200 Response\n\n```json\n{\n  \"manifest_hash\": null,\n  \"state\": \"string\",\n  \"err\": \"string\",\n  \"success\": true,\n  \"packages\": {},\n  \"distributions\": {},\n  \"repository\": {},\n  \"environments\": {\n    \"property1\": [],\n    \"property2\": []\n  }\n}\n```\n\n<h3 id=\"retrieve-the-indexreport-for-a-manifest-responses\">Responses</h3>\n\n|Status|Meaning|Description|Schema|\n|---|---|---|---|\n|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|IndexReport retrieved|[index_report](#schemaindex_report)|\n|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request|[error](#schemaerror)|\n|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found|[error](#schemaerror)|\n|415|[Unsupported Media Type](https://tools.ietf.org/html/rfc7231#section-6.5.13)|Unsupported Media Type|[error](#schemaerror)|\n|default|Default|Internal Server Error|[error](#schemaerror)|\n\n### Response Headers\n\n|Status|Header|Type|Format|Description|\n|---|---|---|---|---|\n|200|Clair-Error|string||This is a trailer containing any errors encountered while writing the response.|\n\n<aside class=\"warning\">\nTo perform this operation, you must be authenticated by means of one of the following methods:\nNone, PSK\n</aside>\n\n## Report the Indexer's State\n\n<a id=\"opIdIndexState\"></a>\n\n> Code samples\n\n```python\nimport requests\nheaders = {\n  'Accept': 'application/vnd.clair.index_state.v1+json'\n}\n\nr = requests.get('/indexer/api/v1/index_state', headers = headers)\n\nprint(r.json())\n\n```\n\n```go\npackage main\n\nimport (\n       \"bytes\"\n       \"net/http\"\n)\n\nfunc main() {\n\n    headers := map[string][]string{\n        \"Accept\": []string{\"application/vnd.clair.index_state.v1+json\"},\n    }\n\n    data := bytes.NewBuffer([]byte{jsonReq})\n    req, err := http.NewRequest(\"GET\", \"/indexer/api/v1/index_state\", data)\n    req.Header = headers\n\n    client := &http.Client{}\n    resp, err := client.Do(req)\n    // ...\n}\n\n```\n\n```javascript\n\nconst headers = {\n  'Accept':'application/vnd.clair.index_state.v1+json'\n};\n\nfetch('/indexer/api/v1/index_state',\n{\n  method: 'GET',\n\n  headers: headers\n})\n.then(function(res) {\n    return res.json();\n}).then(function(body) {\n    console.log(body);\n});\n\n```\n\n`GET /indexer/api/v1/index_state`\n\nThe index state endpoint returns a json structure indicating the indexer's internal configuration state.\nA client may be interested in this as a signal that manifests may need to be re-indexed.\n\n> Example responses\n\n> 200 Response\n\n```json\n{\n  \"state\": \"string\"\n}\n```\n\n<h3 id=\"report-the-indexer's-state-responses\">Responses</h3>\n\n|Status|Meaning|Description|Schema|\n|---|---|---|---|\n|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Indexer State|[index_state](#schemaindex_state)|\n|304|[Not Modified](https://tools.ietf.org/html/rfc7232#section-4.1)|Not Modified|None|\n\n### Response Headers\n\n|Status|Header|Type|Format|Description|\n|---|---|---|---|---|\n|200|Etag|string||HTTP [ETag header](https://httpwg.org/specs/rfc9110.html#field.etag)|\n\n<aside class=\"warning\">\nTo perform this operation, you must be authenticated by means of one of the following methods:\nNone, PSK\n</aside>\n\n<h1 id=\"clair-container-analyzer-matcher\">matcher</h1>\n\nMatcher service endpoints.\n\nThese are responsible for generating reports against current vulnerability data.\n\n## Retrieve a VulnerabilityReport for a Manifest\n\n<a id=\"opIdGetVulnerabilityReport\"></a>\n\n> Code samples\n\n```python\nimport requests\nheaders = {\n  'Accept': 'application/vnd.clair.vulnerability_report.v1+json'\n}\n\nr = requests.get('/matcher/api/v1/vulnerability_report/{digest}', headers = headers)\n\nprint(r.json())\n\n```\n\n```go\npackage main\n\nimport (\n       \"bytes\"\n       \"net/http\"\n)\n\nfunc main() {\n\n    headers := map[string][]string{\n        \"Accept\": []string{\"application/vnd.clair.vulnerability_report.v1+json\"},\n    }\n\n    data := bytes.NewBuffer([]byte{jsonReq})\n    req, err := http.NewRequest(\"GET\", \"/matcher/api/v1/vulnerability_report/{digest}\", data)\n    req.Header = headers\n\n    client := &http.Client{}\n    resp, err := client.Do(req)\n    // ...\n}\n\n```\n\n```javascript\n\nconst headers = {\n  'Accept':'application/vnd.clair.vulnerability_report.v1+json'\n};\n\nfetch('/matcher/api/v1/vulnerability_report/{digest}',\n{\n  method: 'GET',\n\n  headers: headers\n})\n.then(function(res) {\n    return res.json();\n}).then(function(body) {\n    console.log(body);\n});\n\n```\n\n`GET /matcher/api/v1/vulnerability_report/{digest}`\n\nGiven a Manifest's content addressable hash a VulnerabilityReport will be created. The Manifest **must** have been Indexed first via the Index endpoint.\n\n<h3 id=\"retrieve-a-vulnerabilityreport-for-a-manifest-parameters\">Parameters</h3>\n\n|Name|In|Type|Required|Description|\n|---|---|---|---|---|\n|digest|path|[digest](#schemadigest)|true|OCI-compatible digest of a referred object.|\n\n> Example responses\n\n> 201 Response\n\n```json\n{\n  \"manifest_hash\": null,\n  \"packages\": {},\n  \"distributions\": {},\n  \"repository\": {},\n  \"environments\": {\n    \"property1\": [],\n    \"property2\": []\n  },\n  \"vulnerabilities\": {},\n  \"package_vulnerabilities\": {\n    \"property1\": [\n      \"string\"\n    ],\n    \"property2\": [\n      \"string\"\n    ]\n  },\n  \"enrichments\": {\n    \"property1\": [],\n    \"property2\": []\n  }\n}\n```\n\n<h3 id=\"retrieve-a-vulnerabilityreport-for-a-manifest-responses\">Responses</h3>\n\n|Status|Meaning|Description|Schema|\n|---|---|---|---|\n|201|[Created](https://tools.ietf.org/html/rfc7231#section-6.3.2)|Vulnerability Report Created|[vulnerability_report](#schemavulnerability_report)|\n|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request|[error](#schemaerror)|\n|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not Found|[error](#schemaerror)|\n|415|[Unsupported Media Type](https://tools.ietf.org/html/rfc7231#section-6.5.13)|Unsupported Media Type|[error](#schemaerror)|\n|default|Default|Internal Server Error|[error](#schemaerror)|\n\n<aside class=\"warning\">\nTo perform this operation, you must be authenticated by means of one of the following methods:\nNone, PSK\n</aside>\n\n<h1 id=\"clair-container-analyzer-notifier\">notifier</h1>\n\nMatcher service endpoints.\n\nThese are responsible for serving notifications.\n\n## Delete a Notification Set\n\n<a id=\"opIdDeleteNotification\"></a>\n\n> Code samples\n\n```python\nimport requests\nheaders = {\n  'Accept': 'application/vnd.clair.error.v1+json'\n}\n\nr = requests.delete('/notifier/api/v1/notification/{id}', headers = headers)\n\nprint(r.json())\n\n```\n\n```go\npackage main\n\nimport (\n       \"bytes\"\n       \"net/http\"\n)\n\nfunc main() {\n\n    headers := map[string][]string{\n        \"Accept\": []string{\"application/vnd.clair.error.v1+json\"},\n    }\n\n    data := bytes.NewBuffer([]byte{jsonReq})\n    req, err := http.NewRequest(\"DELETE\", \"/notifier/api/v1/notification/{id}\", data)\n    req.Header = headers\n\n    client := &http.Client{}\n    resp, err := client.Do(req)\n    // ...\n}\n\n```\n\n```javascript\n\nconst headers = {\n  'Accept':'application/vnd.clair.error.v1+json'\n};\n\nfetch('/notifier/api/v1/notification/{id}',\n{\n  method: 'DELETE',\n\n  headers: headers\n})\n.then(function(res) {\n    return res.json();\n}).then(function(body) {\n    console.log(body);\n});\n\n```\n\n`DELETE /notifier/api/v1/notification/{id}`\n\nIssues a delete of the provided notification ID and all associated notifications.\nAfter this delete clients will no longer be able to retrieve notifications.\n\n<h3 id=\"delete-a-notification-set-parameters\">Parameters</h3>\n\n|Name|In|Type|Required|Description|\n|---|---|---|---|---|\n|id|path|[token](#schematoken)|true|A notification ID returned by a callback|\n\n> Example responses\n\n> 400 Response\n\n```json\n{\n  \"code\": \"string\",\n  \"message\": \"string\"\n}\n```\n\n<h3 id=\"delete-a-notification-set-responses\">Responses</h3>\n\n|Status|Meaning|Description|Schema|\n|---|---|---|---|\n|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Delete the notification referenced by the \"id\" parameter.|None|\n|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request|[error](#schemaerror)|\n|415|[Unsupported Media Type](https://tools.ietf.org/html/rfc7231#section-6.5.13)|Unsupported Media Type|[error](#schemaerror)|\n|default|Default|Internal Server Error|[error](#schemaerror)|\n\n### Response Headers\n\n|Status|Header|Type|Format|Description|\n|---|---|---|---|---|\n|200|Clair-Error|string||This is a trailer containing any errors encountered while writing the response.|\n\n<aside class=\"warning\">\nTo perform this operation, you must be authenticated by means of one of the following methods:\nNone, PSK\n</aside>\n\n## Retrieve Pages of a Notification Set\n\n<a id=\"opIdGetNotification\"></a>\n\n> Code samples\n\n```python\nimport requests\nheaders = {\n  'Accept': 'application/vnd.clair.notification_page.v1+json'\n}\n\nr = requests.get('/notifier/api/v1/notification/{id}', headers = headers)\n\nprint(r.json())\n\n```\n\n```go\npackage main\n\nimport (\n       \"bytes\"\n       \"net/http\"\n)\n\nfunc main() {\n\n    headers := map[string][]string{\n        \"Accept\": []string{\"application/vnd.clair.notification_page.v1+json\"},\n    }\n\n    data := bytes.NewBuffer([]byte{jsonReq})\n    req, err := http.NewRequest(\"GET\", \"/notifier/api/v1/notification/{id}\", data)\n    req.Header = headers\n\n    client := &http.Client{}\n    resp, err := client.Do(req)\n    // ...\n}\n\n```\n\n```javascript\n\nconst headers = {\n  'Accept':'application/vnd.clair.notification_page.v1+json'\n};\n\nfetch('/notifier/api/v1/notification/{id}',\n{\n  method: 'GET',\n\n  headers: headers\n})\n.then(function(res) {\n    return res.json();\n}).then(function(body) {\n    console.log(body);\n});\n\n```\n\n`GET /notifier/api/v1/notification/{id}`\n\nBy performing a GET with an id as a path parameter, the client will retrieve a paginated response of notification objects.\n\n<h3 id=\"retrieve-pages-of-a-notification-set-parameters\">Parameters</h3>\n\n|Name|In|Type|Required|Description|\n|---|---|---|---|---|\n|page_size|query|integer|false|The maximum number of notifications to deliver in a single page.|\n|next|query|[token](#schematoken)|false|The next page to fetch via id. Typically this number is provided on initial response in the \"page.next\" field. The first request should omit this field.|\n|id|path|[token](#schematoken)|true|A notification ID returned by a callback|\n\n> Example responses\n\n> 200 Response\n\n```json\n{\n  \"page\": {\n    \"size\": 100,\n    \"next\": \"1b4d0db2-e757-4150-bbbb-543658144205\"\n  },\n  \"notifications\": [\n    {\n      \"id\": \"5e4b387e-88d3-4364-86fd-063447a6fad2\",\n      \"manifest\": \"sha256:35c102085707f703de2d9eaad8752d6fe1b8f02b5d2149f1d8357c9cc7fb7d0a\",\n      \"reason\": \"added\",\n      \"vulnerability\": {\n        \"name\": \"CVE-2009-5155\",\n        \"fixed_in_version\": \"v0.0.1\",\n        \"links\": \"http://example.com/CVE-2009-5155\",\n        \"description\": \"In the GNU C Library (aka glibc or libc6) before 2.28, parse_reg_exp in posix/regcomp.c misparses alternatives, which allows attackers to cause a denial of service (assertion failure and application exit) or trigger an incorrect result by attempting a regular-expression match.\\\"\",\n        \"normalized_severity\": \"Unknown\",\n        \"package\": {\n          \"id\": \"10\",\n          \"name\": \"libapt-pkg5.0\",\n          \"version\": \"1.6.11\",\n          \"kind\": \"BINARY\",\n          \"arch\": \"x86\",\n          \"source\": {\n            \"id\": \"9\",\n            \"name\": \"apt\",\n            \"version\": \"1.6.11\",\n            \"kind\": \"SOURCE\",\n            \"source\": null\n          }\n        },\n        \"distribution\": {\n          \"id\": \"1\",\n          \"did\": \"ubuntu\",\n          \"name\": \"Ubuntu\",\n          \"version\": \"18.04.3 LTS (Bionic Beaver)\",\n          \"version_code_name\": \"bionic\",\n          \"version_id\": \"18.04\",\n          \"pretty_name\": \"Ubuntu 18.04.3 LTS\"\n        }\n      }\n    }\n  ]\n}\n```\n\n<h3 id=\"retrieve-pages-of-a-notification-set-responses\">Responses</h3>\n\n|Status|Meaning|Description|Schema|\n|---|---|---|---|\n|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|A paginated list of notifications|[notification_page](#schemanotification_page)|\n|304|[Not Modified](https://tools.ietf.org/html/rfc7232#section-4.1)|Not Modified|None|\n|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request|[error](#schemaerror)|\n|415|[Unsupported Media Type](https://tools.ietf.org/html/rfc7231#section-6.5.13)|Unsupported Media Type|[error](#schemaerror)|\n|default|Default|Internal Server Error|[error](#schemaerror)|\n\n### Response Headers\n\n|Status|Header|Type|Format|Description|\n|---|---|---|---|---|\n|200|Clair-Error|string||This is a trailer containing any errors encountered while writing the response.|\n\n<aside class=\"warning\">\nTo perform this operation, you must be authenticated by means of one of the following methods:\nNone, PSK\n</aside>\n\n<h1 id=\"clair-container-analyzer-internal\">internal</h1>\n\nThese are internal endpoints, documented for completeness.\n\nThey are exempted from API stability guarentees.\n\n## Retrieve Manifests Affected by a Vulnerability\n\n<a id=\"opIdAffectedManifests\"></a>\n\n> Code samples\n\n```python\nimport requests\nheaders = {\n  'Content-Type': 'application/vnd.clair.vulnerability_summaries.v1+json',\n  'Accept': 'application/vnd.clair.affected_manifests.v1+json'\n}\n\nr = requests.post('/indexer/api/v1/internal/affected_manifest', headers = headers)\n\nprint(r.json())\n\n```\n\n```go\npackage main\n\nimport (\n       \"bytes\"\n       \"net/http\"\n)\n\nfunc main() {\n\n    headers := map[string][]string{\n        \"Content-Type\": []string{\"application/vnd.clair.vulnerability_summaries.v1+json\"},\n        \"Accept\": []string{\"application/vnd.clair.affected_manifests.v1+json\"},\n    }\n\n    data := bytes.NewBuffer([]byte{jsonReq})\n    req, err := http.NewRequest(\"POST\", \"/indexer/api/v1/internal/affected_manifest\", data)\n    req.Header = headers\n\n    client := &http.Client{}\n    resp, err := client.Do(req)\n    // ...\n}\n\n```\n\n```javascript\nconst inputBody = '[]';\nconst headers = {\n  'Content-Type':'application/vnd.clair.vulnerability_summaries.v1+json',\n  'Accept':'application/vnd.clair.affected_manifests.v1+json'\n};\n\nfetch('/indexer/api/v1/internal/affected_manifest',\n{\n  method: 'POST',\n  body: inputBody,\n  headers: headers\n})\n.then(function(res) {\n    return res.json();\n}).then(function(body) {\n    console.log(body);\n});\n\n```\n\n`POST /indexer/api/v1/internal/affected_manifest`\n\nThe provided vulnerability summaries are attempted to be run \"backwards\" through the indexer to produce a set of manifests.\n\n> Body parameter\n\n```json\n[]\n```\n\n<h3 id=\"retrieve-manifests-affected-by-a-vulnerability-parameters\">Parameters</h3>\n\n|Name|In|Type|Required|Description|\n|---|---|---|---|---|\n|body|body|[vulnerability_summaries](#schemavulnerability_summaries)|true|Array of vulnerability summaries to report on.|\n\n> Example responses\n\n> 200 Response\n\n```json\n{\n  \"vulnerabilities\": {\n    \"42\": {\n      \"id\": \"42\"\n    }\n  },\n  \"vulnerable_manifests\": {\n    \"sha256:01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b\": [\n      \"42\"\n    ]\n  }\n}\n```\n\n<h3 id=\"retrieve-manifests-affected-by-a-vulnerability-responses\">Responses</h3>\n\n|Status|Meaning|Description|Schema|\n|---|---|---|---|\n|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|The list of manifests and the corresponding vulnerabilities.|[affected_manifests](#schemaaffected_manifests)|\n|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request|[error](#schemaerror)|\n|415|[Unsupported Media Type](https://tools.ietf.org/html/rfc7231#section-6.5.13)|Unsupported Media Type|[error](#schemaerror)|\n|default|Default|Internal Server Error|[error](#schemaerror)|\n\n### Response Headers\n\n|Status|Header|Type|Format|Description|\n|---|---|---|---|---|\n|200|Clair-Error|string||This is a trailer containing any errors encountered while writing the response.|\n\n<aside class=\"warning\">\nTo perform this operation, you must be authenticated by means of one of the following methods:\nNone, PSK\n</aside>\n\n## Retrieve Vulnerability Changes Between Two Update Operations\n\n<a id=\"opIdGetUpdateDiff\"></a>\n\n> Code samples\n\n```python\nimport requests\nheaders = {\n  'Accept': 'application/vnd.clair.update_diff.v1+json'\n}\n\nr = requests.get('/matcher/api/v1/internal/update_diff', params={\n  'prev': 'string'\n}, headers = headers)\n\nprint(r.json())\n\n```\n\n```go\npackage main\n\nimport (\n       \"bytes\"\n       \"net/http\"\n)\n\nfunc main() {\n\n    headers := map[string][]string{\n        \"Accept\": []string{\"application/vnd.clair.update_diff.v1+json\"},\n    }\n\n    data := bytes.NewBuffer([]byte{jsonReq})\n    req, err := http.NewRequest(\"GET\", \"/matcher/api/v1/internal/update_diff\", data)\n    req.Header = headers\n\n    client := &http.Client{}\n    resp, err := client.Do(req)\n    // ...\n}\n\n```\n\n```javascript\n\nconst headers = {\n  'Accept':'application/vnd.clair.update_diff.v1+json'\n};\n\nfetch('/matcher/api/v1/internal/update_diff?prev=string',\n{\n  method: 'GET',\n\n  headers: headers\n})\n.then(function(res) {\n    return res.json();\n}).then(function(body) {\n    console.log(body);\n});\n\n```\n\n`GET /matcher/api/v1/internal/update_diff`\n\nGiven IDs for two Update Operations, this will return the difference between them. This is used in the notification flow.\n\n<h3 id=\"retrieve-vulnerability-changes-between-two-update-operations-parameters\">Parameters</h3>\n\n|Name|In|Type|Required|Description|\n|---|---|---|---|---|\n|cur|query|[token](#schematoken)|false|\"Current\" Update Operation ref.|\n|prev|query|[token](#schematoken)|true|\"Previous\" Update Operation ref.|\n\n> Example responses\n\n> 200 Response\n\n```json\n{\n  \"prev\": null,\n  \"cur\": null,\n  \"added\": [],\n  \"removed\": []\n}\n```\n\n<h3 id=\"retrieve-vulnerability-changes-between-two-update-operations-responses\">Responses</h3>\n\n|Status|Meaning|Description|Schema|\n|---|---|---|---|\n|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Changes between two Update Operations.|[update_diff](#schemaupdate_diff)|\n|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request|[error](#schemaerror)|\n|415|[Unsupported Media Type](https://tools.ietf.org/html/rfc7231#section-6.5.13)|Unsupported Media Type|[error](#schemaerror)|\n|default|Default|Internal Server Error|[error](#schemaerror)|\n\n### Response Headers\n\n|Status|Header|Type|Format|Description|\n|---|---|---|---|---|\n|200|Clair-Error|string||This is a trailer containing any errors encountered while writing the response.|\n\n<aside class=\"warning\">\nTo perform this operation, you must be authenticated by means of one of the following methods:\nNone, PSK\n</aside>\n\n## Retrieve Update Operations\n\n<a id=\"opIdGetUpdateOperation\"></a>\n\n> Code samples\n\n```python\nimport requests\nheaders = {\n  'Accept': 'application/vnd.clair.update_operations.v1+json'\n}\n\nr = requests.get('/matcher/api/v1/internal/update_operation', headers = headers)\n\nprint(r.json())\n\n```\n\n```go\npackage main\n\nimport (\n       \"bytes\"\n       \"net/http\"\n)\n\nfunc main() {\n\n    headers := map[string][]string{\n        \"Accept\": []string{\"application/vnd.clair.update_operations.v1+json\"},\n    }\n\n    data := bytes.NewBuffer([]byte{jsonReq})\n    req, err := http.NewRequest(\"GET\", \"/matcher/api/v1/internal/update_operation\", data)\n    req.Header = headers\n\n    client := &http.Client{}\n    resp, err := client.Do(req)\n    // ...\n}\n\n```\n\n```javascript\n\nconst headers = {\n  'Accept':'application/vnd.clair.update_operations.v1+json'\n};\n\nfetch('/matcher/api/v1/internal/update_operation',\n{\n  method: 'GET',\n\n  headers: headers\n})\n.then(function(res) {\n    return res.json();\n}).then(function(body) {\n    console.log(body);\n});\n\n```\n\n`GET /matcher/api/v1/internal/update_operation`\n\nRetrive all known or just the latest Update Operations.\n\n<h3 id=\"retrieve-update-operations-parameters\">Parameters</h3>\n\n|Name|In|Type|Required|Description|\n|---|---|---|---|---|\n|kind|query|any|false|The \"kind\" of updaters to query.|\n|latest|query|boolean|false|Return only the latest Update Operations instead of all known Update Operations.|\n\n#### Enumerated Values\n\n|Parameter|Value|\n|---|---|\n|kind|vulnerability|\n|kind|enrichment|\n\n> Example responses\n\n> 200 Response\n\n```json\n{\n  \"property1\": [],\n  \"property2\": []\n}\n```\n\n<h3 id=\"retrieve-update-operations-responses\">Responses</h3>\n\n|Status|Meaning|Description|Schema|\n|---|---|---|---|\n|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Update Operations, keyed by updater.|[update_operations](#schemaupdate_operations)|\n|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request|[error](#schemaerror)|\n|415|[Unsupported Media Type](https://tools.ietf.org/html/rfc7231#section-6.5.13)|Unsupported Media Type|[error](#schemaerror)|\n|default|Default|Internal Server Error|[error](#schemaerror)|\n\n### Response Headers\n\n|Status|Header|Type|Format|Description|\n|---|---|---|---|---|\n|200|Clair-Error|string||This is a trailer containing any errors encountered while writing the response.|\n\n<aside class=\"warning\">\nTo perform this operation, you must be authenticated by means of one of the following methods:\nNone, PSK\n</aside>\n\n## Delete an Update Operation\n\n<a id=\"opIdDeleteUpdateOperation\"></a>\n\n> Code samples\n\n```python\nimport requests\nheaders = {\n  'Accept': 'application/vnd.clair.error.v1+json'\n}\n\nr = requests.delete('/matcher/api/v1/internal/update_operation/{digest}', headers = headers)\n\nprint(r.json())\n\n```\n\n```go\npackage main\n\nimport (\n       \"bytes\"\n       \"net/http\"\n)\n\nfunc main() {\n\n    headers := map[string][]string{\n        \"Accept\": []string{\"application/vnd.clair.error.v1+json\"},\n    }\n\n    data := bytes.NewBuffer([]byte{jsonReq})\n    req, err := http.NewRequest(\"DELETE\", \"/matcher/api/v1/internal/update_operation/{digest}\", data)\n    req.Header = headers\n\n    client := &http.Client{}\n    resp, err := client.Do(req)\n    // ...\n}\n\n```\n\n```javascript\n\nconst headers = {\n  'Accept':'application/vnd.clair.error.v1+json'\n};\n\nfetch('/matcher/api/v1/internal/update_operation/{digest}',\n{\n  method: 'DELETE',\n\n  headers: headers\n})\n.then(function(res) {\n    return res.json();\n}).then(function(body) {\n    console.log(body);\n});\n\n```\n\n`DELETE /matcher/api/v1/internal/update_operation/{digest}`\n\nIssues a delete of the provided Update Operation ID and all associated data.\nAfter this delete clients will no longer be able to generate a diff against this Update Operation.\n\n<h3 id=\"delete-an-update-operation-parameters\">Parameters</h3>\n\n|Name|In|Type|Required|Description|\n|---|---|---|---|---|\n|digest|path|[digest](#schemadigest)|true|OCI-compatible digest of a referred object.|\n\n> Example responses\n\n> 400 Response\n\n```json\n{\n  \"code\": \"string\",\n  \"message\": \"string\"\n}\n```\n\n<h3 id=\"delete-an-update-operation-responses\">Responses</h3>\n\n|Status|Meaning|Description|Schema|\n|---|---|---|---|\n|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Success|None|\n|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad Request|[error](#schemaerror)|\n|415|[Unsupported Media Type](https://tools.ietf.org/html/rfc7231#section-6.5.13)|Unsupported Media Type|[error](#schemaerror)|\n|default|Default|Internal Server Error|[error](#schemaerror)|\n\n### Response Headers\n\n|Status|Header|Type|Format|Description|\n|---|---|---|---|---|\n|200|Clair-Error|string||This is a trailer containing any errors encountered while writing the response.|\n\n<aside class=\"warning\">\nTo perform this operation, you must be authenticated by means of one of the following methods:\nNone, PSK\n</aside>\n\n# Schemas\n\n<h2 id=\"tocS_token\">token</h2>\n<!-- backwards compatibility -->\n<a id=\"schematoken\"></a>\n<a id=\"schema_token\"></a>\n<a id=\"tocStoken\"></a>\n<a id=\"tocstoken\"></a>\n\n```json\n\"string\"\n\n```\n\nAn opaque token previously obtained from the service.\n\n### Properties\n\n|Name|Type|Required|Restrictions|Description|\n|---|---|---|---|---|\n|*anonymous*|string|false|none|An opaque token previously obtained from the service.|\n\n<h2 id=\"tocS_affected_manifests\">affected_manifests</h2>\n<!-- backwards compatibility -->\n<a id=\"schemaaffected_manifests\"></a>\n<a id=\"schema_affected_manifests\"></a>\n<a id=\"tocSaffected_manifests\"></a>\n<a id=\"tocsaffected_manifests\"></a>\n\n```json\n{\n  \"vulnerabilities\": {\n    \"42\": {\n      \"id\": \"42\"\n    }\n  },\n  \"vulnerable_manifests\": {\n    \"sha256:01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b\": [\n      \"42\"\n    ]\n  }\n}\n\n```\n\nAffected Manifests\n\n### Properties\n\n|Name|Type|Required|Restrictions|Description|\n|---|---|---|---|---|\n|vulnerabilities|object|true|none|Vulnerability objects.|\n|» **additionalProperties**|[vulnerability.schema.json](#schemavulnerability.schema.json)|false|none|none|\n|vulnerable_manifests|object|true|none|Mapping of manifest digests to vulnerability identifiers.|\n|» **additionalProperties**|[string]|false|none|none|\n\n<h2 id=\"tocS_bulk_delete\">bulk_delete</h2>\n<!-- backwards compatibility -->\n<a id=\"schemabulk_delete\"></a>\n<a id=\"schema_bulk_delete\"></a>\n<a id=\"tocSbulk_delete\"></a>\n<a id=\"tocsbulk_delete\"></a>\n\n```json\n[\n  \"sha256:fc84b5febd328eccaa913807716887b3eb5ed08bc22cc6933a9ebf82766725e3\"\n]\n\n```\n\nBulk Delete\n\n### Properties\n\n|Name|Type|Required|Restrictions|Description|\n|---|---|---|---|---|\n|Bulk Delete|[[digest.schema.json](#schemadigest.schema.json)]|false|none|Array of manifest digests to delete from the system.|\n\n<h2 id=\"tocS_cpe\">cpe</h2>\n<!-- backwards compatibility -->\n<a id=\"schemacpe\"></a>\n<a id=\"schema_cpe\"></a>\n<a id=\"tocScpe\"></a>\n<a id=\"tocscpe\"></a>\n\n```json\n\"cpe:/a:microsoft:internet_explorer:8.0.6001:beta\"\n\n```\n\nCommon Platform Enumeration Name\n\n### Properties\n\n|Name|Type|Required|Restrictions|Description|\n|---|---|---|---|---|\n|Common Platform Enumeration Name|any|false|none|This is a CPE Name in either v2.2 \"URI\" form or v2.3 \"Formatted String\" form.|\n\noneOf\n\n|Name|Type|Required|Restrictions|Description|\n|---|---|---|---|---|\n|*anonymous*|string|false|none|This is the CPE 2.2 regexp: https://cpe.mitre.org/specification/2.2/cpe-language_2.2.xsd|\n\nxor\n\n|Name|Type|Required|Restrictions|Description|\n|---|---|---|---|---|\n|*anonymous*|string|false|none|This is the CPE 2.3 regexp: https://csrc.nist.gov/schema/cpe/2.3/cpe-naming_2.3.xsd|\n\n<h2 id=\"tocS_digest\">digest</h2>\n<!-- backwards compatibility -->\n<a id=\"schemadigest\"></a>\n<a id=\"schema_digest\"></a>\n<a id=\"tocSdigest\"></a>\n<a id=\"tocsdigest\"></a>\n\n```json\n\"sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a\"\n\n```\n\nDigest\n\n### Properties\n\n|Name|Type|Required|Restrictions|Description|\n|---|---|---|---|---|\n|Digest|string|false|none|A digest acts as a content identifier, enabling content addressability.|\n\nanyOf\n\n|Name|Type|Required|Restrictions|Description|\n|---|---|---|---|---|\n|*anonymous*|string|false|none|SHA256|\n\nor\n\n|Name|Type|Required|Restrictions|Description|\n|---|---|---|---|---|\n|*anonymous*|string|false|none|SHA512|\n\nor\n\n|Name|Type|Required|Restrictions|Description|\n|---|---|---|---|---|\n|*anonymous*|string|false|none|BLAKE3<br><br>**Currently not implemented.**|\n\n<h2 id=\"tocS_distribution\">distribution</h2>\n<!-- backwards compatibility -->\n<a id=\"schemadistribution\"></a>\n<a id=\"schema_distribution\"></a>\n<a id=\"tocSdistribution\"></a>\n<a id=\"tocsdistribution\"></a>\n\n```json\n{\n  \"id\": \"1\",\n  \"did\": \"ubuntu\",\n  \"name\": \"Ubuntu\",\n  \"version\": \"18.04.3 LTS (Bionic Beaver)\",\n  \"version_code_name\": \"bionic\",\n  \"version_id\": \"18.04\",\n  \"pretty_name\": \"Ubuntu 18.04.3 LTS\"\n}\n\n```\n\nDistribution\n\n### Properties\n\n|Name|Type|Required|Restrictions|Description|\n|---|---|---|---|---|\n|id|string|true|none|Unique ID for this Distribution. May be unique to the response document, not the whole system.|\n|did|string|false|none|A lower-case string (no spaces or other characters outside of 0–9, a–z, \".\", \"_\", and \"-\") identifying the operating system, excluding any version information and suitable for processing by scripts or usage in generated filenames.|\n|name|string|false|none|A string identifying the operating system.|\n|version|string|false|none|A string identifying the operating system version, excluding any OS name information, possibly including a release code name, and suitable for presentation to the user.|\n|version_code_name|string|false|none|A lower-case string (no spaces or other characters outside of 0–9, a–z, \".\", \"_\", and \"-\") identifying the operating system release code name, excluding any OS name information or release version, and suitable for processing by scripts or usage in generated filenames.|\n|version_id|string|false|none|A lower-case string (mostly numeric, no spaces or other characters outside of 0–9, a–z, \".\", \"_\", and \"-\") identifying the operating system version, excluding any OS name information or release code name.|\n|arch|string|false|none|A string identifying the OS architecture.|\n|cpe|[cpe.schema.json](#schemacpe.schema.json)|false|none|Common Platform Enumeration name.|\n|pretty_name|string|false|none|A pretty operating system name in a format suitable for presentation to the user.|\n\n<h2 id=\"tocS_environment\">environment</h2>\n<!-- backwards compatibility -->\n<a id=\"schemaenvironment\"></a>\n<a id=\"schema_environment\"></a>\n<a id=\"tocSenvironment\"></a>\n<a id=\"tocsenvironment\"></a>\n\n```json\n{\n  \"value\": {\n    \"package_db\": \"var/lib/dpkg/status\",\n    \"introduced_in\": \"sha256:35c102085707f703de2d9eaad8752d6fe1b8f02b5d2149f1d8357c9cc7fb7d0a\",\n    \"distribution_id\": \"1\"\n  }\n}\n\n```\n\nEnvironment\n\n### Properties\n\n|Name|Type|Required|Restrictions|Description|\n|---|---|---|---|---|\n|package_db|string|false|none|The database the associated Package was discovered in.|\n|distribution_id|string|false|none|The ID of the Distribution of the associated Package.|\n|introduced_in|[digest.schema.json](#schemadigest.schema.json)|false|none|The Layer the associated Package was introduced in.|\n|repository_ids|[string]|false|none|The IDs of the Repositories of the associated Package.|\n\n<h2 id=\"tocS_error\">error</h2>\n<!-- backwards compatibility -->\n<a id=\"schemaerror\"></a>\n<a id=\"schema_error\"></a>\n<a id=\"tocSerror\"></a>\n<a id=\"tocserror\"></a>\n\n```json\n{\n  \"code\": \"string\",\n  \"message\": \"string\"\n}\n\n```\n\nError\n\n### Properties\n\n|Name|Type|Required|Restrictions|Description|\n|---|---|---|---|---|\n|code|string|false|none|a code for this particular error|\n|message|string|true|none|a message with further detail|\n\n<h2 id=\"tocS_index_report\">index_report</h2>\n<!-- backwards compatibility -->\n<a id=\"schemaindex_report\"></a>\n<a id=\"schema_index_report\"></a>\n<a id=\"tocSindex_report\"></a>\n<a id=\"tocsindex_report\"></a>\n\n```json\n{\n  \"manifest_hash\": null,\n  \"state\": \"string\",\n  \"err\": \"string\",\n  \"success\": true,\n  \"packages\": {},\n  \"distributions\": {},\n  \"repository\": {},\n  \"environments\": {\n    \"property1\": [],\n    \"property2\": []\n  }\n}\n\n```\n\nIndex Report\n\n### Properties\n\n|Name|Type|Required|Restrictions|Description|\n|---|---|---|---|---|\n|manifest_hash|[digest.schema.json](#schemadigest.schema.json)|true|none|The Manifest's digest.|\n|state|string|true|none|The current state of the index operation|\n|err|string|false|none|An error message on event of unsuccessful index|\n|success|boolean|true|none|A bool indicating succcessful index|\n|packages|object|false|none|A map of Package objects indexed by a document-local identifier.|\n|» **additionalProperties**|[package.schema.json](#schemapackage.schema.json)|false|none|none|\n|distributions|object|false|none|A map of Distribution objects indexed by a document-local identifier.|\n|» **additionalProperties**|[distribution.schema.json](#schemadistribution.schema.json)|false|none|none|\n|repository|object|false|none|A map of Repository objects indexed by a document-local identifier.|\n|» **additionalProperties**|[repository.schema.json](#schemarepository.schema.json)|false|none|none|\n|environments|object|false|none|A map of Environment arrays indexed by a Package's identifier.|\n|» **additionalProperties**|[[environment.schema.json](#schemaenvironment.schema.json)]|false|none|none|\n\n<h2 id=\"tocS_index_state\">index_state</h2>\n<!-- backwards compatibility -->\n<a id=\"schemaindex_state\"></a>\n<a id=\"schema_index_state\"></a>\n<a id=\"tocSindex_state\"></a>\n<a id=\"tocsindex_state\"></a>\n\n```json\n{\n  \"state\": \"string\"\n}\n\n```\n\nIndex State\n\n### Properties\n\n|Name|Type|Required|Restrictions|Description|\n|---|---|---|---|---|\n|state|string|true|none|an opaque token|\n\n<h2 id=\"tocS_layer\">layer</h2>\n<!-- backwards compatibility -->\n<a id=\"schemalayer\"></a>\n<a id=\"schema_layer\"></a>\n<a id=\"tocSlayer\"></a>\n<a id=\"tocslayer\"></a>\n\n```json\n{\n  \"hash\": null,\n  \"uri\": \"string\",\n  \"headers\": {},\n  \"media_type\": \"string\"\n}\n\n```\n\nLayer\n\n### Properties\n\n|Name|Type|Required|Restrictions|Description|\n|---|---|---|---|---|\n|hash|[digest.schema.json](#schemadigest.schema.json)|true|none|Digest of the layer blob.|\n|uri|string|true|none|A URI indicating where the layer blob can be downloaded from.|\n|headers|object|false|none|Any additional HTTP-style headers needed for requesting layers.|\n|» ^[a-zA-Z0-9\\-_]+$|[string]|false|none|none|\n|media_type|string|false|none|The OCI Layer media type for this layer.|\n\n<h2 id=\"tocS_manifest\">manifest</h2>\n<!-- backwards compatibility -->\n<a id=\"schemamanifest\"></a>\n<a id=\"schema_manifest\"></a>\n<a id=\"tocSmanifest\"></a>\n<a id=\"tocsmanifest\"></a>\n\n```json\n{\n  \"hash\": \"sha256:fc84b5febd328eccaa913807716887b3eb5ed08bc22cc6933a9ebf82766725e3\",\n  \"layers\": [\n    {\n      \"hash\": \"sha256:2f077db56abccc19f16f140f629ae98e904b4b7d563957a7fc319bd11b82ba36\",\n      \"uri\": \"https://storage.example.com/blob/2f077db56abccc19f16f140f629ae98e904b4b7d563957a7fc319bd11b82ba36\",\n      \"headers\": {\n        \"Authoriztion\": [\n          \"Bearer hunter2\"\n        ]\n      }\n    }\n  ]\n}\n\n```\n\nManifest\n\n### Properties\n\n|Name|Type|Required|Restrictions|Description|\n|---|---|---|---|---|\n|hash|[digest.schema.json](#schemadigest.schema.json)|true|none|The OCI Image Manifest's digest.<br><br>This is used as an identifier throughout the system. This **SHOULD** be the same as the OCI Image Manifest's digest, but this is not enforced.|\n|layers|[[layer.schema.json](#schemalayer.schema.json)]|false|none|The OCI Layers making up the Image, in order.|\n\n<h2 id=\"tocS_normalized_severity\">normalized_severity</h2>\n<!-- backwards compatibility -->\n<a id=\"schemanormalized_severity\"></a>\n<a id=\"schema_normalized_severity\"></a>\n<a id=\"tocSnormalized_severity\"></a>\n<a id=\"tocsnormalized_severity\"></a>\n\n```json\n\"Unknown\"\n\n```\n\nNormalized Severity\n\n### Properties\n\n|Name|Type|Required|Restrictions|Description|\n|---|---|---|---|---|\n|Normalized Severity|any|false|none|Standardized severity values.|\n\n#### Enumerated Values\n\n|Property|Value|\n|---|---|\n|Normalized Severity|Unknown|\n|Normalized Severity|Negligible|\n|Normalized Severity|Low|\n|Normalized Severity|Medium|\n|Normalized Severity|High|\n|Normalized Severity|Critical|\n\n<h2 id=\"tocS_notification_page\">notification_page</h2>\n<!-- backwards compatibility -->\n<a id=\"schemanotification_page\"></a>\n<a id=\"schema_notification_page\"></a>\n<a id=\"tocSnotification_page\"></a>\n<a id=\"tocsnotification_page\"></a>\n\n```json\n{\n  \"page\": {\n    \"size\": 100,\n    \"next\": \"1b4d0db2-e757-4150-bbbb-543658144205\"\n  },\n  \"notifications\": [\n    {\n      \"id\": \"5e4b387e-88d3-4364-86fd-063447a6fad2\",\n      \"manifest\": \"sha256:35c102085707f703de2d9eaad8752d6fe1b8f02b5d2149f1d8357c9cc7fb7d0a\",\n      \"reason\": \"added\",\n      \"vulnerability\": {\n        \"name\": \"CVE-2009-5155\",\n        \"fixed_in_version\": \"v0.0.1\",\n        \"links\": \"http://example.com/CVE-2009-5155\",\n        \"description\": \"In the GNU C Library (aka glibc or libc6) before 2.28, parse_reg_exp in posix/regcomp.c misparses alternatives, which allows attackers to cause a denial of service (assertion failure and application exit) or trigger an incorrect result by attempting a regular-expression match.\\\"\",\n        \"normalized_severity\": \"Unknown\",\n        \"package\": {\n          \"id\": \"10\",\n          \"name\": \"libapt-pkg5.0\",\n          \"version\": \"1.6.11\",\n          \"kind\": \"BINARY\",\n          \"arch\": \"x86\",\n          \"source\": {\n            \"id\": \"9\",\n            \"name\": \"apt\",\n            \"version\": \"1.6.11\",\n            \"kind\": \"SOURCE\",\n            \"source\": null\n          }\n        },\n        \"distribution\": {\n          \"id\": \"1\",\n          \"did\": \"ubuntu\",\n          \"name\": \"Ubuntu\",\n          \"version\": \"18.04.3 LTS (Bionic Beaver)\",\n          \"version_code_name\": \"bionic\",\n          \"version_id\": \"18.04\",\n          \"pretty_name\": \"Ubuntu 18.04.3 LTS\"\n        }\n      }\n    }\n  ]\n}\n\n```\n\nNotification Page\n\n### Properties\n\n|Name|Type|Required|Restrictions|Description|\n|---|---|---|---|---|\n|page|object|true|none|An object informing the client the next page to retrieve.|\n|» size|integer|true|none|The number of notifications contained in this page.|\n|» next|string|false|none|The identififer to pass into the \"next\" parameter of a future GetNotification request.<br><br>If not present, there are no additional pages.|\n|notifications|[[notification.schema.json](#schemanotification.schema.json)]|true|none|Notifications within this page.|\n\n<h2 id=\"tocS_notification\">notification</h2>\n<!-- backwards compatibility -->\n<a id=\"schemanotification\"></a>\n<a id=\"schema_notification\"></a>\n<a id=\"tocSnotification\"></a>\n<a id=\"tocsnotification\"></a>\n\n```json\n{\n  \"id\": \"string\",\n  \"manifest\": null,\n  \"reason\": \"added\",\n  \"vulnerability\": null\n}\n\n```\n\nNotification\n\n### Properties\n\n|Name|Type|Required|Restrictions|Description|\n|---|---|---|---|---|\n|id|string|true|none|Unique identifier for this notification.|\n|manifest|[digest.schema.json](#schemadigest.schema.json)|true|none|The digest of the manifest affected by the provided vulnerability.|\n|reason|any|true|none|The reason for the notifcation.|\n|vulnerability|[vulnerability_summary.schema.json](#schemavulnerability_summary.schema.json)|true|none|none|\n\n#### Enumerated Values\n\n|Property|Value|\n|---|---|\n|reason|added|\n|reason|removed|\n\n<h2 id=\"tocS_notification_webhook\">notification_webhook</h2>\n<!-- backwards compatibility -->\n<a id=\"schemanotification_webhook\"></a>\n<a id=\"schema_notification_webhook\"></a>\n<a id=\"tocSnotification_webhook\"></a>\n<a id=\"tocsnotification_webhook\"></a>\n\n```json\n{\n  \"notification_id\": \"string\",\n  \"callback\": \"http://example.com\"\n}\n\n```\n\nNotification Webhook\n\n### Properties\n\n|Name|Type|Required|Restrictions|Description|\n|---|---|---|---|---|\n|notification_id|string|true|none|Unique identifier for this notification.|\n|callback|string(uri)|true|none|A URL to retrieve paginated Notification objects.|\n\n<h2 id=\"tocS_package\">package</h2>\n<!-- backwards compatibility -->\n<a id=\"schemapackage\"></a>\n<a id=\"schema_package\"></a>\n<a id=\"tocSpackage\"></a>\n<a id=\"tocspackage\"></a>\n\n```json\n{\n  \"id\": \"10\",\n  \"name\": \"libapt-pkg5.0\",\n  \"version\": \"1.6.11\",\n  \"kind\": \"binary\",\n  \"normalized_version\": \"\",\n  \"arch\": \"x86\",\n  \"module\": \"\",\n  \"cpe\": \"\",\n  \"source\": {\n    \"id\": \"9\",\n    \"name\": \"apt\",\n    \"version\": \"1.6.11\",\n    \"kind\": \"source\",\n    \"source\": null\n  }\n}\n\n```\n\nPackage\n\n### Properties\n\n|Name|Type|Required|Restrictions|Description|\n|---|---|---|---|---|\n|id|string|false|none|Unique ID for this Package. May be unique to the response document, not the whole system.|\n|name|string|true|none|Identifier of this Package.<br><br>The uniqueness and scoping of this name depends on the packaging system.|\n|version|string|true|none|Version of this Package, as reported by the packaging system.|\n|kind|any|false|none|The \"kind\" of this Package.|\n|source|[#](#schema#)|false|none|Source Package that produced the current binary Package, if known.|\n|normalized_version|string|false|none|Normalized representation of the discoverd version.<br><br>The format is not specific, but is guarenteed to be forward compatible.|\n|module|string|false|none|An identifier for intra-Repository grouping of packages.<br><br>Likely only relevant on rpm-based systems.|\n|arch|string|false|none|Native architecture for the Package.|\n|cpe|[cpe.schema.json](#schemacpe.schema.json)|false|none|CPE Name for the Package.|\n\n#### Enumerated Values\n\n|Property|Value|\n|---|---|\n|kind|BINARY|\n|kind|SOURCE|\n\n<h2 id=\"tocS_range\">range</h2>\n<!-- backwards compatibility -->\n<a id=\"schemarange\"></a>\n<a id=\"schema_range\"></a>\n<a id=\"tocSrange\"></a>\n<a id=\"tocsrange\"></a>\n\n```json\n{\n  \"[\": \"string\",\n  \")\": \"string\"\n}\n\n```\n\nRange\n\n### Properties\n\n|Name|Type|Required|Restrictions|Description|\n|---|---|---|---|---|\n|[|string|false|none|Lower bound, inclusive.|\n|)|string|false|none|Upper bound, exclusive.|\n\n<h2 id=\"tocS_repository\">repository</h2>\n<!-- backwards compatibility -->\n<a id=\"schemarepository\"></a>\n<a id=\"schema_repository\"></a>\n<a id=\"tocSrepository\"></a>\n<a id=\"tocsrepository\"></a>\n\n```json\n{\n  \"id\": \"string\",\n  \"name\": \"string\",\n  \"key\": \"string\",\n  \"uri\": \"http://example.com\",\n  \"cpe\": null\n}\n\n```\n\nRepository\n\n### Properties\n\n|Name|Type|Required|Restrictions|Description|\n|---|---|---|---|---|\n|id|string|true|none|Unique ID for this Repository. May be unique to the response document, not the whole system.|\n|name|string|false|none|Human-relevant name for the Repository.|\n|key|string|false|none|Machine-relevant name for the Repository.|\n|uri|string(uri)|false|none|URI describing the Repository.|\n|cpe|[cpe.schema.json](#schemacpe.schema.json)|false|none|CPE name for the Repository.|\n\n<h2 id=\"tocS_update_diff\">update_diff</h2>\n<!-- backwards compatibility -->\n<a id=\"schemaupdate_diff\"></a>\n<a id=\"schema_update_diff\"></a>\n<a id=\"tocSupdate_diff\"></a>\n<a id=\"tocsupdate_diff\"></a>\n\n```json\n{\n  \"prev\": null,\n  \"cur\": null,\n  \"added\": [],\n  \"removed\": []\n}\n\n```\n\nUpdate Difference\n\n### Properties\n\n|Name|Type|Required|Restrictions|Description|\n|---|---|---|---|---|\n|prev|[update_operation.schema.json](#schemaupdate_operation.schema.json)|false|none|The previous Update Operation.|\n|cur|[update_operation.schema.json](#schemaupdate_operation.schema.json)|true|none|The current Update Operation.|\n|added|[[vulnerability.schema.json](#schemavulnerability.schema.json)]|true|none|Vulnerabilities present in \"cur\", but not \"prev\".|\n|removed|[[vulnerability.schema.json](#schemavulnerability.schema.json)]|true|none|Vulnerabilities present in \"prev\", but not \"cur\".|\n\n<h2 id=\"tocS_update_operation\">update_operation</h2>\n<!-- backwards compatibility -->\n<a id=\"schemaupdate_operation\"></a>\n<a id=\"schema_update_operation\"></a>\n<a id=\"tocSupdate_operation\"></a>\n<a id=\"tocsupdate_operation\"></a>\n\n```json\n{\n  \"ref\": \"d0fad5d6-e996-4437-8fb9-5f40bbcfd7cc\",\n  \"updater\": \"string\",\n  \"fingerprint\": \"string\",\n  \"date\": \"2019-08-24T14:15:22Z\",\n  \"kind\": \"vulnerability\"\n}\n\n```\n\nUpdate Operation\n\n### Properties\n\n|Name|Type|Required|Restrictions|Description|\n|---|---|---|---|---|\n|ref|string(uuid)|true|none|A unique identifier for this update operation.|\n|updater|string|true|none|The \"updater\" component that was run.|\n|fingerprint|string|true|none|The stored \"fingerprint\" of this run.|\n|date|string(date-time)|true|none|When this operation was run.|\n|kind|any|true|none|The kind of data this operation updated.|\n\n#### Enumerated Values\n\n|Property|Value|\n|---|---|\n|kind|vulnerability|\n|kind|enrichment|\n\n<h2 id=\"tocS_update_operations\">update_operations</h2>\n<!-- backwards compatibility -->\n<a id=\"schemaupdate_operations\"></a>\n<a id=\"schema_update_operations\"></a>\n<a id=\"tocSupdate_operations\"></a>\n<a id=\"tocsupdate_operations\"></a>\n\n```json\n{\n  \"property1\": [],\n  \"property2\": []\n}\n\n```\n\nUpdate Operations\n\n### Properties\n\n|Name|Type|Required|Restrictions|Description|\n|---|---|---|---|---|\n|**additionalProperties**|[[update_operation.schema.json](#schemaupdate_operation.schema.json)]|false|none|none|\n\n<h2 id=\"tocS_vulnerability_core\">vulnerability_core</h2>\n<!-- backwards compatibility -->\n<a id=\"schemavulnerability_core\"></a>\n<a id=\"schema_vulnerability_core\"></a>\n<a id=\"tocSvulnerability_core\"></a>\n<a id=\"tocsvulnerability_core\"></a>\n\n```json\n{\n  \"name\": \"string\",\n  \"fixed_in_version\": \"string\",\n  \"severity\": \"string\",\n  \"normalized_severity\": null,\n  \"range\": null,\n  \"arch_op\": \"equals\",\n  \"package\": null,\n  \"distribution\": null,\n  \"repository\": null\n}\n\n```\n\nVulnerability Core\n\n### Properties\n\n|Name|Type|Required|Restrictions|Description|\n|---|---|---|---|---|\n|name|string|true|none|Human-readable name, as presented in the vendor data.|\n|fixed_in_version|string|false|none|Version string, as presented in the vendor data.|\n|severity|string|false|none|Severity, as presented in the vendor data.|\n|normalized_severity|[normalized_severity.schema.json](#schemanormalized_severity.schema.json)|true|none|A well defined set of severity strings guaranteed to be present.|\n|range|[range.schema.json](#schemarange.schema.json)|false|none|Range of versions the vulnerability applies to.|\n|arch_op|any|false|none|Flag indicating how the referenced package's \"arch\" member should be interpreted.|\n|package|[package.schema.json](#schemapackage.schema.json)|false|none|A package description|\n|distribution|[distribution.schema.json](#schemadistribution.schema.json)|false|none|A distribution description|\n|repository|[repository.schema.json](#schemarepository.schema.json)|false|none|A repository description|\n\nanyOf\n\n|Name|Type|Required|Restrictions|Description|\n|---|---|---|---|---|\n|*anonymous*|object|false|none|none|\n\nor\n\n|Name|Type|Required|Restrictions|Description|\n|---|---|---|---|---|\n|*anonymous*|object|false|none|none|\n\nor\n\n|Name|Type|Required|Restrictions|Description|\n|---|---|---|---|---|\n|*anonymous*|object|false|none|none|\n\n#### Enumerated Values\n\n|Property|Value|\n|---|---|\n|arch_op|equals|\n|arch_op|not equals|\n|arch_op|pattern match|\n\n<h2 id=\"tocS_vulnerability_report\">vulnerability_report</h2>\n<!-- backwards compatibility -->\n<a id=\"schemavulnerability_report\"></a>\n<a id=\"schema_vulnerability_report\"></a>\n<a id=\"tocSvulnerability_report\"></a>\n<a id=\"tocsvulnerability_report\"></a>\n\n```json\n{\n  \"manifest_hash\": null,\n  \"packages\": {},\n  \"distributions\": {},\n  \"repository\": {},\n  \"environments\": {\n    \"property1\": [],\n    \"property2\": []\n  },\n  \"vulnerabilities\": {},\n  \"package_vulnerabilities\": {\n    \"property1\": [\n      \"string\"\n    ],\n    \"property2\": [\n      \"string\"\n    ]\n  },\n  \"enrichments\": {\n    \"property1\": [],\n    \"property2\": []\n  }\n}\n\n```\n\nVulnerability Report\n\n### Properties\n\n|Name|Type|Required|Restrictions|Description|\n|---|---|---|---|---|\n|manifest_hash|[digest.schema.json](#schemadigest.schema.json)|true|none|The Manifest's digest.|\n|packages|object|true|none|A map of Package objects indexed by a document-local identifier.|\n|» **additionalProperties**|[package.schema.json](#schemapackage.schema.json)|false|none|none|\n|distributions|object|true|none|A map of Distribution objects indexed by a document-local identifier.|\n|» **additionalProperties**|[distribution.schema.json](#schemadistribution.schema.json)|false|none|none|\n|repository|object|false|none|A map of Repository objects indexed by a document-local identifier.|\n|» **additionalProperties**|[repository.schema.json](#schemarepository.schema.json)|false|none|none|\n|environments|object|true|none|A map of Environment arrays indexed by a Package's identifier.|\n|» **additionalProperties**|[[environment.schema.json](#schemaenvironment.schema.json)]|false|none|none|\n|vulnerabilities|object|true|none|A map of Vulnerabilities indexed by a document-local identifier.|\n|» **additionalProperties**|[vulnerability.schema.json](#schemavulnerability.schema.json)|false|none|none|\n|package_vulnerabilities|object|true|none|A mapping of Vulnerability identifier lists indexed by Package identifier.|\n|» **additionalProperties**|[string]|false|none|none|\n|enrichments|object|false|none|A mapping of extra \"enrichment\" data by type|\n|» **additionalProperties**|array|false|none|none|\n\n<h2 id=\"tocS_vulnerability\">vulnerability</h2>\n<!-- backwards compatibility -->\n<a id=\"schemavulnerability\"></a>\n<a id=\"schema_vulnerability\"></a>\n<a id=\"tocSvulnerability\"></a>\n<a id=\"tocsvulnerability\"></a>\n\n```json\nfalse\n\n```\n\nVulnerability\n\n### Properties\n\n*None*\n\n<h2 id=\"tocS_vulnerability_summaries\">vulnerability_summaries</h2>\n<!-- backwards compatibility -->\n<a id=\"schemavulnerability_summaries\"></a>\n<a id=\"schema_vulnerability_summaries\"></a>\n<a id=\"tocSvulnerability_summaries\"></a>\n<a id=\"tocsvulnerability_summaries\"></a>\n\n```json\n[]\n\n```\n\nVulnerability Summaries\n\n### Properties\n\n|Name|Type|Required|Restrictions|Description|\n|---|---|---|---|---|\n|Vulnerability Summaries|[[vulnerability_summary.schema.json](#schemavulnerability_summary.schema.json)]|false|none|**This is an internal type, documented for completeness.**<br><br>This is an array of pseudo-Vulnerability objects used for reverse-lookup.|\n\n<h2 id=\"tocS_vulnerability_summary\">vulnerability_summary</h2>\n<!-- backwards compatibility -->\n<a id=\"schemavulnerability_summary\"></a>\n<a id=\"schema_vulnerability_summary\"></a>\n<a id=\"tocSvulnerability_summary\"></a>\n<a id=\"tocsvulnerability_summary\"></a>\n\n```json\nfalse\n\n```\n\nVulnerability Summary\n\n### Properties\n\n*None*\n\n"
  },
  {
    "path": "Documentation/reference/clairctl.md",
    "content": "# Clairctl\n\n`clairctl` is a command line tool for working with Clair.\nThis CLI is capable of generating manifests from most public registries\n(dockerhub, quay.io, Red Hat Container Catalog) and submitting them for\nanalysis to a running Clair.\n\nNote that if the Clair instance has authentication configured, the value\nprovided to the `issuer` flag must be on the list accepted by the server.\n\n```\nNAME:\n   clairctl - interact with a clair API\n\nUSAGE:\n   clairctl [global options] command [command options] [arguments...]\n\nVERSION:\n   0.1.0\n\nDESCRIPTION:\n   A command-line tool for clair v4.\n\nCOMMANDS:\n   manifest         print a clair manifest for the named container\n   report           request vulnerability reports for the named containers\n   export-updaters  run updaters and export results\n   import-updaters  import updates\n   help, h          Shows a list of commands or help for one command\n\nGLOBAL OPTIONS:\n   -D                           print debugging logs (default: false)\n   --config value, -c value     clair configuration file (default: \"config.yaml\") [$CLAIR_CONF]\n   --issuer value, --iss value  jwt \"issuer\" to use when making authenticated requests (default: \"clairctl\")\n   --help, -h                   show help (default: false)\n   --version, -v                print the version (default: false)\n```\n\n```\nNAME:\n   clairctl manifest - print a clair manifest for the named container\n\nUSAGE:\n   clairctl manifest [arguments...]\n\nDESCRIPTION:\n   print a clair manifest for the named container\n```\n\n```\nNAME:\n   clairctl report - request vulnerability reports for the named containers\n\nUSAGE:\n   clairctl report [command options] container...\n\nDESCRIPTION:\n   Request and print a Clair vulnerability report for the named container(s).\n\nOPTIONS:\n   --host value           URL for the clairv4 v1 API. (default: \"http://localhost:6060/\") [$CLAIR_API]\n   --out value, -o value  output format: text, json, xml (default: text)\n```\n\n```\nNAME:\n   clairctl export-updaters - run updaters and export results\n\nUSAGE:\n   clairctl export-updaters [command options] [out]\n\nDESCRIPTION:\n   Run configured exporters and export to a file.\n\n   A configuration file is needed to run this command, see 'clairctl help'\n   for how to specify one.\n\nOPTIONS:\n   --strict  Return non-zero exit when updaters report errors. (default: false)\n```\n\n```\nNAME:\n   clairctl import-updaters - import updates\n\nUSAGE:\n   clairctl import-updaters input...\n\nDESCRIPTION:\n   Import updates from files or HTTP URIs.\n\n   A configuration file is needed to run this command, see 'clairctl help'\n   for how to specify one.\n```\n"
  },
  {
    "path": "Documentation/reference/config.md",
    "content": "# Config\n\n## CLI Flags And Environment Variables\n\nClair is configured by a structured yaml or JSON[^1] file and an optional directory of \"merge\" and \"patch\" documents[^1].\nEach Clair node needs to specify what mode it will run in and a path to a configuration file via CLI flags or environment variables.\n\nFor example:\n```shell\n$ clair -conf ./path/to/config.yaml -mode indexer\n$ clair -conf ./path/to/config.yaml -mode matcher\n```\n\n```\n-mode \n    (also specified by CLAIR_MODE env variable)\n    One of the following strings\n    Sets which mode the clair instances will run in\n    \n    \"indexer\": runs just the indexer node\n    \"matcher\": runs just the matcher node\n    \"notifier\": runs just the notifier node\n    \"combo\": will run all services on the same node.\n-conf\n    (also specified by CLAIR_CONF env variable)\n    A file system path to Clair's config file\n```\n\nThe above example starts two Clair nodes using the same configuration.\nOne will only run the indexing facilities while the other will only run the matching facilities.\n\nEnvironment variables respected by the Go standard library can be specified\nif needed. Some notable examples:\n\n* `HTTP_PROXY`\n* `HTTPS_PROXY`\n* `SSL_CERT_DIR`\n\nIf running in \"combo\" mode you **must** supply the `indexer`, `matcher`,\nand `notifier` configuration blocks in the configuration.\n\n## Configuration dropins\n\nStarting in Clair version `4.7.0`, dropin configuration files are supported.\n\nGiven a root configurtaion file of `/etc/clair/config.json`, all files matching the globs `/etc/clair/config.json.d/*.json` and `/etc/clair/config.json.d/*.json-patch` would be loaded in lexical order after the root configuration file.\nSimilarly, given `/etc/clair/config.yaml`, all files matching the globs `/etc/clair/config.yaml.d/*.yaml` and `/etc/clair/config.yaml.d/*.yaml-patch` would be loaded.\nOnly the extensions `yaml` and `json` are supported, and indicate yaml and JSON formatting, respectively.\n\nThe dropin files must have the same extension and format as the root file.\nDropins with the bare suffix are treated as [merge documents](rfc7386).\nDropins with the `-patch` suffix are treated as [patch documents](rfc6902) and must contain a valid [RFC 6902](rfc6902) structure.\nYaml documents must be resolvable to the JSON subset.\n\nTake care with the [merge](rfc7386) behavior around lists; a patch operation may be more suitable.\nThe `clairctl check-config` command can be used to ensure a merged configuration is what is intended.\nIn addition, placing `test` operations in a patch file that's evaluated last (such as `zz-validate.json-patch`) can be used to have Clair refuse to start if some configuration values are not what is intended.\n\nThe application defaults are applied *after* the configuration is loaded and as such, not reflected in the `clairctl check-config` command.\nThe output of that command is also not currently suitable to be used to \"compile\" a config to a single file.\n\n[rfc7386]: https://datatracker.ietf.org/doc/html/rfc7386\n[rfc6902]: https://datatracker.ietf.org/doc/html/rfc6902\n\n## Deprecations and Changes\n\nStarting in version `4.7.0`, unknown keys are disallowed.\nConfigurations that looked valid previously and loaded fine may now cause Clair to refuse to start.\n\nIn version `4.8.0`, using Jaeger for trace submission was deprecated.\nConfigurations that use Jaeger will print a warning.\nIn future versions, using the Jaeger format may cause an error.\n\n## Configuration Reference\n\nPlease see the [go module documentation][godoc_config] for additional documentation on defaults and use.\n\n[godoc_config]: https://pkg.go.dev/github.com/quay/clair/config\n\n```\nhttp_listen_addr: \"\"\nintrospection_addr: \"\"\nlog_level: \"\"\ntls: {}\nindexer:\n    connstring: \"\"\n    scanlock_retry: 0\n    layer_scan_concurrency: 0\n    migrations: false\n    scanner: {}\n    airgap: false\nmatcher:\n    connstring: \"\"\n    indexer_addr: \"\"\n    migrations: false\n    period: \"\"\n    disable_updaters: false\n    update_retention: 2\nmatchers:\n    names: nil\n    config: nil\nupdaters:\n    sets: nil\n    config: nil\nnotifier:\n    connstring: \"\"\n    migrations: false\n    indexer_addr: \"\"\n    matcher_addr: \"\"\n    poll_interval: \"\"\n    delivery_interval: \"\"\n    disable_summary: false\n    webhook: null\n    amqp: null\n    stomp: null\nauth: \n  psk: nil\ntrace:\n    name: \"\"\n    probability: null\n    jaeger:\n        agent:\n            endpoint: \"\"\n        collector:\n            endpoint: \"\"\n            username: null\n            password: null\n        service_name: \"\"\n        tags: nil\n        buffer_max: 0\n    otlp:\n\t  http: {}\n      grpc: {}\nmetrics:\n    name: \"\"\n    prometheus:\n        endpoint: null\n```\n\nNote: the above just lists every key for completeness. Copy-pasting the above as\na starting point for configuration will result in some options not having their\ndefaults set normally.\n<!---\nThe following are purposefully omitted. See comments in the config package for\nmore information.\n\n# `$.tls.root_ca`\n# `$.updaters.filter`\n# `$.notifier.webhook.signed`\n# `$.auth.keyserver`\n# `$.auth.keyserver.api`\n# `$.auth.keyserver.intraservice`\n# `$.trace.otlp.http.client_tls`\n# `$.trace.otlp.http.client_tls.root_ca`\n# `$.trace.otlp.grpc.client_tls`\n# `$.trace.otlp.grpc.client_tls.root_ca`\n# `$.metrics.otlp.http.client_tls`\n# `$.metrics.otlp.http.client_tls.root_ca`\n# `$.metrics.otlp.grpc.client_tls`\n# `$.metrics.otlp.grpc.client_tls.root_ca`\n-->\n\n### `$.http_listen_addr`\nA string in `<host>:<port>` format where `<host>` can be an empty string.\n\nThis configures where the HTTP API is exposed.\nSee `/openapi/v1` for the API spec.\n\n### `$.introspection_addr`\nA string in `<host>:<port>` format where `<host>` can be an empty string.\n\nThis configures where Clair's metrics and health endpoints are exposed.\n\n### `$.log_level`\nSet the logging level.\n\nOne of the following strings:\n* debug-color\n* debug\n* info\n* warn\n* error\n* fatal\n* panic\n\n### `$.tls`\nTLS is a map containing the config for serving the HTTP API over TLS (and\nHTTP/2).\n\n#### `$.tls.cert`\nThe TLS certificate to be used. Must be a full-chain certificate, as in nginx.\n\n#### `$.tls.key`\nA key file for the TLS certificate. Encryption is not supported on the key.\n\n### `$.indexer`\nIndexer provides Clair Indexer node configuration.\n\n#### `$.indexer.airgap`\nDisables HTTP access to the Internet for indexers and fetchers.\nPrivate IPv4 and IPv6 addresses are allowed.\nDatabase connections are unaffected.\n\n#### `$.indexer.connstring`\nA Postgres connection string.\n\nAccepts a format as a url (e.g.,\n`postgres://pqgotest:password@localhost/pqgotest?sslmode=verify-full`)\nor a libpq connection string (e.g.,\n`user=pqgotest dbname=pqgotest sslmode=verify-full`).\n\n#### `$.indexer.index_report_request_concurrency`\nInteger.\n\nRate limits the number of index report creation requests.\n\nSetting this to 0 will attempt to auto-size this value. Setting a negative value\nmeans \"unlimited.\" The auto-sizing is a multiple of the number of available\ncores.\n\nThe API will return a 429 status code if concurrency is exceeded.\n\n#### `$.indexer.scanlock_retry`\nA positive integer representing seconds.\n\nConcurrent Indexers lock on manifest scans to avoid clobbering.\nThis value tunes how often a waiting Indexer will poll for the lock.\n<!--TODO: Move to async operating mode -->\n\n#### `$.indexer.layer_scan_concurrency`\nPositive integer limiting the number of concurrent layer scans.\n\nIndexers will index a Manifest's layers concurrently.\nThis value tunes the number of layers an Indexer will scan in parallel.\n\n#### `$.indexer.migrations`\nA boolean value.\n\nWhether Indexer nodes handle migrations to their database.\n\n#### `$.indexer.scanner`\nIndexer configurations.\n\nScanner allows for passing configuration options to layer scanners.\nThe scanner will have this configuration passed to it on construction if\ndesigned to do so.\n\n#### `$.indexer.scanner.dist`\nA map with the name of a particular scanner and arbitrary yaml as a value.\n\n#### `$.indexer.scanner.package`\nA map with the name of a particular scanner and arbitrary yaml as a value.\n\n#### `$.indexer.scanner.repo`\nA map with the name of a particular scanner and arbitrary yaml as a value.\n\n### `$.matcher`\nMatcher provides Clair matcher node configuration.\n\n#### `$.matcher.cache_age`\nDuration string.\n\nControls how long clients should be hinted to cache responses for.\n\n#### `$.matcher.connstring`\nA Postgres connection string.\n\nAccepts a format as a url (e.g.,\n`postgres://pqgotest:password@localhost/pqgotest?sslmode=verify-full`)\nor a libpq connection string (e.g.,\n`user=pqgotest dbname=pqgotest sslmode=verify-full`).\n\n#### `$.matcher.max_conn_pool`\nA positive integer limiting the database connection pool size.\n\nClair allows for a custom connection pool size.\nThis number will directly set how many active database\nconnections are allowed concurrently.\n\nThis parameter will be ignored in a future version.\nUsers should configure this through the connection string.\n\n#### `$.matcher.indexer_addr`\nA string in `<host>:<port>` format where `<host>` can be an empty string.\n\nA Matcher contacts an Indexer to create a VulnerabilityReport.\nThe location of this Indexer is required.\n\n#### `$.matcher.migrations`\nA boolean value.\n\nWhether Matcher nodes handle migrations to their databases.\n\n#### `$.matcher.period`\nA time.ParseDuration parseable string.\n\nDetermines how often updates for new security advisories will take place.\n\nDefaults to 6 hours.\n\n#### `$.matcher.disable_updaters`\nA boolean value.\n\nWhether to run background updates or not.\n\n#### `$.matcher.disable_enrichment`\nA boolean value.\n\nWhether to enrich the returned vulnerabilities or not.\n\n#### `$.matcher.update_retention`\nAn integer value limiting the number of update operations kept in the database.\n\nSets the number of update operations to retain between garbage collection\ncycles. This should be set to a safe MAX value based on database size\nconstraints.\n\nDefaults to 10.\n\nIf a value less than 0 is provided, GC is disabled. 2 is the minimum value to\nensure updates can be compared for notifications. \n\n### `$.matchers`\nMatchers provides configuration for the in-tree Matchers and RemoteMatchers.\n\n#### `$.matchers.names`\nA list of string values informing the matcher factory about enabled matchers.\n\nIf the value is nil the default list of Matchers will run:\n* alpine-matcher\n* aws-matcher\n* debian-matcher\n* gobin\n* java-maven\n* oracle\n* photon\n* python\n* rhel\n* rhel-container-matcher\n* suse\n* ubuntu-matcher\n\nIf an empty list is provided zero matchers will run.\n\n#### `$.matchers.config`\nProvides configuration to specific matcher.\n\nA map keyed by the name of the matcher containing a sub-object which\nwill be provided to the matchers factory constructor.\n\nA hypothetical example:\n\n    config:\n      python:\n        ignore_vulns:\n          - CVE-XYZ\n          - CVE-ABC\n\n### `$.updaters`\nUpdaters provides configuration for the Matcher's update manager.\n\n#### `$.updaters.sets`\nA list of string values informing the update manager which Updaters to run.\n\nIf the value is nil (or `null` in yaml) the default set of Updaters will run:\n* alpine\n* aws\n* debian\n* oracle\n* osv\n* photon\n* rhcc\n* rhel\n* suse\n* ubuntu\n\nIf an empty list is provided zero updaters will run.\n\n#### `$.updaters.config`\nProvides configuration to specific updater sets.\n\nA map keyed by the name of the updater set name containing a sub-object\nwhich will be provided to the updater set's constructor.\n\nA hypothetical example:\n\n    config:\n      ubuntu:\n        security_tracker_url: http://security.url\n        ignore_distributions: \n          - cosmic\n\n### `$.notifier`\nNotifier provides Clair notifier node configuration.\n\n#### `$.notifier.connstring`\nA Postgres connection string.\n\nAccepts a format as a url (e.g.,\n`postgres://pqgotest:password@localhost/pqgotest?sslmode=verify-full`)\nor a libpq connection string (e.g.,\n`user=pqgotest dbname=pqgotest sslmode=verify-full`).\n\n#### `$.notifier.migrations`\nA boolean value.\n\nWhether Notifier nodes handle migrations to their database.\n\n#### `$.notifier.indexer_addr`\nA string in `<host>:<port>` format where `<host>` can be an empty string.\n\nA Notifier contacts an Indexer to create obtain manifests affected by\nvulnerabilities. The location of this Indexer is required.\n\n#### `$.notifier.matcher_addr`\nA string in `<host>:<port>` format where `<host>` can be an empty string.\n\nA Notifier contacts a Matcher to list update operations and acquire diffs.\nThe location of this Indexer is required.\n\n#### `$.notifier.poll_interval`\nA time.ParseDuration parsable string.\n\nThe frequency at which the notifier will query at Matcher for Update Operations.\n\n#### `$.notifier.delivery_interval`\nA time.ParseDuration parsable string.\n\nThe frequency at which the notifier attempt delivery of created or previously\nfailed notifications.\n\n#### `$.notifier.disable_summary`\nA boolean.\n\nControls whether notifications should be summarized to one per manifest or not.\n\n#### `$.notifier.webhook`\nConfigures the notifier for webhook delivery.\n\n#### `$.notifier.webhook.target`\nURL where the webhook will be delivered.\n\n#### `$.notifier.webhook.callback`\nThe callback url where notifications can be retrieved.\nThe notification ID will be appended to this url.\n\nThis will typically be where the clair notifier is hosted.\n\n#### `$.notifier.webhook.headers`\nA map associating a header name to a list of values.\n\n#### `$.notifier.amqp`\nConfigures the notifier for AMQP delivery.\n\nNote: Clair does not declare any AMQP components on its own.  All attempts\nto use an exchange or queue are passive only and will fail The broker\nadministrators should setup exchanges and queues ahead of time.\n\n#### `$.notifier.amqp.direct`\nA boolean value.\n\nIf true the Notifier will deliver individual notifications (not a callback)\nto the configured AMQP broker.\n\n#### `$.notifier.amqp.rollup`\nInteger 0 or greater.\n\nIf `direct` is true this value will inform notifier how many notifications\nto send in a single direct delivery.  For example, if `direct` is set to\n`true` and `rollup` is set to `5`, the notifier will deliver no more then\n5 notifications in a single json payload to the broker. Setting the value\nto 0 will effectively set it to 1.\n\n#### `$.notifier.amqp.exchange`\nThe AMQP Exchange to connect to.\n\n#### `$.notifier.amqp.exchange.name`\nstring value\n\nThe name of the exchange to connect to.\n\n#### `$.notifier.amqp.exchange.type`\nstring value\n\nThe type of the exchange. Typically:\n* direct\n* fanout\n* topic\n* headers\n\n#### `$.notifier.amqp.exchange.durability`\nbool value\n\nWhether the configured queue is durable or not.\n\n#### `$.notifier.amqp.exchange.auto_delete`\nbool value\n\nWhether the configured queue uses an auto_delete policy.\n\n#### `$.notifier.amqp.routing_key`\nstring value\n\nThe name of the routing key each notification will be sent with.\n\n#### `$.notifier.amqp.callback`\na URL string\n\nIf `direct` is `false`, this URL is provided in the notification callback sent\nto the broker. This URL should point to Clair's notification API endpoint.\n\n#### `$.notifier.amqp.uris`\nlist of URL strings\n\nA list of one or more AMQP brokers to connect to, in priority order.\n\n#### `$.notifier.amqp.tls`\nConfigures TLS connection to AMQP broker.\n\n#### `$.notifier.amqp.tls.root_ca`\nstring value\n\nThe filesystem path where a root CA can be read.\n\n#### `$.notifier.amqp.tls.cert`\nstring value\n\nThe filesystem path where a tls certificate can be read. Note that clair\nalso respects `SSL_CERT_DIR`, as documented for the Go `crypto/x509` package.\n\n#### `$.notifier.amqp.tls.key`\nstring value\n\nThe filesystem path where a TLS private key can be read.\n\n#### `$.notifier.stomp`\nConfigures the notifier for STOMP delivery.\n\n#### `$.notifier.stomp.direct`\nA boolean value.\n\nIf `true`, the Notifier will deliver individual notifications (not a\ncallback) to the configured STOMP broker.\n\n#### `$.notifier.stomp.rollup`\nInteger 0 or greater.\n\nIf `direct` is `true`, this value will limit the number of notifications\nsent in a single direct delivery.  For example, if `direct` is set to\n`true` and `rollup` is set to `5`, the notifier will deliver no more\nthen 5 notifications in a single json payload to the broker. Setting the value\nto 0 will effectively set it to 1.\n\n#### `$.notifier.stomp.callback`\na URL string\n\nIf `direct` is `false`, this URL is provided in the notification callback sent\nto the broker. This URL should point to Clair's notification API endpoint.\n\n#### `$.notifier.stomp.destination`\na string value\n\nThe STOMP destination to deliver notifications to. \n\n#### `$.notifier.stomp.uris`\nlist of URL strings\n\nA list of one or more STOMP brokers to connect to in priority order.\n\n#### `$.notifier.stomp.tls`\nConfigures TLS connection to STOMP broker.\n\n#### `$.notifier.stomp.tls.root_ca`\nstring value\n\nThe filesystem path where a root CA can be read.\nNote that clair also respects `SSL_CERT_DIR`, as documented for the Go\n`crypto/x509` package.\n\n#### `$.notifier.stomp.tls.cert`\nstring value\n\nThe filesystem path where a tls certificate can be read.\n\n#### `$.notifier.stomp.tls.key`\nstring value\n\nThe filesystem path where a tls private key can be read.\n\n#### `$.notifier.stomp.user`\nConfigures login details for the STOMP broker.\n\n#### `$.notifier.stomp.user.login`\nstring value\n\nThe STOMP login to connect with.\n\n#### `$.notifier.stomp.user.passcode`\nstring value\n\nThe STOMP passcode to connect with.\n\n### `$.auth`\nDefines ClairV4's external and intra-service JWT based authentication.\n\nIf multiple auth mechanisms are defined, Clair will pick one. Currently, there\nare not multiple mechanisms.\n\n### `$.auth.psk`\nDefines preshared key authentication.\n\n#### `$.auth.psk.key`\na string value\n\nA shared base64 encoded key distributed between all parties signing and\nverifying JWTs.\n\n#### `$.auth.psk.iss`\na list of string value\n\nA list of JWT issuers to verify. An empty list will accept any issuer in a\nJWT claim.\n\n### `$.trace`\nDefines distributed tracing configuration based on OpenTelemetry.\n\n#### `$.trace.name`\nWhich submission format to use, one of:\n- jaeger\n- otlp\n- sentry\n\n#### `$.trace.probability`\na float value\n\nThe probability a trace will occur.\n\n### `$.trace.jaeger`\nDefines values for Jaeger tracing.\n\n***NOTE***: Jaeger has deprecated using the `jaeger` protocol and encouraging users to migrate to OTLP,\nwhich Jaeger can ingest natively.\n\n#### `$.trace.jaeger.agent`\nDefines values for configuring delivery to a Jaeger agent.\n\n#### `$.trace.jaeger.agent.endpoint`\na string value\n\nAn address in `<host>:<post>` syntax where traces can be submitted.\n\n#### `$.trace.jaeger.collector`\nDefines values for configuring delivery to a Jaeger collector.\n\n#### `$.trace.jaeger.collector.endpoint`\na string value\n\nAn address in `<host>:<post>` syntax where traces can be submitted.\n\n#### `$.trace.jaeger.collector.username`\na string value\n\n#### `$.trace.jaeger.collector.password`\na string value\n\n#### `$.trace.jaeger.service_name`\na string value\n\n#### `$.trace.jaeger.tags`\na mapping of a string to a string\n\n#### `$.trace.jaeger.buffer_max`\nan integer value\n\n### `$.trace.otlp`\nConfiguration for OTLP traces.\n\nOnly one of the `http` or `grpc` keys should be provided.\n\n#### `$.trace.otlp.http`\nConfiguration for OTLP traces submitted by HTTP.\n\n##### `$.trace.otlp.http.url_path`\nRequest path to use for submissions.\nDefaults to `/v1/traces`.\n\n##### `$.trace.otlp.http.compression`\nCompression for payloads.\nOne of:\n- gzip\n- none\n\n##### `$.trace.otlp.http.endpoint`\n`Host:port` for submission. Defaults to `localhost:4318`.\n\n##### `$.trace.otlp.http.headers`\nKey-value pairs of additional headers for submissions.\n\n##### `$.trace.otlp.http.insecure`\nUse HTTP instead of HTTPS.\n\n##### `$.trace.otlp.http.timeout`\nMaximum of of time for a trace submission.\n\n##### `$.trace.otlp.http.client_tls.cert`\nClient certificate for connection.\n\n##### `$.trace.otlp.http.client_tls.key`\nKey for the certificate specified in `cert`.\n\n#### `$.trace.otlp.grpc`\nConfiguration for OTLP traces submitted by gRPC.\n\n##### `$.trace.otlp.grpc.reconnect`\nSets the minimum time between connection attempts.\n\n##### `$.trace.otlp.grpc.service_config`\nA string containing a JSON-format gRPC service config.\n\n##### `$.trace.otlp.grpc.compression`\nCompression for payloads.\nOne of:\n- gzip\n- none\n\n##### `$.trace.otlp.grpc.endpoint`\n`Host:port` for submission. Defaults to `localhost:4317`.\n\n##### `$.trace.otlp.grpc.headers`\nKey-value pairs of additional headers for submissions.\n\n##### `$.trace.otlp.grpc.insecure`\nDo not verify the server certificate.\n\n##### `$.trace.otlp.grpc.timeout`\nMaximum of of time for a trace submission.\n\n##### `$.trace.otlp.grpc.client_tls.cert`\nClient certificate for connection.\n\n##### `$.trace.otlp.grpc.client_tls.key`\nKey for the certificate specified in `cert`.\n\n### `$.trace.sentry`\nConfiguration for submitting traces to Sentry.\n\nThis is done via OpenTelemetry instrumentation, so may not provide identical\nresults compared to other tracing backends or native Sentry instrumentation.\n\nThere's no integration for error submission.\nThis means that alternative implementations of the Sentry API that do not support submitting traces (such as [GlitchTip]) are not usable.\n\n[GlitchTip]: https://glitchtip.com/\n\n#### `$.trace.sentry.dsn`\nSentry DSN to use.\nSee also [`sentry-go.ClientOptions`](https://pkg.go.dev/github.com/getsentry/sentry-go#ClientOptions).\n\n#### `$.trace.sentry.environment`\nSentry environment to use.\nSee also [`sentry-go.ClientOptions`](https://pkg.go.dev/github.com/getsentry/sentry-go#ClientOptions).\n\n### `$.metrics`\nDefines distributed tracing configuration based on OpenTelemetry.\n\n#### `$.metrics.name`\na string value\n\n### `$.metrics.prometheus`\nConfiguration for a prometheus metrics exporter.\n\n#### `$.metrics.prometheus.endpoint`\na string value\n\nDefines the path where metrics will be served.\n\n### `$.metrics.otlp`\nConfiguration for OTLP metrics.\n\nOnly one of the `http` or `grpc` keys should be provided.\n\n#### `$.metrics.otlp.http`\nConfiguration for OTLP metrics submitted by HTTP.\n\n##### `$.metrics.otlp.http.url_path`\nRequest path to use for submissions.\nDefaults to `/v1/metrics`.\n\n##### `$.metrics.otlp.http.compression`\nCompression for payloads.\nOne of:\n- gzip\n- none\n\n##### `$.metrics.otlp.http.endpoint`\n`Host:port` for submission. Defaults to `localhost:4318`.\n\n##### `$.metrics.otlp.http.headers`\nKey-value pairs of additional headers for submissions.\n\n##### `$.metrics.otlp.http.insecure`\nUse HTTP instead of HTTPS.\n\n##### `$.metrics.otlp.http.timeout`\nMaximum of of time for a metrics submission.\n\n##### `$.metrics.otlp.http.client_tls.cert`\nClient certificate for connection.\n\n##### `$.metrics.otlp.http.client_tls.key`\nKey for the certificate specified in `cert`.\n\n#### `$.metrics.otlp.grpc`\nConfiguration for OTLP metrics submitted by gRPC.\n\n##### `$.metrics.otlp.grpc.reconnect`\nSets the minimum time between connection attempts.\n\n##### `$.metrics.otlp.grpc.service_config`\nA string containing a JSON-format gRPC service config.\n\n##### `$.metrics.otlp.grpc.compression`\nCompression for payloads.\nOne of:\n- gzip\n- none\n\n##### `$.metrics.otlp.grpc.endpoint`\n`Host:port` for submission. Defaults to `localhost:4318`.\n\n##### `$.metrics.otlp.grpc.headers`\nKey-value pairs of additional headers for submissions.\n\n##### `$.metrics.otlp.grpc.insecure`\nUse HTTP instead of HTTPS.\n\n##### `$.metrics.otlp.grpc.timeout`\nMaximum of of time for a metrics submission.\n\n##### `$.metrics.otlp.grpc.client_tls.cert`\nClient certificate for connection.\n\n##### `$.metrics.otlp.grpc.client_tls.key`\nKey for the certificate specified in `cert`.\n\n* * *\n\n[^1]: Support added in version `4.7.0`.\n"
  },
  {
    "path": "Documentation/reference/indexer.md",
    "content": "# Indexer\n\nWhen Clair is running in Indexer mode, it is responsible for receiving Manifests and generating IndexReports. An IndexReport is an intermediate representation of a manifest's content and is used to discover vulnerabilities.\n"
  },
  {
    "path": "Documentation/reference/matcher.md",
    "content": "# Matcher\n\nWhen Clair is running in Matcher mode it is responsible for receiving IndexReports and generating VulnerabilityReports. A VulnerabilityReport describes the contents of a manifest and any vulnerabilities affecting it.\n"
  },
  {
    "path": "Documentation/reference/metrics.md",
    "content": "Clair exports [metrics](./config.md#metrics) on the [introspection\nport](./config.md#introspection_addr) in the Prometheus format.\n\nThe exact metrics exposed are not considered API (and so are subject to change\nbetween releases) but should be well described. An up-to-date grafana dashboard\nexample is in `contrib/openshift/grafana`.\n"
  },
  {
    "path": "Documentation/reference/notifier.md",
    "content": "# Notifier \n\nWhen Clair is running in Notifier mode, it is responsible for generating notifications when new vulnerabilities affecting a previously indexed manifest enters the system. The notifier will send notifications via the configured mechanisms.\n"
  },
  {
    "path": "Documentation/reference.md",
    "content": "# Reference\nThe following sections provide reference information useful when exploring the documentation or working with Clair directly.\n\n- [API](./reference/api.md)\n- [Clairctl](./reference/clairctl.md)\n- [Config](./reference/config.md)\n- [Indexer](./reference/indexer.md)\n- [Matcher](./reference/matcher.md)\n- [Notifier](./reference/notifier.md)\n"
  },
  {
    "path": "Documentation/reference_test.go",
    "content": "package Documentation\n\nimport (\n\t\"bufio\"\n\t\"fmt\"\n\t\"os\"\n\t\"reflect\"\n\t\"regexp\"\n\t\"sort\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/quay/clair/config\"\n)\n\nfunc TestConfigReference(t *testing.T) {\n\tf, err := os.Open(\"reference/config.md\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer f.Close()\n\n\theader := regexp.MustCompile(\"^#+ `\\\\$[^`]+`\")\n\tvar got []string\n\ts := bufio.NewScanner(f)\n\tfor s.Scan() {\n\t\tif header.Match(s.Bytes()) {\n\t\t\tgot = append(got, strings.Trim(s.Text(), \" #`\"))\n\t\t}\n\t}\n\tif err := s.Err(); err != nil {\n\t\tt.Error(err)\n\t}\n\tvar want []string\n\tif err := walk(&want, \"$\", reflect.TypeOf(config.Config{})); err != nil {\n\t\tt.Error(err)\n\t}\n\tsort.Strings(want)\n\tsort.Strings(got)\n\tif !cmp.Equal(got, want) {\n\t\tt.Error(cmp.Diff(got, want))\n\t}\n}\n\ntype walkFunc func(interface{}) ([]string, error)\n\nfunc walk(ws *[]string, path string, t reflect.Type) error {\n\t// Dereference the pointer, if this is a pointer.\n\tif t.Kind() == reflect.Ptr {\n\t\tt = t.Elem()\n\t}\n\tif t.Kind() == reflect.Struct {\n\t\tfor i, lim := 0, t.NumField(); i < lim; i++ {\n\t\t\tf := t.Field(i)\n\t\t\tif f.Anonymous {\n\t\t\t\tif err := walk(ws, path, t.Field(i).Type); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tvar n string\n\t\t\tswitch t := f.Tag.Get(\"json\"); t {\n\t\t\tcase \"-\", \"\":\n\t\t\t\tcontinue\n\t\t\tdefault:\n\t\t\t\tif i := strings.IndexByte(t, ','); i != -1 {\n\t\t\t\t\tt = t[:i]\n\t\t\t\t}\n\t\t\t\tn = t\n\t\t\t}\n\t\t\tp := fmt.Sprintf(`%s.%s`, path, n)\n\t\t\t*ws = append(*ws, p)\n\t\t\tif err := walk(ws, p, t.Field(i).Type); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "Documentation/whatis.md",
    "content": "# What is Clair\n\nClair is an application for parsing image contents and reporting vulnerabilities affecting the contents. This is done via static analysis and not at runtime.\n\nClair supports the extraction of contents and assignment of vulnerabilities from the following official base containers:\n- Ubuntu\n- Debian\n- RHEL\n- Suse\n- Oracle\n- Alpine\n- AWS Linux\n- VMWare Photon\n- Python\n\nThe above list defines Clair's current support matrix.\n\n## Architecture\n\nClair v4 utilizes the [ClairCore](https://quay.github.io/claircore/) library as its engine for examining contents and reporting vulnerabilities. At a high level you can consider Clair a service wrapper to the functionality provided in the ClairCore library. \n\n![diagram of clairV4 highlevel architecture](./clairv4_arch.png)\n\nThe above diagram expresses the separation of concerns between Clair and the ClairCore library. Most development involving new distributions, vulnerability sources, and layer indexing will occur in ClairCore.\n\n## How Clair Works\n\nClair's analysis is broken into three distinct parts.\n\n### Indexing\n\nIndexing starts with submitting a Manifest to Clair. On receipt, Clair will fetch layers, scan their contents, and return an intermediate representation called an IndexReport. \n\nManifests are Clair's representation of a container image. Clair leverages the fact that OCI Manifests and Layers are content-addressed to reduce duplicated work.\n\nOnce a Manifest is indexed, the IndexReport is persisted for later retrieval. \n\n### Matching\n\nMatching is taking an IndexReport and correlating vulnerabilities affecting the manifest the report represents. \n\nClair is continually ingesting new security data and a request to the matcher will always provide you with the most up to date vulnerability analysis of an IndexReport.\n\n*How we implement indexing and matching in detail is covered in [ClairCore's](https://quay.github.io/claircore/) documentation.*\n\n### Notifications\n\nClair implements a notification service. \n\nWhen new vulnerabilities are discovered, the notifier service will determine if these vulnerabilities affect any indexed Manifests. The notifier will then take action according to its configuration.\n\n### Getting Started\n\nAt this point you'll probably want to check out [Getting Started With Clair](./howto/getting_started.md).\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\n"
  },
  {
    "path": "Makefile",
    "content": "# Copyright 2024 clair authors\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\n# Set the \"DEBUG\" variable to enable some debugging output for the Makefile\n# itself.\n\n.ONESHELL:\n.SHELL = /usr/bin/bash\n.SHELLFLAGS = -o pipefail -uec\n.DELETE_ON_ERROR:\ncomma:=,\nsplice = $(subst $(eval ) ,$(comma),$1)\n# See this file for all the variables that can be tuned by the environment.\ninclude etc/config.mk\n# Append shell patterns to this to hook the \"clean\" target.\nrm_pat =\n\nall:\n\t$(go) build ./cmd/...\n\ncontrib/openshift/grafana/dashboards/dashboard-clair.configmap.yaml: \\\n\tlocal-dev/grafana/provisioning/dashboards/clair.json \\\n\tcontrib/openshift/grafana/dashboard-clair.configmap.yaml.tpl\n\tname=$$(sed 's/[\\&/]/\\\\&/g;s/$$/\\\\n/;s/^/    /' $< | tr -d '\\n')\n\tsed \"s/GRAFANA_MANIFEST/$$name/\"\\\n\t\t$(word 2,$^)\\\n\t\t> $@\n# Intended to be checked-in, so not cleaned.\n\ninclude etc/container.mk\ninclude etc/dev.mk\ninclude etc/dist.mk\ninclude etc/doc.mk\n\nrm_flag := -rf\nifdef DEBUG\nrm_flag += -v\nendif\n.PHONY: clean\nclean:\n\t$(go) clean\n\trm $(rm_flag) -- $(rm_pat)\n"
  },
  {
    "path": "NOTICE",
    "content": "CoreOS Project\nCopyright 2015 CoreOS, Inc\n\nThis product includes software developed at CoreOS, Inc.\n(http://www.coreos.com/).\n"
  },
  {
    "path": "README.md",
    "content": "# Clair\n\n[![Docker Repository on Quay](https://quay.io/repository/projectquay/clair/status \"Docker Repository on Quay\")](https://quay.io/repository/projectquay/clair)\n[![PkgGoDev](https://pkg.go.dev/badge/github.com/quay/clair/v4 \"Go Documentation\")](https://pkg.go.dev/github.com/quay/clair/v4)\n[![IRC Channel](https://img.shields.io/badge/freenode-%23clair-blue.svg \"IRC Channel\")](http://webchat.freenode.net/?channels=clair)\n\n**Note**: The `main` branch may be in an *unstable or even broken state* during development.\nPlease use [releases] instead of the `main` branch in order to get stable binaries.\n\n![Clair Logo](https://cloud.githubusercontent.com/assets/343539/21630811/c5081e5c-d202-11e6-92eb-919d5999c77a.png)\n\nClair is an open source project for the [static analysis] of vulnerabilities in\napplication containers (currently including [OCI] and [docker]).\n\nClients use the Clair API to index their container images and can then match it against known vulnerabilities.\n\nOur goal is to enable a more transparent view of the security of container-based infrastructure.\nThus, the project was named `Clair` after the French term which translates to *clear*, *bright*, *transparent*.\n\n[The book] contains all the documentation on Clair's architecture and operation.\n\n[OCI]: https://github.com/opencontainers/image-spec/blob/master/spec.md\n[docker]: https://github.com/docker/docker/blob/master/image/spec/v1.2.md\n[releases]: https://github.com/quay/clair/releases\n[static analysis]: https://en.wikipedia.org/wiki/Static_program_analysis\n[The book]: https://quay.github.io/clair/\n\n## Community\n\n- Mailing List: [clair-dev@googlegroups.com](https://groups.google.com/forum/#!forum/clair-dev)\n- IRC: #[clair](irc://irc.freenode.org:6667/#clair) on freenode.org\n- Bugs: [issues](https://github.com/quay/clair/issues)\n\n## Contributing\n\nSee [CONTRIBUTING](.github/CONTRIBUTING.md) for details on submitting patches and the contribution workflow.\n\n## License\n\nClair is under the Apache 2.0 license. See the [LICENSE](LICENSE) file for details.\n"
  },
  {
    "path": "ROADMAP.md",
    "content": "# Clair Roadmap\n\nThis document defines a high level roadmap for Clair development.\n\nThe dates below should not be considered authoritative, but rather indicative of the projected timeline of the project.\nThe [milestones defined in GitHub](https://github.com/coreos/clair/milestones) represent the most up-to-date and issue-for-issue plans.\n\nThe roadmap below outlines new features that will be added to Clair, and while subject to change, define what future stable will look like.\n\n- Support multiple namespaces per image\n  - This enables language-level package managers (e.g. npm, pip) in the future\n- Take advantage of OCI/Docker content-addressiblity to avoid duplicated work\n  - This simplifies the amount of work required for an offline clair in the future\n- Support mappings between source packages and binary packages\n- Versioned detectors that are present in API results\n  - This will enable clients to determine when images need to be reindexed\n- gRPC API that works on sets of layers rather than individual layers\n- Structured logging in JSON\n- Improve coverage and readability of documentation\n"
  },
  {
    "path": "book.toml",
    "content": "[book]\ntitle = \"Clair Documentation\"\nauthors = [\"Clair Authors\"]\ndescription = \"Documentation for Clair.\"\nsrc = \"Documentation\"\nlanguage = \"en\"\n\n[build]\nbuild-dir = \"book\"\ncreate-missing = true\n\n[output.html]\ngit-repository-url= \"https://github.com/quay/clair\"\npreferred-dark-theme = \"coal\"\n"
  },
  {
    "path": "clair-error/errors.go",
    "content": "package clairerror\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/google/uuid\"\n)\n\n// ErrRequestFail indicates an http request failure\ntype ErrRequestFail struct {\n\tCode   int\n\tStatus string\n}\n\nfunc (e *ErrRequestFail) Error() string {\n\treturn fmt.Sprintf(\"code: %v status %v\", e.Code, e.Status)\n}\n\n// ErrBadManifest inidcates a manifest could not be parsed\ntype ErrBadManifest struct {\n\tE error\n}\n\nfunc (e *ErrBadManifest) Error() string {\n\treturn e.E.Error()\n}\n\nfunc (e *ErrBadManifest) Unwrap() error {\n\treturn e.E\n}\n\n// ErrBadManifest inidcates a manifest could not be parsed\ntype ErrBadIndexReport struct {\n\tE error\n}\n\nfunc (e *ErrBadIndexReport) Error() string {\n\treturn e.E.Error()\n}\n\nfunc (e *ErrBadIndexReport) Unwrap() error {\n\treturn e.E\n}\n\n// IndexStartErr indicates an index operation failed to start\ntype ErrIndexStart struct {\n\tE error\n}\n\nfunc (e *ErrIndexStart) Error() string {\n\treturn e.E.Error()\n}\n\nfunc (e *ErrIndexStart) Unwrap() error {\n\treturn e.E\n}\n\n// ErrIndexReportNotFound indicates a requested IndexReport was not found\ntype ErrIndexReportNotFound struct {\n\tHash string\n}\n\nfunc (e *ErrIndexReportNotFound) Error() string {\n\treturn fmt.Sprintf(\"failed to find manifest: %v\", e.Hash)\n}\n\n// ErrIndexReportRetrieval indicates an error while attempting to retrieve an IndexReport\ntype ErrIndexReportRetrieval struct {\n\tE error\n}\n\nfunc (e *ErrIndexReportRetrieval) Error() string {\n\treturn e.E.Error()\n}\n\nfunc (e *ErrIndexReportRetrieval) Unwrap() error {\n\treturn e.E\n}\n\n// ErrMatch indicates an issue with matching a IndexReport to a VulnerabilityReport\ntype ErrMatch struct {\n\tE error\n}\n\nfunc (e *ErrMatch) Error() string {\n\treturn e.E.Error()\n}\n\nfunc (e *ErrMatch) Unwrap() error {\n\treturn e.E\n}\n\n// ErrNotInitialized indicates an issue with initialization.\ntype ErrNotInitialized struct {\n\tMsg string\n}\n\nfunc (e ErrNotInitialized) Error() string {\n\treturn e.Msg\n}\n\n// ErrBadVulnerabilities indicates an issue where a set of Vulnerabilities could not be marshalled or unmarshalled\n// into JSON.\ntype ErrBadVulnerabilities struct {\n\tE error\n}\n\nfunc (e *ErrBadVulnerabilities) Error() string {\n\treturn e.E.Error()\n}\n\nfunc (e *ErrBadVulnerabilities) Unwrap() error {\n\treturn e.E\n}\n\n// ErrBadAffectedManifests indicates an issue where an AffectedManifests could not be marshalled or unmarshalled\n// into JSON.\ntype ErrBadAffectedManifests struct {\n\tE error\n}\n\nfunc (e *ErrBadAffectedManifests) Error() string {\n\treturn e.E.Error()\n}\n\nfunc (e *ErrBadAffectedManifests) Unwrap() error {\n\treturn e.E\n}\n\ntype ErrKeyNotFound struct {\n\tID uuid.UUID\n}\n\nfunc (e ErrKeyNotFound) Error() string {\n\treturn \"key with id \" + e.ID.String() + \" not found\"\n}\n"
  },
  {
    "path": "clair-error/notifications.go",
    "content": "package clairerror\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/google/uuid\"\n)\n\n// ErrNoUpdateOperation inidcates that the queried updater has no\n// update operations associated.\ntype ErrNoUpdateOperation struct {\n\tUpdater string\n}\n\nfunc (e ErrNoUpdateOperation) Error() string {\n\treturn fmt.Sprintf(\"updater %s has no associated update operations\", e.Updater)\n}\n\n// ErrBadNotification indicates a notification was malformed.\n// The wrapped error will contain further details.\ntype ErrBadNotification struct {\n\tNotificationID uuid.UUID\n\tE              error\n}\n\nfunc (e ErrBadNotification) Error() string {\n\treturn fmt.Sprintf(\"notification associated with id %s is malformed: %v\", e.NotificationID, e.E)\n}\n\nfunc (e ErrBadNotification) Unwrap() error {\n\treturn e.E\n}\n\n// ErrDeleteNotification indicates an error while deleting notifications.\n// The wrapped error will contain further details.\ntype ErrDeleteNotification struct {\n\tNotificationID uuid.UUID\n\tE              error\n}\n\nfunc (e ErrDeleteNotification) Error() string {\n\treturn fmt.Sprintf(\"notifications associated with id %s were not deleted: %v\", e.NotificationID, e.E)\n}\n\nfunc (e ErrDeleteNotification) Unwrap() error {\n\treturn e.E\n}\n\n// ErrNoReceipt is returned when a notification id has no associated Receipt.\ntype ErrNoReceipt struct {\n\tNotificationID uuid.UUID\n}\n\nfunc (e ErrNoReceipt) Error() string {\n\treturn fmt.Sprintf(\"no receipt exists for notification id %s\", e.NotificationID)\n}\n\n// ErrReceipt indicates an error retreiving a receipt for referenced notification id.\ntype ErrReceipt struct {\n\tNotificationID uuid.UUID\n\tE              error\n}\n\nfunc (e ErrReceipt) Error() string {\n\treturn fmt.Sprintf(\"failed to retrieve receipt for notification id %s: %v\", e.NotificationID, e.E)\n}\n\nfunc (e ErrReceipt) Unwrap() error {\n\treturn e.E\n}\n\n// ErrCreated indicates an error occurred when retrieving created notification ids.\ntype ErrCreated struct {\n\tE error\n}\n\nfunc (e ErrCreated) Error() string {\n\treturn fmt.Sprintf(\"failed to retrieve created notification ids: %v\", e.E)\n}\n\nfunc (e ErrCreated) Unwrap() error {\n\treturn e.E\n}\n\n// ErrFailed indicates an error occurred when retrieving created notification ids.\ntype ErrFailed struct {\n\tE error\n}\n\nfunc (e ErrFailed) Error() string {\n\treturn fmt.Sprintf(\"failed to retrieve failed notification ids: %v\", e.E)\n}\n\nfunc (e ErrFailed) Unwrap() error {\n\treturn e.E\n}\n\n// ErrPutNotifications indicates an issues occurred when persisting a slice of\n// computed notifications.\n// The wrapped error will contain further details.\ntype ErrPutNotifications struct {\n\tNotificationID uuid.UUID\n\tE              error\n}\n\nfunc (e ErrPutNotifications) Error() string {\n\treturn fmt.Sprintf(\"failed to persist notification associated with id %s: %v\", e.NotificationID.String(), e.E)\n}\n\nfunc (e ErrPutNotifications) Unwrap() error {\n\treturn e.E\n}\n\n// ErrDeliveryFailed indicates a failure to deliver a notification.\ntype ErrDeliveryFailed struct {\n\tE error\n}\n\nfunc (e ErrDeliveryFailed) Error() string {\n\treturn \"failed to deliver notification: \" + e.E.Error()\n}\n\nfunc (e ErrDeliveryFailed) Unwrap() error {\n\treturn e.E\n}\n"
  },
  {
    "path": "cmd/build.go",
    "content": "// Package cmd provides some common information to clair's binaries.\npackage cmd // import \"github.com/quay/clair/v4/cmd\"\n\nimport (\n\t\"runtime\"\n\t\"runtime/debug\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/prometheus/client_golang/prometheus\"\n\t\"github.com/prometheus/client_golang/prometheus/promauto\"\n)\n\n// Injected via git-export(1). See that man page and gitattributes(5).\nconst (\n\t// Needs a length check because GitHub zipballs/tarballs don't do the\n\t// describe and just strip the pattern.\n\tdescribe = `$Format:%(describe:match=v4.*)$`\n\trevision = `$Format:%h (%cI)$`\n)\n\nvar versionInfo = promauto.NewGaugeVec(\n\tprometheus.GaugeOpts{\n\t\tNamespace: \"clair\",\n\t\tSubsystem: \"cmd\",\n\t\tName:      \"version_info\",\n\t\tHelp:      \"Version information.\",\n\t},\n\t[]string{\n\t\t\"claircore_version\",\n\t\t\"goversion\",\n\t\t\"modified\",\n\t\t\"revision\",\n\t\t\"version\",\n\t},\n)\n\nfunc init() {\n\tif revision[0] != '$' {\n\t\t_, d, _ := strings.Cut(revision, \"(\")\n\t\tt, err := time.Parse(time.RFC3339, strings.TrimSuffix(d, \")\"))\n\t\tif err == nil {\n\t\t\tCommitDate = t\n\t\t}\n\t}\n\n\tmeta := prometheus.Labels{\n\t\t\"claircore_version\": \"\",\n\t\t\"goversion\":         runtime.Version(),\n\t\t\"modified\":          \"\",\n\t\t\"revision\":          revision,\n\t\t\"version\":           \"\",\n\t}\n\tinfo, infoOK := debug.ReadBuildInfo()\n\tvar vcs []string\n\tvar core string\n\tif infoOK {\n\t\t// If not OK, built without modules? Weird.\n\t\tfor _, s := range info.Settings {\n\t\t\tswitch s.Key {\n\t\t\tcase `vcs.revision`:\n\t\t\t\tmeta[\"revision\"] = s.Value\n\t\t\t\tvcs = append(vcs, `rev`, s.Value)\n\t\t\tcase `vcs.modified`:\n\t\t\t\tmeta[\"modified\"] = s.Value\n\t\t\t\tif s.Value == `true` {\n\t\t\t\t\tvcs = append(vcs, `(dirty)`)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t// If we can read out the current binary's debug info, find the\n\t\t// claircore version.\n\t\tfor _, m := range info.Deps {\n\t\t\tif m.Path != \"github.com/quay/claircore\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tcore = m.Version\n\t\t\tif m.Replace != nil && m.Replace.Version != m.Version {\n\t\t\t\tcore = m.Replace.Version\n\t\t\t}\n\t\t}\n\t\tmeta[\"claircore_version\"] = core\n\t}\n\n\tswitch {\n\tcase Version != \"\":\n\t\t// Had our version injected at build: do nothing.\n\tcase len(describe) > 0 && describe[0] != '$' && !strings.HasPrefix(describe, \"%(describe:\"):\n\t\t// Some git versions apparently don't know about the describe format\n\t\t// verb, so need to check that it's not just \"%(describe...\"\n\t\tVersion = describe\n\tcase revision[0] == '$':\n\t\tVersion = `(random source build)`\n\t\tif len(vcs) == 0 {\n\t\t\t// A `go run` invocation, perhaps.\n\t\t\tbreak\n\t\t}\n\t\tVersion = strings.Join(vcs, \" \")\n\tdefault:\n\t\tVersion = revision\n\t}\n\tmeta[\"version\"] = Version\n\tif core != \"\" {\n\t\tVersion += \" (claircore \" + core + \")\"\n\t}\n\tversionInfo.With(meta).Set(1)\n}\n\n// Version is a version string, injected at release time for release builds.\nvar Version string\n\n// CommitDate is the best guess of the source commit date.\n//\n// May be zero when the resulting code is not produced by a git export.\nvar CommitDate time.Time\n"
  },
  {
    "path": "cmd/clair/main.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"flag\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"net\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/signal\"\n\t\"runtime\"\n\t\"time\"\n\n\t\"github.com/quay/clair/config\"\n\t_ \"github.com/quay/claircore/updater/defaults\"\n\t\"golang.org/x/sync/errgroup\"\n\n\t\"github.com/quay/clair/v4/cmd\"\n\t\"github.com/quay/clair/v4/health\"\n\t\"github.com/quay/clair/v4/httptransport\"\n\t\"github.com/quay/clair/v4/initialize\"\n\t\"github.com/quay/clair/v4/initialize/auto\"\n\t\"github.com/quay/clair/v4/introspection\"\n)\n\nconst (\n\tenvConfig = `CLAIR_CONF`\n\tenvMode   = `CLAIR_MODE`\n)\n\nfunc main() {\n\tfail := false\n\tdefer func() {\n\t\tif fail {\n\t\t\tos.Exit(1)\n\t\t}\n\t}()\n\tbail := func(msg string, args ...any) {\n\t\tslog.Error(msg, args...)\n\t\tfail = true\n\t\truntime.Goexit()\n\t}\n\n\t// parse conf from cli\n\tvar conf config.Config\n\tflag.String(\"conf\", \"\", \"The file system path to Clair's config file.\")\n\tflag.String(\"mode\", \"\", \"The operation mode for this server, will default to combo.\")\n\tflag.Parse()\n\tflag.VisitAll(func(f *flag.Flag) {\n\t\tfv := f.Value.(flag.Getter).Get().(string)\n\t\tvar key string\n\t\tswitch f.Name {\n\t\tcase \"conf\":\n\t\t\tkey = envConfig\n\t\tcase \"mode\":\n\t\t\tkey = envMode\n\t\t}\n\t\tv, ok := os.LookupEnv(key)\n\t\tif fv == \"\" && !ok {\n\t\t\tbail(\"missing flag or environment variable\", \"flag\", \"-\"+f.Name, \"variable\", key)\n\t\t}\n\t\tif fv == \"\" && ok {\n\t\t\tfv = v\n\t\t}\n\t\tswitch f.Name {\n\t\tcase \"conf\":\n\t\t\tif err := cmd.LoadConfig(&conf, fv, true); err != nil {\n\t\t\t\tbail(\"failed loading config\", \"reason\", err)\n\t\t\t}\n\t\tcase \"mode\":\n\t\t\tif fv == \"\" {\n\t\t\t\tfv = \"combo\"\n\t\t\t}\n\t\t\tm, err := config.ParseMode(fv)\n\t\t\tif err != nil {\n\t\t\t\tbail(\"bad mode\", \"mode\", fv, \"reason\", err)\n\t\t\t}\n\t\t\tconf.Mode = m\n\t\t}\n\t})\n\n\t// Grab the warnings to print after the logger is configured.\n\tws, err := config.Validate(&conf)\n\tif err != nil {\n\t\tbail(\"failed to validate config\", \"reason\", err)\n\t}\n\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer cancel()\n\n\tif err := initialize.Logging(ctx, &conf); err != nil {\n\t\tbail(\"failed to set up logging\", \"reason\", err)\n\t}\n\tslog.InfoContext(ctx, \"starting\", \"version\", cmd.Version)\n\tif len(ws) != 0 {\n\t\tslog.InfoContext(ctx, \"configuration lints\",\n\t\t\t\"lint\", ws)\n\t}\n\tauto.PrintLogs(ctx)\n\n\t// Signal handler, for orderly shutdown.\n\tsig, stop := signal.NotifyContext(ctx, append(platformShutdown, os.Interrupt)...)\n\tdefer stop()\n\tslog.InfoContext(ctx, \"registered signal handler\")\n\tgo func() {\n\t\t<-sig.Done()\n\t\tstop()\n\t\tslog.InfoContext(ctx, \"unregistered signal handler\")\n\t}()\n\n\tsrvs, srvctx := errgroup.WithContext(sig)\n\tsrvs.Go(serveIntrospection(srvctx, &conf))\n\tsrvs.Go(serveAPI(srvctx, &conf))\n\n\tslog.InfoContext(ctx, \"ready\", \"version\", cmd.Version)\n\tif err := srvs.Wait(); err != nil {\n\t\tslog.ErrorContext(ctx, \"fatal error\", \"reason\", err)\n\t\tfail = true\n\t}\n}\n\nfunc serveAPI(ctx context.Context, cfg *config.Config) func() error {\n\treturn func() error {\n\t\tslog.InfoContext(ctx, \"launching http transport\")\n\t\tsrvs, err := initialize.Services(ctx, cfg)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"service initialization failed: %w\", err)\n\t\t}\n\t\tsrv := http.Server{\n\t\t\tBaseContext: func(_ net.Listener) context.Context {\n\t\t\t\treturn context.WithoutCancel(ctx)\n\t\t\t},\n\t\t}\n\t\tsrv.Handler, err = httptransport.New(ctx, cfg, srvs.Indexer, srvs.Matcher, srvs.Notifier)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"http transport configuration failed: %w\", err)\n\t\t}\n\t\tl, err := net.Listen(\"tcp\", cfg.HTTPListenAddr)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"http transport configuration failed: %w\", err)\n\t\t}\n\t\tif cfg.TLS != nil {\n\t\t\tcfg, err := cfg.TLS.Config()\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"tls configuration failed: %w\", err)\n\t\t\t}\n\t\t\tcfg.NextProtos = []string{\"h2\"}\n\t\t\tsrv.TLSConfig = cfg\n\t\t\tl = tls.NewListener(l, cfg)\n\t\t}\n\t\thealth.Ready()\n\n\t\tvar eg errgroup.Group\n\t\teg.Go(func() error {\n\t\t\tif err := srv.Serve(l); !errors.Is(err, http.ErrServerClosed) {\n\t\t\t\treturn fmt.Errorf(\"http transport failed to launch: %w\", err)\n\t\t\t}\n\t\t\treturn nil\n\t\t})\n\t\teg.Go(func() error {\n\t\t\t<-ctx.Done()\n\t\t\tctx, done := context.WithTimeoutCause(context.Background(), 10*time.Second, context.Cause(ctx))\n\t\t\tdefer done()\n\t\t\treturn srv.Shutdown(ctx)\n\t\t})\n\t\treturn eg.Wait()\n\t}\n}\n\nfunc serveIntrospection(ctx context.Context, cfg *config.Config) func() error {\n\treturn func() error {\n\t\tslog.InfoContext(ctx, \"launching introspection server\")\n\t\tsrv, err := introspection.New(ctx, cfg, nil)\n\t\tif err != nil {\n\t\t\tslog.WarnContext(ctx, \"introspection server configuration failed; continuing anyway\",\n\t\t\t\t\"reason\", err)\n\t\t\treturn nil\n\t\t}\n\n\t\tvar eg errgroup.Group\n\t\teg.Go(func() error {\n\t\t\tif err := srv.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {\n\t\t\t\tslog.WarnContext(ctx, \"introspection server failed to launch; continuing anyway\",\n\t\t\t\t\t\"reason\", err)\n\t\t\t}\n\t\t\treturn nil\n\t\t})\n\t\teg.Go(func() error {\n\t\t\t<-ctx.Done()\n\t\t\tctx, done := context.WithTimeoutCause(context.Background(), 10*time.Second, context.Cause(ctx))\n\t\t\tdefer done()\n\t\t\treturn srv.Shutdown(ctx)\n\t\t})\n\t\treturn eg.Wait()\n\t}\n}\n"
  },
  {
    "path": "cmd/clair/os_other.go",
    "content": "//go:build !unix\n\npackage main\n\nimport \"os\"\n\nvar platformShutdown = []os.Signal{}\n"
  },
  {
    "path": "cmd/clair/os_unix.go",
    "content": "//go:build unix\n\npackage main\n\nimport (\n\t\"os\"\n\t\"syscall\"\n)\n\nvar platformShutdown = []os.Signal{syscall.SIGTERM}\n"
  },
  {
    "path": "cmd/config.go",
    "content": "package cmd\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\tjsonpatch \"github.com/evanphx/json-patch/v5\"\n\t\"github.com/quay/clair/config\"\n\t\"gopkg.in/yaml.v3\"\n)\n\n// LoadConfig loads the named config file or reports an error.\n//\n// JSON and YAML formatted files are supported, as determined by the file extension (\"json\" or \"yaml\" -- \"yml\" is not supported).\n// If a directory suffixed with \".d\" exists (e.g. a file \"config.json\" and a directory \"config.json.d\"),\n// then all files with the same extension or the same extension suffixed with \"-patch\" will be loaded in lexical order and merged with the main configuration or applied as an RFC6902 patch, respectively.\n//\n// For example, given the paths:\n//\n//\tconfig.yaml\n//\tconfig.yaml.d/\n//\tconfig.yaml.d/secrets.yaml\n//\tconfig.yaml.d/override.yaml-patch\n//\tconfig.yaml.d/unloved.json-patch\n//\n// \"Config.yaml\" will be the base config,\n// \"override.yaml-patch\" will be applied as a patch to the base config,\n// \"secrets.yaml\" will be merged into the base config,\n// and \"unloved.json-patch\" will be ignored.\n//\n// The \"strict\" argument controls whether the function returns on the first\n// error, or runs the full routine and returns all accumulated errors at the\n// end.\nfunc LoadConfig(cfg *config.Config, name string, strict bool) error {\n\t// This function would probably benefit from some logging, but the logging\n\t// configuration is specified _inside_ the configuration, so it's hard to\n\t// say what should be done here.\n\tname = filepath.Clean(name)\n\text := filepath.Ext(name)\n\tswitch ext {\n\tcase \".yaml\": // OK\n\tcase \".json\": // OK\n\tdefault:\n\t\treturn fmt.Errorf(\"unknown config kind %q\", ext)\n\t}\n\tvar errs []error\n\n\tb, err := loadAsJSON(name)\n\tif err != nil {\n\t\tif strict {\n\t\t\treturn err\n\t\t}\n\t\terrs = append(errs, err)\n\t}\n\tdropinDir := name + \".d\"\n\terr = filepath.WalkDir(dropinDir, func(path string, d fs.DirEntry, err error) error {\n\t\tswitch {\n\t\tcase path == dropinDir:\n\t\t\treturn nil\n\t\tcase !errors.Is(err, nil):\n\t\t\treturn fmt.Errorf(\"error walking filesystem: %w\", err)\n\t\tcase d.IsDir():\n\t\t\treturn fs.SkipDir\n\t\t}\n\t\t// After this, make sure everything assigns errors to \"err\" so that the\n\t\t// non-strict behavior works.\n\n\t\tvar doc []byte\n\t\tswitch dext := filepath.Ext(path); {\n\t\tcase dext == ext:\n\t\t\tdoc, err = loadAsJSON(path)\n\t\t\tif err != nil {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tb, err = jsonpatch.MergePatch(b, doc)\n\t\t\tif err != nil {\n\t\t\t\terr = fmt.Errorf(\"error merging drop-in %q: %w\", path, err)\n\t\t\t\tbreak\n\t\t\t}\n\t\tcase dext == ext+\"-patch\":\n\t\t\tdoc, err = loadAsJSON(path)\n\t\t\tif err != nil {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tvar p jsonpatch.Patch\n\t\t\tp, err = jsonpatch.DecodePatch(doc)\n\t\t\tif err != nil {\n\t\t\t\terr = fmt.Errorf(\"bad patch %q: %w\", path, err)\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tb, err = p.Apply(b)\n\t\t\tif err != nil {\n\t\t\t\terr = fmt.Errorf(\"error applying patch %q: %w\", path, err)\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif err != nil {\n\t\t\tif strict {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\terrs = append(errs, err)\n\t\t}\n\t\treturn nil\n\t})\n\tswitch {\n\tcase errors.Is(err, nil):\n\tcase errors.Is(err, fs.ErrNotExist): // OK\n\tcase strict:\n\t\treturn err\n\tdefault:\n\t\terrs = append(errs, err)\n\t}\n\n\tif len(b) == 0 {\n\t\terr := fmt.Errorf(\"error load config %q: empty document after merges\", name)\n\t\tif strict {\n\t\t\treturn err\n\t\t}\n\t\terrs = append(errs, err)\n\t}\n\tdec := json.NewDecoder(bytes.NewReader(b))\n\tdec.DisallowUnknownFields()\n\tif err := dec.Decode(cfg); err != nil {\n\t\t// Hide that this error is coming from the `json` package, as it might\n\t\t// confuse people.\n\t\terr := fmt.Errorf(\"error decoding config %q: %s\", name, strings.TrimPrefix(err.Error(), `json: `))\n\t\tif strict {\n\t\t\treturn err\n\t\t}\n\t\terrs = append(errs, err)\n\t}\n\treturn errors.Join(errs...)\n}\n\nfunc loadAsJSON(path string) ([]byte, error) {\n\tb, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error reading file %q: %w\", path, err)\n\t}\n\text := filepath.Ext(path)\n\tswitch ext {\n\tcase \".json\", \".json-patch\":\n\t\tif len(b) < 2 {\n\t\t\treturn nil, fmt.Errorf(\"malformed file %q: not a JSON document\", path)\n\t\t}\n\tcase \".yaml\", \".yaml-patch\":\n\t\tvar y interface{}\n\t\tif err := yaml.Unmarshal(b, &y); err != nil {\n\t\t\tmsg := strings.TrimPrefix(err.Error(), `yaml: `)\n\t\t\treturn nil, fmt.Errorf(\"malformed file %q: %v\", path, msg)\n\t\t}\n\t\t// For arbitrary yaml documents we'd have to do a step to ensure there's\n\t\t// no disallowed constructs (binary keys, binary data tags) but we know\n\t\t// this should only ever be some snippet of our config.Config type.\n\t\tb, err = json.Marshal(y)\n\t\tif err != nil { // Not sure how this would happen. 🤔\n\t\t\tmsg := strings.TrimPrefix(err.Error(), `json: `)\n\t\t\treturn nil, fmt.Errorf(\"malformed file %q: %s\", path, msg)\n\t\t}\n\tdefault:\n\t\tpanic(\"programmer error: called on bad path\")\n\t}\n\tswitch ext {\n\tcase \".json\":\n\t\tif b[0] != '{' {\n\t\t\treturn nil, fmt.Errorf(\"malformed file %q: not a JSON object\", path)\n\t\t}\n\tcase \".json-patch\", \".yaml-patch\":\n\t\tif b[0] != '[' {\n\t\t\treturn nil, fmt.Errorf(\"malformed file %q: not a patch document\", path)\n\t\t}\n\tcase \".yaml\":\n\t\tif b[0] != '{' {\n\t\t\t// If this was an empty file (for some reason), note it and return an\n\t\t\t// empty JSON object. This can't happen with JSON -- we checked if it\n\t\t\t// meets the minimum size above.\n\t\t\tb = []byte(\"{}\")\n\t\t}\n\t}\n\treturn b, nil\n}\n"
  },
  {
    "path": "cmd/config_test.go",
    "content": "package cmd_test\n\nimport (\n\t\"encoding/json\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/quay/clair/config\"\n\n\t\"github.com/quay/clair/v4/cmd\"\n)\n\nfunc TestLoadConfig(t *testing.T) {\n\tms, err := filepath.Glob(`testdata/*/config.*[^d]`)\n\tif err != nil {\n\t\tpanic(\"programmer error\")\n\t}\n\tfor _, m := range ms {\n\t\tname := filepath.Base(filepath.Dir(m))\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\twantpath := filepath.Join(filepath.Dir(m), \"want.json\")\n\t\t\twf, err := os.Open(wantpath)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tdefer wf.Close()\n\t\t\tvar got, want config.Config\n\t\t\tif err := json.NewDecoder(wf).Decode(&want); err != nil {\n\t\t\t\tt.Error(err)\n\t\t\t}\n\t\t\tif err := cmd.LoadConfig(&got, m, true); err != nil {\n\t\t\t\tt.Error(err)\n\t\t\t}\n\t\t\tif !cmp.Equal(got, want) {\n\t\t\t\tt.Error(cmp.Diff(got, want))\n\t\t\t}\n\t\t})\n\t}\n\tms, err = filepath.Glob(`testdata/Error/*[^d]`)\n\tif err != nil {\n\t\tpanic(\"programmer error\")\n\t}\n\tt.Run(\"Error\", func(t *testing.T) {\n\t\tfor _, m := range ms {\n\t\t\tname := filepath.Base(m)\n\t\t\tname = strings.TrimSuffix(name, filepath.Ext(name))\n\t\t\tt.Run(name, func(t *testing.T) {\n\t\t\t\tvar got config.Config\n\t\t\t\terr := cmd.LoadConfig(&got, m, false)\n\t\t\t\tt.Log(err)\n\t\t\t\tif err == nil {\n\t\t\t\t\tt.Fail()\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "cmd/testdata/ComplexJSON/config.json",
    "content": "{\n\t\"http_listen_addr\": \":80\",\n\t\"log_level\": \"error\",\n\t\"matcher\": {}\n}\n"
  },
  {
    "path": "cmd/testdata/ComplexJSON/config.json.d/dropin.json",
    "content": "{\n  \"log_level\": \"error\"\n}\n"
  },
  {
    "path": "cmd/testdata/ComplexJSON/want.json",
    "content": "{\n\t\"http_listen_addr\": \":80\",\n\t\"log_level\": \"error\"\n}\n"
  },
  {
    "path": "cmd/testdata/ComplexYAML/config.yaml",
    "content": "---\nlog_level: debug-color\nintrospection_addr: \":8089\"\nhttp_listen_addr: \":6060\"\nupdaters:\n  sets:\n    - ubuntu\n    - debian\n    - rhel\n    - alpine\nauth:\n  psk:\n    key: 'c2VjcmV0'\n    iss:\n      - quay\n      - clairctl\nindexer:\n  connstring: host=clair-database user=clair dbname=indexer sslmode=disable\n  scanlock_retry: 10\n  layer_scan_concurrency: 5\n  migrations: true\nmatcher:\n  indexer_addr: http://clair-indexer:6060/\n  connstring: host=clair-database user=clair dbname=matcher sslmode=disable\n  max_conn_pool: 100\n  migrations: true\nmatchers: {}\nnotifier:\n  indexer_addr: http://clair-indexer:6060/\n  matcher_addr: http://clair-matcher:6060/\n  connstring: host=clair-database user=clair dbname=notifier sslmode=disable\n  migrations: true\n  delivery_interval: 30s\n  poll_interval: 1m\n  webhook:\n    target: \"http://webhook-target/\"\n    callback: \"http://clair-notifier:6060/notifier/api/v1/notification/\"\n  # amqp:\n  #   direct: true\n  #   exchange:\n  #     name: \"\"\n  #     type: \"direct\"\n  #     durable: true\n  #     auto_delete: false\n  #   uris: [\"amqp://guest:guest@clair-rabbitmq:5672/\"]\n  #   routing_key: \"notifications\"\n  #   callback: \"http://clair-notifier/notifier/api/v1/notifications\"\n# tracing and metrics config\ntrace:\n  name: \"jaeger\"\n#  probability: 1\n  jaeger:\n    agent:\n      endpoint: \"clair-jaeger:6831\"\n    service_name: \"clair\"\nmetrics:\n  name: \"prometheus\"\n"
  },
  {
    "path": "cmd/testdata/ComplexYAML/config.yaml.d/dropin.yaml",
    "content": "log_level: null\n"
  },
  {
    "path": "cmd/testdata/ComplexYAML/config.yaml.d/empty.yaml",
    "content": "\n"
  },
  {
    "path": "cmd/testdata/ComplexYAML/config.yaml.d/ignored.json-patch",
    "content": "# This file is ignored -- look at all this invalid JSON!\n[\n  {\n    \"op\": \"add\",\n    \"path\": \"/updaters/sets/-\",\n    \"value\": \"osv\",\n  },\n]\n"
  },
  {
    "path": "cmd/testdata/ComplexYAML/config.yaml.d/later.yaml",
    "content": "log_level: panic\nupdaters:\n  sets:\n    - rhel\n"
  },
  {
    "path": "cmd/testdata/ComplexYAML/config.yaml.d/updater.yaml-patch",
    "content": "- op: add\n  path: /updaters/sets/-\n  value: osv\n"
  },
  {
    "path": "cmd/testdata/ComplexYAML/want.json",
    "content": "{\n  \"log_level\": \"panic\",\n  \"introspection_addr\": \":8089\",\n  \"http_listen_addr\": \":6060\",\n  \"updaters\": {\n    \"sets\": [\n      \"rhel\",\n      \"osv\"\n    ]\n  },\n  \"auth\": {\n    \"psk\": {\n      \"key\": \"c2VjcmV0\",\n      \"iss\": [\n        \"quay\",\n        \"clairctl\"\n      ]\n    }\n  },\n  \"indexer\": {\n    \"connstring\": \"host=clair-database user=clair dbname=indexer sslmode=disable\",\n    \"scanlock_retry\": 10,\n    \"layer_scan_concurrency\": 5,\n    \"migrations\": true\n  },\n  \"matcher\": {\n    \"indexer_addr\": \"http://clair-indexer:6060/\",\n    \"connstring\": \"host=clair-database user=clair dbname=matcher sslmode=disable\",\n    \"max_conn_pool\": 100,\n    \"migrations\": true\n  },\n  \"notifier\": {\n    \"indexer_addr\": \"http://clair-indexer:6060/\",\n    \"matcher_addr\": \"http://clair-matcher:6060/\",\n    \"connstring\": \"host=clair-database user=clair dbname=notifier sslmode=disable\",\n    \"migrations\": true,\n    \"delivery_interval\": \"30s\",\n    \"poll_interval\": \"1m\",\n    \"webhook\": {\n      \"target\": \"http://webhook-target/\",\n      \"callback\": \"http://clair-notifier:6060/notifier/api/v1/notification/\"\n    }\n  },\n  \"trace\": {\n    \"name\": \"jaeger\",\n    \"jaeger\": {\n      \"agent\": {\n        \"endpoint\": \"clair-jaeger:6831\"\n      },\n      \"service_name\": \"clair\"\n    }\n  },\n  \"metrics\": {\n    \"name\": \"prometheus\"\n  }\n}\n"
  },
  {
    "path": "cmd/testdata/Error/BadKind.toml",
    "content": ""
  },
  {
    "path": "cmd/testdata/Error/BadPatch.json",
    "content": "{}\n"
  },
  {
    "path": "cmd/testdata/Error/BadPatch.json.d/decode.json-patch",
    "content": "[\n  [\n    null\n  ]\n]\n"
  },
  {
    "path": "cmd/testdata/Error/BadPatch.json.d/invalid.json-patch",
    "content": "[\n  {\n    \"op\": \"invalid\",\n    \"path\": \"any\",\n    \"value\": null\n  }\n]\n"
  },
  {
    "path": "cmd/testdata/Error/Indents.yaml",
    "content": "auth:\r\n    psk:\r\n        iss:\r\n            - quay\r\n            - clairctl\r\n        key: dmVyeXNlY3JldA0K  #gitleaks:allow\r\nhttp_listen_addr: :80\r\nindexer:\r\n    connstring: host=/var/run/postgresql\r\n    migrations: true\r\n    airgap: true\r\n    scanner:\r\n    repo:\r\n      rhel-repository-scanner:\r\n        repo2cpe_mapping_file: /config/repository-to-cpe.json\r\n    package:\r\n      rhel_containerscanner:\r\n        name2repos_mapping_file: /config/container-name-repos-map.json\r\nlog_level: info\r\nmatcher:\r\n    connstring: host=/var/run/postgresql\r\n    migrations: true\r\n    disable_updaters: true\r\nmetrics:\r\n    name: prometheus\r\nnotifier:\r\n    connstring: host=/var/run/postgresql\r\n    delivery_interval: 1m0s\r\n    migrations: true\r\n    poll_interval: 5m0s\r\n    webhook:\r\n        callback: http://clair/notifier/api/v1/notifications\r\n        target: https://quay/secscan/notification\r\n"
  },
  {
    "path": "cmd/testdata/Error/NotAnArray.json",
    "content": "{}\n"
  },
  {
    "path": "cmd/testdata/Error/NotAnArray.json.d/badpatch.json-patch",
    "content": "{}\n"
  },
  {
    "path": "cmd/testdata/Error/NotAnObject.json",
    "content": "[]\n"
  },
  {
    "path": "cmd/testdata/Error/NotYAML.yaml",
    "content": "key:\n\t- no tabs allowed\n"
  },
  {
    "path": "cmd/testdata/Error/TooShort.json",
    "content": "0"
  },
  {
    "path": "cmd/testdata/SimpleJSON/config.json",
    "content": "{\n\t\"http_listen_addr\": \":80\",\n\t\"log_level\": \"error\",\n\t\"matcher\": {}\n}\n"
  },
  {
    "path": "cmd/testdata/SimpleJSON/want.json",
    "content": "{\n\t\"http_listen_addr\": \":80\",\n\t\"log_level\": \"error\"\n}\n"
  },
  {
    "path": "cmd/testdata/SimpleYAML/config.yaml",
    "content": "---\nlog_level: debug-color\nintrospection_addr: \":8089\"\nhttp_listen_addr: \":6060\"\nupdaters:\n  sets:\n    - ubuntu\n    - debian\n    - rhel\n    - alpine\nauth:\n  psk:\n    key: 'c2VjcmV0'\n    iss:\n      - quay\n      - clairctl\nindexer:\n  connstring: host=clair-database user=clair dbname=indexer sslmode=disable\n  scanlock_retry: 10\n  layer_scan_concurrency: 5\n  migrations: true\nmatcher:\n  indexer_addr: http://clair-indexer:6060/\n  connstring: host=clair-database user=clair dbname=matcher sslmode=disable\n  max_conn_pool: 100\n  migrations: true\nmatchers: {}\nnotifier:\n  indexer_addr: http://clair-indexer:6060/\n  matcher_addr: http://clair-matcher:6060/\n  connstring: host=clair-database user=clair dbname=notifier sslmode=disable\n  migrations: true\n  delivery_interval: 30s\n  poll_interval: 1m\n  webhook:\n    target: \"http://webhook-target/\"\n    callback: \"http://clair-notifier:6060/notifier/api/v1/notification/\"\n  # amqp:\n  #   direct: true\n  #   exchange:\n  #     name: \"\"\n  #     type: \"direct\"\n  #     durable: true\n  #     auto_delete: false\n  #   uris: [\"amqp://guest:guest@clair-rabbitmq:5672/\"]\n  #   routing_key: \"notifications\"\n  #   callback: \"http://clair-notifier/notifier/api/v1/notifications\"\n# tracing and metrics config\ntrace:\n  name: \"jaeger\"\n#  probability: 1\n  jaeger:\n    agent:\n      endpoint: \"clair-jaeger:6831\"\n    service_name: \"clair\"\nmetrics:\n  name: \"prometheus\"\n"
  },
  {
    "path": "cmd/testdata/SimpleYAML/want.json",
    "content": "{\n  \"log_level\": \"debug-color\",\n  \"introspection_addr\": \":8089\",\n  \"http_listen_addr\": \":6060\",\n  \"updaters\": {\n    \"sets\": [\n      \"ubuntu\",\n      \"debian\",\n      \"rhel\",\n      \"alpine\"\n    ]\n  },\n  \"auth\": {\n    \"psk\": {\n      \"key\": \"c2VjcmV0\",\n      \"iss\": [\n        \"quay\",\n        \"clairctl\"\n      ]\n    }\n  },\n  \"indexer\": {\n    \"connstring\": \"host=clair-database user=clair dbname=indexer sslmode=disable\",\n    \"scanlock_retry\": 10,\n    \"layer_scan_concurrency\": 5,\n    \"migrations\": true\n  },\n  \"matcher\": {\n    \"indexer_addr\": \"http://clair-indexer:6060/\",\n    \"connstring\": \"host=clair-database user=clair dbname=matcher sslmode=disable\",\n    \"max_conn_pool\": 100,\n    \"migrations\": true\n  },\n  \"notifier\": {\n    \"indexer_addr\": \"http://clair-indexer:6060/\",\n    \"matcher_addr\": \"http://clair-matcher:6060/\",\n    \"connstring\": \"host=clair-database user=clair dbname=notifier sslmode=disable\",\n    \"migrations\": true,\n    \"delivery_interval\": \"30s\",\n    \"poll_interval\": \"1m\",\n    \"webhook\": {\n      \"target\": \"http://webhook-target/\",\n      \"callback\": \"http://clair-notifier:6060/notifier/api/v1/notification/\"\n    }\n  },\n  \"trace\": {\n    \"name\": \"jaeger\",\n    \"jaeger\": {\n      \"agent\": {\n        \"endpoint\": \"clair-jaeger:6831\"\n      },\n      \"service_name\": \"clair\"\n    }\n  },\n  \"metrics\": {\n    \"name\": \"prometheus\"\n  }\n}\n"
  },
  {
    "path": "code-of-conduct.md",
    "content": "## CoreOS Community Code of Conduct\n\n### Contributor Code of Conduct\n\nAs contributors and maintainers of this project, and in the interest of\nfostering an open and welcoming community, we pledge to respect all people who\ncontribute through reporting issues, posting feature requests, updating\ndocumentation, submitting pull requests or patches, and other activities.\n\nWe are committed to making participation in this project a harassment-free\nexperience for everyone, regardless of level of experience, gender, gender\nidentity and expression, sexual orientation, disability, personal appearance,\nbody size, race, ethnicity, age, religion, or nationality.\n\nExamples of unacceptable behavior by participants include:\n\n* The use of sexualized language or imagery\n* Personal attacks\n* Trolling or insulting/derogatory comments\n* Public or private harassment\n* Publishing others' private information, such as physical or electronic addresses, without explicit permission\n* Other unethical or unprofessional conduct.\n\nProject maintainers have the right and responsibility to remove, edit, or\nreject comments, commits, code, wiki edits, issues, and other contributions\nthat are not aligned to this Code of Conduct. By adopting this Code of Conduct,\nproject maintainers commit themselves to fairly and consistently applying these\nprinciples to every aspect of managing this project. Project maintainers who do\nnot follow or enforce the Code of Conduct may be permanently removed from the\nproject team.\n\nThis code of conduct applies both within project spaces and in public spaces\nwhen an individual is representing the project or its community.\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported by contacting a project maintainer, Brandon Philips\n<brandon.philips@coreos.com>, and/or Rithu John <rithu.john@coreos.com>.\n\nThis Code of Conduct is adapted from the Contributor Covenant\n(http://contributor-covenant.org), version 1.2.0, available at\nhttp://contributor-covenant.org/version/1/2/0/\n\n### CoreOS Events Code of Conduct\n\nCoreOS events are working conferences intended for professional networking and\ncollaboration in the CoreOS community. Attendees are expected to behave\naccording to professional standards and in accordance with their employer’s\npolicies on appropriate workplace behavior.\n\nWhile at CoreOS events or related social networking opportunities, attendees\nshould not engage in discriminatory or offensive speech or actions including\nbut not limited to gender, sexuality, race, age, disability, or religion.\nSpeakers should be especially aware of these concerns.\n\nCoreOS does not condone any statements by speakers contrary to these standards.\nCoreOS reserves the right to deny entrance and/or eject from an event (without\nrefund) any individual found to be engaging in discriminatory or offensive\nspeech or actions.\n\nPlease bring any concerns to the immediate attention of designated on-site\nstaff, Brandon Philips <brandon.philips@coreos.com>, and/or Rithu John <rithu.john@coreos.com>.\n"
  },
  {
    "path": "config/auth.go",
    "content": "package config\n\nimport (\n\t\"encoding\"\n\t\"encoding/base64\"\n\t\"fmt\"\n)\n\n// Base64 is a byte slice that encodes to and from base64-encoded strings.\ntype Base64 []byte\n\nvar (\n\t_ encoding.TextMarshaler   = (Base64)(nil)\n\t_ encoding.TextUnmarshaler = (*Base64)(nil)\n)\n\n// MarshalText implements encoding.TextMarshaler.\nfunc (b Base64) MarshalText() ([]byte, error) {\n\tsz := base64.StdEncoding.EncodedLen(len(b))\n\tout := make([]byte, sz)\n\tbase64.StdEncoding.Encode(out, b)\n\treturn out, nil\n}\n\n// UnmarshalText implements encoding.TextUnmarshaler.\nfunc (b *Base64) UnmarshalText(in []byte) error {\n\tsz := base64.StdEncoding.DecodedLen(len(in))\n\ts := make([]byte, sz)\n\tn, err := base64.StdEncoding.Decode(s, in)\n\tif err != nil {\n\t\treturn err\n\t}\n\t*b = s[:n]\n\treturn nil\n}\n\n// Auth holds the specific configs for different authentication methods.\n//\n// These should be pointers to structs, so that it's possible to distinguish\n// between \"absent\" and \"present and misconfigured.\"\ntype Auth struct {\n\tPSK       *AuthPSK       `yaml:\"psk,omitempty\" json:\"psk,omitempty\"`\n\tKeyserver *AuthKeyserver `yaml:\"keyserver,omitempty\" json:\"keyserver,omitempty\"`\n}\n\n// Any reports whether any sort of authentication is configured.\nfunc (a Auth) Any() bool {\n\treturn a.PSK != nil\n}\n\nfunc (a *Auth) lint() ([]Warning, error) {\n\treturn nil, nil\n}\n\n// AuthKeyserver is the configuration for doing authentication with the Quay\n// keyserver protocol.\n//\n// The \"Intraservice\" key is only needed when the overall config mode is not\n// \"combo\".\n//\n// Deprecated: This authentication method was never used. It was planned for\n// integration with Quay, but ultimately the Quay team decided to remove the\n// keyserver feature altogether.\ntype AuthKeyserver struct {\n\tAPI          string `yaml:\"api\" json:\"api\"`\n\tIntraservice Base64 `yaml:\"intraservice\" json:\"intraservice\"`\n}\n\nfunc (a *AuthKeyserver) lint() ([]Warning, error) {\n\treturn nil, &Warning{\n\t\tinner: fmt.Errorf(`authentication method deprecated: %w`, ErrDeprecated),\n\t}\n}\n\n// AuthPSK is the configuration for doing pre-shared key based authentication.\n//\n// The \"Issuer\" key is what the service expects to verify as the \"issuer\" claim.\ntype AuthPSK struct {\n\tKey    Base64   `yaml:\"key\" json:\"key\"`\n\tIssuer []string `yaml:\"iss\" json:\"iss\"`\n}\n\nfunc (a *AuthPSK) validate(_ Mode) ([]Warning, error) {\n\tif len(a.Key) == 0 {\n\t\treturn nil, &Warning{\n\t\t\tmsg: \"key is empty\",\n\t\t}\n\t}\n\tif len(a.Issuer) == 0 {\n\t\treturn nil, &Warning{\n\t\t\tpath: \".iss\",\n\t\t\tmsg:  \"no issuers defined\",\n\t\t}\n\t}\n\treturn nil, nil\n}\n"
  },
  {
    "path": "config/auth_test.go",
    "content": "package config_test\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"testing\"\n\t\"testing/quick\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\n\t\"github.com/quay/clair/config\"\n)\n\nfunc TestBase64(t *testing.T) {\n\troundtrip := func(in []byte) bool {\n\t\tb1 := config.Base64(in)\n\t\ttxt, err := b1.MarshalText()\n\t\tif err != nil {\n\t\t\treturn false\n\t\t}\n\t\tout := new(config.Base64)\n\t\tif err := out.UnmarshalText(txt); err != nil {\n\t\t\treturn false\n\t\t}\n\t\treturn bytes.Equal(in, []byte(*out))\n\t}\n\tif err := quick.Check(roundtrip, nil); err != nil {\n\t\tt.Error(err)\n\t}\n}\n\nfunc TestAuthUnmarshal(t *testing.T) {\n\tt.Run(\"PSK\", func(t *testing.T) {\n\t\ttype testcase struct {\n\t\t\tIn   string\n\t\t\tWant config.AuthPSK\n\t\t}\n\t\ttt := []testcase{\n\t\t\t{\n\t\t\t\tIn: `{\"key\":\"ZGVhZGJlZWZkZWFkYmVlZg==\",\"iss\":[\"iss\"]}`,\n\t\t\t\tWant: config.AuthPSK{\n\t\t\t\t\tKey:    []byte(\"deadbeefdeadbeef\"),\n\t\t\t\t\tIssuer: []string{\"iss\"},\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\n\t\tcheck := func(t *testing.T, tc testcase) {\n\t\t\tv := config.AuthPSK{}\n\t\t\tif err := json.Unmarshal([]byte(tc.In), &v); err != nil {\n\t\t\t\tt.Error(err)\n\t\t\t}\n\t\t\tif got, want := v, tc.Want; !cmp.Equal(got, want) {\n\t\t\t\tt.Error(cmp.Diff(got, want))\n\t\t\t}\n\t\t}\n\t\tfor _, tc := range tt {\n\t\t\tcheck(t, tc)\n\t\t}\n\t})\n\n\tt.Run(\"Keyserver\", func(t *testing.T) {\n\t\ttype testcase struct {\n\t\t\tIn   string\n\t\t\tWant config.AuthKeyserver\n\t\t}\n\t\ttt := []testcase{\n\t\t\t{\n\t\t\t\tIn: `{\"api\":\"quay/keys\",\"intraservice\":\"ZGVhZGJlZWZkZWFkYmVlZg==\"}`,\n\t\t\t\tWant: config.AuthKeyserver{\n\t\t\t\t\tAPI:          \"quay/keys\",\n\t\t\t\t\tIntraservice: []byte(\"deadbeefdeadbeef\"),\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\n\t\tcheck := func(t *testing.T, tc testcase) {\n\t\t\tv := config.AuthKeyserver{}\n\t\t\tif err := json.Unmarshal([]byte(tc.In), &v); err != nil {\n\t\t\t\tt.Error(err)\n\t\t\t}\n\t\t\tif got, want := v, tc.Want; !cmp.Equal(got, want) {\n\t\t\t\tt.Error(cmp.Diff(got, want))\n\t\t\t}\n\t\t}\n\t\tfor _, tc := range tt {\n\t\t\tcheck(t, tc)\n\t\t}\n\t})\n}\n\nfunc TestAuthMarshal(t *testing.T) {\n\twant := `{\"key\":\"ZGVhZGJlZWZkZWFkYmVlZg==\",\"iss\":[\"iss\"]}`\n\tin := config.AuthPSK{\n\t\tKey:    []byte(\"deadbeefdeadbeef\"),\n\t\tIssuer: []string{\"iss\"},\n\t}\n\tgotb, err := json.Marshal(in)\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n\tif got := string(gotb); !cmp.Equal(got, want) {\n\t\tt.Error(cmp.Diff(got, want))\n\t}\n}\n"
  },
  {
    "path": "config/config.go",
    "content": "package config\n\nimport (\n\t\"fmt\"\n\t\"net\"\n\t\"time\"\n)\n\n// Config is the configuration object for the commands in\n// github.com/quay/clair/v4/cmd/...\ntype Config struct {\n\t// TLS configures HTTPS support.\n\t//\n\t// Note that any non-trivial deployment means the certificate provided here\n\t// will need to be for the name the load balancer uses to connect to a given\n\t// Clair instance.\n\t//\n\t// This is not used for outgoing requests; setting the SSL_CERT_DIR\n\t// environment variable is the recommended way to do that. The release\n\t// container has `/var/run/certs` added to the list already.\n\tTLS *TLS `yaml:\"tls,omitempty\" json:\"tls,omitempty\"`\n\t// Sets which mode the clair instance will run.\n\tMode Mode `yaml:\"-\" json:\"-\"`\n\t// A string in <host>:<port> format where <host> can be an empty string.\n\t//\n\t// exposes Clair node's functionality to the network.\n\t// see /openapi/v1 for api spec.\n\tHTTPListenAddr string `yaml:\"http_listen_addr\" json:\"http_listen_addr\"`\n\t// A string in <host>:<port> format where <host> can be an empty string.\n\t//\n\t// exposes Clair's metrics and health endpoints.\n\tIntrospectionAddr string `yaml:\"introspection_addr\" json:\"introspection_addr\"`\n\t// Set the logging level.\n\tLogLevel LogLevel `yaml:\"log_level\" json:\"log_level\"`\n\tIndexer  Indexer  `yaml:\"indexer,omitempty\" json:\"indexer,omitempty\"`\n\tMatcher  Matcher  `yaml:\"matcher,omitempty\" json:\"matcher,omitempty\"`\n\tMatchers Matchers `yaml:\"matchers,omitempty\" json:\"matchers,omitempty\"`\n\tUpdaters Updaters `yaml:\"updaters,omitempty\" json:\"updaters,omitempty\"`\n\tNotifier Notifier `yaml:\"notifier,omitempty\" json:\"notifier,omitempty\"`\n\tAuth     Auth     `yaml:\"auth,omitempty\" json:\"auth,omitempty\"`\n\tTrace    Trace    `yaml:\"trace,omitempty\" json:\"trace,omitempty\"`\n\tMetrics  Metrics  `yaml:\"metrics,omitempty\" json:\"metrics,omitempty\"`\n}\n\nfunc (c *Config) validate(mode Mode) ([]Warning, error) {\n\tif c.HTTPListenAddr == \"\" {\n\t\tc.HTTPListenAddr = DefaultAddress\n\t}\n\tif c.Matcher.DisableUpdaters {\n\t\tc.Updaters.Sets = []string{}\n\t}\n\tswitch mode {\n\tcase ComboMode, IndexerMode, MatcherMode, NotifierMode:\n\t\t// OK\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unknown mode: %q\", mode)\n\t}\n\tif _, _, err := net.SplitHostPort(c.HTTPListenAddr); err != nil {\n\t\treturn nil, err\n\t}\n\treturn c.lint()\n}\n\nfunc (c *Config) lint() (ws []Warning, err error) {\n\tif c.HTTPListenAddr == \"\" {\n\t\tws = append(ws, Warning{\n\t\t\tpath: \".http_listen_addr\",\n\t\t\tmsg:  `http listen address not provided, default will be used`,\n\t\t})\n\t}\n\tif c.IntrospectionAddr == \"\" {\n\t\tws = append(ws, Warning{\n\t\t\tpath: \".introspection_addr\",\n\t\t\tmsg:  `introspection address not provided, default will be used`,\n\t\t})\n\t}\n\treturn ws, nil\n}\n\n// Duration is a serializeable [time.Duration].\ntype Duration time.Duration\n\n// UnmarshalText implements [encoding.TextUnmarshaler].\nfunc (d *Duration) UnmarshalText(b []byte) error {\n\tdur, err := time.ParseDuration(string(b))\n\tif err != nil {\n\t\treturn err\n\t}\n\t*d = Duration(dur)\n\treturn nil\n}\n\n// MarshalText implements [encoding.TextMarshaler].\nfunc (d *Duration) MarshalText() ([]byte, error) {\n\treturn []byte(time.Duration(*d).String()), nil\n}\n"
  },
  {
    "path": "config/config_test.go",
    "content": "package config_test\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/quay/clair/config\"\n)\n\ntype ValidateTestcase struct {\n\tName  string\n\tCheck func(*testing.T, *config.Config, error)\n\tConf  config.Config\n}\n\nfunc (tc ValidateTestcase) Run(t *testing.T) {\n\tws, err := config.Validate(&tc.Conf)\n\tfor _, w := range ws {\n\t\tt.Logf(\"lint: %v\", &w)\n\t}\n\tif tc.Check != nil {\n\t\ttc.Check(t, &tc.Conf, err)\n\t} else {\n\t\tif err != nil {\n\t\t\tt.Errorf(\"unexpected error: %v\", err)\n\t\t}\n\t}\n}\n\n// This test looks a little sprawling, but it's structured by the field name\n// into subtests, with the leaf test being the element triggering the failure.\n//\n// Doing this means the go test \"run\" flag is much easier to use.\nfunc TestValidateFailure(t *testing.T) {\n\tshouldFail := func(t *testing.T, _ *config.Config, err error) {\n\t\tif err == nil {\n\t\t\tt.Error(\"unexpected success\")\n\t\t}\n\t}\n\n\t// Tests on the base Config struct.\n\ttt := []ValidateTestcase{\n\t\t{\n\t\t\tName: \"InvalidMode\",\n\t\t\tConf: config.Config{\n\t\t\t\tMode: config.Mode(-1),\n\t\t\t},\n\t\t\tCheck: shouldFail,\n\t\t},\n\t\t{\n\t\t\tName: \"MalformedListenAddr\",\n\t\t\tConf: config.Config{\n\t\t\t\tMode:           config.ComboMode,\n\t\t\t\tHTTPListenAddr: \"xyz\",\n\t\t\t},\n\t\t\tCheck: shouldFail,\n\t\t},\n\t}\n\tfor _, tc := range tt {\n\t\tt.Run(tc.Name, tc.Run)\n\t}\n\n\tt.Run(\"Matcher\", func(t *testing.T) {\n\t\ttt := []ValidateTestcase{\n\t\t\t{\n\t\t\t\tName: \"IndexerAddr\",\n\t\t\t\tConf: config.Config{\n\t\t\t\t\tMode:           config.MatcherMode,\n\t\t\t\t\tHTTPListenAddr: \"localhost:8080\",\n\t\t\t\t\tMatcher: config.Matcher{\n\t\t\t\t\t\tIndexerAddr: \"\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tCheck: shouldFail,\n\t\t\t},\n\t\t}\n\t\tfor _, tc := range tt {\n\t\t\tt.Run(tc.Name, tc.Run)\n\t\t}\n\t})\n\n\tt.Run(\"Auth\", func(t *testing.T) {\n\t\ttt := []ValidateTestcase{\n\t\t\t{\n\t\t\t\tName: \"BadPSKKey\",\n\t\t\t\tConf: config.Config{\n\t\t\t\t\tMode: config.IndexerMode,\n\t\t\t\t\tAuth: config.Auth{\n\t\t\t\t\t\tPSK: &config.AuthPSK{},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tCheck: shouldFail,\n\t\t\t},\n\t\t\t{\n\t\t\t\tName: \"BadPSKIssuer\",\n\t\t\t\tConf: config.Config{\n\t\t\t\t\tMode: config.IndexerMode,\n\t\t\t\t\tAuth: config.Auth{\n\t\t\t\t\t\tPSK: &config.AuthPSK{\n\t\t\t\t\t\t\tKey: config.Base64([]byte{0xde, 0xad, 0xbe, 0xef}),\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tCheck: shouldFail,\n\t\t\t},\n\t\t}\n\t\tfor _, tc := range tt {\n\t\t\tt.Run(tc.Name, tc.Run)\n\t\t}\n\t})\n\n\tt.Run(\"Notifier\", func(t *testing.T) {\n\t\ttt := []ValidateTestcase{\n\t\t\t{\n\t\t\t\tName: \"Multiple\",\n\t\t\t\tConf: config.Config{\n\t\t\t\t\tMode: config.NotifierMode,\n\t\t\t\t\tNotifier: config.Notifier{\n\t\t\t\t\t\tAMQP:    &config.AMQP{},\n\t\t\t\t\t\tSTOMP:   &config.STOMP{},\n\t\t\t\t\t\tWebhook: &config.Webhook{},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tCheck: shouldFail,\n\t\t\t},\n\t\t}\n\t\tfor _, tc := range tt {\n\t\t\tt.Run(tc.Name, tc.Run)\n\t\t}\n\n\t\tt.Run(\"Webhook\", func(t *testing.T) {\n\t\t\ttt := []ValidateTestcase{\n\t\t\t\t{\n\t\t\t\t\tName: \"Target\",\n\t\t\t\t\tConf: config.Config{\n\t\t\t\t\t\tMode: config.NotifierMode,\n\t\t\t\t\t\tNotifier: config.Notifier{\n\t\t\t\t\t\t\tIndexerAddr: \"http://example.com/\",\n\t\t\t\t\t\t\tMatcherAddr: \"http://example.com/\",\n\t\t\t\t\t\t\tWebhook: &config.Webhook{\n\t\t\t\t\t\t\t\tTarget: \" http://example.com/\",\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\tCheck: shouldFail,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tName: \"Callback\",\n\t\t\t\t\tConf: config.Config{\n\t\t\t\t\t\tMode: config.NotifierMode,\n\t\t\t\t\t\tNotifier: config.Notifier{\n\t\t\t\t\t\t\tIndexerAddr: \"http://example.com/\",\n\t\t\t\t\t\t\tMatcherAddr: \"http://example.com/\",\n\t\t\t\t\t\t\tWebhook: &config.Webhook{\n\t\t\t\t\t\t\t\tCallback: \" http://example.com/\",\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\tCheck: shouldFail,\n\t\t\t\t},\n\t\t\t}\n\t\t\tfor _, tc := range tt {\n\t\t\t\tt.Run(tc.Name, tc.Run)\n\t\t\t}\n\t\t})\n\n\t\tt.Run(\"AMQP\", func(t *testing.T) {\n\t\t\ttt := []ValidateTestcase{\n\t\t\t\t{\n\t\t\t\t\tName: \"RoutingKey\",\n\t\t\t\t\tConf: config.Config{\n\t\t\t\t\t\tMode: config.NotifierMode,\n\t\t\t\t\t\tNotifier: config.Notifier{\n\t\t\t\t\t\t\tIndexerAddr: \"http://example.com/\",\n\t\t\t\t\t\t\tMatcherAddr: \"http://example.com/\",\n\t\t\t\t\t\t\tAMQP:        &config.AMQP{},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tCheck: shouldFail,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tName: \"URIs\",\n\t\t\t\t\tConf: config.Config{\n\t\t\t\t\t\tMode: config.NotifierMode,\n\t\t\t\t\t\tNotifier: config.Notifier{\n\t\t\t\t\t\t\tIndexerAddr: \"http://example.com/\",\n\t\t\t\t\t\t\tMatcherAddr: \"http://example.com/\",\n\t\t\t\t\t\t\tAMQP: &config.AMQP{\n\t\t\t\t\t\t\t\tRoutingKey: \"test\",\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\tCheck: shouldFail,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tName: \"InvalidURI\",\n\t\t\t\t\tConf: config.Config{\n\t\t\t\t\t\tMode: config.NotifierMode,\n\t\t\t\t\t\tNotifier: config.Notifier{\n\t\t\t\t\t\t\tIndexerAddr: \"http://example.com/\",\n\t\t\t\t\t\t\tMatcherAddr: \"http://example.com/\",\n\t\t\t\t\t\t\tAMQP: &config.AMQP{\n\t\t\t\t\t\t\t\tRoutingKey: \"test\",\n\t\t\t\t\t\t\t\tURIs:       []string{\" amqp://\"},\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\tCheck: shouldFail,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tName: \"Callback\",\n\t\t\t\t\tConf: config.Config{\n\t\t\t\t\t\tMode: config.NotifierMode,\n\t\t\t\t\t\tNotifier: config.Notifier{\n\t\t\t\t\t\t\tIndexerAddr: \"http://example.com/\",\n\t\t\t\t\t\t\tMatcherAddr: \"http://example.com/\",\n\t\t\t\t\t\t\tAMQP: &config.AMQP{\n\t\t\t\t\t\t\t\tRoutingKey: \"test\",\n\t\t\t\t\t\t\t\tURIs:       []string{\"amqp://\"},\n\t\t\t\t\t\t\t\tCallback:   \" http://example.com\",\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\tCheck: shouldFail,\n\t\t\t\t},\n\t\t\t}\n\t\t\tfor _, tc := range tt {\n\t\t\t\tt.Run(tc.Name, tc.Run)\n\t\t\t}\n\t\t})\n\n\t\tt.Run(\"STOMP\", func(t *testing.T) {\n\t\t\ttt := []ValidateTestcase{\n\t\t\t\t{\n\t\t\t\t\tName: \"URIs\",\n\t\t\t\t\tConf: config.Config{\n\t\t\t\t\t\tMode: config.NotifierMode,\n\t\t\t\t\t\tNotifier: config.Notifier{\n\t\t\t\t\t\t\tIndexerAddr: \"http://example.com/\",\n\t\t\t\t\t\t\tMatcherAddr: \"http://example.com/\",\n\t\t\t\t\t\t\tSTOMP:       &config.STOMP{},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tCheck: shouldFail,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tName: \"InvalidURI\",\n\t\t\t\t\tConf: config.Config{\n\t\t\t\t\t\tMode: config.NotifierMode,\n\t\t\t\t\t\tNotifier: config.Notifier{\n\t\t\t\t\t\t\tIndexerAddr: \"http://example.com/\",\n\t\t\t\t\t\t\tMatcherAddr: \"http://example.com/\",\n\t\t\t\t\t\t\tSTOMP: &config.STOMP{\n\t\t\t\t\t\t\t\tURIs: []string{\"::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\tCheck: shouldFail,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tName: \"Callback\",\n\t\t\t\t\tConf: config.Config{\n\t\t\t\t\t\tMode: config.NotifierMode,\n\t\t\t\t\t\tNotifier: config.Notifier{\n\t\t\t\t\t\t\tIndexerAddr: \"http://example.com/\",\n\t\t\t\t\t\t\tMatcherAddr: \"http://example.com/\",\n\t\t\t\t\t\t\tSTOMP: &config.STOMP{\n\t\t\t\t\t\t\t\tURIs:     []string{\"stomp:567\"},\n\t\t\t\t\t\t\t\tCallback: \" http://example.com\",\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\tCheck: shouldFail,\n\t\t\t\t},\n\t\t\t}\n\t\t\tfor _, tc := range tt {\n\t\t\t\tt.Run(tc.Name, tc.Run)\n\t\t\t}\n\n\t\t\tt.Run(\"TLS\", func(t *testing.T) {\n\t\t\t\ttt := []ValidateTestcase{\n\t\t\t\t\t{\n\t\t\t\t\t\tName: \"Key\",\n\t\t\t\t\t\tConf: config.Config{\n\t\t\t\t\t\t\tMode: config.NotifierMode,\n\t\t\t\t\t\t\tNotifier: config.Notifier{\n\t\t\t\t\t\t\t\tIndexerAddr: \"http://example.com/\",\n\t\t\t\t\t\t\t\tMatcherAddr: \"http://example.com/\",\n\t\t\t\t\t\t\t\tSTOMP: &config.STOMP{\n\t\t\t\t\t\t\t\t\tURIs:     []string{\"stomp:567\"},\n\t\t\t\t\t\t\t\t\tCallback: \"http://example.com/\",\n\t\t\t\t\t\t\t\t\tTLS: &config.TLS{\n\t\t\t\t\t\t\t\t\t\tCert: \"fail.crt\",\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\tCheck: shouldFail,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tName: \"Cert\",\n\t\t\t\t\t\tConf: config.Config{\n\t\t\t\t\t\t\tMode: config.NotifierMode,\n\t\t\t\t\t\t\tNotifier: config.Notifier{\n\t\t\t\t\t\t\t\tIndexerAddr: \"http://example.com/\",\n\t\t\t\t\t\t\t\tMatcherAddr: \"http://example.com/\",\n\t\t\t\t\t\t\t\tSTOMP: &config.STOMP{\n\t\t\t\t\t\t\t\t\tURIs:     []string{\"stomp:567\"},\n\t\t\t\t\t\t\t\t\tCallback: \"http://example.com/\",\n\t\t\t\t\t\t\t\t\tTLS: &config.TLS{\n\t\t\t\t\t\t\t\t\t\tKey: \"fail.key\",\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\tCheck: shouldFail,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tName: \"RootCA\",\n\t\t\t\t\t\tConf: config.Config{\n\t\t\t\t\t\t\tMode: config.NotifierMode,\n\t\t\t\t\t\t\tNotifier: config.Notifier{\n\t\t\t\t\t\t\t\tIndexerAddr: \"http://example.com/\",\n\t\t\t\t\t\t\t\tMatcherAddr: \"http://example.com/\",\n\t\t\t\t\t\t\t\tSTOMP: &config.STOMP{\n\t\t\t\t\t\t\t\t\tURIs:     []string{\"stomp:567\"},\n\t\t\t\t\t\t\t\t\tCallback: \"http://example.com/\",\n\t\t\t\t\t\t\t\t\tTLS: &config.TLS{\n\t\t\t\t\t\t\t\t\t\tRootCA: \"fail.pem\",\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\tCheck: shouldFail,\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\tfor _, tc := range tt {\n\t\t\t\t\tt.Run(tc.Name, tc.Run)\n\t\t\t\t}\n\t\t\t})\n\t\t})\n\t})\n}\n\nfunc TestUpdateRetention(t *testing.T) {\n\texpect := func(n int) func(*testing.T, *config.Config, error) {\n\t\treturn func(t *testing.T, c *config.Config, err error) {\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"unexpected error: %v\", err)\n\t\t\t}\n\t\t\tif got, want := c.Matcher.UpdateRetention, n; got != want {\n\t\t\t\tt.Errorf(\"got: %d, want: %d\", got, want)\n\t\t\t}\n\t\t}\n\t}\n\n\t// Construct a bunch of test cases from (in, out) pairs.\n\ttt := func(p [][2]int) (tt []ValidateTestcase) {\n\t\tfor _, p := range p {\n\t\t\ttt = append(tt, ValidateTestcase{\n\t\t\t\tName: fmt.Sprintf(\"%d\", p[0]),\n\t\t\t\tConf: config.Config{\n\t\t\t\t\tMode:           config.ComboMode,\n\t\t\t\t\tHTTPListenAddr: \"localhost:8080\",\n\t\t\t\t\tMatcher: config.Matcher{\n\t\t\t\t\t\tUpdateRetention: p[0],\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tCheck: expect(p[1]),\n\t\t\t})\n\t\t}\n\t\treturn\n\t}([][2]int{\n\t\t{-1, 0},\n\t\t{0, config.DefaultUpdateRetention},\n\t\t{1, config.DefaultUpdateRetention},\n\t\t{2, 2},\n\t})\n\tfor _, tc := range tt {\n\t\tt.Run(tc.Name, tc.Run)\n\t}\n}\n\nfunc TestDisableUpdaters(t *testing.T) {\n\tsetsEmpty := func(t *testing.T, c *config.Config, err error) {\n\t\tif err != nil {\n\t\t\tt.Errorf(\"unexpected error: %v\", err)\n\t\t}\n\t\tif !cmp.Equal(c.Updaters.Sets, []string{}) {\n\t\t\tt.Error(cmp.Diff(c.Updaters.Sets, []string{}))\n\t\t}\n\t}\n\ttt := []ValidateTestcase{\n\t\t{\n\t\t\tName: \"ComboMode\",\n\t\t\tConf: config.Config{\n\t\t\t\tMode: config.ComboMode,\n\t\t\t\tMatcher: config.Matcher{\n\t\t\t\t\tDisableUpdaters: true,\n\t\t\t\t},\n\t\t\t\tUpdaters: config.Updaters{\n\t\t\t\t\tSets: []string{\"alpine\", \"aws\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tCheck: setsEmpty,\n\t\t},\n\t\t{\n\t\t\tName: \"MatcherMode\",\n\t\t\tConf: config.Config{\n\t\t\t\tMode: config.MatcherMode,\n\t\t\t\tMatcher: config.Matcher{\n\t\t\t\t\tIndexerAddr:     \"http://example.com/\",\n\t\t\t\t\tDisableUpdaters: true,\n\t\t\t\t},\n\t\t\t\tUpdaters: config.Updaters{\n\t\t\t\t\tSets: []string{\"alpine\", \"aws\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tCheck: setsEmpty,\n\t\t},\n\t}\n\n\tfor _, tc := range tt {\n\t\tt.Run(tc.Name, tc.Run)\n\t}\n}\n"
  },
  {
    "path": "config/database.go",
    "content": "package config\n\nimport (\n\t\"net/url\"\n\t\"os\"\n\t\"strings\"\n)\n\nfunc checkDSN(s string) (w []Warning, err error) {\n\tswitch {\n\tcase s == \"\":\n\t\t// Nothing specified, make sure something's in the environment.\n\t\tenvSet := false\n\t\tfor _, k := range os.Environ() {\n\t\t\tif strings.HasPrefix(k, `PG`) {\n\t\t\t\tenvSet = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif !envSet {\n\t\t\tw = append(w, Warning{\n\t\t\t\tmsg: \"connection string is empty and no relevant environment variables found\",\n\t\t\t})\n\t\t}\n\tcase strings.HasPrefix(s, \"postgresql://\"):\n\t\t// Looks like a URL\n\t\tif _, err := url.Parse(s); err != nil {\n\t\t\tw = append(w, Warning{inner: err})\n\t\t}\n\tcase strings.ContainsRune(s, '='):\n\t\t// Looks like a DSN\n\tcase strings.Contains(s, `://`):\n\t\tw = append(w, Warning{\n\t\t\tmsg: \"connection string looks like a URL but scheme is unrecognized\",\n\t\t})\n\tdefault:\n\t\tw = append(w, Warning{\n\t\t\tmsg: \"unable to make sense of connection string\",\n\t\t})\n\t}\n\treturn w, nil\n}\n"
  },
  {
    "path": "config/defaults.go",
    "content": "package config\n\nimport \"time\"\n\n// These are defaults, used in the documented spots.\nconst (\n\t// DefaultAddress is used if an \"http_listen_addr\" is not provided in the config.\n\tDefaultAddress = \":6060\"\n\t// DefaultScanLockRetry is the default retry period for attempting locks\n\t// during the indexing process. Its name is a historical accident.\n\tDefaultScanLockRetry = 1\n\t// DefaultMatcherPeriod is the default interval for running updaters.\n\tDefaultMatcherPeriod = 6 * time.Hour\n\t// DefaultUpdateRetention is the number of updates per vulnerability\n\t// database to retain.\n\tDefaultUpdateRetention = 10\n\t// DefaultNotifierPollInterval is the default (and minimum) interval for the\n\t// notifier's change poll interval. The notifier will poll the database for\n\t// updated vulnerability databases at this rate.\n\tDefaultNotifierPollInterval = 6 * time.Hour\n\t// DefaultNotifierDeliveryInterval is the default (and minimum) interval for\n\t// the notifier's delivery interval. The notifier will attempt to deliver\n\t// outstanding notifications at this rate.\n\tDefaultNotifierDeliveryInterval = 1 * time.Hour\n)\n"
  },
  {
    "path": "config/doc.go",
    "content": "// Package config is the configuration package for Clair's binaries. See the\n// [Config] type for the main entry point.\n//\n// It's currently meant for reading configurations and tested against YAML and\n// JSON.\n//\n// # Version Scheme\n//\n// This package uses an idiosyncratic versioning scheme:\n//\n//   - The major version tracks the input format.\n//   - The minor version tracks the Go source API.\n//   - The patch version increases with fixes and additions.\n//\n// This means that any valid configuration accepted by `v1.0.0` should continue\n// to be accepted for all revisions of the v1 module, but `v1.1.0` may force\n// changes on a program importing the module.\npackage config\n\n// This package can't use \"omitempty\" tags on slices because \"not present\" and\n// \"empty\" aren't distinguished. This would be much easier if code didn't\n// serialize our config struct. It's impossible to implement custom YAML\n// marshalling without importing the yaml.v3 package.\n"
  },
  {
    "path": "config/enums.go",
    "content": "package config\n\nimport (\n\t\"encoding\"\n\t\"errors\"\n\t\"fmt\"\n\t\"strings\"\n)\n\n//go:generate -command stringer go run golang.org/x/tools/cmd/stringer@v0.8.0\n//go:generate stringer -type Mode,LogLevel -linecomment -output enums_string.go\n\n// A Mode is an operating mode recognized by Clair.\n//\n// This is not directly settable by serializing into a Config object.\ntype Mode int\n\n// Clair modes, with their string representations as the comments.\nconst (\n\tComboMode    Mode = iota // combo\n\tIndexerMode              // indexer\n\tMatcherMode              // matcher\n\tNotifierMode             // notifier\n)\n\n// ParseMode returns a mode for the given string.\n//\n// The passed string is case-insensitive.\nfunc ParseMode(s string) (Mode, error) {\n\tfor i, lim := 0, len(_Mode_index); i < lim; i++ {\n\t\tm := Mode(i)\n\t\tif strings.EqualFold(s, m.String()) {\n\t\t\treturn m, nil\n\t\t}\n\t}\n\treturn Mode(-1), fmt.Errorf(`unknown mode %q`, s)\n}\n\n// A LogLevel is a log level recognized by Clair.\n//\n// The zero value is \"info\".\ntype LogLevel int\n\n// The recognized log levels, with their string representations as the comments.\n//\n// NB \"Fatal\" and \"Panic\" are not used in clair or claircore, and will result in\n// almost no logging.\nconst (\n\tInfoLog       LogLevel = iota // info\n\tDebugColorLog                 // debug-color\n\tDebugLog                      // debug\n\tWarnLog                       // warn\n\tErrorLog                      // error\n\tFatalLog                      // fatal\n\tPanicLog                      // panic\n)\n\n// ParseLogLevel returns the log lever for the given string.\n//\n// The passed string is case-insensitive.\nfunc ParseLogLevel(s string) (LogLevel, error) {\n\tfor i, lim := 0, len(_LogLevel_index); i < lim; i++ {\n\t\tl := LogLevel(i)\n\t\tif strings.EqualFold(s, l.String()) {\n\t\t\treturn l, nil\n\t\t}\n\t}\n\treturn LogLevel(-1), fmt.Errorf(`unknown log level %q`, s)\n}\n\n// UnmarshalText implements encoding.TextUnmarshaler.\nfunc (l *LogLevel) UnmarshalText(b []byte) (err error) {\n\t*l, err = ParseLogLevel(string(b))\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\n// MarshalText implements encoding.TextMarshaler.\nfunc (l *LogLevel) MarshalText() ([]byte, error) {\n\tif l == nil {\n\t\treturn nil, errors.New(\"invalid LogLevel pointer: <nil>\")\n\t}\n\ti := *l\n\tif i < 0 || i >= LogLevel(len(_LogLevel_index)-1) {\n\t\treturn nil, fmt.Errorf(\"invalid LogLevel: %q\", l.String())\n\t}\n\treturn []byte(_LogLevel_name[_LogLevel_index[i]:_LogLevel_index[i+1]]), nil\n}\n\n// Assert LogLevel implements TextUnmarshaler and TextMarshaler.\nvar (\n\t_ encoding.TextUnmarshaler = (*LogLevel)(nil)\n\t_ encoding.TextMarshaler   = (*LogLevel)(nil)\n)\n"
  },
  {
    "path": "config/enums_string.go",
    "content": "// Code generated by \"stringer -type Mode,LogLevel -linecomment -output enums_string.go\"; DO NOT EDIT.\n\npackage config\n\nimport \"strconv\"\n\nfunc _() {\n\t// An \"invalid array index\" compiler error signifies that the constant values have changed.\n\t// Re-run the stringer command to generate them again.\n\tvar x [1]struct{}\n\t_ = x[ComboMode-0]\n\t_ = x[IndexerMode-1]\n\t_ = x[MatcherMode-2]\n\t_ = x[NotifierMode-3]\n}\n\nconst _Mode_name = \"comboindexermatchernotifier\"\n\nvar _Mode_index = [...]uint8{0, 5, 12, 19, 27}\n\nfunc (i Mode) String() string {\n\tif i < 0 || i >= Mode(len(_Mode_index)-1) {\n\t\treturn \"Mode(\" + strconv.FormatInt(int64(i), 10) + \")\"\n\t}\n\treturn _Mode_name[_Mode_index[i]:_Mode_index[i+1]]\n}\nfunc _() {\n\t// An \"invalid array index\" compiler error signifies that the constant values have changed.\n\t// Re-run the stringer command to generate them again.\n\tvar x [1]struct{}\n\t_ = x[InfoLog-0]\n\t_ = x[DebugColorLog-1]\n\t_ = x[DebugLog-2]\n\t_ = x[WarnLog-3]\n\t_ = x[ErrorLog-4]\n\t_ = x[FatalLog-5]\n\t_ = x[PanicLog-6]\n}\n\nconst _LogLevel_name = \"infodebug-colordebugwarnerrorfatalpanic\"\n\nvar _LogLevel_index = [...]uint8{0, 4, 15, 20, 24, 29, 34, 39}\n\nfunc (i LogLevel) String() string {\n\tif i < 0 || i >= LogLevel(len(_LogLevel_index)-1) {\n\t\treturn \"LogLevel(\" + strconv.FormatInt(int64(i), 10) + \")\"\n\t}\n\treturn _LogLevel_name[_LogLevel_index[i]:_LogLevel_index[i+1]]\n}\n"
  },
  {
    "path": "config/enums_test.go",
    "content": "package config_test\n\nimport (\n\t\"bytes\"\n\t\"testing\"\n\n\t\"github.com/quay/clair/config\"\n)\n\nfunc TestEnumMarshal(t *testing.T) {\n\tt.Run(\"LogLevel\", func(t *testing.T) {\n\t\ttt := [][]byte{\n\t\t\t[]byte(\"info\"),\n\t\t\t[]byte(\"debug-color\"),\n\t\t\t[]byte(\"debug\"),\n\t\t\t[]byte(\"warn\"),\n\t\t\t[]byte(\"error\"),\n\t\t\t[]byte(\"fatal\"),\n\t\t\t[]byte(\"panic\"),\n\t\t}\n\t\tt.Run(\"Marshal\", func(t *testing.T) {\n\t\t\tfor i, want := range tt {\n\t\t\t\tl := config.LogLevel(i)\n\t\t\t\tgot, err := l.MarshalText()\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Error(err)\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif !bytes.Equal(got, want) {\n\t\t\t\t\tt.Errorf(\"got: %q, want: %q\", got, want)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t\tt.Run(\"Unmarshal\", func(t *testing.T) {\n\t\t\tfor want, in := range tt {\n\t\t\t\tvar l config.LogLevel\n\t\t\t\tif err := l.UnmarshalText(in); err != nil {\n\t\t\t\t\tt.Error(err)\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif got := int(l); got != want {\n\t\t\t\t\tt.Errorf(\"got: %q, want: %q\", got, want)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t})\n}\n"
  },
  {
    "path": "config/go.mod",
    "content": "module github.com/quay/clair/config\n\ngo 1.22.0\n\nrequire github.com/google/go-cmp v0.7.0\n"
  },
  {
    "path": "config/go.sum",
    "content": "github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\n"
  },
  {
    "path": "config/indexer.go",
    "content": "package config\n\nimport \"runtime\"\n\n// Indexer provides Clair Indexer node configuration\ntype Indexer struct {\n\t// Scanner allows for passing configuration options to layer scanners.\n\tScanner ScannerConfig `yaml:\"scanner,omitempty\" json:\"scanner,omitempty\"`\n\t// A Postgres connection string.\n\t//\n\t// formats\n\t// url: \"postgres://pqgotest:password@localhost/pqgotest?sslmode=verify-full\"\n\t// or\n\t// string: \"user=pqgotest dbname=pqgotest sslmode=verify-full\"\n\tConnString string `yaml:\"connstring\" json:\"connstring\"`\n\t// A positive value representing seconds.\n\t//\n\t// Concurrent Indexers lock on manifest scans to avoid clobbering.\n\t// This value tunes how often a waiting Indexer will poll for the lock.\n\tScanLockRetry int `yaml:\"scanlock_retry,omitempty\" json:\"scanlock_retry,omitempty\"`\n\t// A positive values representing quantity.\n\t//\n\t// Indexers will index a Manifest's layers concurrently.\n\t// This value tunes the number of layers an Indexer will scan in parallel.\n\tLayerScanConcurrency int `yaml:\"layer_scan_concurrency,omitempty\" json:\"layer_scan_concurrency,omitempty\"`\n\t// Rate limits the number of index report creation requests.\n\t//\n\t// Setting this to 0 will attempt to auto-size this value. Setting a\n\t// negative value means \"unlimited.\" The auto-sizing is a multiple of the\n\t// number of available cores.\n\t//\n\t// The API will return a 429 status code if concurrency is exceeded.\n\tIndexReportRequestConcurrency int `yaml:\"index_report_request_concurrency,omitempty\" json:\"index_report_request_concurrency,omitempty\"`\n\t// A \"true\" or \"false\" value\n\t//\n\t// Whether Indexer nodes handle migrations to their database.\n\tMigrations bool `yaml:\"migrations,omitempty\" json:\"migrations,omitempty\"`\n\t// Airgap disables HTTP access to the Internet. This affects both indexers and\n\t// the layer fetcher. Database connections are unaffected.\n\t//\n\t// \"Airgap\" is a bit of a misnomer, as [RFC 4193] and [RFC 1918] addresses\n\t// are always allowed. This means that setting this flag and also\n\t// configuring a proxy on a private network does not prevent contact with\n\t// the Internet.\n\t//\n\t// [RFC 1918]: https://datatracker.ietf.org/doc/html/rfc1918\n\t// [RFC 4193]: https://datatracker.ietf.org/doc/html/rfc4193\n\tAirgap bool `yaml:\"airgap,omitempty\" json:\"airgap,omitempty\"`\n}\n\nfunc (i *Indexer) validate(mode Mode) (ws []Warning, err error) {\n\tconst DefaultScanLockRetry = 1\n\tif mode != ComboMode && mode != IndexerMode {\n\t\treturn nil, nil\n\t}\n\tif i.ScanLockRetry == 0 {\n\t\ti.ScanLockRetry = DefaultScanLockRetry\n\t}\n\tif i.IndexReportRequestConcurrency == 0 {\n\t\t// GOMAXPROCS should be set to the number of cores available.\n\t\tgmp := runtime.GOMAXPROCS(0)\n\t\tconst wildGuess = 4\n\t\ti.IndexReportRequestConcurrency = gmp * wildGuess\n\t\tws = append(ws, Warning{\n\t\t\tpath: \".index_report_request_concurrency\",\n\t\t\tmsg:  `automatically sizing number of concurrent requests`,\n\t\t})\n\t}\n\tlws, err := i.lint()\n\treturn append(ws, lws...), err\n}\n\nfunc (i *Indexer) lint() (ws []Warning, err error) {\n\tws, err = checkDSN(i.ConnString)\n\tif err != nil {\n\t\treturn ws, err\n\t}\n\tfor i := range ws {\n\t\tws[i].path = \".connstring\"\n\t}\n\tif i.ScanLockRetry > 10 { // Guess at what a \"large\" value is here.\n\t\tws = append(ws, Warning{\n\t\t\tpath: \".scanlock_retry\",\n\t\t\tmsg:  `large values will increase latency`,\n\t\t})\n\t}\n\tswitch {\n\tcase i.LayerScanConcurrency == 0:\n\t\t// Skip, autosized.\n\tcase i.LayerScanConcurrency < 4:\n\t\tws = append(ws, Warning{\n\t\t\tpath: \".layer_scan_concurrency\",\n\t\t\tmsg:  `small values will limit resource utilization and increase latency`,\n\t\t})\n\tcase i.LayerScanConcurrency > 32:\n\t\tws = append(ws, Warning{\n\t\t\tpath: \".layer_scan_concurrency\",\n\t\t\tmsg:  `large values may exceed resource quotas`,\n\t\t})\n\t}\n\n\treturn ws, nil\n}\n\n// ScannerConfig is the object consulted for configuring the various types of\n// scanners.\ntype ScannerConfig struct {\n\tPackage map[string]interface{} `yaml:\"package,omitempty\" json:\"package,omitempty\"`\n\tDist    map[string]interface{} `yaml:\"dist,omitempty\" json:\"dist,omitempty\"`\n\tRepo    map[string]interface{} `yaml:\"repo,omitempty\" json:\"repo,omitempty\"`\n}\n"
  },
  {
    "path": "config/introspection.go",
    "content": "package config\n\nimport \"fmt\"\n\n// Trace specifies how to configure Clair's tracing support.\n//\n// The \"Name\" key must match the provider to use.\ntype Trace struct {\n\tName        string    `yaml:\"name\" json:\"name\"`\n\tProbability *float64  `yaml:\"probability,omitempty\" json:\"probability,omitempty\"`\n\tJaeger      Jaeger    `yaml:\"jaeger,omitempty\" json:\"jaeger,omitempty\"`\n\tOTLP        TraceOTLP `yaml:\"otlp,omitempty\" json:\"otlp,omitempty\"`\n\tSentry      Sentry    `yaml:\"sentry,omitempty\" json:\"sentry,omitempty\"`\n}\n\nfunc (t *Trace) lint() ([]Warning, error) {\n\tswitch t.Name {\n\tcase \"\":\n\tcase \"otlp\":\n\tcase \"sentry\":\n\tcase \"jaeger\":\n\t\treturn []Warning{{\n\t\t\tpath: \".name\",\n\t\t\tmsg:  `trace provider \"jaeger\" is deprecated; migrate to \"otlp\"`,\n\t\t}}, nil\n\tdefault:\n\t\treturn []Warning{{\n\t\t\tpath: \".name\",\n\t\t\tmsg:  fmt.Sprintf(`unrecognized trace provider: %q`, t.Name),\n\t\t}}, nil\n\t}\n\treturn nil, nil\n}\n\n// Jaeger specific distributed tracing configuration.\n//\n// Deprecated: The Jaeger project recommends using their OTLP ingestion support\n// and the OpenTelemetry exporter for Jaeger has since been removed. Users\n// should migrate to OTLP. Clair may refuse to start when configured to emit\n// Jaeger traces.\ntype Jaeger struct {\n\tTags  map[string]string `yaml:\"tags,omitempty\" json:\"tags,omitempty\"`\n\tAgent struct {\n\t\tEndpoint string `yaml:\"endpoint\" json:\"endpoint\"`\n\t} `yaml:\"agent,omitempty\" json:\"agent,omitempty\"`\n\tCollector struct {\n\t\tUsername *string `yaml:\"username,omitempty\" json:\"username,omitempty\"`\n\t\tPassword *string `yaml:\"password,omitempty\" json:\"password,omitempty\"`\n\t\tEndpoint string  `yaml:\"endpoint\" json:\"endpoint\"`\n\t} `yaml:\"collector,omitempty\" json:\"collector,omitempty\"`\n\tServiceName string `yaml:\"service_name,omitempty\" json:\"service_name,omitempty\"`\n\tBufferMax   int    `yaml:\"buffer_max,omitempty\" json:\"buffer_max,omitempty\"`\n}\n\n// Sentry is the [Sentry] specific tracing configuration.\n//\n// [Sentry]: https://sentry.io\ntype Sentry struct {\n\t// DSN to be passed to [github.com/getsentry/sentry-go.ClientOptions.Dsn].\n\tDSN string `yaml:\"dsn\" json:\"dsn\"`\n\t// Environment to be passed to\n\t// [github.com/getsentry/sentry-go.ClientOptions.Environment].\n\tEnvironment string `yaml:\"environment,omitempty\" json:\"environment,omitempty\"`\n}\n\n// Metrics specifies how to configure Clair's metrics exporting.\n//\n// The \"Name\" key must match the provider to use.\ntype Metrics struct {\n\tName       string     `yaml:\"name\" json:\"name\"`\n\tPrometheus Prometheus `yaml:\"prometheus,omitempty\" json:\"prometheus,omitempty\"`\n\tOTLP       MetricOTLP `yaml:\"otlp,omitempty\" json:\"otlp,omitempty\"`\n}\n\nfunc (m *Metrics) lint() ([]Warning, error) {\n\tswitch m.Name {\n\tcase \"\":\n\tcase \"otlp\":\n\t\treturn []Warning{{\n\t\t\tpath: \".name\",\n\t\t\tmsg:  `please consult the documentation for the status of metrics via \"otlp\"`,\n\t\t}}, nil\n\tcase \"prometheus\":\n\tdefault:\n\t\treturn []Warning{{\n\t\t\tpath: \".name\",\n\t\t\tmsg:  fmt.Sprintf(`unrecognized metrics provider: %q`, m.Name),\n\t\t}}, nil\n\t}\n\treturn nil, nil\n}\n\n// Prometheus specific metrics configuration.\ntype Prometheus struct {\n\t// Endpoint is a URL path where Prometheus metrics will be hosted.\n\tEndpoint *string `yaml:\"endpoint,omitempty\" json:\"endpoint,omitempty\"`\n}\n"
  },
  {
    "path": "config/lint.go",
    "content": "package config\n\nimport (\n\t\"errors\"\n\t\"strings\"\n)\n\n// Lint runs lints on the provided Config.\n//\n// An error is reported only if an error occurred while running the lints. An\n// invalid Config may still report a nil error along with a slice of Warnings.\n//\n// Most validation steps run by Validate will also run lints.\nfunc Lint(c *Config) ([]Warning, error) {\n\treturn forEach(c, func(i interface{}) ([]Warning, error) {\n\t\tif l, ok := i.(linter); ok {\n\t\t\treturn l.lint()\n\t\t}\n\t\treturn nil, nil\n\t})\n}\n\n// Types in this package can implement this interface to report common issues or\n// deprecation warnings.\ntype linter interface {\n\tlint() ([]Warning, error)\n}\n\n// Warning is a linter warning.\n//\n// Users can treat them like errors and use the sentinel values exported by this\n// package.\ntype Warning struct {\n\tinner error\n\tpath  string // json-schema style path\n\tmsg   string\n}\n\n// Should have inner xor msg\n\nfunc (w *Warning) Error() string {\n\tvar b strings.Builder\n\tif w.inner != nil {\n\t\tb.WriteString(w.inner.Error())\n\t} else {\n\t\tb.WriteString(w.msg)\n\t}\n\tb.WriteString(\" (at \")\n\tb.WriteString(w.path)\n\tb.WriteRune(')')\n\treturn b.String()\n}\n\nfunc (w *Warning) Unwrap() error { return w.inner }\n\n// These are some common kinds of Warnings.\nvar (\n\tErrDeprecated = errors.New(\"setting will be removed in a future release\")\n)\n"
  },
  {
    "path": "config/lint_test.go",
    "content": "package config\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n)\n\nfunc init() {\n\t// Forcibly clear PostgreSQL environment variables to get consistent example\n\t// results:\n\tfor _, kv := range os.Environ() {\n\t\tk, _, _ := strings.Cut(kv, \"=\")\n\t\tif strings.HasPrefix(k, `PG`) {\n\t\t\tos.Unsetenv(k)\n\t\t}\n\t}\n}\n\nfunc ExampleLint() {\n\tvar c Config\n\tc.Auth.PSK = &AuthPSK{}\n\tws, err := Lint(&c)\n\tfmt.Println(\"error:\", err)\n\tfor _, w := range ws {\n\t\tfmt.Printf(\"warning: %v\\n\", &w)\n\t}\n\t// Output:\n\t// error: <nil>\n\t// warning: http listen address not provided, default will be used (at $.http_listen_addr)\n\t// warning: introspection address not provided, default will be used (at $.introspection_addr)\n\t// warning: connection string is empty and no relevant environment variables found (at $.indexer.connstring)\n\t// warning: connection string is empty and no relevant environment variables found (at $.matcher.connstring)\n\t// warning: updater period is very aggressive: most sources are updated daily (at $.matcher.period)\n\t// warning: update garbage collection is off (at $.matcher.update_retention)\n\t// warning: connection string is empty and no relevant environment variables found (at $.notifier.connstring)\n\t// warning: interval is very fast: may result in increased workload (at $.notifier.poll_interval)\n\t// warning: interval is very fast: may result in increased workload (at $.notifier.delivery_interval)\n}\n"
  },
  {
    "path": "config/matcher.go",
    "content": "package config\n\nimport (\n\t\"fmt\"\n\t\"net/url\"\n)\n\n// Matcher is the configuration for the matcher service.\ntype Matcher struct {\n\t// A Postgres connection string.\n\t//\n\t// Formats:\n\t// url: \"postgres://pqgotest:password@localhost/pqgotest?sslmode=verify-full\"\n\t// or\n\t// string: \"user=pqgotest dbname=pqgotest sslmode=verify-full\"\n\tConnString string `yaml:\"connstring\" json:\"connstring\"`\n\t// A string in <host>:<port> format where <host> can be an empty string.\n\t//\n\t// A Matcher contacts an Indexer to create a VulnerabilityReport.\n\t// The location of this Indexer is required.\n\tIndexerAddr string `yaml:\"indexer_addr\" json:\"indexer_addr\"`\n\t// Period controls how often updaters are run.\n\t//\n\t// The default is 6 hours.\n\tPeriod Duration `yaml:\"period,omitempty\" json:\"period,omitempty\"`\n\t// UpdateRetention controls the number of updates to retain between\n\t// garbage collection periods.\n\t//\n\t// The lowest possible value is 2 in order to compare updates for notification\n\t// purposes.\n\t//\n\t// A value of 0 disables GC.\n\tUpdateRetention int `yaml:\"update_retention\" json:\"update_retention\"`\n\t// A positive integer\n\t//\n\t// Clair allows for a custom connection pool size.  This number will\n\t// directly set how many active sql connections are allowed concurrently.\n\t//\n\t// Deprecated: Pool size should be set through the ConnString member.\n\t// Currently, Clair only uses the \"pgxpool\" package to connect to the\n\t// database, so see\n\t// https://pkg.go.dev/github.com/jackc/pgx/v5/pgxpool#ParseConfig for more\n\t// information.\n\tMaxConnPool int `yaml:\"max_conn_pool,omitempty\" json:\"max_conn_pool,omitempty\"`\n\t// CacheAge controls how long clients should be hinted to cache responses\n\t// for.\n\t//\n\t// If empty, the duration set in \"Period\" will be used. This means client\n\t// may cache \"stale\" results for 2(Period) - 1 seconds.\n\tCacheAge Duration `yaml:\"cache_age,omitempty\" json:\"cache_age,omitempty\"`\n\t// A \"true\" or \"false\" value\n\t//\n\t// Whether Matcher nodes handle migrations to their databases.\n\tMigrations bool `yaml:\"migrations,omitempty\" json:\"migrations,omitempty\"`\n\t// DisableUpdaters disables the updater's running of matchers.\n\t//\n\t// This should be toggled on if vulnerabilities are being provided by\n\t// another mechanism.\n\tDisableUpdaters bool `yaml:\"disable_updaters,omitempty\" json:\"disable_updaters,omitempty\"`\n\t// DisableEnrichment disables the enrichment of vulnerability data.\n\tDisableEnrichment bool `yaml:\"disable_enrichment,omitempty\" json:\"disable_enrichment,omitempty\"`\n}\n\nfunc (m *Matcher) validate(mode Mode) ([]Warning, error) {\n\tif mode != ComboMode && mode != MatcherMode {\n\t\treturn nil, nil\n\t}\n\tif m.Period == 0 {\n\t\tm.Period = Duration(DefaultMatcherPeriod)\n\t}\n\tswitch {\n\tcase m.UpdateRetention < 0:\n\t\t// Less than 0 means GC is off.\n\t\tm.UpdateRetention = 0\n\tcase m.UpdateRetention < 2:\n\t\t// Anything less than 2 gets the default.\n\t\tm.UpdateRetention = DefaultUpdateRetention\n\t}\n\tif m.CacheAge == 0 {\n\t\tm.CacheAge = m.Period\n\t}\n\tswitch mode {\n\tcase ComboMode:\n\tcase MatcherMode:\n\t\tif m.IndexerAddr == \"\" {\n\t\t\treturn nil, fmt.Errorf(\"matcher mode requires a remote Indexer address\")\n\t\t}\n\t\t_, err := url.Parse(m.IndexerAddr)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to parse matcher mode IndexerAddr string: %v\", err)\n\t\t}\n\tdefault:\n\t\tpanic(\"programmer error\")\n\t}\n\treturn m.lint()\n}\n\nfunc (m *Matcher) lint() (ws []Warning, err error) {\n\tws, err = checkDSN(m.ConnString)\n\tif err != nil {\n\t\treturn ws, err\n\t}\n\tfor i := range ws {\n\t\tws[i].path = \".connstring\"\n\t}\n\n\tif m.Period < Duration(DefaultMatcherPeriod) {\n\t\tws = append(ws, Warning{\n\t\t\tpath: \".period\",\n\t\t\tmsg:  \"updater period is very aggressive: most sources are updated daily\",\n\t\t})\n\t}\n\tif m.CacheAge < m.Period/2 {\n\t\tws = append(ws, Warning{\n\t\t\tpath: \".cache_age\",\n\t\t\tmsg:  \"expiry very low: may result in increased workload\",\n\t\t})\n\t}\n\tif m.UpdateRetention == 0 {\n\t\tws = append(ws, Warning{\n\t\t\tpath: \".update_retention\",\n\t\t\tmsg:  \"update garbage collection is off\",\n\t\t})\n\t}\n\tif m.MaxConnPool != 0 {\n\t\tws = append(ws, Warning{\n\t\t\tpath: \".max_conn_pool\",\n\t\t\tmsg:  \"this parameter will be ignored in a future release\",\n\t\t})\n\t}\n\n\treturn ws, nil\n}\n"
  },
  {
    "path": "config/matchers.go",
    "content": "package config\n\n// Matchers configures the individual matchers run by the matcher system.\ntype Matchers struct {\n\t// Config holds configuration blocks for MatcherFactories and Matchers,\n\t// keyed by name.\n\tConfig map[string]interface{} `yaml:\"config,omitempty\" json:\"config,omitempty\"`\n\t// A slice of strings representing which matchers will be used.\n\t//\n\t// If nil all default Matchers will be used\n\t//\n\t// The following names are supported by default:\n\t// \"alpine\"\n\t// \"aws\"\n\t// \"debian\"\n\t// \"oracle\"\n\t// \"photon\"\n\t// \"python\"\n\t// \"rhel\"\n\t// \"suse\"\n\t// \"ubuntu\"\n\t// \"crda\" - remotematcher calls hosted api via RPC.\n\tNames []string `yaml:\"names,omitempty\" json:\"names,omitempty\"`\n}\n"
  },
  {
    "path": "config/notifier.go",
    "content": "package config\n\nimport (\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"reflect\"\n\t\"strings\"\n\t\"time\"\n)\n\n// Notifier provides Clair Notifier node configuration\ntype Notifier struct {\n\t// Only one of the following should be provided in the configuration\n\t//\n\t// Configures the notifier for webhook delivery\n\tWebhook *Webhook `yaml:\"webhook,omitempty\" json:\"webhook,omitempty\"`\n\t// Configures the notifier for AMQP delivery.\n\tAMQP *AMQP `yaml:\"amqp,omitempty\" json:\"amqp,omitempty\"`\n\t// Configures the notifier for STOMP delivery.\n\tSTOMP *STOMP `yaml:\"stomp,omitempty\" json:\"stomp,omitempty\"`\n\t// A Postgres connection string.\n\t//\n\t// Formats:\n\t// url: \"postgres://pqgotest:password@localhost/pqgotest?sslmode=verify-full\"\n\t// or\n\t// string: \"user=pqgotest dbname=pqgotest sslmode=verify-full\"\n\tConnString string `yaml:\"connstring\" json:\"connstring\"`\n\t// A string in <host>:<port> format where <host> can be an empty string.\n\t//\n\t// A Notifier contacts an Indexer to create obtain manifests affected by vulnerabilities.\n\t// The location of this Indexer is required.\n\tIndexerAddr string `yaml:\"indexer_addr\" json:\"indexer_addr\"`\n\t// A string in <host>:<port> format where <host> can be an empty string.\n\t//\n\t// A Notifier contacts a Matcher to list update operations and acquire diffs.\n\t// The location of this Indexer is required.\n\tMatcherAddr string `yaml:\"matcher_addr\" json:\"matcher_addr\"`\n\t// A time.ParseDuration parsable string\n\t//\n\t// The frequency at which the notifier will query at Matcher for Update Operations.\n\t// If a value smaller then 1 minute is provided it will be replaced with the\n\t// default 6 hour poll interval as the default updater period is 6 hours.\n\tPollInterval Duration `yaml:\"poll_interval,omitempty\" json:\"poll_interval,omitempty\"`\n\t// A time.ParseDuration parsable string\n\t//\n\t// The frequency at which the notifier attempt delivery of created or previously failed\n\t// notifications\n\t// If a value smaller then 1 minute is provided it will be replaced with the\n\t// default 1 hour delivery interval.\n\tDeliveryInterval Duration `yaml:\"delivery_interval,omitempty\" json:\"delivery_interval,omitempty\"`\n\t// DisableSummary disables summarizing vulnerabilities per-manifest.\n\t//\n\t// The default is to summarize any new vulnerabilities to the most severe\n\t// one, in the thought that any additional processing for end-user\n\t// notifications can have policies around severity and fetch a complete\n\t// VulnerabilityReport if it'd like.\n\t//\n\t// For a machine-consumption use case, it may be easier to instead have the\n\t// notifier push all the data.\n\tDisableSummary bool `yaml:\"disable_summary,omitempty\" json:\"disable_summary,omitempty\"`\n\t// A \"true\" or \"false\" value\n\t//\n\t// Whether Notifier nodes handle migrations to their database.\n\tMigrations bool `yaml:\"migrations,omitempty\" json:\"migrations,omitempty\"`\n}\n\nfunc (n *Notifier) validate(mode Mode) ([]Warning, error) {\n\tif mode != ComboMode && mode != NotifierMode {\n\t\treturn nil, nil\n\t}\n\tif n.PollInterval < Duration(1*time.Minute) {\n\t\tn.PollInterval = Duration(DefaultNotifierPollInterval)\n\t}\n\tif n.DeliveryInterval < Duration(1*time.Minute) {\n\t\tn.DeliveryInterval = Duration(DefaultNotifierDeliveryInterval)\n\t}\n\tswitch mode {\n\tcase ComboMode:\n\tcase NotifierMode:\n\t\tif n.IndexerAddr == \"\" {\n\t\t\treturn nil, fmt.Errorf(\"notifier mode requires a remote Indexer\")\n\t\t}\n\t\tif n.MatcherAddr == \"\" {\n\t\t\treturn nil, fmt.Errorf(\"notifier mode requires a remote Matcher\")\n\t\t}\n\tdefault:\n\t\tpanic(\"programmer error\")\n\t}\n\treturn n.lint()\n}\n\nfunc (n *Notifier) lint() (ws []Warning, err error) {\n\tws, err = checkDSN(n.ConnString)\n\tif err != nil {\n\t\treturn ws, err\n\t}\n\tfor i := range ws {\n\t\tws[i].path = \".connstring\"\n\t}\n\tgot := 0\n\tif n.AMQP != nil {\n\t\tgot++\n\t}\n\tif n.STOMP != nil {\n\t\tgot++\n\t}\n\tif n.Webhook != nil {\n\t\tgot++\n\t}\n\tswitch {\n\tcase got == 0 && !reflect.ValueOf(n).Elem().IsZero():\n\t\tws = append(ws, Warning{\n\t\t\tmsg: \"no delivery mechanisms specified\",\n\t\t})\n\tcase got > 1:\n\t\tws = append(ws, Warning{\n\t\t\tmsg: \"multiple delivery mechanisms specified\",\n\t\t})\n\t}\n\n\tif n.PollInterval < Duration(DefaultNotifierPollInterval) {\n\t\tws = append(ws, Warning{\n\t\t\tpath: \".poll_interval\",\n\t\t\tmsg:  \"interval is very fast: may result in increased workload\",\n\t\t})\n\t}\n\tif n.DeliveryInterval < Duration(DefaultNotifierDeliveryInterval) {\n\t\tws = append(ws, Warning{\n\t\t\tpath: \".delivery_interval\",\n\t\t\tmsg:  \"interval is very fast: may result in increased workload\",\n\t\t})\n\t}\n\tif n.DisableSummary {\n\t\tws = append(ws, Warning{\n\t\t\tpath: \".disable_summary\",\n\t\t\tmsg:  \"disabling notification summary significantly increases memory consumption\",\n\t\t})\n\t}\n\n\treturn ws, nil\n}\n\n// Webhook configures the \"webhook\" notification mechanism.\ntype Webhook struct {\n\t// any HTTP headers necessary for the request to Target\n\tHeaders http.Header `yaml:\"headers,omitempty\" json:\"headers,omitempty\"`\n\t// the URL where our webhook will be delivered\n\tTarget string `yaml:\"target\" json:\"target\"`\n\t// the callback url where notifications can be received\n\t// the notification will be appended to this url\n\tCallback string `yaml:\"callback\" json:\"callback\"`\n\t// whether the webhook deliverer will sign out going.\n\t// if true webhooks will be sent with a jwt signed by\n\t// the notifier's private key.\n\tSigned bool `yaml:\"signed,omitempty\" json:\"signed,omitempty\"`\n}\n\n// Validate will return a copy of the Config on success.\n// If any validation fails an error will be returned.\nfunc (w *Webhook) validate(mode Mode) ([]Warning, error) {\n\tif mode != ComboMode && mode != NotifierMode {\n\t\treturn nil, nil\n\t}\n\tvar ws []Warning\n\tif _, err := url.Parse(w.Target); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to parse target url: %w\", err)\n\t}\n\n\t// Require trailing slash so url.Parse() can easily append notification id.\n\tif !strings.HasSuffix(w.Callback, \"/\") {\n\t\tw.Callback = w.Callback + \"/\"\n\t\tws = append(ws, Warning{\n\t\t\tpath: \".callback\",\n\t\t\tmsg:  `URL should end in a \"/\"`,\n\t\t})\n\t}\n\n\tif _, err := url.Parse(w.Callback); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to parse callback url: %w\", err)\n\t}\n\tls, err := w.lint()\n\tws = append(ws, ls...)\n\tif err != nil {\n\t\treturn ws, err\n\t}\n\treturn ws, nil\n}\n\nfunc (w *Webhook) lint() ([]Warning, error) {\n\tif w.Signed {\n\t\treturn []Warning{{\n\t\t\tpath:  \".signed\",\n\t\t\tinner: ErrDeprecated,\n\t\t}}, nil\n\t}\n\treturn nil, nil\n}\n\n// Exchange are the required fields necessary to check\n// the existence of an Exchange\n//\n// For more details see: https://godoc.org/github.com/streadway/amqp#Channel.ExchangeDeclarePassive\ntype Exchange struct {\n\t// The name of the exchange\n\tName string `yaml:\"name\" json:\"name\"`\n\t// The type of the exchange. Typically:\n\t// \"direct\"\n\t// \"fanout\"\n\t// \"topic\"\n\t// \"headers\"\n\tType string `yaml:\"type\" json:\"type\"`\n\t// Whether the exchange survives server restarts\n\tDurable bool `yaml:\"durability,omitempty\" json:\"durability,omitempty\"`\n\t// Whether bound consumers define the lifecycle of the Exchange.\n\tAutoDelete bool `yaml:\"auto_delete,omitempty\" json:\"auto_delete,omitempty\"`\n}\n\nfunc (e *Exchange) validate(_ Mode) ([]Warning, error) {\n\tif e.Type == \"\" {\n\t\treturn nil, fmt.Errorf(\"field required\")\n\t}\n\treturn nil, nil\n}\n\n// AMQP configures the AMQP notification mechanism.\ntype AMQP struct {\n\tTLS *TLS `yaml:\"tls,omitempty\" json:\"tls,omitempty\"`\n\t// The AMQP exchange notifications will be delivered to.\n\t// A passive declare is performed and if the exchange does not exist\n\t// the declare will fail.\n\tExchange Exchange `yaml:\"exchange\" json:\"exchange\"`\n\t// The routing key used to route notifications to the desired queue.\n\tRoutingKey string `yaml:\"routing_key\" json:\"routing_key\"`\n\t// The callback url where notifications are retrieved.\n\tCallback string `yaml:\"callback\" json:\"callback\"`\n\t// A list of AMQP compliant URI scheme. see: https://www.rabbitmq.com/uri-spec.html\n\t// example: \"amqp://user:pass@host:10000/vhost\"\n\t//\n\t// The first successful connection will be used by the amqp deliverer.\n\t//\n\t// If \"amqps://\" broker URI schemas are provided the TLS configuration below is required.\n\tURIs []string `yaml:\"uris\" json:\"uris\"`\n\t// Specifies the number of notifications delivered in single AMQP message\n\t// when Direct is true.\n\t//\n\t// Ignored if Direct is not true\n\t// If 0 or 1 is provided no rollup occurs and each notification is delivered\n\t// separately.\n\tRollup int `yaml:\"rollup,omitempty\" json:\"rollup,omitempty\"`\n\t// AMQPConfigures the AMQP delivery to deliver notifications directly to\n\t// the configured Exchange.\n\t//\n\t// If true \"Callback\" is ignored.\n\t// If false a notifier.Callback is delivered to the queue and clients\n\t// utilize the pagination API to retrieve.\n\tDirect bool `yaml:\"direct,omitempty\" json:\"direct,omitempty\"`\n}\n\n// Validate confirms configuration is valid.\nfunc (c *AMQP) validate(mode Mode) ([]Warning, error) {\n\tif mode != ComboMode && mode != NotifierMode {\n\t\treturn nil, nil\n\t}\n\tvar ws []Warning\n\tif c.RoutingKey == \"\" {\n\t\treturn nil, fmt.Errorf(\"AMQP config requires the routing key field\")\n\t}\n\tif len(c.URIs) == 0 {\n\t\treturn nil, fmt.Errorf(\"missing URIs for AMQP broker\")\n\t}\n\tfor _, uri := range c.URIs {\n\t\tif _, err := url.Parse(uri); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"invalid URI %q: %w\", uri, err)\n\t\t}\n\t}\n\n\tif !c.Direct {\n\t\tif !strings.HasSuffix(c.Callback, \"/\") {\n\t\t\tc.Callback = c.Callback + \"/\"\n\t\t\tws = append(ws, Warning{\n\t\t\t\tpath: \".callback\",\n\t\t\t\tmsg:  `URL should end in a \"/\"`,\n\t\t\t})\n\t\t}\n\t\tif _, err := url.Parse(c.Callback); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to parse callback url: %w\", err)\n\t\t}\n\t}\n\tls, err := c.lint()\n\tws = append(ws, ls...)\n\tif err != nil {\n\t\treturn ws, err\n\t}\n\treturn ws, nil\n}\n\nfunc (c *AMQP) lint() (w []Warning, err error) {\n\tif c.Rollup == 1 {\n\t\tw = append(w, Warning{\n\t\t\tmsg: \"`Rollup` set to 1: this means nothing\",\n\t\t})\n\t}\n\tif c.Direct && c.Callback != \"\" {\n\t\tw = append(w, Warning{\n\t\t\tmsg: \"`Callback` and `Direct` set: `Callback` will be ignored\",\n\t\t})\n\t}\n\treturn w, nil\n}\n\n// Login is the login details for a STOMP broker.\ntype Login struct {\n\tLogin    string `yaml:\"login\" json:\"login\"`\n\tPasscode string `yaml:\"passcode\" json:\"passcode\"`\n}\n\n// STOMP configures the STOMP notification mechanism.\ntype STOMP struct {\n\t// optional tls portion of config\n\tTLS *TLS `yaml:\"tls,omitempty\" json:\"tls,omitempty\"`\n\t// optional user login portion of config\n\tLogin *Login `yaml:\"user,omitempty\" json:\"user,omitempty\"`\n\t// The callback url where notifications are retrieved.\n\tCallback string `yaml:\"callback\" json:\"callback\"`\n\t// the destination messages will be delivered to\n\tDestination string `yaml:\"destination\" json:\"destination\"`\n\t// a list of URIs to send messages to.\n\t// a linear search of this list is always performed.\n\t//\n\t// Note that \"URI\" is a misnomer, this must be host:port pairs.\n\tURIs []string `yaml:\"uris\" json:\"uris\"`\n\t// Specifies the number of notifications delivered in single STOMP message\n\t// when Direct is true.\n\t//\n\t// Ignored if Direct is not true\n\t// If 0 or 1 is provided no rollup occurs and each notification is delivered\n\t// separately.\n\tRollup int `yaml:\"rollup,omitempty\" json:\"rollup,omitempty\"`\n\t// Configures the STOMP delivery to deliver notifications directly to\n\t// the configured Destination.\n\t//\n\t// If true \"Callback\" is ignored.\n\t// If false a notifier.Callback is delivered to the queue and clients\n\t// utilize the pagination API to retrieve.\n\tDirect bool `yaml:\"direct,omitempty\" json:\"direct,omitempty\"`\n}\n\nfunc (c *STOMP) validate(mode Mode) ([]Warning, error) {\n\tif mode != ComboMode && mode != NotifierMode {\n\t\treturn nil, nil\n\t}\n\tvar ws []Warning\n\tif len(c.URIs) == 0 {\n\t\treturn nil, fmt.Errorf(\"missing URIs for STOMP broker\")\n\t}\n\tfor _, u := range c.URIs {\n\t\tif _, _, err := net.SplitHostPort(u); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"bad host:port %q: %w\", u, err)\n\t\t}\n\t}\n\tif !c.Direct {\n\t\tif !strings.HasSuffix(c.Callback, \"/\") {\n\t\t\tc.Callback = c.Callback + \"/\"\n\t\t\tws = append(ws, Warning{\n\t\t\t\tpath: \".callback\",\n\t\t\t\tmsg:  `URL should end in a \"/\"`,\n\t\t\t})\n\t\t}\n\t\tif _, err := url.Parse(c.Callback); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to parse callback url: %w\", err)\n\t\t}\n\t}\n\tls, err := c.lint()\n\tws = append(ws, ls...)\n\tif err != nil {\n\t\treturn ws, err\n\t}\n\treturn ws, nil\n}\n\nfunc (c *STOMP) lint() (w []Warning, err error) {\n\tif c.Rollup == 1 {\n\t\tw = append(w, Warning{\n\t\t\tmsg: \"`Rollup` set to 1: this means nothing\",\n\t\t})\n\t}\n\tif c.Direct && c.Callback != \"\" {\n\t\tw = append(w, Warning{\n\t\t\tmsg: \"`Callback` and `Direct` set: `Callback` will be ignored\",\n\t\t})\n\t}\n\treturn w, nil\n}\n"
  },
  {
    "path": "config/otlp.go",
    "content": "package config\n\nimport (\n\t\"fmt\"\n\t\"path\"\n\t\"strings\"\n)\n\n// OTLPCommon is common configuration options for an OTLP client.\ntype OTLPCommon struct {\n\t// Compression configures payload compression.\n\t//\n\t// Only \"gzip\" is guaranteed to exist for both HTTP and gRPC.\n\tCompression OTLPCompressor `yaml:\"compression,omitempty\" json:\"compression,omitempty\"`\n\t// Endpoint is the host and port pair that the client should connect to.\n\t// This is not a URL and must not have a scheme or trailing slashes.\n\t//\n\t// The default is \"localhost:4317\" for gRPC and \"localhost:4318\" for HTTP.\n\tEndpoint string `yaml:\"endpoint,omitempty\" json:\"endpoint,omitempty\"`\n\t// Headers adds additional headers to requests.\n\tHeaders map[string]string `yaml:\"headers,omitempty\" json:\"headers,omitempty\"`\n\t// Insecure allows using an unsecured connection to the collector.\n\t//\n\t// For gRPC, this means certificate validation is not done.\n\t// For HTTP, this means HTTP is used instead of HTTPS.\n\tInsecure bool `yaml:\"insecure,omitempty\" json:\"insecure,omitempty\"`\n\t// Timeout is the maximum amount of time for a submission.\n\t//\n\t// The default is 10 seconds.\n\tTimeout *Duration `yaml:\"timeout,omitempty\" json:\"timeout,omitempty\"`\n\t// ClientTLS configures client TLS certificates, meaning a user should\n\t// ignore the \"RootCA\" member and look only at the \"Cert\" and \"Key\" members.\n\t//\n\t// See the documentation for the TLS struct for recommendations on\n\t// configuring certificate authorities.\n\tClientTLS *TLS `yaml:\"client_tls,omitempty\" json:\"client_tls,omitempty\"`\n}\n\n// Lint implements [linter].\nfunc (c *OTLPCommon) lint() (ws []Warning, _ error) {\n\tif c.Timeout != nil && *c.Timeout == 0 {\n\t\tws = append(ws, Warning{\n\t\t\tpath: \".timeout\",\n\t\t\tmsg:  \"timeout of 0 is almost certainly wrong\",\n\t\t})\n\t}\n\treturn ws, nil\n}\n\n// Validate implements [validator].\nfunc (c *OTLPCommon) validate(_ Mode) (ws []Warning, _ error) {\n\treturn c.lint()\n}\n\n// OTLPHTTPCommon is common configuration options for an OTLP HTTP client.\ntype OTLPHTTPCommon struct {\n\tOTLPCommon\n\t// URLPath overrides the URL path for sending traces. If unset, the default\n\t// is \"/v1/traces\".\n\tURLPath string `yaml:\"url_path,omitempty\" json:\"url_path,omitempty\"`\n}\n\n// Lint implements [linter].\nfunc (c *OTLPHTTPCommon) lint() (ws []Warning, _ error) {\n\tif c.URLPath != \"\" && strings.HasSuffix(c.URLPath, \"/\") {\n\t\tws = append(ws, Warning{\n\t\t\tpath: \".URLPath\",\n\t\t\tmsg:  fmt.Sprintf(\"path %q has a trailing slash; this is probably incorrect\", c.URLPath),\n\t\t})\n\t}\n\treturn ws, nil\n}\n\n// Validate implements [validator].\nfunc (c *OTLPHTTPCommon) validate(_ Mode) (ws []Warning, err error) {\n\tws, err = c.lint()\n\tif err != nil {\n\t\treturn ws, err\n\t}\n\tif c.URLPath != \"\" {\n\t\tc.URLPath = path.Clean(c.URLPath)\n\t\tif !path.IsAbs(c.URLPath) {\n\t\t\treturn ws, &Warning{\n\t\t\t\tpath: \".URLPath\",\n\t\t\t\tmsg:  fmt.Sprintf(\"path %q must be absolute\", c.URLPath),\n\t\t\t}\n\t\t}\n\t}\n\treturn ws, nil\n}\n\n// OTLPgRPCCommon is common configuration options for an OTLP gRPC client.\ntype OTLPgRPCCommon struct {\n\tOTLPCommon\n\t// Reconnect sets the minimum amount of time between connection attempts.\n\tReconnect *Duration `yaml:\"reconnect,omitempty\" json:\"reconnect,omitempty\"`\n\t// ServiceConfig specifies a gRPC service config as a string containing JSON.\n\t// See the [doc] for the format and possibilities.\n\t//\n\t// [doc]: https://github.com/grpc/grpc/blob/master/doc/service_config.md\n\tServiceConfig string `yaml:\"service_config,omitempty\" json:\"service_config,omitempty\"`\n}\n\n// TraceOTLP is the configuration for an OTLP traces client.\n//\n// See the [OpenTelemetry docs] for more information on traces.\n// See the Clair docs for the current status of of the instrumentation.\n//\n// [OpenTelemetry docs]: https://opentelemetry.io/docs/concepts/signals/traces/\ntype TraceOTLP struct {\n\t// HTTP configures OTLP via HTTP.\n\tHTTP *TraceOTLPHTTP `yaml:\"http,omitempty\" json:\"http,omitempty\"`\n\t// GRPC configures OTLP via gRPC.\n\tGRPC *TraceOTLPgRPC `yaml:\"grpc,omitempty\" json:\"grpc,omitempty\"`\n}\n\n// Lint implements [linter].\nfunc (t *TraceOTLP) lint() (ws []Warning, _ error) {\n\tif t.HTTP != nil && t.GRPC != nil {\n\t\tws = append(ws, Warning{\n\t\t\tmsg: `both \"http\" and \"grpc\" are configured, this may cause duplicate submissions`,\n\t\t})\n\t}\n\treturn ws, nil\n}\n\n// TraceOTLPHTTP is the configuration for an OTLP traces HTTP client.\ntype TraceOTLPHTTP struct {\n\tOTLPHTTPCommon\n}\n\n// TraceOTLPgRPC is the configuration for an OTLP traces gRPC client.\ntype TraceOTLPgRPC struct {\n\tOTLPgRPCCommon\n}\n\n// MetricOTLP is the configuration for an OTLP metrics client.\n//\n// See the [OpenTelemetry docs] for more information on metrics.\n// See the Clair docs for the current status of of the instrumentation.\n//\n// [OpenTelemetry docs]: https://opentelemetry.io/docs/concepts/signals/metrics/\ntype MetricOTLP struct {\n\t// HTTP configures OTLP via HTTP.\n\tHTTP *MetricOTLPHTTP `yaml:\"http,omitempty\" json:\"http,omitempty\"`\n\t// GRPC configures OTLP via gRPC.\n\tGRPC *MetricOTLPgRPC `yaml:\"grpc,omitempty\" json:\"grpc,omitempty\"`\n}\n\n// Lint implements [linter].\nfunc (m *MetricOTLP) lint() (ws []Warning, _ error) {\n\tif m.HTTP != nil && m.GRPC != nil {\n\t\tws = append(ws, Warning{\n\t\t\tmsg: `both \"http\" and \"grpc\" are configured, this may cause duplicate submissions`,\n\t\t})\n\t}\n\treturn ws, nil\n}\n\n// MetricOTLPHTTP is the configuration for an OTLP metrics HTTP client.\ntype MetricOTLPHTTP struct {\n\tOTLPHTTPCommon\n}\n\n// MetricOTLPgRPC is the configuration for an OTLP metrics gRPC client.\ntype MetricOTLPgRPC struct {\n\tOTLPgRPCCommon\n}\n\n//go:generate go run golang.org/x/tools/cmd/stringer@latest -type OTLPCompressor -linecomment\n\n// OTLPCompressor is the valid options for compressing OTLP payloads.\ntype OTLPCompressor int\n\n// OTLPCompressor values\nconst (\n\tOTLPCompressUnset OTLPCompressor = iota //\n\tOTLPCompressNone                        // none\n\tOTLPCompressGzip                        // gzip\n)\n"
  },
  {
    "path": "config/otlpcompressor_string.go",
    "content": "// Code generated by \"stringer -type OTLPCompressor -linecomment\"; DO NOT EDIT.\n\npackage config\n\nimport \"strconv\"\n\nfunc _() {\n\t// An \"invalid array index\" compiler error signifies that the constant values have changed.\n\t// Re-run the stringer command to generate them again.\n\tvar x [1]struct{}\n\t_ = x[OTLPCompressUnset-0]\n\t_ = x[OTLPCompressNone-1]\n\t_ = x[OTLPCompressGzip-2]\n}\n\nconst _OTLPCompressor_name = \"nonegzip\"\n\nvar _OTLPCompressor_index = [...]uint8{0, 0, 4, 8}\n\nfunc (i OTLPCompressor) String() string {\n\tif i < 0 || i >= OTLPCompressor(len(_OTLPCompressor_index)-1) {\n\t\treturn \"OTLPCompressor(\" + strconv.FormatInt(int64(i), 10) + \")\"\n\t}\n\treturn _OTLPCompressor_name[_OTLPCompressor_index[i]:_OTLPCompressor_index[i+1]]\n}\n"
  },
  {
    "path": "config/reflect.go",
    "content": "package config\n\nimport (\n\t\"fmt\"\n\t\"reflect\"\n\t\"strings\"\n)\n\ntype walkFunc func(interface{}) ([]Warning, error)\n\nfunc forEach(i interface{}, f walkFunc) ([]Warning, error) {\n\tvar ws []Warning\n\tv := reflect.ValueOf(i)\n\treturn ws, walk(&ws, \"$\", v, f)\n}\n\nfunc walk(ws *[]Warning, path string, v reflect.Value, wf walkFunc) error {\n\tt := v.Type()\n\tvar vi interface{}\n\t// Figure out if we should take the address to do the interface\n\t// assertion, or if the value is already a pointer.\n\tswitch {\n\tcase t.Kind() != reflect.Ptr && v.CanAddr():\n\t\tvi = v.Addr().Interface()\n\tcase v.CanInterface() && v.IsValid() && !v.IsZero():\n\t\tvi = v.Interface()\n\t}\n\n\tif vi != nil {\n\t\tw, err := wf(vi)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tfor i := range w {\n\t\t\t// Adjust the path here, so that the lint method doesn't need to\n\t\t\t// know where it is.\n\t\t\tw[i].path = path + w[i].path\n\t\t}\n\t\t*ws = append(*ws, w...)\n\t}\n\n\t// Dereference the pointer, if this is a pointer.\n\tif t.Kind() == reflect.Ptr {\n\t\tt = t.Elem()\n\t\tv = v.Elem()\n\t}\n\tif !v.IsValid() {\n\t\treturn nil\n\t}\n\tswitch t.Kind() {\n\tcase reflect.Struct:\n\t\tfor i, lim := 0, t.NumField(); i < lim; i++ {\n\t\t\tf := t.Field(i)\n\t\t\tn := f.Name\n\t\t\tif t := f.Tag.Get(\"json\"); t != \"\" && t != \"-\" {\n\t\t\t\t// Handle the comma options.\n\t\t\t\tif i := strings.IndexByte(t, ','); i != -1 {\n\t\t\t\t\tt = t[:i]\n\t\t\t\t}\n\t\t\t\tn = t\n\t\t\t}\n\t\t\tp := fmt.Sprintf(`%s.%s`, path, n)\n\t\t\tif err := walk(ws, p, v.Field(i), wf); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\tcase reflect.Map:\n\t\ti := v.MapRange()\n\t\tfor i.Next() {\n\t\t\tp := fmt.Sprintf(`%s.[%s]`, path, i.Key().String())\n\t\t\tif err := walk(ws, p, i.Value(), wf); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t}\n\tcase reflect.Slice:\n\t\tfor i, lim := 0, v.Len(); i < lim; i++ {\n\t\t\tp := fmt.Sprintf(`%s.[%d]`, path, i)\n\t\t\tif err := walk(ws, p, v.Index(i), wf); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\tdefault:\n\t\t// everything else, just pass\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "config/tags_test.go",
    "content": "package config\n\nimport (\n\t\"reflect\"\n\t\"testing\"\n)\n\n// TestTags checks that exported types and fields used in the root Config struct\n// have struct tags.\nfunc TestTags(t *testing.T) {\n\tt.Logf(\"checking for: %v\", wanttags)\n\tnt := reflect.TypeOf(Config{})\n\tt.Run(nt.Name(), func(t *testing.T) { typecheck(t, nt) })\n}\n\nvar wanttags = []string{`json`, `yaml`}\n\nfunc typecheck(t *testing.T, typ reflect.Type) {\n\tfor i, lim := 0, typ.NumField(); i < lim; i++ {\n\t\tf := typ.Field(i)\n\t\tif !f.IsExported() {\n\t\t\tcontinue\n\t\t}\n\t\t// track the number of names for this field\n\t\tvals := make(map[string]struct{})\n\t\t// track which tag has which name\n\t\ttagval := make(map[string]string)\n\t\t// If embedded, there shouldn't be any tags.\n\t\tif f.Anonymous {\n\t\t\tif f.Tag != \"\" {\n\t\t\t\tt.Errorf(\"%s.%s: unexpected tag %q\", typ.Name(), f.Name, f.Tag)\n\t\t\t}\n\t\t\tgoto Recurse\n\t\t}\n\t\tfor _, n := range wanttags {\n\t\t\tif v, ok := f.Tag.Lookup(n); !ok {\n\t\t\t\tt.Errorf(\"%s.%s: missing %q tag\", typ.Name(), f.Name, n)\n\t\t\t} else {\n\t\t\t\tvals[v] = struct{}{}\n\t\t\t\ttagval[n] = v\n\t\t\t}\n\t\t}\n\t\tif len(vals) != 1 {\n\t\t\tt.Errorf(\"different names for %q: %v\", f.Name, tagval)\n\t\t}\n\tRecurse:\n\t\t// Recurse on structs and pointers-to-structs.\n\t\tswitch nt := f.Type; nt.Kind() {\n\t\tcase reflect.Ptr:\n\t\t\tpt := nt.Elem()\n\t\t\tif pt.Kind() != reflect.Struct {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tnt = nt.Elem()\n\t\t\tfallthrough\n\t\tcase reflect.Struct:\n\t\t\tt.Run(nt.Name(), func(t *testing.T) { typecheck(t, nt) })\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "config/tls.go",
    "content": "package config\n\nimport (\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n)\n\n// TLS describes some TLS settings.\n//\n// Some uses of this type ignore the RootCA member; see the documentation at the\n// use site to determine if that's the case.\n//\n// Using the environment variables \"SSL_CERT_DIR\" or \"SSL_CERT_FILE\" or\n// modifying the system's trust store are the ways to modify root CAs for all\n// outgoing TLS connections.\ntype TLS struct {\n\t// The filesystem path where a root CA can be read.\n\t//\n\t// This can also be controlled by the SSL_CERT_FILE and SSL_CERT_DIR\n\t// environment variables, or adding the relevant certs to the system trust\n\t// store.\n\tRootCA string `yaml:\"root_ca\" json:\"root_ca\"`\n\t// The filesystem path where a TLS certificate can be read.\n\tCert string `yaml:\"cert\" json:\"cert\"`\n\t// The filesystem path where a TLS private key can be read.\n\tKey string `yaml:\"key\" json:\"key\"`\n}\n\n// Config returns a tls.Config modified according to the TLS struct.\n//\n// If the *TLS is nil, a default tls.Config is returned.\nfunc (t *TLS) Config() (*tls.Config, error) {\n\tvar cfg tls.Config\n\tif t == nil {\n\t\treturn &cfg, nil\n\t}\n\n\tif t.RootCA != \"\" {\n\t\tp, err := x509.SystemCertPool()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tca, err := os.ReadFile(t.RootCA)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to read tls root ca: %w\", err)\n\t\t}\n\t\tif !p.AppendCertsFromPEM(ca) {\n\t\t\treturn nil, errors.New(\"unable to add certificate to pool\")\n\t\t}\n\t\tcfg.RootCAs = p\n\t}\n\n\tcert, err := tls.LoadX509KeyPair(t.Cert, t.Key)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to read x509 cert and key pair: %w\", err)\n\t}\n\tcfg.Certificates = append(cfg.Certificates, cert)\n\tcfg.MinVersion = tls.VersionTLS12\n\n\treturn &cfg, nil\n}\n\nfunc (t *TLS) lint() ([]Warning, error) {\n\tif t.RootCA != \"\" {\n\t\treturn []Warning{{\n\t\t\tpath:  \".root_ca\",\n\t\t\tinner: fmt.Errorf(`use environment variables \"SSL_CERT_FILE\" or \"SSL_CERT_DIR\": %w`, ErrDeprecated),\n\t\t}}, nil\n\t}\n\treturn nil, nil\n}\n\nfunc (t *TLS) validate(_ Mode) ([]Warning, error) {\n\tif (t.Cert != \"\" || t.Key != \"\") && (t.Cert == \"\" || t.Key == \"\") {\n\t\treturn nil, errors.New(\"both tls cert and key are required\")\n\t}\n\tfor _, n := range []string{t.RootCA, t.Cert, t.Key} {\n\t\tif n == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\t_, err := os.Stat(n)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(`error accessing %q: %w`, n, err)\n\t\t}\n\t}\n\treturn nil, nil\n}\n"
  },
  {
    "path": "config/updaters.go",
    "content": "package config\n\n// Updaters configures updater behavior.\ntype Updaters struct {\n\t// Filter is a regexp that disallows updaters that do not match from\n\t// running.\n\t// TODO(louis): this is only used in clairctl, should we keep this?\n\t// it may offer an escape hatch for a particular updater name\n\t// from running, vs disabling the updater set completely.\n\tFilter string `yaml:\"filter,omitempty\" json:\"filter,omitempty\"`\n\t// Config holds configuration blocks for UpdaterFactories and Updaters,\n\t// keyed by name.\n\t//\n\t// These are defined by the updater implementation and can't be documented\n\t// here. Improving the documentation for these is an open issue.\n\tConfig map[string]interface{} `yaml:\"config,omitempty\" json:\"config,omitempty\"`\n\t// A slice of strings representing which updaters will be used.\n\t//\n\t// If nil all default UpdaterSets will be used\n\t//\n\t// The following sets are supported by default:\n\t// \"alpine\"\n\t// \"aws\"\n\t// \"clair.cvss\"\n\t// \"debian\"\n\t// \"oracle\"\n\t// \"osv\"\n\t// \"photon\"\n\t// \"rhcc\"\n\t// \"rhel-vex\"\n\t// \"suse\"\n\t// \"ubuntu\"\n\tSets []string `yaml:\"sets,omitempty\" json:\"sets,omitempty\"`\n}\n"
  },
  {
    "path": "config/validate.go",
    "content": "package config\n\n// Validate confirms the necessary values to support the desired Clair mode\n// exist and sets default values.\nfunc Validate(c *Config) ([]Warning, error) {\n\treturn forEach(c, func(i interface{}) ([]Warning, error) {\n\t\tif v, ok := i.(validator); ok {\n\t\t\treturn v.validate(c.Mode)\n\t\t}\n\t\treturn nil, nil\n\t})\n}\n\n// Types that want complex defaults or to fail validation can implement the\n// validator interface.\ntype validator interface {\n\tvalidate(Mode) ([]Warning, error)\n}\n"
  },
  {
    "path": "config.yaml.sample",
    "content": "# Copyright 2015 clair authors\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---\nintrospection_addr: localhost:8089\nhttp_listen_addr: localhost:8080\nlog_level: debug\nindexer:\n  connstring: host=localhost port=5432 user=clair dbname=clair sslmode=disable\n  scanlock_retry: 10\n  layer_scan_concurrency: 5\n  migrations: true\nmatcher:\n  indexer_addr: \"localhost:8080\"\n  connstring: host=localhost port=5432 user=clair dbname=clair sslmode=disable\n  max_conn_pool: 100\n  migrations: true\n  updater_sets:\n  - \"alpine\"\n  - \"aws\"\n  - \"debian\"\n  - \"oracle\"\n  - \"osv\"\n  - \"photon\"\n  - \"rhcc\"\n  - \"rhel\"\n  - \"suse\"\n  - \"ubuntu\"\nmatchers:\n  names:\n  - \"alpine-matcher\"\n  - \"aws-matcher\"\n  - \"debian-matcher\"\n  - \"gobin\"\n  - \"java-maven\"\n  - \"oracle\"\n  - \"photon\"\n  - \"python\"\n  - \"rhel\"\n  - \"rhel-container-matcher\"\n  - \"suse\"\n  - \"ubuntu\"\n  config: {}\nnotifier:\n  indexer_addr: http://clair-indexer:8080/\n  matcher_addr: http://clair-matcher:8080/\n  connstring: host=localhost port=5432 user=clair dbname=clair sslmode=disable\n  migrations: true\n  delivery_interval: 1m\n  poll_interval: 5m\n  # if multiple delivery methods are defined the only one will be selected.\n  # preference order:\n  # webhook, amqp, stomp\n  webhook:\n    target: \"http://webhook/\"\n    callback: \"http://clair-notifier/notifier/api/v1/notification\"\n  amqp:\n    exchange:\n        name: \"\"\n        type: \"direct\"\n        durable: true\n        auto_delete: false\n    uris: [\"amqp://user:pass@host:10000/vhost\"]\n    direct: false\n    routing_key: \"notifications\"\n    callback: \"http://clair-notifier/notifier/api/v1/notification\"\n    tls:\n     root_ca: \"optional/path/to/rootca\"\n     cert: \"mandatory/path/to/cert\"\n     key: \"mandatory/path/to/key\"\n  stomp:\n    destination: \"notifications\"\n    direct: false\n    callback: \"http://clair-notifier/notifier/api/v1/notification\"\n    login:\n      login: \"username\"\n      passcode: \"passcode\"\n    tls:\n     root_ca: \"optional/path/to/rootca\"\n     cert: \"mandatory/path/to/cert\"\n     key: \"mandatory/path/to/key\"\n\ntrace:\n  name: \"jaeger\"\n  probability: 1\n  jaeger:\n    agent:\n      endpoint: \"localhost:6831\"\n    service_name: \"clair\"\n\nmetrics:\n  name: \"prometheus\"\n"
  },
  {
    "path": "contrib/cmd/quaybackstop/Dockerfile",
    "content": "# syntax=docker.io/docker/dockerfile:1.7\n\n# Copyright 2024 clair authors\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\n# This Dockerfile expects the context to be the root of the repo, e.g.:\n#     go run github.com/moby/buildkit/cmd/buildctl@latest build \\\n#     --frontend dockerfile.v0 \\\n#     --local context=. --local dockerfile=contrib/cmd/quaybackstop\n\nARG GOTOOLCHAIN=local\nARG GO_VERSION=1.25\nFROM --platform=$BUILDPLATFORM quay.io/projectquay/golang:${GO_VERSION} AS build\nWORKDIR /build\nRUN --mount=type=cache,target=/root/.cache/go-build \\\n\t--mount=type=cache,target=/go/pkg/mod \\\n\t--mount=type=bind,source=go.mod,target=go.mod \\\n\t--mount=type=bind,source=go.sum,target=go.sum \\\n\tgo mod download\n\nARG TARGETOS\nARG TARGETARCH\nARG TARGETVARIANT\nRUN --mount=type=bind,target=. \\\n\t--mount=type=cache,target=/root/.cache/go-build \\\n\t--mount=type=cache,target=/go/pkg/mod \\\n\t--network=none \\\n\t<<.\nset -e\nexport GOOS=\"$TARGETOS\" GOARCH=\"$TARGETARCH\" GOBIN=/out/bin CGO_ENABLED=0\nif [ -n \"$TARGETVARIANT\" ]; then\n\tcase \"$TARGETARCH\" in\n\tamd64)  export GOAMD64=\"$TARGETVARIANT\" ;;\n\tppc64*) export GOPPC64=\"$TARGETVARIANT\" ;;\n\tesac\nfi\ninstall -d \"${GOBIN}\"\ngo build -ldflags=\"-s -w\" -trimpath -o \"${GOBIN}\" ./contrib/cmd/quaybackstop\n.\n\nFROM gcr.io/distroless/static:nonroot AS final\nCOPY --from=build /out/bin/quaybackstop /\nWORKDIR /run\nUSER 65532:65532\nENTRYPOINT [\"/quaybackstop\"]\n"
  },
  {
    "path": "contrib/cmd/quaybackstop/clair.go",
    "content": "//go:build go1.23\n\npackage main\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"iter\"\n\t\"log/slog\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"runtime\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/quay/clair/v4/cmd\"\n\n\t\"github.com/go-jose/go-jose/v3\"\n\t\"github.com/jackc/pgx/v5\"\n\t\"github.com/jackc/pgx/v5/pgxpool\"\n\t\"github.com/quay/clair/config\"\n\t\"github.com/rogpeppe/go-internal/lockedfile\"\n\t\"golang.org/x/sync/errgroup\"\n)\n\nfunc (a *App) SetClairGABI(s string) (err error) {\n\ta.ClairGABI, err = url.Parse(s)\n\treturn err\n}\n\nfunc (a *App) SetClairGABIAuth(s string) error {\n\tif s == \"\" {\n\t\treturn errors.New(\"bad Clair GABI auth: empty string\")\n\t}\n\ta.ClairGABIAuth = &s\n\treturn nil\n}\n\nfunc (a *App) SetClairConfig(s string) error {\n\tslog.Debug(\"clair config flag\", \"argument\", s)\n\ta.ClairConfig = new(config.Config)\n\tif err := cmd.LoadConfig(a.ClairConfig, s, false); err != nil {\n\t\treturn err\n\t}\n\n\tsk := jose.SigningKey{\n\t\tAlgorithm: jose.HS256,\n\t\tKey:       []byte(a.ClairConfig.Auth.PSK.Key),\n\t}\n\tsigner, err := jose.NewSigner(sk, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\ta.jwtSigner = signer\n\n\ta.clairDB = sync.OnceValues(func() (*pgxpool.Pool, error) {\n\t\tcfg, err := pgxpool.ParseConfig(a.ClairConfig.Indexer.ConnString)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tcfg.MaxConns = int32(1) // Should only need one -- only used for reading the set of Clair manifests.\n\t\tinit, done := context.WithTimeoutCause(context.Background(), 10*time.Second,\n\t\t\terrors.New(\"too slow to do initial connection to Clair database\"))\n\t\tdefer done()\n\t\treturn pgxpool.NewWithConfig(init, cfg)\n\t})\n\treturn nil\n}\n\nfunc (a *App) SetIndexerAddr(s string) (err error) {\n\ta.IndexerAddr, err = url.Parse(s)\n\treturn err\n}\n\n// AllManifests reports pages (slices of length [App.PageSize]) of all manifests\n// in the Clair database.\n//\n// This process is single-threaded, although it might be able to be made\n// concurrent with sufficient effort.\nfunc (a *App) AllManifests(ctx context.Context) (iter.Seq[[]string], func() error) {\n\tvar retErr error\n\tvar inner func(*int64) iter.Seq2[[]string, error]\n\tvar pageToken int64\n\tvar lastPage bool\n\tqstr := fmt.Sprintf(\n\t\t`SELECT id, hash FROM manifest WHERE id > $1 ORDER BY id ASC LIMIT %d;`,\n\t\ta.PageSize)\n\n\tupdateCursor := func() {}\n\tif a.CursorFile != nil {\n\t\tb, err := lockedfile.Read(*a.CursorFile)\n\t\tswitch {\n\t\tcase err == nil:\n\t\t\t_, err = fmt.Fscanln(bytes.NewReader(b), &pageToken)\n\t\tcase errors.Is(err, fs.ErrNotExist):\n\t\t\terr = nil\n\t\tdefault:\n\t\t}\n\t\tif err != nil {\n\t\t\treturn nil, func() error {\n\t\t\t\treturn fmt.Errorf(\"unable to read cursor file %q: %w\", *a.CursorFile, err)\n\t\t\t}\n\t\t}\n\t\tslog.Info(\"loaded id from cursor\", \"id\", pageToken)\n\n\t\tupdateCursor = func() {\n\t\t\tif lastPage {\n\t\t\t\tslog.Info(\"reached last page; resetting cursor for next run\", \"id\", pageToken)\n\t\t\t\tpageToken = 0\n\t\t\t}\n\t\t\terr := lockedfile.Transform(*a.CursorFile, func(prev []byte) ([]byte, error) {\n\t\t\t\tif !bytes.Equal(b, prev) {\n\t\t\t\t\treturn nil,\n\t\t\t\t\t\tfmt.Errorf(\"cursorfile changed while running, not updating (got %#q, expected %#q)\",\n\t\t\t\t\t\t\tstring(prev), string(b))\n\t\t\t\t}\n\t\t\t\treturn append(strconv.AppendInt(nil, pageToken, 10), '\\n'), nil\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\tslog.Error(\"unable to write cursor file\", \"error\", err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tslog.Info(\"wrote cursor file\", \"file\", *a.CursorFile, \"id\", pageToken)\n\t\t}\n\t}\n\n\tswitch {\n\t// Prefer using the GABI interface.\n\tcase a.ClairGABI != nil:\n\t\tinner = func(id *int64) iter.Seq2[[]string, error] {\n\t\t\tstr := &strings.Builder{}\n\t\t\tbuf := &bytes.Buffer{}\n\t\t\tenc := json.NewEncoder(buf)\n\t\t\tenc.SetEscapeHTML(false)\n\t\t\tfstr := strings.ReplaceAll(qstr, \"$1\", \"%d\")\n\t\t\treturn func(yield func([]string, error) bool) {\n\t\t\t\tfor {\n\t\t\t\t\tstr.Reset()\n\t\t\t\t\tfmt.Fprintf(str, fstr, *id)\n\t\t\t\t\tif err := enc.Encode(query(str)); err != nil {\n\t\t\t\t\t\tyield(nil, err)\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\n\t\t\t\t\tvar qRes *gabiResponse\n\t\t\t\t\tqRes, err := a.GABIQuery(ctx, a.ClairGABI, buf)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tyield(nil, err)\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\n\t\t\t\t\t// Skip the initial value, it's the list of columns\n\t\t\t\t\trows := qRes.Result[1:]\n\t\t\t\t\t*id, err = strconv.ParseInt(rows[len(rows)-1][0], 10, 64)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tyield(nil, err)\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tvals := make([]string, len(rows))\n\t\t\t\t\tfor i, row := range rows {\n\t\t\t\t\t\tvals[i] = row[1]\n\t\t\t\t\t}\n\t\t\t\t\tif !yield(vals, nil) {\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t// Second favorite: direct database access.\n\tcase a.clairDB != nil:\n\t\tpool, err := a.clairDB()\n\t\tif err != nil {\n\t\t\tretErr = err\n\t\t\tbreak\n\t\t}\n\n\t\tinner = func(id *int64) iter.Seq2[[]string, error] {\n\t\t\treturn func(yield func([]string, error) bool) {\n\t\t\t\tfor {\n\t\t\t\t\tvals := make([]string, a.PageCount)\n\t\t\t\t\terr := pool.AcquireFunc(ctx, func(c *pgxpool.Conn) error {\n\t\t\t\t\t\trows, err := c.Query(ctx, qstr, *id)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t}\n\t\t\t\t\t\tdefer rows.Close()\n\n\t\t\t\t\t\ti := 0\n\t\t\t\t\t\tfor rows.Next() {\n\t\t\t\t\t\t\tif err := rows.Scan(id, &vals[i]); err != nil {\n\t\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\ti++\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif err := rows.Err(); err != nil {\n\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t}\n\t\t\t\t\t\tvals = vals[:i]\n\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t})\n\t\t\t\t\tswitch {\n\t\t\t\t\tcase err == nil:\n\t\t\t\t\tcase errors.Is(err, pgx.ErrNoRows):\n\t\t\t\t\tdefault:\n\t\t\t\t\t\tyield(nil, err)\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tif !yield(vals, nil) {\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Seq wraps \"inner\" and does page counting and error handling.\n\tseq := func(yield func([]string) bool) {\n\t\tnext, stop := iter.Pull2(inner(&pageToken))\n\t\tdefer stop()\n\t\tfor i := 0; a.PageCount < 0 || i < a.PageCount; i++ {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\tretErr = context.Cause(ctx)\n\t\t\t\treturn\n\t\t\tdefault:\n\t\t\t}\n\t\t\ts, err, valid := next()\n\t\t\tif err != nil {\n\t\t\t\tretErr = err\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !valid {\n\t\t\t\tslog.Debug(\"done reading manifests\", \"count\", len(s), \"want\", a.PageSize, \"valid\", valid)\n\t\t\t\tlastPage = true\n\t\t\t}\n\t\t\tif !yield(s) || lastPage {\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n\n\treturn seq, func() error {\n\t\tdefer updateCursor()\n\t\tif err := retErr; err != nil {\n\t\t\treturn fmt.Errorf(\"querying Clair DB: %w\", err)\n\t\t}\n\t\treturn nil\n\t}\n}\n\n// IssueDeletes sends bulk manifest delete requests to Clair.\n//\n// If not in dry-run mode, this requires the Indexer address and any needed auth\n// to be configured.\nfunc (a *App) IssueDeletes(ctx context.Context, seq iter.Seq[[]string]) error {\n\t// Memoized endpoint -- done this way to make the dry-run mode play nice.\n\tu := sync.OnceValue(func() *url.URL {\n\t\treturn a.IndexerAddr.JoinPath(\"manifest\")\n\t})\n\t// Channel that feeds the workers. Closed by the reader.\n\t// The reader waits on the passed context.\n\tch := make(chan []string)\n\t// Worker function: has its own dedicated buffer and JSON encoder.\n\tworker := func() error {\n\t\tbuf := &bytes.Buffer{}\n\t\tenc := json.NewEncoder(buf)\n\t\tfor todo := range ch {\n\t\t\tif a.DryRun {\n\t\t\t\tif len(todo) != 0 {\n\t\t\t\t\tslog.Debug(\"would delete\", \"manifests\", todo)\n\t\t\t\t}\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif err := enc.Encode(todo); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\treq, err := a.NewRequestWithContext(ctx, http.MethodDelete, u(), buf)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tres, err := http.DefaultClient.Do(req)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tswitch res.StatusCode {\n\t\t\tcase http.StatusOK:\n\t\t\t\tif err := res.Body.Close(); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\tdefault:\n\t\t\t\treturn errors.Join(\n\t\t\t\t\tfmt.Errorf(\"unexpected response: %s\", res.Status),\n\t\t\t\t\tres.Body.Close(),\n\t\t\t\t)\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}\n\n\teg, ctx := errgroup.WithContext(ctx)\n\teg.SetLimit(runtime.GOMAXPROCS(0) + 1) // allow GOMAXPROCS workers and 1 reader.\n\t// Reader goroutine\n\teg.Go(func() error {\n\t\tnext, stop := iter.Pull(seq)\n\t\tdefer stop()\n\t\tdefer close(ch)\n\t\tzCt, i := 0, 0\n\t\tdefer func() {\n\t\t\tif i%10 != 0 { // Don't print the status if the last page already did.\n\t\t\t\ttotal := i * a.PageSize\n\t\t\t\tslog.Info(\"manifests checked\", \"total\", total, \"exists\", zCt, \"removeable\", total-zCt)\n\t\t\t}\n\t\t}()\n\n\t\tfor todo, ok := next(); ok; todo, ok = next() {\n\t\t\ti++\n\t\t\tzCt += (a.PageSize - len(todo))\n\t\t\tif i%10 == 0 {\n\t\t\t\ttotal := i * a.PageSize\n\t\t\t\tslog.Info(\"manifests checked\", \"total\", total, \"exists\", zCt, \"removeable\", total-zCt)\n\t\t\t}\n\t\t\tselect {\n\t\t\tcase ch <- todo:\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn context.Cause(ctx)\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t})\n\t// Worker goroutines.\n\tfor eg.TryGo(worker) {\n\t}\n\n\treturn eg.Wait()\n}\n"
  },
  {
    "path": "contrib/cmd/quaybackstop/main.go",
    "content": "//go:build go1.23\n\n// Quaybackstop is a helper command to ensure that Quay's GC decisions are\n// propagated back to Clair.\n//\n// This command can read either from [GABI] services or backing databases\n// directly. For Quay, database support is limited to PostgreSQL; Clair only\n// supports PostgreSQL.\n//\n// There's support for controlling the load on the database via the\n// \"page-count\", \"page-size\", and \"cursor-file\" flags. See the help output for\n// more information.\n//\n// [GABI]: https://github.com/app-sre/gabi\npackage main\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"flag\"\n\t\"fmt\"\n\t\"io\"\n\t\"log/slog\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"os/signal\"\n\t\"runtime/debug\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-jose/go-jose/v3\"\n\t\"github.com/go-jose/go-jose/v3/jwt\"\n\t\"github.com/jackc/pgx/v5/pgxpool\"\n\t\"github.com/quay/clair/config\"\n)\n\nfunc main() {\n\tvar code int\n\tdefer func() {\n\t\tif code != 0 {\n\t\t\tos.Exit(code)\n\t\t}\n\t}()\n\tctx := context.Background()\n\tctx, done := signal.NotifyContext(ctx, append(signals, os.Interrupt)...)\n\tdefer done()\n\tvar app App\n\topts := &slog.HandlerOptions{\n\t\tLevel: slog.LevelInfo,\n\t}\n\tflag.CommandLine.Usage = usage(flag.CommandLine)\n\tflag.BoolFunc(\"D\", \"print debugging output (-D=2 for more output)\", setLogging(opts))\n\tflag.BoolVar(&app.DryRun, \"n\", false, \"dry-run: do not issue delete requests\")\n\tflag.IntVar(&app.PageSize, \"page-size\", 100, \"pull pages of `SIZE` from the Quay database\")\n\tflag.IntVar(&app.PageCount, \"page-count\", -1, \"only process `N` pages before stopping (-1 for \\\"all\\\")\")\n\tflag.Func(\"cursor-file\", \"resume state from `FILE` and write state if \\\"page-count\\\" is set\", app.SetCursorFile)\n\tflag.Func(\"clair-gabi\", \"query Clair database via specified GABI `URL`\", app.SetClairGABI)\n\tflag.Func(\"clair-gabi-auth\", \"use provided `TOKEN` for Clair GABI queries\", app.SetClairGABIAuth)\n\tflag.Func(\"clair-config\", \"load Clair configuration from `FILE` and connect to database directly\", app.SetClairConfig)\n\tflag.Func(\"quay-gabi\", \"query Quay database via specified GABI `URL`\", app.SetQuayGABI)\n\tflag.Func(\"quay-gabi-auth\", \"use provided `TOKEN` for Quay GABI queries\", app.SetQuayGABIAuth)\n\tflag.Func(\"quay-config\", \"load Quay configuration from `FILE` and connect to database directly\", app.SetQuayConfig)\n\tflag.Func(\"indexer-addr\", \"issue deletes to indexer at `URL` (using credentials from \\\"clair-config\\\")\", app.SetIndexerAddr)\n\tflag.Parse()\n\tslog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, opts)))\n\n\tif err := Run(ctx, app); err != nil {\n\t\tslog.Error(\"exiting with error\", \"err\", err)\n\t\tcode = 1\n\t}\n}\n\n// Usage returns a function printing the customized help output for the provided\n// [flag.FlagSet].\nfunc usage(set *flag.FlagSet) func() {\n\tbuf := bufio.NewWriter(set.Output())\n\twords := []string{\n\t\t\"Usage: quaybackstop\", \"[-D]\", \"[-page-size SIZE]\", \"[-page-count N]\",\n\t\t\"[-cursor-file FILE]\", \"[-indexer-addr URL | -n]\",\n\t\t\"[-clair-gabi URL [-clair-gabi-auth TOKEN]]\", \"[-clair-config FILE]\",\n\t\t\"[-quay-gabi URL [-quay-gabi-auth TOKEN] | -quay-config FILE]\",\n\t}\n\tcols := 80\n\tif v, ok := os.LookupEnv(\"COLUMNS\"); ok && v != \"\" {\n\t\tif c, err := strconv.Atoi(v); err == nil { // backwards conditional\n\t\t\tcols = c\n\t\t}\n\t}\n\n\treturn func() {\n\t\tp, l := 0, 0\n\t\tfor _, w := range words {\n\t\t\tif p+len(w)+1 > cols {\n\t\t\t\tbuf.WriteByte('\\n')\n\t\t\t\tp = 0\n\t\t\t\tl++\n\t\t\t}\n\t\t\tif p == 0 && l != 0 {\n\t\t\t\tp, _ = buf.WriteString(strings.Repeat(\" \", len(words[0])))\n\t\t\t}\n\t\t\tif p != 0 {\n\t\t\t\tbuf.WriteByte(' ')\n\t\t\t\tp++\n\t\t\t}\n\t\t\tct, _ := buf.WriteString(w)\n\t\t\tp += ct\n\t\t}\n\t\tbuf.WriteByte('\\n')\n\t\tbuf.WriteByte('\\n')\n\t\tbuf.WriteString(\"Quaybackstop is a helper command to ensure that Quay's GC decisions are\\npropagated back to Clair.\\n\")\n\t\tbuf.WriteByte('\\n')\n\t\tbuf.WriteString(\"OPTIONS:\\n\")\n\t\tbuf.Flush()\n\t\tset.PrintDefaults()\n\t}\n}\n\n// SetLogging returns a function to be used as a [flag.BoolFunc] that sets the\n// Level of the closed-over [*slog.HandlerOptions].\nfunc setLogging(opts *slog.HandlerOptions) func(string) error {\n\treturn func(s string) error {\n\t\tif ct, err := strconv.Atoi(s); err == nil {\n\t\t\tif ct != 0 {\n\t\t\t\topts.Level = opts.Level.Level() + slog.Level(ct*int(slog.LevelDebug))\n\t\t\t} else {\n\t\t\t\topts.Level = slog.LevelInfo\n\t\t\t}\n\t\t\treturn nil\n\t\t}\n\t\tok, err := strconv.ParseBool(s)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif ok {\n\t\t\topts.Level = opts.Level.Level() + slog.LevelDebug\n\t\t} else {\n\t\t\topts.Level = slog.LevelInfo\n\t\t}\n\t\treturn nil\n\t}\n}\n\n// LevelTrace is a more granular logging level.\nconst LevelTrace = slog.LevelDebug + slog.LevelDebug\n\n// Run is the main entrypoint.\nfunc Run(ctx context.Context, app App) error {\n\tapp.Status()\n\tif err := app.OK(); err != nil {\n\t\treturn err\n\t}\n\n\tdigests, clairErr := app.AllManifests(ctx)\n\trm, quayErr := app.SelectMissing(ctx, digests)\n\treturn errors.Join(app.IssueDeletes(ctx, rm), clairErr(), quayErr(), app.Close())\n}\n\n// App is the giant bag of state for the process.\n//\n// If tinkering with this struct, prefer \"Set*\" functions along with\n// [flag.FlagSet.Func] to add new elements.\ntype App struct {\n\tclairDB func() (*pgxpool.Pool, error)\n\tquayDB  func() (*pgxpool.Pool, error)\n\n\tClairGABI     *url.URL\n\tClairGABIAuth *string\n\tClairConfig   *config.Config\n\n\tQuayGABI     *url.URL\n\tQuayGABIAuth *string\n\tQuayConfig   *quayConfig\n\n\tIndexerAddr      *url.URL\n\tjwtSigner        jose.Signer\n\tclairTokenMu     *sync.RWMutex\n\tclairToken       *string\n\tclairTokenResign time.Time\n\n\tCursorFile *string\n\tPageSize   int\n\tPageCount  int\n\n\tDryRun bool\n}\n\nfunc (a *App) SetCursorFile(s string) (err error) {\n\tslog.Debug(\"cursor file flag\", \"argument\", s)\n\tif s != \"\" {\n\t\ta.CursorFile = &s\n\t}\n\treturn nil\n}\n\n// OK reports if the App is configured sanely.\nfunc (a *App) OK() error {\n\tvar errs []error\n\tif a.ClairConfig == nil && a.ClairGABI == nil {\n\t\terrs = append(errs, errors.New(\"no Clair config provided\"))\n\t}\n\tif a.QuayConfig == nil && a.QuayGABI == nil {\n\t\terrs = append(errs, errors.New(\"no Quay config provided\"))\n\t}\n\tif a.PageCount == 0 {\n\t\terrs = append(errs, errors.New(\"asked for 0 pages\"))\n\t}\n\treturn errors.Join(errs...)\n}\n\n// Status prints the current configuration as Debug messages.\nfunc (a *App) Status() {\n\tslog.Debug(\"log level\", \"level\", slog.LevelDebug)\n\tslog.Debug(\"page size\", \"count\", a.PageSize)\n\tslog.Debug(\"page count\", \"count\", a.PageCount)\n\tslog.LogAttrs(context.Background(), slog.LevelDebug, \"cursor file\", func() (as []slog.Attr) {\n\t\tok := a.CursorFile != nil\n\t\tas = []slog.Attr{\n\t\t\tslog.Bool(\"provided\", ok),\n\t\t}\n\t\tif ok {\n\t\t\tas = append(as, slog.String(\"file\", *a.CursorFile))\n\t\t}\n\t\treturn as\n\t}()...)\n\tslog.Debug(\"Clair GABI\", \"enabled\", a.ClairGABI != nil, \"URL\", a.ClairGABI)\n\tslog.Debug(\"Clair GABI auth\", \"provided\", a.ClairGABIAuth != nil)\n\tslog.Debug(\"Clair config\", \"provided\", a.ClairConfig != nil)\n\tslog.Debug(\"Quay GABI\", \"enabled\", a.QuayGABI != nil, \"URL\", a.QuayGABI)\n\tslog.Debug(\"Quay GABI auth\", \"provided\", a.QuayGABIAuth != nil)\n\tslog.Debug(\"Quay config\", \"provided\", a.QuayConfig != nil)\n\tslog.Debug(\"indexer address\", \"URL\", a.IndexerAddr)\n\tslog.Debug(\"dry-run mode\", \"enabled\", a.DryRun)\n}\n\n// Close closes any constructed database pools.\nfunc (a *App) Close() error {\n\tvar errs []error\n\tfor _, f := range []func() (*pgxpool.Pool, error){\n\t\ta.clairDB,\n\t\ta.quayDB,\n\t} {\n\t\tif f == nil {\n\t\t\tcontinue\n\t\t}\n\t\tpool, err := f()\n\t\terrs = append(errs, err)\n\t\tif pool != nil {\n\t\t\tpool.Close()\n\t\t}\n\t}\n\treturn errors.Join(errs...)\n}\n\n// NewRequestWithContext is a wrapper around [http.NewRequestWithContext] that\n// sets defaults and authentication.\nfunc (a *App) NewRequestWithContext(ctx context.Context, method string, url *url.URL, body io.Reader) (*http.Request, error) {\n\treq, err := http.NewRequestWithContext(ctx, method, url.String(), body)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to construct request: %w\", err)\n\t}\n\treq.Header.Set(\"user-agent\", ua())\n\t// Set auth headers if needed.\n\tvar auth *string\n\tswitch h := url.Hostname(); {\n\tcase a.ClairGABI != nil && a.ClairGABI.Hostname() == h:\n\t\tauth = a.ClairGABIAuth\n\tcase a.QuayGABI != nil && a.QuayGABI.Hostname() == h:\n\t\tauth = a.QuayGABIAuth\n\tcase a.IndexerAddr != nil && a.ClairConfig != nil &&\n\t\ta.IndexerAddr.Hostname() == h:\n\t\t// This looks more complicated than it is.\n\t\tnow := time.Now()\n\n\t\ta.clairTokenMu.RLock()\n\t\tif !a.clairTokenResign.IsZero() && a.clairTokenResign.Sub(now) > jwt.DefaultLeeway {\n\t\t\tauth = a.clairToken\n\t\t}\n\t\ta.clairTokenMu.RUnlock()\n\t\tif auth != nil {\n\t\t\tbreak\n\t\t}\n\n\t\tcl := jwt.Claims{Issuer: `quay`}\n\t\tcl.IssuedAt = jwt.NewNumericDate(now)\n\t\tcl.NotBefore = jwt.NewNumericDate(now.Add(-jwt.DefaultLeeway))\n\t\ta.clairTokenResign = now.Add(15 * time.Minute)\n\t\tcl.Expiry = jwt.NewNumericDate(a.clairTokenResign)\n\t\ttok, err := jwt.Signed(a.jwtSigner).Claims(&cl).CompactSerialize()\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"jwt construction: %w\", err)\n\t\t}\n\n\t\ta.clairTokenMu.Lock()\n\t\t// Only update the stored token if we need to, but since we've taken the\n\t\t// expensive lock, make sure to at least populate the pointer.\n\t\tif a.clairTokenResign.IsZero() || a.clairTokenResign.Sub(now) < jwt.DefaultLeeway {\n\t\t\ta.clairToken = &tok\n\t\t}\n\t\tauth = a.clairToken\n\t\ta.clairTokenMu.Unlock()\n\tdefault:\n\t}\n\tif auth != nil {\n\t\treq.Header.Set(\"authorization\", \"Bearer \"+*auth)\n\t}\n\tslog.Debug(\"constructed request\", \"URL\", url, \"auth\", auth != nil)\n\treturn req, nil\n}\n\n// GABIQuery does a GABI query to the server at \"u\".\nfunc (a *App) GABIQuery(ctx context.Context, u *url.URL, buf *bytes.Buffer) (*gabiResponse, error) {\n\turl := u.JoinPath(\"query\")\n\tslog.Log(ctx, LevelTrace, \"GABI query\", \"query\", buf, \"url\", url.String())\n\n\treq, err := a.NewRequestWithContext(ctx, http.MethodPost, url, buf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to construct query: %w\", err)\n\t}\n\tresp, err := http.DefaultClient.Do(req)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable execute query: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tvar res gabiResponse\n\tswitch resp.StatusCode {\n\tcase http.StatusOK:\n\t\tif err := json.NewDecoder(resp.Body).Decode(&res); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"unable to read response: %w\", err)\n\t\t}\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unexpected response: %s\", resp.Status)\n\t}\n\tif res.Error != \"\" {\n\t\treturn nil, fmt.Errorf(\"gabi API error: %s\", res.Error)\n\t}\n\n\treturn &res, nil\n}\n\n// Query takes a full-formed SQL query (i.e. no parameter expansion is done) and\n// returns a [gabiQuery] that can be marshaled to the correct JSON.\nfunc query(str *strings.Builder) gabiQuery {\n\treturn gabiQuery{str.String()}\n}\n\n// GabiQuery marshals to a JSON request body for GABI.\ntype gabiQuery struct {\n\tQuery string `json:\"query\"`\n}\n\n// GabiResponse is the data returned from a GABI request.\ntype gabiResponse struct {\n\tError  string     `json:\"error\"`\n\tResult [][]string `json:\"result\"`\n}\n\n// Ua builds and returns a \"user-agent\" header value.\nvar ua = sync.OnceValue(func() string {\n\tua := \"quaybackstop/\"\n\tif info, ok := debug.ReadBuildInfo(); ok {\n\t\tua += info.Main.Version\n\t} else {\n\t\tua += \"???\"\n\t}\n\treturn ua\n})\n\n// FmtPostgresqlArray writes an array literal with the contents of \"strs\" to\n// \"w\".\n//\n// The input must not contain \"'\" characters.\nfunc fmtPostgresqlArray(w io.Writer, strs []string) {\n\tio.WriteString(w, `ARRAY[`)\n\tfor i, s := range strs {\n\t\tif i != 0 {\n\t\t\tw.Write([]byte(\",\"))\n\t\t}\n\t\tw.Write([]byte(\"'\"))\n\t\tio.WriteString(w, s)\n\t\tw.Write([]byte(\"'\"))\n\t}\n\tw.Write([]byte(\"]\"))\n}\n"
  },
  {
    "path": "contrib/cmd/quaybackstop/main_old.go",
    "content": "//go:build !go1.23\n\npackage main\n\nimport (\n\t\"fmt\"\n\t\"os\"\n)\n\nfunc main() {\n\tfmt.Fprintln(os.Stderr, \"This command was compiled with an old go version: 1.23 or greater is required.\")\n\tos.Exit(2)\n}\n"
  },
  {
    "path": "contrib/cmd/quaybackstop/quay.go",
    "content": "//go:build go1.23\n\npackage main\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/x509\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"iter\"\n\t\"log/slog\"\n\t\"net/url\"\n\t\"os\"\n\t\"runtime\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/jackc/pgx/v5/pgxpool\"\n\t\"golang.org/x/sync/errgroup\"\n\t\"gopkg.in/yaml.v3\"\n)\n\nfunc (a *App) SetQuayGABIAuth(s string) error {\n\tif s == \"\" {\n\t\treturn errors.New(\"bad Quay GABI auth: empty string\")\n\t}\n\ta.QuayGABIAuth = &s\n\treturn nil\n}\n\nfunc (a *App) SetQuayGABI(s string) (err error) {\n\ta.QuayGABI, err = url.Parse(s)\n\treturn err\n}\n\nfunc (a *App) SetQuayConfig(s string) error {\n\tslog.Debug(\"quay config flag\", \"argument\", s)\n\ta.QuayConfig = new(quayConfig)\n\tf, err := os.Open(s)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer f.Close()\n\tif err := yaml.NewDecoder(f).Decode(&a.QuayConfig); err != nil {\n\t\treturn err\n\t}\n\tif !strings.HasPrefix(a.QuayConfig.URI, \"postgresql://\") {\n\t\treturn fmt.Errorf(`unrecognized database URI: %q (only \"postgresql\" is supported)`, a.QuayConfig.URI)\n\t}\n\n\ta.quayDB = sync.OnceValues(func() (*pgxpool.Pool, error) {\n\t\tcfg, err := pgxpool.ParseConfig(a.QuayConfig.URI)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif p := a.QuayConfig.Args.SSL.CA; p != nil {\n\t\t\ttlsCfg := cfg.ConnConfig.TLSConfig.Clone()\n\t\t\tcertPool := tlsCfg.RootCAs\n\t\t\tif certPool == nil {\n\t\t\t\tcertPool, err = x509.SystemCertPool()\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t}\n\t\t\tpem, err := os.ReadFile(*p)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tif !certPool.AppendCertsFromPEM(pem) {\n\t\t\t\treturn nil, fmt.Errorf(\"unable to add CA from %q (is it PEM encoded CA certificate(s)?)\", *p)\n\t\t\t}\n\t\t}\n\t\tcfg.MaxConns = int32(runtime.GOMAXPROCS(0)) // This is how many goroutines we'll have checking manifest existence.\n\t\tinit, done := context.WithTimeoutCause(context.Background(), 10*time.Second,\n\t\t\terrors.New(\"too slow to do initial connection to Quay database\"))\n\t\tdefer done()\n\t\treturn pgxpool.NewWithConfig(init, cfg)\n\t})\n\n\treturn nil\n}\n\n// Just enough Quay config to be dangerous.\ntype quayConfig struct {\n\tArgs struct {\n\t\tSSL struct {\n\t\t\tCA *string `json:\"ca\" yaml:\"ca\"`\n\t\t} `json:\"ssl\" yaml:\"ssl\"`\n\t} `json:\"DB_CONNECTION_ARGS\" yaml:\"DB_CONNECTION_ARGS\"`\n\tURI string `json:\"DB_URI\" yaml:\"DB_URI\"`\n}\n\n// SelectMissing selects manifests that are absent from Quay (or filters\n// manifests that are present in Quay, if one prefers).\n//\n// The current implementation fans out to GOMAXPROCS goroutines and fans back in\n// to an iterator.\nfunc (a *App) SelectMissing(ctx context.Context, manifests iter.Seq[[]string]) (iter.Seq[[]string], func() error) {\n\t// Gone signals that the returned iterator's reader has stopped.\n\tgone := make(chan struct{})\n\t// In is pages from the \"manifests\" iterator.\n\t// Out is filtered pages for returning to the iterator reader.\n\tin, out := make(chan []string), make(chan []string)\n\n\teg, ctx := errgroup.WithContext(ctx)\n\teg.SetLimit(runtime.GOMAXPROCS(0) + 1) // allow GOMAXPROCS workers and 1 reader.\n\t// Reader goroutine\n\teg.Go(func() error {\n\t\tnext, stop := iter.Pull(manifests)\n\t\tdefer stop()\n\t\tdefer close(in)\n\t\tfor i := 1; ; i++ {\n\t\t\tms, ok := next()\n\t\t\tif !ok {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\tselect {\n\t\t\tcase in <- ms:\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn context.Cause(ctx)\n\t\t\tcase <-gone:\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t})\n\t// Inner is a function closing over the channel and returning an iterator\n\t// returning filtered manifests.\n\tvar inner func(<-chan []string) iter.Seq2[[]string, error]\n\n\tswitch {\n\t// Prefer the GABI interface.\n\tcase a.QuayGABI != nil:\n\t\tqueryFormatter := func(enc *json.Encoder, str *strings.Builder) func([]string) error {\n\t\t\treturn func(ms []string) error {\n\t\t\t\tstr.Reset()\n\t\t\t\tstr.WriteString(`SELECT * FROM unnest(`)\n\t\t\t\tfmtPostgresqlArray(str, ms)\n\t\t\t\tstr.WriteString(`) EXCEPT ALL SELECT digest FROM manifest WHERE digest = ANY(`)\n\t\t\t\tfmtPostgresqlArray(str, ms)\n\t\t\t\tstr.WriteString(`);`)\n\t\t\t\treturn enc.Encode(query(str))\n\t\t\t}\n\t\t}\n\t\tinner = func(in <-chan []string) iter.Seq2[[]string, error] {\n\t\t\tbuf := &bytes.Buffer{}\n\t\t\tenc := json.NewEncoder(buf)\n\t\t\tenc.SetEscapeHTML(false)\n\t\t\tmkQuery := queryFormatter(enc, new(strings.Builder))\n\t\t\treturn func(yield func([]string, error) bool) {\n\t\t\t\tvar ok bool\n\t\t\t\tfor {\n\t\t\t\t\tvar ms []string\n\t\t\t\t\tselect {\n\t\t\t\t\tcase ms, ok = <-in:\n\t\t\t\t\t\tif !ok {\n\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t}\n\t\t\t\t\tcase <-ctx.Done():\n\t\t\t\t\t\tyield(nil, context.Cause(ctx))\n\t\t\t\t\t\treturn\n\t\t\t\t\tcase <-gone:\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tif err := mkQuery(ms); err != nil {\n\t\t\t\t\t\tyield(nil, err)\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\n\t\t\t\t\tqRes, err := a.GABIQuery(ctx, a.QuayGABI, buf)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tyield(nil, err)\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\n\t\t\t\t\t// Skip the initial value, it's the list of columns\n\t\t\t\t\trows := qRes.Result[1:]\n\t\t\t\t\tvals := make([]string, len(rows))\n\t\t\t\t\tfor i, row := range rows {\n\t\t\t\t\t\tvals[i] = row[0]\n\t\t\t\t\t}\n\t\t\t\t\tif !yield(nil, err) {\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t// Second favorite: direct database access.\n\tcase a.quayDB != nil:\n\t\tpool, err := a.quayDB()\n\t\tif err != nil {\n\t\t\treturn nil, func() error { return err }\n\t\t}\n\t\tconst query = `SELECT * FROM unnest($1::TEXT[]) EXCEPT ALL SELECT digest FROM manifest WHERE digest = ANY($1::TEXT[]);`\n\t\tinner = func(in <-chan []string) iter.Seq2[[]string, error] {\n\t\t\treturn func(yield func([]string, error) bool) {\n\t\t\t\tvar ok bool\n\t\t\t\tfor {\n\t\t\t\t\tvar ms []string\n\t\t\t\t\tselect {\n\t\t\t\t\tcase ms, ok = <-in:\n\t\t\t\t\t\tif !ok {\n\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t}\n\t\t\t\t\tcase <-ctx.Done():\n\t\t\t\t\t\tyield(nil, context.Cause(ctx))\n\t\t\t\t\t\treturn\n\t\t\t\t\tcase <-gone:\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tvals := make([]string, a.PageCount)\n\t\t\t\t\terr := pool.AcquireFunc(ctx, func(c *pgxpool.Conn) error {\n\t\t\t\t\t\trows, err := c.Query(ctx, query, ms)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t}\n\t\t\t\t\t\tdefer rows.Close()\n\n\t\t\t\t\t\ti := 0\n\t\t\t\t\t\tfor rows.Next() {\n\t\t\t\t\t\t\tif err := rows.Scan(&vals[i]); err != nil {\n\t\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\ti++\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif err := rows.Err(); err != nil {\n\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t}\n\t\t\t\t\t\tvals = vals[:i]\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t})\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tyield(nil, err)\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tif !yield(vals, nil) {\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Worker goroutines.\n\tfor eg.TryGo(func() error {\n\t\tdefer func() {\n\t\t\tslog.Debug(\"worker done\", \"worker\", \"quay\")\n\t\t}()\n\t\tfor ms, err := range inner(in) {\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn context.Cause(ctx)\n\t\t\tcase <-gone:\n\t\t\t\tslog.Debug(\"dropped value\", \"worker\", \"quay\")\n\t\t\tcase out <- ms:\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}) {\n\t}\n\t// Wait for workers+reader to finish and close the output channel. Causes\n\t// the returned iterator to fall out of its reading loop.\n\tgo func() {\n\t\teg.Wait()\n\t\tclose(out)\n\t\tslog.Debug(\"output channel closed\", \"worker\", \"quay\")\n\t}()\n\n\treturn func(yield func([]string) bool) {\n\t\tct := 0\n\t\tdefer func() {\n\t\t\tslog.Debug(\"sequence done\", \"worker\", \"quay\", \"sent\", ct)\n\t\t}()\n\t\tdefer close(gone)\n\t\tfor ms := range out {\n\t\t\tct++\n\t\t\tif !yield(ms) {\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}, eg.Wait\n}\n"
  },
  {
    "path": "contrib/cmd/quaybackstop/sig_linux.go",
    "content": "//go:build linux\n\npackage main\n\nimport (\n\t\"os\"\n\t\"syscall\"\n)\n\nvar signals = []os.Signal{\n\tsyscall.SIGTERM, syscall.SIGINT, syscall.SIGHUP,\n}\n"
  },
  {
    "path": "contrib/cmd/quaybackstop/sig_other.go",
    "content": "//go:build !linux\n\npackage main\n\nimport \"os\"\n\nvar signals = []os.Signal{}\n"
  },
  {
    "path": "contrib/openshift/build_and_deploy.sh",
    "content": "#!/usr/bin/bash\nset -euo pipefail\n\nsplice() { local IFS=\"$1\"; shift; echo \"$*\"; }\n\nwhile getopts nxz name; do\n\tcase \"$name\" in\n\tx) set -x ;;\n\tn) dryrun=1 ;;\n\tz) declare -g login_done ;;\n\t?)\n\t\tprintf \"Usage: %s: [-nxz]\\n\" \"$0\"\n\t\texit 2\n\t\t;;\n\tesac\ndone\n\n: \"${REGISTRY:=quay.io}\"\n: \"${NAMESPACE:=app-sre}\"\n: \"${REPOSITORY:=clair}\"\n: \"${IMAGE:=$(splice / \"$REGISTRY\" \"$NAMESPACE\" \"$REPOSITORY\")}\"\n: \"${BUILDKIT_VERSION:=0.15.2}\"\n: \"${BUILDKIT_IMAGE:=$(splice / \"$REGISTRY\" \"$NAMESPACE\" \"buildkit:latest\")}\"\n: \"${CONTAINER_ENGINE:=$(command -v podman 2>/dev/null || command -v docker 2>/dev/null)}\"\n: \"${cidfile:=buildkit.cid}\"\nGIT_HASH=\"$(git rev-parse --short=7 HEAD)\"\ntags=(\"${IMAGE}:latest\" \"${IMAGE}:${GIT_HASH}\")\ntrap 'rm -rf clair-v*.{tar*,oci} ' ERR\nexport GOTOOLCHAIN=auto # have any local invocations of go use the correct version magically\n\npatch_source() {\n\tin=$1\n\twant=$(git ls-files ':**Dockerfile' | wc -l)\n\tif [[ $(tar tf \"$in\" '*/Dockerfile' | wc -l) -gt $want ]]; then\n\t\techo already patched >&2\n\t\treturn\n\tfi\n\tif [[ \"${BUILDKIT_IMAGE%%/*}\" = docker.io ]]; then\n\t\techo guessing no patching needed, based on BUILDKIT_IMAGE \"(${BUILDKIT_IMAGE})\" >&2\n\t\treturn\n\tfi\n\n\t( # Subshell to set a cleanup trap.\n\ttmp=$(mktemp -d)\n\ttrap 'rm -r \"$tmp\"' EXIT\n\tgunzip \"$in\"\n\tin=${in%.gz}\n\n\t# remove the syntax line -- should be OK as long as we're not using features\n\t# beyond the buildkit-shipped version.\n\tfor file in $(tar tf \"$in\" '*/Dockerfile'); do\n\t\ttar -xOf \"$in\" \"$file\" |\n\t\t\tsed '/# syntax/d' >\"${tmp}/Dockerfile\"\n\t\ttar -rf \"$in\" --transform \"s,.*,${file},\" \"${tmp}/Dockerfile\"\n\tdone\n\tgzip -n -q -f \"$in\"\n\t)\n}\n\nregistry_login(){\n\t[[ -v login_done ]] && return\n\tf=\"$(mktemp -d)/auth.json\"\n\texport REGISTRY_AUTH_FILE=\"$f\" DOCKER_CONFIG=\"${f%/*}\"\n\t${CONTAINER_ENGINE} login -u=\"${QUAY_USER}\" -p=\"${QUAY_TOKEN}\" \"${REGISTRY}\"\n\tdeclare -g login_done\n}\n\nif [[ -d bin ]]; then\n\tPATH=$(realpath bin):${PATH}\nfi\nif ! command -v buildctl >/dev/null 2>&1; then\n\techo Fetching buildctl:\n\tmkdir -p bin\n\tPATH=$(realpath bin):${PATH}\n\tcurl -sSfL \"https://github.com/moby/buildkit/releases/download/v${BUILDKIT_VERSION}/buildkit-v${BUILDKIT_VERSION}.linux-amd64.tar.gz\" |\n\t\ttar -xzC bin --strip-components=1 bin/buildctl\n\tcommand -V buildctl\n\tbuildctl --version\nfi\n\ncleanup() {\n\ttodo=( ${login_done:+${REGISTRY_AUTH_FILE}} )\n\tif [[ -f \"${cidfile}\" ]]; then\n\t\techo Stopping buildkitd container:\n\t\t[[ -o x ]] && ${CONTAINER_ENGINE} logs \"$(cat \"${cidfile}\")\"\n\t\t${CONTAINER_ENGINE} stop --cidfile \"${cidfile}\" || echo Failed to stop buildkit container >&2\n\t\ttodo+=( \"${cidfile}\" )\n\tfi\n\t[[ \"${#todo[@]}\" -ne 0 ]] && rm -rf \"${todo[@]}\"\n}\n\ntrap 'cleanup' EXIT\n# Unconditionally log in if we have credentials because AppSRE CI can't be\n# bothered to clear them between Jenkins jobs.\nif [[ -n \"${QUAY_USER-}\" && -n \"${QUAY_TOKEN-}\" ]]; then\n\tregistry_login\nfi\nif [[ ! -v BUILDKIT_HOST ]]; then\n\techo Starting buildkitd container:\n\t[[ -x o ]] && skopeo list-tags \"docker://${BUILDKIT_IMAGE%:*}\"\n\t${CONTAINER_ENGINE} run \\\n\t\t--cidfile \"${cidfile}\" \\\n\t\t--detach \\\n\t\t--privileged \\\n\t\t--rm \\\n\t\t\"${BUILDKIT_IMAGE}\"\n\tBUILDKIT_HOST=\"$(basename \"${CONTAINER_ENGINE}\")-container://$(cat \"$cidfile\")\"\n\texport BUILDKIT_HOST\nfi\n\necho Exporting source:\nmake dist\n\necho Applying self-inflicted wound:\nif [[ $(find . -maxdepth 1 -type f -name 'clair-v*.tar*' | wc -l) -ne 1 ]]; then\n\techo found multiple dist tarballs, exiting: >&2\n\tls clair-v*.tar* >&2\n\texit 99\nfi\npatch_source clair-v*.tar.gz\n\necho Building container:\nmake \"IMAGE_NAME=$(splice , \"${tags[@]}\")\" dist-container\n\n# Make repeated runs work more-or-less correctly.\ntouch -m -t 197001010000 clair-v*.{tar.gz,oci}\n\n[[ -n \"${dryrun-}\" ]] && exit 0\n\n: \"${QUAY_USER:?Missing QUAY_USER variable.}\"\n: \"${QUAY_TOKEN:?Missing QUAY_TOKEN variable.}\"\nregistry_login\n\nar=$(echo clair-v4.*.oci)\nfor t in \"${tags[@]}\"; do\n\techo Copy to \"${t@Q}:\"\n\tskopeo copy --all --preserve-digests \"oci-archive:${ar}:${tags[0]##*:}\" \"docker://${t}\"\ndone\n"
  },
  {
    "path": "contrib/openshift/grafana/dashboard-clair.configmap.yaml.tpl",
    "content": "# WARNING: This is generated from local-dev/grafana/data/dashboards/clair.json\n# please modify there and run make contrib/openshift/grafana/dashboards/dashboard-clair.configmap.yaml\napiVersion: v1\nkind: ConfigMap\nmetadata:\n  creationTimestamp: null\n  name: grafana-dashboard-clair\n  labels:\n    grafana_dashboard: \"true\"\n  annotations:\n    grafana-folder: /grafana-dashboard-definitions/Clair\ndata:\n  clair-dashboard.json: |-\nGRAFANA_MANIFEST\n"
  },
  {
    "path": "contrib/openshift/grafana/dashboards/dashboard-clair.configmap.yaml",
    "content": "# WARNING: This is generated from local-dev/grafana/data/dashboards/clair.json\n# please modify there and run make contrib/openshift/grafana/dashboards/dashboard-clair.configmap.yaml\napiVersion: v1\nkind: ConfigMap\nmetadata:\n  creationTimestamp: null\n  name: grafana-dashboard-clair\n  labels:\n    grafana_dashboard: \"true\"\n  annotations:\n    grafana-folder: /grafana-dashboard-definitions/Clair\ndata:\n  clair-dashboard.json: |-\n    {\n      \"annotations\": {\n        \"list\": [\n          {\n            \"builtIn\": 1,\n            \"datasource\": \"-- Grafana --\",\n            \"enable\": true,\n            \"hide\": true,\n            \"iconColor\": \"rgba(0, 211, 255, 1)\",\n            \"name\": \"Annotations & Alerts\",\n            \"target\": {\n              \"limit\": 100,\n              \"matchAny\": false,\n              \"tags\": [],\n              \"type\": \"dashboard\"\n            },\n            \"type\": \"dashboard\"\n          }\n        ]\n      },\n      \"editable\": true,\n      \"gnetId\": null,\n      \"graphTooltip\": 1,\n      \"iteration\": 1694452951165,\n      \"links\": [],\n      \"panels\": [\n        {\n          \"datasource\": \"$datasource\",\n          \"gridPos\": {\n            \"h\": 1,\n            \"w\": 24,\n            \"x\": 0,\n            \"y\": 0\n          },\n          \"id\": 24,\n          \"title\": \"Runtime metrics\",\n          \"type\": \"row\"\n        },\n        {\n          \"datasource\": \"$datasource\",\n          \"fieldConfig\": {\n            \"defaults\": {\n              \"color\": {\n                \"mode\": \"palette-classic\"\n              },\n              \"custom\": {\n                \"axisLabel\": \"\",\n                \"axisPlacement\": \"auto\",\n                \"barAlignment\": 0,\n                \"drawStyle\": \"line\",\n                \"fillOpacity\": 0,\n                \"gradientMode\": \"none\",\n                \"hideFrom\": {\n                  \"legend\": false,\n                  \"tooltip\": false,\n                  \"viz\": false\n                },\n                \"lineInterpolation\": \"linear\",\n                \"lineWidth\": 1,\n                \"pointSize\": 5,\n                \"scaleDistribution\": {\n                  \"type\": \"linear\"\n                },\n                \"showPoints\": \"auto\",\n                \"spanNulls\": false,\n                \"stacking\": {\n                  \"group\": \"A\",\n                  \"mode\": \"none\"\n                },\n                \"thresholdsStyle\": {\n                  \"mode\": \"off\"\n                }\n              },\n              \"mappings\": [],\n              \"thresholds\": {\n                \"mode\": \"absolute\",\n                \"steps\": [\n                  {\n                    \"color\": \"green\",\n                    \"value\": null\n                  },\n                  {\n                    \"color\": \"red\",\n                    \"value\": 80\n                  }\n                ]\n              }\n            },\n            \"overrides\": []\n          },\n          \"gridPos\": {\n            \"h\": 8,\n            \"w\": 12,\n            \"x\": 0,\n            \"y\": 1\n          },\n          \"id\": 26,\n          \"options\": {\n            \"legend\": {\n              \"calcs\": [],\n              \"displayMode\": \"list\",\n              \"placement\": \"bottom\"\n            },\n            \"tooltip\": {\n              \"mode\": \"single\"\n            }\n          },\n          \"targets\": [\n            {\n              \"exemplar\": true,\n              \"expr\": \"sum by (scanned_before) (rate(claircore_indexer_scanned_manifests[$rate]))\",\n              \"interval\": \"\",\n              \"legendFormat\": \"{{scanned_before}}\",\n              \"refId\": \"A\"\n            }\n          ],\n          \"title\": \"Previously scanned manifests / s\",\n          \"type\": \"timeseries\"\n        },\n        {\n          \"datasource\": \"$datasource\",\n          \"fieldConfig\": {\n            \"defaults\": {\n              \"color\": {\n                \"mode\": \"palette-classic\"\n              },\n              \"custom\": {\n                \"hideFrom\": {\n                  \"legend\": false,\n                  \"tooltip\": false,\n                  \"viz\": false\n                }\n              },\n              \"mappings\": []\n            },\n            \"overrides\": []\n          },\n          \"gridPos\": {\n            \"h\": 8,\n            \"w\": 12,\n            \"x\": 12,\n            \"y\": 1\n          },\n          \"id\": 32,\n          \"options\": {\n            \"legend\": {\n              \"displayMode\": \"list\",\n              \"placement\": \"bottom\"\n            },\n            \"pieType\": \"pie\",\n            \"reduceOptions\": {\n              \"calcs\": [\n                \"lastNotNull\"\n              ],\n              \"fields\": \"\",\n              \"values\": false\n            },\n            \"tooltip\": {\n              \"mode\": \"single\"\n            }\n          },\n          \"targets\": [\n            {\n              \"exemplar\": true,\n              \"expr\": \"sum by (scanned_before) (rate(claircore_indexer_scanned_manifests[$rate]))\",\n              \"interval\": \"\",\n              \"legendFormat\": \"{{scanned_before}}\",\n              \"refId\": \"A\"\n            }\n          ],\n          \"title\": \"Previously scanned rate\",\n          \"type\": \"piechart\"\n        },\n        {\n          \"datasource\": \"$datasource\",\n          \"fieldConfig\": {\n            \"defaults\": {\n              \"color\": {\n                \"mode\": \"palette-classic\"\n              },\n              \"custom\": {\n                \"axisLabel\": \"\",\n                \"axisPlacement\": \"auto\",\n                \"barAlignment\": 0,\n                \"drawStyle\": \"line\",\n                \"fillOpacity\": 0,\n                \"gradientMode\": \"none\",\n                \"hideFrom\": {\n                  \"legend\": false,\n                  \"tooltip\": false,\n                  \"viz\": false\n                },\n                \"lineInterpolation\": \"linear\",\n                \"lineWidth\": 1,\n                \"pointSize\": 5,\n                \"scaleDistribution\": {\n                  \"type\": \"linear\"\n                },\n                \"showPoints\": \"auto\",\n                \"spanNulls\": false,\n                \"stacking\": {\n                  \"group\": \"A\",\n                  \"mode\": \"none\"\n                },\n                \"thresholdsStyle\": {\n                  \"mode\": \"off\"\n                }\n              },\n              \"mappings\": [],\n              \"thresholds\": {\n                \"mode\": \"absolute\",\n                \"steps\": [\n                  {\n                    \"color\": \"green\",\n                    \"value\": null\n                  },\n                  {\n                    \"color\": \"red\",\n                    \"value\": 80\n                  }\n                ]\n              }\n            },\n            \"overrides\": []\n          },\n          \"gridPos\": {\n            \"h\": 8,\n            \"w\": 12,\n            \"x\": 0,\n            \"y\": 9\n          },\n          \"id\": 35,\n          \"options\": {\n            \"legend\": {\n              \"calcs\": [],\n              \"displayMode\": \"list\",\n              \"placement\": \"bottom\"\n            },\n            \"tooltip\": {\n              \"mode\": \"single\"\n            }\n          },\n          \"targets\": [\n            {\n              \"exemplar\": true,\n              \"expr\": \"sum (clair_cmd_version_info) by (job, goversion)\",\n              \"hide\": false,\n              \"interval\": \"\",\n              \"legendFormat\": \"{{job}} - {{goversion}}\",\n              \"refId\": \"B\"\n            },\n            {\n              \"exemplar\": true,\n              \"expr\": \"sum (clair_cmd_version_info) by(job, version)\",\n              \"hide\": false,\n              \"interval\": \"\",\n              \"legendFormat\": \"{{job}} clair - {{version}}\",\n              \"refId\": \"C\"\n            },\n            {\n              \"exemplar\": true,\n              \"expr\": \"sum (clair_cmd_version_info) by(job, claircore_version)\",\n              \"interval\": \"\",\n              \"legendFormat\": \"{{job}} claircore - {{claircore_version}}\",\n              \"refId\": \"A\"\n            }\n          ],\n          \"title\": \"Versions\",\n          \"type\": \"timeseries\"\n        },\n        {\n          \"datasource\": \"$datasource\",\n          \"description\": \"\",\n          \"fieldConfig\": {\n            \"defaults\": {\n              \"color\": {\n                \"mode\": \"palette-classic\"\n              },\n              \"custom\": {\n                \"axisLabel\": \"\",\n                \"axisPlacement\": \"auto\",\n                \"barAlignment\": 0,\n                \"drawStyle\": \"line\",\n                \"fillOpacity\": 0,\n                \"gradientMode\": \"none\",\n                \"hideFrom\": {\n                  \"legend\": false,\n                  \"tooltip\": false,\n                  \"viz\": false\n                },\n                \"lineInterpolation\": \"linear\",\n                \"lineWidth\": 1,\n                \"pointSize\": 5,\n                \"scaleDistribution\": {\n                  \"type\": \"linear\"\n                },\n                \"showPoints\": \"auto\",\n                \"spanNulls\": false,\n                \"stacking\": {\n                  \"group\": \"A\",\n                  \"mode\": \"none\"\n                },\n                \"thresholdsStyle\": {\n                  \"mode\": \"off\"\n                }\n              },\n              \"mappings\": [],\n              \"thresholds\": {\n                \"mode\": \"absolute\",\n                \"steps\": [\n                  {\n                    \"color\": \"green\",\n                    \"value\": null\n                  },\n                  {\n                    \"color\": \"red\",\n                    \"value\": 80\n                  }\n                ]\n              }\n            },\n            \"overrides\": []\n          },\n          \"gridPos\": {\n            \"h\": 8,\n            \"w\": 12,\n            \"x\": 12,\n            \"y\": 9\n          },\n          \"id\": 45,\n          \"options\": {\n            \"legend\": {\n              \"calcs\": [],\n              \"displayMode\": \"list\",\n              \"placement\": \"bottom\"\n            },\n            \"tooltip\": {\n              \"mode\": \"single\"\n            }\n          },\n          \"targets\": [\n            {\n              \"exemplar\": true,\n              \"expr\": \"rate(clair_http_concurrencylimited_total[$rate])\",\n              \"interval\": \"\",\n              \"legendFormat\": \"Endpoint: {{ endpoint }} Method: {{ method }}\",\n              \"refId\": \"A\"\n            }\n          ],\n          \"title\": \"Ratelimiting / s\",\n          \"type\": \"timeseries\"\n        },\n        {\n          \"collapsed\": false,\n          \"datasource\": \"$datasource\",\n          \"gridPos\": {\n            \"h\": 1,\n            \"w\": 24,\n            \"x\": 0,\n            \"y\": 17\n          },\n          \"id\": 18,\n          \"panels\": [],\n          \"title\": \"Database Indexer\",\n          \"type\": \"row\"\n        },\n        {\n          \"datasource\": \"$datasource\",\n          \"fieldConfig\": {\n            \"defaults\": {\n              \"color\": {\n                \"mode\": \"palette-classic\"\n              },\n              \"custom\": {\n                \"axisLabel\": \"\",\n                \"axisPlacement\": \"auto\",\n                \"barAlignment\": 0,\n                \"drawStyle\": \"line\",\n                \"fillOpacity\": 0,\n                \"gradientMode\": \"none\",\n                \"hideFrom\": {\n                  \"legend\": false,\n                  \"tooltip\": false,\n                  \"viz\": false\n                },\n                \"lineInterpolation\": \"linear\",\n                \"lineWidth\": 1,\n                \"pointSize\": 5,\n                \"scaleDistribution\": {\n                  \"type\": \"linear\"\n                },\n                \"showPoints\": \"auto\",\n                \"spanNulls\": false,\n                \"stacking\": {\n                  \"group\": \"A\",\n                  \"mode\": \"none\"\n                },\n                \"thresholdsStyle\": {\n                  \"mode\": \"off\"\n                }\n              },\n              \"mappings\": [],\n              \"thresholds\": {\n                \"mode\": \"absolute\",\n                \"steps\": [\n                  {\n                    \"color\": \"green\",\n                    \"value\": null\n                  },\n                  {\n                    \"color\": \"red\",\n                    \"value\": 80\n                  }\n                ]\n              }\n            },\n            \"overrides\": []\n          },\n          \"gridPos\": {\n            \"h\": 7,\n            \"w\": 24,\n            \"x\": 0,\n            \"y\": 18\n          },\n          \"id\": 20,\n          \"options\": {\n            \"legend\": {\n              \"calcs\": [],\n              \"displayMode\": \"list\",\n              \"placement\": \"bottom\"\n            },\n            \"tooltip\": {\n              \"mode\": \"single\"\n            }\n          },\n          \"targets\": [\n            {\n              \"exemplar\": true,\n              \"expr\": \"rate(claircore_indexer_setindexreport_total[$rate])\",\n              \"interval\": \"\",\n              \"legendFormat\": \"IndexReport: {{instance }}: {{ query }}\",\n              \"refId\": \"A\"\n            },\n            {\n              \"exemplar\": true,\n              \"expr\": \"rate(claircore_indexer_layerscanned_total[$rate])\",\n              \"hide\": false,\n              \"interval\": \"\",\n              \"legendFormat\": \"LayerScanned: {{instance }}: {{ query }}\",\n              \"refId\": \"B\"\n            },\n            {\n              \"exemplar\": true,\n              \"expr\": \"rate(claircore_indexer_manifestscanned_total[$rate])\",\n              \"hide\": false,\n              \"interval\": \"\",\n              \"legendFormat\": \"ManifestScanned: {{instance }}: {{ query }}\",\n              \"refId\": \"C\"\n            },\n            {\n              \"exemplar\": true,\n              \"expr\": \"rate(claircore_indexer_persistmanifest_total[$rate])\",\n              \"hide\": false,\n              \"interval\": \"\",\n              \"legendFormat\": \"PersistManifest: {{instance }}: {{ query }}\",\n              \"refId\": \"D\"\n            },\n            {\n              \"exemplar\": true,\n              \"expr\": \"rate(claircore_indexer_registerscanners_total[$rate])\",\n              \"hide\": false,\n              \"interval\": \"\",\n              \"legendFormat\": \"RegisterScanners: {{instance }}: {{ query }}\",\n              \"refId\": \"E\"\n            },\n            {\n              \"exemplar\": true,\n              \"expr\": \"rate(claircore_indexer_setindexreport_total[$rate])\",\n              \"hide\": false,\n              \"interval\": \"\",\n              \"legendFormat\": \"SetIndexReport: {{instance }}: {{ query }}\",\n              \"refId\": \"F\"\n            }\n          ],\n          \"title\": \"Database query count (indexer)\",\n          \"type\": \"timeseries\"\n        },\n        {\n          \"cards\": {\n            \"cardPadding\": null,\n            \"cardRound\": null\n          },\n          \"color\": {\n            \"cardColor\": \"#b4ff00\",\n            \"colorScale\": \"linear\",\n            \"colorScheme\": \"interpolateOranges\",\n            \"exponent\": 0.5,\n            \"mode\": \"opacity\"\n          },\n          \"dataFormat\": \"tsbuckets\",\n          \"datasource\": \"$datasource\",\n          \"description\": \"\",\n          \"gridPos\": {\n            \"h\": 8,\n            \"w\": 4,\n            \"x\": 0,\n            \"y\": 25\n          },\n          \"heatmap\": {},\n          \"hideZeroBuckets\": true,\n          \"highlightCards\": true,\n          \"id\": 22,\n          \"legend\": {\n            \"show\": true\n          },\n          \"maxDataPoints\": 50,\n          \"repeat\": null,\n          \"reverseYBuckets\": false,\n          \"targets\": [\n            {\n              \"exemplar\": true,\n              \"expr\": \"sum(rate(claircore_indexer_setindexreport_duration_seconds_bucket[5m])) by (le)\",\n              \"format\": \"heatmap\",\n              \"hide\": false,\n              \"interval\": \"\",\n              \"legendFormat\": \"{{ le }}\",\n              \"refId\": \"A\"\n            }\n          ],\n          \"title\": \"Set index_report query duration\",\n          \"tooltip\": {\n            \"show\": true,\n            \"showHistogram\": false\n          },\n          \"transformations\": [\n            {\n              \"id\": \"groupBy\",\n              \"options\": {\n                \"fields\": {\n                  \"IndexReport\": {\n                    \"aggregations\": [\n                      \"lastNotNull\"\n                    ],\n                    \"operation\": null\n                  },\n                  \"LayerScanned\": {\n                    \"aggregations\": [],\n                    \"operation\": null\n                  },\n                  \"ManifestScanned\": {\n                    \"aggregations\": [],\n                    \"operation\": null\n                  },\n                  \"PersistManifest\": {\n                    \"aggregations\": [],\n                    \"operation\": null\n                  },\n                  \"RegisterScanners\": {\n                    \"aggregations\": [],\n                    \"operation\": null\n                  },\n                  \"SetIndexReport\": {\n                    \"aggregations\": [],\n                    \"operation\": null\n                  },\n                  \"Time\": {\n                    \"aggregations\": [\n                      \"sum\"\n                    ],\n                    \"operation\": null\n                  }\n                }\n              }\n            }\n          ],\n          \"type\": \"heatmap\",\n          \"xAxis\": {\n            \"show\": true\n          },\n          \"xBucketNumber\": null,\n          \"xBucketSize\": null,\n          \"yAxis\": {\n            \"decimals\": 0,\n            \"format\": \"s\",\n            \"logBase\": 1,\n            \"max\": null,\n            \"min\": null,\n            \"show\": true,\n            \"splitFactor\": null\n          },\n          \"yBucketBound\": \"auto\",\n          \"yBucketNumber\": null,\n          \"yBucketSize\": null\n        },\n        {\n          \"cards\": {\n            \"cardPadding\": null,\n            \"cardRound\": null\n          },\n          \"color\": {\n            \"cardColor\": \"#b4ff00\",\n            \"colorScale\": \"linear\",\n            \"colorScheme\": \"interpolateOranges\",\n            \"exponent\": 0.5,\n            \"mode\": \"opacity\"\n          },\n          \"dataFormat\": \"tsbuckets\",\n          \"datasource\": \"$datasource\",\n          \"description\": \"\",\n          \"gridPos\": {\n            \"h\": 8,\n            \"w\": 4,\n            \"x\": 4,\n            \"y\": 25\n          },\n          \"heatmap\": {},\n          \"hideZeroBuckets\": true,\n          \"highlightCards\": true,\n          \"id\": 48,\n          \"legend\": {\n            \"show\": true\n          },\n          \"maxDataPoints\": 50,\n          \"reverseYBuckets\": false,\n          \"targets\": [\n            {\n              \"exemplar\": true,\n              \"expr\": \"sum(rate(claircore_indexer_layerscanned_duration_seconds_bucket[5m])) by (le)\",\n              \"format\": \"heatmap\",\n              \"hide\": false,\n              \"interval\": \"\",\n              \"legendFormat\": \"{{ le }}\",\n              \"refId\": \"B\"\n            }\n          ],\n          \"title\": \"Layer scanned query duration\",\n          \"tooltip\": {\n            \"show\": true,\n            \"showHistogram\": false\n          },\n          \"transformations\": [\n            {\n              \"id\": \"groupBy\",\n              \"options\": {\n                \"fields\": {\n                  \"IndexReport\": {\n                    \"aggregations\": [\n                      \"lastNotNull\"\n                    ],\n                    \"operation\": null\n                  },\n                  \"LayerScanned\": {\n                    \"aggregations\": [],\n                    \"operation\": null\n                  },\n                  \"ManifestScanned\": {\n                    \"aggregations\": [],\n                    \"operation\": null\n                  },\n                  \"PersistManifest\": {\n                    \"aggregations\": [],\n                    \"operation\": null\n                  },\n                  \"RegisterScanners\": {\n                    \"aggregations\": [],\n                    \"operation\": null\n                  },\n                  \"SetIndexReport\": {\n                    \"aggregations\": [],\n                    \"operation\": null\n                  },\n                  \"Time\": {\n                    \"aggregations\": [\n                      \"sum\"\n                    ],\n                    \"operation\": null\n                  }\n                }\n              }\n            }\n          ],\n          \"type\": \"heatmap\",\n          \"xAxis\": {\n            \"show\": true\n          },\n          \"xBucketNumber\": null,\n          \"xBucketSize\": null,\n          \"yAxis\": {\n            \"decimals\": 0,\n            \"format\": \"s\",\n            \"logBase\": 1,\n            \"max\": null,\n            \"min\": null,\n            \"show\": true,\n            \"splitFactor\": null\n          },\n          \"yBucketBound\": \"auto\",\n          \"yBucketNumber\": null,\n          \"yBucketSize\": null\n        },\n        {\n          \"cards\": {\n            \"cardPadding\": null,\n            \"cardRound\": null\n          },\n          \"color\": {\n            \"cardColor\": \"#b4ff00\",\n            \"colorScale\": \"linear\",\n            \"colorScheme\": \"interpolateOranges\",\n            \"exponent\": 0.5,\n            \"mode\": \"opacity\"\n          },\n          \"dataFormat\": \"tsbuckets\",\n          \"datasource\": \"$datasource\",\n          \"description\": \"\",\n          \"gridPos\": {\n            \"h\": 8,\n            \"w\": 4,\n            \"x\": 8,\n            \"y\": 25\n          },\n          \"heatmap\": {},\n          \"hideZeroBuckets\": true,\n          \"highlightCards\": true,\n          \"id\": 51,\n          \"legend\": {\n            \"show\": true\n          },\n          \"maxDataPoints\": 50,\n          \"reverseYBuckets\": false,\n          \"targets\": [\n            {\n              \"exemplar\": true,\n              \"expr\": \"sum(rate(claircore_indexer_persistmanifest_duration_seconds_bucket[$rate])) by (le)\",\n              \"format\": \"heatmap\",\n              \"hide\": false,\n              \"interval\": \"\",\n              \"legendFormat\": \"{{ le }}\",\n              \"refId\": \"D\"\n            }\n          ],\n          \"title\": \"Persist manifest query duration\",\n          \"tooltip\": {\n            \"show\": true,\n            \"showHistogram\": false\n          },\n          \"transformations\": [\n            {\n              \"id\": \"groupBy\",\n              \"options\": {\n                \"fields\": {\n                  \"IndexReport\": {\n                    \"aggregations\": [\n                      \"lastNotNull\"\n                    ],\n                    \"operation\": null\n                  },\n                  \"LayerScanned\": {\n                    \"aggregations\": [],\n                    \"operation\": null\n                  },\n                  \"ManifestScanned\": {\n                    \"aggregations\": [],\n                    \"operation\": null\n                  },\n                  \"PersistManifest\": {\n                    \"aggregations\": [],\n                    \"operation\": null\n                  },\n                  \"RegisterScanners\": {\n                    \"aggregations\": [],\n                    \"operation\": null\n                  },\n                  \"SetIndexReport\": {\n                    \"aggregations\": [],\n                    \"operation\": null\n                  },\n                  \"Time\": {\n                    \"aggregations\": [\n                      \"sum\"\n                    ],\n                    \"operation\": null\n                  }\n                }\n              }\n            }\n          ],\n          \"type\": \"heatmap\",\n          \"xAxis\": {\n            \"show\": true\n          },\n          \"xBucketNumber\": null,\n          \"xBucketSize\": null,\n          \"yAxis\": {\n            \"decimals\": 0,\n            \"format\": \"s\",\n            \"logBase\": 1,\n            \"max\": null,\n            \"min\": null,\n            \"show\": true,\n            \"splitFactor\": null\n          },\n          \"yBucketBound\": \"auto\",\n          \"yBucketNumber\": null,\n          \"yBucketSize\": null\n        },\n        {\n          \"cards\": {\n            \"cardPadding\": null,\n            \"cardRound\": null\n          },\n          \"color\": {\n            \"cardColor\": \"#b4ff00\",\n            \"colorScale\": \"linear\",\n            \"colorScheme\": \"interpolateOranges\",\n            \"exponent\": 0.5,\n            \"mode\": \"opacity\"\n          },\n          \"dataFormat\": \"tsbuckets\",\n          \"datasource\": \"$datasource\",\n          \"description\": \"\",\n          \"gridPos\": {\n            \"h\": 8,\n            \"w\": 4,\n            \"x\": 12,\n            \"y\": 25\n          },\n          \"heatmap\": {},\n          \"hideZeroBuckets\": true,\n          \"highlightCards\": true,\n          \"id\": 52,\n          \"legend\": {\n            \"show\": true\n          },\n          \"maxDataPoints\": 50,\n          \"reverseYBuckets\": false,\n          \"targets\": [\n            {\n              \"exemplar\": true,\n              \"expr\": \"sum(rate(claircore_indexer_manifestscanned_duration_seconds_bucket[5m])) by (le)\",\n              \"format\": \"heatmap\",\n              \"hide\": false,\n              \"interval\": \"\",\n              \"legendFormat\": \"{{ le }}\",\n              \"refId\": \"C\"\n            }\n          ],\n          \"title\": \"Manifest scanned query duration\",\n          \"tooltip\": {\n            \"show\": true,\n            \"showHistogram\": false\n          },\n          \"transformations\": [\n            {\n              \"id\": \"groupBy\",\n              \"options\": {\n                \"fields\": {\n                  \"IndexReport\": {\n                    \"aggregations\": [\n                      \"lastNotNull\"\n                    ],\n                    \"operation\": null\n                  },\n                  \"LayerScanned\": {\n                    \"aggregations\": [],\n                    \"operation\": null\n                  },\n                  \"ManifestScanned\": {\n                    \"aggregations\": [],\n                    \"operation\": null\n                  },\n                  \"PersistManifest\": {\n                    \"aggregations\": [],\n                    \"operation\": null\n                  },\n                  \"RegisterScanners\": {\n                    \"aggregations\": [],\n                    \"operation\": null\n                  },\n                  \"SetIndexReport\": {\n                    \"aggregations\": [],\n                    \"operation\": null\n                  },\n                  \"Time\": {\n                    \"aggregations\": [\n                      \"sum\"\n                    ],\n                    \"operation\": null\n                  }\n                }\n              }\n            }\n          ],\n          \"type\": \"heatmap\",\n          \"xAxis\": {\n            \"show\": true\n          },\n          \"xBucketNumber\": null,\n          \"xBucketSize\": null,\n          \"yAxis\": {\n            \"decimals\": 0,\n            \"format\": \"s\",\n            \"logBase\": 1,\n            \"max\": null,\n            \"min\": null,\n            \"show\": true,\n            \"splitFactor\": null\n          },\n          \"yBucketBound\": \"auto\",\n          \"yBucketNumber\": null,\n          \"yBucketSize\": null\n        },\n        {\n          \"cards\": {\n            \"cardPadding\": null,\n            \"cardRound\": null\n          },\n          \"color\": {\n            \"cardColor\": \"#b4ff00\",\n            \"colorScale\": \"linear\",\n            \"colorScheme\": \"interpolateOranges\",\n            \"exponent\": 0.5,\n            \"mode\": \"opacity\"\n          },\n          \"dataFormat\": \"tsbuckets\",\n          \"datasource\": \"$datasource\",\n          \"description\": \"\",\n          \"gridPos\": {\n            \"h\": 8,\n            \"w\": 4,\n            \"x\": 16,\n            \"y\": 25\n          },\n          \"heatmap\": {},\n          \"hideZeroBuckets\": true,\n          \"highlightCards\": true,\n          \"id\": 53,\n          \"legend\": {\n            \"show\": true\n          },\n          \"maxDataPoints\": 50,\n          \"reverseYBuckets\": false,\n          \"targets\": [\n            {\n              \"exemplar\": true,\n              \"expr\": \"sum(rate(claircore_indexer_registerscanners_duration_seconds_bucket[5m])) by (le)\",\n              \"format\": \"heatmap\",\n              \"hide\": false,\n              \"interval\": \"\",\n              \"legendFormat\": \"{{ le }}\",\n              \"refId\": \"E\"\n            }\n          ],\n          \"title\": \"Register scanners query duration\",\n          \"tooltip\": {\n            \"show\": true,\n            \"showHistogram\": false\n          },\n          \"transformations\": [\n            {\n              \"id\": \"groupBy\",\n              \"options\": {\n                \"fields\": {\n                  \"IndexReport\": {\n                    \"aggregations\": [\n                      \"lastNotNull\"\n                    ],\n                    \"operation\": null\n                  },\n                  \"LayerScanned\": {\n                    \"aggregations\": [],\n                    \"operation\": null\n                  },\n                  \"ManifestScanned\": {\n                    \"aggregations\": [],\n                    \"operation\": null\n                  },\n                  \"PersistManifest\": {\n                    \"aggregations\": [],\n                    \"operation\": null\n                  },\n                  \"RegisterScanners\": {\n                    \"aggregations\": [],\n                    \"operation\": null\n                  },\n                  \"SetIndexReport\": {\n                    \"aggregations\": [],\n                    \"operation\": null\n                  },\n                  \"Time\": {\n                    \"aggregations\": [\n                      \"sum\"\n                    ],\n                    \"operation\": null\n                  }\n                }\n              }\n            }\n          ],\n          \"type\": \"heatmap\",\n          \"xAxis\": {\n            \"show\": true\n          },\n          \"xBucketNumber\": null,\n          \"xBucketSize\": null,\n          \"yAxis\": {\n            \"decimals\": 0,\n            \"format\": \"s\",\n            \"logBase\": 1,\n            \"max\": null,\n            \"min\": null,\n            \"show\": true,\n            \"splitFactor\": null\n          },\n          \"yBucketBound\": \"auto\",\n          \"yBucketNumber\": null,\n          \"yBucketSize\": null\n        },\n        {\n          \"cards\": {\n            \"cardPadding\": null,\n            \"cardRound\": null\n          },\n          \"color\": {\n            \"cardColor\": \"#b4ff00\",\n            \"colorScale\": \"linear\",\n            \"colorScheme\": \"interpolateOranges\",\n            \"exponent\": 0.5,\n            \"mode\": \"opacity\"\n          },\n          \"dataFormat\": \"tsbuckets\",\n          \"datasource\": \"$datasource\",\n          \"description\": \"\",\n          \"gridPos\": {\n            \"h\": 8,\n            \"w\": 4,\n            \"x\": 20,\n            \"y\": 25\n          },\n          \"heatmap\": {},\n          \"hideZeroBuckets\": true,\n          \"highlightCards\": true,\n          \"id\": 54,\n          \"legend\": {\n            \"show\": true\n          },\n          \"maxDataPoints\": 50,\n          \"reverseYBuckets\": false,\n          \"targets\": [\n            {\n              \"exemplar\": true,\n              \"expr\": \"sum(rate(claircore_indexer_setindexfinished_duration_seconds_bucket[5m])) by (le)\",\n              \"format\": \"heatmap\",\n              \"hide\": false,\n              \"interval\": \"\",\n              \"legendFormat\": \"{{ le }}\",\n              \"refId\": \"F\"\n            }\n          ],\n          \"title\": \"Set index finished query duration\",\n          \"tooltip\": {\n            \"show\": true,\n            \"showHistogram\": false\n          },\n          \"transformations\": [\n            {\n              \"id\": \"groupBy\",\n              \"options\": {\n                \"fields\": {\n                  \"IndexReport\": {\n                    \"aggregations\": [\n                      \"lastNotNull\"\n                    ],\n                    \"operation\": null\n                  },\n                  \"LayerScanned\": {\n                    \"aggregations\": [],\n                    \"operation\": null\n                  },\n                  \"ManifestScanned\": {\n                    \"aggregations\": [],\n                    \"operation\": null\n                  },\n                  \"PersistManifest\": {\n                    \"aggregations\": [],\n                    \"operation\": null\n                  },\n                  \"RegisterScanners\": {\n                    \"aggregations\": [],\n                    \"operation\": null\n                  },\n                  \"SetIndexReport\": {\n                    \"aggregations\": [],\n                    \"operation\": null\n                  },\n                  \"Time\": {\n                    \"aggregations\": [\n                      \"sum\"\n                    ],\n                    \"operation\": null\n                  }\n                }\n              }\n            }\n          ],\n          \"type\": \"heatmap\",\n          \"xAxis\": {\n            \"show\": true\n          },\n          \"xBucketNumber\": null,\n          \"xBucketSize\": null,\n          \"yAxis\": {\n            \"decimals\": 0,\n            \"format\": \"s\",\n            \"logBase\": 1,\n            \"max\": null,\n            \"min\": null,\n            \"show\": true,\n            \"splitFactor\": null\n          },\n          \"yBucketBound\": \"auto\",\n          \"yBucketNumber\": null,\n          \"yBucketSize\": null\n        },\n        {\n          \"datasource\": \"$datasource\",\n          \"fieldConfig\": {\n            \"defaults\": {\n              \"color\": {\n                \"mode\": \"palette-classic\"\n              },\n              \"custom\": {\n                \"axisLabel\": \"\",\n                \"axisPlacement\": \"auto\",\n                \"barAlignment\": 0,\n                \"drawStyle\": \"line\",\n                \"fillOpacity\": 0,\n                \"gradientMode\": \"none\",\n                \"hideFrom\": {\n                  \"legend\": false,\n                  \"tooltip\": false,\n                  \"viz\": false\n                },\n                \"lineInterpolation\": \"linear\",\n                \"lineWidth\": 1,\n                \"pointSize\": 5,\n                \"scaleDistribution\": {\n                  \"type\": \"linear\"\n                },\n                \"showPoints\": \"auto\",\n                \"spanNulls\": false,\n                \"stacking\": {\n                  \"group\": \"A\",\n                  \"mode\": \"none\"\n                },\n                \"thresholdsStyle\": {\n                  \"mode\": \"off\"\n                }\n              },\n              \"mappings\": [],\n              \"thresholds\": {\n                \"mode\": \"absolute\",\n                \"steps\": [\n                  {\n                    \"color\": \"green\",\n                    \"value\": null\n                  },\n                  {\n                    \"color\": \"red\",\n                    \"value\": 80\n                  }\n                ]\n              }\n            },\n            \"overrides\": []\n          },\n          \"gridPos\": {\n            \"h\": 8,\n            \"w\": 24,\n            \"x\": 0,\n            \"y\": 33\n          },\n          \"id\": 63,\n          \"options\": {\n            \"legend\": {\n              \"calcs\": [],\n              \"displayMode\": \"list\",\n              \"placement\": \"bottom\"\n            },\n            \"tooltip\": {\n              \"mode\": \"single\"\n            }\n          },\n          \"targets\": [\n            {\n              \"exemplar\": true,\n              \"expr\": \"sum by (application_name) (pgxpool_idle_conns{application_name=\\\"libindex\\\"})\",\n              \"interval\": \"\",\n              \"legendFormat\": \"idle\",\n              \"refId\": \"A\"\n            },\n            {\n              \"exemplar\": true,\n              \"expr\": \"sum by (application_name) (pgxpool_max_conns{application_name=\\\"libindex\\\"})\",\n              \"hide\": false,\n              \"interval\": \"\",\n              \"legendFormat\": \"max\",\n              \"refId\": \"B\"\n            },\n            {\n              \"exemplar\": true,\n              \"expr\": \"sum by (application_name) (pgxpool_total_conns{application_name=\\\"libindex\\\"})\",\n              \"hide\": false,\n              \"interval\": \"\",\n              \"legendFormat\": \"total\",\n              \"refId\": \"C\"\n            },\n            {\n              \"exemplar\": true,\n              \"expr\": \"sum by (application_name) (pgxpool_acquired_conns{application_name=\\\"libindex\\\"})\",\n              \"hide\": false,\n              \"interval\": \"\",\n              \"legendFormat\": \"acquired\",\n              \"refId\": \"D\"\n            }\n          ],\n          \"title\": \"Connections (Indexer)\",\n          \"type\": \"timeseries\"\n        },\n        {\n          \"collapsed\": false,\n          \"datasource\": null,\n          \"gridPos\": {\n            \"h\": 1,\n            \"w\": 24,\n            \"x\": 0,\n            \"y\": 41\n          },\n          \"id\": 50,\n          \"panels\": [],\n          \"title\": \"Database matcher\",\n          \"type\": \"row\"\n        },\n        {\n          \"datasource\": \"$datasource\",\n          \"fieldConfig\": {\n            \"defaults\": {\n              \"color\": {\n                \"mode\": \"palette-classic\"\n              },\n              \"custom\": {\n                \"axisLabel\": \"\",\n                \"axisPlacement\": \"auto\",\n                \"barAlignment\": 0,\n                \"drawStyle\": \"line\",\n                \"fillOpacity\": 0,\n                \"gradientMode\": \"none\",\n                \"hideFrom\": {\n                  \"legend\": false,\n                  \"tooltip\": false,\n                  \"viz\": false\n                },\n                \"lineInterpolation\": \"linear\",\n                \"lineWidth\": 1,\n                \"pointSize\": 5,\n                \"scaleDistribution\": {\n                  \"type\": \"linear\"\n                },\n                \"showPoints\": \"auto\",\n                \"spanNulls\": false,\n                \"stacking\": {\n                  \"group\": \"A\",\n                  \"mode\": \"none\"\n                },\n                \"thresholdsStyle\": {\n                  \"mode\": \"off\"\n                }\n              },\n              \"mappings\": [],\n              \"thresholds\": {\n                \"mode\": \"absolute\",\n                \"steps\": [\n                  {\n                    \"color\": \"green\",\n                    \"value\": null\n                  },\n                  {\n                    \"color\": \"red\",\n                    \"value\": 80\n                  }\n                ]\n              }\n            },\n            \"overrides\": []\n          },\n          \"gridPos\": {\n            \"h\": 7,\n            \"w\": 24,\n            \"x\": 0,\n            \"y\": 42\n          },\n          \"id\": 21,\n          \"options\": {\n            \"legend\": {\n              \"calcs\": [],\n              \"displayMode\": \"list\",\n              \"placement\": \"bottom\"\n            },\n            \"tooltip\": {\n              \"mode\": \"single\"\n            }\n          },\n          \"targets\": [\n            {\n              \"exemplar\": true,\n              \"expr\": \"rate(claircore_vulnstore_getvulnerabilities_total[$rate])\",\n              \"interval\": \"\",\n              \"legendFormat\": \"VulnStoreGet: {{instance }}: {{ query }}\",\n              \"refId\": \"A\"\n            },\n            {\n              \"exemplar\": true,\n              \"expr\": \"rate(claircore_vulnstore_getlatestrefs_total[$rate])\",\n              \"hide\": false,\n              \"interval\": \"\",\n              \"legendFormat\": \"GetLatestRefs: {{instance }}: {{ query }}\",\n              \"refId\": \"B\"\n            },\n            {\n              \"exemplar\": true,\n              \"expr\": \"rate(claircore_vulnstore_getlatestupdateref_total[$rate])\",\n              \"hide\": false,\n              \"interval\": \"\",\n              \"legendFormat\": \"GetLatestUpdateRef: {{instance }}: {{ query }}\",\n              \"refId\": \"C\"\n            },\n            {\n              \"exemplar\": true,\n              \"expr\": \"rate(claircore_vulnstore_getupdateoperations_total[$rate])\",\n              \"hide\": false,\n              \"interval\": \"\",\n              \"legendFormat\": \"GetUpdateOperations: {{instance }}: {{ query }}\",\n              \"refId\": \"D\"\n            },\n            {\n              \"exemplar\": true,\n              \"expr\": \"rate(claircore_vulnstore_updatevulnerabilities_total[$rate])\",\n              \"hide\": false,\n              \"interval\": \"\",\n              \"legendFormat\": \"UpdateVulnerabilities: {{instance }}: {{ query }}\",\n              \"refId\": \"E\"\n            }\n          ],\n          \"title\": \"Database query count (matcher)\",\n          \"type\": \"timeseries\"\n        },\n        {\n          \"cards\": {\n            \"cardPadding\": null,\n            \"cardRound\": null\n          },\n          \"color\": {\n            \"cardColor\": \"#b4ff00\",\n            \"colorScale\": \"linear\",\n            \"colorScheme\": \"interpolateOranges\",\n            \"exponent\": 0.5,\n            \"mode\": \"opacity\"\n          },\n          \"dataFormat\": \"tsbuckets\",\n          \"datasource\": \"$datasource\",\n          \"gridPos\": {\n            \"h\": 8,\n            \"w\": 4,\n            \"x\": 0,\n            \"y\": 49\n          },\n          \"heatmap\": {},\n          \"hideZeroBuckets\": true,\n          \"highlightCards\": true,\n          \"id\": 33,\n          \"legend\": {\n            \"show\": false\n          },\n          \"maxDataPoints\": 50,\n          \"reverseYBuckets\": false,\n          \"targets\": [\n            {\n              \"exemplar\": true,\n              \"expr\": \"sum(rate(claircore_vulnstore_getvulnerabilities_duration_seconds_bucket[$rate])) by (le)\",\n              \"format\": \"heatmap\",\n              \"hide\": false,\n              \"interval\": \"\",\n              \"legendFormat\": \"{{ le }}\",\n              \"refId\": \"A\"\n            }\n          ],\n          \"title\": \"Get vulnerabilities latency\",\n          \"tooltip\": {\n            \"show\": true,\n            \"showHistogram\": false\n          },\n          \"type\": \"heatmap\",\n          \"xAxis\": {\n            \"show\": true\n          },\n          \"xBucketNumber\": null,\n          \"xBucketSize\": null,\n          \"yAxis\": {\n            \"decimals\": 0,\n            \"format\": \"s\",\n            \"logBase\": 1,\n            \"max\": null,\n            \"min\": null,\n            \"show\": true,\n            \"splitFactor\": null\n          },\n          \"yBucketBound\": \"auto\",\n          \"yBucketNumber\": null,\n          \"yBucketSize\": null\n        },\n        {\n          \"cards\": {\n            \"cardPadding\": null,\n            \"cardRound\": null\n          },\n          \"color\": {\n            \"cardColor\": \"#b4ff00\",\n            \"colorScale\": \"linear\",\n            \"colorScheme\": \"interpolateOranges\",\n            \"exponent\": 0.5,\n            \"mode\": \"opacity\"\n          },\n          \"dataFormat\": \"tsbuckets\",\n          \"datasource\": \"$datasource\",\n          \"gridPos\": {\n            \"h\": 8,\n            \"w\": 4,\n            \"x\": 4,\n            \"y\": 49\n          },\n          \"heatmap\": {},\n          \"hideZeroBuckets\": true,\n          \"highlightCards\": true,\n          \"id\": 55,\n          \"legend\": {\n            \"show\": false\n          },\n          \"maxDataPoints\": 50,\n          \"reverseYBuckets\": false,\n          \"targets\": [\n            {\n              \"exemplar\": true,\n              \"expr\": \"sum(rate(claircore_vulnstore_getlatestrefs_duration_seconds_bucket[$rate])) by (le)\",\n              \"format\": \"heatmap\",\n              \"hide\": false,\n              \"interval\": \"\",\n              \"legendFormat\": \"{{ le }}\",\n              \"refId\": \"B\"\n            }\n          ],\n          \"title\": \"Get latest refs latency\",\n          \"tooltip\": {\n            \"show\": true,\n            \"showHistogram\": false\n          },\n          \"type\": \"heatmap\",\n          \"xAxis\": {\n            \"show\": true\n          },\n          \"xBucketNumber\": null,\n          \"xBucketSize\": null,\n          \"yAxis\": {\n            \"decimals\": 0,\n            \"format\": \"s\",\n            \"logBase\": 1,\n            \"max\": null,\n            \"min\": null,\n            \"show\": true,\n            \"splitFactor\": null\n          },\n          \"yBucketBound\": \"auto\",\n          \"yBucketNumber\": null,\n          \"yBucketSize\": null\n        },\n        {\n          \"cards\": {\n            \"cardPadding\": null,\n            \"cardRound\": null\n          },\n          \"color\": {\n            \"cardColor\": \"#b4ff00\",\n            \"colorScale\": \"linear\",\n            \"colorScheme\": \"interpolateOranges\",\n            \"exponent\": 0.5,\n            \"mode\": \"opacity\"\n          },\n          \"dataFormat\": \"tsbuckets\",\n          \"datasource\": \"$datasource\",\n          \"gridPos\": {\n            \"h\": 8,\n            \"w\": 4,\n            \"x\": 8,\n            \"y\": 49\n          },\n          \"heatmap\": {},\n          \"hideZeroBuckets\": true,\n          \"highlightCards\": true,\n          \"id\": 56,\n          \"legend\": {\n            \"show\": false\n          },\n          \"maxDataPoints\": 50,\n          \"reverseYBuckets\": false,\n          \"targets\": [\n            {\n              \"exemplar\": true,\n              \"expr\": \"sum(rate(claircore_vulnstore_getlatestupdateref_duration_seconds_bucket[$rate])) by (le)\",\n              \"format\": \"heatmap\",\n              \"hide\": false,\n              \"interval\": \"\",\n              \"legendFormat\": \"{{ le }}\",\n              \"refId\": \"C\"\n            }\n          ],\n          \"title\": \"Get latest update refs latency\",\n          \"tooltip\": {\n            \"show\": true,\n            \"showHistogram\": false\n          },\n          \"type\": \"heatmap\",\n          \"xAxis\": {\n            \"show\": true\n          },\n          \"xBucketNumber\": null,\n          \"xBucketSize\": null,\n          \"yAxis\": {\n            \"decimals\": 0,\n            \"format\": \"s\",\n            \"logBase\": 1,\n            \"max\": null,\n            \"min\": null,\n            \"show\": true,\n            \"splitFactor\": null\n          },\n          \"yBucketBound\": \"auto\",\n          \"yBucketNumber\": null,\n          \"yBucketSize\": null\n        },\n        {\n          \"cards\": {\n            \"cardPadding\": null,\n            \"cardRound\": null\n          },\n          \"color\": {\n            \"cardColor\": \"#b4ff00\",\n            \"colorScale\": \"linear\",\n            \"colorScheme\": \"interpolateOranges\",\n            \"exponent\": 0.5,\n            \"mode\": \"opacity\"\n          },\n          \"dataFormat\": \"tsbuckets\",\n          \"datasource\": \"$datasource\",\n          \"gridPos\": {\n            \"h\": 8,\n            \"w\": 4,\n            \"x\": 12,\n            \"y\": 49\n          },\n          \"heatmap\": {},\n          \"hideZeroBuckets\": true,\n          \"highlightCards\": true,\n          \"id\": 57,\n          \"legend\": {\n            \"show\": false\n          },\n          \"maxDataPoints\": 50,\n          \"reverseYBuckets\": false,\n          \"targets\": [\n            {\n              \"exemplar\": true,\n              \"expr\": \"sum(rate(claircore_vulnstore_getupdateoperations_duration_seconds_bucket[$rate])) by (le)\",\n              \"format\": \"heatmap\",\n              \"hide\": false,\n              \"interval\": \"\",\n              \"legendFormat\": \"{{ le }}\",\n              \"refId\": \"D\"\n            }\n          ],\n          \"title\": \"Get update operations latency\",\n          \"tooltip\": {\n            \"show\": true,\n            \"showHistogram\": false\n          },\n          \"type\": \"heatmap\",\n          \"xAxis\": {\n            \"show\": true\n          },\n          \"xBucketNumber\": null,\n          \"xBucketSize\": null,\n          \"yAxis\": {\n            \"decimals\": 0,\n            \"format\": \"s\",\n            \"logBase\": 1,\n            \"max\": null,\n            \"min\": null,\n            \"show\": true,\n            \"splitFactor\": null\n          },\n          \"yBucketBound\": \"auto\",\n          \"yBucketNumber\": null,\n          \"yBucketSize\": null\n        },\n        {\n          \"cards\": {\n            \"cardPadding\": null,\n            \"cardRound\": null\n          },\n          \"color\": {\n            \"cardColor\": \"#b4ff00\",\n            \"colorScale\": \"linear\",\n            \"colorScheme\": \"interpolateOranges\",\n            \"exponent\": 0.5,\n            \"mode\": \"opacity\"\n          },\n          \"dataFormat\": \"tsbuckets\",\n          \"datasource\": \"$datasource\",\n          \"gridPos\": {\n            \"h\": 8,\n            \"w\": 4,\n            \"x\": 16,\n            \"y\": 49\n          },\n          \"heatmap\": {},\n          \"hideZeroBuckets\": true,\n          \"highlightCards\": true,\n          \"id\": 58,\n          \"legend\": {\n            \"show\": false\n          },\n          \"maxDataPoints\": 50,\n          \"reverseYBuckets\": false,\n          \"targets\": [\n            {\n              \"exemplar\": true,\n              \"expr\": \"sum(rate(claircore_vulnstore_updatevulnerabilities_duration_seconds_bucket[$rate])) by (le)\",\n              \"format\": \"heatmap\",\n              \"hide\": false,\n              \"interval\": \"\",\n              \"legendFormat\": \"{{ le }}\",\n              \"refId\": \"E\"\n            }\n          ],\n          \"title\": \"Update vulnerabilities latency\",\n          \"tooltip\": {\n            \"show\": true,\n            \"showHistogram\": false\n          },\n          \"type\": \"heatmap\",\n          \"xAxis\": {\n            \"show\": true\n          },\n          \"xBucketNumber\": null,\n          \"xBucketSize\": null,\n          \"yAxis\": {\n            \"decimals\": 0,\n            \"format\": \"s\",\n            \"logBase\": 1,\n            \"max\": null,\n            \"min\": null,\n            \"show\": true,\n            \"splitFactor\": null\n          },\n          \"yBucketBound\": \"auto\",\n          \"yBucketNumber\": null,\n          \"yBucketSize\": null\n        },\n        {\n          \"datasource\": \"$datasource\",\n          \"fieldConfig\": {\n            \"defaults\": {\n              \"color\": {\n                \"mode\": \"palette-classic\"\n              },\n              \"custom\": {\n                \"axisLabel\": \"\",\n                \"axisPlacement\": \"auto\",\n                \"barAlignment\": 0,\n                \"drawStyle\": \"line\",\n                \"fillOpacity\": 0,\n                \"gradientMode\": \"none\",\n                \"hideFrom\": {\n                  \"legend\": false,\n                  \"tooltip\": false,\n                  \"viz\": false\n                },\n                \"lineInterpolation\": \"linear\",\n                \"lineWidth\": 1,\n                \"pointSize\": 5,\n                \"scaleDistribution\": {\n                  \"type\": \"linear\"\n                },\n                \"showPoints\": \"auto\",\n                \"spanNulls\": false,\n                \"stacking\": {\n                  \"group\": \"A\",\n                  \"mode\": \"none\"\n                },\n                \"thresholdsStyle\": {\n                  \"mode\": \"off\"\n                }\n              },\n              \"mappings\": [],\n              \"thresholds\": {\n                \"mode\": \"absolute\",\n                \"steps\": [\n                  {\n                    \"color\": \"green\",\n                    \"value\": null\n                  },\n                  {\n                    \"color\": \"red\",\n                    \"value\": 80\n                  }\n                ]\n              }\n            },\n            \"overrides\": []\n          },\n          \"gridPos\": {\n            \"h\": 7,\n            \"w\": 24,\n            \"x\": 0,\n            \"y\": 57\n          },\n          \"id\": 43,\n          \"options\": {\n            \"legend\": {\n              \"calcs\": [],\n              \"displayMode\": \"list\",\n              \"placement\": \"bottom\"\n            },\n            \"tooltip\": {\n              \"mode\": \"single\"\n            }\n          },\n          \"targets\": [\n            {\n              \"exemplar\": true,\n              \"expr\": \"rate(claircore_vulnstore_gc_total[$rate])\",\n              \"hide\": false,\n              \"interval\": \"\",\n              \"legendFormat\": \"GarbageCollection: {{instance }}: {{ query }}\",\n              \"refId\": \"E\"\n            }\n          ],\n          \"title\": \"Database query count GC\",\n          \"type\": \"timeseries\"\n        },\n        {\n          \"datasource\": \"$datasource\",\n          \"fieldConfig\": {\n            \"defaults\": {\n              \"color\": {\n                \"mode\": \"palette-classic\"\n              },\n              \"custom\": {\n                \"axisLabel\": \"\",\n                \"axisPlacement\": \"auto\",\n                \"barAlignment\": 0,\n                \"drawStyle\": \"line\",\n                \"fillOpacity\": 0,\n                \"gradientMode\": \"none\",\n                \"hideFrom\": {\n                  \"legend\": false,\n                  \"tooltip\": false,\n                  \"viz\": false\n                },\n                \"lineInterpolation\": \"linear\",\n                \"lineWidth\": 1,\n                \"pointSize\": 5,\n                \"scaleDistribution\": {\n                  \"type\": \"linear\"\n                },\n                \"showPoints\": \"auto\",\n                \"spanNulls\": false,\n                \"stacking\": {\n                  \"group\": \"A\",\n                  \"mode\": \"none\"\n                },\n                \"thresholdsStyle\": {\n                  \"mode\": \"off\"\n                }\n              },\n              \"mappings\": [],\n              \"thresholds\": {\n                \"mode\": \"absolute\",\n                \"steps\": [\n                  {\n                    \"color\": \"green\",\n                    \"value\": null\n                  },\n                  {\n                    \"color\": \"red\",\n                    \"value\": 80\n                  }\n                ]\n              }\n            },\n            \"overrides\": []\n          },\n          \"gridPos\": {\n            \"h\": 8,\n            \"w\": 24,\n            \"x\": 0,\n            \"y\": 64\n          },\n          \"id\": 64,\n          \"options\": {\n            \"legend\": {\n              \"calcs\": [],\n              \"displayMode\": \"list\",\n              \"placement\": \"bottom\"\n            },\n            \"tooltip\": {\n              \"mode\": \"single\"\n            }\n          },\n          \"targets\": [\n            {\n              \"exemplar\": true,\n              \"expr\": \"sum by (application_name) (pgxpool_idle_conns{application_name=\\\"libvuln\\\"})\",\n              \"interval\": \"\",\n              \"legendFormat\": \"idle\",\n              \"refId\": \"A\"\n            },\n            {\n              \"exemplar\": true,\n              \"expr\": \"sum by (application_name) (pgxpool_max_conns{application_name=\\\"libvuln\\\"})\",\n              \"hide\": false,\n              \"interval\": \"\",\n              \"legendFormat\": \"max\",\n              \"refId\": \"B\"\n            },\n            {\n              \"exemplar\": true,\n              \"expr\": \"sum by (application_name) (pgxpool_total_conns{application_name=\\\"libvuln\\\"})\",\n              \"hide\": false,\n              \"interval\": \"\",\n              \"legendFormat\": \"total\",\n              \"refId\": \"C\"\n            },\n            {\n              \"exemplar\": true,\n              \"expr\": \"sum by (application_name) (pgxpool_acquired_conns{application_name=\\\"libvuln\\\"})\",\n              \"hide\": false,\n              \"interval\": \"\",\n              \"legendFormat\": \"acquired\",\n              \"refId\": \"D\"\n            }\n          ],\n          \"title\": \"Connections (Matcher)\",\n          \"type\": \"timeseries\"\n        },\n        {\n          \"cards\": {\n            \"cardPadding\": null,\n            \"cardRound\": null\n          },\n          \"color\": {\n            \"cardColor\": \"#b4ff00\",\n            \"colorScale\": \"linear\",\n            \"colorScheme\": \"interpolateOranges\",\n            \"exponent\": 0.5,\n            \"mode\": \"opacity\"\n          },\n          \"dataFormat\": \"tsbuckets\",\n          \"datasource\": \"$datasource\",\n          \"gridPos\": {\n            \"h\": 8,\n            \"w\": 12,\n            \"x\": 0,\n            \"y\": 72\n          },\n          \"heatmap\": {},\n          \"hideZeroBuckets\": true,\n          \"highlightCards\": true,\n          \"id\": 44,\n          \"legend\": {\n            \"show\": false\n          },\n          \"maxDataPoints\": 50,\n          \"reverseYBuckets\": false,\n          \"targets\": [\n            {\n              \"exemplar\": true,\n              \"expr\": \"sum(rate(claircore_vulnstore_gc_duration_seconds_bucket{query=\\\"updateOps\\\"}[$rate])) by (le)\",\n              \"format\": \"heatmap\",\n              \"hide\": false,\n              \"interval\": \"\",\n              \"legendFormat\": \"{{ le }}\",\n              \"refId\": \"F\"\n            }\n          ],\n          \"title\": \"GC - updateOps latency\",\n          \"tooltip\": {\n            \"show\": true,\n            \"showHistogram\": false\n          },\n          \"type\": \"heatmap\",\n          \"xAxis\": {\n            \"show\": true\n          },\n          \"xBucketNumber\": null,\n          \"xBucketSize\": null,\n          \"yAxis\": {\n            \"decimals\": 0,\n            \"format\": \"s\",\n            \"logBase\": 1,\n            \"max\": null,\n            \"min\": null,\n            \"show\": true,\n            \"splitFactor\": null\n          },\n          \"yBucketBound\": \"auto\",\n          \"yBucketNumber\": null,\n          \"yBucketSize\": null\n        },\n        {\n          \"cards\": {\n            \"cardPadding\": null,\n            \"cardRound\": null\n          },\n          \"color\": {\n            \"cardColor\": \"#b4ff00\",\n            \"colorScale\": \"linear\",\n            \"colorScheme\": \"interpolateOranges\",\n            \"exponent\": 0.5,\n            \"mode\": \"opacity\"\n          },\n          \"dataFormat\": \"tsbuckets\",\n          \"datasource\": \"$datasource\",\n          \"gridPos\": {\n            \"h\": 8,\n            \"w\": 12,\n            \"x\": 12,\n            \"y\": 72\n          },\n          \"heatmap\": {},\n          \"hideZeroBuckets\": true,\n          \"highlightCards\": true,\n          \"id\": 59,\n          \"legend\": {\n            \"show\": false\n          },\n          \"maxDataPoints\": 50,\n          \"reverseYBuckets\": false,\n          \"targets\": [\n            {\n              \"exemplar\": true,\n              \"expr\": \"sum(rate(claircore_vulnstore_gc_duration_seconds_bucket{query=\\\"deleteVulns\\\"}[$rate])) by (le)\",\n              \"format\": \"heatmap\",\n              \"hide\": false,\n              \"interval\": \"\",\n              \"legendFormat\": \"{{ le }}\",\n              \"refId\": \"F\"\n            }\n          ],\n          \"title\": \"GC - deleteVulns latency\",\n          \"tooltip\": {\n            \"show\": true,\n            \"showHistogram\": false\n          },\n          \"type\": \"heatmap\",\n          \"xAxis\": {\n            \"show\": true\n          },\n          \"xBucketNumber\": null,\n          \"xBucketSize\": null,\n          \"yAxis\": {\n            \"decimals\": 0,\n            \"format\": \"s\",\n            \"logBase\": 1,\n            \"max\": null,\n            \"min\": null,\n            \"show\": true,\n            \"splitFactor\": null\n          },\n          \"yBucketBound\": \"auto\",\n          \"yBucketNumber\": null,\n          \"yBucketSize\": null\n        },\n        {\n          \"collapsed\": true,\n          \"datasource\": null,\n          \"gridPos\": {\n            \"h\": 1,\n            \"w\": 24,\n            \"x\": 0,\n            \"y\": 80\n          },\n          \"id\": 61,\n          \"panels\": [\n            {\n              \"datasource\": \"$datasource\",\n              \"fieldConfig\": {\n                \"defaults\": {\n                  \"color\": {\n                    \"mode\": \"palette-classic\"\n                  },\n                  \"custom\": {\n                    \"axisLabel\": \"\",\n                    \"axisPlacement\": \"auto\",\n                    \"barAlignment\": 0,\n                    \"drawStyle\": \"line\",\n                    \"fillOpacity\": 0,\n                    \"gradientMode\": \"none\",\n                    \"hideFrom\": {\n                      \"legend\": false,\n                      \"tooltip\": false,\n                      \"viz\": false\n                    },\n                    \"lineInterpolation\": \"linear\",\n                    \"lineWidth\": 1,\n                    \"pointSize\": 5,\n                    \"scaleDistribution\": {\n                      \"type\": \"linear\"\n                    },\n                    \"showPoints\": \"auto\",\n                    \"spanNulls\": false,\n                    \"stacking\": {\n                      \"group\": \"A\",\n                      \"mode\": \"none\"\n                    },\n                    \"thresholdsStyle\": {\n                      \"mode\": \"off\"\n                    }\n                  },\n                  \"mappings\": [],\n                  \"thresholds\": {\n                    \"mode\": \"absolute\",\n                    \"steps\": [\n                      {\n                        \"color\": \"green\",\n                        \"value\": null\n                      },\n                      {\n                        \"color\": \"red\",\n                        \"value\": 80\n                      }\n                    ]\n                  }\n                },\n                \"overrides\": []\n              },\n              \"gridPos\": {\n                \"h\": 8,\n                \"w\": 12,\n                \"x\": 0,\n                \"y\": 20\n              },\n              \"id\": 36,\n              \"options\": {\n                \"legend\": {\n                  \"calcs\": [],\n                  \"displayMode\": \"list\",\n                  \"placement\": \"bottom\"\n                },\n                \"tooltip\": {\n                  \"mode\": \"single\"\n                }\n              },\n              \"targets\": [\n                {\n                  \"exemplar\": true,\n                  \"expr\": \"rate(clair_notifier_created_total[$rate])\",\n                  \"interval\": \"\",\n                  \"legendFormat\": \"CreatedTotal: {{instance }}: {{ query }}\",\n                  \"refId\": \"A\"\n                },\n                {\n                  \"exemplar\": true,\n                  \"expr\": \"rate(clair_notifier_failed_total[$rate])\",\n                  \"hide\": false,\n                  \"interval\": \"\",\n                  \"legendFormat\": \"NotifierFailed: {{instance }}: {{ query }}\",\n                  \"refId\": \"B\"\n                },\n                {\n                  \"exemplar\": true,\n                  \"expr\": \"rate(clair_notifier_putreceipt_total[$rate])\",\n                  \"hide\": false,\n                  \"interval\": \"\",\n                  \"legendFormat\": \"NotifierPutReceipt: {{instance }}: {{ query }}\",\n                  \"refId\": \"C\"\n                },\n                {\n                  \"exemplar\": true,\n                  \"expr\": \"rate(clair_notifier_receiptbyuoid_total[$rate])\",\n                  \"hide\": false,\n                  \"interval\": \"\",\n                  \"legendFormat\": \"NotifierReceiptByUOID: {{instance }}: {{ query }}\",\n                  \"refId\": \"D\"\n                }\n              ],\n              \"title\": \"Database query count (notifier)\",\n              \"type\": \"timeseries\"\n            },\n            {\n              \"datasource\": \"$datasource\",\n              \"fieldConfig\": {\n                \"defaults\": {\n                  \"color\": {\n                    \"mode\": \"palette-classic\"\n                  },\n                  \"custom\": {\n                    \"axisLabel\": \"\",\n                    \"axisPlacement\": \"auto\",\n                    \"barAlignment\": 0,\n                    \"drawStyle\": \"line\",\n                    \"fillOpacity\": 30,\n                    \"gradientMode\": \"none\",\n                    \"hideFrom\": {\n                      \"legend\": false,\n                      \"tooltip\": false,\n                      \"viz\": false\n                    },\n                    \"lineInterpolation\": \"linear\",\n                    \"lineWidth\": 1,\n                    \"pointSize\": 5,\n                    \"scaleDistribution\": {\n                      \"type\": \"linear\"\n                    },\n                    \"showPoints\": \"auto\",\n                    \"spanNulls\": false,\n                    \"stacking\": {\n                      \"group\": \"A\",\n                      \"mode\": \"none\"\n                    },\n                    \"thresholdsStyle\": {\n                      \"mode\": \"off\"\n                    }\n                  },\n                  \"mappings\": [],\n                  \"thresholds\": {\n                    \"mode\": \"absolute\",\n                    \"steps\": [\n                      {\n                        \"color\": \"green\",\n                        \"value\": null\n                      },\n                      {\n                        \"color\": \"red\",\n                        \"value\": 80\n                      }\n                    ]\n                  }\n                },\n                \"overrides\": []\n              },\n              \"gridPos\": {\n                \"h\": 8,\n                \"w\": 12,\n                \"x\": 12,\n                \"y\": 20\n              },\n              \"id\": 38,\n              \"options\": {\n                \"legend\": {\n                  \"calcs\": [],\n                  \"displayMode\": \"list\",\n                  \"placement\": \"bottom\"\n                },\n                \"tooltip\": {\n                  \"mode\": \"single\"\n                }\n              },\n              \"targets\": [\n                {\n                  \"exemplar\": true,\n                  \"expr\": \"histogram_quantile($dbquantile, rate(clair_notifier_created_duration_seconds_bucket[$rate]))\",\n                  \"interval\": \"\",\n                  \"legendFormat\": \"CreatedTotal: {{instance }}: {{ query }}\",\n                  \"refId\": \"A\"\n                },\n                {\n                  \"exemplar\": true,\n                  \"expr\": \"histogram_quantile($dbquantile, rate(clair_notifier_failed_duration_seconds_bucket[$rate]))\",\n                  \"hide\": false,\n                  \"interval\": \"\",\n                  \"legendFormat\": \"NotifierFailed: {{instance }}: {{ query }}\",\n                  \"refId\": \"B\"\n                },\n                {\n                  \"exemplar\": true,\n                  \"expr\": \"histogram_quantile($dbquantile, rate(clair_notifier_putreceipt_duration_seconds_bucket[$rate]))\",\n                  \"hide\": false,\n                  \"interval\": \"\",\n                  \"legendFormat\": \"NotifierPutReceipt: {{instance }}: {{ query }}\",\n                  \"refId\": \"C\"\n                },\n                {\n                  \"exemplar\": true,\n                  \"expr\": \"histogram_quantile($dbquantile, rate(clair_notifier_receiptbyuoid_duration_seconds_bucket[$rate]))\",\n                  \"hide\": false,\n                  \"interval\": \"\",\n                  \"legendFormat\": \"NotifierReceiptByUOID: {{instance }}: {{ query }}\",\n                  \"refId\": \"D\"\n                }\n              ],\n              \"title\": \"Database query duration (notifier) (p$dbquantile)\",\n              \"type\": \"timeseries\"\n            }\n          ],\n          \"title\": \"Database - Notifier\",\n          \"type\": \"row\"\n        },\n        {\n          \"collapsed\": false,\n          \"datasource\": \"$datasource\",\n          \"gridPos\": {\n            \"h\": 1,\n            \"w\": 24,\n            \"x\": 0,\n            \"y\": 81\n          },\n          \"id\": 2,\n          \"panels\": [],\n          \"title\": \"API Requests\",\n          \"type\": \"row\"\n        },\n        {\n          \"datasource\": \"$datasource\",\n          \"fieldConfig\": {\n            \"defaults\": {\n              \"color\": {\n                \"mode\": \"palette-classic\"\n              },\n              \"custom\": {\n                \"axisLabel\": \"\",\n                \"axisPlacement\": \"auto\",\n                \"barAlignment\": 0,\n                \"drawStyle\": \"line\",\n                \"fillOpacity\": 0,\n                \"gradientMode\": \"none\",\n                \"hideFrom\": {\n                  \"legend\": false,\n                  \"tooltip\": false,\n                  \"viz\": false\n                },\n                \"lineInterpolation\": \"linear\",\n                \"lineWidth\": 1,\n                \"pointSize\": 5,\n                \"scaleDistribution\": {\n                  \"type\": \"linear\"\n                },\n                \"showPoints\": \"auto\",\n                \"spanNulls\": false,\n                \"stacking\": {\n                  \"group\": \"A\",\n                  \"mode\": \"none\"\n                },\n                \"thresholdsStyle\": {\n                  \"mode\": \"off\"\n                }\n              },\n              \"mappings\": [],\n              \"thresholds\": {\n                \"mode\": \"absolute\",\n                \"steps\": [\n                  {\n                    \"color\": \"green\",\n                    \"value\": null\n                  },\n                  {\n                    \"color\": \"red\",\n                    \"value\": 80\n                  }\n                ]\n              }\n            },\n            \"overrides\": []\n          },\n          \"gridPos\": {\n            \"h\": 8,\n            \"w\": 12,\n            \"x\": 0,\n            \"y\": 82\n          },\n          \"id\": 4,\n          \"options\": {\n            \"legend\": {\n              \"calcs\": [],\n              \"displayMode\": \"list\",\n              \"placement\": \"bottom\"\n            },\n            \"tooltip\": {\n              \"mode\": \"single\"\n            }\n          },\n          \"targets\": [\n            {\n              \"exemplar\": true,\n              \"expr\": \"sum by (code) (rate(clair_http_matcherv1_request_total{handler=\\\"/matcher/api/v1/vulnerability_report/\\\"}[$rate]))\",\n              \"instant\": false,\n              \"interval\": \"\",\n              \"legendFormat\": \"Status {{ code }} \",\n              \"refId\": \"A\"\n            }\n          ],\n          \"title\": \"Vulnerability Report Requests / s\",\n          \"type\": \"timeseries\"\n        },\n        {\n          \"cards\": {\n            \"cardPadding\": null,\n            \"cardRound\": null\n          },\n          \"color\": {\n            \"cardColor\": \"#b4ff00\",\n            \"colorScale\": \"linear\",\n            \"colorScheme\": \"interpolateOranges\",\n            \"exponent\": 0.5,\n            \"mode\": \"opacity\"\n          },\n          \"dataFormat\": \"tsbuckets\",\n          \"datasource\": \"$datasource\",\n          \"gridPos\": {\n            \"h\": 8,\n            \"w\": 12,\n            \"x\": 12,\n            \"y\": 82\n          },\n          \"heatmap\": {},\n          \"hideZeroBuckets\": true,\n          \"highlightCards\": true,\n          \"id\": 15,\n          \"legend\": {\n            \"show\": true\n          },\n          \"maxDataPoints\": 50,\n          \"repeat\": null,\n          \"reverseYBuckets\": false,\n          \"targets\": [\n            {\n              \"exemplar\": true,\n              \"expr\": \"sum(rate(clair_http_matcherv1_request_duration_seconds_bucket{handler=\\\"/matcher/api/v1/vulnerability_report/\\\"}[5m])) by (le)\",\n              \"format\": \"heatmap\",\n              \"instant\": false,\n              \"interval\": \"\",\n              \"intervalFactor\": 1,\n              \"legendFormat\": \"{{ le }}\",\n              \"refId\": \"A\"\n            }\n          ],\n          \"title\": \"Vulnerability Report Request Latency\",\n          \"tooltip\": {\n            \"show\": true,\n            \"showHistogram\": false\n          },\n          \"type\": \"heatmap\",\n          \"xAxis\": {\n            \"show\": true\n          },\n          \"xBucketNumber\": null,\n          \"xBucketSize\": null,\n          \"yAxis\": {\n            \"decimals\": null,\n            \"format\": \"s\",\n            \"logBase\": 1,\n            \"max\": null,\n            \"min\": null,\n            \"show\": true,\n            \"splitFactor\": null\n          },\n          \"yBucketBound\": \"middle\",\n          \"yBucketNumber\": null,\n          \"yBucketSize\": null\n        },\n        {\n          \"datasource\": \"$datasource\",\n          \"fieldConfig\": {\n            \"defaults\": {\n              \"color\": {\n                \"mode\": \"palette-classic\"\n              },\n              \"custom\": {\n                \"axisLabel\": \"\",\n                \"axisPlacement\": \"auto\",\n                \"barAlignment\": 0,\n                \"drawStyle\": \"line\",\n                \"fillOpacity\": 0,\n                \"gradientMode\": \"none\",\n                \"hideFrom\": {\n                  \"legend\": false,\n                  \"tooltip\": false,\n                  \"viz\": false\n                },\n                \"lineInterpolation\": \"linear\",\n                \"lineWidth\": 1,\n                \"pointSize\": 5,\n                \"scaleDistribution\": {\n                  \"type\": \"linear\"\n                },\n                \"showPoints\": \"auto\",\n                \"spanNulls\": false,\n                \"stacking\": {\n                  \"group\": \"A\",\n                  \"mode\": \"none\"\n                },\n                \"thresholdsStyle\": {\n                  \"mode\": \"off\"\n                }\n              },\n              \"mappings\": [],\n              \"thresholds\": {\n                \"mode\": \"absolute\",\n                \"steps\": [\n                  {\n                    \"color\": \"green\",\n                    \"value\": null\n                  },\n                  {\n                    \"color\": \"red\",\n                    \"value\": 80\n                  }\n                ]\n              }\n            },\n            \"overrides\": []\n          },\n          \"gridPos\": {\n            \"h\": 8,\n            \"w\": 12,\n            \"x\": 0,\n            \"y\": 90\n          },\n          \"id\": 7,\n          \"options\": {\n            \"legend\": {\n              \"calcs\": [],\n              \"displayMode\": \"list\",\n              \"placement\": \"bottom\"\n            },\n            \"tooltip\": {\n              \"mode\": \"single\"\n            }\n          },\n          \"targets\": [\n            {\n              \"exemplar\": true,\n              \"expr\": \"sum by (code) (rate(clair_http_indexerv1_request_total{handler=\\\"/indexer/api/v1/index_report\\\", method=\\\"post\\\"}[$rate]))\",\n              \"instant\": false,\n              \"interval\": \"1\",\n              \"legendFormat\": \"Status {{ code }} \",\n              \"refId\": \"A\"\n            }\n          ],\n          \"title\": \"Create Index Report Requests / s\",\n          \"type\": \"timeseries\"\n        },\n        {\n          \"cards\": {\n            \"cardPadding\": null,\n            \"cardRound\": null\n          },\n          \"color\": {\n            \"cardColor\": \"#b4ff00\",\n            \"colorScale\": \"linear\",\n            \"colorScheme\": \"interpolateGreys\",\n            \"exponent\": 0.5,\n            \"mode\": \"opacity\"\n          },\n          \"dataFormat\": \"tsbuckets\",\n          \"datasource\": \"$datasource\",\n          \"description\": \"\",\n          \"gridPos\": {\n            \"h\": 8,\n            \"w\": 12,\n            \"x\": 12,\n            \"y\": 90\n          },\n          \"heatmap\": {},\n          \"hideZeroBuckets\": true,\n          \"highlightCards\": true,\n          \"id\": 14,\n          \"legend\": {\n            \"show\": true\n          },\n          \"maxDataPoints\": 50,\n          \"reverseYBuckets\": false,\n          \"targets\": [\n            {\n              \"exemplar\": true,\n              \"expr\": \"sum(increase(clair_http_indexerv1_request_duration_seconds_bucket{method=\\\"post\\\", handler=\\\"/indexer/api/v1/index_report\\\", code=\\\"201\\\"} [5m])) by (le)\",\n              \"format\": \"heatmap\",\n              \"instant\": false,\n              \"interval\": \"1\",\n              \"legendFormat\": \"{{ le }}\",\n              \"refId\": \"A\"\n            }\n          ],\n          \"title\": \"Create Index Report Request Latency\",\n          \"tooltip\": {\n            \"show\": true,\n            \"showHistogram\": false\n          },\n          \"type\": \"heatmap\",\n          \"xAxis\": {\n            \"show\": true\n          },\n          \"xBucketNumber\": null,\n          \"xBucketSize\": null,\n          \"yAxis\": {\n            \"decimals\": null,\n            \"format\": \"s\",\n            \"logBase\": 1,\n            \"max\": null,\n            \"min\": null,\n            \"show\": true,\n            \"splitFactor\": null\n          },\n          \"yBucketBound\": \"auto\",\n          \"yBucketNumber\": null,\n          \"yBucketSize\": null\n        },\n        {\n          \"datasource\": \"$datasource\",\n          \"fieldConfig\": {\n            \"defaults\": {\n              \"color\": {\n                \"mode\": \"palette-classic\"\n              },\n              \"custom\": {\n                \"axisLabel\": \"\",\n                \"axisPlacement\": \"auto\",\n                \"barAlignment\": 0,\n                \"drawStyle\": \"line\",\n                \"fillOpacity\": 0,\n                \"gradientMode\": \"none\",\n                \"hideFrom\": {\n                  \"legend\": false,\n                  \"tooltip\": false,\n                  \"viz\": false\n                },\n                \"lineInterpolation\": \"stepBefore\",\n                \"lineWidth\": 1,\n                \"pointSize\": 5,\n                \"scaleDistribution\": {\n                  \"type\": \"linear\"\n                },\n                \"showPoints\": \"auto\",\n                \"spanNulls\": false,\n                \"stacking\": {\n                  \"group\": \"A\",\n                  \"mode\": \"none\"\n                },\n                \"thresholdsStyle\": {\n                  \"mode\": \"off\"\n                }\n              },\n              \"mappings\": [],\n              \"thresholds\": {\n                \"mode\": \"absolute\",\n                \"steps\": [\n                  {\n                    \"color\": \"green\",\n                    \"value\": null\n                  },\n                  {\n                    \"color\": \"red\",\n                    \"value\": 80\n                  }\n                ]\n              }\n            },\n            \"overrides\": []\n          },\n          \"gridPos\": {\n            \"h\": 8,\n            \"w\": 12,\n            \"x\": 0,\n            \"y\": 98\n          },\n          \"id\": 6,\n          \"options\": {\n            \"legend\": {\n              \"calcs\": [],\n              \"displayMode\": \"list\",\n              \"placement\": \"bottom\"\n            },\n            \"tooltip\": {\n              \"mode\": \"single\"\n            }\n          },\n          \"targets\": [\n            {\n              \"exemplar\": true,\n              \"expr\": \"sum by (code) (rate(clair_http_indexerv1_request_total{handler=\\\"/indexer/api/v1/index_state\\\", method=\\\"get\\\"}[$rate]))\",\n              \"instant\": false,\n              \"interval\": \"\",\n              \"legendFormat\": \"Status {{code}}\",\n              \"refId\": \"A\"\n            }\n          ],\n          \"title\": \"Indexer State Requests / s\",\n          \"type\": \"timeseries\"\n        },\n        {\n          \"cards\": {\n            \"cardPadding\": null,\n            \"cardRound\": null\n          },\n          \"color\": {\n            \"cardColor\": \"#b4ff00\",\n            \"colorScale\": \"linear\",\n            \"colorScheme\": \"interpolateGreys\",\n            \"exponent\": 0.5,\n            \"mode\": \"opacity\"\n          },\n          \"dataFormat\": \"tsbuckets\",\n          \"datasource\": \"$datasource\",\n          \"description\": \"\",\n          \"gridPos\": {\n            \"h\": 8,\n            \"w\": 12,\n            \"x\": 12,\n            \"y\": 98\n          },\n          \"heatmap\": {},\n          \"hideZeroBuckets\": true,\n          \"highlightCards\": true,\n          \"id\": 47,\n          \"legend\": {\n            \"show\": true\n          },\n          \"maxDataPoints\": 50,\n          \"reverseYBuckets\": false,\n          \"targets\": [\n            {\n              \"exemplar\": true,\n              \"expr\": \"sum(increase(clair_http_indexerv1_request_duration_seconds_bucket{method=\\\"get\\\", handler=\\\"/indexer/api/v1/index_state\\\"} [5m])) by (le)\",\n              \"format\": \"heatmap\",\n              \"instant\": false,\n              \"interval\": \"1\",\n              \"legendFormat\": \"{{ le }}\",\n              \"refId\": \"A\"\n            }\n          ],\n          \"title\": \"Indexer State Request Latency\",\n          \"tooltip\": {\n            \"show\": true,\n            \"showHistogram\": false\n          },\n          \"type\": \"heatmap\",\n          \"xAxis\": {\n            \"show\": true\n          },\n          \"xBucketNumber\": null,\n          \"xBucketSize\": null,\n          \"yAxis\": {\n            \"decimals\": null,\n            \"format\": \"s\",\n            \"logBase\": 1,\n            \"max\": null,\n            \"min\": null,\n            \"show\": true,\n            \"splitFactor\": null\n          },\n          \"yBucketBound\": \"auto\",\n          \"yBucketNumber\": null,\n          \"yBucketSize\": null\n        },\n        {\n          \"datasource\": \"$datasource\",\n          \"fieldConfig\": {\n            \"defaults\": {\n              \"color\": {\n                \"mode\": \"palette-classic\"\n              },\n              \"custom\": {\n                \"axisLabel\": \"\",\n                \"axisPlacement\": \"auto\",\n                \"barAlignment\": 0,\n                \"drawStyle\": \"line\",\n                \"fillOpacity\": 0,\n                \"gradientMode\": \"none\",\n                \"hideFrom\": {\n                  \"legend\": false,\n                  \"tooltip\": false,\n                  \"viz\": false\n                },\n                \"lineInterpolation\": \"linear\",\n                \"lineWidth\": 1,\n                \"pointSize\": 5,\n                \"scaleDistribution\": {\n                  \"type\": \"linear\"\n                },\n                \"showPoints\": \"auto\",\n                \"spanNulls\": false,\n                \"stacking\": {\n                  \"group\": \"A\",\n                  \"mode\": \"none\"\n                },\n                \"thresholdsStyle\": {\n                  \"mode\": \"off\"\n                }\n              },\n              \"mappings\": [],\n              \"thresholds\": {\n                \"mode\": \"absolute\",\n                \"steps\": [\n                  {\n                    \"color\": \"green\",\n                    \"value\": null\n                  },\n                  {\n                    \"color\": \"red\",\n                    \"value\": 80\n                  }\n                ]\n              }\n            },\n            \"overrides\": []\n          },\n          \"gridPos\": {\n            \"h\": 8,\n            \"w\": 12,\n            \"x\": 0,\n            \"y\": 106\n          },\n          \"id\": 5,\n          \"options\": {\n            \"legend\": {\n              \"calcs\": [],\n              \"displayMode\": \"list\",\n              \"placement\": \"bottom\"\n            },\n            \"tooltip\": {\n              \"mode\": \"single\"\n            }\n          },\n          \"targets\": [\n            {\n              \"exemplar\": true,\n              \"expr\": \"sum by (code) (rate(clair_http_indexerv1_request_total{method=\\\"get\\\", handler=\\\"/indexer/api/v1/index_report/:digest\\\"}[$rate]))\",\n              \"instant\": false,\n              \"interval\": \"1\",\n              \"legendFormat\": \"Status {{ code }} \",\n              \"refId\": \"A\"\n            }\n          ],\n          \"title\": \"Index Report Requests / s\",\n          \"type\": \"timeseries\"\n        },\n        {\n          \"cards\": {\n            \"cardPadding\": null,\n            \"cardRound\": null\n          },\n          \"color\": {\n            \"cardColor\": \"#b4ff00\",\n            \"colorScale\": \"linear\",\n            \"colorScheme\": \"interpolateGreys\",\n            \"exponent\": 0.5,\n            \"mode\": \"opacity\"\n          },\n          \"dataFormat\": \"tsbuckets\",\n          \"datasource\": \"$datasource\",\n          \"description\": \"\",\n          \"gridPos\": {\n            \"h\": 8,\n            \"w\": 12,\n            \"x\": 12,\n            \"y\": 106\n          },\n          \"heatmap\": {},\n          \"hideZeroBuckets\": true,\n          \"highlightCards\": true,\n          \"id\": 46,\n          \"legend\": {\n            \"show\": true\n          },\n          \"maxDataPoints\": 50,\n          \"reverseYBuckets\": false,\n          \"targets\": [\n            {\n              \"exemplar\": true,\n              \"expr\": \"sum(increase(clair_http_indexerv1_request_duration_seconds_bucket{method=\\\"get\\\", handler=\\\"/indexer/api/v1/index_report/:digest\\\"} [5m])) by (le)\",\n              \"format\": \"heatmap\",\n              \"instant\": false,\n              \"interval\": \"1\",\n              \"legendFormat\": \"{{ le }}\",\n              \"refId\": \"A\"\n            }\n          ],\n          \"title\": \"Index Report Request Latency\",\n          \"tooltip\": {\n            \"show\": true,\n            \"showHistogram\": false\n          },\n          \"type\": \"heatmap\",\n          \"xAxis\": {\n            \"show\": true\n          },\n          \"xBucketNumber\": null,\n          \"xBucketSize\": null,\n          \"yAxis\": {\n            \"decimals\": null,\n            \"format\": \"s\",\n            \"logBase\": 1,\n            \"max\": null,\n            \"min\": null,\n            \"show\": true,\n            \"splitFactor\": null\n          },\n          \"yBucketBound\": \"auto\",\n          \"yBucketNumber\": null,\n          \"yBucketSize\": null\n        },\n        {\n          \"datasource\": \"$datasource\",\n          \"fieldConfig\": {\n            \"defaults\": {\n              \"color\": {\n                \"mode\": \"palette-classic\"\n              },\n              \"custom\": {\n                \"axisLabel\": \"\",\n                \"axisPlacement\": \"auto\",\n                \"barAlignment\": 0,\n                \"drawStyle\": \"line\",\n                \"fillOpacity\": 0,\n                \"gradientMode\": \"none\",\n                \"hideFrom\": {\n                  \"legend\": false,\n                  \"tooltip\": false,\n                  \"viz\": false\n                },\n                \"lineInterpolation\": \"linear\",\n                \"lineWidth\": 1,\n                \"pointSize\": 5,\n                \"scaleDistribution\": {\n                  \"type\": \"linear\"\n                },\n                \"showPoints\": \"auto\",\n                \"spanNulls\": false,\n                \"stacking\": {\n                  \"group\": \"A\",\n                  \"mode\": \"none\"\n                },\n                \"thresholdsStyle\": {\n                  \"mode\": \"off\"\n                }\n              },\n              \"mappings\": [],\n              \"thresholds\": {\n                \"mode\": \"absolute\",\n                \"steps\": [\n                  {\n                    \"color\": \"green\",\n                    \"value\": null\n                  },\n                  {\n                    \"color\": \"red\",\n                    \"value\": 80\n                  }\n                ]\n              }\n            },\n            \"overrides\": []\n          },\n          \"gridPos\": {\n            \"h\": 8,\n            \"w\": 12,\n            \"x\": 0,\n            \"y\": 114\n          },\n          \"id\": 66,\n          \"options\": {\n            \"legend\": {\n              \"calcs\": [],\n              \"displayMode\": \"list\",\n              \"placement\": \"bottom\"\n            },\n            \"tooltip\": {\n              \"mode\": \"single\"\n            }\n          },\n          \"targets\": [\n            {\n              \"exemplar\": true,\n              \"expr\": \"sum by (code) (rate(clair_http_indexerv1_request_total{method=\\\"delete\\\", handler=\\\"/indexer/api/v1/index_report\\\"}[$rate]))\",\n              \"hide\": false,\n              \"instant\": false,\n              \"interval\": \"1\",\n              \"legendFormat\": \"Status {{ code }} \",\n              \"refId\": \"B\"\n            }\n          ],\n          \"title\": \"Index Report Bulk Deletion Requests / s\",\n          \"type\": \"timeseries\"\n        },\n        {\n          \"cards\": {\n            \"cardPadding\": null,\n            \"cardRound\": null\n          },\n          \"color\": {\n            \"cardColor\": \"#b4ff00\",\n            \"colorScale\": \"linear\",\n            \"colorScheme\": \"interpolateGreys\",\n            \"exponent\": 0.5,\n            \"mode\": \"opacity\"\n          },\n          \"dataFormat\": \"tsbuckets\",\n          \"datasource\": \"$datasource\",\n          \"description\": \"\",\n          \"gridPos\": {\n            \"h\": 8,\n            \"w\": 12,\n            \"x\": 12,\n            \"y\": 114\n          },\n          \"heatmap\": {},\n          \"hideZeroBuckets\": true,\n          \"highlightCards\": true,\n          \"id\": 67,\n          \"legend\": {\n            \"show\": true\n          },\n          \"maxDataPoints\": 50,\n          \"reverseYBuckets\": false,\n          \"targets\": [\n            {\n              \"exemplar\": true,\n              \"expr\": \"sum(increase(clair_http_indexerv1_request_duration_seconds_bucket{method=\\\"delete\\\", handler=\\\"/indexer/api/v1/index_report\\\"} [5m])) by (le)\",\n              \"format\": \"heatmap\",\n              \"instant\": false,\n              \"interval\": \"1\",\n              \"legendFormat\": \"{{ le }}\",\n              \"refId\": \"A\"\n            }\n          ],\n          \"title\": \"Index Report Bulk Deletion Request Latency\",\n          \"tooltip\": {\n            \"show\": true,\n            \"showHistogram\": false\n          },\n          \"type\": \"heatmap\",\n          \"xAxis\": {\n            \"show\": true\n          },\n          \"xBucketNumber\": null,\n          \"xBucketSize\": null,\n          \"yAxis\": {\n            \"decimals\": null,\n            \"format\": \"s\",\n            \"logBase\": 1,\n            \"max\": null,\n            \"min\": null,\n            \"show\": true,\n            \"splitFactor\": null\n          },\n          \"yBucketBound\": \"auto\",\n          \"yBucketNumber\": null,\n          \"yBucketSize\": null\n        },\n        {\n          \"datasource\": \"$datasource\",\n          \"fieldConfig\": {\n            \"defaults\": {\n              \"color\": {\n                \"mode\": \"palette-classic\"\n              },\n              \"custom\": {\n                \"axisLabel\": \"\",\n                \"axisPlacement\": \"auto\",\n                \"barAlignment\": 0,\n                \"drawStyle\": \"line\",\n                \"fillOpacity\": 0,\n                \"gradientMode\": \"none\",\n                \"hideFrom\": {\n                  \"legend\": false,\n                  \"tooltip\": false,\n                  \"viz\": false\n                },\n                \"lineInterpolation\": \"linear\",\n                \"lineWidth\": 1,\n                \"pointSize\": 5,\n                \"scaleDistribution\": {\n                  \"type\": \"linear\"\n                },\n                \"showPoints\": \"auto\",\n                \"spanNulls\": false,\n                \"stacking\": {\n                  \"group\": \"A\",\n                  \"mode\": \"none\"\n                },\n                \"thresholdsStyle\": {\n                  \"mode\": \"off\"\n                }\n              },\n              \"mappings\": [],\n              \"thresholds\": {\n                \"mode\": \"absolute\",\n                \"steps\": [\n                  {\n                    \"color\": \"green\",\n                    \"value\": null\n                  },\n                  {\n                    \"color\": \"red\",\n                    \"value\": 80\n                  }\n                ]\n              }\n            },\n            \"overrides\": []\n          },\n          \"gridPos\": {\n            \"h\": 8,\n            \"w\": 12,\n            \"x\": 0,\n            \"y\": 122\n          },\n          \"id\": 68,\n          \"options\": {\n            \"legend\": {\n              \"calcs\": [],\n              \"displayMode\": \"list\",\n              \"placement\": \"bottom\"\n            },\n            \"tooltip\": {\n              \"mode\": \"single\"\n            }\n          },\n          \"targets\": [\n            {\n              \"exemplar\": true,\n              \"expr\": \"sum by (code) (rate(clair_http_indexerv1_request_total{method=\\\"delete\\\", handler=\\\"/indexer/api/v1/index_report/:digest\\\"}[$rate]))\",\n              \"instant\": false,\n              \"interval\": \"1\",\n              \"legendFormat\": \"Status {{ code }} \",\n              \"refId\": \"A\"\n            }\n          ],\n          \"title\": \"Index Report Deletion Requests / s\",\n          \"type\": \"timeseries\"\n        },\n        {\n          \"cards\": {\n            \"cardPadding\": null,\n            \"cardRound\": null\n          },\n          \"color\": {\n            \"cardColor\": \"#b4ff00\",\n            \"colorScale\": \"linear\",\n            \"colorScheme\": \"interpolateGreys\",\n            \"exponent\": 0.5,\n            \"mode\": \"opacity\"\n          },\n          \"dataFormat\": \"tsbuckets\",\n          \"datasource\": \"$datasource\",\n          \"description\": \"\",\n          \"gridPos\": {\n            \"h\": 8,\n            \"w\": 12,\n            \"x\": 12,\n            \"y\": 122\n          },\n          \"heatmap\": {},\n          \"hideZeroBuckets\": true,\n          \"highlightCards\": true,\n          \"id\": 69,\n          \"legend\": {\n            \"show\": true\n          },\n          \"maxDataPoints\": 50,\n          \"reverseYBuckets\": false,\n          \"targets\": [\n            {\n              \"exemplar\": true,\n              \"expr\": \"sum(increase(clair_http_indexerv1_request_duration_seconds_bucket{method=\\\"delete\\\", handler=\\\"/indexer/api/v1/index_report/:digest\\\"} [5m])) by (le)\",\n              \"format\": \"heatmap\",\n              \"instant\": false,\n              \"interval\": \"1\",\n              \"legendFormat\": \"{{ le }}\",\n              \"refId\": \"A\"\n            }\n          ],\n          \"title\": \"Index Report Deletion Request Latency\",\n          \"tooltip\": {\n            \"show\": true,\n            \"showHistogram\": false\n          },\n          \"type\": \"heatmap\",\n          \"xAxis\": {\n            \"show\": true\n          },\n          \"xBucketNumber\": null,\n          \"xBucketSize\": null,\n          \"yAxis\": {\n            \"decimals\": null,\n            \"format\": \"s\",\n            \"logBase\": 1,\n            \"max\": null,\n            \"min\": null,\n            \"show\": true,\n            \"splitFactor\": null\n          },\n          \"yBucketBound\": \"auto\",\n          \"yBucketNumber\": null,\n          \"yBucketSize\": null\n        },\n        {\n          \"datasource\": \"$datasource\",\n          \"fieldConfig\": {\n            \"defaults\": {\n              \"color\": {\n                \"mode\": \"palette-classic\"\n              },\n              \"custom\": {\n                \"axisLabel\": \"\",\n                \"axisPlacement\": \"auto\",\n                \"barAlignment\": 0,\n                \"drawStyle\": \"line\",\n                \"fillOpacity\": 0,\n                \"gradientMode\": \"none\",\n                \"hideFrom\": {\n                  \"legend\": false,\n                  \"tooltip\": false,\n                  \"viz\": false\n                },\n                \"lineInterpolation\": \"linear\",\n                \"lineWidth\": 1,\n                \"pointSize\": 5,\n                \"scaleDistribution\": {\n                  \"type\": \"linear\"\n                },\n                \"showPoints\": \"auto\",\n                \"spanNulls\": false,\n                \"stacking\": {\n                  \"group\": \"A\",\n                  \"mode\": \"none\"\n                },\n                \"thresholdsStyle\": {\n                  \"mode\": \"off\"\n                }\n              },\n              \"mappings\": [],\n              \"thresholds\": {\n                \"mode\": \"absolute\",\n                \"steps\": [\n                  {\n                    \"color\": \"green\",\n                    \"value\": null\n                  },\n                  {\n                    \"color\": \"red\",\n                    \"value\": 80\n                  }\n                ]\n              }\n            },\n            \"overrides\": []\n          },\n          \"gridPos\": {\n            \"h\": 8,\n            \"w\": 12,\n            \"x\": 0,\n            \"y\": 130\n          },\n          \"id\": 41,\n          \"options\": {\n            \"legend\": {\n              \"calcs\": [],\n              \"displayMode\": \"list\",\n              \"placement\": \"bottom\"\n            },\n            \"tooltip\": {\n              \"mode\": \"single\"\n            }\n          },\n          \"targets\": [\n            {\n              \"exemplar\": true,\n              \"expr\": \"sum by (code) (rate(clair_http_notifier_api_v1_notification_request_total{method=\\\"get\\\"}[$rate]))\",\n              \"instant\": false,\n              \"interval\": \"1\",\n              \"legendFormat\": \"Status {{ code }} \",\n              \"refId\": \"A\"\n            }\n          ],\n          \"title\": \"Get Notifications Requests\",\n          \"type\": \"timeseries\"\n        },\n        {\n          \"datasource\": \"$datasource\",\n          \"fieldConfig\": {\n            \"defaults\": {\n              \"color\": {\n                \"mode\": \"palette-classic\"\n              },\n              \"custom\": {\n                \"axisLabel\": \"Seconds\",\n                \"axisPlacement\": \"auto\",\n                \"barAlignment\": 0,\n                \"drawStyle\": \"line\",\n                \"fillOpacity\": 30,\n                \"gradientMode\": \"none\",\n                \"hideFrom\": {\n                  \"legend\": false,\n                  \"tooltip\": false,\n                  \"viz\": false\n                },\n                \"lineInterpolation\": \"linear\",\n                \"lineWidth\": 1,\n                \"pointSize\": 5,\n                \"scaleDistribution\": {\n                  \"type\": \"linear\"\n                },\n                \"showPoints\": \"auto\",\n                \"spanNulls\": false,\n                \"stacking\": {\n                  \"group\": \"A\",\n                  \"mode\": \"none\"\n                },\n                \"thresholdsStyle\": {\n                  \"mode\": \"off\"\n                }\n              },\n              \"mappings\": [],\n              \"thresholds\": {\n                \"mode\": \"absolute\",\n                \"steps\": [\n                  {\n                    \"color\": \"green\",\n                    \"value\": null\n                  },\n                  {\n                    \"color\": \"red\",\n                    \"value\": 80\n                  }\n                ]\n              }\n            },\n            \"overrides\": []\n          },\n          \"gridPos\": {\n            \"h\": 8,\n            \"w\": 12,\n            \"x\": 12,\n            \"y\": 130\n          },\n          \"id\": 40,\n          \"options\": {\n            \"legend\": {\n              \"calcs\": [],\n              \"displayMode\": \"list\",\n              \"placement\": \"bottom\"\n            },\n            \"tooltip\": {\n              \"mode\": \"single\"\n            }\n          },\n          \"targets\": [\n            {\n              \"exemplar\": true,\n              \"expr\": \"histogram_quantile($apiquantile, rate(clair_http_notifier_api_v1_notification_request_duration_seconds_bucket[$rate]))\",\n              \"instant\": false,\n              \"interval\": \"1\",\n              \"legendFormat\": \"\",\n              \"refId\": \"A\"\n            }\n          ],\n          \"title\": \"Get Notification Request Latency (p$apiquantile)\",\n          \"type\": \"timeseries\"\n        },\n        {\n          \"datasource\": \"$datasource\",\n          \"fieldConfig\": {\n            \"defaults\": {\n              \"color\": {\n                \"mode\": \"palette-classic\"\n              },\n              \"custom\": {\n                \"axisLabel\": \"\",\n                \"axisPlacement\": \"auto\",\n                \"barAlignment\": 0,\n                \"drawStyle\": \"line\",\n                \"fillOpacity\": 0,\n                \"gradientMode\": \"none\",\n                \"hideFrom\": {\n                  \"legend\": false,\n                  \"tooltip\": false,\n                  \"viz\": false\n                },\n                \"lineInterpolation\": \"linear\",\n                \"lineWidth\": 1,\n                \"pointSize\": 5,\n                \"scaleDistribution\": {\n                  \"type\": \"linear\"\n                },\n                \"showPoints\": \"auto\",\n                \"spanNulls\": false,\n                \"stacking\": {\n                  \"group\": \"A\",\n                  \"mode\": \"none\"\n                },\n                \"thresholdsStyle\": {\n                  \"mode\": \"off\"\n                }\n              },\n              \"mappings\": [],\n              \"thresholds\": {\n                \"mode\": \"absolute\",\n                \"steps\": [\n                  {\n                    \"color\": \"green\",\n                    \"value\": null\n                  },\n                  {\n                    \"color\": \"red\",\n                    \"value\": 80\n                  }\n                ]\n              }\n            },\n            \"overrides\": []\n          },\n          \"gridPos\": {\n            \"h\": 8,\n            \"w\": 12,\n            \"x\": 0,\n            \"y\": 138\n          },\n          \"id\": 39,\n          \"options\": {\n            \"legend\": {\n              \"calcs\": [],\n              \"displayMode\": \"list\",\n              \"placement\": \"bottom\"\n            },\n            \"tooltip\": {\n              \"mode\": \"single\"\n            }\n          },\n          \"targets\": [\n            {\n              \"exemplar\": true,\n              \"expr\": \"sum by (code) (rate(clair_http_notifier_api_v1_notification_request_total{method=\\\"delete\\\"}[$rate]))\",\n              \"instant\": false,\n              \"interval\": \"1\",\n              \"legendFormat\": \"Status {{ code }}\",\n              \"refId\": \"A\"\n            }\n          ],\n          \"title\": \"Delete Notification Requests\",\n          \"type\": \"timeseries\"\n        },\n        {\n          \"datasource\": \"$datasource\",\n          \"fieldConfig\": {\n            \"defaults\": {\n              \"color\": {\n                \"mode\": \"palette-classic\"\n              },\n              \"custom\": {\n                \"axisLabel\": \"Seconds\",\n                \"axisPlacement\": \"auto\",\n                \"barAlignment\": 0,\n                \"drawStyle\": \"line\",\n                \"fillOpacity\": 30,\n                \"gradientMode\": \"none\",\n                \"hideFrom\": {\n                  \"legend\": false,\n                  \"tooltip\": false,\n                  \"viz\": false\n                },\n                \"lineInterpolation\": \"linear\",\n                \"lineWidth\": 1,\n                \"pointSize\": 5,\n                \"scaleDistribution\": {\n                  \"type\": \"linear\"\n                },\n                \"showPoints\": \"auto\",\n                \"spanNulls\": false,\n                \"stacking\": {\n                  \"group\": \"A\",\n                  \"mode\": \"none\"\n                },\n                \"thresholdsStyle\": {\n                  \"mode\": \"off\"\n                }\n              },\n              \"mappings\": [],\n              \"thresholds\": {\n                \"mode\": \"absolute\",\n                \"steps\": [\n                  {\n                    \"color\": \"green\",\n                    \"value\": null\n                  },\n                  {\n                    \"color\": \"red\",\n                    \"value\": 80\n                  }\n                ]\n              }\n            },\n            \"overrides\": []\n          },\n          \"gridPos\": {\n            \"h\": 8,\n            \"w\": 12,\n            \"x\": 12,\n            \"y\": 138\n          },\n          \"id\": 42,\n          \"options\": {\n            \"legend\": {\n              \"calcs\": [],\n              \"displayMode\": \"list\",\n              \"placement\": \"bottom\"\n            },\n            \"tooltip\": {\n              \"mode\": \"single\"\n            }\n          },\n          \"targets\": [\n            {\n              \"exemplar\": true,\n              \"expr\": \"histogram_quantile($apiquantile, rate(clair_http_notifier_api_v1_notification_request_duration_seconds_bucket[$rate]))\",\n              \"instant\": false,\n              \"interval\": \"1\",\n              \"legendFormat\": \"\",\n              \"refId\": \"A\"\n            }\n          ],\n          \"title\": \"Delete Notification Request Latency (p$apiquantile)\",\n          \"type\": \"timeseries\"\n        },\n        {\n          \"collapsed\": false,\n          \"datasource\": \"$datasource\",\n          \"gridPos\": {\n            \"h\": 1,\n            \"w\": 24,\n            \"x\": 0,\n            \"y\": 146\n          },\n          \"id\": 9,\n          \"panels\": [],\n          \"title\": \"Memory metrics\",\n          \"type\": \"row\"\n        },\n        {\n          \"datasource\": \"$datasource\",\n          \"fieldConfig\": {\n            \"defaults\": {\n              \"color\": {\n                \"mode\": \"palette-classic\"\n              },\n              \"custom\": {\n                \"axisLabel\": \"\",\n                \"axisPlacement\": \"auto\",\n                \"barAlignment\": 0,\n                \"drawStyle\": \"line\",\n                \"fillOpacity\": 0,\n                \"gradientMode\": \"none\",\n                \"hideFrom\": {\n                  \"legend\": false,\n                  \"tooltip\": false,\n                  \"viz\": false\n                },\n                \"lineInterpolation\": \"linear\",\n                \"lineWidth\": 1,\n                \"pointSize\": 5,\n                \"scaleDistribution\": {\n                  \"type\": \"linear\"\n                },\n                \"showPoints\": \"auto\",\n                \"spanNulls\": false,\n                \"stacking\": {\n                  \"group\": \"A\",\n                  \"mode\": \"none\"\n                },\n                \"thresholdsStyle\": {\n                  \"mode\": \"off\"\n                }\n              },\n              \"mappings\": [],\n              \"thresholds\": {\n                \"mode\": \"absolute\",\n                \"steps\": [\n                  {\n                    \"color\": \"green\",\n                    \"value\": null\n                  },\n                  {\n                    \"color\": \"red\",\n                    \"value\": 80\n                  }\n                ]\n              }\n            },\n            \"overrides\": []\n          },\n          \"gridPos\": {\n            \"h\": 8,\n            \"w\": 12,\n            \"x\": 0,\n            \"y\": 147\n          },\n          \"id\": 11,\n          \"options\": {\n            \"legend\": {\n              \"calcs\": [],\n              \"displayMode\": \"list\",\n              \"placement\": \"bottom\"\n            },\n            \"tooltip\": {\n              \"mode\": \"single\"\n            }\n          },\n          \"targets\": [\n            {\n              \"exemplar\": true,\n              \"expr\": \"sum by (job)(go_goroutines)\",\n              \"interval\": \"\",\n              \"legendFormat\": \"{{ job }}\",\n              \"refId\": \"A\"\n            }\n          ],\n          \"title\": \"Goroutine count\",\n          \"type\": \"timeseries\"\n        },\n        {\n          \"datasource\": \"$datasource\",\n          \"fieldConfig\": {\n            \"defaults\": {\n              \"color\": {\n                \"mode\": \"palette-classic\"\n              },\n              \"custom\": {\n                \"axisLabel\": \"\",\n                \"axisPlacement\": \"auto\",\n                \"barAlignment\": 0,\n                \"drawStyle\": \"line\",\n                \"fillOpacity\": 10,\n                \"gradientMode\": \"none\",\n                \"hideFrom\": {\n                  \"legend\": false,\n                  \"tooltip\": false,\n                  \"viz\": false\n                },\n                \"lineInterpolation\": \"linear\",\n                \"lineWidth\": 1,\n                \"pointSize\": 5,\n                \"scaleDistribution\": {\n                  \"type\": \"linear\"\n                },\n                \"showPoints\": \"auto\",\n                \"spanNulls\": false,\n                \"stacking\": {\n                  \"group\": \"A\",\n                  \"mode\": \"none\"\n                },\n                \"thresholdsStyle\": {\n                  \"mode\": \"off\"\n                }\n              },\n              \"mappings\": [],\n              \"thresholds\": {\n                \"mode\": \"absolute\",\n                \"steps\": [\n                  {\n                    \"color\": \"green\",\n                    \"value\": null\n                  },\n                  {\n                    \"color\": \"red\",\n                    \"value\": 80\n                  }\n                ]\n              },\n              \"unit\": \"bytes\"\n            },\n            \"overrides\": []\n          },\n          \"gridPos\": {\n            \"h\": 8,\n            \"w\": 12,\n            \"x\": 12,\n            \"y\": 147\n          },\n          \"id\": 12,\n          \"options\": {\n            \"legend\": {\n              \"calcs\": [],\n              \"displayMode\": \"list\",\n              \"placement\": \"bottom\"\n            },\n            \"tooltip\": {\n              \"mode\": \"single\"\n            }\n          },\n          \"targets\": [\n            {\n              \"exemplar\": true,\n              \"expr\": \"sum by (job)(go_memstats_heap_inuse_bytes)\",\n              \"interval\": \"\",\n              \"legendFormat\": \"{{ job }}\",\n              \"refId\": \"A\"\n            }\n          ],\n          \"title\": \"Memory\",\n          \"type\": \"timeseries\"\n        }\n      ],\n      \"refresh\": false,\n      \"schemaVersion\": 30,\n      \"style\": \"dark\",\n      \"tags\": [],\n      \"templating\": {\n        \"list\": [\n          {\n            \"auto\": false,\n            \"auto_count\": 30,\n            \"auto_min\": \"10s\",\n            \"current\": {\n              \"selected\": false,\n              \"text\": \"1m\",\n              \"value\": \"1m\"\n            },\n            \"description\": null,\n            \"error\": null,\n            \"hide\": 0,\n            \"label\": null,\n            \"name\": \"rate\",\n            \"options\": [\n              {\n                \"selected\": true,\n                \"text\": \"1m\",\n                \"value\": \"1m\"\n              },\n              {\n                \"selected\": false,\n                \"text\": \"5m\",\n                \"value\": \"5m\"\n              },\n              {\n                \"selected\": false,\n                \"text\": \"10m\",\n                \"value\": \"10m\"\n              },\n              {\n                \"selected\": false,\n                \"text\": \"30m\",\n                \"value\": \"30m\"\n              },\n              {\n                \"selected\": false,\n                \"text\": \"1h\",\n                \"value\": \"1h\"\n              },\n              {\n                \"selected\": false,\n                \"text\": \"6h\",\n                \"value\": \"6h\"\n              },\n              {\n                \"selected\": false,\n                \"text\": \"12h\",\n                \"value\": \"12h\"\n              },\n              {\n                \"selected\": false,\n                \"text\": \"1d\",\n                \"value\": \"1d\"\n              },\n              {\n                \"selected\": false,\n                \"text\": \"7d\",\n                \"value\": \"7d\"\n              },\n              {\n                \"selected\": false,\n                \"text\": \"14d\",\n                \"value\": \"14d\"\n              },\n              {\n                \"selected\": false,\n                \"text\": \"30d\",\n                \"value\": \"30d\"\n              }\n            ],\n            \"query\": \"1m,5m,10m,30m,1h,6h,12h,1d,7d,14d,30d\",\n            \"refresh\": 2,\n            \"skipUrlSync\": false,\n            \"type\": \"interval\"\n          },\n          {\n            \"allValue\": null,\n            \"current\": {\n              \"selected\": false,\n              \"text\": \"0.95\",\n              \"value\": \"0.95\"\n            },\n            \"description\": null,\n            \"error\": null,\n            \"hide\": 0,\n            \"includeAll\": false,\n            \"label\": \"Database Latency Quantile\",\n            \"multi\": false,\n            \"name\": \"dbquantile\",\n            \"options\": [\n              {\n                \"selected\": true,\n                \"text\": \"0.95\",\n                \"value\": \"0.95\"\n              },\n              {\n                \"selected\": false,\n                \"text\": \"0.90\",\n                \"value\": \"0.90\"\n              },\n              {\n                \"selected\": false,\n                \"text\": \"0.5\",\n                \"value\": \"0.5\"\n              },\n              {\n                \"selected\": false,\n                \"text\": \"0.20\",\n                \"value\": \"0.20\"\n              },\n              {\n                \"selected\": false,\n                \"text\": \"0\",\n                \"value\": \"0\"\n              }\n            ],\n            \"query\": \"0.95,0.90,0.5,0.20,0\",\n            \"queryValue\": \"\",\n            \"skipUrlSync\": false,\n            \"type\": \"custom\"\n          },\n          {\n            \"allValue\": null,\n            \"current\": {\n              \"selected\": true,\n              \"text\": \"0.95\",\n              \"value\": \"0.95\"\n            },\n            \"description\": null,\n            \"error\": null,\n            \"hide\": 0,\n            \"includeAll\": false,\n            \"label\": \"API Latency Quantile\",\n            \"multi\": false,\n            \"name\": \"apiquantile\",\n            \"options\": [\n              {\n                \"selected\": true,\n                \"text\": \"0.95\",\n                \"value\": \"0.95\"\n              },\n              {\n                \"selected\": false,\n                \"text\": \"0.90\",\n                \"value\": \"0.90\"\n              },\n              {\n                \"selected\": false,\n                \"text\": \"0.5\",\n                \"value\": \"0.5\"\n              },\n              {\n                \"selected\": false,\n                \"text\": \"0.20\",\n                \"value\": \"0.20\"\n              },\n              {\n                \"selected\": false,\n                \"text\": \"0\",\n                \"value\": \"0\"\n              }\n            ],\n            \"query\": \"0.95,0.90,0.5,0.20,0\",\n            \"queryValue\": \"\",\n            \"skipUrlSync\": false,\n            \"type\": \"custom\"\n          },\n          {\n            \"current\": {\n              \"selected\": true,\n              \"text\": \"Prometheus\",\n              \"value\": \"Prometheus\"\n            },\n            \"description\": null,\n            \"error\": null,\n            \"hide\": 0,\n            \"includeAll\": false,\n            \"label\": null,\n            \"multi\": false,\n            \"name\": \"datasource\",\n            \"options\": [],\n            \"query\": \"prometheus\",\n            \"queryValue\": \"\",\n            \"refresh\": 1,\n            \"regex\": \"/(^clair|^app-sre-stage-01|^Prometheus$|^appsre)(?!.*cluster)/\",\n            \"skipUrlSync\": false,\n            \"type\": \"datasource\"\n          }\n        ]\n      },\n      \"time\": {\n        \"from\": \"now-1h\",\n        \"to\": \"now\"\n      },\n      \"timepicker\": {},\n      \"timezone\": \"\",\n      \"title\": \"Clair V4\",\n      \"uid\": \"I1JBFlRnz\",\n      \"version\": 4\n    }\n\n"
  },
  {
    "path": "contrib/openshift/manifests/backstop.yaml",
    "content": "---\napiVersion: template.openshift.io/v1\nkind: Template\nmetadata:\n  name: clair-gc-quaybackstop\n  labels:\n    app: clair\nlabels:\n  app: clair\nobjects:\n- apiVersion: batch/v1\n  kind: CronJob\n  metadata:\n    name: quaybackstop\n  spec:\n    schedule: '23 1 * * 1'\n    timeZone: Etc/UTC\n    concurrencyPolicy: Forbid\n    successfulJobsHistoryLimit: 1\n    jobTemplate:\n      spec:\n        template:\n          spec:\n            serviceAccountName: ${{SERVICE_ACCOUNT}}\n            volumes:\n              - name: clair-config\n                secret:\n                  secretName: ${{CLAIR_CONFIG_SECRET}}\n              - name: quay-config\n                secret:\n                  secretName: ${{QUAY_CONFIG_SECRET}}\n              - name: database-cert\n                emptyDir:\n                  sizeLimit: 50Mi\n            restartPolicy: OnFailure\n            initContainers:\n            - name: fetch-certs\n              image: registry.access.redhat.com/ubi9/ubi:latest\n              command:\n              - /usr/bin/sh\n              args:\n              - -c\n              - |\n                set -ex\n                cd /run/certs\n                curl -sSfLO https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem\n              volumeMounts:\n              - name: database-cert\n                mountPath: /run/certs\n            containers:\n            - name: quaybackstop\n              resources:\n                limits:\n                  cpu: 4\n                requests:\n                  cpu: 2\n              image: ${IMAGE_NAME}:${IMAGE_TAG}\n              imagePullPolicy: Always\n              env:\n              - name: SSL_CERT_DIR\n                value: /run/certs\n              args:\n              - -D=${DEBUG}\n              - -clairconfig=/run/clair/config.yaml\n              - -quayconfig=/run/quay/config.yaml\n              - -indexer-addr=${INDEXER_ADDRESS}\n              - -page-size=${PAGE_SIZE}\n              - -page-count=${PAGE_COUNT}\n              - -cursor=${CURSOR_FILE}\n              volumeMounts:\n              - name: clair-config\n                mountPath: /run/clair\n              - name: quay-config\n                mountPath: /run/quay\n              - name: database-cert\n                mountPath: /run/certs\nparameters:\n- name: IMAGE_NAME\n  value: quay.io/app-sre/clair\n  displayName: Image\n  description: Image name for the quaybackstop job.\n- name: IMAGE_TAG\n  value: quaybackstop-latest\n  displayName: Image Tag\n  description: Tag name for the quaybackstop job.\n- name: CLAIR_CONFIG_SECRET\n  value: config\n  displayName: Clair Config\n  description: Clair config secret name.\n- name: QUAY_CONFIG_SECRET\n  value: quay-config\n  displayName: Quay Config\n  description: Quay config secret name.\n- name: SERVICE_ACCOUNT\n  value: clair\n  displayName: Service Account\n  description: Service account name to use for API interaction.\n- name: INDEXER_ADDRESS\n  value: http://clair-indexer/\n  displayName: Indexer Address\n  description: Indexer service to make API requests to.\n- name: PAGE_SIZE\n  value: 100\n  displayName: Page Size\n  description: Pull pages of this size from the Quay database.\n- name: PAGE_COUNT\n  value: -1\n  displayName: Page Count\n  description: Only process this number of pages before stopping (-1 for \"all\").\n- name: CURSOR_FILE\n  value: ''\n  displayName: Cursor\n  description: File to store the cursor to. Allows for incremental work.\n- name: DEBUG\n  value: 0\n  displayName: Debug Output\n  description: Enable debugging output. (Must be numeric or one of t, f, T, F, true, false, TRUE, FALSE, True, False)\nmessage: |\n  Made cronjob \"quaybackstop\", it might have worked.\n"
  },
  {
    "path": "contrib/openshift/manifests/db-job.yaml",
    "content": "---\napiVersion: v1\nkind: Template\nmetadata:\n  name: clair-db-jobs\nparameters:\n- name: SUBCOMMAND\n  value: \"pre\"\n  required: true\n- name: VERSION\n  value: \"\"\n  required: true\n- name: IMAGE\n  value: \"quay.io/app-sre/clair\"\n  required: true\n- name: IMAGE_TAG\n  value: \"\"\n  required: true\n- name: JOB_NAME\n  value: \"\"\n  required: true\n- name: SERVICE_ACCOUNT\n  value: \"clair\"\n  displayName: clair service account\n  required: true\n- name: SECRET_NAME\n  value: \"config\"\n  displayName: Name of the config secret\n  required: true\n\nobjects:\n- apiVersion: batch/v1\n  kind: Job\n  metadata:\n    name: clair-db-jobs-${JOB_NAME}\n  spec:\n    template:\n      metadata:\n        labels:\n          app: clair-db-jobs-${JOB_NAME}\n      spec:\n        serviceAccountName: ${{SERVICE_ACCOUNT}}\n        volumes:\n          - name: clair-config\n            secret:\n              secretName: ${{SECRET_NAME}}\n        backoffLimit: 1\n        completions: 1\n        parallelism: 1\n        restartPolicy: Never\n        containers:\n        - name: clair-db-jobs-${JOB_NAME}\n          image: ${IMAGE}:${IMAGE_TAG}\n          resources:\n            limits:\n              cpu: 100m\n              memory: 128Mi\n            requests:\n              cpu: 100m\n              memory: 128Mi\n          command: [clairctl]\n          args:\n          - \"-D\"\n          - \"--config\"\n          - \"/etc/clair/config.yaml\"\n          - \"admin\"\n          - ${SUBCOMMAND}\n          - ${VERSION}\n          volumeMounts:\n          - name: clair-config\n            mountPath: /etc/clair\n\n"
  },
  {
    "path": "contrib/openshift/manifests/manifests.yaml",
    "content": "---\napiVersion: v1\nkind: Template\nmetadata:\n  name: clair\n  labels:\n    app: clair\nobjects:\n  #\n  # indexer deployment\n  #\n  - apiVersion: apps/v1\n    kind: StatefulSet\n    metadata:\n      annotations:\n        ${{CLAIR_DISABLE_MIN_REPLICAS_CHECK}}: ${{CLAIR_DISABLE_MIN_REPLICAS_REASON}}\n        ${{CLAIR_DISABLE_ANTI_AFFINITY_CHECK}}: ${{CLAIR_DISABLE_ANTI_AFFINITY_REASON}}\n      name: clair-indexer\n      labels:\n        service: indexer\n        app: clair\n    spec:\n      podManagementPolicy: Parallel\n      replicas: ${{INDEXER_DEPLOYMENT_REPLICAS}}\n      selector:\n        matchLabels:\n          service: indexer\n          app: clair\n      volumeClaimTemplates:\n        - metadata:\n            name: indexer-layer-storage\n          spec:\n            accessModes: [ \"ReadWriteOnce\" ]\n            storageClassName: ${{STORAGE_CLASS_NAME}}\n            resources:\n              requests:\n                storage: ${{INDEXER_STORAGE_REQS}}\n      template:\n        metadata:\n          name: clair-indexer\n          labels:\n            service: indexer\n            app: clair\n            ${{INDEXER_COMPONENT_LABEL_KEY}}: ${{INDEXER_COMPONENT_LABEL_VALUE}}\n        spec:\n          serviceAccountName: ${SERVICE_ACCOUNT}\n          affinity:\n            podAntiAffinity:\n              preferredDuringSchedulingIgnoredDuringExecution:\n                - weight: 1\n                  podAffinityTerm:\n                    labelSelector:\n                      matchExpressions:\n                        - key: ${{INDEXER_COMPONENT_LABEL_KEY}}\n                          operator: In\n                          values:\n                            - ${{INDEXER_COMPONENT_LABEL_VALUE}}\n                    topologyKey: kubernetes.io/hostname\n          volumes:\n            - name: clair-config\n              secret:\n                secretName: config\n          containers:\n            - name: clair-indexer\n              resources:\n                limits:\n                  cpu: ${{INDEXER_CPU_LIMITS}}\n                  memory: ${{INDEXER_MEM_LIMITS}}\n                requests:\n                  cpu: ${{INDEXER_CPU_REQS}}\n                  memory: ${{INDEXER_MEM_REQS}}\n              command: [clair]\n              env:\n                - name: CLAIR_CONF\n                  value: '/etc/clair/config.yaml'\n                - name: CLAIR_MODE\n                  value: indexer\n              image: ${CLAIR_IMAGE}:${IMAGE_TAG}\n              ports:\n                - containerPort: ${{HTTP_TRANSPORT_PORT}}\n                  name: http-transport\n                - containerPort: ${{INTROSPECTION_PORT}}\n                  name: introspection\n              livenessProbe:\n                httpGet:\n                  path: ${{HEALTH_PATH}}\n                  port: ${{HEALTH_PORT}}\n              readinessProbe:\n                httpGet:\n                  path: ${{READY_PATH}}\n                  port: ${{HEALTH_PORT}}\n              startupProbe:\n                httpGet:\n                  path: ${{READY_PATH}}\n                  port: ${{HEALTH_PORT}}\n                failureThreshold: 400\n                periodSeconds: 10\n              volumeMounts:\n                - name: clair-config\n                  mountPath: /etc/clair\n                - name: indexer-layer-storage\n                  mountPath: /var/tmp\n\n  #\n  # matcher deployment\n  #\n  - apiVersion: apps/v1\n    kind: Deployment\n    metadata:\n      name: clair-matcher\n      labels:\n        service: matcher\n        app: clair\n      annotations:\n        ${{CLAIR_DISABLE_MIN_REPLICAS_CHECK}}: ${{CLAIR_DISABLE_MIN_REPLICAS_REASON}}\n        ${{CLAIR_DISABLE_ANTI_AFFINITY_CHECK}}: ${{CLAIR_DISABLE_ANTI_AFFINITY_REASON}}\n    spec:\n      replicas: ${{MATCHER_DEPLOYMENT_REPLICAS}}\n      selector:\n        matchLabels:\n          service: matcher\n          app: clair\n      template:\n        metadata:\n          name: clair-matcher\n          labels:\n            service: matcher\n            app: clair\n            ${{MATCHER_COMPONENT_LABEL_KEY}}: ${{MATCHER_COMPONENT_LABEL_VALUE}}\n        spec:\n          serviceAccountName: ${SERVICE_ACCOUNT}\n          affinity:\n            podAntiAffinity:\n              preferredDuringSchedulingIgnoredDuringExecution:\n                - weight: 1\n                  podAffinityTerm:\n                    labelSelector:\n                      matchExpressions:\n                        - key: ${{MATCHER_COMPONENT_LABEL_KEY}}\n                          operator: In\n                          values:\n                            - ${{MATCHER_COMPONENT_LABEL_VALUE}}\n                    topologyKey: kubernetes.io/hostname\n          volumes:\n            - name: clair-config\n              secret:\n                secretName: config\n          containers:\n            - name: clair-matcher\n              resources:\n                limits:\n                  cpu: ${{MATCHER_CPU_LIMITS}}\n                  memory: ${{MATCHER_MEM_LIMITS}}\n                requests:\n                  cpu: ${{MATCHER_CPU_REQS}}\n                  memory: ${{MATCHER_MEM_REQS}}\n              command: [clair]\n              env:\n                - name: CLAIR_CONF\n                  value: '/etc/clair/config.yaml'\n                - name: CLAIR_MODE\n                  value: matcher\n              image: ${CLAIR_IMAGE}:${IMAGE_TAG}\n              ports:\n                - containerPort: ${{HTTP_TRANSPORT_PORT}}\n                  name: http-transport\n                - containerPort: ${{INTROSPECTION_PORT}}\n                  name: introspection\n              livenessProbe:\n                httpGet:\n                  path: ${{HEALTH_PATH}}\n                  port: ${{HEALTH_PORT}}\n              readinessProbe:\n                httpGet:\n                  path: ${{READY_PATH}}\n                  port: ${{HEALTH_PORT}}\n              startupProbe:\n                httpGet:\n                  path: ${{READY_PATH}}\n                  port: ${{HEALTH_PORT}}\n                failureThreshold: 400\n                periodSeconds: 10\n              volumeMounts:\n                - name: clair-config\n                  mountPath: /etc/clair\n  #\n  # notifier deployment\n  #\n  - apiVersion: apps/v1\n    kind: Deployment\n    metadata:\n      name: clair-notifier\n      annotations:\n        ${{CLAIR_DISABLE_MIN_REPLICAS_CHECK}}: ${{CLAIR_DISABLE_MIN_REPLICAS_REASON}}\n        ${{CLAIR_DISABLE_ANTI_AFFINITY_CHECK}}: ${{CLAIR_DISABLE_ANTI_AFFINITY_REASON}}\n      labels:\n        service: notifier\n        app: clair\n    spec:\n      replicas: ${{NOTIFIER_DEPLOYMENT_REPLICAS}}\n      selector:\n        matchLabels:\n          service: notifier\n          app: clair\n      template:\n        metadata:\n          name: clair-notifier\n          labels:\n            service: notifier\n            app: clair\n            ${{NOTIFIER_COMPONENT_LABEL_KEY}}: ${{NOTIFIER_COMPONENT_LABEL_VALUE}}\n        spec:\n          serviceAccountName: ${SERVICE_ACCOUNT}\n          affinity:\n            podAntiAffinity:\n              preferredDuringSchedulingIgnoredDuringExecution:\n                - weight: 1\n                  podAffinityTerm:\n                    labelSelector:\n                      matchExpressions:\n                        - key: ${{NOTIFIER_COMPONENT_LABEL_KEY}}\n                          operator: In\n                          values:\n                            - ${{NOTIFIER_COMPONENT_LABEL_VALUE}}\n                    topologyKey: kubernetes.io/hostname\n          volumes:\n            - name: clair-config\n              secret:\n                secretName: config\n          containers:\n            - name: clair-notifier\n              resources:\n                limits:\n                  cpu: ${{NOTIFIER_CPU_LIMITS}}\n                  memory: ${{NOTIFIER_MEM_LIMITS}}\n                requests:\n                  cpu: ${{NOTIFIER_CPU_REQS}}\n                  memory: ${{NOTIFIER_MEM_REQS}}\n              command: [clair]\n              env:\n                - name: CLAIR_CONF\n                  value: '/etc/clair/config.yaml'\n                - name: CLAIR_MODE\n                  value: notifier\n              image: ${CLAIR_IMAGE}:${IMAGE_TAG}\n              ports:\n                - containerPort: ${{HTTP_TRANSPORT_PORT}}\n                  name: http-transport\n                - containerPort: ${{INTROSPECTION_PORT}}\n                  name: introspection\n              livenessProbe:\n                httpGet:\n                  path: ${{HEALTH_PATH}}\n                  port: ${{HEALTH_PORT}}\n              readinessProbe:\n                httpGet:\n                  path: ${{READY_PATH}}\n                  port: ${{HEALTH_PORT}}\n              startupProbe:\n                httpGet:\n                  path: ${{READY_PATH}}\n                  port: ${{HEALTH_PORT}}\n                failureThreshold: 400\n                periodSeconds: 10\n              volumeMounts:\n                - name: clair-config\n                  mountPath: /etc/clair\n  #\n  # indexer service\n  #\n  - apiVersion: v1\n    kind: Service\n    metadata:\n      name: clair-indexer\n      labels:\n        service: indexer\n        app: clair\n      annotations:\n        prometheus.io/scrape: 'true'\n    spec:\n      ports:\n        - name: http-transport\n          protocol: TCP\n          port: 80\n          targetPort: ${{HTTP_TRANSPORT_PORT}}\n        - name: introspection\n          protocol: TCP\n          port: 8089\n          targetPort: ${{INTROSPECTION_PORT}}\n      selector:\n        service: indexer\n        app: clair\n  #\n  # matcher service\n  #\n  - apiVersion: v1\n    kind: Service\n    metadata:\n      name: clair-matcher\n      labels:\n        service: matcher\n        app: clair\n      annotations:\n        prometheus.io/scrape: 'true'\n    spec:\n      ports:\n        - name: http-transport\n          protocol: TCP\n          port: 80\n          targetPort: ${{HTTP_TRANSPORT_PORT}}\n        - name: introspection\n          protocol: TCP\n          port: 8089\n          targetPort: ${{INTROSPECTION_PORT}}\n      selector:\n        service: matcher\n        app: clair\n  #\n  # notifier service\n  #\n  - apiVersion: v1\n    kind: Service\n    metadata:\n      name: clair-notifier\n      labels:\n        service: notifier\n        app: clair\n      annotations:\n        prometheus.io/scrape: 'true'\n    spec:\n      ports:\n        - name: http-transport\n          protocol: TCP\n          port: 80\n          targetPort: ${{HTTP_TRANSPORT_PORT}}\n        - name: introspection\n          protocol: TCP\n          port: 8089\n          targetPort: ${{INTROSPECTION_PORT}}\n      selector:\n        service: notifier\n        app: clair\n  #\n  # service account\n  #\n  - apiVersion: v1\n    kind: ServiceAccount\n    metadata:\n      name: ${SERVICE_ACCOUNT}\n    imagePullSecrets:\n      - name: quay.io\n\nparameters:\n  #\n  # indexer params\n  #\n  - name: INDEXER_DEPLOYMENT_REPLICAS\n    value: \"40\"\n    displayName: the number of indexers deployed\n  - name: INDEXER_CPU_LIMITS\n    value: \"4\"\n    displayName: the indexer's cpu limits in vCPUs\n  - name: INDEXER_CPU_REQS\n    value: \"2\"\n    displayName: the indexer's cpu requests in vCPUs\n  - name: INDEXER_MEM_LIMITS\n    value: \"8192Mi\"\n    displayName: the indexer's memory limits\n  - name: INDEXER_MEM_REQS\n    value: \"4096Mi\"\n    displayName: the indexer's memory requests\n  - name: INDEXER_STORAGE_REQS\n    value: \"200Gi\"\n    displayName: the indexer's VPC volume size\n  - name: INDEXER_COMPONENT_LABEL_KEY\n    value: \"clair-indexer-component\"\n    displayName: the label key used for indexer pod anti-affinity\n  - name: INDEXER_COMPONENT_LABEL_VALUE\n    value: \"indexer\"\n    displayName: the label value used for indexer pod anti-affinity\n  #\n  # matcher params\n  #\n  - name: MATCHER_DEPLOYMENT_REPLICAS\n    value: \"10\"\n    displayName: the number of matchers deployed\n  - name: MATCHER_CPU_LIMITS\n    value: \"4\"\n    displayName: the matcher's cpu limits in vCPUs\n  - name: MATCHER_CPU_REQS\n    value: \"2\"\n    displayName: the matcher's cpu requests in vCPUs\n  - name: MATCHER_MEM_LIMITS\n    value: \"8192Mi\"\n    displayName: the matcher's memory limits in vCPUs\n  - name: MATCHER_MEM_REQS\n    value: \"4096Mi\"\n    displayName: the matcher's memory requests in vCPUs\n  - name: MATCHER_COMPONENT_LABEL_KEY\n    value: \"clair-matcher-component\"\n    displayName: the label key used for matcher pod anti-affinity\n  - name: MATCHER_COMPONENT_LABEL_VALUE\n    value: \"matcher\"\n    displayName: the label value used for matcher pod anti-affinity\n  #\n  # notifier params\n  #\n  - name: NOTIFIER_DEPLOYMENT_REPLICAS\n    value: \"10\"\n    displayName: the number of matchers deployed\n  - name: NOTIFIER_CPU_LIMITS\n    value: \"4\"\n    displayName: the matcher's cpu limits in vCPUs\n  - name: NOTIFIER_CPU_REQS\n    value: \"2\"\n    displayName: the matcher's cpu requests in vCPUs\n  - name: NOTIFIER_MEM_LIMITS\n    value: \"8192Mi\"\n    displayName: the matcher's memory limits in vCPUs\n  - name: NOTIFIER_MEM_REQS\n    value: \"4096Mi\"\n    displayName: the matcher's memory requests in vCPUs\n  - name: NOTIFIER_COMPONENT_LABEL_KEY\n    value: \"clair-notifier-component\"\n    displayName: the label key used for notifier pod anti-affinity\n  - name: NOTIFIER_COMPONENT_LABEL_VALUE\n    value: \"notifier\"\n    displayName: the label value used for notifier pod anti-affinity\n  #\n  # shared params\n  #\n  - name: CLAIR_IMAGE\n    value: \"quay.io/app-sre/clair\"\n    displayName: the image of clair v4 to deploy\n  - name: IMAGE_TAG\n    value: \"latest\"\n    displayName: the image tag of clair v4 to deploy\n  - name: HTTP_TRANSPORT_PORT\n    value: \"8080\"\n    displayName: http port where clair's main functionality is provided\n  - name: INTROSPECTION_PORT\n    value: \"8089\"\n    displayName: http port where clair's health/metrics endpoints are provided\n  - name: HEALTH_PATH\n    value: \"/healthz\"\n    displayName: the http path to clair's health check endpoint\n  - name: READY_PATH\n    value: \"/readyz\"\n    displayName: the http path to clair's ready endpoint\n  - name: HEALTH_PORT\n    value: \"8089\"\n    displayName: the port to clair's health check endpoint\n  - name: STORAGE_CLASS_NAME\n    value: \"gp2\"\n    displayName: the storage class to use when creating VPCs\n  - name: SERVICE_ACCOUNT\n    value: \"clair\"\n    displayName: clair service account\n    description: name of the service account to use for API interaction\n  # DVO\n  - name: CLAIR_DISABLE_MIN_REPLICAS_CHECK\n    displayName: Disable Minimum Replicas Check\n    value: \"min-replicas-check-not-disabled\"\n  - name: CLAIR_DISABLE_MIN_REPLICAS_REASON\n    displayName: Reason for Disabling Minimum Replicas Check\n    value: \"\"\n  - name: CLAIR_DISABLE_ANTI_AFFINITY_CHECK\n    value: \"anti-affinity-check-not-disabled\"\n    displayName: disable DVO check for anti-affinity\n  - name: CLAIR_DISABLE_ANTI_AFFINITY_REASON\n    value: \"\"\n    displayName: reason for disabling anti-affinity check\n"
  },
  {
    "path": "contrib/openshift/pr_check.sh",
    "content": "#!/usr/bin/bash\nset -euo pipefail\n[ -n \"${DEBUG-}\" ] && set -x\n\n# This should be run from the repo root, but enforce that:\npushd \"$(git rev-parse --show-toplevel)\"\n./contrib/openshift/build_and_deploy.sh \"-n${-//[^x]}${SKIP_LOGIN:+z}\"\npopd\n\n# If anything specific for the quay.io Clair instance needs to happen, add that here.\n"
  },
  {
    "path": "docker-compose.yaml",
    "content": "---\n# This is just to hold a bunch of yaml anchors and try to consolidate parts of\n# the config.\nx-anchors:\n  go: &go-image quay.io/projectquay/golang:1.25\n  grafana: &grafana-image docker.io/grafana/grafana:10.3.1\n  jaeger: &jaeger-image docker.io/jaegertracing/all-in-one:1\n  pgadmin: &pgadmin-image docker.io/dpage/pgadmin4:5.7\n  postgres: &postgres-image docker.io/library/postgres:15\n  postgres-exporter: &postgres-exporter-image docker.io/prometheuscommunity/postgres-exporter:latest\n  prom: &prom-image docker.io/prom/prometheus:latest\n  pyroscope: &pyroscope-image docker.io/grafana/pyroscope:latest\n  quay: &quay-image quay.io/projectquay/quay:latest\n  rabbitmq: &rabbitmq-image docker.io/library/rabbitmq:3\n  redis: &redis-image docker.io/library/redis:6\n  skopeo: &skopeo-image quay.io/skopeo/stable:latest\n  traefik: &traefik-image docker.io/library/traefik:v3.0\n  clair-service: &clair-service\n    image: *go-image\n    depends_on:\n      clair-database:\n        condition: service_healthy\n    volumes:\n      - \"./local-dev/clair:/etc/clair:ro\"\n      - \".:/src\"\n    # Can't specify the config via environment because maps are not recursively\n    # merged.\n    command:\n      - go\n      - run\n      - .\n      - -conf\n      - /etc/clair/${CLAIR_CONFIG:-config.yaml}\n    restart: unless-stopped\n    working_dir: /src/cmd/clair\n\nservices:\n  indexer:\n    <<: *clair-service\n    container_name: clair-indexer\n    environment:\n      CLAIR_MODE: \"indexer\"\n  matcher:\n    <<: *clair-service\n    container_name: clair-matcher\n    environment:\n      CLAIR_MODE: \"matcher\"\n  clair-database:\n    container_name: clair-database\n    image: *postgres-image\n    environment:\n      POSTGRES_HOST_AUTH_METHOD: trust\n    volumes:\n      - type: bind\n        source: ./local-dev/clair/init.sql\n        target: /docker-entrypoint-initdb.d/init.sql\n    healthcheck:\n      test:\n        - CMD-SHELL\n        - \"pg_isready -U postgres\"\n      interval: 5s\n      timeout: 4s\n      retries: 12\n      start_period: 10s\n  traefik:\n    container_name: clair-traefik\n    image: *traefik-image\n    depends_on:\n      - matcher\n      - indexer\n    ports:\n      - '6060:6060'\n      - '8080:8080'\n      - '8443'\n      - '5432'\n    volumes:\n      - './local-dev/traefik/:/etc/traefik/:ro'\n\n  # Debugging services -- use profile 'debug'\n  pgadmin:\n    container_name: clair-pgadmin\n    profiles:\n      - debug\n    image: *pgadmin-image\n    environment:\n      PGADMIN_DEFAULT_EMAIL: clair@clair.com\n      PGADMIN_DEFAULT_PASSWORD: clair\n      PGADMIN_SERVER_JSON_FILE: /pgadmin4/config/servers.json\n      SCRIPT_NAME: /pgadmin\n    volumes:\n      - \"./local-dev/pgadmin:/pgadmin4/config\"\n    depends_on:\n      - clair-database\n  jaeger:\n    container_name: clair-jaeger\n    profiles:\n      - debug\n    image: *jaeger-image\n    environment:\n      QUERY_BASE_PATH: '/jaeger'\n      COLLECTOR_OTLP_ENABLED: 'true'\n  prometheus:\n    container_name: clair-prometheus\n    profiles:\n      - debug\n    image: *prom-image\n    volumes:\n      - \"./local-dev/prometheus:/etc/prometheus/\"\n    command:\n      - '--config.file=/etc/prometheus/prometheus.yml'\n      - '--storage.tsdb.path=/prometheus'\n      - '--web.external-url=http://localhost:8080/prom/'\n      - '--web.console.libraries=/usr/share/prometheus/console_libraries'\n      - '--web.console.templates=/usr/share/prometheus/consoles'\n  pyroscope:\n    container_name: clair-pyroscope\n    profiles:\n      - debug\n    image: *pyroscope-image\n    volumes:\n      - ./local-dev/pyroscope:/etc/pyroscope/\n    command:\n      - server\n    environment:\n      PYROSCOPE_LOG_LEVEL: info\n      PYROSCOPE_WAIT_AFTER_STOP: 'true'\n  grafana:\n    container_name: clair-grafana\n    profiles:\n      - debug\n    image: *grafana-image\n    user: \"472\"\n    environment:\n      GF_SERVER_ROOT_URL: /grafana\n      GF_SERVER_SERVE_FROM_SUB_PATH: 'true'\n      GF_INSTALL_PLUGINS: 'pyroscope-datasource,pyroscope-panel'\n    volumes:\n      - ./local-dev/grafana/provisioning/:/etc/grafana/provisioning/\n    depends_on:\n      - prometheus\n      - pyroscope\n  postgres-exporter:\n    container_name: clair-postgres-exporter\n    profiles:\n      - debug\n    image: *postgres-exporter-image\n    environment:\n      DATA_SOURCE_NAME: \"postgresql://postgres@clair-database:5432/postgres?sslmode=disable\"\n    depends_on:\n      clair-database:\n        condition: service_healthy\n\n  # Notifier services -- use profile 'notifier'\n  notifier: &notifier\n    <<: *clair-service\n    container_name: clair-notifier\n    profiles:\n      - notifier\n    environment:\n      CLAIR_MODE: \"notifier\"\n      NOTIFIER_TEST_MODE: \"true\"\n  webhook-target:\n    <<: *clair-service\n    container_name: webhook-target\n    profiles:\n      - notifier\n    working_dir: /src\n    depends_on: {}\n    command:\n      - go\n      - run\n      - ./notifier/webhook/cmd/webhookd\n      - -D\n      - -key\n      - c2VjcmV0\n  rabbitmq:\n    # This provides STOMP and AMQP on the usual ports.\n    # The web UI is available on /rabbitmq\n    container_name: clair-rabbitmq\n    profiles:\n      - notifier\n    image: *rabbitmq-image\n    command:\n      - sh\n      - -c\n      - |\n        set -e\n        rabbitmq-plugins enable rabbitmq_stomp rabbitmq_management >/dev/null\n        exec rabbitmq-server\n\n  # Quay -- starts a Quay stack for integration testing.\n  # Use profile 'quay'\n  quay:\n    container_name: clair-quay\n    profiles:\n      - quay\n    image: *quay-image\n    volumes:\n      - \"./local-dev/quay:/quay-registry/conf/stack\"\n    environment:\n      DEBUGLOG: \"true\"\n      IGNORE_VALIDATION: \"true\"\n    depends_on:\n      - redis\n      - clair-database\n  redis:\n    container_name: quay-redis\n    profiles:\n      - quay\n    image: *redis-image\n  quay-notifier:\n    <<: *notifier\n    profiles:\n      - quay\n    environment:\n      CLAIR_MODE: \"notifier\"\n  skopeo:\n    container_name: quay-skopeo\n    profiles:\n      - quay\n    image: *skopeo-image\n    entrypoint:\n      - sleep\n      - inf\n"
  },
  {
    "path": "etc/.gitignore",
    "content": "config.local.mk\n"
  },
  {
    "path": "etc/config.mk",
    "content": "# `docker` and `docker-compose` control the commands invoked for a container\n# engine and a \"compose file\" handler, respectively.\ndocker ?= $(notdir $(shell command -v podman 2>/dev/null || command -v docker 2>/dev/null))\nifndef docker\n$(error 'docker' must not be defined as empty [checked for: podman, docker])\nendif\ndocker-compose ?= $(notdir $(shell command -v docker-compose 2>/dev/null))\nifndef docker-compose\n$(error 'docker-compose' must not be defined as empty [checked for: docker-compose])\nendif\n\n# `Skopeo` controls the specific skopeo command invoked when needed.\nskopeo ?= $(notdir $(shell command -v skopeo 2>/dev/null))\nifndef skopeo\n$(error 'skopeo' must not be defined as empty [checked for: skopeo])\nendif\n\n# `go` controls the specific go command invoked when needed.\ngo ?= $(notdir $(shell command -v go 2>/dev/null))\nifndef go\n$(error 'go' must not be defined as empty [checked for: go])\nendif\n\n# The package used (via `go run`) to format go files.\ngoimports ?= golang.org/x/tools/cmd/goimports@latest\n\n# `Buildctl` controls the specific buildctl command invoked when needed.\nbuildctl ?= $(notdir $(shell command -v buildctl 2>/dev/null))\nifndef buildctl\nbuildctl = $(go) run github.com/moby/buildkit/cmd/buildctl@latest\nendif\n\n# This is the command invoked when `git archive` is needed.\n# The config option forces consistent line endings.\ngit_archive = git -c core.autocrlf=false archive\n\n# These are arguments added to `go test` invocations.\ntestargs =\n\n# VERSION is the version used when guessing at the current version to be used\n# in dist artifacts.\n#\n# This should be able to be interpreted according to gitrevisions(7) and a\n# semver. The default does this by processing `git describe` output.\nVERSION ?= $(shell git describe --match 'v*' --long | sed 's/\\(.\\+\\)-\\([0-9]\\+-g[a-f0-9]\\)/\\1+\\2/')\nifndef VERSION\n$(error 'VERSION' must not be defined as empty)\nendif\n\n# `IMAGE_NAME` is the names(s) used for the `buildctl` invocations.\n# Can be provided as a comma-separated list for multiple names.\nIMAGE_NAME ?= localhost/clair:latest\nifndef IMAGE_NAME\n$(error 'IMAGE_NAME' not defined, need at least one comma-separated value)\nendif\n\n# `CONTAINER_PLATFORMS` controls the architectures (in OCI notation) of the\n# platforms to build container images for. The OS component is always \"linux.\"\nCONTAINER_PLATFORMS ?= amd64 arm64 ppc64le s390x\nifndef CONTAINER_PLATFORMS\n$(error 'CONTAINER_PLATFORMS' not defined, need at least one space-separated value)\nendif\n\n# The following environment variables are passed as build arguments to the buildctl command, if present:\n#\n# - `CLAIR_VERSION`\n# - `GO_VERSION`\n# - `GOTOOLCHAIN`\n# - `SOURCE_DATE_EPOCH`\n#\n# The first three are used in the Dockerfile.\n# The last is an argument to the dockerfile fronend. See also:\n# https://github.com/moby/buildkit/blob/master/frontend/dockerfile/docs/reference.md#buildkit-built-in-build-args\nbuildkit_passthru := CLAIR_VERSION GO_VERSION GOTOOLCHAIN SOURCE_DATE_EPOCH\n\n# Any overrides can be put here:\n-include etc/config.local.mk\n\nifdef DEBUG\n$(let vars,\\\n\tdocker docker-compose go goimports testargs VERSION IMAGE_NAME CONTAINER_PLATFORMS $(buildkit_passthru),\\\n\t$(foreach var,$(vars), $(info DEBUG: $(var): $($(var))))\\\n)\nendif\n"
  },
  {
    "path": "etc/container.mk",
    "content": "# The following builds a command in the \"buildctl_cmd\" variable that expects a\n# context in the shell variable \"src\" and for the make variable \"@\" (the\n# target) to be present. NB these purposefully are not immediately expanded.\noutput_args = type=oci \\\"name=$(IMAGE_NAME)\\\" oci-mediatypes=true compression=estargz rewrite-timestamp=true dest=$@\nbuildctl_cmd = $(buildctl) build --frontend dockerfile.v0\nbuildctl_cmd += --opt platform=$(call splice,$(foreach a,$(CONTAINER_PLATFORMS),linux/$a))\n# DEBUG toggles on the \"plain\" buildkit output.\nifdef DEBUG\nbuildctl_cmd += --progress plain\nendif\nbuildctl_cmd += --opt build-arg:BUILDKIT_MULTI_PLATFORM=1\nbuildctl_cmd += $(strip $(foreach v,$(buildkit_passthru),$(if $($v),--opt build-arg:$v=$($v),)))\nbuildctl_cmd += --local context=$$src\nbuildctl_cmd += --local dockerfile=$$src\nbuildctl_cmd += --output $(call splice,$(output_args))\nifdef GITHUB_ACTIONS\nbuildctl_cmd += --export-cache type=gha,mode=max\nbuildctl_cmd += --import-cache type=gha\nendif\nifndef BUILDKIT_HOST\n# Add a (hopefully) helpful warning that's printed on buildctl use.\nbuildctl_cmd += $(warning 'BUILDKIT_HOST' is not defined)\nendif\n\n# The \"container\" target builds a container using the current state of the tree.\n.PHONY: container\ncontainer: clair.oci\nrm_pat += *.oci\n\n# Clair.oci builds a container using the current state of the tree.\nclair.oci: $(shell git ls-files -- ':*.go' ':go.mod' ':go.sum') Makefile Dockerfile\n\tsrc=.\n\t$(buildctl_cmd)\n\n# Container-build creates a container using the current state of the tree and\n# loads it into the container engine.\n.PHONY: container-build\ncontainer-build: clair.oci\n\t$(docker) load <$<\n\n# Clair-nightly.oci builds a container after applying our \"nightly\" modifications.\nclair-nightly.oci: $(shell git ls-files -- ':*.go' ':go.mod' ':go.sum') Makefile Dockerfile\n\t$(MAKE) nightly-deps\n\tsrc=$$(mktemp -d)\n\ttrap 'rm -rf $$src' EXIT\n\t$(git_archive) --add-file=go.mod --add-file=go.sum HEAD |\n\t\ttar -x -C \"$$src\"\n\t$(buildctl_cmd)\n\n# The \"dist-container\" target builds a container using a created dist archive.\n.PHONY: dist-container\ndist-container: clair-$(VERSION).oci\n\n# Clair-%.oci builds a container using the state of the tree at the commit\n# indicated by the pattern.\nclair-%.oci: clair-%.tar.gz\n\tsrc=$$(mktemp -d)\n\ttrap 'rm -rf $$src' EXIT\n\ttar -xzf $< -C $$src --strip-components=1\n\t$(buildctl_cmd)\n\n# The \"dist-clairctl\" target builds a container containing all the platforms\n# where upstream supports clairctl.\n.PHONY: dist-clairctl\ndist-clairctl: clairctl-$(VERSION)\n\n# Clairctl-% builds a set of clairctl binaries using the state of the tree at\n# the commit indicated by the pattern\nclairctl-%: clair-%.tar.gz\n\tsrc=$$(mktemp -d)\n\ttrap 'rm -rf $$src' EXIT\n\ttar -xzf $< -C $$src --strip-components=1\n\t$(patsubst type=%,$(strip $(call splice,type=local dest=$@)),\\\n\t$(patsubst platform=%,\\\n\tplatform=$(call splice,$(strip\\\n\t\t$(foreach a,amd64 arm64 ppc64le s390x,linux/$a)\\\n\t\t$(foreach a,amd64 arm64,darwin/$a)\\\n\t\t$(foreach a,amd64 arm64,windows/$a)\\\n\t)),\\\n\t$(buildctl_cmd))) --opt target=ctl\nrm_pat += clairctl-*\n"
  },
  {
    "path": "etc/dev.mk",
    "content": "# The \"vendor\" target aliases the actual file that controls the go vendor\n# directory.\n.PHONY: vendor\nvendor: vendor/modules.txt\n\nfindpat := find . -name vendor -prune -o -name\n\n# Helper target for warming the local module cache.\n.PHONY: __moddownload\n__moddownload:\n\t$(findpat) go.mod -execdir $(go) mod download \\;\n\n# `Vendor/modules.txt` is the file that records vendored modules.\n#\n# It's touched on every run of `go mod vendor`, so should be a good proxy for\n# the whole tree.\nvendor/modules.txt: go.mod $(shell $(findpat) *.go -print) | __moddownload\n\t$(go) mod vendor\nrm_pat += vendor\n\n# Nightly-deps modifies the current `go.mod`\n.PHONY: nightly-deps\nnightly-deps: | clean\n\tdeclare -A require exclude replace\n\treplace=(\n\t\t[github.com/quay/claircore]=\"$${CLAIRCORE_BRANCH:=main}\"\n\t)\nifdef GITHUB_ACTIONS\n\techo \"::group::Edits\"\nendif\n\t$(go) mod edit \\\n\t\t$$(for k in \"$${!require[@]}\"; do echo \"-require=$$k@$${require[$$k]}\"; done)\\\n\t\t$$(for k in \"$${!exclude[@]}\"; do echo \"-exclude=$$k@$${exclude[$$k]}\"; done)\\\n\t\t$$(for k in \"$${!replace[@]}\"; do echo \"-replace=$$k=$$k@$${replace[$$k]}\"; done)\n\t$(go) mod tidy\n\t$(go) mod download\nifdef GITHUB_ACTIONS\n\techo \"::endgroup::\"\n\t: \"$${GITHUB_OUTPUT:=/dev/null}\"\n\t: \"$${GITHUB_STEP_SUMMARY:=/dev/stderr}\"\n\tclair_version=\"$$(git describe --tags --always --dirty --match 'v4.*')\"\n\techo \"clair_version=$${clair_version}\" >> \"$$GITHUB_OUTPUT\"\n\tcat <<. >>\"$$GITHUB_STEP_SUMMARY\"\n\t### Changes\n\n\t- **Go version:** $$($(go) version)\n\t- **Clair version:** $${clair_version}\n\t$$(printf '```patch\\n%s\\n```\\n' \"$$(git diff go.mod)\")\n\t.\nendif\n\n# Formats all imports to place local packages below out of tree packages.\n.PHONY: fmt\nfmt:\n\t$(go) list -f '{{$$d := .Dir}}{{range .GoFiles}}{{printf \"%s/%s\\n\" $$d .}}{{end}}' ./... |\\\n\t\txargs sed -i'' '/import (/,/)/{ /^$$/d }'\n\t$(go) list -f '{{.Dir}}' ./... |\\\n\t\txargs $(go) run $(goimports) -local $(shell $(go) list -m) -w\n\na := $(if $(testargs), $(testargs),)\n# Check runs tests for all modules in the local repository.\n#\n# Use the `testargs` variable to add flags to `go test`.\n.PHONY: check\ncheck:\n\t$(findpat) go.mod -execdir $(go) test$(a) ./... \\;\n\n# Checkall runs tests for all modules in the local repository and all\n# dependencies starting with `github.com/quay/clair`.\n#\n# Use the `testargs` variable to add flags to `go test`.\n.PHONY: checkall\ncheckall:\n\t$(go) test$(a) $$( $(go) list -m all | awk '$$1~/github\\.com\\/quay\\/clair/{print $$1\"/...\"}' )\n\n# Start a local development environment.\n#\n# Each service runs in its own container to test service-to-service\n# communication. Local dev configuration can be found in\n# \"/local-dev/clair/config.yaml\"\n.PHONY: local-dev\nlocal-dev: vendor/modules.txt\n\t$(docker-compose) up -d\n\tprintf 'postgresql on port:\\t%s\\n' \"$$($(docker-compose) port traefik 5432)\"\n\n# Targets for docker-compose profiles.\n#\n# TODO(hank) This should build the list of profiles from the docker-compose.yaml file?\n#\tyq '[.services[]|.profiles //[]]|flatten|unique|.[]' docker-compose.yaml\ncompose_profiles := quay notifier debug\ncompose_targets := $(addprefix local-dev-,$(compose_profiles))\ncompose_clair_configs := $(addprefix local-dev/clair/,$(addsuffix .yaml,$(compose_profiles)))\n.PHONY: $(compose_targets)\nlocal-dev-%: local-dev/clair/$*.yaml vendor/modules.txt\n\tCLAIR_CONFIG=$(<F) $(docker-compose) --profile $* up -d\n\tprintf 'postgresql on port:\\t%s\\n' \"$$($(docker-compose) port traefik 5432)\"\n\tprintf 'quay on port:\\t%s\\n' \"$$($(docker-compose) port traefik 8443 || echo N/A)\"\n\n# Some light metaprogramming to construct overridable recipes for per-profile configs.\ndev-config = $(let in out,$1,cp $(in) $(out))\ndev-config-quay = $(let in out,$1,\\\n\tsed '/target:/s,webhook-target/,clair-quay:8443/secscan/notification,' <$(in) >$(out))\n# The following uses the \"call\" on the contents of \"dev-config-${profile}\" if\n# defined, falling back to \"dev-config\".\n$(compose_clair_configs): local-dev/clair/%.yaml: local-dev/clair/config.yaml\n\t$(call $(if $(dev-config-$*),dev-config-$*,dev-config),$< $@)\nrm_pat += local-dev/clair/{$(call splice,$(compose_profiles))}.yaml\n"
  },
  {
    "path": "etc/dist.mk",
    "content": "# The \"dist\" target builds a dist archive at git revision \"VERSION\".\n.PHONY: dist\ndist: clair-$(VERSION).tar.gz\n\n# Clair-%.tar.gz builds a dist archive using the state of the tree at the commit\n# indicated by the pattern.\nclair-%.tar.gz: vendor/modules.txt\n\ttarball=$(subst .gz,,$@)\n\tprefix=$(subst .tar.gz,/,$@)\n\t$(git_archive) --format tar\\\n\t\t--prefix \"$$prefix\"\\\n\t\t--output \"$$tarball\"\\\n\t\t$*\n\tdt=$$(tar --list --file \"$$tarball\" --utc --full-time \"$${prefix}go.mod\" | awk '{print $$4 \"T\" $$5 \"Z\"}')\n\ttar --append --file \"$$tarball\"\\\n\t\t--transform \"s,^,$${prefix},\"\\\n\t\t--mtime \"$$(date -Iseconds --date $$dt)\"\\\n\t\t--sort name\\\n\t\t--pax-option=exthdr.name=%d/PaxHeaders/%f,delete=atime,delete=ctime\\\n\t\tvendor\n\tgzip -n -q -f \"$$tarball\"\nrm_pat += clair-*.tar.gz\n"
  },
  {
    "path": "etc/doc.mk",
    "content": "# Book builds the documentation into the `book` directory.\nbook:\n\tmdbook build\nrm_pat += book\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/quay/clair/v4\n\ngo 1.25.0\n\nrequire (\n\tgithub.com/Masterminds/semver v1.5.0\n\tgithub.com/evanphx/json-patch/v5 v5.9.11\n\tgithub.com/go-jose/go-jose/v3 v3.0.4\n\tgithub.com/go-stomp/stomp/v3 v3.1.5\n\tgithub.com/google/go-cmp v0.7.0\n\tgithub.com/google/go-containerregistry v0.20.7\n\tgithub.com/google/uuid v1.6.0\n\tgithub.com/grafana/pyroscope-go/godeltaprof v0.1.9\n\tgithub.com/jackc/pgx/v5 v5.8.0\n\tgithub.com/klauspost/compress v1.18.4\n\tgithub.com/prometheus/client_golang v1.23.2\n\tgithub.com/quay/clair/config v1.4.3\n\tgithub.com/quay/claircore v1.5.50\n\tgithub.com/quay/claircore/toolkit v1.4.0\n\tgithub.com/quay/zlog/v2 v2.1.0\n\tgithub.com/rabbitmq/amqp091-go v1.10.0\n\tgithub.com/remind101/migrate v0.0.0-20170729031349-52c1edff7319\n\tgithub.com/rogpeppe/go-internal v1.14.1\n\tgithub.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80\n\tgithub.com/ugorji/go/codec v1.2.14\n\tgithub.com/urfave/cli/v2 v2.27.7\n\tgo.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.65.0\n\tgo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0\n\tgo.opentelemetry.io/otel v1.41.0\n\tgo.opentelemetry.io/otel/exporters/jaeger v1.17.0\n\tgo.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.41.0\n\tgo.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.41.0\n\tgo.opentelemetry.io/otel/exporters/otlp/otlptrace v1.41.0\n\tgo.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.41.0\n\tgo.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.41.0\n\tgo.opentelemetry.io/otel/exporters/prometheus v0.63.0\n\tgo.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.41.0\n\tgo.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.41.0\n\tgo.opentelemetry.io/otel/sdk v1.41.0\n\tgo.opentelemetry.io/otel/sdk/metric v1.41.0\n\tgo.opentelemetry.io/otel/trace v1.41.0\n\tgolang.org/x/net v0.51.0\n\tgolang.org/x/sync v0.19.0\n\tgolang.org/x/time v0.14.0\n\tgoogle.golang.org/grpc v1.79.1\n\tgopkg.in/yaml.v3 v3.0.1\n)\n\nrequire (\n\tgithub.com/beorn7/perks v1.0.1 // indirect\n\tgithub.com/cenkalti/backoff/v5 v5.0.3 // indirect\n\tgithub.com/cespare/xxhash/v2 v2.3.0 // indirect\n\tgithub.com/containerd/stargz-snapshotter/estargz v0.18.1 // indirect\n\tgithub.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect\n\tgithub.com/docker/cli v29.0.3+incompatible // indirect\n\tgithub.com/docker/distribution v2.8.3+incompatible // indirect\n\tgithub.com/docker/docker-credential-helpers v0.9.3 // indirect\n\tgithub.com/doug-martin/goqu/v8 v8.6.0 // indirect\n\tgithub.com/dustin/go-humanize v1.0.1 // indirect\n\tgithub.com/felixge/httpsnoop v1.0.4 // indirect\n\tgithub.com/go-logr/logr v1.4.3 // indirect\n\tgithub.com/go-logr/stdr v1.2.2 // indirect\n\tgithub.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect\n\tgithub.com/jackc/chunkreader/v2 v2.0.1 // indirect\n\tgithub.com/jackc/pgconn v1.14.3 // indirect\n\tgithub.com/jackc/pgio v1.0.0 // indirect\n\tgithub.com/jackc/pgpassfile v1.0.0 // indirect\n\tgithub.com/jackc/pgproto3/v2 v2.3.3 // indirect\n\tgithub.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect\n\tgithub.com/jackc/puddle/v2 v2.2.2 // indirect\n\tgithub.com/knqyf263/go-apk-version v0.0.0-20200609155635-041fdbb8563f // indirect\n\tgithub.com/knqyf263/go-deb-version v0.0.0-20190517075300-09fca494f03d // indirect\n\tgithub.com/knqyf263/go-rpm-version v0.0.0-20170716094938-74609b86c936 // indirect\n\tgithub.com/kylelemons/godebug v1.1.0 // indirect\n\tgithub.com/mattn/go-isatty v0.0.20 // indirect\n\tgithub.com/mitchellh/go-homedir v1.1.0 // indirect\n\tgithub.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect\n\tgithub.com/ncruces/go-strftime v1.0.0 // indirect\n\tgithub.com/opencontainers/go-digest v1.0.0 // indirect\n\tgithub.com/opencontainers/image-spec v1.1.1 // indirect\n\tgithub.com/package-url/packageurl-go v0.1.4 // indirect\n\tgithub.com/prometheus/client_model v0.6.2 // indirect\n\tgithub.com/prometheus/common v0.67.5 // indirect\n\tgithub.com/prometheus/otlptranslator v1.0.0 // indirect\n\tgithub.com/prometheus/procfs v0.19.2 // indirect\n\tgithub.com/quay/claircore/updater/driver v1.0.0 // indirect\n\tgithub.com/quay/goval-parser v0.8.8 // indirect\n\tgithub.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect\n\tgithub.com/russross/blackfriday/v2 v2.1.0 // indirect\n\tgithub.com/sirupsen/logrus v1.9.3 // indirect\n\tgithub.com/ulikunitz/xz v0.5.15 // indirect\n\tgithub.com/vbatts/tar-split v0.12.2 // indirect\n\tgithub.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect\n\tgo.opentelemetry.io/auto/sdk v1.2.1 // indirect\n\tgo.opentelemetry.io/otel/metric v1.41.0 // indirect\n\tgo.opentelemetry.io/proto/otlp v1.9.0 // indirect\n\tgo.uber.org/mock v0.6.0 // indirect\n\tgo.yaml.in/yaml/v2 v2.4.3 // indirect\n\tgolang.org/x/crypto v0.48.0 // indirect\n\tgolang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect\n\tgolang.org/x/mod v0.33.0 // indirect\n\tgolang.org/x/sys v0.41.0 // indirect\n\tgolang.org/x/text v0.34.0 // indirect\n\tgolang.org/x/tools v0.42.0 // indirect\n\tgoogle.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 // indirect\n\tgoogle.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect\n\tgoogle.golang.org/protobuf v1.36.11 // 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": "github.com/DATA-DOG/go-sqlmock v1.3.3 h1:CWUqKXe0s8A2z6qCgkP4Kru7wC11YoAnoupUKFDnH08=\ngithub.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=\ngithub.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=\ngithub.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=\ngithub.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=\ngithub.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=\ngithub.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=\ngithub.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=\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/containerd/stargz-snapshotter/estargz v0.18.1 h1:cy2/lpgBXDA3cDKSyEfNOFMA/c10O1axL69EU7iirO8=\ngithub.com/containerd/stargz-snapshotter/estargz v0.18.1/go.mod h1:ALIEqa7B6oVDsrF37GkGN20SuvG/pIMm7FwP7ZmRb0Q=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/docker/cli v29.0.3+incompatible h1:8J+PZIcF2xLd6h5sHPsp5pvvJA+Sr2wGQxHkRl53a1E=\ngithub.com/docker/cli v29.0.3+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-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8=\ngithub.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo=\ngithub.com/doug-martin/goqu/v8 v8.6.0 h1:KWuDGL135poBgY+SceArvOtIIEpieNKgIZCvgerI228=\ngithub.com/doug-martin/goqu/v8 v8.6.0/go.mod h1:wiiYWkiguNXK5d4kGIkYmOxBScEL37d9Cfv9tXhPsTk=\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/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU=\ngithub.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM=\ngithub.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=\ngithub.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=\ngithub.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY=\ngithub.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=\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-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=\ngithub.com/go-stomp/stomp/v3 v3.1.5 h1:Pikz1OSusmSKUm5mRKYfXQZaDatfZ+EnBBA1JJ2xENQ=\ngithub.com/go-stomp/stomp/v3 v3.1.5/go.mod h1:ztzZej6T2W4Y6FlD+Tb5n7HQP3/O5UNQiuC169pIp10=\ngithub.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=\ngithub.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=\ngithub.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=\ngithub.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=\ngithub.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=\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.20.7 h1:24VGNpS0IwrOZ2ms2P1QE3Xa5X9p4phx0aUgzYzHW6I=\ngithub.com/google/go-containerregistry v0.20.7/go.mod h1:Lx5LCZQjLH1QBaMPeGwsME9biPeo1lPx6lbGj/UmzgM=\ngithub.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=\ngithub.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=\ngithub.com/google/uuid v1.3.0/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/grafana/pyroscope-go/godeltaprof v0.1.9 h1:c1Us8i6eSmkW+Ez05d3co8kasnuOY813tbMN8i/a3Og=\ngithub.com/grafana/pyroscope-go/godeltaprof v0.1.9/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU=\ngithub.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs=\ngithub.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=\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/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=\ngithub.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=\ngithub.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=\ngithub.com/jackc/pgconn v1.14.3 h1:bVoTr12EGANZz66nZPkMInAV/KHD2TxH9npjXXgiB3w=\ngithub.com/jackc/pgconn v1.14.3/go.mod h1:RZbme4uasqzybK2RK5c65VsHxoyaml09lx3tXOcO/VM=\ngithub.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=\ngithub.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=\ngithub.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc=\ngithub.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak=\ngithub.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=\ngithub.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=\ngithub.com/jackc/pgproto3/v2 v2.3.3 h1:1HLSx5H+tXR9pW3in3zaztoEwQYRC9SQaYUHjTSUOag=\ngithub.com/jackc/pgproto3/v2 v2.3.3/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=\ngithub.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=\ngithub.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=\ngithub.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo=\ngithub.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw=\ngithub.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=\ngithub.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=\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/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-20190517075300-09fca494f03d h1:X4cedH4Kn3JPupAwwWuo4AzYp16P0OyLO9d7OnMZc/c=\ngithub.com/knqyf263/go-deb-version v0.0.0-20190517075300-09fca494f03d/go.mod h1:o8sgWoz3JADecfc/cTYD92/Et1yMqMy0utV1z+VaZao=\ngithub.com/knqyf263/go-rpm-version v0.0.0-20170716094938-74609b86c936 h1:HDjRqotkViMNcGMGicb7cgxklx8OwnjtCBmyWEqrRvM=\ngithub.com/knqyf263/go-rpm-version v0.0.0-20170716094938-74609b86c936/go.mod h1:i4sF0l1fFnY1aiw08QQSwVAFxHEm311Me3WsU/X7nL0=\ngithub.com/kr/pretty v0.2.1/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/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=\ngithub.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=\ngithub.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=\ngithub.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8=\ngithub.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=\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-sqlite3 v1.10.0 h1:jbhqpg7tQe4SupckyijYiy0mJJ/pRyHvXf7JdWK860o=\ngithub.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=\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/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=\ngithub.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=\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/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/package-url/packageurl-go v0.1.4 h1:RHfiiN1SSY+Kic537DXch6fy593rxGJW6WDzAiOwNdk=\ngithub.com/package-url/packageurl-go v0.1.4/go.mod h1:nKAWB8E6uk1MHqiS/lQb9pYBGH2+mdJ2PJc2s50dQY0=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=\ngithub.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=\ngithub.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=\ngithub.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=\ngithub.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=\ngithub.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=\ngithub.com/prometheus/otlptranslator v1.0.0 h1:s0LJW/iN9dkIH+EnhiD3BlkkP5QVIUVEoIwkU+A6qos=\ngithub.com/prometheus/otlptranslator v1.0.0/go.mod h1:vRYWnXvI6aWGpsdY/mOT/cbeVRBlPWtBNDb7kGR3uKM=\ngithub.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=\ngithub.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=\ngithub.com/quay/clair/config v1.4.3 h1:ZrvqKJrAR2Noyl1XcMl9oOucdMJsdjGzqQqT/rzxb50=\ngithub.com/quay/clair/config v1.4.3/go.mod h1:MyYm2qGw55+I598zEpwpFFmBq1jp5NLIhBhCigv6tRM=\ngithub.com/quay/claircore v1.5.50 h1:ASYcI4YCdSdSoaNYJvYbihtIzs9cbS9FRjVyUeKjI9k=\ngithub.com/quay/claircore v1.5.50/go.mod h1:MUpoIaI2d83GMn7OwiTJMAXC/11KsEQQJZ8GhDsWFcI=\ngithub.com/quay/claircore/toolkit v1.0.0/go.mod h1:3ELtgf92x7o1JCTSKVOAqhcnCTXc4s5qiGaEDx62i20=\ngithub.com/quay/claircore/toolkit v1.4.0 h1:ygHG1pLAOTSk7r2Wmo/UbIz9WHJb+K3hhQnNIvLrBSQ=\ngithub.com/quay/claircore/toolkit v1.4.0/go.mod h1:0cQXEt/BIYSxo/Wq6ItyAJOJzdvD6ty1wPZJ9xR3b6E=\ngithub.com/quay/claircore/updater/driver v1.0.0 h1:w7dAUjO3GBK6RjNyTZ2Kwz0l/Wuic3ykKJWPB80uA94=\ngithub.com/quay/claircore/updater/driver v1.0.0/go.mod h1:My5aY1wBpgxcWaHQZ0VoPmmj/EzuH7fq4ntzJbos4OI=\ngithub.com/quay/goval-parser v0.8.8 h1:Uf+f9iF2GIR5GPUY2pGoa9il2+4cdES44ZlM0mWm4cA=\ngithub.com/quay/goval-parser v0.8.8/go.mod h1:Y0NTNfPYOC7yxsYKzJOrscTWUPq1+QbtHw4XpPXWPMc=\ngithub.com/quay/zlog/v2 v2.1.0 h1:U55jQXHqB3NyWEt0iPESzIOjQho3PG4HWfXj9j40vEs=\ngithub.com/quay/zlog/v2 v2.1.0/go.mod h1:+FmBwcNIbXOKRWfaIqHGPF+g9WW4hn+OiomLN7eC/dM=\ngithub.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw=\ngithub.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o=\ngithub.com/remind101/migrate v0.0.0-20170729031349-52c1edff7319 h1:ukjThsA2ou7AmovpwtMVkNQSuoN/v5U16+JomTz3c7o=\ngithub.com/remind101/migrate v0.0.0-20170729031349-52c1edff7319/go.mod h1:rhSvwcijY9wfmrBYrfCvapX8/xOTV46NAUjBRgUyJqc=\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/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 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=\ngithub.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=\ngithub.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=\ngithub.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=\ngithub.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=\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.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\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/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 h1:nrZ3ySNYwJbSpD6ce9duiP+QkD3JuLCcWkdaehUS/3Y=\ngithub.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80/go.mod h1:iFyPdL66DjUD96XmzVL3ZntbzcflLnznH0fr99w5VqE=\ngithub.com/ugorji/go/codec v1.2.14 h1:yOQvXCBc3Ij46LRkRoh4Yd5qK6LVOgi0bYOXfb7ifjw=\ngithub.com/ugorji/go/codec v1.2.14/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=\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/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU=\ngithub.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4=\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/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=\ngithub.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=\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=\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/instrumentation/net/http/httptrace/otelhttptrace v0.65.0 h1:ab5U7DpTjjN8pNgwqlA/s0Csb+N2Raqo9eTSDhfg4Z8=\ngo.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.65.0/go.mod h1:nwFJC46Dxhqz5R9k7IV8To/Z46JPvW+GNKhTxQQlUzg=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0=\ngo.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c=\ngo.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE=\ngo.opentelemetry.io/otel/exporters/jaeger v1.17.0 h1:D7UpUy2Xc2wsi1Ras6V40q806WM07rqoCWzXu7Sqy+4=\ngo.opentelemetry.io/otel/exporters/jaeger v1.17.0/go.mod h1:nPCqOnEH9rNLKqH/+rrUjiMzHJdV1BlpKcTwRTyKkKI=\ngo.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.41.0 h1:VO3BL6OZXRQ1yQc8W6EVfJzINeJ35BkiHx4MYfoQf44=\ngo.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.41.0/go.mod h1:qRDnJ2nv3CQXMK2HUd9K9VtvedsPAce3S+/4LZHjX/s=\ngo.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.41.0 h1:MMrOAN8H1FrvDyq9UJ4lu5/+ss49Qgfgb7Zpm0m8ABo=\ngo.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.41.0/go.mod h1:Na+2NNASJtF+uT4NxDe0G+NQb+bUgdPDfwxY/6JmS/c=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace v1.41.0 h1:ao6Oe+wSebTlQ1OEht7jlYTzQKE+pnx/iNywFvTbuuI=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace v1.41.0/go.mod h1:u3T6vz0gh/NVzgDgiwkgLxpsSF6PaPmo2il0apGJbls=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.41.0 h1:mq/Qcf28TWz719lE3/hMB4KkyDuLJIvgJnFGcd0kEUI=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.41.0/go.mod h1:yk5LXEYhsL2htyDNJbEq7fWzNEigeEdV5xBF/Y+kAv0=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.41.0 h1:inYW9ZhgqiDqh6BioM7DVHHzEGVq76Db5897WLGZ5Go=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.41.0/go.mod h1:Izur+Wt8gClgMJqO/cZ8wdeeMryJ/xxiOVgFSSfpDTY=\ngo.opentelemetry.io/otel/exporters/prometheus v0.63.0 h1:OLo1FNb0pBZykLqbKRZolKtGZd0Waqlr240YdMEnhhg=\ngo.opentelemetry.io/otel/exporters/prometheus v0.63.0/go.mod h1:8yeQAdhrK5xsWuFehO13Dk/Xb9FuhZoVpJfpoNCfJnw=\ngo.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.41.0 h1:8+lzlbtX0QZ2TfILr7utn3YBipxmIjPHuHgcI1hDLI4=\ngo.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.41.0/go.mod h1:sYzlrHkIULlZBHvhA0wqbR8tHt23pv4eJ87fINn4/Tc=\ngo.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.41.0 h1:61oRQmYGMW7pXmFjPg1Muy84ndqMxQ6SH2L8fBG8fSY=\ngo.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.41.0/go.mod h1:c0z2ubK4RQL+kSDuuFu9WnuXimObon3IiKjJf4NACvU=\ngo.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ=\ngo.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps=\ngo.opentelemetry.io/otel/sdk v1.41.0 h1:YPIEXKmiAwkGl3Gu1huk1aYWwtpRLeskpV+wPisxBp8=\ngo.opentelemetry.io/otel/sdk v1.41.0/go.mod h1:ahFdU0G5y8IxglBf0QBJXgSe7agzjE4GiTJ6HT9ud90=\ngo.opentelemetry.io/otel/sdk/metric v1.41.0 h1:siZQIYBAUd1rlIWQT2uCxWJxcCO7q3TriaMlf08rXw8=\ngo.opentelemetry.io/otel/sdk/metric v1.41.0/go.mod h1:HNBuSvT7ROaGtGI50ArdRLUnvRTRGniSUZbxiWxSO8Y=\ngo.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0=\ngo.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis=\ngo.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=\ngo.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=\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/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=\ngo.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=\ngo.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=\ngo.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=\ngolang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=\ngolang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=\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/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\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.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=\ngolang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=\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-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=\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.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=\ngolang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=\ngolang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/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.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=\ngolang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/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-20210615035016-665e8c7367d1/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.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\ngolang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=\ngolang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=\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.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=\ngolang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=\ngolang.org/x/text v0.3.0/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.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.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=\ngolang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=\ngolang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=\ngolang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=\ngolang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=\ngolang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190924052046-3ac2a5bbd98a/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.1.1/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.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=\ngolang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=\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=\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/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 h1:JLQynH/LBHfCTSbDWl+py8C+Rg/k1OVH3xfcaiANuF0=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=\ngoogle.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=\ngoogle.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=\ngoogle.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=\ngoogle.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/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/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/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=\ngotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0=\ngotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8=\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=\n"
  },
  {
    "path": "health/readinesshandler.go",
    "content": "package health\n\nimport (\n\t\"net/http\"\n\t\"sync/atomic\"\n)\n\nvar ready atomic.Uint32\n\n// Ready instructs the ReadinessHandler to begin serving 200 OK status.\nfunc Ready() {\n\tready.Store(uint32(1))\n}\n\n// Unready instructs the ReadinessHandler to begin serving 503\n// Service Unavailable.\nfunc Unready() {\n\tready.Store(uint32(0))\n}\n\n// ReadinessHandler will return a 200 OK or 503 \"Service Unavailable\" status\n// depending on whether the Ready or Unready functions have been called.\n//\n// The Ready() method must be called to begin returning 200 OK.\nfunc ReadinessHandler() http.Handler {\n\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\th := w.Header()\n\t\th.Set(\"X-Content-Type-Options\", \"nosniff\")\n\t\th.Set(\"Content-Type\", \"text/plain; charset=utf-8\")\n\t\th.Set(\"Content-Length\", \"0\")\n\t\th.Set(\"Cache-Control\", \"no-store\")\n\t\tif r.Method != http.MethodGet {\n\t\t\tw.WriteHeader(http.StatusMethodNotAllowed)\n\t\t\treturn\n\t\t}\n\n\t\tif ready.Load() != 1 {\n\t\t\tw.WriteHeader(http.StatusServiceUnavailable)\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "health/readinesshandler_test.go",
    "content": "package health_test\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/quay/clair/v4/health\"\n\t\"github.com/quay/clair/v4/internal/httputil\"\n)\n\nfunc TestReadinessHandler(t *testing.T) {\n\tctx := context.Background()\n\tserver := httptest.NewServer(health.ReadinessHandler())\n\tclient := server.Client()\n\n\treq, err := httputil.NewRequestWithContext(ctx, http.MethodGet, server.URL, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to create request: %v\", err)\n\t}\n\t// default handler state should return StatusServiceUnavailable\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to do request: %v\", err)\n\t}\n\tif resp.StatusCode != http.StatusServiceUnavailable {\n\t\tt.Fatalf(\"expected %d got %d\", http.StatusServiceUnavailable, resp.StatusCode)\n\t}\n\n\t// signal to handler that process is ready. should return StatusOK\n\thealth.Ready()\n\tresp, err = client.Do(req)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to do request: %v\", err)\n\t}\n\tif resp.StatusCode != http.StatusOK {\n\t\tt.Fatalf(\"expected %d got %d\", http.StatusOK, resp.StatusCode)\n\t}\n\n\t// signal to handler that process is unready. should return StatusServiceUnavailable\n\thealth.Unready()\n\tresp, err = client.Do(req)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to do request: %v\", err)\n\t}\n\tif resp.StatusCode != http.StatusServiceUnavailable {\n\t\tt.Fatalf(\"expected %d got %d\", http.StatusServiceUnavailable, resp.StatusCode)\n\t}\n}\n"
  },
  {
    "path": "httptransport/api/.gitattributes",
    "content": "v*/openapi.json linguist-generated\nv*/openapi.yaml linguist-generated\nv*/openapi.etag linguist-generated\n"
  },
  {
    "path": "httptransport/api/lib/oapi.jq",
    "content": "# vim: set expandtab ts=2 sw=2:\nmodule {\n  name: \"openapi\",\n};\n\n# Some helper functions:\n\ndef ref($ref): # Construct a JSON Schema reference object.\n  { \"$ref\": \"\\($ref)\" }\n;\n\ndef lref($kind; $id): # Construct a ref object to an OpenAPI component.\n  ref(\"#/components/\\($kind)/\\($id)\")\n;\n\ndef param_ref($id): # Construct a ref object to an OpenAPI parameter component.\n  lref(\"parameters\"; $id)\n;\n\ndef response_ref($id): # Construct a ref object to an OpenAPI response component.\n  lref(\"responses\"; $id)\n;\n\ndef header_ref($id): # Construct a ref object to an OpenAPI header component.\n  lref(\"headers\"; $id)\n;\n\ndef schema_ref($id): # Construct a ref object to an OpenAPI schema component.\n  lref(\"schemas\"; $id)\n;\n\ndef mediatype($t; $v): # Return the local vendor mediatype for $t, version $v.\n  \"application/vnd.clair.\\($t).\\($v)+json\"\n;\n\ndef mediatype($t): # As mediatype/2, but with the default of \"v1\".\n  mediatype($t; \"v1\")\n;\n\ndef contenttype($t; $v): # Construct an OpenAPI content type object for $t, version $v.\n  { (mediatype($t; $v)): { \"schema\": schema_ref($t) } }\n;\n\ndef contenttype($t): # As contenttype/2, but with the default version.\n  { (mediatype($t)): { \"schema\": schema_ref($t) } }\n;\n\ndef cli_hints: # Add some hints that CLI tools can pick up on to ignore our internal paths.\n  (.paths[][] | select(objects and (.tags|contains([\"internal\"]))) ) |= . + {\"x-cli-ignore\": true}\n;\n\ndef sort_paths: # Sort the paths object.\n  .paths |= (. | to_entries | sort_by(.key) | from_entries)\n;\n\ndef content_defaults: # All responses that don't have a \"default\" type, pick the first one.\n  \"application/json\" as $t |\n  [[\"example\"], [\"examples\"]] as $rm |\n  ( .paths[][] | select(objects) | .responses[].content | select(objects and (has($t)|not)) ) |= (. + { $t: (to_entries[0].value | delpaths($rm)) })\n  |\n  ( .paths[][] | select(objects) | .requestBody.content | select(objects and (has($t)|not)) ) |= (. + { $t: (to_entries[0].value | delpaths($rm)) })\n  |\n  ( .components.responses[].content | select(has($t)|not) ) |= (. + { $t: (to_entries[0].value | delpaths($rm)) })\n;\n"
  },
  {
    "path": "httptransport/api/openapi.zsh",
    "content": "#!/usr/bin/zsh\nset -euo pipefail\n\n# This script builds the OpenAPI documents, rendering them into YAML and JSON.\n#\n# The main inputs for this are the \"openapi.jq\" files in the \"v?\" directories.\n# These are jq(1) scripts that are executed with no input in the relevant\n# directory; they're expected to output a valid OpenAPI document. All the JSON\n# Schema documents in the matching \"httptransport/types/v?\" directory are\n# copied into the working directory. Matching files in the \"examples\"\n# subdirectory will be slipstreamed to the expected field.\n#\n# The result is then \"bundled\" into one document, then linted, rendered out to\n# both YAML and JSON, and strings to be used as HTTP Etags are written out.\n\nfor cmd in sha256sum git jq yq npx; do\n\tif ! command -v \"$cmd\" &>/dev/null; then\n\t\tprint missing needed command: \"$cmd\" >&2\n\t\texit 1\n\tfi\ndone\nlocal root=$(git rev-parse --show-toplevel)\n\nfunction jq() {\n\tcommand jq --exit-status \"$@\"\n}\n\nfunction yq() {\n\tcommand yq --exit-status \"$@\"\n}\n\nfunction schemalint() {\n\tlocal tmp=$(mktemp --tmpdir schemalint.out.XXX)\n\ttrap \"\n\t\t[[ \\\"\\$?\\\" -eq 0 ]] || cat \\\"$tmp\\\"\n\t\t[[ -f \\\"$tmp\\\" ]] && rm \\\"$tmp\\\"\n\t\" EXIT\n\tnpx --yes @sourcemeta/jsonschema metaschema --resolve \"$1\" \"$1\" &>>\"$tmp\"\n\tnpx --yes @sourcemeta/jsonschema lint       --resolve \"$1\" \"$1\" &>>\"$tmp\"\n}\n\nfunction widdershins() {\n\tlocal tmp=$(mktemp --tmpdir widdershins.out.XXX)\n\ttrap \"\n\t\t[[ \\\"\\$?\\\" -eq 0 ]] || cat \\\"$tmp\\\"\n\t\t[[ -f \\\"$tmp\\\" ]] && rm \\\"$tmp\\\"\n\t\" EXIT\n\tnpx --yes widdershins \\\n\t\t-o \"${root}/Documentation/reference/api.md\" \\\n\t\t--search false \\\n\t\t--language_tabs python:Python go:Golang javascript:Javascript \\\n\t\t--summary \\\n\t\t\"${1?missing OpenAPI document}\" &>\"$tmp\"\n}\n\nfunction render() {\n\ttrap '\n\t\trm openapi.*.{json,yaml}(N) *.schema.json(N)\n\t\tpopd -q\n\t' EXIT\n\tpushd -q \"${1?missing directory argument}\"\n\tlocal v=${1:A:t}\n\tlocal t=${1:A:h:h}/types/$v\n\n\tschemalint \"$t\"\n\tfor f in ${t}/*.schema.json; do\n\t\tlocal ex=examples/${${f:t}%.schema.json}.json\n\t\tif [[ -f \"$ex\" ]]; then\n\t\t\tjq --slurpfile ex \"${ex}\" 'setpath([\"examples\"]; $ex)' \"$f\" > \"${f:t}\"\n\t\telse\n\t\t\tcp \"$f\" .\n\t\tfi\n\tdone\n\n\tjq --null-input \\\n\t\t'reduce (inputs|(.[\"$id\"]|split(\"/\")|.[-1]|rtrimstr(\".schema.json\")) as $k|{components:{schemas:{$k:.}}}) as $it({};. * $it)'\\\n\t\t*.schema.json >openapi.types.json\n\n\n\tjq --null-input -L \"${1:A:h}/lib\" --from-file openapi.jq >openapi.frag.json\n\tjq --null-input 'reduce inputs as $it({};. * $it)' openapi.{frag,types}.json >openapi.json\n\n\tyq -pj eval . <openapi.json >openapi.yaml\n\t# Need some validator that actually works >:(\n\n\tsha256sum openapi.json |\n\t\tawk '{printf \"\\\"%s\\\"\", $1 >\"openapi.etag\" }'\n}\n\n# Process all the openapi generation scripts.\nfor d in ${root}/httptransport/api/v*/openapi.jq(om); do\n\trender \"${d:h}\"\ndone\n\n# Generate documentation for the highest version.\nwiddershins \"${root}\"/httptransport/api/v*/openapi.yaml(NnOn[1])\n"
  },
  {
    "path": "httptransport/api/v1/examples/affected_manifests.json",
    "content": "{\"vulnerabilities\":{\"42\":{\"id\":\"42\"}},\"vulnerable_manifests\":{\"sha256:01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b\":[\"42\"]}}\n"
  },
  {
    "path": "httptransport/api/v1/examples/bulk_delete.json",
    "content": "[\n  \"sha256:fc84b5febd328eccaa913807716887b3eb5ed08bc22cc6933a9ebf82766725e3\"\n]\n"
  },
  {
    "path": "httptransport/api/v1/examples/cpe.json",
    "content": "\"cpe:/a:microsoft:internet_explorer:8.0.6001:beta\"\n\"cpe:2.3:a:microsoft:internet_explorer:8.0.6001:beta:*:*:*:*:*:*\"\n"
  },
  {
    "path": "httptransport/api/v1/examples/digest.json",
    "content": "\"sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a\"\n\"sha512:27c74670adb75075fad058d5ceaf7b20c4e7786c83bae8a32f626f9782af34c9a33c2046ef60fd2a7878d378e29fec851806bbd9a67878f3a9f1cda4830763fd\"\n\"blake3:6e46dd10defc9b56c29a6ec56b508c21f54c08192194e4df25bf36f0c9c3c279\"\n"
  },
  {
    "path": "httptransport/api/v1/examples/distribution.json",
    "content": "{\n  \"id\": \"1\",\n  \"did\": \"ubuntu\",\n  \"name\": \"Ubuntu\",\n  \"version\": \"18.04.3 LTS (Bionic Beaver)\",\n  \"version_code_name\": \"bionic\",\n  \"version_id\": \"18.04\",\n  \"pretty_name\": \"Ubuntu 18.04.3 LTS\"\n}\n"
  },
  {
    "path": "httptransport/api/v1/examples/environment.json",
    "content": "{\n  \"value\": {\n    \"package_db\": \"var/lib/dpkg/status\",\n    \"introduced_in\": \"sha256:35c102085707f703de2d9eaad8752d6fe1b8f02b5d2149f1d8357c9cc7fb7d0a\",\n    \"distribution_id\": \"1\"\n  }\n}\n"
  },
  {
    "path": "httptransport/api/v1/examples/manifest.json",
    "content": "{\n  \"hash\": \"sha256:fc84b5febd328eccaa913807716887b3eb5ed08bc22cc6933a9ebf82766725e3\",\n  \"layers\": [\n    {\n      \"hash\": \"sha256:2f077db56abccc19f16f140f629ae98e904b4b7d563957a7fc319bd11b82ba36\",\n      \"uri\": \"https://storage.example.com/blob/2f077db56abccc19f16f140f629ae98e904b4b7d563957a7fc319bd11b82ba36\",\n      \"headers\": {\n        \"Authoriztion\": [\n          \"Bearer hunter2\"\n        ]\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "httptransport/api/v1/examples/notification_page.json",
    "content": "{\n  \"page\": {\n    \"size\": 100,\n    \"next\": \"1b4d0db2-e757-4150-bbbb-543658144205\"\n  },\n  \"notifications\": [\n    {\n      \"id\": \"5e4b387e-88d3-4364-86fd-063447a6fad2\",\n      \"manifest\": \"sha256:35c102085707f703de2d9eaad8752d6fe1b8f02b5d2149f1d8357c9cc7fb7d0a\",\n      \"reason\": \"added\",\n      \"vulnerability\": {\n        \"name\": \"CVE-2009-5155\",\n        \"fixed_in_version\": \"v0.0.1\",\n        \"links\": \"http://example.com/CVE-2009-5155\",\n        \"description\": \"In the GNU C Library (aka glibc or libc6) before 2.28, parse_reg_exp in posix/regcomp.c misparses alternatives, which allows attackers to cause a denial of service (assertion failure and application exit) or trigger an incorrect result by attempting a regular-expression match.\\\"\",\n        \"normalized_severity\": \"Unknown\",\n        \"package\": {\n          \"id\": \"10\",\n          \"name\": \"libapt-pkg5.0\",\n          \"version\": \"1.6.11\",\n          \"kind\": \"BINARY\",\n          \"arch\": \"x86\",\n          \"source\": {\n            \"id\": \"9\",\n            \"name\": \"apt\",\n            \"version\": \"1.6.11\",\n            \"kind\": \"SOURCE\",\n            \"source\": null\n          }\n        },\n        \"distribution\": {\n          \"id\": \"1\",\n          \"did\": \"ubuntu\",\n          \"name\": \"Ubuntu\",\n          \"version\": \"18.04.3 LTS (Bionic Beaver)\",\n          \"version_code_name\": \"bionic\",\n          \"version_id\": \"18.04\",\n          \"pretty_name\": \"Ubuntu 18.04.3 LTS\"\n        }\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "httptransport/api/v1/examples/package.json",
    "content": "{\n  \"id\": \"10\",\n  \"name\": \"libapt-pkg5.0\",\n  \"version\": \"1.6.11\",\n  \"kind\": \"binary\",\n  \"normalized_version\": \"\",\n  \"arch\": \"x86\",\n  \"module\": \"\",\n  \"cpe\": \"\",\n  \"source\": {\n    \"id\": \"9\",\n    \"name\": \"apt\",\n    \"version\": \"1.6.11\",\n    \"kind\": \"source\",\n    \"source\": null\n  }\n}\n"
  },
  {
    "path": "httptransport/api/v1/examples/vulnerability.json",
    "content": "{\n  \"id\": \"356835\",\n  \"updater\": \"ubuntu\",\n  \"name\": \"CVE-2009-5155\",\n  \"description\": \"In the GNU C Library (aka glibc or libc6) before 2.28, parse_reg_exp in posix/regcomp.c misparses alternatives, which allows attackers to cause a denial of service (assertion failure and application exit) or trigger an incorrect result by attempting a regular-expression match.\",\n  \"links\": \"https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2009-5155 http://people.canonical.com/~ubuntu-security/cve/2009/CVE-2009-5155.html https://sourceware.org/bugzilla/show_bug.cgi?id=11053 https://debbugs.gnu.org/cgi/bugreport.cgi?bug=22793 https://debbugs.gnu.org/cgi/bugreport.cgi?bug=32806 https://debbugs.gnu.org/cgi/bugreport.cgi?bug=34238 https://sourceware.org/bugzilla/show_bug.cgi?id=18986\",\n  \"severity\": \"Low\",\n  \"normalized_severity\": \"Low\",\n  \"package\": {\n    \"id\": \"0\",\n    \"name\": \"glibc\",\n    \"version\": \"2.27-0ubuntu1\",\n    \"kind\": \"binary\",\n    \"source\": null\n  },\n  \"dist\": {\n    \"id\": \"0\",\n    \"did\": \"ubuntu\",\n    \"name\": \"Ubuntu\",\n    \"version\": \"18.04.3 LTS (Bionic Beaver)\",\n    \"version_code_name\": \"bionic\",\n    \"version_id\": \"18.04\",\n    \"arch\": \"amd64\"\n  },\n  \"repo\": {\n    \"id\": \"0\",\n    \"name\": \"Ubuntu 18.04.3 LTS\"\n  },\n  \"issued\": \"2019-10-12T07:20:50.52Z\",\n  \"fixed_in_version\": \"2.28-0ubuntu1\"\n}\n"
  },
  {
    "path": "httptransport/api/v1/examples/vulnerability_summary.json",
    "content": "{\n  \"name\": \"CVE-2009-5155\",\n  \"description\": \"In the GNU C Library (aka glibc or libc6) before 2.28, parse_reg_exp in posix/regcomp.c misparses alternatives, which allows attackers to cause a denial of service (assertion failure and application exit) or trigger an incorrect result by attempting a regular-expression match.\",\n  \"normalized_severity\": \"Low\",\n  \"fixed_in_version\": \"v0.0.1\",\n  \"links\": \"http://link-to-advisory\",\n  \"package\": {\n    \"id\": \"0\",\n    \"name\": \"glibc\",\n    \"version\": \"v0.0.1-rc1\"\n  },\n  \"dist\": {\n    \"id\": \"0\",\n    \"did\": \"ubuntu\",\n    \"name\": \"Ubuntu\",\n    \"version\": \"18.04.3 LTS (Bionic Beaver)\",\n    \"version_code_name\": \"bionic\",\n    \"version_id\": \"18.04\"\n  },\n  \"repo\": {\n    \"id\": \"0\",\n    \"name\": \"Ubuntu 18.04.3 LTS\"\n  }\n}\n"
  },
  {
    "path": "httptransport/api/v1/openapi.etag",
    "content": "\"c8f7a50c0be60ee322a40f622e544801f5014effe8ff338ea35b18ed4c07806b\""
  },
  {
    "path": "httptransport/api/v1/openapi.jq",
    "content": "# vim: set expandtab ts=2 sw=2:\ninclude \"oapi\";\n\n# Some helper functions:\ndef example_ref($id): ref(\"examples/\\($id).json\"); # Files are local at build time.\ndef responses($r):\n{\n  \"200\": {\n    description: \"Success\",\n    headers: {\n      \"Clair-Error\": header_ref(\"Clair-Error\"),\n    },\n  },\n  \"400\": response_ref(\"bad_request\"),\n  \"415\": response_ref(\"unsupported_media_type\"),\n  default: response_ref(\"oops\"),\n} * $r\n;\n\n# Some variables:\n\"/notifier/api/v1\" as $path_notif |\n\"/matcher/api/v1\" as $path_match |\n\"/indexer/api/v1\" as $path_index |\n\n# The OpenAPI object:\n{\n  openapi: \"3.1.0\",\n  info: {\n    title: \"Clair Container Analyzer\",\n    description: ([\n      \"Clair is a set of cooperating microservices which can index and match a container image's content with known vulnerabilities.\",\n      \"\",\n      \"**Note:** Any endpoints tagged \\\"internal\\\" are documented for completeness but are considered exempt from versioning.\",\n      \"\"] | join(\"\\n\") | sub(\"[[:space:]]*$\"; \"\")),\n    version: \"1.2.0\",\n    contact: {\n      name: \"Clair Team\",\n      url: \"http://github.com/quay/clair\",\n      email: \"clair-devel@googlegroups.com\",\n    },\n    license: {\n      name: \"Apache License 2.0\",\n      url: \"http://www.apache.org/licenses/\",\n    }\n  },\n  externalDocs: { url: \"https://quay.github.io/clair/\" },\n  tags: ({\n    indexer: \"Indexer service endpoints.\\n\\nThese are responsible for determining the contents of containers.\",\n    matcher: \"Matcher service endpoints.\\n\\nThese are responsible for generating reports against current vulnerability data.\",\n    notifier: \"Matcher service endpoints.\\n\\nThese are responsible for serving notifications.\",\n    internal: \"These are internal endpoints, documented for completeness.\\n\\nThey are exempted from API stability guarentees.\",\n  } | to_entries | map({name: .key, description: .value}) ),\n  paths: {\n    \"\\($path_notif)/notification/{id}\": {\n      parameters: [ {\n        in: \"path\",\n        name: \"id\",\n        required: true,\n        schema: schema_ref(\"token\"),\n        description: \"A notification ID returned by a callback\"\n      } ],\n      delete: {\n        operationId: \"DeleteNotification\",\n        responses: responses({\n          \"200\": { description: \"Delete the notification referenced by the \\\"id\\\" parameter.\" },\n        }),\n      },\n      get: {\n        operationId: \"GetNotification\",\n        parameters: [\n          {\n            in: \"query\",\n            name: \"page_size\",\n            schema: {\n              type: \"integer\",\n              default: 500,\n            },\n            description: \"The maximum number of notifications to deliver in a single page.\"\n          },\n          {\n            in: \"query\",\n            name: \"next\",\n            schema: schema_ref(\"token\"),\n            description: \"The next page to fetch via id. Typically this number is provided on initial response in the \\\"page.next\\\" field. The first request should omit this field.\"\n          }\n        ],\n        responses: responses({\n          \"200\": {\n            description: \"A paginated list of notifications\",\n            content: contenttype(\"notification_page\"),\n          },\n          \"304\": {\n            description: \"Not Modified\",\n          },\n        })\n      }\n    },\n    \"\\($path_index)/index_report\": {\n      post: {\n        operationId: \"Index\",\n        requestBody: {\n          description: \"Manifest to index.\",\n          required: true,\n          content: contenttype(\"manifest\"),\n        },\n        responses: (responses({\n          \"201\": {\n            description: \"IndexReport created.\\n\\nClients may want to avoid reading the body if simply submitting the manifest for later vulnerability reporting.\",\n            content: contenttype(\"index_report\"),\n            headers: {\n              Location: header_ref(\"Location\"),\n              Link: header_ref(\"Link\"),\n            },\n            links: {\n              retrieve: {\n                operationId: \"GetIndexReport\",\n                parameters: {\n                  digest: \"$request.body#/hash\"\n                },\n              },\n              delete: {\n                operationId: \"DeleteManifest\",\n                parameters: {\n                  digest: \"$request.body#/hash\"\n                },\n              },\n              report: {\n                operationId: \"GetVulnerabilityReport\",\n                parameters: {\n                  digest: \"$request.body#/hash\"\n                },\n              },\n            },\n          },\n          \"412\": {\n            description: \"Precondition Failed\",\n          },\n        }) | del(.[\"200\"])),\n      },\n      delete: {\n        operationId: \"DeleteManifests\",\n        requestBody: {\n          description: \"Array of manifest digests to delete.\",\n          required: true,\n          content: contenttype(\"bulk_delete\"),\n        },\n        responses: responses({\n          \"200\": {\n            description: \"Successfully deleted manifests.\",\n            content: contenttype(\"bulk_delete\"),\n          },\n        }),\n      }\n    },\n    \"\\($path_index)/index_report/{digest}\": {\n      delete: {\n        operationId: \"DeleteManifest\",\n        responses: (responses({\"204\": {\n            description: \"Success\",\n        }}) |\n          del(.[\"200\"])),\n      },\n      get: {\n        operationId: \"GetIndexReport\",\n        responses: responses({\n          \"200\": {\n            description: \"IndexReport retrieved\",\n            content: contenttype(\"index_report\"),\n          },\n          \"404\": response_ref(\"not_found\"),\n        }),\n      },\n      parameters: [ param_ref(\"digest\") ],\n    },\n    \"\\($path_index)/internal/affected_manifest\": {\n      post: {\n        operationId: \"AffectedManifests\",\n        requestBody: {\n          description: \"Array of vulnerability summaries to report on.\",\n          required: true,\n          content: contenttype(\"vulnerability_summaries\"),\n        },\n        responses: responses({\n          \"200\": {\n            description: \"The list of manifests and the corresponding vulnerabilities.\",\n            content: contenttype(\"affected_manifests\"),\n          },\n        }),\n      },\n    },\n    \"\\($path_index)/index_state\": {\n      get: {\n        operationId: \"IndexState\",\n        responses: {\n          \"200\": {\n            description: \"Indexer State\",\n            headers: {\n              Etag: header_ref(\"Etag\"),\n            },\n            content: contenttype(\"index_state\"),\n          },\n          \"304\": {\n            description: \"Not Modified\",\n          },\n        }\n      }\n    },\n    \"\\($path_match)/vulnerability_report/{digest}\": {\n      get: {\n        operationId: \"GetVulnerabilityReport\",\n        responses: (responses({\n          \"201\": {\n            description: \"Vulnerability Report Created\",\n            content: contenttype(\"vulnerability_report\"),\n          }\n          ,\n          \"404\": response_ref(\"not_found\"),\n        }) | del(.[\"200\"])),\n      },\n      parameters: [ param_ref(\"digest\") ],\n    },\n    \"\\($path_match)/internal/update_operation\": {\n      get: {\n        operationId: \"GetUpdateOperation\",\n        responses: responses({\n          \"200\": {\n            description: \"Update Operations, keyed by updater.\",\n            content: contenttype(\"update_operations\"),\n          },\n        }),\n        parameters: [\n          {\n            in: \"query\",\n            name: \"kind\",\n            schema: {\n              enum:[\"vulnerability\", \"enrichment\"],\n              default:\"vulnerability\",\n            },\n            description: \"The \\\"kind\\\" of updaters to query.\"\n          },\n          {\n            in: \"query\",\n            name: \"latest\",\n            schema: {\n              type:\"boolean\",\n              default: false\n            },\n            description: \"Return only the latest Update Operations instead of all known Update Operations.\"\n          }\n        ],\n      },\n    },\n    \"\\($path_match)/internal/update_operation/{digest}\": {\n      delete: {\n        operationId: \"DeleteUpdateOperation\",\n        responses: (responses({})),\n      },\n      parameters: [ param_ref(\"digest\") ],\n    },\n    \"\\($path_match)/internal/update_diff\": {\n      get: {\n        operationId: \"GetUpdateDiff\",\n        responses: responses({\n          \"200\": {\n            description: \"Changes between two Update Operations.\",\n            content: contenttype(\"update_diff\"),\n          },\n        }),\n        parameters: [\n          {\n            in: \"query\",\n            name: \"cur\",\n            schema: schema_ref(\"token\"),\n            description: \"\\\"Current\\\" Update Operation ref.\"\n          },\n          {\n            in: \"query\",\n            name: \"prev\",\n            required: true,\n            schema: schema_ref(\"token\"),\n            description: \"\\\"Previous\\\" Update Operation ref.\"\n          }\n        ],\n      },\n    },\n  },\n  security: [\n    {},\n    { PSK: [] }\n  ],\n  webhooks: {\n    notification: {\n      post: {\n        tags: [\"notifier\"],\n        description: \"If configured, Clair will issue webhooks when notifications are available for retrieval.\",\n        requestBody: {\n          content: contenttype(\"notification_webhook\"),\n        },\n        responses: {\n          \"200\": {\n            description: \"OK\",\n          },\n        },\n      },\n    },\n  },\n  components: {\n    schemas: {\n      # Anything here will get overwritten by standalone JSON Schema objects\n      # if the keys are duplicated.\n      #\n      # Generally, anything that goes in a response/request body should have a\n      # schema over in the types directory.\n      token: {\n        type: \"string\",\n        description: \"An opaque token previously obtained from the service.\",\n      },\n    },\n    responses: {\n      bad_request: {\n        description: \"Bad Request\",\n        content: contenttype(\"error\"),\n      },\n      oops: {\n        description: \"Internal Server Error\",\n        content: contenttype(\"error\"),\n      },\n      not_found: {\n        description: \"Not Found\",\n        content: contenttype(\"error\"),\n      },\n      # Not expressible in OpenAPI:\n      #method_not_allowed: {\n      #  description: \"Method Not Allowed\",\n      #  headers: {\n      #    Allow: header_ref(\"Allow\"),\n      #  },\n      #  content: contenttype(\"error\"),\n      #},\n      unsupported_media_type: {\n        description: \"Unsupported Media Type\",\n        content: contenttype(\"error\"),\n      },\n    },\n    parameters: {\n      digest: {\n        description: \"OCI-compatible digest of a referred object.\",\n        name: \"digest\",\n        in: \"path\",\n        schema: schema_ref(\"digest\"),\n        required: true,\n      }\n    },\n    headers: {\n      # Only used for 415 Method Not Allowed responses, which aren't expressible in OpenAPI.\n      #Allow: {\n      #  description: \"TKTK\",\n      #  style: \"simple\",\n      #  schema: { type: \"string\" },\n      #  required: true,\n      #},\n      \"Clair-Error\": {\n        description: \"This is a trailer containing any errors encountered while writing the response.\",\n        style: \"simple\",\n        schema: { type: \"string\" },\n      },\n      Etag: {\n        description: \"HTTP [ETag header](https://httpwg.org/specs/rfc9110.html#field.etag)\",\n        style: \"simple\",\n        schema: { type: \"string\" }\n      },\n      Link: {\n        description: \"Web Linking [Link header](https://httpwg.org/specs/rfc8288.html#header)\",\n        style: \"simple\",\n        schema: { type: \"string\" },\n      },\n      Location: {\n        description: \"HTTP [Location header](https://httpwg.org/specs/rfc9110.html#field.location)\",\n        style: \"simple\",\n        required: true,\n        schema: { type: \"string\" },\n      },\n    },\n    securitySchemes: {\n      PSK: {\n        type: \"http\",\n        scheme: \"bearer\",\n        bearerFormat: \"JWT with preshared key and allow-listed issuers\",\n        description: \"Clair's authentication scheme.\\n\\nThis is a [JWT](https://datatracker.ietf.org/doc/html/rfc7519) signed with a configured pre-shared key containing an allowlisted `iss` claim.\",\n      },\n    },\n  },\n}\n|\n# And now, a bunch of fixups:\ndef add_tags: # Match the path prefixes and add default tags.\n  .paths |= with_entries(\n    (\n      if (.key|contains(\"internal\")) then\n        \"internal\"\n      elif (.key|startswith($path_index)) then\n        \"indexer\"\n      elif (.key|startswith($path_match)) then\n        \"matcher\"\n      elif (.key|startswith($path_notif)) then\n        \"notifier\"\n      else\n        \"\"\n      end\n    ) as $k |\n    if ($k==\"\") then\n      .\n    else\n      (.value[]|select(objects)) |= . + {\n        tags: ((.tags//[]) + [$k]),\n      }\n    end\n  )\n;\ndef operation_metadata: # Slipstream some metadata into response objects.\n  {\n    AffectedManifests: {\n      summary: \"Retrieve Manifests Affected by a Vulnerability\",\n      description: \"The provided vulnerability summaries are attempted to be run \\\"backwards\\\" through the indexer to produce a set of manifests.\",\n    },\n    DeleteManifest: {\n      summary: \"Delete an Indexed Manifest\",\n      description: \"Given a Manifest's content addressable hash, any data related to it will be removed it it exists.\",\n    },\n    DeleteManifests: {\n      summary: \"Delete Indexed Manifests\",\n      description: \"Given a Manifest's content addressable hash, any data related to it will be removed if it exists.\",\n    },\n    DeleteNotification: {\n      summary: \"Delete a Notification Set\",\n      description: \"Issues a delete of the provided notification ID and all associated notifications.\\nAfter this delete clients will no longer be able to retrieve notifications.\",\n    },\n    DeleteUpdateOperation: {\n      summary: \"Delete an Update Operation\",\n      description: \"Issues a delete of the provided Update Operation ID and all associated data.\\nAfter this delete clients will no longer be able to generate a diff against this Update Operation.\",\n    },\n    GetIndexReport: {\n      summary: \"Retrieve the IndexReport for a Manifest\",\n      description: \"Given a Manifest's content addressable hash, an IndexReport will be retrieved if it exists.\",\n    },\n    GetNotification: {\n      summary: \"Retrieve Pages of a Notification Set\",\n      description: \"By performing a GET with an id as a path parameter, the client will retrieve a paginated response of notification objects.\",\n    },\n    GetUpdateDiff: {\n      summary: \"Retrieve Vulnerability Changes Between Two Update Operations\",\n      description: \"Given IDs for two Update Operations, this will return the difference between them. This is used in the notification flow.\",\n    },\n    GetUpdateOperation: {\n      summary: \"Retrieve Update Operations\",\n      description: \"Retrive all known or just the latest Update Operations.\",\n    },\n    GetVulnerabilityReport: {\n      summary: \"Retrieve a VulnerabilityReport for a Manifest\",\n      description: \"Given a Manifest's content addressable hash a VulnerabilityReport will be created. The Manifest **must** have been Indexed first via the Index endpoint.\",\n    },\n    Index: {\n      summary: \"Index a Manifest\",\n      description: \"By submitting a Manifest object to this endpoint Clair will fetch the layers, scan each layer's contents, and provide an index of discovered packages, repository and distribution information.\",\n    },\n    IndexState: {\n      summary: \"Report the Indexer's State\",\n      description: \"The index state endpoint returns a json structure indicating the indexer's internal configuration state.\\nA client may be interested in this as a signal that manifests may need to be re-indexed.\",\n    },\n  } as $m |\n  ( .paths[][] | select(objects) ) |= (\n    .operationId as $id |\n    ($m[$id]?) as $m |\n    if ($m) then\n      . + $m\n    else\n      .\n    end\n  )\n;\n\nsort_paths |\ncontent_defaults |\nadd_tags |\noperation_metadata |\ncli_hints |\n.\n"
  },
  {
    "path": "httptransport/api/v1/openapi.json",
    "content": "{\n  \"openapi\": \"3.1.0\",\n  \"info\": {\n    \"title\": \"Clair Container Analyzer\",\n    \"description\": \"Clair is a set of cooperating microservices which can index and match a container image's content with known vulnerabilities.\\n\\n**Note:** Any endpoints tagged \\\"internal\\\" are documented for completeness but are considered exempt from versioning.\",\n    \"version\": \"1.2.0\",\n    \"contact\": {\n      \"name\": \"Clair Team\",\n      \"url\": \"http://github.com/quay/clair\",\n      \"email\": \"clair-devel@googlegroups.com\"\n    },\n    \"license\": {\n      \"name\": \"Apache License 2.0\",\n      \"url\": \"http://www.apache.org/licenses/\"\n    }\n  },\n  \"externalDocs\": {\n    \"url\": \"https://quay.github.io/clair/\"\n  },\n  \"tags\": [\n    {\n      \"name\": \"indexer\",\n      \"description\": \"Indexer service endpoints.\\n\\nThese are responsible for determining the contents of containers.\"\n    },\n    {\n      \"name\": \"matcher\",\n      \"description\": \"Matcher service endpoints.\\n\\nThese are responsible for generating reports against current vulnerability data.\"\n    },\n    {\n      \"name\": \"notifier\",\n      \"description\": \"Matcher service endpoints.\\n\\nThese are responsible for serving notifications.\"\n    },\n    {\n      \"name\": \"internal\",\n      \"description\": \"These are internal endpoints, documented for completeness.\\n\\nThey are exempted from API stability guarentees.\"\n    }\n  ],\n  \"paths\": {\n    \"/indexer/api/v1/index_report\": {\n      \"post\": {\n        \"operationId\": \"Index\",\n        \"requestBody\": {\n          \"description\": \"Manifest to index.\",\n          \"required\": true,\n          \"content\": {\n            \"application/vnd.clair.manifest.v1+json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/manifest\"\n              }\n            },\n            \"application/json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/manifest\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"400\": {\n            \"$ref\": \"#/components/responses/bad_request\"\n          },\n          \"415\": {\n            \"$ref\": \"#/components/responses/unsupported_media_type\"\n          },\n          \"default\": {\n            \"$ref\": \"#/components/responses/oops\"\n          },\n          \"201\": {\n            \"description\": \"IndexReport created.\\n\\nClients may want to avoid reading the body if simply submitting the manifest for later vulnerability reporting.\",\n            \"content\": {\n              \"application/vnd.clair.index_report.v1+json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/index_report\"\n                }\n              },\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/index_report\"\n                }\n              }\n            },\n            \"headers\": {\n              \"Location\": {\n                \"$ref\": \"#/components/headers/Location\"\n              },\n              \"Link\": {\n                \"$ref\": \"#/components/headers/Link\"\n              }\n            },\n            \"links\": {\n              \"retrieve\": {\n                \"operationId\": \"GetIndexReport\",\n                \"parameters\": {\n                  \"digest\": \"$request.body#/hash\"\n                }\n              },\n              \"delete\": {\n                \"operationId\": \"DeleteManifest\",\n                \"parameters\": {\n                  \"digest\": \"$request.body#/hash\"\n                }\n              },\n              \"report\": {\n                \"operationId\": \"GetVulnerabilityReport\",\n                \"parameters\": {\n                  \"digest\": \"$request.body#/hash\"\n                }\n              }\n            }\n          },\n          \"412\": {\n            \"description\": \"Precondition Failed\"\n          }\n        },\n        \"tags\": [\n          \"indexer\"\n        ],\n        \"summary\": \"Index a Manifest\",\n        \"description\": \"By submitting a Manifest object to this endpoint Clair will fetch the layers, scan each layer's contents, and provide an index of discovered packages, repository and distribution information.\"\n      },\n      \"delete\": {\n        \"operationId\": \"DeleteManifests\",\n        \"requestBody\": {\n          \"description\": \"Array of manifest digests to delete.\",\n          \"required\": true,\n          \"content\": {\n            \"application/vnd.clair.bulk_delete.v1+json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/bulk_delete\"\n              }\n            },\n            \"application/json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/bulk_delete\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successfully deleted manifests.\",\n            \"headers\": {\n              \"Clair-Error\": {\n                \"$ref\": \"#/components/headers/Clair-Error\"\n              }\n            },\n            \"content\": {\n              \"application/vnd.clair.bulk_delete.v1+json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/bulk_delete\"\n                }\n              },\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/bulk_delete\"\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"$ref\": \"#/components/responses/bad_request\"\n          },\n          \"415\": {\n            \"$ref\": \"#/components/responses/unsupported_media_type\"\n          },\n          \"default\": {\n            \"$ref\": \"#/components/responses/oops\"\n          }\n        },\n        \"tags\": [\n          \"indexer\"\n        ],\n        \"summary\": \"Delete Indexed Manifests\",\n        \"description\": \"Given a Manifest's content addressable hash, any data related to it will be removed if it exists.\"\n      }\n    },\n    \"/indexer/api/v1/index_report/{digest}\": {\n      \"delete\": {\n        \"operationId\": \"DeleteManifest\",\n        \"responses\": {\n          \"400\": {\n            \"$ref\": \"#/components/responses/bad_request\"\n          },\n          \"415\": {\n            \"$ref\": \"#/components/responses/unsupported_media_type\"\n          },\n          \"default\": {\n            \"$ref\": \"#/components/responses/oops\"\n          },\n          \"204\": {\n            \"description\": \"Success\"\n          }\n        },\n        \"tags\": [\n          \"indexer\"\n        ],\n        \"summary\": \"Delete an Indexed Manifest\",\n        \"description\": \"Given a Manifest's content addressable hash, any data related to it will be removed it it exists.\"\n      },\n      \"get\": {\n        \"operationId\": \"GetIndexReport\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"IndexReport retrieved\",\n            \"headers\": {\n              \"Clair-Error\": {\n                \"$ref\": \"#/components/headers/Clair-Error\"\n              }\n            },\n            \"content\": {\n              \"application/vnd.clair.index_report.v1+json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/index_report\"\n                }\n              },\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/index_report\"\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"$ref\": \"#/components/responses/bad_request\"\n          },\n          \"415\": {\n            \"$ref\": \"#/components/responses/unsupported_media_type\"\n          },\n          \"default\": {\n            \"$ref\": \"#/components/responses/oops\"\n          },\n          \"404\": {\n            \"$ref\": \"#/components/responses/not_found\"\n          }\n        },\n        \"tags\": [\n          \"indexer\"\n        ],\n        \"summary\": \"Retrieve the IndexReport for a Manifest\",\n        \"description\": \"Given a Manifest's content addressable hash, an IndexReport will be retrieved if it exists.\"\n      },\n      \"parameters\": [\n        {\n          \"$ref\": \"#/components/parameters/digest\"\n        }\n      ]\n    },\n    \"/indexer/api/v1/index_state\": {\n      \"get\": {\n        \"operationId\": \"IndexState\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Indexer State\",\n            \"headers\": {\n              \"Etag\": {\n                \"$ref\": \"#/components/headers/Etag\"\n              }\n            },\n            \"content\": {\n              \"application/vnd.clair.index_state.v1+json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/index_state\"\n                }\n              },\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/index_state\"\n                }\n              }\n            }\n          },\n          \"304\": {\n            \"description\": \"Not Modified\"\n          }\n        },\n        \"tags\": [\n          \"indexer\"\n        ],\n        \"summary\": \"Report the Indexer's State\",\n        \"description\": \"The index state endpoint returns a json structure indicating the indexer's internal configuration state.\\nA client may be interested in this as a signal that manifests may need to be re-indexed.\"\n      }\n    },\n    \"/indexer/api/v1/internal/affected_manifest\": {\n      \"post\": {\n        \"operationId\": \"AffectedManifests\",\n        \"requestBody\": {\n          \"description\": \"Array of vulnerability summaries to report on.\",\n          \"required\": true,\n          \"content\": {\n            \"application/vnd.clair.vulnerability_summaries.v1+json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/vulnerability_summaries\"\n              }\n            },\n            \"application/json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/vulnerability_summaries\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"The list of manifests and the corresponding vulnerabilities.\",\n            \"headers\": {\n              \"Clair-Error\": {\n                \"$ref\": \"#/components/headers/Clair-Error\"\n              }\n            },\n            \"content\": {\n              \"application/vnd.clair.affected_manifests.v1+json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/affected_manifests\"\n                }\n              },\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/affected_manifests\"\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"$ref\": \"#/components/responses/bad_request\"\n          },\n          \"415\": {\n            \"$ref\": \"#/components/responses/unsupported_media_type\"\n          },\n          \"default\": {\n            \"$ref\": \"#/components/responses/oops\"\n          }\n        },\n        \"tags\": [\n          \"internal\"\n        ],\n        \"summary\": \"Retrieve Manifests Affected by a Vulnerability\",\n        \"description\": \"The provided vulnerability summaries are attempted to be run \\\"backwards\\\" through the indexer to produce a set of manifests.\",\n        \"x-cli-ignore\": true\n      }\n    },\n    \"/matcher/api/v1/internal/update_diff\": {\n      \"get\": {\n        \"operationId\": \"GetUpdateDiff\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Changes between two Update Operations.\",\n            \"headers\": {\n              \"Clair-Error\": {\n                \"$ref\": \"#/components/headers/Clair-Error\"\n              }\n            },\n            \"content\": {\n              \"application/vnd.clair.update_diff.v1+json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/update_diff\"\n                }\n              },\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/update_diff\"\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"$ref\": \"#/components/responses/bad_request\"\n          },\n          \"415\": {\n            \"$ref\": \"#/components/responses/unsupported_media_type\"\n          },\n          \"default\": {\n            \"$ref\": \"#/components/responses/oops\"\n          }\n        },\n        \"parameters\": [\n          {\n            \"in\": \"query\",\n            \"name\": \"cur\",\n            \"schema\": {\n              \"$ref\": \"#/components/schemas/token\"\n            },\n            \"description\": \"\\\"Current\\\" Update Operation ref.\"\n          },\n          {\n            \"in\": \"query\",\n            \"name\": \"prev\",\n            \"required\": true,\n            \"schema\": {\n              \"$ref\": \"#/components/schemas/token\"\n            },\n            \"description\": \"\\\"Previous\\\" Update Operation ref.\"\n          }\n        ],\n        \"tags\": [\n          \"internal\"\n        ],\n        \"summary\": \"Retrieve Vulnerability Changes Between Two Update Operations\",\n        \"description\": \"Given IDs for two Update Operations, this will return the difference between them. This is used in the notification flow.\",\n        \"x-cli-ignore\": true\n      }\n    },\n    \"/matcher/api/v1/internal/update_operation\": {\n      \"get\": {\n        \"operationId\": \"GetUpdateOperation\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Update Operations, keyed by updater.\",\n            \"headers\": {\n              \"Clair-Error\": {\n                \"$ref\": \"#/components/headers/Clair-Error\"\n              }\n            },\n            \"content\": {\n              \"application/vnd.clair.update_operations.v1+json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/update_operations\"\n                }\n              },\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/update_operations\"\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"$ref\": \"#/components/responses/bad_request\"\n          },\n          \"415\": {\n            \"$ref\": \"#/components/responses/unsupported_media_type\"\n          },\n          \"default\": {\n            \"$ref\": \"#/components/responses/oops\"\n          }\n        },\n        \"parameters\": [\n          {\n            \"in\": \"query\",\n            \"name\": \"kind\",\n            \"schema\": {\n              \"enum\": [\n                \"vulnerability\",\n                \"enrichment\"\n              ],\n              \"default\": \"vulnerability\"\n            },\n            \"description\": \"The \\\"kind\\\" of updaters to query.\"\n          },\n          {\n            \"in\": \"query\",\n            \"name\": \"latest\",\n            \"schema\": {\n              \"type\": \"boolean\",\n              \"default\": false\n            },\n            \"description\": \"Return only the latest Update Operations instead of all known Update Operations.\"\n          }\n        ],\n        \"tags\": [\n          \"internal\"\n        ],\n        \"summary\": \"Retrieve Update Operations\",\n        \"description\": \"Retrive all known or just the latest Update Operations.\",\n        \"x-cli-ignore\": true\n      }\n    },\n    \"/matcher/api/v1/internal/update_operation/{digest}\": {\n      \"delete\": {\n        \"operationId\": \"DeleteUpdateOperation\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Success\",\n            \"headers\": {\n              \"Clair-Error\": {\n                \"$ref\": \"#/components/headers/Clair-Error\"\n              }\n            }\n          },\n          \"400\": {\n            \"$ref\": \"#/components/responses/bad_request\"\n          },\n          \"415\": {\n            \"$ref\": \"#/components/responses/unsupported_media_type\"\n          },\n          \"default\": {\n            \"$ref\": \"#/components/responses/oops\"\n          }\n        },\n        \"tags\": [\n          \"internal\"\n        ],\n        \"summary\": \"Delete an Update Operation\",\n        \"description\": \"Issues a delete of the provided Update Operation ID and all associated data.\\nAfter this delete clients will no longer be able to generate a diff against this Update Operation.\",\n        \"x-cli-ignore\": true\n      },\n      \"parameters\": [\n        {\n          \"$ref\": \"#/components/parameters/digest\"\n        }\n      ]\n    },\n    \"/matcher/api/v1/vulnerability_report/{digest}\": {\n      \"get\": {\n        \"operationId\": \"GetVulnerabilityReport\",\n        \"responses\": {\n          \"400\": {\n            \"$ref\": \"#/components/responses/bad_request\"\n          },\n          \"415\": {\n            \"$ref\": \"#/components/responses/unsupported_media_type\"\n          },\n          \"default\": {\n            \"$ref\": \"#/components/responses/oops\"\n          },\n          \"201\": {\n            \"description\": \"Vulnerability Report Created\",\n            \"content\": {\n              \"application/vnd.clair.vulnerability_report.v1+json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/vulnerability_report\"\n                }\n              },\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/vulnerability_report\"\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"$ref\": \"#/components/responses/not_found\"\n          }\n        },\n        \"tags\": [\n          \"matcher\"\n        ],\n        \"summary\": \"Retrieve a VulnerabilityReport for a Manifest\",\n        \"description\": \"Given a Manifest's content addressable hash a VulnerabilityReport will be created. The Manifest **must** have been Indexed first via the Index endpoint.\"\n      },\n      \"parameters\": [\n        {\n          \"$ref\": \"#/components/parameters/digest\"\n        }\n      ]\n    },\n    \"/notifier/api/v1/notification/{id}\": {\n      \"parameters\": [\n        {\n          \"in\": \"path\",\n          \"name\": \"id\",\n          \"required\": true,\n          \"schema\": {\n            \"$ref\": \"#/components/schemas/token\"\n          },\n          \"description\": \"A notification ID returned by a callback\"\n        }\n      ],\n      \"delete\": {\n        \"operationId\": \"DeleteNotification\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Delete the notification referenced by the \\\"id\\\" parameter.\",\n            \"headers\": {\n              \"Clair-Error\": {\n                \"$ref\": \"#/components/headers/Clair-Error\"\n              }\n            }\n          },\n          \"400\": {\n            \"$ref\": \"#/components/responses/bad_request\"\n          },\n          \"415\": {\n            \"$ref\": \"#/components/responses/unsupported_media_type\"\n          },\n          \"default\": {\n            \"$ref\": \"#/components/responses/oops\"\n          }\n        },\n        \"tags\": [\n          \"notifier\"\n        ],\n        \"summary\": \"Delete a Notification Set\",\n        \"description\": \"Issues a delete of the provided notification ID and all associated notifications.\\nAfter this delete clients will no longer be able to retrieve notifications.\"\n      },\n      \"get\": {\n        \"operationId\": \"GetNotification\",\n        \"parameters\": [\n          {\n            \"in\": \"query\",\n            \"name\": \"page_size\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"default\": 500\n            },\n            \"description\": \"The maximum number of notifications to deliver in a single page.\"\n          },\n          {\n            \"in\": \"query\",\n            \"name\": \"next\",\n            \"schema\": {\n              \"$ref\": \"#/components/schemas/token\"\n            },\n            \"description\": \"The next page to fetch via id. Typically this number is provided on initial response in the \\\"page.next\\\" field. The first request should omit this field.\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"A paginated list of notifications\",\n            \"headers\": {\n              \"Clair-Error\": {\n                \"$ref\": \"#/components/headers/Clair-Error\"\n              }\n            },\n            \"content\": {\n              \"application/vnd.clair.notification_page.v1+json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/notification_page\"\n                }\n              },\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/notification_page\"\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"$ref\": \"#/components/responses/bad_request\"\n          },\n          \"415\": {\n            \"$ref\": \"#/components/responses/unsupported_media_type\"\n          },\n          \"default\": {\n            \"$ref\": \"#/components/responses/oops\"\n          },\n          \"304\": {\n            \"description\": \"Not Modified\"\n          }\n        },\n        \"tags\": [\n          \"notifier\"\n        ],\n        \"summary\": \"Retrieve Pages of a Notification Set\",\n        \"description\": \"By performing a GET with an id as a path parameter, the client will retrieve a paginated response of notification objects.\"\n      }\n    }\n  },\n  \"security\": [\n    {},\n    {\n      \"PSK\": []\n    }\n  ],\n  \"webhooks\": {\n    \"notification\": {\n      \"post\": {\n        \"tags\": [\n          \"notifier\"\n        ],\n        \"description\": \"If configured, Clair will issue webhooks when notifications are available for retrieval.\",\n        \"requestBody\": {\n          \"content\": {\n            \"application/vnd.clair.notification_webhook.v1+json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/notification_webhook\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"OK\"\n          }\n        }\n      }\n    }\n  },\n  \"components\": {\n    \"schemas\": {\n      \"token\": {\n        \"type\": \"string\",\n        \"description\": \"An opaque token previously obtained from the service.\"\n      },\n      \"affected_manifests\": {\n        \"$id\": \"https://clairproject.org/api/http/v1/affected_manifests.schema.json\",\n        \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n        \"title\": \"Affected Manifests\",\n        \"type\": \"object\",\n        \"description\": \"**This is an internal type, documented for completeness.**\\n\\nManifests affected by the specified vulnerability objects.\",\n        \"properties\": {\n          \"vulnerabilities\": {\n            \"type\": \"object\",\n            \"description\": \"Vulnerability objects.\",\n            \"additionalProperties\": {\n              \"$ref\": \"vulnerability.schema.json\"\n            }\n          },\n          \"vulnerable_manifests\": {\n            \"type\": \"object\",\n            \"description\": \"Mapping of manifest digests to vulnerability identifiers.\",\n            \"additionalProperties\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\",\n                \"description\": \"An identifier to be used in the \\\"vulnerabilities\\\" object.\"\n              }\n            }\n          }\n        },\n        \"required\": [\n          \"vulnerabilities\",\n          \"vulnerable_manifests\"\n        ],\n        \"examples\": [\n          {\n            \"vulnerabilities\": {\n              \"42\": {\n                \"id\": \"42\"\n              }\n            },\n            \"vulnerable_manifests\": {\n              \"sha256:01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b\": [\n                \"42\"\n              ]\n            }\n          }\n        ]\n      },\n      \"bulk_delete\": {\n        \"$id\": \"https://clairproject.org/api/http/v1/bulk_delete.schema.json\",\n        \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n        \"title\": \"Bulk Delete\",\n        \"type\": \"array\",\n        \"description\": \"Array of manifest digests to delete from the system.\",\n        \"items\": {\n          \"$ref\": \"digest.schema.json\",\n          \"description\": \"Manifest digest to delete from the system.\"\n        },\n        \"examples\": [\n          [\n            \"sha256:fc84b5febd328eccaa913807716887b3eb5ed08bc22cc6933a9ebf82766725e3\"\n          ]\n        ]\n      },\n      \"cpe\": {\n        \"$id\": \"https://clairproject.org/api/http/v1/cpe.schema.json\",\n        \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n        \"title\": \"Common Platform Enumeration Name\",\n        \"description\": \"This is a CPE Name in either v2.2 \\\"URI\\\" form or v2.3 \\\"Formatted String\\\" form.\",\n        \"$comment\": \"Clair only produces v2.3 CPE Names. Any v2.2 Names will be normalized into v2.3 form.\",\n        \"oneOf\": [\n          {\n            \"description\": \"This is the CPE 2.2 regexp: https://cpe.mitre.org/specification/2.2/cpe-language_2.2.xsd\",\n            \"type\": \"string\",\n            \"pattern\": \"^[c][pP][eE]:/[AHOaho]?(:[A-Za-z0-9\\\\._\\\\-~%]*){0,6}$\"\n          },\n          {\n            \"description\": \"This is the CPE 2.3 regexp: https://csrc.nist.gov/schema/cpe/2.3/cpe-naming_2.3.xsd\",\n            \"type\": \"string\",\n            \"pattern\": \"^cpe:2\\\\.3:[aho\\\\*\\\\-](:(((\\\\?*|\\\\*?)([a-zA-Z0-9\\\\-\\\\._]|(\\\\\\\\[\\\\\\\\\\\\*\\\\?!\\\"#$$%&'\\\\(\\\\)\\\\+,/:;<=>@\\\\[\\\\]\\\\^`\\\\{\\\\|}~]))+(\\\\?*|\\\\*?))|[\\\\*\\\\-])){5}(:(([a-zA-Z]{2,3}(-([a-zA-Z]{2}|[0-9]{3}))?)|[\\\\*\\\\-]))(:(((\\\\?*|\\\\*?)([a-zA-Z0-9\\\\-\\\\._]|(\\\\\\\\[\\\\\\\\\\\\*\\\\?!\\\"#$$%&'\\\\(\\\\)\\\\+,/:;<=>@\\\\[\\\\]\\\\^`\\\\{\\\\|}~]))+(\\\\?*|\\\\*?))|[\\\\*\\\\-])){4}$\"\n          }\n        ],\n        \"examples\": [\n          \"cpe:/a:microsoft:internet_explorer:8.0.6001:beta\",\n          \"cpe:2.3:a:microsoft:internet_explorer:8.0.6001:beta:*:*:*:*:*:*\"\n        ]\n      },\n      \"digest\": {\n        \"$id\": \"https://clairproject.org/api/http/v1/digest.schema.json\",\n        \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n        \"title\": \"Digest\",\n        \"description\": \"A digest acts as a content identifier, enabling content addressability.\",\n        \"type\": \"string\",\n        \"anyOf\": [\n          {\n            \"$comment\": \"SHA256: MUST be implemented\",\n            \"description\": \"SHA256\",\n            \"type\": \"string\",\n            \"pattern\": \"^sha256:[a-f0-9]{64}$\"\n          },\n          {\n            \"$comment\": \"SHA512: MAY be implemented\",\n            \"description\": \"SHA512\",\n            \"type\": \"string\",\n            \"pattern\": \"^sha512:[a-f0-9]{128}$\"\n          },\n          {\n            \"$comment\": \"BLAKE3: MAY be implemented\",\n            \"description\": \"BLAKE3\\n\\n**Currently not implemented.**\",\n            \"type\": \"string\",\n            \"pattern\": \"^blake3:[a-f0-9]{64}$\"\n          }\n        ],\n        \"examples\": [\n          \"sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a\",\n          \"sha512:27c74670adb75075fad058d5ceaf7b20c4e7786c83bae8a32f626f9782af34c9a33c2046ef60fd2a7878d378e29fec851806bbd9a67878f3a9f1cda4830763fd\",\n          \"blake3:6e46dd10defc9b56c29a6ec56b508c21f54c08192194e4df25bf36f0c9c3c279\"\n        ]\n      },\n      \"distribution\": {\n        \"$id\": \"https://clairproject.org/api/http/v1/distribution.schema.json\",\n        \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n        \"title\": \"Distribution\",\n        \"type\": \"object\",\n        \"description\": \"Distribution is the accompanying system context of a Package.\",\n        \"properties\": {\n          \"id\": {\n            \"description\": \"Unique ID for this Distribution. May be unique to the response document, not the whole system.\",\n            \"type\": \"string\"\n          },\n          \"did\": {\n            \"description\": \"A lower-case string (no spaces or other characters outside of 0–9, a–z, \\\".\\\", \\\"_\\\", and \\\"-\\\") identifying the operating system, excluding any version information and suitable for processing by scripts or usage in generated filenames.\",\n            \"type\": \"string\"\n          },\n          \"name\": {\n            \"description\": \"A string identifying the operating system.\",\n            \"type\": \"string\"\n          },\n          \"version\": {\n            \"description\": \"A string identifying the operating system version, excluding any OS name information, possibly including a release code name, and suitable for presentation to the user.\",\n            \"type\": \"string\"\n          },\n          \"version_code_name\": {\n            \"description\": \"A lower-case string (no spaces or other characters outside of 0–9, a–z, \\\".\\\", \\\"_\\\", and \\\"-\\\") identifying the operating system release code name, excluding any OS name information or release version, and suitable for processing by scripts or usage in generated filenames.\",\n            \"type\": \"string\"\n          },\n          \"version_id\": {\n            \"description\": \"A lower-case string (mostly numeric, no spaces or other characters outside of 0–9, a–z, \\\".\\\", \\\"_\\\", and \\\"-\\\") identifying the operating system version, excluding any OS name information or release code name.\",\n            \"type\": \"string\"\n          },\n          \"arch\": {\n            \"description\": \"A string identifying the OS architecture.\",\n            \"type\": \"string\"\n          },\n          \"cpe\": {\n            \"description\": \"Common Platform Enumeration name.\",\n            \"$ref\": \"cpe.schema.json\"\n          },\n          \"pretty_name\": {\n            \"description\": \"A pretty operating system name in a format suitable for presentation to the user.\",\n            \"type\": \"string\"\n          }\n        },\n        \"additionalProperties\": false,\n        \"required\": [\n          \"id\"\n        ],\n        \"examples\": [\n          {\n            \"id\": \"1\",\n            \"did\": \"ubuntu\",\n            \"name\": \"Ubuntu\",\n            \"version\": \"18.04.3 LTS (Bionic Beaver)\",\n            \"version_code_name\": \"bionic\",\n            \"version_id\": \"18.04\",\n            \"pretty_name\": \"Ubuntu 18.04.3 LTS\"\n          }\n        ]\n      },\n      \"environment\": {\n        \"$id\": \"https://clairproject.org/api/http/v1/environment.schema.json\",\n        \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n        \"title\": \"Environment\",\n        \"type\": \"object\",\n        \"description\": \"Environment describes the surrounding environment a package was discovered in.\",\n        \"properties\": {\n          \"package_db\": {\n            \"description\": \"The database the associated Package was discovered in.\",\n            \"type\": \"string\"\n          },\n          \"distribution_id\": {\n            \"description\": \"The ID of the Distribution of the associated Package.\",\n            \"type\": \"string\"\n          },\n          \"introduced_in\": {\n            \"description\": \"The Layer the associated Package was introduced in.\",\n            \"$ref\": \"digest.schema.json\"\n          },\n          \"repository_ids\": {\n            \"description\": \"The IDs of the Repositories of the associated Package.\",\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"string\"\n            }\n          }\n        },\n        \"additionalProperties\": false,\n        \"examples\": [\n          {\n            \"value\": {\n              \"package_db\": \"var/lib/dpkg/status\",\n              \"introduced_in\": \"sha256:35c102085707f703de2d9eaad8752d6fe1b8f02b5d2149f1d8357c9cc7fb7d0a\",\n              \"distribution_id\": \"1\"\n            }\n          }\n        ]\n      },\n      \"error\": {\n        \"$id\": \"https://clairproject.org/api/http/v1/error.schema.json\",\n        \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n        \"title\": \"Error\",\n        \"type\": \"object\",\n        \"description\": \"A general error response.\",\n        \"properties\": {\n          \"code\": {\n            \"type\": \"string\",\n            \"description\": \"a code for this particular error\"\n          },\n          \"message\": {\n            \"type\": \"string\",\n            \"description\": \"a message with further detail\"\n          }\n        },\n        \"required\": [\n          \"message\"\n        ]\n      },\n      \"index_report\": {\n        \"$id\": \"https://clairproject.org/api/http/v1/index_report.schema.json\",\n        \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n        \"title\": \"Index Report\",\n        \"type\": \"object\",\n        \"description\": \"An index of the contents of a Manifest.\",\n        \"properties\": {\n          \"manifest_hash\": {\n            \"$ref\": \"digest.schema.json\",\n            \"description\": \"The Manifest's digest.\"\n          },\n          \"state\": {\n            \"type\": \"string\",\n            \"description\": \"The current state of the index operation\"\n          },\n          \"err\": {\n            \"type\": \"string\",\n            \"description\": \"An error message on event of unsuccessful index\"\n          },\n          \"success\": {\n            \"type\": \"boolean\",\n            \"description\": \"A bool indicating succcessful index\"\n          },\n          \"packages\": {\n            \"type\": \"object\",\n            \"description\": \"A map of Package objects indexed by a document-local identifier.\",\n            \"additionalProperties\": {\n              \"$ref\": \"package.schema.json\"\n            }\n          },\n          \"distributions\": {\n            \"type\": \"object\",\n            \"description\": \"A map of Distribution objects indexed by a document-local identifier.\",\n            \"additionalProperties\": {\n              \"$ref\": \"distribution.schema.json\"\n            }\n          },\n          \"repository\": {\n            \"type\": \"object\",\n            \"description\": \"A map of Repository objects indexed by a document-local identifier.\",\n            \"additionalProperties\": {\n              \"$ref\": \"repository.schema.json\"\n            }\n          },\n          \"environments\": {\n            \"type\": \"object\",\n            \"description\": \"A map of Environment arrays indexed by a Package's identifier.\",\n            \"additionalProperties\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"$ref\": \"environment.schema.json\"\n              }\n            }\n          }\n        },\n        \"additionalProperties\": false,\n        \"required\": [\n          \"manifest_hash\",\n          \"state\",\n          \"success\"\n        ]\n      },\n      \"index_state\": {\n        \"$id\": \"https://clairproject.org/api/http/v1/index_state.schema.json\",\n        \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n        \"title\": \"Index State\",\n        \"type\": \"object\",\n        \"description\": \"Information on the state of the indexer system.\",\n        \"properties\": {\n          \"state\": {\n            \"type\": \"string\",\n            \"description\": \"an opaque token\"\n          }\n        },\n        \"required\": [\n          \"state\"\n        ]\n      },\n      \"layer\": {\n        \"$id\": \"https://clairproject.org/api/http/v1/layer.schema.json\",\n        \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n        \"title\": \"Layer\",\n        \"type\": \"object\",\n        \"description\": \"Layer is a description of a container layer. It should contain enough information to fetch the layer.\",\n        \"properties\": {\n          \"hash\": {\n            \"$ref\": \"digest.schema.json\",\n            \"description\": \"Digest of the layer blob.\"\n          },\n          \"uri\": {\n            \"type\": \"string\",\n            \"description\": \"A URI indicating where the layer blob can be downloaded from.\"\n          },\n          \"headers\": {\n            \"description\": \"Any additional HTTP-style headers needed for requesting layers.\",\n            \"type\": \"object\",\n            \"patternProperties\": {\n              \"^[a-zA-Z0-9\\\\-_]+$\": {\n                \"type\": \"array\",\n                \"items\": {\n                  \"type\": \"string\"\n                }\n              }\n            }\n          },\n          \"media_type\": {\n            \"description\": \"The OCI Layer media type for this layer.\",\n            \"type\": \"string\",\n            \"pattern\": \"^application/vnd\\\\.oci\\\\.image\\\\.layer\\\\.v1\\\\.tar(\\\\+(gzip|zstd))?$\"\n          }\n        },\n        \"additionalProperties\": false,\n        \"required\": [\n          \"hash\",\n          \"uri\"\n        ]\n      },\n      \"manifest\": {\n        \"$id\": \"https://clairproject.org/api/http/v1/manifest.schema.json\",\n        \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n        \"title\": \"Manifest\",\n        \"type\": \"object\",\n        \"description\": \"A description of an OCI Image Manifest.\",\n        \"properties\": {\n          \"hash\": {\n            \"$ref\": \"digest.schema.json\",\n            \"description\": \"The OCI Image Manifest's digest.\\n\\nThis is used as an identifier throughout the system. This **SHOULD** be the same as the OCI Image Manifest's digest, but this is not enforced.\"\n          },\n          \"layers\": {\n            \"type\": \"array\",\n            \"description\": \"The OCI Layers making up the Image, in order.\",\n            \"items\": {\n              \"$ref\": \"layer.schema.json\"\n            }\n          }\n        },\n        \"additionalProperties\": false,\n        \"required\": [\n          \"hash\"\n        ],\n        \"examples\": [\n          {\n            \"hash\": \"sha256:fc84b5febd328eccaa913807716887b3eb5ed08bc22cc6933a9ebf82766725e3\",\n            \"layers\": [\n              {\n                \"hash\": \"sha256:2f077db56abccc19f16f140f629ae98e904b4b7d563957a7fc319bd11b82ba36\",\n                \"uri\": \"https://storage.example.com/blob/2f077db56abccc19f16f140f629ae98e904b4b7d563957a7fc319bd11b82ba36\",\n                \"headers\": {\n                  \"Authoriztion\": [\n                    \"Bearer hunter2\"\n                  ]\n                }\n              }\n            ]\n          }\n        ]\n      },\n      \"normalized_severity\": {\n        \"$id\": \"https://clairproject.org/api/http/v1/normalized_severity.schema.json\",\n        \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n        \"title\": \"Normalized Severity\",\n        \"description\": \"Standardized severity values.\",\n        \"enum\": [\n          \"Unknown\",\n          \"Negligible\",\n          \"Low\",\n          \"Medium\",\n          \"High\",\n          \"Critical\"\n        ]\n      },\n      \"notification_page\": {\n        \"$id\": \"https://clairproject.org/api/http/v1/notification_page.schema.json\",\n        \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n        \"title\": \"Notification Page\",\n        \"type\": \"object\",\n        \"description\": \"A page description and list of notifications.\",\n        \"properties\": {\n          \"page\": {\n            \"description\": \"An object informing the client the next page to retrieve.\",\n            \"type\": \"object\",\n            \"properties\": {\n              \"size\": {\n                \"description\": \"The number of notifications contained in this page.\",\n                \"type\": \"integer\"\n              },\n              \"next\": {\n                \"description\": \"The identififer to pass into the \\\"next\\\" parameter of a future GetNotification request.\\n\\nIf not present, there are no additional pages.\",\n                \"type\": \"string\"\n              }\n            },\n            \"additionalProperties\": false,\n            \"required\": [\n              \"size\"\n            ]\n          },\n          \"notifications\": {\n            \"description\": \"Notifications within this page.\",\n            \"type\": \"array\",\n            \"items\": {\n              \"$ref\": \"notification.schema.json\"\n            }\n          }\n        },\n        \"additionalProperties\": false,\n        \"required\": [\n          \"page\",\n          \"notifications\"\n        ],\n        \"examples\": [\n          {\n            \"page\": {\n              \"size\": 100,\n              \"next\": \"1b4d0db2-e757-4150-bbbb-543658144205\"\n            },\n            \"notifications\": [\n              {\n                \"id\": \"5e4b387e-88d3-4364-86fd-063447a6fad2\",\n                \"manifest\": \"sha256:35c102085707f703de2d9eaad8752d6fe1b8f02b5d2149f1d8357c9cc7fb7d0a\",\n                \"reason\": \"added\",\n                \"vulnerability\": {\n                  \"name\": \"CVE-2009-5155\",\n                  \"fixed_in_version\": \"v0.0.1\",\n                  \"links\": \"http://example.com/CVE-2009-5155\",\n                  \"description\": \"In the GNU C Library (aka glibc or libc6) before 2.28, parse_reg_exp in posix/regcomp.c misparses alternatives, which allows attackers to cause a denial of service (assertion failure and application exit) or trigger an incorrect result by attempting a regular-expression match.\\\"\",\n                  \"normalized_severity\": \"Unknown\",\n                  \"package\": {\n                    \"id\": \"10\",\n                    \"name\": \"libapt-pkg5.0\",\n                    \"version\": \"1.6.11\",\n                    \"kind\": \"BINARY\",\n                    \"arch\": \"x86\",\n                    \"source\": {\n                      \"id\": \"9\",\n                      \"name\": \"apt\",\n                      \"version\": \"1.6.11\",\n                      \"kind\": \"SOURCE\",\n                      \"source\": null\n                    }\n                  },\n                  \"distribution\": {\n                    \"id\": \"1\",\n                    \"did\": \"ubuntu\",\n                    \"name\": \"Ubuntu\",\n                    \"version\": \"18.04.3 LTS (Bionic Beaver)\",\n                    \"version_code_name\": \"bionic\",\n                    \"version_id\": \"18.04\",\n                    \"pretty_name\": \"Ubuntu 18.04.3 LTS\"\n                  }\n                }\n              }\n            ]\n          }\n        ]\n      },\n      \"notification\": {\n        \"$id\": \"https://clairproject.org/api/http/v1/notification.schema.json\",\n        \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n        \"title\": \"Notification\",\n        \"type\": \"object\",\n        \"description\": \"A change in a manifest affected by a vulnerability.\",\n        \"properties\": {\n          \"id\": {\n            \"description\": \"Unique identifier for this notification.\",\n            \"type\": \"string\"\n          },\n          \"manifest\": {\n            \"$ref\": \"digest.schema.json\",\n            \"description\": \"The digest of the manifest affected by the provided vulnerability.\"\n          },\n          \"reason\": {\n            \"description\": \"The reason for the notifcation.\",\n            \"enum\": [\n              \"added\",\n              \"removed\"\n            ]\n          },\n          \"vulnerability\": {\n            \"$ref\": \"vulnerability_summary.schema.json\"\n          }\n        },\n        \"additionalProperties\": false,\n        \"required\": [\n          \"id\",\n          \"manifest\",\n          \"reason\",\n          \"vulnerability\"\n        ]\n      },\n      \"notification_webhook\": {\n        \"$id\": \"https://clairproject.org/api/http/v1/notification_webhook.schema.json\",\n        \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n        \"title\": \"Notification Webhook\",\n        \"type\": \"object\",\n        \"description\": \"Webhook sent to a configured service to begin retrieving notifications.\",\n        \"properties\": {\n          \"notification_id\": {\n            \"description\": \"Unique identifier for this notification.\",\n            \"type\": \"string\"\n          },\n          \"callback\": {\n            \"description\": \"A URL to retrieve paginated Notification objects.\",\n            \"type\": \"string\",\n            \"format\": \"uri\"\n          }\n        },\n        \"additionalProperties\": false,\n        \"required\": [\n          \"notification_id\",\n          \"callback\"\n        ]\n      },\n      \"package\": {\n        \"$id\": \"https://clairproject.org/api/http/v1/package.schema.json\",\n        \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n        \"title\": \"Package\",\n        \"type\": \"object\",\n        \"description\": \"Description of installed software.\",\n        \"properties\": {\n          \"id\": {\n            \"description\": \"Unique ID for this Package. May be unique to the response document, not the whole system.\",\n            \"type\": \"string\"\n          },\n          \"name\": {\n            \"description\": \"Identifier of this Package.\\n\\nThe uniqueness and scoping of this name depends on the packaging system.\",\n            \"type\": \"string\"\n          },\n          \"version\": {\n            \"description\": \"Version of this Package, as reported by the packaging system.\",\n            \"type\": \"string\"\n          },\n          \"kind\": {\n            \"description\": \"The \\\"kind\\\" of this Package.\",\n            \"enum\": [\n              \"BINARY\",\n              \"SOURCE\"\n            ],\n            \"default\": \"BINARY\"\n          },\n          \"source\": {\n            \"$ref\": \"#\",\n            \"description\": \"Source Package that produced the current binary Package, if known.\"\n          },\n          \"normalized_version\": {\n            \"description\": \"Normalized representation of the discoverd version.\\n\\nThe format is not specific, but is guarenteed to be forward compatible.\",\n            \"type\": \"string\"\n          },\n          \"module\": {\n            \"description\": \"An identifier for intra-Repository grouping of packages.\\n\\nLikely only relevant on rpm-based systems.\",\n            \"type\": \"string\"\n          },\n          \"arch\": {\n            \"description\": \"Native architecture for the Package.\",\n            \"type\": \"string\",\n            \"$comment\": \"This should become and enum in the future.\"\n          },\n          \"cpe\": {\n            \"$ref\": \"cpe.schema.json\",\n            \"description\": \"CPE Name for the Package.\"\n          }\n        },\n        \"additionalProperties\": false,\n        \"required\": [\n          \"name\",\n          \"version\"\n        ],\n        \"examples\": [\n          {\n            \"id\": \"10\",\n            \"name\": \"libapt-pkg5.0\",\n            \"version\": \"1.6.11\",\n            \"kind\": \"binary\",\n            \"normalized_version\": \"\",\n            \"arch\": \"x86\",\n            \"module\": \"\",\n            \"cpe\": \"\",\n            \"source\": {\n              \"id\": \"9\",\n              \"name\": \"apt\",\n              \"version\": \"1.6.11\",\n              \"kind\": \"source\",\n              \"source\": null\n            }\n          }\n        ]\n      },\n      \"range\": {\n        \"$id\": \"https://clairproject.org/api/http/v1/range.schema.json\",\n        \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n        \"title\": \"Range\",\n        \"type\": \"object\",\n        \"description\": \"A range of versions.\",\n        \"properties\": {\n          \"[\": {\n            \"type\": \"string\",\n            \"description\": \"Lower bound, inclusive.\"\n          },\n          \")\": {\n            \"type\": \"string\",\n            \"description\": \"Upper bound, exclusive.\"\n          }\n        },\n        \"minProperties\": 1,\n        \"additionalProperties\": false\n      },\n      \"repository\": {\n        \"$id\": \"https://clairproject.org/api/http/v1/repository.schema.json\",\n        \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n        \"title\": \"Repository\",\n        \"type\": \"object\",\n        \"description\": \"Description of a software repository\",\n        \"properties\": {\n          \"id\": {\n            \"description\": \"Unique ID for this Repository. May be unique to the response document, not the whole system.\",\n            \"type\": \"string\"\n          },\n          \"name\": {\n            \"description\": \"Human-relevant name for the Repository.\",\n            \"type\": \"string\"\n          },\n          \"key\": {\n            \"description\": \"Machine-relevant name for the Repository.\",\n            \"type\": \"string\"\n          },\n          \"uri\": {\n            \"description\": \"URI describing the Repository.\",\n            \"type\": \"string\",\n            \"format\": \"uri\"\n          },\n          \"cpe\": {\n            \"description\": \"CPE name for the Repository.\",\n            \"$ref\": \"cpe.schema.json\"\n          }\n        },\n        \"additionalProperties\": false,\n        \"required\": [\n          \"id\"\n        ]\n      },\n      \"update_diff\": {\n        \"$id\": \"https://clairproject.org/api/http/v1/update_diff.schema.json\",\n        \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n        \"title\": \"Update Difference\",\n        \"type\": \"object\",\n        \"description\": \"**This is an internal type, documented for completeness.**\\n\\nAn update difference describes changes between two Update Operations.\",\n        \"properties\": {\n          \"prev\": {\n            \"description\": \"The previous Update Operation.\",\n            \"$ref\": \"update_operation.schema.json\"\n          },\n          \"cur\": {\n            \"description\": \"The current Update Operation.\",\n            \"$ref\": \"update_operation.schema.json\"\n          },\n          \"added\": {\n            \"description\": \"Vulnerabilities present in \\\"cur\\\", but not \\\"prev\\\".\",\n            \"type\": \"array\",\n            \"items\": {\n              \"$ref\": \"vulnerability.schema.json\"\n            }\n          },\n          \"removed\": {\n            \"description\": \"Vulnerabilities present in \\\"prev\\\", but not \\\"cur\\\".\",\n            \"type\": \"array\",\n            \"items\": {\n              \"$ref\": \"vulnerability.schema.json\"\n            }\n          }\n        },\n        \"additionalProperties\": false,\n        \"required\": [\n          \"cur\",\n          \"added\",\n          \"removed\"\n        ]\n      },\n      \"update_operation\": {\n        \"$id\": \"https://clairproject.org/api/http/v1/update_operation.schema.json\",\n        \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n        \"title\": \"Update Operation\",\n        \"type\": \"object\",\n        \"description\": \"**This is an internal type, documented for completeness.**\\n\\nAn update operations describes an update of the internal vulnerability database.\",\n        \"properties\": {\n          \"ref\": {\n            \"type\": \"string\",\n            \"description\": \"A unique identifier for this update operation.\",\n            \"format\": \"uuid\"\n          },\n          \"updater\": {\n            \"description\": \"The \\\"updater\\\" component that was run.\",\n            \"$comment\": \"This is not as useful as it could be: an end user needs to know too much about Clair(core)'s internals to make sense of it.\",\n            \"type\": \"string\"\n          },\n          \"fingerprint\": {\n            \"description\": \"The stored \\\"fingerprint\\\" of this run.\",\n            \"type\": \"string\"\n          },\n          \"date\": {\n            \"type\": \"string\",\n            \"description\": \"When this operation was run.\",\n            \"format\": \"date-time\"\n          },\n          \"kind\": {\n            \"description\": \"The kind of data this operation updated.\",\n            \"enum\": [\n              \"vulnerability\",\n              \"enrichment\"\n            ]\n          }\n        },\n        \"additionalProperties\": false,\n        \"required\": [\n          \"ref\",\n          \"updater\",\n          \"fingerprint\",\n          \"date\",\n          \"kind\"\n        ]\n      },\n      \"update_operations\": {\n        \"$id\": \"https://clairproject.org/api/http/v1/update_operations.schema.json\",\n        \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n        \"title\": \"Update Operations\",\n        \"type\": \"object\",\n        \"description\": \"**This is an internal type, documented for completeness.**\\n\\nA mapping of updater id to Update Operation(s).\",\n        \"additionalProperties\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"$ref\": \"update_operation.schema.json\"\n          }\n        }\n      },\n      \"vulnerability_core\": {\n        \"$id\": \"https://clairproject.org/api/http/v1/vulnerability_core.schema.json\",\n        \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n        \"title\": \"Vulnerability Core\",\n        \"type\": \"object\",\n        \"description\": \"The core elements of vulnerabilities in the Clair system.\",\n        \"properties\": {\n          \"name\": {\n            \"type\": \"string\",\n            \"description\": \"Human-readable name, as presented in the vendor data.\"\n          },\n          \"fixed_in_version\": {\n            \"type\": \"string\",\n            \"description\": \"Version string, as presented in the vendor data.\"\n          },\n          \"severity\": {\n            \"type\": \"string\",\n            \"description\": \"Severity, as presented in the vendor data.\"\n          },\n          \"normalized_severity\": {\n            \"$ref\": \"normalized_severity.schema.json\",\n            \"description\": \"A well defined set of severity strings guaranteed to be present.\"\n          },\n          \"range\": {\n            \"$ref\": \"range.schema.json\",\n            \"description\": \"Range of versions the vulnerability applies to.\"\n          },\n          \"arch_op\": {\n            \"description\": \"Flag indicating how the referenced package's \\\"arch\\\" member should be interpreted.\",\n            \"enum\": [\n              \"equals\",\n              \"not equals\",\n              \"pattern match\"\n            ]\n          },\n          \"package\": {\n            \"$ref\": \"package.schema.json\",\n            \"description\": \"A package description\"\n          },\n          \"distribution\": {\n            \"$ref\": \"distribution.schema.json\",\n            \"description\": \"A distribution description\"\n          },\n          \"repository\": {\n            \"$ref\": \"repository.schema.json\",\n            \"description\": \"A repository description\"\n          }\n        },\n        \"required\": [\n          \"name\",\n          \"normalized_severity\"\n        ],\n        \"dependentRequired\": {\n          \"package\": [\n            \"arch_op\"\n          ]\n        },\n        \"anyOf\": [\n          {\n            \"required\": [\n              \"package\"\n            ]\n          },\n          {\n            \"required\": [\n              \"repository\"\n            ]\n          },\n          {\n            \"required\": [\n              \"distribution\"\n            ]\n          }\n        ]\n      },\n      \"vulnerability_report\": {\n        \"$id\": \"https://clairproject.org/api/http/v1/vulnerability_report.schema.json\",\n        \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n        \"title\": \"Vulnerability Report\",\n        \"type\": \"object\",\n        \"description\": \"A report with discovered packages, package environments, and package vulnerabilities within a Manifest.\",\n        \"properties\": {\n          \"manifest_hash\": {\n            \"$ref\": \"digest.schema.json\",\n            \"description\": \"The Manifest's digest.\"\n          },\n          \"packages\": {\n            \"type\": \"object\",\n            \"description\": \"A map of Package objects indexed by a document-local identifier.\",\n            \"additionalProperties\": {\n              \"$ref\": \"package.schema.json\"\n            }\n          },\n          \"distributions\": {\n            \"type\": \"object\",\n            \"description\": \"A map of Distribution objects indexed by a document-local identifier.\",\n            \"additionalProperties\": {\n              \"$ref\": \"distribution.schema.json\"\n            }\n          },\n          \"repository\": {\n            \"type\": \"object\",\n            \"description\": \"A map of Repository objects indexed by a document-local identifier.\",\n            \"additionalProperties\": {\n              \"$ref\": \"repository.schema.json\"\n            }\n          },\n          \"environments\": {\n            \"type\": \"object\",\n            \"description\": \"A map of Environment arrays indexed by a Package's identifier.\",\n            \"additionalProperties\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"$ref\": \"environment.schema.json\"\n              }\n            }\n          },\n          \"vulnerabilities\": {\n            \"type\": \"object\",\n            \"description\": \"A map of Vulnerabilities indexed by a document-local identifier.\",\n            \"additionalProperties\": {\n              \"$ref\": \"vulnerability.schema.json\"\n            }\n          },\n          \"package_vulnerabilities\": {\n            \"type\": \"object\",\n            \"description\": \"A mapping of Vulnerability identifier lists indexed by Package identifier.\",\n            \"additionalProperties\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          \"enrichments\": {\n            \"type\": \"object\",\n            \"description\": \"A mapping of extra \\\"enrichment\\\" data by type\",\n            \"additionalProperties\": {\n              \"type\": \"array\"\n            }\n          }\n        },\n        \"additionalProperties\": false,\n        \"required\": [\n          \"distributions\",\n          \"environments\",\n          \"manifest_hash\",\n          \"packages\",\n          \"package_vulnerabilities\",\n          \"vulnerabilities\"\n        ]\n      },\n      \"vulnerability\": {\n        \"$id\": \"https://clairproject.org/api/http/v1/vulnerability.schema.json\",\n        \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n        \"title\": \"Vulnerability\",\n        \"type\": \"object\",\n        \"description\": \"Description of a software flaw.\",\n        \"$ref\": \"vulnerability_core.schema.json\",\n        \"properties\": {\n          \"id\": {\n            \"description\": \"Unique ID for this Vulnerabiltity. May be unique to the response document, not the whole system.\",\n            \"type\": \"string\"\n          },\n          \"updater\": {\n            \"description\": \"The updater component this Vulnerability came from.\",\n            \"type\": \"string\"\n          },\n          \"description\": {\n            \"description\": \"A human-readable description of the vulnerability.\",\n            \"type\": \"string\"\n          },\n          \"issued\": {\n            \"description\": \"The datetime this Vulnerability was issued, if known.\",\n            \"type\": \"string\",\n            \"format\": \"date-time\"\n          },\n          \"links\": {\n            \"description\": \"Space-separated URIs to more information.\",\n            \"type\": \"string\"\n          }\n        },\n        \"unevaluatedProperties\": false,\n        \"required\": [\n          \"id\",\n          \"updater\"\n        ],\n        \"examples\": [\n          {\n            \"id\": \"356835\",\n            \"updater\": \"ubuntu\",\n            \"name\": \"CVE-2009-5155\",\n            \"description\": \"In the GNU C Library (aka glibc or libc6) before 2.28, parse_reg_exp in posix/regcomp.c misparses alternatives, which allows attackers to cause a denial of service (assertion failure and application exit) or trigger an incorrect result by attempting a regular-expression match.\",\n            \"links\": \"https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2009-5155 http://people.canonical.com/~ubuntu-security/cve/2009/CVE-2009-5155.html https://sourceware.org/bugzilla/show_bug.cgi?id=11053 https://debbugs.gnu.org/cgi/bugreport.cgi?bug=22793 https://debbugs.gnu.org/cgi/bugreport.cgi?bug=32806 https://debbugs.gnu.org/cgi/bugreport.cgi?bug=34238 https://sourceware.org/bugzilla/show_bug.cgi?id=18986\",\n            \"severity\": \"Low\",\n            \"normalized_severity\": \"Low\",\n            \"package\": {\n              \"id\": \"0\",\n              \"name\": \"glibc\",\n              \"version\": \"2.27-0ubuntu1\",\n              \"kind\": \"binary\",\n              \"source\": null\n            },\n            \"dist\": {\n              \"id\": \"0\",\n              \"did\": \"ubuntu\",\n              \"name\": \"Ubuntu\",\n              \"version\": \"18.04.3 LTS (Bionic Beaver)\",\n              \"version_code_name\": \"bionic\",\n              \"version_id\": \"18.04\",\n              \"arch\": \"amd64\"\n            },\n            \"repo\": {\n              \"id\": \"0\",\n              \"name\": \"Ubuntu 18.04.3 LTS\"\n            },\n            \"issued\": \"2019-10-12T07:20:50.52Z\",\n            \"fixed_in_version\": \"2.28-0ubuntu1\"\n          }\n        ]\n      },\n      \"vulnerability_summaries\": {\n        \"$id\": \"https://clairproject.org/api/http/v1/vulnerability_summaries.schema.json\",\n        \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n        \"title\": \"Vulnerability Summaries\",\n        \"type\": \"array\",\n        \"description\": \"**This is an internal type, documented for completeness.**\\n\\nThis is an array of pseudo-Vulnerability objects used for reverse-lookup.\",\n        \"items\": {\n          \"description\": \"Summary vulnerability objects.\",\n          \"$ref\": \"vulnerability_summary.schema.json\"\n        }\n      },\n      \"vulnerability_summary\": {\n        \"$id\": \"https://clairproject.org/api/http/v1/vulnerability_summary.schema.json\",\n        \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n        \"title\": \"Vulnerability Summary\",\n        \"type\": \"object\",\n        \"description\": \"A summary of a vulnerability.\",\n        \"$ref\": \"vulnerability_core.schema.json\",\n        \"unevaluatedProperties\": false,\n        \"examples\": [\n          {\n            \"name\": \"CVE-2009-5155\",\n            \"description\": \"In the GNU C Library (aka glibc or libc6) before 2.28, parse_reg_exp in posix/regcomp.c misparses alternatives, which allows attackers to cause a denial of service (assertion failure and application exit) or trigger an incorrect result by attempting a regular-expression match.\",\n            \"normalized_severity\": \"Low\",\n            \"fixed_in_version\": \"v0.0.1\",\n            \"links\": \"http://link-to-advisory\",\n            \"package\": {\n              \"id\": \"0\",\n              \"name\": \"glibc\",\n              \"version\": \"v0.0.1-rc1\"\n            },\n            \"dist\": {\n              \"id\": \"0\",\n              \"did\": \"ubuntu\",\n              \"name\": \"Ubuntu\",\n              \"version\": \"18.04.3 LTS (Bionic Beaver)\",\n              \"version_code_name\": \"bionic\",\n              \"version_id\": \"18.04\"\n            },\n            \"repo\": {\n              \"id\": \"0\",\n              \"name\": \"Ubuntu 18.04.3 LTS\"\n            }\n          }\n        ]\n      }\n    },\n    \"responses\": {\n      \"bad_request\": {\n        \"description\": \"Bad Request\",\n        \"content\": {\n          \"application/vnd.clair.error.v1+json\": {\n            \"schema\": {\n              \"$ref\": \"#/components/schemas/error\"\n            }\n          },\n          \"application/json\": {\n            \"schema\": {\n              \"$ref\": \"#/components/schemas/error\"\n            }\n          }\n        }\n      },\n      \"oops\": {\n        \"description\": \"Internal Server Error\",\n        \"content\": {\n          \"application/vnd.clair.error.v1+json\": {\n            \"schema\": {\n              \"$ref\": \"#/components/schemas/error\"\n            }\n          },\n          \"application/json\": {\n            \"schema\": {\n              \"$ref\": \"#/components/schemas/error\"\n            }\n          }\n        }\n      },\n      \"not_found\": {\n        \"description\": \"Not Found\",\n        \"content\": {\n          \"application/vnd.clair.error.v1+json\": {\n            \"schema\": {\n              \"$ref\": \"#/components/schemas/error\"\n            }\n          },\n          \"application/json\": {\n            \"schema\": {\n              \"$ref\": \"#/components/schemas/error\"\n            }\n          }\n        }\n      },\n      \"unsupported_media_type\": {\n        \"description\": \"Unsupported Media Type\",\n        \"content\": {\n          \"application/vnd.clair.error.v1+json\": {\n            \"schema\": {\n              \"$ref\": \"#/components/schemas/error\"\n            }\n          },\n          \"application/json\": {\n            \"schema\": {\n              \"$ref\": \"#/components/schemas/error\"\n            }\n          }\n        }\n      }\n    },\n    \"parameters\": {\n      \"digest\": {\n        \"description\": \"OCI-compatible digest of a referred object.\",\n        \"name\": \"digest\",\n        \"in\": \"path\",\n        \"schema\": {\n          \"$ref\": \"#/components/schemas/digest\"\n        },\n        \"required\": true\n      }\n    },\n    \"headers\": {\n      \"Clair-Error\": {\n        \"description\": \"This is a trailer containing any errors encountered while writing the response.\",\n        \"style\": \"simple\",\n        \"schema\": {\n          \"type\": \"string\"\n        }\n      },\n      \"Etag\": {\n        \"description\": \"HTTP [ETag header](https://httpwg.org/specs/rfc9110.html#field.etag)\",\n        \"style\": \"simple\",\n        \"schema\": {\n          \"type\": \"string\"\n        }\n      },\n      \"Link\": {\n        \"description\": \"Web Linking [Link header](https://httpwg.org/specs/rfc8288.html#header)\",\n        \"style\": \"simple\",\n        \"schema\": {\n          \"type\": \"string\"\n        }\n      },\n      \"Location\": {\n        \"description\": \"HTTP [Location header](https://httpwg.org/specs/rfc9110.html#field.location)\",\n        \"style\": \"simple\",\n        \"required\": true,\n        \"schema\": {\n          \"type\": \"string\"\n        }\n      }\n    },\n    \"securitySchemes\": {\n      \"PSK\": {\n        \"type\": \"http\",\n        \"scheme\": \"bearer\",\n        \"bearerFormat\": \"JWT with preshared key and allow-listed issuers\",\n        \"description\": \"Clair's authentication scheme.\\n\\nThis is a [JWT](https://datatracker.ietf.org/doc/html/rfc7519) signed with a configured pre-shared key containing an allowlisted `iss` claim.\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "httptransport/api/v1/openapi.yaml",
    "content": "openapi: 3.1.0\ninfo:\n  title: Clair Container Analyzer\n  description: |-\n    Clair is a set of cooperating microservices which can index and match a container image's content with known vulnerabilities.\n\n    **Note:** Any endpoints tagged \"internal\" are documented for completeness but are considered exempt from versioning.\n  version: 1.2.0\n  contact:\n    name: Clair Team\n    url: http://github.com/quay/clair\n    email: clair-devel@googlegroups.com\n  license:\n    name: Apache License 2.0\n    url: http://www.apache.org/licenses/\nexternalDocs:\n  url: https://quay.github.io/clair/\ntags:\n  - name: indexer\n    description: |-\n      Indexer service endpoints.\n\n      These are responsible for determining the contents of containers.\n  - name: matcher\n    description: |-\n      Matcher service endpoints.\n\n      These are responsible for generating reports against current vulnerability data.\n  - name: notifier\n    description: |-\n      Matcher service endpoints.\n\n      These are responsible for serving notifications.\n  - name: internal\n    description: |-\n      These are internal endpoints, documented for completeness.\n\n      They are exempted from API stability guarentees.\npaths:\n  /indexer/api/v1/index_report:\n    post:\n      operationId: Index\n      requestBody:\n        description: Manifest to index.\n        required: true\n        content:\n          application/vnd.clair.manifest.v1+json:\n            schema:\n              $ref: '#/components/schemas/manifest'\n          application/json:\n            schema:\n              $ref: '#/components/schemas/manifest'\n      responses:\n        \"400\":\n          $ref: '#/components/responses/bad_request'\n        \"415\":\n          $ref: '#/components/responses/unsupported_media_type'\n        default:\n          $ref: '#/components/responses/oops'\n        \"201\":\n          description: |-\n            IndexReport created.\n\n            Clients may want to avoid reading the body if simply submitting the manifest for later vulnerability reporting.\n          content:\n            application/vnd.clair.index_report.v1+json:\n              schema:\n                $ref: '#/components/schemas/index_report'\n            application/json:\n              schema:\n                $ref: '#/components/schemas/index_report'\n          headers:\n            Location:\n              $ref: '#/components/headers/Location'\n            Link:\n              $ref: '#/components/headers/Link'\n          links:\n            retrieve:\n              operationId: GetIndexReport\n              parameters:\n                digest: $request.body#/hash\n            delete:\n              operationId: DeleteManifest\n              parameters:\n                digest: $request.body#/hash\n            report:\n              operationId: GetVulnerabilityReport\n              parameters:\n                digest: $request.body#/hash\n        \"412\":\n          description: Precondition Failed\n      tags:\n        - indexer\n      summary: Index a Manifest\n      description: By submitting a Manifest object to this endpoint Clair will fetch the layers, scan each layer's contents, and provide an index of discovered packages, repository and distribution information.\n    delete:\n      operationId: DeleteManifests\n      requestBody:\n        description: Array of manifest digests to delete.\n        required: true\n        content:\n          application/vnd.clair.bulk_delete.v1+json:\n            schema:\n              $ref: '#/components/schemas/bulk_delete'\n          application/json:\n            schema:\n              $ref: '#/components/schemas/bulk_delete'\n      responses:\n        \"200\":\n          description: Successfully deleted manifests.\n          headers:\n            Clair-Error:\n              $ref: '#/components/headers/Clair-Error'\n          content:\n            application/vnd.clair.bulk_delete.v1+json:\n              schema:\n                $ref: '#/components/schemas/bulk_delete'\n            application/json:\n              schema:\n                $ref: '#/components/schemas/bulk_delete'\n        \"400\":\n          $ref: '#/components/responses/bad_request'\n        \"415\":\n          $ref: '#/components/responses/unsupported_media_type'\n        default:\n          $ref: '#/components/responses/oops'\n      tags:\n        - indexer\n      summary: Delete Indexed Manifests\n      description: Given a Manifest's content addressable hash, any data related to it will be removed if it exists.\n  /indexer/api/v1/index_report/{digest}:\n    delete:\n      operationId: DeleteManifest\n      responses:\n        \"400\":\n          $ref: '#/components/responses/bad_request'\n        \"415\":\n          $ref: '#/components/responses/unsupported_media_type'\n        default:\n          $ref: '#/components/responses/oops'\n        \"204\":\n          description: Success\n      tags:\n        - indexer\n      summary: Delete an Indexed Manifest\n      description: Given a Manifest's content addressable hash, any data related to it will be removed it it exists.\n    get:\n      operationId: GetIndexReport\n      responses:\n        \"200\":\n          description: IndexReport retrieved\n          headers:\n            Clair-Error:\n              $ref: '#/components/headers/Clair-Error'\n          content:\n            application/vnd.clair.index_report.v1+json:\n              schema:\n                $ref: '#/components/schemas/index_report'\n            application/json:\n              schema:\n                $ref: '#/components/schemas/index_report'\n        \"400\":\n          $ref: '#/components/responses/bad_request'\n        \"415\":\n          $ref: '#/components/responses/unsupported_media_type'\n        default:\n          $ref: '#/components/responses/oops'\n        \"404\":\n          $ref: '#/components/responses/not_found'\n      tags:\n        - indexer\n      summary: Retrieve the IndexReport for a Manifest\n      description: Given a Manifest's content addressable hash, an IndexReport will be retrieved if it exists.\n    parameters:\n      - $ref: '#/components/parameters/digest'\n  /indexer/api/v1/index_state:\n    get:\n      operationId: IndexState\n      responses:\n        \"200\":\n          description: Indexer State\n          headers:\n            Etag:\n              $ref: '#/components/headers/Etag'\n          content:\n            application/vnd.clair.index_state.v1+json:\n              schema:\n                $ref: '#/components/schemas/index_state'\n            application/json:\n              schema:\n                $ref: '#/components/schemas/index_state'\n        \"304\":\n          description: Not Modified\n      tags:\n        - indexer\n      summary: Report the Indexer's State\n      description: |-\n        The index state endpoint returns a json structure indicating the indexer's internal configuration state.\n        A client may be interested in this as a signal that manifests may need to be re-indexed.\n  /indexer/api/v1/internal/affected_manifest:\n    post:\n      operationId: AffectedManifests\n      requestBody:\n        description: Array of vulnerability summaries to report on.\n        required: true\n        content:\n          application/vnd.clair.vulnerability_summaries.v1+json:\n            schema:\n              $ref: '#/components/schemas/vulnerability_summaries'\n          application/json:\n            schema:\n              $ref: '#/components/schemas/vulnerability_summaries'\n      responses:\n        \"200\":\n          description: The list of manifests and the corresponding vulnerabilities.\n          headers:\n            Clair-Error:\n              $ref: '#/components/headers/Clair-Error'\n          content:\n            application/vnd.clair.affected_manifests.v1+json:\n              schema:\n                $ref: '#/components/schemas/affected_manifests'\n            application/json:\n              schema:\n                $ref: '#/components/schemas/affected_manifests'\n        \"400\":\n          $ref: '#/components/responses/bad_request'\n        \"415\":\n          $ref: '#/components/responses/unsupported_media_type'\n        default:\n          $ref: '#/components/responses/oops'\n      tags:\n        - internal\n      summary: Retrieve Manifests Affected by a Vulnerability\n      description: The provided vulnerability summaries are attempted to be run \"backwards\" through the indexer to produce a set of manifests.\n      x-cli-ignore: true\n  /matcher/api/v1/internal/update_diff:\n    get:\n      operationId: GetUpdateDiff\n      responses:\n        \"200\":\n          description: Changes between two Update Operations.\n          headers:\n            Clair-Error:\n              $ref: '#/components/headers/Clair-Error'\n          content:\n            application/vnd.clair.update_diff.v1+json:\n              schema:\n                $ref: '#/components/schemas/update_diff'\n            application/json:\n              schema:\n                $ref: '#/components/schemas/update_diff'\n        \"400\":\n          $ref: '#/components/responses/bad_request'\n        \"415\":\n          $ref: '#/components/responses/unsupported_media_type'\n        default:\n          $ref: '#/components/responses/oops'\n      parameters:\n        - in: query\n          name: cur\n          schema:\n            $ref: '#/components/schemas/token'\n          description: '\"Current\" Update Operation ref.'\n        - in: query\n          name: prev\n          required: true\n          schema:\n            $ref: '#/components/schemas/token'\n          description: '\"Previous\" Update Operation ref.'\n      tags:\n        - internal\n      summary: Retrieve Vulnerability Changes Between Two Update Operations\n      description: Given IDs for two Update Operations, this will return the difference between them. This is used in the notification flow.\n      x-cli-ignore: true\n  /matcher/api/v1/internal/update_operation:\n    get:\n      operationId: GetUpdateOperation\n      responses:\n        \"200\":\n          description: Update Operations, keyed by updater.\n          headers:\n            Clair-Error:\n              $ref: '#/components/headers/Clair-Error'\n          content:\n            application/vnd.clair.update_operations.v1+json:\n              schema:\n                $ref: '#/components/schemas/update_operations'\n            application/json:\n              schema:\n                $ref: '#/components/schemas/update_operations'\n        \"400\":\n          $ref: '#/components/responses/bad_request'\n        \"415\":\n          $ref: '#/components/responses/unsupported_media_type'\n        default:\n          $ref: '#/components/responses/oops'\n      parameters:\n        - in: query\n          name: kind\n          schema:\n            enum:\n              - vulnerability\n              - enrichment\n            default: vulnerability\n          description: The \"kind\" of updaters to query.\n        - in: query\n          name: latest\n          schema:\n            type: boolean\n            default: false\n          description: Return only the latest Update Operations instead of all known Update Operations.\n      tags:\n        - internal\n      summary: Retrieve Update Operations\n      description: Retrive all known or just the latest Update Operations.\n      x-cli-ignore: true\n  /matcher/api/v1/internal/update_operation/{digest}:\n    delete:\n      operationId: DeleteUpdateOperation\n      responses:\n        \"200\":\n          description: Success\n          headers:\n            Clair-Error:\n              $ref: '#/components/headers/Clair-Error'\n        \"400\":\n          $ref: '#/components/responses/bad_request'\n        \"415\":\n          $ref: '#/components/responses/unsupported_media_type'\n        default:\n          $ref: '#/components/responses/oops'\n      tags:\n        - internal\n      summary: Delete an Update Operation\n      description: |-\n        Issues a delete of the provided Update Operation ID and all associated data.\n        After this delete clients will no longer be able to generate a diff against this Update Operation.\n      x-cli-ignore: true\n    parameters:\n      - $ref: '#/components/parameters/digest'\n  /matcher/api/v1/vulnerability_report/{digest}:\n    get:\n      operationId: GetVulnerabilityReport\n      responses:\n        \"400\":\n          $ref: '#/components/responses/bad_request'\n        \"415\":\n          $ref: '#/components/responses/unsupported_media_type'\n        default:\n          $ref: '#/components/responses/oops'\n        \"201\":\n          description: Vulnerability Report Created\n          content:\n            application/vnd.clair.vulnerability_report.v1+json:\n              schema:\n                $ref: '#/components/schemas/vulnerability_report'\n            application/json:\n              schema:\n                $ref: '#/components/schemas/vulnerability_report'\n        \"404\":\n          $ref: '#/components/responses/not_found'\n      tags:\n        - matcher\n      summary: Retrieve a VulnerabilityReport for a Manifest\n      description: Given a Manifest's content addressable hash a VulnerabilityReport will be created. The Manifest **must** have been Indexed first via the Index endpoint.\n    parameters:\n      - $ref: '#/components/parameters/digest'\n  /notifier/api/v1/notification/{id}:\n    parameters:\n      - in: path\n        name: id\n        required: true\n        schema:\n          $ref: '#/components/schemas/token'\n        description: A notification ID returned by a callback\n    delete:\n      operationId: DeleteNotification\n      responses:\n        \"200\":\n          description: Delete the notification referenced by the \"id\" parameter.\n          headers:\n            Clair-Error:\n              $ref: '#/components/headers/Clair-Error'\n        \"400\":\n          $ref: '#/components/responses/bad_request'\n        \"415\":\n          $ref: '#/components/responses/unsupported_media_type'\n        default:\n          $ref: '#/components/responses/oops'\n      tags:\n        - notifier\n      summary: Delete a Notification Set\n      description: |-\n        Issues a delete of the provided notification ID and all associated notifications.\n        After this delete clients will no longer be able to retrieve notifications.\n    get:\n      operationId: GetNotification\n      parameters:\n        - in: query\n          name: page_size\n          schema:\n            type: integer\n            default: 500\n          description: The maximum number of notifications to deliver in a single page.\n        - in: query\n          name: next\n          schema:\n            $ref: '#/components/schemas/token'\n          description: The next page to fetch via id. Typically this number is provided on initial response in the \"page.next\" field. The first request should omit this field.\n      responses:\n        \"200\":\n          description: A paginated list of notifications\n          headers:\n            Clair-Error:\n              $ref: '#/components/headers/Clair-Error'\n          content:\n            application/vnd.clair.notification_page.v1+json:\n              schema:\n                $ref: '#/components/schemas/notification_page'\n            application/json:\n              schema:\n                $ref: '#/components/schemas/notification_page'\n        \"400\":\n          $ref: '#/components/responses/bad_request'\n        \"415\":\n          $ref: '#/components/responses/unsupported_media_type'\n        default:\n          $ref: '#/components/responses/oops'\n        \"304\":\n          description: Not Modified\n      tags:\n        - notifier\n      summary: Retrieve Pages of a Notification Set\n      description: By performing a GET with an id as a path parameter, the client will retrieve a paginated response of notification objects.\nsecurity:\n  - {}\n  - PSK: []\nwebhooks:\n  notification:\n    post:\n      tags:\n        - notifier\n      description: If configured, Clair will issue webhooks when notifications are available for retrieval.\n      requestBody:\n        content:\n          application/vnd.clair.notification_webhook.v1+json:\n            schema:\n              $ref: '#/components/schemas/notification_webhook'\n      responses:\n        \"200\":\n          description: OK\ncomponents:\n  schemas:\n    token:\n      type: string\n      description: An opaque token previously obtained from the service.\n    affected_manifests:\n      $id: https://clairproject.org/api/http/v1/affected_manifests.schema.json\n      $schema: https://json-schema.org/draft/2020-12/schema\n      title: Affected Manifests\n      type: object\n      description: |-\n        **This is an internal type, documented for completeness.**\n\n        Manifests affected by the specified vulnerability objects.\n      properties:\n        vulnerabilities:\n          type: object\n          description: Vulnerability objects.\n          additionalProperties:\n            $ref: vulnerability.schema.json\n        vulnerable_manifests:\n          type: object\n          description: Mapping of manifest digests to vulnerability identifiers.\n          additionalProperties:\n            type: array\n            items:\n              type: string\n              description: An identifier to be used in the \"vulnerabilities\" object.\n      required:\n        - vulnerabilities\n        - vulnerable_manifests\n      examples:\n        - vulnerabilities:\n            \"42\":\n              id: \"42\"\n          vulnerable_manifests:\n            sha256:01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b:\n              - \"42\"\n    bulk_delete:\n      $id: https://clairproject.org/api/http/v1/bulk_delete.schema.json\n      $schema: https://json-schema.org/draft/2020-12/schema\n      title: Bulk Delete\n      type: array\n      description: Array of manifest digests to delete from the system.\n      items:\n        $ref: digest.schema.json\n        description: Manifest digest to delete from the system.\n      examples:\n        - - sha256:fc84b5febd328eccaa913807716887b3eb5ed08bc22cc6933a9ebf82766725e3\n    cpe:\n      $id: https://clairproject.org/api/http/v1/cpe.schema.json\n      $schema: https://json-schema.org/draft/2020-12/schema\n      title: Common Platform Enumeration Name\n      description: This is a CPE Name in either v2.2 \"URI\" form or v2.3 \"Formatted String\" form.\n      $comment: Clair only produces v2.3 CPE Names. Any v2.2 Names will be normalized into v2.3 form.\n      oneOf:\n        - description: 'This is the CPE 2.2 regexp: https://cpe.mitre.org/specification/2.2/cpe-language_2.2.xsd'\n          type: string\n          pattern: ^[c][pP][eE]:/[AHOaho]?(:[A-Za-z0-9\\._\\-~%]*){0,6}$\n        - description: 'This is the CPE 2.3 regexp: https://csrc.nist.gov/schema/cpe/2.3/cpe-naming_2.3.xsd'\n          type: string\n          pattern: ^cpe:2\\.3:[aho\\*\\-](:(((\\?*|\\*?)([a-zA-Z0-9\\-\\._]|(\\\\[\\\\\\*\\?!\"#$$%&'\\(\\)\\+,/:;<=>@\\[\\]\\^`\\{\\|}~]))+(\\?*|\\*?))|[\\*\\-])){5}(:(([a-zA-Z]{2,3}(-([a-zA-Z]{2}|[0-9]{3}))?)|[\\*\\-]))(:(((\\?*|\\*?)([a-zA-Z0-9\\-\\._]|(\\\\[\\\\\\*\\?!\"#$$%&'\\(\\)\\+,/:;<=>@\\[\\]\\^`\\{\\|}~]))+(\\?*|\\*?))|[\\*\\-])){4}$\n      examples:\n        - cpe:/a:microsoft:internet_explorer:8.0.6001:beta\n        - cpe:2.3:a:microsoft:internet_explorer:8.0.6001:beta:*:*:*:*:*:*\n    digest:\n      $id: https://clairproject.org/api/http/v1/digest.schema.json\n      $schema: https://json-schema.org/draft/2020-12/schema\n      title: Digest\n      description: A digest acts as a content identifier, enabling content addressability.\n      type: string\n      anyOf:\n        - $comment: 'SHA256: MUST be implemented'\n          description: SHA256\n          type: string\n          pattern: ^sha256:[a-f0-9]{64}$\n        - $comment: 'SHA512: MAY be implemented'\n          description: SHA512\n          type: string\n          pattern: ^sha512:[a-f0-9]{128}$\n        - $comment: 'BLAKE3: MAY be implemented'\n          description: |-\n            BLAKE3\n\n            **Currently not implemented.**\n          type: string\n          pattern: ^blake3:[a-f0-9]{64}$\n      examples:\n        - sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a\n        - sha512:27c74670adb75075fad058d5ceaf7b20c4e7786c83bae8a32f626f9782af34c9a33c2046ef60fd2a7878d378e29fec851806bbd9a67878f3a9f1cda4830763fd\n        - blake3:6e46dd10defc9b56c29a6ec56b508c21f54c08192194e4df25bf36f0c9c3c279\n    distribution:\n      $id: https://clairproject.org/api/http/v1/distribution.schema.json\n      $schema: https://json-schema.org/draft/2020-12/schema\n      title: Distribution\n      type: object\n      description: Distribution is the accompanying system context of a Package.\n      properties:\n        id:\n          description: Unique ID for this Distribution. May be unique to the response document, not the whole system.\n          type: string\n        did:\n          description: A lower-case string (no spaces or other characters outside of 0–9, a–z, \".\", \"_\", and \"-\") identifying the operating system, excluding any version information and suitable for processing by scripts or usage in generated filenames.\n          type: string\n        name:\n          description: A string identifying the operating system.\n          type: string\n        version:\n          description: A string identifying the operating system version, excluding any OS name information, possibly including a release code name, and suitable for presentation to the user.\n          type: string\n        version_code_name:\n          description: A lower-case string (no spaces or other characters outside of 0–9, a–z, \".\", \"_\", and \"-\") identifying the operating system release code name, excluding any OS name information or release version, and suitable for processing by scripts or usage in generated filenames.\n          type: string\n        version_id:\n          description: A lower-case string (mostly numeric, no spaces or other characters outside of 0–9, a–z, \".\", \"_\", and \"-\") identifying the operating system version, excluding any OS name information or release code name.\n          type: string\n        arch:\n          description: A string identifying the OS architecture.\n          type: string\n        cpe:\n          description: Common Platform Enumeration name.\n          $ref: cpe.schema.json\n        pretty_name:\n          description: A pretty operating system name in a format suitable for presentation to the user.\n          type: string\n      additionalProperties: false\n      required:\n        - id\n      examples:\n        - id: \"1\"\n          did: ubuntu\n          name: Ubuntu\n          version: 18.04.3 LTS (Bionic Beaver)\n          version_code_name: bionic\n          version_id: \"18.04\"\n          pretty_name: Ubuntu 18.04.3 LTS\n    environment:\n      $id: https://clairproject.org/api/http/v1/environment.schema.json\n      $schema: https://json-schema.org/draft/2020-12/schema\n      title: Environment\n      type: object\n      description: Environment describes the surrounding environment a package was discovered in.\n      properties:\n        package_db:\n          description: The database the associated Package was discovered in.\n          type: string\n        distribution_id:\n          description: The ID of the Distribution of the associated Package.\n          type: string\n        introduced_in:\n          description: The Layer the associated Package was introduced in.\n          $ref: digest.schema.json\n        repository_ids:\n          description: The IDs of the Repositories of the associated Package.\n          type: array\n          items:\n            type: string\n      additionalProperties: false\n      examples:\n        - value:\n            package_db: var/lib/dpkg/status\n            introduced_in: sha256:35c102085707f703de2d9eaad8752d6fe1b8f02b5d2149f1d8357c9cc7fb7d0a\n            distribution_id: \"1\"\n    error:\n      $id: https://clairproject.org/api/http/v1/error.schema.json\n      $schema: https://json-schema.org/draft/2020-12/schema\n      title: Error\n      type: object\n      description: A general error response.\n      properties:\n        code:\n          type: string\n          description: a code for this particular error\n        message:\n          type: string\n          description: a message with further detail\n      required:\n        - message\n    index_report:\n      $id: https://clairproject.org/api/http/v1/index_report.schema.json\n      $schema: https://json-schema.org/draft/2020-12/schema\n      title: Index Report\n      type: object\n      description: An index of the contents of a Manifest.\n      properties:\n        manifest_hash:\n          $ref: digest.schema.json\n          description: The Manifest's digest.\n        state:\n          type: string\n          description: The current state of the index operation\n        err:\n          type: string\n          description: An error message on event of unsuccessful index\n        success:\n          type: boolean\n          description: A bool indicating succcessful index\n        packages:\n          type: object\n          description: A map of Package objects indexed by a document-local identifier.\n          additionalProperties:\n            $ref: package.schema.json\n        distributions:\n          type: object\n          description: A map of Distribution objects indexed by a document-local identifier.\n          additionalProperties:\n            $ref: distribution.schema.json\n        repository:\n          type: object\n          description: A map of Repository objects indexed by a document-local identifier.\n          additionalProperties:\n            $ref: repository.schema.json\n        environments:\n          type: object\n          description: A map of Environment arrays indexed by a Package's identifier.\n          additionalProperties:\n            type: array\n            items:\n              $ref: environment.schema.json\n      additionalProperties: false\n      required:\n        - manifest_hash\n        - state\n        - success\n    index_state:\n      $id: https://clairproject.org/api/http/v1/index_state.schema.json\n      $schema: https://json-schema.org/draft/2020-12/schema\n      title: Index State\n      type: object\n      description: Information on the state of the indexer system.\n      properties:\n        state:\n          type: string\n          description: an opaque token\n      required:\n        - state\n    layer:\n      $id: https://clairproject.org/api/http/v1/layer.schema.json\n      $schema: https://json-schema.org/draft/2020-12/schema\n      title: Layer\n      type: object\n      description: Layer is a description of a container layer. It should contain enough information to fetch the layer.\n      properties:\n        hash:\n          $ref: digest.schema.json\n          description: Digest of the layer blob.\n        uri:\n          type: string\n          description: A URI indicating where the layer blob can be downloaded from.\n        headers:\n          description: Any additional HTTP-style headers needed for requesting layers.\n          type: object\n          patternProperties:\n            ^[a-zA-Z0-9\\-_]+$:\n              type: array\n              items:\n                type: string\n        media_type:\n          description: The OCI Layer media type for this layer.\n          type: string\n          pattern: ^application/vnd\\.oci\\.image\\.layer\\.v1\\.tar(\\+(gzip|zstd))?$\n      additionalProperties: false\n      required:\n        - hash\n        - uri\n    manifest:\n      $id: https://clairproject.org/api/http/v1/manifest.schema.json\n      $schema: https://json-schema.org/draft/2020-12/schema\n      title: Manifest\n      type: object\n      description: A description of an OCI Image Manifest.\n      properties:\n        hash:\n          $ref: digest.schema.json\n          description: |-\n            The OCI Image Manifest's digest.\n\n            This is used as an identifier throughout the system. This **SHOULD** be the same as the OCI Image Manifest's digest, but this is not enforced.\n        layers:\n          type: array\n          description: The OCI Layers making up the Image, in order.\n          items:\n            $ref: layer.schema.json\n      additionalProperties: false\n      required:\n        - hash\n      examples:\n        - hash: sha256:fc84b5febd328eccaa913807716887b3eb5ed08bc22cc6933a9ebf82766725e3\n          layers:\n            - hash: sha256:2f077db56abccc19f16f140f629ae98e904b4b7d563957a7fc319bd11b82ba36\n              uri: https://storage.example.com/blob/2f077db56abccc19f16f140f629ae98e904b4b7d563957a7fc319bd11b82ba36\n              headers:\n                Authoriztion:\n                  - Bearer hunter2\n    normalized_severity:\n      $id: https://clairproject.org/api/http/v1/normalized_severity.schema.json\n      $schema: https://json-schema.org/draft/2020-12/schema\n      title: Normalized Severity\n      description: Standardized severity values.\n      enum:\n        - Unknown\n        - Negligible\n        - Low\n        - Medium\n        - High\n        - Critical\n    notification_page:\n      $id: https://clairproject.org/api/http/v1/notification_page.schema.json\n      $schema: https://json-schema.org/draft/2020-12/schema\n      title: Notification Page\n      type: object\n      description: A page description and list of notifications.\n      properties:\n        page:\n          description: An object informing the client the next page to retrieve.\n          type: object\n          properties:\n            size:\n              description: The number of notifications contained in this page.\n              type: integer\n            next:\n              description: |-\n                The identififer to pass into the \"next\" parameter of a future GetNotification request.\n\n                If not present, there are no additional pages.\n              type: string\n          additionalProperties: false\n          required:\n            - size\n        notifications:\n          description: Notifications within this page.\n          type: array\n          items:\n            $ref: notification.schema.json\n      additionalProperties: false\n      required:\n        - page\n        - notifications\n      examples:\n        - page:\n            size: 100\n            next: 1b4d0db2-e757-4150-bbbb-543658144205\n          notifications:\n            - id: 5e4b387e-88d3-4364-86fd-063447a6fad2\n              manifest: sha256:35c102085707f703de2d9eaad8752d6fe1b8f02b5d2149f1d8357c9cc7fb7d0a\n              reason: added\n              vulnerability:\n                name: CVE-2009-5155\n                fixed_in_version: v0.0.1\n                links: http://example.com/CVE-2009-5155\n                description: In the GNU C Library (aka glibc or libc6) before 2.28, parse_reg_exp in posix/regcomp.c misparses alternatives, which allows attackers to cause a denial of service (assertion failure and application exit) or trigger an incorrect result by attempting a regular-expression match.\"\n                normalized_severity: Unknown\n                package:\n                  id: \"10\"\n                  name: libapt-pkg5.0\n                  version: 1.6.11\n                  kind: BINARY\n                  arch: x86\n                  source:\n                    id: \"9\"\n                    name: apt\n                    version: 1.6.11\n                    kind: SOURCE\n                    source: null\n                distribution:\n                  id: \"1\"\n                  did: ubuntu\n                  name: Ubuntu\n                  version: 18.04.3 LTS (Bionic Beaver)\n                  version_code_name: bionic\n                  version_id: \"18.04\"\n                  pretty_name: Ubuntu 18.04.3 LTS\n    notification:\n      $id: https://clairproject.org/api/http/v1/notification.schema.json\n      $schema: https://json-schema.org/draft/2020-12/schema\n      title: Notification\n      type: object\n      description: A change in a manifest affected by a vulnerability.\n      properties:\n        id:\n          description: Unique identifier for this notification.\n          type: string\n        manifest:\n          $ref: digest.schema.json\n          description: The digest of the manifest affected by the provided vulnerability.\n        reason:\n          description: The reason for the notifcation.\n          enum:\n            - added\n            - removed\n        vulnerability:\n          $ref: vulnerability_summary.schema.json\n      additionalProperties: false\n      required:\n        - id\n        - manifest\n        - reason\n        - vulnerability\n    notification_webhook:\n      $id: https://clairproject.org/api/http/v1/notification_webhook.schema.json\n      $schema: https://json-schema.org/draft/2020-12/schema\n      title: Notification Webhook\n      type: object\n      description: Webhook sent to a configured service to begin retrieving notifications.\n      properties:\n        notification_id:\n          description: Unique identifier for this notification.\n          type: string\n        callback:\n          description: A URL to retrieve paginated Notification objects.\n          type: string\n          format: uri\n      additionalProperties: false\n      required:\n        - notification_id\n        - callback\n    package:\n      $id: https://clairproject.org/api/http/v1/package.schema.json\n      $schema: https://json-schema.org/draft/2020-12/schema\n      title: Package\n      type: object\n      description: Description of installed software.\n      properties:\n        id:\n          description: Unique ID for this Package. May be unique to the response document, not the whole system.\n          type: string\n        name:\n          description: |-\n            Identifier of this Package.\n\n            The uniqueness and scoping of this name depends on the packaging system.\n          type: string\n        version:\n          description: Version of this Package, as reported by the packaging system.\n          type: string\n        kind:\n          description: The \"kind\" of this Package.\n          enum:\n            - BINARY\n            - SOURCE\n          default: BINARY\n        source:\n          $ref: '#'\n          description: Source Package that produced the current binary Package, if known.\n        normalized_version:\n          description: |-\n            Normalized representation of the discoverd version.\n\n            The format is not specific, but is guarenteed to be forward compatible.\n          type: string\n        module:\n          description: |-\n            An identifier for intra-Repository grouping of packages.\n\n            Likely only relevant on rpm-based systems.\n          type: string\n        arch:\n          description: Native architecture for the Package.\n          type: string\n          $comment: This should become and enum in the future.\n        cpe:\n          $ref: cpe.schema.json\n          description: CPE Name for the Package.\n      additionalProperties: false\n      required:\n        - name\n        - version\n      examples:\n        - id: \"10\"\n          name: libapt-pkg5.0\n          version: 1.6.11\n          kind: binary\n          normalized_version: \"\"\n          arch: x86\n          module: \"\"\n          cpe: \"\"\n          source:\n            id: \"9\"\n            name: apt\n            version: 1.6.11\n            kind: source\n            source: null\n    range:\n      $id: https://clairproject.org/api/http/v1/range.schema.json\n      $schema: https://json-schema.org/draft/2020-12/schema\n      title: Range\n      type: object\n      description: A range of versions.\n      properties:\n        '[':\n          type: string\n          description: Lower bound, inclusive.\n        ):\n          type: string\n          description: Upper bound, exclusive.\n      minProperties: 1\n      additionalProperties: false\n    repository:\n      $id: https://clairproject.org/api/http/v1/repository.schema.json\n      $schema: https://json-schema.org/draft/2020-12/schema\n      title: Repository\n      type: object\n      description: Description of a software repository\n      properties:\n        id:\n          description: Unique ID for this Repository. May be unique to the response document, not the whole system.\n          type: string\n        name:\n          description: Human-relevant name for the Repository.\n          type: string\n        key:\n          description: Machine-relevant name for the Repository.\n          type: string\n        uri:\n          description: URI describing the Repository.\n          type: string\n          format: uri\n        cpe:\n          description: CPE name for the Repository.\n          $ref: cpe.schema.json\n      additionalProperties: false\n      required:\n        - id\n    update_diff:\n      $id: https://clairproject.org/api/http/v1/update_diff.schema.json\n      $schema: https://json-schema.org/draft/2020-12/schema\n      title: Update Difference\n      type: object\n      description: |-\n        **This is an internal type, documented for completeness.**\n\n        An update difference describes changes between two Update Operations.\n      properties:\n        prev:\n          description: The previous Update Operation.\n          $ref: update_operation.schema.json\n        cur:\n          description: The current Update Operation.\n          $ref: update_operation.schema.json\n        added:\n          description: Vulnerabilities present in \"cur\", but not \"prev\".\n          type: array\n          items:\n            $ref: vulnerability.schema.json\n        removed:\n          description: Vulnerabilities present in \"prev\", but not \"cur\".\n          type: array\n          items:\n            $ref: vulnerability.schema.json\n      additionalProperties: false\n      required:\n        - cur\n        - added\n        - removed\n    update_operation:\n      $id: https://clairproject.org/api/http/v1/update_operation.schema.json\n      $schema: https://json-schema.org/draft/2020-12/schema\n      title: Update Operation\n      type: object\n      description: |-\n        **This is an internal type, documented for completeness.**\n\n        An update operations describes an update of the internal vulnerability database.\n      properties:\n        ref:\n          type: string\n          description: A unique identifier for this update operation.\n          format: uuid\n        updater:\n          description: The \"updater\" component that was run.\n          $comment: 'This is not as useful as it could be: an end user needs to know too much about Clair(core)''s internals to make sense of it.'\n          type: string\n        fingerprint:\n          description: The stored \"fingerprint\" of this run.\n          type: string\n        date:\n          type: string\n          description: When this operation was run.\n          format: date-time\n        kind:\n          description: The kind of data this operation updated.\n          enum:\n            - vulnerability\n            - enrichment\n      additionalProperties: false\n      required:\n        - ref\n        - updater\n        - fingerprint\n        - date\n        - kind\n    update_operations:\n      $id: https://clairproject.org/api/http/v1/update_operations.schema.json\n      $schema: https://json-schema.org/draft/2020-12/schema\n      title: Update Operations\n      type: object\n      description: |-\n        **This is an internal type, documented for completeness.**\n\n        A mapping of updater id to Update Operation(s).\n      additionalProperties:\n        type: array\n        items:\n          $ref: update_operation.schema.json\n    vulnerability_core:\n      $id: https://clairproject.org/api/http/v1/vulnerability_core.schema.json\n      $schema: https://json-schema.org/draft/2020-12/schema\n      title: Vulnerability Core\n      type: object\n      description: The core elements of vulnerabilities in the Clair system.\n      properties:\n        name:\n          type: string\n          description: Human-readable name, as presented in the vendor data.\n        fixed_in_version:\n          type: string\n          description: Version string, as presented in the vendor data.\n        severity:\n          type: string\n          description: Severity, as presented in the vendor data.\n        normalized_severity:\n          $ref: normalized_severity.schema.json\n          description: A well defined set of severity strings guaranteed to be present.\n        range:\n          $ref: range.schema.json\n          description: Range of versions the vulnerability applies to.\n        arch_op:\n          description: Flag indicating how the referenced package's \"arch\" member should be interpreted.\n          enum:\n            - equals\n            - not equals\n            - pattern match\n        package:\n          $ref: package.schema.json\n          description: A package description\n        distribution:\n          $ref: distribution.schema.json\n          description: A distribution description\n        repository:\n          $ref: repository.schema.json\n          description: A repository description\n      required:\n        - name\n        - normalized_severity\n      dependentRequired:\n        package:\n          - arch_op\n      anyOf:\n        - required:\n            - package\n        - required:\n            - repository\n        - required:\n            - distribution\n    vulnerability_report:\n      $id: https://clairproject.org/api/http/v1/vulnerability_report.schema.json\n      $schema: https://json-schema.org/draft/2020-12/schema\n      title: Vulnerability Report\n      type: object\n      description: A report with discovered packages, package environments, and package vulnerabilities within a Manifest.\n      properties:\n        manifest_hash:\n          $ref: digest.schema.json\n          description: The Manifest's digest.\n        packages:\n          type: object\n          description: A map of Package objects indexed by a document-local identifier.\n          additionalProperties:\n            $ref: package.schema.json\n        distributions:\n          type: object\n          description: A map of Distribution objects indexed by a document-local identifier.\n          additionalProperties:\n            $ref: distribution.schema.json\n        repository:\n          type: object\n          description: A map of Repository objects indexed by a document-local identifier.\n          additionalProperties:\n            $ref: repository.schema.json\n        environments:\n          type: object\n          description: A map of Environment arrays indexed by a Package's identifier.\n          additionalProperties:\n            type: array\n            items:\n              $ref: environment.schema.json\n        vulnerabilities:\n          type: object\n          description: A map of Vulnerabilities indexed by a document-local identifier.\n          additionalProperties:\n            $ref: vulnerability.schema.json\n        package_vulnerabilities:\n          type: object\n          description: A mapping of Vulnerability identifier lists indexed by Package identifier.\n          additionalProperties:\n            type: array\n            items:\n              type: string\n        enrichments:\n          type: object\n          description: A mapping of extra \"enrichment\" data by type\n          additionalProperties:\n            type: array\n      additionalProperties: false\n      required:\n        - distributions\n        - environments\n        - manifest_hash\n        - packages\n        - package_vulnerabilities\n        - vulnerabilities\n    vulnerability:\n      $id: https://clairproject.org/api/http/v1/vulnerability.schema.json\n      $schema: https://json-schema.org/draft/2020-12/schema\n      title: Vulnerability\n      type: object\n      description: Description of a software flaw.\n      $ref: vulnerability_core.schema.json\n      properties:\n        id:\n          description: Unique ID for this Vulnerabiltity. May be unique to the response document, not the whole system.\n          type: string\n        updater:\n          description: The updater component this Vulnerability came from.\n          type: string\n        description:\n          description: A human-readable description of the vulnerability.\n          type: string\n        issued:\n          description: The datetime this Vulnerability was issued, if known.\n          type: string\n          format: date-time\n        links:\n          description: Space-separated URIs to more information.\n          type: string\n      unevaluatedProperties: false\n      required:\n        - id\n        - updater\n      examples:\n        - id: \"356835\"\n          updater: ubuntu\n          name: CVE-2009-5155\n          description: In the GNU C Library (aka glibc or libc6) before 2.28, parse_reg_exp in posix/regcomp.c misparses alternatives, which allows attackers to cause a denial of service (assertion failure and application exit) or trigger an incorrect result by attempting a regular-expression match.\n          links: https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2009-5155 http://people.canonical.com/~ubuntu-security/cve/2009/CVE-2009-5155.html https://sourceware.org/bugzilla/show_bug.cgi?id=11053 https://debbugs.gnu.org/cgi/bugreport.cgi?bug=22793 https://debbugs.gnu.org/cgi/bugreport.cgi?bug=32806 https://debbugs.gnu.org/cgi/bugreport.cgi?bug=34238 https://sourceware.org/bugzilla/show_bug.cgi?id=18986\n          severity: Low\n          normalized_severity: Low\n          package:\n            id: \"0\"\n            name: glibc\n            version: 2.27-0ubuntu1\n            kind: binary\n            source: null\n          dist:\n            id: \"0\"\n            did: ubuntu\n            name: Ubuntu\n            version: 18.04.3 LTS (Bionic Beaver)\n            version_code_name: bionic\n            version_id: \"18.04\"\n            arch: amd64\n          repo:\n            id: \"0\"\n            name: Ubuntu 18.04.3 LTS\n          issued: \"2019-10-12T07:20:50.52Z\"\n          fixed_in_version: 2.28-0ubuntu1\n    vulnerability_summaries:\n      $id: https://clairproject.org/api/http/v1/vulnerability_summaries.schema.json\n      $schema: https://json-schema.org/draft/2020-12/schema\n      title: Vulnerability Summaries\n      type: array\n      description: |-\n        **This is an internal type, documented for completeness.**\n\n        This is an array of pseudo-Vulnerability objects used for reverse-lookup.\n      items:\n        description: Summary vulnerability objects.\n        $ref: vulnerability_summary.schema.json\n    vulnerability_summary:\n      $id: https://clairproject.org/api/http/v1/vulnerability_summary.schema.json\n      $schema: https://json-schema.org/draft/2020-12/schema\n      title: Vulnerability Summary\n      type: object\n      description: A summary of a vulnerability.\n      $ref: vulnerability_core.schema.json\n      unevaluatedProperties: false\n      examples:\n        - name: CVE-2009-5155\n          description: In the GNU C Library (aka glibc or libc6) before 2.28, parse_reg_exp in posix/regcomp.c misparses alternatives, which allows attackers to cause a denial of service (assertion failure and application exit) or trigger an incorrect result by attempting a regular-expression match.\n          normalized_severity: Low\n          fixed_in_version: v0.0.1\n          links: http://link-to-advisory\n          package:\n            id: \"0\"\n            name: glibc\n            version: v0.0.1-rc1\n          dist:\n            id: \"0\"\n            did: ubuntu\n            name: Ubuntu\n            version: 18.04.3 LTS (Bionic Beaver)\n            version_code_name: bionic\n            version_id: \"18.04\"\n          repo:\n            id: \"0\"\n            name: Ubuntu 18.04.3 LTS\n  responses:\n    bad_request:\n      description: Bad Request\n      content:\n        application/vnd.clair.error.v1+json:\n          schema:\n            $ref: '#/components/schemas/error'\n        application/json:\n          schema:\n            $ref: '#/components/schemas/error'\n    oops:\n      description: Internal Server Error\n      content:\n        application/vnd.clair.error.v1+json:\n          schema:\n            $ref: '#/components/schemas/error'\n        application/json:\n          schema:\n            $ref: '#/components/schemas/error'\n    not_found:\n      description: Not Found\n      content:\n        application/vnd.clair.error.v1+json:\n          schema:\n            $ref: '#/components/schemas/error'\n        application/json:\n          schema:\n            $ref: '#/components/schemas/error'\n    unsupported_media_type:\n      description: Unsupported Media Type\n      content:\n        application/vnd.clair.error.v1+json:\n          schema:\n            $ref: '#/components/schemas/error'\n        application/json:\n          schema:\n            $ref: '#/components/schemas/error'\n  parameters:\n    digest:\n      description: OCI-compatible digest of a referred object.\n      name: digest\n      in: path\n      schema:\n        $ref: '#/components/schemas/digest'\n      required: true\n  headers:\n    Clair-Error:\n      description: This is a trailer containing any errors encountered while writing the response.\n      style: simple\n      schema:\n        type: string\n    Etag:\n      description: HTTP [ETag header](https://httpwg.org/specs/rfc9110.html#field.etag)\n      style: simple\n      schema:\n        type: string\n    Link:\n      description: Web Linking [Link header](https://httpwg.org/specs/rfc8288.html#header)\n      style: simple\n      schema:\n        type: string\n    Location:\n      description: HTTP [Location header](https://httpwg.org/specs/rfc9110.html#field.location)\n      style: simple\n      required: true\n      schema:\n        type: string\n  securitySchemes:\n    PSK:\n      type: http\n      scheme: bearer\n      bearerFormat: JWT with preshared key and allow-listed issuers\n      description: |-\n        Clair's authentication scheme.\n\n        This is a [JWT](https://datatracker.ietf.org/doc/html/rfc7519) signed with a configured pre-shared key containing an allowlisted `iss` claim.\n"
  },
  {
    "path": "httptransport/auth.go",
    "content": "package httptransport\n\nimport (\n\t\"errors\"\n\t\"net/http\"\n\n\t\"github.com/quay/clair/config\"\n\n\t\"github.com/quay/clair/v4/middleware/auth\"\n)\n\n// AuthHandler returns an http.Handler wrapping the provided Handler, as\n// described by the provided Config.\nfunc authHandler(cfg *config.Config, next http.Handler) (http.Handler, error) {\n\tvar checks []auth.Checker\n\n\t// Keep this ordered \"best\" to \"worst\".\n\tswitch {\n\tcase cfg.Auth.PSK != nil:\n\t\tcfg := cfg.Auth.PSK\n\t\tissuers := make([]string, 0, 1+len(cfg.Issuer))\n\t\tissuers = append(issuers, IntraserviceIssuer)\n\t\tissuers = append(issuers, cfg.Issuer...)\n\n\t\tpsk, err := auth.NewPSK(cfg.Key, issuers)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tchecks = append(checks, psk)\n\tcase cfg.Auth.Keyserver != nil:\n\t\treturn nil, errors.New(\"quay keyserver support has been removed\")\n\tdefault:\n\t\treturn next, nil\n\t}\n\n\treturn auth.Handler(next, checks...), nil\n}\n"
  },
  {
    "path": "httptransport/auth_test.go",
    "content": "package httptransport\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/rand\"\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/go-jose/go-jose/v3/jwt\"\n\t\"github.com/quay/clair/config\"\n\t\"github.com/quay/claircore/test\"\n\n\t\"github.com/quay/clair/v4/internal/httputil\"\n)\n\ntype authTestcase struct {\n\tClaims     *jwt.Claims\n\tConfigMod  func(*testing.T, *config.Config)\n\tConfig     config.Config\n\tName       string\n\tShouldFail bool\n}\n\nvar defaultClaims = jwt.Claims{\n\tIssuer: IntraserviceIssuer,\n}\n\nfunc (tc *authTestcase) Run(ctx context.Context) func(*testing.T) {\n\treturn func(t *testing.T) {\n\t\tctx := test.Logging(t, ctx)\n\t\t// Generate a nonce to return upon request.\n\t\tb := make([]byte, 16)\n\t\tif _, err := io.ReadFull(rand.Reader, b); err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tnonce := hex.EncodeToString(b)\n\n\t\t// Return the nonce when called.\n\t\tnext := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tif a := r.Header.Get(\"authorization\"); a != \"\" {\n\t\t\t\tt.Logf(\"Authorization: %s\", a)\n\t\t\t}\n\t\t\tfmt.Fprint(w, nonce)\n\t\t})\n\n\t\t// Create a handler that has auth according to the config.\n\t\th, err := authHandler(&tc.Config, next)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t}\n\n\t\t// Wire up the handler to a test server.\n\t\tsrv := httptest.NewUnstartedServer(h)\n\t\tsrv.Config.BaseContext = func(_ net.Listener) context.Context { return ctx }\n\t\tsrv.Start()\n\t\tdefer srv.Close()\n\n\t\ttc.Config.Matcher.IndexerAddr = srv.URL\n\t\t// Modify the config, if present\n\t\tif f := tc.ConfigMod; f != nil {\n\t\t\tf(t, &tc.Config)\n\t\t}\n\n\t\t// Use a default intraservice claim if not set.\n\t\tif tc.Claims == nil {\n\t\t\ttc.Claims = &defaultClaims\n\t\t}\n\t\ts, err := httputil.NewSigner(ctx, &tc.Config, *tc.Claims)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t}\n\t\treq, err := httputil.NewRequestWithContext(ctx, http.MethodGet, srv.URL, nil)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t}\n\t\tif err := s.Sign(ctx, req); err != nil {\n\t\t\tt.Error(err)\n\t\t}\n\t\tif t.Failed() {\n\t\t\tt.FailNow()\n\t\t}\n\n\t\t// Make the request.\n\t\tres, err := srv.Client().Do(req)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tdefer res.Body.Close()\n\t\twantStatus := http.StatusOK\n\t\tif tc.ShouldFail {\n\t\t\twantStatus = http.StatusUnauthorized\n\t\t}\n\t\tt.Logf(\"status code: %v\", res.StatusCode)\n\t\tif res.StatusCode != wantStatus {\n\t\t\tt.Fail()\n\t\t}\n\t\tvar buf bytes.Buffer\n\t\tif _, err := io.Copy(&buf, res.Body); err != nil {\n\t\t\tt.Error(err)\n\t\t}\n\n\t\t// Compare the nonce.\n\t\tgot, want := buf.String(), nonce\n\t\tt.Logf(\"http request, got: %q want: %q\", got, want)\n\t\tif got != want && !tc.ShouldFail {\n\t\t\tt.Fail()\n\t\t}\n\t}\n}\n\n// TestAuth tests configuring both http server and client.\nfunc TestAuth(t *testing.T) {\n\tfakeKey := []byte(\"deadbeef\")\n\ttt := []authTestcase{\n\t\t{Name: \"None\"},\n\t\t{\n\t\t\tName: \"PSK\",\n\t\t\tConfig: config.Config{\n\t\t\t\tAuth: config.Auth{\n\t\t\t\t\tPSK: &config.AuthPSK{\n\t\t\t\t\t\tIssuer: []string{`sweet-bro`},\n\t\t\t\t\t\tKey:    fakeKey,\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: \"PSKMultipleIssuer\",\n\t\t\tConfig: config.Config{\n\t\t\t\tAuth: config.Auth{\n\t\t\t\t\tPSK: &config.AuthPSK{\n\t\t\t\t\t\tIssuer: []string{`sweet-bro`, `hella-jeff`, `geromy`},\n\t\t\t\t\t\tKey:    fakeKey,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tClaims: &jwt.Claims{Issuer: `geromy`},\n\t\t},\n\t\t{\n\t\t\tName: \"PSKBadKey\",\n\t\t\tConfig: config.Config{\n\t\t\t\tAuth: config.Auth{\n\t\t\t\t\tPSK: &config.AuthPSK{\n\t\t\t\t\t\tIssuer: []string{`sweet-bro`},\n\t\t\t\t\t\tKey:    fakeKey,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tShouldFail: true,\n\t\t\tConfigMod:  func(_ *testing.T, cfg *config.Config) { cfg.Auth.PSK.Key = []byte(\"badbeef\") },\n\t\t},\n\t\t{\n\t\t\tName: \"PSKFail\",\n\t\t\tConfig: config.Config{\n\t\t\t\tAuth: config.Auth{\n\t\t\t\t\tPSK: &config.AuthPSK{\n\t\t\t\t\t\tIssuer: []string{`sweet-bro`},\n\t\t\t\t\t\tKey:    fakeKey,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tShouldFail: true,\n\t\t\tConfigMod:  func(_ *testing.T, cfg *config.Config) { cfg.Auth.PSK = nil },\n\t\t},\n\t}\n\n\tctx := test.Logging(t)\n\tfor _, tc := range tt {\n\t\tt.Run(tc.Name, tc.Run(ctx))\n\t}\n}\n"
  },
  {
    "path": "httptransport/client/httpclient.go",
    "content": "package client\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"sync\"\n\t\"sync/atomic\"\n\n\t\"github.com/quay/claircore/libvuln/driver\"\n)\n\n// UoCache caches an UpdateOperation map when the server provides a conditional\n// response.\ntype uoCache struct {\n\tsync.RWMutex\n\tuo        map[string][]driver.UpdateOperation\n\tvalidator string\n}\n\n// Set persists the update operations map and its associated validator string\n// used in conditional requests.\n//\n// It is safe for concurrent use.\nfunc (c *uoCache) Set(m map[string][]driver.UpdateOperation, v string) {\n\tc.Lock()\n\tdefer c.Unlock()\n\tc.uo = m\n\tc.validator = v\n}\n\n// Copy returns a copy of the cache contents to the caller.\n//\n// It is safe for concurrent use.\nfunc (c *uoCache) Copy() map[string][]driver.UpdateOperation {\n\tm := map[string][]driver.UpdateOperation{}\n\tc.RLock()\n\tdefer c.RUnlock()\n\tfor u, ops := range c.uo {\n\t\to := make([]driver.UpdateOperation, len(ops))\n\t\tcopy(o, ops)\n\t\tm[u] = o\n\t}\n\treturn m\n}\n\nfunc newOUCache() *uoCache {\n\treturn &uoCache{\n\t\tRWMutex: sync.RWMutex{},\n\t}\n}\n\n// HTTP implements access to clair interfaces over HTTP\ntype HTTP struct {\n\tdiffValidator atomic.Value\n\taddr          *url.URL\n\tc             *http.Client\n\tuoCache       *uoCache\n\tuoLatestCache *uoCache\n\tsigner        Signer\n}\n\n// DefaultAddr is used if the WithAddr Option isn't provided to New.\n//\n// This uses the default service port, and should just work if a containerized\n// deployment has a service configured that hairpins and routes correctly.\nconst DefaultAddr = `http://clair:6060/`\n\n// NewHTTP is a constructor for an HTTP client.\nfunc NewHTTP(ctx context.Context, opt ...Option) (*HTTP, error) {\n\taddr, err := url.Parse(DefaultAddr)\n\tif err != nil {\n\t\tpanic(\"programmer error\") // Why didn't the DefaultAddr parse?\n\t}\n\n\tc := &HTTP{\n\t\taddr:          addr,\n\t\tc:             http.DefaultClient,\n\t\tuoCache:       newOUCache(),\n\t\tuoLatestCache: newOUCache(),\n\t}\n\tc.diffValidator.Store(\"\")\n\n\tfor _, o := range opt {\n\t\tif err := o(c); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\treturn c, nil\n}\n\n// Option sets an option on an HTTP.\ntype Option func(*HTTP) error\n\n// WithAddr sets the address to talk to.\n//\n// The client doesn't support providing multiple addresses, so the provided\n// address should most likely have some form of load balancing or routing.\n//\n// The provided URL should not include the `/api/v1` prefix.\nfunc WithAddr(root string) Option {\n\tu, err := url.Parse(root)\n\treturn func(s *HTTP) error {\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\ts.addr = u\n\t\treturn nil\n\t}\n}\n\n// WithClient sets the http.Client used for requests.\n//\n// If WithClient is not supplied to NewHTTP, http.DefaultClient is used.\nfunc WithClient(c *http.Client) Option {\n\treturn func(s *HTTP) error {\n\t\ts.c = c\n\t\treturn nil\n\t}\n}\n\nfunc WithSigner(v Signer) Option {\n\treturn func(s *HTTP) error {\n\t\ts.signer = v\n\t\treturn nil\n\t}\n}\n\ntype Signer interface {\n\tSign(context.Context, *http.Request) error\n}\n\nfunc (s *HTTP) sign(ctx context.Context, req *http.Request) error {\n\tif s.signer == nil {\n\t\treturn nil\n\t}\n\treturn s.signer.Sign(ctx, req)\n}\n"
  },
  {
    "path": "httptransport/client/indexer.go",
    "content": "package client\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"path\"\n\n\t\"github.com/quay/claircore\"\n\n\tclairerror \"github.com/quay/clair/v4/clair-error\"\n\t\"github.com/quay/clair/v4/httptransport\"\n\t\"github.com/quay/clair/v4/indexer\"\n\t\"github.com/quay/clair/v4/internal/codec\"\n\t\"github.com/quay/clair/v4/internal/httputil\"\n)\n\nvar _ indexer.Service = (*HTTP)(nil)\n\nfunc (s *HTTP) AffectedManifests(ctx context.Context, v []claircore.Vulnerability) (*claircore.AffectedManifests, error) {\n\tu, err := s.addr.Parse(httptransport.AffectedManifestAPIPath)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to parse api address: %v\", err)\n\t}\n\trd := codec.JSONReader(struct {\n\t\tV []claircore.Vulnerability `json:\"vulnerabilities\"`\n\t}{\n\t\tv,\n\t})\n\treq, err := httputil.NewRequestWithContext(ctx, http.MethodPost, u.String(), rd)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create request: %v\", err)\n\t}\n\tif err := s.sign(ctx, req); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create request: %v\", err)\n\t}\n\treq.Header.Set(\"content-type\", `application/json`)\n\tresp, err := s.c.Do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer resp.Body.Close()\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn nil, &clairerror.ErrRequestFail{\n\t\t\tCode:   resp.StatusCode,\n\t\t\tStatus: resp.Status,\n\t\t}\n\t}\n\n\tvar a claircore.AffectedManifests\n\tswitch ct := req.Header.Get(\"content-type\"); ct {\n\tcase \"\", `application/json`:\n\t\tdec := codec.GetDecoder(resp.Body)\n\t\tdefer codec.PutDecoder(dec)\n\t\tif err := dec.Decode(&a); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unrecognized content-type %q\", ct)\n\t}\n\treturn &a, nil\n}\n\n// Index receives a Manifest and returns a IndexReport providing the indexed\n// items in the resulting image.\n//\n// Index blocks until completion. An error is returned if the index operation\n// could not start. If an error occurs during the index operation the error will\n// be preset on the IndexReport.Err field of the returned IndexReport.\nfunc (s *HTTP) Index(ctx context.Context, manifest *claircore.Manifest) (*claircore.IndexReport, error) {\n\tu, err := s.addr.Parse(httptransport.IndexAPIPath)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create request: %v\", err)\n\t}\n\n\treq, err := httputil.NewRequestWithContext(ctx, http.MethodPost, u.String(), codec.JSONReader(manifest))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create request: %v\", err)\n\t}\n\tif err := s.sign(ctx, req); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create request: %v\", err)\n\t}\n\treq.Header.Set(\"content-type\", `application/json`)\n\tresp, err := s.c.Do(req)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to do request: %v\", err)\n\t}\n\tdefer resp.Body.Close()\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn nil, &clairerror.ErrRequestFail{\n\t\t\tCode:   resp.StatusCode,\n\t\t\tStatus: resp.Status,\n\t\t}\n\t}\n\n\tvar ir claircore.IndexReport\n\tswitch ct := resp.Header.Get(\"content-type\"); ct {\n\tcase \"\", `application/json`:\n\t\tdec := codec.GetDecoder(resp.Body)\n\t\tdefer codec.PutDecoder(dec)\n\t\tif err := dec.Decode(&ir); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unrecognized content-type %q\", ct)\n\t}\n\treturn &ir, nil\n}\n\n// IndexReport retrieves a IndexReport given a manifest hash string\nfunc (s *HTTP) IndexReport(ctx context.Context, manifest claircore.Digest) (*claircore.IndexReport, bool, error) {\n\tu, err := s.addr.Parse(path.Join(httptransport.IndexReportAPIPath, manifest.String()))\n\tif err != nil {\n\t\treturn nil, false, fmt.Errorf(\"failed to create request: %v\", err)\n\t}\n\n\treq, err := httputil.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)\n\tif err != nil {\n\t\treturn nil, false, fmt.Errorf(\"failed to create request: %v\", err)\n\t}\n\tif err := s.sign(ctx, req); err != nil {\n\t\treturn nil, false, fmt.Errorf(\"failed to create request: %v\", err)\n\t}\n\tresp, err := s.c.Do(req)\n\tif err != nil {\n\t\treturn nil, false, fmt.Errorf(\"failed to do request: %v\", err)\n\t}\n\tdefer resp.Body.Close()\n\tswitch resp.StatusCode {\n\tcase http.StatusOK:\n\tcase http.StatusNotFound:\n\t\treturn nil, false, nil\n\tdefault:\n\t\treturn nil, false, &clairerror.ErrIndexReportRetrieval{\n\t\t\tE: &clairerror.ErrRequestFail{\n\t\t\t\tCode:   resp.StatusCode,\n\t\t\t\tStatus: resp.Status,\n\t\t\t},\n\t\t}\n\t}\n\n\tir := &claircore.IndexReport{}\n\tdec := codec.GetDecoder(resp.Body)\n\tdefer codec.PutDecoder(dec)\n\tif err := dec.Decode(ir); err != nil {\n\t\treturn nil, false, &clairerror.ErrBadIndexReport{E: err}\n\t}\n\treturn ir, true, nil\n}\n\nfunc (s *HTTP) State(ctx context.Context) (string, error) {\n\tu, err := s.addr.Parse(httptransport.IndexStateAPIPath)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create request: %v\", err)\n\t}\n\treq, err := httputil.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create request: %v\", err)\n\t}\n\tif err := s.sign(ctx, req); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create request: %v\", err)\n\t}\n\tresp, err := s.c.Do(req)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to do request: %v\", err)\n\t}\n\tdefer resp.Body.Close()\n\tbuf := &bytes.Buffer{}\n\tif _, err := buf.ReadFrom(resp.Body); err != nil {\n\t\treturn \"\", err\n\t}\n\treturn buf.String(), nil\n}\n\n// DeleteManifests deletes the specified manifests.\n//\n// Passing a digest of an unknown manifest is not an error.\nfunc (s *HTTP) DeleteManifests(ctx context.Context, d ...claircore.Digest) ([]claircore.Digest, error) {\n\t// This implementation always uses the bulk delete endpoint.\n\tu, err := s.addr.Parse(httptransport.IndexAPIPath)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create request: %v\", err)\n\t}\n\treq, err := httputil.NewRequestWithContext(ctx, http.MethodDelete, u.String(), codec.JSONReader(d))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create request: %v\", err)\n\t}\n\tif err := s.sign(ctx, req); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create request: %v\", err)\n\t}\n\tresp, err := s.c.Do(req)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to do request: %v\", err)\n\t}\n\tdefer resp.Body.Close()\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn nil, fmt.Errorf(\"unexpected response status: %v\", resp.Status)\n\t}\n\tvar ret []claircore.Digest\n\tdec := codec.GetDecoder(resp.Body)\n\tdefer codec.PutDecoder(dec)\n\tif err := dec.Decode(&ret); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to decode response: %w\", err)\n\t}\n\treturn ret, nil\n}\n"
  },
  {
    "path": "httptransport/client/matcher.go",
    "content": "package client\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/google/uuid\"\n\t\"github.com/quay/claircore\"\n\t\"github.com/quay/claircore/libvuln/driver\"\n\n\tclairerror \"github.com/quay/clair/v4/clair-error\"\n\t\"github.com/quay/clair/v4/httptransport\"\n\t\"github.com/quay/clair/v4/internal/codec\"\n\t\"github.com/quay/clair/v4/internal/httputil\"\n\t\"github.com/quay/clair/v4/matcher\"\n)\n\nvar _ matcher.Service = (*HTTP)(nil)\n\nfunc (c *HTTP) Scan(ctx context.Context, ir *claircore.IndexReport) (*claircore.VulnerabilityReport, error) {\n\tu, err := c.addr.Parse(httptransport.VulnerabilityReportPath)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treq, err := httputil.NewRequestWithContext(ctx, http.MethodPost, u.String(), codec.JSONReader(ir))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create request: %v\", err)\n\t}\n\tif err := c.sign(ctx, req); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create request: %v\", err)\n\t}\n\treq.Header.Set(\"content-type\", `application/json`)\n\tresp, err := c.c.Do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer resp.Body.Close()\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn nil, &clairerror.ErrRequestFail{\n\t\t\tCode:   resp.StatusCode,\n\t\t\tStatus: resp.Status,\n\t\t}\n\t}\n\n\tvar vr claircore.VulnerabilityReport\n\tswitch ct := req.Header.Get(\"content-type\"); ct {\n\tcase \"\", `application/json`:\n\t\tdec := codec.GetDecoder(resp.Body)\n\t\tdefer codec.PutDecoder(dec)\n\t\tif err := dec.Decode(&vr); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unrecognized content-type %q\", ct)\n\t}\n\treturn &vr, nil\n}\n\n// DeleteUpdateOperations attempts to delete the referenced update operations.\nfunc (c *HTTP) DeleteUpdateOperations(ctx context.Context, ref ...uuid.UUID) (int64, error) {\n\tu, err := c.addr.Parse(httptransport.UpdateOperationDeleteAPIPath)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\t// Spawn a few requests that will write their result into \"errs\".\n\t//\n\t// These'll most likely be multiplexed and to the same host, so pick a nice\n\t// lowish number like 4.\n\t//\n\t// Don't use an errgroup because we want to actually issue all the DELETEs,\n\t// not stop all requests on the first error.\n\tvar wg sync.WaitGroup\n\titem := make(chan int)\n\terrs := make([]error, len(ref))\n\tfor i := 0; i < 4; i++ {\n\t\twg.Add(1)\n\t\tgo func() {\n\t\t\tdefer wg.Done()\n\t\t\tfor i := range item {\n\t\t\t\tu, err := u.Parse(ref[i].String())\n\t\t\t\tif err != nil {\n\t\t\t\t\terrs[i] = err\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\treq, err := httputil.NewRequestWithContext(ctx, http.MethodDelete, u.String(), nil)\n\t\t\t\tif err != nil {\n\t\t\t\t\terrs[i] = err\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif err := c.sign(ctx, req); err != nil {\n\t\t\t\t\terrs[i] = fmt.Errorf(\"failed to create request: %v\", err)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tres, err := c.c.Do(req)\n\t\t\t\tif err != nil {\n\t\t\t\t\terrs[i] = err\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tdefer res.Body.Close()\n\t\t\t\tif got, want := res.StatusCode, http.StatusOK; got != want {\n\t\t\t\t\terrs[i] = fmt.Errorf(\"%v: unexpected status: %s\", u.Path, res.Status)\n\t\t\t\t}\n\t\t\t}\n\t\t}()\n\t}\n\tfor i, lim := 0, len(ref); i < lim; i++ {\n\t\titem <- i\n\t}\n\tclose(item)\n\twg.Wait()\n\n\tvar b strings.Builder\n\tvar errd bool\n\tdeleted := int64(len(ref))\n\tfor _, err := range errs {\n\t\tif err != nil {\n\t\t\tdeleted--\n\t\t\tif errd {\n\t\t\t\tb.WriteByte('\\n')\n\t\t\t}\n\t\t\tb.WriteString(err.Error())\n\t\t\terrd = true\n\t\t}\n\t}\n\n\tif errd {\n\t\treturn deleted, errors.New(\"deletion errors: \" + b.String())\n\t}\n\treturn deleted, nil\n}\n\n// LatestUpdateOperation shouldn't be used by client code and is implemented\n// only to satisfy the matcher.Differ interface.\nfunc (c *HTTP) LatestUpdateOperation(_ context.Context, _ driver.UpdateKind) (uuid.UUID, error) {\n\treturn uuid.Nil, nil\n}\n\n// UpdateOperations returns all the known UpdateOperations per updater.\nfunc (c *HTTP) UpdateOperations(ctx context.Context, k driver.UpdateKind, updaters ...string) (map[string][]driver.UpdateOperation, error) {\n\tu, err := c.addr.Parse(httptransport.UpdateOperationAPIPath)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tv := url.Values{}\n\tv.Add(\"kind\", string(k))\n\tu.RawQuery = v.Encode()\n\treq, err := httputil.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif err := c.sign(ctx, req); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create request: %v\", err)\n\t}\n\treturn c.updateOperations(ctx, req, c.uoCache)\n}\n\n// LatestUpdateOperations returns the most recent UpdateOperation per updater.\nfunc (c *HTTP) LatestUpdateOperations(ctx context.Context, k driver.UpdateKind) (map[string][]driver.UpdateOperation, error) {\n\tu, err := c.addr.Parse(httptransport.UpdateOperationAPIPath)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tv := url.Values{}\n\tv.Add(\"latest\", \"true\")\n\tv.Add(\"kind\", string(k))\n\tu.RawQuery = v.Encode()\n\n\treq, err := httputil.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// check the cache validator and pass our ouCache to\n\t// updateOperations\n\tc.uoLatestCache.RLock()\n\tif c.uoLatestCache.validator != \"\" {\n\t\treq.Header.Set(\"if-none-match\", c.uoLatestCache.validator)\n\t}\n\tc.uoLatestCache.RUnlock()\n\treturn c.updateOperations(ctx, req, c.uoLatestCache)\n}\n\n// updateOperations is a private method implementing the common bits for retrieving UpdateOperations\n//\n// an ouCache is passed in by the caller to cache any responses providing an etag.\n// if a subsequent response provides a StatusNotModified status, the map of UpdateOprations is served from cache.\nfunc (c *HTTP) updateOperations(ctx context.Context, req *http.Request, cache *uoCache) (map[string][]driver.UpdateOperation, error) {\n\tif err := c.sign(ctx, req); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create request: %v\", err)\n\t}\n\tres, err := c.c.Do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer res.Body.Close()\n\tswitch res.StatusCode {\n\tcase http.StatusOK:\n\t\tm := make(map[string][]driver.UpdateOperation)\n\t\tdec := codec.GetDecoder(res.Body)\n\t\tdefer codec.PutDecoder(dec)\n\t\tif err := dec.Decode(&m); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\t// check for etag, if exists store the value and add returned map\n\t\t// to cache\n\t\tif v := res.Header.Get(\"etag\"); v != \"\" && !strings.HasPrefix(v, \"W/\") {\n\t\t\tcache.Set(m, v)\n\t\t}\n\t\treturn cache.Copy(), nil\n\tcase http.StatusNotModified:\n\t\treturn cache.Copy(), nil\n\tdefault:\n\t}\n\treturn nil, fmt.Errorf(\"%v: unexpected status: %s\", req.URL.Path, res.Status)\n}\n\n// UpdateDiff reports the diff of two update operations, identified by the\n// provided refs.\n//\n// \"Prev\" may be passed uuid.Nil if the client's last known state has been\n// forgotten by the server.\nfunc (c *HTTP) UpdateDiff(ctx context.Context, prev, cur uuid.UUID) (*driver.UpdateDiff, error) {\n\tu, err := c.addr.Parse(httptransport.UpdateDiffAPIPath)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treq, err := httputil.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tv := req.URL.Query()\n\tif prev != uuid.Nil {\n\t\tv.Set(\"prev\", prev.String())\n\t}\n\tv.Set(\"cur\", cur.String())\n\treq.URL.RawQuery = v.Encode()\n\tif err := c.sign(ctx, req); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create request: %v\", err)\n\t}\n\n\tres, err := c.c.Do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer res.Body.Close()\n\tif res.StatusCode != http.StatusOK {\n\t\treturn nil, fmt.Errorf(\"%v: unexpected status: %s\", u.Path, res.Status)\n\t}\n\td := driver.UpdateDiff{}\n\tdec := codec.GetDecoder(res.Body)\n\tdefer codec.PutDecoder(dec)\n\tif err := dec.Decode(&d); err != nil {\n\t\treturn nil, err\n\t}\n\treturn &d, nil\n}\n\n// Initialized is present to fulfill the interface, but isn't exposed as part of\n// the HTTP API. This method is stubbed out.\nfunc (c *HTTP) Initialized(_ context.Context) (bool, error) {\n\treturn true, nil\n}\n"
  },
  {
    "path": "httptransport/client/matcher_test.go",
    "content": "package client_test\n\nimport (\n\t\"encoding/json\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"path\"\n\t\"strconv\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/google/uuid\"\n\t\"github.com/quay/claircore/libvuln/driver\"\n\n\t\"github.com/quay/clair/v4/httptransport\"\n\t\"github.com/quay/clair/v4/httptransport/client\"\n)\n\n// TestDiffer puts the Differ methods of the client through its paces.\nfunc TestDiffer(t *testing.T) {\n\tctx := t.Context()\n\n\tt.Run(\"OK\", func(t *testing.T) {\n\t\tt.Run(\"Delete\", func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\t// Generate a set of refs.\n\t\t\trefs := make([]uuid.UUID, 10)\n\t\t\texpected := make(map[string]struct{}, 10)\n\t\t\tfor i := range refs {\n\t\t\t\tid := uuid.New()\n\t\t\t\trefs[i] = id\n\t\t\t\texpected[id.String()] = struct{}{}\n\t\t\t}\n\n\t\t\t// Spin up a server that mocks a delete call.\n\t\t\tsrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\tif r.Method != http.MethodDelete {\n\t\t\t\t\tw.WriteHeader(http.StatusMethodNotAllowed)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif !strings.HasPrefix(r.URL.Path, httptransport.UpdateOperationAPIPath) {\n\t\t\t\t\tw.WriteHeader(http.StatusNotFound)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tgot := path.Base(r.URL.Path)\n\t\t\t\tt.Logf(\"got: %s\", got)\n\t\t\t\tif _, ok := expected[got]; !ok {\n\t\t\t\t\tw.WriteHeader(http.StatusInternalServerError)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tw.WriteHeader(http.StatusOK)\n\t\t\t}))\n\t\t\tdefer srv.Close()\n\n\t\t\t// Create a client.\n\t\t\tc, err := client.NewHTTP(ctx, client.WithAddr(srv.URL))\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\t// Do the call.\n\t\t\tdeleted, err := c.DeleteUpdateOperations(ctx, refs...)\n\t\t\tif err != nil {\n\t\t\t\tt.Error(err)\n\t\t\t}\n\t\t\tif deleted != int64(len(refs)) {\n\t\t\t\tt.Errorf(\"got: %v, want: %v\", deleted, len(refs))\n\t\t\t}\n\t\t})\n\n\t\tt.Run(\"Latest\", func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\t// Generate a set of names and refs.\n\t\t\twant := make(map[string][]driver.UpdateOperation)\n\t\t\tfor i := 0; i < 10; i++ {\n\t\t\t\twant[strconv.Itoa(i)] = []driver.UpdateOperation{\n\t\t\t\t\t{\n\t\t\t\t\t\tRef:         uuid.New(),\n\t\t\t\t\t\tDate:        time.Now(),\n\t\t\t\t\t\tFingerprint: \"xyz\",\n\t\t\t\t\t\tUpdater:     \"test-updater\",\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t}\n\t\t\tvalidator := `\"validator\"`\n\n\t\t\t// Spin up a server that mocks a latest call.\n\t\t\tsrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\tif r.Method != http.MethodGet {\n\t\t\t\t\tw.WriteHeader(http.StatusMethodNotAllowed)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif !strings.HasPrefix(r.URL.Path, httptransport.UpdateOperationAPIPath) {\n\t\t\t\t\tw.WriteHeader(http.StatusNotFound)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif v := r.Header.Get(\"If-None-Match\"); v != \"\" && v == validator {\n\t\t\t\t\tw.WriteHeader(http.StatusNotModified)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tw.Header().Set(\"etag\", validator)\n\n\t\t\t\tif err := json.NewEncoder(w).Encode(want); err != nil {\n\t\t\t\t\tt.Error(err)\n\t\t\t\t}\n\t\t\t}))\n\t\t\tdefer srv.Close()\n\n\t\t\t// Create a client.\n\t\t\tc, err := client.NewHTTP(ctx, client.WithAddr(srv.URL))\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\tt.Run(\"Initial\", func(t *testing.T) {\n\t\t\t\t// Do the call.\n\t\t\t\tgot, err := c.LatestUpdateOperations(ctx, driver.VulnerabilityKind)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Error(err)\n\t\t\t\t}\n\t\t\t\tif !cmp.Equal(got, want) {\n\t\t\t\t\tt.Error(cmp.Diff(got, want))\n\t\t\t\t}\n\t\t\t})\n\t\t\t// second attempt will be served from cache\n\t\t\tt.Run(\"Second\", func(t *testing.T) {\n\t\t\t\t// Do the call.\n\t\t\t\tgot, err := c.LatestUpdateOperations(ctx, driver.VulnerabilityKind)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Error(err)\n\t\t\t\t}\n\t\t\t\tif !cmp.Equal(got, want) {\n\t\t\t\t\tt.Error(cmp.Diff(got, want))\n\t\t\t\t}\n\t\t\t})\n\t\t})\n\n\t\tt.Run(\"Diff\", func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\t// Create two refs and a delta between them.\n\t\t\tprev, cur := uuid.New(), uuid.New()\n\t\t\twant := &driver.UpdateDiff{\n\t\t\t\tPrev:    driver.UpdateOperation{Ref: prev},\n\t\t\t\tCur:     driver.UpdateOperation{Ref: cur},\n\t\t\t\tAdded:   nil,\n\t\t\t\tRemoved: nil,\n\t\t\t}\n\n\t\t\t// Spin up a server that mocks the diff call.\n\t\t\tsrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\tif r.Method != http.MethodGet {\n\t\t\t\t\tw.WriteHeader(http.StatusMethodNotAllowed)\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tprevStr, curStr := r.FormValue(\"prev\"), r.FormValue(\"cur\")\n\t\t\t\tif got, want := prevStr, prev.String(); got != want {\n\t\t\t\t\tt.Errorf(\"got: %q, want: %q\", got, want)\n\t\t\t\t}\n\t\t\t\tif got, want := curStr, cur.String(); got != want {\n\t\t\t\t\tt.Errorf(\"got: %q, want: %q\", got, want)\n\t\t\t\t}\n\t\t\t\tif t.Failed() {\n\t\t\t\t\tw.WriteHeader(http.StatusBadRequest)\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tif err := json.NewEncoder(w).Encode(want); err != nil {\n\t\t\t\t\tt.Error(err)\n\t\t\t\t}\n\t\t\t}))\n\t\t\tdefer srv.Close()\n\n\t\t\t// Create a client.\n\t\t\tc, err := client.NewHTTP(ctx, client.WithAddr(srv.URL))\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\t// Do the call.\n\t\t\tgot, err := c.UpdateDiff(ctx, prev, cur)\n\t\t\tif err != nil {\n\t\t\t\tt.Error(err)\n\t\t\t}\n\t\t\tif !cmp.Equal(got, want) {\n\t\t\t\tt.Error(cmp.Diff(got, want))\n\t\t\t}\n\t\t})\n\t})\n}\n"
  },
  {
    "path": "httptransport/common.go",
    "content": "package httptransport\n\nimport (\n\tcrand \"crypto/rand\"\n\t\"encoding/binary\"\n\t\"errors\"\n\t\"fmt\"\n\t\"math/rand\"\n\t\"mime\"\n\t\"net/http\"\n\t\"path\"\n\t\"runtime/pprof\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/quay/claircore\"\n\t\"github.com/quay/claircore/toolkit/log\"\n\t\"go.opentelemetry.io/otel/trace\"\n)\n\n// GetDigest removes the last path element and parses it as a digest.\nfunc getDigest(_ http.ResponseWriter, r *http.Request) (d claircore.Digest, err error) {\n\tdStr := path.Base(r.URL.Path)\n\tif dStr == \"\" {\n\t\treturn d, errors.New(\"provide a single manifest hash\")\n\t}\n\treturn claircore.ParseDigest(dStr)\n}\n\n// PickContentType sets the response's \"Content-Type\" header.\n//\n// If \"Accept\" headers are not present in the request, the first element of the\n// \"allow\" slice is used.\n//\n// If \"Accept\" headers are present, the first (ordered by \"q\" value) media type\n// in the \"allow\" slice is chosen. If there are no common media types, \"415\n// Unsupported Media Type\" is written and ErrMediaType is reported.\nfunc pickContentType(w http.ResponseWriter, r *http.Request, allow []string) error {\n\t// There's no canonical algorithm for this, it's all server-dependent\n\t// behavior. Our algorithm is:\n\t//\n\t//\t- Parse the Accept header(s) as MIME media types joined by commas.\n\t//\t- Stable sort according to the \"q\" parameter, defaulting to 1.0 if\n\t//\t  omitted (as specified)\n\t//\t- Pick the first match.\n\t//\n\t// BUG(hank) Content type negotiation does an O(n*m) comparison driven on\n\t// user input, which may be a DoS issue.\n\tas, ok := r.Header[\"Accept\"]\n\tif !ok {\n\t\tw.Header().Set(\"content-type\", allow[0])\n\t\treturn nil\n\t}\n\tvar acceptable []accept\n\tfor _, part := range as {\n\t\tfor _, s := range strings.Split(part, \",\") {\n\t\t\ta := accept{}\n\t\t\tmt, p, err := mime.ParseMediaType(strings.TrimSpace(s))\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\ta.Q = 1.0\n\t\t\tif qs, ok := p[\"q\"]; ok {\n\t\t\t\ta.Q, _ = strconv.ParseFloat(qs, 64)\n\t\t\t}\n\t\t\ttyp := strings.Split(mt, \"/\")\n\t\t\ta.Type = typ[0]\n\t\t\ta.Subtype = typ[1]\n\t\t\tacceptable = append(acceptable, a)\n\t\t}\n\t}\n\tif len(acceptable) == 0 {\n\t\tw.Header().Set(\"content-type\", allow[0])\n\t\treturn nil\n\t}\n\tsort.SliceStable(acceptable, func(i, j int) bool { return acceptable[i].Q > acceptable[j].Q })\n\tfor _, l := range acceptable {\n\t\tfor _, a := range allow {\n\t\t\tif l.Match(a) {\n\t\t\t\tw.Header().Set(\"content-type\", a)\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t}\n\tw.WriteHeader(http.StatusUnsupportedMediaType)\n\treturn ErrMediaType\n}\n\n// ErrMediaType is returned if no common media types can be found for a given\n// request.\nvar ErrMediaType = errors.New(\"no common media type\")\n\ntype accept struct {\n\tType, Subtype string\n\tQ             float64\n}\n\n// Match reports whether the type in the \"accept\" struct matches the provided\n// media type, honoring wildcards.\n//\n// Match panics if the provided media type is not well-formed.\nfunc (a *accept) Match(mt string) bool {\n\tif a.Type == \"*\" && a.Subtype == \"*\" {\n\t\treturn true\n\t}\n\ti := strings.IndexByte(mt, '/')\n\tif i == -1 {\n\t\t// Programmer error -- inputs to this function should be static strings.\n\t\tpanic(fmt.Sprintf(\"bad media type: %q\", mt))\n\t}\n\tt, s := mt[:i], mt[i+1:]\n\treturn a.Type == t && (a.Subtype == s || a.Subtype == \"*\")\n}\n\nvar idPool = sync.Pool{\n\tNew: func() interface{} {\n\t\tb := make([]byte, 8)\n\t\tif _, err := crand.Read(b); err != nil {\n\t\t\tpanic(err) // ???\n\t\t}\n\t\ts := binary.LittleEndian.Uint64(b)\n\t\tsrc := rand.NewSource(int64(s))\n\t\treturn rand.New(src)\n\t},\n}\n\n// WithRequestID sets up a per-request ID and annotates the logging context and\n// profile labels.\nfunc withRequestID(r *http.Request) *http.Request {\n\tconst key = `request_id`\n\tconst profile = `profile_id`\n\tctx := r.Context()\n\tsctx := trace.SpanContextFromContext(ctx)\n\tvar tid string\n\tif sctx.HasTraceID() {\n\t\ttid = sctx.TraceID().String()\n\t} else {\n\t\trng := idPool.Get().(*rand.Rand)\n\t\tdefer idPool.Put(rng)\n\t\ttid = fmt.Sprintf(\"%016x\", rng.Uint64())\n\t}\n\tctx = log.With(ctx, key, tid)\n\tctx = pprof.WithLabels(ctx, pprof.Labels(profile, tid))\n\treturn r.WithContext(ctx)\n}\n"
  },
  {
    "path": "httptransport/concurrentlimit.go",
    "content": "package httptransport\n\nimport (\n\t\"log/slog\"\n\t\"net/http\"\n\n\t\"github.com/prometheus/client_golang/prometheus\"\n\t\"github.com/prometheus/client_golang/prometheus/promauto\"\n\t\"golang.org/x/sync/semaphore\"\n)\n\nvar concurrentLimitedCounter = promauto.NewCounterVec(\n\tprometheus.CounterOpts{\n\t\tNamespace: metricNamespace,\n\t\tSubsystem: metricSubsystem,\n\t\tName:      \"concurrencylimited_total\",\n\t\tHelp:      \"Total number of requests that have been concurrency limited.\",\n\t},\n\t[]string{\"endpoint\", \"method\"},\n)\n\n// LimitHandler is a wrapper to help with concurrency limiting. This is slightly\n// more complicated than the naive approach to allow for filtering on multiple\n// aspects of the request.\n//\n// \"Check\" and \"Next\" need to be populated.\ntype limitHandler struct {\n\t// The Check func inspects the request, and returns the semaphore to use and\n\t// the endpoint to use in metrics. If a nil is returned, the request is\n\t// allowed.\n\tCheck func(*http.Request) (*semaphore.Weighted, string)\n\t// Next is the Handler to forward requests to, if allowed.\n\tNext http.Handler\n}\n\nfunc (l *limitHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {\n\tsem, endpt := l.Check(r)\n\tif sem != nil {\n\t\tif !sem.TryAcquire(1) {\n\t\t\tconcurrentLimitedCounter.WithLabelValues(endpt, r.Method).Add(1)\n\t\t\tctx := r.Context()\n\t\t\tslog.InfoContext(ctx, \"rate limited HTTP request\",\n\t\t\t\t\"remote_addr\", r.RemoteAddr,\n\t\t\t\t\"method\", r.Method,\n\t\t\t\t\"request_uri\", r.RequestURI,\n\t\t\t\t\"status\", http.StatusTooManyRequests)\n\n\t\t\tapiError(ctx, w, http.StatusTooManyRequests, \"server handling too many requests\")\n\t\t}\n\t\tdefer sem.Release(1)\n\t}\n\tl.Next.ServeHTTP(w, r)\n}\n"
  },
  {
    "path": "httptransport/concurrentlimit_test.go",
    "content": "package httptransport\n\nimport (\n\t\"context\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"testing\"\n\n\t\"github.com/quay/claircore/test\"\n\t\"golang.org/x/sync/semaphore\"\n\n\t\"github.com/quay/clair/v4/internal/httputil\"\n)\n\nfunc TestConcurrentRequests(t *testing.T) {\n\tctx := test.Logging(t)\n\tsem := semaphore.NewWeighted(1)\n\t// Ret controls when the http server returns.\n\t// Ready is strobed once the first request is seen.\n\tret, ready := make(chan struct{}), make(chan struct{})\n\tct := new(int64)\n\tvar once sync.Once\n\tsrv := httptest.NewUnstartedServer(&limitHandler{\n\t\tCheck: func(_ *http.Request) (*semaphore.Weighted, string) {\n\t\t\treturn sem, \"\"\n\t\t},\n\t\tNext: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {\n\t\t\tatomic.AddInt64(ct, 1)\n\t\t\tonce.Do(func() { close(ready) })\n\t\t\t<-ret\n\t\t\tw.WriteHeader(http.StatusNoContent)\n\t\t}),\n\t})\n\tsrv.Config.BaseContext = func(_ net.Listener) context.Context { return ctx }\n\tsrv.Start()\n\tdefer srv.Close()\n\tc := srv.Client()\n\n\tdone := make(chan struct{})\n\treq, err := httputil.NewRequestWithContext(ctx, http.MethodGet, srv.URL, nil)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\t// Long-poll goroutine.\n\tgo func() {\n\t\tdefer close(done)\n\t\tres, err := c.Do(req)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\treturn\n\t\t}\n\t\tdefer res.Body.Close()\n\t\tif got, want := res.StatusCode, http.StatusNoContent; got != want {\n\t\t\tt.Errorf(\"got: %d, want: %d\", got, want)\n\t\t}\n\t}()\n\n\t// Wait for the above goroutine to hit the handler.\n\t<-ready\n\tfor i := 0; i < 10; i++ {\n\t\treq, err := httputil.NewRequestWithContext(ctx, http.MethodGet, srv.URL, nil)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"%d: %v\", i, err)\n\t\t}\n\t\tres, err := c.Do(req)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"%d: %v\", i, err)\n\t\t}\n\t\tres.Body.Close()\n\t\tif got, want := res.StatusCode, http.StatusTooManyRequests; got != want {\n\t\t\tt.Errorf(\"got: %d, want: %d\", got, want)\n\t\t}\n\t}\n\tclose(ret)\n\t<-done\n\tif got, want := *ct, int64(1); got != want {\n\t\tt.Errorf(\"got: %d requests, want: %d requests\", got, want)\n\t}\n}\n"
  },
  {
    "path": "httptransport/discoveryhandler.go",
    "content": "package httptransport\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t_ \"embed\" // for json and etag\n\t\"encoding/json\"\n\t\"errors\"\n\t\"io\"\n\t\"log/slog\"\n\t\"net/http\"\n\t\"slices\"\n\t\"sync\"\n\t\"time\"\n\n\t\"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp\"\n\n\t\"github.com/quay/clair/v4/internal/httputil\"\n\t\"github.com/quay/clair/v4/middleware/compress\"\n)\n\n//go:generate env -C api zsh ./openapi.zsh\n\nvar (\n\t//go:embed api/v1/openapi.json\n\topenapiJSON []byte\n\t//go:embed api/v1/openapi.yaml\n\topenapiYAML []byte\n\t//go:embed api/v1/openapi.etag\n\topenapiEtag string\n\n\t// Compacted version of [openapiJSON] for the wire.\n\t//\n\t// Doing this means we can keep a nicer-diffing version checked in.\n\tcompactOpenapiJSON = sync.OnceValue(func() []byte {\n\t\tvar buf bytes.Buffer\n\t\tbuf.Grow(len(openapiJSON))\n\t\tif err := json.Compact(&buf, openapiJSON); err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\tb := buf.Bytes()\n\t\treturn slices.Clip(b)\n\t})\n)\n\n// DiscoveryHandler serves the embedded OpenAPI spec.\nfunc DiscoveryHandler(_ context.Context, prefix string, topt otelhttp.Option) http.Handler {\n\tallow := []string{\n\t\t`application/openapi+json`, `application/openapi+yaml`, // New types: https://datatracker.ietf.org/doc/draft-ietf-httpapi-rest-api-mediatypes/\n\t\t`application/json`, `application/yaml`, // Format types.\n\t\t`application/vnd.oai.openapi+json`, `application/vnd.oai.openapi+yaml`, // Older vendor-tree types.\n\t}\n\t// These functions are written back-to-front.\n\tvar inner http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tctx := r.Context()\n\t\tif r.Method != http.MethodGet {\n\t\t\tapiError(ctx, w, http.StatusMethodNotAllowed, \"endpoint only allows GET\")\n\t\t}\n\t\tswitch err := pickContentType(w, r, allow); {\n\t\tcase errors.Is(err, nil):\n\t\tcase errors.Is(err, ErrMediaType):\n\t\t\tapiError(ctx, w, http.StatusUnsupportedMediaType, \"unable to negotiate common media type for %v\", allow)\n\t\tdefault:\n\t\t\tapiError(ctx, w, http.StatusInternalServerError, \"unexpected error: %v\", err)\n\t\t}\n\t\th := w.Header()\n\t\tkind := h.Get(`Content-Type`)\n\t\tvar src *bytes.Reader\n\t\tswitch kind[len(kind)-4:] {\n\t\tcase \"json\":\n\t\t\tsrc = bytes.NewReader(compactOpenapiJSON())\n\t\tcase \"yaml\":\n\t\t\tsrc = bytes.NewReader(openapiYAML)\n\t\tdefault:\n\t\t\tapiError(ctx, w, http.StatusInternalServerError, \"unexpected error: unknown content-type kind: %q\", kind)\n\t\t}\n\t\th.Set(\"Etag\", openapiEtag)\n\t\tvar err error\n\t\tdefer writerError(w, &err)()\n\t\t_, err = io.Copy(w, src)\n\t})\n\tinner = otelhttp.NewHandler(\n\t\tcompress.Handler(discoverywrapper.wrap(prefix, inner)),\n\t\t\"discovery\",\n\t\totelhttp.WithMessageEvents(otelhttp.ReadEvents, otelhttp.WriteEvents),\n\t\ttopt,\n\t)\n\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tstart := time.Now()\n\t\tr = withRequestID(r)\n\t\tctx := r.Context()\n\t\tvar status int\n\t\tvar length int64\n\t\tw = httputil.ResponseRecorder(&status, &length, w)\n\t\tdefer func() {\n\t\t\tswitch err := http.NewResponseController(w).Flush(); {\n\t\t\tcase errors.Is(err, nil):\n\t\t\tcase errors.Is(err, http.ErrNotSupported):\n\t\t\t\t// Skip\n\t\t\tdefault:\n\t\t\t\tslog.WarnContext(ctx, \"unable to flush http response\",\n\t\t\t\t\t\"reason\", err)\n\t\t\t}\n\t\t\tslog.InfoContext(ctx, \"handled HTTP request\",\n\t\t\t\t\"remote_addr\", r.RemoteAddr,\n\t\t\t\t\"method\", r.Method,\n\t\t\t\t\"request_uri\", r.RequestURI,\n\t\t\t\t\"status\", status,\n\t\t\t\t\"written\", length,\n\t\t\t\t\"duration\", time.Since(start))\n\t\t}()\n\t\tinner.ServeHTTP(w, r)\n\t})\n}\n\nfunc init() {\n\tdiscoverywrapper.init(\"discovery\")\n}\n\nvar discoverywrapper wrapper\n"
  },
  {
    "path": "httptransport/discoveryhandler_test.go",
    "content": "package httptransport\n\nimport (\n\t\"encoding/json\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/quay/claircore/test\"\n\t\"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp\"\n\t\"go.opentelemetry.io/otel/trace/noop\"\n)\n\nfunc TestDiscovery(t *testing.T) {\n\tt.Run(\"Endpoint\", func(t *testing.T) {\n\t\tctx := test.Logging(t)\n\t\th := DiscoveryHandler(ctx, OpenAPIV1Path, otelhttp.WithTracerProvider(noop.NewTracerProvider()))\n\n\t\tr := httptest.NewRecorder()\n\t\treq := httptest.NewRequest(\"GET\", OpenAPIV1Path, nil).WithContext(ctx)\n\t\treq.Header.Set(\"Accept\", \"application/yaml; q=0.4, application/json; q=0.4, application/vnd.oai.openapi+json; q=0.6, application/openapi+json\")\n\t\th.ServeHTTP(r, req)\n\n\t\tresp := r.Result()\n\t\tif resp.StatusCode != http.StatusOK {\n\t\t\tt.Fatalf(\"got status code: %v want status code: %v\", resp.StatusCode, http.StatusOK)\n\t\t}\n\t\tif got, want := resp.Header.Get(\"content-type\"), \"application/openapi+json\"; got != want {\n\t\t\tt.Errorf(\"got: %q, want: %q\", got, want)\n\t\t}\n\n\t\tbuf, err := io.ReadAll(resp.Body)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"failed to ready response body: %v\", err)\n\t\t}\n\n\t\tm := make(map[string]any)\n\t\terr = json.Unmarshal(buf, &m)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"failed to json parse returned bytes: %v\", err)\n\t\t}\n\n\t\tif _, ok := m[\"openapi\"]; !ok {\n\t\t\tt.Fatalf(\"returned json did not container openapi key at the root\")\n\t\t}\n\t\tt.Logf(\"openapi verion: %v\", m[\"openapi\"])\n\t})\n\n\tt.Run(\"Failure\", func(t *testing.T) {\n\t\tctx := test.Logging(t)\n\t\th := DiscoveryHandler(ctx, OpenAPIV1Path, otelhttp.WithTracerProvider(noop.NewTracerProvider()))\n\n\t\tr := httptest.NewRecorder()\n\t\t// Needed because handlers exit the goroutine.\n\t\tdone := make(chan struct{})\n\t\tgo func() {\n\t\t\tdefer close(done)\n\t\t\treq := httptest.NewRequest(\"GET\", OpenAPIV1Path, nil).WithContext(ctx)\n\t\t\treq.Header.Set(\"Accept\", \"application/xml\")\n\t\t\th.ServeHTTP(r, req)\n\t\t}()\n\t\t<-done\n\n\t\tresp := r.Result()\n\t\tt.Log(resp.Status)\n\t\tif got, want := resp.StatusCode, http.StatusUnsupportedMediaType; got != want {\n\t\t\tt.Errorf(\"got status code: %v want status code: %v\", got, want)\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "httptransport/error.go",
    "content": "package httptransport\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"net/http\"\n)\n\n// StatusClientClosedRequest is a nonstandard HTTP status code used when the\n// client has gone away.\n//\n// This convention is cribbed from Nginx.\nconst statusClientClosedRequest = 499\n\n// ApiError writes an untyped (that is, \"application/json\") error with the\n// provided HTTP status code and message.\n//\n// ApiError does not return, but instead causes the goroutine to exit.\nfunc apiError(ctx context.Context, w http.ResponseWriter, code int, f string, v ...interface{}) {\n\tconst errheader = `Clair-Error`\n\tdisconnect := false\n\tselect {\n\tcase <-ctx.Done():\n\t\tdisconnect = true\n\tdefault:\n\t}\n\tif l := slog.Default(); l.Enabled(ctx, slog.LevelDebug) {\n\t\tl.DebugContext(ctx, \"http error response\",\n\t\t\t\"disconnect\", disconnect,\n\t\t\t\"code\", code,\n\t\t\t\"message\", fmt.Sprintf(f, v...))\n\t}\n\tif disconnect {\n\t\t// Exit immediately if there's no client to read the response, anyway.\n\t\tw.WriteHeader(statusClientClosedRequest)\n\t\tpanic(http.ErrAbortHandler)\n\t}\n\n\th := w.Header()\n\th.Del(\"link\")\n\th.Set(\"content-type\", \"application/json\")\n\th.Set(\"x-content-type-options\", \"nosniff\")\n\th.Set(\"trailer\", errheader)\n\tw.WriteHeader(code)\n\n\tvar buf bytes.Buffer\n\tbuf.WriteString(`{\"code\":\"`)\n\tswitch code {\n\tcase http.StatusBadRequest:\n\t\tbuf.WriteString(\"bad-request\")\n\tcase http.StatusMethodNotAllowed:\n\t\tbuf.WriteString(\"method-not-allowed\")\n\tcase http.StatusNotFound:\n\t\tbuf.WriteString(\"not-found\")\n\tcase http.StatusTooManyRequests:\n\t\tbuf.WriteString(\"too-many-requests\")\n\tdefault:\n\t\tbuf.WriteString(\"internal-error\")\n\t}\n\tbuf.WriteByte('\"')\n\tif f != \"\" {\n\t\tbuf.WriteString(`,\"message\":`)\n\t\tb, _ := json.Marshal(fmt.Sprintf(f, v...)) // OK use of encoding/json.\n\t\tbuf.Write(b)\n\t}\n\tbuf.WriteByte('}')\n\n\tif _, err := buf.WriteTo(w); err != nil {\n\t\th.Set(errheader, err.Error())\n\t}\n\tswitch err := http.NewResponseController(w).Flush(); {\n\tcase errors.Is(err, nil):\n\tcase errors.Is(err, http.ErrNotSupported):\n\t\t// Skip\n\tdefault:\n\t\tslog.WarnContext(ctx, \"unable to flush http response\",\n\t\t\t\"reason\", err)\n\t}\n\tpanic(http.ErrAbortHandler)\n}\n"
  },
  {
    "path": "httptransport/error_test.go",
    "content": "package httptransport\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/quay/claircore/test\"\n\n\t\"github.com/quay/clair/v4/internal/httputil\"\n)\n\nfunc TestClientDisconnect(t *testing.T) {\n\tvar status int\n\t// Bunch of sequencing events:\n\treqStart := make(chan struct{})\n\treqDone := make(chan struct{})\n\thandlerDone := make(chan struct{})\n\n\t// Server side:\n\t//\t- Emit reqStart once the request is received.\n\t//\t- Emit handlerDone once the request is done and \"status\" should be populated.\n\tsrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tdefer func() { close(handlerDone) }()\n\t\tw = httputil.ResponseRecorder(&status, nil, w)\n\t\tctx := test.Logging(t, r.Context()) // The error handler emits logs.\n\t\tclose(reqStart)\n\t\t<-ctx.Done()\n\t\tapiError(ctx, w, http.StatusOK, \"hello from the handler\")\n\t}))\n\tt.Cleanup(srv.Close)\n\n\tctx, done := context.WithCancel(context.Background())\n\t// Closing \"done\" will cancel the client connection.\n\treq, err := http.NewRequestWithContext(ctx, \"GET\", srv.URL+\"/\", nil)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\t// Emit reqDone when the client connection is done.\n\tgo func() {\n\t\t_, err = srv.Client().Do(req)\n\t\tclose(reqDone)\n\t}()\n\n\t<-reqStart\n\tdone()\n\t<-reqDone\n\tt.Logf(\"got request error: %v\", err)\n\tif err == nil {\n\t\tt.Error(\"expected non-nil error\")\n\t}\n\n\t<-handlerDone\n\tif got, want := status, statusClientClosedRequest; got != want {\n\t\tt.Errorf(\"bad status code recorded: got: %d, want: %d\", got, want)\n\t}\n}\n"
  },
  {
    "path": "httptransport/gone.go",
    "content": "package httptransport\n\nimport \"net/http\"\n\nvar gone = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\tconst msg = `{\"code\":\"gone\",\"message\":\"endpoint removed\"}`\n\thttp.Error(w, msg, http.StatusGone)\n})\n"
  },
  {
    "path": "httptransport/indexer_v1.go",
    "content": "package httptransport\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"net/http\"\n\t\"path\"\n\t\"time\"\n\n\t\"github.com/quay/claircore\"\n\t\"github.com/quay/claircore/pkg/tarfs\"\n\t\"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp\"\n\n\t\"github.com/quay/clair/v4/indexer\"\n\t\"github.com/quay/clair/v4/internal/codec\"\n\t\"github.com/quay/clair/v4/internal/httputil\"\n\t\"github.com/quay/clair/v4/middleware/compress\"\n)\n\n// NewIndexerV1 returns an http.Handler serving the Indexer V1 API rooted at\n// \"prefix\".\nfunc NewIndexerV1(_ context.Context, prefix string, srv indexer.Service, topt otelhttp.Option) (*IndexerV1, error) {\n\tprefix = path.Join(\"/\", prefix) // Ensure the prefix is rooted and cleaned.\n\tm := http.NewServeMux()\n\th := IndexerV1{\n\t\tinner: otelhttp.NewHandler(\n\t\t\tcompress.Handler(m),\n\t\t\t\"indexerv1\",\n\t\t\totelhttp.WithMessageEvents(otelhttp.ReadEvents, otelhttp.WriteEvents),\n\t\t\ttopt,\n\t\t),\n\t\tsrv: srv,\n\t}\n\tp := path.Join(prefix, \"index_report\")\n\tm.Handle(p, indexerv1wrapper.wrapFunc(p, h.indexReport))\n\tp += \"/\"\n\tm.Handle(p, indexerv1wrapper.wrapFunc(path.Join(p, \":digest\"), h.indexReportOne))\n\tp = path.Join(prefix, \"index_state\")\n\tm.Handle(p, indexerv1wrapper.wrapFunc(p, h.indexState))\n\tp = path.Join(prefix, \"internal\", \"affected_manifest\") + \"/\"\n\tm.Handle(p, indexerv1wrapper.wrapFunc(p, h.affectedManifests))\n\n\treturn &h, nil\n}\n\n// IndexerV1 is a consolidated Indexer endpoint.\ntype IndexerV1 struct {\n\tinner http.Handler\n\tsrv   indexer.Service\n}\n\nvar _ http.Handler = (*IndexerV1)(nil)\n\n// ServeHTTP implements http.Handler.\nfunc (h *IndexerV1) ServeHTTP(w http.ResponseWriter, r *http.Request) {\n\tstart := time.Now()\n\tr = withRequestID(r)\n\tctx := r.Context()\n\tvar status int\n\tvar length int64\n\tw = httputil.ResponseRecorder(&status, &length, w)\n\tdefer func() {\n\t\tswitch err := http.NewResponseController(w).Flush(); {\n\t\tcase errors.Is(err, nil):\n\t\tcase errors.Is(err, http.ErrNotSupported): // Skip\n\t\tdefault:\n\t\t\tslog.WarnContext(ctx, \"unable to flush http response\",\n\t\t\t\t\"reason\", err)\n\t\t}\n\t\tslog.InfoContext(ctx, \"handled HTTP request\",\n\t\t\t\"remote_addr\", r.RemoteAddr,\n\t\t\t\"method\", r.Method,\n\t\t\t\"request_uri\", r.RequestURI,\n\t\t\t\"status\", status,\n\t\t\t\"written\", length,\n\t\t\t\"duration\", time.Since(start))\n\t}()\n\th.inner.ServeHTTP(w, r)\n}\n\nfunc (h *IndexerV1) indexReport(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\tswitch r.Method {\n\tcase http.MethodPost:\n\tcase http.MethodDelete:\n\tdefault:\n\t\tapiError(ctx, w, http.StatusMethodNotAllowed, \"method disallowed: %s\", r.Method)\n\t}\n\tdefer r.Body.Close()\n\tdec := codec.GetDecoder(r.Body)\n\tdefer codec.PutDecoder(dec)\n\tswitch r.Method {\n\tcase http.MethodPost:\n\t\tstate, err := h.srv.State(ctx)\n\t\tif err != nil {\n\t\t\tapiError(ctx, w, http.StatusInternalServerError, \"could not retrieve indexer state: %v\", err)\n\t\t}\n\t\tvar m claircore.Manifest\n\t\tif err := dec.Decode(&m); err != nil {\n\t\t\tapiError(ctx, w, http.StatusBadRequest, \"failed to deserialize manifest: %v\", err)\n\t\t}\n\t\tif m.Hash.String() == \"\" || len(m.Layers) == 0 {\n\t\t\tapiError(ctx, w, http.StatusBadRequest, \"bogus manifest\")\n\t\t}\n\t\tnext := path.Join(r.URL.Path, m.Hash.String())\n\n\t\tw.Header().Add(\"link\", fmt.Sprintf(linkIndex, next))\n\t\tw.Header().Add(\"link\", fmt.Sprintf(linkReport, path.Join(VulnerabilityReportPath, m.Hash.String())))\n\t\tvalidator := `\"` + state + `\"`\n\t\tif unmodified(r, validator) {\n\t\t\tw.WriteHeader(http.StatusPreconditionFailed)\n\t\t\treturn\n\t\t}\n\n\t\t// TODO Do we need some sort of background context embedded in the HTTP\n\t\t// struct?\n\t\treport, err := h.srv.Index(ctx, &m)\n\t\tswitch {\n\t\tcase errors.Is(err, nil):\n\t\tcase errors.Is(err, tarfs.ErrFormat):\n\t\t\tapiError(ctx, w, http.StatusBadRequest, \"failed to start scan: %v\", err)\n\t\tdefault:\n\t\t\tapiError(ctx, w, http.StatusInternalServerError, \"failed to start scan: %v\", err)\n\t\t}\n\n\t\tw.Header().Set(\"etag\", validator)\n\t\tw.Header().Set(\"location\", next)\n\t\tdefer writerError(w, &err)()\n\t\tw.WriteHeader(http.StatusCreated)\n\t\tenc := codec.GetEncoder(w)\n\t\tdefer codec.PutEncoder(enc)\n\t\terr = enc.Encode(report)\n\tcase http.MethodDelete:\n\t\tvar ds []claircore.Digest\n\t\tif err := dec.Decode(&ds); err != nil {\n\t\t\tapiError(ctx, w, http.StatusBadRequest, \"failed to deserialize bulk delete: %v\", err)\n\t\t}\n\t\tds, err := h.srv.DeleteManifests(ctx, ds...)\n\t\tif err != nil {\n\t\t\tapiError(ctx, w, http.StatusInternalServerError, \"could not delete manifests: %v\", err)\n\t\t}\n\t\tslog.DebugContext(ctx, \"manifests deleted\",\n\t\t\t\"count\", len(ds))\n\t\tdefer writerError(w, &err)()\n\t\tw.WriteHeader(http.StatusOK)\n\t\tenc := codec.GetEncoder(w)\n\t\tdefer codec.PutEncoder(enc)\n\t\terr = enc.Encode(ds)\n\t}\n}\n\nconst (\n\tlinkIndex  = `<%s>; rel=\"https://projectquay.io/clair/v1/index_report\"`\n\tlinkReport = `<%s>; rel=\"https://projectquay.io/clair/v1/vulnerability_report\"`\n)\n\nfunc (h *IndexerV1) indexReportOne(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\tswitch r.Method {\n\tcase http.MethodGet:\n\tcase http.MethodDelete:\n\tdefault:\n\t\tapiError(ctx, w, http.StatusMethodNotAllowed, \"method disallowed: %s\", r.Method)\n\t}\n\td, err := getDigest(w, r)\n\tif err != nil {\n\t\tapiError(ctx, w, http.StatusBadRequest, \"malformed path: %v\", err)\n\t}\n\tswitch r.Method {\n\tcase http.MethodGet:\n\t\tallow := []string{\"application/vnd.clair.index_report.v1+json\", \"application/json\"}\n\t\tswitch err := pickContentType(w, r, allow); {\n\t\tcase errors.Is(err, nil): // OK\n\t\tcase errors.Is(err, ErrMediaType):\n\t\t\tapiError(ctx, w, http.StatusUnsupportedMediaType, \"unable to negotiate common media type for %v\", allow)\n\t\tdefault:\n\t\t\tapiError(ctx, w, http.StatusBadRequest, \"malformed request: %v\", err)\n\t\t}\n\n\t\tstate, err := h.srv.State(ctx)\n\t\tif err != nil {\n\t\t\tapiError(ctx, w, http.StatusInternalServerError, \"could not retrieve indexer state: %v\", err)\n\t\t}\n\t\tvalidator := `\"` + state + `\"`\n\t\tif unmodified(r, validator) {\n\t\t\tw.WriteHeader(http.StatusNotModified)\n\t\t\treturn\n\t\t}\n\n\t\treport, ok, err := h.srv.IndexReport(ctx, d)\n\t\tif !ok {\n\t\t\tapiError(ctx, w, http.StatusNotFound, \"index report not found\")\n\t\t}\n\t\tif err != nil {\n\t\t\tapiError(ctx, w, http.StatusInternalServerError, \"could not retrieve index report: %v\", err)\n\t\t}\n\n\t\tw.Header().Add(\"etag\", validator)\n\t\tdefer writerError(w, &err)()\n\t\tenc := codec.GetEncoder(w)\n\t\tdefer codec.PutEncoder(enc)\n\t\terr = enc.Encode(report)\n\tcase http.MethodDelete:\n\t\tif _, err := h.srv.DeleteManifests(ctx, d); err != nil {\n\t\t\tapiError(ctx, w, http.StatusInternalServerError, \"unable to delete manifest: %v\", err)\n\t\t}\n\t\tw.WriteHeader(http.StatusNoContent)\n\t}\n}\n\nfunc (h *IndexerV1) indexState(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\tif r.Method != http.MethodGet {\n\t\tapiError(ctx, w, http.StatusMethodNotAllowed, \"method disallowed: %s\", r.Method)\n\t}\n\tallow := []string{\"application/vnd.clair.index_state.v1+json\", \"application/json\"}\n\tswitch err := pickContentType(w, r, allow); {\n\tcase errors.Is(err, nil): // OK\n\tcase errors.Is(err, ErrMediaType):\n\t\tapiError(ctx, w, http.StatusUnsupportedMediaType, \"unable to negotiate common media type for %v\", allow)\n\tdefault:\n\t\tapiError(ctx, w, http.StatusBadRequest, \"malformed request: %v\", err)\n\t}\n\ts, err := h.srv.State(ctx)\n\tif err != nil {\n\t\tapiError(ctx, w, http.StatusInternalServerError, \"could not retrieve indexer state: %v\", err)\n\t}\n\n\ttag := `\"` + s + `\"`\n\tw.Header().Add(\"etag\", tag)\n\n\tif unmodified(r, tag) {\n\t\tw.WriteHeader(http.StatusNotModified)\n\t\treturn\n\t}\n\n\tdefer writerError(w, &err)()\n\t// TODO(hank) Don't use an encoder to write out like 40 bytes of json.\n\tenc := codec.GetEncoder(w)\n\tdefer codec.PutEncoder(enc)\n\terr = enc.Encode(struct {\n\t\tState string `json:\"state\"`\n\t}{\n\t\tState: s,\n\t})\n}\n\nfunc (h *IndexerV1) affectedManifests(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\tif r.Method != http.MethodPost {\n\t\tapiError(ctx, w, http.StatusMethodNotAllowed, \"method disallowed: %s\", r.Method)\n\t}\n\tallow := []string{\"application/vnd.clair.affected_manifests.v1+json\", \"application/json\"}\n\tswitch err := pickContentType(w, r, allow); {\n\tcase errors.Is(err, nil): // OK\n\tcase errors.Is(err, ErrMediaType):\n\t\tapiError(ctx, w, http.StatusUnsupportedMediaType, \"unable to negotiate common media type for %v\", allow)\n\tdefault:\n\t\tapiError(ctx, w, http.StatusBadRequest, \"malformed request: %v\", err)\n\t}\n\n\tvar vulnerabilities struct {\n\t\tV []claircore.Vulnerability `json:\"vulnerabilities\"`\n\t}\n\tdec := codec.GetDecoder(r.Body)\n\tdefer codec.PutDecoder(dec)\n\tif err := dec.Decode(&vulnerabilities); err != nil {\n\t\tapiError(ctx, w, http.StatusBadRequest, \"failed to deserialize vulnerabilities: %v\", err)\n\t}\n\n\taffected, err := h.srv.AffectedManifests(ctx, vulnerabilities.V)\n\tif err != nil {\n\t\tapiError(ctx, w, http.StatusInternalServerError, \"could not retrieve affected manifests: %v\", err)\n\t}\n\n\tdefer writerError(w, &err)\n\tenc := codec.GetEncoder(w)\n\tdefer codec.PutEncoder(enc)\n\terr = enc.Encode(affected)\n}\n\nfunc init() {\n\tindexerv1wrapper.init(\"indexerv1\")\n}\n\nvar indexerv1wrapper wrapper\n"
  },
  {
    "path": "httptransport/indexer_v1_test.go",
    "content": "package httptransport\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/quay/claircore\"\n\t\"github.com/quay/claircore/pkg/tarfs\"\n\t\"github.com/quay/claircore/test\"\n\t\"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp\"\n\t\"go.opentelemetry.io/otel/trace\"\n\n\t\"github.com/quay/clair/v4/indexer\"\n\t\"github.com/quay/clair/v4/internal/httputil\"\n)\n\nfunc TestIndexReportBadLayer(t *testing.T) {\n\tctx := test.Logging(t)\n\n\ti := &indexer.Mock{\n\t\tState_: func(ctx context.Context) (string, error) {\n\t\t\treturn `deadbeef`, nil\n\t\t},\n\t\tIndex_: func(ctx context.Context, m *claircore.Manifest) (*claircore.IndexReport, error) {\n\t\t\treturn nil, tarfs.ErrFormat\n\t\t},\n\t}\n\tv1, err := NewIndexerV1(ctx, \"\", i, otelhttp.WithTracerProvider(trace.NewNoopTracerProvider()))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tsrv := httptest.NewUnstartedServer(v1)\n\tsrv.Config.BaseContext = func(_ net.Listener) context.Context { return ctx }\n\tsrv.Start()\n\tdefer srv.Close()\n\tt.Run(\"Report\", func(t *testing.T) {\n\t\tctx := test.Logging(t, ctx)\n\t\tconst path = `/index_report`\n\t\tt.Run(\"POST\", func(t *testing.T) {\n\t\t\tconst body = `{\"hash\":\"sha256:0000000000000000000000000000000000000000000000000000000000000000\",` +\n\t\t\t\t`\"layers\":[{}]}`\n\t\t\treq, err := httputil.NewRequestWithContext(ctx, http.MethodPost, srv.URL+path, strings.NewReader(body))\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tres, err := srv.Client().Do(req)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tgot, want := res.StatusCode, http.StatusBadRequest\n\t\t\tt.Logf(\"got: %d, want: %d\", got, want)\n\t\t\tif got != want {\n\t\t\t\tt.Error()\n\t\t\t}\n\t\t})\n\t})\n}\n\nfunc TestIndexerV1(t *testing.T) {\n\tctx := test.Logging(t)\n\n\tdigest := claircore.MustParseDigest(\"sha256:0000000000000000000000000000000000000000000000000000000000000000\")\n\ti := &indexer.Mock{\n\t\tState_: func(ctx context.Context) (string, error) {\n\t\t\treturn `deadbeef`, nil\n\t\t},\n\t\tDeleteManifests_: func(_ context.Context, ds ...claircore.Digest) ([]claircore.Digest, error) {\n\t\t\tfor _, d := range ds {\n\t\t\t\tif got, want := d.String(), digest.String(); got != want {\n\t\t\t\t\treturn nil, fmt.Errorf(\"unexpected digest: %v\", d)\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn ds, nil\n\t\t},\n\t\tIndex_: func(ctx context.Context, m *claircore.Manifest) (*claircore.IndexReport, error) {\n\t\t\treturn new(claircore.IndexReport), nil\n\t\t},\n\t\tIndexReport_: func(_ context.Context, d claircore.Digest) (*claircore.IndexReport, bool, error) {\n\t\t\tif got, want := d.String(), digest.String(); got != want {\n\t\t\t\treturn nil, false, fmt.Errorf(\"unexpected digest: %v\", d)\n\t\t\t}\n\t\t\treturn new(claircore.IndexReport), true, nil\n\t\t},\n\t\tAffectedManifests_: func(_ context.Context, _ []claircore.Vulnerability) (*claircore.AffectedManifests, error) {\n\t\t\treturn new(claircore.AffectedManifests), nil\n\t\t},\n\t}\n\n\tv1, err := NewIndexerV1(ctx, \"\", i, otelhttp.WithTracerProvider(trace.NewNoopTracerProvider()))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tsrv := httptest.NewUnstartedServer(v1)\n\tsrv.Config.BaseContext = func(_ net.Listener) context.Context { return ctx }\n\tsrv.Start()\n\tdefer srv.Close()\n\n\tt.Run(\"State\", func(t *testing.T) {\n\t\tctx := test.Logging(t, ctx)\n\t\tconst path = `/index_state`\n\t\tt.Run(\"GET\", func(t *testing.T) {\n\t\t\treq, err := httputil.NewRequestWithContext(ctx, http.MethodGet, srv.URL+path, nil)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tres, err := srv.Client().Do(req)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tgot, want := res.StatusCode, http.StatusOK\n\t\t\tt.Logf(\"got: %d, want: %d\", got, want)\n\t\t\tif got != want {\n\t\t\t\tt.Error()\n\t\t\t}\n\t\t})\n\t})\n\tt.Run(\"Report\", func(t *testing.T) {\n\t\tctx := test.Logging(t, ctx)\n\t\tconst path = `/index_report`\n\t\tt.Run(\"POST\", func(t *testing.T) {\n\t\t\tconst body = `{\"hash\":\"sha256:0000000000000000000000000000000000000000000000000000000000000000\",` +\n\t\t\t\t`\"layers\":[{}]}`\n\t\t\treq, err := httputil.NewRequestWithContext(ctx, http.MethodPost, srv.URL+path, strings.NewReader(body))\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tres, err := srv.Client().Do(req)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tgot, want := res.StatusCode, http.StatusCreated\n\t\t\tt.Logf(\"got: %d, want: %d\", got, want)\n\t\t\tif got != want {\n\t\t\t\tt.Error()\n\t\t\t}\n\t\t})\n\t\tt.Run(\"DELETE\", func(t *testing.T) {\n\t\t\tconst body = `[\"sha256:0000000000000000000000000000000000000000000000000000000000000000\"]`\n\t\t\treq, err := httputil.NewRequestWithContext(ctx, http.MethodDelete, srv.URL+path, strings.NewReader(body))\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tres, err := srv.Client().Do(req)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tdefer res.Body.Close()\n\t\t\tvar buf bytes.Buffer\n\t\t\tgot, want := res.StatusCode, http.StatusOK\n\t\t\tt.Logf(\"got: %d, want: %d\", got, want)\n\t\t\tif got != want {\n\t\t\t\tt.Error()\n\t\t\t}\n\t\t\tif _, err := io.Copy(&buf, res.Body); err != nil {\n\t\t\t\tt.Error(err)\n\t\t\t}\n\t\t\t// Should get back what we put in, so this is a little hack.\n\t\t\tif got, want := buf.String(), body; got != want {\n\t\t\t\tt.Errorf(\"got: %q, want: %q\", got, want)\n\t\t\t}\n\t\t})\n\t\tt.Run(\"GET\", func(t *testing.T) {\n\t\t\treq, err := httputil.NewRequestWithContext(ctx, http.MethodGet, srv.URL+path, nil)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tres, err := srv.Client().Do(req)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tgot, want := res.StatusCode, http.StatusMethodNotAllowed\n\t\t\tt.Logf(\"got: %d, want: %d\", got, want)\n\t\t\tif got != want {\n\t\t\t\tt.Error()\n\t\t\t}\n\t\t})\n\t})\n\tt.Run(\"ReportOne\", func(t *testing.T) {\n\t\tctx := test.Logging(t, ctx)\n\t\tconst path = `/index_report/sha256:0000000000000000000000000000000000000000000000000000000000000000`\n\t\tt.Run(\"DELETE\", func(t *testing.T) {\n\t\t\treq, err := httputil.NewRequestWithContext(ctx, http.MethodDelete, srv.URL+path, nil)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tres, err := srv.Client().Do(req)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tgot, want := res.StatusCode, http.StatusNoContent\n\t\t\tt.Logf(\"got: %d, want: %d\", got, want)\n\t\t\tif got != want {\n\t\t\t\tt.Error()\n\t\t\t}\n\t\t})\n\t\tt.Run(\"GET\", func(t *testing.T) {\n\t\t\treq, err := httputil.NewRequestWithContext(ctx, http.MethodGet, srv.URL+path, nil)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tres, err := srv.Client().Do(req)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tgot, want := res.StatusCode, http.StatusOK\n\t\t\tt.Logf(\"got: %d, want: %d\", got, want)\n\t\t\tif got != want {\n\t\t\t\tt.Error()\n\t\t\t}\n\t\t})\n\t})\n\tt.Run(\"AffectedManifests\", func(t *testing.T) {\n\t\tctx := test.Logging(t, ctx)\n\t\tconst path = `/internal/affected_manifest/`\n\t\tt.Run(\"POST\", func(t *testing.T) {\n\t\t\tconst body = `{\"vulnerabilities\":[]}`\n\t\t\treq, err := httputil.NewRequestWithContext(ctx, http.MethodPost, srv.URL+path, strings.NewReader(body))\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tres, err := srv.Client().Do(req)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tgot, want := res.StatusCode, http.StatusOK\n\t\t\tt.Logf(\"got: %d, want: %d\", got, want)\n\t\t\tif got != want {\n\t\t\t\tt.Error()\n\t\t\t}\n\t\t})\n\t})\n}\n"
  },
  {
    "path": "httptransport/instrumentation.go",
    "content": "package httptransport\n\nimport (\n\t\"errors\"\n\t\"log/slog\"\n\t\"net/http\"\n\n\t\"github.com/prometheus/client_golang/prometheus\"\n\t\"github.com/prometheus/client_golang/prometheus/promhttp\"\n\t\"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp\"\n)\n\nconst (\n\tmetricNamespace = `clair`\n\tmetricSubsystem = `http`\n)\n\ntype wrapper struct {\n\tRequestCount    *prometheus.CounterVec\n\tRequestSize     *prometheus.HistogramVec\n\tResponseSize    *prometheus.HistogramVec\n\tRequestDuration *prometheus.HistogramVec\n\tInFlight        *prometheus.GaugeVec\n}\n\n// InitRegisterer registers with the provided [prometheus.Registerer].\n//\n// The [*wrapper.init] method is short for\n//\n//\t(*wrapper).initRegisterer(name, prometheus.DefaultRegisterer)\nfunc (m *wrapper) initRegisterer(name string, reg prometheus.Registerer) {\n\tif m.RequestCount == nil {\n\t\tm.RequestCount = prometheus.NewCounterVec(\n\t\t\tprometheus.CounterOpts{\n\t\t\t\tNamespace: metricNamespace,\n\t\t\t\tSubsystem: metricSubsystem,\n\t\t\t\tName:      name + \"_request_total\",\n\t\t\t\tHelp:      \"A total count of http requests for the given path\",\n\t\t\t},\n\t\t\t[]string{\"handler\", \"code\", \"method\"},\n\t\t)\n\t}\n\tif m.RequestSize == nil {\n\t\tm.RequestSize = prometheus.NewHistogramVec(\n\t\t\tprometheus.HistogramOpts{\n\t\t\t\tNamespace: metricNamespace,\n\t\t\t\tSubsystem: metricSubsystem,\n\t\t\t\tName:      name + \"_request_size_bytes\",\n\t\t\t\tHelp:      \"Distribution of request sizes for the given path\",\n\t\t\t},\n\t\t\t[]string{\"handler\", \"code\", \"method\"},\n\t\t)\n\t}\n\tif m.ResponseSize == nil {\n\t\tm.ResponseSize = prometheus.NewHistogramVec(\n\t\t\tprometheus.HistogramOpts{\n\t\t\t\tNamespace: metricNamespace,\n\t\t\t\tSubsystem: metricSubsystem,\n\t\t\t\tName:      name + \"_response_size_bytes\",\n\t\t\t\tHelp:      \"Distribution of response sizes for the given path\",\n\t\t\t}, []string{\"handler\", \"code\", \"method\"},\n\t\t)\n\t}\n\tif m.RequestDuration == nil {\n\t\tm.RequestDuration = prometheus.NewHistogramVec(\n\t\t\tprometheus.HistogramOpts{\n\t\t\t\tNamespace: metricNamespace,\n\t\t\t\tSubsystem: metricSubsystem,\n\t\t\t\tName:      name + \"_request_duration_seconds\",\n\t\t\t\tHelp:      \"Distribution of request durations for the given path\",\n\t\t\t\t// These are roughly exponential from 0.5 to 300 seconds\n\t\t\t\tBuckets: []float64{0.5, 0.7, 1.1, 1.7, 2.7, 4.2, 6.5, 10, 15, 23, 36, 54, 83, 128, 196, 300},\n\t\t\t}, []string{\"handler\", \"code\", \"method\"},\n\t\t)\n\t}\n\tif m.InFlight == nil {\n\t\tm.InFlight = prometheus.NewGaugeVec(\n\t\t\tprometheus.GaugeOpts{\n\t\t\t\tNamespace: metricNamespace,\n\t\t\t\tSubsystem: metricSubsystem,\n\t\t\t\tName:      name + \"_in_flight_requests\",\n\t\t\t\tHelp:      \"Gauge of requests in flight\",\n\t\t\t},\n\t\t\t[]string{\"handler\"},\n\t\t)\n\t}\n\treg.MustRegister(m.RequestCount, m.RequestSize, m.ResponseSize, m.RequestDuration, m.InFlight)\n}\n\nfunc (m *wrapper) init(name string) {\n\tm.initRegisterer(name, prometheus.DefaultRegisterer)\n}\n\nfunc (m *wrapper) wrap(tag string, h http.Handler) http.Handler {\n\t// Stack all these metrics handlers.\n\th = promhttp.InstrumentHandlerCounter(m.RequestCount.MustCurryWith(prometheus.Labels{\"handler\": tag}),\n\t\tpromhttp.InstrumentHandlerRequestSize(m.RequestSize.MustCurryWith(prometheus.Labels{\"handler\": tag}),\n\t\t\tpromhttp.InstrumentHandlerResponseSize(m.ResponseSize.MustCurryWith(prometheus.Labels{\"handler\": tag}),\n\t\t\t\tpromhttp.InstrumentHandlerDuration(m.RequestDuration.MustCurryWith(prometheus.Labels{\"handler\": tag}),\n\t\t\t\t\tpromhttp.InstrumentHandlerInFlight(m.InFlight.WithLabelValues(tag),\n\t\t\t\t\t\tcatchAbort(h))))))\n\treturn otelhttp.NewHandler(h, tag)\n}\n\nfunc (m *wrapper) wrapFunc(tag string, h http.HandlerFunc) http.Handler {\n\treturn m.wrap(tag, h)\n}\n\n// Make sure the prom instrumentation works.\n// This can get reworked when the metrics are reworked to be otel native.\nfunc catchAbort(h http.Handler) http.Handler {\n\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tdefer func() {\n\t\t\trec := recover()\n\t\t\tif rec == nil {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif err, ok := rec.(error); ok && errors.Is(err, http.ErrAbortHandler) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tw.WriteHeader(http.StatusInternalServerError)\n\t\t\tslog.WarnContext(r.Context(), \"handler panicked; please file a bug\",\n\t\t\t\t\"panic\", rec)\n\t\t}()\n\t\th.ServeHTTP(w, r)\n\t})\n}\n"
  },
  {
    "path": "httptransport/instrumentation_test.go",
    "content": "package httptransport\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/prometheus/client_golang/prometheus\"\n\t\"github.com/prometheus/client_golang/prometheus/testutil\"\n\t\"github.com/quay/claircore/test\"\n)\n\nfunc TestMetric(t *testing.T) {\n\tctx := test.Logging(t)\n\twant := strings.NewReader(`\n# HELP clair_http_test_request_total A total count of http requests for the given path\n# TYPE clair_http_test_request_total counter\nclair_http_test_request_total{code=\"200\",handler=\"ok\",method=\"get\"} 1\nclair_http_test_request_total{code=\"504\",handler=\"err\",method=\"get\"} 1\nclair_http_test_request_total{code=\"500\",handler=\"err\",method=\"get\"} 1\n`)\n\n\treg := prometheus.NewRegistry()\n\tvar wr wrapper\n\twr.initRegisterer(\"test\", reg)\n\tm := http.NewServeMux()\n\tm.Handle(\"/ok\",\n\t\twr.wrapFunc(\"ok\", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tfmt.Fprintln(w, \"ok\")\n\t\t})))\n\tm.Handle(\"/err\",\n\t\twr.wrapFunc(\"err\", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tif err := r.ParseForm(); err != nil {\n\t\t\t\tpanic(err)\n\t\t\t}\n\t\t\tif r.Form.Has(\"panic\") {\n\t\t\t\tpanic(\"we're just normal men\")\n\t\t\t}\n\t\t\tapiError(r.Context(), w, http.StatusGatewayTimeout, \"expected error\")\n\t\t})))\n\n\tsrv := httptest.NewUnstartedServer(m)\n\tsrv.Config.BaseContext = func(_ net.Listener) context.Context { return ctx }\n\n\t// Create a scope for doing the actual requests.\n\t//\n\t// Doing this and having the server teardown run synchronously should be\n\t// enough time to ensure the metrics are actually collected.\n\tfunc() {\n\t\tsrv.Start()\n\t\tdefer srv.Close()\n\n\t\tc := srv.Client()\n\t\tfor _, p := range []string{\"ok\", \"err\", \"err?panic=1\"} {\n\t\t\tu := srv.URL + \"/\" + p\n\t\t\tt.Logf(\"making request: %q\", u)\n\t\t\tres, err := c.Get(u)\n\t\t\tif err != nil {\n\t\t\t\tt.Error(err)\n\t\t\t}\n\t\t\tt.Logf(\"got status: %q\", res.Status)\n\t\t}\n\t}()\n\n\tif err := testutil.GatherAndCompare(reg, want, \"clair_http_test_request_total\"); err != nil {\n\t\tt.Error(err)\n\t} else {\n\t\tt.Log(\"metrics OK\")\n\t}\n}\n"
  },
  {
    "path": "httptransport/matcher_v1.go",
    "content": "package httptransport\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"log/slog\"\n\t\"net/http\"\n\t\"net/http/httptrace\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/google/uuid\"\n\t\"github.com/quay/claircore\"\n\tindexerController \"github.com/quay/claircore/indexer/controller\"\n\t\"github.com/quay/claircore/libvuln/driver\"\n\toteltrace \"go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace\"\n\t\"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp\"\n\n\t\"github.com/quay/clair/v4/indexer\"\n\t\"github.com/quay/clair/v4/internal/codec\"\n\t\"github.com/quay/clair/v4/internal/httputil\"\n\t\"github.com/quay/clair/v4/matcher\"\n\t\"github.com/quay/clair/v4/middleware/compress\"\n)\n\n// NewMatcherV1 returns an http.Handler serving the Matcher V1 API rooted at\n// \"prefix\".\nfunc NewMatcherV1(_ context.Context, prefix string, srv matcher.Service, indexerSrv indexer.Service, cacheAge time.Duration, topt otelhttp.Option) *MatcherV1 {\n\tprefix = path.Join(\"/\", prefix) // Ensure the prefix is rooted and cleaned.\n\tm := http.NewServeMux()\n\th := MatcherV1{\n\t\tinner: otelhttp.NewHandler(\n\t\t\tcompress.Handler(m),\n\t\t\t\"matcherv1\",\n\t\t\totelhttp.WithMessageEvents(otelhttp.ReadEvents, otelhttp.WriteEvents),\n\t\t\ttopt,\n\t\t),\n\t\tsrv:        srv,\n\t\tindexerSrv: indexerSrv,\n\t\tCache:      cacheAge,\n\t}\n\tp := path.Join(prefix, \"vulnerability_report\") + \"/\"\n\tm.Handle(p, matcherv1wrapper.wrapFunc(p, h.vulnerabilityReport))\n\tp = path.Join(prefix, \"internal\", \"update_operation\")\n\tm.Handle(p, matcherv1wrapper.wrapFunc(p, h.updateOperationHandlerGet))\n\tp = path.Join(prefix, \"internal\", \"update_operation\") + \"/\"\n\tm.Handle(p, matcherv1wrapper.wrapFunc(p, h.updateOperationHandlerDelete))\n\tp = path.Join(prefix, \"internal\", \"update_diff\")\n\tm.Handle(p, matcherv1wrapper.wrapFunc(p, h.updateDiffHandler))\n\n\treturn &h\n}\n\n// MatcherV1 is a consolidated Matcher endpoint.\ntype MatcherV1 struct {\n\tinner      http.Handler\n\tsrv        matcher.Service\n\tindexerSrv indexer.Service\n\tCache      time.Duration\n}\n\nvar _ http.Handler = (*MatcherV1)(nil)\n\n// ServeHTTP implements http.Handler.\nfunc (h *MatcherV1) ServeHTTP(w http.ResponseWriter, r *http.Request) {\n\tstart := time.Now()\n\tr = withRequestID(r)\n\tctx := r.Context()\n\tvar status int\n\tvar length int64\n\tw = httputil.ResponseRecorder(&status, &length, w)\n\tdefer func() {\n\t\tswitch err := http.NewResponseController(w).Flush(); {\n\t\tcase errors.Is(err, nil):\n\t\tcase errors.Is(err, http.ErrNotSupported): // Skip\n\t\tdefault:\n\t\t\tslog.WarnContext(ctx, \"unable to flush http response\",\n\t\t\t\t\"reason\", err)\n\t\t}\n\t\tslog.InfoContext(ctx, \"handled HTTP request\",\n\t\t\t\"remote_addr\", r.RemoteAddr,\n\t\t\t\"method\", r.Method,\n\t\t\t\"request_uri\", r.RequestURI,\n\t\t\t\"status\", status,\n\t\t\t\"written\", length,\n\t\t\t\"duration\", time.Since(start))\n\t}()\n\th.inner.ServeHTTP(w, r)\n}\n\n// TODO(hank) All of these handlers need to do content negotiation.\n\nfunc (h *MatcherV1) vulnerabilityReport(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\tif r.Method != http.MethodGet {\n\t\tapiError(ctx, w, http.StatusMethodNotAllowed, \"endpoint only allows GET\")\n\t}\n\tctx, done := context.WithCancel(ctx)\n\tdefer done()\n\tctx = httptrace.WithClientTrace(ctx, oteltrace.NewClientTrace(ctx))\n\n\tmanifestStr := path.Base(r.URL.Path)\n\tif manifestStr == \"\" {\n\t\tapiError(ctx, w, http.StatusBadRequest, \"malformed path. provide a single manifest hash\")\n\t}\n\tmanifest, err := claircore.ParseDigest(manifestStr)\n\tif err != nil {\n\t\tapiError(ctx, w, http.StatusBadRequest, \"malformed path: %v\", err)\n\t}\n\n\tinitd, err := h.srv.Initialized(ctx)\n\tif err != nil {\n\t\tapiError(ctx, w, http.StatusInternalServerError, \"initialization error: %v\", err)\n\t}\n\tif !initd {\n\t\tw.WriteHeader(http.StatusAccepted)\n\t\treturn\n\t}\n\n\tindexReport, ok, err := h.indexerSrv.IndexReport(ctx, manifest)\n\t// check err first\n\tif err != nil {\n\t\tapiError(ctx, w, http.StatusInternalServerError, \"experienced a server side error: %v\", err)\n\t}\n\t// now check present and finished only after confirming no err\n\tif !ok || indexReport.State != indexerController.IndexFinished.String() {\n\t\tapiError(ctx, w, http.StatusNotFound, \"index report for manifest %q not found\", manifest.String())\n\t\treturn\n\t}\n\n\tvulnReport, err := h.srv.Scan(ctx, indexReport)\n\tif err != nil {\n\t\tapiError(ctx, w, http.StatusInternalServerError, \"failed to start scan: %v\", err)\n\t}\n\n\tw.Header().Set(\"content-type\", \"application/vnd.clair.index_report.v1+json\")\n\tsetCacheControl(w, h.Cache)\n\n\tdefer writerError(w, &err)()\n\tenc := codec.GetEncoder(w)\n\tdefer codec.PutEncoder(enc)\n\terr = enc.Encode(vulnReport)\n}\n\nfunc (h *MatcherV1) updateDiffHandler(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\tif r.Method != http.MethodGet {\n\t\tapiError(ctx, w, http.StatusMethodNotAllowed, \"endpoint only allows GET\")\n\t}\n\t// prev param is optional.\n\tvar prev uuid.UUID\n\tvar err error\n\tif param := r.URL.Query().Get(\"prev\"); param != \"\" {\n\t\tprev, err = uuid.Parse(param)\n\t\tif err != nil {\n\t\t\tapiError(ctx, w, http.StatusBadRequest, \"could not parse \\\"prev\\\" query param into uuid\")\n\t\t}\n\t}\n\n\t// cur param is required\n\tvar cur uuid.UUID\n\tvar param string\n\tif param = r.URL.Query().Get(\"cur\"); param == \"\" {\n\t\tapiError(ctx, w, http.StatusBadRequest, \"\\\"cur\\\" query param is required\")\n\t}\n\tif cur, err = uuid.Parse(param); err != nil {\n\t\tapiError(ctx, w, http.StatusBadRequest, \"could not parse \\\"cur\\\" query param into uuid\")\n\t}\n\n\tdiff, err := h.srv.UpdateDiff(ctx, prev, cur)\n\tif err != nil {\n\t\tapiError(ctx, w, http.StatusInternalServerError, \"could not get update operations: %v\", err)\n\t}\n\n\tdefer writerError(w, &err)()\n\tenc := codec.GetEncoder(w)\n\tdefer codec.PutEncoder(enc)\n\terr = enc.Encode(&diff)\n}\n\nfunc (h *MatcherV1) updateOperationHandlerGet(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\tswitch r.Method {\n\tcase http.MethodGet:\n\tdefault:\n\t\tapiError(ctx, w, http.StatusMethodNotAllowed, \"method disallowed: %s\", r.Method)\n\t}\n\n\tkind := driver.VulnerabilityKind\n\tswitch k := r.URL.Query().Get(\"kind\"); k {\n\tcase \"enrichment\":\n\t\tkind = driver.EnrichmentKind\n\tcase \"\", \"vulnerability\":\n\t\t// Leave as default\n\tdefault:\n\t\tapiError(ctx, w, http.StatusBadRequest, \"unknown kind: %q\", k)\n\t}\n\n\t// handle conditional request. this is an optimization\n\tif ref, err := h.srv.LatestUpdateOperation(ctx, kind); err == nil {\n\t\tvalidator := `\"` + ref.String() + `\"`\n\t\tif unmodified(r, validator) {\n\t\t\tw.WriteHeader(http.StatusNotModified)\n\t\t\treturn\n\t\t}\n\t\tw.Header().Set(\"etag\", validator)\n\t}\n\n\tlatest := r.URL.Query().Get(\"latest\")\n\n\tvar uos map[string][]driver.UpdateOperation\n\tvar err error\n\tif b, _ := strconv.ParseBool(latest); b {\n\t\tuos, err = h.srv.LatestUpdateOperations(ctx, kind)\n\t} else {\n\t\tuos, err = h.srv.UpdateOperations(ctx, kind)\n\t}\n\tif err != nil {\n\t\tapiError(ctx, w, http.StatusInternalServerError, \"could not get update operations: %v\", err)\n\t}\n\n\tdefer writerError(w, &err)()\n\tenc := codec.GetEncoder(w)\n\tdefer codec.PutEncoder(enc)\n\terr = enc.Encode(&uos)\n}\n\nfunc (h *MatcherV1) updateOperationHandlerDelete(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\tswitch r.Method {\n\tcase http.MethodDelete:\n\tdefault:\n\t\tapiError(ctx, w, http.StatusMethodNotAllowed, \"method disallowed: %s\", r.Method)\n\t}\n\n\tpath := r.URL.Path\n\tid := filepath.Base(path)\n\tuuid, err := uuid.Parse(id)\n\tif err != nil {\n\t\tslog.WarnContext(ctx, \"could not deserialize manifest\", \"reason\", err)\n\t\tapiError(ctx, w, http.StatusBadRequest, \"could not deserialize manifest: %v\", err)\n\t}\n\n\t_, err = h.srv.DeleteUpdateOperations(ctx, uuid)\n\tif err != nil {\n\t\tapiError(ctx, w, http.StatusInternalServerError, \"could not get update operations: %v\", err)\n\t}\n\t// TODO(hank) This should return HTTP 204.\n}\n\nfunc init() {\n\tmatcherv1wrapper.init(\"matcherv1\")\n}\n\nvar matcherv1wrapper wrapper\n"
  },
  {
    "path": "httptransport/matcher_v1_test.go",
    "content": "package httptransport\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"path\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/google/uuid\"\n\t\"github.com/quay/claircore/libvuln/driver\"\n\t\"github.com/quay/claircore/test\"\n\t\"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp\"\n\t\"go.opentelemetry.io/otel/trace\"\n\n\t\"github.com/quay/clair/v4/indexer\"\n\t\"github.com/quay/clair/v4/internal/httputil\"\n\t\"github.com/quay/clair/v4/matcher\"\n)\n\n// TestUpdateDiffHandler is a parallel harness for testing a UpdateDiff handler.\nfunc TestUpdateDiffHandler(t *testing.T) {\n\tt.Run(\"Matcher\", testUpdateDiffMatcher)\n\tt.Run(\"Params\", testUpdateDiffHandlerParams)\n\tt.Run(\"Methods\", testUpdateDiffHandlerMethods)\n}\n\n// TestUpdateDiffMatcher confirms the UpdateDiff handler provides\n// the correct status codes when a matcher returns an error or success\nfunc testUpdateDiffMatcher(t *testing.T) {\n\tctx := test.Logging(t)\n\tt.Parallel()\n\tmOK := &matcher.Mock{\n\t\tUpdateDiff_: func(context.Context, uuid.UUID, uuid.UUID) (*driver.UpdateDiff, error) {\n\t\t\treturn nil, nil\n\t\t},\n\t}\n\thOK := NewMatcherV1(ctx, \"\", mOK, &indexer.Mock{}, time.Second*10, otelhttp.WithTracerProvider(trace.NewNoopTracerProvider()))\n\tmErr := &matcher.Mock{\n\t\tUpdateDiff_: func(context.Context, uuid.UUID, uuid.UUID) (*driver.UpdateDiff, error) {\n\t\t\treturn nil, fmt.Errorf(\"expected error\")\n\t\t},\n\t}\n\thErr := NewMatcherV1(ctx, \"\", mErr, &indexer.Mock{}, time.Second*10, otelhttp.WithTracerProvider(trace.NewNoopTracerProvider()))\n\n\tsrvOK := httptest.NewUnstartedServer(hOK)\n\tsrvOK.Config.BaseContext = func(_ net.Listener) context.Context { return ctx }\n\tsrvOK.Start()\n\tdefer srvOK.Close()\n\n\tsrvErr := httptest.NewUnstartedServer(hErr)\n\tsrvErr.Config.BaseContext = func(_ net.Listener) context.Context { return ctx }\n\tsrvErr.Start()\n\tdefer srvErr.Close()\n\n\t// test matcher returns nil error\n\turl, err := url.Parse(srvOK.URL)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to parse test server URL into *net/URL\")\n\t}\n\turl.Path = path.Join(\"/\", \"internal\", \"update_diff\")\n\tq := url.Query()\n\tq.Set(\"cur\", \"892737b2-a616-4113-a7a9-137139c8f91e\")\n\turl.RawQuery = q.Encode()\n\tt.Log(url)\n\n\treq, err := httputil.NewRequestWithContext(ctx, http.MethodGet, url.String(), nil)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to construct request: %v\", err)\n\t}\n\tresp, err := srvOK.Client().Do(req)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to do request: %v\", err)\n\t}\n\tdefer resp.Body.Close()\n\tgot, want := resp.StatusCode, http.StatusOK\n\tif got != want {\n\t\tbody, err := io.ReadAll(resp.Body)\n\t\tif err != nil {\n\t\t\tt.Log(\"could not read body of unexpected response\")\n\t\t}\n\t\tt.Logf(\"unexpected response body: %s\", string(body))\n\t\tt.Fatalf(\"got: %v, want: %v\", got, want)\n\t}\n\n\t// test matcher returns error\n\turl, err = url.Parse(srvErr.URL)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to parse test server URL into *net/URL\")\n\t}\n\turl.Path = path.Join(\"/\", \"internal\", \"update_diff\")\n\tq = url.Query()\n\tq.Set(\"cur\", \"892737b2-a616-4113-a7a9-137139c8f91e\")\n\turl.RawQuery = q.Encode()\n\tt.Log(url)\n\n\treq, err = httputil.NewRequestWithContext(ctx, http.MethodGet, url.String(), nil)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to construct request: %v\", err)\n\t}\n\tresp, err = srvErr.Client().Do(req)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to do request: %v\", err)\n\t}\n\tgot, want = resp.StatusCode, http.StatusInternalServerError\n\tif got != want {\n\t\tt.Fatalf(\"got: %v, want: %v\", got, want)\n\t}\n}\n\n// TestUpdateDiffHandlerParams confirms the UpdateDiff handler\n// returns correct status codes given a set or url parameters\nfunc testUpdateDiffHandlerParams(t *testing.T) {\n\tctx := test.Logging(t)\n\tt.Parallel()\n\ttable := []struct {\n\t\tname       string\n\t\tcur        string\n\t\tprev       string\n\t\tstatusCode int\n\t}{\n\t\t{\n\t\t\tname:       \"no params\",\n\t\t\tcur:        \"\",\n\t\t\tprev:       \"\",\n\t\t\tstatusCode: http.StatusBadRequest,\n\t\t},\n\t\t{\n\t\t\tname:       \"missing prev\",\n\t\t\tcur:        \"892737b2-a616-4113-a7a9-137139c8f91e\",\n\t\t\tprev:       \"\",\n\t\t\tstatusCode: http.StatusOK,\n\t\t},\n\t\t{\n\t\t\tname:       \"missing cur\",\n\t\t\tcur:        \"\",\n\t\t\tprev:       \"892737b2-a616-4113-a7a9-137139c8f91e\",\n\t\t\tstatusCode: http.StatusBadRequest,\n\t\t},\n\t\t{\n\t\t\tname:       \"all params\",\n\t\t\tcur:        \"6ea97b35-d886-4845-8ba2-5a4b0a074bfe\",\n\t\t\tprev:       \"892737b2-a616-4113-a7a9-137139c8f91e\",\n\t\t\tstatusCode: http.StatusOK,\n\t\t},\n\t}\n\n\tmOK := &matcher.Mock{\n\t\tUpdateDiff_: func(context.Context, uuid.UUID, uuid.UUID) (*driver.UpdateDiff, error) {\n\t\t\treturn nil, nil\n\t\t},\n\t}\n\th := NewMatcherV1(ctx, \"\", mOK, &indexer.Mock{}, time.Second*10, otelhttp.WithTracerProvider(trace.NewNoopTracerProvider()))\n\tsrv := httptest.NewUnstartedServer(h)\n\tsrv.Config.BaseContext = func(_ net.Listener) context.Context { return ctx }\n\tsrv.Start()\n\tdefer srv.Close()\n\n\tc := srv.Client()\n\tu, err := url.Parse(srv.URL)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to parse test server URL into *net/URL\")\n\t}\n\tu.Path = path.Join(\"/\", \"internal\", \"update_diff\")\n\n\tfor _, test := range table {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tq := u.Query()\n\t\t\tq.Set(\"cur\", test.cur)\n\t\t\tq.Set(\"prev\", test.cur)\n\t\t\tu.RawQuery = q.Encode()\n\n\t\t\treq, err := httputil.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"failed to construct request: %v\", err)\n\t\t\t}\n\t\t\tresp, err := c.Do(req)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"failed to do request: %v\", err)\n\t\t\t}\n\t\t\tgot, want := resp.StatusCode, test.statusCode\n\t\t\tif got != want {\n\t\t\t\tt.Fatalf(\"got: %v, want: %v\", got, want)\n\t\t\t}\n\t\t})\n\t}\n}\n\n// TestUpdateDiffHandlerMethods confirms the UpdateDiffHandler responds correctly\n// to unaccepted HTTP methods.\nfunc testUpdateDiffHandlerMethods(t *testing.T) {\n\tctx := test.Logging(t)\n\tt.Parallel()\n\tmOK := &matcher.Mock{\n\t\tUpdateDiff_: func(context.Context, uuid.UUID, uuid.UUID) (*driver.UpdateDiff, error) {\n\t\t\treturn nil, nil\n\t\t},\n\t}\n\th := NewMatcherV1(ctx, \"\", mOK, &indexer.Mock{}, time.Second*10, otelhttp.WithTracerProvider(trace.NewNoopTracerProvider()))\n\tsrv := httptest.NewUnstartedServer(h)\n\tsrv.Config.BaseContext = func(_ net.Listener) context.Context { return ctx }\n\tsrv.Start()\n\tdefer srv.Close()\n\n\tc := srv.Client()\n\n\tu, err := url.Parse(srv.URL)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to parse test server URL into *net/URL\")\n\t}\n\tu.Path = path.Join(\"/\", \"internal\", \"update_diff\")\n\n\tfor _, m := range []string{\n\t\thttp.MethodConnect,\n\t\thttp.MethodHead,\n\t\thttp.MethodOptions,\n\t\thttp.MethodPatch,\n\t\thttp.MethodPost,\n\t\thttp.MethodPut,\n\t\thttp.MethodTrace,\n\t} {\n\t\treq, err := httputil.NewRequestWithContext(ctx, m, u.String(), nil)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"failed to create request: %v\", err)\n\t\t}\n\t\tresp, err := c.Do(req)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"failed to make request: %v\", err)\n\t\t}\n\t\tif resp.StatusCode != http.StatusMethodNotAllowed {\n\t\t\tt.Fatalf(\"method: %v got: %v want: %v\", m, resp.Status, http.StatusMethodNotAllowed)\n\t\t}\n\t}\n}\n\n// TestUpdateOperationHandler is a parallel harness for testing a UpdateOperation handler.\nfunc TestUpdateOperationHandler(t *testing.T) {\n\tt.Run(\"Methods\", testUpdateOperationHandlerMethods)\n\tt.Run(\"GetAndDelete\", testUpdateOperationHandlerGet)\n\tt.Run(\"Errors\", testUpdateOperationHandlerErrors)\n}\n\n// testUpdateOperationHandlerErrors confirms the handler perfoms the correct\n// actions when a matcher.Differ is failing.\nfunc testUpdateOperationHandlerErrors(t *testing.T) {\n\tctx := test.Logging(t)\n\tt.Parallel()\n\n\tid := uuid.New().String()\n\tErrExpected := fmt.Errorf(\"expected error\")\n\tm := &matcher.Mock{\n\t\tDeleteUpdateOperations_: func(context.Context, ...uuid.UUID) (int64, error) { return 0, ErrExpected },\n\t\t// this will not immediately fail the handler\n\t\tLatestUpdateOperation_: func(context.Context, driver.UpdateKind) (uuid.UUID, error) {\n\t\t\treturn uuid.Nil, ErrExpected\n\t\t},\n\t\tLatestUpdateOperations_: func(context.Context, driver.UpdateKind) (map[string][]driver.UpdateOperation, error) {\n\t\t\treturn nil, ErrExpected\n\t\t},\n\t\tUpdateOperations_: func(context.Context, driver.UpdateKind, ...string) (map[string][]driver.UpdateOperation, error) {\n\t\t\treturn nil, ErrExpected\n\t\t},\n\t}\n\th := NewMatcherV1(ctx, \"\", m, &indexer.Mock{}, time.Second*10, otelhttp.WithTracerProvider(trace.NewNoopTracerProvider()))\n\tsrv := httptest.NewUnstartedServer(h)\n\tsrv.Config.BaseContext = func(_ net.Listener) context.Context { return ctx }\n\tsrv.Start()\n\tdefer srv.Close()\n\tc := srv.Client()\n\n\t// perform get with failing differ\n\treq, err := httputil.NewRequestWithContext(ctx, http.MethodGet, srv.URL+path.Join(\"/\", \"internal\", \"update_operation\"), nil)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to create request: %v\", err)\n\t}\n\tresp, err := c.Do(req)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to make request: %v\", err)\n\t}\n\tif resp.StatusCode != http.StatusInternalServerError {\n\t\tt.Fatalf(\"got: %v, want: %v\", resp.StatusCode, http.StatusInternalServerError)\n\t}\n\n\t// perform delete with failing differ\n\tu := srv.URL + path.Join(\"/\", \"internal\", \"update_operation\") + \"/\" + id\n\treq, err = httputil.NewRequestWithContext(ctx, http.MethodDelete, u, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to create request: %v\", err)\n\t}\n\tresp, err = c.Do(req)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to make request: %v\", err)\n\t}\n\tif resp.StatusCode != http.StatusInternalServerError {\n\t\tt.Fatalf(\"got: %v, want: %v\", resp.StatusCode, http.StatusInternalServerError)\n\t}\n}\n\n// testUpdateOperationHandlerMethods confirms the handler only responds\n// to the desired methods.\nfunc testUpdateOperationHandlerMethods(t *testing.T) {\n\tctx := test.Logging(t)\n\tt.Parallel()\n\th := NewMatcherV1(ctx, \"\", &matcher.Mock{}, &indexer.Mock{}, time.Second*10, otelhttp.WithTracerProvider(trace.NewNoopTracerProvider()))\n\tsrv := httptest.NewUnstartedServer(h)\n\tsrv.Config.BaseContext = func(_ net.Listener) context.Context { return ctx }\n\tsrv.Start()\n\tdefer srv.Close()\n\tc := srv.Client()\n\n\tfor _, m := range []string{\n\t\thttp.MethodConnect,\n\t\thttp.MethodHead,\n\t\thttp.MethodOptions,\n\t\thttp.MethodPatch,\n\t\thttp.MethodPost,\n\t\thttp.MethodPut,\n\t\thttp.MethodTrace,\n\t} {\n\t\treq, err := httputil.NewRequestWithContext(ctx, m, srv.URL+path.Join(\"/\", \"internal\", \"update_operation\"), nil)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"failed to create request: %v\", err)\n\t\t}\n\t\tresp, err := c.Do(req)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"failed to make request: %v\", err)\n\t\t}\n\t\tif resp.StatusCode != http.StatusMethodNotAllowed {\n\t\t\tt.Fatalf(\"method: %v got: %v want: %v\", m, resp.Status, http.StatusMethodNotAllowed)\n\t\t}\n\t}\n}\n\n// testUpdateOperationHandlerGet confirms the handler performs the correct\n// actions on GET.\nfunc testUpdateOperationHandlerGet(t *testing.T) {\n\tctx := test.Logging(t)\n\tt.Parallel()\n\n\tid := uuid.New()\n\tidStr := \"\\\"\" + id.String() + \"\\\"\"\n\tvar called bool\n\tvar latestCalled bool\n\tm := &matcher.Mock{\n\t\tLatestUpdateOperation_: func(context.Context, driver.UpdateKind) (uuid.UUID, error) {\n\t\t\treturn id, nil\n\t\t},\n\t\tLatestUpdateOperations_: func(context.Context, driver.UpdateKind) (map[string][]driver.UpdateOperation, error) {\n\t\t\tlatestCalled = true\n\t\t\treturn nil, nil\n\t\t},\n\t\tUpdateOperations_: func(context.Context, driver.UpdateKind, ...string) (map[string][]driver.UpdateOperation, error) {\n\t\t\tcalled = true\n\t\t\treturn nil, nil\n\t\t},\n\t}\n\th := NewMatcherV1(ctx, \"\", m, &indexer.Mock{}, time.Second*10, otelhttp.WithTracerProvider(trace.NewNoopTracerProvider()))\n\tsrv := httptest.NewUnstartedServer(h)\n\tsrv.Config.BaseContext = func(_ net.Listener) context.Context { return ctx }\n\tsrv.Start()\n\tdefer srv.Close()\n\tc := srv.Client()\n\n\t// get without latest param\n\treq, err := httputil.NewRequestWithContext(ctx, http.MethodGet, srv.URL+path.Join(\"/\", \"internal\", \"update_operation\"), nil)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to create request: %v\", err)\n\t}\n\tresp, err := c.Do(req)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to make request: %v\", err)\n\t}\n\tif resp.StatusCode != http.StatusOK {\n\t\tt.Fatalf(\"got: %v, want: %v\", resp.StatusCode, http.StatusOK)\n\t}\n\tif !called {\n\t\tt.Fatalf(\"got: %v, want: %v\", called, true)\n\t}\n\tetag := resp.Header.Get(\"etag\")\n\tif etag != idStr {\n\t\tt.Fatalf(\"got: %v, want: %v\", etag, id.String())\n\t}\n\n\t// get with latest param\n\tu, _ := url.Parse(srv.URL)\n\tu.Path = path.Join(\"/\", \"internal\", \"update_operation\")\n\tq := u.Query()\n\tq.Add(\"latest\", \"true\")\n\tu.RawQuery = q.Encode()\n\n\treq, err = httputil.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to create request: %v\", err)\n\t}\n\tresp, err = c.Do(req)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to make request: %v\", err)\n\t}\n\tif resp.StatusCode != http.StatusOK {\n\t\tt.Fatalf(\"got: %v, want: %v\", resp.StatusCode, http.StatusOK)\n\t}\n\tif !latestCalled {\n\t\tt.Fatalf(\"got: %v, want: %v\", latestCalled, true)\n\t}\n\tetag = resp.Header.Get(\"etag\")\n\tif etag != idStr {\n\t\tt.Fatalf(\"got: %v, want: %v\", etag, id.String())\n\t}\n}\n"
  },
  {
    "path": "httptransport/notification_v1.go",
    "content": "package httptransport\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"log/slog\"\n\t\"net/http\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/google/uuid\"\n\t\"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp\"\n\n\t\"github.com/quay/clair/v4/internal/codec\"\n\t\"github.com/quay/clair/v4/internal/httputil\"\n\t\"github.com/quay/clair/v4/middleware/compress\"\n\t\"github.com/quay/clair/v4/notifier\"\n)\n\nconst defaultPageSize = 500\n\ntype notificationResponse struct {\n\tPage          notifier.Page           `json:\"page\"`\n\tNotifications []notifier.Notification `json:\"notifications\"`\n}\n\n// NotificationV1 is a Notification endpoint.\ntype NotificationV1 struct {\n\tinner http.Handler\n\tserv  notifier.Service\n}\n\nvar _ http.Handler = (*NotificationV1)(nil)\n\n// NewNotificationV1 returns an http.Handler serving the Notification V1 API rooted at\n// \"prefix\".\nfunc NewNotificationV1(_ context.Context, prefix string, srv notifier.Service, topt otelhttp.Option) (*NotificationV1, error) {\n\tprefix = path.Join(\"/\", prefix) // Ensure the prefix is rooted and cleaned.\n\tm := http.NewServeMux()\n\th := NotificationV1{\n\t\tinner: otelhttp.NewHandler(\n\t\t\tcompress.Handler(m),\n\t\t\t\"notificationv1\",\n\t\t\totelhttp.WithMessageEvents(otelhttp.ReadEvents, otelhttp.WriteEvents),\n\t\t\ttopt,\n\t\t),\n\t\tserv: srv,\n\t}\n\tp := path.Join(prefix, \"notification\") + \"/\"\n\tm.Handle(p, notificationv1wrapper.wrapFunc(path.Join(p, \":id\"), h.serveHTTP))\n\treturn &h, nil\n}\n\n// ServeHTTP implements http.Handler.\nfunc (h *NotificationV1) ServeHTTP(w http.ResponseWriter, r *http.Request) {\n\tstart := time.Now()\n\tr = withRequestID(r)\n\tctx := r.Context()\n\tvar status int\n\tvar length int64\n\tw = httputil.ResponseRecorder(&status, &length, w)\n\tdefer func() {\n\t\tswitch err := http.NewResponseController(w).Flush(); {\n\t\tcase errors.Is(err, nil):\n\t\tcase errors.Is(err, http.ErrNotSupported): // Skip\n\t\tdefault:\n\t\t\tslog.WarnContext(ctx, \"unable to flush http response\", \"reason\", err)\n\t\t}\n\t\tslog.InfoContext(ctx, \"handled HTTP request\",\n\t\t\t\"remote_addr\", r.RemoteAddr,\n\t\t\t\"method\", r.Method,\n\t\t\t\"request_uri\", r.RequestURI,\n\t\t\t\"status\", status,\n\t\t\t\"written\", length,\n\t\t\t\"duration\", time.Since(start))\n\t}()\n\th.inner.ServeHTTP(w, r)\n}\n\nfunc (h *NotificationV1) serveHTTP(w http.ResponseWriter, r *http.Request) {\n\tswitch r.Method {\n\tcase http.MethodGet:\n\t\th.get(w, r)\n\tcase http.MethodDelete:\n\t\th.delete(w, r)\n\tdefault:\n\t\tapiError(r.Context(), w, http.StatusMethodNotAllowed, \"endpoint only allows GET or DELETE\")\n\t}\n}\n\nfunc (h *NotificationV1) delete(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\tpath := r.URL.Path\n\tid := filepath.Base(path)\n\tnotificationID, err := uuid.Parse(id)\n\tif err != nil {\n\t\tslog.WarnContext(ctx, \"could not parse notification id\", \"reason\", err)\n\t\tapiError(ctx, w, http.StatusBadRequest, \"could not parse notification id: %v\", err)\n\t}\n\n\terr = h.serv.DeleteNotifications(ctx, notificationID)\n\tif err != nil {\n\t\tslog.WarnContext(ctx, \"could not delete notification\", \"reason\", err)\n\t\tapiError(ctx, w, http.StatusInternalServerError, \"could not delete notification: %v\", err)\n\t}\n\t// TODO(hank) This should return HTTP 204.\n}\n\n// Get will return paginated notifications to the caller.\nfunc (h *NotificationV1) get(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\tpath := r.URL.Path\n\tid := filepath.Base(path)\n\tnotificationID, err := uuid.Parse(id)\n\tif err != nil {\n\t\tslog.WarnContext(ctx, \"could not parse notification id\", \"reason\", err)\n\t\tapiError(ctx, w, http.StatusBadRequest, \"could not parse notification id: %v\", err)\n\t}\n\n\t// optional page_size parameter\n\tvar pageSize int\n\tif param := r.URL.Query().Get(\"page_size\"); param != \"\" {\n\t\tp, err := strconv.ParseInt(param, 10, 64)\n\t\tif err != nil {\n\t\t\tapiError(ctx, w, http.StatusBadRequest, \"could not parse %q query param into integer\", \"page_size\")\n\t\t}\n\t\tpageSize = int(p)\n\t}\n\tif pageSize == 0 {\n\t\tpageSize = defaultPageSize\n\t}\n\n\t// optional page parameter\n\tvar next *uuid.UUID\n\tif param := r.URL.Query().Get(\"next\"); param != \"\" {\n\t\tn, err := uuid.Parse(param)\n\t\tif err != nil {\n\t\t\tapiError(ctx, w, http.StatusBadRequest, \"could not parse %q query param into integer\", \"next\")\n\t\t}\n\t\tif n != uuid.Nil {\n\t\t\tnext = &n\n\t\t}\n\t}\n\n\tallow := []string{\"application/vnd.clair.notification_page.v1+json\", \"application/json\"}\n\tswitch err := pickContentType(w, r, allow); {\n\tcase errors.Is(err, nil): // OK\n\tcase errors.Is(err, ErrMediaType):\n\t\tapiError(ctx, w, http.StatusUnsupportedMediaType, \"unable to negotiate common media type for %v\", allow)\n\tdefault:\n\t\tapiError(ctx, w, http.StatusBadRequest, \"malformed request: %v\", err)\n\t}\n\n\tinP := &notifier.Page{\n\t\tSize: pageSize,\n\t\tNext: next,\n\t}\n\tnotifications, outP, err := h.serv.Notifications(ctx, notificationID, inP)\n\tif err != nil {\n\t\tapiError(ctx, w, http.StatusInternalServerError, \"failed to retrieve notifications: %v\", err)\n\t}\n\n\tresponse := notificationResponse{\n\t\tPage:          outP,\n\t\tNotifications: notifications,\n\t}\n\n\tdefer writerError(w, &err)()\n\tenc := codec.GetEncoder(w)\n\tdefer codec.PutEncoder(enc)\n\terr = enc.Encode(&response)\n}\n\nfunc init() {\n\tnotificationv1wrapper.init(\"notificationv1\")\n}\n\nvar notificationv1wrapper wrapper\n"
  },
  {
    "path": "httptransport/notification_v1_test.go",
    "content": "package httptransport\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/google/uuid\"\n\t\"github.com/quay/claircore/test\"\n\t\"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp\"\n\t\"go.opentelemetry.io/otel/trace\"\n\n\t\"github.com/quay/clair/v4/internal/httputil\"\n\t\"github.com/quay/clair/v4/notifier\"\n\t\"github.com/quay/clair/v4/notifier/service\"\n)\n\n// TestUpdateOperationHandler is a parallel harness for testing a UpdateOperation handler.\nfunc TestNotificationsHandler(t *testing.T) {\n\tctx := test.Logging(t)\n\tt.Run(\"Methods\", testNotificationsHandlerMethods(ctx))\n\tt.Run(\"Get\", testNotificationHandlerGet(ctx))\n\tt.Run(\"GetParams\", testNotificationHandlerGetParams(ctx))\n\tt.Run(\"Delete\", testNotificationHandlerDelete(ctx))\n}\n\nvar notifierTraceOpt = otelhttp.WithTracerProvider(trace.NewNoopTracerProvider())\n\n// testNotificationHandlerDelete confirms the handler performs a delete\n// correctly\nfunc testNotificationHandlerDelete(ctx context.Context) func(*testing.T) {\n\treturn func(t *testing.T) {\n\t\tt.Parallel()\n\t\tctx := test.Logging(t, ctx)\n\t\tnoteID := uuid.New()\n\n\t\tnm := &service.Mock{\n\t\t\tDeleteNotifications_: func(ctx context.Context, id uuid.UUID) error {\n\t\t\t\tif !cmp.Equal(id, noteID) {\n\t\t\t\t\tt.Fatalf(\"got: %v, want: %v\", id, noteID)\n\t\t\t\t}\n\t\t\t\treturn nil\n\t\t\t},\n\t\t}\n\n\t\th, err := NewNotificationV1(ctx, `/notifier/api/v1/`, nm, notifierTraceOpt)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t}\n\t\trr := httptest.NewRecorder()\n\t\tu, _ := url.Parse(\"http://clair-notifier/notifier/api/v1/notification/\" + noteID.String())\n\t\treq, err := httputil.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t}\n\n\t\th.delete(rr, req)\n\t\tres := rr.Result()\n\t\tif res.StatusCode != http.StatusOK {\n\t\t\tt.Fatalf(\"got: %v, wanted: %v\", res.StatusCode, http.StatusOK)\n\t\t}\n\t}\n}\n\n// testNotificationHandlerGet confirms the Get handler works correctly\nfunc testNotificationHandlerGet(ctx context.Context) func(*testing.T) {\n\treturn func(t *testing.T) {\n\t\tt.Parallel()\n\t\tctx := test.Logging(t, ctx)\n\t\tvar (\n\t\t\tnextID     = uuid.New()\n\t\t\tinPageWant = notifier.Page{\n\t\t\t\tSize: 500,\n\t\t\t}\n\t\t\tnoteID      = uuid.New()\n\t\t\toutPageWant = notifier.Page{\n\t\t\t\tSize: 500,\n\t\t\t\tNext: &nextID,\n\t\t\t}\n\t\t)\n\n\t\tnm := &service.Mock{\n\t\t\tNotifications_: func(ctx context.Context, id uuid.UUID, page *notifier.Page) ([]notifier.Notification, notifier.Page, error) {\n\t\t\t\tif !cmp.Equal(id, noteID) {\n\t\t\t\t\tt.Fatalf(\"got: %v, wanted: %v\", id, noteID)\n\t\t\t\t}\n\t\t\t\tif !cmp.Equal(page, &inPageWant) {\n\t\t\t\t\tt.Fatalf(\"got: %v, wanted: %v\", page, inPageWant)\n\t\t\t\t}\n\t\t\t\treturn []notifier.Notification{}, notifier.Page{\n\t\t\t\t\tSize: inPageWant.Size,\n\t\t\t\t\tNext: &nextID,\n\t\t\t\t}, nil\n\t\t\t},\n\t\t}\n\n\t\th, err := NewNotificationV1(ctx, `/notifier/api/v1/`, nm, notifierTraceOpt)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t}\n\t\trr := httptest.NewRecorder()\n\t\tu, _ := url.Parse(\"http://clair-notifier/notifier/api/v1/notification/\" + noteID.String())\n\t\treq, err := httputil.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t}\n\n\t\th.get(rr, req)\n\t\tres := rr.Result()\n\t\tif res.StatusCode != http.StatusOK {\n\t\t\tt.Errorf(\"got: %v, wanted: %v\", res.StatusCode, http.StatusOK)\n\t\t}\n\t\tvar noteResp notificationResponse\n\t\tif err := json.NewDecoder(res.Body).Decode(&noteResp); err != nil {\n\t\t\tt.Errorf(\"failed to deserialize notification response: %v\", err)\n\t\t}\n\t\tif !cmp.Equal(noteResp.Page, outPageWant) {\n\t\t\tt.Errorf(\"got: %v, want: %v\", noteResp.Page, outPageWant)\n\t\t}\n\t}\n}\n\n// testNotificationHandlerGetParams confirms the Get handler works correctly\n// when parameters are present\nfunc testNotificationHandlerGetParams(ctx context.Context) func(*testing.T) {\n\treturn func(t *testing.T) {\n\t\tt.Parallel()\n\t\tctx := test.Logging(t, ctx)\n\t\tconst (\n\t\t\tpageSizeParam = \"100\"\n\t\t\tpageParam     = \"10\"\n\t\t)\n\t\tvar (\n\t\t\tnextID     = uuid.New()\n\t\t\tinPageWant = notifier.Page{\n\t\t\t\tSize: 100,\n\t\t\t}\n\t\t\tnoteID      = uuid.New()\n\t\t\toutPageWant = notifier.Page{\n\t\t\t\tSize: 100,\n\t\t\t\tNext: &nextID,\n\t\t\t}\n\t\t)\n\n\t\tnm := &service.Mock{\n\t\t\tNotifications_: func(ctx context.Context, id uuid.UUID, page *notifier.Page) ([]notifier.Notification, notifier.Page, error) {\n\t\t\t\tif !cmp.Equal(id, noteID) {\n\t\t\t\t\tt.Fatalf(\"got: %v, wanted: %v\", id, noteID)\n\t\t\t\t}\n\t\t\t\tif !cmp.Equal(page, &inPageWant) {\n\t\t\t\t\tt.Fatalf(\"got: %v, wanted: %v\", page, inPageWant)\n\t\t\t\t}\n\t\t\t\treturn []notifier.Notification{}, notifier.Page{\n\t\t\t\t\tSize: inPageWant.Size,\n\t\t\t\t\tNext: &nextID,\n\t\t\t\t}, nil\n\t\t\t},\n\t\t}\n\n\t\th, err := NewNotificationV1(ctx, `/notifier/api/v1/`, nm, notifierTraceOpt)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t}\n\t\trr := httptest.NewRecorder()\n\t\tu, _ := url.Parse(\"http://clair-notifier/notifier/api/v1/notification/\" + noteID.String())\n\t\tv := url.Values{}\n\t\tv.Set(\"page_size\", pageSizeParam)\n\t\tv.Set(\"page\", pageParam)\n\t\tu.RawQuery = v.Encode()\n\t\treq, err := httputil.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t}\n\n\t\th.get(rr, req)\n\t\tres := rr.Result()\n\t\tif res.StatusCode != http.StatusOK {\n\t\t\tt.Errorf(\"got: %v, wanted: %v\", res.StatusCode, http.StatusOK)\n\t\t}\n\t\tvar noteResp notificationResponse\n\t\tif err := json.NewDecoder(res.Body).Decode(&noteResp); err != nil {\n\t\t\tt.Errorf(\"failed to deserialize notification response: %v\", err)\n\t\t}\n\t\tif !cmp.Equal(noteResp.Page, outPageWant) {\n\t\t\tt.Errorf(\"got: %v, want: %v\", noteResp.Page, outPageWant)\n\t\t}\n\t}\n}\n\nfunc testNotificationsHandlerMethods(ctx context.Context) func(*testing.T) {\n\treturn func(t *testing.T) {\n\t\tt.Parallel()\n\t\tctx := test.Logging(t, ctx)\n\t\th, err := NewNotificationV1(ctx, `/notifier/api/v1/`, &service.Mock{}, notifierTraceOpt)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t}\n\t\tsrv := httptest.NewUnstartedServer(h)\n\t\tsrv.Config.BaseContext = func(_ net.Listener) context.Context { return ctx }\n\t\tsrv.Start()\n\t\tdefer srv.Close()\n\t\tc := srv.Client()\n\t\tu := srv.URL + `/notifier/api/v1/notification/` + uuid.Nil.String()\n\n\t\tfor _, m := range []string{\n\t\t\thttp.MethodConnect,\n\t\t\thttp.MethodHead,\n\t\t\thttp.MethodOptions,\n\t\t\thttp.MethodPatch,\n\t\t\thttp.MethodPost,\n\t\t\thttp.MethodPut,\n\t\t\thttp.MethodTrace,\n\t\t} {\n\t\t\treq, err := httputil.NewRequestWithContext(ctx, m, u, nil)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"failed to create request: %v\", err)\n\t\t\t}\n\t\t\tresp, err := c.Do(req)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"failed to make request: %v\", err)\n\t\t\t}\n\t\t\tif resp.StatusCode != http.StatusMethodNotAllowed {\n\t\t\t\tt.Errorf(\"method: %v got: %v want: %v\", m, resp.Status, http.StatusMethodNotAllowed)\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "httptransport/robotshandler.go",
    "content": "package httptransport\n\nimport (\n\t\"net/http\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/quay/clair/v4/cmd\"\n)\n\nvar startup = time.Now()\n\nconst robotstxt = \"User-agent: *\\nDisallow: /\\n\"\n\n// RobotsHandler provides a \"robots.txt\" endpoint.\nvar robotsHandler http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\th := w.Header()\n\th.Set(\"X-Content-Type-Options\", \"nosniff\")\n\th.Set(\"Content-Type\", \"text/plain; charset=utf-8\")\n\th.Set(\"Cache-Control\", \"no-store\")\n\td := cmd.CommitDate\n\tif d.IsZero() {\n\t\td = startup\n\t}\n\thttp.ServeContent(w, r, \"robots.txt\", d, strings.NewReader(robotstxt))\n})\n"
  },
  {
    "path": "httptransport/robotshandler_test.go",
    "content": "package httptransport\n\nimport (\n\t\"bytes\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/http/httputil\"\n\t\"testing\"\n\n\t\"github.com/google/go-cmp/cmp\"\n)\n\nfunc TestRobotsTXT(t *testing.T) {\n\tr := httptest.NewRequest(http.MethodGet, \"/robots.txt\", nil)\n\tw := httptest.NewRecorder()\n\trobotsHandler.ServeHTTP(w, r)\n\tres, err := httputil.DumpResponse(w.Result(), false)\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n\tt.Logf(\"response:\\n%s\", string(res))\n\n\tif got, want := w.Body.Bytes(), []byte(robotstxt); !bytes.Equal(got, want) {\n\t\tt.Error(cmp.Diff(string(got), string(want)))\n\t}\n}\n"
  },
  {
    "path": "httptransport/server.go",
    "content": "// Package httptransport contains the HTTP logic for implementing the Clair(v4)\n// HTTP API v1.\npackage httptransport\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/quay/clair/config\"\n\t\"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp\"\n\t\"go.opentelemetry.io/otel\"\n\t\"golang.org/x/sync/semaphore\"\n\n\t\"github.com/quay/clair/v4/indexer\"\n\t\"github.com/quay/clair/v4/matcher\"\n\t\"github.com/quay/clair/v4/notifier\"\n)\n\n// These are the various endpoints of the v1 API.\nconst (\n\tapiRoot                      = \"/api/v1/\"\n\tindexerRoot                  = \"/indexer\"\n\tmatcherRoot                  = \"/matcher\"\n\tnotifierRoot                 = \"/notifier\"\n\tinternalRoot                 = apiRoot + \"internal/\"\n\tIndexAPIPath                 = indexerRoot + apiRoot + \"index_report\"\n\tIndexReportAPIPath           = indexerRoot + apiRoot + \"index_report/\"\n\tIndexStateAPIPath            = indexerRoot + apiRoot + \"index_state\"\n\tAffectedManifestAPIPath      = indexerRoot + internalRoot + \"affected_manifest/\"\n\tVulnerabilityReportPath      = matcherRoot + apiRoot + \"vulnerability_report/\"\n\tUpdateOperationAPIPath       = matcherRoot + internalRoot + \"update_operation\"\n\tUpdateOperationDeleteAPIPath = matcherRoot + internalRoot + \"update_operation/\"\n\tUpdateDiffAPIPath            = matcherRoot + internalRoot + \"update_diff\"\n\tNotificationAPIPath          = notifierRoot + apiRoot + \"notification/\"\n\tKeysAPIPath                  = notifierRoot + apiRoot + \"services/notifier/keys\"\n\tKeyByIDAPIPath               = notifierRoot + apiRoot + \"services/notifier/keys/\"\n\tOpenAPIV1Path                = \"/openapi/v1\"\n\t// TODO(hank) These are not actually used anymore. Is it worth keeping them\n\t// just to keep API compatibility/documentation?\n)\n\n// New configures an http.Handler serving the v1 API or a portion of it,\n// according to the passed Config object.\nfunc New(ctx context.Context, conf *config.Config, indexer indexer.Service, matcher matcher.Service, notifier notifier.Service) (http.Handler, error) {\n\tmux := http.NewServeMux()\n\ttraceOpt := otelhttp.WithTracerProvider(otel.GetTracerProvider())\n\n\tmux.Handle(OpenAPIV1Path, DiscoveryHandler(ctx, OpenAPIV1Path, traceOpt))\n\tslog.InfoContext(ctx, \"openapi discovery configured\", \"path\", OpenAPIV1Path)\n\n\t// NOTE(hank) My brain always wants to rewrite constructions like the\n\t// following as a switch, but this is actually cleaner as an \"if\" sequence.\n\n\tif conf.Mode == config.IndexerMode || conf.Mode == config.ComboMode {\n\t\tif indexer == nil {\n\t\t\treturn nil, fmt.Errorf(\"mode %q requires an indexer service\", conf.Mode)\n\t\t}\n\t\tprefix := indexerRoot + apiRoot\n\n\t\tv1, err := NewIndexerV1(ctx, prefix, indexer, traceOpt)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"indexer configuration: %w\", err)\n\t\t}\n\t\tvar sem *semaphore.Weighted\n\t\tif ct := conf.Indexer.IndexReportRequestConcurrency; ct > 0 {\n\t\t\tsem = semaphore.NewWeighted(int64(ct))\n\t\t}\n\t\trl := &limitHandler{\n\t\t\tCheck: func(r *http.Request) (*semaphore.Weighted, string) {\n\t\t\t\tif r.Method != http.MethodPost && r.URL.Path != IndexAPIPath {\n\t\t\t\t\treturn nil, \"\"\n\t\t\t\t}\n\t\t\t\t// Nil if the relevant config option isn't set.\n\t\t\t\treturn sem, IndexAPIPath\n\t\t\t},\n\t\t\tNext: v1,\n\t\t}\n\t\tmux.Handle(prefix, rl)\n\t}\n\tif conf.Mode == config.MatcherMode || conf.Mode == config.ComboMode {\n\t\tif indexer == nil || matcher == nil {\n\t\t\treturn nil, fmt.Errorf(\"mode %q requires both an indexer service and a matcher service\", conf.Mode)\n\t\t}\n\t\tprefix := matcherRoot + apiRoot\n\t\tv1 := NewMatcherV1(ctx, prefix, matcher, indexer, time.Duration(conf.Matcher.CacheAge), traceOpt)\n\t\tmux.Handle(prefix, v1)\n\t}\n\tif conf.Mode == config.NotifierMode || (conf.Mode == config.ComboMode && notifier != nil) {\n\t\tif notifier == nil {\n\t\t\treturn nil, fmt.Errorf(\"mode %q requires a notifier service\", conf.Mode)\n\t\t}\n\t\tprefix := notifierRoot + apiRoot\n\t\tv1, err := NewNotificationV1(ctx, prefix, notifier, traceOpt)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"notifier configuration: %w\", err)\n\t\t}\n\t\tmux.Handle(prefix, v1)\n\t}\n\tif conf.Mode == config.ComboMode && notifier == nil {\n\t\tslog.DebugContext(ctx, \"skipping unconfigured notifier\")\n\t}\n\t// Add endpoint authentication if configured to add auth. Must happen after\n\t// mux was configured for given mode.\n\tif conf.Auth.Any() {\n\t\th, err := authHandler(conf, mux)\n\t\tif err != nil {\n\t\t\tslog.WarnContext(ctx, \"received error configuring auth middleware\", \"reason\", err)\n\t\t\treturn nil, err\n\t\t}\n\t\tfinal := http.NewServeMux()\n\t\tfinal.Handle(\"/robots.txt\", robotsHandler)\n\t\tfinal.Handle(\"/\", h)\n\t\treturn final, nil\n\t}\n\tmux.Handle(\"/robots.txt\", robotsHandler)\n\treturn mux, nil\n}\n\n// IntraserviceIssuer is the issuer that will be used if Clair is configured to\n// mint its own JWTs.\nconst IntraserviceIssuer = `clair-intraservice`\n\n// Unmodified determines whether to return a conditional response.\nfunc unmodified(r *http.Request, v string) bool {\n\tif vs, ok := r.Header[\"If-None-Match\"]; ok {\n\t\tfor _, rv := range vs {\n\t\t\tif rv == v {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t}\n\treturn false\n}\n\n// WriterError is a helper that closes over an error that may be returned after\n// writing a response body starts.\n//\n// The normal error flow can't be used, because the HTTP status code will have\n// been sent and some amount of body data may have been written.\n//\n// To use this, make sure an error variable is predeclared and the returned\n// function is deferred:\n//\n//\tvar err error\n//\tdefer writerError(w, &err)()\n//\t_, err = fallibleWrite(w)\nfunc writerError(w http.ResponseWriter, e *error) func() {\n\tconst errHeader = `Clair-Error`\n\tw.Header().Add(\"trailer\", errHeader)\n\treturn func() {\n\t\tif *e == nil {\n\t\t\treturn\n\t\t}\n\t\tw.Header().Add(errHeader, (*e).Error())\n\t}\n}\n\n// SetCacheControl sets the \"Cache-Control\" header on the response.\nfunc setCacheControl(w http.ResponseWriter, age time.Duration) {\n\t// The odd format string means \"print float as wide as needed and to 0\n\t// precision.\"\n\tconst f = `max-age=%.f`\n\tw.Header().Set(\"cache-control\", fmt.Sprintf(f, age.Seconds()))\n}\n"
  },
  {
    "path": "httptransport/server_test.go",
    "content": "package httptransport\n\nimport (\n\t\"context\"\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/google/uuid\"\n\t\"github.com/quay/clair/config\"\n\t\"github.com/quay/claircore\"\n\t\"github.com/quay/claircore/libvuln/driver\"\n\t\"github.com/quay/claircore/test\"\n\n\t\"github.com/quay/clair/v4/indexer\"\n\t\"github.com/quay/clair/v4/matcher\"\n)\n\n// TestUpdateEndpoints registers the handlers and tests that they're registered\n// at the correct endpoint.\nfunc TestUpdateEndpoints(t *testing.T) {\n\tm := &matcher.Mock{\n\t\tDeleteUpdateOperations_: func(context.Context, ...uuid.UUID) (int64, error) { return 0, nil },\n\t\tUpdateOperations_: func(context.Context, driver.UpdateKind, ...string) (map[string][]driver.UpdateOperation, error) {\n\t\t\treturn nil, nil\n\t\t},\n\t\tLatestUpdateOperation_:  func(context.Context, driver.UpdateKind) (uuid.UUID, error) { return uuid.Nil, nil },\n\t\tLatestUpdateOperations_: func(context.Context, driver.UpdateKind) (map[string][]driver.UpdateOperation, error) { return nil, nil },\n\t\tUpdateDiff_:             func(context.Context, uuid.UUID, uuid.UUID) (*driver.UpdateDiff, error) { return nil, nil },\n\t\tScan_:                   func(context.Context, *claircore.IndexReport) (*claircore.VulnerabilityReport, error) { return nil, nil },\n\t}\n\ti := &indexer.Mock{\n\t\tIndex_: func(ctx context.Context, manifest *claircore.Manifest) (*claircore.IndexReport, error) {\n\t\t\treturn nil, nil\n\t\t},\n\t\tIndexReport_: func(ctx context.Context, digest claircore.Digest) (*claircore.IndexReport, bool, error) {\n\t\t\treturn nil, true, nil\n\t\t},\n\t\tState_: func(ctx context.Context) (string, error) { return \"\", nil },\n\t\tAffectedManifests_: func(ctx context.Context, vulns []claircore.Vulnerability) (*claircore.AffectedManifests, error) {\n\t\t\treturn nil, nil\n\t\t},\n\t}\n\tctx := test.Logging(t)\n\th, err := New(ctx, &config.Config{Mode: config.MatcherMode}, i, m, nil)\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n\tsrv := httptest.NewUnstartedServer(h)\n\tsrv.Config.BaseContext = func(_ net.Listener) context.Context {\n\t\treturn ctx\n\t}\n\tsrv.Start()\n\tdefer srv.Close()\n\tu, err := url.Parse(srv.URL)\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n\tu.Path = path.Join(u.Path, UpdateOperationAPIPath, \"\")\n\tt.Log(u)\n\n\tres, err := srv.Client().Get(u.String())\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n\tif got, want := res.StatusCode, http.StatusOK; got != want {\n\t\tt.Errorf(\"got: %v, want: %v\", got, want)\n\t}\n}\n"
  },
  {
    "path": "httptransport/types/v1/affected_manifests.schema.json",
    "content": "{\n  \"$id\": \"https://clairproject.org/api/http/v1/affected_manifests.schema.json\",\n  \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n  \"title\": \"Affected Manifests\",\n  \"type\": \"object\",\n  \"description\": \"**This is an internal type, documented for completeness.**\\n\\nManifests affected by the specified vulnerability objects.\",\n  \"properties\": {\n    \"vulnerabilities\": {\n      \"type\": \"object\",\n      \"description\": \"Vulnerability objects.\",\n      \"additionalProperties\": {\n        \"$ref\": \"vulnerability.schema.json\"\n      }\n    },\n    \"vulnerable_manifests\": {\n      \"type\": \"object\",\n      \"description\": \"Mapping of manifest digests to vulnerability identifiers.\",\n      \"additionalProperties\": {\n        \"type\": \"array\",\n        \"items\": {\n          \"type\": \"string\",\n          \"description\": \"An identifier to be used in the \\\"vulnerabilities\\\" object.\"\n        }\n      }\n    }\n  },\n  \"required\": [\n    \"vulnerabilities\",\n    \"vulnerable_manifests\"\n  ]\n}\n"
  },
  {
    "path": "httptransport/types/v1/bulk_delete.schema.json",
    "content": "{\n  \"$id\": \"https://clairproject.org/api/http/v1/bulk_delete.schema.json\",\n  \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n  \"title\": \"Bulk Delete\",\n  \"type\": \"array\",\n  \"description\": \"Array of manifest digests to delete from the system.\",\n  \"items\": {\n    \"$ref\": \"digest.schema.json\",\n    \"description\": \"Manifest digest to delete from the system.\"\n  }\n}\n"
  },
  {
    "path": "httptransport/types/v1/cpe.schema.json",
    "content": "{\n  \"$id\": \"https://clairproject.org/api/http/v1/cpe.schema.json\",\n  \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n  \"title\": \"Common Platform Enumeration Name\",\n  \"description\": \"This is a CPE Name in either v2.2 \\\"URI\\\" form or v2.3 \\\"Formatted String\\\" form.\",\n  \"$comment\": \"Clair only produces v2.3 CPE Names. Any v2.2 Names will be normalized into v2.3 form.\",\n  \"type\": \"string\",\n  \"oneOf\": [\n    {\n      \"description\": \"This is the CPE 2.2 regexp: https://cpe.mitre.org/specification/2.2/cpe-language_2.2.xsd\",\n      \"type\": \"string\",\n      \"pattern\": \"^[c][pP][eE]:/[AHOaho]?(:[A-Za-z0-9\\\\._\\\\-~%]*){0,6}$\"\n    },\n    {\n      \"description\": \"This is the CPE 2.3 regexp: https://csrc.nist.gov/schema/cpe/2.3/cpe-naming_2.3.xsd\",\n      \"type\": \"string\",\n      \"pattern\": \"^cpe:2\\\\.3:[aho\\\\*\\\\-](:(((\\\\?*|\\\\*?)([a-zA-Z0-9\\\\-\\\\._]|(\\\\\\\\[\\\\\\\\\\\\*\\\\?!\\\"#$$%&'\\\\(\\\\)\\\\+,/:;<=>@\\\\[\\\\]\\\\^`\\\\{\\\\|}~]))+(\\\\?*|\\\\*?))|[\\\\*\\\\-])){5}(:(([a-zA-Z]{2,3}(-([a-zA-Z]{2}|[0-9]{3}))?)|[\\\\*\\\\-]))(:(((\\\\?*|\\\\*?)([a-zA-Z0-9\\\\-\\\\._]|(\\\\\\\\[\\\\\\\\\\\\*\\\\?!\\\"#$$%&'\\\\(\\\\)\\\\+,/:;<=>@\\\\[\\\\]\\\\^`\\\\{\\\\|}~]))+(\\\\?*|\\\\*?))|[\\\\*\\\\-])){4}$\"\n    }\n  ]\n}\n"
  },
  {
    "path": "httptransport/types/v1/digest.schema.json",
    "content": "{\n  \"$id\": \"https://clairproject.org/api/http/v1/digest.schema.json\",\n  \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n  \"title\": \"Digest\",\n  \"description\": \"A digest acts as a content identifier, enabling content addressability.\",\n  \"type\": \"string\",\n  \"anyOf\": [\n    {\n      \"$comment\": \"SHA256: MUST be implemented\",\n      \"description\": \"SHA256\",\n      \"type\": \"string\",\n      \"pattern\": \"^sha256:[a-f0-9]{64}$\"\n    },\n    {\n      \"$comment\": \"SHA512: MAY be implemented\",\n      \"description\": \"SHA512\",\n      \"type\": \"string\",\n      \"pattern\": \"^sha512:[a-f0-9]{128}$\"\n    },\n    {\n      \"$comment\": \"BLAKE3: MAY be implemented\",\n      \"description\": \"BLAKE3\\n\\n**Currently not implemented.**\",\n      \"type\": \"string\",\n      \"pattern\": \"^blake3:[a-f0-9]{64}$\"\n    }\n  ]\n}\n"
  },
  {
    "path": "httptransport/types/v1/distribution.schema.json",
    "content": "{\n  \"$id\": \"https://clairproject.org/api/http/v1/distribution.schema.json\",\n  \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n  \"title\": \"Distribution\",\n  \"type\": \"object\",\n  \"description\": \"Distribution is the accompanying system context of a Package.\",\n  \"properties\": {\n    \"id\": {\n      \"description\": \"Unique ID for this Distribution. May be unique to the response document, not the whole system.\",\n      \"type\": \"string\"\n    },\n    \"did\": {\n      \"description\": \"A lower-case string (no spaces or other characters outside of 0–9, a–z, \\\".\\\", \\\"_\\\", and \\\"-\\\") identifying the operating system, excluding any version information and suitable for processing by scripts or usage in generated filenames.\",\n      \"type\": \"string\"\n    },\n    \"name\": {\n      \"description\": \"A string identifying the operating system.\",\n      \"type\": \"string\"\n    },\n    \"version\": {\n      \"description\": \"A string identifying the operating system version, excluding any OS name information, possibly including a release code name, and suitable for presentation to the user.\",\n      \"type\": \"string\"\n    },\n    \"version_code_name\": {\n      \"description\": \"A lower-case string (no spaces or other characters outside of 0–9, a–z, \\\".\\\", \\\"_\\\", and \\\"-\\\") identifying the operating system release code name, excluding any OS name information or release version, and suitable for processing by scripts or usage in generated filenames.\",\n      \"type\": \"string\"\n    },\n    \"version_id\": {\n      \"description\": \"A lower-case string (mostly numeric, no spaces or other characters outside of 0–9, a–z, \\\".\\\", \\\"_\\\", and \\\"-\\\") identifying the operating system version, excluding any OS name information or release code name.\",\n      \"type\": \"string\"\n    },\n    \"arch\": {\n      \"description\": \"A string identifying the OS architecture.\",\n      \"type\": \"string\"\n    },\n    \"cpe\": {\n      \"description\": \"Common Platform Enumeration name.\",\n      \"$ref\": \"cpe.schema.json\"\n    },\n    \"pretty_name\": {\n      \"description\": \"A pretty operating system name in a format suitable for presentation to the user.\",\n      \"type\": \"string\"\n    }\n  },\n  \"additionalProperties\": false,\n  \"required\": [\n    \"id\"\n  ]\n}\n"
  },
  {
    "path": "httptransport/types/v1/environment.schema.json",
    "content": "{\n  \"$id\": \"https://clairproject.org/api/http/v1/environment.schema.json\",\n  \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n  \"title\": \"Environment\",\n  \"type\": \"object\",\n  \"description\": \"Environment describes the surrounding environment a package was discovered in.\",\n  \"properties\": {\n    \"package_db\": {\n      \"description\": \"The database the associated Package was discovered in.\",\n      \"type\": \"string\"\n    },\n    \"distribution_id\": {\n      \"description\": \"The ID of the Distribution of the associated Package.\",\n      \"type\": \"string\"\n    },\n    \"introduced_in\": {\n      \"description\": \"The Layer the associated Package was introduced in.\",\n      \"$ref\": \"digest.schema.json\"\n    },\n    \"repository_ids\": {\n      \"description\": \"The IDs of the Repositories of the associated Package.\",\n      \"type\": \"array\",\n      \"items\": {\n        \"type\": \"string\"\n      }\n    }\n  },\n  \"additionalProperties\": false\n}\n"
  },
  {
    "path": "httptransport/types/v1/error.schema.json",
    "content": "{\n  \"$id\": \"https://clairproject.org/api/http/v1/error.schema.json\",\n  \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n  \"title\": \"Error\",\n  \"type\": \"object\",\n  \"description\": \"A general error response.\",\n  \"properties\": {\n    \"code\": {\n      \"type\": \"string\",\n      \"description\": \"a code for this particular error\"\n    },\n    \"message\": {\n      \"type\": \"string\",\n      \"description\": \"a message with further detail\"\n    }\n  },\n  \"required\": [\n    \"message\"\n  ]\n}\n"
  },
  {
    "path": "httptransport/types/v1/index_report.schema.json",
    "content": "{\n  \"$id\": \"https://clairproject.org/api/http/v1/index_report.schema.json\",\n  \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n  \"title\": \"Index Report\",\n  \"type\": \"object\",\n  \"description\": \"An index of the contents of a Manifest.\",\n  \"properties\": {\n    \"manifest_hash\": {\n      \"$ref\": \"digest.schema.json\",\n      \"description\": \"The Manifest's digest.\"\n    },\n    \"state\": {\n      \"type\": \"string\",\n      \"description\": \"The current state of the index operation\"\n    },\n    \"err\": {\n      \"type\": \"string\",\n      \"description\": \"An error message on event of unsuccessful index\"\n    },\n    \"success\": {\n      \"type\": \"boolean\",\n      \"description\": \"A bool indicating succcessful index\"\n    },\n    \"packages\": {\n      \"type\": \"object\",\n      \"description\": \"A map of Package objects indexed by a document-local identifier.\",\n      \"additionalProperties\": {\n        \"$ref\": \"package.schema.json\"\n      }\n    },\n    \"distributions\": {\n      \"type\": \"object\",\n      \"description\": \"A map of Distribution objects indexed by a document-local identifier.\",\n      \"additionalProperties\": {\n        \"$ref\": \"distribution.schema.json\"\n      }\n    },\n    \"repository\": {\n      \"type\": \"object\",\n      \"description\": \"A map of Repository objects indexed by a document-local identifier.\",\n      \"additionalProperties\": {\n        \"$ref\": \"repository.schema.json\"\n      }\n    },\n    \"environments\": {\n      \"type\": \"object\",\n      \"description\": \"A map of Environment arrays indexed by a Package's identifier.\",\n      \"additionalProperties\": {\n        \"type\": \"array\",\n        \"items\": {\n          \"$ref\": \"environment.schema.json\"\n        }\n      }\n    }\n  },\n  \"additionalProperties\": false,\n  \"required\": [\n    \"manifest_hash\",\n    \"state\",\n    \"success\"\n  ]\n}\n"
  },
  {
    "path": "httptransport/types/v1/index_state.schema.json",
    "content": "{\n  \"$id\": \"https://clairproject.org/api/http/v1/index_state.schema.json\",\n  \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n  \"title\": \"Index State\",\n  \"type\": \"object\",\n  \"description\": \"Information on the state of the indexer system.\",\n  \"properties\": {\n    \"state\": {\n      \"type\": \"string\",\n      \"description\": \"an opaque token\"\n    }\n  },\n  \"required\": [\n    \"state\"\n  ]\n}\n"
  },
  {
    "path": "httptransport/types/v1/layer.schema.json",
    "content": "{\n  \"$id\": \"https://clairproject.org/api/http/v1/layer.schema.json\",\n  \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n  \"title\": \"Layer\",\n  \"type\": \"object\",\n  \"description\": \"Layer is a description of a container layer. It should contain enough information to fetch the layer.\",\n  \"properties\": {\n    \"hash\": {\n      \"$ref\": \"digest.schema.json\",\n      \"description\": \"Digest of the layer blob.\"\n    },\n    \"uri\": {\n      \"type\": \"string\",\n      \"format\": \"uri\",\n      \"description\": \"A URI indicating where the layer blob can be downloaded from.\"\n    },\n    \"headers\": {\n      \"description\": \"Any additional HTTP-style headers needed for requesting layers.\",\n      \"type\": \"object\",\n      \"patternProperties\": {\n        \"^[a-zA-Z0-9\\\\-_]+$\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"string\"\n          }\n        }\n      }\n    },\n    \"media_type\": {\n      \"description\": \"The OCI Layer media type for this layer.\",\n      \"type\": \"string\",\n      \"pattern\": \"^application/vnd\\\\.oci\\\\.image\\\\.layer\\\\.v1\\\\.tar(\\\\+(gzip|zstd))?$\"\n    }\n  },\n  \"additionalProperties\": false,\n  \"required\": [\n    \"hash\",\n    \"uri\"\n  ]\n}\n"
  },
  {
    "path": "httptransport/types/v1/manifest.schema.json",
    "content": "{\n  \"$id\": \"https://clairproject.org/api/http/v1/manifest.schema.json\",\n  \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n  \"title\": \"Manifest\",\n  \"type\": \"object\",\n  \"description\": \"A description of an OCI Image Manifest.\",\n  \"properties\": {\n    \"hash\": {\n      \"$ref\": \"digest.schema.json\",\n      \"description\": \"The OCI Image Manifest's digest.\\n\\nThis is used as an identifier throughout the system. This **SHOULD** be the same as the OCI Image Manifest's digest, but this is not enforced.\"\n    },\n    \"layers\": {\n      \"type\": \"array\",\n      \"description\": \"The OCI Layers making up the Image, in order.\",\n      \"items\": {\n        \"$ref\": \"layer.schema.json\"\n      }\n    }\n  },\n  \"additionalProperties\": false,\n  \"required\": [\n    \"hash\"\n  ]\n}\n"
  },
  {
    "path": "httptransport/types/v1/normalized_severity.schema.json",
    "content": "{\n  \"$id\": \"https://clairproject.org/api/http/v1/normalized_severity.schema.json\",\n  \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n  \"title\": \"Normalized Severity\",\n  \"description\": \"Standardized severity values.\",\n  \"enum\": [\n    \"Unknown\",\n    \"Negligible\",\n    \"Low\",\n    \"Medium\",\n    \"High\",\n    \"Critical\"\n  ]\n}\n"
  },
  {
    "path": "httptransport/types/v1/notification.schema.json",
    "content": "{\n  \"$id\": \"https://clairproject.org/api/http/v1/notification.schema.json\",\n  \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n  \"title\": \"Notification\",\n  \"type\": \"object\",\n  \"description\": \"A change in a manifest affected by a vulnerability.\",\n  \"properties\": {\n    \"id\": {\n      \"description\": \"Unique identifier for this notification.\",\n      \"type\": \"string\"\n    },\n    \"manifest\": {\n      \"$ref\": \"digest.schema.json\",\n      \"description\": \"The digest of the manifest affected by the provided vulnerability.\"\n    },\n    \"reason\": {\n      \"description\": \"The reason for the notifcation.\",\n      \"enum\": [\n        \"added\",\n        \"removed\"\n      ]\n    },\n    \"vulnerability\": {\n      \"$ref\": \"vulnerability_summary.schema.json\"\n    }\n  },\n  \"additionalProperties\": false,\n  \"required\": [\n    \"id\",\n    \"manifest\",\n    \"reason\",\n    \"vulnerability\"\n  ]\n}\n"
  },
  {
    "path": "httptransport/types/v1/notification_page.schema.json",
    "content": "{\n  \"$id\": \"https://clairproject.org/api/http/v1/notification_page.schema.json\",\n  \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n  \"title\": \"Notification Page\",\n  \"type\": \"object\",\n  \"description\": \"A page description and list of notifications.\",\n  \"properties\": {\n    \"page\": {\n      \"description\": \"An object informing the client the next page to retrieve.\",\n      \"type\": \"object\",\n      \"properties\": {\n        \"size\": {\n          \"description\": \"The number of notifications contained in this page.\",\n          \"type\": \"integer\"\n        },\n        \"next\": {\n          \"description\": \"The identififer to pass into the \\\"next\\\" parameter of a future GetNotification request.\\n\\nIf not present, there are no additional pages.\",\n          \"type\": \"string\"\n        }\n      },\n      \"additionalProperties\": false,\n      \"required\": [\n        \"size\"\n      ]\n    },\n    \"notifications\": {\n      \"description\": \"Notifications within this page.\",\n      \"type\": \"array\",\n      \"items\": {\n        \"$ref\": \"notification.schema.json\"\n      }\n    }\n  },\n  \"additionalProperties\": false,\n  \"required\": [\n    \"page\",\n    \"notifications\"\n  ]\n}\n"
  },
  {
    "path": "httptransport/types/v1/notification_webhook.schema.json",
    "content": "{\n  \"$id\": \"https://clairproject.org/api/http/v1/notification_webhook.schema.json\",\n  \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n  \"title\": \"Notification Webhook\",\n  \"type\": \"object\",\n  \"description\": \"Webhook sent to a configured service to begin retrieving notifications.\",\n  \"properties\": {\n    \"notification_id\": {\n      \"description\": \"Unique identifier for this notification.\",\n      \"type\": \"string\"\n    },\n    \"callback\": {\n      \"description\": \"A URL to retrieve paginated Notification objects.\",\n      \"type\": \"string\",\n      \"format\": \"uri\"\n    }\n  },\n  \"additionalProperties\": false,\n  \"required\": [\n    \"notification_id\",\n    \"callback\"\n  ]\n}\n"
  },
  {
    "path": "httptransport/types/v1/package.schema.json",
    "content": "{\n  \"$id\": \"https://clairproject.org/api/http/v1/package.schema.json\",\n  \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n  \"title\": \"Package\",\n  \"type\": \"object\",\n  \"description\": \"Description of installed software.\",\n  \"properties\": {\n    \"id\": {\n      \"description\": \"Unique ID for this Package. May be unique to the response document, not the whole system.\",\n      \"type\": \"string\"\n    },\n    \"name\": {\n      \"description\": \"Identifier of this Package.\\n\\nThe uniqueness and scoping of this name depends on the packaging system.\",\n      \"type\": \"string\"\n    },\n    \"version\": {\n      \"description\": \"Version of this Package, as reported by the packaging system.\",\n      \"type\": \"string\"\n    },\n    \"kind\": {\n      \"description\": \"The \\\"kind\\\" of this Package.\",\n      \"enum\": [\n        \"BINARY\",\n        \"SOURCE\"\n      ],\n      \"default\": \"BINARY\"\n    },\n    \"source\": {\n      \"$ref\": \"#\",\n      \"description\": \"Source Package that produced the current binary Package, if known.\"\n    },\n    \"normalized_version\": {\n      \"description\": \"Normalized representation of the discoverd version.\\n\\nThe format is not specific, but is guarenteed to be forward compatible.\",\n      \"type\": \"string\"\n    },\n    \"module\": {\n      \"description\": \"An identifier for intra-Repository grouping of packages.\\n\\nLikely only relevant on rpm-based systems.\",\n      \"type\": \"string\"\n    },\n    \"arch\": {\n      \"description\": \"Native architecture for the Package.\",\n      \"type\": \"string\",\n      \"$comment\": \"This should become and enum in the future.\"\n    },\n    \"cpe\": {\n      \"$ref\": \"cpe.schema.json\",\n      \"description\": \"CPE Name for the Package.\"\n    }\n  },\n  \"additionalProperties\": false,\n  \"required\": [\n    \"name\",\n    \"version\"\n  ]\n}\n"
  },
  {
    "path": "httptransport/types/v1/range.schema.json",
    "content": "{\n  \"$id\": \"https://clairproject.org/api/http/v1/range.schema.json\",\n  \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n  \"title\": \"Range\",\n  \"type\": \"object\",\n  \"description\": \"A range of versions.\",\n  \"properties\": {\n    \"[\": {\n      \"type\": \"string\",\n      \"description\": \"Lower bound, inclusive.\"\n    },\n    \")\": {\n      \"type\": \"string\",\n      \"description\": \"Upper bound, exclusive.\"\n    }\n  },\n  \"minProperties\": 1,\n  \"additionalProperties\": false\n}\n"
  },
  {
    "path": "httptransport/types/v1/repository.schema.json",
    "content": "{\n  \"$id\": \"https://clairproject.org/api/http/v1/repository.schema.json\",\n  \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n  \"title\": \"Repository\",\n  \"type\": \"object\",\n  \"description\": \"Description of a software repository\",\n  \"properties\": {\n    \"id\": {\n      \"description\": \"Unique ID for this Repository. May be unique to the response document, not the whole system.\",\n      \"type\": \"string\"\n    },\n    \"name\": {\n      \"description\": \"Human-relevant name for the Repository.\",\n      \"type\": \"string\"\n    },\n    \"key\": {\n      \"description\": \"Machine-relevant name for the Repository.\",\n      \"type\": \"string\"\n    },\n    \"uri\": {\n      \"description\": \"URI describing the Repository.\",\n      \"type\": \"string\",\n      \"format\": \"uri\"\n    },\n    \"cpe\": {\n      \"description\": \"CPE name for the Repository.\",\n      \"$ref\": \"cpe.schema.json\"\n    }\n  },\n  \"additionalProperties\": false,\n  \"required\": [\n    \"id\"\n  ]\n}\n"
  },
  {
    "path": "httptransport/types/v1/types.go",
    "content": "// Package types provides JSON Schemas for the HTTP API.\npackage types\n\nimport (\n\t\"embed\"\n)\n\n//go:generate sh -euc \"for f in *.json; do <$DOLLAR{f} >$DOLLAR{f}_ jq -e .; mv $DOLLAR{f}_ $DOLLAR{f}; done\"\n\n//go:embed *.schema.json\nvar Schema embed.FS\n"
  },
  {
    "path": "httptransport/types/v1/update_diff.schema.json",
    "content": "{\n  \"$id\": \"https://clairproject.org/api/http/v1/update_diff.schema.json\",\n  \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n  \"title\": \"Update Difference\",\n  \"type\": \"object\",\n  \"description\": \"**This is an internal type, documented for completeness.**\\n\\nAn update difference describes changes between two Update Operations.\",\n  \"properties\": {\n    \"prev\": {\n      \"description\": \"The previous Update Operation.\",\n      \"$ref\": \"update_operation.schema.json\"\n    },\n    \"cur\": {\n      \"description\": \"The current Update Operation.\",\n      \"$ref\": \"update_operation.schema.json\"\n    },\n    \"added\": {\n      \"description\": \"Vulnerabilities present in \\\"cur\\\", but not \\\"prev\\\".\",\n      \"type\": \"array\",\n      \"items\": {\n        \"$ref\": \"vulnerability.schema.json\"\n      }\n    },\n    \"removed\": {\n      \"description\": \"Vulnerabilities present in \\\"prev\\\", but not \\\"cur\\\".\",\n      \"type\": \"array\",\n      \"items\": {\n        \"$ref\": \"vulnerability.schema.json\"\n      }\n    }\n  },\n  \"additionalProperties\": false,\n  \"required\": [\n    \"cur\",\n    \"added\",\n    \"removed\"\n  ]\n}\n"
  },
  {
    "path": "httptransport/types/v1/update_operation.schema.json",
    "content": "{\n  \"$id\": \"https://clairproject.org/api/http/v1/update_operation.schema.json\",\n  \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n  \"title\": \"Update Operation\",\n  \"type\": \"object\",\n  \"description\": \"**This is an internal type, documented for completeness.**\\n\\nAn update operations describes an update of the internal vulnerability database.\",\n  \"properties\": {\n    \"ref\": {\n      \"type\": \"string\",\n      \"description\": \"A unique identifier for this update operation.\"\n    },\n    \"updater\": {\n      \"description\": \"The \\\"updater\\\" component that was run.\",\n      \"$comment\": \"This is not as useful as it could be: an end user needs to know too much about Clair(core)'s internals to make sense of it.\",\n      \"type\": \"string\"\n    },\n    \"fingerprint\": {\n      \"description\": \"The stored \\\"fingerprint\\\" of this run.\",\n      \"type\": \"string\"\n    },\n    \"date\": {\n      \"type\": \"string\",\n      \"description\": \"When this operation was run.\",\n      \"format\": \"date-time\"\n    },\n    \"kind\": {\n      \"description\": \"The kind of data this operation updated.\",\n      \"enum\": [\n        \"vulnerability\",\n        \"enrichment\"\n      ]\n    }\n  },\n  \"additionalProperties\": false,\n  \"required\": [\n    \"ref\",\n    \"updater\",\n    \"fingerprint\",\n    \"date\",\n    \"kind\"\n  ]\n}\n"
  },
  {
    "path": "httptransport/types/v1/update_operations.schema.json",
    "content": "{\n  \"$id\": \"https://clairproject.org/api/http/v1/update_operations.schema.json\",\n  \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n  \"title\": \"Update Operations\",\n  \"type\": \"object\",\n  \"description\": \"**This is an internal type, documented for completeness.**\\n\\nA mapping of updater id to Update Operation(s).\",\n  \"additionalProperties\": {\n    \"type\": \"array\",\n    \"items\": {\n      \"$ref\": \"update_operation.schema.json\"\n    }\n  }\n}\n"
  },
  {
    "path": "httptransport/types/v1/vulnerability.schema.json",
    "content": "{\n  \"$id\": \"https://clairproject.org/api/http/v1/vulnerability.schema.json\",\n  \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n  \"title\": \"Vulnerability\",\n  \"type\": \"object\",\n  \"description\": \"Description of a software flaw.\",\n  \"$ref\": \"vulnerability_core.schema.json\",\n  \"properties\": {\n    \"id\": {\n      \"description\": \"Unique ID for this Vulnerabiltity. May be unique to the response document, not the whole system.\",\n      \"type\": \"string\"\n    },\n    \"updater\": {\n      \"description\": \"The updater component this Vulnerability came from.\",\n      \"type\": \"string\"\n    },\n    \"description\": {\n      \"description\": \"A human-readable description of the vulnerability.\",\n      \"type\": \"string\"\n    },\n    \"issued\": {\n      \"description\": \"The datetime this Vulnerability was issued, if known.\",\n      \"type\": \"string\",\n      \"format\": \"date-time\"\n    },\n    \"links\": {\n      \"description\": \"Space-separated URIs to more information.\",\n      \"type\": \"string\"\n    }\n  },\n  \"unevaluatedProperties\": false,\n  \"required\": [\n    \"id\",\n    \"updater\"\n  ]\n}\n"
  },
  {
    "path": "httptransport/types/v1/vulnerability_core.schema.json",
    "content": "{\n  \"$id\": \"https://clairproject.org/api/http/v1/vulnerability_core.schema.json\",\n  \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n  \"title\": \"Vulnerability Core\",\n  \"type\": \"object\",\n  \"description\": \"The core elements of vulnerabilities in the Clair system.\",\n  \"properties\": {\n    \"name\": {\n      \"type\": \"string\",\n      \"description\": \"Human-readable name, as presented in the vendor data.\"\n    },\n    \"fixed_in_version\": {\n      \"type\": \"string\",\n      \"description\": \"Version string, as presented in the vendor data.\"\n    },\n    \"severity\": {\n      \"type\": \"string\",\n      \"description\": \"Severity, as presented in the vendor data.\"\n    },\n    \"normalized_severity\": {\n      \"$ref\": \"normalized_severity.schema.json\",\n      \"description\": \"A well defined set of severity strings guaranteed to be present.\"\n    },\n    \"range\": {\n      \"$ref\": \"range.schema.json\",\n      \"description\": \"Range of versions the vulnerability applies to.\"\n    },\n    \"arch_op\": {\n      \"description\": \"Flag indicating how the referenced package's \\\"arch\\\" member should be interpreted.\",\n      \"enum\": [\n        \"equals\",\n        \"not equals\",\n        \"pattern match\"\n      ]\n    },\n    \"package\": {\n      \"$ref\": \"package.schema.json\",\n      \"description\": \"A package description\"\n    },\n    \"distribution\": {\n      \"$ref\": \"distribution.schema.json\",\n      \"description\": \"A distribution description\"\n    },\n    \"repository\": {\n      \"$ref\": \"repository.schema.json\",\n      \"description\": \"A repository description\"\n    }\n  },\n  \"required\": [\n    \"name\",\n    \"normalized_severity\"\n  ],\n  \"dependentRequired\": {\n    \"package\": [\n      \"arch_op\"\n    ]\n  },\n  \"anyOf\": [\n    {\n      \"required\": [\n        \"package\"\n      ]\n    },\n    {\n      \"required\": [\n        \"repository\"\n      ]\n    },\n    {\n      \"required\": [\n        \"distribution\"\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": "httptransport/types/v1/vulnerability_report.schema.json",
    "content": "{\n  \"$id\": \"https://clairproject.org/api/http/v1/vulnerability_report.schema.json\",\n  \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n  \"title\": \"Vulnerability Report\",\n  \"type\": \"object\",\n  \"description\": \"A report with discovered packages, package environments, and package vulnerabilities within a Manifest.\",\n  \"properties\": {\n    \"manifest_hash\": {\n      \"$ref\": \"digest.schema.json\",\n      \"description\": \"The Manifest's digest.\"\n    },\n    \"packages\": {\n      \"type\": \"object\",\n      \"description\": \"A map of Package objects indexed by a document-local identifier.\",\n      \"additionalProperties\": {\n        \"$ref\": \"package.schema.json\"\n      }\n    },\n    \"distributions\": {\n      \"type\": \"object\",\n      \"description\": \"A map of Distribution objects indexed by a document-local identifier.\",\n      \"additionalProperties\": {\n        \"$ref\": \"distribution.schema.json\"\n      }\n    },\n    \"repository\": {\n      \"type\": \"object\",\n      \"description\": \"A map of Repository objects indexed by a document-local identifier.\",\n      \"additionalProperties\": {\n        \"$ref\": \"repository.schema.json\"\n      }\n    },\n    \"environments\": {\n      \"type\": \"object\",\n      \"description\": \"A map of Environment arrays indexed by a Package's identifier.\",\n      \"additionalProperties\": {\n        \"type\": \"array\",\n        \"items\": {\n          \"$ref\": \"environment.schema.json\"\n        }\n      }\n    },\n    \"vulnerabilities\": {\n      \"type\": \"object\",\n      \"description\": \"A map of Vulnerabilities indexed by a document-local identifier.\",\n      \"additionalProperties\": {\n        \"$ref\": \"vulnerability.schema.json\"\n      }\n    },\n    \"package_vulnerabilities\": {\n      \"type\": \"object\",\n      \"description\": \"A mapping of Vulnerability identifier lists indexed by Package identifier.\",\n      \"additionalProperties\": {\n        \"type\": \"array\",\n        \"items\": {\n          \"type\": \"string\"\n        }\n      }\n    },\n    \"enrichments\": {\n      \"type\": \"object\",\n      \"description\": \"A mapping of extra \\\"enrichment\\\" data by type\",\n      \"additionalProperties\": {\n        \"type\": \"array\"\n      }\n    }\n  },\n  \"additionalProperties\": false,\n  \"required\": [\n    \"distributions\",\n    \"environments\",\n    \"manifest_hash\",\n    \"packages\",\n    \"package_vulnerabilities\",\n    \"vulnerabilities\"\n  ]\n}\n"
  },
  {
    "path": "httptransport/types/v1/vulnerability_summaries.schema.json",
    "content": "{\n  \"$id\": \"https://clairproject.org/api/http/v1/vulnerability_summaries.schema.json\",\n  \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n  \"title\": \"Vulnerability Summaries\",\n  \"type\": \"array\",\n  \"description\": \"**This is an internal type, documented for completeness.**\\n\\nThis is an array of pseudo-Vulnerability objects used for reverse-lookup.\",\n  \"items\": {\n    \"description\": \"Summary vulnerability objects.\",\n    \"$ref\": \"vulnerability_summary.schema.json\"\n  }\n}\n"
  },
  {
    "path": "httptransport/types/v1/vulnerability_summary.schema.json",
    "content": "{\n  \"$id\": \"https://clairproject.org/api/http/v1/vulnerability_summary.schema.json\",\n  \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n  \"title\": \"Vulnerability Summary\",\n  \"type\": \"object\",\n  \"description\": \"A summary of a vulnerability.\",\n  \"$ref\": \"vulnerability_core.schema.json\",\n  \"unevaluatedProperties\": false\n}\n"
  },
  {
    "path": "indexer/mock.go",
    "content": "package indexer\n\nimport (\n\t\"context\"\n\n\t\"github.com/quay/claircore\"\n)\n\nvar _ Service = (*Mock)(nil)\n\n// Mock implements a mock indexer Service\n//\n// If a particular method is not provided an implementation to a constructed Mock\n// an \"unexpected call\" panic will occur.\ntype Mock struct {\n\tIndex_             func(ctx context.Context, manifest *claircore.Manifest) (*claircore.IndexReport, error)\n\tIndexReport_       func(ctx context.Context, digest claircore.Digest) (*claircore.IndexReport, bool, error)\n\tState_             func(ctx context.Context) (string, error)\n\tAffectedManifests_ func(ctx context.Context, vulns []claircore.Vulnerability) (*claircore.AffectedManifests, error)\n\tDeleteManifests_   func(context.Context, ...claircore.Digest) ([]claircore.Digest, error)\n}\n\nfunc (i *Mock) Index(ctx context.Context, manifest *claircore.Manifest) (*claircore.IndexReport, error) {\n\tif i.Index_ == nil {\n\t\tpanic(\"mock indexer: unexpected call to Index\")\n\t}\n\treturn i.Index_(ctx, manifest)\n}\n\nfunc (i *Mock) IndexReport(ctx context.Context, digest claircore.Digest) (*claircore.IndexReport, bool, error) {\n\tif i.IndexReport_ == nil {\n\t\tpanic(\"mock indexer: unexpected call to IndexReport\")\n\t}\n\treturn i.IndexReport_(ctx, digest)\n}\n\nfunc (i *Mock) State(ctx context.Context) (string, error) {\n\tif i.State_ == nil {\n\t\tpanic(\"mock indexer: unexpected call to State\")\n\t}\n\treturn i.State_(ctx)\n}\n\nfunc (i *Mock) AffectedManifests(ctx context.Context, vulns []claircore.Vulnerability) (*claircore.AffectedManifests, error) {\n\tif i.AffectedManifests_ == nil {\n\t\tpanic(\"mock indexer: unexpected call to AffectedManifests\")\n\t}\n\treturn i.AffectedManifests_(ctx, vulns)\n}\n\nfunc (i *Mock) DeleteManifests(ctx context.Context, d ...claircore.Digest) ([]claircore.Digest, error) {\n\tif i.DeleteManifests_ == nil {\n\t\tpanic(\"mock indexer: unexpected call to DeleteManifests\")\n\t}\n\treturn i.DeleteManifests_(ctx, d...)\n}\n"
  },
  {
    "path": "indexer/service.go",
    "content": "package indexer\n\nimport (\n\t\"context\"\n\n\t\"github.com/quay/claircore\"\n)\n\n// Service is an aggregate interface wrapping claircore.Libindex functionality.\n//\n// Implementation may use a local instance of claircore.Libindex or a remote\n// instance via http or grpc client.\ntype Service interface {\n\tIndexer\n\tReporter\n\tStater\n\tAffected\n}\n\n// StateReporter is an aggregate interface providing both a Reporter and\n// a Stater method set\ntype StateReporter interface {\n\tReporter\n\tStater\n}\n\n// StateIndexer is an aggregate interface providing both a Indexer\n// and a Stater method set\ntype StateIndexer interface {\n\tIndexer\n\tStater\n}\n\n// Indexer is an interface for computing a IndexReport given a Manifest.\ntype Indexer interface {\n\tIndex(ctx context.Context, manifest *claircore.Manifest) (*claircore.IndexReport, error)\n\tDeleteManifests(context.Context, ...claircore.Digest) ([]claircore.Digest, error)\n}\n\n// Reporter is an interface for retreiving an IndexReport given a manifest digest.\ntype Reporter interface {\n\tIndexReport(ctx context.Context, digest claircore.Digest) (*claircore.IndexReport, bool, error)\n}\n\n// Stater is an interface which provides a unique token symbolizing a Clair's state.\ntype Stater interface {\n\tState(ctx context.Context) (string, error)\n}\n\n// Affected is an interface for reporting the manifests affected by a set of vulnerabilities.\ntype Affected interface {\n\tAffectedManifests(ctx context.Context, vulns []claircore.Vulnerability) (*claircore.AffectedManifests, error)\n}\n"
  },
  {
    "path": "initialize/auto/auto.go",
    "content": "// Package auto does automatic detection and runtime configuration for certain\n// environments.\n//\n// All top-level functions are not safe to call concurrently.\npackage auto\n\nimport (\n\t\"context\"\n\t\"log/slog\"\n)\n\nvar msgs = []func(context.Context){}\n\nfunc init() {\n\tCPU()\n\tMemory()\n\tProfiling()\n}\n\n// PrintLogs uses slog to report any messages queued up from the runs of\n// functions since the last call to PrintLogs.\nfunc PrintLogs(ctx context.Context) {\n\tfor _, f := range msgs {\n\t\tf(ctx)\n\t}\n\tmsgs = msgs[:0]\n}\n\n// DebugLog is a helper to log static strings.\nfunc debugLog(m string) {\n\tmsgs = append(msgs, func(ctx context.Context) {\n\t\tslog.DebugContext(ctx, m)\n\t})\n}\n\n// InfoLog is a helper to log static strings.\nfunc infoLog(m string) {\n\tmsgs = append(msgs, func(ctx context.Context) {\n\t\tslog.InfoContext(ctx, m)\n\t})\n}\n"
  },
  {
    "path": "initialize/auto/auto_test.go",
    "content": "package auto\n\nimport (\n\t\"os\"\n\t\"testing\"\n)\n\nfunc TestMain(m *testing.M) {\n\t// Reset the logging slice, as the init function will have triggered and\n\t// written things into it.\n\tmsgs = msgs[:0]\n\tos.Exit(m.Run())\n}\n"
  },
  {
    "path": "initialize/auto/cpu.go",
    "content": "//go:build !linux\n// +build !linux\n\npackage auto\n\n// CPU is a no-op on this platform.\nfunc CPU() {}\n"
  },
  {
    "path": "initialize/auto/cpu_linux.go",
    "content": "package auto\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"io/fs\"\n\t\"log/slog\"\n\t\"os\"\n\t\"path\"\n\t\"runtime\"\n\t\"strconv\"\n)\n\n// CPU guesses a good number for GOMAXPROCS based on information gleaned from\n// the current process's cgroup.\nfunc CPU() {\n\tif os.Getenv(\"GOMAXPROCS\") != \"\" {\n\t\tinfoLog(\"GOMAXPROCS set in the environment, skipping auto detection\")\n\t\treturn\n\t}\n\troot := os.DirFS(\"/\")\n\tgmp, err := cgLookup(root)\n\tif err != nil {\n\t\tmsgs = append(msgs, func(ctx context.Context) {\n\t\t\tslog.ErrorContext(ctx, \"unable to guess GOMAXPROCS value\",\n\t\t\t\t\"reason\", err)\n\t\t})\n\t\treturn\n\t}\n\tprev := runtime.GOMAXPROCS(gmp)\n\tmsgs = append(msgs, func(ctx context.Context) {\n\t\tslog.InfoContext(ctx, \"set GOMAXPROCS value\",\n\t\t\t\"cur\", gmp,\n\t\t\t\"prev\", prev)\n\t})\n}\n\nfunc cgLookup(r fs.FS) (int, error) {\n\tconst usingDefault = \"no CPU quota set, using default\"\n\tvar gmp int\n\tb, err := fs.ReadFile(r, \"proc/self/cgroup\")\n\tswitch {\n\tcase err == nil:\n\tcase errors.Is(err, fs.ErrNotExist):\n\t\tdebugLog(\"cgroups seemingly not enabled\")\n\t\tinfoLog(usingDefault)\n\t\treturn gmp, nil\n\tdefault:\n\t\treturn gmp, err\n\t}\n\tvar q, p uint64 = 0, 1\n\ts := bufio.NewScanner(bytes.NewReader(b))\n\ts.Split(bufio.ScanLines)\n\tfor s.Scan() {\n\t\tsl := bytes.SplitN(s.Bytes(), []byte(\":\"), 3)\n\t\thid, ctls, pb := sl[0], sl[1], sl[2]\n\t\tif bytes.Equal(hid, []byte(\"0\")) && len(ctls) == 0 { // If cgroupsv2:\n\t\t\tdebugLog(\"found cgroups v2\")\n\t\t\tn := path.Join(\"sys/fs/cgroup\", string(pb), \"cpu.max\")\n\t\t\tb, err := fs.ReadFile(r, n)\n\t\t\tswitch {\n\t\t\tcase err == nil:\n\t\t\tcase errors.Is(err, fs.ErrNotExist):\n\t\t\t\tinfoLog(usingDefault)\n\t\t\t\treturn gmp, nil\n\t\t\tdefault:\n\t\t\t\treturn gmp, err\n\t\t\t}\n\t\t\tl := bytes.Fields(b)\n\t\t\tqt, per := string(l[0]), string(l[1])\n\t\t\tif qt == \"max\" {\n\t\t\t\t// No quota, so bail.\n\t\t\t\tinfoLog(usingDefault)\n\t\t\t\treturn gmp, nil\n\t\t\t}\n\t\t\tq, err = strconv.ParseUint(qt, 10, 64)\n\t\t\tif err != nil {\n\t\t\t\treturn gmp, err\n\t\t\t}\n\t\t\tp, err = strconv.ParseUint(per, 10, 64)\n\t\t\tif err != nil {\n\t\t\t\treturn gmp, err\n\t\t\t}\n\t\t\tbreak\n\t\t}\n\t\t// If here, we're doing cgroups v1.\n\t\tisCPU := false\n\t\tfor _, b := range bytes.Split(ctls, []byte(\",\")) {\n\t\t\tif bytes.Equal(b, []byte(\"cpu\")) {\n\t\t\t\tisCPU = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif !isCPU {\n\t\t\t// This line is not the cpu group.\n\t\t\tcontinue\n\t\t}\n\t\tdebugLog(\"found cgroups v1 and cpu controller\")\n\t\tprefix := path.Join(\"sys/fs/cgroup\", string(ctls), string(pb))\n\t\t// Check for the existence of the named cgroup. If it doesn't exist,\n\t\t// look at the root of the controller. The named group not existing\n\t\t// probably means the process is in a container and is having remounting\n\t\t// tricks done. If, for some reason this is actually the root cgroup,\n\t\t// it'll be unlimited and fall back to the default.\n\t\tif _, err := fs.Stat(r, prefix); errors.Is(err, fs.ErrNotExist) {\n\t\t\tdebugLog(\"falling back to root hierarchy\")\n\t\t\tprefix = path.Join(\"sys/fs/cgroup\", string(ctls))\n\t\t}\n\n\t\tb, err = fs.ReadFile(r, path.Join(prefix, \"cpu.cfs_quota_us\"))\n\t\tif err != nil {\n\t\t\treturn gmp, err\n\t\t}\n\t\tqi, err := strconv.ParseInt(string(bytes.TrimSpace(b)), 10, 64)\n\t\tif err != nil {\n\t\t\treturn gmp, err\n\t\t}\n\t\tif qi == -1 {\n\t\t\t// No quota, so bail.\n\t\t\tinfoLog(usingDefault)\n\t\t\treturn gmp, nil\n\t\t}\n\t\tq = uint64(qi)\n\t\tb, err = fs.ReadFile(r, path.Join(prefix, \"cpu.cfs_period_us\"))\n\t\tif err != nil {\n\t\t\treturn gmp, err\n\t\t}\n\t\tp, err = strconv.ParseUint(string(bytes.TrimSpace(b)), 10, 64)\n\t\tif err != nil {\n\t\t\treturn gmp, err\n\t\t}\n\t\tbreak\n\t}\n\tif err := s.Err(); err != nil {\n\t\treturn gmp, err\n\t}\n\tgmp = int(q / p)\n\treturn gmp, nil\n}\n"
  },
  {
    "path": "initialize/auto/cpu_linux_test.go",
    "content": "//go:build linux\n// +build linux\n\npackage auto\n\nimport (\n\t\"context\"\n\t\"testing\"\n\t\"testing/fstest\"\n\n\t\"github.com/quay/claircore/test\"\n)\n\nvar (\n\tcgv1 = &fstest.MapFile{\n\t\tData: []byte(`11:pids:/user.slice/user-1000.slice/session-4.scope\n10:cpuset:/\n9:blkio:/user.slice\n8:hugetlb:/\n7:perf_event:/\n6:devices:/user.slice\n5:net_cls,net_prio:/\n4:cpu,cpuacct:/user.slice\n3:freezer:/\n2:memory:/user.slice/user-1000.slice/session-4.scope\n1:name=systemd:/user.slice/user-1000.slice/session-4.scope\n0::/user.slice/user-1000.slice/session-4.scope\n`),\n\t}\n\tcgv2 = &fstest.MapFile{\n\t\tData: []byte(\"0::/\\n\"),\n\t}\n)\n\ntype cgTestcase struct {\n\tIn   fstest.MapFS\n\tErr  error\n\tName string\n\tWant int\n}\n\nfunc (tc cgTestcase) Run(ctx context.Context, t *testing.T) {\n\tt.Run(tc.Name, func(t *testing.T) {\n\t\tctx := test.Logging(t)\n\t\tgmp, err := cgLookup(tc.In)\n\t\tif err != tc.Err {\n\t\t\tt.Error(err)\n\t\t}\n\t\tif got, want := gmp, tc.Want; tc.Err == nil && got != want {\n\t\t\tt.Errorf(\"got: %v, want: %v\", got, want)\n\t\t}\n\t\tPrintLogs(ctx)\n\t})\n}\n\nfunc TestCPUDetection(t *testing.T) {\n\tctx := test.Logging(t)\n\tt.Run(\"V1\", func(t *testing.T) {\n\t\ttt := []cgTestcase{\n\t\t\t{\n\t\t\t\tName: \"NoLimit\",\n\t\t\t\tIn: fstest.MapFS{\n\t\t\t\t\t\"proc/self/cgroup\": cgv1,\n\t\t\t\t\t\"sys/fs/cgroup/cpu,cpuacct/user.slice/cpu.cfs_quota_us\": &fstest.MapFile{\n\t\t\t\t\t\tData: []byte(\"-1\\n\"),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tWant: 0,\n\t\t\t},\n\t\t\t{\n\t\t\t\tName: \"Limit1\",\n\t\t\t\tIn: fstest.MapFS{\n\t\t\t\t\t\"proc/self/cgroup\": cgv1,\n\t\t\t\t\t\"sys/fs/cgroup/cpu,cpuacct/user.slice/cpu.cfs_quota_us\": &fstest.MapFile{\n\t\t\t\t\t\tData: []byte(\"100000\\n\"),\n\t\t\t\t\t},\n\t\t\t\t\t\"sys/fs/cgroup/cpu,cpuacct/user.slice/cpu.cfs_period_us\": &fstest.MapFile{\n\t\t\t\t\t\tData: []byte(\"100000\\n\"),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tWant: 1,\n\t\t\t},\n\t\t\t{\n\t\t\t\tName: \"RootFallback\",\n\t\t\t\tIn: fstest.MapFS{\n\t\t\t\t\t\"proc/self/cgroup\": cgv1,\n\t\t\t\t\t\"sys/fs/cgroup/cpu,cpuacct/cpu.cfs_quota_us\": &fstest.MapFile{\n\t\t\t\t\t\tData: []byte(\"100000\\n\"),\n\t\t\t\t\t},\n\t\t\t\t\t\"sys/fs/cgroup/cpu,cpuacct/cpu.cfs_period_us\": &fstest.MapFile{\n\t\t\t\t\t\tData: []byte(\"100000\\n\"),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tWant: 1,\n\t\t\t},\n\t\t}\n\t\tctx := test.Logging(t, ctx)\n\t\tfor _, tc := range tt {\n\t\t\ttc.Run(ctx, t)\n\t\t}\n\t})\n\tt.Run(\"V2\", func(t *testing.T) {\n\t\ttt := []cgTestcase{\n\t\t\t{\n\t\t\t\tName: \"NoLimit\",\n\t\t\t\tIn: fstest.MapFS{\n\t\t\t\t\t\"proc/self/cgroup\": cgv2,\n\t\t\t\t\t\"sys/fs/cgroup/cpu.max\": &fstest.MapFile{\n\t\t\t\t\t\tData: []byte(\"max 100000\\n\"),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tWant: 0,\n\t\t\t},\n\t\t\t{\n\t\t\t\tName: \"Limit4\",\n\t\t\t\tIn: fstest.MapFS{\n\t\t\t\t\t\"proc/self/cgroup\": cgv2,\n\t\t\t\t\t\"sys/fs/cgroup/cpu.max\": &fstest.MapFile{\n\t\t\t\t\t\tData: []byte(\"400000 100000\\n\"),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tWant: 4,\n\t\t\t},\n\t\t}\n\t\tctx := test.Logging(t, ctx)\n\t\tfor _, tc := range tt {\n\t\t\ttc.Run(ctx, t)\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "initialize/auto/memory.go",
    "content": "//go:build !linux || (linux && !go1.19)\n\npackage auto\n\n// Memory is a no-op on this platform.\nfunc Memory() {}\n"
  },
  {
    "path": "initialize/auto/memory_linux.go",
    "content": "package auto\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"io/fs\"\n\t\"log/slog\"\n\t\"os\"\n\t\"path\"\n\t\"runtime/debug\"\n\t\"strconv\"\n)\n\n// Memory sets the runtime's memory limit based on information gleaned from the\n// current process's cgroup. See [debug.SetMemoryLimit] for details on the effects\n// of setting the limit. This does mean that attempting to run Clair in an aggressively\n// constrained environment may cause excessive CPU time spent in garbage\n// collection. Excessive GC can be prevented by increasing the resources allowed or\n// pacing Clair as a whole by reducing the CPU allocation or limiting the number of\n// concurrent requests.\n//\n// The process' \"memory.max\" limit (for cgroups v2) or\n// \"memory.limit_in_bytes\" (for cgroups v1) are the values consulted.\nfunc Memory() {\n\troot := os.DirFS(\"/\")\n\tlim, err := memLookup(root)\n\tswitch {\n\tcase err != nil:\n\t\tmsgs = append(msgs, func(ctx context.Context) {\n\t\t\tslog.ErrorContext(ctx, \"unable to guess memory limit\",\n\t\t\t\t\"reason\", err)\n\t\t})\n\t\treturn\n\tcase lim == doNothing:\n\t\tinfoLog(\"no memory limit configured\")\n\t\treturn\n\tcase lim == setMax:\n\t\tinfoLog(\"memory limit unset\")\n\t\treturn\n\t}\n\t// Following the GC guide and taking a haircut: https://tip.golang.org/doc/gc-guide#Suggested_uses\n\ttgt := lim - (lim / 20)\n\tdebug.SetMemoryLimit(tgt)\n\tmsgs = append(msgs, func(ctx context.Context) {\n\t\tslog.InfoContext(ctx, \"set memory limit\",\n\t\t\t\"lim\", lim,\n\t\t\t\"target\", tgt)\n\t})\n}\n\nconst (\n\tdoNothing = -1\n\tsetMax    = -2\n)\n\nfunc memLookup(r fs.FS) (int64, error) {\n\tb, err := fs.ReadFile(r, \"proc/self/cgroup\")\n\tswitch {\n\tcase err == nil:\n\tcase errors.Is(err, fs.ErrNotExist):\n\t\tdebugLog(\"cgroups seemingly not enabled\")\n\t\treturn doNothing, nil\n\tdefault:\n\t\treturn 0, err\n\t}\n\ts := bufio.NewScanner(bytes.NewReader(b))\n\ts.Split(bufio.ScanLines)\n\tfor s.Scan() {\n\t\tsl := bytes.SplitN(s.Bytes(), []byte(\":\"), 3)\n\t\thid, ctls, pb := sl[0], sl[1], sl[2]\n\t\tif bytes.Equal(hid, []byte(\"0\")) && len(ctls) == 0 { // If cgroupsv2:\n\t\t\tdebugLog(\"found cgroups v2\")\n\t\t\tn := path.Join(\"sys/fs/cgroup\", string(pb), \"memory.max\")\n\t\t\tb, err := fs.ReadFile(r, n)\n\t\t\tswitch {\n\t\t\tcase errors.Is(err, nil):\n\t\t\tcase errors.Is(err, fs.ErrNotExist):\n\t\t\t\tdebugLog(`no \"memory.max\" file`)\n\t\t\t\treturn doNothing, nil\n\t\t\tdefault:\n\t\t\t\treturn 0, err\n\t\t\t}\n\t\t\tv := string(bytes.TrimSpace(b))\n\t\t\tif v == \"max\" { // No quota, so bail.\n\t\t\t\treturn setMax, nil\n\t\t\t}\n\t\t\treturn strconv.ParseInt(v, 10, 64)\n\t\t}\n\t\t// If here, we're doing cgroups v1.\n\t\tisMem := false\n\t\tfor _, b := range bytes.Split(ctls, []byte(\",\")) {\n\t\t\tif bytes.Equal(b, []byte(\"memory\")) {\n\t\t\t\tisMem = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif !isMem { // This line is not the memory group.\n\t\t\tcontinue\n\t\t}\n\t\tdebugLog(\"found cgroups v1 and memory controller\")\n\t\tprefix := path.Join(\"sys/fs/cgroup\", string(ctls), string(pb))\n\t\t// Check for the existence of the named cgroup. If it doesn't exist,\n\t\t// look at the root of the controller. The named group not existing\n\t\t// probably means the process is in a container and is having remounting\n\t\t// tricks done. If, for some reason this is actually the root cgroup,\n\t\t// it'll be unlimited and fall back to the default.\n\t\tif _, err := fs.Stat(r, prefix); errors.Is(err, fs.ErrNotExist) {\n\t\t\tdebugLog(\"falling back to root hierarchy\")\n\t\t\tprefix = path.Join(\"sys/fs/cgroup\", string(ctls))\n\t\t}\n\n\t\tb, err = fs.ReadFile(r, path.Join(prefix, \"memory.limit_in_bytes\"))\n\t\tswitch {\n\t\tcase errors.Is(err, nil):\n\t\tcase errors.Is(err, fs.ErrNotExist):\n\t\t\tdebugLog(`no \"memory.limit_in_bytes\" file`)\n\t\t\treturn doNothing, nil\n\t\tdefault:\n\t\t\treturn 0, err\n\t\t}\n\t\tv := string(bytes.TrimSpace(b))\n\t\treturn strconv.ParseInt(v, 10, 64)\n\t}\n\tif err := s.Err(); err != nil {\n\t\treturn 0, err\n\t}\n\treturn 0, nil\n}\n"
  },
  {
    "path": "initialize/auto/memory_linux_test.go",
    "content": "//go:build linux && go1.19\n\npackage auto\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"testing\"\n\t\"testing/fstest\"\n\n\t\"github.com/quay/claircore/test\"\n)\n\ntype memTestcase struct {\n\tIn   fstest.MapFS\n\tErr  error\n\tName string\n\tWant int64\n}\n\nfunc (tc memTestcase) Run(ctx context.Context, t *testing.T) {\n\tt.Helper()\n\tt.Run(tc.Name, func(t *testing.T) {\n\t\tt.Helper()\n\t\tctx := test.Logging(t)\n\t\tlim, err := memLookup(tc.In)\n\t\tif err != tc.Err {\n\t\t\tt.Error(err)\n\t\t}\n\t\tif got, want := lim, tc.Want; tc.Err == nil && got != want {\n\t\t\tt.Errorf(\"got: %v, want: %v\", got, want)\n\t\t}\n\t\tPrintLogs(ctx)\n\t})\n}\n\nfunc TestMemoryDetection(t *testing.T) {\n\tconst (\n\t\tlimInt   = 268435456\n\t\tnoLimInt = -1\n\t)\n\tvar (\n\t\tlim   = &fstest.MapFile{Data: []byte(fmt.Sprintln(limInt))}\n\t\tnoLim = &fstest.MapFile{Data: []byte(fmt.Sprintln(noLimInt))}\n\t)\n\tctx := test.Logging(t)\n\tt.Run(\"V1\", func(t *testing.T) {\n\t\ttt := []memTestcase{\n\t\t\t{\n\t\t\t\tName: \"NoLimit\",\n\t\t\t\tIn: fstest.MapFS{\n\t\t\t\t\t\"proc/self/cgroup\": cgv1,\n\t\t\t\t\t\"sys/fs/cgroup/memory/user.slice/user-1000.slice/session-4.scope/memory.limit_in_bytes\": noLim,\n\t\t\t\t},\n\t\t\t\tWant: noLimInt,\n\t\t\t},\n\t\t\t{\n\t\t\t\tName: \"RootFallback\",\n\t\t\t\tIn: fstest.MapFS{\n\t\t\t\t\t\"proc/self/cgroup\":                           cgv1,\n\t\t\t\t\t\"sys/fs/cgroup/memory/memory.limit_in_bytes\": noLim,\n\t\t\t\t},\n\t\t\t\tWant: noLimInt,\n\t\t\t},\n\t\t\t{\n\t\t\t\tName: \"256MiB\",\n\t\t\t\tIn: fstest.MapFS{\n\t\t\t\t\t\"proc/self/cgroup\": cgv1,\n\t\t\t\t\t\"sys/fs/cgroup/memory/user.slice/user-1000.slice/session-4.scope/memory.limit_in_bytes\": lim,\n\t\t\t\t},\n\t\t\t\tWant: limInt,\n\t\t\t},\n\t\t}\n\t\tctx := test.Logging(t, ctx)\n\t\tfor _, tc := range tt {\n\t\t\ttc.Run(ctx, t)\n\t\t}\n\t})\n\tt.Run(\"V2\", func(t *testing.T) {\n\t\ttt := []memTestcase{\n\t\t\t{\n\t\t\t\tName: \"NoLimit\",\n\t\t\t\tIn:   fstest.MapFS{\"proc/self/cgroup\": cgv2},\n\t\t\t\tWant: noLimInt,\n\t\t\t},\n\t\t\t{\n\t\t\t\tName: \"LimitMax\",\n\t\t\t\tIn: fstest.MapFS{\n\t\t\t\t\t\"proc/self/cgroup\": cgv2,\n\t\t\t\t\t\"sys/fs/cgroup/memory.max\": &fstest.MapFile{\n\t\t\t\t\t\tData: []byte(\"max\\n\"),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tWant: setMax,\n\t\t\t},\n\t\t\t{\n\t\t\t\tName: \"256MiB\",\n\t\t\t\tIn: fstest.MapFS{\n\t\t\t\t\t\"proc/self/cgroup\":         cgv2,\n\t\t\t\t\t\"sys/fs/cgroup/memory.max\": lim,\n\t\t\t\t},\n\t\t\t\tWant: limInt,\n\t\t\t},\n\t\t}\n\t\tctx := test.Logging(t, ctx)\n\t\tfor _, tc := range tt {\n\t\t\ttc.Run(ctx, t)\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "initialize/auto/profiling.go",
    "content": "package auto\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"os\"\n\t\"runtime\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n)\n\n// Profiling enables block and mutex profiling.\n//\n// This function uses the magic environment variable \"CLAIRDEBUG\" to control the\n// values used. This escape hatch will go away in the future.\nfunc Profiling() {\n\t// Catch contention at a granularity of 1 microsecond.\n\tblockCur := 1000\n\t// Catch 1/10 mutex contention events.\n\tmutexCur := 10\n\tfromEnv := false\n\tif s, ok := os.LookupEnv(`CLAIRDEBUG`); ok {\n\t\tvar err error\n\t\tfor _, kv := range strings.Split(s, \",\") {\n\t\t\tk, v, ok := strings.Cut(kv, \"=\")\n\t\t\tif !ok {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tswitch k {\n\t\t\tcase \"blockprofile\":\n\t\t\t\tfromEnv = true\n\t\t\t\tblockCur, err = strconv.Atoi(v)\n\t\t\tcase \"mutexprofile\":\n\t\t\t\tfromEnv = true\n\t\t\t\tmutexCur, err = strconv.Atoi(v)\n\t\t\tdefault:\n\t\t\t}\n\t\t\tif err != nil {\n\t\t\t\tpanic(err)\n\t\t\t}\n\t\t}\n\t}\n\n\truntime.SetBlockProfileRate(blockCur)\n\tmutexPrev := runtime.SetMutexProfileFraction(mutexCur)\n\tmsgs = append(msgs, func(ctx context.Context) {\n\t\tslog.InfoContext(ctx, \"profiling rates configured\",\n\t\t\t\"from_env\", fromEnv,\n\t\t\t\"block_rate\", time.Duration(blockCur)*time.Nanosecond,\n\t\t\t\"prev_mutex_frac\", fmt.Sprintf(\"1/%d\", mutexPrev),\n\t\t\t\"cur_mutex_frac\", fmt.Sprintf(\"1/%d\", mutexCur))\n\t})\n}\n"
  },
  {
    "path": "initialize/logging.go",
    "content": "package initialize\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\n\t\"github.com/quay/clair/config\"\n\n\t\"github.com/quay/clair/v4/internal/logging\"\n)\n\n// Logging configures slog according to the provided configuration.\nfunc Logging(ctx context.Context, cfg *config.Config) error {\n\tswitch cfg.LogLevel {\n\tcase config.DebugColorLog:\n\t\topts := logging.DefaultOptions()\n\t\topts.ProseFormat = true\n\t\tlogging.SetLogger(opts)\n\t\tfallthrough\n\tcase config.DebugLog:\n\t\tlogging.Level.Set(slog.LevelDebug)\n\tcase config.InfoLog:\n\t\tlogging.Level.Set(slog.LevelInfo)\n\tcase config.WarnLog:\n\t\tlogging.Level.Set(slog.LevelWarn)\n\tcase config.ErrorLog:\n\t\tlogging.Level.Set(slog.LevelError)\n\tcase config.FatalLog:\n\t\tlogging.Level.Set(slog.LevelError + 4)\n\tcase config.PanicLog:\n\t\tlogging.Level.Set(slog.LevelError + 8)\n\tdefault:\n\t\treturn fmt.Errorf(\"unknown log level: %v\", cfg.LogLevel)\n\t}\n\n\tslog.DebugContext(ctx, \"logging configured\")\n\treturn nil\n}\n"
  },
  {
    "path": "initialize/services.go",
    "content": "package initialize\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"net/http\"\n\t\"net/http/cookiejar\"\n\t\"time\"\n\n\t\"github.com/go-jose/go-jose/v3/jwt\"\n\t\"github.com/jackc/pgx/v5/pgxpool\"\n\t\"github.com/quay/clair/config\"\n\t\"github.com/quay/claircore/datastore/postgres\"\n\t\"github.com/quay/claircore/enricher/cvss\"\n\t\"github.com/quay/claircore/libindex\"\n\t\"github.com/quay/claircore/libvuln\"\n\t\"github.com/quay/claircore/libvuln/driver\"\n\t\"github.com/quay/claircore/pkg/ctxlock/v2\"\n\t\"golang.org/x/net/publicsuffix\"\n\n\tclairerror \"github.com/quay/clair/v4/clair-error\"\n\t\"github.com/quay/clair/v4/httptransport\"\n\t\"github.com/quay/clair/v4/httptransport/client\"\n\t\"github.com/quay/clair/v4/indexer\"\n\t\"github.com/quay/clair/v4/internal/httputil\"\n\t\"github.com/quay/clair/v4/matcher\"\n\t\"github.com/quay/clair/v4/notifier\"\n\tnotifierpg \"github.com/quay/clair/v4/notifier/postgres\"\n\t\"github.com/quay/clair/v4/notifier/service\"\n)\n\nconst (\n\t// NotifierIssuer is the value used for the issuer claim of any outgoing\n\t// HTTP requests the notifier makes, if PSK auth is configured.\n\tNotifierIssuer = `clair-notifier`\n)\n\nvar (\n\tintraserviceClaim = jwt.Claims{Issuer: httptransport.IntraserviceIssuer}\n\tnotifierClaim     = jwt.Claims{Issuer: NotifierIssuer}\n)\n\n// Srv is a bundle of configured Services.\n//\n// The members are populated according to the configuration that was passed to\n// Services.\ntype Srv struct {\n\tIndexer  indexer.Service\n\tMatcher  matcher.Service\n\tNotifier notifier.Service\n}\n\n// Services configures the services needed for a given mode according to the\n// provided configuration.\nfunc Services(ctx context.Context, cfg *config.Config) (*Srv, error) {\n\tslog.InfoContext(ctx, \"begin service initialization\")\n\tdefer slog.InfoContext(ctx, \"end service initialization\")\n\n\tvar srv Srv\n\tvar err error\n\tswitch cfg.Mode {\n\tcase config.ComboMode:\n\t\tsrv.Indexer, err = localIndexer(ctx, cfg)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tsrv.Matcher, err = localMatcher(ctx, cfg)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tsrv.Notifier, err = localNotifier(ctx, cfg, srv.Indexer, srv.Matcher)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\tcase config.IndexerMode:\n\t\tsrv.Indexer, err = localIndexer(ctx, cfg)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\tcase config.MatcherMode:\n\t\tsrv.Matcher, err = localMatcher(ctx, cfg)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tsrv.Indexer, err = remoteIndexer(ctx, cfg, cfg.Matcher.IndexerAddr)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\tcase config.NotifierMode:\n\t\tsrv.Indexer, err = remoteIndexer(ctx, cfg, cfg.Notifier.IndexerAddr)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tsrv.Matcher, err = remoteMatcher(ctx, cfg, cfg.Notifier.MatcherAddr)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tsrv.Notifier, err = localNotifier(ctx, cfg, srv.Indexer, srv.Matcher)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"could not determine passed in mode: %v\", cfg.Mode)\n\t}\n\n\treturn &srv, nil\n}\n\n// BUG(hank) The various resources (database connections, lock services)\n// constructed in some internal functions are not properly cleaned up.\n\nfunc localIndexer(ctx context.Context, cfg *config.Config) (indexer.Service, error) {\n\tconst msg = \"failed to initialize indexer: \"\n\tmkErr := func(err error) *clairerror.ErrNotInitialized {\n\t\treturn &clairerror.ErrNotInitialized{Msg: msg + err.Error()}\n\t}\n\n\tpool, err := postgres.Connect(ctx, cfg.Indexer.ConnString, \"libindex\")\n\tif err != nil {\n\t\treturn nil, mkErr(err)\n\t}\n\tstore, err := postgres.InitPostgresIndexerStore(ctx, pool, cfg.Indexer.Migrations)\n\tif err != nil {\n\t\treturn nil, mkErr(err)\n\t}\n\tlocker, err := ctxlock.New(ctx, pool)\n\tif err != nil {\n\t\treturn nil, mkErr(err)\n\t}\n\n\topts := libindex.Options{\n\t\tStore:                store,\n\t\tLocker:               locker,\n\t\tScanLockRetry:        time.Duration(cfg.Indexer.ScanLockRetry) * time.Second,\n\t\tLayerScanConcurrency: cfg.Indexer.LayerScanConcurrency,\n\t}\n\tif cfg.Indexer.Scanner.Package != nil {\n\t\topts.ScannerConfig.Package = make(map[string]func(interface{}) error, len(cfg.Indexer.Scanner.Package))\n\t\tfor name, node := range cfg.Indexer.Scanner.Package {\n\t\t\tnode := node\n\t\t\topts.ScannerConfig.Package[name] = func(v interface{}) error {\n\t\t\t\tb, err := json.Marshal(node)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\treturn json.Unmarshal(b, v)\n\t\t\t}\n\t\t}\n\t}\n\tif cfg.Indexer.Scanner.Dist != nil {\n\t\topts.ScannerConfig.Dist = make(map[string]func(interface{}) error, len(cfg.Indexer.Scanner.Dist))\n\t\tfor name, node := range cfg.Indexer.Scanner.Dist {\n\t\t\tnode := node\n\t\t\topts.ScannerConfig.Dist[name] = func(v interface{}) error {\n\t\t\t\tb, err := json.Marshal(node)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\treturn json.Unmarshal(b, v)\n\t\t\t}\n\t\t}\n\t}\n\tif cfg.Indexer.Scanner.Repo != nil {\n\t\topts.ScannerConfig.Repo = make(map[string]func(interface{}) error, len(cfg.Indexer.Scanner.Repo))\n\t\tfor name, node := range cfg.Indexer.Scanner.Repo {\n\t\t\tnode := node\n\t\t\topts.ScannerConfig.Repo[name] = func(v interface{}) error {\n\t\t\t\tb, err := json.Marshal(node)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\treturn json.Unmarshal(b, v)\n\t\t\t}\n\t\t}\n\t}\n\tc, err := httputil.NewClient(ctx, cfg.Indexer.Airgap)\n\tif err != nil {\n\t\treturn nil, mkErr(err)\n\t}\n\n\topts.FetchArena = libindex.NewRemoteFetchArena(c, \"\")\n\n\ts, err := libindex.New(ctx, &opts, c)\n\tif err != nil {\n\t\treturn nil, mkErr(err)\n\t}\n\treturn s, nil\n}\n\nfunc remoteIndexer(ctx context.Context, cfg *config.Config, addr string) (indexer.Service, error) {\n\tconst msg = \"failed to initialize indexer client: \"\n\tmkErr := func(err error) *clairerror.ErrNotInitialized {\n\t\treturn &clairerror.ErrNotInitialized{Msg: msg + err.Error()}\n\t}\n\trc, err := remoteClient(ctx, cfg, intraserviceClaim, addr)\n\tif err != nil {\n\t\treturn nil, mkErr(err)\n\t}\n\treturn rc, nil\n}\n\nfunc remoteClient(ctx context.Context, cfg *config.Config, claim jwt.Claims, addr string) (*client.HTTP, error) {\n\tc, err := httputil.NewClient(ctx, false) // ???\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\topts := []client.Option{client.WithAddr(addr), client.WithClient(c)}\n\tif cfg.Auth.Any() {\n\t\ts, err := httputil.NewSigner(ctx, cfg, claim)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\topts = append(opts, client.WithSigner(s))\n\t}\n\treturn client.NewHTTP(ctx, opts...)\n}\n\nfunc localMatcher(ctx context.Context, cfg *config.Config) (matcher.Service, error) {\n\tconst msg = \"failed to initialize matcher: \"\n\tmkErr := func(err error) *clairerror.ErrNotInitialized {\n\t\treturn &clairerror.ErrNotInitialized{\n\t\t\tMsg: msg + err.Error(),\n\t\t}\n\t}\n\n\ttr := http.DefaultTransport.(*http.Transport).Clone()\n\t// Some servers return weak validators when the Content-Encoding is not\n\t// \"identity\". Setting this prevents automatically negotiating up to \"gzip\".\n\ttr.DisableCompression = true\n\tjar, err := cookiejar.New(&cookiejar.Options{\n\t\tPublicSuffixList: publicsuffix.List,\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tcl := &http.Client{\n\t\tJar:       jar,\n\t\tTransport: httputil.RateLimiter(tr),\n\t}\n\tupdaterConfigs := make(map[string]driver.ConfigUnmarshaler)\n\tfor name, node := range cfg.Updaters.Config {\n\t\tnode := node\n\t\tupdaterConfigs[name] = func(v interface{}) error {\n\t\t\tb, err := json.Marshal(node)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\treturn json.Unmarshal(b, v)\n\t\t}\n\t}\n\tmatcherConfigs := make(map[string]driver.MatcherConfigUnmarshaler)\n\tfor name, node := range cfg.Matchers.Config {\n\t\tnode := node\n\t\tmatcherConfigs[name] = func(v interface{}) error {\n\t\t\tb, err := json.Marshal(node)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\treturn json.Unmarshal(b, v)\n\t\t}\n\t}\n\tpool, err := postgres.Connect(ctx, cfg.Matcher.ConnString, \"libvuln\")\n\tif err != nil {\n\t\treturn nil, mkErr(err)\n\t}\n\tstore, err := postgres.InitPostgresMatcherStore(ctx, pool, cfg.Matcher.Migrations)\n\tif err != nil {\n\t\treturn nil, mkErr(err)\n\t}\n\tlocker, err := ctxlock.New(ctx, pool)\n\tif err != nil {\n\t\treturn nil, mkErr(err)\n\t}\n\n\ters := []driver.Enricher{}\n\tif !cfg.Matcher.DisableEnrichment {\n\t\tslog.InfoContext(ctx, \"enrichment enabled\")\n\t\ters = append(ers, &cvss.Enricher{})\n\t}\n\n\ts, err := libvuln.New(ctx, &libvuln.Options{\n\t\tStore:           store,\n\t\tLocker:          locker,\n\t\tUpdaterSets:     cfg.Updaters.Sets,\n\t\tUpdateInterval:  time.Duration(cfg.Matcher.Period),\n\t\tUpdaterConfigs:  updaterConfigs,\n\t\tUpdateRetention: cfg.Matcher.UpdateRetention,\n\t\tMatcherNames:    cfg.Matchers.Names,\n\t\tMatcherConfigs:  matcherConfigs,\n\t\tClient:          cl,\n\t\tEnrichers:       ers,\n\t})\n\tif err != nil {\n\t\treturn nil, mkErr(err)\n\t}\n\treturn s, nil\n}\n\nfunc remoteMatcher(ctx context.Context, cfg *config.Config, addr string) (matcher.Service, error) {\n\tconst msg = \"failed to initialize matcher client: \"\n\tmkErr := func(err error) *clairerror.ErrNotInitialized {\n\t\treturn &clairerror.ErrNotInitialized{Msg: msg + err.Error()}\n\t}\n\trc, err := remoteClient(ctx, cfg, intraserviceClaim, addr)\n\tif err != nil {\n\t\treturn nil, mkErr(err)\n\t}\n\treturn rc, nil\n}\n\nfunc localNotifier(ctx context.Context, cfg *config.Config, i indexer.Service, m matcher.Service) (notifier.Service, error) {\n\tconst msg = \"failed to initialize notifier: \"\n\tmkErr := func(err error) *clairerror.ErrNotInitialized {\n\t\treturn &clairerror.ErrNotInitialized{\n\t\t\tMsg: msg + err.Error(),\n\t\t}\n\t}\n\n\tc, err := httputil.NewClient(ctx, false) // No airgap flag.\n\tif err != nil {\n\t\treturn nil, mkErr(err)\n\t}\n\tsigner, err := httputil.NewSigner(ctx, cfg, notifierClaim)\n\tif err != nil {\n\t\treturn nil, mkErr(err)\n\t}\n\n\tncfg := &cfg.Notifier\n\tpoolcfg, err := pgxpool.ParseConfig(ncfg.ConnString)\n\tif err != nil {\n\t\treturn nil, mkErr(err)\n\t}\n\tif cfg.Notifier.Migrations {\n\t\tif err := notifierpg.Init(ctx, poolcfg.ConnConfig); err != nil {\n\t\t\treturn nil, mkErr(err)\n\t\t}\n\t}\n\tpool, err := pgxpool.NewWithConfig(ctx, poolcfg)\n\tif err != nil {\n\t\treturn nil, mkErr(err)\n\t}\n\tstore := notifierpg.NewStore(pool)\n\tlocks, err := ctxlock.New(ctx, pool)\n\tif err != nil {\n\t\treturn nil, mkErr(err)\n\t}\n\n\ts, err := service.New(ctx, store, locks, service.Opts{\n\t\tDeliveryInterval: time.Duration(cfg.Notifier.DeliveryInterval),\n\t\tIndexer:          i,\n\t\tMatcher:          m,\n\t\tClient:           c,\n\t\tSigner:           signer,\n\t\tPollInterval:     time.Duration(cfg.Notifier.PollInterval),\n\t\tDisableSummary:   cfg.Notifier.DisableSummary,\n\t\tWebhook:          cfg.Notifier.Webhook,\n\t\tAMQP:             cfg.Notifier.AMQP,\n\t\tSTOMP:            cfg.Notifier.STOMP,\n\t})\n\tswitch {\n\tcase err == nil:\n\tcase errors.Is(err, service.ErrNoDelivery):\n\t\tslog.InfoContext(ctx, \"notifier disabled\", \"reason\", err)\n\t\treturn nil, nil\n\tdefault:\n\t\treturn nil, mkErr(err)\n\t}\n\tgo func() {\n\t\tif err := s.Run(ctx); err != context.Canceled {\n\t\t\tslog.ErrorContext(ctx, \"unexpected notifier error\", \"reason\", err)\n\t\t}\n\t}()\n\treturn s, nil\n}\n"
  },
  {
    "path": "internal/codec/codec.go",
    "content": "// Package codec is a unified place for configuring and allocating JSON encoders\n// and decoders.\npackage codec\n\nimport (\n\t\"io\"\n\t\"sync\"\n\n\t\"github.com/ugorji/go/codec\"\n)\n\nvar jsonHandle codec.JsonHandle\n\nfunc init() {\n\t// This is documented to cause \"smart buffering\".\n\tjsonHandle.WriterBufferSize = 4096\n\tjsonHandle.ReaderBufferSize = 4096\n\t// Force calling time.Time's Marshal function. This causes an allocation on\n\t// every time.Time value, but is the same behavior as the stdlib json\n\t// encoder. If we decide nulls are OK, this should get removed.\n\tjsonHandle.TimeNotBuiltin = true\n}\n\n// Encoder and decoder pools, to reuse if possible.\nvar (\n\tencPool = sync.Pool{\n\t\tNew: func() interface{} {\n\t\t\treturn codec.NewEncoder(nil, &jsonHandle)\n\t\t},\n\t}\n\tdecPool = sync.Pool{\n\t\tNew: func() interface{} {\n\t\t\treturn codec.NewDecoder(nil, &jsonHandle)\n\t\t},\n\t}\n)\n\n// Encoder encodes.\ntype Encoder = codec.Encoder\n\n// GetEncoder returns an encoder configured to write to w.\nfunc GetEncoder(w io.Writer) *Encoder {\n\te := encPool.Get().(*Encoder)\n\te.Reset(w)\n\treturn e\n}\n\n// PutEncoder returns an encoder to the pool.\nfunc PutEncoder(e *Encoder) {\n\te.Reset(nil)\n\tencPool.Put(e)\n}\n\n// Decoder decodes.\ntype Decoder = codec.Decoder\n\n// GetDecoder returns a decoder configured to read from r.\nfunc GetDecoder(r io.Reader) *Decoder {\n\td := decPool.Get().(*Decoder)\n\td.Reset(r)\n\treturn d\n}\n\n// PutDecoder returns a decoder to the pool.\nfunc PutDecoder(d *Decoder) {\n\td.Reset(nil)\n\tdecPool.Put(d)\n}\n"
  },
  {
    "path": "internal/codec/codec_test.go",
    "content": "package codec\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/google/go-cmp/cmp\"\n)\n\nfunc Example() {\n\tenc := GetEncoder(os.Stdout)\n\tdefer PutEncoder(enc)\n\tenc.MustEncode([]string{\"a\", \"slice\", \"of\", \"strings\"})\n\tfmt.Fprintln(os.Stdout)\n\tenc.MustEncode(nil)\n\tfmt.Fprintln(os.Stdout)\n\tenc.MustEncode(map[string]string{})\n\tfmt.Fprintln(os.Stdout)\n\t// Output: [\"a\",\"slice\",\"of\",\"strings\"]\n\t// null\n\t// {}\n}\n\nfunc BenchmarkDecode(b *testing.B) {\n\tb.ReportAllocs()\n\twant := map[string]string{\n\t\t\"a\": strings.Repeat(`A`, 2048),\n\t\t\"b\": strings.Repeat(`B`, 2048),\n\t\t\"c\": strings.Repeat(`C`, 2048),\n\t\t\"d\": strings.Repeat(`D`, 2048),\n\t}\n\tgot := make(map[string]string, len(want))\n\tb.ResetTimer()\n\n\tfor i := 0; i < b.N; i++ {\n\t\tdec := GetDecoder(JSONReader(want))\n\t\terr := dec.Decode(&got)\n\t\tPutDecoder(dec)\n\t\tif err != nil {\n\t\t\tb.Error(err)\n\t\t}\n\t\tif !cmp.Equal(got, want) {\n\t\t\tb.Error(cmp.Diff(got, want))\n\t\t}\n\t}\n}\n\nfunc BenchmarkDecodeStdlib(b *testing.B) {\n\tb.ReportAllocs()\n\twant := map[string]string{\n\t\t\"a\": strings.Repeat(`A`, 2048),\n\t\t\"b\": strings.Repeat(`B`, 2048),\n\t\t\"c\": strings.Repeat(`C`, 2048),\n\t\t\"d\": strings.Repeat(`D`, 2048),\n\t}\n\tgot := make(map[string]string, len(want))\n\tb.ResetTimer()\n\n\tfor i := 0; i < b.N; i++ {\n\t\tx, err := json.Marshal(want)\n\t\tif err != nil {\n\t\t\tb.Error(err)\n\t\t}\n\t\tif err := json.Unmarshal(x, &got); err != nil {\n\t\t\tb.Error(err)\n\t\t}\n\t\tif !cmp.Equal(got, want) {\n\t\t\tb.Error(cmp.Diff(got, want))\n\t\t}\n\t}\n}\n\nfunc TestTimeNotNull(t *testing.T) {\n\ttype s struct {\n\t\tTime time.Time\n\t}\n\tvar b bytes.Buffer\n\tenc := GetEncoder(&b)\n\tdefer PutEncoder(enc)\n\n\t// Example encoding of a populated time:\n\tif err := enc.Encode(s{Time: time.Unix(0, 0).UTC()}); err != nil {\n\t\tt.Error(err)\n\t}\n\tt.Log(b.String())\n\tb.Reset()\n\n\t// Now encode a zero time and make sure it's a string.\n\tif err := enc.Encode(s{}); err != nil {\n\t\tt.Error(err)\n\t}\n\tt.Log(b.String())\n\tif strings.Contains(b.String(), \"null\") {\n\t\tt.Error(\"wanted non-null encoding\")\n\t}\n}\n"
  },
  {
    "path": "internal/codec/reader.go",
    "content": "package codec\n\nimport \"io\"\n\n// JSONReader returns an io.ReadCloser backed by a pipe being fed by a JSON\n// encoder.\nfunc JSONReader(v interface{}) io.ReadCloser {\n\tr, w := io.Pipe()\n\t// This unsupervised goroutine should be fine, because the writer will error\n\t// once the reader is closed.\n\tgo func() {\n\t\tenc := GetEncoder(w)\n\t\tdefer PutEncoder(enc)\n\t\tdefer w.Close()\n\t\tif err := enc.Encode(v); err != nil {\n\t\t\tw.CloseWithError(err)\n\t\t}\n\t}()\n\treturn r\n}\n"
  },
  {
    "path": "internal/httputil/client.go",
    "content": "package httputil\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/http/cookiejar\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"syscall\"\n\n\t\"golang.org/x/net/publicsuffix\"\n\n\t\"github.com/quay/clair/v4/cmd\"\n)\n\n// NewClient constructs an [http.Client] that disallows access to public\n// networks, controlled by the localOnly flag.\n//\n// If disallowed, the reported error will be a [*net.AddrError] with the \"Err\"\n// value of \"disallowed by policy\".\nfunc NewClient(ctx context.Context, localOnly bool) (*http.Client, error) {\n\ttr := http.DefaultTransport.(*http.Transport).Clone()\n\tdialer := &net.Dialer{}\n\t// Set a control function if we're restricting subnets.\n\tif localOnly {\n\t\tdialer.Control = ctlLocalOnly\n\t}\n\ttr.DialContext = dialer.DialContext\n\n\tjar, err := cookiejar.New(&cookiejar.Options{\n\t\tPublicSuffixList: publicsuffix.List,\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &http.Client{\n\t\tTransport: tr,\n\t\tJar:       jar,\n\t}, nil\n}\n\nfunc ctlLocalOnly(network, address string, _ syscall.RawConn) error {\n\t// Future-proof for QUIC by allowing UDP here.\n\tif !strings.HasPrefix(network, \"tcp\") && !strings.HasPrefix(network, \"udp\") {\n\t\treturn &net.AddrError{\n\t\t\tAddr: network + \"!\" + address,\n\t\t\tErr:  \"disallowed by policy\",\n\t\t}\n\t}\n\thost, _, err := net.SplitHostPort(address)\n\tif err != nil {\n\t\treturn &net.AddrError{\n\t\t\tAddr: network + \"!\" + address,\n\t\t\tErr:  \"martian address\",\n\t\t}\n\t}\n\taddr := net.ParseIP(host)\n\tif addr == nil {\n\t\treturn &net.AddrError{\n\t\t\tAddr: network + \"!\" + address,\n\t\t\tErr:  \"martian address\",\n\t\t}\n\t}\n\tif !addr.IsPrivate() &&\n\t\t!addr.IsLoopback() &&\n\t\t!addr.IsLinkLocalUnicast() {\n\t\treturn &net.AddrError{\n\t\t\tAddr: network + \"!\" + address,\n\t\t\tErr:  \"disallowed by policy\",\n\t\t}\n\t}\n\treturn nil\n}\n\n// NewRequestWithContext is a wrapper around [http.NewRequestWithContext] that\n// sets some defaults in the returned request.\nfunc NewRequestWithContext(ctx context.Context, method, url string, body io.Reader) (*http.Request, error) {\n\t// The one OK use of the normal function.\n\treq, err := http.NewRequestWithContext(ctx, method, url, body)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tp, err := os.Executable()\n\tif err != nil {\n\t\tp = `clair?`\n\t} else {\n\t\tp = filepath.Base(p)\n\t}\n\treq.Header.Set(\"user-agent\", fmt.Sprintf(\"%s/%s\", p, cmd.Version))\n\treturn req, nil\n}\n"
  },
  {
    "path": "internal/httputil/client_test.go",
    "content": "package httputil\n\nimport (\n\t\"errors\"\n\t\"net\"\n\t\"testing\"\n)\n\nfunc TestLocalOnly(t *testing.T) {\n\ttt := []struct {\n\t\tNetwork string\n\t\tAddr    string\n\t\tErr     *net.AddrError\n\t}{\n\t\t{\n\t\t\tNetwork: \"tcp4\",\n\t\t\tAddr:    \"192.168.0.1:443\",\n\t\t\tErr:     nil,\n\t\t},\n\t\t{\n\t\t\tNetwork: \"tcp4\",\n\t\t\tAddr:    \"127.0.0.1:443\",\n\t\t\tErr:     nil,\n\t\t},\n\t\t{\n\t\t\tNetwork: \"tcp6\",\n\t\t\tAddr:    \"[fe80::]:443\",\n\t\t\tErr:     nil,\n\t\t},\n\t\t{\n\t\t\tNetwork: \"tcp4\",\n\t\t\tAddr:    \"8.8.8.8:443\",\n\t\t\tErr: &net.AddrError{\n\t\t\t\tAddr: \"tcp4!8.8.8.8:443\",\n\t\t\t\tErr:  \"disallowed by policy\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tNetwork: \"tcp6\",\n\t\t\tAddr:    \"[2000::]:443\",\n\t\t\tErr: &net.AddrError{\n\t\t\t\tAddr: \"tcp6![2000::]:443\",\n\t\t\t\tErr:  \"disallowed by policy\",\n\t\t\t},\n\t\t},\n\t}\n\tfor _, tc := range tt {\n\t\tt.Logf(\"%s!%s\", tc.Network, tc.Addr)\n\t\tvar nErr *net.AddrError\n\t\tgot := ctlLocalOnly(tc.Network, tc.Addr, nil)\n\t\tif errors.As(got, &nErr) {\n\t\t\tif tc.Err.Err != nErr.Err || tc.Err.Addr != nErr.Addr {\n\t\t\t\tt.Errorf(\"got: %v, want: %v\", got, tc.Err)\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/httputil/ratelimiter.go",
    "content": "package httputil\n\nimport (\n\t\"net/http\"\n\t\"sync\"\n\n\t\"golang.org/x/time/rate\"\n)\n\n// RateLimiter wraps the provided RoundTripper with a limiter allowing 10\n// requests/second/host.\n//\n// It responds to HTTP 429 responses by automatically decreasing the rate.\nfunc RateLimiter(next http.RoundTripper) http.RoundTripper {\n\treturn &ratelimiter{\n\t\trt: next,\n\t\tlm: sync.Map{},\n\t}\n}\n\n// Ratelimiter implements the limiting by using a concurrent map and Limiter\n// structs.\ntype ratelimiter struct {\n\tlm sync.Map\n\trt http.RoundTripper\n}\n\nconst rateCap = 10\n\n// RoundTrip implements http.RoundTripper.\nfunc (r *ratelimiter) RoundTrip(req *http.Request) (*http.Response, error) {\n\tkey := req.URL.Host\n\tli, ok := r.lm.Load(key)\n\tif !ok {\n\t\t// Limiter allows \"rateCap\" per sec, one at a time.\n\t\tl := rate.NewLimiter(rate.Limit(rateCap), 1)\n\t\tli, _ = r.lm.LoadOrStore(key, l)\n\t}\n\tl := li.(*rate.Limiter)\n\tif err := l.Wait(req.Context()); err != nil {\n\t\treturn nil, err\n\t}\n\tres, err := r.rt.RoundTrip(req)\n\t// This seems to be the contract that http.Transport implements.\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tswitch res.StatusCode {\n\tcase http.StatusOK:\n\t\t// Try increasing on OK.\n\t\tif lim := l.Limit(); lim < rateCap {\n\t\t\tl.SetLimit(lim + 1)\n\t\t}\n\tcase http.StatusTooManyRequests:\n\t\t// Try to allow some requests, eventually.\n\t\tl.SetLimit(detune(l.Limit()))\n\t}\n\treturn res, nil\n}\n\n// Detune reduces the rate.\nfunc detune(in rate.Limit) rate.Limit {\n\tif in <= 1 {\n\t\treturn in / 2\n\t}\n\treturn in - 1\n}\n"
  },
  {
    "path": "internal/httputil/ratelimiter_test.go",
    "content": "package httputil\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n)\n\nfunc TestRate(t *testing.T) {\n\tconst nReq = 20\n\n\tvar wg sync.WaitGroup\n\twg.Add(nReq)\n\tbegin := make(chan struct{})\n\tvar last struct {\n\t\tsync.Mutex\n\t\tt time.Time\n\t}\n\tsrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {\n\t\tlast.Lock()\n\t\tlast.t = time.Now()\n\t\tlast.Unlock()\n\t\tw.WriteHeader(http.StatusOK)\n\t}))\n\tdefer srv.Close()\n\tcl := srv.Client()\n\tcl.Transport = RateLimiter(cl.Transport)\n\n\tfor i := 0; i < nReq; i++ {\n\t\tgo func() {\n\t\t\tdefer wg.Done()\n\t\t\t<-begin\n\t\t\tres, err := cl.Get(srv.URL)\n\t\t\tif err != nil {\n\t\t\t\tt.Error(err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tres.Body.Close()\n\t\t}()\n\t}\n\n\tfirst := time.Now()\n\tclose(begin)\n\twg.Wait()\n\n\tt.Logf(\"begin: %v\", first)\n\tt.Logf(\"end:   %v\", last.t)\n\trate := nReq / last.t.Sub(first).Seconds()\n\tt.Logf(\"rate:  %v\", rate)\n\n\tif rate < (rateCap-1) || rate > (rateCap+1) {\n\t\tt.Error(\"rate outside acceptable bounds\")\n\t}\n}\n"
  },
  {
    "path": "internal/httputil/responserecorder.go",
    "content": "package httputil\n\nimport \"net/http\"\n\n// ResponseRecorder returns a ResponseWriter that records the HTTP status and\n// body length into the provided pointers, and returns another response writer\n// that understand the go 1.20 http `Unwrap` scheme.\nfunc ResponseRecorder(status *int, length *int64, w http.ResponseWriter) http.ResponseWriter {\n\t// Handle nils being passed, just to be nice.\n\tif length == nil {\n\t\tlength = new(int64)\n\t}\n\tif status == nil {\n\t\tstatus = new(int)\n\t}\n\treturn &responseRecord{\n\t\tResponseWriter: w,\n\t\tstatus:         status,\n\t\tlength:         length,\n\t}\n}\n\nvar _ http.ResponseWriter = (*responseRecord)(nil)\n\ntype responseRecord struct {\n\thttp.ResponseWriter\n\tstatus    *int\n\tlength    *int64\n\twritecall bool\n}\n\nfunc (r *responseRecord) Unwrap() http.ResponseWriter {\n\treturn r.ResponseWriter\n}\n\nfunc (r *responseRecord) WriteHeader(c int) {\n\tif r.writecall {\n\t\treturn\n\t}\n\t*r.status = c\n\tr.ResponseWriter.WriteHeader(c)\n\tr.writecall = true\n}\n\nfunc (r *responseRecord) Write(b []byte) (int, error) {\n\tr.WriteHeader(http.StatusOK)\n\tn, err := r.ResponseWriter.Write(b)\n\t*r.length += int64(n)\n\treturn n, err\n}\n"
  },
  {
    "path": "internal/httputil/responserecorder_test.go",
    "content": "package httputil\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n)\n\nfunc TestResponseRecorder(t *testing.T) {\n\tt.Run(\"OK\", func(t *testing.T) {\n\t\tvar status int\n\t\tvar length int64\n\n\t\trec := httptest.NewRecorder()\n\t\tw := ResponseRecorder(&status, &length, rec)\n\n\t\tsz := 512\n\t\tif n, err := w.Write(make([]byte, sz)); err != nil || n != sz {\n\t\t\tt.Errorf(\"unexpected Write return: (%v, %v)\", n, err)\n\t\t}\n\t\tt.Logf(\"wrote %d bytes, status %q\", length, http.StatusText(status))\n\t\tif got, want := status, http.StatusOK; got != want {\n\t\t\tt.Errorf(\"bad status; got: %d, want: %d\", got, want)\n\t\t}\n\t\tif got, want := length, int64(sz); got != want {\n\t\t\tt.Errorf(\"bad length; got: %d, want: %d\", got, want)\n\t\t}\n\t})\n\n\tt.Run(\"Error\", func(t *testing.T) {\n\t\tvar status int\n\t\tvar length int64\n\t\tsc := http.StatusInternalServerError\n\n\t\trec := httptest.NewRecorder()\n\t\tw := ResponseRecorder(&status, &length, rec)\n\n\t\tsz := 512\n\t\tw.WriteHeader(sc)\n\t\tif n, err := w.Write(make([]byte, sz)); err != nil || n != sz {\n\t\t\tt.Errorf(\"unexpected Write return: (%v, %v)\", n, err)\n\t\t}\n\t\tt.Logf(\"wrote %d bytes, status %q\", length, http.StatusText(status))\n\t\tif got, want := status, sc; got != want {\n\t\t\tt.Errorf(\"bad status; got: %d, want: %d\", got, want)\n\t\t}\n\t\tif got, want := length, int64(sz); got != want {\n\t\t\tt.Errorf(\"bad length; got: %d, want: %d\", got, want)\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "internal/httputil/signer.go",
    "content": "package httputil\n\nimport (\n\t\"context\"\n\t\"log/slog\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/go-jose/go-jose/v3\"\n\t\"github.com/go-jose/go-jose/v3/jwt\"\n\t\"github.com/quay/clair/config\"\n)\n\n// NewSigner constructs a signer according to the provided Config and claim.\n//\n// The returned Signer only adds headers for the hosts specified in the\n// following spots:\n//\n//   - $.notifier.webhook.target\n//   - $.notifier.indexer_addr\n//   - $.notifier.matcher_addr\n//   - $.matcher.indexer_addr\nfunc NewSigner(ctx context.Context, cfg *config.Config, cl jwt.Claims) (*Signer, error) {\n\ts := Signer{\n\t\tuse:   make(map[string]struct{}),\n\t\tclaim: cl,\n\t}\n\tif cfg.Auth.PSK == nil {\n\t\tslog.DebugContext(ctx, \"authentication disabled\")\n\t\treturn &s, nil\n\t}\n\tif cfg.Notifier.Webhook != nil {\n\t\tif err := s.Add(ctx, cfg.Notifier.Webhook.Target); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\tif err := s.Add(ctx, cfg.Notifier.IndexerAddr); err != nil {\n\t\treturn nil, err\n\t}\n\tif err := s.Add(ctx, cfg.Notifier.MatcherAddr); err != nil {\n\t\treturn nil, err\n\t}\n\tif err := s.Add(ctx, cfg.Matcher.IndexerAddr); err != nil {\n\t\treturn nil, err\n\t}\n\n\tsk := jose.SigningKey{\n\t\tAlgorithm: jose.HS256,\n\t\tKey:       []byte(cfg.Auth.PSK.Key),\n\t}\n\tsigner, err := jose.NewSigner(sk, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\ts.signer = signer\n\tif l := slog.Default(); l.Enabled(ctx, slog.LevelDebug) {\n\t\tas := make([]string, 0, len(s.use))\n\t\tfor a := range s.use {\n\t\t\tas = append(as, a)\n\t\t}\n\t\tl.DebugContext(ctx, \"enabling signing for authorities\",\n\t\t\t\"authorities\", as)\n\t}\n\treturn &s, nil\n}\n\n// Add marks the authority in \"uri\" as one that expects signed requests.\nfunc (s *Signer) Add(ctx context.Context, uri string) error {\n\tif uri == \"\" {\n\t\treturn nil\n\t}\n\tu, err := url.Parse(uri)\n\tif err != nil {\n\t\treturn err\n\t}\n\ta := u.Host\n\ts.use[a] = struct{}{}\n\treturn nil\n}\n\n// Signer signs requests.\ntype Signer struct {\n\tsigner jose.Signer\n\tuse    map[string]struct{}\n\tclaim  jwt.Claims\n}\n\n// Sign modifies the passed [http.Request] as needed.\nfunc (s *Signer) Sign(ctx context.Context, req *http.Request) error {\n\tif s == nil || s.signer == nil {\n\t\treturn nil\n\t}\n\thost := req.Host\n\tif host == \"\" {\n\t\thost = req.URL.Host\n\t}\n\tif _, ok := s.use[host]; !ok {\n\t\treturn nil\n\t}\n\tcl := s.claim\n\tnow := time.Now()\n\tcl.IssuedAt = jwt.NewNumericDate(now)\n\tcl.NotBefore = jwt.NewNumericDate(now.Add(-jwt.DefaultLeeway))\n\tcl.Expiry = jwt.NewNumericDate(now.Add(jwt.DefaultLeeway))\n\th, err := jwt.Signed(s.signer).Claims(&cl).CompactSerialize()\n\tif err != nil {\n\t\treturn err\n\t}\n\treq.Header.Add(\"authorization\", \"Bearer \"+h)\n\treturn nil\n}\n"
  },
  {
    "path": "internal/logging/logging.go",
    "content": "// Package logging holds the logging singletons for Clair.\n//\n// An init function sets the [slog] default Logger.\npackage logging\n\nimport (\n\t\"log/slog\"\n\t\"os\"\n\n\t\"github.com/quay/claircore/toolkit/log\"\n\t\"github.com/quay/zlog/v2\"\n)\n\nfunc init() {\n\tslog.SetLogLoggerLevel(slog.LevelDebug)\n\tSetLogger(DefaultOptions())\n}\n\n// Level is the [slog.Leveler] that the [zlog.Options] returned by\n// [DefaultOptions] points to.\nvar Level slog.LevelVar\n\n// DefaultOptions returns a default set of options for a zlog/v2 [slog.Handler].\nfunc DefaultOptions() *zlog.Options {\n\treturn &zlog.Options{\n\t\tLevel:      &Level,\n\t\tContextKey: log.AttrsKey,\n\t\tLevelKey:   log.LevelKey,\n\t}\n}\n\n// SetLogger configures the default [slog.Logger] to use a zlog-backed\n// [slog.Handler] writing to [os.Stderr] using the passed [zlog.Options].\nfunc SetLogger(opts *zlog.Options) {\n\tslog.SetDefault(slog.New(zlog.NewHandler(os.Stderr, opts)))\n}\n"
  },
  {
    "path": "introspection/otlp.go",
    "content": "package introspection\n\nimport (\n\t\"crypto/tls\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/quay/clair/config\"\n\t\"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc\"\n\t\"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp\"\n\t\"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc\"\n\t\"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp\"\n\t\"google.golang.org/grpc/credentials\"\n)\n\n// This file holds all the OTLP weirdness.\n//\n// Most of the options are the same internally, but have a bunch of type ceremony around them to obfuscate this.\n\n// OtlpHooks is a hook structure for using the correct types with the various OTLP exporters.\n// The declared variables are largely duplicates, with the real logic living in [otlpHooks.Options].\n//\n// The type parameter \"O\" should really be a sum type, but that's currently inexpressible.\ntype otlpHooks[O any] struct {\n\tWithCompressor      func(config.OTLPCompressor) O\n\tWithEndpoint        func(string) O\n\tWithHeaders         func(map[string]string) O\n\tWithInsecure        func() O\n\tWithTimeout         func(time.Duration) O\n\tWithTLSClientConfig func(*tls.Config) O\n\n\tWithURLPath func(string) O\n\n\tWithReconnectionPeriod func(time.Duration) O\n\tWithServiceConfig      func(string) O\n}\n\n// Options returns the correct Options to pass into the constructor based on the receiver type.\n//\n// This function will panic if called in unexpected ways. To be safe:\n//\n//   - Only use the provided instances ([omhHooks], [omgHooks], [othHooks], [otgHooks]).\n//   - Read the implementation.\nfunc (h *otlpHooks[O]) Options(v any) (opts []O, err error) {\n\tswitch cfg := v.(type) {\n\t// Signal-specific options.\n\t//\n\t// Currently, none; recurse to the transport options.\n\tcase *config.MetricOTLPHTTP:\n\t\topts, err = h.Options(&cfg.OTLPHTTPCommon)\n\tcase *config.TraceOTLPHTTP:\n\t\topts, err = h.Options(&cfg.OTLPHTTPCommon)\n\tcase *config.MetricOTLPgRPC:\n\t\topts, err = h.Options(&cfg.OTLPgRPCCommon)\n\tcase *config.TraceOTLPgRPC:\n\t\topts, err = h.Options(&cfg.OTLPgRPCCommon)\n\n\t// Transport-specific options.\n\t//\n\t// Recurse to the common options then return the transport options, in case of some ordering oddness.\n\t// Will panic if called on the wrong receiver, as some of the members will (purposefully!) be nil.\n\tcase *config.OTLPHTTPCommon:\n\t\topts, err = h.Options(&cfg.OTLPCommon)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif p := cfg.URLPath; p != \"\" {\n\t\t\topts = append(opts, h.WithURLPath(p))\n\t\t}\n\tcase *config.OTLPgRPCCommon:\n\t\topts, err = h.Options(&cfg.OTLPCommon)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif r := cfg.Reconnect; r != nil {\n\t\t\topts = append(opts, h.WithReconnectionPeriod(time.Duration(*r)))\n\t\t}\n\t\tif srv := cfg.ServiceConfig; srv != \"\" {\n\t\t\topts = append(opts, h.WithServiceConfig(srv))\n\t\t}\n\n\t// Common options.\n\tcase *config.OTLPCommon:\n\t\tif e := cfg.Endpoint; e != \"\" {\n\t\t\topts = append(opts, h.WithEndpoint(e))\n\t\t}\n\t\topts = append(opts, h.WithCompressor(cfg.Compression))\n\t\tif len(cfg.Headers) != 0 {\n\t\t\topts = append(opts, h.WithHeaders(cfg.Headers))\n\t\t}\n\t\tif cfg.Insecure {\n\t\t\topts = append(opts, h.WithInsecure())\n\t\t}\n\t\tif t := cfg.Timeout; t != nil {\n\t\t\topts = append(opts, h.WithTimeout(time.Duration(*t)))\n\t\t}\n\t\tif tc := cfg.ClientTLS; tc != nil {\n\t\t\ttlscfg, err := tc.Config()\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"TLS client configuration error: %w\", err)\n\t\t\t}\n\t\t\topts = append(opts, h.WithTLSClientConfig(tlscfg))\n\t\t}\n\n\t// Make the switch exhaustive.\n\tdefault:\n\t\tpanic(fmt.Sprintf(\"programmer error: unexpected type: %T\", v))\n\t}\n\treturn opts, nil\n}\n\n// In order, these instances are for:\n//\n//   - Metrics HTTP\n//   - Metrics gRPC\n//   - Traces HTTP\n//   - Traces gRPC\nvar (\n\tomhHooks = otlpHooks[otlpmetrichttp.Option]{\n\t\tWithCompressor: otlpCompressorHook(\n\t\t\totlpmetrichttp.WithCompression(otlpmetrichttp.NoCompression),\n\t\t\totlpmetrichttp.WithCompression(otlpmetrichttp.GzipCompression),\n\t\t),\n\t\tWithEndpoint:        otlpmetrichttp.WithEndpoint,\n\t\tWithHeaders:         otlpmetrichttp.WithHeaders,\n\t\tWithInsecure:        otlpmetrichttp.WithInsecure,\n\t\tWithTimeout:         otlpmetrichttp.WithTimeout,\n\t\tWithTLSClientConfig: otlpmetrichttp.WithTLSClientConfig,\n\t\tWithURLPath:         otlpmetrichttp.WithURLPath,\n\t}\n\tomgHooks = otlpHooks[otlpmetricgrpc.Option]{\n\t\tWithCompressor: otlpCompressorHook(\n\t\t\totlpmetricgrpc.WithCompressor(\"none\"),\n\t\t\totlpmetricgrpc.WithCompressor(\"gzip\"),\n\t\t),\n\t\tWithEndpoint:           otlpmetricgrpc.WithEndpoint,\n\t\tWithHeaders:            otlpmetricgrpc.WithHeaders,\n\t\tWithInsecure:           otlpmetricgrpc.WithInsecure,\n\t\tWithTimeout:            otlpmetricgrpc.WithTimeout,\n\t\tWithTLSClientConfig:    grpcTLSHook(otlpmetricgrpc.WithTLSCredentials),\n\t\tWithReconnectionPeriod: otlpmetricgrpc.WithReconnectionPeriod,\n\t\tWithServiceConfig:      otlpmetricgrpc.WithServiceConfig,\n\t}\n\tothHooks = otlpHooks[otlptracehttp.Option]{\n\t\tWithCompressor: otlpCompressorHook(\n\t\t\totlptracehttp.WithCompression(otlptracehttp.NoCompression),\n\t\t\totlptracehttp.WithCompression(otlptracehttp.GzipCompression),\n\t\t),\n\t\tWithEndpoint:        otlptracehttp.WithEndpoint,\n\t\tWithHeaders:         otlptracehttp.WithHeaders,\n\t\tWithInsecure:        otlptracehttp.WithInsecure,\n\t\tWithTimeout:         otlptracehttp.WithTimeout,\n\t\tWithTLSClientConfig: otlptracehttp.WithTLSClientConfig,\n\t\tWithURLPath:         otlptracehttp.WithURLPath,\n\t}\n\totgHooks = otlpHooks[otlptracegrpc.Option]{\n\t\tWithCompressor: otlpCompressorHook(\n\t\t\totlptracegrpc.WithCompressor(\"none\"),\n\t\t\totlptracegrpc.WithCompressor(\"gzip\"),\n\t\t),\n\t\tWithEndpoint:           otlptracegrpc.WithEndpoint,\n\t\tWithHeaders:            otlptracegrpc.WithHeaders,\n\t\tWithInsecure:           otlptracegrpc.WithInsecure,\n\t\tWithTimeout:            otlptracegrpc.WithTimeout,\n\t\tWithTLSClientConfig:    grpcTLSHook(otlptracegrpc.WithTLSCredentials),\n\t\tWithReconnectionPeriod: otlptracegrpc.WithReconnectionPeriod,\n\t\tWithServiceConfig:      otlptracegrpc.WithServiceConfig,\n\t}\n)\n\n// OtlpCompressorHook maps from the [config.OTLPCompressor] type to the correct option.\n//\n// The type parameter is too broad, see also [otlpHooks].\n// This function causes some extra garbage to be created.\n// Inlining and simplifying at use sites would prevent the options from being constructed until needed,\n// but consolidates the precedence and default logic.\nfunc otlpCompressorHook[O any](none, gzip O) func(config.OTLPCompressor) O {\n\treturn func(z config.OTLPCompressor) O {\n\t\tswitch z {\n\t\tcase config.OTLPCompressUnset: // Actual default:\n\t\t\tfallthrough\n\t\tcase config.OTLPCompressNone:\n\t\t\treturn none\n\t\tcase config.OTLPCompressGzip:\n\t\t\treturn gzip\n\t\tdefault:\n\t\t\tpanic(\"unreachable: exhaustive switch\")\n\t\t}\n\t}\n}\n\n// GrpcTLSHook maps a [tls.Config] to a correctly typed option.\n//\n// The type parameter is too broad, see also [otlpHooks].\nfunc grpcTLSHook[O any](f func(credentials.TransportCredentials) O) func(*tls.Config) O {\n\treturn func(c *tls.Config) O { return f(credentials.NewTLS(c)) }\n}\n"
  },
  {
    "path": "introspection/server.go",
    "content": "// Package introspection holds the implementation details for the\n// \"introspection\" HTTP server that Clair hosts.\npackage introspection\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/http/pprof\"\n\t\"time\"\n\n\tdeltapprof \"github.com/grafana/pyroscope-go/godeltaprof/http/pprof\"\n\t\"github.com/prometheus/client_golang/prometheus/promhttp\"\n\t\"github.com/quay/clair/config\"\n\t\"go.opentelemetry.io/otel\"\n\t\"go.opentelemetry.io/otel/exporters/jaeger\"\n\t\"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc\"\n\t\"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp\"\n\t\"go.opentelemetry.io/otel/exporters/otlp/otlptrace\"\n\t\"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc\"\n\t\"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp\"\n\t\"go.opentelemetry.io/otel/exporters/prometheus\"\n\t\"go.opentelemetry.io/otel/exporters/stdout/stdoutmetric\"\n\t\"go.opentelemetry.io/otel/exporters/stdout/stdouttrace\"\n\t\"go.opentelemetry.io/otel/sdk/metric\"\n\t\"go.opentelemetry.io/otel/sdk/resource\"\n\tsdktrace \"go.opentelemetry.io/otel/sdk/trace\"\n\tsemconv \"go.opentelemetry.io/otel/semconv/v1.27.0\"\n\n\t\"github.com/quay/clair/v4/health\"\n)\n\n// Valid backends for both metrics and traces.\nconst (\n\tStdout = \"stdout\"\n\tOTLP   = \"otlp\"\n)\n\n// Valid backends for metrics.\nconst (\n\tProm = \"prometheus\"\n)\n\n// Valid backends for traces.\nconst (\n\tJaeger = \"jaeger\"\n)\n\n// Endpoints on the introspection HTTP server.\nconst (\n\tDefaultPromEndpoint = \"/metrics\"\n\tHealthEndpoint      = \"/healthz\"\n\tReadyEndpoint       = \"/readyz\"\n)\n\n// DefaultIntrospectionAddr is the default address if not provided in the configuration.\nconst DefaultIntrospectionAddr = \":8089\"\n\n// Server provides an HTTP server exposing Clair metrics and debugging information.\ntype Server struct {\n\t// configuration provided when starting Clair\n\tconf *config.Config\n\t// Server embeds a http.Server and http.ServeMux.\n\t// The http.Server will be configured with the ServeMux on successful\n\t// initialization.\n\t*http.Server\n\t*http.ServeMux\n\t// a health check function\n\thealth func() bool\n}\n\n// New constructs a [*Server], which has an embedded [*http.Server].\nfunc New(ctx context.Context, conf *config.Config, health func() bool) (*Server, error) {\n\tvar err error\n\tvar addr string\n\tif conf.IntrospectionAddr == \"\" {\n\t\taddr = DefaultIntrospectionAddr\n\t\tslog.InfoContext(ctx, \"no introspection address provided; using default\",\n\t\t\t\"address\", addr)\n\t} else {\n\t\taddr = conf.IntrospectionAddr\n\t}\n\n\ti := &Server{\n\t\tconf: conf,\n\t\tServer: &http.Server{\n\t\t\tAddr:        addr,\n\t\t\tBaseContext: func(_ net.Listener) context.Context { return ctx },\n\t\t},\n\t\tServeMux: http.NewServeMux(),\n\t}\n\n\t// check for health\n\tif health == nil {\n\t\tslog.WarnContext(ctx, \"no health check configured; unconditionally reporting OK\")\n\t\ti.health = func() bool { return true }\n\t} else {\n\t\ti.health = health\n\t}\n\n\t// Configure metrics\n\tvar mr metric.Reader\n\tswitch conf.Metrics.Name {\n\tcase Stdout:\n\t\tvar ex metric.Exporter\n\t\tex, err = stdoutmetric.New()\n\t\tif err != nil {\n\t\t\tbreak\n\t\t}\n\t\tmr = metric.NewPeriodicReader(ex)\n\tcase Prom, \"\":\n\t\tendpoint := DefaultPromEndpoint\n\t\tif p := conf.Metrics.Prometheus.Endpoint; p != nil {\n\t\t\tendpoint = *p\n\t\t}\n\t\tslog.InfoContext(ctx, \"configuring prometheus\",\n\t\t\t\"endpoint\", endpoint,\n\t\t\t\"server\", i.Addr)\n\n\t\ti.Handle(endpoint, promhttp.Handler())\n\n\t\tmr, err = prometheus.New()\n\tcase OTLP:\n\t\tconf := i.conf.Trace.OTLP\n\t\tif conf.GRPC == nil && conf.HTTP == nil {\n\t\t\treturn nil, fmt.Errorf(`must define either \"grpc\" or \"http\" transport for otlp traces`)\n\t\t}\n\n\t\tvar ex metric.Exporter\n\t\tswitch {\n\t\tcase conf.GRPC != nil:\n\t\t\tvar opts []otlpmetricgrpc.Option\n\t\t\topts, err = omgHooks.Options(conf.GRPC)\n\t\t\tif err != nil {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tex, err = otlpmetricgrpc.New(ctx, opts...)\n\t\tcase conf.HTTP != nil:\n\t\t\tvar opts []otlpmetrichttp.Option\n\t\t\topts, err = omhHooks.Options(conf.HTTP)\n\t\t\tif err != nil {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tex, err = otlpmetrichttp.New(ctx, opts...)\n\t\tdefault:\n\t\t\tpanic(\"programmer error: exhaustive switch\")\n\t\t}\n\t\tif err != nil {\n\t\t\tbreak\n\t\t}\n\n\t\t// Print a warning as long as direct prometheus metrics exist in \"our\" packages.\n\t\tslog.WarnContext(ctx, \"OTLP metrics should be considered beta; metrics may be missing\")\n\t\tmr = metric.NewPeriodicReader(ex)\n\tdefault:\n\t\tslog.InfoContext(ctx, \"no metrics enabled\")\n\t}\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error configuring metrics: %w\", err)\n\t}\n\tif mr != nil {\n\t\tmp := metric.NewMeterProvider(\n\t\t\tmetric.WithReader(mr),\n\t\t\tmetric.WithResource(resource.NewWithAttributes(\n\t\t\t\tsemconv.SchemaURL,\n\t\t\t\tsemconv.ServiceNameKey.String(fmt.Sprintf(\"clairv4/%v\", i.conf.Mode)),\n\t\t\t)),\n\t\t)\n\t\totel.SetMeterProvider(mp)\n\t\ti.Server.RegisterOnShutdown(func() {\n\t\t\tctx, cancel := context.WithTimeout(ctx, 5*time.Second)\n\t\t\tdefer cancel()\n\t\t\tif err := mp.Shutdown(ctx); err != nil {\n\t\t\t\tslog.ErrorContext(ctx, \"error shutting down metric provider\",\n\t\t\t\t\t\"reason\", err)\n\t\t\t}\n\t\t})\n\t}\n\n\t// configure tracing\n\t// sampler\n\tvar sampler sdktrace.Sampler\n\tswitch {\n\tcase i.conf.LogLevel == config.DebugLog || i.conf.LogLevel == config.DebugColorLog:\n\t\tsampler = sdktrace.AlwaysSample()\n\tcase i.conf.Trace.Probability != nil:\n\t\tp := *i.conf.Trace.Probability\n\t\tsampler = sdktrace.ParentBased(\n\t\t\tsdktrace.TraceIDRatioBased(p),\n\t\t)\n\tdefault:\n\t\tsampler = sdktrace.ParentBased(sdktrace.NeverSample())\n\t}\n\n\t// trace exporter\n\tvar exporter sdktrace.SpanExporter\n\tswitch conf.Trace.Name {\n\tcase Stdout:\n\t\texporter, err = stdouttrace.New()\n\tcase Jaeger:\n\t\tconf := i.conf.Trace.Jaeger\n\t\tvar mode string\n\t\tvar endpoint string\n\n\t\t// configure whether jaeger exporter pushes to an agent\n\t\t// or a collector\n\t\tswitch {\n\t\tcase conf.Agent.Endpoint != \"\":\n\t\t\tmode = \"agent\"\n\t\t\tendpoint = conf.Agent.Endpoint\n\t\tcase conf.Collector.Endpoint != \"\":\n\t\t\tmode = \"collector\"\n\t\t\tendpoint = conf.Collector.Endpoint\n\t\tdefault:\n\t\t\tmode = \"agent\"\n\t\t}\n\n\t\tvar e jaeger.EndpointOption\n\t\tswitch mode {\n\t\tcase \"agent\":\n\t\t\tslog.InfoContext(ctx, \"configuring jaeger exporter to push to agent\")\n\t\t\tvar opt []jaeger.AgentEndpointOption\n\t\t\tif endpoint != \"\" {\n\t\t\t\thost, port, err := net.SplitHostPort(endpoint)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, fmt.Errorf(\"error configuring jaeger tracing: %w\", err)\n\t\t\t\t}\n\t\t\t\tif host != \"\" {\n\t\t\t\t\topt = append(opt, jaeger.WithAgentHost(host))\n\t\t\t\t}\n\t\t\t\tif port != \"\" {\n\t\t\t\t\topt = append(opt, jaeger.WithAgentPort(port))\n\t\t\t\t}\n\t\t\t}\n\t\t\te = jaeger.WithAgentEndpoint(opt...)\n\t\tcase \"collector\":\n\t\t\tslog.InfoContext(ctx, \"configuring jaeger exporter to push to collector\")\n\t\t\tvar opt []jaeger.CollectorEndpointOption\n\t\t\tif endpoint != \"\" {\n\t\t\t\topt = append(opt, jaeger.WithEndpoint(endpoint))\n\t\t\t}\n\t\t\tu, p := conf.Collector.Username, conf.Collector.Password\n\t\t\tif u != nil {\n\t\t\t\topt = append(opt, jaeger.WithUsername(*u))\n\t\t\t}\n\t\t\tif p != nil {\n\t\t\t\topt = append(opt, jaeger.WithPassword(*p))\n\t\t\t}\n\t\t\te = jaeger.WithCollectorEndpoint(opt...)\n\t\t}\n\n\t\t// configure the exporter\n\t\texporter, err = jaeger.New(e)\n\tcase OTLP:\n\t\tconf := i.conf.Trace.OTLP\n\t\tif conf.GRPC == nil && conf.HTTP == nil {\n\t\t\treturn nil, fmt.Errorf(`must define either \"grpc\" or \"http\" transport for otlp traces`)\n\t\t}\n\n\t\tvar c otlptrace.Client\n\t\tswitch {\n\t\tcase conf.GRPC != nil:\n\t\t\tvar opts []otlptracegrpc.Option\n\t\t\topts, err = otgHooks.Options(conf.GRPC)\n\t\t\tif err != nil {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tc = otlptracegrpc.NewClient(opts...)\n\t\tcase conf.HTTP != nil:\n\t\t\tvar opts []otlptracehttp.Option\n\t\t\topts, err = othHooks.Options(conf.HTTP)\n\t\t\tif err != nil {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tc = otlptracehttp.NewClient(opts...)\n\t\tdefault:\n\t\t\tpanic(\"programmer error: exhaustive switch\")\n\t\t}\n\t\tif err != nil {\n\t\t\tbreak\n\t\t}\n\n\t\texporter, err = otlptrace.New(ctx, c)\n\tdefault:\n\t\tslog.InfoContext(ctx, \"no distributed tracing enabled\")\n\t}\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error configuring tracing: %w\", err)\n\t}\n\tif exporter != nil {\n\t\ttp := sdktrace.NewTracerProvider(\n\t\t\tsdktrace.WithSampler(sampler),\n\t\t\tsdktrace.WithBatcher(exporter),\n\t\t\tsdktrace.WithResource(resource.NewWithAttributes(\n\t\t\t\tsemconv.SchemaURL,\n\t\t\t\tsemconv.ServiceNameKey.String(fmt.Sprintf(\"clairv4/%v\", i.conf.Mode)),\n\t\t\t)),\n\t\t)\n\t\totel.SetTracerProvider(tp)\n\t\ti.Server.RegisterOnShutdown(func() {\n\t\t\tslog.InfoContext(ctx, \"shutting down trace provider\")\n\t\t\tctx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 5*time.Second)\n\t\t\tdefer cancel()\n\t\t\tif err := tp.Shutdown(ctx); err != nil {\n\t\t\t\tslog.ErrorContext(ctx, \"error shutting down trace provider\",\n\t\t\t\t\t\"reason\", err)\n\t\t\t}\n\t\t})\n\t\tslog.InfoContext(ctx, \"distributed tracing configured\")\n\t}\n\n\t// configure diagnostics\n\terr = i.withDiagnostics(ctx)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error configuring diagnostics: %v\", err)\n\t}\n\tif err := i.withReady(ctx); err != nil {\n\t\treturn nil, fmt.Errorf(\"error configuring ready: %v\", err)\n\t}\n\n\t// attach Introspection to server, this works because we embed http.ServeMux\n\ti.Server.Handler = i\n\n\treturn i, nil\n}\n\n// WithDiagnostics enables healthz and pprof endpoints.\nfunc (i *Server) withDiagnostics(_ context.Context) error {\n\thealth := i.health\n\ti.HandleFunc(HealthEndpoint, func(w http.ResponseWriter, r *http.Request) {\n\t\tw.Header().Set(\"X-Content-Type-Options\", \"nosniff\")\n\t\tw.Header().Set(\"Content-Type\", \"text/plain; charset=utf-8\")\n\t\tif !health() {\n\t\t\tw.WriteHeader(http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\t\tfmt.Fprint(w, `ok`)\n\t})\n\ti.HandleFunc(\"/debug/pprof/\", pprof.Index)\n\ti.HandleFunc(\"/debug/pprof/cmdline\", pprof.Cmdline)\n\ti.HandleFunc(\"/debug/pprof/profile\", pprof.Profile)\n\ti.HandleFunc(\"/debug/pprof/symbol\", pprof.Symbol)\n\ti.HandleFunc(\"/debug/pprof/trace\", pprof.Trace)\n\ti.HandleFunc(\"/debug/pprof/delta_heap\", deltapprof.Heap)\n\ti.HandleFunc(\"/debug/pprof/delta_block\", deltapprof.Block)\n\ti.HandleFunc(\"/debug/pprof/delta_mutex\", deltapprof.Mutex)\n\treturn nil\n}\n\nfunc (i *Server) withReady(_ context.Context) error {\n\ti.ServeMux.Handle(ReadyEndpoint, health.ReadinessHandler())\n\treturn nil\n}\n"
  },
  {
    "path": "local-dev/clair/.gitignore",
    "content": "quay.yaml\n"
  },
  {
    "path": "local-dev/clair/config.yaml",
    "content": "---\nlog_level: debug-color\nintrospection_addr: \":8089\"\nhttp_listen_addr: \":6060\"\nupdaters:\n  sets:\n    - ubuntu\n    - debian\n    - rhel-vex\n    - alpine\n    - osv\nauth:\n  psk:\n    key: 'c2VjcmV0'\n    iss:\n      - quay\n      - clairctl\nindexer:\n  connstring: host=clair-database user=clair dbname=indexer sslmode=disable\n  scanlock_retry: 10\n  layer_scan_concurrency: 5\n  migrations: true\nmatcher:\n  indexer_addr: http://clair-indexer:6060/\n  connstring: host=clair-database user=clair dbname=matcher sslmode=disable\n  max_conn_pool: 100\n  migrations: true\nmatchers: {}\nnotifier:\n  indexer_addr: http://clair-indexer:6060/\n  matcher_addr: http://clair-matcher:6060/\n  connstring: host=clair-database user=clair dbname=notifier sslmode=disable\n  migrations: true\n  delivery_interval: 1m\n  poll_interval: 1m\n  webhook:\n    target: \"http://webhook-target/\"\n    callback: \"http://clair-notifier:6060/notifier/api/v1/notification/\"\n  # amqp:\n  #   direct: true\n  #   exchange:\n  #     name: \"\"\n  #     type: \"direct\"\n  #     durable: true\n  #     auto_delete: false\n  #   uris: [\"amqp://guest:guest@clair-rabbitmq:5672/\"]\n  #   routing_key: \"notifications\"\n  #   callback: \"http://clair-notifier/notifier/api/v1/notification\"\n# tracing and metrics config\ntrace:\n  name: \"otlp\"\n#  probability: 1\n  otlp:\n    http:\n      endpoint: \"clair-jaeger:4318\"\n      insecure: true\nmetrics:\n  name: \"prometheus\"\n"
  },
  {
    "path": "local-dev/clair/config.yaml.d/.gitignore",
    "content": "*\n!.gitignore\n"
  },
  {
    "path": "local-dev/clair/init.sql",
    "content": "CREATE USER clair WITH PASSWORD 'clair';\nCREATE USER quay WITH PASSWORD 'quay';\nCREATE DATABASE indexer WITH OWNER clair;\nCREATE DATABASE matcher WITH OWNER clair;\nCREATE DATABASE notifier WITH OWNER clair;\nCREATE DATABASE quay WITH OWNER quay;\n\\connect matcher\nCREATE EXTENSION \"uuid-ossp\";\n\\connect notifier\nCREATE EXTENSION \"uuid-ossp\";\n\\connect quay\nCREATE EXTENSION \"pg_trgm\";\n"
  },
  {
    "path": "local-dev/clair/quay.yaml.d/.gitignore",
    "content": "*\n!.gitignore\n"
  },
  {
    "path": "local-dev/grafana/provisioning/dashboards/clair.json",
    "content": "{\n  \"annotations\": {\n    \"list\": [\n      {\n        \"builtIn\": 1,\n        \"datasource\": \"-- Grafana --\",\n        \"enable\": true,\n        \"hide\": true,\n        \"iconColor\": \"rgba(0, 211, 255, 1)\",\n        \"name\": \"Annotations & Alerts\",\n        \"target\": {\n          \"limit\": 100,\n          \"matchAny\": false,\n          \"tags\": [],\n          \"type\": \"dashboard\"\n        },\n        \"type\": \"dashboard\"\n      }\n    ]\n  },\n  \"editable\": true,\n  \"gnetId\": null,\n  \"graphTooltip\": 1,\n  \"iteration\": 1694452951165,\n  \"links\": [],\n  \"panels\": [\n    {\n      \"datasource\": \"$datasource\",\n      \"gridPos\": {\n        \"h\": 1,\n        \"w\": 24,\n        \"x\": 0,\n        \"y\": 0\n      },\n      \"id\": 24,\n      \"title\": \"Runtime metrics\",\n      \"type\": \"row\"\n    },\n    {\n      \"datasource\": \"$datasource\",\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 0,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"lineInterpolation\": \"linear\",\n            \"lineWidth\": 1,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 12,\n        \"x\": 0,\n        \"y\": 1\n      },\n      \"id\": 26,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\"\n        },\n        \"tooltip\": {\n          \"mode\": \"single\"\n        }\n      },\n      \"targets\": [\n        {\n          \"exemplar\": true,\n          \"expr\": \"sum by (scanned_before) (rate(claircore_indexer_scanned_manifests[$rate]))\",\n          \"interval\": \"\",\n          \"legendFormat\": \"{{scanned_before}}\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Previously scanned manifests / s\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"datasource\": \"$datasource\",\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            }\n          },\n          \"mappings\": []\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 12,\n        \"x\": 12,\n        \"y\": 1\n      },\n      \"id\": 32,\n      \"options\": {\n        \"legend\": {\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\"\n        },\n        \"pieType\": \"pie\",\n        \"reduceOptions\": {\n          \"calcs\": [\n            \"lastNotNull\"\n          ],\n          \"fields\": \"\",\n          \"values\": false\n        },\n        \"tooltip\": {\n          \"mode\": \"single\"\n        }\n      },\n      \"targets\": [\n        {\n          \"exemplar\": true,\n          \"expr\": \"sum by (scanned_before) (rate(claircore_indexer_scanned_manifests[$rate]))\",\n          \"interval\": \"\",\n          \"legendFormat\": \"{{scanned_before}}\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Previously scanned rate\",\n      \"type\": \"piechart\"\n    },\n    {\n      \"datasource\": \"$datasource\",\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 0,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"lineInterpolation\": \"linear\",\n            \"lineWidth\": 1,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 12,\n        \"x\": 0,\n        \"y\": 9\n      },\n      \"id\": 35,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\"\n        },\n        \"tooltip\": {\n          \"mode\": \"single\"\n        }\n      },\n      \"targets\": [\n        {\n          \"exemplar\": true,\n          \"expr\": \"sum (clair_cmd_version_info) by (job, goversion)\",\n          \"hide\": false,\n          \"interval\": \"\",\n          \"legendFormat\": \"{{job}} - {{goversion}}\",\n          \"refId\": \"B\"\n        },\n        {\n          \"exemplar\": true,\n          \"expr\": \"sum (clair_cmd_version_info) by(job, version)\",\n          \"hide\": false,\n          \"interval\": \"\",\n          \"legendFormat\": \"{{job}} clair - {{version}}\",\n          \"refId\": \"C\"\n        },\n        {\n          \"exemplar\": true,\n          \"expr\": \"sum (clair_cmd_version_info) by(job, claircore_version)\",\n          \"interval\": \"\",\n          \"legendFormat\": \"{{job}} claircore - {{claircore_version}}\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Versions\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"datasource\": \"$datasource\",\n      \"description\": \"\",\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 0,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"lineInterpolation\": \"linear\",\n            \"lineWidth\": 1,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 12,\n        \"x\": 12,\n        \"y\": 9\n      },\n      \"id\": 45,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\"\n        },\n        \"tooltip\": {\n          \"mode\": \"single\"\n        }\n      },\n      \"targets\": [\n        {\n          \"exemplar\": true,\n          \"expr\": \"rate(clair_http_concurrencylimited_total[$rate])\",\n          \"interval\": \"\",\n          \"legendFormat\": \"Endpoint: {{ endpoint }} Method: {{ method }}\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Ratelimiting / s\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"collapsed\": false,\n      \"datasource\": \"$datasource\",\n      \"gridPos\": {\n        \"h\": 1,\n        \"w\": 24,\n        \"x\": 0,\n        \"y\": 17\n      },\n      \"id\": 18,\n      \"panels\": [],\n      \"title\": \"Database Indexer\",\n      \"type\": \"row\"\n    },\n    {\n      \"datasource\": \"$datasource\",\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 0,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"lineInterpolation\": \"linear\",\n            \"lineWidth\": 1,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 7,\n        \"w\": 24,\n        \"x\": 0,\n        \"y\": 18\n      },\n      \"id\": 20,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\"\n        },\n        \"tooltip\": {\n          \"mode\": \"single\"\n        }\n      },\n      \"targets\": [\n        {\n          \"exemplar\": true,\n          \"expr\": \"rate(claircore_indexer_setindexreport_total[$rate])\",\n          \"interval\": \"\",\n          \"legendFormat\": \"IndexReport: {{instance }}: {{ query }}\",\n          \"refId\": \"A\"\n        },\n        {\n          \"exemplar\": true,\n          \"expr\": \"rate(claircore_indexer_layerscanned_total[$rate])\",\n          \"hide\": false,\n          \"interval\": \"\",\n          \"legendFormat\": \"LayerScanned: {{instance }}: {{ query }}\",\n          \"refId\": \"B\"\n        },\n        {\n          \"exemplar\": true,\n          \"expr\": \"rate(claircore_indexer_manifestscanned_total[$rate])\",\n          \"hide\": false,\n          \"interval\": \"\",\n          \"legendFormat\": \"ManifestScanned: {{instance }}: {{ query }}\",\n          \"refId\": \"C\"\n        },\n        {\n          \"exemplar\": true,\n          \"expr\": \"rate(claircore_indexer_persistmanifest_total[$rate])\",\n          \"hide\": false,\n          \"interval\": \"\",\n          \"legendFormat\": \"PersistManifest: {{instance }}: {{ query }}\",\n          \"refId\": \"D\"\n        },\n        {\n          \"exemplar\": true,\n          \"expr\": \"rate(claircore_indexer_registerscanners_total[$rate])\",\n          \"hide\": false,\n          \"interval\": \"\",\n          \"legendFormat\": \"RegisterScanners: {{instance }}: {{ query }}\",\n          \"refId\": \"E\"\n        },\n        {\n          \"exemplar\": true,\n          \"expr\": \"rate(claircore_indexer_setindexreport_total[$rate])\",\n          \"hide\": false,\n          \"interval\": \"\",\n          \"legendFormat\": \"SetIndexReport: {{instance }}: {{ query }}\",\n          \"refId\": \"F\"\n        }\n      ],\n      \"title\": \"Database query count (indexer)\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"cards\": {\n        \"cardPadding\": null,\n        \"cardRound\": null\n      },\n      \"color\": {\n        \"cardColor\": \"#b4ff00\",\n        \"colorScale\": \"linear\",\n        \"colorScheme\": \"interpolateOranges\",\n        \"exponent\": 0.5,\n        \"mode\": \"opacity\"\n      },\n      \"dataFormat\": \"tsbuckets\",\n      \"datasource\": \"$datasource\",\n      \"description\": \"\",\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 4,\n        \"x\": 0,\n        \"y\": 25\n      },\n      \"heatmap\": {},\n      \"hideZeroBuckets\": true,\n      \"highlightCards\": true,\n      \"id\": 22,\n      \"legend\": {\n        \"show\": true\n      },\n      \"maxDataPoints\": 50,\n      \"repeat\": null,\n      \"reverseYBuckets\": false,\n      \"targets\": [\n        {\n          \"exemplar\": true,\n          \"expr\": \"sum(rate(claircore_indexer_setindexreport_duration_seconds_bucket[5m])) by (le)\",\n          \"format\": \"heatmap\",\n          \"hide\": false,\n          \"interval\": \"\",\n          \"legendFormat\": \"{{ le }}\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Set index_report query duration\",\n      \"tooltip\": {\n        \"show\": true,\n        \"showHistogram\": false\n      },\n      \"transformations\": [\n        {\n          \"id\": \"groupBy\",\n          \"options\": {\n            \"fields\": {\n              \"IndexReport\": {\n                \"aggregations\": [\n                  \"lastNotNull\"\n                ],\n                \"operation\": null\n              },\n              \"LayerScanned\": {\n                \"aggregations\": [],\n                \"operation\": null\n              },\n              \"ManifestScanned\": {\n                \"aggregations\": [],\n                \"operation\": null\n              },\n              \"PersistManifest\": {\n                \"aggregations\": [],\n                \"operation\": null\n              },\n              \"RegisterScanners\": {\n                \"aggregations\": [],\n                \"operation\": null\n              },\n              \"SetIndexReport\": {\n                \"aggregations\": [],\n                \"operation\": null\n              },\n              \"Time\": {\n                \"aggregations\": [\n                  \"sum\"\n                ],\n                \"operation\": null\n              }\n            }\n          }\n        }\n      ],\n      \"type\": \"heatmap\",\n      \"xAxis\": {\n        \"show\": true\n      },\n      \"xBucketNumber\": null,\n      \"xBucketSize\": null,\n      \"yAxis\": {\n        \"decimals\": 0,\n        \"format\": \"s\",\n        \"logBase\": 1,\n        \"max\": null,\n        \"min\": null,\n        \"show\": true,\n        \"splitFactor\": null\n      },\n      \"yBucketBound\": \"auto\",\n      \"yBucketNumber\": null,\n      \"yBucketSize\": null\n    },\n    {\n      \"cards\": {\n        \"cardPadding\": null,\n        \"cardRound\": null\n      },\n      \"color\": {\n        \"cardColor\": \"#b4ff00\",\n        \"colorScale\": \"linear\",\n        \"colorScheme\": \"interpolateOranges\",\n        \"exponent\": 0.5,\n        \"mode\": \"opacity\"\n      },\n      \"dataFormat\": \"tsbuckets\",\n      \"datasource\": \"$datasource\",\n      \"description\": \"\",\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 4,\n        \"x\": 4,\n        \"y\": 25\n      },\n      \"heatmap\": {},\n      \"hideZeroBuckets\": true,\n      \"highlightCards\": true,\n      \"id\": 48,\n      \"legend\": {\n        \"show\": true\n      },\n      \"maxDataPoints\": 50,\n      \"reverseYBuckets\": false,\n      \"targets\": [\n        {\n          \"exemplar\": true,\n          \"expr\": \"sum(rate(claircore_indexer_layerscanned_duration_seconds_bucket[5m])) by (le)\",\n          \"format\": \"heatmap\",\n          \"hide\": false,\n          \"interval\": \"\",\n          \"legendFormat\": \"{{ le }}\",\n          \"refId\": \"B\"\n        }\n      ],\n      \"title\": \"Layer scanned query duration\",\n      \"tooltip\": {\n        \"show\": true,\n        \"showHistogram\": false\n      },\n      \"transformations\": [\n        {\n          \"id\": \"groupBy\",\n          \"options\": {\n            \"fields\": {\n              \"IndexReport\": {\n                \"aggregations\": [\n                  \"lastNotNull\"\n                ],\n                \"operation\": null\n              },\n              \"LayerScanned\": {\n                \"aggregations\": [],\n                \"operation\": null\n              },\n              \"ManifestScanned\": {\n                \"aggregations\": [],\n                \"operation\": null\n              },\n              \"PersistManifest\": {\n                \"aggregations\": [],\n                \"operation\": null\n              },\n              \"RegisterScanners\": {\n                \"aggregations\": [],\n                \"operation\": null\n              },\n              \"SetIndexReport\": {\n                \"aggregations\": [],\n                \"operation\": null\n              },\n              \"Time\": {\n                \"aggregations\": [\n                  \"sum\"\n                ],\n                \"operation\": null\n              }\n            }\n          }\n        }\n      ],\n      \"type\": \"heatmap\",\n      \"xAxis\": {\n        \"show\": true\n      },\n      \"xBucketNumber\": null,\n      \"xBucketSize\": null,\n      \"yAxis\": {\n        \"decimals\": 0,\n        \"format\": \"s\",\n        \"logBase\": 1,\n        \"max\": null,\n        \"min\": null,\n        \"show\": true,\n        \"splitFactor\": null\n      },\n      \"yBucketBound\": \"auto\",\n      \"yBucketNumber\": null,\n      \"yBucketSize\": null\n    },\n    {\n      \"cards\": {\n        \"cardPadding\": null,\n        \"cardRound\": null\n      },\n      \"color\": {\n        \"cardColor\": \"#b4ff00\",\n        \"colorScale\": \"linear\",\n        \"colorScheme\": \"interpolateOranges\",\n        \"exponent\": 0.5,\n        \"mode\": \"opacity\"\n      },\n      \"dataFormat\": \"tsbuckets\",\n      \"datasource\": \"$datasource\",\n      \"description\": \"\",\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 4,\n        \"x\": 8,\n        \"y\": 25\n      },\n      \"heatmap\": {},\n      \"hideZeroBuckets\": true,\n      \"highlightCards\": true,\n      \"id\": 51,\n      \"legend\": {\n        \"show\": true\n      },\n      \"maxDataPoints\": 50,\n      \"reverseYBuckets\": false,\n      \"targets\": [\n        {\n          \"exemplar\": true,\n          \"expr\": \"sum(rate(claircore_indexer_persistmanifest_duration_seconds_bucket[$rate])) by (le)\",\n          \"format\": \"heatmap\",\n          \"hide\": false,\n          \"interval\": \"\",\n          \"legendFormat\": \"{{ le }}\",\n          \"refId\": \"D\"\n        }\n      ],\n      \"title\": \"Persist manifest query duration\",\n      \"tooltip\": {\n        \"show\": true,\n        \"showHistogram\": false\n      },\n      \"transformations\": [\n        {\n          \"id\": \"groupBy\",\n          \"options\": {\n            \"fields\": {\n              \"IndexReport\": {\n                \"aggregations\": [\n                  \"lastNotNull\"\n                ],\n                \"operation\": null\n              },\n              \"LayerScanned\": {\n                \"aggregations\": [],\n                \"operation\": null\n              },\n              \"ManifestScanned\": {\n                \"aggregations\": [],\n                \"operation\": null\n              },\n              \"PersistManifest\": {\n                \"aggregations\": [],\n                \"operation\": null\n              },\n              \"RegisterScanners\": {\n                \"aggregations\": [],\n                \"operation\": null\n              },\n              \"SetIndexReport\": {\n                \"aggregations\": [],\n                \"operation\": null\n              },\n              \"Time\": {\n                \"aggregations\": [\n                  \"sum\"\n                ],\n                \"operation\": null\n              }\n            }\n          }\n        }\n      ],\n      \"type\": \"heatmap\",\n      \"xAxis\": {\n        \"show\": true\n      },\n      \"xBucketNumber\": null,\n      \"xBucketSize\": null,\n      \"yAxis\": {\n        \"decimals\": 0,\n        \"format\": \"s\",\n        \"logBase\": 1,\n        \"max\": null,\n        \"min\": null,\n        \"show\": true,\n        \"splitFactor\": null\n      },\n      \"yBucketBound\": \"auto\",\n      \"yBucketNumber\": null,\n      \"yBucketSize\": null\n    },\n    {\n      \"cards\": {\n        \"cardPadding\": null,\n        \"cardRound\": null\n      },\n      \"color\": {\n        \"cardColor\": \"#b4ff00\",\n        \"colorScale\": \"linear\",\n        \"colorScheme\": \"interpolateOranges\",\n        \"exponent\": 0.5,\n        \"mode\": \"opacity\"\n      },\n      \"dataFormat\": \"tsbuckets\",\n      \"datasource\": \"$datasource\",\n      \"description\": \"\",\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 4,\n        \"x\": 12,\n        \"y\": 25\n      },\n      \"heatmap\": {},\n      \"hideZeroBuckets\": true,\n      \"highlightCards\": true,\n      \"id\": 52,\n      \"legend\": {\n        \"show\": true\n      },\n      \"maxDataPoints\": 50,\n      \"reverseYBuckets\": false,\n      \"targets\": [\n        {\n          \"exemplar\": true,\n          \"expr\": \"sum(rate(claircore_indexer_manifestscanned_duration_seconds_bucket[5m])) by (le)\",\n          \"format\": \"heatmap\",\n          \"hide\": false,\n          \"interval\": \"\",\n          \"legendFormat\": \"{{ le }}\",\n          \"refId\": \"C\"\n        }\n      ],\n      \"title\": \"Manifest scanned query duration\",\n      \"tooltip\": {\n        \"show\": true,\n        \"showHistogram\": false\n      },\n      \"transformations\": [\n        {\n          \"id\": \"groupBy\",\n          \"options\": {\n            \"fields\": {\n              \"IndexReport\": {\n                \"aggregations\": [\n                  \"lastNotNull\"\n                ],\n                \"operation\": null\n              },\n              \"LayerScanned\": {\n                \"aggregations\": [],\n                \"operation\": null\n              },\n              \"ManifestScanned\": {\n                \"aggregations\": [],\n                \"operation\": null\n              },\n              \"PersistManifest\": {\n                \"aggregations\": [],\n                \"operation\": null\n              },\n              \"RegisterScanners\": {\n                \"aggregations\": [],\n                \"operation\": null\n              },\n              \"SetIndexReport\": {\n                \"aggregations\": [],\n                \"operation\": null\n              },\n              \"Time\": {\n                \"aggregations\": [\n                  \"sum\"\n                ],\n                \"operation\": null\n              }\n            }\n          }\n        }\n      ],\n      \"type\": \"heatmap\",\n      \"xAxis\": {\n        \"show\": true\n      },\n      \"xBucketNumber\": null,\n      \"xBucketSize\": null,\n      \"yAxis\": {\n        \"decimals\": 0,\n        \"format\": \"s\",\n        \"logBase\": 1,\n        \"max\": null,\n        \"min\": null,\n        \"show\": true,\n        \"splitFactor\": null\n      },\n      \"yBucketBound\": \"auto\",\n      \"yBucketNumber\": null,\n      \"yBucketSize\": null\n    },\n    {\n      \"cards\": {\n        \"cardPadding\": null,\n        \"cardRound\": null\n      },\n      \"color\": {\n        \"cardColor\": \"#b4ff00\",\n        \"colorScale\": \"linear\",\n        \"colorScheme\": \"interpolateOranges\",\n        \"exponent\": 0.5,\n        \"mode\": \"opacity\"\n      },\n      \"dataFormat\": \"tsbuckets\",\n      \"datasource\": \"$datasource\",\n      \"description\": \"\",\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 4,\n        \"x\": 16,\n        \"y\": 25\n      },\n      \"heatmap\": {},\n      \"hideZeroBuckets\": true,\n      \"highlightCards\": true,\n      \"id\": 53,\n      \"legend\": {\n        \"show\": true\n      },\n      \"maxDataPoints\": 50,\n      \"reverseYBuckets\": false,\n      \"targets\": [\n        {\n          \"exemplar\": true,\n          \"expr\": \"sum(rate(claircore_indexer_registerscanners_duration_seconds_bucket[5m])) by (le)\",\n          \"format\": \"heatmap\",\n          \"hide\": false,\n          \"interval\": \"\",\n          \"legendFormat\": \"{{ le }}\",\n          \"refId\": \"E\"\n        }\n      ],\n      \"title\": \"Register scanners query duration\",\n      \"tooltip\": {\n        \"show\": true,\n        \"showHistogram\": false\n      },\n      \"transformations\": [\n        {\n          \"id\": \"groupBy\",\n          \"options\": {\n            \"fields\": {\n              \"IndexReport\": {\n                \"aggregations\": [\n                  \"lastNotNull\"\n                ],\n                \"operation\": null\n              },\n              \"LayerScanned\": {\n                \"aggregations\": [],\n                \"operation\": null\n              },\n              \"ManifestScanned\": {\n                \"aggregations\": [],\n                \"operation\": null\n              },\n              \"PersistManifest\": {\n                \"aggregations\": [],\n                \"operation\": null\n              },\n              \"RegisterScanners\": {\n                \"aggregations\": [],\n                \"operation\": null\n              },\n              \"SetIndexReport\": {\n                \"aggregations\": [],\n                \"operation\": null\n              },\n              \"Time\": {\n                \"aggregations\": [\n                  \"sum\"\n                ],\n                \"operation\": null\n              }\n            }\n          }\n        }\n      ],\n      \"type\": \"heatmap\",\n      \"xAxis\": {\n        \"show\": true\n      },\n      \"xBucketNumber\": null,\n      \"xBucketSize\": null,\n      \"yAxis\": {\n        \"decimals\": 0,\n        \"format\": \"s\",\n        \"logBase\": 1,\n        \"max\": null,\n        \"min\": null,\n        \"show\": true,\n        \"splitFactor\": null\n      },\n      \"yBucketBound\": \"auto\",\n      \"yBucketNumber\": null,\n      \"yBucketSize\": null\n    },\n    {\n      \"cards\": {\n        \"cardPadding\": null,\n        \"cardRound\": null\n      },\n      \"color\": {\n        \"cardColor\": \"#b4ff00\",\n        \"colorScale\": \"linear\",\n        \"colorScheme\": \"interpolateOranges\",\n        \"exponent\": 0.5,\n        \"mode\": \"opacity\"\n      },\n      \"dataFormat\": \"tsbuckets\",\n      \"datasource\": \"$datasource\",\n      \"description\": \"\",\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 4,\n        \"x\": 20,\n        \"y\": 25\n      },\n      \"heatmap\": {},\n      \"hideZeroBuckets\": true,\n      \"highlightCards\": true,\n      \"id\": 54,\n      \"legend\": {\n        \"show\": true\n      },\n      \"maxDataPoints\": 50,\n      \"reverseYBuckets\": false,\n      \"targets\": [\n        {\n          \"exemplar\": true,\n          \"expr\": \"sum(rate(claircore_indexer_setindexfinished_duration_seconds_bucket[5m])) by (le)\",\n          \"format\": \"heatmap\",\n          \"hide\": false,\n          \"interval\": \"\",\n          \"legendFormat\": \"{{ le }}\",\n          \"refId\": \"F\"\n        }\n      ],\n      \"title\": \"Set index finished query duration\",\n      \"tooltip\": {\n        \"show\": true,\n        \"showHistogram\": false\n      },\n      \"transformations\": [\n        {\n          \"id\": \"groupBy\",\n          \"options\": {\n            \"fields\": {\n              \"IndexReport\": {\n                \"aggregations\": [\n                  \"lastNotNull\"\n                ],\n                \"operation\": null\n              },\n              \"LayerScanned\": {\n                \"aggregations\": [],\n                \"operation\": null\n              },\n              \"ManifestScanned\": {\n                \"aggregations\": [],\n                \"operation\": null\n              },\n              \"PersistManifest\": {\n                \"aggregations\": [],\n                \"operation\": null\n              },\n              \"RegisterScanners\": {\n                \"aggregations\": [],\n                \"operation\": null\n              },\n              \"SetIndexReport\": {\n                \"aggregations\": [],\n                \"operation\": null\n              },\n              \"Time\": {\n                \"aggregations\": [\n                  \"sum\"\n                ],\n                \"operation\": null\n              }\n            }\n          }\n        }\n      ],\n      \"type\": \"heatmap\",\n      \"xAxis\": {\n        \"show\": true\n      },\n      \"xBucketNumber\": null,\n      \"xBucketSize\": null,\n      \"yAxis\": {\n        \"decimals\": 0,\n        \"format\": \"s\",\n        \"logBase\": 1,\n        \"max\": null,\n        \"min\": null,\n        \"show\": true,\n        \"splitFactor\": null\n      },\n      \"yBucketBound\": \"auto\",\n      \"yBucketNumber\": null,\n      \"yBucketSize\": null\n    },\n    {\n      \"datasource\": \"$datasource\",\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 0,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"lineInterpolation\": \"linear\",\n            \"lineWidth\": 1,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 24,\n        \"x\": 0,\n        \"y\": 33\n      },\n      \"id\": 63,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\"\n        },\n        \"tooltip\": {\n          \"mode\": \"single\"\n        }\n      },\n      \"targets\": [\n        {\n          \"exemplar\": true,\n          \"expr\": \"sum by (application_name) (pgxpool_idle_conns{application_name=\\\"libindex\\\"})\",\n          \"interval\": \"\",\n          \"legendFormat\": \"idle\",\n          \"refId\": \"A\"\n        },\n        {\n          \"exemplar\": true,\n          \"expr\": \"sum by (application_name) (pgxpool_max_conns{application_name=\\\"libindex\\\"})\",\n          \"hide\": false,\n          \"interval\": \"\",\n          \"legendFormat\": \"max\",\n          \"refId\": \"B\"\n        },\n        {\n          \"exemplar\": true,\n          \"expr\": \"sum by (application_name) (pgxpool_total_conns{application_name=\\\"libindex\\\"})\",\n          \"hide\": false,\n          \"interval\": \"\",\n          \"legendFormat\": \"total\",\n          \"refId\": \"C\"\n        },\n        {\n          \"exemplar\": true,\n          \"expr\": \"sum by (application_name) (pgxpool_acquired_conns{application_name=\\\"libindex\\\"})\",\n          \"hide\": false,\n          \"interval\": \"\",\n          \"legendFormat\": \"acquired\",\n          \"refId\": \"D\"\n        }\n      ],\n      \"title\": \"Connections (Indexer)\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"collapsed\": false,\n      \"datasource\": null,\n      \"gridPos\": {\n        \"h\": 1,\n        \"w\": 24,\n        \"x\": 0,\n        \"y\": 41\n      },\n      \"id\": 50,\n      \"panels\": [],\n      \"title\": \"Database matcher\",\n      \"type\": \"row\"\n    },\n    {\n      \"datasource\": \"$datasource\",\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 0,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"lineInterpolation\": \"linear\",\n            \"lineWidth\": 1,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 7,\n        \"w\": 24,\n        \"x\": 0,\n        \"y\": 42\n      },\n      \"id\": 21,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\"\n        },\n        \"tooltip\": {\n          \"mode\": \"single\"\n        }\n      },\n      \"targets\": [\n        {\n          \"exemplar\": true,\n          \"expr\": \"rate(claircore_vulnstore_getvulnerabilities_total[$rate])\",\n          \"interval\": \"\",\n          \"legendFormat\": \"VulnStoreGet: {{instance }}: {{ query }}\",\n          \"refId\": \"A\"\n        },\n        {\n          \"exemplar\": true,\n          \"expr\": \"rate(claircore_vulnstore_getlatestrefs_total[$rate])\",\n          \"hide\": false,\n          \"interval\": \"\",\n          \"legendFormat\": \"GetLatestRefs: {{instance }}: {{ query }}\",\n          \"refId\": \"B\"\n        },\n        {\n          \"exemplar\": true,\n          \"expr\": \"rate(claircore_vulnstore_getlatestupdateref_total[$rate])\",\n          \"hide\": false,\n          \"interval\": \"\",\n          \"legendFormat\": \"GetLatestUpdateRef: {{instance }}: {{ query }}\",\n          \"refId\": \"C\"\n        },\n        {\n          \"exemplar\": true,\n          \"expr\": \"rate(claircore_vulnstore_getupdateoperations_total[$rate])\",\n          \"hide\": false,\n          \"interval\": \"\",\n          \"legendFormat\": \"GetUpdateOperations: {{instance }}: {{ query }}\",\n          \"refId\": \"D\"\n        },\n        {\n          \"exemplar\": true,\n          \"expr\": \"rate(claircore_vulnstore_updatevulnerabilities_total[$rate])\",\n          \"hide\": false,\n          \"interval\": \"\",\n          \"legendFormat\": \"UpdateVulnerabilities: {{instance }}: {{ query }}\",\n          \"refId\": \"E\"\n        }\n      ],\n      \"title\": \"Database query count (matcher)\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"cards\": {\n        \"cardPadding\": null,\n        \"cardRound\": null\n      },\n      \"color\": {\n        \"cardColor\": \"#b4ff00\",\n        \"colorScale\": \"linear\",\n        \"colorScheme\": \"interpolateOranges\",\n        \"exponent\": 0.5,\n        \"mode\": \"opacity\"\n      },\n      \"dataFormat\": \"tsbuckets\",\n      \"datasource\": \"$datasource\",\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 4,\n        \"x\": 0,\n        \"y\": 49\n      },\n      \"heatmap\": {},\n      \"hideZeroBuckets\": true,\n      \"highlightCards\": true,\n      \"id\": 33,\n      \"legend\": {\n        \"show\": false\n      },\n      \"maxDataPoints\": 50,\n      \"reverseYBuckets\": false,\n      \"targets\": [\n        {\n          \"exemplar\": true,\n          \"expr\": \"sum(rate(claircore_vulnstore_getvulnerabilities_duration_seconds_bucket[$rate])) by (le)\",\n          \"format\": \"heatmap\",\n          \"hide\": false,\n          \"interval\": \"\",\n          \"legendFormat\": \"{{ le }}\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Get vulnerabilities latency\",\n      \"tooltip\": {\n        \"show\": true,\n        \"showHistogram\": false\n      },\n      \"type\": \"heatmap\",\n      \"xAxis\": {\n        \"show\": true\n      },\n      \"xBucketNumber\": null,\n      \"xBucketSize\": null,\n      \"yAxis\": {\n        \"decimals\": 0,\n        \"format\": \"s\",\n        \"logBase\": 1,\n        \"max\": null,\n        \"min\": null,\n        \"show\": true,\n        \"splitFactor\": null\n      },\n      \"yBucketBound\": \"auto\",\n      \"yBucketNumber\": null,\n      \"yBucketSize\": null\n    },\n    {\n      \"cards\": {\n        \"cardPadding\": null,\n        \"cardRound\": null\n      },\n      \"color\": {\n        \"cardColor\": \"#b4ff00\",\n        \"colorScale\": \"linear\",\n        \"colorScheme\": \"interpolateOranges\",\n        \"exponent\": 0.5,\n        \"mode\": \"opacity\"\n      },\n      \"dataFormat\": \"tsbuckets\",\n      \"datasource\": \"$datasource\",\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 4,\n        \"x\": 4,\n        \"y\": 49\n      },\n      \"heatmap\": {},\n      \"hideZeroBuckets\": true,\n      \"highlightCards\": true,\n      \"id\": 55,\n      \"legend\": {\n        \"show\": false\n      },\n      \"maxDataPoints\": 50,\n      \"reverseYBuckets\": false,\n      \"targets\": [\n        {\n          \"exemplar\": true,\n          \"expr\": \"sum(rate(claircore_vulnstore_getlatestrefs_duration_seconds_bucket[$rate])) by (le)\",\n          \"format\": \"heatmap\",\n          \"hide\": false,\n          \"interval\": \"\",\n          \"legendFormat\": \"{{ le }}\",\n          \"refId\": \"B\"\n        }\n      ],\n      \"title\": \"Get latest refs latency\",\n      \"tooltip\": {\n        \"show\": true,\n        \"showHistogram\": false\n      },\n      \"type\": \"heatmap\",\n      \"xAxis\": {\n        \"show\": true\n      },\n      \"xBucketNumber\": null,\n      \"xBucketSize\": null,\n      \"yAxis\": {\n        \"decimals\": 0,\n        \"format\": \"s\",\n        \"logBase\": 1,\n        \"max\": null,\n        \"min\": null,\n        \"show\": true,\n        \"splitFactor\": null\n      },\n      \"yBucketBound\": \"auto\",\n      \"yBucketNumber\": null,\n      \"yBucketSize\": null\n    },\n    {\n      \"cards\": {\n        \"cardPadding\": null,\n        \"cardRound\": null\n      },\n      \"color\": {\n        \"cardColor\": \"#b4ff00\",\n        \"colorScale\": \"linear\",\n        \"colorScheme\": \"interpolateOranges\",\n        \"exponent\": 0.5,\n        \"mode\": \"opacity\"\n      },\n      \"dataFormat\": \"tsbuckets\",\n      \"datasource\": \"$datasource\",\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 4,\n        \"x\": 8,\n        \"y\": 49\n      },\n      \"heatmap\": {},\n      \"hideZeroBuckets\": true,\n      \"highlightCards\": true,\n      \"id\": 56,\n      \"legend\": {\n        \"show\": false\n      },\n      \"maxDataPoints\": 50,\n      \"reverseYBuckets\": false,\n      \"targets\": [\n        {\n          \"exemplar\": true,\n          \"expr\": \"sum(rate(claircore_vulnstore_getlatestupdateref_duration_seconds_bucket[$rate])) by (le)\",\n          \"format\": \"heatmap\",\n          \"hide\": false,\n          \"interval\": \"\",\n          \"legendFormat\": \"{{ le }}\",\n          \"refId\": \"C\"\n        }\n      ],\n      \"title\": \"Get latest update refs latency\",\n      \"tooltip\": {\n        \"show\": true,\n        \"showHistogram\": false\n      },\n      \"type\": \"heatmap\",\n      \"xAxis\": {\n        \"show\": true\n      },\n      \"xBucketNumber\": null,\n      \"xBucketSize\": null,\n      \"yAxis\": {\n        \"decimals\": 0,\n        \"format\": \"s\",\n        \"logBase\": 1,\n        \"max\": null,\n        \"min\": null,\n        \"show\": true,\n        \"splitFactor\": null\n      },\n      \"yBucketBound\": \"auto\",\n      \"yBucketNumber\": null,\n      \"yBucketSize\": null\n    },\n    {\n      \"cards\": {\n        \"cardPadding\": null,\n        \"cardRound\": null\n      },\n      \"color\": {\n        \"cardColor\": \"#b4ff00\",\n        \"colorScale\": \"linear\",\n        \"colorScheme\": \"interpolateOranges\",\n        \"exponent\": 0.5,\n        \"mode\": \"opacity\"\n      },\n      \"dataFormat\": \"tsbuckets\",\n      \"datasource\": \"$datasource\",\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 4,\n        \"x\": 12,\n        \"y\": 49\n      },\n      \"heatmap\": {},\n      \"hideZeroBuckets\": true,\n      \"highlightCards\": true,\n      \"id\": 57,\n      \"legend\": {\n        \"show\": false\n      },\n      \"maxDataPoints\": 50,\n      \"reverseYBuckets\": false,\n      \"targets\": [\n        {\n          \"exemplar\": true,\n          \"expr\": \"sum(rate(claircore_vulnstore_getupdateoperations_duration_seconds_bucket[$rate])) by (le)\",\n          \"format\": \"heatmap\",\n          \"hide\": false,\n          \"interval\": \"\",\n          \"legendFormat\": \"{{ le }}\",\n          \"refId\": \"D\"\n        }\n      ],\n      \"title\": \"Get update operations latency\",\n      \"tooltip\": {\n        \"show\": true,\n        \"showHistogram\": false\n      },\n      \"type\": \"heatmap\",\n      \"xAxis\": {\n        \"show\": true\n      },\n      \"xBucketNumber\": null,\n      \"xBucketSize\": null,\n      \"yAxis\": {\n        \"decimals\": 0,\n        \"format\": \"s\",\n        \"logBase\": 1,\n        \"max\": null,\n        \"min\": null,\n        \"show\": true,\n        \"splitFactor\": null\n      },\n      \"yBucketBound\": \"auto\",\n      \"yBucketNumber\": null,\n      \"yBucketSize\": null\n    },\n    {\n      \"cards\": {\n        \"cardPadding\": null,\n        \"cardRound\": null\n      },\n      \"color\": {\n        \"cardColor\": \"#b4ff00\",\n        \"colorScale\": \"linear\",\n        \"colorScheme\": \"interpolateOranges\",\n        \"exponent\": 0.5,\n        \"mode\": \"opacity\"\n      },\n      \"dataFormat\": \"tsbuckets\",\n      \"datasource\": \"$datasource\",\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 4,\n        \"x\": 16,\n        \"y\": 49\n      },\n      \"heatmap\": {},\n      \"hideZeroBuckets\": true,\n      \"highlightCards\": true,\n      \"id\": 58,\n      \"legend\": {\n        \"show\": false\n      },\n      \"maxDataPoints\": 50,\n      \"reverseYBuckets\": false,\n      \"targets\": [\n        {\n          \"exemplar\": true,\n          \"expr\": \"sum(rate(claircore_vulnstore_updatevulnerabilities_duration_seconds_bucket[$rate])) by (le)\",\n          \"format\": \"heatmap\",\n          \"hide\": false,\n          \"interval\": \"\",\n          \"legendFormat\": \"{{ le }}\",\n          \"refId\": \"E\"\n        }\n      ],\n      \"title\": \"Update vulnerabilities latency\",\n      \"tooltip\": {\n        \"show\": true,\n        \"showHistogram\": false\n      },\n      \"type\": \"heatmap\",\n      \"xAxis\": {\n        \"show\": true\n      },\n      \"xBucketNumber\": null,\n      \"xBucketSize\": null,\n      \"yAxis\": {\n        \"decimals\": 0,\n        \"format\": \"s\",\n        \"logBase\": 1,\n        \"max\": null,\n        \"min\": null,\n        \"show\": true,\n        \"splitFactor\": null\n      },\n      \"yBucketBound\": \"auto\",\n      \"yBucketNumber\": null,\n      \"yBucketSize\": null\n    },\n    {\n      \"datasource\": \"$datasource\",\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 0,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"lineInterpolation\": \"linear\",\n            \"lineWidth\": 1,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 7,\n        \"w\": 24,\n        \"x\": 0,\n        \"y\": 57\n      },\n      \"id\": 43,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\"\n        },\n        \"tooltip\": {\n          \"mode\": \"single\"\n        }\n      },\n      \"targets\": [\n        {\n          \"exemplar\": true,\n          \"expr\": \"rate(claircore_vulnstore_gc_total[$rate])\",\n          \"hide\": false,\n          \"interval\": \"\",\n          \"legendFormat\": \"GarbageCollection: {{instance }}: {{ query }}\",\n          \"refId\": \"E\"\n        }\n      ],\n      \"title\": \"Database query count GC\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"datasource\": \"$datasource\",\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 0,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"lineInterpolation\": \"linear\",\n            \"lineWidth\": 1,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 24,\n        \"x\": 0,\n        \"y\": 64\n      },\n      \"id\": 64,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\"\n        },\n        \"tooltip\": {\n          \"mode\": \"single\"\n        }\n      },\n      \"targets\": [\n        {\n          \"exemplar\": true,\n          \"expr\": \"sum by (application_name) (pgxpool_idle_conns{application_name=\\\"libvuln\\\"})\",\n          \"interval\": \"\",\n          \"legendFormat\": \"idle\",\n          \"refId\": \"A\"\n        },\n        {\n          \"exemplar\": true,\n          \"expr\": \"sum by (application_name) (pgxpool_max_conns{application_name=\\\"libvuln\\\"})\",\n          \"hide\": false,\n          \"interval\": \"\",\n          \"legendFormat\": \"max\",\n          \"refId\": \"B\"\n        },\n        {\n          \"exemplar\": true,\n          \"expr\": \"sum by (application_name) (pgxpool_total_conns{application_name=\\\"libvuln\\\"})\",\n          \"hide\": false,\n          \"interval\": \"\",\n          \"legendFormat\": \"total\",\n          \"refId\": \"C\"\n        },\n        {\n          \"exemplar\": true,\n          \"expr\": \"sum by (application_name) (pgxpool_acquired_conns{application_name=\\\"libvuln\\\"})\",\n          \"hide\": false,\n          \"interval\": \"\",\n          \"legendFormat\": \"acquired\",\n          \"refId\": \"D\"\n        }\n      ],\n      \"title\": \"Connections (Matcher)\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"cards\": {\n        \"cardPadding\": null,\n        \"cardRound\": null\n      },\n      \"color\": {\n        \"cardColor\": \"#b4ff00\",\n        \"colorScale\": \"linear\",\n        \"colorScheme\": \"interpolateOranges\",\n        \"exponent\": 0.5,\n        \"mode\": \"opacity\"\n      },\n      \"dataFormat\": \"tsbuckets\",\n      \"datasource\": \"$datasource\",\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 12,\n        \"x\": 0,\n        \"y\": 72\n      },\n      \"heatmap\": {},\n      \"hideZeroBuckets\": true,\n      \"highlightCards\": true,\n      \"id\": 44,\n      \"legend\": {\n        \"show\": false\n      },\n      \"maxDataPoints\": 50,\n      \"reverseYBuckets\": false,\n      \"targets\": [\n        {\n          \"exemplar\": true,\n          \"expr\": \"sum(rate(claircore_vulnstore_gc_duration_seconds_bucket{query=\\\"updateOps\\\"}[$rate])) by (le)\",\n          \"format\": \"heatmap\",\n          \"hide\": false,\n          \"interval\": \"\",\n          \"legendFormat\": \"{{ le }}\",\n          \"refId\": \"F\"\n        }\n      ],\n      \"title\": \"GC - updateOps latency\",\n      \"tooltip\": {\n        \"show\": true,\n        \"showHistogram\": false\n      },\n      \"type\": \"heatmap\",\n      \"xAxis\": {\n        \"show\": true\n      },\n      \"xBucketNumber\": null,\n      \"xBucketSize\": null,\n      \"yAxis\": {\n        \"decimals\": 0,\n        \"format\": \"s\",\n        \"logBase\": 1,\n        \"max\": null,\n        \"min\": null,\n        \"show\": true,\n        \"splitFactor\": null\n      },\n      \"yBucketBound\": \"auto\",\n      \"yBucketNumber\": null,\n      \"yBucketSize\": null\n    },\n    {\n      \"cards\": {\n        \"cardPadding\": null,\n        \"cardRound\": null\n      },\n      \"color\": {\n        \"cardColor\": \"#b4ff00\",\n        \"colorScale\": \"linear\",\n        \"colorScheme\": \"interpolateOranges\",\n        \"exponent\": 0.5,\n        \"mode\": \"opacity\"\n      },\n      \"dataFormat\": \"tsbuckets\",\n      \"datasource\": \"$datasource\",\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 12,\n        \"x\": 12,\n        \"y\": 72\n      },\n      \"heatmap\": {},\n      \"hideZeroBuckets\": true,\n      \"highlightCards\": true,\n      \"id\": 59,\n      \"legend\": {\n        \"show\": false\n      },\n      \"maxDataPoints\": 50,\n      \"reverseYBuckets\": false,\n      \"targets\": [\n        {\n          \"exemplar\": true,\n          \"expr\": \"sum(rate(claircore_vulnstore_gc_duration_seconds_bucket{query=\\\"deleteVulns\\\"}[$rate])) by (le)\",\n          \"format\": \"heatmap\",\n          \"hide\": false,\n          \"interval\": \"\",\n          \"legendFormat\": \"{{ le }}\",\n          \"refId\": \"F\"\n        }\n      ],\n      \"title\": \"GC - deleteVulns latency\",\n      \"tooltip\": {\n        \"show\": true,\n        \"showHistogram\": false\n      },\n      \"type\": \"heatmap\",\n      \"xAxis\": {\n        \"show\": true\n      },\n      \"xBucketNumber\": null,\n      \"xBucketSize\": null,\n      \"yAxis\": {\n        \"decimals\": 0,\n        \"format\": \"s\",\n        \"logBase\": 1,\n        \"max\": null,\n        \"min\": null,\n        \"show\": true,\n        \"splitFactor\": null\n      },\n      \"yBucketBound\": \"auto\",\n      \"yBucketNumber\": null,\n      \"yBucketSize\": null\n    },\n    {\n      \"collapsed\": true,\n      \"datasource\": null,\n      \"gridPos\": {\n        \"h\": 1,\n        \"w\": 24,\n        \"x\": 0,\n        \"y\": 80\n      },\n      \"id\": 61,\n      \"panels\": [\n        {\n          \"datasource\": \"$datasource\",\n          \"fieldConfig\": {\n            \"defaults\": {\n              \"color\": {\n                \"mode\": \"palette-classic\"\n              },\n              \"custom\": {\n                \"axisLabel\": \"\",\n                \"axisPlacement\": \"auto\",\n                \"barAlignment\": 0,\n                \"drawStyle\": \"line\",\n                \"fillOpacity\": 0,\n                \"gradientMode\": \"none\",\n                \"hideFrom\": {\n                  \"legend\": false,\n                  \"tooltip\": false,\n                  \"viz\": false\n                },\n                \"lineInterpolation\": \"linear\",\n                \"lineWidth\": 1,\n                \"pointSize\": 5,\n                \"scaleDistribution\": {\n                  \"type\": \"linear\"\n                },\n                \"showPoints\": \"auto\",\n                \"spanNulls\": false,\n                \"stacking\": {\n                  \"group\": \"A\",\n                  \"mode\": \"none\"\n                },\n                \"thresholdsStyle\": {\n                  \"mode\": \"off\"\n                }\n              },\n              \"mappings\": [],\n              \"thresholds\": {\n                \"mode\": \"absolute\",\n                \"steps\": [\n                  {\n                    \"color\": \"green\",\n                    \"value\": null\n                  },\n                  {\n                    \"color\": \"red\",\n                    \"value\": 80\n                  }\n                ]\n              }\n            },\n            \"overrides\": []\n          },\n          \"gridPos\": {\n            \"h\": 8,\n            \"w\": 12,\n            \"x\": 0,\n            \"y\": 20\n          },\n          \"id\": 36,\n          \"options\": {\n            \"legend\": {\n              \"calcs\": [],\n              \"displayMode\": \"list\",\n              \"placement\": \"bottom\"\n            },\n            \"tooltip\": {\n              \"mode\": \"single\"\n            }\n          },\n          \"targets\": [\n            {\n              \"exemplar\": true,\n              \"expr\": \"rate(clair_notifier_created_total[$rate])\",\n              \"interval\": \"\",\n              \"legendFormat\": \"CreatedTotal: {{instance }}: {{ query }}\",\n              \"refId\": \"A\"\n            },\n            {\n              \"exemplar\": true,\n              \"expr\": \"rate(clair_notifier_failed_total[$rate])\",\n              \"hide\": false,\n              \"interval\": \"\",\n              \"legendFormat\": \"NotifierFailed: {{instance }}: {{ query }}\",\n              \"refId\": \"B\"\n            },\n            {\n              \"exemplar\": true,\n              \"expr\": \"rate(clair_notifier_putreceipt_total[$rate])\",\n              \"hide\": false,\n              \"interval\": \"\",\n              \"legendFormat\": \"NotifierPutReceipt: {{instance }}: {{ query }}\",\n              \"refId\": \"C\"\n            },\n            {\n              \"exemplar\": true,\n              \"expr\": \"rate(clair_notifier_receiptbyuoid_total[$rate])\",\n              \"hide\": false,\n              \"interval\": \"\",\n              \"legendFormat\": \"NotifierReceiptByUOID: {{instance }}: {{ query }}\",\n              \"refId\": \"D\"\n            }\n          ],\n          \"title\": \"Database query count (notifier)\",\n          \"type\": \"timeseries\"\n        },\n        {\n          \"datasource\": \"$datasource\",\n          \"fieldConfig\": {\n            \"defaults\": {\n              \"color\": {\n                \"mode\": \"palette-classic\"\n              },\n              \"custom\": {\n                \"axisLabel\": \"\",\n                \"axisPlacement\": \"auto\",\n                \"barAlignment\": 0,\n                \"drawStyle\": \"line\",\n                \"fillOpacity\": 30,\n                \"gradientMode\": \"none\",\n                \"hideFrom\": {\n                  \"legend\": false,\n                  \"tooltip\": false,\n                  \"viz\": false\n                },\n                \"lineInterpolation\": \"linear\",\n                \"lineWidth\": 1,\n                \"pointSize\": 5,\n                \"scaleDistribution\": {\n                  \"type\": \"linear\"\n                },\n                \"showPoints\": \"auto\",\n                \"spanNulls\": false,\n                \"stacking\": {\n                  \"group\": \"A\",\n                  \"mode\": \"none\"\n                },\n                \"thresholdsStyle\": {\n                  \"mode\": \"off\"\n                }\n              },\n              \"mappings\": [],\n              \"thresholds\": {\n                \"mode\": \"absolute\",\n                \"steps\": [\n                  {\n                    \"color\": \"green\",\n                    \"value\": null\n                  },\n                  {\n                    \"color\": \"red\",\n                    \"value\": 80\n                  }\n                ]\n              }\n            },\n            \"overrides\": []\n          },\n          \"gridPos\": {\n            \"h\": 8,\n            \"w\": 12,\n            \"x\": 12,\n            \"y\": 20\n          },\n          \"id\": 38,\n          \"options\": {\n            \"legend\": {\n              \"calcs\": [],\n              \"displayMode\": \"list\",\n              \"placement\": \"bottom\"\n            },\n            \"tooltip\": {\n              \"mode\": \"single\"\n            }\n          },\n          \"targets\": [\n            {\n              \"exemplar\": true,\n              \"expr\": \"histogram_quantile($dbquantile, rate(clair_notifier_created_duration_seconds_bucket[$rate]))\",\n              \"interval\": \"\",\n              \"legendFormat\": \"CreatedTotal: {{instance }}: {{ query }}\",\n              \"refId\": \"A\"\n            },\n            {\n              \"exemplar\": true,\n              \"expr\": \"histogram_quantile($dbquantile, rate(clair_notifier_failed_duration_seconds_bucket[$rate]))\",\n              \"hide\": false,\n              \"interval\": \"\",\n              \"legendFormat\": \"NotifierFailed: {{instance }}: {{ query }}\",\n              \"refId\": \"B\"\n            },\n            {\n              \"exemplar\": true,\n              \"expr\": \"histogram_quantile($dbquantile, rate(clair_notifier_putreceipt_duration_seconds_bucket[$rate]))\",\n              \"hide\": false,\n              \"interval\": \"\",\n              \"legendFormat\": \"NotifierPutReceipt: {{instance }}: {{ query }}\",\n              \"refId\": \"C\"\n            },\n            {\n              \"exemplar\": true,\n              \"expr\": \"histogram_quantile($dbquantile, rate(clair_notifier_receiptbyuoid_duration_seconds_bucket[$rate]))\",\n              \"hide\": false,\n              \"interval\": \"\",\n              \"legendFormat\": \"NotifierReceiptByUOID: {{instance }}: {{ query }}\",\n              \"refId\": \"D\"\n            }\n          ],\n          \"title\": \"Database query duration (notifier) (p$dbquantile)\",\n          \"type\": \"timeseries\"\n        }\n      ],\n      \"title\": \"Database - Notifier\",\n      \"type\": \"row\"\n    },\n    {\n      \"collapsed\": false,\n      \"datasource\": \"$datasource\",\n      \"gridPos\": {\n        \"h\": 1,\n        \"w\": 24,\n        \"x\": 0,\n        \"y\": 81\n      },\n      \"id\": 2,\n      \"panels\": [],\n      \"title\": \"API Requests\",\n      \"type\": \"row\"\n    },\n    {\n      \"datasource\": \"$datasource\",\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 0,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"lineInterpolation\": \"linear\",\n            \"lineWidth\": 1,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 12,\n        \"x\": 0,\n        \"y\": 82\n      },\n      \"id\": 4,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\"\n        },\n        \"tooltip\": {\n          \"mode\": \"single\"\n        }\n      },\n      \"targets\": [\n        {\n          \"exemplar\": true,\n          \"expr\": \"sum by (code) (rate(clair_http_matcherv1_request_total{handler=\\\"/matcher/api/v1/vulnerability_report/\\\"}[$rate]))\",\n          \"instant\": false,\n          \"interval\": \"\",\n          \"legendFormat\": \"Status {{ code }} \",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Vulnerability Report Requests / s\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"cards\": {\n        \"cardPadding\": null,\n        \"cardRound\": null\n      },\n      \"color\": {\n        \"cardColor\": \"#b4ff00\",\n        \"colorScale\": \"linear\",\n        \"colorScheme\": \"interpolateOranges\",\n        \"exponent\": 0.5,\n        \"mode\": \"opacity\"\n      },\n      \"dataFormat\": \"tsbuckets\",\n      \"datasource\": \"$datasource\",\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 12,\n        \"x\": 12,\n        \"y\": 82\n      },\n      \"heatmap\": {},\n      \"hideZeroBuckets\": true,\n      \"highlightCards\": true,\n      \"id\": 15,\n      \"legend\": {\n        \"show\": true\n      },\n      \"maxDataPoints\": 50,\n      \"repeat\": null,\n      \"reverseYBuckets\": false,\n      \"targets\": [\n        {\n          \"exemplar\": true,\n          \"expr\": \"sum(rate(clair_http_matcherv1_request_duration_seconds_bucket{handler=\\\"/matcher/api/v1/vulnerability_report/\\\"}[5m])) by (le)\",\n          \"format\": \"heatmap\",\n          \"instant\": false,\n          \"interval\": \"\",\n          \"intervalFactor\": 1,\n          \"legendFormat\": \"{{ le }}\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Vulnerability Report Request Latency\",\n      \"tooltip\": {\n        \"show\": true,\n        \"showHistogram\": false\n      },\n      \"type\": \"heatmap\",\n      \"xAxis\": {\n        \"show\": true\n      },\n      \"xBucketNumber\": null,\n      \"xBucketSize\": null,\n      \"yAxis\": {\n        \"decimals\": null,\n        \"format\": \"s\",\n        \"logBase\": 1,\n        \"max\": null,\n        \"min\": null,\n        \"show\": true,\n        \"splitFactor\": null\n      },\n      \"yBucketBound\": \"middle\",\n      \"yBucketNumber\": null,\n      \"yBucketSize\": null\n    },\n    {\n      \"datasource\": \"$datasource\",\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 0,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"lineInterpolation\": \"linear\",\n            \"lineWidth\": 1,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 12,\n        \"x\": 0,\n        \"y\": 90\n      },\n      \"id\": 7,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\"\n        },\n        \"tooltip\": {\n          \"mode\": \"single\"\n        }\n      },\n      \"targets\": [\n        {\n          \"exemplar\": true,\n          \"expr\": \"sum by (code) (rate(clair_http_indexerv1_request_total{handler=\\\"/indexer/api/v1/index_report\\\", method=\\\"post\\\"}[$rate]))\",\n          \"instant\": false,\n          \"interval\": \"1\",\n          \"legendFormat\": \"Status {{ code }} \",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Create Index Report Requests / s\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"cards\": {\n        \"cardPadding\": null,\n        \"cardRound\": null\n      },\n      \"color\": {\n        \"cardColor\": \"#b4ff00\",\n        \"colorScale\": \"linear\",\n        \"colorScheme\": \"interpolateGreys\",\n        \"exponent\": 0.5,\n        \"mode\": \"opacity\"\n      },\n      \"dataFormat\": \"tsbuckets\",\n      \"datasource\": \"$datasource\",\n      \"description\": \"\",\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 12,\n        \"x\": 12,\n        \"y\": 90\n      },\n      \"heatmap\": {},\n      \"hideZeroBuckets\": true,\n      \"highlightCards\": true,\n      \"id\": 14,\n      \"legend\": {\n        \"show\": true\n      },\n      \"maxDataPoints\": 50,\n      \"reverseYBuckets\": false,\n      \"targets\": [\n        {\n          \"exemplar\": true,\n          \"expr\": \"sum(increase(clair_http_indexerv1_request_duration_seconds_bucket{method=\\\"post\\\", handler=\\\"/indexer/api/v1/index_report\\\", code=\\\"201\\\"} [5m])) by (le)\",\n          \"format\": \"heatmap\",\n          \"instant\": false,\n          \"interval\": \"1\",\n          \"legendFormat\": \"{{ le }}\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Create Index Report Request Latency\",\n      \"tooltip\": {\n        \"show\": true,\n        \"showHistogram\": false\n      },\n      \"type\": \"heatmap\",\n      \"xAxis\": {\n        \"show\": true\n      },\n      \"xBucketNumber\": null,\n      \"xBucketSize\": null,\n      \"yAxis\": {\n        \"decimals\": null,\n        \"format\": \"s\",\n        \"logBase\": 1,\n        \"max\": null,\n        \"min\": null,\n        \"show\": true,\n        \"splitFactor\": null\n      },\n      \"yBucketBound\": \"auto\",\n      \"yBucketNumber\": null,\n      \"yBucketSize\": null\n    },\n    {\n      \"datasource\": \"$datasource\",\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 0,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"lineInterpolation\": \"stepBefore\",\n            \"lineWidth\": 1,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 12,\n        \"x\": 0,\n        \"y\": 98\n      },\n      \"id\": 6,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\"\n        },\n        \"tooltip\": {\n          \"mode\": \"single\"\n        }\n      },\n      \"targets\": [\n        {\n          \"exemplar\": true,\n          \"expr\": \"sum by (code) (rate(clair_http_indexerv1_request_total{handler=\\\"/indexer/api/v1/index_state\\\", method=\\\"get\\\"}[$rate]))\",\n          \"instant\": false,\n          \"interval\": \"\",\n          \"legendFormat\": \"Status {{code}}\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Indexer State Requests / s\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"cards\": {\n        \"cardPadding\": null,\n        \"cardRound\": null\n      },\n      \"color\": {\n        \"cardColor\": \"#b4ff00\",\n        \"colorScale\": \"linear\",\n        \"colorScheme\": \"interpolateGreys\",\n        \"exponent\": 0.5,\n        \"mode\": \"opacity\"\n      },\n      \"dataFormat\": \"tsbuckets\",\n      \"datasource\": \"$datasource\",\n      \"description\": \"\",\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 12,\n        \"x\": 12,\n        \"y\": 98\n      },\n      \"heatmap\": {},\n      \"hideZeroBuckets\": true,\n      \"highlightCards\": true,\n      \"id\": 47,\n      \"legend\": {\n        \"show\": true\n      },\n      \"maxDataPoints\": 50,\n      \"reverseYBuckets\": false,\n      \"targets\": [\n        {\n          \"exemplar\": true,\n          \"expr\": \"sum(increase(clair_http_indexerv1_request_duration_seconds_bucket{method=\\\"get\\\", handler=\\\"/indexer/api/v1/index_state\\\"} [5m])) by (le)\",\n          \"format\": \"heatmap\",\n          \"instant\": false,\n          \"interval\": \"1\",\n          \"legendFormat\": \"{{ le }}\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Indexer State Request Latency\",\n      \"tooltip\": {\n        \"show\": true,\n        \"showHistogram\": false\n      },\n      \"type\": \"heatmap\",\n      \"xAxis\": {\n        \"show\": true\n      },\n      \"xBucketNumber\": null,\n      \"xBucketSize\": null,\n      \"yAxis\": {\n        \"decimals\": null,\n        \"format\": \"s\",\n        \"logBase\": 1,\n        \"max\": null,\n        \"min\": null,\n        \"show\": true,\n        \"splitFactor\": null\n      },\n      \"yBucketBound\": \"auto\",\n      \"yBucketNumber\": null,\n      \"yBucketSize\": null\n    },\n    {\n      \"datasource\": \"$datasource\",\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 0,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"lineInterpolation\": \"linear\",\n            \"lineWidth\": 1,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 12,\n        \"x\": 0,\n        \"y\": 106\n      },\n      \"id\": 5,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\"\n        },\n        \"tooltip\": {\n          \"mode\": \"single\"\n        }\n      },\n      \"targets\": [\n        {\n          \"exemplar\": true,\n          \"expr\": \"sum by (code) (rate(clair_http_indexerv1_request_total{method=\\\"get\\\", handler=\\\"/indexer/api/v1/index_report/:digest\\\"}[$rate]))\",\n          \"instant\": false,\n          \"interval\": \"1\",\n          \"legendFormat\": \"Status {{ code }} \",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Index Report Requests / s\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"cards\": {\n        \"cardPadding\": null,\n        \"cardRound\": null\n      },\n      \"color\": {\n        \"cardColor\": \"#b4ff00\",\n        \"colorScale\": \"linear\",\n        \"colorScheme\": \"interpolateGreys\",\n        \"exponent\": 0.5,\n        \"mode\": \"opacity\"\n      },\n      \"dataFormat\": \"tsbuckets\",\n      \"datasource\": \"$datasource\",\n      \"description\": \"\",\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 12,\n        \"x\": 12,\n        \"y\": 106\n      },\n      \"heatmap\": {},\n      \"hideZeroBuckets\": true,\n      \"highlightCards\": true,\n      \"id\": 46,\n      \"legend\": {\n        \"show\": true\n      },\n      \"maxDataPoints\": 50,\n      \"reverseYBuckets\": false,\n      \"targets\": [\n        {\n          \"exemplar\": true,\n          \"expr\": \"sum(increase(clair_http_indexerv1_request_duration_seconds_bucket{method=\\\"get\\\", handler=\\\"/indexer/api/v1/index_report/:digest\\\"} [5m])) by (le)\",\n          \"format\": \"heatmap\",\n          \"instant\": false,\n          \"interval\": \"1\",\n          \"legendFormat\": \"{{ le }}\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Index Report Request Latency\",\n      \"tooltip\": {\n        \"show\": true,\n        \"showHistogram\": false\n      },\n      \"type\": \"heatmap\",\n      \"xAxis\": {\n        \"show\": true\n      },\n      \"xBucketNumber\": null,\n      \"xBucketSize\": null,\n      \"yAxis\": {\n        \"decimals\": null,\n        \"format\": \"s\",\n        \"logBase\": 1,\n        \"max\": null,\n        \"min\": null,\n        \"show\": true,\n        \"splitFactor\": null\n      },\n      \"yBucketBound\": \"auto\",\n      \"yBucketNumber\": null,\n      \"yBucketSize\": null\n    },\n    {\n      \"datasource\": \"$datasource\",\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 0,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"lineInterpolation\": \"linear\",\n            \"lineWidth\": 1,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 12,\n        \"x\": 0,\n        \"y\": 114\n      },\n      \"id\": 66,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\"\n        },\n        \"tooltip\": {\n          \"mode\": \"single\"\n        }\n      },\n      \"targets\": [\n        {\n          \"exemplar\": true,\n          \"expr\": \"sum by (code) (rate(clair_http_indexerv1_request_total{method=\\\"delete\\\", handler=\\\"/indexer/api/v1/index_report\\\"}[$rate]))\",\n          \"hide\": false,\n          \"instant\": false,\n          \"interval\": \"1\",\n          \"legendFormat\": \"Status {{ code }} \",\n          \"refId\": \"B\"\n        }\n      ],\n      \"title\": \"Index Report Bulk Deletion Requests / s\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"cards\": {\n        \"cardPadding\": null,\n        \"cardRound\": null\n      },\n      \"color\": {\n        \"cardColor\": \"#b4ff00\",\n        \"colorScale\": \"linear\",\n        \"colorScheme\": \"interpolateGreys\",\n        \"exponent\": 0.5,\n        \"mode\": \"opacity\"\n      },\n      \"dataFormat\": \"tsbuckets\",\n      \"datasource\": \"$datasource\",\n      \"description\": \"\",\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 12,\n        \"x\": 12,\n        \"y\": 114\n      },\n      \"heatmap\": {},\n      \"hideZeroBuckets\": true,\n      \"highlightCards\": true,\n      \"id\": 67,\n      \"legend\": {\n        \"show\": true\n      },\n      \"maxDataPoints\": 50,\n      \"reverseYBuckets\": false,\n      \"targets\": [\n        {\n          \"exemplar\": true,\n          \"expr\": \"sum(increase(clair_http_indexerv1_request_duration_seconds_bucket{method=\\\"delete\\\", handler=\\\"/indexer/api/v1/index_report\\\"} [5m])) by (le)\",\n          \"format\": \"heatmap\",\n          \"instant\": false,\n          \"interval\": \"1\",\n          \"legendFormat\": \"{{ le }}\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Index Report Bulk Deletion Request Latency\",\n      \"tooltip\": {\n        \"show\": true,\n        \"showHistogram\": false\n      },\n      \"type\": \"heatmap\",\n      \"xAxis\": {\n        \"show\": true\n      },\n      \"xBucketNumber\": null,\n      \"xBucketSize\": null,\n      \"yAxis\": {\n        \"decimals\": null,\n        \"format\": \"s\",\n        \"logBase\": 1,\n        \"max\": null,\n        \"min\": null,\n        \"show\": true,\n        \"splitFactor\": null\n      },\n      \"yBucketBound\": \"auto\",\n      \"yBucketNumber\": null,\n      \"yBucketSize\": null\n    },\n    {\n      \"datasource\": \"$datasource\",\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 0,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"lineInterpolation\": \"linear\",\n            \"lineWidth\": 1,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 12,\n        \"x\": 0,\n        \"y\": 122\n      },\n      \"id\": 68,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\"\n        },\n        \"tooltip\": {\n          \"mode\": \"single\"\n        }\n      },\n      \"targets\": [\n        {\n          \"exemplar\": true,\n          \"expr\": \"sum by (code) (rate(clair_http_indexerv1_request_total{method=\\\"delete\\\", handler=\\\"/indexer/api/v1/index_report/:digest\\\"}[$rate]))\",\n          \"instant\": false,\n          \"interval\": \"1\",\n          \"legendFormat\": \"Status {{ code }} \",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Index Report Deletion Requests / s\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"cards\": {\n        \"cardPadding\": null,\n        \"cardRound\": null\n      },\n      \"color\": {\n        \"cardColor\": \"#b4ff00\",\n        \"colorScale\": \"linear\",\n        \"colorScheme\": \"interpolateGreys\",\n        \"exponent\": 0.5,\n        \"mode\": \"opacity\"\n      },\n      \"dataFormat\": \"tsbuckets\",\n      \"datasource\": \"$datasource\",\n      \"description\": \"\",\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 12,\n        \"x\": 12,\n        \"y\": 122\n      },\n      \"heatmap\": {},\n      \"hideZeroBuckets\": true,\n      \"highlightCards\": true,\n      \"id\": 69,\n      \"legend\": {\n        \"show\": true\n      },\n      \"maxDataPoints\": 50,\n      \"reverseYBuckets\": false,\n      \"targets\": [\n        {\n          \"exemplar\": true,\n          \"expr\": \"sum(increase(clair_http_indexerv1_request_duration_seconds_bucket{method=\\\"delete\\\", handler=\\\"/indexer/api/v1/index_report/:digest\\\"} [5m])) by (le)\",\n          \"format\": \"heatmap\",\n          \"instant\": false,\n          \"interval\": \"1\",\n          \"legendFormat\": \"{{ le }}\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Index Report Deletion Request Latency\",\n      \"tooltip\": {\n        \"show\": true,\n        \"showHistogram\": false\n      },\n      \"type\": \"heatmap\",\n      \"xAxis\": {\n        \"show\": true\n      },\n      \"xBucketNumber\": null,\n      \"xBucketSize\": null,\n      \"yAxis\": {\n        \"decimals\": null,\n        \"format\": \"s\",\n        \"logBase\": 1,\n        \"max\": null,\n        \"min\": null,\n        \"show\": true,\n        \"splitFactor\": null\n      },\n      \"yBucketBound\": \"auto\",\n      \"yBucketNumber\": null,\n      \"yBucketSize\": null\n    },\n    {\n      \"datasource\": \"$datasource\",\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 0,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"lineInterpolation\": \"linear\",\n            \"lineWidth\": 1,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 12,\n        \"x\": 0,\n        \"y\": 130\n      },\n      \"id\": 41,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\"\n        },\n        \"tooltip\": {\n          \"mode\": \"single\"\n        }\n      },\n      \"targets\": [\n        {\n          \"exemplar\": true,\n          \"expr\": \"sum by (code) (rate(clair_http_notifier_api_v1_notification_request_total{method=\\\"get\\\"}[$rate]))\",\n          \"instant\": false,\n          \"interval\": \"1\",\n          \"legendFormat\": \"Status {{ code }} \",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Get Notifications Requests\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"datasource\": \"$datasource\",\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisLabel\": \"Seconds\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 30,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"lineInterpolation\": \"linear\",\n            \"lineWidth\": 1,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 12,\n        \"x\": 12,\n        \"y\": 130\n      },\n      \"id\": 40,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\"\n        },\n        \"tooltip\": {\n          \"mode\": \"single\"\n        }\n      },\n      \"targets\": [\n        {\n          \"exemplar\": true,\n          \"expr\": \"histogram_quantile($apiquantile, rate(clair_http_notifier_api_v1_notification_request_duration_seconds_bucket[$rate]))\",\n          \"instant\": false,\n          \"interval\": \"1\",\n          \"legendFormat\": \"\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Get Notification Request Latency (p$apiquantile)\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"datasource\": \"$datasource\",\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 0,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"lineInterpolation\": \"linear\",\n            \"lineWidth\": 1,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 12,\n        \"x\": 0,\n        \"y\": 138\n      },\n      \"id\": 39,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\"\n        },\n        \"tooltip\": {\n          \"mode\": \"single\"\n        }\n      },\n      \"targets\": [\n        {\n          \"exemplar\": true,\n          \"expr\": \"sum by (code) (rate(clair_http_notifier_api_v1_notification_request_total{method=\\\"delete\\\"}[$rate]))\",\n          \"instant\": false,\n          \"interval\": \"1\",\n          \"legendFormat\": \"Status {{ code }}\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Delete Notification Requests\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"datasource\": \"$datasource\",\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisLabel\": \"Seconds\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 30,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"lineInterpolation\": \"linear\",\n            \"lineWidth\": 1,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 12,\n        \"x\": 12,\n        \"y\": 138\n      },\n      \"id\": 42,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\"\n        },\n        \"tooltip\": {\n          \"mode\": \"single\"\n        }\n      },\n      \"targets\": [\n        {\n          \"exemplar\": true,\n          \"expr\": \"histogram_quantile($apiquantile, rate(clair_http_notifier_api_v1_notification_request_duration_seconds_bucket[$rate]))\",\n          \"instant\": false,\n          \"interval\": \"1\",\n          \"legendFormat\": \"\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Delete Notification Request Latency (p$apiquantile)\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"collapsed\": false,\n      \"datasource\": \"$datasource\",\n      \"gridPos\": {\n        \"h\": 1,\n        \"w\": 24,\n        \"x\": 0,\n        \"y\": 146\n      },\n      \"id\": 9,\n      \"panels\": [],\n      \"title\": \"Memory metrics\",\n      \"type\": \"row\"\n    },\n    {\n      \"datasource\": \"$datasource\",\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 0,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"lineInterpolation\": \"linear\",\n            \"lineWidth\": 1,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 12,\n        \"x\": 0,\n        \"y\": 147\n      },\n      \"id\": 11,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\"\n        },\n        \"tooltip\": {\n          \"mode\": \"single\"\n        }\n      },\n      \"targets\": [\n        {\n          \"exemplar\": true,\n          \"expr\": \"sum by (job)(go_goroutines)\",\n          \"interval\": \"\",\n          \"legendFormat\": \"{{ job }}\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Goroutine count\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"datasource\": \"$datasource\",\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 10,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"lineInterpolation\": \"linear\",\n            \"lineWidth\": 1,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          },\n          \"unit\": \"bytes\"\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 12,\n        \"x\": 12,\n        \"y\": 147\n      },\n      \"id\": 12,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\"\n        },\n        \"tooltip\": {\n          \"mode\": \"single\"\n        }\n      },\n      \"targets\": [\n        {\n          \"exemplar\": true,\n          \"expr\": \"sum by (job)(go_memstats_heap_inuse_bytes)\",\n          \"interval\": \"\",\n          \"legendFormat\": \"{{ job }}\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Memory\",\n      \"type\": \"timeseries\"\n    }\n  ],\n  \"refresh\": false,\n  \"schemaVersion\": 30,\n  \"style\": \"dark\",\n  \"tags\": [],\n  \"templating\": {\n    \"list\": [\n      {\n        \"auto\": false,\n        \"auto_count\": 30,\n        \"auto_min\": \"10s\",\n        \"current\": {\n          \"selected\": false,\n          \"text\": \"1m\",\n          \"value\": \"1m\"\n        },\n        \"description\": null,\n        \"error\": null,\n        \"hide\": 0,\n        \"label\": null,\n        \"name\": \"rate\",\n        \"options\": [\n          {\n            \"selected\": true,\n            \"text\": \"1m\",\n            \"value\": \"1m\"\n          },\n          {\n            \"selected\": false,\n            \"text\": \"5m\",\n            \"value\": \"5m\"\n          },\n          {\n            \"selected\": false,\n            \"text\": \"10m\",\n            \"value\": \"10m\"\n          },\n          {\n            \"selected\": false,\n            \"text\": \"30m\",\n            \"value\": \"30m\"\n          },\n          {\n            \"selected\": false,\n            \"text\": \"1h\",\n            \"value\": \"1h\"\n          },\n          {\n            \"selected\": false,\n            \"text\": \"6h\",\n            \"value\": \"6h\"\n          },\n          {\n            \"selected\": false,\n            \"text\": \"12h\",\n            \"value\": \"12h\"\n          },\n          {\n            \"selected\": false,\n            \"text\": \"1d\",\n            \"value\": \"1d\"\n          },\n          {\n            \"selected\": false,\n            \"text\": \"7d\",\n            \"value\": \"7d\"\n          },\n          {\n            \"selected\": false,\n            \"text\": \"14d\",\n            \"value\": \"14d\"\n          },\n          {\n            \"selected\": false,\n            \"text\": \"30d\",\n            \"value\": \"30d\"\n          }\n        ],\n        \"query\": \"1m,5m,10m,30m,1h,6h,12h,1d,7d,14d,30d\",\n        \"refresh\": 2,\n        \"skipUrlSync\": false,\n        \"type\": \"interval\"\n      },\n      {\n        \"allValue\": null,\n        \"current\": {\n          \"selected\": false,\n          \"text\": \"0.95\",\n          \"value\": \"0.95\"\n        },\n        \"description\": null,\n        \"error\": null,\n        \"hide\": 0,\n        \"includeAll\": false,\n        \"label\": \"Database Latency Quantile\",\n        \"multi\": false,\n        \"name\": \"dbquantile\",\n        \"options\": [\n          {\n            \"selected\": true,\n            \"text\": \"0.95\",\n            \"value\": \"0.95\"\n          },\n          {\n            \"selected\": false,\n            \"text\": \"0.90\",\n            \"value\": \"0.90\"\n          },\n          {\n            \"selected\": false,\n            \"text\": \"0.5\",\n            \"value\": \"0.5\"\n          },\n          {\n            \"selected\": false,\n            \"text\": \"0.20\",\n            \"value\": \"0.20\"\n          },\n          {\n            \"selected\": false,\n            \"text\": \"0\",\n            \"value\": \"0\"\n          }\n        ],\n        \"query\": \"0.95,0.90,0.5,0.20,0\",\n        \"queryValue\": \"\",\n        \"skipUrlSync\": false,\n        \"type\": \"custom\"\n      },\n      {\n        \"allValue\": null,\n        \"current\": {\n          \"selected\": true,\n          \"text\": \"0.95\",\n          \"value\": \"0.95\"\n        },\n        \"description\": null,\n        \"error\": null,\n        \"hide\": 0,\n        \"includeAll\": false,\n        \"label\": \"API Latency Quantile\",\n        \"multi\": false,\n        \"name\": \"apiquantile\",\n        \"options\": [\n          {\n            \"selected\": true,\n            \"text\": \"0.95\",\n            \"value\": \"0.95\"\n          },\n          {\n            \"selected\": false,\n            \"text\": \"0.90\",\n            \"value\": \"0.90\"\n          },\n          {\n            \"selected\": false,\n            \"text\": \"0.5\",\n            \"value\": \"0.5\"\n          },\n          {\n            \"selected\": false,\n            \"text\": \"0.20\",\n            \"value\": \"0.20\"\n          },\n          {\n            \"selected\": false,\n            \"text\": \"0\",\n            \"value\": \"0\"\n          }\n        ],\n        \"query\": \"0.95,0.90,0.5,0.20,0\",\n        \"queryValue\": \"\",\n        \"skipUrlSync\": false,\n        \"type\": \"custom\"\n      },\n      {\n        \"current\": {\n          \"selected\": true,\n          \"text\": \"Prometheus\",\n          \"value\": \"Prometheus\"\n        },\n        \"description\": null,\n        \"error\": null,\n        \"hide\": 0,\n        \"includeAll\": false,\n        \"label\": null,\n        \"multi\": false,\n        \"name\": \"datasource\",\n        \"options\": [],\n        \"query\": \"prometheus\",\n        \"queryValue\": \"\",\n        \"refresh\": 1,\n        \"regex\": \"/(^clair|^app-sre-stage-01|^Prometheus$|^appsre)(?!.*cluster)/\",\n        \"skipUrlSync\": false,\n        \"type\": \"datasource\"\n      }\n    ]\n  },\n  \"time\": {\n    \"from\": \"now-1h\",\n    \"to\": \"now\"\n  },\n  \"timepicker\": {},\n  \"timezone\": \"\",\n  \"title\": \"Clair V4\",\n  \"uid\": \"I1JBFlRnz\",\n  \"version\": 4\n}\n"
  },
  {
    "path": "local-dev/grafana/provisioning/dashboards/dashboard.yml",
    "content": "apiVersion: 1\n\nproviders:\n- name: 'Prometheus'\n  orgId: 1\n  folder: ''\n  type: file\n  disableDeletion: false\n  editable: true\n  options:\n    path: /etc/grafana/provisioning/dashboards"
  },
  {
    "path": "local-dev/grafana/provisioning/dashboards/database.json",
    "content": "{\n    \"id\": null,\n    \"title\": \"PostgreSQL Performance Dashboard\",\n    \"uid\": \"postgres-performance-dashboard4\",\n    \"tags\": [\"postgres\", \"database\", \"performance\", \"new\"],\n    \"timezone\": \"browser\",\n    \"schemaVersion\": 30,\n    \"version\": 1,\n    \"refresh\": \"30s\",\n    \"time\": {\n      \"from\": \"now-1h\",\n      \"to\": \"now\"\n    },\n    \"templating\": {\n      \"list\": [\n        {\n          \"name\": \"datasource\",\n          \"type\": \"datasource\",\n          \"query\": \"prometheus\",\n          \"current\": {\n            \"selected\": false,\n            \"text\": \"Prometheus\",\n            \"value\": \"Prometheus\"\n          },\n          \"hide\": 0,\n          \"includeAll\": false,\n          \"multi\": false,\n          \"refresh\": 1\n        },\n        {\n          \"name\": \"instance\",\n          \"type\": \"query\",\n          \"query\": \"label_values(pg_up, instance)\",\n          \"current\": {\n            \"selected\": true,\n            \"text\": \"All\",\n            \"value\": \"$__all\"\n          },\n          \"hide\": 0,\n          \"includeAll\": true,\n          \"multi\": true,\n          \"refresh\": 1,\n          \"allValue\": \".*\",\n          \"datasource\": \"$datasource\"\n        }\n      ]\n    },\n    \"graphTooltip\": 2,\n    \"panels\": [\n      {\n        \"title\": \"Overview\",\n        \"type\": \"row\",\n        \"gridPos\": {\n          \"x\": 0,\n          \"y\": 0,\n          \"w\": 24,\n          \"h\": 1\n        },\n        \"id\": 100\n      },\n      {\n        \"title\": \"Database Status\",\n        \"type\": \"stat\",\n        \"gridPos\": {\n          \"x\": 0,\n          \"y\": 1,\n          \"w\": 4,\n          \"h\": 4\n        },\n        \"id\": 101,\n        \"targets\": [\n          {\n            \"expr\": \"pg_up{instance=~\\\"$instance\\\"}\",\n            \"legendFormat\": \"Database Status\",\n            \"refId\": \"A\"\n          }\n        ],\n        \"fieldConfig\": {\n          \"defaults\": {\n            \"color\": {\n              \"mode\": \"thresholds\"\n            },\n            \"thresholds\": {\n              \"steps\": [\n                {\n                  \"color\": \"red\",\n                  \"value\": 0\n                },\n                {\n                  \"color\": \"green\",\n                  \"value\": 1\n                }\n              ]\n            },\n            \"mappings\": [\n              {\n                \"options\": {\n                  \"0\": {\n                    \"text\": \"DOWN\"\n                  },\n                  \"1\": {\n                    \"text\": \"UP\"\n                  }\n                },\n                \"type\": \"value\"\n              }\n            ]\n          }\n        },\n        \"options\": {\n          \"reduceOptions\": {\n            \"values\": false,\n            \"calcs\": [\"lastNotNull\"],\n            \"fields\": \"\"\n          },\n          \"orientation\": \"auto\",\n          \"textMode\": \"auto\",\n          \"colorMode\": \"background\"\n        }\n      },\n      {\n        \"title\": \"Total Databases\",\n        \"type\": \"stat\",\n        \"gridPos\": {\n          \"x\": 4,\n          \"y\": 1,\n          \"w\": 4,\n          \"h\": 4\n        },\n        \"id\": 102,\n        \"targets\": [\n          {\n            \"expr\": \"count(pg_stat_database_numbackends{instance=~\\\"$instance\\\", datname!=\\\"template0\\\", datname!=\\\"template1\\\"})\",\n            \"legendFormat\": \"Databases\",\n            \"refId\": \"A\"\n          }\n        ],\n        \"fieldConfig\": {\n          \"defaults\": {\n            \"color\": {\n              \"mode\": \"palette-classic\"\n            }\n          }\n        }\n      },\n      {\n        \"title\": \"Total Connections\",\n        \"type\": \"stat\",\n        \"gridPos\": {\n          \"x\": 8,\n          \"y\": 1,\n          \"w\": 4,\n          \"h\": 4\n        },\n        \"id\": 103,\n        \"targets\": [\n          {\n            \"expr\": \"sum(pg_stat_database_numbackends{instance=~\\\"$instance\\\"})\",\n            \"legendFormat\": \"Connections\",\n            \"refId\": \"A\"\n          }\n        ],\n        \"fieldConfig\": {\n          \"defaults\": {\n            \"color\": {\n              \"mode\": \"palette-classic\"\n            }\n          }\n        }\n      },\n      {\n        \"title\": \"Queries Per Second\",\n        \"type\": \"stat\",\n        \"gridPos\": {\n          \"x\": 12,\n          \"y\": 1,\n          \"w\": 4,\n          \"h\": 4\n        },\n        \"id\": 104,\n        \"targets\": [\n          {\n            \"expr\": \"sum(rate(pg_stat_database_xact_commit{instance=~\\\"$instance\\\"}[5m]) + rate(pg_stat_database_xact_rollback{instance=~\\\"$instance\\\"}[5m]))\",\n            \"legendFormat\": \"QPS\",\n            \"refId\": \"A\"\n          }\n        ],\n                 \"fieldConfig\": {\n           \"defaults\": {\n             \"color\": {\n               \"mode\": \"palette-classic\"\n             },\n             \"unit\": \"ops\"\n           }\n         }\n      },\n      {\n        \"title\": \"Active Queries\",\n        \"type\": \"stat\",\n        \"gridPos\": {\n          \"x\": 16,\n          \"y\": 1,\n          \"w\": 8,\n          \"h\": 4\n        },\n        \"id\": 105,\n        \"targets\": [\n          {\n            \"expr\": \"sum by (datname) (pg_stat_activity_count{instance=~\\\"$instance\\\", state=\\\"active\\\"})\",\n            \"legendFormat\": \"{{datname}}\",\n            \"refId\": \"A\"\n          }\n        ],\n        \"fieldConfig\": {\n          \"defaults\": {\n            \"color\": {\n              \"mode\": \"thresholds\"\n            },\n            \"thresholds\": {\n              \"steps\": [\n                {\n                  \"color\": \"green\",\n                  \"value\": 0\n                },\n                {\n                  \"color\": \"yellow\",\n                  \"value\": 10\n                },\n                {\n                  \"color\": \"red\",\n                  \"value\": 25\n                }\n              ]\n            },\n            \"unit\": \"short\"\n          }\n        }\n      },\n      {\n        \"title\": \"Connections & Activity\",\n        \"type\": \"row\",\n        \"gridPos\": {\n          \"x\": 0,\n          \"y\": 5,\n          \"w\": 24,\n          \"h\": 1\n        },\n        \"id\": 200\n      },\n      {\n        \"title\": \"Active Connections by Database\",\n        \"type\": \"timeseries\",\n        \"gridPos\": {\n          \"x\": 0,\n          \"y\": 6,\n          \"w\": 12,\n          \"h\": 8\n        },\n        \"id\": 201,\n        \"targets\": [\n          {\n            \"expr\": \"pg_stat_database_numbackends{instance=~\\\"$instance\\\", datname!=\\\"template0\\\", datname!=\\\"template1\\\"}\",\n            \"legendFormat\": \"{{datname}}\",\n            \"refId\": \"A\"\n          }\n        ],\n        \"fieldConfig\": {\n          \"defaults\": {\n            \"custom\": {\n              \"drawStyle\": \"line\",\n              \"lineInterpolation\": \"linear\",\n              \"fillOpacity\": 10,\n              \"stacking\": {\n                \"mode\": \"none\"\n              }\n            }\n          }\n        }\n      },\n      {\n        \"title\": \"Connection States\",\n        \"type\": \"timeseries\",\n        \"gridPos\": {\n          \"x\": 12,\n          \"y\": 6,\n          \"w\": 12,\n          \"h\": 8\n        },\n        \"id\": 202,\n        \"targets\": [\n          {\n            \"expr\": \"sum by (state) (pg_stat_activity_count{instance=~\\\"$instance\\\"})\",\n            \"legendFormat\": \"{{state}}\",\n            \"refId\": \"A\"\n          }\n        ],\n        \"fieldConfig\": {\n          \"defaults\": {\n            \"custom\": {\n              \"drawStyle\": \"line\",\n              \"lineInterpolation\": \"linear\",\n              \"fillOpacity\": 10,\n              \"stacking\": {\n                \"mode\": \"normal\"\n              }\n            }\n          }\n        }\n      },\n      {\n        \"title\": \"Max Connections vs Current\",\n        \"type\": \"timeseries\",\n        \"gridPos\": {\n          \"x\": 0,\n          \"y\": 14,\n          \"w\": 24,\n          \"h\": 6\n        },\n        \"id\": 203,\n        \"targets\": [\n          {\n            \"expr\": \"pg_settings_max_connections{instance=~\\\"$instance\\\"}\",\n            \"legendFormat\": \"Max Connections\",\n            \"refId\": \"A\"\n          },\n          {\n            \"expr\": \"sum(pg_stat_database_numbackends{instance=~\\\"$instance\\\"})\",\n            \"legendFormat\": \"Current Connections\",\n            \"refId\": \"B\"\n          }\n        ]\n      },\n      \n       \n      {\n        \"title\": \"Transaction Activity\",\n        \"type\": \"row\",\n        \"gridPos\": {\n          \"x\": 0,\n          \"y\": 20,\n          \"w\": 24,\n          \"h\": 1\n        },\n        \"id\": 300\n      },\n      {\n        \"title\": \"Transactions per Second\",\n        \"type\": \"timeseries\",\n        \"gridPos\": {\n          \"x\": 0,\n          \"y\": 21,\n          \"w\": 12,\n          \"h\": 8\n        },\n        \"id\": 301,\n        \"targets\": [\n          {\n            \"expr\": \"sum(rate(pg_stat_database_xact_commit{instance=~\\\"$instance\\\"}[5m]))\",\n            \"legendFormat\": \"Commits/sec\",\n            \"refId\": \"A\"\n          },\n          {\n            \"expr\": \"sum(rate(pg_stat_database_xact_rollback{instance=~\\\"$instance\\\"}[5m]))\",\n            \"legendFormat\": \"Rollbacks/sec\",\n            \"refId\": \"B\"\n          }\n        ],\n        \"fieldConfig\": {\n          \"defaults\": {\n            \"unit\": \"reqps\"\n          }\n        }\n      },\n      {\n        \"title\": \"Transaction Activity by Database\",\n        \"type\": \"timeseries\",\n        \"gridPos\": {\n          \"x\": 12,\n          \"y\": 21,\n          \"w\": 12,\n          \"h\": 8\n        },\n        \"id\": 302,\n        \"targets\": [\n          {\n            \"expr\": \"rate(pg_stat_database_xact_commit{instance=~\\\"$instance\\\", datname!=\\\"template0\\\", datname!=\\\"template1\\\"}[5m])\",\n            \"legendFormat\": \"{{datname}} commits\",\n            \"refId\": \"A\"\n          },\n          {\n            \"expr\": \"rate(pg_stat_database_xact_rollback{instance=~\\\"$instance\\\", datname!=\\\"template0\\\", datname!=\\\"template1\\\"}[5m])\",\n            \"legendFormat\": \"{{datname}} rollbacks\",\n            \"refId\": \"B\"\n          }\n        ]\n      },\n      {\n        \"title\": \"Deadlocks\",\n        \"type\": \"timeseries\",\n        \"gridPos\": {\n          \"x\": 0,\n          \"y\": 29,\n          \"w\": 8,\n          \"h\": 6\n        },\n        \"id\": 303,\n        \"targets\": [\n          {\n            \"expr\": \"rate(pg_stat_database_deadlocks{instance=~\\\"$instance\\\"}[5m])\",\n            \"legendFormat\": \"{{datname}}\",\n            \"refId\": \"A\"\n          }\n        ]\n      },\n      {\n        \"title\": \"Conflicts\",\n        \"type\": \"timeseries\",\n        \"gridPos\": {\n          \"x\": 8,\n          \"y\": 29,\n          \"w\": 8,\n          \"h\": 6\n        },\n        \"id\": 304,\n        \"targets\": [\n          {\n            \"expr\": \"rate(pg_stat_database_conflicts{instance=~\\\"$instance\\\"}[5m])\",\n            \"legendFormat\": \"{{datname}}\",\n            \"refId\": \"A\"\n          }\n        ]\n      },\n      {\n        \"title\": \"Temporary Files\",\n        \"type\": \"timeseries\",\n        \"gridPos\": {\n          \"x\": 16,\n          \"y\": 29,\n          \"w\": 8,\n          \"h\": 6\n        },\n        \"id\": 305,\n        \"targets\": [\n          {\n            \"expr\": \"rate(pg_stat_database_temp_files{instance=~\\\"$instance\\\"}[5m])\",\n            \"legendFormat\": \"{{datname}} Files\",\n            \"refId\": \"A\"\n          },\n          {\n            \"expr\": \"rate(pg_stat_database_temp_bytes{instance=~\\\"$instance\\\"}[5m])\",\n            \"legendFormat\": \"{{datname}} Bytes\",\n            \"refId\": \"B\"\n          }\n        ]\n      },\n      {\n        \"title\": \"I/O & Performance\",\n        \"type\": \"row\",\n        \"gridPos\": {\n          \"x\": 0,\n          \"y\": 35,\n          \"w\": 24,\n          \"h\": 1\n        },\n        \"id\": 400\n      },\n      {\n        \"title\": \"Disk I/O - Blocks Read vs Hit\",\n        \"type\": \"timeseries\",\n        \"gridPos\": {\n          \"x\": 0,\n          \"y\": 36,\n          \"w\": 12,\n          \"h\": 8\n        },\n        \"id\": 401,\n        \"targets\": [\n          {\n            \"expr\": \"sum(rate(pg_stat_database_blks_read{instance=~\\\"$instance\\\"}[5m]))\",\n            \"legendFormat\": \"Blocks Read from Disk\",\n            \"refId\": \"A\"\n          },\n          {\n            \"expr\": \"sum(rate(pg_stat_database_blks_hit{instance=~\\\"$instance\\\"}[5m]))\",\n            \"legendFormat\": \"Blocks Hit from Cache\",\n            \"refId\": \"B\"\n          }\n        ]\n      },\n      {\n        \"title\": \"Buffer Cache Hit Ratio by Database\",\n        \"type\": \"timeseries\",\n        \"gridPos\": {\n          \"x\": 12,\n          \"y\": 36,\n          \"w\": 12,\n          \"h\": 8\n        },\n        \"id\": 402,\n        \"targets\": [\n          {\n            \"expr\": \"rate(pg_stat_database_blks_hit{instance=~\\\"$instance\\\", datname!=\\\"template0\\\", datname!=\\\"template1\\\"}[5m]) / (rate(pg_stat_database_blks_hit{instance=~\\\"$instance\\\", datname!=\\\"template0\\\", datname!=\\\"template1\\\"}[5m]) + rate(pg_stat_database_blks_read{instance=~\\\"$instance\\\", datname!=\\\"template0\\\", datname!=\\\"template1\\\"}[5m])) * 100\",\n            \"legendFormat\": \"{{datname}}\",\n            \"refId\": \"A\"\n          }\n        ],\n        \"fieldConfig\": {\n          \"defaults\": {\n            \"unit\": \"percent\",\n            \"min\": 0,\n            \"max\": 100\n          }\n        }\n      },\n      {\n        \"title\": \"Tuple Activity\",\n        \"type\": \"timeseries\",\n        \"gridPos\": {\n          \"x\": 0,\n          \"y\": 44,\n          \"w\": 12,\n          \"h\": 8\n        },\n        \"id\": 403,\n        \"targets\": [\n          {\n            \"expr\": \"sum(rate(pg_stat_database_tup_returned{instance=~\\\"$instance\\\"}[5m]))\",\n            \"legendFormat\": \"Rows Returned\",\n            \"refId\": \"A\"\n          },\n          {\n            \"expr\": \"sum(rate(pg_stat_database_tup_fetched{instance=~\\\"$instance\\\"}[5m]))\",\n            \"legendFormat\": \"Rows Fetched\",\n            \"refId\": \"B\"\n          },\n          {\n            \"expr\": \"sum(rate(pg_stat_database_tup_inserted{instance=~\\\"$instance\\\"}[5m]))\",\n            \"legendFormat\": \"Rows Inserted\",\n            \"refId\": \"C\"\n          },\n          {\n            \"expr\": \"sum(rate(pg_stat_database_tup_updated{instance=~\\\"$instance\\\"}[5m]))\",\n            \"legendFormat\": \"Rows Updated\",\n            \"refId\": \"D\"\n          },\n          {\n            \"expr\": \"sum(rate(pg_stat_database_tup_deleted{instance=~\\\"$instance\\\"}[5m]))\",\n            \"legendFormat\": \"Rows Deleted\",\n            \"refId\": \"E\"\n          }\n        ]\n      },\n      {\n        \"title\": \"Sequential vs Index Scans\",\n        \"type\": \"timeseries\",\n        \"gridPos\": {\n          \"x\": 12,\n          \"y\": 44,\n          \"w\": 12,\n          \"h\": 8\n        },\n        \"id\": 404,\n        \"targets\": [\n          {\n            \"expr\": \"sum(rate(pg_stat_user_tables_seq_scan{instance=~\\\"$instance\\\"}[5m]))\",\n            \"legendFormat\": \"Sequential Scans\",\n            \"refId\": \"A\"\n          },\n          {\n            \"expr\": \"sum(rate(pg_stat_user_tables_idx_scan{instance=~\\\"$instance\\\"}[5m]))\",\n            \"legendFormat\": \"Index Scans\",\n            \"refId\": \"B\"\n          }\n        ]\n      },\n      {\n        \"title\": \"Locks & Blocking\",\n        \"type\": \"row\",\n        \"gridPos\": {\n          \"x\": 0,\n          \"y\": 52,\n          \"w\": 24,\n          \"h\": 1\n        },\n        \"id\": 500\n      },\n      {\n        \"title\": \"Lock Types\",\n        \"type\": \"timeseries\",\n        \"gridPos\": {\n          \"x\": 0,\n          \"y\": 53,\n          \"w\": 12,\n          \"h\": 8\n        },\n        \"id\": 501,\n        \"targets\": [\n          {\n            \"expr\": \"pg_locks_count{instance=~\\\"$instance\\\"}\",\n            \"legendFormat\": \"{{mode}}\",\n            \"refId\": \"A\"\n          }\n        ],\n        \"fieldConfig\": {\n          \"defaults\": {\n            \"custom\": {\n              \"stacking\": {\n                \"mode\": \"normal\"\n              }\n            }\n          }\n        }\n      },\n      {\n        \"title\": \"Lock Waits\",\n        \"type\": \"timeseries\",\n        \"gridPos\": {\n          \"x\": 12,\n          \"y\": 53,\n          \"w\": 12,\n          \"h\": 8\n        },\n        \"id\": 502,\n        \"targets\": [\n          {\n            \"expr\": \"pg_stat_activity_count{instance=~\\\"$instance\\\", state=\\\"active\\\", wait_event_type=\\\"Lock\\\"}\",\n            \"legendFormat\": \"Waiting for Locks\",\n            \"refId\": \"A\"\n          }\n        ]\n      },\n      {\n        \"title\": \"System Resources\",\n        \"type\": \"row\",\n        \"gridPos\": {\n          \"x\": 0,\n          \"y\": 61,\n          \"w\": 24,\n          \"h\": 1\n        },\n        \"id\": 600\n      },\n      {\n        \"title\": \"Memory Usage\",\n        \"type\": \"timeseries\",\n        \"gridPos\": {\n          \"x\": 0,\n          \"y\": 62,\n          \"w\": 8,\n          \"h\": 8\n        },\n        \"id\": 601,\n        \"targets\": [\n          {\n            \"expr\": \"process_resident_memory_bytes{instance=~\\\"$instance\\\"}\",\n            \"legendFormat\": \"Resident Memory\",\n            \"refId\": \"A\"\n          },\n          {\n            \"expr\": \"process_virtual_memory_bytes{instance=~\\\"$instance\\\"}\",\n            \"legendFormat\": \"Virtual Memory\",\n            \"refId\": \"B\"\n          }\n        ],\n        \"fieldConfig\": {\n          \"defaults\": {\n            \"unit\": \"bytes\"\n          }\n        }\n      },\n      {\n        \"title\": \"CPU Usage\",\n        \"type\": \"timeseries\",\n        \"gridPos\": {\n          \"x\": 8,\n          \"y\": 62,\n          \"w\": 8,\n          \"h\": 8\n        },\n        \"id\": 602,\n        \"targets\": [\n          {\n            \"expr\": \"rate(process_cpu_seconds_total{instance=~\\\"$instance\\\"}[5m]) * 100\",\n            \"legendFormat\": \"{{job}}\",\n            \"refId\": \"A\"\n          }\n        ],\n        \"fieldConfig\": {\n          \"defaults\": {\n            \"unit\": \"percent\",\n            \"min\": 0,\n            \"max\": 100\n          }\n        }\n      },\n      {\n        \"title\": \"Database Size\",\n        \"type\": \"timeseries\",\n        \"gridPos\": {\n          \"x\": 16,\n          \"y\": 62,\n          \"w\": 8,\n          \"h\": 8\n        },\n        \"id\": 603,\n        \"targets\": [\n          {\n            \"expr\": \"pg_database_size_bytes{instance=~\\\"$instance\\\", datname!=\\\"template0\\\", datname!=\\\"template1\\\"}\",\n            \"legendFormat\": \"{{datname}}\",\n            \"refId\": \"A\"\n          }\n        ],\n        \"fieldConfig\": {\n          \"defaults\": {\n            \"unit\": \"bytes\"\n          }\n                 }\n       }\n     ]\n  }\n"
  },
  {
    "path": "local-dev/grafana/provisioning/datasources/datasource.yml",
    "content": "---\n# config file version\napiVersion: 1\n\n# list of datasources that should be deleted from the database\ndeleteDatasources:\n  - name: Prometheus\n    orgId: 1\n\n# list of datasources to insert/update depending\n# whats available in the database\ndatasources:\n  # <string, required> name of the datasource. Required\n- name: Prometheus\n  # <string, required> datasource type. Required\n  type: prometheus\n  # <string, required> access mode. direct or proxy. Required\n  access: proxy\n  # <int> org id. will default to orgId 1 if not specified\n  orgId: 1\n  # <string> url\n  url: http://clair-prometheus:9090/prom/\n  # <bool> mark as default datasource. Max one per org\n  isDefault: true\n  version: 1\n  editable: false\n- name: Pyroscope\n  type: pyroscope-datasource\n  access: proxy\n  orgId: 1\n  url: http://clair-pyroscope:4040/\n  jsonData:\n    path: http://clair-pyroscope:4040/\n  editable: false\n- name: Jaeger\n  type: jaeger\n  access: proxy\n  orgId: 1\n  url: http://clair-jaeger:16686/jaeger/\n  editable: false\n"
  },
  {
    "path": "local-dev/pgadmin/passfile.txt",
    "content": "# hostname:port:database:username:password\nclair-database:5432:notifier:clair:clair\nclair-database:5432:matcher:clair:clair\nclair-database:5432:indexer:clair:clair\n"
  },
  {
    "path": "local-dev/pgadmin/servers.json",
    "content": "{\n    \"Servers\": {\n        \"1\": {\n            \"Name\": \"clair\",\n            \"Group\": \"Servers\",\n            \"Port\": 5432,\n            \"Username\": \"postgres\",\n            \"Host\": \"clair-database\",\n            \"SSLMode\": \"disable\",\n            \"MaintenanceDB\": \"postgres\",\n            \"PassFile\": \"/pgadmin4/config/passfile.txt\"\n        }\n    }\n}\n"
  },
  {
    "path": "local-dev/prometheus/prometheus.yml",
    "content": "# global config\n---\nglobal:\n  scrape_interval: 4s\n  evaluation_interval: 30s\n  # scrape_timeout is set to the global default (10s).\nscrape_configs:\n  - job_name: indexer\n    metrics_path: \"/metrics\"\n    static_configs:\n      - targets: ['clair-indexer:8089']\n\n  - job_name: matcher\n    metrics_path: \"/metrics\"\n    static_configs:\n      - targets: ['clair-matcher:8089']\n\n  - job_name: clair-notifier\n    metrics_path: \"/metrics\"\n    static_configs:\n      - targets: ['clair-notifier:8089']\n\n  - job_name: notifier\n    metrics_path: \"/metrics\"\n    static_configs:\n      - targets: ['notifier:8089']\n\n  - job_name: indexer-quay\n    metrics_path: \"/metrics\"\n    static_configs:\n      - targets: ['indexer-quay:8089']\n\n  - job_name: postgres\n    metrics_path: \"/metrics\"\n    static_configs:\n      - targets: ['clair-postgres-exporter:9187']\n\n"
  },
  {
    "path": "local-dev/pyroscope/server.yml",
    "content": "---\nanalytics-opt-out: 'true'\nbase-url: /pyroscope\ndisable-pprof-endpoint: 'true'\nscrape-configs:\n- job-name: pyroscope\n  scrape-interval: 10s\n  scrape-timeout: 15s\n  enabled-profiles: [cpu, mem, goroutines, mutex, block]\n  use-delta-profiles: true\n  static-configs:\n  - application: clair-indexer\n    spy-name: gospy\n    targets:\n      - clair-indexer:8089\n  - application: clair-matcher\n    spy-name: gospy\n    targets:\n      - clair-matcher:8089\n"
  },
  {
    "path": "local-dev/quay/config.yaml",
    "content": "SUPER_USERS:\n- admin\nAUTHENTICATION_TYPE: Database\nBUILDLOGS_REDIS:\n  host: clair-redis\n  port: 6379\nDATABASE_SECRET_KEY: '30060361640793187613697366923211113205676925445650250274752125083971638376224'\nDB_URI: postgresql://quay@clair-database/quay\nDEFAULT_TAG_EXPIRATION: 2w\nDISTRIBUTED_STORAGE_CONFIG:\n  default:\n  - LocalStorage\n  - storage_path: /datastorage/registry\nDISTRIBUTED_STORAGE_DEFAULT_LOCATIONS: []\nDISTRIBUTED_STORAGE_PREFERENCE:\n- default\nENTERPRISE_LOGO_URL: /static/img/quay-horizontal-color.svg\nEXTERNAL_TLS_TERMINATION: true\nFEATURE_ACI_CONVERSION: false\nFEATURE_ANONYMOUS_ACCESS: true\nFEATURE_APP_REGISTRY: false\nFEATURE_APP_SPECIFIC_TOKENS: true\nFEATURE_BUILD_SUPPORT: false\nFEATURE_CHANGE_TAG_EXPIRATION: true\nFEATURE_DIRECT_LOGIN: true\nFEATURE_MAILING: false\nFEATURE_PARTIAL_USER_AUTOCOMPLETE: true\nFEATURE_REPO_MIRROR: false\nFEATURE_REQUIRE_TEAM_INVITE: true\nFEATURE_RESTRICTED_V1_PUSH: false\nFEATURE_SECURITY_NOTIFICATIONS: true\nFEATURE_SECURITY_SCANNER: true\nFEATURE_USERNAME_CONFIRMATION: true\nFEATURE_USER_CREATION: true\nFEATURE_USER_LOG_ACCESS: true\nGITHUB_LOGIN_CONFIG: {}\nGITHUB_TRIGGER_CONFIG: {}\nGITLAB_TRIGGER_KIND: {}\nGPG2_PRIVATE_KEY_FILENAME: signing-private.gpg\nGPG2_PUBLIC_KEY_FILENAME: signing-public.gpg\nLOG_ARCHIVE_LOCATION: default\nMAIL_DEFAULT_SENDER: support@quay.io\nMAIL_PORT: 587\nMAIL_USE_TLS: true\nPREFERRED_URL_SCHEME: http\nREGISTRY_TITLE: Local Testing Quay\nREGISTRY_TITLE_SHORT: Test Quay\nREPO_MIRROR_SERVER_HOSTNAME: null\nREPO_MIRROR_TLS_VERIFY: true\nSECURITY_SCANNER_V4_ENDPOINT: http://clair-traefik:6060\nSECURITY_SCANNER_ISSUER_NAME: quay\nSECURITY_SCANNER_V4_PSK: 'c2VjcmV0'\nSERVER_HOSTNAME: clair-quay:8080\nSETUP_COMPLETE: true\nSIGNING_ENGINE: gpg2\nTAG_EXPIRATION_OPTIONS:\n- 0s\n- 1d\n- 1w\n- 2w\n- 4w\nTEAM_RESYNC_STALE_TIME: 60m\nTESTING: false\nUSERFILES_LOCATION: default\nUSERFILES_PATH: userfiles/\nUSER_EVENTS_REDIS:\n  host: quay-redis\n  port: 6379\nUSE_CDN: false\n"
  },
  {
    "path": "local-dev/traefik/config/clair.yaml",
    "content": "---\nhttp:\n  routers:\n    indexer:\n      entryPoints: [clair]\n      rule: 'PathPrefix(`/indexer`)'\n      service: indexer\n    matcher:\n      entryPoints: [clair]\n      rule: 'PathPrefix(`/matcher`)'\n      service: matcher\n    notifier:\n      entryPoints: [clair]\n      rule: 'PathPrefix(`/notifier`)'\n      service: notifier\n  services:\n    indexer:\n      loadBalancer:\n        servers:\n        - url: \"http://clair-indexer:6060/\"\n        healthCheck:\n          path: /healthz\n          port: 8089\n    matcher:\n      loadBalancer:\n        servers:\n        - url: \"http://clair-matcher:6060/\"\n        healthCheck:\n          path: /healthz\n          port: 8089\n    notifier:\n      loadBalancer:\n        servers:\n        - url: \"http://clair-notifier:6060/\"\n        healthCheck:\n          path: /healthz\n          port: 8089\n"
  },
  {
    "path": "local-dev/traefik/config/dashboard.yaml",
    "content": "---\nhttp:\n  routers:\n    api:\n      entryPoints: [traefik]\n      rule: 'PathPrefix(`/api`) || PathPrefix(`/dashboard`)'\n      service: 'api@internal'\n    dashboard-redirect:\n      entryPoints: [traefik]\n      rule: 'Path(`/`)'\n      middlewares: [dashboard-redirect]\n      service: 'api@internal'\n  middlewares:\n    dashboard-redirect:\n      redirectRegex:\n        regex: '.*'\n        replacement: '${1}/dashboard/'\n"
  },
  {
    "path": "local-dev/traefik/config/grafana.yaml",
    "content": "---\nhttp:\n  routers:\n    grafana:\n      entryPoints: [traefik]\n      rule: 'PathPrefix(`/grafana`)'\n      service: grafana\n  services:\n    grafana:\n      loadBalancer:\n        servers:\n        - url: \"http://clair-grafana:3000/\"\n        healthCheck:\n          path: /grafana/api/health\n"
  },
  {
    "path": "local-dev/traefik/config/jaeger.yaml",
    "content": "---\nhttp:\n  routers:\n    jaeger:\n      entryPoints: [traefik]\n      rule: 'PathPrefix(`/jaeger`)'\n      service: jaeger\n  services:\n    jaeger:\n      loadBalancer:\n        servers:\n        - url: \"http://clair-jaeger:16686/\"\n        healthCheck:\n          path: /\n"
  },
  {
    "path": "local-dev/traefik/config/pgadmin.yaml",
    "content": "---\nhttp:\n  routers:\n    pgadmin:\n      entryPoints: [traefik]\n      rule: 'PathPrefix(`/pgadmin`)'\n      service: pgadmin\n  services:\n    pgadmin:\n      loadBalancer:\n        servers:\n        - url: \"http://clair-pgadmin/\"\n        healthCheck:\n          path: /pgadmin\n"
  },
  {
    "path": "local-dev/traefik/config/postgresql.yaml",
    "content": "---\ntcp:\n  routers:\n    postgresql:\n      entryPoints: [postgresql]\n      service: postgresql\n      # Traefik docs say this hack is needed if not using TLS.\n      rule: 'HostSNI(`*`)'\n  services:\n    postgresql:\n      loadBalancer:\n        servers:\n          - address: 'clair-database:5432'\n"
  },
  {
    "path": "local-dev/traefik/config/prom.yaml",
    "content": "---\nhttp:\n  routers:\n    prom:\n      entryPoints: [traefik]\n      rule: 'PathPrefix(`/prom`)'\n      service: prom\n  services:\n    prom:\n      loadBalancer:\n        servers:\n        - url: \"http://clair-prometheus:9090/\"\n        healthCheck:\n          path: /prom/-/healthy\n"
  },
  {
    "path": "local-dev/traefik/config/pyroscope.yaml",
    "content": "---\nhttp:\n  routers:\n    pyroscope:\n      entryPoints: [traefik]\n      rule: 'PathPrefix(`/pyroscope`)'\n      service: pyroscope\n      middlewares:\n      - pyroscope-stripprefix\n  middlewares:\n    pyroscope-stripprefix:\n      stripPrefix:\n        prefixes:\n        - /pyroscope\n  services:\n    pyroscope:\n      loadBalancer:\n        servers:\n        - url: \"http://clair-pyroscope:4040/\"\n        healthCheck:\n          path: /\n"
  },
  {
    "path": "local-dev/traefik/config/quay.yaml",
    "content": "---\nhttp:\n  routers:\n    quay:\n      entryPoints: [quay]\n      rule: 'PathPrefix(`/`)'\n      service: quay\n    quay-api:\n      entryPoints: [traefik]\n      rule: 'PathPrefix(`/v2`)'\n      service: quay\n  services:\n    quay:\n      loadBalancer:\n        passHostHeader: false\n        servers:\n        - url: \"http://clair-quay:8080/\"\n        healthCheck:\n          path: /health\n          port: 8080\n"
  },
  {
    "path": "local-dev/traefik/config/rabbitmq.yaml",
    "content": "---\nhttp:\n  routers:\n    rabbitmq:\n      entryPoints: [traefik]\n      rule: 'PathPrefix(`/rabbitmq`)'\n      middlewares:\n      - rewrite-api\n      - rewrite\n      service: rabbitmq\n  services:\n    rabbitmq:\n      loadBalancer:\n        servers:\n        - url: \"http://clair-rabbitmq:15672/\"\n        healthCheck:\n          path: /\n  middlewares:\n    rewrite-api:\n      replacePathRegex:\n        regex: '^/rabbitmq/api/(.*?)/(.*)'\n        replacement: '/api/%2F/$2'\n    rewrite:\n      replacePathRegex:\n        regex: '^/rabbitmq/(.*)$'\n        replacement: '/$1'\n"
  },
  {
    "path": "local-dev/traefik/traefik.yaml",
    "content": "---\nglobal:\n  sendAnonymousUsage: false\napi:\n  insecure: false\n  dashboard: true\nentryPoints:\n  traefik:\n    address: ':8080'\n  quay:\n    address: ':8443'\n  clair:\n    address: ':6060'\n  postgresql:\n    address: ':5432'\nproviders:\n  file:\n    directory: /etc/traefik/config\nmetrics:\n  prometheus:\n    addServicesLabels: true\ntracing:\n  otlp:\n    http:\n      endpoint: http://clair-jaeger:4318/v1/traces\naccessLog: {}\n"
  },
  {
    "path": "matcher/mock.go",
    "content": "package matcher\n\nimport (\n\t\"context\"\n\t\"sync\"\n\n\t\"github.com/google/uuid\"\n\t\"github.com/quay/claircore\"\n\t\"github.com/quay/claircore/libvuln/driver\"\n)\n\nvar _ Service = (*Mock)(nil)\n\n// Mock implements a mock matcher service\n//\n// If a method is not provided to a constructed Mock a panic\n// will occur on call.\ntype Mock struct {\n\tDeleteUpdateOperations_ func(context.Context, ...uuid.UUID) (int64, error)\n\tUpdateOperations_       func(context.Context, driver.UpdateKind, ...string) (map[string][]driver.UpdateOperation, error)\n\tLatestUpdateOperation_  func(context.Context, driver.UpdateKind) (uuid.UUID, error)\n\tLatestUpdateOperations_ func(context.Context, driver.UpdateKind) (map[string][]driver.UpdateOperation, error)\n\tUpdateDiff_             func(context.Context, uuid.UUID, uuid.UUID) (*driver.UpdateDiff, error)\n\tScan_                   func(context.Context, *claircore.IndexReport) (*claircore.VulnerabilityReport, error)\n\tInitialized_            func(context.Context) (bool, error)\n\t// TestUOs provide memory for the mock.\n\t// usage of this field can be dictated by the test case's needs.\n\tsync.Mutex\n\tTestUOs map[string][]driver.UpdateOperation\n}\n\n// DeleteUpdateOperations marks the provided refs as seen and processed.\nfunc (d *Mock) DeleteUpdateOperations(ctx context.Context, refs ...uuid.UUID) (int64, error) {\n\tif d.DeleteUpdateOperations_ == nil {\n\t\tpanic(\"mock matcher: unexpected call to DeleteUpdateOperations\")\n\t}\n\treturn d.DeleteUpdateOperations_(ctx, refs...)\n}\n\n// UpdateDiff reports the differences between the provided refs.\n//\n// \"Prev\" can be `uuid.Nil` to indicate \"earliest known ref.\"\nfunc (d *Mock) UpdateDiff(ctx context.Context, prev uuid.UUID, cur uuid.UUID) (*driver.UpdateDiff, error) {\n\tif d.UpdateDiff_ == nil {\n\t\tpanic(\"mock matcher: unexpected call to UpdateDiff\")\n\t}\n\treturn d.UpdateDiff_(ctx, prev, cur)\n}\n\n// UpdateOperations returns all the known UpdateOperations per updater.\nfunc (d *Mock) UpdateOperations(ctx context.Context, k driver.UpdateKind, updaters ...string) (map[string][]driver.UpdateOperation, error) {\n\tif d.UpdateOperations_ == nil {\n\t\tpanic(\"mock matcher: unexpected call to UpdateOperations\")\n\t}\n\treturn d.UpdateOperations_(ctx, k, updaters...)\n}\n\n// LatestUpdateOperations returns the most recent UpdateOperation per updater.\nfunc (d *Mock) LatestUpdateOperations(ctx context.Context, k driver.UpdateKind) (map[string][]driver.UpdateOperation, error) {\n\tif d.LatestUpdateOperations_ == nil {\n\t\tpanic(\"mock matcher: unexpected call to LatestUpdateOperations\")\n\t}\n\treturn d.LatestUpdateOperations_(ctx, k)\n}\n\n// LatestUpdateOperation returns a ref for the most recent update operation\n// across all updaters.\nfunc (d *Mock) LatestUpdateOperation(ctx context.Context, k driver.UpdateKind) (uuid.UUID, error) {\n\tif d.LatestUpdateOperation_ == nil {\n\t\tpanic(\"mock matcher: unexpected call to LatestUpdateOperation\")\n\t}\n\treturn d.LatestUpdateOperation_(ctx, k)\n}\n\nfunc (d *Mock) Scan(ctx context.Context, ir *claircore.IndexReport) (*claircore.VulnerabilityReport, error) {\n\tif d.Scan_ == nil {\n\t\tpanic(\"mock matcher: unexpected call to Scan\")\n\t}\n\treturn d.Scan_(ctx, ir)\n}\n\nfunc (d *Mock) Initialized(ctx context.Context) (bool, error) {\n\tif d.Initialized_ == nil {\n\t\tpanic(\"mock matcher: unexpected call to Initialized\")\n\t}\n\treturn d.Initialized_(ctx)\n}\n"
  },
  {
    "path": "matcher/service.go",
    "content": "package matcher\n\nimport (\n\t\"context\"\n\n\t\"github.com/google/uuid\"\n\t\"github.com/quay/claircore\"\n\t\"github.com/quay/claircore/libvuln/driver\"\n)\n\n// Service is an aggregate interface wrapping claircore.Libvuln functionality.\n//\n// Implementation may use a local instance of claircore.Libindex or a remote\n// instance via http or grpc client.\ntype Service interface {\n\tScanner\n\tDiffer\n}\n\n// Scanner is an interface providing a claircore.VulnerabilityReport given a claircore.IndexReport\ntype Scanner interface {\n\tInitialized(context.Context) (bool, error)\n\tScan(ctx context.Context, ir *claircore.IndexReport) (*claircore.VulnerabilityReport, error)\n}\n\n// Differ is an interface providing information on update operations.\ntype Differ interface {\n\t// DeleteUpdateOperations marks the provided refs as seen and processed.\n\tDeleteUpdateOperations(context.Context, ...uuid.UUID) (int64, error)\n\t// UpdateDiff reports the differences between the provided refs.\n\t//\n\t// \"Prev\" can be `uuid.Nil` to indicate \"earliest known ref.\"\n\tUpdateDiff(_ context.Context, prev, cur uuid.UUID) (*driver.UpdateDiff, error)\n\t// UpdateOperations returns all the known UpdateOperations per updater.\n\tUpdateOperations(context.Context, driver.UpdateKind, ...string) (map[string][]driver.UpdateOperation, error)\n\t// LatestUpdateOperations returns the most recent UpdateOperation per updater.\n\tLatestUpdateOperations(context.Context, driver.UpdateKind) (map[string][]driver.UpdateOperation, error)\n\t// LatestUpdateOperation returns a ref for the most recent update operation\n\t// across all updaters.\n\tLatestUpdateOperation(context.Context, driver.UpdateKind) (uuid.UUID, error)\n}\n"
  },
  {
    "path": "middleware/auth/handler.go",
    "content": "package auth\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\t\"strings\"\n)\n\n// Checker is an interface that reports whether the passed request should be\n// allowed to continue.\ntype Checker interface {\n\tCheck(context.Context, *http.Request) bool\n}\n\ntype handler struct {\n\tauth Checker\n\tnext http.Handler\n}\n\nfunc (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {\n\tif !h.auth.Check(r.Context(), r) {\n\t\tw.WriteHeader(http.StatusUnauthorized)\n\t\treturn\n\t}\n\th.next.ServeHTTP(w, r)\n}\n\n// Handler returns a http.Handler that gates access to the passed Handler behind\n// the passed Checker.\nfunc Handler(h http.Handler, f ...Checker) http.Handler {\n\tr := &handler{\n\t\tauth: fail{},\n\t\tnext: h,\n\t}\n\tif len(f) == 1 {\n\t\tr.auth = f[0]\n\t} else {\n\t\tr.auth = any(f)\n\t}\n\treturn r\n}\n\n// Any attempts all Checkers in order and reports true if any succeeds.\ntype any []Checker\n\n// Check implements Checker.\nfunc (a any) Check(ctx context.Context, r *http.Request) bool {\n\tfor _, c := range a {\n\t\tif ok := c.Check(ctx, r); ok {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// Fail is a Checker that always fails.\ntype fail struct{}\n\n// Check implements Checker.\nfunc (fail) Check(_ context.Context, _ *http.Request) bool { return false }\n\nfunc fromHeader(r *http.Request) (string, bool) {\n\ths, ok := r.Header[\"Authorization\"]\n\tif !ok {\n\t\treturn \"\", false\n\t}\n\tfor _, h := range hs {\n\t\tif strings.HasPrefix(h, \"Bearer \") {\n\t\t\treturn strings.TrimPrefix(h, \"Bearer \"), true\n\t\t}\n\t}\n\treturn \"\", false\n}\n"
  },
  {
    "path": "middleware/auth/httpauth_psk.go",
    "content": "package auth\n\nimport (\n\t\"context\"\n\t\"log/slog\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-jose/go-jose/v3/jwt\"\n)\n\n// PSK implements the AuthCheck interface.\n//\n// When Check is called the JWT on the incoming http request\n// will be validated against a pre-shared-key.\ntype PSK struct {\n\tkey []byte\n\tiss []string\n}\n\n// NewPSK returns an instance of a PSK\nfunc NewPSK(key []byte, issuer []string) (*PSK, error) {\n\treturn &PSK{\n\t\tkey: key,\n\t\tiss: issuer,\n\t}, nil\n}\n\n// Check implements AuthCheck\nfunc (p *PSK) Check(ctx context.Context, r *http.Request) bool {\n\twt, ok := fromHeader(r)\n\tif !ok {\n\t\tslog.DebugContext(ctx, \"failed to retrieve jwt from header\")\n\t\treturn false\n\t}\n\ttok, err := jwt.ParseSigned(wt)\n\tif err != nil {\n\t\tslog.DebugContext(ctx, \"failed to parse jwt\", \"reason\", err)\n\t\treturn false\n\t}\n\tcl := jwt.Claims{}\n\tif err := tok.Claims(p.key, &cl); err != nil {\n\t\tslog.DebugContext(ctx, \"failed to parse jwt\", \"reason\", err)\n\t\treturn false\n\t}\n\n\tlog := slog.With(\"iss\", cl.Issuer)\n\tif err := cl.ValidateWithLeeway(jwt.Expected{\n\t\tTime: time.Now(),\n\t}, 15*time.Second); err != nil {\n\t\tlog.DebugContext(ctx, \"could not validate claims\", \"reason\", err)\n\t\treturn false\n\t}\n\n\tfor i, iss := range p.iss {\n\t\tif iss == cl.Issuer {\n\t\t\tbreak\n\t\t}\n\t\tif i == len(p.iss)-1 {\n\t\t\tslog.DebugContext(ctx, \"could not verify issuer\", \"reason\", err)\n\t\t\treturn false\n\t\t}\n\t}\n\n\treturn true\n}\n"
  },
  {
    "path": "middleware/auth/httpauth_psk_test.go",
    "content": "package auth\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/base64\"\n\t\"fmt\"\n\t\"math/rand\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"reflect\"\n\t\"strings\"\n\t\"testing\"\n\t\"testing/quick\"\n\t\"time\"\n\n\t\"github.com/go-jose/go-jose/v3\"\n\t\"github.com/go-jose/go-jose/v3/jwt\"\n\n\t\"github.com/quay/clair/v4/internal/httputil\"\n)\n\ntype pskTestcase struct {\n\tkey    []byte\n\tissuer []string\n\tnonce  string\n\talg    jose.SignatureAlgorithm\n}\n\nfunc (tc *pskTestcase) String() string {\n\treturn fmt.Sprintf(\"\\nalg:\\t%s\\nkey:\\t%x\\nissuer:\\t%s\\nnonce:\\t%s\",\n\t\ttc.alg, tc.key, tc.issuer, tc.nonce)\n}\n\nvar signAlgo = []jose.SignatureAlgorithm{\n\tjose.HS256,\n\tjose.HS384,\n\tjose.HS512,\n}\n\n// implements the Generate interface from testing/quick package.\nfunc (tc *pskTestcase) Generate(rand *rand.Rand, sz int) reflect.Value {\n\tb := make([]byte, sz)\n\tt := &pskTestcase{\n\t\tkey:    make([]byte, sz),\n\t\talg:    signAlgo[rand.Intn(len(signAlgo))],\n\t\tissuer: make([]string, 0),\n\t}\n\tswitch n, err := rand.Read(t.key); {\n\tcase n != sz:\n\t\tpanic(fmt.Errorf(\"read %d, expected %d\", n, sz))\n\tcase err != nil:\n\t\tpanic(err)\n\t}\n\n\tswitch n, err := rand.Read(b); {\n\tcase n != sz:\n\t\tpanic(fmt.Errorf(\"read %d, expected %d\", n, sz))\n\tcase err != nil:\n\t\tpanic(err)\n\tdefault:\n\t\tt.issuer = append(t.issuer, base64.StdEncoding.EncodeToString(b))\n\t}\n\n\tswitch n, err := rand.Read(b); {\n\tcase n != sz:\n\t\tpanic(fmt.Errorf(\"read %d, expected %d\", n, sz))\n\tcase err != nil:\n\t\tpanic(err)\n\tdefault:\n\t\tt.nonce = base64.StdEncoding.EncodeToString(b)\n\t}\n\n\treturn reflect.ValueOf(t)\n}\n\nfunc (tc *pskTestcase) Handler(t *testing.T) http.Handler {\n\taf, err := NewPSK(tc.key, tc.issuer)\n\tif err != nil {\n\t\tt.Error(err)\n\t\treturn nil\n\t}\n\th := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tah := strings.TrimPrefix(r.Header.Get(\"authorization\"), \"Bearer \")\n\t\tt.Logf(\"got jwt: %s\", ah)\n\t\tfmt.Fprint(w, tc.nonce)\n\t})\n\treturn Handler(h, af)\n}\n\n// Roundtrips returns a function suitable for passing to quick.Check.\nfunc roundtrips(t *testing.T) func(*pskTestcase) bool {\n\treturn func(tc *pskTestcase) bool {\n\t\tctx := context.Background()\n\t\tt.Log(tc)\n\t\t// Set up the jwt signer.\n\t\tsk := jose.SigningKey{\n\t\t\tAlgorithm: tc.alg,\n\t\t\tKey:       tc.key,\n\t\t}\n\t\ts, err := jose.NewSigner(sk, nil)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\treturn false\n\t\t}\n\t\tnow := time.Now()\n\n\t\t// Mint the jwt.\n\t\ttok, err := jwt.Signed(s).Claims(&jwt.Claims{\n\t\t\tIssuer:    tc.issuer[0],\n\t\t\tExpiry:    jwt.NewNumericDate(now.Add(time.Minute)),\n\t\t\tIssuedAt:  jwt.NewNumericDate(now),\n\t\t\tNotBefore: jwt.NewNumericDate(now),\n\t\t}).CompactSerialize()\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\treturn false\n\t\t}\n\n\t\t// Set up the http server.\n\t\th := tc.Handler(t)\n\t\tif t.Failed() {\n\t\t\treturn false\n\t\t}\n\t\tsrv := httptest.NewServer(h)\n\t\tdefer srv.Close()\n\n\t\t// Mint a request.\n\t\treq, err := httputil.NewRequestWithContext(ctx, http.MethodGet, srv.URL, nil)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\treturn false\n\t\t}\n\t\treq.Header.Set(\"authorization\", \"Bearer \"+tok)\n\n\t\t// Execute the request and read back the body.\n\t\tres, err := srv.Client().Do(req)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\treturn false\n\t\t}\n\t\tdefer res.Body.Close()\n\t\tif res.StatusCode != http.StatusOK {\n\t\t\tt.Error(fmt.Errorf(\"unexpected response: %d %s\", res.StatusCode, res.Status))\n\t\t\treturn false\n\t\t}\n\t\tbuf := &bytes.Buffer{}\n\t\tif _, err := buf.ReadFrom(res.Body); err != nil {\n\t\t\tt.Error(err)\n\t\t\treturn false\n\t\t}\n\n\t\t// Compare the body read to the nonce we were expecting.\n\t\tt.Logf(\"\\nread:\\t%s\", buf.String())\n\t\tif got, want := buf.String(), tc.nonce; got != want {\n\t\t\tt.Error(fmt.Errorf(\"got: %q, want: %q\", got, want))\n\t\t\treturn false\n\t\t}\n\t\treturn true\n\t}\n}\n\n// TestPSKAuth generates random keys and checks signing with it.\nfunc TestPSKAuth(t *testing.T) {\n\tt.Parallel()\n\t// Generate random keys and check them via the roundtrips function.\n\tcfg := quick.Config{}\n\tif err := quick.Check(roundtrips(t), &cfg); err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n"
  },
  {
    "path": "middleware/compress/handler.go",
    "content": "// Package compress implements an RFC9110 compliant handler for the\n// \"Accept-Encoding\" header.\n//\n// This package supports \"identity\", \"gzip\", \"deflate\", and \"zstd\".\npackage compress\n\nimport (\n\t\"errors\"\n\t\"io\"\n\t\"mime\"\n\t\"net/http\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/klauspost/compress/flate\"\n\t\"github.com/klauspost/compress/gzip\"\n\t\"github.com/klauspost/compress/zstd\"\n)\n\n// Handler wraps the provided http.Handler and provides transparent body\n// compression based on a Request's \"Accept-Encoding\" header.\n//\n// Each handler instance pools its own compressors.\nfunc Handler(next http.Handler) http.Handler {\n\th := handler{\n\t\tnext: next,\n\t}\n\th.zstd.New = func() interface{} {\n\t\tw, err := zstd.NewWriter(nil, zstd.WithEncoderLevel(zstd.SpeedFastest))\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\treturn w\n\t}\n\th.gzip.New = func() interface{} {\n\t\tw, err := gzip.NewWriterLevel(nil, gzip.BestSpeed)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\treturn w\n\t}\n\th.flate.New = func() interface{} {\n\t\tw, err := flate.NewWriter(nil, flate.BestSpeed)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\treturn w\n\t}\n\n\treturn &h\n}\n\nvar _ http.Handler = (*handler)(nil)\n\n// Handler performs transparent HTTP body compression.\ntype handler struct {\n\tzstd, gzip, flate sync.Pool\n\tnext              http.Handler\n}\n\n// ParseAccept parses an \"Accept-Encoding\" header.\n//\n// Reports a sorted list of encodings and a map of disallowed encodings.\n// Reports nil if no selections were present.\nfunc parseAccept(h string) ([]accept, map[string]struct{}) {\n\tsegs := strings.Split(h, \",\")\n\tret := make([]accept, 0, len(segs))\n\tnok := make(map[string]struct{})\n\tfor _, s := range segs {\n\t\ta := accept{}\n\t\tt, param, err := mime.ParseMediaType(s)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\ta.Type = t\n\t\tif q, ok := param[\"q\"]; ok {\n\t\t\tif q == \"0\" {\n\t\t\t\tnok[t] = struct{}{}\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tqv, err := strconv.ParseFloat(param[\"q\"], 64)\n\t\t\tif err != nil {\n\t\t\t\tnok[t] = struct{}{}\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\ta.Q = qv\n\t\t}\n\t\tret = append(ret, a)\n\t}\n\n\tsort.SliceStable(ret, func(i, j int) bool {\n\t\treturn ret[i].Q > ret[j].Q\n\t})\n\treturn ret, nok\n}\n\ntype accept struct {\n\tType string\n\tQ    float64\n}\n\n// ServeHTTP implements http.Handler.\nfunc (c *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {\n\tv, ok := r.Header[\"Accept-Encoding\"]\n\tif !ok { // No header, use \"identity\".\n\t\tc.next.ServeHTTP(w, r)\n\t\treturn\n\t}\n\tae, nok := parseAccept(v[0])\n\tvar zw zwriter\n\t// Find the first accept-encoding we support. See\n\t// https://www.rfc-editor.org/rfc/rfc9110.html#section-12.5.3 for all the\n\t// semantics.\n\t//\n\t// NB The \"identity\" encoding shouldn't show up in the Content-Encoding\n\t// response header.\n\tfor _, a := range ae {\n\t\tswitch a.Type {\n\t\tcase \"gzip\", \"x-gzip\":\n\t\t\tw.Header().Set(\"content-encoding\", \"gzip\")\n\t\t\tgz := c.gzip.Get().(*gzip.Writer)\n\t\t\tgz.Reset(w)\n\t\t\tdefer c.gzip.Put(gz)\n\t\t\tzw = gz\n\t\tcase \"deflate\":\n\t\t\tw.Header().Set(\"content-encoding\", \"deflate\")\n\t\t\tz := c.flate.Get().(*flate.Writer)\n\t\t\tz.Reset(w)\n\t\t\tdefer c.flate.Put(z)\n\t\t\tzw = z\n\t\tcase \"zstd\":\n\t\t\tw.Header().Set(\"content-encoding\", \"zstd\")\n\t\t\ts := c.zstd.Get().(*zstd.Encoder)\n\t\t\ts.Reset(w)\n\t\t\tdefer c.zstd.Put(s)\n\t\t\tzw = s\n\t\tcase \"identity\":\n\t\t\tw.Header().Set(\"accept-encoding\", acceptable)\n\t\tcase \"*\":\n\t\t\t// If we hit a star, it's technically OK to return any encoding not\n\t\t\t// already specified. So, attempt to use gzip and then identity and\n\t\t\t// give up.\n\t\t\t// Clients that do extremely weird things like\n\t\t\t//\t*;q=1.0, gzip;q=0.1, identity;q=0.1\"\n\t\t\t// deserve extremely weird replies.\n\t\t\t_, gznok := nok[\"gzip\"]\n\t\t\t_, idnok := nok[\"identity\"]\n\t\t\tswitch {\n\t\t\tcase !gznok:\n\t\t\t\tw.Header().Set(\"content-encoding\", \"gzip\")\n\t\t\t\tgz := c.gzip.Get().(*gzip.Writer)\n\t\t\t\tgz.Reset(w)\n\t\t\t\tdefer c.gzip.Put(gz)\n\t\t\t\tzw = gz\n\t\t\tcase !idnok:\n\t\t\t\t// \"Identity\" isn't not OK, so fallthrough.\n\t\t\tdefault:\n\t\t\t\tw.Header().Set(\"accept-encoding\", acceptable)\n\t\t\t\tw.WriteHeader(http.StatusUnsupportedMediaType)\n\t\t\t\treturn\n\t\t\t}\n\t\tdefault:\n\t\t\tcontinue\n\t\t}\n\t\tbreak\n\t}\n\t// Now \"zw\" should be populated if it can be.\n\tif zw == nil {\n\t\tw.Header().Set(\"accept-encoding\", acceptable)\n\t\t// If it's not, we need to make sure identity or \"any\" aren't\n\t\t// disallowed.\n\t\t_, idnok := nok[\"identity\"]\n\t\t_, anynok := nok[\"*\"]\n\t\tif idnok || anynok {\n\t\t\tw.WriteHeader(http.StatusUnsupportedMediaType)\n\t\t\treturn\n\t\t}\n\t\t// Couldn't pick something, fall back to identity.\n\t\tc.next.ServeHTTP(w, r)\n\t\treturn\n\t}\n\t// Do some setup so we can see the error, albeit as a trailer.\n\tconst errHeader = `Clair-Error`\n\tw.Header().Add(\"trailer\", errHeader)\n\tdefer func() {\n\t\tif err := zw.Close(); err != nil {\n\t\t\tw.Header().Add(errHeader, err.Error())\n\t\t}\n\t}()\n\tnext := compressWriter{\n\t\tResponseWriter: w,\n\t\tzwriter:        zw,\n\t}\n\tc.next.ServeHTTP(&next, r)\n}\n\n// Acceptable is a preformatted list of acceptable encodings.\nconst acceptable = `zstd, gzip, deflate`\n\n// CompressWriter is compressing http.ResponseWriter that understands the go1.20\n// ResponseController scheme.\ntype compressWriter struct {\n\thttp.ResponseWriter\n\tzwriter\n}\n\ntype zwriter interface {\n\tio.WriteCloser\n\tFlush() error\n}\n\nvar _ http.ResponseWriter = (*compressWriter)(nil)\n\nfunc (c *compressWriter) Unwrap() http.ResponseWriter {\n\treturn c.ResponseWriter\n}\nfunc (c *compressWriter) Write(b []byte) (int, error) {\n\treturn c.zwriter.Write(b)\n}\nfunc (c *compressWriter) FlushError() error {\n\tzFlush := c.zwriter.Flush()\n\thttpFlush := http.NewResponseController(c.ResponseWriter).Flush()\n\tif errors.Is(httpFlush, http.ErrNotSupported) {\n\t\thttpFlush = nil\n\t}\n\treturn errors.Join(zFlush, httpFlush)\n}\n"
  },
  {
    "path": "middleware/compress/handler_test.go",
    "content": "package compress\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/rand\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"sync\"\n\t\"testing\"\n\n\t\"github.com/klauspost/compress/flate\"\n\t\"github.com/klauspost/compress/gzip\"\n\t\"github.com/klauspost/compress/zstd\"\n)\n\nvar (\n\tsetupOnce   sync.Once\n\tsetupErr    error\n\ttesthandler http.Handler\n\tbody        []byte\n)\n\nfunc setupHandler(t *testing.T) {\n\tconst writeSz = 1024 * 1024\n\tsetupOnce.Do(func() {\n\t\tbody = make([]byte, writeSz)\n\t\tif _, err := io.ReadFull(rand.Reader, body); err != nil {\n\t\t\tsetupErr = err\n\t\t\treturn\n\t\t}\n\t\ttesthandler = Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\trd := bytes.NewReader(body)\n\t\t\tif _, err := io.Copy(w, rd); err != nil {\n\t\t\t\tpanic(err)\n\t\t\t}\n\t\t}))\n\t})\n\tif setupErr != nil {\n\t\tt.Fatal(setupErr)\n\t}\n}\n\ntype mkFunc func(io.Reader) (io.ReadCloser, error)\n\nfunc testencoding(t *testing.T, enc string, status int, mk mkFunc) {\n\tt.Helper()\n\tsetupHandler(t)\n\tsrv := httptest.NewServer(testhandler)\n\tt.Cleanup(srv.Close)\n\treq, err := http.NewRequestWithContext(context.Background(), http.MethodGet, srv.URL+`/`+enc, nil)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treq.Header.Set(\"accept-encoding\", enc)\n\tt.Logf(\"Accept-Encoding: %q\", enc)\n\tres, err := srv.Client().Do(req)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tt.Cleanup(func() {\n\t\tif err := res.Body.Close(); err != nil {\n\t\t\tt.Error(err)\n\t\t}\n\t})\n\tif ce := res.Header.Get(\"content-encoding\"); ce != \"\" {\n\t\tt.Logf(\"Content-Encoding: %q\", ce)\n\t}\n\tif ae := res.Header.Get(\"accept-encoding\"); ae != \"\" {\n\t\tt.Logf(\"Accept-Encoding: %q\", ae)\n\t}\n\tt.Logf(\"got: %s, want: %d %s\", res.Status, status, http.StatusText(status))\n\tif got, want := res.StatusCode, status; got != want {\n\t\tt.Fail()\n\t}\n\tif status != http.StatusOK {\n\t\tt.Log(\"non-200 status expected, skipping body check\")\n\t\treturn\n\t}\n\tz, err := mk(res.Body)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tt.Cleanup(func() {\n\t\tif err := z.Close(); err != nil {\n\t\t\tt.Error(err)\n\t\t}\n\t})\n\tvar got bytes.Buffer\n\tif _, err := got.ReadFrom(z); err != nil {\n\t\tt.Error(err)\n\t}\n\tif !bytes.Equal(got.Bytes(), body) {\n\t\tt.Error(\"body not correct\")\n\t}\n\tt.Log(\"body OK\")\n}\n\nfunc TestCompressor(t *testing.T) {\n\ttt := []struct {\n\t\tName   string\n\t\tEnc    string\n\t\tMk     mkFunc\n\t\tStatus int\n\t}{\n\t\t{\n\t\t\tName:   \"Empty\",\n\t\t\tEnc:    ``,\n\t\t\tMk:     func(r io.Reader) (io.ReadCloser, error) { return io.NopCloser(r), nil },\n\t\t\tStatus: http.StatusOK,\n\t\t},\n\t\t{\n\t\t\tName:   \"Identity\",\n\t\t\tEnc:    `identity`,\n\t\t\tMk:     func(r io.Reader) (io.ReadCloser, error) { return io.NopCloser(r), nil },\n\t\t\tStatus: http.StatusOK,\n\t\t},\n\t\t{\n\t\t\tName:   \"Gzip\",\n\t\t\tEnc:    `gzip`,\n\t\t\tMk:     func(r io.Reader) (io.ReadCloser, error) { return gzip.NewReader(r) },\n\t\t\tStatus: http.StatusOK,\n\t\t},\n\t\t{\n\t\t\tName:   \"Deflate\",\n\t\t\tEnc:    `deflate`,\n\t\t\tMk:     func(r io.Reader) (io.ReadCloser, error) { return flate.NewReader(r), nil },\n\t\t\tStatus: http.StatusOK,\n\t\t},\n\t\t{\n\t\t\tName: \"Zstd\",\n\t\t\tEnc:  `zstd`,\n\t\t\tMk: func(r io.Reader) (io.ReadCloser, error) {\n\t\t\t\tz, err := zstd.NewReader(r)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t\treturn z.IOReadCloser(), nil\n\t\t\t},\n\t\t\tStatus: http.StatusOK,\n\t\t},\n\t\t// The examples in the RFC:\n\t\t{\n\t\t\tName:   \"RFC9110\",\n\t\t\tEnc:    `compress, gzip`,\n\t\t\tMk:     func(r io.Reader) (io.ReadCloser, error) { return gzip.NewReader(r) },\n\t\t\tStatus: http.StatusOK,\n\t\t},\n\t\t{\n\t\t\tName:   \"RFC9110\",\n\t\t\tEnc:    ``,\n\t\t\tMk:     func(r io.Reader) (io.ReadCloser, error) { return io.NopCloser(r), nil },\n\t\t\tStatus: http.StatusOK,\n\t\t},\n\t\t{\n\t\t\tName:   \"RFC9110\",\n\t\t\tEnc:    `*`,\n\t\t\tMk:     func(r io.Reader) (io.ReadCloser, error) { return gzip.NewReader(r) },\n\t\t\tStatus: http.StatusOK,\n\t\t},\n\t\t{\n\t\t\tName:   \"RFC9110\",\n\t\t\tEnc:    `compress;q=0.5, gzip;q=1.0`,\n\t\t\tMk:     func(r io.Reader) (io.ReadCloser, error) { return gzip.NewReader(r) },\n\t\t\tStatus: http.StatusOK,\n\t\t},\n\t\t{\n\t\t\tName:   \"RFC9110\",\n\t\t\tEnc:    `gzip;q=1.0, identity; q=0.5, *;q=0`,\n\t\t\tMk:     func(r io.Reader) (io.ReadCloser, error) { return gzip.NewReader(r) },\n\t\t\tStatus: http.StatusOK,\n\t\t},\n\t\t{\n\t\t\tName:   \"OldGzip\",\n\t\t\tEnc:    `x-gzip, *;q=0`,\n\t\t\tMk:     func(r io.Reader) (io.ReadCloser, error) { return gzip.NewReader(r) },\n\t\t\tStatus: http.StatusOK,\n\t\t},\n\t\t{\n\t\t\tName:   \"Unacceptable\",\n\t\t\tEnc:    `test, *;q=0`,\n\t\t\tStatus: http.StatusUnsupportedMediaType,\n\t\t},\n\t}\n\n\tfor _, tc := range tt {\n\t\tt.Run(tc.Name, func(t *testing.T) {\n\t\t\ttestencoding(t, tc.Enc, tc.Status, tc.Mk)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "notifier/amqp/deliverer.go",
    "content": "package amqp\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/url\"\n\t\"path\"\n\n\t\"github.com/google/uuid\"\n\t\"github.com/quay/clair/config\"\n\tamqp \"github.com/rabbitmq/amqp091-go\"\n\n\tclairerror \"github.com/quay/clair/v4/clair-error\"\n\t\"github.com/quay/clair/v4/notifier\"\n)\n\n// Deliverer is an AMQP deliverer which publishes a notifier.Callback to the\n// the broker.\n//\n// It's an error to configure this deliverer with an Exchange that does not exist.\n// Administrators should configure the Exchange, Queue, and Bindings before starting\n// this deliverer.\ntype Deliverer struct {\n\tcallback   *url.URL\n\tfo         failOver\n\troutingKey string\n\texchange   config.Exchange\n\trollup     int\n\tdirect     bool\n}\n\nfunc New(conf *config.AMQP) (*Deliverer, error) {\n\tvar d Deliverer\n\tif err := d.load(conf); err != nil {\n\t\treturn nil, err\n\t}\n\treturn &d, nil\n}\n\nfunc (d *Deliverer) load(conf *config.AMQP) error {\n\tvar err error\n\tif !conf.Direct {\n\t\td.callback, err = url.Parse(conf.Callback)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tif conf.TLS != nil {\n\t\td.fo.tls, err = conf.TLS.Config()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\t// Copy everything else out of the config:\n\td.direct = conf.Direct\n\td.rollup = conf.Rollup\n\td.exchange = conf.Exchange\n\td.routingKey = conf.RoutingKey\n\td.fo.uris = make([]*url.URL, len(conf.URIs))\n\tfor i, u := range conf.URIs {\n\t\td.fo.uris[i], err = url.Parse(u)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\td.fo.exchange = &d.exchange\n\treturn nil\n}\n\nfunc (d *Deliverer) Name() string {\n\treturn fmt.Sprintf(\"amqp-%s\", d.exchange.Name)\n}\n\nfunc (d *Deliverer) Deliver(ctx context.Context, nID uuid.UUID) error {\n\tconn, err := d.fo.Connection(ctx)\n\tif err != nil {\n\t\treturn &clairerror.ErrDeliveryFailed{err}\n\t}\n\tdefer conn.Close()\n\n\tch, err := conn.Channel()\n\tif err != nil {\n\t\treturn &clairerror.ErrDeliveryFailed{err}\n\t}\n\tdefer ch.Close()\n\n\tcallback := *d.callback\n\tcallback.Path = path.Join(callback.Path, nID.String())\n\n\tcb := notifier.Callback{\n\t\tNotificationID: nID,\n\t\tCallback:       callback,\n\t}\n\tb, err := json.Marshal(&cb)\n\tif err != nil {\n\t\treturn &clairerror.ErrDeliveryFailed{err}\n\t}\n\tmsg := amqp.Publishing{\n\t\tContentType: \"application/json\",\n\t\tAppId:       \"clairV4-notifier\",\n\t\tBody:        b,\n\t}\n\terr = ch.Publish(\n\t\td.exchange.Name,\n\t\td.routingKey,\n\t\tfalse,\n\t\tfalse,\n\t\tmsg,\n\t)\n\tif err != nil {\n\t\treturn &clairerror.ErrDeliveryFailed{err}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "notifier/amqp/deliverer_integration_test.go",
    "content": "package amqp\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/google/uuid\"\n\t\"github.com/quay/clair/config\"\n\t\"github.com/quay/claircore/test\"\n\t\"github.com/quay/claircore/test/integration\"\n\tamqp \"github.com/rabbitmq/amqp091-go\"\n\t\"golang.org/x/sync/errgroup\"\n)\n\nconst (\n\tdefaultRabbitMQURI = \"amqp://guest:guest@localhost:5672/\"\n)\n\n// TestDeliverer delivery confirms a notification\n// callback is successfully delivered to the amqp broker.\nfunc TestDeliverer(t *testing.T) {\n\tintegration.Skip(t)\n\tctx := test.Logging(t)\n\tconst (\n\t\tcallback = \"http://clair-notifier/notifier/api/v1/notification\"\n\t)\n\tvar (\n\t\turi         = os.Getenv(\"RABBITMQ_CONNECTION_STRING\")\n\t\tqueueAndKey = uuid.New().String()\n\t\t// our test assumes a default exchange\n\t\tconf = config.AMQP{\n\t\t\tCallback: callback,\n\t\t\tExchange: config.Exchange{\n\t\t\t\tName:       \"\",\n\t\t\t\tType:       \"direct\",\n\t\t\t\tDurable:    true,\n\t\t\t\tAutoDelete: false,\n\t\t\t},\n\t\t\tRoutingKey: queueAndKey,\n\t\t}\n\t)\n\tif uri == \"\" {\n\t\turi = defaultRabbitMQURI\n\t}\n\tt.Logf(\"using uri: %q\", uri)\n\n\tconf.URIs = []string{\n\t\t// give a few bogus URIs to confirm failover mechanisms are working\n\t\t\"amqp://guest:guest@nohost1:5672/\",\n\t\t\"amqp://guest:guest@nohost2:5672/\",\n\t\t\"amqp://guest:guest@nohost3:5672/\",\n\t\turi,\n\t}\n\n\tconn, err := amqp.Dial(uri)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to connect to broker at %v: %v\", uri, err)\n\t}\n\tdefer conn.Close()\n\n\tch, err := conn.Channel()\n\tif err != nil {\n\t\tt.Fatalf(\"failed to obtain channel from broker %v: %v\", uri, err)\n\t}\n\tdefer ch.Close()\n\n\t// this queue will autobind to the default \"direct\" exchange\n\t// and the queue name may be used as the routing key.\n\t_, err = ch.QueueDeclare(\n\t\tqueueAndKey,\n\t\ttrue,\n\t\tfalse,\n\t\tfalse,\n\t\tfalse,\n\t\tnil,\n\t)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to declare queue: %v\", err)\n\t}\n\n\t// test parallel usage\n\tg := errgroup.Group{}\n\tfor i := 0; i < 4; i++ {\n\t\tg.Go(func() error {\n\t\t\tnoteID := uuid.New()\n\t\t\td, err := New(&conf)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"could not create deliverer: %v\", err)\n\t\t\t}\n\t\t\t// we simply need to check for an error. amqp\n\t\t\t// will error if message cannot be delivered to broker\n\t\t\terr = d.Deliver(ctx, noteID)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to deliver message: %v\", err)\n\t\t\t}\n\t\t\treturn nil\n\t\t})\n\t}\n\tif err := g.Wait(); err != nil {\n\t\tt.Fatalf(\"test failed: %v\", err)\n\t}\n\n\t// create consumer\n\tconsumerConn, err := amqp.Dial(uri)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to create consumer connection: %v\", err)\n\t}\n\tdefer consumerConn.Close()\n\n\tconsumerCh, err := consumerConn.Channel()\n\tif err != nil {\n\t\tt.Fatalf(\"failed to create consumer channel: %v\", err)\n\t}\n\tdefer consumerCh.Close()\n\n\tmsgs, err := consumerCh.Consume(\n\t\tqueueAndKey,\n\t\t\"test\",\n\t\tfalse,\n\t\tfalse,\n\t\tfalse,\n\t\tfalse,\n\t\tnil,\n\t)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to start consuming messages: %v\", err)\n\t}\n\n\t// read messages\n\tfor i := 0; i < 4; i++ {\n\t\tm := <-msgs\n\t\tif m.ContentType != \"application/json\" {\n\t\t\tt.Errorf(\"msg content type mismatch: expected %s, got %s\", \"application/json\", m.ContentType)\n\t\t}\n\t\tif m.AppId != \"clairV4-notifier\" {\n\t\t\tt.Errorf(\"msg app ID mismatch: expected %s, got %s\", \"clairV4-notifier\", m.AppId)\n\t\t}\n\t\tvar msgBody map[string]string\n\t\tif err = json.Unmarshal(m.Body, &msgBody); err != nil {\n\t\t\tt.Errorf(\"cannot unmarshall msg body into map: %v\", err)\n\t\t}\n\t\tnid, ok := msgBody[\"notification_id\"]\n\t\tif !ok {\n\t\t\tt.Errorf(\"cannot find \\\"notification_id\\\" key in msg body\")\n\t\t}\n\t\tcb, ok := msgBody[\"callback\"]\n\t\tif !ok {\n\t\t\tt.Errorf(\"cannot find \\\"callback\\\" key in msg body\")\n\t\t}\n\t\tif cb != fmt.Sprintf(\"%s/%s\", callback, nid) {\n\t\t\tt.Errorf(\"callback mismatch: expected: %s, got %s\", fmt.Sprintf(\"%s/%s\", callback, nid), cb)\n\t\t}\n\t\tm.Ack(false)\n\t}\n\n\t// check if msgs channel is empty\n\tselect {\n\tcase m := <-msgs:\n\t\tt.Fatalf(\"there is still msg in msgs channel: %#v\", m)\n\tcase <-time.After(1 * time.Millisecond): // no msg found, as expected\n\t}\n}\n"
  },
  {
    "path": "notifier/amqp/directdeliverer.go",
    "content": "package amqp\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\n\t\"github.com/google/uuid\"\n\t\"github.com/quay/clair/config\"\n\tamqp \"github.com/rabbitmq/amqp091-go\"\n\n\tclairerror \"github.com/quay/clair/v4/clair-error\"\n\t\"github.com/quay/clair/v4/notifier\"\n)\n\n// DirectDeliverer is an AMQP deliverer which publishes notifications\n// directly to the broker.\n//\n// It's an error to configure this deliverer with an exchange that does not exist.\n// Administrators should configure the Exchange, Queue, and Bindings before starting\n// this deliverer.\ntype DirectDeliverer struct {\n\tn []notifier.Notification\n\tDeliverer\n}\n\nfunc NewDirectDeliverer(conf *config.AMQP) (*DirectDeliverer, error) {\n\tvar d DirectDeliverer\n\tif err := d.load(conf); err != nil {\n\t\treturn nil, err\n\t}\n\td.n = make([]notifier.Notification, 0, 1024)\n\treturn &d, nil\n}\n\nfunc (d *DirectDeliverer) Name() string {\n\treturn fmt.Sprintf(\"amqp-direct-%s\", d.exchange.Name)\n}\n\n// Notifications will copy the provided notifications into a buffer for AMQP\n// delivery.\nfunc (d *DirectDeliverer) Notifications(ctx context.Context, n []notifier.Notification) error {\n\t// if we can reslice instead of allocate do so.\n\tif len(n) <= len(d.n) {\n\t\td.n = d.n[:len(n)]\n\t\tcopy(d.n, n)\n\t\treturn nil\n\t}\n\ttmp := make([]notifier.Notification, len(n))\n\tcopy(tmp, n)\n\td.n = tmp\n\treturn nil\n}\n\nfunc (d *DirectDeliverer) Deliver(ctx context.Context, _ uuid.UUID) error {\n\tconn, err := d.fo.Connection(ctx)\n\tif err != nil {\n\t\treturn &clairerror.ErrDeliveryFailed{err}\n\t}\n\tdefer conn.Close()\n\n\tch, err := conn.Channel()\n\tif err != nil {\n\t\treturn &clairerror.ErrDeliveryFailed{err}\n\t}\n\tdefer ch.Close()\n\n\terr = ch.Tx()\n\tif err != nil {\n\t\treturn &clairerror.ErrDeliveryFailed{err}\n\t}\n\t// TODO: can tx.Rollback be safely defered?\n\n\t// block loop publishing smaller blocks of max(rollup) length via reslicing.\n\trollup := d.rollup\n\tif rollup == 0 {\n\t\trollup++\n\t}\n\n\tvar buf bytes.Buffer\n\tenc := json.NewEncoder(&buf)\n\tvar currentBlock []notifier.Notification\n\tfor bs, be := 0, rollup; bs < len(d.n); bs, be = be, be+rollup {\n\t\tbuf.Reset()\n\t\t// if block-end exceeds array bounds, slice block underflow.\n\t\t// next block-start will cause loop to exit.\n\t\tif be > len(d.n) {\n\t\t\tbe = len(d.n)\n\t\t}\n\n\t\tcurrentBlock = d.n[bs:be]\n\t\terr := enc.Encode(&currentBlock)\n\t\tif err != nil {\n\t\t\tch.TxRollback()\n\t\t\treturn &clairerror.ErrDeliveryFailed{err}\n\t\t}\n\t\tmsg := amqp.Publishing{\n\t\t\tContentType: \"application/json\",\n\t\t\tAppId:       \"clairV4-notifier\",\n\t\t\tBody:        buf.Bytes(),\n\t\t}\n\t\terr = ch.Publish(\n\t\t\td.exchange.Name,\n\t\t\td.routingKey,\n\t\t\tfalse,\n\t\t\tfalse,\n\t\t\tmsg,\n\t\t)\n\t\tif err != nil {\n\t\t\tch.TxRollback()\n\t\t\treturn &clairerror.ErrDeliveryFailed{err}\n\t\t}\n\t}\n\n\terr = ch.TxCommit()\n\tif err != nil {\n\t\treturn &clairerror.ErrDeliveryFailed{err}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "notifier/amqp/directdeliverer_integration_test.go",
    "content": "package amqp\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/google/uuid\"\n\t\"github.com/quay/clair/config\"\n\t\"github.com/quay/claircore\"\n\t\"github.com/quay/claircore/test\"\n\t\"github.com/quay/claircore/test/integration\"\n\tamqp \"github.com/rabbitmq/amqp091-go\"\n\t\"golang.org/x/sync/errgroup\"\n\n\t\"github.com/quay/clair/v4/notifier\"\n)\n\n// TestDirectDeliverer confirms delivery of notifications directly\n// to the AMQP queue with rollup works correctly.\nfunc TestDirectDeliverer(t *testing.T) {\n\tintegration.Skip(t)\n\tctx := test.Logging(t)\n\t// test start\n\ttable := []struct {\n\t\tname         string\n\t\trollup       int\n\t\tnotes        int\n\t\texpectedMsgs int\n\t}{\n\t\t{\n\t\t\tname:         \"0\",\n\t\t\trollup:       0,\n\t\t\tnotes:        1,\n\t\t\texpectedMsgs: 1,\n\t\t},\n\t\t{\n\t\t\tname:         \"1\",\n\t\t\trollup:       1,\n\t\t\tnotes:        5,\n\t\t\texpectedMsgs: 5,\n\t\t},\n\t\t{\n\t\t\tname:         \"RollupOverflow\",\n\t\t\trollup:       10,\n\t\t\tnotes:        5,\n\t\t\texpectedMsgs: 1,\n\t\t},\n\t\t{\n\t\t\tname:         \"Odds\",\n\t\t\trollup:       3,\n\t\t\tnotes:        7,\n\t\t\texpectedMsgs: 3,\n\t\t},\n\t\t{\n\t\t\tname:         \"OddsRollup\",\n\t\t\trollup:       3,\n\t\t\tnotes:        8,\n\t\t\texpectedMsgs: 3,\n\t\t},\n\t\t{\n\t\t\tname:         \"OddsNotes\",\n\t\t\trollup:       4,\n\t\t\tnotes:        7,\n\t\t\texpectedMsgs: 2,\n\t\t},\n\t\t{\n\t\t\tname:         \"Large\",\n\t\t\trollup:       100,\n\t\t\tnotes:        1000,\n\t\t\texpectedMsgs: 10,\n\t\t},\n\t}\n\n\turi := os.Getenv(\"RABBITMQ_CONNECTION_STRING\")\n\tif uri == \"\" {\n\t\turi = defaultRabbitMQURI\n\t}\n\tt.Logf(\"using uri: %q\", uri)\n\tconn, err := amqp.Dial(uri)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to connect to broker at %v: %v\", uri, err)\n\t}\n\tdefer conn.Close()\n\t// our test assumes a default exchange\n\texchange := config.Exchange{\n\t\tName:       \"\",\n\t\tType:       \"direct\",\n\t\tDurable:    true,\n\t\tAutoDelete: false,\n\t}\n\tfor _, tt := range table {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tctx := test.Logging(t, ctx)\n\t\t\t// rabbitmq queue declare\n\n\t\t\tqueueAndKey := tt.name + \"-\" + uuid.New().String()\n\n\t\t\tch, err := conn.Channel()\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"failed to obtain channel from broker %v: %v\", uri, err)\n\t\t\t}\n\t\t\tdefer ch.Close()\n\t\t\t// this queue will autobind to the default \"direct\" exchange\n\t\t\t// and the queue name may be used as the routing key.\n\t\t\t_, err = ch.QueueDeclare(\n\t\t\t\tqueueAndKey,\n\t\t\t\ttrue,\n\t\t\t\tfalse,\n\t\t\t\tfalse,\n\t\t\t\tfalse,\n\t\t\t\tnil,\n\t\t\t)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"failed to declare queue: %v\", err)\n\t\t\t}\n\n\t\t\t// deliverer test\n\t\t\tconf := config.AMQP{\n\t\t\t\tDirect: true,\n\t\t\t\tRollup: tt.rollup,\n\t\t\t\t// values come from rabbitmq setup\n\t\t\t\tRoutingKey: queueAndKey,\n\t\t\t\tExchange:   exchange,\n\t\t\t\tURIs: []string{\n\t\t\t\t\t// give a few bogus URIs to confirm failover mechanisms are working\n\t\t\t\t\t\"amqp://guest:guest@nohost1:5672/\",\n\t\t\t\t\t\"amqp://guest:guest@nohost2:5672/\",\n\t\t\t\t\t\"amqp://guest:guest@nohost3:5672/\",\n\t\t\t\t\turi,\n\t\t\t\t},\n\t\t\t}\n\n\t\t\tnoteID := uuid.New()\n\t\t\tnotes := make([]notifier.Notification, 0, tt.notes)\n\t\t\tfor i := 0; i < tt.notes; i++ {\n\t\t\t\tnotes = append(notes, notifier.Notification{\n\t\t\t\t\tID:       uuid.New(),\n\t\t\t\t\tManifest: claircore.MustParseDigest(\"sha256:35c102085707f703de2d9eaad8752d6fe1b8f02b5d2149f1d8357c9cc7fb7d0a\"),\n\t\t\t\t\tReason:   notifier.Added,\n\t\t\t\t\tVulnerability: notifier.VulnSummary{\n\t\t\t\t\t\tDescription: fmt.Sprintf(\"test-vuln-%d\", i),\n\t\t\t\t\t},\n\t\t\t\t})\n\t\t\t}\n\n\t\t\t// test parallel usage\n\t\t\tg := errgroup.Group{}\n\t\t\tfor i := 0; i < 4; i++ {\n\t\t\t\tg.Go(func() error {\n\t\t\t\t\td, err := NewDirectDeliverer(&conf)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"could not create deliverer: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\terr = d.Notifications(ctx, notes)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to provide notifications to direct deliverer: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\t// we simply need to check for an error. rabbitmq\n\t\t\t\t\t// will error if message cannot be delivered to broker\n\t\t\t\t\terr = d.Deliver(ctx, noteID)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to deliver message: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\treturn nil\n\t\t\t\t})\n\t\t\t}\n\t\t\tif err := g.Wait(); err != nil {\n\t\t\t\tt.Fatalf(\"test failed: %v\", err)\n\t\t\t}\n\n\t\t\t// create consumer\n\t\t\tconsumerConn, err := amqp.Dial(uri)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"failed to create consumer connection: %v\", err)\n\t\t\t}\n\t\t\tdefer consumerConn.Close()\n\n\t\t\tconsumerCh, err := consumerConn.Channel()\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"failed to create consumer channel: %v\", err)\n\t\t\t}\n\t\t\tdefer consumerCh.Close()\n\n\t\t\tmsgs, err := consumerCh.Consume(\n\t\t\t\tqueueAndKey,\n\t\t\t\t\"test\",\n\t\t\t\tfalse,\n\t\t\t\tfalse,\n\t\t\t\tfalse,\n\t\t\t\tfalse,\n\t\t\t\tnil,\n\t\t\t)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"failed to start consuming messages: %v\", err)\n\t\t\t}\n\n\t\t\t// read messages\n\t\t\ttotalExpectedMsgs := tt.expectedMsgs * 4\n\t\t\tfor i := 0; i < totalExpectedMsgs; i++ {\n\t\t\t\tm := <-msgs\n\t\t\t\tif m.ContentType != \"application/json\" {\n\t\t\t\t\tt.Errorf(\"msg content type mismatch: expected %s, got %s\", \"application/json\", m.ContentType)\n\t\t\t\t}\n\t\t\t\tif m.AppId != \"clairV4-notifier\" {\n\t\t\t\t\tt.Errorf(\"msg app ID mismatch: expected %s, got %s\", \"clairV4-notifier\", m.AppId)\n\t\t\t\t}\n\t\t\t\tvar msgBody []notifier.Notification\n\t\t\t\tif err = json.Unmarshal(m.Body, &msgBody); err != nil {\n\t\t\t\t\tt.Errorf(\"cannot unmarshall msg body into slice of notifications: %v\", err)\n\t\t\t\t}\n\t\t\t\trollup := tt.rollup\n\t\t\t\tif tt.rollup == 0 {\n\t\t\t\t\trollup++\n\t\t\t\t}\n\t\t\t\tif len(msgBody) > rollup {\n\t\t\t\t\tt.Errorf(\"found more notifications in msg than expected: rollup %d, got %d\", rollup, len(msgBody))\n\t\t\t\t}\n\t\t\t\tm.Ack(false)\n\t\t\t}\n\n\t\t\t// check if msgs channel is empty\n\t\t\tselect {\n\t\t\tcase <-msgs:\n\t\t\t\tt.Fatal(\"there is still msg in msgs channel\")\n\t\t\tcase <-time.After(1 * time.Millisecond): // no msg found, as expected\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "notifier/amqp/doc.go",
    "content": "// Package amqp implements a [Deliverer] over the AMQP protocol.\n//\n// Deprecated: This package will be removed in a future version. Users should\n// write a webhook to AMQP transducer.\npackage amqp\n"
  },
  {
    "path": "notifier/amqp/failover.go",
    "content": "package amqp\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"net/url\"\n\t\"sync\"\n\n\t\"github.com/quay/clair/config\"\n\tamqp \"github.com/rabbitmq/amqp091-go\"\n)\n\n// failOver will return the first successful connection made against the provided\n// brokers, or an existing connection if not closed.\n//\n// failOver is safe for concurrent usage.\ntype failOver struct {\n\tsync.RWMutex\n\tconn     *amqp.Connection\n\ttls      *tls.Config\n\texchange *config.Exchange\n\turis     []*url.URL\n}\n\n// Connection returns an AMQP connection to the first broker which successfully\n// handshakes.\nfunc (f *failOver) Connection(ctx context.Context) (*amqp.Connection, error) {\n\tf.RLock()\n\tif f.conn != nil && !f.conn.IsClosed() {\n\t\tslog.DebugContext(ctx, \"reusing connection\", \"address\", f.conn.LocalAddr())\n\t\tf.RUnlock()\n\t\treturn f.conn, nil\n\t}\n\tf.RUnlock()\n\n\tfor _, uri := range f.uris {\n\t\tlog := slog.With(\"broker\", uri)\n\t\t// safe to always call DialTLS per docs:\n\t\t// 'DialTLS will use the provided tls.Config when it encounters an amqps:// scheme and will dial a plain connection when it encounters an amqp:// scheme.'\n\t\tconn, err := amqp.DialTLS(uri.String(), f.tls)\n\t\tif err != nil {\n\t\t\tif conn != nil {\n\t\t\t\tconn.Close()\n\t\t\t}\n\t\t\tlog.InfoContext(ctx, \"failed to connect to AMQP broker; attempting next broker\",\n\t\t\t\t\"reason\", err)\n\t\t\tcontinue\n\t\t}\n\t\tch, err := conn.Channel()\n\t\tif err != nil {\n\t\t\tif conn != nil {\n\t\t\t\tconn.Close()\n\t\t\t}\n\t\t\tlog.InfoContext(ctx, \"could not obtain initial AMQP channel; attempting next broker\",\n\t\t\t\t\"reason\", err)\n\t\t\tcontinue\n\t\t}\n\t\t// if the name is \"\" it's the default exchange which\n\t\t// cannot be declared.\n\t\tif f.exchange.Name != \"\" {\n\t\t\terr = ch.ExchangeDeclarePassive(\n\t\t\t\tf.exchange.Name,\n\t\t\t\tf.exchange.Type,\n\t\t\t\tf.exchange.Durable,\n\t\t\t\tf.exchange.AutoDelete,\n\t\t\t\t// these will not be considered in a passive declare\n\t\t\t\tfalse,\n\t\t\t\tfalse,\n\t\t\t\tnil,\n\t\t\t)\n\t\t\tif err != nil {\n\t\t\t\tif conn != nil {\n\t\t\t\t\tconn.Close()\n\t\t\t\t}\n\t\t\t\tlog.InfoContext(ctx, \"could not declare AMQP exchange; attempting next broker\",\n\t\t\t\t\t\"reason\", err)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\t\tch.Close()\n\n\t\tf.Lock()\n\t\tdefer f.Unlock()\n\t\t// only set our connection if its necessary still\n\t\t// if not close the conn to ensure no leak occurs\n\t\tif f.conn == nil || f.conn.IsClosed() {\n\t\t\tf.conn = conn\n\t\t} else {\n\t\t\tconn.Close()\n\t\t}\n\t\treturn f.conn, nil\n\t}\n\treturn nil, fmt.Errorf(\"all failover URIs failed to connect\")\n}\n"
  },
  {
    "path": "notifier/callback.go",
    "content": "package notifier\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/url\"\n\n\t\"github.com/google/uuid\"\n)\n\n// Callback holds the details for clients to call back the Notifier\n// and receive notifications.\ntype Callback struct {\n\tNotificationID uuid.UUID `json:\"notification_id\"`\n\tCallback       url.URL   `json:\"callback\"`\n}\n\nfunc (cb Callback) MarshalJSON() ([]byte, error) {\n\tvar m = map[string]string{\n\t\t\"notification_id\": cb.NotificationID.String(),\n\t\t\"callback\":        cb.Callback.String(),\n\t}\n\treturn json.Marshal(m)\n}\n\nfunc (cb *Callback) UnmarshalJSON(b []byte) error {\n\tvar m = make(map[string]string, 2)\n\terr := json.Unmarshal(b, &m)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif _, ok := m[\"notification_id\"]; !ok {\n\t\treturn fmt.Errorf(\"json unmarshal failed. webhook requires a \\\"notification_id\\\" field\")\n\t}\n\tif _, ok := m[\"callback\"]; !ok {\n\t\treturn fmt.Errorf(\"json unmarshal failed. webhook requires a \\\"callback\\\" field\")\n\t}\n\n\tuid, err := uuid.Parse(m[\"notification_id\"])\n\tif err != nil {\n\t\treturn fmt.Errorf(\"json unmarshal failed. malformed notification uuid: %v\", err)\n\t}\n\tcbURL, err := url.Parse(m[\"callback\"])\n\tif err != nil {\n\t\treturn fmt.Errorf(\"json unmarshal failed. malformed callback url: %v\", err)\n\t}\n\n\t(*cb).NotificationID = uid\n\t(*cb).Callback = *cbURL\n\treturn nil\n}\n"
  },
  {
    "path": "notifier/callback_test.go",
    "content": "package notifier\n\nimport (\n\t\"encoding/json\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/google/uuid\"\n)\n\nfunc TestCallbackSerializtion(t *testing.T) {\n\tvar want = []byte(`{\"callback\":\"https://example.com\",\"notification_id\":\"00000000-0000-0000-0000-000000000000\"}`)\n\tcb := Callback{\n\t\tNotificationID: uuid.Nil,\n\t}\n\tu, err := url.Parse(\"https://example.com\")\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n\tcb.Callback = *u\n\tgot, err := json.Marshal(&cb)\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n\tif !cmp.Equal(got, want) {\n\t\tt.Error(cmp.Diff(got, want))\n\t}\n\n\tvar rt Callback\n\tif err := json.Unmarshal(want, &rt); err != nil {\n\t\tt.Error(err)\n\t}\n\tgot, err = json.Marshal(&rt)\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n\tif !cmp.Equal(got, want) {\n\t\tt.Error(cmp.Diff(got, want))\n\t}\n}\n"
  },
  {
    "path": "notifier/deliverer.go",
    "content": "package notifier\n\nimport (\n\t\"context\"\n\n\t\"github.com/google/uuid\"\n)\n\n// Deliverer provides the method set for delivering notifications\ntype Deliverer interface {\n\t// A unique name for the deliverer implementation\n\tName() string\n\t// Deliver will push the notification ID to subscribed clients.\n\t//\n\t// If delivery fails a clairerror.ErrDeliveryFailed error must be returned.\n\tDeliver(ctx context.Context, nID uuid.UUID) error\n}\n\n// DirectDeliverer implementations are used in coordination with the Deliverer interface.\n//\n// DirectDeliverer(s) expect this method to be called prior to their Deliverer methods.\n// Implementations must still implement both Deliverer and DirectDeliverer methods for correct use.\n//\n// Implementations will be provided a list of notifications in which they can directly deliver to subscribed\n// clients.\ntype DirectDeliverer interface {\n\tNotifications(ctx context.Context, n []Notification) error\n}\n"
  },
  {
    "path": "notifier/delivery.go",
    "content": "package notifier\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"log/slog\"\n\t\"time\"\n\n\t\"github.com/google/uuid\"\n\n\tclairerror \"github.com/quay/clair/v4/clair-error\"\n)\n\n// Delivery handles the business logic of delivering\n// notifications.\ntype Delivery struct {\n\t// a Deliverer implementation to invoke.\n\tDeliverer Deliverer\n\t// a store to retrieve notifications and update their receipts\n\tstore Store\n\t// distributed lock used for mutual exclusion\n\tlocks Locker\n\t// the interval at which we will attempt delivery of notifications.\n\tinterval time.Duration\n}\n\nfunc NewDelivery(store Store, l Locker, d Deliverer, interval time.Duration) *Delivery {\n\treturn &Delivery{\n\t\tDeliverer: d,\n\t\tinterval:  interval,\n\t\tstore:     store,\n\t\tlocks:     l,\n\t}\n}\n\n// Deliver begins delivering notifications.\n//\n// Canceling the ctx will end delivery.\nfunc (d *Delivery) Deliver(ctx context.Context) error {\n\tslog.InfoContext(ctx, \"delivering notifications\")\n\n\tticker := time.NewTicker(d.interval)\n\tdefer ticker.Stop()\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn ctx.Err()\n\t\tcase <-ticker.C:\n\t\t\tslog.DebugContext(ctx, \"delivery tick\")\n\t\t\tif err := d.RunDelivery(ctx); err != nil {\n\t\t\t\tslog.WarnContext(ctx, \"encountered error on tick\",\n\t\t\t\t\t\"reason\", err)\n\t\t\t}\n\t\t}\n\t}\n}\n\n// RunDelivery determines notifications to deliver and\n// calls the implemented Deliverer to perform the actions.\nfunc (d *Delivery) RunDelivery(ctx context.Context) error {\n\ttoDeliver := []uuid.UUID{}\n\t// get created\n\tcreated, err := d.store.Created(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif sz := len(created); sz != 0 {\n\t\tslog.InfoContext(ctx, \"notification ids in created status\",\n\t\t\t\"created\", sz)\n\t\ttoDeliver = append(toDeliver, created...)\n\t}\n\n\t// get failed\n\tfailed, err := d.store.Failed(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif sz := len(failed); sz != 0 {\n\t\tslog.InfoContext(ctx, \"notification ids in failed status\",\n\t\t\t\"failed\", sz)\n\t\ttoDeliver = append(toDeliver, failed...)\n\t}\n\n\tfor _, nID := range toDeliver {\n\t\tvar err error\n\t\tctx, done := d.locks.TryLock(ctx, nID.String())\n\t\tif ok := ctx.Err(); !errors.Is(ok, nil) {\n\t\t\tslog.DebugContext(ctx, \"unable to get lock\",\n\t\t\t\t\"reason\", ok,\n\t\t\t\t\"notification_id\", nID)\n\t\t} else {\n\t\t\terr = d.do(ctx, nID)\n\t\t}\n\t\tdone()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\n// do performs the delivery of notifications via the composed\n// deliverer\n//\n// do's actions should be performed under a distributed lock.\nfunc (d *Delivery) do(ctx context.Context, nID uuid.UUID) error {\n\t// if we have a direct deliverer provide the notifications to it.\n\tif dd, ok := d.Deliverer.(DirectDeliverer); ok {\n\t\tslog.DebugContext(ctx, \"providing direct deliverer notifications\")\n\t\tnotifications, _, err := d.store.Notifications(ctx, nID, nil)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\terr = dd.Notifications(ctx, notifications)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\t// deliver the notification\n\terr := d.Deliverer.Deliver(ctx, nID)\n\tif err != nil {\n\t\tvar dErr clairerror.ErrDeliveryFailed\n\t\tif errors.As(err, &dErr) {\n\t\t\t// OK for this to fail, notification will stay in Created status.\n\t\t\t// store is failing, lets back off it tho until next tick.\n\t\t\tslog.InfoContext(ctx, \"failed to deliver notifications\")\n\t\t\terr := d.store.SetDeliveryFailed(ctx, nID)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\treturn nil\n\t\t}\n\t\treturn err\n\t}\n\terr = d.store.SetDelivered(ctx, nID)\n\tif err != nil {\n\t\t// the message was delivered, but we can't ack this in our db\n\t\t// it will be delivered again unless deleted before next interval\n\t\treturn err\n\t}\n\n\t// if we successfully performed direct delivery\n\t// we can delete notification id\n\tif _, ok := d.Deliverer.(DirectDeliverer); ok {\n\t\terr := d.store.SetDeleted(ctx, nID)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tslog.InfoContext(ctx, \"successfully delivered notifications\")\n\treturn nil\n}\n"
  },
  {
    "path": "notifier/locker.go",
    "content": "package notifier\n\nimport \"context\"\n\n// Locker is any context-based locking API.\ntype Locker interface {\n\tTryLock(context.Context, string) (context.Context, context.CancelFunc)\n\tLock(context.Context, string) (context.Context, context.CancelFunc)\n\tClose(context.Context) error\n}\n"
  },
  {
    "path": "notifier/migrations/01-init.sql",
    "content": "--- an identity table for notifications\nCREATE TABLE IF NOT EXISTS notification (\n    id uuid PRIMARY KEY\n);\n\n--- a relation expressing the latest update operation\n--- processed for a given updater name\nCREATE TABLE IF NOT EXISTS notifier_update_operation (\n    uo_id uuid PRIMARY KEY,\n    updater text,\n    ts timestamptz\n);\n\n--- a relation mapping notifications to their serialized json bodies\nCREATE TABLE IF NOT EXISTS notification_body (\n    id uuid PRIMARY KEY,\n    notification_id uuid REFERENCES notification,\n    body jsonb NOT NULL -- serialized json body of notification\n);\n\nCREATE INDEX notification_body_idx ON notification_body (notification_id, id);\n\n--- an enumeration identifying the possible status a receipt may be in\nCREATE TYPE receiptstatus AS ENUM (\n    'created',\n    'delivered',\n    'delivery_failed',\n    'deleted'\n);\n\n--- a relation expressing the current status of a notification\n--- this acts as a trigger for application business logic\nCREATE TABLE IF NOT EXISTS receipt (\n    notification_id uuid PRIMARY KEY REFERENCES notification,\n    uo_id uuid REFERENCES notifier_update_operation (uo_id),\n    status receiptstatus NOT NULL,\n    ts timestamptz,\n    details jsonb -- any additional details specific to the delivery mechanism\n);\n\nCREATE INDEX receipt_idx ON receipt (notification_id, uo_id);\n\n--- a relation holding a pub_key in PKIX, ASN.1 DER form\n--- expiration is application defined and not associated with the public key\nCREATE TABLE IF NOT EXISTS key (\n    id uuid PRIMARY KEY,\n    expiration timestamptz,\n    pub_key bytea\n);\n\n"
  },
  {
    "path": "notifier/migrations/02-constraints.sql",
    "content": "ALTER TABLE notification_body\n    DROP CONSTRAINT notification_body_notification_id_fkey,\n    ADD CONSTRAINT notification_body_notification_id_fkey FOREIGN KEY (notification_id) REFERENCES notification (id) ON DELETE CASCADE;\n\n"
  },
  {
    "path": "notifier/migrations/03-constraints.sql",
    "content": "ALTER TABLE receipt\n    DROP CONSTRAINT receipt_notification_id_fkey,\n    DROP CONSTRAINT receipt_uo_id_fkey,\n    ADD CONSTRAINT receipt_notification_id_fkey FOREIGN KEY (notification_id) REFERENCES notification (id) ON DELETE CASCADE,\n    ADD CONSTRAINT receipt_uo_id_fkey FOREIGN KEY (uo_id) REFERENCES notifier_update_operation (uo_id) ON DELETE CASCADE;\n\n"
  },
  {
    "path": "notifier/migrations/04-drop-key.sql",
    "content": "-- Drop the key table, as all of its users have been removed.\nDROP TABLE key;\n\n"
  },
  {
    "path": "notifier/migrations/migrations.go",
    "content": "package migrations\n\nimport (\n\t\"database/sql\"\n\t\"embed\"\n\n\t\"github.com/remind101/migrate\"\n)\n\n//go:embed *.sql\nvar fs embed.FS\n\nfunc runFile(n string) func(*sql.Tx) error {\n\tb, err := fs.ReadFile(n)\n\treturn func(tx *sql.Tx) error {\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif _, err := tx.Exec(string(b)); err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn nil\n\t}\n}\n\nconst MigrationTable = \"notifier_migrations\"\n\nvar Migrations = []migrate.Migration{\n\t{\n\t\tID: 1,\n\t\tUp: runFile(\"01-init.sql\"),\n\t},\n\t{\n\t\tID: 2,\n\t\tUp: runFile(\"02-constraints.sql\"),\n\t},\n\t{\n\t\tID: 3,\n\t\tUp: runFile(\"03-constraints.sql\"),\n\t},\n\t// This can be uncommented once 4.4 is released and 4.1 is gone.\n\t/*\n\t\t{\n\t\t\tID: 4,\n\t\t\tUp: runFile(\"04-drop-key.sql\"),\n\t\t},\n\t*/\n}\n"
  },
  {
    "path": "notifier/mockstore.go",
    "content": "package notifier\n\nimport (\n\t\"context\"\n\n\t\"github.com/google/uuid\"\n)\n\n// MockStore implements a mock Store.\ntype MockStore struct {\n\tNotifications_         func(ctx context.Context, id uuid.UUID, page *Page) ([]Notification, Page, error)\n\tPutNotifications_      func(ctx context.Context, opts PutOpts) error\n\tPutReceipt_            func(ctx context.Context, updater string, r Receipt) error\n\tCollectNotitfications_ func(ctx context.Context) error\n\tReceipt_               func(ctx context.Context, id uuid.UUID) (Receipt, error)\n\tReceiptByUOID_         func(ctx context.Context, id uuid.UUID) (Receipt, error)\n\tCreated_               func(ctx context.Context) ([]uuid.UUID, error)\n\tFailed_                func(ctx context.Context) ([]uuid.UUID, error)\n\tDeleted_               func(ctx context.Context) ([]uuid.UUID, error)\n\tSetDelivered_          func(ctx context.Context, id uuid.UUID) error\n\tSetDeliveredFailed_    func(ctx context.Context, id uuid.UUID) error\n\tSetDeleted_            func(ctx context.Context, id uuid.UUID) error\n}\n\n// Notifications retrieves the list of notifications associated with a\n// notification id\nfunc (m *MockStore) Notifications(ctx context.Context, id uuid.UUID, page *Page) ([]Notification, Page, error) {\n\treturn m.Notifications_(ctx, id, page)\n}\n\n// PutNotifications persists the provided notifications and associates\n// them with the provided notification id\n//\n// PutNotifications must update the latest update operation for the provided\n// updater in such a way that UpdateOperation returns the provided update\n// operation id when queried with the updater name\n//\n// PutNotifications must create a Receipt with status created status on\n// successful persistence of notifications in such a way that Receipter.Created()\n// returns the persisted notification id.\nfunc (m *MockStore) PutNotifications(ctx context.Context, opts PutOpts) error {\n\treturn m.PutNotifications_(ctx, opts)\n}\n\n// PutReceipt allows for the caller to directly add a receipt to the store\n// without notifications being created.\n//\n// After this method returns all methods on the Receipter interface must work accordingly.\nfunc (m *MockStore) PutReceipt(ctx context.Context, updater string, r Receipt) error {\n\treturn m.PutReceipt_(ctx, updater, r)\n}\n\n// CollectNotifications garbage collects all notifications.\nfunc (m *MockStore) CollectNotifications(ctx context.Context) error {\n\treturn m.CollectNotitfications_(ctx)\n}\n\n// Receipt returns the Receipt for a given notification id\nfunc (m *MockStore) Receipt(ctx context.Context, id uuid.UUID) (Receipt, error) {\n\treturn m.Receipt_(ctx, id)\n}\n\n// ReceiptByUOID returns the Receipt for a given UOID\nfunc (m *MockStore) ReceiptByUOID(ctx context.Context, id uuid.UUID) (Receipt, error) {\n\treturn m.ReceiptByUOID_(ctx, id)\n}\n\n// Created returns a slice of notification ids in created status\nfunc (m *MockStore) Created(ctx context.Context) ([]uuid.UUID, error) {\n\treturn m.Created_(ctx)\n}\n\n// Failed returns a slice of notification ids in failed status\nfunc (m *MockStore) Failed(ctx context.Context) ([]uuid.UUID, error) {\n\treturn m.Failed_(ctx)\n}\n\n// Deleted returns a slice of notification ids in deleted status\nfunc (m *MockStore) Deleted(ctx context.Context) ([]uuid.UUID, error) {\n\treturn m.Deleted_(ctx)\n}\n\n// SetDelivered marks the provided notification id as delivered\nfunc (m *MockStore) SetDelivered(ctx context.Context, id uuid.UUID) error {\n\treturn m.SetDelivered_(ctx, id)\n}\n\n// SetDeliveryFailed marks the provided notification id failed to be delivere\nfunc (m *MockStore) SetDeliveryFailed(ctx context.Context, id uuid.UUID) error {\n\treturn m.SetDeliveredFailed_(ctx, id)\n}\n\n// SetDeleted marks the provided notification id as deleted\nfunc (m *MockStore) SetDeleted(ctx context.Context, id uuid.UUID) error {\n\treturn m.SetDeleted_(ctx, id)\n}\n"
  },
  {
    "path": "notifier/notification.go",
    "content": "package notifier\n\nimport (\n\t\"github.com/google/uuid\"\n\t\"github.com/quay/claircore\"\n)\n\n// Reason indicates the catalyst for a notification.\ntype Reason string\n\nconst (\n\tAdded   Reason = \"added\"\n\tRemoved Reason = \"removed\"\n\tChanged Reason = \"changed\"\n)\n\n// Notification summarizes a change in the vulnerabilities affecting a manifest.\n//\n// The mentioned Vulnerability will be the most severe vulnerability discovered\n// in an update operation.\n//\n// Receiving clients are expected to filter notifications by severity in such a\n// way that they receive all vulnerabilities at or above a particular\n// claircore.Severity level.\ntype Notification struct {\n\tID            uuid.UUID        `json:\"id\"`\n\tManifest      claircore.Digest `json:\"manifest\"`\n\tReason        Reason           `json:\"reason\"`\n\tVulnerability VulnSummary      `json:\"vulnerability\"`\n}\n"
  },
  {
    "path": "notifier/notificationhandle.go",
    "content": "package notifier\n\nimport \"github.com/google/uuid\"\n\n// NotificationHandle is a handle a client may use to\n// retrieve a list of associated notification models\ntype NotificationHandle struct {\n\tID uuid.UUID `json:\"id\"`\n}\n"
  },
  {
    "path": "notifier/pager.go",
    "content": "package notifier\n\nimport (\n\t\"github.com/google/uuid\"\n)\n\n// Page communicates a bare-minimum paging protocol with clients.\ntype Page struct {\n\t// the next id to retrieve\n\tNext *uuid.UUID `json:\"next,omitempty\"`\n\t// the max number of elements returned in a page\n\tSize int `json:\"size\"`\n}\n"
  },
  {
    "path": "notifier/poller.go",
    "content": "package notifier\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"log/slog\"\n\t\"time\"\n\n\t\"github.com/quay/claircore/libvuln/driver\"\n\n\tclairerror \"github.com/quay/clair/v4/clair-error\"\n\t\"github.com/quay/clair/v4/matcher\"\n)\n\nconst (\n\t// max number of UOIDs that we will queue in a channel\n\tMaxChanSize = 1024\n)\n\n// PollerOpt applies a configuration to a Poller\ntype PollerOpt func(*Poller) error\n\n// Poller implements new Update Operation discovery via an event channel.\ntype Poller struct {\n\t// a store to retrieve known UOIDs and compare\n\t// with the polled UOIDs.\n\tstore Store\n\t// a differ to retrieve latest update operations\n\tdiffer matcher.Differ\n\t// the interval to poll a Matcher node.\n\tinterval time.Duration\n}\n\nfunc NewPoller(store Store, differ matcher.Differ, interval time.Duration) *Poller {\n\treturn &Poller{\n\t\tinterval: interval,\n\t\tstore:    store,\n\t\tdiffer:   differ,\n\t}\n}\n\n// Event is delivered on the poller's channel when a new UpdateOperation is\n// discovered.\ntype Event struct {\n\tupdater string\n\tuo      driver.UpdateOperation\n}\n\n// Poll begins polling the Matcher for UpdateOperations and producing Events on\n// the supplied channel. This method takes ownership of the channel and is\n// responsible for closing it.\n//\n// Cancel ctx to stop the poller.\nfunc (p *Poller) Poll(ctx context.Context, c chan<- Event) error {\n\tdefer close(c)\n\tif err := ctx.Err(); err != nil {\n\t\tslog.InfoContext(ctx, \"context canceled before polling began\")\n\t\treturn err\n\t}\n\n\t// loop on interval tick\n\tt := time.NewTicker(p.interval)\n\tdefer t.Stop()\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\tslog.InfoContext(ctx, \"context canceled; polling ended\")\n\t\t\treturn ctx.Err()\n\t\tcase <-t.C:\n\t\t\tslog.DebugContext(ctx, \"poll interval tick\")\n\t\t\tp.onTick(ctx, c)\n\t\t}\n\t}\n}\n\n// OnTick retrieves the latest update operations for all known updaters and\n// delivers an event if notification creation is necessary.\nfunc (p *Poller) onTick(ctx context.Context, c chan<- Event) {\n\tlatest, err := p.differ.LatestUpdateOperations(ctx, driver.VulnerabilityKind)\n\tif err != nil {\n\t\tslog.WarnContext(ctx, \"client error retrieving latest update operations\",\n\t\t\t\"reason\", err)\n\t\treturn\n\t}\n\n\tfor updater, uo := range latest {\n\t\tlog := slog.With(\"updater\", updater)\n\t\tif len(uo) == 0 {\n\t\t\tlog.DebugContext(ctx, \"received 0 update operations after polling Matcher\")\n\t\t\treturn // Should this be a continue?\n\t\t}\n\t\tlatest := uo[0]\n\t\tlog = log.With(\"UOID\", latest.Ref)\n\t\t// confirm notifications were never created for this UOID.\n\t\tvar errNoReceipt *clairerror.ErrNoReceipt\n\t\t_, err := p.store.ReceiptByUOID(ctx, latest.Ref)\n\t\tif errors.As(err, &errNoReceipt) {\n\t\t\te := Event{\n\t\t\t\tupdater: updater,\n\t\t\t\tuo:      latest,\n\t\t\t}\n\t\t\tselect {\n\t\t\tcase c <- e:\n\t\t\tdefault:\n\t\t\t\tlog.WarnContext(ctx, \"could not deliver event to channel; skipping updater\")\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tif err != nil {\n\t\t\tlog.WarnContext(ctx, \"received error getting receipt by UOID\",\n\t\t\t\t\"reason\", err)\n\t\t\treturn\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "notifier/postgres/e2e_test.go",
    "content": "package postgres\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"math/rand\"\n\t\"sort\"\n\t\"strconv\"\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/quay/claircore\"\n\tcctest \"github.com/quay/claircore/test\"\n\t\"github.com/quay/claircore/test/integration\"\n\n\t\"github.com/quay/clair/v4/notifier\"\n)\n\n// TestE2E performs an end to end test ensuring creating, retrieving,\n// bookkeeping, and deleting of notifications and associated data works\n// correctly.\nfunc TestE2E(t *testing.T) {\n\tintegration.NeedDB(t)\n\tctx := cctest.Logging(t)\n\tfor _, e := range []*e2e{\n\t\tNewE2E(ctx, t, 1),\n\t\tNewE2E(ctx, t, 10),\n\t\tNewE2E(ctx, t, 100),\n\t} {\n\t\tt.Run(strconv.Itoa(len(e.notifications)), func(t *testing.T) {\n\t\t\tctx := cctest.Logging(t, ctx)\n\t\t\te.Run(ctx, t)\n\t\t})\n\t}\n}\n\n// e2e is a series of test cases for handling notification\n// persistence.\n//\n// each method on e2e fulfills the expected function signature\n// for t.Run() and is thus eligible to be used as a sub-test.\n//\n// e2e.Run drives the subtest order and will fail on first subtest\n// failure\ntype e2e struct {\n\t// the updater associated with the set of notifications under test\n\tupdater string\n\t// a store instance implementing notification persistence methods\n\tstore *Store\n\t// the notifications this test persist and retrieves\n\tnotifications []notifier.Notification\n\t// the notification ID this e2e test will use\n\tnotificationID uuid.UUID\n\t// the update operation ID associated with the set of notifications under test\n\tupdateID uuid.UUID\n}\n\nfunc NewE2E(ctx context.Context, t *testing.T, ct int) *e2e {\n\tupdater := fmt.Sprintf(\"%s-%d\", t.Name(), ct)\n\tid := uuid.New()\n\tvs := cctest.GenUniqueVulnerabilities(ct, updater)\n\tns := make([]notifier.Notification, len(vs))\n\tfor i, v := range vs {\n\t\tn := &ns[i]\n\t\tn.Manifest = cctest.RandomSHA256Digest(t)\n\t\tswitch rand.Intn(3) {\n\t\tcase 0:\n\t\t\tn.Reason = notifier.Added\n\t\tcase 1:\n\t\t\tn.Reason = notifier.Changed\n\t\tcase 2:\n\t\t\tn.Reason = notifier.Removed\n\t\t}\n\t\tn.Vulnerability.FromVulnerability(v)\n\t}\n\te := e2e{\n\t\tnotificationID: id,\n\t\tupdater:        updater,\n\t\tupdateID:       uuid.New(),\n\t\tnotifications:  ns,\n\t}\n\treturn &e\n}\n\nfunc (e *e2e) Run(ctx context.Context, t *testing.T) {\n\te.store = TestingStore(ctx, t)\n\ttype subtest struct {\n\t\tdo   func(context.Context) func(t *testing.T)\n\t\tname string\n\t}\n\tfor _, sub := range [...]subtest{\n\t\t{name: \"PutNotifications\", do: e.PutNotifications},\n\t\t{name: \"Created\", do: e.Created},\n\t\t{name: \"Notifications\", do: e.Notifications},\n\t\t{name: \"SetDelivered\", do: e.SetDelivered},\n\t\t{name: \"SetDeliveryFailed\", do: e.SetDeliveryFailed},\n\t\t{name: \"SetDeleted\", do: e.SetDeleted},\n\t\t{name: \"PutReceipt\", do: e.PutReceipt},\n\t\t{name: \"CollectNotifications\", do: e.CollectNotifications},\n\t} {\n\t\tt.Run(sub.name, sub.do(ctx))\n\t\tif t.Failed() {\n\t\t\tt.FailNow()\n\t\t}\n\t}\n}\n\n// PutNotifications adds a set of notifications to the database and confirms no\n// error occurs.\nfunc (e *e2e) PutNotifications(ctx context.Context) func(*testing.T) {\n\treturn func(t *testing.T) {\n\t\tctx := cctest.Logging(t, ctx)\n\t\topts := notifier.PutOpts{\n\t\t\tUpdater:        e.updater,\n\t\t\tNotificationID: e.notificationID,\n\t\t\tNotifications:  e.notifications,\n\t\t\tUpdateID:       e.updateID,\n\t\t}\n\t\tif err := e.store.PutNotifications(ctx, opts); err != nil {\n\t\t\tt.Error(err)\n\t\t}\n\t}\n}\n\n// Created ensures the expected notification ID is returned when persistence\n// layer is queried for all created, a specific receipt, or a receipt by UOID.\nfunc (e *e2e) Created(ctx context.Context) func(*testing.T) {\n\treturn func(t *testing.T) {\n\t\tctx := cctest.Logging(t, ctx)\n\t\tids, err := e.store.Created(ctx)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t}\n\t\tif got, want := len(ids), 1; got != want {\n\t\t\tt.Errorf(\"got: %d, want: %d\", got, want)\n\t\t}\n\t\twant := e.notificationID\n\t\tif got := ids[0]; !cmp.Equal(got, want) {\n\t\t\tt.Error(cmp.Diff(got, want))\n\t\t}\n\n\t\tr, err := e.store.Receipt(ctx, want)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t}\n\t\tif got := r.NotificationID; !cmp.Equal(got, want) {\n\t\t\tt.Error(cmp.Diff(got, want))\n\t\t}\n\t\tif got, want := r.Status, notifier.Created; !cmp.Equal(got, want) {\n\t\t\tt.Error(cmp.Diff(got, want))\n\t\t}\n\n\t\tr, err = e.store.ReceiptByUOID(ctx, e.updateID)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t}\n\t\tif got := r.NotificationID; !cmp.Equal(got, want) {\n\t\t\tt.Error(cmp.Diff(got, want))\n\t\t}\n\t\tif got, want := r.Status, notifier.Created; !cmp.Equal(got, want) {\n\t\t\tt.Error(cmp.Diff(got, want))\n\t\t}\n\t}\n}\n\n// Notifications confirms the correct notifications were returned from the\n// database when providing the notification ID.\nfunc (e *e2e) Notifications(ctx context.Context) func(*testing.T) {\n\treturn func(t *testing.T) {\n\t\tctx := cctest.Logging(t, ctx)\n\t\twant := e.notificationID\n\t\tinner := func(p *notifier.Page) func(*testing.T) {\n\t\t\treturn func(t *testing.T) {\n\t\t\t\tctx := cctest.Logging(t, ctx)\n\t\t\t\tvar ns []notifier.Notification\n\t\t\t\tfor {\n\t\t\t\t\trs, np, err := e.store.Notifications(ctx, want, p)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Error(err)\n\t\t\t\t\t}\n\t\t\t\t\tns = append(ns, rs...)\n\t\t\t\t\tif np.Next == nil {\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t\tp = &np\n\t\t\t\t}\n\t\t\t\tif got, want := len(ns), len(e.notifications); got != want {\n\t\t\t\t\tt.Errorf(\"got: %d, want: %d\", got, want)\n\t\t\t\t}\n\t\t\t\topts := cmp.Options{\n\t\t\t\t\tcmpopts.IgnoreUnexported(claircore.Digest{}),\n\t\t\t\t\tcmpopts.IgnoreFields(claircore.Vulnerability{}, \"ID\"),\n\t\t\t\t\tcmp.Transformer(\"Sort\", func(in []notifier.Notification) []notifier.Notification {\n\t\t\t\t\t\tout := make([]notifier.Notification, len(in))\n\t\t\t\t\t\tcopy(out, in)\n\t\t\t\t\t\tsort.Slice(out, func(i, j int) bool {\n\t\t\t\t\t\t\treturn out[i].ID.String() < out[j].ID.String()\n\t\t\t\t\t\t})\n\t\t\t\t\t\treturn out\n\t\t\t\t\t}),\n\t\t\t\t}\n\t\t\t\tif got, want := ns, e.notifications; !cmp.Equal(got, want, opts) {\n\t\t\t\t\tt.Error(cmp.Diff(got, want, opts))\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tt.Run(\"NilPage\", inner(nil))\n\t\tt.Run(\"5Page\", inner(&notifier.Page{Size: 5}))\n\t\tt.Run(\"500Page\", inner(&notifier.Page{Size: 500}))\n\t}\n}\n\n// SetDelivered confirms a receipt for a notification ID can be set to\n// delivered.\nfunc (e *e2e) SetDelivered(ctx context.Context) func(*testing.T) {\n\treturn func(t *testing.T) {\n\t\tctx := cctest.Logging(t, ctx)\n\t\twant := e.notificationID\n\t\terr := e.store.SetDelivered(ctx, want)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t}\n\t\treceipt, err := e.store.Receipt(ctx, want)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t}\n\t\tif got := receipt.NotificationID; !cmp.Equal(got, want) {\n\t\t\tt.Error(cmp.Diff(got, want))\n\t\t}\n\t\tif got, want := receipt.Status, notifier.Delivered; !cmp.Equal(got, want) {\n\t\t\tt.Error(cmp.Diff(got, want))\n\t\t}\n\t}\n}\n\n// SetDeliveryFailed confirms a receipt for a notification ID can be set to\n// delivered.\nfunc (e *e2e) SetDeliveryFailed(ctx context.Context) func(*testing.T) {\n\treturn func(t *testing.T) {\n\t\tctx := cctest.Logging(t, ctx)\n\t\twant := e.notificationID\n\t\terr := e.store.SetDeliveryFailed(ctx, want)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t}\n\t\treceipt, err := e.store.Receipt(ctx, e.notificationID)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t}\n\t\tif got := receipt.NotificationID; !cmp.Equal(got, want) {\n\t\t\tt.Error(cmp.Diff(got, want))\n\t\t}\n\t\tif got, want := receipt.Status, notifier.DeliveryFailed; !cmp.Equal(got, want) {\n\t\t\tt.Error(cmp.Diff(got, want))\n\t\t}\n\t\tids, err := e.store.Failed(ctx)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t}\n\t\tif got, want := len(ids), 1; got != want {\n\t\t\tt.Errorf(\"got: %d, want: %d\", got, want)\n\t\t}\n\t\tif got := ids[0]; !cmp.Equal(got, want) {\n\t\t\tt.Error(cmp.Diff(got, want))\n\t\t}\n\t}\n}\n\n// SetDeleted ...\nfunc (e *e2e) SetDeleted(ctx context.Context) func(*testing.T) {\n\treturn func(t *testing.T) {\n\t\tctx := cctest.Logging(t, ctx)\n\t\twant := e.notificationID\n\t\terr := e.store.SetDeleted(ctx, want)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t}\n\t\treceipt, err := e.store.Receipt(ctx, want)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t}\n\t\tif got := receipt.NotificationID; !cmp.Equal(got, want) {\n\t\t\tt.Error(cmp.Diff(got, want))\n\t\t}\n\t\tif got, want := receipt.Status, notifier.Deleted; !cmp.Equal(got, want) {\n\t\t\tt.Error(cmp.Diff(got, want))\n\t\t}\n\t\tids, err := e.store.Deleted(ctx)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t}\n\t\tif got, want := len(ids), 1; got != want {\n\t\t\tt.Errorf(\"got: %d, want: %d\", got, want)\n\t\t}\n\t\tif got := ids[0]; !cmp.Equal(got, want) {\n\t\t\tt.Error(cmp.Diff(got, want))\n\t\t}\n\t}\n}\n\n// PutReceipt will confirm a receipt can be directly placed into the database.\nfunc (e *e2e) PutReceipt(ctx context.Context) func(*testing.T) {\n\treturn func(t *testing.T) {\n\t\tctx := cctest.Logging(t, ctx)\n\t\twant := notifier.Receipt{\n\t\t\tNotificationID: uuid.New(),\n\t\t\tUOID:           uuid.New(),\n\t\t\tStatus:         notifier.Delivered,\n\t\t}\n\t\terr := e.store.PutReceipt(ctx, \"test-updater\", want)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"failed to put receipt: %v\", err)\n\t\t}\n\n\t\tgot, err := e.store.Receipt(ctx, want.NotificationID)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t}\n\t\tif !cmp.Equal(got, want, cmpopts.IgnoreFields(got, \"TS\")) {\n\t\t\tt.Error(cmp.Diff(got, want))\n\t\t}\n\n\t\tgot, err = e.store.ReceiptByUOID(ctx, want.UOID)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t}\n\t\tif !cmp.Equal(got, want, cmpopts.IgnoreFields(got, \"TS\")) {\n\t\t\tt.Error(cmp.Diff(got, want))\n\t\t}\n\t}\n}\n\nfunc (e *e2e) CollectNotifications(ctx context.Context) func(*testing.T) {\n\tconst jump = `UPDATE receipt SET ts = (CURRENT_TIMESTAMP - INTERVAL '21 days') WHERE notification_id = $1;`\n\treturn func(t *testing.T) {\n\t\tctx := cctest.Logging(t, ctx)\n\t\tpool := e.store.pool\n\t\t// Jump our receipt back in time.\n\t\tif _, err := pool.Exec(ctx, jump, e.notificationID); err != nil {\n\t\t\tt.Error(err)\n\t\t}\n\n\t\tif err := e.store.CollectNotifications(ctx); err != nil {\n\t\t\tt.Error(err)\n\t\t}\n\n\t\tvar ct int\n\t\tif err := pool.QueryRow(ctx, `SELECT COUNT(id) FROM notification_body;`).Scan(&ct); err != nil {\n\t\t\tt.Error(err)\n\t\t}\n\t\tif got, want := ct, 0; got != want {\n\t\t\tt.Errorf(\"got: %d row remaining, wanted: %d rows remaining\", got, want)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "notifier/postgres/get_status.go",
    "content": "package postgres\n\nimport (\n\t\"context\"\n\n\t\"github.com/google/uuid\"\n\t\"github.com/jackc/pgx/v5\"\n\t\"github.com/jackc/pgx/v5/pgxpool\"\n\t\"github.com/prometheus/client_golang/prometheus\"\n\t\"github.com/prometheus/client_golang/prometheus/promauto\"\n)\n\nvar (\n\tcreatedCounter = promauto.NewCounterVec(\n\t\tprometheus.CounterOpts{\n\t\t\tNamespace: \"clair\",\n\t\t\tSubsystem: \"notifier\",\n\t\t\tName:      \"created_total\",\n\t\t\tHelp:      \"Total number of database queries issued in the created method\",\n\t\t},\n\t\t[]string{\"query\", \"error\"},\n\t)\n\tcreatedDuration = promauto.NewHistogramVec(\n\t\tprometheus.HistogramOpts{\n\t\t\tNamespace: \"clair\",\n\t\t\tSubsystem: \"notifier\",\n\t\t\tName:      \"created_duration_seconds\",\n\t\t\tHelp:      \"Duration of all queries issued in the created method\",\n\t\t},\n\t\t[]string{\"query\", \"error\"},\n\t)\n\tfailedCounter = promauto.NewCounterVec(\n\t\tprometheus.CounterOpts{\n\t\t\tNamespace: \"clair\",\n\t\t\tSubsystem: \"notifier\",\n\t\t\tName:      \"failed_total\",\n\t\t\tHelp:      \"Total number of database queries issued in the failed method\",\n\t\t},\n\t\t[]string{\"query\", \"error\"},\n\t)\n\tfailedDuration = promauto.NewHistogramVec(\n\t\tprometheus.HistogramOpts{\n\t\t\tNamespace: \"clair\",\n\t\t\tSubsystem: \"notifier\",\n\t\t\tName:      \"failed_duration_seconds\",\n\t\t\tHelp:      \"Duration of all queries issued in the failed method\",\n\t\t},\n\t\t[]string{\"query\", \"error\"},\n\t)\n\tdeletedCounter = promauto.NewCounterVec(\n\t\tprometheus.CounterOpts{\n\t\t\tNamespace: \"clair\",\n\t\t\tSubsystem: \"notifier\",\n\t\t\tName:      \"deleted_total\",\n\t\t\tHelp:      \"Total number of database queries issued in the deleted method\",\n\t\t},\n\t\t[]string{\"query\", \"error\"},\n\t)\n\tdeletedDuration = promauto.NewHistogramVec(\n\t\tprometheus.HistogramOpts{\n\t\t\tNamespace: \"clair\",\n\t\t\tSubsystem: \"notifier\",\n\t\t\tName:      \"deleted_duration_seconds\",\n\t\t\tHelp:      \"Duration of all queries issued in the deleted method\",\n\t\t},\n\t\t[]string{\"query\", \"error\"},\n\t)\n)\n\nfunc (s *Store) getStatus(ctx context.Context, status string, m statusMetrics) ([]uuid.UUID, error) {\n\tconst (\n\t\tquery = `SELECT notification_id FROM receipt WHERE status = $1::receiptstatus;`\n\t)\n\n\tids := []uuid.UUID{}\n\terr := s.pool.AcquireFunc(ctx, func(c *pgxpool.Conn) error {\n\t\tvar err error\n\t\tvar rows pgx.Rows\n\t\ttimer := prometheus.NewTimer(prometheus.ObserverFunc(func(v float64) {\n\t\t\tm.dur.WithLabelValues(`query`, errLabel(err)).Observe(v)\n\t\t}))\n\t\tdefer timer.ObserveDuration()\n\t\trows, err = c.Query(ctx, query, status)\n\t\tm.counter.WithLabelValues(`query`, errLabel(err)).Add(1)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tvar id uuid.UUID\n\t\tfor rows.Next() {\n\t\t\tif err := rows.Scan(&id); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tids = append(ids, id)\n\t\t}\n\t\tif err := rows.Err(); err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn ids, nil\n}\n\n// Created will return all notification ids in \"created\" status.\nfunc (s *Store) Created(ctx context.Context) ([]uuid.UUID, error) {\n\tids, err := s.getStatus(ctx, `created`, statusMetrics{\n\t\tcounter: createdCounter,\n\t\tdur:     createdDuration,\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn ids, nil\n}\n\n// Failed will return all notification ids in \"delivery_failed\" status.\nfunc (s *Store) Failed(ctx context.Context) ([]uuid.UUID, error) {\n\tids, err := s.getStatus(ctx, `delivery_failed`, statusMetrics{\n\t\tcounter: failedCounter,\n\t\tdur:     failedDuration,\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn ids, nil\n}\n\n// Deleted will return all notification ids in \"deleted\" status.\nfunc (s *Store) Deleted(ctx context.Context) ([]uuid.UUID, error) {\n\tids, err := s.getStatus(ctx, `deleted`, statusMetrics{\n\t\tcounter: deletedCounter,\n\t\tdur:     deletedDuration,\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn ids, nil\n}\n"
  },
  {
    "path": "notifier/postgres/notifications.go",
    "content": "package postgres\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\n\t\"github.com/google/uuid\"\n\t\"github.com/jackc/pgx/v5\"\n\t\"github.com/jackc/pgx/v5/pgxpool\"\n\t\"github.com/prometheus/client_golang/prometheus\"\n\t\"github.com/prometheus/client_golang/prometheus/promauto\"\n\n\tclairerror \"github.com/quay/clair/v4/clair-error\"\n\t\"github.com/quay/clair/v4/notifier\"\n)\n\nvar (\n\tnotificationsCounter = promauto.NewCounterVec(\n\t\tprometheus.CounterOpts{\n\t\t\tNamespace: \"clair\",\n\t\t\tSubsystem: \"notifier\",\n\t\t\tName:      \"notifications_total\",\n\t\t\tHelp:      \"Total number of database queries issued in the notifications method\",\n\t\t},\n\t\t[]string{\"query\", \"error\"},\n\t)\n\tnotificationsDuration = promauto.NewHistogramVec(\n\t\tprometheus.HistogramOpts{\n\t\t\tNamespace: \"clair\",\n\t\t\tSubsystem: \"notifier\",\n\t\t\tName:      \"notifications_duration_seconds\",\n\t\t\tHelp:      \"Duration of all queries issued in the notifications method\",\n\t\t},\n\t\t[]string{\"query\", \"error\"},\n\t)\n\n\tgcNotificationCounter = promauto.NewCounterVec(\n\t\tprometheus.CounterOpts{\n\t\t\tNamespace: \"clair\",\n\t\t\tSubsystem: \"notifier\",\n\t\t\tName:      \"collectnotification_query_total\",\n\t\t\tHelp:      \"Total number of database queries issued in the CollectNotification method\",\n\t\t},\n\t\t[]string{\"query\", \"error\"},\n\t)\n\tgcNotificationDuration = promauto.NewHistogramVec(\n\t\tprometheus.HistogramOpts{\n\t\t\tNamespace: \"clair\",\n\t\t\tSubsystem: \"notifier\",\n\t\t\tName:      \"collectnotification_duration_seconds\",\n\t\t\tHelp:      \"Duration of all queries issued in the CollectNotification method\",\n\t\t},\n\t\t[]string{\"query\", \"error\"},\n\t)\n\tgcNotificationAffected = promauto.NewCounterVec(\n\t\tprometheus.CounterOpts{\n\t\t\tNamespace: \"clair\",\n\t\t\tSubsystem: \"notifier\",\n\t\t\tName:      \"collectnotification_affected_total\",\n\t\t\tHelp:      \"Total number of rows affected in the CollectNotification method\",\n\t\t},\n\t\t[]string{\"query\", \"error\"},\n\t)\n\n\tputNotificationsCounter = promauto.NewCounterVec(\n\t\tprometheus.CounterOpts{\n\t\t\tNamespace: \"clair\",\n\t\t\tSubsystem: \"notifier\",\n\t\t\tName:      \"putnotifications_query_total\",\n\t\t\tHelp:      \"Total number of database queries issued in the PutNotifications method\",\n\t\t},\n\t\t[]string{\"query\", \"error\"},\n\t)\n\tputNotificationsDuration = promauto.NewHistogramVec(\n\t\tprometheus.HistogramOpts{\n\t\t\tNamespace: \"clair\",\n\t\t\tSubsystem: \"notifier\",\n\t\t\tName:      \"putnotifications_duration_seconds\",\n\t\t\tHelp:      \"Duration of all queries issued in the PutNotifications method\",\n\t\t},\n\t\t[]string{\"query\", \"error\"},\n\t)\n\tputNotificationsAffected = promauto.NewCounterVec(\n\t\tprometheus.CounterOpts{\n\t\t\tNamespace: \"clair\",\n\t\t\tSubsystem: \"notifier\",\n\t\t\tName:      \"putnotifications_affected_total\",\n\t\t\tHelp:      \"Total number of rows affected in the PutNotifications method\",\n\t\t},\n\t\t[]string{\"query\", \"error\"},\n\t)\n)\n\n// Notifications retrieves the list of notifications associated with a\n// notification ID.\nfunc (s *Store) Notifications(ctx context.Context, id uuid.UUID, page *notifier.Page) ([]notifier.Notification, notifier.Page, error) {\n\tconst (\n\t\tquery      = \"SELECT id, body FROM notification_body WHERE notification_id = $1::uuid\"\n\t\tpagedQuery = \"SELECT id, body FROM notification_body WHERE notification_id = $1::uuid AND id > $2 ORDER BY id ASC LIMIT $3\"\n\t)\n\n\t// If no page argument, early return all notifications.\n\tif page == nil {\n\t\tp := notifier.Page{}\n\t\tns := make([]notifier.Notification, 0)\n\t\terr := s.pool.AcquireFunc(ctx, func(c *pgxpool.Conn) error {\n\t\t\tvar err error\n\t\t\ttimer := prometheus.NewTimer(prometheus.ObserverFunc(func(v float64) {\n\t\t\t\tnotificationsDuration.WithLabelValues(`query`, errLabel(err)).Observe(v)\n\t\t\t}))\n\t\t\tdefer timer.ObserveDuration()\n\t\t\tvar rows pgx.Rows\n\t\t\trows, err = c.Query(ctx, query, id)\n\t\t\tnotificationsCounter.WithLabelValues(`query`, errLabel(err)).Add(1)\n\t\t\tfor rows.Next() {\n\t\t\t\tns = append(ns, notifier.Notification{})\n\t\t\t\tn := &ns[len(ns)-1]\n\t\t\t\tif err := rows.Scan(&n.ID, n); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif err := rows.Err(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\treturn nil\n\t\t})\n\t\tif err != nil {\n\t\t\treturn nil, p, &clairerror.ErrBadNotification{\n\t\t\t\tNotificationID: id,\n\t\t\t\tE:              err,\n\t\t\t}\n\t\t}\n\t\treturn ns, p, nil\n\t}\n\n\t// Page.Next being nil indicates a client's first request for a paged set of\n\t// notifications.\n\tif page.Next == nil {\n\t\tpage.Next = &uuid.Nil\n\t}\n\t// If asking for a weird number of results, just error.\n\tif page.Size < 1 {\n\t\treturn nil, notifier.Page{}, &clairerror.ErrBadNotification{\n\t\t\tNotificationID: id,\n\t\t\tE:              fmt.Errorf(\"bad page size: %d\", page.Size),\n\t\t}\n\t}\n\t// Add one to limit to determine if there is another page to fetch.\n\tlimit := page.Size + 1\n\n\tns := make([]notifier.Notification, 0, limit)\n\terr := s.pool.AcquireFunc(ctx, func(c *pgxpool.Conn) error {\n\t\tvar err error\n\t\ttimer := prometheus.NewTimer(prometheus.ObserverFunc(func(v float64) {\n\t\t\tnotificationsDuration.WithLabelValues(`pagedQuery`, errLabel(err)).Observe(v)\n\t\t}))\n\t\tdefer timer.ObserveDuration()\n\t\tvar rows pgx.Rows\n\t\trows, err = c.Query(ctx, pagedQuery, id, page.Next, limit)\n\t\tnotificationsCounter.WithLabelValues(`pagedQuery`, errLabel(err)).Add(1)\n\t\tfor rows.Next() {\n\t\t\tns = append(ns, notifier.Notification{})\n\t\t\tn := &ns[len(ns)-1]\n\t\t\tif err := rows.Scan(&n.ID, n); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\tif err := rows.Err(); err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\treturn nil, notifier.Page{}, &clairerror.ErrBadNotification{\n\t\t\tNotificationID: id,\n\t\t\tE:              err,\n\t\t}\n\t}\n\n\t// Page to return to client.\n\toutPage := notifier.Page{Size: page.Size}\n\tif len(ns) == limit {\n\t\t// Slice off the last element as it was only an indicator that another\n\t\t// page should be delivered.\n\t\t//\n\t\t// Set outPage.Next to the final element id being returned to the\n\t\t// caller.\n\t\tns = ns[:page.Size]\n\t\toutPage.Next = &(ns[len(ns)-1].ID)\n\t}\n\treturn ns, outPage, nil\n}\n\n// PutNotifications persists the provided notifications and associates them with\n// the provided notification ID.\n//\n// PutNotifications must update the latest update operation for the provided\n// updater in such a way that UpdateOperation returns the provided update\n// operation ID when queried with the updater name.\n//\n// PutNotifications must create a Receipt with status created status on\n// successful persistence of notifications in such a way that\n// Receipter.Created() returns the persisted notification ID.\nfunc (s *Store) PutNotifications(ctx context.Context, opts notifier.PutOpts) error {\n\tconst (\n\t\tinsertNotification    = `INSERT INTO notification (id) VALUES ($1);`\n\t\tinsertUpdateOperation = `INSERT INTO notifier_update_operation (updater, uo_id, ts) VALUES ($1, $2, CURRENT_TIMESTAMP);`\n\t\tinsertReceipt         = `INSERT INTO receipt (notification_id, uo_id, status, ts) VALUES ($1, $2, 'created', CURRENT_TIMESTAMP);`\n\t)\n\ttxOpt := pgx.TxOptions{\n\t\tIsoLevel:   pgx.ReadCommitted,\n\t\tAccessMode: pgx.ReadWrite,\n\t}\n\tmetrics := statusMetrics{\n\t\tdur:      putNotificationsDuration,\n\t\tcounter:  putNotificationsCounter,\n\t\taffected: putNotificationsAffected,\n\t}\n\n\terr := pgx.BeginTxFunc(ctx, s.pool, txOpt, func(tx pgx.Tx) error {\n\t\tif err := txExec(ctx, metrics, tx,\n\t\t\t`insertNotification`, insertNotification,\n\t\t\t[]interface{}{opts.NotificationID}); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif err := func() error {\n\t\t\tconst name = `copyNotificationBody`\n\t\t\t// Batch insert via the Copy API. This needs its own little closure\n\t\t\t// here because it's using the lower-level API.\n\t\t\tsrc := copyNotifications(&opts.NotificationID, opts.Notifications)\n\t\t\tvar err error\n\t\t\ttimer := prometheus.NewTimer(prometheus.ObserverFunc(func(v float64) {\n\t\t\t\tputNotificationsDuration.WithLabelValues(name, errLabel(err)).Observe(v)\n\t\t\t}))\n\t\t\tdefer timer.ObserveDuration()\n\t\t\tct, err := tx.CopyFrom(ctx, pgx.Identifier{\"notification_body\"}, src.Columns(), src)\n\t\t\tputNotificationsCounter.WithLabelValues(name, errLabel(err)).Add(1)\n\t\t\tputNotificationsAffected.WithLabelValues(name, errLabel(err)).Add(float64(ct))\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif got, want := ct, int64(len(opts.Notifications)); got != want {\n\t\t\t\treturn fmt.Errorf(\"inserted %d/%d rows\", got, want)\n\t\t\t}\n\t\t\treturn nil\n\t\t}(); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif err := txExec(ctx, metrics, tx,\n\t\t\t`insertUpdateOperation`, insertUpdateOperation,\n\t\t\t[]interface{}{opts.Updater, opts.UpdateID}); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif err := txExec(ctx, metrics, tx,\n\t\t\t`insertReceipt`, insertReceipt,\n\t\t\t[]interface{}{opts.NotificationID, opts.UpdateID}); err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\treturn &clairerror.ErrPutNotifications{\n\t\t\tNotificationID: opts.NotificationID,\n\t\t\tE:              err,\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc copyNotifications(id *uuid.UUID, ns []notifier.Notification) *notificationSource {\n\ts := notificationSource{\n\t\tid: id,\n\t\tns: ns,\n\t}\n\ts.enc = json.NewEncoder(&s.buf)\n\treturn &s\n}\n\ntype notificationSource struct {\n\terr error\n\tenc *json.Encoder\n\tid  *uuid.UUID\n\tbuf bytes.Buffer\n\tns  []notifier.Notification\n}\n\nfunc (r *notificationSource) Next() bool {\n\treturn len(r.ns) > 0\n}\n\nfunc (r *notificationSource) Values() ([]interface{}, error) {\n\tn := &r.ns[0]\n\tr.ns = r.ns[1:]\n\tn.ID = uuid.New()\n\tr.buf.Reset()\n\tif err := r.enc.Encode(n); err != nil {\n\t\tr.err = err\n\t\tr.ns = nil\n\t\treturn nil, err\n\t}\n\treturn []interface{}{n.ID, r.id, r.buf.Bytes()}, nil\n}\n\nfunc (r *notificationSource) Err() error {\n\tif r.err != nil {\n\t\treturn r.err\n\t}\n\treturn nil\n}\n\nfunc (r *notificationSource) Columns() []string {\n\treturn []string{\"id\", \"notification_id\", \"body\"}\n}\n\n// CollectNotifications garbage collects all notifications.\n//\n// Normally Receipter.SetDeleted will be issued first, however application logic\n// may decide to gc notifications which have not been set deleted after some\n// period of time, thus this condition should not be checked.\nfunc (s *Store) CollectNotifications(ctx context.Context) error {\n\tconst (\n\t\ttryLock            = `SELECT pg_try_advisory_xact_lock($1, $2);`\n\t\tdeleteNotification = `DELETE FROM notification USING receipt WHERE id = receipt.notification_id AND receipt.status = 'deleted'::receiptstatus;`\n\t\tdeleteUpdateOp     = `DELETE FROM notifier_update_operation\n\tWHERE uo_id IN (\n\t\tSELECT uo_id FROM notifier_update_operation\n\t\tEXCEPT\n\t\tSELECT uo_id FROM receipt);`\n\t\tdeleteReceipts = `DELETE FROM receipt\n\tWHERE\n\t\tts < date_trunc('day', (now() - INTERVAL '14 days'))\n\t\tAND\n\t\tstatus <> 'created'::receiptstatus;`\n\t)\n\ttxOpt := pgx.TxOptions{\n\t\tIsoLevel:   pgx.ReadCommitted,\n\t\tAccessMode: pgx.ReadWrite,\n\t}\n\tmetrics := statusMetrics{\n\t\tdur:      gcNotificationDuration,\n\t\tcounter:  gcNotificationCounter,\n\t\taffected: gcNotificationAffected,\n\t}\n\n\terr := pgx.BeginTxFunc(ctx, s.pool, txOpt, func(tx pgx.Tx) error {\n\t\tvar ok bool\n\t\tif err := tx.QueryRow(ctx, tryLock, adminKeyspace, gcLock).Scan(&ok); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif !ok {\n\t\t\t// unable to lock\n\t\t\treturn nil\n\t\t}\n\t\tif err := txExec(ctx, metrics, tx, \"deleteNotification\", deleteNotification, nil); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif err := txExec(ctx, metrics, tx, \"deleteReceipts\", deleteReceipts, nil); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif err := txExec(ctx, metrics, tx, \"deleteUpdateOp\", deleteUpdateOp, nil); err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "notifier/postgres/notifications_test.go",
    "content": "package postgres\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"math/rand\"\n\t\"strconv\"\n\t\"testing\"\n\n\t\"github.com/google/uuid\"\n\t\"github.com/jackc/pgx/v5\"\n\t\"github.com/quay/claircore/test\"\n\t\"github.com/quay/claircore/test/integration\"\n\n\t\"github.com/quay/clair/v4/notifier\"\n)\n\nfunc TestNotificationCopy(t *testing.T) {\n\tintegration.NeedDB(t)\n\tctx := test.Logging(t)\n\tfor _, tc := range []notificationCopyTestcase{\n\t\t{Count: 1},\n\t\t{Count: 10},\n\t\t{Count: 100},\n\t\t{Count: 1000},\n\t\t{Count: 10000},\n\t} {\n\t\ttc.Setup(t)\n\t\tt.Run(strconv.Itoa(tc.Count), tc.Func(ctx))\n\t}\n}\n\ntype notificationCopyTestcase struct {\n\tNotifications []notifier.Notification\n\tCount         int\n}\n\nfunc (n *notificationCopyTestcase) Setup(t testing.TB) {\n\tvs := test.GenUniqueVulnerabilities(n.Count, t.Name())\n\tns := make([]notifier.Notification, len(vs))\n\tfor i := range vs {\n\t\tn := &ns[i]\n\t\tn.Manifest = test.RandomSHA256Digest(t)\n\t\tswitch rand.Intn(3) {\n\t\tcase 0:\n\t\t\tn.Reason = notifier.Added\n\t\tcase 1:\n\t\t\tn.Reason = notifier.Changed\n\t\tcase 2:\n\t\t\tn.Reason = notifier.Removed\n\t\t}\n\t\tn.Vulnerability.FromVulnerability(vs[i])\n\t}\n\tn.Notifications = ns\n}\n\nfunc (n notificationCopyTestcase) Func(ctx context.Context) func(*testing.T) {\n\tid := uuid.New()\n\treturn func(t *testing.T) {\n\t\tctx := test.Logging(t, ctx)\n\t\tn.Setup(t)\n\t\tsrc := copyNotifications(&id, n.Notifications)\n\t\ts := TestingStore(ctx, t)\n\t\ttx, err := s.pool.Begin(ctx)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t}\n\t\tdefer func() {\n\t\t\tif err := tx.Rollback(ctx); err != nil && !errors.Is(err, pgx.ErrTxClosed) {\n\t\t\t\tt.Error(err)\n\t\t\t}\n\t\t}()\n\t\ttag, err := tx.Exec(ctx, `INSERT INTO notification (id) VALUES ($1);`, id)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t}\n\t\tif got, want := int(tag.RowsAffected()), 1; got != want {\n\t\t\tt.Errorf(\"got: %d, want: %d\", got, want)\n\t\t}\n\t\tct, err := tx.CopyFrom(ctx, pgx.Identifier{\"notification_body\"}, src.Columns(), src)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t}\n\t\tif got, want := int(ct), len(n.Notifications); got != want {\n\t\t\tt.Errorf(\"got: %d, want: %d\", got, want)\n\t\t}\n\t\tif err := tx.Commit(ctx); err != nil {\n\t\t\tt.Error(err)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "notifier/postgres/pagination_test.go",
    "content": "package postgres\n\nimport (\n\t\"testing\"\n\n\t\"github.com/google/uuid\"\n\t\"github.com/quay/claircore\"\n\t\"github.com/quay/claircore/test\"\n\t\"github.com/quay/claircore/test/integration\"\n\n\t\"github.com/quay/clair/v4/notifier\"\n)\n\n// TestPagination confirms paginating notifications works correctly.\nfunc TestPagination(t *testing.T) {\n\tintegration.NeedDB(t)\n\n\ttable := []struct {\n\t\t// name of test\n\t\tname string\n\t\t// total number of notifications to request\n\t\ttotal int\n\t\t// number of notifications per page to test\n\t\tpageSize int\n\t}{\n\t\t{\n\t\t\tname:     \"TotalZero\",\n\t\t\ttotal:    0,\n\t\t\tpageSize: 1,\n\t\t},\n\t\t{\n\t\t\tname:     \"PageOne\",\n\t\t\ttotal:    5,\n\t\t\tpageSize: 1,\n\t\t},\n\t\t{\n\t\t\tname:     \"Ones\",\n\t\t\ttotal:    1,\n\t\t\tpageSize: 1,\n\t\t},\n\t\t{\n\t\t\tname:     \"OddsGT\",\n\t\t\ttotal:    3,\n\t\t\tpageSize: 7,\n\t\t},\n\t\t{\n\t\t\tname:     \"OddsLT\",\n\t\t\ttotal:    7,\n\t\t\tpageSize: 3,\n\t\t},\n\t\t{\n\t\t\tname:     \"LT\",\n\t\t\ttotal:    1000,\n\t\t\tpageSize: 5,\n\t\t},\n\t\t{\n\t\t\tname:     \"GT\",\n\t\t\ttotal:    5,\n\t\t\tpageSize: 1000,\n\t\t},\n\t\t{\n\t\t\tname:     \"Large\",\n\t\t\ttotal:    5000,\n\t\t\tpageSize: 1000,\n\t\t},\n\t}\n\n\tfor _, tt := range table {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tctx := test.Logging(t)\n\t\t\tstore := TestingStore(ctx, t)\n\n\t\t\tnoteID := uuid.New()\n\t\t\tupdateID := uuid.New()\n\t\t\tmanifestHash := claircore.MustParseDigest(\"sha256:35c102085707f703de2d9eaad8752d6fe1b8f02b5d2149f1d8357c9cc7fb7d0a\")\n\n\t\t\tnotes := make([]notifier.Notification, 0, tt.total)\n\t\t\tfor i := 0; i < tt.total; i++ {\n\t\t\t\tnotes = append(notes, notifier.Notification{\n\t\t\t\t\tManifest: manifestHash,\n\t\t\t\t\tReason:   \"added\",\n\t\t\t\t})\n\t\t\t}\n\t\t\tt.Logf(\"inserting %v notes\", len(notes))\n\t\t\terr := store.PutNotifications(ctx, notifier.PutOpts{\n\t\t\t\tUpdater:        \"test-updater\",\n\t\t\t\tNotificationID: noteID,\n\t\t\t\tNotifications:  notes,\n\t\t\t\tUpdateID:       updateID,\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"failed to insert notifications: %v\", err)\n\t\t\t}\n\n\t\t\tinPage := notifier.Page{\n\t\t\t\tSize: tt.pageSize,\n\t\t\t}\n\n\t\t\ttotal := []notifier.Notification{}\n\t\t\treturned, outPage, err := store.Notifications(ctx, noteID, &inPage)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"failed to retrieve initial page: %v\", err)\n\t\t\t}\n\t\t\ttotal = append(total, returned...)\n\n\t\t\tfor outPage.Next != nil {\n\t\t\t\tif outPage.Size != tt.pageSize {\n\t\t\t\t\tt.Fatalf(\"got: %v, want: %v\", outPage.Size, tt.pageSize)\n\t\t\t\t}\n\t\t\t\tif len(returned) > tt.pageSize {\n\t\t\t\t\tt.Fatalf(\"got: %v, want: %v\", len(returned), tt.pageSize)\n\t\t\t\t}\n\t\t\t\treturned, outPage, err = store.Notifications(ctx, noteID, &outPage)\n\t\t\t\ttotal = append(total, returned...)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Error(err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif len(total) != tt.total {\n\t\t\t\tt.Fatalf(\"got: %v, want: %v\", len(total), tt.total)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "notifier/postgres/postgres_test.go",
    "content": "package postgres\n\nimport (\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/quay/claircore/test/integration\"\n)\n\nfunc TestMain(m *testing.M) {\n\tvar c int\n\tdefer func() { os.Exit(c) }()\n\tdefer integration.DBSetup()()\n\tc = m.Run()\n}\n"
  },
  {
    "path": "notifier/postgres/receipt.go",
    "content": "package postgres\n\nimport (\n\t\"context\"\n\t\"errors\"\n\n\t\"github.com/google/uuid\"\n\t\"github.com/jackc/pgx/v5\"\n\t\"github.com/jackc/pgx/v5/pgxpool\"\n\t\"github.com/prometheus/client_golang/prometheus\"\n\t\"github.com/prometheus/client_golang/prometheus/promauto\"\n\n\tclairerror \"github.com/quay/clair/v4/clair-error\"\n\t\"github.com/quay/clair/v4/notifier\"\n)\n\nvar (\n\treceiptCounter = promauto.NewCounterVec(\n\t\tprometheus.CounterOpts{\n\t\t\tNamespace: \"clair\",\n\t\t\tSubsystem: \"notifier\",\n\t\t\tName:      \"receipt_total\",\n\t\t\tHelp:      \"Total number of database queries issued in the receipt method\",\n\t\t},\n\t\t[]string{\"query\", \"error\"},\n\t)\n\treceiptDuration = promauto.NewHistogramVec(\n\t\tprometheus.HistogramOpts{\n\t\t\tNamespace: \"clair\",\n\t\t\tSubsystem: \"notifier\",\n\t\t\tName:      \"receipt_duration_seconds\",\n\t\t\tHelp:      \"Duration of all queries issued in the receipt method\",\n\t\t},\n\t\t[]string{\"query\", \"error\"},\n\t)\n\treceiptByUOIDCounter = promauto.NewCounterVec(\n\t\tprometheus.CounterOpts{\n\t\t\tNamespace: \"clair\",\n\t\t\tSubsystem: \"notifier\",\n\t\t\tName:      \"receiptbyuoid_total\",\n\t\t\tHelp:      \"Total number of database queries issued in the receiptByUOID method\",\n\t\t},\n\t\t[]string{\"query\", \"error\"},\n\t)\n\treceiptByUOIDDuration = promauto.NewHistogramVec(\n\t\tprometheus.HistogramOpts{\n\t\t\tNamespace: \"clair\",\n\t\t\tSubsystem: \"notifier\",\n\t\t\tName:      \"receiptbyuoid_duration_seconds\",\n\t\t\tHelp:      \"Duration of all queries issued in the receiptByUOID method\",\n\t\t},\n\t\t[]string{\"query\", \"error\"},\n\t)\n\tputReceiptCounter = promauto.NewCounterVec(\n\t\tprometheus.CounterOpts{\n\t\t\tNamespace: \"clair\",\n\t\t\tSubsystem: \"notifier\",\n\t\t\tName:      \"putreceipt_total\",\n\t\t\tHelp:      \"Total number of database queries issued in the putReceipt method\",\n\t\t},\n\t\t[]string{\"query\", \"error\"},\n\t)\n\tputReceiptAffected = promauto.NewCounterVec(\n\t\tprometheus.CounterOpts{\n\t\t\tNamespace: \"clair\",\n\t\t\tSubsystem: \"notifier\",\n\t\t\tName:      \"putreceipt_affected_total\",\n\t\t\tHelp:      \"Total number of rows affected in the putReceipt method\",\n\t\t},\n\t\t[]string{\"query\", \"error\"},\n\t)\n\tputReceiptDuration = promauto.NewHistogramVec(\n\t\tprometheus.HistogramOpts{\n\t\t\tNamespace: \"clair\",\n\t\t\tSubsystem: \"notifier\",\n\t\t\tName:      \"putreceipt_duration_seconds\",\n\t\t\tHelp:      \"Duration of all queries issued in the putReceipt method\",\n\t\t},\n\t\t[]string{\"query\", \"error\"},\n\t)\n)\n\n// Receipt returns the Receipt for a given notification ID.\nfunc (s *Store) Receipt(ctx context.Context, id uuid.UUID) (notifier.Receipt, error) {\n\tconst query = `SELECT uo_id, notification_id, status, ts FROM receipt WHERE notification_id = $1::uuid;`\n\tvar r notifier.Receipt\n\tf := getReceipt(ctx, &r, query, `query`, id, statusMetrics{\n\t\tcounter: receiptCounter,\n\t\tdur:     receiptDuration,\n\t})\n\treturn r, s.pool.AcquireFunc(ctx, f)\n}\n\n// ReceiptByUOID returns the Receipt for a given UpdateOperation ID.\nfunc (s *Store) ReceiptByUOID(ctx context.Context, id uuid.UUID) (notifier.Receipt, error) {\n\tconst query = `SELECT uo_id, notification_id, status, ts FROM receipt WHERE uo_id = $1::uuid;`\n\tvar r notifier.Receipt\n\tf := getReceipt(ctx, &r, query, `query`, id, statusMetrics{\n\t\tcounter: receiptByUOIDCounter,\n\t\tdur:     receiptByUOIDDuration,\n\t})\n\treturn r, s.pool.AcquireFunc(ctx, f)\n}\n\nfunc getReceipt(ctx context.Context, r *notifier.Receipt, query, name string, id uuid.UUID, m statusMetrics) func(*pgxpool.Conn) error {\n\treturn func(c *pgxpool.Conn) error {\n\t\tvar err error\n\t\ttimer := prometheus.NewTimer(prometheus.ObserverFunc(func(v float64) {\n\t\t\tm.dur.WithLabelValues(`query`, errLabel(err)).Observe(v)\n\t\t}))\n\t\tdefer timer.ObserveDuration()\n\t\terr = c.QueryRow(ctx, query, id).Scan(\n\t\t\t&r.UOID,\n\t\t\t&r.NotificationID,\n\t\t\t&r.Status,\n\t\t\t&r.TS,\n\t\t)\n\t\treceiptCounter.WithLabelValues(\"query\", errLabel(err)).Add(1)\n\t\tswitch {\n\t\tcase errors.Is(err, pgx.ErrNoRows):\n\t\t\treturn &clairerror.ErrNoReceipt{\n\t\t\t\tNotificationID: id,\n\t\t\t}\n\t\tcase err != nil:\n\t\t\treturn &clairerror.ErrReceipt{\n\t\t\t\tNotificationID: id,\n\t\t\t\tE:              err,\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}\n}\n\nfunc (s *Store) PutReceipt(ctx context.Context, updater string, r notifier.Receipt) error {\n\tconst (\n\t\tinsertNotification    = `INSERT INTO notification (id) VALUES ($1);`\n\t\tinsertReceipt         = `INSERT INTO receipt (notification_id, uo_id, status, ts) VALUES ($1, $2, $3, CURRENT_TIMESTAMP);`\n\t\tinsertUpdateOperation = `INSERT INTO notifier_update_operation (updater, uo_id, ts) VALUES ($1, $2, CURRENT_TIMESTAMP);`\n\t)\n\ttxOpt := pgx.TxOptions{\n\t\tIsoLevel:   pgx.ReadCommitted,\n\t\tAccessMode: pgx.ReadWrite,\n\t}\n\tmetrics := statusMetrics{\n\t\tdur:      putReceiptDuration,\n\t\tcounter:  putReceiptCounter,\n\t\taffected: putReceiptAffected,\n\t}\n\terr := pgx.BeginTxFunc(ctx, s.pool, txOpt, func(tx pgx.Tx) error {\n\t\tif err := txExec(ctx, metrics, tx,\n\t\t\t`insertNotification`,\n\t\t\tinsertNotification,\n\t\t\t[]interface{}{r.NotificationID}); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif err := txExec(ctx, metrics, tx,\n\t\t\t`insertUpdateOperation`,\n\t\t\tinsertUpdateOperation,\n\t\t\t[]interface{}{updater, r.UOID}); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif err := txExec(ctx, metrics, tx,\n\t\t\t`insertReceipt`,\n\t\t\tinsertReceipt,\n\t\t\t[]interface{}{r.NotificationID, r.UOID, r.Status}); err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "notifier/postgres/set_status.go",
    "content": "package postgres\n\nimport (\n\t\"context\"\n\n\t\"github.com/google/uuid\"\n\t\"github.com/jackc/pgx/v5/pgconn\"\n\t\"github.com/jackc/pgx/v5/pgxpool\"\n\t\"github.com/prometheus/client_golang/prometheus\"\n\t\"github.com/prometheus/client_golang/prometheus/promauto\"\n\n\tclairerror \"github.com/quay/clair/v4/clair-error\"\n)\n\nvar (\n\tsetDeletedCounter = promauto.NewCounterVec(\n\t\tprometheus.CounterOpts{\n\t\t\tNamespace: \"clair\",\n\t\t\tSubsystem: \"notifier\",\n\t\t\tName:      \"setdeleted_total\",\n\t\t\tHelp:      \"Total number of database queries issued in the setDeleted method\",\n\t\t},\n\t\t[]string{\"query\", \"error\"},\n\t)\n\tsetDeletedDuration = promauto.NewHistogramVec(\n\t\tprometheus.HistogramOpts{\n\t\t\tNamespace: \"clair\",\n\t\t\tSubsystem: \"notifier\",\n\t\t\tName:      \"setdeleted_duration_seconds\",\n\t\t\tHelp:      \"Duration of all queries issued in the setDeleted method\",\n\t\t},\n\t\t[]string{\"query\", \"error\"},\n\t)\n\tsetDeliveredCounter = promauto.NewCounterVec(\n\t\tprometheus.CounterOpts{\n\t\t\tNamespace: \"clair\",\n\t\t\tSubsystem: \"notifier\",\n\t\t\tName:      \"setdelivered_total\",\n\t\t\tHelp:      \"Total number of database queries issued in the setDelivered method\",\n\t\t},\n\t\t[]string{\"query\", \"error\"},\n\t)\n\tsetDeliveredDuration = promauto.NewHistogramVec(\n\t\tprometheus.HistogramOpts{\n\t\t\tNamespace: \"clair\",\n\t\t\tSubsystem: \"notifier\",\n\t\t\tName:      \"setdelivered_duration_seconds\",\n\t\t\tHelp:      \"Duration of all queries issued in the setDelivered method\",\n\t\t},\n\t\t[]string{\"query\", \"error\"},\n\t)\n\tsetDeliveryFailedCounter = promauto.NewCounterVec(\n\t\tprometheus.CounterOpts{\n\t\t\tNamespace: \"clair\",\n\t\t\tSubsystem: \"notifier\",\n\t\t\tName:      \"setdeliveryfailed_total\",\n\t\t\tHelp:      \"Total number of database queries issued in the setDeliveryFailed method\",\n\t\t},\n\t\t[]string{\"query\", \"error\"},\n\t)\n\tsetDeliveryFailedDuration = promauto.NewHistogramVec(\n\t\tprometheus.HistogramOpts{\n\t\t\tNamespace: \"clair\",\n\t\t\tSubsystem: \"notifier\",\n\t\t\tName:      \"setdeliveryfailed_duration_seconds\",\n\t\t\tHelp:      \"Duration of all queries issued in the setDeliveryFailed method\",\n\t\t},\n\t\t[]string{\"query\", \"error\"},\n\t)\n)\n\nfunc (s *Store) setStatus(ctx context.Context, id uuid.UUID, status string, m statusMetrics) error {\n\tconst query = `UPDATE receipt SET status = $1::receiptstatus, ts = CURRENT_TIMESTAMP WHERE notification_id = $2::uuid;`\n\treturn s.pool.AcquireFunc(ctx, func(c *pgxpool.Conn) error {\n\t\tvar err error\n\t\tvar tag pgconn.CommandTag\n\t\ttimer := prometheus.NewTimer(prometheus.ObserverFunc(func(v float64) {\n\t\t\tm.dur.WithLabelValues(`query`, errLabel(err)).Observe(v)\n\t\t}))\n\t\tdefer timer.ObserveDuration()\n\t\ttag, err = c.Exec(ctx, query, status, id)\n\t\tm.counter.WithLabelValues(`query`, errLabel(err)).Add(1)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif tag.RowsAffected() == 0 {\n\t\t\treturn &clairerror.ErrNoReceipt{NotificationID: id}\n\t\t}\n\t\treturn nil\n\t})\n}\n\n// SetDelivered marks the provided notification id as delivered\nfunc (s *Store) SetDelivered(ctx context.Context, id uuid.UUID) error {\n\treturn s.setStatus(ctx, id, `delivered`, statusMetrics{\n\t\tcounter: setDeliveredCounter,\n\t\tdur:     setDeliveredDuration,\n\t})\n}\n\n// SetDeliveryFailed marks the provided notification id failed to be delivere\nfunc (s *Store) SetDeliveryFailed(ctx context.Context, id uuid.UUID) error {\n\treturn s.setStatus(ctx, id, `delivery_failed`, statusMetrics{\n\t\tcounter: setDeliveryFailedCounter,\n\t\tdur:     setDeliveryFailedDuration,\n\t})\n}\n\n// SetDeleted marks the provided notification id as deleted\nfunc (s *Store) SetDeleted(ctx context.Context, id uuid.UUID) error {\n\treturn s.setStatus(ctx, id, `deleted`, statusMetrics{\n\t\tcounter: setDeletedCounter,\n\t\tdur:     setDeletedDuration,\n\t})\n}\n"
  },
  {
    "path": "notifier/postgres/store.go",
    "content": "package postgres\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"fmt\"\n\t\"log/slog\"\n\n\t\"github.com/jackc/pgx/v5\"\n\t\"github.com/jackc/pgx/v5/pgconn\"\n\t\"github.com/jackc/pgx/v5/pgxpool\"\n\t\"github.com/jackc/pgx/v5/stdlib\"\n\t\"github.com/prometheus/client_golang/prometheus\"\n\t\"github.com/remind101/migrate\"\n\n\t\"github.com/quay/clair/v4/notifier/migrations\"\n)\n\n// Store implements the notifier.Store interface.\ntype Store struct {\n\tpool *pgxpool.Pool\n}\n\n// NewStore returns a Store using the passed-in Pool.\n//\n// The caller should close the Pool once the store is no longer needed.\nfunc NewStore(pool *pgxpool.Pool) *Store {\n\treturn &Store{pool}\n}\n\n// Init initializes the database using the specified config.\nfunc Init(ctx context.Context, cfg *pgx.ConnConfig) error {\n\tdb, err := sql.Open(\"pgx\", stdlib.RegisterConnConfig(cfg))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to open db: %w\", err)\n\t}\n\tdefer db.Close()\n\n\tslog.InfoContext(ctx, \"performing notifier migrations\")\n\tmigrator := migrate.NewPostgresMigrator(db)\n\tmigrator.Table = migrations.MigrationTable\n\tif err := migrator.Exec(migrate.Up, migrations.Migrations...); err != nil {\n\t\treturn fmt.Errorf(\"failed to perform migrations: %w\", err)\n\t}\n\treturn nil\n}\n\n// As a rule of thumb, admin/worker locks should be in the two-part keyspace to\n// avoid clashing with the manifest locks. They're engine wide, so it's a\n// concern even here in the notifier's bailiwick.\nconst (\n\tadminKeyspace int32 = 4\n\n\t_ int32 = iota\n\tgcLock\n)\n\nfunc errLabel(e error) string {\n\tif e == nil {\n\t\treturn `false`\n\t}\n\treturn `true`\n}\n\ntype statusMetrics struct {\n\tcounter  *prometheus.CounterVec\n\taffected *prometheus.CounterVec\n\tdur      *prometheus.HistogramVec\n}\n\n// TxExec runs the passed query in the provided transaction, recording it as\n// \"name\" with the metrics passed in \"m\".\n//\n// This is highly specific to how metrics are used in this package, and will\n// panic if there are more than two labels unpopulated. It's expected that\n// they're \"query\" and \"error\", respectively.\nfunc txExec(ctx context.Context, m statusMetrics, tx pgx.Tx, name, query string, args []interface{}) error {\n\tvar err error\n\tvar tag pgconn.CommandTag\n\ttimer := prometheus.NewTimer(prometheus.ObserverFunc(func(v float64) {\n\t\tm.dur.WithLabelValues(name, errLabel(err)).Observe(v)\n\t}))\n\tdefer timer.ObserveDuration()\n\ttag, err = tx.Exec(ctx, query, args...)\n\tm.counter.WithLabelValues(name, errLabel(err)).Add(1)\n\tm.affected.WithLabelValues(name, errLabel(err)).Add(float64(tag.RowsAffected()))\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "notifier/postgres/store_test.go",
    "content": "package postgres\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/jackc/pgx/v5/log/testingadapter\"\n\t\"github.com/jackc/pgx/v5/pgxpool\"\n\t\"github.com/jackc/pgx/v5/tracelog\"\n\t\"github.com/quay/claircore/test/integration\"\n)\n\nfunc TestingStore(ctx context.Context, t testing.TB) *Store {\n\tdb, err := integration.NewDB(ctx, t)\n\tif err != nil {\n\t\tt.Fatalf(\"unable to create test database: %v\", err)\n\t}\n\tt.Cleanup(func() { db.Close(ctx, t) })\n\n\tcfg := db.Config()\n\t// This looks backwards, but means that failures get lots of output and\n\t// verbose output gets a moderate amount of output.\n\ttracer := &tracelog.TraceLog{\n\t\tLogLevel: tracelog.LogLevelInfo,\n\t\tLogger:   testingadapter.NewLogger(t),\n\t}\n\tif testing.Verbose() {\n\t\ttracer.LogLevel = tracelog.LogLevelError\n\t}\n\tcfg.ConnConfig.Tracer = tracer\n\n\tif err := Init(ctx, cfg.ConnConfig); err != nil {\n\t\tt.Fatalf(\"failed to init database: %v\", err)\n\t}\n\n\tpool, err := pgxpool.NewWithConfig(ctx, cfg)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to create connpool: %v\", err)\n\t}\n\tt.Cleanup(pool.Close)\n\treturn NewStore(pool)\n}\n"
  },
  {
    "path": "notifier/postgres/testdata/.gitignore",
    "content": "pg*\n!.gitignore\n"
  },
  {
    "path": "notifier/processor.go",
    "content": "package notifier\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"sync\"\n\n\t\"github.com/google/uuid\"\n\t\"github.com/quay/claircore\"\n\t\"github.com/quay/claircore/libvuln/driver\"\n\t\"golang.org/x/sync/errgroup\"\n\n\tclairerror \"github.com/quay/clair/v4/clair-error\"\n\t\"github.com/quay/clair/v4/indexer\"\n\t\"github.com/quay/clair/v4/matcher\"\n)\n\n// Processor listen for new UOIDs, creates notifications, and persists\n// these notifications for later retrieval.\n//\n// Processor(s) create atomic boundaries, no two Processor(s) will be creating\n// notifications for the same UOID at once.\ntype Processor struct {\n\t// distributed lock used for mutual exclusion\n\tlocks Locker\n\t// a handle to an indexer service\n\tindexer indexer.Service\n\t// a handle to a matcher service\n\tmatcher matcher.Service\n\t// a store instance to persist notifications\n\tstore Store\n\n\t// NoSummary controls whether per-manifest vulnerability summarization\n\t// should happen.\n\t//\n\t// The zero value makes the default behavior to do the summary.\n\tNoSummary bool\n}\n\nfunc NewProcessor(store Store, l Locker, indexer indexer.Service, matcher matcher.Service) *Processor {\n\treturn &Processor{\n\t\tlocks:   l,\n\t\tindexer: indexer,\n\t\tmatcher: matcher,\n\t\tstore:   store,\n\t}\n}\n\n// Process receives new UOs as events, creates and persists notifications, and\n// updates the notifier system with the \"latest\" seen UOID.\n//\n// Canceling the ctx will end the processing.\nfunc (p *Processor) Process(ctx context.Context, c <-chan Event) error {\n\tslog.DebugContext(ctx, \"processing events\")\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\tslog.InfoContext(ctx, \"context canceled: ending event processing\")\n\t\t\treturn ctx.Err()\n\t\tcase e := <-c:\n\t\t\tlog := slog.With(\"updater\", e.updater, \"UOID\", e.uo.Ref)\n\t\t\tlog.DebugContext(ctx, \"processing\")\n\t\t\tif err := func() error {\n\t\t\t\tctx, done := p.locks.TryLock(ctx, e.uo.Ref.String())\n\t\t\t\tdefer done()\n\t\t\t\tif err := ctx.Err(); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tsafe, prev := p.safe(ctx, log, e)\n\t\t\t\tif !safe {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t\treturn p.create(ctx, log, e, prev)\n\t\t\t}(); err != nil {\n\t\t\t\tlog.WarnContext(ctx, \"failed to create notifications\",\n\t\t\t\t\t\"reason\", err)\n\t\t\t}\n\t\t}\n\t}\n}\n\n// create implements the business logic of creating and persisting\n// notifications\n//\n// will be performed under a distributed lock\nfunc (p *Processor) create(ctx context.Context, log *slog.Logger, e Event, prev uuid.UUID) error {\n\tlog.DebugContext(ctx, \"retrieving diff\",\n\t\t\"prev\", prev,\n\t\t\"cur\", e.uo.Ref)\n\tdiff, err := p.matcher.UpdateDiff(ctx, prev, e.uo.Ref)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get update diff: %v\", err)\n\t}\n\tlog.DebugContext(ctx, \"diff results\",\n\t\t\"removed\", len(diff.Removed),\n\t\t\"added\", len(diff.Added))\n\n\ttab := notifTab{\n\t\tN:      make([]Notification, 0),\n\t\tlookup: make(map[string]int),\n\t}\n\teg, wctx := errgroup.WithContext(ctx)\n\teg.Go(getAffected(wctx, p.indexer, p.NoSummary, diff.Added, Added, &tab))\n\teg.Go(getAffected(wctx, p.indexer, p.NoSummary, diff.Removed, Removed, &tab))\n\tif err := eg.Wait(); err != nil {\n\t\treturn fmt.Errorf(\"failed to get affected manifests: %v\", err)\n\t}\n\n\t// Don't count up the affected manifests unless we're going to print it.\n\tif log.Enabled(ctx, slog.LevelDebug) {\n\t\tvar added, removed int\n\t\tfor _, n := range tab.N {\n\t\t\tswitch n.Reason {\n\t\t\tcase Added:\n\t\t\t\tadded++\n\t\t\tcase Removed:\n\t\t\t\tremoved++\n\t\t\t}\n\t\t}\n\t\tlog.DebugContext(ctx, \"affected manifest counts\",\n\t\t\t\"added\", added,\n\t\t\t\"removed\", removed)\n\t}\n\n\tif len(tab.N) == 0 {\n\t\t// directly add a \"delivered\" receipt, this will stop subsequent processing\n\t\t// of this update operation and also avoid delivery attempts.\n\t\tr := Receipt{\n\t\t\tNotificationID: uuid.New(),\n\t\t\tUOID:           e.uo.Ref,\n\t\t\tStatus:         Delivered,\n\t\t}\n\t\tlog.DebugContext(ctx, \"no affected manifests for update operation, setting to delivered\")\n\t\terr := p.store.PutReceipt(ctx, e.uo.Updater, r)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to put receipt: %v\", err)\n\t\t}\n\t\treturn nil\n\t}\n\n\topts := PutOpts{\n\t\tUpdater:        e.updater,\n\t\tUpdateID:       e.uo.Ref,\n\t\tNotificationID: uuid.New(),\n\t\tNotifications:  tab.N,\n\t}\n\terr = p.store.PutNotifications(ctx, opts)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to store notifications: %v\", err)\n\t}\n\treturn nil\n}\n\nfunc min(a, b int) int {\n\tif a < b {\n\t\treturn a\n\t}\n\treturn b\n}\n\n// NotifTab is a handle for a slice of Notifications.\n//\n// It has supporting structures for concurrent use and summaries.\ntype notifTab struct {\n\tsync.Mutex\n\tlookup map[string]int // only used in \"summary\" mode\n\tN      []Notification\n}\n\n// GetAffected issues AffectedManifest calls in chunks and merges the result.\n//\n// Its signature is weird to make use in an errgroup a little bit nicer.\nfunc getAffected(ctx context.Context, ic indexer.Service, nosummary bool, vs []claircore.Vulnerability, r Reason, out *notifTab) func() error {\n\tconst chunk = 1000\n\treturn func() error {\n\t\tvar s []claircore.Vulnerability\n\t\tfor len(vs) > 0 {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn ctx.Err()\n\t\t\tdefault:\n\t\t\t}\n\t\t\ts = vs[:min(chunk, len(vs))]\n\t\t\tvs = vs[len(s):]\n\t\t\ta, err := ic.AffectedManifests(ctx, s)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tfor manifest, vulns := range a.VulnerableManifests {\n\t\t\t\tdigest, err := claircore.ParseDigest(manifest)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\t// The vulns slice is sorted most severe to lease severe, so\n\t\t\t\t// when in summary mode, we only need to check the initial vuln.\n\t\t\t\tif !nosummary {\n\t\t\t\t\tvuln := a.Vulnerabilities[vulns[0]]\n\t\t\t\t\tkey := digest.String()\n\t\t\t\t\tvar n *Notification\n\t\t\t\t\tout.Lock()\n\t\t\t\t\t// First, lookup if there's a notification for this\n\t\t\t\t\t// manifest.\n\t\t\t\t\ti, ok := out.lookup[key]\n\t\t\t\t\tif ok {\n\t\t\t\t\t\tn = &out.N[i]\n\t\t\t\t\t}\n\t\t\t\t\tif n == nil {\n\t\t\t\t\t\t// If this is the first appearance of this manifest,\n\t\t\t\t\t\t// insert it.\n\t\t\t\t\t\ti := len(out.N)\n\t\t\t\t\t\tout.N = append(out.N, Notification{\n\t\t\t\t\t\t\tManifest: digest,\n\t\t\t\t\t\t\tReason:   r,\n\t\t\t\t\t\t})\n\t\t\t\t\t\tout.lookup[key] = i\n\t\t\t\t\t\tn = &out.N[i]\n\t\t\t\t\t\tn.Vulnerability.FromVulnerability(vuln)\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// If we've seen this before, check the severity and\n\t\t\t\t\t\t// swap if the new vuln is more severe.\n\t\t\t\t\t\tvar sev claircore.Severity\n\t\t\t\t\t\tif err := sev.UnmarshalText([]byte(n.Vulnerability.Severity)); err != nil {\n\t\t\t\t\t\t\tout.Unlock()\n\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif sev < vuln.NormalizedSeverity {\n\t\t\t\t\t\t\tn.Vulnerability.FromVulnerability(vuln)\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tout.Unlock()\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\t// If not in summary, create all the notifications.\n\t\t\t\tfor idx := range vulns {\n\t\t\t\t\tvuln := a.Vulnerabilities[vulns[idx]]\n\t\t\t\t\tout.Lock()\n\t\t\t\t\ti := len(out.N)\n\t\t\t\t\tout.N = append(out.N, Notification{\n\t\t\t\t\t\tManifest: digest,\n\t\t\t\t\t\tReason:   r,\n\t\t\t\t\t})\n\t\t\t\t\tn := &out.N[i]\n\t\t\t\t\tn.Vulnerability.FromVulnerability(vuln)\n\t\t\t\t\tout.Unlock()\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}\n}\n\n// safe guards against situations where creating notifications is\n// incorrect.\n//\n// if deemed safe to create notifications the previous update operation will be\n// returned\n//\n// will be performed under a distributed lock.\nfunc (p *Processor) safe(ctx context.Context, log *slog.Logger, e Event) (bool, uuid.UUID) {\n\t// confirm we are not making duplicate notifications\n\tvar errNoReceipt *clairerror.ErrNoReceipt\n\t_, err := p.store.ReceiptByUOID(ctx, e.uo.Ref)\n\tswitch {\n\tcase errors.As(err, &errNoReceipt):\n\t\t// hop out of switch\n\tcase err != nil:\n\t\tlog.WarnContext(ctx, \"received error getting receipt by UOID\",\n\t\t\t\"reason\", err)\n\t\treturn false, uuid.Nil\n\tdefault:\n\t\tlog.InfoContext(ctx, \"receipt created by another processor; will not process notifications\")\n\t\treturn false, uuid.Nil\n\t}\n\n\t// confirm UOID is not stale and get previous UOID for diffing if exists.\n\t// if no previous UOID, return false, we don't want a full diff of notifications\n\t// TODO(louis) UpdateOperations signature supports getting \"all\" for a given updater\n\t// but code path is not implemented. implement this to optimize.\n\tall, err := p.matcher.UpdateOperations(ctx, driver.VulnerabilityKind)\n\tif err != nil {\n\t\tlog.WarnContext(ctx, \"received error getting update operations from matcher\",\n\t\t\t\"reason\", err)\n\t\treturn false, uuid.Nil\n\t}\n\tif _, ok := all[e.updater]; !ok {\n\t\tlog.WarnContext(ctx, \"updater missing from update operations returned from matcher (may have been garbage collected)\")\n\t\treturn false, uuid.Nil\n\t}\n\n\tuos := all[e.updater]\n\n\tvar current driver.UpdateOperation\n\tvar prev driver.UpdateOperation\n\n\tif len(uos) == 1 {\n\t\tcurrent = uos[0]\n\t\tprev.Ref = uuid.Nil\n\t} else {\n\t\tcurrent, prev = uos[0], uos[1]\n\t}\n\n\tif current.Ref.String() != e.uo.Ref.String() {\n\t\tlog.InfoContext(ctx, \"newer update operation is present, will not process notifications\",\n\t\t\t\"new\", current.Ref)\n\t\treturn false, uuid.Nil\n\t}\n\treturn true, prev.Ref\n}\n"
  },
  {
    "path": "notifier/processor_create_test.go",
    "content": "package notifier\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"sort\"\n\t\"sync/atomic\"\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/quay/claircore\"\n\t\"github.com/quay/claircore/libvuln/driver\"\n\t\"github.com/quay/claircore/test\"\n\n\t\"github.com/quay/clair/v4/indexer\"\n\t\"github.com/quay/clair/v4/matcher\"\n)\n\nvar (\n\tvulnAdd = &claircore.Vulnerability{\n\t\tID:          \"0\",\n\t\tName:        \"added vulnerability\",\n\t\tDescription: \"a vulnerability added\",\n\t}\n\tvulnRemoved = &claircore.Vulnerability{\n\t\tID:          \"1\",\n\t\tName:        \"removed vulnerability\",\n\t\tDescription: \"a vulnerability removed\",\n\t}\n\tmanifestAdd          = `sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef`\n\tmanifestRemoved      = `sha256:fc92eec5cac70b0c324cec2933cd7db1c0eae7c9e2649e42d02e77eb6da0d15f`\n\taffectedManifestsAdd = &claircore.AffectedManifests{\n\t\tVulnerabilities: map[string]*claircore.Vulnerability{\n\t\t\tvulnAdd.ID: vulnAdd,\n\t\t},\n\t\tVulnerableManifests: map[string][]string{\n\t\t\tmanifestAdd: {vulnAdd.ID},\n\t\t},\n\t}\n\taffectedManifestsRemoved = &claircore.AffectedManifests{\n\t\tVulnerabilities: map[string]*claircore.Vulnerability{\n\t\t\tvulnRemoved.ID: vulnRemoved,\n\t\t},\n\t\tVulnerableManifests: map[string][]string{\n\t\t\tmanifestRemoved: {vulnRemoved.ID},\n\t\t},\n\t}\n\tnotifications = []Notification{\n\t\t{\n\t\t\tManifest: claircore.MustParseDigest(manifestAdd),\n\t\t\tReason:   Added,\n\t\t\tVulnerability: VulnSummary{\n\t\t\t\tDescription: affectedManifestsAdd.Vulnerabilities[vulnAdd.ID].Description,\n\t\t\t\tName:        affectedManifestsAdd.Vulnerabilities[vulnAdd.ID].Name,\n\t\t\t\tSeverity:    claircore.Unknown.String(),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tManifest: claircore.MustParseDigest(manifestRemoved),\n\t\t\tReason:   Removed,\n\t\t\tVulnerability: VulnSummary{\n\t\t\t\tDescription: affectedManifestsRemoved.Vulnerabilities[vulnRemoved.ID].Description,\n\t\t\t\tName:        affectedManifestsRemoved.Vulnerabilities[vulnRemoved.ID].Name,\n\t\t\t\tSeverity:    claircore.Unknown.String(),\n\t\t\t},\n\t\t},\n\t}\n)\n\nfunc TestProcessCreate(t *testing.T) {\n\tt.Run(\"Create\", testProcessorCreate)\n\tt.Run(\"MatcherErr\", testProcessorMatcherErr)\n\tt.Run(\"IndexerErr\", testProcessorIndexerErr)\n\tt.Run(\"StoreErr\", testProcessorStoreErr)\n}\n\n// testProcessorStoreErr confirms create fails when the store is not\n// available\nfunc testProcessorStoreErr(t *testing.T) {\n\tt.Parallel()\n\tctx := test.Logging(t)\n\te := Event{\n\t\tupdater: testUpdater,\n\t\tuo:      processorUpdateOps[testUpdater][0],\n\t}\n\tmm := &matcher.Mock{\n\t\tUpdateDiff_: func(context.Context, uuid.UUID, uuid.UUID) (*driver.UpdateDiff, error) {\n\t\t\treturn &driver.UpdateDiff{\n\t\t\t\tAdded:   []claircore.Vulnerability{*vulnAdd},\n\t\t\t\tRemoved: []claircore.Vulnerability{*vulnRemoved},\n\t\t\t}, nil\n\t\t},\n\t}\n\tim := &indexer.Mock{\n\t\tAffectedManifests_: func(ctx context.Context, vulns []claircore.Vulnerability) (*claircore.AffectedManifests, error) {\n\t\t\t// needs to be populated.\n\t\t\t// create method needs at least one affected manifest\n\t\t\t// for the code path to invoke store.PutNotifications()\n\t\t\treturn affectedManifestsAdd, nil\n\t\t},\n\t}\n\t// perform bulk of checks in this mock method.\n\tsm := &MockStore{\n\t\tPutNotifications_: func(_ context.Context, _ PutOpts) error {\n\t\t\treturn fmt.Errorf(\"expected\")\n\t\t},\n\t}\n\n\tp := Processor{\n\t\tstore:   sm,\n\t\tindexer: im,\n\t\tmatcher: mm,\n\t}\n\n\terr := p.create(ctx, slog.Default(), e, uuid.Nil)\n\tif err == nil {\n\t\tt.Fatalf(\"expected err\")\n\t}\n}\n\n// testProcessorIndexerErr confirms create fails when the indexer is not\n// available\nfunc testProcessorIndexerErr(t *testing.T) {\n\tt.Parallel()\n\tctx := test.Logging(t)\n\te := Event{\n\t\tupdater: testUpdater,\n\t\tuo:      processorUpdateOps[testUpdater][0],\n\t}\n\tmm := &matcher.Mock{\n\t\tUpdateDiff_: func(context.Context, uuid.UUID, uuid.UUID) (*driver.UpdateDiff, error) {\n\t\t\treturn &driver.UpdateDiff{\n\t\t\t\tAdded:   []claircore.Vulnerability{*vulnAdd},\n\t\t\t\tRemoved: []claircore.Vulnerability{*vulnRemoved},\n\t\t\t}, nil\n\t\t},\n\t}\n\tim := &indexer.Mock{\n\t\tAffectedManifests_: func(ctx context.Context, vulns []claircore.Vulnerability) (*claircore.AffectedManifests, error) {\n\t\t\treturn nil, fmt.Errorf(\"expected\")\n\t\t},\n\t}\n\t// perform bulk of checks in this mock method.\n\tsm := &MockStore{\n\t\tPutNotifications_: func(ctx context.Context, opts PutOpts) error {\n\t\t\treturn nil\n\t\t},\n\t}\n\n\tp := Processor{\n\t\tstore:   sm,\n\t\tindexer: im,\n\t\tmatcher: mm,\n\t}\n\n\terr := p.create(ctx, slog.Default(), e, uuid.Nil)\n\tif err == nil {\n\t\tt.Fatalf(\"expected err\")\n\t}\n}\n\n// testProcessorMatcherErr confirms create fails when the matcher is not\n// available\nfunc testProcessorMatcherErr(t *testing.T) {\n\tt.Parallel()\n\tctx := test.Logging(t)\n\te := Event{\n\t\tupdater: testUpdater,\n\t\tuo:      processorUpdateOps[testUpdater][0],\n\t}\n\tmm := &matcher.Mock{\n\t\tUpdateDiff_: func(context.Context, uuid.UUID, uuid.UUID) (*driver.UpdateDiff, error) {\n\t\t\treturn nil, fmt.Errorf(\"expected\")\n\t\t},\n\t}\n\tim := &indexer.Mock{\n\t\tAffectedManifests_: func(ctx context.Context, vulns []claircore.Vulnerability) (*claircore.AffectedManifests, error) {\n\t\t\treturn &claircore.AffectedManifests{}, nil\n\t\t},\n\t}\n\t// perform bulk of checks in this mock method.\n\tsm := &MockStore{\n\t\tPutNotifications_: func(ctx context.Context, opts PutOpts) error {\n\t\t\treturn nil\n\t\t},\n\t}\n\n\tp := Processor{\n\t\tstore:   sm,\n\t\tindexer: im,\n\t\tmatcher: mm,\n\t}\n\n\terr := p.create(ctx, slog.Default(), e, uuid.Nil)\n\tif err == nil {\n\t\tt.Fatalf(\"expected err\")\n\t}\n}\n\n// testProcessorCreate confirms notifications are created correctly.\nfunc testProcessorCreate(t *testing.T) {\n\tt.Parallel()\n\tctx := test.Logging(t)\n\te := Event{\n\t\tupdater: testUpdater,\n\t\tuo:      processorUpdateOps[testUpdater][0],\n\t}\n\tmm := &matcher.Mock{\n\t\tUpdateDiff_: func(context.Context, uuid.UUID, uuid.UUID) (*driver.UpdateDiff, error) {\n\t\t\treturn &driver.UpdateDiff{\n\t\t\t\tAdded:   []claircore.Vulnerability{*vulnAdd},\n\t\t\t\tRemoved: []claircore.Vulnerability{*vulnRemoved},\n\t\t\t}, nil\n\t\t},\n\t}\n\tcount := uint64(0)\n\tim := &indexer.Mock{\n\t\tAffectedManifests_: func(ctx context.Context, vulns []claircore.Vulnerability) (*claircore.AffectedManifests, error) {\n\t\t\tif atomic.LoadUint64(&count) > 1 {\n\t\t\t\treturn nil, fmt.Errorf(\"unexpected number of calls\")\n\t\t\t}\n\t\t\tatomic.AddUint64(&count, 1)\n\t\t\tswitch vulns[0].ID {\n\t\t\tcase \"0\":\n\t\t\t\treturn affectedManifestsAdd, nil\n\t\t\tcase \"1\":\n\t\t\t\treturn affectedManifestsRemoved, nil\n\t\t\t}\n\t\t\treturn nil, fmt.Errorf(\"unexpected call\")\n\t\t},\n\t}\n\t// perform bulk of checks in this mock method.\n\tsm := &MockStore{\n\t\tPutNotifications_: func(ctx context.Context, opts PutOpts) error {\n\t\t\tif opts.Updater != e.updater {\n\t\t\t\tt.Fatalf(\"got: %s, wanted: %s\", opts.Updater, testUpdater)\n\t\t\t}\n\t\t\tif opts.UpdateID != e.uo.Ref {\n\t\t\t\tt.Fatalf(\"got: %v, want: %v\", opts.UpdateID, e.uo.Ref)\n\t\t\t}\n\t\t\tif opts.NotificationID == uuid.Nil {\n\t\t\t\tt.Fatalf(\"malformed notification id: %v\", opts.NotificationID)\n\t\t\t}\n\t\t\t// Need some sort of stable order here:\n\t\t\tsort.Slice(opts.Notifications, func(i, j int) bool {\n\t\t\t\treturn opts.Notifications[i].Reason < opts.Notifications[j].Reason\n\t\t\t})\n\t\t\tif !cmp.Equal(opts.Notifications, notifications, cmpopts.IgnoreUnexported(claircore.Digest{})) {\n\t\t\t\tt.Fatalf(\"%v\", cmp.Diff(opts.Notifications, notifications, cmpopts.IgnoreUnexported(claircore.Digest{})))\n\t\t\t}\n\n\t\t\treturn nil\n\t\t},\n\t}\n\n\tp := Processor{\n\t\tstore:   sm,\n\t\tindexer: im,\n\t\tmatcher: mm,\n\t}\n\n\terr := p.create(ctx, slog.Default(), e, uuid.Nil)\n\tif err != nil {\n\t\tt.Fatalf(\"unexpected err: %v\", err)\n\t}\n}\n"
  },
  {
    "path": "notifier/processor_safe_test.go",
    "content": "package notifier\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/google/uuid\"\n\t\"github.com/quay/claircore/libvuln/driver\"\n\t\"github.com/quay/claircore/test\"\n\n\tclairerror \"github.com/quay/clair/v4/clair-error\"\n\t\"github.com/quay/clair/v4/matcher\"\n)\n\nvar (\n\ttestUpdater        = \"test-updater\"\n\tstart              = time.Now()\n\tprocessorUpdateOps = map[string][]driver.UpdateOperation{\n\t\t// the array will be sorted by newest UO\n\t\ttestUpdater: {\n\t\t\t{\n\t\t\t\tRef:         uuid.New(),\n\t\t\t\tDate:        start,\n\t\t\t\tFingerprint: \"fp\",\n\t\t\t\tUpdater:     testUpdater,\n\t\t\t},\n\t\t\t{\n\t\t\t\tRef:         uuid.New(),\n\t\t\t\tDate:        start.Add(-10 * time.Minute),\n\t\t\t\tFingerprint: \"fp\",\n\t\t\t\tUpdater:     testUpdater,\n\t\t\t},\n\t\t},\n\t}\n)\n\n// TestProcessSafe is a harness for running concurrent tests which ensure notification\n// creation happens safely.\nfunc TestProcessorSafe(t *testing.T) {\n\tt.Run(\"UnsafeDuplications\", testUnsafeDuplications)\n\tt.Run(\"UnsafeStaleUOID\", testUnsafeStaleUOID)\n\tt.Run(\"UnsafeMatcherErr\", testUnsafeMatcherErr)\n\tt.Run(\"UnsafeStoreErr\", testUnsafeStoreErr)\n\tt.Run(\"Safe\", testSafe)\n}\n\n// testSafe confirms when all safety guards pass the processor will\n// create notifications.\nfunc testSafe(t *testing.T) {\n\tctx := test.Logging(t)\n\tsm := &MockStore{\n\t\tReceiptByUOID_: func(ctx context.Context, id uuid.UUID) (Receipt, error) {\n\t\t\treturn Receipt{}, &clairerror.ErrNoReceipt{}\n\t\t},\n\t}\n\tmm := &matcher.Mock{\n\t\tUpdateOperations_: func(context.Context, driver.UpdateKind, ...string) (map[string][]driver.UpdateOperation, error) {\n\t\t\treturn processorUpdateOps, nil\n\t\t},\n\t}\n\tp := Processor{\n\t\tstore:   sm,\n\t\tmatcher: mm,\n\t}\n\te := Event{\n\t\tupdater: testUpdater,\n\t\tuo:      processorUpdateOps[testUpdater][0],\n\t}\n\tb, _ := p.safe(ctx, slog.Default(), e)\n\tif !b {\n\t\tt.Fatalf(\"got: %v, want: %v\", b, true)\n\t}\n}\n\n// testUnsafeStoreErr confirms notifications will not be created if Store is returning an error.\nfunc testUnsafeStoreErr(t *testing.T) {\n\tctx := test.Logging(t)\n\tsm := &MockStore{\n\t\tReceiptByUOID_: func(ctx context.Context, id uuid.UUID) (Receipt, error) {\n\t\t\treturn Receipt{}, fmt.Errorf(\"expected\")\n\t\t},\n\t}\n\tmm := &matcher.Mock{\n\t\tUpdateOperations_: func(context.Context, driver.UpdateKind, ...string) (map[string][]driver.UpdateOperation, error) {\n\t\t\treturn processorUpdateOps, nil\n\t\t},\n\t}\n\n\tp := Processor{\n\t\tstore:   sm,\n\t\tmatcher: mm,\n\t}\n\n\te := Event{\n\t\tupdater: testUpdater,\n\t\tuo:      processorUpdateOps[testUpdater][0],\n\t}\n\tb, _ := p.safe(ctx, slog.Default(), e)\n\tif b {\n\t\tt.Fatalf(\"got: %v, want: %v\", b, false)\n\t}\n}\n\n// testUnsafeMatcherErr confirms notifications will not be created if Matcher is returning an error.\nfunc testUnsafeMatcherErr(t *testing.T) {\n\tctx := test.Logging(t)\n\tsm := &MockStore{\n\t\tReceiptByUOID_: func(ctx context.Context, id uuid.UUID) (Receipt, error) {\n\t\t\treturn Receipt{}, &clairerror.ErrNoReceipt{}\n\t\t},\n\t}\n\tmm := &matcher.Mock{\n\t\tUpdateOperations_: func(context.Context, driver.UpdateKind, ...string) (map[string][]driver.UpdateOperation, error) {\n\t\t\treturn processorUpdateOps, fmt.Errorf(\"expected\")\n\t\t},\n\t}\n\n\tp := Processor{\n\t\tstore:   sm,\n\t\tmatcher: mm,\n\t}\n\n\te := Event{\n\t\tupdater: testUpdater,\n\t\tuo:      processorUpdateOps[testUpdater][0],\n\t}\n\tb, _ := p.safe(ctx, slog.Default(), e)\n\tif b {\n\t\tt.Fatalf(\"got: %v, want: %v\", b, false)\n\t}\n}\n\n// testSafeStaleUOID confirms the guard against creating stale notifications\n// works correctly.\nfunc testUnsafeStaleUOID(t *testing.T) {\n\tctx := test.Logging(t)\n\tsm := &MockStore{\n\t\tReceiptByUOID_: func(ctx context.Context, id uuid.UUID) (Receipt, error) {\n\t\t\treturn Receipt{}, &clairerror.ErrNoReceipt{}\n\t\t},\n\t}\n\tmm := &matcher.Mock{\n\t\tUpdateOperations_: func(context.Context, driver.UpdateKind, ...string) (map[string][]driver.UpdateOperation, error) {\n\t\t\treturn processorUpdateOps, nil\n\t\t},\n\t}\n\n\tp := Processor{\n\t\tstore:   sm,\n\t\tmatcher: mm,\n\t}\n\n\te := Event{\n\t\tupdater: testUpdater,\n\t\tuo:      processorUpdateOps[testUpdater][1],\n\t}\n\n\tb, _ := p.safe(ctx, slog.Default(), e)\n\tif b {\n\t\tt.Fatalf(\"got: %v, want: %v\", b, false)\n\t}\n}\n\n// testUnsafeDuplications confirms the guard against creating\n// duplicate notifications works correctly.\nfunc testUnsafeDuplications(t *testing.T) {\n\tctx := test.Logging(t)\n\tsm := &MockStore{\n\t\tReceiptByUOID_: func(ctx context.Context, id uuid.UUID) (Receipt, error) {\n\t\t\treturn Receipt{}, nil\n\t\t},\n\t}\n\tmm := &matcher.Mock{\n\t\tUpdateOperations_: func(context.Context, driver.UpdateKind, ...string) (map[string][]driver.UpdateOperation, error) {\n\t\t\treturn processorUpdateOps, nil\n\t\t},\n\t}\n\n\tp := Processor{\n\t\tstore:   sm,\n\t\tmatcher: mm,\n\t}\n\n\te := Event{\n\t\tupdater: testUpdater,\n\t\tuo:      processorUpdateOps[testUpdater][0],\n\t}\n\tb, _ := p.safe(ctx, slog.Default(), e)\n\tif b {\n\t\tt.Fatalf(\"got: %v, want: %v\", b, false)\n\t}\n}\n"
  },
  {
    "path": "notifier/receipt.go",
    "content": "package notifier\n\nimport (\n\t\"time\"\n\n\t\"github.com/google/uuid\"\n)\n\n// Status defines the possible states of a notification.\ntype Status string\n\nconst (\n\t// A notification is created and ready to be delivered to a client\n\tCreated Status = \"created\"\n\t// A notification has been successfully delivered to a client\n\tDelivered Status = \"delivered\"\n\t// A notification failed to be delivered\n\tDeliveryFailed Status = \"delivery_failed\"\n\t// The client has read the notification and issued a delete\n\tDeleted Status = \"deleted\"\n)\n\n// Receipt represents the current status of a notification\ntype Receipt struct {\n\t// The update operation associated with this receipt\n\tUOID uuid.UUID\n\t// the id a client may use to retrieve a set of notifications\n\tNotificationID uuid.UUID\n\t// the current status  of the notification\n\tStatus Status\n\t// the timestamp of the last status update\n\tTS time.Time\n}\n"
  },
  {
    "path": "notifier/service/mock.go",
    "content": "package service\n\nimport (\n\t\"context\"\n\n\t\"github.com/google/uuid\"\n\n\t\"github.com/quay/clair/v4/notifier\"\n)\n\nvar _ notifier.Service = (*Mock)(nil)\n\n// Mock implements a mock notifier service\ntype Mock struct {\n\tNotifications_       func(ctx context.Context, id uuid.UUID, page *notifier.Page) ([]notifier.Notification, notifier.Page, error)\n\tDeleteNotifications_ func(ctx context.Context, id uuid.UUID) error\n}\n\nfunc (m *Mock) Notifications(ctx context.Context, id uuid.UUID, page *notifier.Page) ([]notifier.Notification, notifier.Page, error) {\n\treturn m.Notifications_(ctx, id, page)\n}\n\nfunc (m *Mock) DeleteNotifications(ctx context.Context, id uuid.UUID) error {\n\treturn m.DeleteNotifications_(ctx, id)\n}\n"
  },
  {
    "path": "notifier/service/notifier.go",
    "content": "package service\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"net/http\"\n\t\"os\"\n\t\"runtime\"\n\t\"time\"\n\n\t\"github.com/google/uuid\"\n\t\"github.com/quay/clair/config\"\n\t\"golang.org/x/sync/errgroup\"\n\n\t\"github.com/quay/clair/v4/indexer\"\n\t\"github.com/quay/clair/v4/matcher\"\n\t\"github.com/quay/clair/v4/notifier\"\n\t\"github.com/quay/clair/v4/notifier/amqp\"\n\t\"github.com/quay/clair/v4/notifier/stomp\"\n\t\"github.com/quay/clair/v4/notifier/webhook\"\n)\n\nvar (\n\tprocessors = runtime.GOMAXPROCS(0)\n\tdeliveries = runtime.GOMAXPROCS(0)\n)\n\nvar _ notifier.Service = (*Notifier)(nil)\n\n// ErrNoDelivery is returned when there's insufficient configuration for\n// notification delivery.\nvar ErrNoDelivery = errors.New(\"no delivery mechanisms configured\")\n\n// Notifier is a local implementation of a notifier service.\ntype Notifier struct {\n\tstore notifier.Store\n\tpoll  *notifier.Poller\n\tproc  *notifier.Processor\n\tdel   *notifier.Delivery\n}\n\n// Notifications implements notifier.Service.\nfunc (s *Notifier) Notifications(ctx context.Context, id uuid.UUID, page *notifier.Page) ([]notifier.Notification, notifier.Page, error) {\n\treturn s.store.Notifications(ctx, id, page)\n}\n\n// DeleteNotifications implements notifier.Service.\nfunc (s *Notifier) DeleteNotifications(ctx context.Context, id uuid.UUID) error {\n\treturn s.store.SetDeleted(ctx, id)\n}\n\n// Opts configures the notifier service.\ntype Opts struct {\n\tMatcher          matcher.Service\n\tIndexer          indexer.Service\n\tSigner           webhook.Signer\n\tClient           *http.Client\n\tWebhook          *config.Webhook\n\tAMQP             *config.AMQP\n\tSTOMP            *config.STOMP\n\tPollInterval     time.Duration\n\tDeliveryInterval time.Duration\n\tDisableSummary   bool\n}\n\n// New returns a configured notifier subsystem.\nfunc New(ctx context.Context, store notifier.Store, locks notifier.Locker, opts Opts) (*Notifier, error) {\n\tsrv := Notifier{store: store}\n\n\t// Check for test mode.\n\tif tm := os.Getenv(\"NOTIFIER_TEST_MODE\"); tm != \"\" {\n\t\tslog.WarnContext(ctx,\n\t\t\t\"notifier will create test notifications on a set interval\",\n\t\t\t\"test_mode_enabled\", true,\n\t\t\t\"interval\", opts.PollInterval)\n\t\ttestModeInit(ctx, &opts)\n\t}\n\n\t// Configure the Poller.\n\tslog.InfoContext(ctx, \"initializing poller\",\n\t\t\"interval\", opts.PollInterval)\n\tsrv.poll = notifier.NewPoller(store, opts.Matcher, opts.PollInterval)\n\n\t// Configure the Processor.\n\tslog.InfoContext(ctx, \"initializing processors\",\n\t\t\"count\", processors)\n\tsrv.proc = notifier.NewProcessor(store, locks, opts.Indexer, opts.Matcher)\n\tsrv.proc.NoSummary = opts.DisableSummary\n\n\t// Configure a Deliverer.\n\tvar del notifier.Deliverer\n\tvar err error\n\t// BUG(hank) Currently only one delivery mechanism can be configured at a\n\t// time.\n\tswitch {\n\tcase opts.Webhook != nil:\n\t\tslog.InfoContext(ctx, \"initializing webhook deliverers\",\n\t\t\t\"count\", deliveries)\n\t\tdel, err = webhook.New(opts.Webhook, opts.Client, opts.Signer)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create webhook deliverer: %v\", err)\n\t\t}\n\tcase opts.AMQP != nil:\n\t\tconf := opts.AMQP\n\t\tif len(conf.URIs) == 0 {\n\t\t\tslog.WarnContext(ctx, \"amqp delivery misconfigured\",\n\t\t\t\t\"reason\", \"no broker URIs to connect to\")\n\t\t\tbreak\n\t\t}\n\t\tif conf.Direct {\n\t\t\tdel, err = amqp.NewDirectDeliverer(conf)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to create AMQP deliverer: %v\", err)\n\t\t\t}\n\t\t\tbreak\n\t\t}\n\t\tdel, err = amqp.New(conf)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create AMQP deliverer: %v\", err)\n\t\t}\n\tcase opts.STOMP != nil:\n\t\tconf := opts.STOMP\n\t\tif len(conf.URIs) == 0 {\n\t\t\tslog.WarnContext(ctx, \"stomp delivery misconfigured\",\n\t\t\t\t\"reason\", \"no broker URIs to connect to\")\n\t\t\tbreak\n\t\t}\n\t\tif conf.Direct {\n\t\t\tdel, err = stomp.NewDirectDeliverer(conf)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to create STOMP direct deliverer: %v\", err)\n\t\t\t}\n\t\t\tbreak\n\t\t}\n\t\tdel, err = stomp.New(conf)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create STOMP deliverer: %v\", err)\n\t\t}\n\t}\n\tif del == nil {\n\t\t// Report an error if configured such that no notifications are being\n\t\t// processed.\n\t\treturn nil, ErrNoDelivery\n\t}\n\tsrv.del = notifier.NewDelivery(store, locks, del, opts.DeliveryInterval)\n\n\treturn &srv, nil\n}\n\n// TestModeInit will inject a mock Indexer and Matcher into opts\n// to be used in testing mode.\nfunc testModeInit(ctx context.Context, opts *Opts) error {\n\tmm := &matcher.Mock{}\n\tim := &indexer.Mock{}\n\tmatcherForTestMode(mm)\n\tindexerForTestMode(im)\n\topts.Matcher = mm\n\topts.Indexer = im\n\treturn nil\n}\n\n// Run spawns all needed background goroutines and waits for the first error.\n//\n// Canceling the supplied Context should return context.Canceled.\nfunc (s *Notifier) Run(ctx context.Context) error {\n\t// Channel for poller to processor communication.\n\tch := make(chan notifier.Event, notifier.MaxChanSize)\n\teg, ctx := errgroup.WithContext(ctx)\n\t// Poller goroutine.\n\teg.Go(func() error { return s.poll.Poll(ctx, ch) })\n\t// Processor goroutines.\n\tfor i := 0; i < processors; i++ {\n\t\teg.Go(func() error { return s.proc.Process(ctx, ch) })\n\t}\n\t// Garbage collection goroutine.\n\teg.Go(s.gc(ctx))\n\t// Delivery goroutines.\n\tfor i := 0; i < deliveries; i++ {\n\t\teg.Go(func() error { return s.del.Deliver(ctx) })\n\t}\n\treturn eg.Wait()\n}\n\n// Gc is the garbage collection process.\nfunc (s *Notifier) gc(ctx context.Context) func() error {\n\t// BUG(hank) The garbage collection period is currently unconfigurable.\n\tticker := time.NewTicker(time.Hour)\n\treturn func() error {\n\t\tdefer ticker.Stop()\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn ctx.Err()\n\t\t\tcase <-ticker.C:\n\t\t\t\tattrs := make([]slog.Attr, 1, 2)\n\t\t\t\tattrs[0] = slog.Bool(\"success\", true)\n\t\t\t\tif err := s.store.CollectNotifications(ctx); err != nil {\n\t\t\t\t\tattrs[0].Value = slog.BoolValue(false)\n\t\t\t\t\tattrs = append(attrs, slog.String(\"reason\", err.Error()))\n\t\t\t\t}\n\t\t\t\tslog.LogAttrs(ctx, slog.LevelInfo, \"gc done\", attrs...)\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "notifier/service/testmode.go",
    "content": "package service\n\nimport (\n\t\"context\"\n\t\"crypto/rand\"\n\t\"crypto/sha256\"\n\t\"time\"\n\n\t\"github.com/google/uuid\"\n\t\"github.com/quay/claircore\"\n\t\"github.com/quay/claircore/libvuln/driver\"\n\n\t\"github.com/quay/clair/v4/indexer\"\n\t\"github.com/quay/clair/v4/matcher\"\n)\n\n// indexerForTestMode configures a mock Indexer service for notifier test mode.\n//\n// in notifier test mode a notifier.Processor will request \"indexer.AffectedManifest\" with a\n// set of vulnerabilities at which point we will return a mock affected vulnerability.\nfunc indexerForTestMode(mock *indexer.Mock) {\n\taffectedManifests := func(ctx context.Context, vulns []claircore.Vulnerability) (*claircore.AffectedManifests, error) {\n\t\tif len(vulns) == 0 {\n\t\t\treturn &claircore.AffectedManifests{}, nil\n\t\t}\n\n\t\tdata := make([]byte, sha256.Size)\n\t\t_, err := rand.Read(data)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tdigest, err := claircore.NewDigest(\"sha256\", data)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tam := &claircore.AffectedManifests{\n\t\t\tVulnerabilities: map[string]*claircore.Vulnerability{\n\t\t\t\tvulns[0].ID: &(vulns[0]),\n\t\t\t},\n\t\t\tVulnerableManifests: map[string][]string{\n\t\t\t\tdigest.String(): {vulns[0].ID},\n\t\t\t},\n\t\t}\n\t\treturn am, nil\n\t}\n\tmock.AffectedManifests_ = affectedManifests\n}\n\n// MatcherForTestMode configures a mock Matcher service for notifier test mode.\n//\n// in notifier test mode a notifier.Poller will request \"matcher.LatestUpdateOperations\" at which point\n// a new UO pair will be smithed.\n//\n// next a notifier.Processor will request \"matcher.UpdateOperations\" and look for \"test-updater\" UOs in which\n// the smithed pair will be returned.\n//\n// finally a notifier.Processor will request to \"matcher.UpdateDiff\" will be created where a mock added vulnerability\n// will be returned.\nfunc matcherForTestMode(mock *matcher.Mock) {\n\tlatestUpdateOperations := func(context.Context, driver.UpdateKind) (map[string][]driver.UpdateOperation, error) {\n\t\tlatest := driver.UpdateOperation{\n\t\t\tRef:         uuid.New(),\n\t\t\tUpdater:     \"test-updater\",\n\t\t\tFingerprint: \"test-fingerprint\",\n\t\t}\n\t\tolder := driver.UpdateOperation{\n\t\t\tRef:         uuid.New(),\n\t\t\tUpdater:     \"test-updater\",\n\t\t\tFingerprint: \"test-fingerprint\",\n\t\t}\n\t\tmock.Lock()\n\t\tdefer mock.Unlock()\n\t\tmock.TestUOs = map[string][]driver.UpdateOperation{\n\t\t\t\"test-updater\": []driver.UpdateOperation{latest, older},\n\t\t}\n\t\tm := map[string][]driver.UpdateOperation{\n\t\t\tlatest.Updater: []driver.UpdateOperation{\n\t\t\t\tlatest,\n\t\t\t},\n\t\t}\n\t\treturn m, nil\n\t}\n\tupdateOperations := func(context.Context, driver.UpdateKind, ...string) (map[string][]driver.UpdateOperation, error) {\n\t\tmock.Lock()\n\t\tdefer mock.Unlock()\n\t\tm := map[string][]driver.UpdateOperation{}\n\t\tfor k, v := range mock.TestUOs {\n\t\t\tm[k] = v\n\t\t}\n\t\treturn m, nil\n\t}\n\tupdateDiff := func(context.Context, uuid.UUID, uuid.UUID) (*driver.UpdateDiff, error) {\n\t\tv := claircore.Vulnerability{\n\t\t\tID:                 \"0\",\n\t\t\tUpdater:            \"test-updater\",\n\t\t\tName:               \"test-vulnerability\",\n\t\t\tDescription:        \"this vulnerability indicates you are running the notifier in test mode.\",\n\t\t\tIssued:             time.Now(),\n\t\t\tNormalizedSeverity: claircore.Unknown,\n\t\t\tFixedInVersion:     \"\",\n\t\t}\n\t\tmock.Lock()\n\t\tdiff := driver.UpdateDiff{\n\t\t\tCur:   mock.TestUOs[\"test-updater\"][0],\n\t\t\tPrev:  mock.TestUOs[\"test-updater\"][1],\n\t\t\tAdded: []claircore.Vulnerability{v},\n\t\t}\n\t\tmock.Unlock()\n\t\treturn &diff, nil\n\t}\n\tmock.LatestUpdateOperations_ = latestUpdateOperations\n\tmock.UpdateOperations_ = updateOperations\n\tmock.UpdateDiff_ = updateDiff\n\treturn\n}\n"
  },
  {
    "path": "notifier/service.go",
    "content": "package notifier\n\nimport (\n\t\"context\"\n\n\t\"github.com/google/uuid\"\n)\n\n// Service is an interface wrapping ClairV4's notifier functionality.\n//\n// This remains an interface so remote clients may implement as well.\ntype Service interface {\n\t// Retrieves an optional paginated set of notifications given an notification id\n\tNotifications(ctx context.Context, id uuid.UUID, page *Page) ([]Notification, Page, error)\n\t// Deletes the provided notification id\n\tDeleteNotifications(ctx context.Context, id uuid.UUID) error\n}\n"
  },
  {
    "path": "notifier/stomp/deliverer.go",
    "content": "package stomp\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/url\"\n\t\"time\"\n\n\tgostomp \"github.com/go-stomp/stomp/v3\"\n\t\"github.com/google/uuid\"\n\t\"github.com/quay/clair/config\"\n\n\tclairerror \"github.com/quay/clair/v4/clair-error\"\n\t\"github.com/quay/clair/v4/notifier\"\n)\n\n// Deliverer is a STOMP deliverer which publishes a notifier.Callback to the\n// broker.\ntype Deliverer struct {\n\tcallback    *url.URL\n\tdestination string\n\tfo          failOver\n\trollup      int\n}\n\nfunc New(conf *config.STOMP) (*Deliverer, error) {\n\tvar d Deliverer\n\tif err := d.load(conf); err != nil {\n\t\treturn nil, err\n\t}\n\treturn &d, nil\n}\n\nfunc (d *Deliverer) load(cfg *config.STOMP) error {\n\td.fo.timeout = 30 * time.Second\n\t// TODO(hank) Wire up the \"host\" and \"timeout\" config somehow -- probably\n\t// just make the config URIs strings actual URIs and parse them out with\n\t// query parameters.\n\tvar err error\n\tif cfg.TLS != nil {\n\t\td.fo.tls, err = cfg.TLS.Config()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tif !cfg.Direct {\n\t\td.callback, err = url.Parse(cfg.Callback)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\td.fo.addrs = make([]string, len(cfg.URIs))\n\tcopy(d.fo.addrs, cfg.URIs)\n\td.destination = cfg.Destination\n\td.rollup = cfg.Rollup\n\treturn nil\n}\n\nfunc (d *Deliverer) Name() string {\n\treturn fmt.Sprintf(\"stomp-%s\", d.destination)\n}\n\nfunc (d *Deliverer) Deliver(ctx context.Context, nID uuid.UUID) error {\n\tconn, err := d.fo.Connection(ctx)\n\tif err != nil {\n\t\treturn &clairerror.ErrDeliveryFailed{err}\n\t}\n\tdefer conn.Disconnect()\n\n\tu, err := d.callback.Parse(nID.String())\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tcb := notifier.Callback{\n\t\tNotificationID: nID,\n\t\tCallback:       *u,\n\t}\n\tb, err := json.Marshal(&cb)\n\tif err != nil {\n\t\treturn &clairerror.ErrDeliveryFailed{err}\n\t}\n\n\terr = conn.Send(d.destination, \"application/json\", b, gostomp.SendOpt.Receipt)\n\tif err != nil {\n\t\treturn &clairerror.ErrDeliveryFailed{err}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "notifier/stomp/directdeliverer.go",
    "content": "package stomp\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"log/slog\"\n\n\t\"github.com/google/uuid\"\n\t\"github.com/quay/clair/config\"\n\n\tclairerror \"github.com/quay/clair/v4/clair-error\"\n\t\"github.com/quay/clair/v4/notifier\"\n)\n\n// Deliverer is a STOMP deliverer which publishes a notifier.Callback to the\n// the broker.\ntype DirectDeliverer struct {\n\tDeliverer\n\tn []notifier.Notification\n}\n\nfunc NewDirectDeliverer(conf *config.STOMP) (*DirectDeliverer, error) {\n\tvar d DirectDeliverer\n\tif err := d.load(conf); err != nil {\n\t\treturn nil, err\n\t}\n\td.n = make([]notifier.Notification, 0, 1024)\n\treturn &d, nil\n}\n\nfunc (d *DirectDeliverer) Name() string {\n\treturn fmt.Sprintf(\"stomp-direct-%s\", d.destination)\n}\n\n// Notifications will copy the provided notifications into a buffer for STOMP\n// delivery.\nfunc (d *DirectDeliverer) Notifications(ctx context.Context, n []notifier.Notification) error {\n\t// if we can reslice instead of allocate do so.\n\tif len(n) <= len(d.n) {\n\t\td.n = d.n[:len(n)]\n\t\tcopy(d.n, n)\n\t\treturn nil\n\t}\n\ttmp := make([]notifier.Notification, len(n))\n\tcopy(tmp, n)\n\td.n = tmp\n\treturn nil\n}\n\nfunc (d *DirectDeliverer) Deliver(ctx context.Context, nID uuid.UUID) error {\n\tconn, err := d.fo.Connection(ctx)\n\tif err != nil {\n\t\treturn errDeliever(err)\n\t}\n\tdefer conn.Disconnect()\n\n\ttx, err := conn.BeginWithError()\n\tif err != nil {\n\t\treturn errDeliever(err)\n\t}\n\tvar success bool\n\tdefer func() {\n\t\tif success {\n\t\t\treturn\n\t\t}\n\t\tif err := tx.AbortWithReceipt(); err != nil {\n\t\t\tslog.WarnContext(ctx, \"transaction aborted\", \"reason\", err)\n\t\t}\n\t}()\n\n\t// block loop publishing smaller blocks of max(rollup) length via reslicing.\n\trollup := d.rollup\n\tif rollup == 0 {\n\t\trollup++\n\t}\n\n\tvar currentBlock []notifier.Notification\n\tfor bs, be := 0, rollup; bs < len(d.n); bs, be = be, be+rollup {\n\t\t// If block-end exceeds array bounds, slice block underflow.\n\t\t// Next block-start will cause loop to exit.\n\t\tif be > len(d.n) {\n\t\t\tbe = len(d.n)\n\t\t}\n\n\t\tcurrentBlock = d.n[bs:be]\n\t\t// Can't reuse a buffer because without receipts, the client returns\n\t\t// after queuing the send.\n\t\t// Can't use receipts because RabbitMQ treats receipt as a thing that\n\t\t// happens at the end of a transaction (not unreasonable, I suppose).\n\t\tvar buf bytes.Buffer\n\t\tif err := json.NewEncoder(&buf).Encode(&currentBlock); err != nil {\n\t\t\treturn errDeliever(err)\n\t\t}\n\t\tif err := tx.Send(d.destination, \"application/json\", buf.Bytes(), nil); err != nil {\n\t\t\treturn errDeliever(err)\n\t\t}\n\t}\n\n\tif err := tx.CommitWithReceipt(); err != nil {\n\t\treturn errDeliever(err)\n\t}\n\tsuccess = true\n\treturn nil\n}\n\nfunc errDeliever(e error) error {\n\treturn &clairerror.ErrDeliveryFailed{E: e}\n}\n"
  },
  {
    "path": "notifier/stomp/doc.go",
    "content": "// Package stomp implements a [Deliverer] over the STOMP protocol.\n//\n// Deprecated: This package will be removed in a future version. Users should\n// write a webhook to STOMP transducer.\npackage stomp\n"
  },
  {
    "path": "notifier/stomp/failover.go",
    "content": "package stomp\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"net\"\n\t\"time\"\n\n\tgostomp \"github.com/go-stomp/stomp/v3\"\n\t\"github.com/quay/clair/config\"\n)\n\n// failOver will return the first successful connection made against the provided\n// brokers, or an existing connection if not closed.\n//\n// failOver is safe for concurrent usage.\ntype failOver struct {\n\ttls     *tls.Config\n\tlogin   *config.Login\n\taddrs   []string\n\ttimeout time.Duration\n}\n\n// Dial will dial the provided address in accordance with the provided Config.\n//\n// Note: the STOMP protocol does not support multiplexing operations over a\n// single TCP connection. A TCP connection must be made for each STOMP\n// connection.\nfunc (f *failOver) Dial(ctx context.Context, addr string) (*gostomp.Conn, error) {\n\tvar opts []func(*gostomp.Conn) error\n\tif f.login != nil {\n\t\topts = append(opts, gostomp.ConnOpt.Login(f.login.Login, f.login.Passcode))\n\t}\n\tif host, _, err := net.SplitHostPort(addr); err == nil {\n\t\topts = append(opts, gostomp.ConnOpt.Host(host))\n\t}\n\n\tvar d interface {\n\t\tDialContext(context.Context, string, string) (net.Conn, error)\n\t} = &net.Dialer{\n\t\tTimeout: f.timeout,\n\t}\n\tif f.tls != nil {\n\t\td = &tls.Dialer{\n\t\t\tNetDialer: d.(*net.Dialer),\n\t\t\tConfig:    f.tls,\n\t\t}\n\t}\n\tconn, err := d.DialContext(ctx, \"tcp\", addr)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to connect to broker @ %v: %w\", addr, err)\n\t}\n\n\tstompConn, err := gostomp.Connect(conn, opts...)\n\tif err != nil {\n\t\tif conn != nil {\n\t\t\tconn.Close()\n\t\t}\n\t\treturn nil, fmt.Errorf(\"stomp connect handshake to broker @ %v failed: %w\", addr, err)\n\t}\n\n\treturn stompConn, err\n}\n\n// Connection returns a new connection to the first broker that successfully\n// handshakes.\n//\n// The caller MUST call conn.Disconnect() to close the underlying TCP connection\n// when finished.\nfunc (f *failOver) Connection(ctx context.Context) (*gostomp.Conn, error) {\n\tfor _, addr := range f.addrs {\n\t\tconn, err := f.Dial(ctx, addr)\n\t\tif err != nil {\n\t\t\tslog.DebugContext(ctx, \"failed to dial broker, attempting next\",\n\t\t\t\t\"broker\", addr,\n\t\t\t\t\"reason\", err)\n\t\t\tcontinue\n\t\t}\n\t\treturn conn, nil\n\t}\n\treturn nil, fmt.Errorf(\"exhausted all brokers and unable to make connection\")\n}\n"
  },
  {
    "path": "notifier/stomp/integration_test.go",
    "content": "package stomp\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/url\"\n\t\"os\"\n\t\"runtime\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/go-stomp/stomp/v3\"\n\t\"github.com/google/uuid\"\n\t\"github.com/quay/clair/config\"\n\t\"github.com/quay/claircore\"\n\t\"github.com/quay/claircore/test\"\n\t\"github.com/quay/claircore/test/integration\"\n\t\"golang.org/x/sync/errgroup\"\n\n\t\"github.com/quay/clair/v4/notifier\"\n)\n\nfunc setURI(t *testing.T, cfg config.STOMP, uri string) (next config.STOMP, dial string, opt []func(*stomp.Conn) error) {\n\tconst (\n\t\tdefaultStompBrokerURI = \"localhost:61613\"\n\t)\n\tt.Helper()\n\tswitch {\n\tcase uri == \"\":\n\t\tt.Logf(\"using default broker URI: %q\", defaultStompBrokerURI)\n\t\tcfg.URIs = append(cfg.URIs, defaultStompBrokerURI)\n\t\treturn cfg, defaultStompBrokerURI, nil\n\tcase strings.Contains(uri, \"://\"): // probably a URL\n\t\tu, err := url.Parse(uri)\n\t\tif err != nil {\n\t\t\tt.Logf(\"weird test URI: %q: %v\", uri, err)\n\t\t\treturn setURI(t, cfg, \"\")\n\t\t}\n\t\tt.Logf(\"using broker address: %q\", u.Host)\n\t\tcfg.URIs = append(cfg.URIs, u.Host)\n\t\tt.Logf(\"using broker vhost: %q\", u.Hostname())\n\t\topt = append(opt, stomp.ConnOpt.Host(u.Hostname()))\n\t\tif u := u.User; u != nil {\n\t\t\tt.Logf(\"using login: %q\", u.String())\n\t\t\tcfg.Login = &config.Login{\n\t\t\t\tLogin: u.Username(),\n\t\t\t}\n\t\t\tcfg.Login.Passcode, _ = u.Password()\n\t\t\topt = append(opt, stomp.ConnOpt.Login(cfg.Login.Login, cfg.Login.Passcode))\n\t\t}\n\t\treturn cfg, u.Host, opt\n\tdefault:\n\t\tt.Logf(\"using broker URI: %q\", uri)\n\t\tcfg.URIs = append(cfg.URIs, uri)\n\t\treturn cfg, uri, nil\n\t}\n}\n\ntype logAdapter struct{ *testing.T }\n\nvar _ stomp.Logger = logAdapter{}\n\nfunc (a logAdapter) Debugf(format string, value ...interface{})   { a.Logf(format, value...) }\nfunc (a logAdapter) Infof(format string, value ...interface{})    { a.Logf(format, value...) }\nfunc (a logAdapter) Warningf(format string, value ...interface{}) { a.Logf(format, value...) }\nfunc (a logAdapter) Debug(msg string)                             { a.Log(msg) }\nfunc (a logAdapter) Info(msg string)                              { a.Log(msg) }\nfunc (a logAdapter) Warning(msg string)                           { a.Log(msg) }\nfunc (a logAdapter) Error(msg string)                             { a.T.Error(msg) }\n\nfunc consumer(ctx context.Context, t *testing.T, dial string, opt []func(*stomp.Conn) error, queue string, ct int, hook func(*testing.T, *stomp.Message)) func() error {\n\treturn func() error {\n\t\tconn, err := stomp.Dial(\"tcp\", dial, opt...)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to connect to broker at %q: %w\", dial, err)\n\t\t}\n\t\tdefer conn.Disconnect()\n\t\tt.Log(\"consumer: connect OK\")\n\n\t\tsub, err := conn.Subscribe(queue, stomp.AckClient)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to subscribe to %q: %w\", queue, err)\n\t\t}\n\t\tdefer func() {\n\t\t\tdefer func() {\n\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\tt.Logf(\"*stomp.Conn.Unsubscribe panicked (see https://github.com/go-stomp/stomp/pull/139):\\n%v\", r)\n\t\t\t\t}\n\t\t\t}()\n\t\t\terr := sub.Unsubscribe()\n\t\t\tif err != nil {\n\t\t\t\tt.Logf(\"unsubscribing: %v\", err)\n\t\t\t\tif runtime.GOARCH == \"amd64\" {\n\t\t\t\t\tt.Fail()\n\t\t\t\t} else {\n\t\t\t\t\tt.Logf(\"ignoring previous error because of arch %q\", runtime.GOARCH)\n\t\t\t\t}\n\t\t\t}\n\t\t}()\n\t\tt.Log(\"consumer: subscribe OK\")\n\n\t\t// read messages\n\t\tfor i := 0; i < ct; i++ {\n\t\t\tvar m *stomp.Message\n\t\t\tselect {\n\t\t\tcase m = <-sub.C:\n\t\t\t\tconn.Ack(m)\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn context.Cause(ctx)\n\t\t\t}\n\t\t\thook(t, m)\n\t\t\tif t.Failed() {\n\t\t\t\treturn errors.New(\"hook failed\")\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}\n}\n\n// TestDeliverer confirms a notification\n// callback is successfully delivered to the stomp broker.\nfunc TestDeliverer(t *testing.T) {\n\tt.Parallel()\n\t// This is only really made to work with RabbitMQ. Previous revisions of the\n\t// code tested against ActiveMQ, but this was migrated to make the setup\n\t// simpler.\n\tintegration.Skip(t)\n\tctx := test.Logging(t)\n\tconst (\n\t\tcallback = \"http://clair-notifier/notifier/api/v1/notifications/\"\n\t)\n\n\tvar (\n\t\tqueue = `/queue/` + uuid.New().String()\n\t\tconf  = config.STOMP{\n\t\t\tCallback:    callback,\n\t\t\tDestination: queue,\n\t\t\tDirect:      false,\n\t\t\tURIs: []string{\n\t\t\t\t\"nohost1:5672\", // Put a bogus host in here to hit the failover code.\n\t\t\t},\n\t\t}\n\t)\n\tconf, dial, opt := setURI(t, conf, os.Getenv(\"STOMP_CONNECTION_STRING\"))\n\topt = append(opt, stomp.ConnOpt.Logger(logAdapter{t}))\n\n\t// test parallel usage\n\teg, ctx := errgroup.WithContext(ctx)\n\tconst n = 4\n\teg.Go(consumer(ctx, t, dial, opt, queue, n, func(t *testing.T, m *stomp.Message) {\n\t\tif got, want := m.ContentType, \"application/json\"; got != want {\n\t\t\tt.Errorf(\"msg content type mismatch: got %q, want %q\", got, want)\n\t\t}\n\t\tvar msgBody map[string]string\n\t\tif err := json.Unmarshal(m.Body, &msgBody); err != nil {\n\t\t\tt.Errorf(\"cannot unmarshal msg body into map: %v\", err)\n\t\t}\n\t\tnid, ok := msgBody[\"notification_id\"]\n\t\tif !ok {\n\t\t\tt.Error(`cannot find \"notification_id\" key in msg body`)\n\t\t}\n\t\tt.Logf(\"recv note %q\", nid)\n\t\tcb, ok := msgBody[\"callback\"]\n\t\tif !ok {\n\t\t\tt.Error(`cannot find \"callback\" key in msg body`)\n\t\t}\n\t\tif got, want := cb, callback+nid; got != want {\n\t\t\tt.Errorf(\"callback mismatch: got: %q, want: %q\", got, want)\n\t\t}\n\t}))\n\tfor i := 0; i < n; i++ {\n\t\teg.Go(func() error {\n\t\t\tnoteID := uuid.New()\n\t\t\td, err := New(&conf)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"could not create deliverer: %v\", err)\n\t\t\t}\n\t\t\t// will error if message cannot be delivered to broker\n\t\t\terr = d.Deliver(ctx, noteID)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to deliver message: %v\", err)\n\t\t\t}\n\t\t\tt.Logf(\"sent note %q\", noteID)\n\t\t\treturn nil\n\t\t})\n\t}\n\tif err := eg.Wait(); err != nil {\n\t\tt.Error(err)\n\t}\n}\n\n// TestDirectDeliverer confirms delivery of notifications directly\n// to the STOMP queue with rollup works correctly.\nfunc TestDirectDeliverer(t *testing.T) {\n\tt.Parallel()\n\tintegration.Skip(t)\n\tctx := test.Logging(t)\n\n\ttable := []struct {\n\t\tname         string\n\t\trollup       int\n\t\tnotes        int\n\t\texpectedMsgs int\n\t}{\n\t\t{name: \"Rollup0\", rollup: 0, notes: 1, expectedMsgs: 1},\n\t\t{name: \"Rollup1\", rollup: 1, notes: 5, expectedMsgs: 5},\n\t\t{name: \"Overflow\", rollup: 10, notes: 5, expectedMsgs: 1},\n\t\t{name: \"Odds\", rollup: 3, notes: 7, expectedMsgs: 3},\n\t\t{name: \"OddsRollup\", rollup: 3, notes: 8, expectedMsgs: 3},\n\t\t{name: \"OddsNotes\", rollup: 4, notes: 7, expectedMsgs: 2},\n\t\t{name: \"Large\", rollup: 100, notes: 1000, expectedMsgs: 10},\n\t}\n\n\tfor _, tt := range table {\n\t\tqueue := `/queue/` + uuid.New().String()\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tctx := test.Logging(t, ctx)\n\t\t\t// deliverer test\n\t\t\tconf := config.STOMP{\n\t\t\t\tDirect:      true,\n\t\t\t\tRollup:      tt.rollup,\n\t\t\t\tDestination: queue,\n\t\t\t}\n\t\t\tconf, dial, opt := setURI(t, conf, os.Getenv(\"STOMP_CONNECTION_STRING\"))\n\n\t\t\tnoteID := uuid.New()\n\t\t\tnotes := make([]notifier.Notification, 0, tt.notes)\n\t\t\tfor i := 0; i < tt.notes; i++ {\n\t\t\t\tnotes = append(notes, notifier.Notification{\n\t\t\t\t\tID:       uuid.New(),\n\t\t\t\t\tManifest: claircore.MustParseDigest(\"sha256:35c102085707f703de2d9eaad8752d6fe1b8f02b5d2149f1d8357c9cc7fb7d0a\"),\n\t\t\t\t\tReason:   notifier.Added,\n\t\t\t\t\tVulnerability: notifier.VulnSummary{\n\t\t\t\t\t\tDescription: fmt.Sprintf(\"test-vuln-%d\", i),\n\t\t\t\t\t},\n\t\t\t\t})\n\t\t\t}\n\t\t\tt.Logf(\"created %d notes\", len(notes))\n\n\t\t\t// test parallel usage\n\t\t\teg, ctx := errgroup.WithContext(ctx)\n\t\t\tconst n = 4\n\t\t\tvar ct int\n\t\t\teg.Go(consumer(ctx, t, dial, opt, queue, n*tt.expectedMsgs, func(t *testing.T, m *stomp.Message) {\n\t\t\t\tif got, want := m.ContentType, \"application/json\"; got != want {\n\t\t\t\t\tt.Errorf(\"msg content type mismatch: got %q, want %q\", got, want)\n\t\t\t\t}\n\t\t\t\tvar msgBody []notifier.Notification\n\t\t\t\tif err := json.Unmarshal(m.Body, &msgBody); err != nil {\n\t\t\t\t\tt.Errorf(\"cannot unmarshal msg body into slice of notifications: %v\", err)\n\t\t\t\t}\n\t\t\t\trollup := tt.rollup\n\t\t\t\tif tt.rollup == 0 {\n\t\t\t\t\trollup++\n\t\t\t\t}\n\t\t\t\tif got, want := len(msgBody), rollup; got > want {\n\t\t\t\t\tt.Errorf(\"more notes in msg than expected: got %d, want %d\", got, want)\n\t\t\t\t}\n\t\t\t\tct += len(msgBody)\n\t\t\t}))\n\t\t\tdefer func() {\n\t\t\t\tgot, want := ct, tt.notes*n\n\t\t\t\tt.Logf(\"consumer: read notes: got %d, want %d\", got, want)\n\t\t\t\tif got != want {\n\t\t\t\t\tt.Fail()\n\t\t\t\t}\n\t\t\t}()\n\t\t\tfor i := 0; i < n; i++ {\n\t\t\t\tid := i\n\t\t\t\teg.Go(func() error {\n\t\t\t\t\td, err := NewDirectDeliverer(&conf)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"could not create deliverer: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t\tt.Logf(\"deliverer(%d): created %p\", id, d)\n\t\t\t\t\tif err := d.Notifications(ctx, notes); err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to provide notifications to direct deliverer: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t\tt.Logf(\"deliverer(%d): added %d notes\", id, len(notes))\n\t\t\t\t\tif err := d.Deliver(ctx, noteID); err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to deliver message: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t\tt.Logf(\"deliverer(%d): delivered\", id)\n\t\t\t\t\treturn nil\n\t\t\t\t})\n\t\t\t}\n\t\t\tif err := eg.Wait(); err != nil {\n\t\t\t\tt.Error(err)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "notifier/store.go",
    "content": "package notifier\n\nimport (\n\t\"context\"\n\n\t\"github.com/google/uuid\"\n)\n\n// PutOpts is provided to Notificationer.Put\n// with fields necessary to persist a notification id\ntype PutOpts struct {\n\t// the updater triggering a notification\n\tUpdater string\n\t// the update operation id triggering the notification\n\tUpdateID uuid.UUID\n\t// the notification id clients will use to retrieve the\n\t// list of notifications\n\tNotificationID uuid.UUID\n\t// a slice of notifications to persist. these notifications\n\t// will be retrievable via the notification id\n\tNotifications []Notification\n}\n\n// Store is an aggregate interface implementing all methods\n// necessary for a notifier persistence layer\ntype Store interface {\n\tNotificationer\n\tReceipter\n}\n\n// Notificationer implements persistence methods for Notification models\ntype Notificationer interface {\n\t// Notifications retrieves the list of notifications associated with a\n\t// notification id\n\t//\n\t// If a Page is provided the returned notifications will be a subset of the total\n\t// and it's len will be no larger then Page.Size.\n\t//\n\t// This method should interpret the page.Next field as the requested page and\n\t// set the returned page.Next field to the next page to receive or -1 if\n\t// paging has been exhausted.\n\t//\n\t// Page maybe nil to receive all notifications.\n\tNotifications(ctx context.Context, id uuid.UUID, page *Page) ([]Notification, Page, error)\n\t// PutNotifications persists the provided notifications and associates\n\t// them with the provided notification id\n\t//\n\t// PutNotifications must update the latest update operation for the provided\n\t// updater in such a way that UpdateOperation returns the provided update\n\t// operation id when queried with the updater name\n\t//\n\t// PutNotifications must create a Receipt with status created status on\n\t// successful persistence of notifications in such a way that Receipter.Created()\n\t// returns the persisted notification id.\n\tPutNotifications(ctx context.Context, opts PutOpts) error\n\t// PutReceipt allows for the caller to directly add a receipt to the store\n\t// without notifications being created.\n\t//\n\t// After this method returns all methods on the Receipter interface must work accordingly.\n\tPutReceipt(ctx context.Context, updater string, r Receipt) error\n\t// CollectNotifications garbage collects all notifications.\n\t//\n\t// Normally Receipter.SetDeleted will be issues first, however\n\t// application logic may decide to GC notifications which have not been\n\t// set deleted after some period of time, thus this condition should not\n\t// be checked.\n\tCollectNotifications(ctx context.Context) error\n}\n\n// Receipter implements persistence methods for Receipt models\ntype Receipter interface {\n\t// Receipt returns the Receipt for a given notification id\n\tReceipt(ctx context.Context, id uuid.UUID) (Receipt, error)\n\t// ReceiptByUOID returns the Receipt for a given UOID\n\tReceiptByUOID(ctx context.Context, id uuid.UUID) (Receipt, error)\n\t// Created returns a slice of notification ids in created status\n\tCreated(ctx context.Context) ([]uuid.UUID, error)\n\t// Failed returns a slice of notification ids to in delivery failed status\n\tFailed(ctx context.Context) ([]uuid.UUID, error)\n\t// Deleted returns a slice of notification ids in deleted status\n\tDeleted(ctx context.Context) ([]uuid.UUID, error)\n\t// SetDelivered marks the provided notification id as delivered\n\tSetDelivered(ctx context.Context, id uuid.UUID) error\n\t// SetDeliveryFailed marks the provided notification id failed to be delivered\n\tSetDeliveryFailed(ctx context.Context, id uuid.UUID) error\n\t// SetDeleted marks the provided notification id as deleted\n\tSetDeleted(ctx context.Context, id uuid.UUID) error\n}\n"
  },
  {
    "path": "notifier/summary_test.go",
    "content": "package notifier\n\nimport (\n\t\"context\"\n\t\"log/slog\"\n\t\"testing\"\n\n\t\"github.com/google/uuid\"\n\t\"github.com/quay/claircore\"\n\t\"github.com/quay/claircore/libvuln/driver\"\n\t\"github.com/quay/claircore/test\"\n\n\t\"github.com/quay/clair/v4/indexer\"\n\t\"github.com/quay/clair/v4/matcher\"\n)\n\nfunc TestNotificationSummary(t *testing.T) {\n\tctx := test.Logging(t)\n\n\t// This is a bunch of supporting data structures.\n\tupdater := uuid.New().String()\n\te := Event{\n\t\tupdater: updater,\n\t\tuo: driver.UpdateOperation{\n\t\t\tRef:     uuid.New(),\n\t\t\tUpdater: updater,\n\t\t},\n\t}\n\tmanifest := \"sha256:8d502da610b3c153d5aedaaf5323c0d49f61401d4791b4b1ffe9e36c6cbe09a0\"\n\tvs := []claircore.Vulnerability{\n\t\t{ID: \"🖳\", Name: \"uncool vulnerability\"},\n\t\t{ID: \"☃\", Name: \"cool vulnerability\", NormalizedSeverity: claircore.Critical},\n\t}\n\tam := &claircore.AffectedManifests{\n\t\tVulnerabilities: map[string]*claircore.Vulnerability{\n\t\t\t\"☃\": &vs[0],\n\t\t\t\"🖳\": &vs[1],\n\t\t},\n\t\tVulnerableManifests: map[string][]string{},\n\t}\n\tfor _, v := range vs {\n\t\tam.VulnerableManifests[manifest] = append(am.VulnerableManifests[manifest], v.ID)\n\t}\n\tp := Processor{\n\t\tmatcher: &matcher.Mock{\n\t\t\tUpdateDiff_: func(_ context.Context, prev, _ uuid.UUID) (*driver.UpdateDiff, error) {\n\t\t\t\tif got, want := prev, uuid.Nil; got != want {\n\t\t\t\t\tt.Errorf(\"got: %v, want: %v\", got, want)\n\t\t\t\t}\n\t\t\t\treturn &driver.UpdateDiff{\n\t\t\t\t\tAdded:   vs,\n\t\t\t\t\tRemoved: []claircore.Vulnerability{},\n\t\t\t\t}, nil\n\t\t\t},\n\t\t},\n\t\tindexer: &indexer.Mock{\n\t\t\tAffectedManifests_: func(_ context.Context, vs []claircore.Vulnerability) (*claircore.AffectedManifests, error) {\n\t\t\t\tif len(vs) > 0 {\n\t\t\t\t\t// Needs at least one affected manifest\n\t\t\t\t\t// for the code path to invoke store.PutNotifications()\n\t\t\t\t\treturn am, nil\n\t\t\t\t}\n\t\t\t\treturn &claircore.AffectedManifests{}, nil\n\t\t\t},\n\t\t},\n\t}\n\n\t// Enable summarization, set the check function.\n\tp.NoSummary = false\n\tp.store = &MockStore{\n\t\tPutNotifications_: func(_ context.Context, o PutOpts) error {\n\t\t\tt.Logf(\"got notification ID: %v\", o.NotificationID)\n\t\t\tfor _, n := range o.Notifications {\n\t\t\t\tt.Logf(\"manifest(%v): %v %v\", n.Manifest, n.Reason, n.Vulnerability.Name)\n\t\t\t}\n\t\t\tif got, want := len(o.Notifications), 1; got != want {\n\t\t\t\tt.Errorf(\"got: %d, want: %d\", got, want)\n\t\t\t}\n\t\t\tif got, want := vs[1].Name, o.Notifications[0].Vulnerability.Name; got != want {\n\t\t\t\tt.Errorf(\"got: %s, want: %s\", got, want)\n\t\t\t}\n\t\t\treturn nil\n\t\t},\n\t}\n\tif err := p.create(ctx, slog.Default(), e, uuid.Nil); err != nil {\n\t\tt.Error(err)\n\t}\n\n\t// Disable summarization, swap the check function, and run again.\n\tp.NoSummary = true\n\tp.store = &MockStore{\n\t\tPutNotifications_: func(_ context.Context, o PutOpts) error {\n\t\t\tt.Logf(\"got notification ID: %v\", o.NotificationID)\n\t\t\tfor _, n := range o.Notifications {\n\t\t\t\tt.Logf(\"manifest(%v): %v %v\", n.Manifest, n.Reason, n.Vulnerability.Name)\n\t\t\t}\n\t\t\tif got, want := len(o.Notifications), len(vs); got != want {\n\t\t\t\tt.Errorf(\"got: %d, want: %d\", got, want)\n\t\t\t}\n\t\t\treturn nil\n\t\t},\n\t}\n\tif err := p.create(ctx, slog.Default(), e, uuid.Nil); err != nil {\n\t\tt.Error(err)\n\t}\n}\n"
  },
  {
    "path": "notifier/vulnsummary.go",
    "content": "package notifier\n\nimport \"github.com/quay/claircore\"\n\n// VulnSummary summarizes a vulnerability which triggered\n// a notification\ntype VulnSummary struct {\n\tName           string                  `json:\"name\"`\n\tDescription    string                  `json:\"description\"`\n\tPackage        *claircore.Package      `json:\"package,omitempty\"`\n\tDistribution   *claircore.Distribution `json:\"distribution,omitempty\"`\n\tRepo           *claircore.Repository   `json:\"repo,omitempty\"`\n\tSeverity       string                  `json:\"severity\"`\n\tFixedInVersion string                  `json:\"fixed_in_version\"`\n\tLinks          string                  `json:\"links\"`\n}\n\nfunc (vs *VulnSummary) FromVulnerability(v *claircore.Vulnerability) {\n\t*vs = VulnSummary{\n\t\tName:           v.Name,\n\t\tDescription:    v.Description,\n\t\tPackage:        v.Package,\n\t\tDistribution:   v.Dist,\n\t\tRepo:           v.Repo,\n\t\tSeverity:       v.NormalizedSeverity.String(),\n\t\tFixedInVersion: v.FixedInVersion,\n\t\tLinks:          v.Links,\n\t}\n}\n"
  },
  {
    "path": "notifier/webhook/cmd/webhookd/main.go",
    "content": "// Command webhookd is a server implementation of Clair's \"webhook\" notification\n// protocol.\n//\n// This command is exempt from compatibility concerns beyond being compatible\n// with the webhook protocol at the same point in the repository.\n//\n// This implementation is currently only suitable for debugging the notification\n// subsystem, but ideas and implementations for extended functionality is\n// welcome.\npackage main\n\nimport (\n\t\"context\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"flag\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/http/httputil\"\n\t\"os\"\n\t\"os/signal\"\n\t\"path\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/go-jose/go-jose/v3\"\n\t\"github.com/go-jose/go-jose/v3/jwt\"\n\t\"github.com/google/uuid\"\n\n\t\"github.com/quay/clair/v4/notifier\"\n)\n\nfunc main() {\n\tvar code int\n\tctx, done := signal.NotifyContext(context.Background(), os.Interrupt)\n\tdefer done()\n\tdefer func() {\n\t\tdone()\n\t\tif code != 0 {\n\t\t\tos.Exit(code)\n\t\t}\n\t}()\n\tvar level slog.LevelVar\n\tflag.BoolFunc(\"D\", \"print debugging output\", func(arg string) error {\n\t\tl := slog.LevelInfo\n\t\tif ok, _ := strconv.ParseBool(arg); ok {\n\t\t\tl = slog.LevelDebug\n\t\t}\n\t\tlevel.Set(l)\n\t\treturn nil\n\t})\n\taddr := flag.String(\"listen\", \":http\", \"address to listen on\")\n\tkeyEnc := flag.String(\"key\", \"\", \"base64 encoded PSK for signed requests\")\n\tiss := flag.String(\"iss\", \"quay\", \"issuer for signed requests\")\n\tflag.Parse()\n\n\tslog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{\n\t\tLevel: &level,\n\t})))\n\n\th := &Recv{\n\t\tClient: http.DefaultClient,\n\t}\n\n\tif len(*keyEnc) != 0 {\n\t\tb := []byte(*keyEnc)\n\t\tl := base64.StdEncoding.DecodedLen(len(b))\n\t\tkey := make([]byte, l)\n\t\tn, err := base64.StdEncoding.Decode(key, b)\n\t\tif err != nil {\n\t\t\tslog.ErrorContext(ctx, \"unable to decode key\", \"reason\", err)\n\t\t\tcode = 1\n\t\t\treturn\n\t\t}\n\t\tkey = key[:n]\n\t\tslog.DebugContext(ctx, \"decoded key\", \"key\", key)\n\t\tsk := jose.SigningKey{\n\t\t\tAlgorithm: jose.HS256,\n\t\t\tKey:       key,\n\t\t}\n\t\th.Signer, err = jose.NewSigner(sk, nil)\n\t\tif err != nil {\n\t\t\tslog.ErrorContext(ctx, \"unable to create signer\", \"reason\", err)\n\t\t\tcode = 1\n\t\t\treturn\n\t\t}\n\t\th.Claim = &jwt.Claims{Issuer: *iss}\n\t}\n\tsrv := http.Server{\n\t\tAddr:        *addr,\n\t\tHandler:     h,\n\t\tBaseContext: func(_ net.Listener) context.Context { return ctx },\n\t}\n\tgo func() {\n\t\tif err := srv.ListenAndServe(); err != http.ErrServerClosed {\n\t\t\tslog.ErrorContext(ctx, \"unable to start HTTP server\", \"reason\", err)\n\t\t\tdone()\n\t\t\tcode = 1\n\t\t\treturn\n\t\t}\n\t}()\n\n\tslog.InfoContext(ctx, \"ready\")\n\tdefer func() {\n\t\tslog.InfoContext(ctx, \"shutting down\")\n\t\tif err := srv.Shutdown(ctx); err != nil && err != context.Canceled {\n\t\t\tslog.ErrorContext(ctx, \"HTTP server shutdown\", \"reason\", err)\n\t\t}\n\t}()\n\t<-ctx.Done()\n}\n\n// Recv implements the Clair notifier's \"webhook\" protocol.\ntype Recv struct {\n\tClient *http.Client\n\tSigner jose.Signer\n\tClaim  *jwt.Claims\n}\n\nconst contentType = `application/json`\n\n// ServeHTTP implements [http.Handler].\nfunc (h *Recv) ServeHTTP(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\trc := http.NewResponseController(w)\n\tdefer rc.Flush()\n\n\tslog.DebugContext(ctx, \"received hook\", \"request\", (*logRequest)(r))\n\n\tif r.Method != http.MethodPost {\n\t\tw.Header().Set(\"Allow\", http.MethodPost)\n\t\thttp.Error(w, \"\", http.StatusMethodNotAllowed)\n\t\tslog.WarnContext(ctx, \"bad request\", \"method\", r.Method)\n\t\treturn\n\t}\n\tif ct := r.Header.Get(`content-type`); ct != contentType {\n\t\tw.Header().Set(\"Accept-Post\", contentType)\n\t\thttp.Error(w, \"\", http.StatusUnsupportedMediaType)\n\t\tslog.WarnContext(ctx, \"bad request\", \"content-type\", ct)\n\t\treturn\n\t}\n\n\tdefer func() {\n\t\tif err := r.Body.Close(); err != nil {\n\t\t\tslog.WarnContext(ctx, \"unable to close request body\", \"reason\", err)\n\t\t\tpanic(http.ErrAbortHandler)\n\t\t}\n\t}()\n\tvar payload notifier.Callback\n\tdec := json.NewDecoder(r.Body)\n\tif err := dec.Decode(&payload); err != nil {\n\t\thttp.Error(w, fmt.Sprintf(\"bad content: %v\", err), http.StatusBadRequest)\n\t\tslog.WarnContext(ctx, \"bad payload\", \"reason\", err)\n\t\treturn\n\t}\n\twhid := path.Base(payload.Callback.Path)\n\n\tvar resp response\n\tfor next := new(uuid.UUID); next != nil; next = resp.Page.Next {\n\t\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, payload.Callback.String(), nil)\n\t\tif err != nil {\n\t\t\thttp.Error(w, fmt.Sprintf(\"unable to create request: %v\", err), http.StatusInternalServerError)\n\t\t\tslog.WarnContext(ctx, \"unable to create request\", \"reason\", err)\n\t\t\treturn\n\t\t}\n\t\tif pg := resp.Page.Next; pg != nil {\n\t\t\tv := req.URL.Query()\n\t\t\tv.Set(`next`, pg.String())\n\t\t\treq.URL.RawQuery = v.Encode()\n\t\t}\n\t\tif err := h.sign(req); err != nil {\n\t\t\thttp.Error(w, fmt.Sprintf(\"unable to sign request: %v\", err), http.StatusInternalServerError)\n\t\t\tslog.WarnContext(ctx, \"unable to sign request\", \"reason\", err)\n\t\t\treturn\n\t\t}\n\n\t\tslog.DebugContext(ctx, \"making request\", \"request\", (*logRequest)(req))\n\n\t\tres, err := http.DefaultClient.Do(req)\n\t\tif err != nil {\n\t\t\thttp.Error(w, fmt.Sprintf(\"error making request: %v\", err), http.StatusInternalServerError)\n\t\t\tslog.WarnContext(ctx, \"unable to make request\", \"reason\", err)\n\t\t\treturn\n\t\t}\n\t\tdefer res.Body.Close()\n\n\t\tslog.DebugContext(ctx, \"got response\", \"response\", (*logResponse)(res))\n\t\tif res.StatusCode != http.StatusOK {\n\t\t\thttp.Error(w, fmt.Sprintf(\"bad response from upstream: %q\", res.Status), http.StatusInternalServerError)\n\t\t\tslog.WarnContext(ctx, \"bad status\", \"status\", res.Status)\n\t\t\treturn\n\t\t}\n\n\t\tif err := json.NewDecoder(res.Body).Decode(&resp); err != nil {\n\t\t\thttp.Error(w, fmt.Sprintf(\"bad content from upstream: %v\", err), http.StatusTeapot)\n\t\t\tslog.WarnContext(ctx, \"bad content\", \"reason\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfor _, n := range resp.Notifications {\n\t\t\tslog.InfoContext(ctx, \"notification\", \"id\", whid, slog.Group(\"notification\",\n\t\t\t\t\"id\", n.ID,\n\t\t\t\t\"manifest\", n.Manifest,\n\t\t\t\t\"reason\", n.Reason,\n\t\t\t\t\"name\", n.Vulnerability.Name,\n\t\t\t))\n\t\t}\n\t}\n\treq, err := http.NewRequestWithContext(ctx, http.MethodDelete, payload.Callback.String(), nil)\n\tif err != nil {\n\t\thttp.Error(w, fmt.Sprintf(\"unable to create request: %v\", err), http.StatusInternalServerError)\n\t\tslog.WarnContext(ctx, \"unable to create request\", \"reason\", err)\n\t\treturn\n\t}\n\tif err := h.sign(req); err != nil {\n\t\thttp.Error(w, fmt.Sprintf(\"unable to sign request: %v\", err), http.StatusInternalServerError)\n\t\tslog.WarnContext(ctx, \"unable to sign request\", \"reason\", err)\n\t\treturn\n\t}\n\tslog.DebugContext(ctx, \"making request\", \"request\", (*logRequest)(req))\n\n\tres, err := http.DefaultClient.Do(req)\n\tif err != nil {\n\t\thttp.Error(w, fmt.Sprintf(\"error making request: %v\", err), http.StatusInternalServerError)\n\t\tslog.WarnContext(ctx, \"unable to make request\", \"reason\", err)\n\t\treturn\n\t}\n\tdefer res.Body.Close()\n\tslog.DebugContext(ctx, \"got response\", \"response\", (*logResponse)(res))\n\tif res.StatusCode != http.StatusOK {\n\t\thttp.Error(w, fmt.Sprintf(\"bad response from upstream: %q\", res.Status), http.StatusInternalServerError)\n\t\tslog.WarnContext(ctx, \"bad status\", \"status\", res.Status)\n\t\treturn\n\t}\n\tslog.InfoContext(ctx, \"deleted\", \"id\", whid)\n}\n\n// Response is a page of notifications.\ntype response struct {\n\tPage          notifier.Page           `json:\"page\"`\n\tNotifications []notifier.Notification `json:\"notifications\"`\n}\n\n// Sign does what it says on the tin.\nfunc (h *Recv) sign(req *http.Request) error {\n\tif h.Signer == nil {\n\t\treturn nil\n\t}\n\tnow := time.Now()\n\tcl := *h.Claim\n\tcl.IssuedAt = jwt.NewNumericDate(now)\n\tcl.NotBefore = jwt.NewNumericDate(now.Add(-jwt.DefaultLeeway))\n\tcl.Expiry = jwt.NewNumericDate(now.Add(jwt.DefaultLeeway))\n\ttok, err := jwt.Signed(h.Signer).Claims(&cl).CompactSerialize()\n\tif err != nil {\n\t\treturn err\n\t}\n\treq.Header.Set(\"authorization\", \"Bearer \"+tok)\n\treturn nil\n}\n\nvar (\n\t_ slog.LogValuer = (*logRequest)(nil)\n\t_ slog.LogValuer = (*logResponse)(nil)\n)\n\ntype logRequest http.Request\n\n// LogValue implements [slog.LogValuer].\nfunc (l *logRequest) LogValue() slog.Value {\n\treq := (*http.Request)(l)\n\tb, err := httputil.DumpRequest(req, true)\n\tif err != nil {\n\t\treturn slog.GroupValue(slog.String(\"error\", err.Error()))\n\t}\n\treturn slog.StringValue(string(b))\n}\n\ntype logResponse http.Response\n\n// LogValue implements [slog.LogValuer].\nfunc (l *logResponse) LogValue() slog.Value {\n\tres := (*http.Response)(l)\n\tb, err := httputil.DumpResponse(res, true)\n\tif err != nil {\n\t\treturn slog.GroupValue(slog.String(\"error\", err.Error()))\n\t}\n\treturn slog.StringValue(string(b))\n}\n"
  },
  {
    "path": "notifier/webhook/deliverer.go",
    "content": "package webhook\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"log/slog\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"sync\"\n\n\t\"github.com/google/uuid\"\n\t\"github.com/quay/clair/config\"\n\n\tclairerror \"github.com/quay/clair/v4/clair-error\"\n\t\"github.com/quay/clair/v4/internal/codec\"\n\t\"github.com/quay/clair/v4/internal/httputil\"\n\t\"github.com/quay/clair/v4/notifier\"\n)\n\n// SignedOnce is used to print a deprecation notice, but only once per run.\nvar signedOnce sync.Once\n\ntype Deliverer struct {\n\t// a client to use for POSTing webhooks\n\tc        *http.Client\n\tcallback *url.URL\n\ttarget   *url.URL\n\tsigner   Signer\n\theaders  http.Header\n}\n\ntype Signer interface {\n\tSign(context.Context, *http.Request) error\n}\n\n// New returns a new webhook Deliverer\nfunc New(conf *config.Webhook, client *http.Client, signer Signer) (*Deliverer, error) {\n\tswitch {\n\tcase conf == nil:\n\t\treturn nil, errors.New(\"config not provided\")\n\tcase client == nil:\n\t\treturn nil, errors.New(\"http client not provided\")\n\t}\n\tvar d Deliverer\n\tvar err error\n\n\td.callback, err = url.Parse(conf.Callback)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\td.target, err = url.Parse(conf.Target)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\td.headers = conf.Headers.Clone()\n\tif d.headers == nil {\n\t\td.headers = make(map[string][]string)\n\t}\n\td.headers.Set(\"content-type\", \"application/json\")\n\td.signer = signer\n\n\td.c = client\n\treturn &d, nil\n}\n\nfunc (d *Deliverer) Name() string {\n\treturn \"webhook\"\n}\n\n// Deliver implements the notifier.Deliverer interface.\n//\n// Deliver POSTS a webhook data structure to the configured target.\nfunc (d *Deliverer) Deliver(ctx context.Context, nID uuid.UUID) error {\n\tlog := slog.With(\"notification_id\", &nID)\n\n\tcallback, err := d.callback.Parse(nID.String())\n\tif err != nil {\n\t\treturn err\n\t}\n\n\twh := notifier.Callback{\n\t\tNotificationID: nID,\n\t\tCallback:       *callback,\n\t}\n\n\treq, err := httputil.NewRequestWithContext(ctx, http.MethodPost, d.target.String(), codec.JSONReader(&wh))\n\tif err != nil {\n\t\treturn err\n\t}\n\tfor k, vs := range d.headers {\n\t\tfor _, v := range vs {\n\t\t\treq.Header.Add(k, v)\n\t\t}\n\t}\n\tif d.signer != nil {\n\t\tif err := d.signer.Sign(ctx, req); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tlog.InfoContext(ctx, \"dispatching webhook\",\n\t\t\"callback\", callback,\n\t\t\"target\", d.target)\n\n\tresp, err := d.c.Do(req)\n\tif err != nil {\n\t\treturn &clairerror.ErrDeliveryFailed{E: err}\n\t}\n\tdefer resp.Body.Close()\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn &clairerror.ErrDeliveryFailed{\n\t\t\tE: &clairerror.ErrRequestFail{\n\t\t\t\tCode:   resp.StatusCode,\n\t\t\t\tStatus: resp.Status,\n\t\t\t},\n\t\t}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "notifier/webhook/deliverer_test.go",
    "content": "package webhook\n\nimport (\n\t\"encoding/json\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"path\"\n\t\"sync\"\n\t\"testing\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/google/uuid\"\n\t\"github.com/quay/clair/config\"\n\t\"github.com/quay/claircore/test\"\n\n\t\"github.com/quay/clair/v4/notifier\"\n)\n\nvar (\n\tcallback = \"http://clair-notifier/notifier/api/v1/notification/\"\n\tnoteID   = uuid.New()\n)\n\n// TestDeliverer confirms the deliverer correctly sends the webhook\n// data structure to the configured target.\nfunc TestDeliverer(t *testing.T) {\n\tvar whResult struct {\n\t\tsync.Mutex\n\t\tcb notifier.Callback\n\t}\n\tserver := httptest.NewServer(http.HandlerFunc(\n\t\tfunc(w http.ResponseWriter, r *http.Request) {\n\t\t\tif r.Method != http.MethodPost {\n\t\t\t\tw.WriteHeader(http.StatusMethodNotAllowed)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tvar cb notifier.Callback\n\t\t\terr := json.NewDecoder(r.Body).Decode(&cb)\n\t\t\tif err != nil {\n\t\t\t\tw.WriteHeader(http.StatusBadRequest)\n\t\t\t\treturn\n\t\t\t}\n\t\t\twhResult.Lock()\n\t\t\twhResult.cb = cb\n\t\t\twhResult.Unlock()\n\t\t},\n\t))\n\tdefer server.Close()\n\tctx := test.Logging(t)\n\tconf := config.Webhook{\n\t\tCallback: callback,\n\t\tTarget:   server.URL,\n\t}\n\n\td, err := New(&conf, server.Client(), nil)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to create new webhook deliverer: %v\", err)\n\t}\n\terr = d.Deliver(ctx, noteID)\n\tif err != nil {\n\t\tt.Fatalf(\"got: %v, wanted: nil\", err)\n\t}\n\n\twhResult.Lock()\n\twh := whResult.cb\n\twhResult.Unlock()\n\n\tif !cmp.Equal(wh.NotificationID, noteID) {\n\t\tt.Fatalf(\"got: %v, wanted: %v\", wh.NotificationID, noteID)\n\t}\n\n\tcbURL, err := url.Parse(callback)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to parse callback url: %v\", err)\n\t}\n\tcbURL.Path = path.Join(cbURL.Path, noteID.String())\n\n\tif got, want := wh.Callback.String(), cbURL.String(); got != want {\n\t\tt.Fatalf(\"got: %v, wanted: %v\", got, want)\n\t}\n}\n"
  }
]