Repository: anchore/grype
Pull request #2460 (2025-02-19 00:54:40)

fix(java): error out on maven search rate limiting
Following on to #2384 — this makes the failure mode return an error, in addition to logging the issue.

The motivation for this is that otherwise, the unreliability of scan results caused by Maven's rate limiting can't be noticed in programmatic contexts — only by studying log messages.

This adjusts the logic such that if the user wants to use Maven searching to affect vuln matching results, and this is unable to happen successfully, an error is returned indicating as much, which can then prompt the user explicitly to change their approach, such as by turning off the search or adjusting the self rate limiting time.

cc: @rawlingsj @wagoodman 


Patch:
diff --git a/.binny.yaml b/.binny.yaml
index a92586d6..c932c553 100644
--- a/.binny.yaml
+++ b/.binny.yaml
@@ -18,7 +18,7 @@ tools:
   # used to sign mac binaries at release
   - name: quill
     version:
-      want: v0.5.0
+      want: v0.5.1
     method: github-release
     with:
       repo: anchore/quill
@@ -26,7 +26,7 @@ tools:
   # used for linting
   - name: golangci-lint
     version:
-      want: v1.64.2
+      want: v1.64.5
     method: github-release
     with:
       repo: golangci/golangci-lint
@@ -34,7 +34,7 @@ tools:
   # used for showing the changelog at release
   - name: glow
     version:
-      want: v2.0.0
+      want: v2.1.0
     method: github-release
     with:
       repo: charmbracelet/glow
@@ -42,7 +42,7 @@ tools:
   # used for signing the checksums file at release
   - name: cosign
     version:
-      want: v2.4.2
+      want: v2.4.3
     method: github-release
     with:
       repo: sigstore/cosign
@@ -58,7 +58,7 @@ tools:
   # used to release all artifacts
   - name: goreleaser
     version:
-      want: v2.6.1
+      want: v2.7.0
     method: github-release
     with:
       repo: goreleaser/goreleaser
@@ -98,7 +98,7 @@ tools:
   # used for triggering a release
   - name: gh
     version:
-      want: v2.66.1
+      want: v2.67.0
     method: github-release
     with:
       repo: cli/cli
@@ -106,7 +106,7 @@ tools:
   # used for integration tests
   - name: skopeo
     version:
-      want: v1.17.0
+      want: v1.18.0
     method: go-install
     with:
       module: github.com/containers/skopeo
diff --git a/.github/actions/bootstrap/action.yaml b/.github/actions/bootstrap/action.yaml
index 5a3433ca..150284da 100644
--- a/.github/actions/bootstrap/action.yaml
+++ b/.github/actions/bootstrap/action.yaml
@@ -43,7 +43,7 @@ runs:
 
     - name: Restore tool cache
       id: tool-cache
-      uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
+      uses: actions/cache@d4323d4df104b026a6aa633fdb11d772146be0bf # v4.2.2
       if: inputs.tools == 'true'
       with:
         path: ${{ github.workspace }}/.tool
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index cde83ffa..67d1fc17 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -46,7 +46,7 @@ jobs:
       uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
 
     - name: Utilize Go Module Cache
-      uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
+      uses: actions/cache@d4323d4df104b026a6aa633fdb11d772146be0bf # v4.2.2
       with:
         path: |
           ~/go/pkg/mod
@@ -63,7 +63,7 @@ jobs:
 
     # Initializes the CodeQL tools for scanning.
     - name: Initialize CodeQL
-      uses: github/codeql-action/init@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # v3.28.9
+      uses: github/codeql-action/init@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3.28.10
       with:
         languages: ${{ matrix.language }}
         # If you wish to specify custom queries, you can do so here or in a config file.
@@ -85,4 +85,4 @@ jobs:
       run: make grype
 
     - name: Perform CodeQL Analysis
-      uses: github/codeql-action/analyze@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # v3.28.9
+      uses: github/codeql-action/analyze@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3.28.10
diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml
index 93f10a28..10c014b2 100644
--- a/.github/workflows/scorecards.yml
+++ b/.github/workflows/scorecards.yml
@@ -25,7 +25,7 @@ jobs:
           persist-credentials: false
 
       - name: "Run analysis"
-        uses: ossf/scorecard-action@62b2cac7ed8198b15735ed49ab1e5cf35480ba46 # v2.4.0
+        uses: ossf/scorecard-action@f49aabe0b5af0936a0987cfb85d86b75731b0186 # v2.4.1
         with:
           results_file: results.sarif
           results_format: sarif
@@ -38,6 +38,6 @@ jobs:
 
       # Upload the results to GitHub's code scanning dashboard.
       - name: "Upload to code-scanning"
-        uses: github/codeql-action/upload-sarif@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # v1.0.26
+        uses: github/codeql-action/upload-sarif@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v1.0.26
         with:
           sarif_file: results.sarif
diff --git a/.github/workflows/update-anchore-dependencies.yml b/.github/workflows/update-anchore-dependencies.yml
index 28184af6..763c839b 100644
--- a/.github/workflows/update-anchore-dependencies.yml
+++ b/.github/workflows/update-anchore-dependencies.yml
@@ -35,7 +35,7 @@ jobs:
           app_id: ${{ secrets.TOKEN_APP_ID }}
           private_key: ${{ secrets.TOKEN_APP_PRIVATE_KEY }}
 
-      - uses: peter-evans/create-pull-request@67ccf781d68cd99b580ae25a5c18a1cc84ffff1f #v7.0.6
+      - uses: peter-evans/create-pull-request@dd2324fc52d5d43c699a5636bcf19fceaa70c284 #v7.0.7
         with:
           signoff: true
           delete-branch: true
diff --git a/.github/workflows/update-bootstrap-tools.yml b/.github/workflows/update-bootstrap-tools.yml
index 7f358297..a157c0b7 100644
--- a/.github/workflows/update-bootstrap-tools.yml
+++ b/.github/workflows/update-bootstrap-tools.yml
@@ -50,7 +50,7 @@ jobs:
           app_id: ${{ secrets.TOKEN_APP_ID }}
           private_key: ${{ secrets.TOKEN_APP_PRIVATE_KEY }}
 
-      - uses: peter-evans/create-pull-request@67ccf781d68cd99b580ae25a5c18a1cc84ffff1f #v7.0.6
+      - uses: peter-evans/create-pull-request@dd2324fc52d5d43c699a5636bcf19fceaa70c284 #v7.0.7
         with:
           signoff: true
           delete-branch: true
diff --git a/.github/workflows/update-quality-gate-db.yml b/.github/workflows/update-quality-gate-db.yml
index 0de3b56d..af97aa49 100644
--- a/.github/workflows/update-quality-gate-db.yml
+++ b/.github/workflows/update-quality-gate-db.yml
@@ -25,7 +25,7 @@ jobs:
           app_id: ${{ secrets.TOKEN_APP_ID }}
           private_key: ${{ secrets.TOKEN_APP_PRIVATE_KEY }}
 
-      - uses: peter-evans/create-pull-request@67ccf781d68cd99b580ae25a5c18a1cc84ffff1f #v7.0.6
+      - uses: peter-evans/create-pull-request@dd2324fc52d5d43c699a5636bcf19fceaa70c284 #v7.0.7
         with:
           signoff: true
           delete-branch: true
diff --git a/.github/workflows/validations.yaml b/.github/workflows/validations.yaml
index 5bcb628c..b3b540e6 100644
--- a/.github/workflows/validations.yaml
+++ b/.github/workflows/validations.yaml
@@ -60,7 +60,7 @@ jobs:
 
       - name: Upload the provider state archive
         if: ${{ failure() }}
-        uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
+        uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
         with:
           name: qg-capture-state
           path: qg-capture-state.tar.gz
@@ -103,7 +103,7 @@ jobs:
         uses: ./.github/actions/bootstrap
 
       - name: Restore integration test cache
-        uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 #v4.2.0
+        uses: actions/cache@d4323d4df104b026a6aa633fdb11d772146be0bf #v4.2.2
         with:
           path: ${{ github.workspace }}/test/integration/test-fixtures/cache
           key: ${{ runner.os }}-integration-test-cache-${{ hashFiles('test/integration/test-fixtures/cache.fingerprint') }}
@@ -134,7 +134,7 @@ jobs:
       # why not use actions/upload-artifact? It is very slow (3 minutes to upload ~600MB of data, vs 10 seconds with this approach).
       # see https://github.com/actions/upload-artifact/issues/199 for more info
       - name: Upload snapshot artifacts
-        uses: actions/cache/save@1bd1e32a3bdc45362d1e726936510720a7c30a57 #v4.2.0
+        uses: actions/cache/save@d4323d4df104b026a6aa633fdb11d772146be0bf #v4.2.2
         with:
           path: snapshot
           key: snapshot-build-${{ github.run_id }}
@@ -148,14 +148,14 @@ jobs:
       - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
 
       - name: Download snapshot build
-        uses: actions/cache/restore@1bd1e32a3bdc45362d1e726936510720a7c30a57 #v4.2.0
+        uses: actions/cache/restore@d4323d4df104b026a6aa633fdb11d772146be0bf #v4.2.2
         with:
           path: snapshot
           key: snapshot-build-${{ github.run_id }}
 
       - name: Restore install.sh test image cache
         id: install-test-image-cache
-        uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 #v4.2.0
+        uses: actions/cache@d4323d4df104b026a6aa633fdb11d772146be0bf #v4.2.2
         with:
           path: ${{ github.workspace }}/test/install/cache
           key: ${{ runner.os }}-install-test-image-cache-${{ hashFiles('test/install/cache.fingerprint') }}
@@ -178,19 +178,19 @@ jobs:
     runs-on: macos-latest
     steps:
       - name: Install Cosign
-        uses: sigstore/cosign-installer@c56c2d3e59e4281cc41dea2217323ba5694b171e #v3.8.0
+        uses: sigstore/cosign-installer@d7d6bc7722e3daa8354c50bcb52f4837da5e9b6a #v3.8.1
 
       - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
 
       - name: Download snapshot build
-        uses: actions/cache/restore@1bd1e32a3bdc45362d1e726936510720a7c30a57 #v4.2.0
+        uses: actions/cache/restore@d4323d4df104b026a6aa633fdb11d772146be0bf #v4.2.2
         with:
           path: snapshot
           key: snapshot-build-${{ github.run_id }}
 
       - name: Restore docker image cache for compare testing
         id: mac-compare-testing-cache
-        uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 #v4.2.0
+        uses: actions/cache@d4323d4df104b026a6aa633fdb11d772146be0bf #v4.2.2
         with:
           path: image.tar
           key: ${{ runner.os }}-${{ hashFiles('test/compare/mac.sh') }}
@@ -211,13 +211,13 @@ jobs:
         uses: ./.github/actions/bootstrap
 
       - name: Restore CLI test-fixture cache
-        uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 #v4.2.0
+        uses: actions/cache@d4323d4df104b026a6aa633fdb11d772146be0bf #v4.2.2
         with:
           path: ${{ github.workspace }}/test/cli/test-fixtures/cache
           key: ${{ runner.os }}-cli-test-cache-${{ hashFiles('test/cli/test-fixtures/cache.fingerprint') }}
 
       - name: Download snapshot build
-        uses: actions/cache/restore@1bd1e32a3bdc45362d1e726936510720a7c30a57 #v4.2.0
+        uses: actions/cache/restore@d4323d4df104b026a6aa633fdb11d772146be0bf #v4.2.2
         with:
           path: snapshot
           key: snapshot-build-${{ github.run_id }}
diff --git a/Taskfile.yaml b/Taskfile.yaml
index a4cc0a39..454ef2f7 100644
--- a/Taskfile.yaml
+++ b/Taskfile.yaml
@@ -375,7 +375,7 @@ tasks:
       - "{{ .NEXT_VERSION }}"
     cmds:
       - "{{ .TOOL_DIR }}/chronicle -vv -n --version-file {{ .NEXT_VERSION }} > {{ .CHANGELOG }}"
-      - "{{ .TOOL_DIR }}/glow {{ .CHANGELOG }}"
+      - "{{ .TOOL_DIR }}/glow -w 0 {{ .CHANGELOG }}"
 
 
   ## Release targets #################################
diff --git a/go.mod b/go.mod
index 59b36efd..d2c5daee 100644
--- a/go.mod
+++ b/go.mod
@@ -15,23 +15,23 @@ require (
 	github.com/anchore/go-logger v0.0.0-20230725134548-c21dafa1ec5a
 	github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04
 	github.com/anchore/go-version v1.2.2-0.20210903204242-51efa5b487c4
-	github.com/anchore/packageurl-go v0.1.1-0.20250117185454-edf36a908b10
+	github.com/anchore/packageurl-go v0.1.1-0.20250220190351-d62adb6e1115
 	github.com/anchore/stereoscope v0.0.13
-	github.com/anchore/syft v1.19.0
+	github.com/anchore/syft v1.20.0
 	github.com/aquasecurity/go-pep440-version v0.0.1
 	github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de
 	github.com/bmatcuk/doublestar/v2 v2.0.4
-	github.com/charmbracelet/bubbletea v1.3.3
+	github.com/charmbracelet/bubbletea v1.3.4
 	github.com/charmbracelet/lipgloss v1.0.0
 	github.com/dave/jennifer v1.7.1
-	github.com/docker/docker v27.5.1+incompatible
+	github.com/docker/docker v28.0.1+incompatible
 	github.com/dustin/go-humanize v1.0.1
 	github.com/facebookincubator/nvdtools v0.1.5
 	github.com/gabriel-vasile/mimetype v1.4.8
 	github.com/gkampitakis/go-snaps v0.5.11
 	github.com/glebarez/sqlite v1.11.0
 	github.com/go-test/deep v1.1.1
-	github.com/google/go-cmp v0.6.0
+	github.com/google/go-cmp v0.7.0
 	github.com/google/go-containerregistry v0.20.3
 	github.com/google/uuid v1.6.0
 	github.com/gookit/color v1.5.4
@@ -92,7 +92,7 @@ require (
 	github.com/Masterminds/semver/v3 v3.3.0 // indirect
 	github.com/Microsoft/go-winio v0.6.2 // indirect
 	github.com/Microsoft/hcsshim v0.11.7 // indirect
-	github.com/ProtonMail/go-crypto v1.1.3 // indirect
+	github.com/ProtonMail/go-crypto v1.1.5 // indirect
 	github.com/acobaugh/osrelease v0.1.0 // indirect
 	github.com/agext/levenshtein v1.2.1 // indirect
 	github.com/anchore/fangs v0.0.0-20241014225144-4e1713cafd77 // indirect
@@ -106,7 +106,8 @@ require (
 	github.com/bahlo/generic-list-go v0.2.0 // indirect
 	github.com/becheran/wildmatch-go v1.0.0 // indirect
 	github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect
-	github.com/bmatcuk/doublestar/v4 v4.8.0 // indirect
+	github.com/bitnami/go-version v0.0.0-20250131085805-b1f57a8634ef // indirect
+	github.com/bmatcuk/doublestar/v4 v4.8.1 // indirect
 	github.com/buger/jsonparser v1.1.1 // indirect
 	github.com/census-instrumentation/opencensus-proto v0.4.1 // indirect
 	github.com/cespare/xxhash/v2 v2.3.0 // indirect
@@ -144,7 +145,7 @@ require (
 	github.com/envoyproxy/go-control-plane v0.13.1 // indirect
 	github.com/envoyproxy/protoc-gen-validate v1.1.0 // indirect
 	github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
-	github.com/felixge/fgprof v0.9.3 // indirect
+	github.com/felixge/fgprof v0.9.5 // indirect
 	github.com/felixge/httpsnoop v1.0.4 // indirect
 	github.com/fsnotify/fsnotify v1.7.0 // indirect
 	github.com/github/go-spdx/v2 v2.3.2 // indirect
@@ -153,7 +154,7 @@ require (
 	github.com/glebarez/go-sqlite v1.21.2 // indirect
 	github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
 	github.com/go-git/go-billy/v5 v5.6.2 // indirect
-	github.com/go-git/go-git/v5 v5.13.1 // indirect
+	github.com/go-git/go-git/v5 v5.13.2 // indirect
 	github.com/go-logr/logr v1.4.2 // indirect
 	github.com/go-logr/stdr v1.2.2 // indirect
 	github.com/go-restruct/restruct v1.2.0-alpha // indirect
@@ -221,7 +222,7 @@ require (
 	github.com/pelletier/go-toml v1.9.5 // indirect
 	github.com/pelletier/go-toml/v2 v2.2.2 // indirect
 	github.com/pierrec/lz4/v4 v4.1.21 // indirect
-	github.com/pjbgf/sha1cd v0.3.0 // indirect
+	github.com/pjbgf/sha1cd v0.3.2 // indirect
 	github.com/pkg/errors v0.9.1 // indirect
 	github.com/pkg/profile v1.7.0 // indirect
 	github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
@@ -289,10 +290,10 @@ require (
 	google.golang.org/protobuf v1.36.3 // indirect
 	gopkg.in/ini.v1 v1.67.0 // indirect
 	gopkg.in/warnings.v0 v0.1.2 // indirect
-	modernc.org/libc v1.55.3 // indirect
-	modernc.org/mathutil v1.6.0 // indirect
-	modernc.org/memory v1.8.0 // indirect
-	modernc.org/sqlite v1.34.5 // indirect
+	modernc.org/libc v1.61.13 // indirect
+	modernc.org/mathutil v1.7.1 // indirect
+	modernc.org/memory v1.8.2 // indirect
+	modernc.org/sqlite v1.35.0 // indirect
 )
 
 // this is a breaking change, so we need to pin the version until glebarez/go-sqlite is updated to use internal/libc
diff --git a/go.sum b/go.sum
index cdcab5a3..13771c0a 100644
--- a/go.sum
+++ b/go.sum
@@ -662,8 +662,8 @@ github.com/Microsoft/hcsshim v0.11.7/go.mod h1:MV8xMfmECjl5HdO7U/3/hFVnkmSBjAjmA
 github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
 github.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8=
 github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q=
-github.com/ProtonMail/go-crypto v1.1.3 h1:nRBOetoydLeUb4nHajyO2bKqMLfWQ/ZPwkXqXxPxCFk=
-github.com/ProtonMail/go-crypto v1.1.3/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
+github.com/ProtonMail/go-crypto v1.1.5 h1:eoAQfK2dwL+tFSFpr7TbOaPNUbPiJj4fLYwwGE1FQO4=
+github.com/ProtonMail/go-crypto v1.1.5/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
 github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8=
 github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo=
 github.com/acobaugh/osrelease v0.1.0 h1:Yb59HQDGGNhCj4suHaFQQfBps5wyoKLSSX/J/+UifRE=
@@ -700,12 +700,12 @@ github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04 h1:VzprUTpc0v
 github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04/go.mod h1:6dK64g27Qi1qGQZ67gFmBFvEHScy0/C8qhQhNe5B5pQ=
 github.com/anchore/go-version v1.2.2-0.20210903204242-51efa5b487c4 h1:rmZG77uXgE+o2gozGEBoUMpX27lsku+xrMwlmBZJtbg=
 github.com/anchore/go-version v1.2.2-0.20210903204242-51efa5b487c4/go.mod h1:Bkc+JYWjMCF8OyZ340IMSIi2Ebf3uwByOk6ho4wne1E=
-github.com/anchore/packageurl-go v0.1.1-0.20250117185454-edf36a908b10 h1:zBedM9ZGYbs/61QC4ZOKxtChx5njXKHgHqDeHuUxrTw=
-github.com/anchore/packageurl-go v0.1.1-0.20250117185454-edf36a908b10/go.mod h1:KoYIv7tdP5+CC9VGkeZV4/vGCKsY55VvoG+5dadg4YI=
+github.com/anchore/packageurl-go v0.1.1-0.20250220190351-d62adb6e1115 h1:ZyRCmiEjnoGJZ1+Ah0ZZ/mKKqNhGcUZBl0s7PTTDzvY=
+github.com/anchore/packageurl-go v0.1.1-0.20250220190351-d62adb6e1115/go.mod h1:KoYIv7tdP5+CC9VGkeZV4/vGCKsY55VvoG+5dadg4YI=
 github.com/anchore/stereoscope v0.0.13 h1:9Ivkh7k+vOeG3JHrt44jOg/8UdZrCvMsSjLQ7trHBig=
 github.com/anchore/stereoscope v0.0.13/go.mod h1:QfhhFc2pezp5aX/dVJ5qnBFpBUv5+KUTphwaQLxMUig=
-github.com/anchore/syft v1.19.0 h1:cUVVdbOHtYCz+581Aq2hhaEbR1MRowbwXyo+Xw+oW20=
-github.com/anchore/syft v1.19.0/go.mod h1:QyTWjG0LzowbJVNQj5ZX8UVx17eTkU73Xl7jAf6upE8=
+github.com/anchore/syft v1.20.0 h1:4nVM/eiqrb2GJCkW+d1xv8M5mxply8vVblpWOvVCgN8=
+github.com/anchore/syft v1.20.0/go.mod h1:h8U0q+Fk7f1d9ay4oa+gDb//AJYFuQftrBLOuS6llz4=
 github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
 github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
 github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
@@ -748,10 +748,12 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r
 github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1UJrqV3uuy861HCTo708pDMbjHHdCas=
 github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4=
 github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
+github.com/bitnami/go-version v0.0.0-20250131085805-b1f57a8634ef h1:TSFnfbbu2oAOuWbeDDTtwXWE6z+PmpgbSsMBeV7l0ww=
+github.com/bitnami/go-version v0.0.0-20250131085805-b1f57a8634ef/go.mod h1:9iglf1GG4oNRJ39bZ5AZrjgAFD2RwQbXw6Qf7Cs47wo=
 github.com/bmatcuk/doublestar/v2 v2.0.4 h1:6I6oUiT/sU27eE2OFcWqBhL1SwjyvQuOssxT4a1yidI=
 github.com/bmatcuk/doublestar/v2 v2.0.4/go.mod h1:QMmcs3H2AUQICWhfzLXz+IYln8lRQmTZRptLie8RgRw=
-github.com/bmatcuk/doublestar/v4 v4.8.0 h1:DSXtrypQddoug1459viM9X9D3dp1Z7993fw36I2kNcQ=
-github.com/bmatcuk/doublestar/v4 v4.8.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
+github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR5wKP38=
+github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
 github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
 github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
 github.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oMMlVBbn9M=
@@ -772,8 +774,8 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF
 github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE=
 github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU=
-github.com/charmbracelet/bubbletea v1.3.3 h1:WpU6fCY0J2vDWM3zfS3vIDi/ULq3SYphZhkAGGvmEUY=
-github.com/charmbracelet/bubbletea v1.3.3/go.mod h1:dtcUCyCGEX3g9tosuYiut3MXgY/Jsv9nKVdibKKRRXo=
+github.com/charmbracelet/bubbletea v1.3.4 h1:kCg7B+jSCFPLYRA52SDZjr51kG/fMUEoPoZrkaDHyoI=
+github.com/charmbracelet/bubbletea v1.3.4/go.mod h1:dtcUCyCGEX3g9tosuYiut3MXgY/Jsv9nKVdibKKRRXo=
 github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ=
 github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
 github.com/charmbracelet/lipgloss v1.0.0 h1:O7VkGDvqEdGi93X+DeqsQ7PKHDgtQfF8j8/O2qFMQNg=
@@ -783,9 +785,15 @@ github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjT
 github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
 github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
 github.com/cheggaaa/pb v1.0.27/go.mod h1:pQciLPpbU0oxA0h+VJYYLxO+XeDQb5pZijXscXHm81s=
+github.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
+github.com/chromedp/chromedp v0.9.2/go.mod h1:LkSXJKONWTCHAfQasKFUZI+mxqS4tZqhmtGzzhLsnLs=
+github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww=
 github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
+github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
 github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
+github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk=
 github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
+github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
 github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
 github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
 github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
@@ -851,8 +859,8 @@ github.com/docker/cli v27.5.0+incompatible h1:aMphQkcGtpHixwwhAXJT1rrK/detk2JIvD
 github.com/docker/cli v27.5.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
 github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
 github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
-github.com/docker/docker v27.5.1+incompatible h1:4PYU5dnBYqRQi0294d1FBECqT9ECWeQAIfE8q4YnPY8=
-github.com/docker/docker v27.5.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
+github.com/docker/docker v28.0.1+incompatible h1:FCHjSRdXhNRFjlHMTv4jUNlIBbTeRjrWfeFuJp7jpo0=
+github.com/docker/docker v28.0.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
 github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo=
 github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M=
 github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
@@ -870,8 +878,8 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
 github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
 github.com/edsrzf/mmap-go v1.1.0 h1:6EUwBLQ/Mcr1EYLE4Tn1VdW1A4ckqCQWZBw8Hr0kjpQ=
 github.com/edsrzf/mmap-go v1.1.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q=
-github.com/elazarl/goproxy v1.2.3 h1:xwIyKHbaP5yfT6O9KIeYJR5549MXRQkoQMRXGztz8YQ=
-github.com/elazarl/goproxy v1.2.3/go.mod h1:YfEbZtqP4AetfO6d40vWchF3znWX7C7Vd6ZMfdL8z64=
+github.com/elazarl/goproxy v1.4.0 h1:4GyuSbFa+s26+3rmYNSuUVsx+HgPrV1bk1jXI0l9wjM=
+github.com/elazarl/goproxy v1.4.0/go.mod h1:X/5W/t+gzDyLfHW4DrMdpjqYjpXsURlBt9lpBDxZZZQ=
 github.com/elliotchance/phpserialize v1.4.0 h1:cAp/9+KSnEbUC8oYCE32n2n84BeW8HOY3HMDI8hG2OY=
 github.com/elliotchance/phpserialize v1.4.0/go.mod h1:gt7XX9+ETUcLXbtTKEuyrqW3lcLUAeS/AnGZ2e49TZs=
 github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
@@ -907,8 +915,9 @@ github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL
 github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
 github.com/fatih/set v0.2.1 h1:nn2CaJyknWE/6txyUDGwysr3G5QC6xWB/PtVjPBbeaA=
 github.com/fatih/set v0.2.1/go.mod h1:+RKtMCH+favT2+3YecHGxcc0b4KyVWA1QWWJUs4E0CI=
-github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g=
 github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw=
+github.com/felixge/fgprof v0.9.5 h1:8+vR6yu2vvSKn08urWyEuxx75NWPEvybbkBirEpsbVY=
+github.com/felixge/fgprof v0.9.5/go.mod h1:yKl+ERSa++RYOs32d8K6WEXCB4uXdLls4ZaZPpayhMM=
 github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
 github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
@@ -946,8 +955,8 @@ github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UN
 github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU=
 github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
 github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
-github.com/go-git/go-git/v5 v5.13.1 h1:DAQ9APonnlvSWpvolXWIuV6Q6zXy2wHbN4cVlNR5Q+M=
-github.com/go-git/go-git/v5 v5.13.1/go.mod h1:qryJB4cSBoq3FRoBRf5A77joojuBcmPJ0qu3XXXVixc=
+github.com/go-git/go-git/v5 v5.13.2 h1:7O7xvsK7K+rZPKW6AQR1YyNhfywkv7B8/FsP3ki6Zv0=
+github.com/go-git/go-git/v5 v5.13.2/go.mod h1:hWdW5P4YZRjmpGHwRH2v3zkWcNl6HeXaXQEMGb3NJ9A=
 github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
@@ -970,6 +979,9 @@ github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LB
 github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
 github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
 github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
+github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
+github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
+github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
 github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
 github.com/goccy/go-yaml v1.15.13 h1:Xd87Yddmr2rC1SLLTm2MNDcTjeO/GYo0JGiww6gSTDg=
 github.com/goccy/go-yaml v1.15.13/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
@@ -1034,8 +1046,9 @@ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
 github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
 github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
-github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
 github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
 github.com/google/go-containerregistry v0.20.3 h1:oNx7IdTI936V8CQRveCjaxOiegWwvM7kqkbXTpyiovI=
 github.com/google/go-containerregistry v0.20.3/go.mod h1:w00pIgBRDVUDFM6bq+Qx8lwNWK+cxgCuX1vd3PIBDNI=
 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
@@ -1065,6 +1078,7 @@ github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLe
 github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
 github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
 github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg=
+github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
 github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=
 github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
 github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
@@ -1160,6 +1174,7 @@ github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47
 github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
 github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
 github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=
+github.com/ianlancetaylor/demangle v0.0.0-20230524184225-eabc099b10ab/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw=
 github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
 github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
@@ -1223,6 +1238,7 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
 github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
 github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8=
 github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
@@ -1342,6 +1358,7 @@ github.com/opencontainers/selinux v1.11.0 h1:+5Zbo97w3Lbmb3PeqQtpmTkMwsW5nRI3YaL
 github.com/opencontainers/selinux v1.11.0/go.mod h1:E5dMC3VPuVvVHDYmi78qvhJp8+M586T4DlDRYpFkyec=
 github.com/openvex/go-vex v0.2.5 h1:41utdp2rHgAGCsG+UbjmfMG5CWQxs15nGqir1eRgSrQ=
 github.com/openvex/go-vex v0.2.5/go.mod h1:j+oadBxSUELkrKh4NfNb+BPo77U3q7gdKME88IO/0Wo=
+github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
 github.com/owenrumney/go-sarif v1.1.2-0.20231003122901-1000f5e05554 h1:FvA4bwjKpPqik5WsQ8+4z4DKWgA1tO1RTTtNKr5oYNA=
 github.com/owenrumney/go-sarif v1.1.2-0.20231003122901-1000f5e05554/go.mod h1:n73K/hcuJ50MiVznXyN4rde6fZY7naGKWBXOLFTyc94=
 github.com/package-url/packageurl-go v0.1.1 h1:KTRE0bK3sKbFKAk3yy63DpeskU7Cvs/x/Da5l+RtzyU=
@@ -1363,8 +1380,8 @@ github.com/phpdave11/gofpdi v1.0.13/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk
 github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
 github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
 github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
-github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4=
-github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI=
+github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4=
+github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=
 github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
 github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -1424,8 +1441,8 @@ github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA=
 github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
 github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d h1:hrujxIzL1woJ7AwssoOcM/tq5JjjG2yYOc8odClEiXA=
 github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU=
-github.com/sanity-io/litter v1.5.6 h1:hCFycYzhRnW4niFbbmR7QKdmds69PbVa/sNmEN5euSU=
-github.com/sanity-io/litter v1.5.6/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U=
+github.com/sanity-io/litter v1.5.8 h1:uM/2lKrWdGbRXDrIq08Lh9XtVYoeGtcQxk9rtQ7+rYg=
+github.com/sanity-io/litter v1.5.8/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U=
 github.com/sassoftware/go-rpmutils v0.4.0 h1:ojND82NYBxgwrV+mX1CWsd5QJvvEZTKddtCdFLPWhpg=
 github.com/sassoftware/go-rpmutils v0.4.0/go.mod h1:3goNWi7PGAT3/dlql2lv3+MSN5jNYPjT5mVcQcIsYzI=
 github.com/scylladb/go-set v1.0.3-0.20200225121959-cc7b2070d91e h1:7q6NSFZDeGfvvtIRwBrU/aegEYJYmvev0cHAwo17zZQ=
@@ -1900,6 +1917,7 @@ golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -2377,21 +2395,21 @@ lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl
 modernc.org/cc/v3 v3.36.0/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI=
 modernc.org/cc/v3 v3.36.2/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI=
 modernc.org/cc/v3 v3.36.3/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI=
-modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ=
-modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
+modernc.org/cc/v4 v4.24.4 h1:TFkx1s6dCkQpd6dKurBNmpo+G8Zl4Sq/ztJ+2+DEsh0=
+modernc.org/cc/v4 v4.24.4/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
 modernc.org/ccgo/v3 v3.0.0-20220428102840-41399a37e894/go.mod h1:eI31LL8EwEBKPpNpA4bU1/i+sKOwOrQy8D87zWUcRZc=
 modernc.org/ccgo/v3 v3.0.0-20220430103911-bc99d88307be/go.mod h1:bwdAnOoaIt8Ax9YdWGjxWsdkPcZyRPHqrOvJxaKAKGw=
 modernc.org/ccgo/v3 v3.16.4/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ=
 modernc.org/ccgo/v3 v3.16.6/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ=
 modernc.org/ccgo/v3 v3.16.8/go.mod h1:zNjwkizS+fIFDrDjIAgBSCLkWbJuHF+ar3QRn+Z9aws=
 modernc.org/ccgo/v3 v3.16.9/go.mod h1:zNMzC9A9xeNUepy6KuZBbugn3c0Mc9TeiJO4lgvkJDo=
-modernc.org/ccgo/v4 v4.19.2 h1:lwQZgvboKD0jBwdaeVCTouxhxAyN6iawF3STraAal8Y=
-modernc.org/ccgo/v4 v4.19.2/go.mod h1:ysS3mxiMV38XGRTTcgo0DQTeTmAO4oCmJl1nX9VFI3s=
+modernc.org/ccgo/v4 v4.23.16 h1:Z2N+kk38b7SfySC1ZkpGLN2vthNJP1+ZzGZIlH7uBxo=
+modernc.org/ccgo/v4 v4.23.16/go.mod h1:nNma8goMTY7aQZQNTyN9AIoJfxav4nvTnvKThAeMDdo=
 modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ=
 modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
 modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
-modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw=
-modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
+modernc.org/gc/v2 v2.6.3 h1:aJVhcqAte49LF+mGveZ5KPlsp4tdGdAOT4sipJXADjw=
+modernc.org/gc/v2 v2.6.3/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
 modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM=
 modernc.org/libc v0.0.0-20220428101251-2d5f3daf273b/go.mod h1:p7Mg4+koNjc8jkqwcoFBJx7tXkpj00G77X7A72jXPXA=
 modernc.org/libc v1.16.0/go.mod h1:N4LD6DBE9cf+Dzf9buBlzVJndKr/iJHG97vGLHYnb5A=
@@ -2400,30 +2418,31 @@ modernc.org/libc v1.16.17/go.mod h1:hYIV5VZczAmGZAnG15Vdngn5HSF5cSkbvfz2B7GRuVU=
 modernc.org/libc v1.16.19/go.mod h1:p7Mg4+koNjc8jkqwcoFBJx7tXkpj00G77X7A72jXPXA=
 modernc.org/libc v1.17.0/go.mod h1:XsgLldpP4aWlPlsjqKRdHPqCxCjISdHfM/yeWC5GyW0=
 modernc.org/libc v1.17.1/go.mod h1:FZ23b+8LjxZs7XtFMbSzL/EhPxNbfZbErxEHc7cbD9s=
-modernc.org/libc v1.55.3 h1:AzcW1mhlPNrRtjS5sS+eW2ISCgSOLLNyFzRh/V3Qj/U=
-modernc.org/libc v1.55.3/go.mod h1:qFXepLhz+JjFThQ4kzwzOjA/y/artDeg+pcYnY+Q83w=
+modernc.org/libc v1.61.13 h1:3LRd6ZO1ezsFiX1y+bHd1ipyEHIJKvuprv0sLTBwLW8=
+modernc.org/libc v1.61.13/go.mod h1:8F/uJWL/3nNil0Lgt1Dpz+GgkApWh04N3el3hxJcA6E=
 modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
 modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
 modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
-modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
-modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
+modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
+modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
 modernc.org/memory v1.1.1/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw=
 modernc.org/memory v1.2.0/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw=
 modernc.org/memory v1.2.1/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
-modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
-modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU=
+modernc.org/memory v1.8.2 h1:cL9L4bcoAObu4NkxOlKWBWtNHIsnnACGF/TbqQ6sbcI=
+modernc.org/memory v1.8.2/go.mod h1:ZbjSvMO5NQ1A2i3bWeDiVMxIorXwdClKE/0SZ+BMotU=
 modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
-modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
 modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
-modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
-modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=
+modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
+modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
+modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
+modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
 modernc.org/sqlite v1.18.1/go.mod h1:6ho+Gow7oX5V+OiOQ6Tr4xeqbx13UZ6t+Fw9IRUG4d4=
-modernc.org/sqlite v1.34.5 h1:Bb6SR13/fjp15jt70CL4f18JIN7p7dnMExd+UFnF15g=
-modernc.org/sqlite v1.34.5/go.mod h1:YLuNmX9NKs8wRNK2ko1LW1NGYcc9FkBO69JOt1AR9JE=
+modernc.org/sqlite v1.35.0 h1:yQps4fegMnZFdphtzlfQTCNBWtS0CZv48pRpW3RFHRw=
+modernc.org/sqlite v1.35.0/go.mod h1:9cr2sicr7jIaWTBKQmAxQLfBv9LL0su4ZTEV+utt3ic=
 modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw=
 modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw=
-modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
-modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
+modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
+modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
 modernc.org/tcl v1.13.1/go.mod h1:XOLfOwzhkljL4itZkK6T72ckMgvj0BDsnKNdZVUOecw=
 modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
 modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
diff --git a/grype/db/v6/affected_package_store.go b/grype/db/v6/affected_package_store.go
index b5a8982a..f49d2841 100644
--- a/grype/db/v6/affected_package_store.go
+++ b/grype/db/v6/affected_package_store.go
@@ -16,13 +16,8 @@ import (
 )
 
 const (
-	// batchSize affects how many records are fetched at a time from the DB. Note: when using preload, row entries
-	// for related records may convey as parameters in a "WHERE x in (...)" which can lead to a large number of
-	// parameters in the query -- if above 999 then this will result in an error for sqlite. For this reason we
-	// try to keep this value well below 999.
-	batchSize = 300
-	anyPkg    = "any"
-	anyOS     = "any"
+	anyPkg = "any"
+	anyOS  = "any"
 )
 
 var NoOSSpecified = &OSSpecifier{}
@@ -405,7 +400,7 @@ func (s *affectedPackageStore) GetAffectedPackages(pkg *PackageSpecifier, config
 				blobs = append(blobs, r)
 			}
 			if err := s.blobStore.attachBlobValue(blobs...); err != nil {
-				return fmt.Errorf("unable to attach blobs: %w", err)
+				return fmt.Errorf("unable to attach affected package blobs: %w", err)
 			}
 		}
 
diff --git a/grype/db/v6/blobs.go b/grype/db/v6/blobs.go
index 80147828..92f865f0 100644
--- a/grype/db/v6/blobs.go
+++ b/grype/db/v6/blobs.go
@@ -200,3 +200,16 @@ type AffectedVersion struct {
 	// Constraint defines the version range constraint for affected versions.
 	Constraint string `json:"constraint,omitempty"`
 }
+
+type KnownExploitedVulnerabilityBlob struct {
+	Cve                        string     `json:"cve"`
+	VendorProject              string     `json:"vendor_project,omitempty"`
+	Product                    string     `json:"product,omitempty"`
+	DateAdded                  *time.Time `json:"date_added,omitempty"`
+	RequiredAction             string     `json:"required_action,omitempty"`
+	DueDate                    *time.Time `json:"due_date,omitempty"`
+	KnownRansomwareCampaignUse string     `json:"known_ransomware_campaign_use,omitempty"`
+	Notes                      string     `json:"notes,omitempty"`
+	URLs                       []string   `json:"urls,omitempty"`
+	CWEs                       []string   `json:"cwes,omitempty"`
+}
diff --git a/grype/db/v6/db.go b/grype/db/v6/db.go
index b10609c8..eaa8116b 100644
--- a/grype/db/v6/db.go
+++ b/grype/db/v6/db.go
@@ -12,8 +12,6 @@ import (
 )
 
 const (
-	VulnerabilityDBFileName = "vulnerability.db"
-
 	// We follow SchemaVer semantics (see https://snowplow.io/blog/introducing-schemaver-for-semantic-versioning-of-schemas)
 
 	// ModelVersion indicates how many breaking schema changes there have been (which will prevent interaction with any historical data)
@@ -24,9 +22,26 @@ const (
 	Revision = 0
 
 	// Addition indicates how many changes have been introduced that are compatible with all historical data
-	Addition = 0
+	Addition = 2
+
+	// v6 model changelog:
+	// 6.0.0: Initial version 🎉
+	// 6.0.1: Add CISA KEV to VulnerabilityDecorator store
+	// 6.0.2: Add EPSS to VulnerabilityDecorator store
+)
+
+const (
+	VulnerabilityDBFileName = "vulnerability.db"
+
+	// batchSize affects how many records are fetched at a time from the DB. Note: when using preload, row entries
+	// for related records may convey as parameters in a "WHERE x in (...)" which can lead to a large number of
+	// parameters in the query -- if above 999 then this will result in an error for sqlite. For this reason we
+	// try to keep this value well below 999.
+	batchSize = 300
 )
 
+var ErrDBCapabilityNotSupported = fmt.Errorf("capability not supported by DB")
+
 type ReadWriter interface {
 	Reader
 	Writer
@@ -36,15 +51,19 @@ type Reader interface {
 	DBMetadataStoreReader
 	ProviderStoreReader
 	VulnerabilityStoreReader
+	VulnerabilityDecoratorStoreReader
 	AffectedPackageStoreReader
 	AffectedCPEStoreReader
+	io.Closer
 	getDB() *gorm.DB
 	attachBlobValue(...blobable) error
 }
 
 type Writer interface {
 	DBMetadataStoreWriter
+	ProviderStoreWriter
 	VulnerabilityStoreWriter
+	VulnerabilityDecoratorStoreWriter
 	AffectedPackageStoreWriter
 	AffectedCPEStoreWriter
 	io.Closer
diff --git a/grype/db/v6/db_metadata_store.go b/grype/db/v6/db_metadata_store.go
index c5cfa853..d81d3e97 100644
--- a/grype/db/v6/db_metadata_store.go
+++ b/grype/db/v6/db_metadata_store.go
@@ -39,7 +39,7 @@ func (s *dbMetadataStore) GetDBMetadata() (*DBMetadata, error) {
 func (s *dbMetadataStore) SetDBMetadata() error {
 	log.Trace("writing DB metadata")
 
-	if err := s.db.Unscoped().Where("true").Delete(&DBMetadata{}).Error; err != nil {
+	if err := s.db.Where("true").Delete(&DBMetadata{}).Error; err != nil {
 		return fmt.Errorf("failed to delete existing DB metadata record: %w", err)
 	}
 
diff --git a/grype/db/v6/description.go b/grype/db/v6/description.go
index b5229469..47731095 100644
--- a/grype/db/v6/description.go
+++ b/grype/db/v6/description.go
@@ -1,27 +1,17 @@
 package v6
 
 import (
-	"bytes"
-	"crypto/sha256"
 	"errors"
 	"fmt"
-	"io"
 	"os"
 	"path/filepath"
-	"strings"
 	"time"
 
-	"github.com/OneOfOne/xxhash"
-	"github.com/spf13/afero"
-
-	"github.com/anchore/grype/internal/file"
 	"github.com/anchore/grype/internal/schemaver"
 )
 
 var ErrDBDoesNotExist = errors.New("database does not exist")
 
-const ChecksumFileName = VulnerabilityDBFileName + ".checksum"
-
 type Description struct {
 	// SchemaVersion is the version of the DB schema
 	SchemaVersion schemaver.SchemaVer `json:"schemaVersion,omitempty"`
@@ -96,53 +86,6 @@ func ReadDescription(dbFilePath string) (*Description, error) {
 	}, nil
 }
 
-func ReadDBChecksum(dir string) (string, error) {
-	checksumsFilePath := filepath.Join(dir, ChecksumFileName)
-	checksums, err := os.ReadFile(checksumsFilePath)
-	if err != nil {
-		return "", fmt.Errorf("failed to read checksums file: %w", err)
-	}
-
-	if len(checksums) == 0 {
-		return "", fmt.Errorf("checksums file is empty")
-	}
-
-	if !bytes.HasPrefix(checksums, []byte("xxh64:")) {
-		return "", fmt.Errorf("checksums file is not in the expected format")
-	}
-
-	return strings.TrimSpace(string(checksums)), nil
-}
-
-func CalculateDBDigest(dbFilePath string) (string, error) {
-	digest, err := file.HashFile(afero.NewOsFs(), dbFilePath, xxhash.New64())
-	if err != nil {
-		return "", fmt.Errorf("failed to calculate checksum for DB file: %w", err)
-	}
-	return fmt.Sprintf("xxh64:%s", digest), nil
-}
-
-func CalculateArchiveDigest(dbFilePath string) (string, error) {
-	digest, err := file.HashFile(afero.NewOsFs(), dbFilePath, sha256.New())
-	if err != nil {
-		return "", fmt.Errorf("failed to calculate checksum for DB archive file: %w", err)
-	}
-	return fmt.Sprintf("sha256:%s", digest), nil
-}
-
 func (m Description) String() string {
 	return fmt.Sprintf("DB(version=%s built=%s)", m.SchemaVersion, m.Built)
 }
-
-func WriteChecksums(writer io.Writer, value string) error {
-	if value == "" {
-		return fmt.Errorf("checksum is required")
-	}
-
-	if !strings.HasPrefix(value, "xxh64:") {
-		return fmt.Errorf("checksum missing algorithm prefix")
-	}
-
-	_, err := writer.Write([]byte(value))
-	return err
-}
diff --git a/grype/db/v6/distribution/client.go b/grype/db/v6/distribution/client.go
index afac7b61..464e4172 100644
--- a/grype/db/v6/distribution/client.go
+++ b/grype/db/v6/distribution/client.go
@@ -230,21 +230,18 @@ func isSupersededBy(current *v6.Description, candidate v6.Description) bool {
 		return true
 	}
 
-	otherModelPart, otherOk := candidate.SchemaVersion.ModelPart()
-	currentModelPart, currentOk := current.SchemaVersion.ModelPart()
-
-	if !currentOk {
+	if !current.SchemaVersion.Valid() {
 		log.Error("existing database has no schema version, doing nothing...")
 		return false
 	}
 
-	if !otherOk {
+	if !candidate.SchemaVersion.Valid() {
 		log.Error("update has no schema version, doing nothing...")
 		return false
 	}
 
-	if otherModelPart != currentModelPart {
-		log.WithFields("want", currentModelPart, "received", otherModelPart).Warn("update is for a different DB schema, skipping...")
+	if candidate.SchemaVersion.Model != current.SchemaVersion.Model {
+		log.WithFields("want", current.SchemaVersion.Model, "received", candidate.SchemaVersion.Model).Warn("update is for a different DB schema, skipping...")
 		return false
 	}
 
diff --git a/grype/db/v6/import_metadata.go b/grype/db/v6/import_metadata.go
new file mode 100644
index 00000000..fd037a79
--- /dev/null
+++ b/grype/db/v6/import_metadata.go
@@ -0,0 +1,96 @@
+package v6
+
+import (
+	"encoding/json"
+	"fmt"
+	"io"
+	"os"
+	"path/filepath"
+	"strings"
+
+	"github.com/OneOfOne/xxhash"
+	"github.com/spf13/afero"
+
+	"github.com/anchore/grype/internal/file"
+	"github.com/anchore/grype/internal/schemaver"
+)
+
+const ImportMetadataFileName = "import.json"
+
+type ImportMetadata struct {
+	Digest string `json:"digest"`
+
+	ClientVersion string `json:"client_version"`
+}
+
+func ReadImportMetadata(fs afero.Fs, dir string) (*ImportMetadata, error) {
+	checksumsFilePath := filepath.Join(dir, ImportMetadataFileName)
+
+	if _, err := fs.Stat(checksumsFilePath); os.IsNotExist(err) {
+		return nil, nil
+	}
+
+	content, err := afero.ReadFile(fs, checksumsFilePath)
+	if err != nil {
+		return nil, fmt.Errorf("failed to read import metadata file: %w", err)
+	}
+
+	if len(content) == 0 {
+		return nil, nil
+	}
+
+	var doc ImportMetadata
+	if err := json.Unmarshal(content, &doc); err != nil {
+		return nil, fmt.Errorf("failed to unmarshal import metadata: %w", err)
+	}
+
+	if !strings.HasPrefix(doc.Digest, "xxh64:") {
+		return nil, fmt.Errorf("import metadata digest is not in the expected format")
+	}
+
+	return &doc, nil
+}
+
+func CalculateDBDigest(fs afero.Fs, dbFilePath string) (string, error) {
+	digest, err := file.HashFile(fs, dbFilePath, xxhash.New64())
+	if err != nil {
+		return "", fmt.Errorf("failed to digest DB file: %w", err)
+	}
+	return fmt.Sprintf("xxh64:%s", digest), nil
+}
+
+func WriteImportMetadata(fs afero.Fs, dbDir string) (*ImportMetadata, error) {
+	metadataFilePath := filepath.Join(dbDir, ImportMetadataFileName)
+	f, err := fs.OpenFile(metadataFilePath, os.O_TRUNC|os.O_WRONLY|os.O_CREATE, 0644)
+	if err != nil {
+		return nil, fmt.Errorf("failed to create import metadata file: %w", err)
+	}
+	defer f.Close()
+
+	checksums, err := CalculateDBDigest(fs, filepath.Join(dbDir, VulnerabilityDBFileName))
+	if err != nil {
+		return nil, fmt.Errorf("failed to calculate checksum for DB file: %w", err)
+	}
+
+	return writeImportMetadata(f, checksums)
+}
+
+func writeImportMetadata(writer io.Writer, checksums string) (*ImportMetadata, error) {
+	if checksums == "" {
+		return nil, fmt.Errorf("checksum is required")
+	}
+
+	if !strings.HasPrefix(checksums, "xxh64:") {
+		return nil, fmt.Errorf("checksum missing algorithm prefix")
+	}
+
+	enc := json.NewEncoder(writer)
+	enc.SetIndent("", " ")
+
+	doc := ImportMetadata{
+		Digest:        checksums,
+		ClientVersion: schemaver.New(ModelVersion, Revision, Addition).String(),
+	}
+
+	return &doc, enc.Encode(doc)
+}
diff --git a/grype/db/v6/installation/curator.go b/grype/db/v6/installation/curator.go
index a4e9a41d..32077553 100644
--- a/grype/db/v6/installation/curator.go
+++ b/grype/db/v6/installation/curator.go
@@ -22,6 +22,7 @@ import (
 	"github.com/anchore/grype/internal/bus"
 	"github.com/anchore/grype/internal/file"
 	"github.com/anchore/grype/internal/log"
+	"github.com/anchore/grype/internal/schemaver"
 )
 
 const lastUpdateCheckFileName = "last_update_check"
@@ -94,6 +95,50 @@ func (c curator) Reader() (db.Reader, error) {
 		return nil, fmt.Errorf("unable to get vulnerability store metadata: %w", err)
 	}
 
+	var currentDBSchemaVersion *schemaver.SchemaVer
+	if m != nil {
+		v := schemaver.New(m.Model, m.Revision, m.Addition)
+		currentDBSchemaVersion = &v
+	}
+
+	doRehydrate, err := isRehydrationNeeded(c.fs, c.config.DBDirectoryPath(), currentDBSchemaVersion, schemaver.New(db.ModelVersion, db.Revision, db.Addition))
+	if err != nil {
+		log.WithFields("error", err).Warn("unable to check if DB needs to be rehydrated")
+	} else if doRehydrate {
+		if err := s.Close(); err != nil {
+			// DB connection may be in an inconsistent state -- we cannot continue
+			return nil, fmt.Errorf("unable to close reader before rehydration: %w", err)
+		}
+		mon := newMonitor()
+
+		mon.Set("rehydrating DB")
+		log.Info("rehydrating DB")
+
+		// this is a condition where an old client imported a DB with additional capabilities than it can handle at hydration.
+		// this could lead to missing indexes and degraded performance now that a newer client is running (that can handle these capabilities).
+		// the only sensible thing to do is to rehydrate the existing DB to ensure indexes are up-to-date with the current client's capabilities.
+		if err := c.hydrate(c.config.DBDirectoryPath(), mon); err != nil {
+			log.WithFields("error", err).Warn("unable to rehydrate DB")
+		}
+		mon.Set("rehydrated")
+		mon.SetCompleted()
+
+		s, err = db.NewReader(
+			db.Config{
+				DBDirPath: c.config.DBDirectoryPath(),
+				Debug:     c.config.Debug,
+			},
+		)
+		if err != nil {
+			return nil, fmt.Errorf("unable to create new reader after rehydration: %w", err)
+		}
+
+		m, err = s.GetDBMetadata()
+		if err != nil {
+			return nil, fmt.Errorf("unable to get vulnerability store metadata after rehydration: %w", err)
+		}
+	}
+
 	_, err = c.validate(db.DescriptionFromMetadata(m), c.config.ValidateChecksum)
 
 	return s, err
@@ -243,6 +288,46 @@ func (c curator) update(current *db.Description) (*distribution.Archive, error)
 	return update, nil
 }
 
+func isRehydrationNeeded(fs afero.Fs, dirPath string, currentDBVersion *schemaver.SchemaVer, currentClientVersion schemaver.SchemaVer) (bool, error) {
+	if currentDBVersion == nil {
+		// there is no DB to rehydrate
+		return false, nil
+	}
+
+	importMetadata, err := db.ReadImportMetadata(fs, dirPath)
+	if err != nil {
+		return false, fmt.Errorf("unable to read import metadata: %w", err)
+	}
+	if importMetadata == nil {
+		return false, fmt.Errorf("missing import metadata")
+	}
+
+	clientHydrationVersion, err := schemaver.Parse(importMetadata.ClientVersion)
+	if err != nil {
+		return false, fmt.Errorf("unable to parse client version from import metadata: %w", err)
+	}
+
+	hydratedWithOldClient := clientHydrationVersion.LessThan(*currentDBVersion)
+	haveNewerClient := clientHydrationVersion.LessThan(currentClientVersion)
+	doRehydrate := hydratedWithOldClient && haveNewerClient
+
+	msg := "DB rehydration not needed"
+	if doRehydrate {
+		msg = "DB rehydration needed"
+	}
+
+	log.WithFields("clientHydrationVersion", clientHydrationVersion, "currentDBVersion", currentDBVersion, "currentClientVersion", currentClientVersion).Trace(msg)
+
+	if doRehydrate {
+		// this is a condition where an old client imported a DB with additional capabilities than it can handle at hydration.
+		// this could lead to missing indexes and degraded performance now that a newer client is running (that can handle these capabilities).
+		// the only sensible thing to do is to rehydrate the existing DB to ensure indexes are up-to-date with the current client's capabilities.
+		return true, nil
+	}
+
+	return false, nil
+}
+
 func (c curator) durationSinceUpdateCheck() (*time.Duration, error) {
 	// open `$dbDir/last_update_check` file and read the timestamp and do now() - timestamp
 
@@ -345,8 +430,16 @@ func (c curator) Import(dbArchivePath string) error {
 func (c curator) activate(dbDirPath string, mon monitor) error {
 	defer mon.SetCompleted()
 
-	dbFilePath := filepath.Join(dbDirPath, db.VulnerabilityDBFileName)
+	if err := c.hydrate(dbDirPath, mon); err != nil {
+		return fmt.Errorf("failed to hydrate database: %w", err)
+	}
+
+	mon.Set("activating")
+
+	return c.replaceDB(dbDirPath)
+}
 
+func (c curator) hydrate(dbDirPath string, mon monitor) error {
 	if c.hydrator != nil {
 		mon.Set("hydrating")
 		if err := c.hydrator(dbDirPath); err != nil {
@@ -357,28 +450,14 @@ func (c curator) activate(dbDirPath string, mon monitor) error {
 
 	mon.Set("hashing")
 
-	// overwrite the checksums file with the new checksums
-	digest, err := db.CalculateDBDigest(dbFilePath)
+	doc, err := db.WriteImportMetadata(c.fs, dbDirPath)
 	if err != nil {
-		return err
-	}
-
-	checksumsFilePath := filepath.Join(dbDirPath, db.ChecksumFileName)
-	fh, err := c.fs.OpenFile(checksumsFilePath, os.O_TRUNC|os.O_WRONLY|os.O_CREATE, 0644)
-	if err != nil {
-		return fmt.Errorf("failed to open checksums file: %w", err)
-	}
-	if err := db.WriteChecksums(fh, digest); err != nil {
 		return fmt.Errorf("failed to write checksums file: %w", err)
 	}
-	if err := fh.Close(); err != nil {
-		return err
-	}
-	log.WithFields("digest", digest).Trace("captured DB checksum")
 
-	mon.Set("activating")
+	log.WithFields("digest", doc.Digest).Trace("captured DB digest")
 
-	return c.replaceDB(dbDirPath)
+	return nil
 }
 
 // replaceDB swaps over to using the given path.
@@ -402,15 +481,14 @@ func (c curator) replaceDB(dbDirPath string) error {
 	return c.fs.Rename(dbDirPath, dbDir)
 }
 
-func (c curator) validateIntegrity(metadata *db.Description, dbFilePath string, validateChecksum bool) (*db.Description, string, error) {
+func (c curator) validateIntegrity(description *db.Description, dbFilePath string, validateChecksum bool) (*db.Description, string, error) {
 	// check that the disk checksum still matches the db payload
-	if metadata == nil {
+	if description == nil {
 		return nil, "", fmt.Errorf("database not found: %s", dbFilePath)
 	}
 
-	gotModel, ok := metadata.SchemaVersion.ModelPart()
-	if !ok || gotModel != db.ModelVersion {
-		return nil, "", fmt.Errorf("unsupported database version: have=%d want=%d", gotModel, db.ModelVersion)
+	if description.SchemaVersion.Model != db.ModelVersion {
+		return nil, "", fmt.Errorf("unsupported database version: have=%d want=%d", description.SchemaVersion.Model, db.ModelVersion)
 	}
 
 	if _, err := c.fs.Stat(dbFilePath); err != nil {
@@ -423,11 +501,16 @@ func (c curator) validateIntegrity(metadata *db.Description, dbFilePath string,
 	var digest string
 	if validateChecksum {
 		var err error
-		digest, err = db.ReadDBChecksum(filepath.Dir(dbFilePath))
+		importMetadata, err := db.ReadImportMetadata(c.fs, filepath.Dir(dbFilePath))
 		if err != nil {
 			return nil, "", err
 		}
 
+		if importMetadata == nil {
+			return nil, "", fmt.Errorf("missing import metadata")
+		}
+		digest = importMetadata.Digest
+
 		valid, actualHash, err := file.ValidateByHash(c.fs, dbFilePath, digest)
 		if err != nil {
 			return nil, "", err
@@ -437,7 +520,7 @@ func (c curator) validateIntegrity(metadata *db.Description, dbFilePath string,
 		}
 	}
 
-	return metadata, digest, nil
+	return description, digest, nil
 }
 
 // ensureNotStale ensures the vulnerability database has not passed
diff --git a/grype/db/v6/models.go b/grype/db/v6/models.go
index 3054163d..e0f5812b 100644
--- a/grype/db/v6/models.go
+++ b/grype/db/v6/models.go
@@ -11,6 +11,7 @@ import (
 	"gorm.io/gorm"
 
 	"github.com/anchore/grype/internal/log"
+	"github.com/anchore/grype/internal/schemaver"
 )
 
 func Models() []any {
@@ -38,6 +39,11 @@ func Models() []any {
 		// CPE related search tables
 		&AffectedCPEHandle{}, // join on CPE
 		&Cpe{},
+
+		// decorations to vulnerability records
+		&KnownExploitedVulnerabilityHandle{},
+		&EpssHandle{},
+		&EpssMetadata{},
 	}
 }
 
@@ -68,6 +74,10 @@ type DBMetadata struct {
 	Addition       int        `gorm:"column:addition;not null"`
 }
 
+func newSchemaVerFromDBMetadata(m DBMetadata) schemaver.SchemaVer {
+	return schemaver.New(m.Model, m.Revision, m.Addition)
+}
+
 // data source info //////////////////////////////////////////////////////
 
 // Provider is the upstream data processor (usually Vunnel) that is responsible for vulnerability records. Each provider
@@ -731,3 +741,50 @@ func (c *Cpe) AfterCreate(tx *gorm.DB) (err error) {
 	}
 	return nil
 }
+
+type KnownExploitedVulnerabilityHandle struct {
+	ID int64 `gorm:"primaryKey"`
+
+	Cve string `gorm:"column:cve;not null;index,collate:NOCASE"`
+
+	BlobID    ID                               `gorm:"column:blob_id"`
+	BlobValue *KnownExploitedVulnerabilityBlob `gorm:"-"`
+}
+
+func (v KnownExploitedVulnerabilityHandle) getBlobValue() any {
+	if v.BlobValue == nil {
+		return nil // must return untyped nil or getBlobValue() == nil will always be false
+	}
+	return v.BlobValue
+}
+
+func (v *KnownExploitedVulnerabilityHandle) setBlobID(id ID) {
+	v.BlobID = id
+}
+
+func (v KnownExploitedVulnerabilityHandle) getBlobID() ID {
+	return v.BlobID
+}
+
+func (v *KnownExploitedVulnerabilityHandle) setBlob(rawBlobValue []byte) error {
+	var blobValue KnownExploitedVulnerabilityBlob
+	if err := json.Unmarshal(rawBlobValue, &blobValue); err != nil {
+		return fmt.Errorf("unable to unmarshal KEV blob value: %w", err)
+	}
+
+	v.BlobValue = &blobValue
+	return nil
+}
+
+type EpssMetadata struct {
+	Date time.Time `gorm:"column:date;not null"`
+}
+
+type EpssHandle struct {
+	ID int64 `gorm:"primaryKey"`
+
+	Cve        string    `gorm:"column:cve;not null;index,collate:NOCASE"`
+	Epss       float64   `gorm:"column:epss;not null"`
+	Percentile float64   `gorm:"column:percentile;not null"`
+	Date       time.Time `gorm:"-"` // note we do not store the date in this table since it is expected to be the same for all records, that is what EpssMetadata is for
+}
diff --git a/grype/db/v6/provider_store.go b/grype/db/v6/provider_store.go
index 5df66a23..0cfcfbf1 100644
--- a/grype/db/v6/provider_store.go
+++ b/grype/db/v6/provider_store.go
@@ -15,6 +15,10 @@ type ProviderStoreReader interface {
 	fillProviders(handles []ref[string, Provider]) error
 }
 
+type ProviderStoreWriter interface {
+	AddProvider(p Provider) error
+}
+
 type providerStore struct {
 	db *gorm.DB
 }
@@ -25,6 +29,15 @@ func newProviderStore(db *gorm.DB) *providerStore {
 	}
 }
 
+func (s *providerStore) AddProvider(p Provider) error {
+	result := s.db.FirstOrCreate(&p)
+	if result.Error != nil {
+		return fmt.Errorf("failed to create provider record: %w", result.Error)
+	}
+
+	return nil
+}
+
 func (s *providerStore) GetProvider(name string) (*Provider, error) {
 	log.WithFields("name", name).Trace("fetching provider record")
 
diff --git a/grype/db/v6/store.go b/grype/db/v6/store.go
index 17404edc..ccde29d6 100644
--- a/grype/db/v6/store.go
+++ b/grype/db/v6/store.go
@@ -15,6 +15,7 @@ type store struct {
 	*vulnerabilityStore
 	*affectedPackageStore
 	*affectedCPEStore
+	*vulnerabilityDecoratorStore
 	blobStore *blobStore
 	db        *gorm.DB
 	config    Config
@@ -55,28 +56,52 @@ func newStore(cfg Config, empty, writable bool) (*store, error) {
 		return nil, fmt.Errorf("failed to open db: %w", err)
 	}
 
+	metadataStore := newDBMetadataStore(db)
+
+	if empty {
+		if err := metadataStore.SetDBMetadata(); err != nil {
+			return nil, fmt.Errorf("failed to set db metadata: %w", err)
+		}
+	}
+
+	meta, err := metadataStore.GetDBMetadata()
+	if err != nil {
+		return nil, fmt.Errorf("failed to get db metadata: %w", err)
+	}
+
+	if meta == nil {
+		return nil, fmt.Errorf("no DB metadata found")
+	}
+
+	dbVersion := newSchemaVerFromDBMetadata(*meta)
+
 	bs := newBlobStore(db)
 	return &store{
-		dbMetadataStore:      newDBMetadataStore(db),
-		providerStore:        newProviderStore(db),
-		vulnerabilityStore:   newVulnerabilityStore(db, bs),
-		affectedPackageStore: newAffectedPackageStore(db, bs),
-		affectedCPEStore:     newAffectedCPEStore(db, bs),
-		blobStore:            bs,
-		db:                   db,
-		config:               cfg,
-		empty:                empty,
-		writable:             writable,
+		dbMetadataStore:             metadataStore,
+		providerStore:               newProviderStore(db),
+		vulnerabilityStore:          newVulnerabilityStore(db, bs),
+		affectedPackageStore:        newAffectedPackageStore(db, bs),
+		affectedCPEStore:            newAffectedCPEStore(db, bs),
+		vulnerabilityDecoratorStore: newVulnerabilityDecoratorStore(db, bs, dbVersion),
+		blobStore:                   bs,
+		db:                          db,
+		config:                      cfg,
+		empty:                       empty,
+		writable:                    writable,
 	}, nil
 }
 
-// Close closes the store and finalizes the blobs when the DB is open for writing. If open for reading, it does nothing.
+// Close closes the store and finalizes the blobs when the DB is open for writing. If open for reading, only closes the connection to the DB.
 func (s *store) Close() error {
-	log.Debug("closing store")
 	if !s.writable || !s.empty {
+		d, err := s.db.DB()
+		if err == nil {
+			return d.Close()
+		}
 		// if not empty, this writable execution created indexes
 		return nil
 	}
+	log.Debug("closing store")
 
 	// drop all indexes, which saves a lot of space distribution-wise (these get re-created on running gorm auto-migrate)
 	if err := dropAllIndexes(s.db); err != nil {
@@ -95,7 +120,12 @@ func (s *store) Close() error {
 		return fmt.Errorf("integrity check failed: %w", err)
 	}
 
-	return nil
+	d, err := s.db.DB()
+	if err != nil {
+		return err
+	}
+
+	return d.Close()
 }
 
 func dropAllIndexes(db *gorm.DB) error {
diff --git a/grype/db/v6/vulnerability_decorator_store.go b/grype/db/v6/vulnerability_decorator_store.go
new file mode 100644
index 00000000..8ee0ea98
--- /dev/null
+++ b/grype/db/v6/vulnerability_decorator_store.go
@@ -0,0 +1,209 @@
+package v6
+
+import (
+	"fmt"
+	"time"
+
+	"gorm.io/gorm"
+
+	"github.com/anchore/go-logger"
+	"github.com/anchore/grype/internal/log"
+	"github.com/anchore/grype/internal/schemaver"
+)
+
+type VulnerabilityDecoratorStoreWriter interface {
+	AddKnownExploitedVulnerabilities(...*KnownExploitedVulnerabilityHandle) error
+	AddEpss(...*EpssHandle) error
+}
+
+type VulnerabilityDecoratorStoreReader interface {
+	GetKnownExploitedVulnerabilities(cve string) ([]KnownExploitedVulnerabilityHandle, error)
+	GetEpss(cve string) ([]EpssHandle, error)
+}
+
+type vulnerabilityDecoratorStore struct {
+	db          *gorm.DB
+	blobStore   *blobStore
+	kevEnabled  bool
+	epssEnabled bool
+	epssDate    *time.Time
+}
+
+func newVulnerabilityDecoratorStore(db *gorm.DB, bs *blobStore, dbVersion schemaver.SchemaVer) *vulnerabilityDecoratorStore {
+	minSupportedKEVClientVersion := schemaver.New(6, 0, 1)
+	minSupportedEPSSClientVersion := schemaver.New(6, 0, 2)
+	return &vulnerabilityDecoratorStore{
+		db:          db,
+		blobStore:   bs,
+		kevEnabled:  dbVersion.GreaterOrEqualTo(minSupportedKEVClientVersion),
+		epssEnabled: dbVersion.GreaterOrEqualTo(minSupportedEPSSClientVersion),
+	}
+}
+
+func (s *vulnerabilityDecoratorStore) AddEpss(epss ...*EpssHandle) error {
+	if !s.epssEnabled {
+		// when populating a new DB any capability issues found should result in halting
+		return ErrDBCapabilityNotSupported
+	}
+
+	for i := range epss {
+		e := epss[i]
+
+		if err := s.db.Create(e).Error; err != nil {
+			return fmt.Errorf("unable to create EPSS: %w", err)
+		}
+
+		if err := s.setEPSSMetadata(e.Date); err != nil {
+			return fmt.Errorf("unable to set EPSS metadata: %w", err)
+		}
+	}
+	return nil
+}
+
+func (s *vulnerabilityDecoratorStore) setEPSSMetadata(date time.Time) error {
+	if !s.epssEnabled {
+		// when populating a new DB any capability issues found should result in halting
+		return ErrDBCapabilityNotSupported
+	}
+
+	if s.epssDate != nil {
+		if s.epssDate.Equal(date) {
+			return nil
+		}
+		return fmt.Errorf("observed multiple EPSS dates: current=%q new=%q", s.epssDate.String(), date.String())
+	}
+
+	log.Trace("writing EPSS metadata")
+
+	if err := s.db.Where("true").Delete(&EpssMetadata{}).Error; err != nil {
+		return fmt.Errorf("failed to delete existing EPSS metadata record: %w", err)
+	}
+
+	instance := &EpssMetadata{
+		Date: date,
+	}
+
+	if err := s.db.Create(instance).Error; err != nil {
+		return fmt.Errorf("failed to create EPSS metadata record: %w", err)
+	}
+
+	s.epssDate = &date
+	return nil
+}
+
+func (s *vulnerabilityDecoratorStore) getEPSSMetadata() (*EpssMetadata, error) {
+	log.Trace("fetching EPSS metadata")
+
+	var model EpssMetadata
+
+	result := s.db.First(&model)
+	return &model, result.Error
+}
+
+func (s *vulnerabilityDecoratorStore) GetEpss(cve string) ([]EpssHandle, error) {
+	if !s.epssEnabled {
+		// capability incompatibilities should gracefully degrade, returning no data or errors
+		return nil, nil
+	}
+
+	fields := logger.Fields{
+		"cve": cve,
+	}
+	start := time.Now()
+	var count int
+	defer func() {
+		fields["duration"] = time.Since(start)
+		fields["records"] = count
+		log.WithFields(fields).Trace("fetched EPSS records")
+	}()
+
+	var models []EpssHandle
+	var results []*EpssHandle
+
+	if s.epssDate == nil {
+		// fetch and cache the EPSS metadata
+		metadata, err := s.getEPSSMetadata()
+		if err != nil {
+			return nil, fmt.Errorf("unable to fetch EPSS metadata: %w", err)
+		}
+		s.epssDate = &metadata.Date
+	}
+
+	if err := s.db.Where("cve = ?", cve).FindInBatches(&results, batchSize, func(_ *gorm.DB, _ int) error {
+		for _, r := range results {
+			r.Date = *s.epssDate
+			models = append(models, *r)
+		}
+
+		count += len(results)
+
+		return nil
+	}).Error; err != nil {
+		return models, fmt.Errorf("unable to fetch EPSS records: %w", err)
+	}
+
+	return models, nil
+}
+
+func (s *vulnerabilityDecoratorStore) AddKnownExploitedVulnerabilities(kevs ...*KnownExploitedVulnerabilityHandle) error {
+	if !s.kevEnabled {
+		// when populating a new DB any capability issues found should result in halting
+		return ErrDBCapabilityNotSupported
+	}
+
+	for i := range kevs {
+		k := kevs[i]
+		// this adds the blob value to the DB and sets the ID on the kev handle
+		if err := s.blobStore.addBlobable(k); err != nil {
+			return fmt.Errorf("unable to add KEV blob: %w", err)
+		}
+
+		if err := s.db.Create(k).Error; err != nil {
+			return fmt.Errorf("unable to create known exploited vulnerability: %w", err)
+		}
+	}
+	return nil
+}
+
+func (s *vulnerabilityDecoratorStore) GetKnownExploitedVulnerabilities(cve string) ([]KnownExploitedVulnerabilityHandle, error) {
+	if !s.kevEnabled {
+		// capability incompatibilities should gracefully degrade, returning no data or errors
+		return nil, nil
+	}
+
+	fields := logger.Fields{
+		"cve": cve,
+	}
+	start := time.Now()
+	var count int
+	defer func() {
+		fields["duration"] = time.Since(start)
+		fields["records"] = count
+		log.WithFields(fields).Trace("fetched KEV records")
+	}()
+
+	var models []KnownExploitedVulnerabilityHandle
+	var results []*KnownExploitedVulnerabilityHandle
+
+	if err := s.db.Where("cve = ?", cve).FindInBatches(&results, batchSize, func(_ *gorm.DB, _ int) error {
+		var blobs []blobable
+		for _, r := range results {
+			blobs = append(blobs, r)
+		}
+		if err := s.blobStore.attachBlobValue(blobs...); err != nil {
+			return fmt.Errorf("unable to attach KEV blobs: %w", err)
+		}
+
+		for _, r := range results {
+			models = append(models, *r)
+		}
+
+		count += len(results)
+
+		return nil
+	}).Error; err != nil {
+		return models, fmt.Errorf("unable to fetch KEV records: %w", err)
+	}
+
+	return models, nil
+}
diff --git a/grype/db/v6/vulnerability_provider.go b/grype/db/v6/vulnerability_provider.go
index 4aad4a67..513aaaef 100644
--- a/grype/db/v6/vulnerability_provider.go
+++ b/grype/db/v6/vulnerability_provider.go
@@ -6,7 +6,9 @@ import (
 	"io"
 	"strings"
 
+	"github.com/hashicorp/go-multierror"
 	"github.com/iancoleman/strcase"
+	"github.com/scylladb/go-set/strset"
 
 	"github.com/anchore/go-logger"
 	"github.com/anchore/grype/grype/db/v6/name"
@@ -64,14 +66,28 @@ func (s vulnerabilityProvider) VulnerabilityMetadata(ref vulnerability.Reference
 		}, nil
 	}
 
+	cves := getCVEs(vuln)
+
+	kevs, err := s.fetchKnownExploited(cves)
+	if err != nil {
+		log.WithFields("id", ref.ID, "vulnerability", vuln.String(), "error", err).Debug("unable to fetch known exploited from vulnerability")
+	}
+
+	epss, err := s.fetchEpss(cves)
+	if err != nil {
+		log.WithFields("id", ref.ID, "vulnerability", vuln.String(), "error", err).Debug("unable to fetch epss from vulnerability")
+	}
+
 	return &vulnerability.Metadata{
-		ID:          vuln.Name,
-		DataSource:  vuln.Provider.ID,
-		Namespace:   ref.Namespace,
-		Severity:    toSeverityString(sev),
-		URLs:        toURLs(vuln),
-		Description: vuln.BlobValue.Description,
-		Cvss:        cvss,
+		ID:             vuln.Name,
+		DataSource:     vuln.Provider.ID,
+		Namespace:      ref.Namespace,
+		Severity:       toSeverityString(sev),
+		URLs:           toURLs(vuln),
+		Description:    vuln.BlobValue.Description,
+		Cvss:           cvss,
+		KnownExploited: kevs,
+		EPSS:           epss,
 	}, nil
 }
 
@@ -87,6 +103,54 @@ func (s vulnerabilityProvider) fetchVulnerability(ref vulnerability.Reference) (
 	return nil, nil
 }
 
+func (s vulnerabilityProvider) fetchKnownExploited(cves []string) ([]vulnerability.KnownExploited, error) {
+	var out []vulnerability.KnownExploited
+	var errs error
+	for _, cve := range cves {
+		kevs, err := s.reader.GetKnownExploitedVulnerabilities(cve)
+		if err != nil {
+			errs = multierror.Append(errs, err)
+			continue
+		}
+		for _, kev := range kevs {
+			out = append(out, vulnerability.KnownExploited{
+				CVE:                        kev.Cve,
+				VendorProject:              kev.BlobValue.VendorProject,
+				Product:                    kev.BlobValue.Product,
+				DateAdded:                  kev.BlobValue.DateAdded,
+				RequiredAction:             kev.BlobValue.RequiredAction,
+				DueDate:                    kev.BlobValue.DueDate,
+				KnownRansomwareCampaignUse: kev.BlobValue.KnownRansomwareCampaignUse,
+				Notes:                      kev.BlobValue.Notes,
+				URLs:                       kev.BlobValue.URLs,
+				CWEs:                       kev.BlobValue.CWEs,
+			})
+		}
+	}
+	return out, errs
+}
+
+func (s vulnerabilityProvider) fetchEpss(cves []string) ([]vulnerability.EPSS, error) {
+	var out []vulnerability.EPSS
+	var errs error
+	for _, cve := range cves {
+		entries, err := s.reader.GetEpss(cve)
+		if err != nil {
+			errs = multierror.Append(errs, err)
+			continue
+		}
+		for _, entry := range entries {
+			out = append(out, vulnerability.EPSS{
+				CVE:        entry.Cve,
+				EPSS:       entry.Epss,
+				Percentile: entry.Percentile,
+				Date:       entry.Date,
+			})
+		}
+	}
+	return out, errs
+}
+
 func (s vulnerabilityProvider) PackageSearchNames(p pkg.Package) []string {
 	return name.PackageNames(p)
 }
@@ -412,3 +476,36 @@ func toURLs(vuln *VulnerabilityHandle) []string {
 	}
 	return out
 }
+
+func getCVEs(vuln *VulnerabilityHandle) []string {
+	var cves []string
+	set := strset.New()
+
+	addCVE := func(id string) {
+		lower := strings.ToLower(id)
+		if strings.HasPrefix(lower, "cve-") {
+			if !set.Has(lower) {
+				cves = append(cves, id)
+				set.Add(lower)
+			}
+		}
+	}
+
+	if vuln == nil {
+		return cves
+	}
+
+	addCVE(vuln.Name)
+
+	if vuln.BlobValue == nil {
+		return cves
+	}
+
+	addCVE(vuln.BlobValue.ID)
+
+	for _, alias := range vuln.BlobValue.Aliases {
+		addCVE(alias)
+	}
+
+	return cves
+}
diff --git a/grype/db/v6/vulnerability_store.go b/grype/db/v6/vulnerability_store.go
index 76d180c3..3cab8bfb 100644
--- a/grype/db/v6/vulnerability_store.go
+++ b/grype/db/v6/vulnerability_store.go
@@ -306,7 +306,7 @@ func (s *vulnerabilityStore) GetVulnerabilities(vuln *VulnerabilitySpecifier, co
 				blobs = append(blobs, r)
 			}
 			if err := s.blobStore.attachBlobValue(blobs...); err != nil {
-				return fmt.Errorf("unable to attach blobs: %w", err)
+				return fmt.Errorf("unable to attach vulnerability blobs: %w", err)
 			}
 		}
 
diff --git a/grype/internal/packagemetadata/discover_type_names.go b/grype/internal/packagemetadata/discover_type_names.go
index a1a3fbcb..617e18d8 100644
--- a/grype/internal/packagemetadata/discover_type_names.go
+++ b/grype/internal/packagemetadata/discover_type_names.go
@@ -18,6 +18,7 @@ var metadataExceptions = strset.New(
 	"FileMetadata",
 	"PURLFileMetadata",
 	"PURLLiteralMetadata",
+	"CPELiteralMetadata",
 )
 
 func DiscoverTypeNames() ([]string, error) {
diff --git a/grype/match/explicit_ignores.go b/grype/match/explicit_ignores.go
index bdbd1d1e..e1d7d024 100644
--- a/grype/match/explicit_ignores.go
+++ b/grype/match/explicit_ignores.go
@@ -51,6 +51,12 @@ func init() {
 			vulnerabilities: []string{"CVE-2017-14727"},
 			packages:        []string{"logger"},
 		},
+		// https://github.com/anchore/grype/issues/2412#issuecomment-2663656195
+		{
+			typ:             "deb",
+			vulnerabilities: []string{"CVE-2023-45853"},
+			packages:        []string{"zlib1g", "zlib"},
+		},
 	}
 
 	for _, ignore := range explicitIgnores {
@@ -70,22 +76,20 @@ func init() {
 
 // ApplyExplicitIgnoreRules Filters out matches meeting the criteria defined above and those within the grype database
 func ApplyExplicitIgnoreRules(provider ExclusionProvider, matches Matches) (Matches, []IgnoredMatch) {
-	if provider == nil {
-		return matches, nil
-	}
-
 	var ignoreRules []IgnoreRule
 	ignoreRules = append(ignoreRules, explicitIgnoreRules...)
 
-	for _, m := range matches.Sorted() {
-		r, err := provider.IgnoreRules(m.Vulnerability.ID)
+	if provider != nil {
+		for _, m := range matches.Sorted() {
+			r, err := provider.IgnoreRules(m.Vulnerability.ID)
 
-		if err != nil {
-			log.Warnf("unable to get ignore rules for vuln id=%s", m.Vulnerability.ID)
-			continue
-		}
+			if err != nil {
+				log.Warnf("unable to get ignore rules for vuln id=%s", m.Vulnerability.ID)
+				continue
+			}
 
-		ignoreRules = append(ignoreRules, r...)
+			ignoreRules = append(ignoreRules, r...)
+		}
 	}
 
 	return ApplyIgnoreRules(matches, ignoreRules)
diff --git a/grype/matcher/java/matcher.go b/grype/matcher/java/matcher.go
index aec6cdc5..23b5a1a5 100644
--- a/grype/matcher/java/matcher.go
+++ b/grype/matcher/java/matcher.go
@@ -59,7 +59,8 @@ func (m *Matcher) Match(store vulnerability.Provider, p pkg.Package) ([]match.Ma
 			if strings.Contains(err.Error(), "no artifact found") {
 				log.Debugf("no upstream maven artifact found for %s", p.Name)
 			} else {
-				log.WithFields("package", p.Name, "error", err).Warn("failed to resolve package details with maven")
+				log.WithFields("package", p.Name, "error", err).Error("failed to resolve package details with maven")
+				return nil, nil, fmt.Errorf("resolving package details with maven: %w", err)
 			}
 		} else {
 			matches = append(matches, upstreamMatches...)
diff --git a/grype/pkg/cpe_provider.go b/grype/pkg/cpe_provider.go
new file mode 100644
index 00000000..b9226ab9
--- /dev/null
+++ b/grype/pkg/cpe_provider.go
@@ -0,0 +1,100 @@
+package pkg
+
+import (
+	"bufio"
+	"fmt"
+	"io"
+	"strings"
+
+	"github.com/anchore/syft/syft/cpe"
+	"github.com/anchore/syft/syft/pkg"
+	"github.com/anchore/syft/syft/sbom"
+	"github.com/anchore/syft/syft/source"
+)
+
+const cpeInputPrefix = "cpe:"
+
+type CPELiteralMetadata struct {
+	CPE string
+}
+
+func cpeProvider(userInput string) ([]Package, Context, *sbom.SBOM, error) {
+	reader, ctx, err := getCPEReader(userInput)
+	if err != nil {
+		return nil, Context{}, nil, err
+	}
+
+	return decodeCPEsFromReader(reader, ctx)
+}
+
+func getCPEReader(userInput string) (r io.Reader, ctx Context, err error) {
+	if strings.HasPrefix(userInput, cpeInputPrefix) {
+		ctx.Source = &source.Description{
+			Metadata: CPELiteralMetadata{
+				CPE: userInput,
+			},
+		}
+		return strings.NewReader(userInput), ctx, nil
+	}
+	return nil, ctx, errDoesNotProvide
+}
+
+func decodeCPEsFromReader(reader io.Reader, ctx Context) ([]Package, Context, *sbom.SBOM, error) {
+	scanner := bufio.NewScanner(reader)
+	var packages []Package
+	var syftPkgs []pkg.Package
+
+	for scanner.Scan() {
+		rawLine := scanner.Text()
+		p, syftPkg, err := cpeToPackage(rawLine)
+		if err != nil {
+			return nil, Context{}, nil, err
+		}
+
+		if p != nil {
+			packages = append(packages, *p)
+		}
+		if syftPkg != nil {
+			syftPkgs = append(syftPkgs, *syftPkg)
+		}
+	}
+
+	if err := scanner.Err(); err != nil {
+		return nil, Context{}, nil, err
+	}
+
+	s := &sbom.SBOM{
+		Artifacts: sbom.Artifacts{
+			Packages: pkg.NewCollection(syftPkgs...),
+		},
+	}
+
+	return packages, ctx, s, nil
+}
+
+func cpeToPackage(rawLine string) (*Package, *pkg.Package, error) {
+	c, err := cpe.New(rawLine, "")
+	if err != nil {
+		return nil, nil, fmt.Errorf("unable to decode cpe %q: %w", rawLine, err)
+	}
+
+	syftPkg := pkg.Package{
+		Name:    c.Attributes.Product,
+		Version: c.Attributes.Version,
+		CPEs:    []cpe.CPE{c},
+		// TODO infer from cpe target sw, this is not as important since the only matcher for CPEs is the stock matcher
+		//Type:     pkgType,
+		// Language:...
+	}
+
+	syftPkg.SetID()
+
+	return &Package{
+		ID:       ID(c.Attributes.BindToFmtString()),
+		CPEs:     syftPkg.CPEs,
+		Name:     syftPkg.Name,
+		Version:  syftPkg.Version,
+		Type:     syftPkg.Type,
+		Language: syftPkg.Language,
+	}, &syftPkg, nil
+}
diff --git a/grype/pkg/provider.go b/grype/pkg/provider.go
index 9258edd2..985e3a2a 100644
--- a/grype/pkg/provider.go
+++ b/grype/pkg/provider.go
@@ -6,6 +6,7 @@ import (
 
 	"github.com/bmatcuk/doublestar/v2"
 
+	"github.com/anchore/grype/internal/log"
 	"github.com/anchore/syft/syft/file"
 	"github.com/anchore/syft/syft/sbom"
 )
@@ -23,14 +24,23 @@ func Provide(userInput string, config ProviderConfig) ([]Package, Context, *sbom
 				return nil, ctx, s, exclusionsErr
 			}
 		}
+		log.WithFields("input", userInput).Trace("interpreting input from the given SBOM")
 		return packages, ctx, s, err
 	}
 
 	packages, ctx, s, err = purlProvider(userInput)
 	if !errors.Is(err, errDoesNotProvide) {
+		log.WithFields("input", userInput).Trace("interpreting input from the given PURL(s)")
 		return packages, ctx, s, err
 	}
 
+	packages, ctx, s, err = cpeProvider(userInput)
+	if !errors.Is(err, errDoesNotProvide) {
+		log.WithFields("input", userInput).Trace("interpreting input from the given CPE")
+		return packages, ctx, s, err
+	}
+
+	log.WithFields("input", userInput).Trace("passing input to syft for interpretation")
 	return syftProvider(userInput, config)
 }
 
diff --git a/grype/pkg/purl_provider.go b/grype/pkg/purl_provider.go
index c885de2e..692a3b10 100644
--- a/grype/pkg/purl_provider.go
+++ b/grype/pkg/purl_provider.go
@@ -38,10 +38,49 @@ func purlProvider(userInput string) ([]Package, Context, *sbom.SBOM, error) {
 		return nil, Context{}, nil, err
 	}
 
-	return decodePurlFile(reader, ctx)
+	return decodePurlsFromReader(reader, ctx)
 }
 
-func decodePurlFile(reader io.Reader, ctx Context) ([]Package, Context, *sbom.SBOM, error) {
+func getPurlReader(userInput string) (r io.Reader, ctx Context, err error) {
+	switch {
+	case strings.HasPrefix(userInput, purlInputPrefix):
+		path := strings.TrimPrefix(userInput, purlInputPrefix)
+		ctx.Source = &source.Description{
+			Metadata: PURLFileMetadata{
+				Path: path,
+			},
+		}
+		file, err := openPurlFile(path)
+		if err != nil {
+			return nil, ctx, err
+		}
+		return file, ctx, nil
+	case strings.HasPrefix(userInput, singlePurlInputPrefix):
+		ctx.Source = &source.Description{
+			Metadata: PURLLiteralMetadata{
+				PURL: userInput,
+			},
+		}
+		return strings.NewReader(userInput), ctx, nil
+	}
+	return nil, ctx, errDoesNotProvide
+}
+
+func openPurlFile(path string) (*os.File, error) {
+	expandedPath, err := homedir.Expand(path)
+	if err != nil {
+		return nil, fmt.Errorf("unable to open purls: %w", err)
+	}
+
+	f, err := os.Open(expandedPath)
+	if err != nil {
+		return nil, fmt.Errorf("unable to open file %s: %w", expandedPath, err)
+	}
+
+	return f, nil
+}
+
+func decodePurlsFromReader(reader io.Reader, ctx Context) ([]Package, Context, *sbom.SBOM, error) {
 	scanner := bufio.NewScanner(reader)
 	var packages []Package
 	var syftPkgs []pkg.Package
@@ -219,42 +258,3 @@ func handleDefaultUpstream(pkgName string, value string) []UpstreamPackage {
 	}
 	return nil
 }
-
-func getPurlReader(userInput string) (r io.Reader, ctx Context, err error) {
-	switch {
-	case strings.HasPrefix(userInput, purlInputPrefix):
-		path := strings.TrimPrefix(userInput, purlInputPrefix)
-		ctx.Source = &source.Description{
-			Metadata: PURLFileMetadata{
-				Path: path,
-			},
-		}
-		file, err := openPurlFile(path)
-		if err != nil {
-			return nil, ctx, err
-		}
-		return file, ctx, nil
-	case strings.HasPrefix(userInput, singlePurlInputPrefix):
-		ctx.Source = &source.Description{
-			Metadata: PURLLiteralMetadata{
-				PURL: userInput,
-			},
-		}
-		return strings.NewReader(userInput), ctx, nil
-	}
-	return nil, ctx, errDoesNotProvide
-}
-
-func openPurlFile(path string) (*os.File, error) {
-	expandedPath, err := homedir.Expand(path)
-	if err != nil {
-		return nil, fmt.Errorf("unable to open purls: %w", err)
-	}
-
-	f, err := os.Open(expandedPath)
-	if err != nil {
-		return nil, fmt.Errorf("unable to open file %s: %w", expandedPath, err)
-	}
-
-	return f, nil
-}
diff --git a/grype/presenter/cyclonedx/presenter.go b/grype/presenter/cyclonedx/presenter.go
index 2ae63229..b090e995 100644
--- a/grype/presenter/cyclonedx/presenter.go
+++ b/grype/presenter/cyclonedx/presenter.go
@@ -83,5 +83,5 @@ func (pres *Presenter) Present(output io.Writer) error {
 	enc.SetPretty(true)
 	enc.SetEscapeHTML(false)
 
-	return enc.Encode(cyclonedxBOM)
+	return enc.EncodeVersion(cyclonedxBOM, cyclonedxBOM.SpecVersion)
 }
diff --git a/grype/presenter/models/source.go b/grype/presenter/models/source.go
index f97871e0..6bbb1fe6 100644
--- a/grype/presenter/models/source.go
+++ b/grype/presenter/models/source.go
@@ -3,6 +3,7 @@ package models
 import (
 	"fmt"
 
+	"github.com/anchore/grype/grype/pkg"
 	syftSource "github.com/anchore/syft/syft/source"
 )
 
@@ -14,6 +15,21 @@ type source struct {
 // newSource creates a new source object to be represented into JSON.
 func newSource(src syftSource.Description) (source, error) {
 	switch m := src.Metadata.(type) {
+	case pkg.PURLFileMetadata:
+		return source{
+			Type:   "purl-file",
+			Target: m.Path,
+		}, nil
+	case pkg.PURLLiteralMetadata:
+		return source{
+			Type:   "purl",
+			Target: m.PURL,
+		}, nil
+	case pkg.CPELiteralMetadata:
+		return source{
+			Type:   "cpe",
+			Target: m.CPE,
+		}, nil
 	case syftSource.ImageMetadata:
 		// ensure that empty collections are not shown as null
 		if m.RepoDigests == nil {
diff --git a/grype/presenter/models/vulnerability_metadata.go b/grype/presenter/models/vulnerability_metadata.go
index ced69aea..1e4583fa 100644
--- a/grype/presenter/models/vulnerability_metadata.go
+++ b/grype/presenter/models/vulnerability_metadata.go
@@ -1,15 +1,41 @@
 package models
 
-import "github.com/anchore/grype/grype/vulnerability"
+import (
+	"time"
+
+	"github.com/anchore/grype/grype/vulnerability"
+)
 
 type VulnerabilityMetadata struct {
-	ID          string   `json:"id"`
-	DataSource  string   `json:"dataSource"`
-	Namespace   string   `json:"namespace,omitempty"`
-	Severity    string   `json:"severity,omitempty"`
-	URLs        []string `json:"urls"`
-	Description string   `json:"description,omitempty"`
-	Cvss        []Cvss   `json:"cvss"`
+	ID             string           `json:"id"`
+	DataSource     string           `json:"dataSource"`
+	Namespace      string           `json:"namespace,omitempty"`
+	Severity       string           `json:"severity,omitempty"`
+	URLs           []string         `json:"urls"`
+	Description    string           `json:"description,omitempty"`
+	Cvss           []Cvss           `json:"cvss"`
+	KnownExploited []KnownExploited `json:"knownExploited,omitempty"`
+	EPSS           []EPSS           `json:"epss,omitempty"`
+}
+
+type KnownExploited struct {
+	CVE                        string   `json:"cve"`
+	VendorProject              string   `json:"vendorProject,omitempty"`
+	Product                    string   `json:"product,omitempty"`
+	DateAdded                  string   `json:"dateAdded,omitempty"`
+	RequiredAction             string   `json:"requiredAction,omitempty"`
+	DueDate                    string   `json:"dueDate,omitempty"`
+	KnownRansomwareCampaignUse string   `json:"knownRansomwareCampaignUse"`
+	Notes                      string   `json:"notes,omitempty"`
+	URLs                       []string `json:"urls,omitempty"`
+	CWEs                       []string `json:"cwes,omitempty"`
+}
+
+type EPSS struct {
+	CVE        string  `json:"cve"`
+	EPSS       float64 `json:"epss"`
+	Percentile float64 `json:"percentile"`
+	Date       string  `json:"date"`
 }
 
 func NewVulnerabilityMetadata(id, namespace string, metadata *vulnerability.Metadata) VulnerabilityMetadata {
@@ -26,14 +52,48 @@ func NewVulnerabilityMetadata(id, namespace string, metadata *vulnerability.Meta
 	}
 
 	return VulnerabilityMetadata{
-		ID:          id,
-		DataSource:  metadata.DataSource,
-		Namespace:   metadata.Namespace,
-		Severity:    metadata.Severity,
-		URLs:        urls,
-		Description: metadata.Description,
-		Cvss:        NewCVSS(metadata),
+		ID:             id,
+		DataSource:     metadata.DataSource,
+		Namespace:      metadata.Namespace,
+		Severity:       metadata.Severity,
+		URLs:           urls,
+		Description:    metadata.Description,
+		Cvss:           NewCVSS(metadata),
+		KnownExploited: toKnownExploited(metadata.KnownExploited),
+		EPSS:           toEPSS(metadata.EPSS),
+	}
+}
+
+func toKnownExploited(knownExploited []vulnerability.KnownExploited) []KnownExploited {
+	result := make([]KnownExploited, len(knownExploited))
+	for idx, ke := range knownExploited {
+		result[idx] = KnownExploited{
+			CVE:                        ke.CVE,
+			VendorProject:              ke.VendorProject,
+			Product:                    ke.Product,
+			DateAdded:                  ke.DateAdded.Format(time.DateOnly),
+			RequiredAction:             ke.RequiredAction,
+			DueDate:                    ke.DueDate.Format(time.DateOnly),
+			KnownRansomwareCampaignUse: ke.KnownRansomwareCampaignUse,
+			Notes:                      ke.Notes,
+			URLs:                       ke.URLs,
+			CWEs:                       ke.CWEs,
+		}
+	}
+	return result
+}
+
+func toEPSS(epss []vulnerability.EPSS) []EPSS {
+	result := make([]EPSS, len(epss))
+	for idx, e := range epss {
+		result[idx] = EPSS{
+			CVE:        e.CVE,
+			EPSS:       e.EPSS,
+			Percentile: e.Percentile,
+			Date:       e.Date.Format(time.DateOnly),
+		}
 	}
+	return result
 }
 
 // returns severtiy score for presenter sorting purposes
diff --git a/grype/version/golang_version.go b/grype/version/golang_version.go
index a52256ee..6cea6dd3 100644
--- a/grype/version/golang_version.go
+++ b/grype/version/golang_version.go
@@ -48,9 +48,21 @@ func newGolangVersion(v string) (*golangVersion, error) {
 	if v == "(devel)" {
 		return nil, ErrUnsupportedVersion
 	}
+
+	// Invalid Semver fix ups
+
 	// go stdlib is reported by syft as a go package with version like "go1.24.1"
 	// other versions have "v" as a prefix, which the semver lib handles automatically
-	semver, err := hashiVer.NewSemver(strings.TrimPrefix(v, "go"))
+	fixedUp := strings.TrimPrefix(v, "go")
+
+	// go1.24 creates non-dot separated build metadata fields, e.g. +incompatible+dirty
+	// Fix up as per semver spec
+	before, after, found := strings.Cut(fixedUp, "+")
+	if found {
+		fixedUp = before + "+" + strings.ReplaceAll(after, "+", ".")
+	}
+
+	semver, err := hashiVer.NewSemver(fixedUp)
 	if err != nil {
 		return nil, err
 	}
diff --git a/grype/vulnerability/metadata.go b/grype/vulnerability/metadata.go
index a3219f66..92d0d8e0 100644
--- a/grype/vulnerability/metadata.go
+++ b/grype/vulnerability/metadata.go
@@ -1,13 +1,17 @@
 package vulnerability
 
+import "time"
+
 type Metadata struct {
-	ID          string
-	DataSource  string
-	Namespace   string
-	Severity    string
-	URLs        []string
-	Description string
-	Cvss        []Cvss
+	ID             string
+	DataSource     string
+	Namespace      string
+	Severity       string
+	URLs           []string
+	Description    string
+	Cvss           []Cvss
+	KnownExploited []KnownExploited
+	EPSS           []EPSS
 }
 
 type Cvss struct {
@@ -32,3 +36,23 @@ func NewCvssMetrics(baseScore, exploitabilityScore, impactScore float64) CvssMet
 		ImpactScore:         &impactScore,
 	}
 }
+
+type KnownExploited struct {
+	CVE                        string
+	VendorProject              string
+	Product                    string
+	DateAdded                  *time.Time
+	RequiredAction             string
+	DueDate                    *time.Time
+	KnownRansomwareCampaignUse string
+	Notes                      string
+	URLs                       []string
+	CWEs                       []string
+}
+
+type EPSS struct {
+	CVE        string
+	EPSS       float64
+	Percentile float64
+	Date       time.Time
+}
diff --git a/internal/schemaver/schema_ver.go b/internal/schemaver/schema_ver.go
index f850e9c7..83423f3e 100644
--- a/internal/schemaver/schema_ver.go
+++ b/internal/schemaver/schema_ver.go
@@ -1,45 +1,88 @@
 package schemaver
 
 import (
+	"encoding/json"
 	"fmt"
 	"strconv"
 	"strings"
 )
 
-type SchemaVer string
-
-func New(model, revision, addition int) SchemaVer {
-	return SchemaVer(fmt.Sprintf("%d.%d.%d", model, revision, addition))
+type SchemaVer struct {
+	Model    int // breaking changes
+	Revision int // potentially-breaking changes
+	Addition int // additions only
 }
 
-func (s SchemaVer) String() string {
-	return string(s)
+func New(model, revision, addition int) SchemaVer {
+	return SchemaVer{
+		Model:    model,
+		Revision: revision,
+		Addition: addition,
+	}
 }
 
-func (s SchemaVer) ModelPart() (int, bool) {
-	v, ok := parseVersionPart(s, 0)
-	if v == 0 {
-		ok = false
+func Parse(s string) (SchemaVer, error) {
+	// must provide model.revision.addition
+	parts := strings.Split(strings.TrimSpace(s), ".")
+	if len(parts) != 3 {
+		return SchemaVer{}, fmt.Errorf("invalid schema version format: %s", s)
 	}
-	return v, ok
+	// check that all parts are integers
+	var values [3]int
+	for i, part := range parts {
+		if i == 0 {
+			part = strings.TrimPrefix(part, "v")
+		}
+		v, err := strconv.Atoi(part)
+		if err != nil || v < 0 {
+			return SchemaVer{}, fmt.Errorf("invalid schema version format: %s", s)
+		}
+		values[i] = v
+	}
+	if values[0] < 1 {
+		return SchemaVer{}, fmt.Errorf("model value must be greater than 0: %s", s)
+	}
+	return New(values[0], values[1], values[2]), nil
 }
 
-func (s SchemaVer) RevisionPart() (int, bool) {
-	return parseVersionPart(s, 1)
+func (s SchemaVer) Valid() bool {
+	return s.Model > 0 && s.Revision >= 0 && s.Addition >= 0
 }
 
-func (s SchemaVer) AdditionPart() (int, bool) {
-	return parseVersionPart(s, 2)
+func (s SchemaVer) MarshalJSON() ([]byte, error) {
+	return []byte(fmt.Sprintf(`"%s"`, s.String())), nil
 }
 
-func parseVersionPart(s SchemaVer, index int) (int, bool) {
-	parts := strings.Split(string(s), ".")
-	if len(parts) <= index {
-		return 0, false
+func (s *SchemaVer) UnmarshalJSON(data []byte) error {
+	var str string
+	if err := json.Unmarshal(data, &str); err != nil {
+		return fmt.Errorf("failed to unmarshal schema version as string: %w", err)
 	}
-	value, err := strconv.Atoi(parts[index])
+
+	parsed, err := Parse(str)
 	if err != nil {
-		return 0, false
+		return fmt.Errorf("failed to parse schema version: %w", err)
 	}
-	return value, true
+	*s = parsed
+	return nil
+}
+
+func (s SchemaVer) String() string {
+	return fmt.Sprintf("v%d.%d.%d", s.Model, s.Revision, s.Addition)
+}
+
+func (s SchemaVer) LessThan(other SchemaVer) bool {
+	if s.Model != other.Model {
+		return s.Model < other.Model
+	}
+
+	if s.Revision != other.Revision {
+		return s.Revision < other.Revision
+	}
+
+	return s.Addition < other.Addition
+}
+
+func (s SchemaVer) GreaterOrEqualTo(other SchemaVer) bool {
+	return !s.LessThan(other)
 }


Test patch:
diff --git a/cmd/grype/cli/commands/db_check_test.go b/cmd/grype/cli/commands/db_check_test.go
index ff95a386..82c5026c 100644
--- a/cmd/grype/cli/commands/db_check_test.go
+++ b/cmd/grype/cli/commands/db_check_test.go
@@ -11,17 +11,18 @@ import (
 
 	db "github.com/anchore/grype/grype/db/v6"
 	"github.com/anchore/grype/grype/db/v6/distribution"
+	"github.com/anchore/grype/internal/schemaver"
 )
 
 func TestPresentNewDBCheck(t *testing.T) {
 	currentDB := &db.Description{
-		SchemaVersion: "v6.0.0",
+		SchemaVersion: schemaver.New(6, 0, 0),
 		Built:         db.Time{Time: time.Date(2023, 11, 25, 12, 0, 0, 0, time.UTC)},
 	}
 
 	candidateDB := &distribution.Archive{
 		Description: db.Description{
-			SchemaVersion: "v6.0.1",
+			SchemaVersion: schemaver.New(6, 0, 1),
 			Built:         db.Time{Time: time.Date(2023, 11, 26, 12, 0, 0, 0, time.UTC)},
 		},
 		Path:     "vulnerability-db_6.0.1_2023-11-26T12:00:00Z_6238463.tar.gz",
diff --git a/cmd/grype/cli/commands/db_list_test.go b/cmd/grype/cli/commands/db_list_test.go
index e5b9d9f9..25eec621 100644
--- a/cmd/grype/cli/commands/db_list_test.go
+++ b/cmd/grype/cli/commands/db_list_test.go
@@ -123,7 +123,7 @@ func TestPresentNewDBList(t *testing.T) {
 			format: textOutputFormat,
 			latest: latestDoc,
 			expectedText: `Status:   active
-Schema:   6.0.0
+Schema:   v6.0.0
 Built:    2024-11-27T14:43:17Z
 Listing:  http://localhost:8000/latest.json
 DB URL:   http://localhost:8000/vulnerability-db_v6.0.0_2024-11-25T01:31:56Z_1732718597.tar.zst
@@ -137,7 +137,7 @@ Checksum: sha256:16bcb6551c748056f752f299fcdb4fa50fe61589d086be3889e670261ff21ca
 			latest: latestDoc,
 			expectedText: `{
  "status": "active",
- "schemaVersion": "6.0.0",
+ "schemaVersion": "v6.0.0",
  "built": "2024-11-27T14:43:17Z",
  "path": "vulnerability-db_v6.0.0_2024-11-25T01:31:56Z_1732718597.tar.zst",
  "checksum": "sha256:16bcb6551c748056f752f299fcdb4fa50fe61589d086be3889e670261ff21ca4"
diff --git a/grype/db/v6/db_metadata_store_test.go b/grype/db/v6/db_metadata_store_test.go
index 69542c63..8eb6ef12 100644
--- a/grype/db/v6/db_metadata_store_test.go
+++ b/grype/db/v6/db_metadata_store_test.go
@@ -9,7 +9,9 @@ import (
 )
 
 func TestDbMetadataStore_empty(t *testing.T) {
-	s := newDBMetadataStore(setupTestStore(t).db)
+	db := setupTestStore(t).db
+	require.NoError(t, db.Where("true").Delete(&DBMetadata{}).Error) // delete all existing records
+	s := newDBMetadataStore(db)
 
 	// attempt to fetch a non-existent record
 	actualMetadata, err := s.GetDBMetadata()
@@ -59,5 +61,16 @@ func setupTestStore(t testing.TB, d ...string) *store {
 	}, true, true)
 	require.NoError(t, err)
 
+	require.NoError(t, s.SetDBMetadata())
+
+	return s
+}
+
+func setupReadOnlyTestStore(t testing.TB, dir string) *store {
+	s, err := newStore(Config{
+		DBDirPath: dir,
+	}, false, false)
+	require.NoError(t, err)
+
 	return s
 }
diff --git a/grype/db/v6/description_test.go b/grype/db/v6/description_test.go
index 4e42709d..a6955c90 100644
--- a/grype/db/v6/description_test.go
+++ b/grype/db/v6/description_test.go
@@ -2,10 +2,7 @@ package v6
 
 import (
 	"encoding/json"
-	"os"
 	"path"
-	"path/filepath"
-	"strings"
 	"testing"
 	"time"
 
@@ -112,129 +109,3 @@ func TestTime_JSONUnmarshalling(t *testing.T) {
 		})
 	}
 }
-
-func TestWriteChecksums(t *testing.T) {
-
-	cases := []struct {
-		name     string
-		digest   string
-		expected string
-		wantErr  require.ErrorAssertionFunc
-	}{
-		{
-			name:     "go case",
-			digest:   "xxh64:dummychecksum",
-			expected: "xxh64:dummychecksum",
-		},
-		{
-			name:    "empty checksum",
-			wantErr: require.Error,
-		},
-		{
-			name:    "missing prefix",
-			digest:  "dummychecksum",
-			wantErr: require.Error,
-		},
-	}
-
-	for _, tc := range cases {
-		t.Run(tc.name, func(t *testing.T) {
-			if tc.wantErr == nil {
-				tc.wantErr = require.NoError
-			}
-			sb := strings.Builder{}
-			err := WriteChecksums(&sb, tc.digest)
-			tc.wantErr(t, err)
-			if err == nil {
-				assert.Equal(t, tc.expected, sb.String())
-			}
-		})
-	}
-}
-
-func TestReadDBChecksum(t *testing.T) {
-	tests := []struct {
-		name             string
-		checksumContent  string
-		expectedErr      string
-		expectedChecksum string
-	}{
-		{
-			name:            "checksum file missing",
-			checksumContent: "",
-			expectedErr:     "failed to read checksums file",
-		},
-		{
-			name:            "invalid checksum format",
-			checksumContent: "invalid",
-			expectedErr:     "checksums file is not in the expected format",
-		},
-		{
-			name:             "valid checksum format",
-			checksumContent:  "xxh64:checksum",
-			expectedChecksum: "xxh64:checksum",
-		},
-	}
-
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			dir := t.TempDir()
-			if tt.checksumContent != "" {
-				err := os.WriteFile(filepath.Join(dir, ChecksumFileName), []byte(tt.checksumContent), 0644)
-				require.NoError(t, err)
-			}
-
-			checksum, err := ReadDBChecksum(dir)
-
-			if tt.expectedErr != "" {
-				require.ErrorContains(t, err, tt.expectedErr)
-				require.Empty(t, checksum)
-			} else {
-				require.NoError(t, err)
-				require.Equal(t, tt.expectedChecksum, checksum)
-			}
-		})
-	}
-}
-
-func TestCalculateDigest(t *testing.T) {
-	tests := []struct {
-		name           string
-		fileContent    string
-		expectedErr    string
-		expectedDigest string
-	}{
-		{
-			name:        "file does not exist",
-			fileContent: "",
-			expectedErr: "failed to calculate checksum for DB file",
-		},
-		{
-			name:           "valid file",
-			fileContent:    "testcontent",
-			expectedDigest: "xxh64:d37ed71e4fee2ebd",
-		},
-	}
-
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			dir := t.TempDir()
-			filePath := filepath.Join(dir, VulnerabilityDBFileName)
-
-			if tt.fileContent != "" {
-				err := os.WriteFile(filePath, []byte(tt.fileContent), 0644)
-				require.NoError(t, err)
-			}
-
-			digest, err := CalculateDBDigest(filePath)
-
-			if tt.expectedErr != "" {
-				require.ErrorContains(t, err, tt.expectedErr)
-				require.Empty(t, digest)
-			} else {
-				require.NoError(t, err)
-				require.Equal(t, tt.expectedDigest, digest)
-			}
-		})
-	}
-}
diff --git a/grype/db/v6/distribution/client_test.go b/grype/db/v6/distribution/client_test.go
index b9ac8841..357f88ac 100644
--- a/grype/db/v6/distribution/client_test.go
+++ b/grype/db/v6/distribution/client_test.go
@@ -15,6 +15,7 @@ import (
 	"github.com/wagoodman/go-progress"
 
 	db "github.com/anchore/grype/grype/db/v6"
+	"github.com/anchore/grype/internal/schemaver"
 )
 
 func TestClient_LatestFromURL(t *testing.T) {
@@ -31,7 +32,7 @@ func TestClient_LatestFromURL(t *testing.T) {
 					Status: "active",
 					Archive: Archive{
 						Description: db.Description{
-							SchemaVersion: "1.0.0",
+							SchemaVersion: schemaver.New(1, 0, 0),
 							Built:         db.Time{Time: time.Date(2023, 9, 26, 12, 0, 0, 0, time.UTC)},
 						},
 						Path:     "path/to/archive",
@@ -51,7 +52,7 @@ func TestClient_LatestFromURL(t *testing.T) {
 				Status: "active",
 				Archive: Archive{
 					Description: db.Description{
-						SchemaVersion: "1.0.0",
+						SchemaVersion: schemaver.New(1, 0, 0),
 						Built:         db.Time{Time: time.Date(2023, 9, 26, 12, 0, 0, 0, time.UTC)},
 					},
 					Path:     "path/to/archive",
@@ -189,7 +190,7 @@ func TestClient_Download(t *testing.T) {
 
 func TestClient_IsUpdateAvailable(t *testing.T) {
 	current := &db.Description{
-		SchemaVersion: "1.0.0",
+		SchemaVersion: schemaver.New(1, 0, 0),
 		Built:         db.Time{Time: time.Date(2023, 9, 26, 12, 0, 0, 0, time.UTC)},
 	}
 
@@ -205,7 +206,7 @@ func TestClient_IsUpdateAvailable(t *testing.T) {
 				Status: StatusActive,
 				Archive: Archive{
 					Description: db.Description{
-						SchemaVersion: "1.0.0",
+						SchemaVersion: schemaver.New(1, 0, 0),
 						Built:         db.Time{Time: time.Date(2023, 9, 27, 12, 0, 0, 0, time.UTC)},
 					},
 					Path:     "path/to/archive.tar.gz",
@@ -214,7 +215,7 @@ func TestClient_IsUpdateAvailable(t *testing.T) {
 			},
 			archive: &Archive{
 				Description: db.Description{
-					SchemaVersion: "1.0.0",
+					SchemaVersion: schemaver.New(1, 0, 0),
 					Built:         db.Time{Time: time.Date(2023, 9, 27, 12, 0, 0, 0, time.UTC)},
 				},
 				Path:     "path/to/archive.tar.gz",
@@ -227,7 +228,7 @@ func TestClient_IsUpdateAvailable(t *testing.T) {
 				Status: "active",
 				Archive: Archive{
 					Description: db.Description{
-						SchemaVersion: "1.0.0",
+						SchemaVersion: schemaver.New(1, 0, 0),
 						Built:         db.Time{Time: time.Date(2023, 9, 26, 12, 0, 0, 0, time.UTC)},
 					},
 					Path:     "path/to/archive.tar.gz",
@@ -247,7 +248,7 @@ func TestClient_IsUpdateAvailable(t *testing.T) {
 				Status: StatusDeprecated,
 				Archive: Archive{
 					Description: db.Description{
-						SchemaVersion: "1.0.0",
+						SchemaVersion: schemaver.New(1, 0, 0),
 						Built:         db.Time{Time: time.Date(2023, 9, 27, 12, 0, 0, 0, time.UTC)},
 					},
 					Path:     "path/to/archive.tar.gz",
@@ -256,7 +257,7 @@ func TestClient_IsUpdateAvailable(t *testing.T) {
 			},
 			archive: &Archive{
 				Description: db.Description{
-					SchemaVersion: "1.0.0",
+					SchemaVersion: schemaver.New(1, 0, 0),
 					Built:         db.Time{Time: time.Date(2023, 9, 27, 12, 0, 0, 0, time.UTC)},
 				},
 				Path:     "path/to/archive.tar.gz",
@@ -270,7 +271,7 @@ func TestClient_IsUpdateAvailable(t *testing.T) {
 				Status: StatusEndOfLife,
 				Archive: Archive{
 					Description: db.Description{
-						SchemaVersion: "1.0.0",
+						SchemaVersion: schemaver.New(1, 0, 0),
 						Built:         db.Time{Time: time.Date(2023, 9, 27, 12, 0, 0, 0, time.UTC)},
 					},
 					Path:     "path/to/archive.tar.gz",
@@ -279,7 +280,7 @@ func TestClient_IsUpdateAvailable(t *testing.T) {
 			},
 			archive: &Archive{
 				Description: db.Description{
-					SchemaVersion: "1.0.0",
+					SchemaVersion: schemaver.New(1, 0, 0),
 					Built:         db.Time{Time: time.Date(2023, 9, 27, 12, 0, 0, 0, time.UTC)},
 				},
 				Path:     "path/to/archive.tar.gz",
@@ -308,22 +309,22 @@ func TestDatabaseDescription_IsSupersededBy(t *testing.T) {
 	t2 := time.Date(2023, 9, 27, 12, 0, 0, 0, time.UTC)
 
 	currentMetadata := db.Description{
-		SchemaVersion: "1.0.0",
+		SchemaVersion: schemaver.New(1, 0, 0),
 		Built:         db.Time{Time: t1},
 	}
 
 	newerMetadata := db.Description{
-		SchemaVersion: "1.0.0",
+		SchemaVersion: schemaver.New(1, 0, 0),
 		Built:         db.Time{Time: t2},
 	}
 
 	olderMetadata := db.Description{
-		SchemaVersion: "1.0.0",
+		SchemaVersion: schemaver.New(1, 0, 0),
 		Built:         db.Time{Time: t1},
 	}
 
 	differentModelMetadata := db.Description{
-		SchemaVersion: "2.0.0",
+		SchemaVersion: schemaver.New(2, 0, 0),
 		Built:         db.Time{Time: t2},
 	}
 
diff --git a/grype/db/v6/distribution/latest.go b/grype/db/v6/distribution/latest.go
index 6c2ff33a..b0a12e77 100644
--- a/grype/db/v6/distribution/latest.go
+++ b/grype/db/v6/distribution/latest.go
@@ -1,6 +1,7 @@
 package distribution
 
 import (
+	"crypto/sha256"
 	"encoding/json"
 	"fmt"
 	"io"
@@ -8,7 +9,10 @@ import (
 	"sort"
 	"time"
 
+	"github.com/spf13/afero"
+
 	db "github.com/anchore/grype/grype/db/v6"
+	"github.com/anchore/grype/internal/file"
 	"github.com/anchore/grype/internal/schemaver"
 )
 
@@ -37,7 +41,7 @@ type Archive struct {
 func NewLatestDocument(entries ...Archive) *LatestDocument {
 	var validEntries []Archive
 	for _, entry := range entries {
-		if modelPart, ok := entry.SchemaVersion.ModelPart(); ok && modelPart == db.ModelVersion {
+		if entry.SchemaVersion.Model == db.ModelVersion {
 			validEntries = append(validEntries, entry)
 		}
 	}
@@ -71,7 +75,7 @@ func NewLatestFromReader(reader io.Reader) (*LatestDocument, error) {
 }
 
 func NewArchive(path string, t time.Time, model, revision, addition int) (*Archive, error) {
-	checksum, err := db.CalculateArchiveDigest(path)
+	checksum, err := calculateArchiveDigest(path)
 	if err != nil {
 		return nil, fmt.Errorf("failed to calculate archive checksum: %w", err)
 	}
@@ -88,7 +92,7 @@ func NewArchive(path string, t time.Time, model, revision, addition int) (*Archi
 }
 
 func (l LatestDocument) Write(writer io.Writer) error {
-	if l.SchemaVersion == "" {
+	if l.SchemaVersion.Model == 0 {
 		return fmt.Errorf("missing schema version")
 	}
 
@@ -116,3 +120,11 @@ func (l LatestDocument) Write(writer io.Writer) error {
 	_, err = writer.Write(contents)
 	return err
 }
+
+func calculateArchiveDigest(dbFilePath string) (string, error) {
+	digest, err := file.HashFile(afero.NewOsFs(), dbFilePath, sha256.New())
+	if err != nil {
+		return "", fmt.Errorf("failed to calculate checksum for DB archive file: %w", err)
+	}
+	return fmt.Sprintf("sha256:%s", digest), nil
+}
diff --git a/grype/db/v6/distribution/latest_test.go b/grype/db/v6/distribution/latest_test.go
index f841a3e3..9e09392b 100644
--- a/grype/db/v6/distribution/latest_test.go
+++ b/grype/db/v6/distribution/latest_test.go
@@ -34,9 +34,7 @@ func TestNewLatestDocument(t *testing.T) {
 		latestDoc := NewLatestDocument(archive1, archive2)
 		require.NotNil(t, latestDoc)
 		require.Equal(t, latestDoc.Archive, archive1) // most recent archive
-		actual, ok := latestDoc.SchemaVersion.ModelPart()
-		require.True(t, ok)
-		require.Equal(t, actual, db.ModelVersion)
+		require.Equal(t, latestDoc.SchemaVersion.Model, db.ModelVersion)
 	})
 
 	t.Run("filter entries", func(t *testing.T) {
@@ -56,9 +54,7 @@ func TestNewLatestDocument(t *testing.T) {
 		latestDoc := NewLatestDocument(archive1, archive2)
 		require.NotNil(t, latestDoc)
 		require.Equal(t, latestDoc.Archive, archive2) // most recent archive with valid version
-		actual, ok := latestDoc.SchemaVersion.ModelPart()
-		require.True(t, ok)
-		require.Equal(t, actual, db.ModelVersion)
+		require.Equal(t, latestDoc.SchemaVersion.Model, db.ModelVersion)
 	})
 
 	t.Run("no entries", func(t *testing.T) {
@@ -73,7 +69,8 @@ func TestNewLatestFromReader(t *testing.T) {
 		latestDoc := LatestDocument{
 			Archive: Archive{
 				Description: db.Description{
-					Built: db.Time{Time: time.Now().Truncate(time.Second).UTC()},
+					SchemaVersion: schemaver.New(db.ModelVersion, db.Revision, db.Addition),
+					Built:         db.Time{Time: time.Now().Truncate(time.Second).UTC()},
 				},
 			},
 			Status: "active",
diff --git a/grype/db/v6/import_metadata_test.go b/grype/db/v6/import_metadata_test.go
new file mode 100644
index 00000000..7be855fb
--- /dev/null
+++ b/grype/db/v6/import_metadata_test.go
@@ -0,0 +1,166 @@
+package v6
+
+import (
+	"bytes"
+	"encoding/json"
+	"os"
+	"path/filepath"
+	"testing"
+
+	"github.com/spf13/afero"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	"github.com/anchore/grype/internal/schemaver"
+)
+
+func TestReadImportMetadata(t *testing.T) {
+	tests := []struct {
+		name           string
+		fileContent    string
+		emptyFile      bool
+		expectedErr    string
+		expectedResult *ImportMetadata
+	}{
+		{
+			name:        "file does not exist",
+			fileContent: "",
+		},
+		{
+			name:      "empty file",
+			emptyFile: true,
+		},
+		{
+			name:        "invalid json",
+			fileContent: "invalid json",
+			expectedErr: "failed to unmarshal import metadata",
+		},
+		{
+			name:        "missing checksum prefix",
+			fileContent: `{"digest": "invalid", "client_version": "1.0.0"}`,
+			expectedErr: "import metadata digest is not in the expected format",
+		},
+		{
+			name:        "valid metadata",
+			fileContent: `{"digest": "xxh64:testdigest", "client_version": "1.0.0"}`,
+			expectedResult: &ImportMetadata{
+				Digest:        "xxh64:testdigest",
+				ClientVersion: "1.0.0",
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			dir := t.TempDir()
+			filePath := filepath.Join(dir, ImportMetadataFileName)
+
+			if tt.fileContent != "" {
+				err := os.WriteFile(filePath, []byte(tt.fileContent), 0644)
+				require.NoError(t, err)
+			} else if tt.emptyFile {
+				_, err := os.Create(filePath)
+				require.NoError(t, err)
+			}
+
+			result, err := ReadImportMetadata(afero.NewOsFs(), dir)
+
+			if tt.expectedErr != "" {
+				require.ErrorContains(t, err, tt.expectedErr)
+				require.Nil(t, result)
+			} else {
+				require.NoError(t, err)
+				require.Equal(t, tt.expectedResult, result)
+			}
+		})
+	}
+}
+
+func TestWriteImportMetadata(t *testing.T) {
+	cases := []struct {
+		name            string
+		checksum        string
+		expectedVersion string
+		wantErr         require.ErrorAssertionFunc
+	}{
+		{
+			name:            "valid checksum",
+			checksum:        "xxh64:testdigest",
+			expectedVersion: schemaver.New(ModelVersion, Revision, Addition).String(),
+			wantErr:         require.NoError,
+		},
+		{
+			name:     "empty checksum",
+			checksum: "",
+			wantErr:  require.Error,
+		},
+		{
+			name:     "missing prefix",
+			checksum: "testdigest",
+			wantErr:  require.Error,
+		},
+	}
+
+	for _, tc := range cases {
+		t.Run(tc.name, func(t *testing.T) {
+			var buf bytes.Buffer
+			claim, err := writeImportMetadata(&buf, tc.checksum)
+			tc.wantErr(t, err)
+
+			if err == nil {
+				result := buf.String()
+
+				var doc ImportMetadata
+				err := json.Unmarshal([]byte(result), &doc)
+				require.NoError(t, err)
+
+				assert.Equal(t, tc.checksum, doc.Digest)
+				assert.Equal(t, tc.checksum, claim.Digest)
+				assert.Equal(t, tc.expectedVersion, doc.ClientVersion)
+				assert.Equal(t, tc.expectedVersion, claim.ClientVersion)
+			}
+		})
+	}
+}
+
+func TestCalculateDBDigest(t *testing.T) {
+	tests := []struct {
+		name           string
+		fileContent    string
+		expectedErr    string
+		expectedDigest string
+	}{
+		{
+			name:        "file does not exist",
+			fileContent: "",
+			expectedErr: "failed to digest DB file",
+		},
+		{
+			name:           "valid file",
+			fileContent:    "testcontent",
+			expectedDigest: "xxh64:d37ed71e4fee2ebd",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			dir := t.TempDir()
+			filePath := filepath.Join(dir, VulnerabilityDBFileName)
+
+			if tt.fileContent != "" {
+				err := os.WriteFile(filePath, []byte(tt.fileContent), 0644)
+				require.NoError(t, err)
+			}
+
+			digest, err := CalculateDBDigest(afero.NewOsFs(), filePath)
+
+			if tt.expectedErr != "" {
+				require.ErrorContains(t, err, tt.expectedErr)
+				require.Empty(t, digest)
+			} else {
+				require.NoError(t, err)
+				require.Equal(t, tt.expectedDigest, digest)
+			}
+		})
+	}
+}
diff --git a/grype/db/v6/installation/curator_test.go b/grype/db/v6/installation/curator_test.go
index 1954dd2b..9af297f7 100644
--- a/grype/db/v6/installation/curator_test.go
+++ b/grype/db/v6/installation/curator_test.go
@@ -1,6 +1,7 @@
 package installation
 
 import (
+	"encoding/json"
 	"errors"
 	"os"
 	"path"
@@ -108,52 +109,61 @@ func setupCuratorForUpdate(t *testing.T, opts ...setupOption) curator {
 	return c
 }
 
-func writeTestChecksumsFile(t *testing.T, fs afero.Fs, dir string, checksums string) {
-	require.NoError(t, fs.MkdirAll(dir, 0755))
-
-	metadataFilePath := path.Join(dir, db.ChecksumFileName)
-	require.NoError(t, afero.WriteFile(fs, metadataFilePath, []byte(checksums), 0644))
-}
-
 func writeTestDescriptionToDB(t *testing.T, dir string, desc db.Description) string {
 	c := db.Config{DBDirPath: dir}
-	d, err := db.NewLowLevelDB(c.DBFilePath(), false, false, true)
+	d, err := db.NewLowLevelDB(c.DBFilePath(), false, true, true)
 	require.NoError(t, err)
 
-	if err := d.Unscoped().Where("true").Delete(&db.DBMetadata{}).Error; err != nil {
+	if err := d.Where("true").Delete(&db.DBMetadata{}).Error; err != nil {
 		t.Fatalf("failed to delete existing DB metadata record: %v", err)
 	}
 
-	mod, ok := desc.SchemaVersion.ModelPart()
-	require.True(t, ok)
-
-	revision, ok := desc.SchemaVersion.RevisionPart()
-	require.True(t, ok)
-
-	addition, ok := desc.SchemaVersion.AdditionPart()
-	require.True(t, ok)
+	require.NotEmpty(t, desc.SchemaVersion.Model)
+	require.NotEmpty(t, desc.SchemaVersion.String())
 
 	ts := time.Now().UTC()
 	instance := &db.DBMetadata{
 		BuildTimestamp: &ts,
-		Model:          mod,
-		Revision:       revision,
-		Addition:       addition,
+		Model:          desc.SchemaVersion.Model,
+		Revision:       desc.SchemaVersion.Revision,
+		Addition:       desc.SchemaVersion.Revision,
 	}
 
 	require.NoError(t, d.Create(instance).Error)
 
 	require.NoError(t, d.Exec("VACUUM").Error)
 
-	digest, err := db.CalculateDBDigest(c.DBFilePath())
+	digest, err := db.CalculateDBDigest(afero.NewOsFs(), c.DBFilePath())
 	require.NoError(t, err)
 
-	// write the checksums file
-	writeTestChecksumsFile(t, afero.NewOsFs(), dir, digest)
+	writeTestImportMetadata(t, afero.NewOsFs(), dir, digest)
 
 	return digest
 }
 
+func writeTestImportMetadata(t *testing.T, fs afero.Fs, dir string, checksums string) {
+	writeTestImportMetadataWithCustomVersion(t, fs, dir, checksums, schemaver.New(db.ModelVersion, db.Revision, db.Addition).String())
+}
+
+func writeTestImportMetadataWithCustomVersion(t *testing.T, fs afero.Fs, dir string, checksums string, ver string) {
+	require.NoError(t, fs.MkdirAll(dir, 0755))
+
+	metadataFilePath := path.Join(dir, db.ImportMetadataFileName)
+
+	writer, err := afero.NewOsFs().Create(metadataFilePath)
+	require.NoError(t, err)
+	defer writer.Close()
+	enc := json.NewEncoder(writer)
+	enc.SetIndent("", " ")
+
+	doc := db.ImportMetadata{
+		Digest:        checksums,
+		ClientVersion: ver,
+	}
+
+	require.NoError(t, enc.Encode(doc))
+}
+
 func writeTestDB(t *testing.T, fs afero.Fs, dir string) string {
 	require.NoError(t, fs.MkdirAll(dir, 0755))
 
@@ -165,9 +175,11 @@ func writeTestDB(t *testing.T, fs afero.Fs, dir string) string {
 	require.NoError(t, rw.SetDBMetadata())
 	require.NoError(t, rw.Close())
 
-	checksum, err := db.ReadDBChecksum(dir)
+	doc, err := db.WriteImportMetadata(fs, dir)
+	require.NoError(t, err)
+	require.NotNil(t, doc)
 
-	return checksum
+	return doc.Digest
 }
 
 func TestCurator_Update(t *testing.T) {
@@ -490,16 +502,16 @@ func TestCurator_ValidateIntegrity(t *testing.T) {
 
 		require.NoError(t, os.MkdirAll(cfg.DBDirectoryPath(), 0755))
 
-		s := setupTestDB(t, cfg.DBDirectoryPath())
-		require.NoError(t, s.SetDBMetadata())
-		require.NoError(t, s.Close())
+		sw := setupTestDB(t, cfg.DBDirectoryPath())
+		require.NoError(t, sw.SetDBMetadata())
+		require.NoError(t, sw.Close())
+		s := setupReadOnlyTestDB(t, cfg.DBDirectoryPath())
 
 		// assume that we already have a valid checksum file
-		digest, err := db.CalculateDBDigest(cfg.DBFilePath())
+		digest, err := db.CalculateDBDigest(afero.NewOsFs(), cfg.DBFilePath())
 		require.NoError(t, err)
 
-		checksumsFilePath := filepath.Join(cfg.DBDirectoryPath(), db.ChecksumFileName)
-		require.NoError(t, os.WriteFile(checksumsFilePath, []byte(digest), 0644))
+		writeTestImportMetadata(t, afero.NewOsFs(), cfg.DBDirectoryPath(), digest)
 
 		ci, err := NewCurator(cfg, new(mockClient))
 		require.NoError(t, err)
@@ -528,19 +540,19 @@ func TestCurator_ValidateIntegrity(t *testing.T) {
 		require.ErrorContains(t, err, "database does not exist")
 	})
 
-	t.Run("checksum file does not exist", func(t *testing.T) {
+	t.Run("import metadata file does not exist", func(t *testing.T) {
 		c, d := newCurator(t)
 		dbDir := c.config.DBDirectoryPath()
-		require.NoError(t, os.Remove(filepath.Join(dbDir, db.ChecksumFileName)))
+		require.NoError(t, os.Remove(filepath.Join(dbDir, db.ImportMetadataFileName)))
 		_, _, err := c.validateIntegrity(d, c.config.DBFilePath(), true)
-		require.ErrorContains(t, err, "no such file or directory")
+		require.ErrorContains(t, err, "missing import metadata")
 	})
 
 	t.Run("invalid checksum", func(t *testing.T) {
 		c, d := newCurator(t)
 		dbDir := c.config.DBDirectoryPath()
 
-		writeTestChecksumsFile(t, c.fs, dbDir, "xxh64:invalidchecksum")
+		writeTestImportMetadata(t, c.fs, dbDir, "xxh64:invalidchecksum")
 
 		_, _, err := c.validateIntegrity(d, c.config.DBFilePath(), true)
 		require.ErrorContains(t, err, "bad db checksum")
@@ -663,6 +675,85 @@ func TestReplaceDB(t *testing.T) {
 		})
 	}
 }
+func Test_isRehydrationNeeded(t *testing.T) {
+	tests := []struct {
+		name               string
+		currentDBVersion   schemaver.SchemaVer
+		hydrationClientVer schemaver.SchemaVer
+		currentClientVer   schemaver.SchemaVer
+		expectedResult     bool
+		expectedErr        string
+	}{
+		{
+			name:             "no database exists",
+			currentDBVersion: schemaver.SchemaVer{},
+			currentClientVer: schemaver.New(6, 2, 0),
+			expectedResult:   false,
+		},
+		{
+			name:             "no import metadata exists",
+			currentDBVersion: schemaver.New(6, 0, 0),
+			currentClientVer: schemaver.New(6, 2, 0),
+			expectedErr:      "missing import metadata",
+			expectedResult:   false,
+		},
+		{
+			name:               "invalid client version in metadata",
+			currentDBVersion:   schemaver.New(6, 0, 0),
+			hydrationClientVer: schemaver.SchemaVer{-19, 0, 0},
+			currentClientVer:   schemaver.New(6, 2, 0),
+			expectedResult:     false,
+			expectedErr:        "unable to parse client version from import metadata",
+		},
+		{
+			name:               "rehydration needed",
+			currentDBVersion:   schemaver.New(6, 0, 1),
+			hydrationClientVer: schemaver.New(6, 0, 0),
+			currentClientVer:   schemaver.New(6, 0, 2),
+			expectedResult:     true,
+		},
+		{
+			name:               "no rehydration needed - client version equals current client version",
+			currentDBVersion:   schemaver.New(6, 0, 0),
+			hydrationClientVer: schemaver.New(6, 2, 0),
+			currentClientVer:   schemaver.New(6, 2, 0),
+			expectedResult:     false,
+		},
+		{
+			name:               "no rehydration needed - client version greater than current client version",
+			currentDBVersion:   schemaver.New(6, 0, 0),
+			hydrationClientVer: schemaver.New(6, 3, 0),
+			currentClientVer:   schemaver.New(6, 2, 0),
+			expectedResult:     false,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			fs := afero.NewOsFs()
+			testDir := t.TempDir()
+
+			if tt.hydrationClientVer.Model != 0 {
+				writeTestImportMetadataWithCustomVersion(t, fs, testDir, "xxh64:something", tt.hydrationClientVer.String())
+			}
+
+			var dbVersion *schemaver.SchemaVer
+			if tt.currentDBVersion.Model != 0 {
+				dbVersion = &tt.currentDBVersion
+			}
+
+			result, err := isRehydrationNeeded(fs, testDir, dbVersion, tt.currentClientVer)
+
+			if tt.expectedErr != "" {
+				require.Error(t, err)
+				assert.Contains(t, err.Error(), tt.expectedErr)
+			} else {
+				require.NoError(t, err)
+				assert.Equal(t, tt.expectedResult, result)
+			}
+		})
+	}
+}
 
 func setupTestDB(t *testing.T, dbDir string) db.ReadWriter {
 	s, err := db.NewWriter(db.Config{
@@ -672,3 +763,12 @@ func setupTestDB(t *testing.T, dbDir string) db.ReadWriter {
 
 	return s
 }
+
+func setupReadOnlyTestDB(t *testing.T, dbDir string) db.Reader {
+	s, err := db.NewReader(db.Config{
+		DBDirPath: dbDir,
+	})
+	require.NoError(t, err)
+
+	return s
+}
diff --git a/grype/db/v6/store_test.go b/grype/db/v6/store_test.go
index a3b95de0..b508d615 100644
--- a/grype/db/v6/store_test.go
+++ b/grype/db/v6/store_test.go
@@ -10,22 +10,32 @@ import (
 func TestStoreClose(t *testing.T) {
 
 	t.Run("readonly mode does nothing", func(t *testing.T) {
-		s := setupTestStore(t)
+		dir := t.TempDir()
+		s := setupTestStore(t, dir)
 		s.empty = false
 		s.writable = false
 
 		err := s.Close()
 		require.NoError(t, err)
 
-		// ensure we have our indexes
+		// ensure the connection is no longer open
 		var indexes []string
 		s.db.Raw(`SELECT name FROM sqlite_master WHERE type = 'index' AND name NOT LIKE 'sqlite_autoindex%'`).Scan(&indexes)
+		assert.Empty(t, indexes)
+
+		// get a new connection (readonly)
+		s = setupReadOnlyTestStore(t, dir)
+
+		// ensure we have our indexes
+		indexes = nil
+		s.db.Raw(`SELECT name FROM sqlite_master WHERE type = 'index' AND name NOT LIKE 'sqlite_autoindex%'`).Scan(&indexes)
 		assert.NotEmpty(t, indexes)
 
 	})
 
 	t.Run("successful close in writable mode", func(t *testing.T) {
-		s := setupTestStore(t)
+		dir := t.TempDir()
+		s := setupTestStore(t, dir)
 
 		// ensure we have indexes to start with
 		var indexes []string
@@ -35,6 +45,9 @@ func TestStoreClose(t *testing.T) {
 		err := s.Close()
 		require.NoError(t, err)
 
+		// get a new connection (readonly)
+		s = setupReadOnlyTestStore(t, dir)
+
 		// ensure all of our indexes were dropped
 		indexes = nil
 		s.db.Raw(`SELECT name FROM sqlite_master WHERE type = 'index' AND name NOT LIKE 'sqlite_autoindex%'`).Scan(&indexes)
diff --git a/grype/db/v6/vulnerability_decorator_store_test.go b/grype/db/v6/vulnerability_decorator_store_test.go
new file mode 100644
index 00000000..7f94d830
--- /dev/null
+++ b/grype/db/v6/vulnerability_decorator_store_test.go
@@ -0,0 +1,296 @@
+package v6
+
+import (
+	"slices"
+	"testing"
+	"time"
+
+	"github.com/google/go-cmp/cmp"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	"github.com/anchore/grype/internal/schemaver"
+)
+
+func TestVulnerabilityDecoratorStore(t *testing.T) {
+	tests := []struct {
+		name        string
+		kevEnabled  bool
+		setupStore  func(*vulnerabilityDecoratorStore) error
+		input       []*KnownExploitedVulnerabilityHandle
+		expectError require.ErrorAssertionFunc
+	}{
+		{
+			name:       "happy path - single KEV",
+			kevEnabled: true,
+			input: []*KnownExploitedVulnerabilityHandle{
+				{
+					Cve: "CVE-2023-1234",
+					BlobValue: &KnownExploitedVulnerabilityBlob{
+						Cve:           "CVE-2023-1234",
+						VendorProject: "Test Vendor",
+						Product:       "Test Product",
+						DateAdded:     timeRef(time.Now()),
+					},
+				},
+			},
+		},
+		{
+			name:       "happy path - multiple KEVs",
+			kevEnabled: true,
+			input: []*KnownExploitedVulnerabilityHandle{
+				{
+					Cve: "CVE-2023-1234",
+					BlobValue: &KnownExploitedVulnerabilityBlob{
+						Cve:           "CVE-2023-1234",
+						VendorProject: "Vendor 1",
+					},
+				},
+				{
+					Cve: "CVE-2023-5678",
+					BlobValue: &KnownExploitedVulnerabilityBlob{
+						Cve:           "CVE-2023-5678",
+						VendorProject: "Vendor 2",
+					},
+				},
+			},
+		},
+		{
+			name:        "error - KEV disabled",
+			kevEnabled:  false,
+			input:       []*KnownExploitedVulnerabilityHandle{{Cve: "CVE-2023-1234"}},
+			expectError: require.Error,
+		},
+		{
+			name:       "duplicate CVEs (unexpected but allowed)",
+			kevEnabled: true,
+			input: []*KnownExploitedVulnerabilityHandle{
+				{
+					Cve: "CVE-2023-1234",
+					BlobValue: &KnownExploitedVulnerabilityBlob{
+						Cve:            "CVE-2023-1234",
+						RequiredAction: "1",
+					},
+				},
+				{
+					Cve: "CVE-2023-1234",
+					BlobValue: &KnownExploitedVulnerabilityBlob{
+						Cve:            "CVE-2023-1234",
+						RequiredAction: "2",
+					},
+				},
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if tt.expectError == nil {
+				tt.expectError = require.NoError
+			}
+
+			db := setupTestStore(t).db
+			bs := newBlobStore(db)
+
+			s := &vulnerabilityDecoratorStore{
+				db:         db,
+				blobStore:  bs,
+				kevEnabled: tt.kevEnabled,
+			}
+
+			if tt.setupStore != nil {
+				require.NoError(t, tt.setupStore(s))
+			}
+
+			err := s.AddKnownExploitedVulnerabilities(tt.input...)
+			tt.expectError(t, err)
+			if err != nil {
+				return
+			}
+
+			var cves []string
+			for _, kev := range tt.input {
+				if !slices.Contains(cves, kev.Cve) {
+					cves = append(cves, kev.Cve)
+				}
+			}
+
+			var actual []*KnownExploitedVulnerabilityHandle
+			for _, cve := range cves {
+				intermediate, err := s.GetKnownExploitedVulnerabilities(cve)
+				require.NoError(t, err)
+				for i := range intermediate {
+					actual = append(actual, &intermediate[i])
+				}
+			}
+
+			for _, a := range actual {
+				assert.NotZero(t, a.ID)
+				assert.NotZero(t, a.BlobID)
+			}
+
+			if d := cmp.Diff(tt.input, actual); d != "" {
+				t.Errorf("unexpected known exploited vulnerabilities (-expected, +actual): %s", d)
+			}
+		})
+	}
+}
+
+func TestVulnerabilityDecoratorStore_AddKnownExploitedVulnerabilities_VersionCompatibility(t *testing.T) {
+	tests := []struct {
+		name          string
+		dbVersion     schemaver.SchemaVer
+		input         []*KnownExploitedVulnerabilityHandle
+		expectEnabled bool
+		expectError   require.ErrorAssertionFunc
+		expectedCount int
+	}{
+		{
+			name:      "supported db version",
+			dbVersion: schemaver.New(6, 0, 1),
+			input: []*KnownExploitedVulnerabilityHandle{
+				{
+					Cve: "CVE-2023-1234",
+					BlobValue: &KnownExploitedVulnerabilityBlob{
+						Cve:           "CVE-2023-1234",
+						VendorProject: "Test Vendor",
+						DateAdded:     timeRef(time.Now()),
+					},
+				},
+			},
+			expectEnabled: true,
+			expectError:   require.NoError,
+			expectedCount: 1,
+		},
+		{
+			name:      "unsupported db version",
+			dbVersion: schemaver.New(6, 0, 0),
+			input: []*KnownExploitedVulnerabilityHandle{
+				{
+					Cve: "CVE-2023-1234",
+					BlobValue: &KnownExploitedVulnerabilityBlob{
+						Cve: "CVE-2023-1234",
+					},
+				},
+			},
+			expectEnabled: false,
+			expectError:   require.Error,
+			expectedCount: 0,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if tt.expectError == nil {
+				tt.expectError = require.NoError
+			}
+
+			db := setupTestStore(t).db
+			bs := newBlobStore(db)
+
+			s := newVulnerabilityDecoratorStore(db, bs, tt.dbVersion)
+			assert.Equal(t, tt.expectEnabled, s.kevEnabled)
+
+			err := s.AddKnownExploitedVulnerabilities(tt.input...)
+			tt.expectError(t, err)
+			if err != nil {
+				return
+			}
+
+			results, err := s.GetKnownExploitedVulnerabilities(tt.input[0].Cve)
+			require.NoError(t, err)
+			assert.Len(t, results, tt.expectedCount)
+		})
+	}
+}
+
+func TestVulnerabilityDecoratorStore_GetKnownExploitedVulnerabilities_VersionCompatibility(t *testing.T) {
+	tests := []struct {
+		name          string
+		dbVersion     schemaver.SchemaVer
+		input         []*KnownExploitedVulnerabilityHandle
+		expectEnabled bool
+		expectError   require.ErrorAssertionFunc
+		expectedCount int
+	}{
+		{
+			name:      "supported db version",
+			dbVersion: schemaver.New(6, 0, 1),
+			input: []*KnownExploitedVulnerabilityHandle{
+				{
+					Cve: "CVE-2023-1234",
+					BlobValue: &KnownExploitedVulnerabilityBlob{
+						Cve: "CVE-2023-1234",
+					},
+				},
+			},
+			expectEnabled: true,
+			expectError:   require.NoError,
+			expectedCount: 1,
+		},
+		{
+			name:      "unsupported db version",
+			dbVersion: schemaver.New(6, 0, 0),
+			input: []*KnownExploitedVulnerabilityHandle{
+				{
+					Cve: "CVE-2023-1234",
+					BlobValue: &KnownExploitedVulnerabilityBlob{
+						Cve: "CVE-2023-1234",
+					},
+				},
+			},
+			expectEnabled: false,
+			expectError:   require.NoError,
+			expectedCount: 0,
+		},
+		{
+			name:      "future db version",
+			dbVersion: schemaver.New(6, 1, 0),
+			input: []*KnownExploitedVulnerabilityHandle{
+				{
+					Cve: "CVE-2023-1234",
+					BlobValue: &KnownExploitedVulnerabilityBlob{
+						Cve: "CVE-2023-1234",
+					},
+				},
+			},
+			expectEnabled: true,
+			expectError:   require.NoError,
+			expectedCount: 1,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if tt.expectError == nil {
+				tt.expectError = require.NoError
+			}
+
+			db := setupTestStore(t).db
+			bs := newBlobStore(db)
+
+			s := newVulnerabilityDecoratorStore(db, bs, tt.dbVersion)
+			assert.Equal(t, tt.expectEnabled, s.kevEnabled)
+
+			// this is just to get around not being able to write entries...
+			supportedStore := newVulnerabilityDecoratorStore(db, bs, schemaver.New(6, 0, 1))
+			err := supportedStore.AddKnownExploitedVulnerabilities(tt.input...)
+			require.NoError(t, err)
+
+			results, err := s.GetKnownExploitedVulnerabilities(tt.input[0].Cve)
+			tt.expectError(t, err)
+			assert.Len(t, results, tt.expectedCount)
+
+			if tt.expectedCount > 0 {
+				for _, result := range results {
+					assert.NotNil(t, result.BlobValue)
+					assert.Equal(t, tt.input[0].Cve, result.BlobValue.Cve)
+				}
+			}
+		})
+	}
+}
+
+func timeRef(t time.Time) *time.Time {
+	return &t
+}
diff --git a/grype/db/v6/vulnerability_provider_mocks_test.go b/grype/db/v6/vulnerability_provider_mocks_test.go
index d1b1b275..c436847e 100644
--- a/grype/db/v6/vulnerability_provider_mocks_test.go
+++ b/grype/db/v6/vulnerability_provider_mocks_test.go
@@ -249,5 +249,5 @@ func testVulnerabilityProvider(t *testing.T) vulnerability.Provider {
 		}
 	}
 
-	return NewVulnerabilityProvider(w)
+	return NewVulnerabilityProvider(setupReadOnlyTestStore(t, tmp))
 }
diff --git a/grype/matcher/java/matcher_mocks_test.go b/grype/matcher/java/matcher_mocks_test.go
index 85a8c88b..f9b703e0 100644
--- a/grype/matcher/java/matcher_mocks_test.go
+++ b/grype/matcher/java/matcher_mocks_test.go
@@ -2,6 +2,7 @@ package java
 
 import (
 	"context"
+	"errors"
 
 	"github.com/anchore/grype/grype/pkg"
 	"github.com/anchore/grype/grype/version"
@@ -32,15 +33,13 @@ func newMockProvider() vulnerability.Provider {
 }
 
 type mockMavenSearcher struct {
-	pkg pkg.Package
+	pkg                  pkg.Package
+	simulateRateLimiting bool
 }
 
 func (m mockMavenSearcher) GetMavenPackageBySha(context.Context, string) (*pkg.Package, error) {
-	return &m.pkg, nil
-}
-
-func newMockSearcher(pkg pkg.Package) MavenSearcher {
-	return mockMavenSearcher{
-		pkg,
+	if m.simulateRateLimiting {
+		return nil, errors.New("you been rate limited")
 	}
+	return &m.pkg, nil
 }
diff --git a/grype/matcher/java/matcher_test.go b/grype/matcher/java/matcher_test.go
index 3658c756..ad9531de 100644
--- a/grype/matcher/java/matcher_test.go
+++ b/grype/matcher/java/matcher_test.go
@@ -14,6 +14,18 @@ import (
 )
 
 func TestMatcherJava_matchUpstreamMavenPackage(t *testing.T) {
+	newMatcher := func(searcher MavenSearcher) *Matcher {
+		return &Matcher{
+			cfg: MatcherConfig{
+				ExternalSearchConfig: ExternalSearchConfig{
+					SearchMavenUpstream: true,
+				},
+			},
+			MavenSearcher: searcher,
+		}
+	}
+	store := newMockProvider()
+
 	p := pkg.Package{
 		ID:       pkg.ID(uuid.NewString()),
 		Name:     "org.springframework.spring-webmvc",
@@ -29,38 +41,42 @@ func TestMatcherJava_matchUpstreamMavenPackage(t *testing.T) {
 			},
 		},
 	}
-	matcher := Matcher{
-		cfg: MatcherConfig{
-			ExternalSearchConfig: ExternalSearchConfig{
-				SearchMavenUpstream: true,
-			},
-			UseCPEs: false,
-		},
-		MavenSearcher: newMockSearcher(p),
-	}
-	store := newMockProvider()
-	actual, _ := matcher.matchUpstreamMavenPackages(store, p)
 
-	assert.Len(t, actual, 2, "unexpected matches count")
+	t.Run("matching from maven search results", func(t *testing.T) {
+		matcher := newMatcher(mockMavenSearcher{
+			pkg: p,
+		})
+		actual, _ := matcher.matchUpstreamMavenPackages(store, p)
+
+		assert.Len(t, actual, 2, "unexpected matches count")
 
-	foundCVEs := stringutil.NewStringSet()
-	for _, v := range actual {
-		foundCVEs.Add(v.Vulnerability.ID)
+		foundCVEs := stringutil.NewStringSet()
+		for _, v := range actual {
+			foundCVEs.Add(v.Vulnerability.ID)
 
-		require.NotEmpty(t, v.Details)
-		for _, d := range v.Details {
-			assert.Equal(t, match.ExactIndirectMatch, d.Type, "indirect match not indicated")
-			assert.Equal(t, matcher.Type(), d.Matcher, "failed to capture matcher type")
+			require.NotEmpty(t, v.Details)
+			for _, d := range v.Details {
+				assert.Equal(t, match.ExactIndirectMatch, d.Type, "indirect match not indicated")
+				assert.Equal(t, matcher.Type(), d.Matcher, "failed to capture matcher type")
+			}
+			assert.Equal(t, p.Name, v.Package.Name, "failed to capture original package name")
 		}
-		assert.Equal(t, p.Name, v.Package.Name, "failed to capture original package name")
-	}
 
-	for _, id := range []string{"CVE-2014-fake-2", "CVE-2013-fake-3"} {
-		if !foundCVEs.Contains(id) {
-			t.Errorf("missing discovered CVE: %s", id)
+		for _, id := range []string{"CVE-2014-fake-2", "CVE-2013-fake-3"} {
+			if !foundCVEs.Contains(id) {
+				t.Errorf("missing discovered CVE: %s", id)
+			}
 		}
-	}
-	if t.Failed() {
-		t.Logf("discovered CVES: %+v", foundCVEs)
-	}
+		if t.Failed() {
+			t.Logf("discovered CVES: %+v", foundCVEs)
+		}
+	})
+
+	t.Run("handles maven rate limiting", func(t *testing.T) {
+		matcher := newMatcher(mockMavenSearcher{simulateRateLimiting: true})
+
+		_, err := matcher.matchUpstreamMavenPackages(store, p)
+
+		assert.Errorf(t, err, "should have gotten an error from the rate limiting")
+	})
 }
diff --git a/grype/pkg/cpe_provider_test.go b/grype/pkg/cpe_provider_test.go
new file mode 100644
index 00000000..bbc5b0bd
--- /dev/null
+++ b/grype/pkg/cpe_provider_test.go
@@ -0,0 +1,169 @@
+package pkg
+
+import (
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+	"github.com/google/go-cmp/cmp/cmpopts"
+	"github.com/stretchr/testify/require"
+
+	"github.com/anchore/syft/syft/cpe"
+	"github.com/anchore/syft/syft/file"
+	"github.com/anchore/syft/syft/pkg"
+	"github.com/anchore/syft/syft/sbom"
+	"github.com/anchore/syft/syft/source"
+)
+
+func Test_CPEProvider(t *testing.T) {
+	tests := []struct {
+		name      string
+		userInput string
+		context   Context
+		pkgs      []Package
+		sbom      *sbom.SBOM
+		wantErr   require.ErrorAssertionFunc
+	}{
+		{
+			name:      "takes a single cpe",
+			userInput: "cpe:/a:apache:log4j:2.14.1",
+			context: Context{
+				Source: &source.Description{
+					Metadata: CPELiteralMetadata{
+						CPE: "cpe:/a:apache:log4j:2.14.1",
+					},
+				},
+			},
+			pkgs: []Package{
+				{
+					Name:    "log4j",
+					Version: "2.14.1",
+					CPEs: []cpe.CPE{
+						cpe.Must("cpe:/a:apache:log4j:2.14.1", ""),
+					},
+				},
+			},
+			sbom: &sbom.SBOM{
+				Artifacts: sbom.Artifacts{
+					Packages: pkg.NewCollection(pkg.Package{
+						Name:    "log4j",
+						Version: "2.14.1",
+						CPEs: []cpe.CPE{
+							cpe.Must("cpe:/a:apache:log4j:2.14.1", ""),
+						},
+					}),
+				},
+			},
+		},
+		{
+			name:      "takes cpe with no version",
+			userInput: "cpe:/a:apache:log4j",
+			context: Context{
+				Source: &source.Description{
+					Metadata: CPELiteralMetadata{
+						CPE: "cpe:/a:apache:log4j",
+					},
+				},
+			},
+			pkgs: []Package{
+				{
+					Name: "log4j",
+					CPEs: []cpe.CPE{
+						cpe.Must("cpe:/a:apache:log4j", ""),
+					},
+				},
+			},
+			sbom: &sbom.SBOM{
+				Artifacts: sbom.Artifacts{
+					Packages: pkg.NewCollection(pkg.Package{
+						Name: "log4j",
+						CPEs: []cpe.CPE{
+							cpe.Must("cpe:/a:apache:log4j", ""),
+						},
+					}),
+				},
+			},
+		},
+		{
+			name:      "takes CPE 2.3 format",
+			userInput: "cpe:2.3:a:apache:log4j:2.14.1:*:*:*:*:*:*:*",
+			context: Context{
+				Source: &source.Description{
+					Metadata: CPELiteralMetadata{
+						CPE: "cpe:2.3:a:apache:log4j:2.14.1:*:*:*:*:*:*:*",
+					},
+				},
+			},
+			pkgs: []Package{
+				{
+					Name:    "log4j",
+					Version: "2.14.1",
+					CPEs: []cpe.CPE{
+						cpe.Must("cpe:2.3:a:apache:log4j:2.14.1:*:*:*:*:*:*:*", ""),
+					},
+				},
+			},
+			sbom: &sbom.SBOM{
+				Artifacts: sbom.Artifacts{
+					Packages: pkg.NewCollection(pkg.Package{
+						Name:    "log4j",
+						Version: "2.14.1",
+						CPEs: []cpe.CPE{
+							cpe.Must("cpe:2.3:a:apache:log4j:2.14.1:*:*:*:*:*:*:*", ""),
+						},
+					}),
+				},
+			},
+		},
+
+		{
+			name:      "invalid prefix",
+			userInput: "dir:test-fixtures/cpe",
+			wantErr:   require.Error,
+		},
+	}
+
+	opts := []cmp.Option{
+		cmpopts.IgnoreFields(Package{}, "ID", "Locations", "Licenses", "Metadata", "Type", "Language"),
+	}
+
+	syftPkgOpts := []cmp.Option{
+		cmpopts.IgnoreFields(pkg.Package{}, "id", "Type", "Language"),
+		cmpopts.IgnoreUnexported(pkg.Package{}, file.LocationSet{}, pkg.LicenseSet{}),
+	}
+
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			if tc.wantErr == nil {
+				tc.wantErr = require.NoError
+			}
+
+			packages, ctx, gotSBOM, err := cpeProvider(tc.userInput)
+
+			tc.wantErr(t, err)
+			if err != nil {
+				require.Nil(t, packages)
+				return
+			}
+
+			if d := cmp.Diff(tc.context, ctx, opts...); d != "" {
+				t.Errorf("unexpected context (-want +got):\n%s", d)
+			}
+
+			require.Len(t, packages, len(tc.pkgs))
+			for idx, expected := range tc.pkgs {
+				if d := cmp.Diff(expected, packages[idx], opts...); d != "" {
+					t.Errorf("unexpected package (-want +got):\n%s", d)
+				}
+			}
+
+			gotSyftPkgs := gotSBOM.Artifacts.Packages.Sorted()
+			wantSyftPkgs := tc.sbom.Artifacts.Packages.Sorted()
+			require.Equal(t, len(gotSyftPkgs), len(wantSyftPkgs))
+			for idx, wantPkg := range wantSyftPkgs {
+				if d := cmp.Diff(wantPkg, gotSyftPkgs[idx], syftPkgOpts...); d != "" {
+					t.Errorf("unexpected Syft Package (-want +got):\n%s", d)
+				}
+			}
+		})
+	}
+}
diff --git a/grype/pkg/package_test.go b/grype/pkg/package_test.go
index ec9a2b85..0c02886b 100644
--- a/grype/pkg/package_test.go
+++ b/grype/pkg/package_test.go
@@ -742,6 +742,15 @@ func TestNew(t *testing.T) {
 				},
 			},
 		},
+		{
+			name: "bitnami-sbom-entry",
+			syftPkg: syftPkg.Package{
+				Metadata: syftPkg.BitnamiSBOMEntry{
+					Name:    "a",
+					Version: "1",
+				},
+			},
+		},
 		{
 			name: "terraform-lock-provider-entry",
 			syftPkg: syftPkg.Package{
diff --git a/grype/presenter/cyclonedx/presenter_test.go b/grype/presenter/cyclonedx/presenter_test.go
index 9d29c471..5d5d5048 100644
--- a/grype/presenter/cyclonedx/presenter_test.go
+++ b/grype/presenter/cyclonedx/presenter_test.go
@@ -11,10 +11,35 @@ import (
 	"github.com/anchore/go-testutils"
 	"github.com/anchore/grype/grype/presenter/internal"
 	"github.com/anchore/grype/grype/presenter/models"
+	"github.com/anchore/syft/syft/file"
+	"github.com/anchore/syft/syft/sbom"
 )
 
 var update = flag.Bool("update", false, "update the *.golden files for cyclonedx presenters")
 
+func Test_noTypedNils(t *testing.T) {
+	s := sbom.SBOM{
+		Artifacts: sbom.Artifacts{
+			FileMetadata: map[file.Coordinates]file.Metadata{},
+			FileDigests:  map[file.Coordinates][]file.Digest{},
+		},
+	}
+	c := file.NewCoordinates("/file", "123")
+	s.Artifacts.FileMetadata[c] = file.Metadata{
+		Path: "/file",
+	}
+	s.Artifacts.FileDigests[c] = []file.Digest{}
+
+	p := NewJSONPresenter(models.PresenterConfig{
+		SBOM:   &s,
+		Pretty: false,
+	})
+	contents := bytes.Buffer{}
+	err := p.Present(&contents)
+	require.NoError(t, err)
+	require.NotContains(t, contents.String(), "null")
+}
+
 func TestCycloneDxPresenterImage(t *testing.T) {
 	var buffer bytes.Buffer
 
diff --git a/grype/presenter/cyclonedx/test-fixtures/snapshot/TestCycloneDxPresenterDir.golden b/grype/presenter/cyclonedx/test-fixtures/snapshot/TestCycloneDxPresenterDir.golden
index 2ee39dd2..781fe895 100644
--- a/grype/presenter/cyclonedx/test-fixtures/snapshot/TestCycloneDxPresenterDir.golden
+++ b/grype/presenter/cyclonedx/test-fixtures/snapshot/TestCycloneDxPresenterDir.golden
@@ -2,10 +2,10 @@
   "$schema": "http://cyclonedx.org/schema/bom-1.6.schema.json",
   "bomFormat": "CycloneDX",
   "specVersion": "1.6",
-  "serialNumber": "urn:uuid:7fd309b5-5ffd-4d2f-9b24-b040c0bb5af7",
+  "serialNumber": "urn:uuid:e158c45e-708d-4623-9d6f-19ee89420452",
   "version": 1,
   "metadata": {
-    "timestamp": "2024-12-04T15:30:43+02:00",
+    "timestamp": "2025-02-25T15:06:07-05:00",
     "tools": {
       "components": [
         {
@@ -84,7 +84,7 @@
   ],
   "vulnerabilities": [
     {
-      "bom-ref": "urn:uuid:a4b38bae-8a6e-4dd2-bdea-53e9cf590c12",
+      "bom-ref": "urn:uuid:61d949ac-5674-4fe0-bc76-1486435bba04",
       "id": "CVE-1999-0001",
       "source": {},
       "references": [
@@ -102,7 +102,6 @@
         }
       ],
       "description": "1999-01 description",
-      "advisories": [],
       "affects": [
         {
           "ref": "9baa2db122fea516"
@@ -110,7 +109,7 @@
       ]
     },
     {
-      "bom-ref": "urn:uuid:829dc942-6d41-4d7d-ad5d-41c9b6b12b62",
+      "bom-ref": "urn:uuid:38dd2b1e-5761-48c1-a5e7-e7fe90d57318",
       "id": "CVE-1999-0002",
       "source": {},
       "references": [
@@ -128,7 +127,6 @@
         }
       ],
       "description": "1999-02 description",
-      "advisories": [],
       "affects": [
         {
           "ref": "pkg:deb/package-2@2.2.2?package-id=7bb53d560434bc7f"
diff --git a/grype/presenter/cyclonedx/test-fixtures/snapshot/TestCycloneDxPresenterImage.golden b/grype/presenter/cyclonedx/test-fixtures/snapshot/TestCycloneDxPresenterImage.golden
index 36105bff..0f35b1ae 100644
--- a/grype/presenter/cyclonedx/test-fixtures/snapshot/TestCycloneDxPresenterImage.golden
+++ b/grype/presenter/cyclonedx/test-fixtures/snapshot/TestCycloneDxPresenterImage.golden
@@ -2,10 +2,10 @@
   "$schema": "http://cyclonedx.org/schema/bom-1.6.schema.json",
   "bomFormat": "CycloneDX",
   "specVersion": "1.6",
-  "serialNumber": "urn:uuid:596cd775-e2a5-4932-a807-d1abfe4d804c",
+  "serialNumber": "urn:uuid:ca9bfd93-9fb8-4f58-8f84-ff764935da1f",
   "version": 1,
   "metadata": {
-    "timestamp": "2024-12-04T15:30:43+02:00",
+    "timestamp": "2025-02-25T15:06:07-05:00",
     "tools": {
       "components": [
         {
@@ -84,7 +84,7 @@
   ],
   "vulnerabilities": [
     {
-      "bom-ref": "urn:uuid:2fe19024-858a-4864-9a9b-b52f105e992c",
+      "bom-ref": "urn:uuid:ba32c3c8-31fd-42fd-9daa-1c082405210b",
       "id": "CVE-1999-0001",
       "source": {},
       "references": [
@@ -102,7 +102,6 @@
         }
       ],
       "description": "1999-01 description",
-      "advisories": [],
       "affects": [
         {
           "ref": "9baa2db122fea516"
@@ -110,7 +109,7 @@
       ]
     },
     {
-      "bom-ref": "urn:uuid:1082956b-2ee6-48e4-9d92-183d492a3be8",
+      "bom-ref": "urn:uuid:67745d83-3d79-43bd-b230-2bad4f072af9",
       "id": "CVE-1999-0002",
       "source": {},
       "references": [
@@ -128,7 +127,6 @@
         }
       ],
       "description": "1999-02 description",
-      "advisories": [],
       "affects": [
         {
           "ref": "pkg:deb/package-2@2.2.2?package-id=7bb53d560434bc7f"
diff --git a/grype/presenter/models/source_test.go b/grype/presenter/models/source_test.go
index 2e389345..325b772a 100644
--- a/grype/presenter/models/source_test.go
+++ b/grype/presenter/models/source_test.go
@@ -6,6 +6,7 @@ import (
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 
+	"github.com/anchore/grype/grype/pkg"
 	syftSource "github.com/anchore/syft/syft/source"
 )
 
@@ -61,6 +62,52 @@ func TestNewSource(t *testing.T) {
 				Target: "/foo/bar/test.zip",
 			},
 		},
+		{
+			name: "purl-file",
+			metadata: syftSource.Description{
+				Metadata: pkg.PURLFileMetadata{
+					Path: "/path/to/purls.txt",
+				},
+			},
+			expected: source{
+				Type:   "purl-file",
+				Target: "/path/to/purls.txt",
+			},
+		},
+		{
+			name: "purl-literal",
+			metadata: syftSource.Description{
+				Metadata: pkg.PURLLiteralMetadata{
+					PURL: "pkg:maven/org.apache.logging.log4j/log4j-core@2.14.1",
+				},
+			},
+			expected: source{
+				Type:   "purl",
+				Target: "pkg:maven/org.apache.logging.log4j/log4j-core@2.14.1",
+			},
+		},
+		{
+			name: "cpe-literal",
+			metadata: syftSource.Description{
+				Metadata: pkg.CPELiteralMetadata{
+					CPE: "cpe:/a:apache:log4j:2.14.1",
+				},
+			},
+			expected: source{
+				Type:   "cpe",
+				Target: "cpe:/a:apache:log4j:2.14.1",
+			},
+		},
+		{
+			name: "nil metadata",
+			metadata: syftSource.Description{
+				Metadata: nil,
+			},
+			expected: source{
+				Type:   "unknown",
+				Target: "unknown",
+			},
+		},
 	}
 
 	for _, testCase := range testCases {
diff --git a/grype/version/golang_version_test.go b/grype/version/golang_version_test.go
index 283c1312..deab9ae7 100644
--- a/grype/version/golang_version_test.go
+++ b/grype/version/golang_version_test.go
@@ -40,6 +40,14 @@ func TestNewGolangVersion(t *testing.T) {
 				semVer: hashiVer.Must(hashiVer.NewSemver("v24.0.7+incompatible")),
 			},
 		},
+		{
+			name:  "semver with +incompatible+dirty",
+			input: "v24.0.7+incompatible+dirty",
+			expected: golangVersion{
+				raw:    "v24.0.7+incompatible+dirty",
+				semVer: hashiVer.Must(hashiVer.NewSemver("v24.0.7+incompatible.dirty")),
+			},
+		},
 		{
 			name:  "standard library",
 			input: "go1.21.4",
@@ -71,6 +79,7 @@ func TestNewGolangVersion(t *testing.T) {
 				require.Error(t, err)
 				return
 			}
+			assert.Nil(t, err)
 			assert.Equal(t, tc.expected, *v)
 		})
 	}
diff --git a/internal/schemaver/schema_ver_test.go b/internal/schemaver/schema_ver_test.go
index e4d591cf..04d5bf84 100644
--- a/internal/schemaver/schema_ver_test.go
+++ b/internal/schemaver/schema_ver_test.go
@@ -1,95 +1,245 @@
 package schemaver
 
 import (
-	"fmt"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
-	"github.com/stretchr/testify/require"
 )
 
-func TestSchemaVer_VersionComponents(t *testing.T) {
+func TestSchemaVerComparisons(t *testing.T) {
 	tests := []struct {
-		name             string
-		version          SchemaVer
-		expectedModel    int
-		expectedRevision int
-		expectedAddition int
+		name           string
+		v1             SchemaVer
+		v2             SchemaVer
+		lessThan       bool
+		greaterOrEqual bool
 	}{
 		{
-			name:             "go case",
-			version:          "1.2.3",
-			expectedModel:    1,
-			expectedRevision: 2,
-			expectedAddition: 3,
+			name:           "equal versions",
+			v1:             New(1, 0, 0),
+			v2:             New(1, 0, 0),
+			lessThan:       false,
+			greaterOrEqual: true,
 		},
 		{
-			name:             "model only",
-			version:          "1.0.0",
-			expectedModel:    1,
-			expectedRevision: 0,
-			expectedAddition: 0,
+			name:           "different model versions",
+			v1:             New(1, 0, 0),
+			v2:             New(2, 0, 0),
+			lessThan:       true,
+			greaterOrEqual: false,
 		},
 		{
-			name:             "invalid model",
-			version:          "0.2.3",
-			expectedModel:    -1,
-			expectedRevision: 2,
-			expectedAddition: 3,
+			name:           "different revision versions",
+			v1:             New(1, 1, 0),
+			v2:             New(1, 2, 0),
+			lessThan:       true,
+			greaterOrEqual: false,
 		},
 		{
-			name:             "invalid version format",
-			version:          "invalid.version",
-			expectedModel:    -1,
-			expectedRevision: -1,
-			expectedAddition: -1,
+			name:           "different addition versions",
+			v1:             New(1, 0, 1),
+			v2:             New(1, 0, 2),
+			lessThan:       true,
+			greaterOrEqual: false,
 		},
 		{
-			name:             "zero version",
-			version:          "0.0.0",
-			expectedModel:    -1,
-			expectedRevision: 0,
-			expectedAddition: 0,
+			name:           "inverted addition versions",
+			v1:             New(1, 0, 2),
+			v2:             New(1, 0, 1),
+			lessThan:       false,
+			greaterOrEqual: true,
+		},
+		{
+			name:           "greater model overrides lower revision",
+			v1:             New(2, 0, 0),
+			v2:             New(1, 9, 9),
+			lessThan:       false,
+			greaterOrEqual: true,
+		},
+		{
+			name:           "greater revision overrides lower addition",
+			v1:             New(1, 2, 0),
+			v2:             New(1, 1, 9),
+			lessThan:       false,
+			greaterOrEqual: true,
 		},
 	}
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			type subject struct {
-				name string
-				exp  int
-				fn   func() (int, bool)
+			if got := tt.v1.LessThan(tt.v2); got != tt.lessThan {
+				t.Errorf("LessThan() = %v, want %v", got, tt.lessThan)
 			}
+			if got := tt.v1.GreaterOrEqualTo(tt.v2); got != tt.greaterOrEqual {
+				t.Errorf("GreaterOrEqualTo() = %v, want %v", got, tt.greaterOrEqual)
+			}
+		})
+	}
+}
 
-			for _, sub := range []subject{
-				{
-					name: "model",
-					exp:  tt.expectedModel,
-					fn:   tt.version.ModelPart,
-				},
-				{
-					name: "revision",
-					exp:  tt.expectedRevision,
-					fn:   tt.version.RevisionPart,
-				},
-				{
-					name: "addition",
-					exp:  tt.expectedAddition,
-					fn:   tt.version.AdditionPart,
-				},
-			} {
-				t.Run(sub.name, func(t *testing.T) {
-					act, ok := sub.fn()
+func TestParse(t *testing.T) {
+	tests := []struct {
+		name    string
+		input   string
+		want    SchemaVer
+		wantErr bool
+	}{
+		{
+			name:    "valid version",
+			input:   "1.2.3",
+			want:    New(1, 2, 3),
+			wantErr: false,
+		},
+		{
+			name:    "valid large numbers",
+			input:   "999.888.777",
+			want:    New(999, 888, 777),
+			wantErr: false,
+		},
+		{
+			name:    "valid with whitespace",
+			input:   "  1.2.3  ",
+			want:    New(1, 2, 3),
+			wantErr: false,
+		},
+		{
+			name:    "invalid version with zeros",
+			input:   "0.0.0",
+			want:    New(0, 0, 0),
+			wantErr: true,
+		},
+		{
+			name:    "invalid empty string",
+			input:   "",
+			wantErr: true,
+		},
+		{
+			name:    "invalid too few parts",
+			input:   "1.2",
+			wantErr: true,
+		},
+		{
+			name:    "invalid too many parts",
+			input:   "1.2.3.4",
+			wantErr: true,
+		},
+		{
+			name:    "invalid non-numeric model",
+			input:   "a.2.3",
+			wantErr: true,
+		},
+		{
+			name:    "invalid non-numeric revision",
+			input:   "1.b.3",
+			wantErr: true,
+		},
+		{
+			name:    "invalid non-numeric addition",
+			input:   "1.2.c",
+			wantErr: true,
+		},
+		{
+			name:    "invalid negative number",
+			input:   "-1.2.3",
+			wantErr: true,
+		},
+		{
+			name:    "invalid format with spaces",
+			input:   "1 . 2 . 3",
+			wantErr: true,
+		},
+	}
 
-					if sub.exp == -1 {
-						require.False(t, ok, fmt.Sprintf("Expected %s to be invalid", sub.name))
-						return
-					}
-					require.True(t, ok, fmt.Sprintf("Expected %s to be valid", sub.name))
-					assert.Equal(t, sub.exp, act, fmt.Sprintf("Expected %s to be %d, got %d", sub.name, sub.exp, act))
-				})
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := Parse(tt.input)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr)
+				return
 			}
+			if !tt.wantErr && (got.Model != tt.want.Model ||
+				got.Revision != tt.want.Revision ||
+				got.Addition != tt.want.Addition) {
+				t.Errorf("Parse() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestSchemaVer_Valid(t *testing.T) {
+	tests := []struct {
+		name     string
+		schema   SchemaVer
+		expected bool
+	}{
+		{
+			name: "valid schema version - all positive",
+			schema: SchemaVer{
+				Model:    1,
+				Revision: 1,
+				Addition: 1,
+			},
+			expected: true,
+		},
+		{
+			name: "valid schema version - zero revision and addition",
+			schema: SchemaVer{
+				Model:    1,
+				Revision: 0,
+				Addition: 0,
+			},
+			expected: true,
+		},
+		{
+			name: "invalid - zero model",
+			schema: SchemaVer{
+				Model:    0,
+				Revision: 1,
+				Addition: 1,
+			},
+			expected: false,
+		},
+		{
+			name: "invalid - negative model",
+			schema: SchemaVer{
+				Model:    -1,
+				Revision: 1,
+				Addition: 1,
+			},
+			expected: false,
+		},
+		{
+			name: "invalid - negative revision",
+			schema: SchemaVer{
+				Model:    1,
+				Revision: -1,
+				Addition: 1,
+			},
+			expected: false,
+		},
+		{
+			name: "invalid - negative addition",
+			schema: SchemaVer{
+				Model:    1,
+				Revision: 1,
+				Addition: -1,
+			},
+			expected: false,
+		},
+		{
+			name: "invalid - all negative",
+			schema: SchemaVer{
+				Model:    -1,
+				Revision: -1,
+				Addition: -1,
+			},
+			expected: false,
+		},
+	}
 
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert.Equal(t, tt.expected, tt.schema.Valid())
 		})
 	}
 }
diff --git a/test/integration/compare_sbom_input_vs_lib_test.go b/test/integration/compare_sbom_input_vs_lib_test.go
index 5d25dd97..20a0ca5e 100644
--- a/test/integration/compare_sbom_input_vs_lib_test.go
+++ b/test/integration/compare_sbom_input_vs_lib_test.go
@@ -51,6 +51,7 @@ func TestCompareSBOMInputToLibResults(t *testing.T) {
 	// exceptions: rust, php, dart, msrc (kb), etc. are not under test
 	definedPkgTypes.Remove(
 		string(syftPkg.BinaryPkg), // these are removed due to overlap-by-file-ownership
+		string(syftPkg.BitnamiPkg),
 		string(syftPkg.PhpPeclPkg),
 		string(syftPkg.RustPkg),
 		string(syftPkg.KbPkg),
@@ -72,6 +73,7 @@ func TestCompareSBOMInputToLibResults(t *testing.T) {
 		string(syftPkg.SwiftPkg),
 		string(syftPkg.GithubActionPkg),
 		string(syftPkg.GithubActionWorkflowPkg),
+		string(syftPkg.GraalVMNativeImagePkg),
 		string(syftPkg.ErlangOTPPkg),
 		string(syftPkg.WordpressPluginPkg), // TODO: remove me when there is a matcher for this merged in https://github.com/anchore/grype/pull/1553
 		string(syftPkg.LuaRocksPkg),