Repository: google/osv-scanner
Pull request #462 (2023-08-01 21:47:23)

fix: use correct plural and singular forms based on count


Patch:
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 4a5fde9..107b12b 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -44,7 +44,7 @@ jobs:
 
       # Initializes the CodeQL tools for scanning.
       - name: Initialize CodeQL
-        uses: github/codeql-action/init@1813ca74c3faaa3a2da2070b9b8a0b3e7373a0d8 # v2.21.0
+        uses: github/codeql-action/init@0ba4244466797eb048eb91a6cd43d5c03ca8bd05 # v2.21.2
         with:
           languages: ${{ matrix.language }}
           # If you wish to specify custom queries, you can do so here or in a config file.
@@ -55,7 +55,7 @@ jobs:
       # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).
       # If this step fails, then you should remove it and run the build manually (see below)
       - name: Autobuild
-        uses: github/codeql-action/autobuild@1813ca74c3faaa3a2da2070b9b8a0b3e7373a0d8 # v2.21.0
+        uses: github/codeql-action/autobuild@0ba4244466797eb048eb91a6cd43d5c03ca8bd05 # v2.21.2
 
       # ℹ️ Command-line programs to run using the OS shell.
       # 📚 https://git.io/JvXDl
@@ -69,4 +69,4 @@ jobs:
       #   make release
 
       - name: Perform CodeQL Analysis
-        uses: github/codeql-action/analyze@1813ca74c3faaa3a2da2070b9b8a0b3e7373a0d8 # v2.21.0
+        uses: github/codeql-action/analyze@0ba4244466797eb048eb91a6cd43d5c03ca8bd05 # v2.21.2
diff --git a/.github/workflows/osv-scanner-reusable-pr.yml b/.github/workflows/osv-scanner-reusable-pr.yml
index 9bf1ce6..ab1f7c8 100644
--- a/.github/workflows/osv-scanner-reusable-pr.yml
+++ b/.github/workflows/osv-scanner-reusable-pr.yml
@@ -43,13 +43,13 @@ jobs:
           results-file: new-results.json
           to-scan: .
         continue-on-error: true
-      - name: "Run osv-diff"
-        uses: google/osv-scanner/actions/diff@main
+      - name: "Run osv-scanner-reporter"
+        uses: another-rex/osv-scanner/actions/reporter@fix-github-action
         with:
-          results-format: sarif
-          output-file: final-results.sarif
+          output-sarif-file: final-results.sarif
           old-results: old-results.json
           new-results: new-results.json
+          gh-annotations: true
       # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
       # format to the repository Actions tab.
       - name: "Upload artifact"
@@ -76,7 +76,7 @@ jobs:
       # Upload the results to GitHub's code scanning dashboard.
       - name: "Upload to code-scanning"
         if: '!cancelled()'
-        uses: github/codeql-action/upload-sarif@6c089f53dd51dc3fc7e599c3cb5356453a52ca9e # v2.20.0
+        uses: github/codeql-action/upload-sarif@0ba4244466797eb048eb91a6cd43d5c03ca8bd05 # v2.21.2
         with:
           sarif_file: final-results.sarif
 
diff --git a/.github/workflows/osv-scanner-reusable-scheduled.yml b/.github/workflows/osv-scanner-reusable-scheduled.yml
index cc3f247..21c0232 100644
--- a/.github/workflows/osv-scanner-reusable-scheduled.yml
+++ b/.github/workflows/osv-scanner-reusable-scheduled.yml
@@ -12,7 +12,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-name: OSV-Scanner PR scanning
+name: OSV-Scanner Scheduled scanning
 
 on:
   workflow_call:
@@ -41,7 +41,7 @@ jobs:
       # Upload the results to GitHub's code scanning dashboard.
       - name: "Upload to code-scanning"
         if: '!cancelled()'
-        uses: github/codeql-action/upload-sarif@6c089f53dd51dc3fc7e599c3cb5356453a52ca9e # v2.20.0
+        uses: github/codeql-action/upload-sarif@0ba4244466797eb048eb91a6cd43d5c03ca8bd05 # v2.21.2
         with:
           sarif_file: results.sarif
 
diff --git a/.github/workflows/osv-scanner-scheduled.yml b/.github/workflows/osv-scanner-scheduled.yml
index 60e77d5..fcd6827 100644
--- a/.github/workflows/osv-scanner-scheduled.yml
+++ b/.github/workflows/osv-scanner-scheduled.yml
@@ -21,7 +21,9 @@ on:
     branches: [ "main" ]
 
 # Declare default permissions as read only.
-permissions: read-all
+permissions: 
+  security-events: write
+  contents: read
 
 jobs:
   scan-pr-attempt:
diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml
index 1673667..e81cc02 100644
--- a/.github/workflows/scorecards.yml
+++ b/.github/workflows/scorecards.yml
@@ -67,6 +67,6 @@ jobs:
 
       # Upload the results to GitHub's code scanning dashboard.
       - name: "Upload to code-scanning"
-        uses: github/codeql-action/upload-sarif@1813ca74c3faaa3a2da2070b9b8a0b3e7373a0d8 # v2.21.0
+        uses: github/codeql-action/upload-sarif@0ba4244466797eb048eb91a6cd43d5c03ca8bd05 # v2.21.2
         with:
           sarif_file: results.sarif
diff --git a/actions/diff/action.yml b/actions/reporter/action.yml
similarity index 50%
rename from actions/diff/action.yml
rename to actions/reporter/action.yml
index 563a446..e4bb3e2 100644
--- a/actions/diff/action.yml
+++ b/actions/reporter/action.yml
@@ -1,16 +1,17 @@
 # Currently experimental.
-name: 'osv-scanner-diff'
-description: 'Finds the difference between two osv-scanner json results'
+name: 'osv-scanner-reporter'
+description: 'Specialized reporting of scanner results for github actions'
 inputs:
-  output-file:
-    description: 'Output path'
+  output-sarif-file:
+    description: 'Output SARIF file path'
     required: true
-  results-format:
-    description: 'Output result format'
-    default: 'sarif'
+  gh-annotations:
+    description: 'Whether to print github annotations'
+    default: false
   old-results:
     description: 'Old results to get the difference against'
-    required: true
+    required: false
+    default: ""
   new-results:
     description: 'New results to get the difference against'
     required: true
@@ -19,7 +20,7 @@ runs:
   image: '../../action.dockerfile'
   entrypoint: /root/osv-diff
   args:
-    - '--output=${{ inputs.output-file }}'
-    - '--format=${{ inputs.results-format }}'
+    - '--output=${{ inputs.output-sarif-file }}'
     - '--old=${{ inputs.old-results }}'
     - '--new=${{ inputs.new-results }}'
+    - '--gh-annotations=${{ inputs.gh-annotations }}'
diff --git a/cmd/osv-diff/main.go b/cmd/osv-diff/main.go
index 7d55135..a74ad2f 100644
--- a/cmd/osv-diff/main.go
+++ b/cmd/osv-diff/main.go
@@ -5,13 +5,12 @@ import (
 	"fmt"
 	"io"
 	"os"
-	"strings"
 
 	"github.com/google/osv-scanner/internal/ci"
+	"github.com/google/osv-scanner/pkg/models"
 	"github.com/google/osv-scanner/pkg/osvscanner"
 	"github.com/google/osv-scanner/pkg/reporter"
 	"github.com/urfave/cli/v2"
-	"golang.org/x/exp/slices"
 	"golang.org/x/term"
 )
 
@@ -23,28 +22,29 @@ var (
 )
 
 func run(args []string, stdout, stderr io.Writer) int {
-	var r reporter.Reporter
+	var tableReporter reporter.Reporter
 
 	cli.VersionPrinter = func(ctx *cli.Context) {
 		// Use the app Writer and ErrWriter since they will be the writers to keep parallel tests consistent
-		r = reporter.NewTableReporter(ctx.App.Writer, ctx.App.ErrWriter, false, 0)
-		r.PrintText(fmt.Sprintf("osv-scanner version: %s\ncommit: %s\nbuilt at: %s\n", ctx.App.Version, commit, date))
+		tableReporter = reporter.NewTableReporter(ctx.App.Writer, ctx.App.ErrWriter, false, 0)
+		tableReporter.PrintText(fmt.Sprintf("osv-scanner version: %s\ncommit: %s\nbuilt at: %s\n", ctx.App.Version, commit, date))
 	}
 
 	app := &cli.App{
-		Name:        "osv-scanner-diff",
+		Name:        "osv-scanner-action-reporter",
 		Version:     version,
-		Usage:       "compares the output of multiple osv-scanner runs to find new vulnerabilities",
-		Description: "Remove vulnerabilities in the old OSV JSON output from the new OSV JSON output",
+		Usage:       "(Experimental) generates github action output",
+		Description: "(Experimental) Used specifically to generate github action output ",
 		Suggest:     true,
 		Writer:      stdout,
 		ErrWriter:   stderr,
 		Flags: []cli.Flag{
 			&cli.StringFlag{
-				Name:      "old",
-				Usage:     "the old osv json output",
-				TakesFile: true,
-				Required:  true,
+				Name:        "old",
+				Usage:       "the old osv json output",
+				TakesFile:   true,
+				Required:    false,
+				DefaultText: "",
 			},
 			&cli.StringFlag{
 				Name:      "new",
@@ -52,59 +52,39 @@ func run(args []string, stdout, stderr io.Writer) int {
 				TakesFile: true,
 				Required:  true,
 			},
-			&cli.StringFlag{
-				Name:    "format",
-				Aliases: []string{"f"},
-				Usage:   "sets the output format",
-				Value:   "table",
-				Action: func(context *cli.Context, s string) error {
-					if slices.Contains(reporter.Format(), s) {
-						return nil
-					}
-
-					return fmt.Errorf("unsupported output format \"%s\" - must be one of: %s", s, strings.Join(reporter.Format(), ", "))
-				},
-			},
 			&cli.StringFlag{
 				Name:      "output",
-				Usage:     "saves the result to the given file path",
+				Usage:     "saves the SARIF result to the given file path",
 				TakesFile: true,
 			},
+			&cli.BoolFlag{
+				Name:  "gh-annotations",
+				Usage: "prints github action annotations",
+			},
 		},
 		Action: func(context *cli.Context) error {
-			format := context.String("format")
-
-			outputPath := context.String("output")
-			if outputPath != "" {
-				var err error
-				stdout, err = os.Create(outputPath)
-				if err != nil {
-					return fmt.Errorf("failed to create output file: %w", err)
-				}
-			}
-
 			var termWidth int
-			if outputPath != "" {
-				var err error
-				termWidth, _, err = term.GetSize(int(os.Stdout.Fd()))
+			var err error
+			if stdoutAsFile, ok := stdout.(*os.File); ok {
+				termWidth, _, err = term.GetSize(int(stdoutAsFile.Fd()))
 				if err != nil { // If output is not a terminal,
 					termWidth = 0
 				}
-			} else { // Output is a file
-				termWidth = 0
 			}
 
-			var err error
-			if r, err = reporter.New(format, stdout, stderr, termWidth); err != nil {
+			if tableReporter, err = reporter.New("table", stdout, stderr, termWidth); err != nil {
 				return err
 			}
 
 			oldPath := context.String("old")
 			newPath := context.String("new")
 
-			oldVulns, err := ci.LoadVulnResults(oldPath)
-			if err != nil {
-				return fmt.Errorf("failed to open old results at %s: %w", oldPath, err)
+			oldVulns := models.VulnerabilityResults{}
+			if oldPath != "" {
+				oldVulns, err = ci.LoadVulnResults(oldPath)
+				if err != nil {
+					return fmt.Errorf("failed to open old results at %s: %w", oldPath, err)
+				}
 			}
 
 			newVulns, err := ci.LoadVulnResults(newPath)
@@ -114,10 +94,39 @@ func run(args []string, stdout, stderr io.Writer) int {
 
 			diffVulns := ci.DiffVulnerabilityResults(oldVulns, newVulns)
 
-			if errPrint := r.PrintResult(&diffVulns); errPrint != nil {
+			if errPrint := tableReporter.PrintResult(&diffVulns); errPrint != nil {
 				return fmt.Errorf("failed to write output: %w", errPrint)
 			}
 
+			if context.Bool("gh-annotations") {
+				var ghAnnotationsReporter reporter.Reporter
+				if ghAnnotationsReporter, err = reporter.New("gh-annotations", stdout, stderr, termWidth); err != nil {
+					return err
+				}
+
+				if errPrint := ghAnnotationsReporter.PrintResult(&diffVulns); errPrint != nil {
+					return fmt.Errorf("failed to write output: %w", errPrint)
+				}
+			}
+
+			outputPath := context.String("output")
+			if outputPath != "" {
+				var err error
+				stdout, err = os.Create(outputPath)
+				if err != nil {
+					return fmt.Errorf("failed to create output file: %w", err)
+				}
+				termWidth = 0
+				var sarifReporter reporter.Reporter
+				if sarifReporter, err = reporter.New("sarif", stdout, stderr, termWidth); err != nil {
+					return err
+				}
+
+				if errPrint := sarifReporter.PrintResult(&diffVulns); errPrint != nil {
+					return fmt.Errorf("failed to write output: %w", errPrint)
+				}
+			}
+
 			// if vulnerability exists it should return error
 			if len(diffVulns.Results) > 0 {
 				// If any vulnerabilities are called, then we return VulnerabilitiesFoundErr
@@ -135,8 +144,8 @@ func run(args []string, stdout, stderr io.Writer) int {
 	}
 
 	if err := app.Run(args); err != nil {
-		if r == nil {
-			r = reporter.NewTableReporter(stdout, stderr, false, 0)
+		if tableReporter == nil {
+			tableReporter = reporter.NewTableReporter(stdout, stderr, false, 0)
 		}
 		if errors.Is(err, osvscanner.VulnerabilitiesFoundErr) {
 			return 1
@@ -148,16 +157,16 @@ func run(args []string, stdout, stderr io.Writer) int {
 		}
 
 		if errors.Is(err, osvscanner.NoPackagesFoundErr) {
-			r.PrintError("No package sources found, --help for usage information.\n")
+			tableReporter.PrintError("No package sources found, --help for usage information.\n")
 			return 128
 		}
 
-		r.PrintError(fmt.Sprintf("%v\n", err))
+		tableReporter.PrintError(fmt.Sprintf("%v\n", err))
 	}
 
 	// if we've been told to print an error, and not already exited with
 	// a specific error code, then exit with a generic non-zero code
-	if r != nil && r.HasPrintedError() {
+	if tableReporter != nil && tableReporter.HasPrintedError() {
 		return 127
 	}
 
diff --git a/cmd/osv-scanner/fixtures/sbom-insecure/alpine.cdx.xml b/cmd/osv-scanner/fixtures/sbom-insecure/alpine.cdx.xml
new file mode 100644
index 0000000..f70df0c
--- /dev/null
+++ b/cmd/osv-scanner/fixtures/sbom-insecure/alpine.cdx.xml
@@ -0,0 +1,604 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<bom xmlns="http://cyclonedx.org/schema/bom/1.4" serialNumber="urn:uuid:4243b783-229c-48e7-982b-febe2bb5bc2b" version="1">
+  <metadata>
+    <timestamp>2023-03-02T12:04:22+11:00</timestamp>
+    <tools>
+      <tool>
+        <vendor>anchore</vendor>
+        <name>syft</name>
+        <version>0.73.0</version>
+      </tool>
+    </tools>
+    <component bom-ref="5339058ca5e06f8a" type="container">
+      <name>alpine:latest</name>
+      <version>sha256:fd6275a37d2472b9d3be70c3261087b8d65e441c21342ae7313096312bcda2b3</version>
+    </component>
+  </metadata>
+  <components>
+    <component bom-ref="pkg:apk/alpine/alpine-baselayout@3.4.0-r0?arch=x86_64&amp;upstream=alpine-baselayout&amp;distro=alpine-3.17.2&amp;package-id=92b19c7750fb559d" type="library">
+      <publisher>Natanael Copa &lt;ncopa@alpinelinux.org&gt;</publisher>
+      <name>alpine-baselayout</name>
+      <version>3.4.0-r0</version>
+      <description>Alpine base dir structure and init scripts</description>
+      <licenses>
+        <license>
+          <id>GPL-2.0-only</id>
+        </license>
+      </licenses>
+      <cpe>cpe:2.3:a:alpine-baselayout:alpine-baselayout:3.4.0-r0:*:*:*:*:*:*:*</cpe>
+      <purl>pkg:apk/alpine/alpine-baselayout@3.4.0-r0?arch=x86_64&amp;upstream=alpine-baselayout&amp;distro=alpine-3.17.2</purl>
+      <externalReferences>
+        <reference type="distribution">
+          <url>https://git.alpinelinux.org/cgit/aports/tree/main/alpine-baselayout</url>
+        </reference>
+      </externalReferences>
+      <properties>
+        <property name="syft:package:foundBy">apkdb-cataloger</property>
+        <property name="syft:package:metadataType">ApkMetadata</property>
+        <property name="syft:package:type">apk</property>
+        <property name="syft:cpe23">cpe:2.3:a:alpine-baselayout:alpine_baselayout:3.4.0-r0:*:*:*:*:*:*:*</property>
+        <property name="syft:cpe23">cpe:2.3:a:alpine_baselayout:alpine-baselayout:3.4.0-r0:*:*:*:*:*:*:*</property>
+        <property name="syft:cpe23">cpe:2.3:a:alpine_baselayout:alpine_baselayout:3.4.0-r0:*:*:*:*:*:*:*</property>
+        <property name="syft:cpe23">cpe:2.3:a:alpine:alpine-baselayout:3.4.0-r0:*:*:*:*:*:*:*</property>
+        <property name="syft:cpe23">cpe:2.3:a:alpine:alpine_baselayout:3.4.0-r0:*:*:*:*:*:*:*</property>
+        <property name="syft:location:0:layerID">sha256:7cd52847ad775a5ddc4b58326cf884beee34544296402c6292ed76474c686d39</property>
+        <property name="syft:location:0:path">/lib/apk/db/installed</property>
+        <property name="syft:metadata:gitCommitOfApkPort">bd965a7ebf7fd8f07d7a0cc0d7375bf3e4eb9b24</property>
+        <property name="syft:metadata:installedSize">331776</property>
+        <property name="syft:metadata:originPackage">alpine-baselayout</property>
+        <property name="syft:metadata:pullChecksum">Q1/eXfmbYT1WXenFSqKjroYyK84NE=</property>
+        <property name="syft:metadata:pullDependencies:0">alpine-baselayout-data=3.4.0-r0</property>
+        <property name="syft:metadata:pullDependencies:1">/bin/sh</property>
+        <property name="syft:metadata:size">8890</property>
+      </properties>
+    </component>
+    <component bom-ref="pkg:apk/alpine/alpine-baselayout-data@3.4.0-r0?arch=x86_64&amp;upstream=alpine-baselayout&amp;distro=alpine-3.17.2&amp;package-id=291d1267b40d636f" type="library">
+      <publisher>Natanael Copa &lt;ncopa@alpinelinux.org&gt;</publisher>
+      <name>alpine-baselayout-data</name>
+      <version>3.4.0-r0</version>
+      <description>Alpine base dir structure and init scripts</description>
+      <licenses>
+        <license>
+          <id>GPL-2.0-only</id>
+        </license>
+      </licenses>
+      <cpe>cpe:2.3:a:alpine-baselayout-data:alpine-baselayout-data:3.4.0-r0:*:*:*:*:*:*:*</cpe>
+      <purl>pkg:apk/alpine/alpine-baselayout-data@3.4.0-r0?arch=x86_64&amp;upstream=alpine-baselayout&amp;distro=alpine-3.17.2</purl>
+      <externalReferences>
+        <reference type="distribution">
+          <url>https://git.alpinelinux.org/cgit/aports/tree/main/alpine-baselayout</url>
+        </reference>
+      </externalReferences>
+      <properties>
+        <property name="syft:package:foundBy">apkdb-cataloger</property>
+        <property name="syft:package:metadataType">ApkMetadata</property>
+        <property name="syft:package:type">apk</property>
+        <property name="syft:cpe23">cpe:2.3:a:alpine-baselayout-data:alpine_baselayout_data:3.4.0-r0:*:*:*:*:*:*:*</property>
+        <property name="syft:cpe23">cpe:2.3:a:alpine_baselayout_data:alpine-baselayout-data:3.4.0-r0:*:*:*:*:*:*:*</property>
+        <property name="syft:cpe23">cpe:2.3:a:alpine_baselayout_data:alpine_baselayout_data:3.4.0-r0:*:*:*:*:*:*:*</property>
+        <property name="syft:cpe23">cpe:2.3:a:alpine-baselayout:alpine-baselayout-data:3.4.0-r0:*:*:*:*:*:*:*</property>
+        <property name="syft:cpe23">cpe:2.3:a:alpine-baselayout:alpine_baselayout_data:3.4.0-r0:*:*:*:*:*:*:*</property>
+        <property name="syft:cpe23">cpe:2.3:a:alpine_baselayout:alpine-baselayout-data:3.4.0-r0:*:*:*:*:*:*:*</property>
+        <property name="syft:cpe23">cpe:2.3:a:alpine_baselayout:alpine_baselayout_data:3.4.0-r0:*:*:*:*:*:*:*</property>
+        <property name="syft:cpe23">cpe:2.3:a:alpine:alpine-baselayout-data:3.4.0-r0:*:*:*:*:*:*:*</property>
+        <property name="syft:cpe23">cpe:2.3:a:alpine:alpine_baselayout_data:3.4.0-r0:*:*:*:*:*:*:*</property>
+        <property name="syft:location:0:layerID">sha256:7cd52847ad775a5ddc4b58326cf884beee34544296402c6292ed76474c686d39</property>
+        <property name="syft:location:0:path">/lib/apk/db/installed</property>
+        <property name="syft:metadata:gitCommitOfApkPort">bd965a7ebf7fd8f07d7a0cc0d7375bf3e4eb9b24</property>
+        <property name="syft:metadata:installedSize">77824</property>
+        <property name="syft:metadata:originPackage">alpine-baselayout</property>
+        <property name="syft:metadata:pullChecksum">Q1/JgpM8J6DWI/541tUX+uHEzSjqo=</property>
+        <property name="syft:metadata:size">11664</property>
+      </properties>
+    </component>
+    <component bom-ref="pkg:apk/alpine/alpine-keys@2.4-r1?arch=x86_64&amp;upstream=alpine-keys&amp;distro=alpine-3.17.2&amp;package-id=2b5e23d349b556cf" type="library">
+      <publisher>Natanael Copa &lt;ncopa@alpinelinux.org&gt;</publisher>
+      <name>alpine-keys</name>
+      <version>2.4-r1</version>
+      <description>Public keys for Alpine Linux packages</description>
+      <licenses>
+        <license>
+          <id>MIT</id>
+        </license>
+      </licenses>
+      <cpe>cpe:2.3:a:alpine-keys:alpine-keys:2.4-r1:*:*:*:*:*:*:*</cpe>
+      <purl>pkg:apk/alpine/alpine-keys@2.4-r1?arch=x86_64&amp;upstream=alpine-keys&amp;distro=alpine-3.17.2</purl>
+      <externalReferences>
+        <reference type="distribution">
+          <url>https://alpinelinux.org</url>
+        </reference>
+      </externalReferences>
+      <properties>
+        <property name="syft:package:foundBy">apkdb-cataloger</property>
+        <property name="syft:package:metadataType">ApkMetadata</property>
+        <property name="syft:package:type">apk</property>
+        <property name="syft:cpe23">cpe:2.3:a:alpine-keys:alpine_keys:2.4-r1:*:*:*:*:*:*:*</property>
+        <property name="syft:cpe23">cpe:2.3:a:alpine_keys:alpine-keys:2.4-r1:*:*:*:*:*:*:*</property>
+        <property name="syft:cpe23">cpe:2.3:a:alpine_keys:alpine_keys:2.4-r1:*:*:*:*:*:*:*</property>
+        <property name="syft:cpe23">cpe:2.3:a:alpine:alpine-keys:2.4-r1:*:*:*:*:*:*:*</property>
+        <property name="syft:cpe23">cpe:2.3:a:alpine:alpine_keys:2.4-r1:*:*:*:*:*:*:*</property>
+        <property name="syft:location:0:layerID">sha256:7cd52847ad775a5ddc4b58326cf884beee34544296402c6292ed76474c686d39</property>
+        <property name="syft:location:0:path">/lib/apk/db/installed</property>
+        <property name="syft:metadata:gitCommitOfApkPort">aab68f8c9ab434a46710de8e12fb3206e2930a59</property>
+        <property name="syft:metadata:installedSize">159744</property>
+        <property name="syft:metadata:originPackage">alpine-keys</property>
+        <property name="syft:metadata:pullChecksum">Q1KM01lfKVp+gEZn23awujqjSkrN8=</property>
+        <property name="syft:metadata:size">13361</property>
+      </properties>
+    </component>
+    <component bom-ref="pkg:apk/alpine/apk-tools@2.12.10-r1?arch=x86_64&amp;upstream=apk-tools&amp;distro=alpine-3.17.2&amp;package-id=e5f757b0df1f62bc" type="library">
+      <publisher>Natanael Copa &lt;ncopa@alpinelinux.org&gt;</publisher>
+      <name>apk-tools</name>
+      <version>2.12.10-r1</version>
+      <description>Alpine Package Keeper - package manager for alpine</description>
+      <licenses>
+        <license>
+          <id>GPL-2.0-only</id>
+        </license>
+      </licenses>
+      <cpe>cpe:2.3:a:apk-tools:apk-tools:2.12.10-r1:*:*:*:*:*:*:*</cpe>
+      <purl>pkg:apk/alpine/apk-tools@2.12.10-r1?arch=x86_64&amp;upstream=apk-tools&amp;distro=alpine-3.17.2</purl>
+      <externalReferences>
+        <reference type="distribution">
+          <url>https://gitlab.alpinelinux.org/alpine/apk-tools</url>
+        </reference>
+      </externalReferences>
+      <properties>
+        <property name="syft:package:foundBy">apkdb-cataloger</property>
+        <property name="syft:package:metadataType">ApkMetadata</property>
+        <property name="syft:package:type">apk</property>
+        <property name="syft:cpe23">cpe:2.3:a:apk-tools:apk_tools:2.12.10-r1:*:*:*:*:*:*:*</property>
+        <property name="syft:cpe23">cpe:2.3:a:apk_tools:apk-tools:2.12.10-r1:*:*:*:*:*:*:*</property>
+        <property name="syft:cpe23">cpe:2.3:a:apk_tools:apk_tools:2.12.10-r1:*:*:*:*:*:*:*</property>
+        <property name="syft:cpe23">cpe:2.3:a:apk:apk-tools:2.12.10-r1:*:*:*:*:*:*:*</property>
+        <property name="syft:cpe23">cpe:2.3:a:apk:apk_tools:2.12.10-r1:*:*:*:*:*:*:*</property>
+        <property name="syft:location:0:layerID">sha256:7cd52847ad775a5ddc4b58326cf884beee34544296402c6292ed76474c686d39</property>
+        <property name="syft:location:0:path">/lib/apk/db/installed</property>
+        <property name="syft:metadata:gitCommitOfApkPort">0188f510baadbae393472103427b9c1875117136</property>
+        <property name="syft:metadata:installedSize">307200</property>
+        <property name="syft:metadata:originPackage">apk-tools</property>
+        <property name="syft:metadata:provides:0">so:libapk.so.3.12.0=3.12.0</property>
+        <property name="syft:metadata:provides:1">cmd:apk=2.12.10-r1</property>
+        <property name="syft:metadata:pullChecksum">Q1Ef3iwt+cMdGngEgaFr2URIJhKzQ=</property>
+        <property name="syft:metadata:pullDependencies:0">musl&gt;=1.2</property>
+        <property name="syft:metadata:pullDependencies:1">ca-certificates-bundle</property>
+        <property name="syft:metadata:pullDependencies:2">so:libc.musl-x86_64.so.1</property>
+        <property name="syft:metadata:pullDependencies:3">so:libcrypto.so.3</property>
+        <property name="syft:metadata:pullDependencies:4">so:libssl.so.3</property>
+        <property name="syft:metadata:pullDependencies:5">so:libz.so.1</property>
+        <property name="syft:metadata:size">120973</property>
+      </properties>
+    </component>
+    <component bom-ref="e93bc067bebd50a9" type="application">
+      <name>busybox</name>
+      <version>1.35.0</version>
+      <cpe>cpe:2.3:a:busybox:busybox:1.35.0:*:*:*:*:*:*:*</cpe>
+      <properties>
+        <property name="syft:package:foundBy">binary-cataloger</property>
+        <property name="syft:package:metadataType">BinaryMetadata</property>
+        <property name="syft:package:type">binary</property>
+        <property name="syft:cpe23">cpe:2.3:a:busybox:busybox:1.35.0:*:*:*:*:*:*:*</property>
+        <property name="syft:location:0:layerID">sha256:7cd52847ad775a5ddc4b58326cf884beee34544296402c6292ed76474c686d39</property>
+        <property name="syft:location:0:path">/bin/busybox</property>
+      </properties>
+    </component>
+    <component bom-ref="pkg:apk/alpine/busybox@1.35.0-r29?arch=x86_64&amp;upstream=busybox&amp;distro=alpine-3.17.2&amp;package-id=623d53216342d45e" type="library">
+      <publisher>Sören Tempel &lt;soeren+alpine@soeren-tempel.net&gt;</publisher>
+      <name>busybox</name>
+      <version>1.35.0-r29</version>
+      <description>Size optimized toolbox of many common UNIX utilities</description>
+      <licenses>
+        <license>
+          <id>GPL-2.0-only</id>
+        </license>
+      </licenses>
+      <cpe>cpe:2.3:a:busybox:busybox:1.35.0-r29:*:*:*:*:*:*:*</cpe>
+      <purl>pkg:apk/alpine/busybox@1.35.0-r29?arch=x86_64&amp;upstream=busybox&amp;distro=alpine-3.17.2</purl>
+      <externalReferences>
+        <reference type="distribution">
+          <url>https://busybox.net/</url>
+        </reference>
+      </externalReferences>
+      <properties>
+        <property name="syft:package:foundBy">apkdb-cataloger</property>
+        <property name="syft:package:metadataType">ApkMetadata</property>
+        <property name="syft:package:type">apk</property>
+        <property name="syft:location:0:layerID">sha256:7cd52847ad775a5ddc4b58326cf884beee34544296402c6292ed76474c686d39</property>
+        <property name="syft:location:0:path">/lib/apk/db/installed</property>
+        <property name="syft:metadata:gitCommitOfApkPort">1dbf7a793afae640ea643a055b6dd4f430ac116b</property>
+        <property name="syft:metadata:installedSize">962560</property>
+        <property name="syft:metadata:originPackage">busybox</property>
+        <property name="syft:metadata:provides:0">cmd:busybox=1.35.0-r29</property>
+        <property name="syft:metadata:pullChecksum">Q1NN3sp0yr99btRysqty3nQUrWHaY=</property>
+        <property name="syft:metadata:pullDependencies:0">so:libc.musl-x86_64.so.1</property>
+        <property name="syft:metadata:size">509600</property>
+      </properties>
+    </component>
+    <component bom-ref="pkg:apk/alpine/busybox-binsh@1.35.0-r29?arch=x86_64&amp;upstream=busybox&amp;distro=alpine-3.17.2&amp;package-id=256fc96b4a8c4da8" type="library">
+      <publisher>Sören Tempel &lt;soeren+alpine@soeren-tempel.net&gt;</publisher>
+      <name>busybox-binsh</name>
+      <version>1.35.0-r29</version>
+      <description>busybox ash /bin/sh</description>
+      <licenses>
+        <license>
+          <id>GPL-2.0-only</id>
+        </license>
+      </licenses>
+      <cpe>cpe:2.3:a:busybox-binsh:busybox-binsh:1.35.0-r29:*:*:*:*:*:*:*</cpe>
+      <purl>pkg:apk/alpine/busybox-binsh@1.35.0-r29?arch=x86_64&amp;upstream=busybox&amp;distro=alpine-3.17.2</purl>
+      <externalReferences>
+        <reference type="distribution">
+          <url>https://busybox.net/</url>
+        </reference>
+      </externalReferences>
+      <properties>
+        <property name="syft:package:foundBy">apkdb-cataloger</property>
+        <property name="syft:package:metadataType">ApkMetadata</property>
+        <property name="syft:package:type">apk</property>
+        <property name="syft:cpe23">cpe:2.3:a:busybox-binsh:busybox_binsh:1.35.0-r29:*:*:*:*:*:*:*</property>
+        <property name="syft:cpe23">cpe:2.3:a:busybox_binsh:busybox-binsh:1.35.0-r29:*:*:*:*:*:*:*</property>
+        <property name="syft:cpe23">cpe:2.3:a:busybox_binsh:busybox_binsh:1.35.0-r29:*:*:*:*:*:*:*</property>
+        <property name="syft:cpe23">cpe:2.3:a:busybox:busybox-binsh:1.35.0-r29:*:*:*:*:*:*:*</property>
+        <property name="syft:cpe23">cpe:2.3:a:busybox:busybox_binsh:1.35.0-r29:*:*:*:*:*:*:*</property>
+        <property name="syft:location:0:layerID">sha256:7cd52847ad775a5ddc4b58326cf884beee34544296402c6292ed76474c686d39</property>
+        <property name="syft:location:0:path">/lib/apk/db/installed</property>
+        <property name="syft:metadata:gitCommitOfApkPort">1dbf7a793afae640ea643a055b6dd4f430ac116b</property>
+        <property name="syft:metadata:installedSize">8192</property>
+        <property name="syft:metadata:originPackage">busybox</property>
+        <property name="syft:metadata:provides:0">/bin/sh</property>
+        <property name="syft:metadata:provides:1">cmd:sh=1.35.0-r29</property>
+        <property name="syft:metadata:pullChecksum">Q1miWwyhWKXVEiRYLhmArV1TKMs6A=</property>
+        <property name="syft:metadata:pullDependencies:0">busybox=1.35.0-r29</property>
+        <property name="syft:metadata:size">1547</property>
+      </properties>
+    </component>
+    <component bom-ref="pkg:apk/alpine/ca-certificates-bundle@20220614-r4?arch=x86_64&amp;upstream=ca-certificates&amp;distro=alpine-3.17.2&amp;package-id=b805d823ae624f04" type="library">
+      <publisher>Natanael Copa &lt;ncopa@alpinelinux.org&gt;</publisher>
+      <name>ca-certificates-bundle</name>
+      <version>20220614-r4</version>
+      <description>Pre generated bundle of Mozilla certificates</description>
+      <licenses>
+        <license>
+          <id>MPL-2.0</id>
+        </license>
+        <license>
+          <id>MIT</id>
+        </license>
+      </licenses>
+      <cpe>cpe:2.3:a:ca-certificates-bundle:ca-certificates-bundle:20220614-r4:*:*:*:*:*:*:*</cpe>
+      <purl>pkg:apk/alpine/ca-certificates-bundle@20220614-r4?arch=x86_64&amp;upstream=ca-certificates&amp;distro=alpine-3.17.2</purl>
+      <externalReferences>
+        <reference type="distribution">
+          <url>https://www.mozilla.org/en-US/about/governance/policies/security-group/certs/</url>
+        </reference>
+      </externalReferences>
+      <properties>
+        <property name="syft:package:foundBy">apkdb-cataloger</property>
+        <property name="syft:package:metadataType">ApkMetadata</property>
+        <property name="syft:package:type">apk</property>
+        <property name="syft:cpe23">cpe:2.3:a:ca-certificates-bundle:ca_certificates_bundle:20220614-r4:*:*:*:*:*:*:*</property>
+        <property name="syft:cpe23">cpe:2.3:a:ca_certificates_bundle:ca-certificates-bundle:20220614-r4:*:*:*:*:*:*:*</property>
+        <property name="syft:cpe23">cpe:2.3:a:ca_certificates_bundle:ca_certificates_bundle:20220614-r4:*:*:*:*:*:*:*</property>
+        <property name="syft:cpe23">cpe:2.3:a:ca-certificates:ca-certificates-bundle:20220614-r4:*:*:*:*:*:*:*</property>
+        <property name="syft:cpe23">cpe:2.3:a:ca-certificates:ca_certificates_bundle:20220614-r4:*:*:*:*:*:*:*</property>
+        <property name="syft:cpe23">cpe:2.3:a:ca_certificates:ca-certificates-bundle:20220614-r4:*:*:*:*:*:*:*</property>
+        <property name="syft:cpe23">cpe:2.3:a:ca_certificates:ca_certificates_bundle:20220614-r4:*:*:*:*:*:*:*</property>
+        <property name="syft:cpe23">cpe:2.3:a:ca:ca-certificates-bundle:20220614-r4:*:*:*:*:*:*:*</property>
+        <property name="syft:cpe23">cpe:2.3:a:ca:ca_certificates_bundle:20220614-r4:*:*:*:*:*:*:*</property>
+        <property name="syft:location:0:layerID">sha256:7cd52847ad775a5ddc4b58326cf884beee34544296402c6292ed76474c686d39</property>
+        <property name="syft:location:0:path">/lib/apk/db/installed</property>
+        <property name="syft:metadata:gitCommitOfApkPort">e1839fd45a096c9e21ac24f8a61991d357d11628</property>
+        <property name="syft:metadata:installedSize">237568</property>
+        <property name="syft:metadata:originPackage">ca-certificates</property>
+        <property name="syft:metadata:provides:0">ca-certificates-cacert=20220614-r4</property>
+        <property name="syft:metadata:pullChecksum">Q14PFUzkDXTGDcHkiuEdFuzb+EvxQ=</property>
+        <property name="syft:metadata:size">126296</property>
+      </properties>
+    </component>
+    <component bom-ref="pkg:apk/alpine/libc-utils@0.7.2-r3?arch=x86_64&amp;upstream=libc-dev&amp;distro=alpine-3.17.2&amp;package-id=8126b232e2d3c608" type="library">
+      <publisher>Natanael Copa &lt;ncopa@alpinelinux.org&gt;</publisher>
+      <name>libc-utils</name>
+      <version>0.7.2-r3</version>
+      <description>Meta package to pull in correct libc</description>
+      <licenses>
+        <license>
+          <id>BSD-2-Clause</id>
+        </license>
+        <license>
+          <id>BSD-3-Clause</id>
+        </license>
+      </licenses>
+      <cpe>cpe:2.3:a:libc-utils:libc-utils:0.7.2-r3:*:*:*:*:*:*:*</cpe>
+      <purl>pkg:apk/alpine/libc-utils@0.7.2-r3?arch=x86_64&amp;upstream=libc-dev&amp;distro=alpine-3.17.2</purl>
+      <externalReferences>
+        <reference type="distribution">
+          <url>https://alpinelinux.org</url>
+        </reference>
+      </externalReferences>
+      <properties>
+        <property name="syft:package:foundBy">apkdb-cataloger</property>
+        <property name="syft:package:metadataType">ApkMetadata</property>
+        <property name="syft:package:type">apk</property>
+        <property name="syft:cpe23">cpe:2.3:a:libc-utils:libc_utils:0.7.2-r3:*:*:*:*:*:*:*</property>
+        <property name="syft:cpe23">cpe:2.3:a:libc_utils:libc-utils:0.7.2-r3:*:*:*:*:*:*:*</property>
+        <property name="syft:cpe23">cpe:2.3:a:libc_utils:libc_utils:0.7.2-r3:*:*:*:*:*:*:*</property>
+        <property name="syft:cpe23">cpe:2.3:a:libc:libc-utils:0.7.2-r3:*:*:*:*:*:*:*</property>
+        <property name="syft:cpe23">cpe:2.3:a:libc:libc_utils:0.7.2-r3:*:*:*:*:*:*:*</property>
+        <property name="syft:location:0:layerID">sha256:7cd52847ad775a5ddc4b58326cf884beee34544296402c6292ed76474c686d39</property>
+        <property name="syft:location:0:path">/lib/apk/db/installed</property>
+        <property name="syft:metadata:gitCommitOfApkPort">60424133be2e79bbfeff3d58147a22886f817ce2</property>
+        <property name="syft:metadata:installedSize">4096</property>
+        <property name="syft:metadata:originPackage">libc-dev</property>
+        <property name="syft:metadata:pullChecksum">Q19Gg06pBPiiG9UN94ql7qImsHSUQ=</property>
+        <property name="syft:metadata:pullDependencies:0">musl-utils</property>
+        <property name="syft:metadata:size">1485</property>
+      </properties>
+    </component>
+    <component bom-ref="pkg:apk/alpine/libcrypto3@3.0.8-r0?arch=x86_64&amp;upstream=openssl&amp;distro=alpine-3.17.2&amp;package-id=b0e92b2ef962ab6d" type="library">
+      <publisher>Ariadne Conill &lt;ariadne@dereferenced.org&gt;</publisher>
+      <name>libcrypto3</name>
+      <version>3.0.8-r0</version>
+      <description>Crypto library from openssl</description>
+      <licenses>
+        <license>
+          <id>Apache-2.0</id>
+        </license>
+      </licenses>
+      <cpe>cpe:2.3:a:libcrypto3:libcrypto3:3.0.8-r0:*:*:*:*:*:*:*</cpe>
+      <purl>pkg:apk/alpine/libcrypto3@3.0.8-r0?arch=x86_64&amp;upstream=openssl&amp;distro=alpine-3.17.2</purl>
+      <externalReferences>
+        <reference type="distribution">
+          <url>https://www.openssl.org/</url>
+        </reference>
+      </externalReferences>
+      <properties>
+        <property name="syft:package:foundBy">apkdb-cataloger</property>
+        <property name="syft:package:metadataType">ApkMetadata</property>
+        <property name="syft:package:type">apk</property>
+        <property name="syft:location:0:layerID">sha256:7cd52847ad775a5ddc4b58326cf884beee34544296402c6292ed76474c686d39</property>
+        <property name="syft:location:0:path">/lib/apk/db/installed</property>
+        <property name="syft:metadata:gitCommitOfApkPort">524302e205a5b43c2bb48d041bcb10ccf2b480f9</property>
+        <property name="syft:metadata:installedSize">4206592</property>
+        <property name="syft:metadata:originPackage">openssl</property>
+        <property name="syft:metadata:provides:0">so:libcrypto.so.3=3</property>
+        <property name="syft:metadata:pullChecksum">Q1lyWpurYeMlLEt60ys+OlTABmzgs=</property>
+        <property name="syft:metadata:pullDependencies:0">so:libc.musl-x86_64.so.1</property>
+        <property name="syft:metadata:size">1710217</property>
+      </properties>
+    </component>
+    <component bom-ref="pkg:apk/alpine/libssl3@3.0.8-r0?arch=x86_64&amp;upstream=openssl&amp;distro=alpine-3.17.2&amp;package-id=9005623d3896bb87" type="library">
+      <publisher>Ariadne Conill &lt;ariadne@dereferenced.org&gt;</publisher>
+      <name>libssl3</name>
+      <version>3.0.8-r0</version>
+      <description>SSL shared libraries</description>
+      <licenses>
+        <license>
+          <id>Apache-2.0</id>
+        </license>
+      </licenses>
+      <cpe>cpe:2.3:a:libssl3:libssl3:3.0.8-r0:*:*:*:*:*:*:*</cpe>
+      <purl>pkg:apk/alpine/libssl3@3.0.8-r0?arch=x86_64&amp;upstream=openssl&amp;distro=alpine-3.17.2</purl>
+      <externalReferences>
+        <reference type="distribution">
+          <url>https://www.openssl.org/</url>
+        </reference>
+      </externalReferences>
+      <properties>
+        <property name="syft:package:foundBy">apkdb-cataloger</property>
+        <property name="syft:package:metadataType">ApkMetadata</property>
+        <property name="syft:package:type">apk</property>
+        <property name="syft:location:0:layerID">sha256:7cd52847ad775a5ddc4b58326cf884beee34544296402c6292ed76474c686d39</property>
+        <property name="syft:location:0:path">/lib/apk/db/installed</property>
+        <property name="syft:metadata:gitCommitOfApkPort">524302e205a5b43c2bb48d041bcb10ccf2b480f9</property>
+        <property name="syft:metadata:installedSize">622592</property>
+        <property name="syft:metadata:originPackage">openssl</property>
+        <property name="syft:metadata:provides:0">so:libssl.so.3=3</property>
+        <property name="syft:metadata:pullChecksum">Q1Z6/d/FKYkPehWzNtOtYnJ74oIkY=</property>
+        <property name="syft:metadata:pullDependencies:0">so:libc.musl-x86_64.so.1</property>
+        <property name="syft:metadata:pullDependencies:1">so:libcrypto.so.3</property>
+        <property name="syft:metadata:size">246853</property>
+      </properties>
+    </component>
+    <component bom-ref="pkg:apk/alpine/musl@1.2.3-r4?arch=x86_64&amp;upstream=musl&amp;distro=alpine-3.17.2&amp;package-id=d9700f02cf26e8b8" type="library">
+      <publisher>Timo Teräs &lt;timo.teras@iki.fi&gt;</publisher>
+      <name>musl</name>
+      <version>1.2.3-r4</version>
+      <description>the musl c library (libc) implementation</description>
+      <licenses>
+        <license>
+          <id>MIT</id>
+        </license>
+      </licenses>
+      <cpe>cpe:2.3:a:musl:musl:1.2.3-r4:*:*:*:*:*:*:*</cpe>
+      <purl>pkg:apk/alpine/musl@1.2.3-r4?arch=x86_64&amp;upstream=musl&amp;distro=alpine-3.17.2</purl>
+      <externalReferences>
+        <reference type="distribution">
+          <url>https://musl.libc.org/</url>
+        </reference>
+      </externalReferences>
+      <properties>
+        <property name="syft:package:foundBy">apkdb-cataloger</property>
+        <property name="syft:package:metadataType">ApkMetadata</property>
+        <property name="syft:package:type">apk</property>
+        <property name="syft:location:0:layerID">sha256:7cd52847ad775a5ddc4b58326cf884beee34544296402c6292ed76474c686d39</property>
+        <property name="syft:location:0:path">/lib/apk/db/installed</property>
+        <property name="syft:metadata:gitCommitOfApkPort">f93af038c3de7146121c2ea8124ba5ce29b4b058</property>
+        <property name="syft:metadata:installedSize">634880</property>
+        <property name="syft:metadata:originPackage">musl</property>
+        <property name="syft:metadata:provides:0">so:libc.musl-x86_64.so.1=1</property>
+        <property name="syft:metadata:pullChecksum">Q1Pk7x1woArbB1nzkMPJPq1TECwus=</property>
+        <property name="syft:metadata:size">388955</property>
+      </properties>
+    </component>
+    <component bom-ref="pkg:apk/alpine/musl-utils@1.2.3-r4?arch=x86_64&amp;upstream=musl&amp;distro=alpine-3.17.2&amp;package-id=f71ecf5267e6c37b" type="library">
+      <publisher>Timo Teräs &lt;timo.teras@iki.fi&gt;</publisher>
+      <name>musl-utils</name>
+      <version>1.2.3-r4</version>
+      <description>the musl c library (libc) implementation</description>
+      <licenses>
+        <license>
+          <id>MIT</id>
+        </license>
+        <license>
+          <id>BSD-2-Clause</id>
+        </license>
+        <license>
+          <id>GPL-2.0-or-later</id>
+        </license>
+      </licenses>
+      <cpe>cpe:2.3:a:musl-utils:musl-utils:1.2.3-r4:*:*:*:*:*:*:*</cpe>
+      <purl>pkg:apk/alpine/musl-utils@1.2.3-r4?arch=x86_64&amp;upstream=musl&amp;distro=alpine-3.17.2</purl>
+      <externalReferences>
+        <reference type="distribution">
+          <url>https://musl.libc.org/</url>
+        </reference>
+      </externalReferences>
+      <properties>
+        <property name="syft:package:foundBy">apkdb-cataloger</property>
+        <property name="syft:package:metadataType">ApkMetadata</property>
+        <property name="syft:package:type">apk</property>
+        <property name="syft:cpe23">cpe:2.3:a:musl-utils:musl_utils:1.2.3-r4:*:*:*:*:*:*:*</property>
+        <property name="syft:cpe23">cpe:2.3:a:musl_utils:musl-utils:1.2.3-r4:*:*:*:*:*:*:*</property>
+        <property name="syft:cpe23">cpe:2.3:a:musl_utils:musl_utils:1.2.3-r4:*:*:*:*:*:*:*</property>
+        <property name="syft:cpe23">cpe:2.3:a:musl:musl-utils:1.2.3-r4:*:*:*:*:*:*:*</property>
+        <property name="syft:cpe23">cpe:2.3:a:musl:musl_utils:1.2.3-r4:*:*:*:*:*:*:*</property>
+        <property name="syft:location:0:layerID">sha256:7cd52847ad775a5ddc4b58326cf884beee34544296402c6292ed76474c686d39</property>
+        <property name="syft:location:0:path">/lib/apk/db/installed</property>
+        <property name="syft:metadata:gitCommitOfApkPort">f93af038c3de7146121c2ea8124ba5ce29b4b058</property>
+        <property name="syft:metadata:installedSize">135168</property>
+        <property name="syft:metadata:originPackage">musl</property>
+        <property name="syft:metadata:provides:0">cmd:getconf=1.2.3-r4</property>
+        <property name="syft:metadata:provides:1">cmd:getent=1.2.3-r4</property>
+        <property name="syft:metadata:provides:2">cmd:iconv=1.2.3-r4</property>
+        <property name="syft:metadata:provides:3">cmd:ldconfig=1.2.3-r4</property>
+        <property name="syft:metadata:provides:4">cmd:ldd=1.2.3-r4</property>
+        <property name="syft:metadata:pullChecksum">Q1ZWJL4eySx8nPSjF1FAJgQyvuNs4=</property>
+        <property name="syft:metadata:pullDependencies:0">scanelf</property>
+        <property name="syft:metadata:pullDependencies:1">so:libc.musl-x86_64.so.1</property>
+        <property name="syft:metadata:size">36697</property>
+      </properties>
+    </component>
+    <component bom-ref="pkg:apk/alpine/scanelf@1.3.5-r1?arch=x86_64&amp;upstream=pax-utils&amp;distro=alpine-3.17.2&amp;package-id=e903138d19e85b80" type="library">
+      <publisher>Natanael Copa &lt;ncopa@alpinelinux.org&gt;</publisher>
+      <name>scanelf</name>
+      <version>1.3.5-r1</version>
+      <description>Scan ELF binaries for stuff</description>
+      <licenses>
+        <license>
+          <id>GPL-2.0-only</id>
+        </license>
+      </licenses>
+      <cpe>cpe:2.3:a:scanelf:scanelf:1.3.5-r1:*:*:*:*:*:*:*</cpe>
+      <purl>pkg:apk/alpine/scanelf@1.3.5-r1?arch=x86_64&amp;upstream=pax-utils&amp;distro=alpine-3.17.2</purl>
+      <externalReferences>
+        <reference type="distribution">
+          <url>https://wiki.gentoo.org/wiki/Hardened/PaX_Utilities</url>
+        </reference>
+      </externalReferences>
+      <properties>
+        <property name="syft:package:foundBy">apkdb-cataloger</property>
+        <property name="syft:package:metadataType">ApkMetadata</property>
+        <property name="syft:package:type">apk</property>
+        <property name="syft:location:0:layerID">sha256:7cd52847ad775a5ddc4b58326cf884beee34544296402c6292ed76474c686d39</property>
+        <property name="syft:location:0:path">/lib/apk/db/installed</property>
+        <property name="syft:metadata:gitCommitOfApkPort">e52243dbb02069f10d48440ccc5fd41fa5fc2236</property>
+        <property name="syft:metadata:installedSize">98304</property>
+        <property name="syft:metadata:originPackage">pax-utils</property>
+        <property name="syft:metadata:provides:0">cmd:scanelf=1.3.5-r1</property>
+        <property name="syft:metadata:pullChecksum">Q11dxYFsHvBFAzzHGDo5gOTDNJDyQ=</property>
+        <property name="syft:metadata:pullDependencies:0">so:libc.musl-x86_64.so.1</property>
+        <property name="syft:metadata:size">37687</property>
+      </properties>
+    </component>
+    <component bom-ref="pkg:apk/alpine/ssl_client@1.35.0-r29?arch=x86_64&amp;upstream=busybox&amp;distro=alpine-3.17.2&amp;package-id=b15247aafcd4a647" type="library">
+      <publisher>Sören Tempel &lt;soeren+alpine@soeren-tempel.net&gt;</publisher>
+      <name>ssl_client</name>
+      <version>1.35.0-r29</version>
+      <description>EXternal ssl_client for busybox wget</description>
+      <licenses>
+        <license>
+          <id>GPL-2.0-only</id>
+        </license>
+      </licenses>
+      <cpe>cpe:2.3:a:ssl-client:ssl-client:1.35.0-r29:*:*:*:*:*:*:*</cpe>
+      <purl>pkg:apk/alpine/ssl_client@1.35.0-r29?arch=x86_64&amp;upstream=busybox&amp;distro=alpine-3.17.2</purl>
+      <externalReferences>
+        <reference type="distribution">
+          <url>https://busybox.net/</url>
+        </reference>
+      </externalReferences>
+      <properties>
+        <property name="syft:package:foundBy">apkdb-cataloger</property>
+        <property name="syft:package:metadataType">ApkMetadata</property>
+        <property name="syft:package:type">apk</property>
+        <property name="syft:cpe23">cpe:2.3:a:ssl-client:ssl_client:1.35.0-r29:*:*:*:*:*:*:*</property>
+        <property name="syft:cpe23">cpe:2.3:a:ssl_client:ssl-client:1.35.0-r29:*:*:*:*:*:*:*</property>
+        <property name="syft:cpe23">cpe:2.3:a:ssl_client:ssl_client:1.35.0-r29:*:*:*:*:*:*:*</property>
+        <property name="syft:cpe23">cpe:2.3:a:ssl:ssl-client:1.35.0-r29:*:*:*:*:*:*:*</property>
+        <property name="syft:cpe23">cpe:2.3:a:ssl:ssl_client:1.35.0-r29:*:*:*:*:*:*:*</property>
+        <property name="syft:location:0:layerID">sha256:7cd52847ad775a5ddc4b58326cf884beee34544296402c6292ed76474c686d39</property>
+        <property name="syft:location:0:path">/lib/apk/db/installed</property>
+        <property name="syft:metadata:gitCommitOfApkPort">1dbf7a793afae640ea643a055b6dd4f430ac116b</property>
+        <property name="syft:metadata:installedSize">28672</property>
+        <property name="syft:metadata:originPackage">busybox</property>
+        <property name="syft:metadata:provides:0">cmd:ssl_client=1.35.0-r29</property>
+        <property name="syft:metadata:pullChecksum">Q1QuqZjeP6XG85I29tOiCWofL8Cj0=</property>
+        <property name="syft:metadata:pullDependencies:0">so:libc.musl-x86_64.so.1</property>
+        <property name="syft:metadata:pullDependencies:1">so:libcrypto.so.3</property>
+        <property name="syft:metadata:pullDependencies:2">so:libssl.so.3</property>
+        <property name="syft:metadata:size">4929</property>
+      </properties>
+    </component>
+    <component bom-ref="pkg:apk/alpine/zlib@1.2.13-r0?arch=x86_64&amp;upstream=zlib&amp;distro=alpine-3.17.2&amp;package-id=94014313cfcd2b71" type="library">
+      <publisher>Natanael Copa &lt;ncopa@alpinelinux.org&gt;</publisher>
+      <name>zlib</name>
+      <version>1.2.13-r0</version>
+      <description>A compression/decompression Library</description>
+      <licenses>
+        <license>
+          <id>Zlib</id>
+        </license>
+      </licenses>
+      <cpe>cpe:2.3:a:zlib:zlib:1.2.13-r0:*:*:*:*:*:*:*</cpe>
+      <purl>pkg:apk/alpine/zlib@1.2.10-r2?arch=x86_64&amp;upstream=zlib&amp;distro=alpine-3.17.2</purl>
+      <externalReferences>
+        <reference type="distribution">
+          <url>https://zlib.net/</url>
+        </reference>
+      </externalReferences>
+      <properties>
+        <property name="syft:package:foundBy">apkdb-cataloger</property>
+        <property name="syft:package:metadataType">ApkMetadata</property>
+        <property name="syft:package:type">apk</property>
+        <property name="syft:location:0:layerID">sha256:7cd52847ad775a5ddc4b58326cf884beee34544296402c6292ed76474c686d39</property>
+        <property name="syft:location:0:path">/lib/apk/db/installed</property>
+        <property name="syft:metadata:gitCommitOfApkPort">bb37266b06a72d21d1fd850ef4b86665cf9ef70f</property>
+        <property name="syft:metadata:installedSize">110592</property>
+        <property name="syft:metadata:originPackage">zlib</property>
+        <property name="syft:metadata:provides:0">so:libz.so.1=1.2.13</property>
+        <property name="syft:metadata:pullChecksum">Q1rjnXT01l1PAxXheUxe4Oldl5rFk=</property>
+        <property name="syft:metadata:pullDependencies:0">so:libc.musl-x86_64.so.1</property>
+        <property name="syft:metadata:size">54258</property>
+      </properties>
+    </component>
+    <component type="operating-system">
+      <name>alpine</name>
+      <version>3.17.2</version>
+      <description>Alpine Linux v3.17</description>
+      <swid tagId="alpine" name="alpine" version="3.17.2"></swid>
+      <externalReferences>
+        <reference type="issue-tracker">
+          <url>https://gitlab.alpinelinux.org/alpine/aports/-/issues</url>
+        </reference>
+        <reference type="website">
+          <url>https://alpinelinux.org/</url>
+        </reference>
+      </externalReferences>
+      <properties>
+        <property name="syft:distro:id">alpine</property>
+        <property name="syft:distro:prettyName">Alpine Linux v3.17</property>
+        <property name="syft:distro:versionID">3.17.2</property>
+      </properties>
+    </component>
+  </components>
+</bom>
\ No newline at end of file
diff --git a/cmd/osv-scanner/fixtures/sbom-insecure/osv-scanner.toml b/cmd/osv-scanner/fixtures/sbom-insecure/osv-scanner.toml
index 3f21fd8..a414da9 100644
--- a/cmd/osv-scanner/fixtures/sbom-insecure/osv-scanner.toml
+++ b/cmd/osv-scanner/fixtures/sbom-insecure/osv-scanner.toml
@@ -47,3 +47,8 @@ reason = "This is a intentionally vulnerable test sbom"
 id = "DLA-3051-1"
 # ignoreUntil = n/a
 reason = "This is a intentionally vulnerable test sbom"
+
+[[IgnoredVulns]]
+id = "CVE-2022-37434"
+# ignoreUntil = n/a
+reason = "This is a intentionally vulnerable test sbom"
diff --git a/go.mod b/go.mod
index 9cd24f3..4e347ae 100644
--- a/go.mod
+++ b/go.mod
@@ -6,16 +6,16 @@ require (
 	github.com/BurntSushi/toml v1.3.2
 	github.com/CycloneDX/cyclonedx-go v0.7.1
 	github.com/go-git/go-billy/v5 v5.4.1
-	github.com/go-git/go-git/v5 v5.8.0
+	github.com/go-git/go-git/v5 v5.8.1
 	github.com/goark/go-cvss v1.6.6
 	github.com/google/go-cmp v0.5.9
 	github.com/jedib0t/go-pretty/v6 v6.4.6
 	github.com/kr/pretty v0.3.1
 	github.com/owenrumney/go-sarif/v2 v2.2.0
 	github.com/package-url/packageurl-go v0.1.1
-	github.com/spdx/tools-golang v0.5.2
+	github.com/spdx/tools-golang v0.5.3
 	github.com/urfave/cli/v2 v2.25.7
-	golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1
+	golang.org/x/exp v0.0.0-20230801115018-d63ba01acd4b
 	golang.org/x/mod v0.12.0
 	golang.org/x/sync v0.3.0
 	golang.org/x/term v0.10.0
@@ -24,8 +24,9 @@ require (
 )
 
 require (
-	github.com/Microsoft/go-winio v0.5.2 // indirect
-	github.com/ProtonMail/go-crypto v0.0.0-20230518184743-7afd39499903 // indirect
+	dario.cat/mergo v1.0.0 // indirect
+	github.com/Microsoft/go-winio v0.6.1 // indirect
+	github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95 // indirect
 	github.com/acomagu/bufpipe v1.0.4 // indirect
 	github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092 // indirect
 	github.com/cloudflare/circl v1.3.3 // indirect
@@ -34,7 +35,6 @@ require (
 	github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
 	github.com/goark/errs v1.1.0 // indirect
 	github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
-	github.com/imdario/mergo v0.3.15 // indirect
 	github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
 	github.com/kevinburke/ssh_config v1.2.0 // indirect
 	github.com/kr/text v0.2.0 // indirect
@@ -44,7 +44,7 @@ require (
 	github.com/rogpeppe/go-internal v1.9.0 // indirect
 	github.com/russross/blackfriday/v2 v2.1.0 // indirect
 	github.com/sergi/go-diff v1.1.0 // indirect
-	github.com/skeema/knownhosts v1.1.1 // indirect
+	github.com/skeema/knownhosts v1.2.0 // indirect
 	github.com/spdx/gordf v0.0.0-20221230105357-b735bd5aac89 // indirect
 	github.com/xanzy/ssh-agent v0.3.3 // indirect
 	github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
diff --git a/go.sum b/go.sum
index 6eab4c1..b58c1af 100644
--- a/go.sum
+++ b/go.sum
@@ -1,11 +1,14 @@
+dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
+dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
 github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
 github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
 github.com/CycloneDX/cyclonedx-go v0.7.1 h1:5w1SxjGm9MTMNTuRbEPyw21ObdbaagTWF/KfF0qHTRE=
 github.com/CycloneDX/cyclonedx-go v0.7.1/go.mod h1:N/nrdWQI2SIjaACyyDs/u7+ddCkyl/zkNs8xFsHF2Ps=
-github.com/Microsoft/go-winio v0.5.2 h1:a9IhgEQBCUEk6QCdml9CiJGhAws+YwffDHEMp1VMrpA=
 github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
-github.com/ProtonMail/go-crypto v0.0.0-20230518184743-7afd39499903 h1:ZK3C5DtzV2nVAQTx5S5jQvMeDqWtD1By5mOoyY/xJek=
-github.com/ProtonMail/go-crypto v0.0.0-20230518184743-7afd39499903/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE=
+github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
+github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
+github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95 h1:KLq8BE0KwCL+mmXnjLWEAOYO+2l2AE4YMmqG1ZpZHBs=
+github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
 github.com/acomagu/bufpipe v1.0.4 h1:e3H4WUzM3npvo5uv95QuJM3cQspFNtFBzvJ2oNjKIDQ=
 github.com/acomagu/bufpipe v1.0.4/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4=
 github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092 h1:aM1rlcoLz8y5B2r4tTLMiVTrMtpfY0O8EScKJxaSaEc=
@@ -14,8 +17,7 @@ github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFI
 github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo=
 github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
 github.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oMMlVBbn9M=
-github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
-github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I=
+github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
 github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs=
 github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
 github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
@@ -33,8 +35,8 @@ github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmS
 github.com/go-git/go-billy/v5 v5.4.1 h1:Uwp5tDRkPr+l/TnbHOQzp+tmJfLceOlbVucgpTz8ix4=
 github.com/go-git/go-billy/v5 v5.4.1/go.mod h1:vjbugF6Fz7JIflbVpl1hJsGjSHNltrSw45YK/ukIvQg=
 github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20230305113008-0c11038e723f h1:Pz0DHeFij3XFhoBRGUDPzSJ+w2UcK5/0JvF8DRI58r8=
-github.com/go-git/go-git/v5 v5.8.0 h1:Rc543s6Tyq+YcyPwZRvU4jzZGM8rB/wWu94TnTIYALQ=
-github.com/go-git/go-git/v5 v5.8.0/go.mod h1:coJHKEOk5kUClpsNlXrUvPrDxY3w3gjHvhcZd8Fodw8=
+github.com/go-git/go-git/v5 v5.8.1 h1:Zo79E4p7TRk0xoRgMq0RShiTHGKcKI4+DI6BfJc/Q+A=
+github.com/go-git/go-git/v5 v5.8.1/go.mod h1:FHFuoD6yGz5OSKEBK+aWN9Oah0q54Jxl0abmj6GnqAo=
 github.com/goark/errs v1.1.0 h1:FKnyw4LVyRADIjM8Nj0Up6r0/y5cfADvZAd1E+tthXE=
 github.com/goark/errs v1.1.0/go.mod h1:TtaPEoadm2mzqzfXdkkfpN2xuniCFm2q4JH+c1qzaqw=
 github.com/goark/go-cvss v1.6.6 h1:WJFuIWqmAw1Ilb9USv0vuX+nYzOWJp8lIujseJ/y3sU=
@@ -46,8 +48,6 @@ github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaW
 github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
 github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
-github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM=
-github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
 github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
 github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
 github.com/jedib0t/go-pretty/v6 v6.4.6 h1:v6aG9h6Uby3IusSSEjHaZNXpHFhzqMmjXcPq1Rjl9Jw=
@@ -89,13 +89,13 @@ github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD
 github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
 github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
 github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
-github.com/skeema/knownhosts v1.1.1 h1:MTk78x9FPgDFVFkDLTrsnnfCJl7g1C/nnKvePgrIngE=
-github.com/skeema/knownhosts v1.1.1/go.mod h1:g4fPeYpque7P0xefxtGzV81ihjC8sX2IqpAoNkjxbMo=
+github.com/skeema/knownhosts v1.2.0 h1:h9r9cf0+u7wSE+M183ZtMGgOJKiL96brpaz5ekfJCpM=
+github.com/skeema/knownhosts v1.2.0/go.mod h1:g4fPeYpque7P0xefxtGzV81ihjC8sX2IqpAoNkjxbMo=
 github.com/spdx/gordf v0.0.0-20201111095634-7098f93598fb/go.mod h1:uKWaldnbMnjsSAXRurWqqrdyZen1R7kxl8TkmWk2OyM=
 github.com/spdx/gordf v0.0.0-20221230105357-b735bd5aac89 h1:dArkMwZ7Mf2JiU8OfdmqIv8QaHT4oyifLIe1UhsF1SY=
 github.com/spdx/gordf v0.0.0-20221230105357-b735bd5aac89/go.mod h1:uKWaldnbMnjsSAXRurWqqrdyZen1R7kxl8TkmWk2OyM=
-github.com/spdx/tools-golang v0.5.2 h1:dtMNjJreWPe37584ajk7m/rQtfJaLpRMk7pUGgvekOg=
-github.com/spdx/tools-golang v0.5.2/go.mod h1:/ETOahiAo96Ob0/RAIBmFZw6XN0yTnyr/uFZm2NTMhI=
+github.com/spdx/tools-golang v0.5.3 h1:ialnHeEYUC4+hkm5vJm4qz2x+oEJbS0mAMFrNXdQraY=
+github.com/spdx/tools-golang v0.5.3/go.mod h1:/ETOahiAo96Ob0/RAIBmFZw6XN0yTnyr/uFZm2NTMhI=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
 github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
@@ -120,11 +120,12 @@ github.com/zclconf/go-cty v1.10.0/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uU
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
 golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
 golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
 golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
-golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 h1:MGwJjxBy0HJshjDNfLsYO8xppfqWlA5ZT9OhtUUhTNw=
-golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
+golang.org/x/exp v0.0.0-20230801115018-d63ba01acd4b h1:r+vk0EmXNmekl0S0BascoeeoHk/L7wmaW2QF90K+kYI=
+golang.org/x/exp v0.0.0-20230801115018-d63ba01acd4b/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
 golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
@@ -135,6 +136,7 @@ golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLL
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
 golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
 golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
 golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50=
@@ -150,11 +152,11 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -162,6 +164,7 @@ golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
 golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
 golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
 golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
 golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c=
@@ -172,6 +175,7 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
 golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
diff --git a/internal/output/form.go b/internal/output/form.go
new file mode 100644
index 0000000..f5e5a4d
--- /dev/null
+++ b/internal/output/form.go
@@ -0,0 +1,10 @@
+package output
+
+// Form returns the singular or plural form that should be used based on the given count
+func Form(count int, singular, plural string) string {
+	if count == 1 {
+		return singular
+	}
+
+	return plural
+}
diff --git a/internal/output/githubannotation.go b/internal/output/githubannotation.go
new file mode 100644
index 0000000..72133eb
--- /dev/null
+++ b/internal/output/githubannotation.go
@@ -0,0 +1,46 @@
+package output
+
+import (
+	"fmt"
+	"io"
+	"log"
+	"os"
+	"path/filepath"
+	"strings"
+
+	"github.com/google/osv-scanner/pkg/models"
+)
+
+// PrintGHAnnotationReport prints Github specific annotations to outputWriter
+func PrintGHAnnotationReport(vulnResult *models.VulnerabilityResults, outputWriter io.Writer) error {
+	flattened := vulnResult.Flatten()
+
+	// TODO: Also support last affected
+	groupFixedVersions := GroupFixedVersions(flattened)
+	workingDir, err := os.Getwd()
+	if err != nil {
+		log.Panicf("can't get working dir: %v", err)
+	}
+
+	for _, source := range vulnResult.Results {
+		// TODO: Support docker images
+
+		var artifactPath string
+		var err error
+		artifactPath, err = filepath.Rel(workingDir, source.Source.Path)
+		if err != nil {
+			artifactPath = source.Source.Path
+		}
+
+		remediationTable := CreateSourceRemediationTable(source, groupFixedVersions)
+
+		renderedTable := remediationTable.Render()
+		// This is required as github action annotations must be on the same terminal line
+		// so we URL encode the new line character
+		renderedTable = strings.ReplaceAll(renderedTable, "\n", "%0A")
+		// Prepend the table with a new line to look nicer in the output
+		fmt.Fprintf(outputWriter, "::error file=%s::%s%s", artifactPath, artifactPath, "%0A"+renderedTable)
+	}
+
+	return nil
+}
diff --git a/internal/output/sarif.go b/internal/output/sarif.go
index bc1f14e..0a5274d 100644
--- a/internal/output/sarif.go
+++ b/internal/output/sarif.go
@@ -3,6 +3,7 @@ package output
 import (
 	"fmt"
 	"io"
+	"log"
 	"os"
 	"path/filepath"
 	"strings"
@@ -43,7 +44,7 @@ func GroupFixedVersions(flattened []models.VulnerabilityFlattened) map[string][]
 // CreateSourceRemediationTable creates a vulnerability table which includes the fixed versions for a specific source file
 func CreateSourceRemediationTable(source models.PackageSource, groupFixedVersions map[string][]string) table.Writer {
 	remediationTable := table.NewWriter()
-	remediationTable.AppendHeader(table.Row{"Package", "Vulnerability ID", "Current Version", "Fixed Version"})
+	remediationTable.AppendHeader(table.Row{"Package", "Vulnerability ID", "CVSS", "Current Version", "Fixed Version"})
 
 	for _, pv := range source.Packages {
 		for _, group := range pv.Groups {
@@ -56,6 +57,7 @@ func CreateSourceRemediationTable(source models.PackageSource, groupFixedVersion
 			remediationTable.AppendRow(table.Row{
 				pv.Package.Name,
 				strings.Join(vulnIDs, "\n"),
+				MaxSeverity(group, pv),
 				pv.Package.Version,
 				strings.Join(fixedVersions, "\n")})
 		}
@@ -78,18 +80,16 @@ func PrintSARIFReport(vulnResult *models.VulnerabilityResults, outputWriter io.W
 
 	// TODO: Also support last affected
 	groupFixedVersions := GroupFixedVersions(flattened)
-	workingDir, workingDirErr := os.Getwd()
-
+	workingDir, err := os.Getwd()
+	if err != nil {
+		log.Panicf("can't get working dir: %v", err)
+	}
 	for _, source := range vulnResult.Results {
 		// TODO: Support docker images
 
 		var artifactPath string
-		if workingDirErr == nil {
-			artifactPath, err = filepath.Rel(workingDir, source.Source.Path)
-			if err != nil {
-				artifactPath = source.Source.Path
-			}
-		} else {
+		artifactPath, err = filepath.Rel(workingDir, source.Source.Path)
+		if err != nil {
 			artifactPath = source.Source.Path
 		}
 		run.AddDistinctArtifact(artifactPath)
diff --git a/internal/output/table.go b/internal/output/table.go
index 11fc029..871f9d6 100644
--- a/internal/output/table.go
+++ b/internal/output/table.go
@@ -3,6 +3,8 @@ package output
 import (
 	"fmt"
 	"io"
+	"log"
+	"math"
 	"os"
 	"path/filepath"
 	"strings"
@@ -69,15 +71,16 @@ type tbInnerResponse struct {
 func tableBuilderInner(vulnResult *models.VulnerabilityResults, addStyling bool, calledVulns bool) []tbInnerResponse {
 	allOutputRows := []tbInnerResponse{}
 	// Working directory used to simplify path
-	workingDir, workingDirErr := os.Getwd()
+	workingDir, err := os.Getwd()
+	if err != nil {
+		log.Panicf("can't get working dir: %v", err)
+	}
 	for _, sourceRes := range vulnResult.Results {
 		for _, pkg := range sourceRes.Packages {
 			source := sourceRes.Source
-			if workingDirErr == nil {
-				sourcePath, err := filepath.Rel(workingDir, source.Path)
-				if err == nil { // Simplify the path if possible
-					source.Path = sourcePath
-				}
+			sourcePath, err := filepath.Rel(workingDir, source.Path)
+			if err == nil { // Simplify the path if possible
+				source.Path = sourcePath
 			}
 
 			// Merge groups into the same row
@@ -100,32 +103,7 @@ func tableBuilderInner(vulnResult *models.VulnerabilityResults, addStyling bool,
 				}
 
 				outputRow = append(outputRow, strings.Join(links, "\n"))
-
-				var outputSeverities []string
-				for _, vulnID := range group.IDs {
-					var severities []models.Severity
-					for _, vuln := range pkg.Vulnerabilities {
-						if vuln.ID == vulnID {
-							severities = vuln.Severity
-						}
-					}
-					for _, severity := range severities {
-						var outputSeverity string
-						switch severity.Type {
-						case models.SeverityCVSSV2:
-							numericSeverity, _ := v2_metric.NewBase().Decode(severity.Score)
-							outputSeverity = fmt.Sprintf("%v", numericSeverity.Score())
-						case models.SeverityCVSSV3:
-							numericSeverity, _ := v3_metric.NewBase().Decode(severity.Score)
-							outputSeverity = fmt.Sprintf("%v", numericSeverity.Score())
-						default:
-							outputSeverity = severity.Score
-						}
-
-						outputSeverities = append(outputSeverities, outputSeverity)
-					}
-				}
-				outputRow = append(outputRow, strings.Join(outputSeverities, "\n"))
+				outputRow = append(outputRow, MaxSeverity(group, pkg))
 
 				if pkg.Package.Ecosystem == "GIT" {
 					outputRow = append(outputRow, "GIT", pkg.Package.Version, pkg.Package.Version)
@@ -145,3 +123,31 @@ func tableBuilderInner(vulnResult *models.VulnerabilityResults, addStyling bool,
 
 	return allOutputRows
 }
+
+func MaxSeverity(group models.GroupInfo, pkg models.PackageVulns) string {
+	var maxSeverity float64
+	for _, vulnID := range group.IDs {
+		var severities []models.Severity
+		for _, vuln := range pkg.Vulnerabilities {
+			if vuln.ID == vulnID {
+				severities = vuln.Severity
+			}
+		}
+		for _, severity := range severities {
+			switch severity.Type {
+			case models.SeverityCVSSV2:
+				numericSeverity, _ := v2_metric.NewBase().Decode(severity.Score)
+				maxSeverity = math.Max(maxSeverity, numericSeverity.Score())
+			case models.SeverityCVSSV3:
+				numericSeverity, _ := v3_metric.NewBase().Decode(severity.Score)
+				maxSeverity = math.Max(maxSeverity, numericSeverity.Score())
+			}
+		}
+	}
+
+	if maxSeverity == 0 {
+		return ""
+	}
+
+	return fmt.Sprintf("%v", maxSeverity)
+}
diff --git a/pkg/models/purl_to_package.go b/pkg/models/purl_to_package.go
index 4641dc8..8ca28e6 100644
--- a/pkg/models/purl_to_package.go
+++ b/pkg/models/purl_to_package.go
@@ -4,18 +4,40 @@ import (
 	"github.com/package-url/packageurl-go"
 )
 
-var purlEcosystems = map[string]Ecosystem{
-	"cargo":    EcosystemCratesIO,
-	"deb":      EcosystemDebian,
-	"hex":      EcosystemHex,
-	"golang":   EcosystemGo,
-	"maven":    EcosystemMaven,
-	"nuget":    EcosystemNuGet,
-	"npm":      EcosystemNPM,
-	"composer": EcosystemPackagist,
-	"generic":  EcosystemOSSFuzz,
-	"pypi":     EcosystemPyPI,
-	"gem":      EcosystemRubyGems,
+// used like so: purlEcosystems[PkgURL.Type][PkgURL.Namespace]
+// * means it should match any namespace string
+var purlEcosystems = map[string]map[string]Ecosystem{
+	"apk":      {"alpine": EcosystemAlpine},
+	"cargo":    {"*": EcosystemCratesIO},
+	"deb":      {"debian": EcosystemDebian},
+	"hex":      {"*": EcosystemHex},
+	"golang":   {"*": EcosystemGo},
+	"maven":    {"*": EcosystemMaven},
+	"nuget":    {"*": EcosystemNuGet},
+	"npm":      {"*": EcosystemNPM},
+	"composer": {"*": EcosystemPackagist},
+	"generic":  {"*": EcosystemOSSFuzz},
+	"pypi":     {"*": EcosystemPyPI},
+	"gem":      {"*": EcosystemRubyGems},
+}
+
+func getPURLEcosystem(pkgURL packageurl.PackageURL) Ecosystem {
+	ecoMap, ok := purlEcosystems[pkgURL.Type]
+	if !ok {
+		return Ecosystem(pkgURL.Type + ":" + pkgURL.Namespace)
+	}
+
+	wildcardRes, hasWildcard := ecoMap["*"]
+	if hasWildcard {
+		return wildcardRes
+	}
+
+	ecosystem, ok := ecoMap[pkgURL.Namespace]
+	if !ok {
+		return Ecosystem(pkgURL.Type + ":" + pkgURL.Namespace)
+	}
+
+	return ecosystem
 }
 
 // PURLToPackage converts a Package URL string to models.PackageInfo
@@ -24,10 +46,7 @@ func PURLToPackage(purl string) (PackageInfo, error) {
 	if err != nil {
 		return PackageInfo{}, err
 	}
-	ecosystem, ok := purlEcosystems[parsedPURL.Type]
-	if !ok {
-		ecosystem = Ecosystem(parsedPURL.Type)
-	}
+	ecosystem := getPURLEcosystem(parsedPURL)
 
 	// PackageInfo expects the full namespace in the name for ecosystems that specify it.
 	name := parsedPURL.Name
diff --git a/pkg/models/results.go b/pkg/models/results.go
index dbb1520..4bc7d76 100644
--- a/pkg/models/results.go
+++ b/pkg/models/results.go
@@ -100,6 +100,10 @@ func (v *Vulnerability) FixedVersions() map[Package][]string {
 			for _, e := range r.Events {
 				if e.Fixed != "" {
 					output[packageKey] = append(output[packageKey], e.Fixed)
+					if strings.Contains(string(packageKey.Ecosystem), ":") {
+						packageKey.Ecosystem = Ecosystem(strings.Split(string(packageKey.Ecosystem), ":")[0])
+					}
+					output[packageKey] = append(output[packageKey], e.Fixed)
 				}
 			}
 		}
diff --git a/pkg/osvscanner/osvscanner.go b/pkg/osvscanner/osvscanner.go
index d3c8d3f..bb891cd 100644
--- a/pkg/osvscanner/osvscanner.go
+++ b/pkg/osvscanner/osvscanner.go
@@ -9,6 +9,7 @@ import (
 	"path/filepath"
 	"strings"
 
+	"github.com/google/osv-scanner/internal/output"
 	"github.com/google/osv-scanner/internal/sbom"
 	"github.com/google/osv-scanner/pkg/config"
 	"github.com/google/osv-scanner/pkg/lockfile"
@@ -205,7 +206,13 @@ func scanLockfile(r reporter.Reporter, query *osv.BatchedQuery, path string, par
 		parsedAsComment = fmt.Sprintf("as a %s ", parseAs)
 	}
 
-	r.PrintText(fmt.Sprintf("Scanned %s file %sand found %d packages\n", path, parsedAsComment, len(parsedLockfile.Packages)))
+	r.PrintText(fmt.Sprintf(
+		"Scanned %s file %sand found %d %s\n",
+		path,
+		parsedAsComment,
+		len(parsedLockfile.Packages),
+		output.Form(len(parsedLockfile.Packages), "package", "packages"),
+	))
 
 	for _, pkgDetail := range parsedLockfile.Packages {
 		pkgDetailQuery := osv.MakePkgRequest(pkgDetail)
@@ -273,9 +280,19 @@ func scanSBOMFile(r reporter.Reporter, query *osv.BatchedQuery, path string, fro
 
 				continue
 			}
-			r.PrintText(fmt.Sprintf("Scanned %s as %s SBOM and found %d packages\n", path, provider.Name(), count))
+			r.PrintText(fmt.Sprintf(
+				"Scanned %s as %s SBOM and found %d %s\n",
+				path,
+				provider.Name(),
+				count,
+				output.Form(count, "package", "packages"),
+			))
 			if ignoredCount > 0 {
-				r.PrintText(fmt.Sprintf("Ignored %d packages with invalid PURLs\n", ignoredCount))
+				r.PrintText(fmt.Sprintf(
+					"Ignored %d %s with invalid PURLs\n",
+					ignoredCount,
+					output.Form(ignoredCount, "package", "packages"),
+				))
 			}
 
 			return nil
@@ -378,7 +395,11 @@ func scanDebianDocker(r reporter.Reporter, query *osv.BatchedQuery, dockerImageN
 		query.Queries = append(query.Queries, pkgDetailsQuery)
 		packages += 1
 	}
-	r.PrintText(fmt.Sprintf("Scanned docker image with %d packages\n", packages))
+	r.PrintText(fmt.Sprintf(
+		"Scanned docker image with %d %s\n",
+		packages,
+		output.Form(packages, "package", "packages"),
+	))
 
 	return nil
 }
@@ -554,7 +575,11 @@ func DoScan(actions ScannerActions, r reporter.Reporter) (models.VulnerabilityRe
 
 	filtered := filterResults(r, &vulnerabilityResults, &configManager)
 	if filtered > 0 {
-		r.PrintText(fmt.Sprintf("Filtered %d vulnerabilities from output\n", filtered))
+		r.PrintText(fmt.Sprintf(
+			"Filtered %d %s from output\n",
+			filtered,
+			output.Form(filtered, "vulnerability", "vulnerabilities"),
+		))
 	}
 
 	// if vulnerability exists it should return error
diff --git a/pkg/reporter/format.go b/pkg/reporter/format.go
index ab02e40..9cb1f70 100644
--- a/pkg/reporter/format.go
+++ b/pkg/reporter/format.go
@@ -5,7 +5,7 @@ import (
 	"io"
 )
 
-var format = []string{"table", "json", "markdown", "sarif"}
+var format = []string{"table", "json", "markdown", "sarif", "gh-annotations"}
 
 func Format() []string {
 	return format
@@ -23,6 +23,8 @@ func New(format string, stdout, stderr io.Writer, terminalWidth int) (Reporter,
 		return NewTableReporter(stdout, stderr, true, terminalWidth), nil
 	case "sarif":
 		return NewSarifReporter(stdout, stderr), nil
+	case "gh-annotations":
+		return NewGHAnnotationsReporter(stdout, stderr), nil
 	default:
 		return nil, fmt.Errorf("%v is not a valid format", format)
 	}
diff --git a/pkg/reporter/gh-annotations_reporter.go b/pkg/reporter/gh-annotations_reporter.go
new file mode 100644
index 0000000..01ca072
--- /dev/null
+++ b/pkg/reporter/gh-annotations_reporter.go
@@ -0,0 +1,40 @@
+package reporter
+
+import (
+	"fmt"
+	"io"
+
+	"github.com/google/osv-scanner/internal/output"
+	"github.com/google/osv-scanner/pkg/models"
+)
+
+type GHAnnotationsReporter struct {
+	hasPrintedError bool
+	stdout          io.Writer
+	stderr          io.Writer
+}
+
+func NewGHAnnotationsReporter(stdout io.Writer, stderr io.Writer) *GHAnnotationsReporter {
+	return &GHAnnotationsReporter{
+		stdout:          stdout,
+		stderr:          stderr,
+		hasPrintedError: false,
+	}
+}
+
+func (r *GHAnnotationsReporter) PrintError(msg string) {
+	fmt.Fprint(r.stderr, msg)
+	r.hasPrintedError = true
+}
+
+func (r *GHAnnotationsReporter) HasPrintedError() bool {
+	return r.hasPrintedError
+}
+
+func (r *GHAnnotationsReporter) PrintText(msg string) {
+	fmt.Fprint(r.stderr, msg)
+}
+
+func (r *GHAnnotationsReporter) PrintResult(vulnResult *models.VulnerabilityResults) error {
+	return output.PrintGHAnnotationReport(vulnResult, r.stderr)
+}


Test patch:
diff --git a/cmd/osv-scanner/main_test.go b/cmd/osv-scanner/main_test.go
index a2671d9..16b64d1 100644
--- a/cmd/osv-scanner/main_test.go
+++ b/cmd/osv-scanner/main_test.go
@@ -131,7 +131,7 @@ func TestRun(t *testing.T) {
 			wantExitCode: 0,
 			wantStdout: `
 				Scanning dir ./fixtures/locks-many/composer.lock
-				Scanned %%/fixtures/locks-many/composer.lock file and found 1 packages
+				Scanned %%/fixtures/locks-many/composer.lock file and found 1 package
 				No vulnerabilities found
 			`,
 			wantStderr: "",
@@ -139,14 +139,16 @@ func TestRun(t *testing.T) {
 		// one specific supported sbom with vulns
 		{
 			name:         "",
-			args:         []string{"", "--config=./fixtures/osv-scanner-empty-config.toml", "./fixtures/sbom-insecure/postgres-stretch.cdx.xml"},
+			args:         []string{"", "--config=./fixtures/osv-scanner-empty-config.toml", "./fixtures/sbom-insecure/"},
 			wantExitCode: 1,
 			wantStdout: `
-				Scanning dir ./fixtures/sbom-insecure/postgres-stretch.cdx.xml
+				Scanning dir ./fixtures/sbom-insecure/
+				Scanned %%/fixtures/sbom-insecure/alpine.cdx.xml as CycloneDX SBOM and found 15 packages
 				Scanned %%/fixtures/sbom-insecure/postgres-stretch.cdx.xml as CycloneDX SBOM and found 136 packages
 				+-------------------------------------+------+-----------+--------------------------------+------------------------------------+-------------------------------------------------+
 				| OSV URL                             | CVSS | ECOSYSTEM | PACKAGE                        | VERSION                            | SOURCE                                          |
 				+-------------------------------------+------+-----------+--------------------------------+------------------------------------+-------------------------------------------------+
+				| https://osv.dev/CVE-2022-37434      |      | Alpine    | zlib                           | 1.2.10-r2                          | fixtures/sbom-insecure/alpine.cdx.xml           |
 				| https://osv.dev/DLA-3022-1          |      | Debian    | dpkg                           | 1.18.25                            | fixtures/sbom-insecure/postgres-stretch.cdx.xml |
 				| https://osv.dev/GHSA-v95c-p5hm-xq8f | 6    | Go        | github.com/opencontainers/runc | v1.0.1                             | fixtures/sbom-insecure/postgres-stretch.cdx.xml |
 				| https://osv.dev/GO-2022-0274        |      |           |                                |                                    |                                                 |
@@ -182,14 +184,14 @@ func TestRun(t *testing.T) {
 			wantExitCode: 0,
 			wantStdout: `
 				Scanning dir ./fixtures/locks-many
-				Scanned %%/fixtures/locks-many/Gemfile.lock file and found 1 packages
+				Scanned %%/fixtures/locks-many/Gemfile.lock file and found 1 package
 				Scanned %%/fixtures/locks-many/alpine.cdx.xml as CycloneDX SBOM and found 15 packages
-				Scanned %%/fixtures/locks-many/composer.lock file and found 1 packages
-				Scanned %%/fixtures/locks-many/package-lock.json file and found 1 packages
-				Scanned %%/fixtures/locks-many/yarn.lock file and found 1 packages
+				Scanned %%/fixtures/locks-many/composer.lock file and found 1 package
+				Scanned %%/fixtures/locks-many/package-lock.json file and found 1 package
+				Scanned %%/fixtures/locks-many/yarn.lock file and found 1 package
 				Loaded filter from: %%/fixtures/locks-many/osv-scanner.toml
 				GHSA-whgm-jr23-g3j9 has been filtered out because: Test manifest file
-				Filtered 1 vulnerabilities from output
+				Filtered 1 vulnerability from output
 				No vulnerabilities found
 			`,
 			wantStderr: "",
@@ -201,8 +203,8 @@ func TestRun(t *testing.T) {
 			wantExitCode: 127,
 			wantStdout: `
 				Scanning dir ./fixtures/locks-many-with-invalid
-				Scanned %%/fixtures/locks-many-with-invalid/Gemfile.lock file and found 1 packages
-				Scanned %%/fixtures/locks-many-with-invalid/yarn.lock file and found 1 packages
+				Scanned %%/fixtures/locks-many-with-invalid/Gemfile.lock file and found 1 package
+				Scanned %%/fixtures/locks-many-with-invalid/yarn.lock file and found 1 package
 			`,
 			wantStderr: `
 				Attempted to scan lockfile but failed: %%/fixtures/locks-many-with-invalid/composer.lock
@@ -215,7 +217,7 @@ func TestRun(t *testing.T) {
 			wantExitCode: 0,
 			wantStdout: `
 				Scanning dir ./fixtures/locks-one-with-nested
-				Scanned %%/fixtures/locks-one-with-nested/yarn.lock file and found 1 packages
+				Scanned %%/fixtures/locks-one-with-nested/yarn.lock file and found 1 package
 				No vulnerabilities found
 			`,
 			wantStderr: "",
@@ -227,8 +229,8 @@ func TestRun(t *testing.T) {
 			wantExitCode: 0,
 			wantStdout: `
 				Scanning dir ./fixtures/locks-one-with-nested
-				Scanned %%/fixtures/locks-one-with-nested/nested/composer.lock file and found 1 packages
-				Scanned %%/fixtures/locks-one-with-nested/yarn.lock file and found 1 packages
+				Scanned %%/fixtures/locks-one-with-nested/nested/composer.lock file and found 1 package
+				Scanned %%/fixtures/locks-one-with-nested/yarn.lock file and found 1 package
 				No vulnerabilities found
 			`,
 			wantStderr: "",
@@ -240,8 +242,8 @@ func TestRun(t *testing.T) {
 			wantExitCode: 0,
 			wantStdout: `
 				Scanning dir ./fixtures/locks-gitignore
-				Scanned %%/fixtures/locks-gitignore/Gemfile.lock file and found 1 packages
-				Scanned %%/fixtures/locks-gitignore/subdir/yarn.lock file and found 1 packages
+				Scanned %%/fixtures/locks-gitignore/Gemfile.lock file and found 1 package
+				Scanned %%/fixtures/locks-gitignore/subdir/yarn.lock file and found 1 package
 				No vulnerabilities found
 			`,
 			wantStderr: "",
@@ -253,14 +255,14 @@ func TestRun(t *testing.T) {
 			wantExitCode: 0,
 			wantStdout: `
 				Scanning dir ./fixtures/locks-gitignore
-				Scanned %%/fixtures/locks-gitignore/Gemfile.lock file and found 1 packages
-				Scanned %%/fixtures/locks-gitignore/composer.lock file and found 1 packages
-				Scanned %%/fixtures/locks-gitignore/ignored/Gemfile.lock file and found 1 packages
-				Scanned %%/fixtures/locks-gitignore/ignored/yarn.lock file and found 1 packages
-				Scanned %%/fixtures/locks-gitignore/subdir/Gemfile.lock file and found 1 packages
-				Scanned %%/fixtures/locks-gitignore/subdir/composer.lock file and found 1 packages
-				Scanned %%/fixtures/locks-gitignore/subdir/yarn.lock file and found 1 packages
-				Scanned %%/fixtures/locks-gitignore/yarn.lock file and found 1 packages
+				Scanned %%/fixtures/locks-gitignore/Gemfile.lock file and found 1 package
+				Scanned %%/fixtures/locks-gitignore/composer.lock file and found 1 package
+				Scanned %%/fixtures/locks-gitignore/ignored/Gemfile.lock file and found 1 package
+				Scanned %%/fixtures/locks-gitignore/ignored/yarn.lock file and found 1 package
+				Scanned %%/fixtures/locks-gitignore/subdir/Gemfile.lock file and found 1 package
+				Scanned %%/fixtures/locks-gitignore/subdir/composer.lock file and found 1 package
+				Scanned %%/fixtures/locks-gitignore/subdir/yarn.lock file and found 1 package
+				Scanned %%/fixtures/locks-gitignore/yarn.lock file and found 1 package
 				No vulnerabilities found
 			`,
 			wantStderr: "",
@@ -277,7 +279,7 @@ func TestRun(t *testing.T) {
 			`,
 			wantStderr: `
 				Scanning dir ./fixtures/locks-many/composer.lock
-				Scanned %%/fixtures/locks-many/composer.lock file and found 1 packages
+				Scanned %%/fixtures/locks-many/composer.lock file and found 1 package
 			`,
 		},
 		{
@@ -291,7 +293,7 @@ func TestRun(t *testing.T) {
 			`,
 			wantStderr: `
 				Scanning dir ./fixtures/locks-many/composer.lock
-				Scanned %%/fixtures/locks-many/composer.lock file and found 1 packages
+				Scanned %%/fixtures/locks-many/composer.lock file and found 1 package
 			`,
 		},
 		// output format: sarif
@@ -326,7 +328,7 @@ func TestRun(t *testing.T) {
 			`,
 			wantStderr: `
 				Scanning dir ./fixtures/locks-many/composer.lock
-				Scanned %%/fixtures/locks-many/composer.lock file and found 1 packages
+				Scanned %%/fixtures/locks-many/composer.lock file and found 1 package
 			`,
 		},
 		{
@@ -367,7 +369,7 @@ func TestRun(t *testing.T) {
 									"ruleIndex": 0,
 									"level": "warning",
 									"message": {
-										"text": "+-----------+-------------------------------------+-----------------+---------------+\n| PACKAGE \u0026nbsp; | VULNERABILITY ID \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp;| CURRENT VERSION | FIXED VERSION |\n+-----------+-------------------------------------+-----------------+---------------+\n| ansi-html | https://osv.dev/GHSA-whgm-jr23-g3j9 | 0.0.1 \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; | 0.0.8 \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; |\n+-----------+-------------------------------------+-----------------+---------------+"
+										"text": "+-----------+-------------------------------------+------+-----------------+---------------+\n| PACKAGE \u0026nbsp; | VULNERABILITY ID \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp;| CVSS | CURRENT VERSION | FIXED VERSION |\n+-----------+-------------------------------------+------+-----------------+---------------+\n| ansi-html | https://osv.dev/GHSA-whgm-jr23-g3j9 | 7.5 \u0026nbsp;| 0.0.1 \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; | 0.0.8 \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; |\n+-----------+-------------------------------------+------+-----------------+---------------+"
 									},
 									"locations": [
 										{
@@ -386,7 +388,7 @@ func TestRun(t *testing.T) {
 			`,
 			wantStderr: `
 				Scanning dir ./fixtures/locks-many/package-lock.json
-				Scanned %%/fixtures/locks-many/package-lock.json file and found 1 packages
+				Scanned %%/fixtures/locks-many/package-lock.json file and found 1 package
 			`,
 		},
 		// output format: markdown table
@@ -396,7 +398,7 @@ func TestRun(t *testing.T) {
 			wantExitCode: 0,
 			wantStdout: `
 				Scanning dir ./fixtures/locks-many/composer.lock
-				Scanned %%/fixtures/locks-many/composer.lock file and found 1 packages
+				Scanned %%/fixtures/locks-many/composer.lock file and found 1 package
 				No vulnerabilities found
 			`,
 			wantStderr: "",
@@ -436,7 +438,7 @@ func TestRun_LockfileWithExplicitParseAs(t *testing.T) {
 			},
 			wantExitCode: 0,
 			wantStdout: `
-				Scanned %%/fixtures/locks-many/composer.lock file and found 1 packages
+				Scanned %%/fixtures/locks-many/composer.lock file and found 1 package
 				No vulnerabilities found
 			`,
 			wantStderr: "",
@@ -479,7 +481,7 @@ func TestRun_LockfileWithExplicitParseAs(t *testing.T) {
 			},
 			wantExitCode: 1,
 			wantStdout: `
-				Scanned %%/fixtures/locks-insecure/my-package-lock.json file as a package-lock.json and found 1 packages
+				Scanned %%/fixtures/locks-insecure/my-package-lock.json file as a package-lock.json and found 1 package
 				Scanning dir ./fixtures/locks-insecure
 				Scanned %%/fixtures/locks-insecure/composer.lock file and found 0 packages
 				+-------------------------------------+------+-----------+-----------+---------+----------------------------------------------+
@@ -501,8 +503,8 @@ func TestRun_LockfileWithExplicitParseAs(t *testing.T) {
 			},
 			wantExitCode: 1,
 			wantStdout: `
-				Scanned %%/fixtures/locks-insecure/my-package-lock.json file as a package-lock.json and found 1 packages
-				Scanned %%/fixtures/locks-insecure/my-yarn.lock file as a yarn.lock and found 1 packages
+				Scanned %%/fixtures/locks-insecure/my-package-lock.json file as a package-lock.json and found 1 package
+				Scanned %%/fixtures/locks-insecure/my-yarn.lock file as a yarn.lock and found 1 package
 				Scanning dir ./fixtures/locks-insecure
 				Scanned %%/fixtures/locks-insecure/composer.lock file and found 0 packages
 				+-------------------------------------+------+-----------+-----------+---------+----------------------------------------------+
@@ -524,8 +526,8 @@ func TestRun_LockfileWithExplicitParseAs(t *testing.T) {
 			},
 			wantExitCode: 1,
 			wantStdout: `
-				Scanned %%/fixtures/locks-insecure/my-yarn.lock file as a yarn.lock and found 1 packages
-				Scanned %%/fixtures/locks-insecure/my-package-lock.json file as a package-lock.json and found 1 packages
+				Scanned %%/fixtures/locks-insecure/my-yarn.lock file as a yarn.lock and found 1 package
+				Scanned %%/fixtures/locks-insecure/my-package-lock.json file as a package-lock.json and found 1 package
 				Scanning dir ./fixtures/locks-insecure
 				Scanned %%/fixtures/locks-insecure/composer.lock file and found 0 packages
 				+-------------------------------------+------+-----------+-----------+---------+----------------------------------------------+
@@ -577,7 +579,7 @@ func TestRun_LockfileWithExplicitParseAs(t *testing.T) {
 			},
 			wantExitCode: 0,
 			wantStdout: `
-				Scanned %%/fixtures/locks-many/installed file as a apk-installed and found 1 packages
+				Scanned %%/fixtures/locks-many/installed file as a apk-installed and found 1 package
 				No vulnerabilities found
 			`,
 			wantStderr: "",
@@ -592,7 +594,7 @@ func TestRun_LockfileWithExplicitParseAs(t *testing.T) {
 			},
 			wantExitCode: 0,
 			wantStdout: `
-				Scanned %%/fixtures/locks-many/status file as a dpkg-status and found 1 packages
+				Scanned %%/fixtures/locks-many/status file as a dpkg-status and found 1 package
 				No vulnerabilities found
 			`,
 			wantStderr: "",
diff --git a/internal/output/form_test.go b/internal/output/form_test.go
new file mode 100644
index 0000000..ef85b95
--- /dev/null
+++ b/internal/output/form_test.go
@@ -0,0 +1,60 @@
+package output_test
+
+import (
+	"testing"
+
+	"github.com/google/osv-scanner/internal/output"
+)
+
+func TestForm(t *testing.T) {
+	t.Parallel()
+
+	type args struct {
+		count    int
+		singular string
+		plural   string
+	}
+	tests := []struct {
+		name string
+		args args
+		want string
+	}{
+		{
+			name: "",
+			args: args{
+				count:    0,
+				singular: "package",
+				plural:   "packages",
+			},
+			want: "packages",
+		},
+		{
+			name: "",
+			args: args{
+				count:    1,
+				singular: "package",
+				plural:   "packages",
+			},
+			want: "package",
+		},
+		{
+			name: "",
+			args: args{
+				count:    2,
+				singular: "package",
+				plural:   "packages",
+			},
+			want: "packages",
+		},
+	}
+	for _, tt := range tests {
+		tt := tt
+		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
+
+			if got := output.Form(tt.args.count, tt.args.singular, tt.args.plural); got != tt.want {
+				t.Errorf("Form() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
diff --git a/internal/output/sarif_test.go b/internal/output/sarif_test.go
index a10bf94..c4714f9 100644
--- a/internal/output/sarif_test.go
+++ b/internal/output/sarif_test.go
@@ -1,8 +1,9 @@
-package output
+package output_test
 
 import (
 	"testing"
 
+	"github.com/google/osv-scanner/internal/output"
 	"github.com/google/osv-scanner/internal/testutility"
 	"github.com/google/osv-scanner/pkg/models"
 )
@@ -30,7 +31,7 @@ func TestGroupFixedVersions(t *testing.T) {
 		tt := tt
 		t.Run(tt.name, func(t *testing.T) {
 			t.Parallel()
-			got := GroupFixedVersions(tt.args.flattened)
+			got := output.GroupFixedVersions(tt.args.flattened)
 			testutility.AssertMatchFixtureJSON(t, tt.wantPath, got)
 		})
 	}
diff --git a/pkg/lockfile/helpers_test.go b/pkg/lockfile/helpers_test.go
index 3f50464..8e5c4e5 100644
--- a/pkg/lockfile/helpers_test.go
+++ b/pkg/lockfile/helpers_test.go
@@ -5,6 +5,7 @@ import (
 	"strings"
 	"testing"
 
+	"github.com/google/osv-scanner/internal/output"
 	"github.com/google/osv-scanner/pkg/lockfile"
 )
 
@@ -70,7 +71,12 @@ func expectPackages(t *testing.T, actualPackages []lockfile.PackageDetails, expe
 	t.Helper()
 
 	if len(expectedPackages) != len(actualPackages) {
-		t.Errorf("Expected to get %d packages, but got %d", len(expectedPackages), len(actualPackages))
+		t.Errorf(
+			"Expected to get %d %s, but got %d",
+			len(expectedPackages),
+			output.Form(len(expectedPackages), "package", "packages"),
+			len(actualPackages),
+		)
 	}
 
 	missingActualPackages := findMissingPackages(actualPackages, expectedPackages)
diff --git a/pkg/lockfile/parse_test.go b/pkg/lockfile/parse_test.go
index 40f3b52..cdda673 100644
--- a/pkg/lockfile/parse_test.go
+++ b/pkg/lockfile/parse_test.go
@@ -7,6 +7,7 @@ import (
 	"strings"
 	"testing"
 
+	"github.com/google/osv-scanner/internal/output"
 	"github.com/google/osv-scanner/pkg/lockfile"
 )
 
@@ -30,8 +31,9 @@ func expectNumberOfParsersCalled(t *testing.T, numberOfParsersCalled int) {
 
 	if numberOfParsersCalled != count {
 		t.Errorf(
-			"Expected %d parsers to have been called, but had %d",
+			"Expected %d %s to have been called, but had %d",
 			count,
+			output.Form(count, "parser", "parsers"),
 			numberOfParsersCalled,
 		)
 	}
diff --git a/pkg/models/purl_to_package_test.go b/pkg/models/purl_to_package_test.go
index c6623e7..3106076 100644
--- a/pkg/models/purl_to_package_test.go
+++ b/pkg/models/purl_to_package_test.go
@@ -52,7 +52,7 @@ func TestPURLToPackage(t *testing.T) {
 			},
 		},
 		{
-			name: "valid Debian maven",
+			name: "valid PURL Debian",
 			args: args{
 				purl: "pkg:deb/debian/nginx@2.36.1-8+deb11u1",
 			},
@@ -62,6 +62,17 @@ func TestPURLToPackage(t *testing.T) {
 				Ecosystem: string(models.EcosystemDebian),
 			},
 		},
+		{
+			name: "valid PURL alpine",
+			args: args{
+				purl: "pkg:apk/alpine/zlib@1.2.13-r0?arch=x86_64upstream=zlib&distro=alpine-3.17.2",
+			},
+			want: models.PackageInfo{
+				Name:      "zlib",
+				Version:   "1.2.13-r0",
+				Ecosystem: string(models.EcosystemAlpine),
+			},
+		},
 		{
 			name: "invalid PURL",
 			args: args{