Repository: google/osv-scanner
Pull request #109 (2023-01-02 12:33:53)

docs: describe build process
I would suggest to document the release process, and in particular how to reproduce the builds locally.

another-rex: /gcbrun

Patch:
diff --git a/.github/workflows/goreleaser.yml b/.github/workflows/goreleaser.yml
index 9f5e01f..c6040a6 100644
--- a/.github/workflows/goreleaser.yml
+++ b/.github/workflows/goreleaser.yml
@@ -15,16 +15,27 @@ jobs:
     permissions:
       contents: write  # for goreleaser/goreleaser-action to create a GitHub release
     runs-on: ubuntu-latest
+    env:
+      # Required for buildx on docker 19.x
+      DOCKER_CLI_EXPERIMENTAL: "enabled"
     steps:
       - name: Checkout
         uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b # v3.2.0
         with:
           fetch-depth: 0
+      - uses: docker/setup-qemu-action@38836d31bd1e0f8f29b8c7ad0412da32020f15b3 # v2
+      - uses: docker/setup-buildx-action@39a1a82492fd1ad19af19d61b5f748e4cb6cd1af # v2
       - name: Set up Go
         uses: actions/setup-go@6edd4406fa81c3da01a34fa6f6343087c207a568 # v3.5.0
         with:
           go-version: 1.19
           check-latest: true
+      - name: ghcr-login
+        uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a # v1
+        with:
+          registry: ghcr.io
+          username: ${{ github.repository_owner }}
+          password: ${{ secrets.GITHUB_TOKEN }}
       - name: Run GoReleaser
         id: run-goreleaser
         uses: goreleaser/goreleaser-action@8f67e590f2d095516493f017008adc464e63adb1 # v4.1.0
@@ -50,4 +61,4 @@ jobs:
     uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.4.0
     with:
       base64-subjects: "${{ needs.goreleaser.outputs.hashes }}"
-      upload-assets: true # upload to a new release
\ No newline at end of file
+      upload-assets: true # upload to a new release
diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml
index 215fdc1..e5b8b24 100644
--- a/.github/workflows/lint.yaml
+++ b/.github/workflows/lint.yaml
@@ -14,7 +14,12 @@
 
 name: lint
 
-on: [pull_request]
+on:
+  push:
+    branches: [ main ]
+  pull_request:
+    # The branches below must be a subset of the branches above
+    branches: [ main ]
 
 permissions: read-all
 
diff --git a/.gitignore b/.gitignore
index e50ac48..4818851 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,3 @@
 .history/
-.vscode/
\ No newline at end of file
+.vscode/
+dist/
diff --git a/.goreleaser.yml b/.goreleaser.yml
index 579ceec..e64d706 100644
--- a/.goreleaser.yml
+++ b/.goreleaser.yml
@@ -28,7 +28,54 @@ builds:
       # - arm
       - arm64
     main: ./cmd/osv-scanner/main.go
-    binary: '{{ .ProjectName }}_v{{ .Version }}'
+
+dockers:
+  # Arch: amd64
+  - image_templates:
+      - 'ghcr.io/google/osv-scanner:{{ .Tag }}-amd64'
+    dockerfile: goreleaser.dockerfile
+    use: buildx
+    build_flag_templates:
+      - "--pull"
+      - "--label=org.opencontainers.image.title=osv-scanner"
+      - "--label=org.opencontainers.image.description=Vulnerability scanner written in Go which uses the data provided by https://osv.dev"
+      - "--label=org.opencontainers.image.licenses=Apache License 2.0"
+      - "--label=org.opencontainers.image.created={{.Date}}"
+      - "--label=org.opencontainers.image.name={{.ProjectName}}"
+      - "--label=org.opencontainers.image.revision={{.FullCommit}}"
+      - "--label=org.opencontainers.image.version={{.Version}}"
+      - "--label=org.opencontainers.image.source={{.GitURL}}"
+      - "--label=org.opencontainers.image.url={{.GitURL}}"
+      - "--platform=linux/amd64"
+  # Arch: arm64
+  - image_templates:
+      - 'ghcr.io/google/osv-scanner:{{ .Tag }}-arm64'
+    dockerfile: goreleaser.dockerfile
+    use: buildx
+    build_flag_templates:
+      - "--pull"
+      - "--label=org.opencontainers.image.title=osv-scanner"
+      - "--label=org.opencontainers.image.description=Vulnerability scanner written in Go which uses the data provided by https://osv.dev"
+      - "--label=org.opencontainers.image.licenses=Apache-2.0"
+      - "--label=org.opencontainers.image.created={{.Date}}"
+      - "--label=org.opencontainers.image.name={{.ProjectName}}"
+      - "--label=org.opencontainers.image.revision={{.FullCommit}}"
+      - "--label=org.opencontainers.image.version={{.Version}}"
+      - "--label=org.opencontainers.image.source={{.GitURL}}"
+      - "--label=org.opencontainers.image.url={{.GitURL}}"
+      - "--platform=linux/arm64"
+    goarch: arm64
+
+docker_manifests:
+  - name_template: 'ghcr.io/google/osv-scanner:{{ .Tag }}'
+    image_templates:
+      - 'ghcr.io/google/osv-scanner:{{ .Tag }}-amd64'
+      - 'ghcr.io/google/osv-scanner:{{ .Tag }}-arm64'
+  - name_template: 'ghcr.io/google/osv-scanner:latest'
+    image_templates:
+      - 'ghcr.io/google/osv-scanner:{{ .Tag }}-amd64'
+      - 'ghcr.io/google/osv-scanner:{{ .Tag }}-arm64'
+
 archives:
   - format: binary
     name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}'
diff --git a/README.md b/README.md
index 9cfb29f..4cb336b 100644
--- a/README.md
+++ b/README.md
@@ -27,6 +27,7 @@ The above all results in fewer, more actionable vulnerability notifications, whi
     - [Input an SBOM](#input-an-sbom)
     - [Input a lockfile](#input-a-lockfile)
     - [Scanning a Debian based docker image packages (preview)](#scanning-a-debian-based-docker-image-packages-preview)
+    - [Running in a Docker Container](running-in-a-docker-container)
   - [Configure OSV-Scanner](#configure-osv-scanner)
     - [Ignore vulnerabilities by ID](#ignore-vulnerabilities-by-id)
   - [JSON output](#json-output)
@@ -60,6 +61,19 @@ go install github.com/google/osv-scanner/cmd/osv-scanner@v1
 
 This requires Go 1.18+ to be installed.
 
+### Building
+
+You can build from source using [GoReleaser](https://goreleaser.com/) and running:
+
+```bash
+$ goreleaser build --rm-dist --single-target --snapshot
+```
+
+See GoReleaser [documentation](https://goreleaser.com/cmd/goreleaser_build/) for build options.
+
+You can also reproduce the downloadable builds by checking out the specific tag and running `goreleaser build`,
+using the same Go version as the one used during the actual release (see goreleaser workflows).
+
 ### SemVer Adherence
 
 All releases on the same Major version will be guaranteed to have backward compatible JSON output and CLI arguments.
@@ -109,22 +123,23 @@ osv-scanner --sbom=/path/to/your/sbom.json
 
 A wide range of lockfiles are supported by utilizing this [lockfile package](https://github.com/google/osv-scanner/tree/main/pkg/lockfile). This is the current list of supported lockfiles:
 
+- `buildscript-gradle.lockfile`
 - `Cargo.lock`
-- `package-lock.json`
-- `packages.lock.json`
-- `yarn.lock`
-- `pnpm-lock.yaml`
 - `composer.lock`
 - `Gemfile.lock`
 - `go.mod`
+- `gradle.lockfile`
 - `mix.lock`
+- `package-lock.json`
+- `packages.lock.json`
+- `Pipfile.lock`
+- `pnpm-lock.yaml`
 - `poetry.lock`
-- `pubspec.lock`
 - `pom.xml`[\*](https://github.com/google/osv-scanner/issues/35)
+- `pubspec.lock`
 - `requirements.txt`[\*](https://github.com/google/osv-scanner/issues/34)
-- `gradle.lockfile`
-- `buildscript-gradle.lockfile`
-- `Pipfile.lock`
+- `yarn.lock`
+- `/lib/apk/db/installed` (Alpine)
 
 #### Example
 
@@ -148,6 +163,27 @@ This currently does not scan the filesystem of the Docker container, and has var
 osv-scanner --docker image_name:latest
 ```
 
+### Running in a Docker Container
+
+The simplest way to get the osv-scanner docker image is to pull from GitHub Container Registry:
+
+```bash
+docker pull ghcr.io/google/osv-scanner:latest
+```
+
+Once you have the image, you can test that it works by running:
+
+```bash
+docker run -it ghcr.io/google/osv-scanner -h
+```
+
+Finally, to run it, mount the directory you want to scan to `/src` and pass the
+appropriate osv-scanner flags:
+
+```bash
+docker run -it -v ${PWD}:/src ghcr.io/google/osv-scanner -L /src/go.mod
+```
+
 ## Configure OSV-Scanner
 
 To configure scanning, place an osv-scanner.toml file in the scanned file's directory. To override this osv-scanner.toml file, pass the `--config=/path/to/config.toml` flag with the path to the configuration you want to apply instead.
@@ -189,7 +225,7 @@ When using the --json flag, only the JSON output will be printed to stdout, with
       },
       "packages": [
         {
-          "Package": {
+          "package": {
             "name": "github.com/gogo/protobuf",
             "version": "1.3.1",
             "ecosystem": "Go"
@@ -231,7 +267,7 @@ When using the --json flag, only the JSON output will be printed to stdout, with
       },
       "packages": [
         {
-          "Package": {
+          "package": {
             "name": "regex",
             "version": "1.5.1",
             "ecosystem": "crates.io"
diff --git a/action.yml b/action.yml
index 34b0916..f91f74f 100644
--- a/action.yml
+++ b/action.yml
@@ -1,13 +1,20 @@
 name: 'osv-scanner'
 description: 'Scans your directory against the OSV database'
 inputs:
-  to-scan: 
+  to-scan:
     description: 'Directory to scan'
     required: true
     default: '/github/workspace'
+  version:
+    description: 'osv-scanner version to use'
+    required: true
+  arch:
+    description: 'osv-scanner architecture'
+    required: false
+    default: 'amd64'
 runs:
   using: 'docker'
-  image: 'Dockerfile'
+  image: 'ghcr.io/google/osv-scanner:${{ inputs.version }}-${{ inputs.arch }}'
   args:
     - '--skip-git'
-    - ${{ inputs.to-scan }}
\ No newline at end of file
+    - ${{ inputs.to-scan }}
diff --git a/go.mod b/go.mod
index b3116c1..bb8fe8e 100644
--- a/go.mod
+++ b/go.mod
@@ -13,6 +13,8 @@ require (
 	golang.org/x/exp v0.0.0-20221212164502-fae10dda9338
 	golang.org/x/mod v0.7.0
 	golang.org/x/term v0.3.0
+	golang.org/x/tools v0.4.1-0.20221217013628-b4dfc36097e2
+	golang.org/x/vuln v0.0.0-20230108212441-ff355248a728
 	gopkg.in/yaml.v2 v2.4.0
 )
 
diff --git a/go.sum b/go.sum
index f2e3213..d17ed12 100644
--- a/go.sum
+++ b/go.sum
@@ -3,6 +3,7 @@ github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbi
 github.com/CycloneDX/cyclonedx-go v0.7.0 h1:jNxp8hL7UpcvPDFXjY+Y1ibFtsW+e5zyF9QoSmhK/zg=
 github.com/CycloneDX/cyclonedx-go v0.7.0/go.mod h1:W5Z9w8pTTL+t+yG3PCiFRGlr8PUlE0pGWzKSJbsyXkg=
 github.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oMMlVBbn9M=
+github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI=
 github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
 github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -41,11 +42,16 @@ golang.org/x/exp v0.0.0-20221212164502-fae10dda9338 h1:OvjRkcNHnf6/W5FZXSxODbxwD
 golang.org/x/exp v0.0.0-20221212164502-fae10dda9338/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
 golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA=
 golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ=
 golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI=
 golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
+golang.org/x/tools v0.4.1-0.20221217013628-b4dfc36097e2 h1:xJW6CltANFz/N/OyFltaf/kJs6Mnaq9Etj8aSBvx7MQ=
+golang.org/x/tools v0.4.1-0.20221217013628-b4dfc36097e2/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ=
+golang.org/x/vuln v0.0.0-20230108212441-ff355248a728 h1:Z7DgqswtDlTrNmQfj5h9bqQtiitZkWFeiIL1ZTjBVDI=
+golang.org/x/vuln v0.0.0-20230108212441-ff355248a728/go.mod h1:XJiVExZgoZfrrxoTeVsFYrSSk1snhfpOEC95JL+A4T0=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
@@ -53,3 +59,5 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+honnef.co/go/tools v0.2.2 h1:MNh1AVMyVX23VUHE2O27jm6lNj3vjO5DexS4A1xvnzk=
+mvdan.cc/unparam v0.0.0-20211214103731-d0ef000c54e5 h1:Jh3LAeMt1eGpxomyu3jVkmVZWW2MxZ1qIIV2TZ/nRio=
diff --git a/goreleaser.dockerfile b/goreleaser.dockerfile
new file mode 100644
index 0000000..8807cd0
--- /dev/null
+++ b/goreleaser.dockerfile
@@ -0,0 +1,26 @@
+# Copyright 2022 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+FROM alpine:latest
+
+RUN apk add --no-cache \
+    ca-certificates \
+    git
+
+# Allow git to run on mounted directories
+RUN git config --global --add safe.directory '*'
+
+COPY osv-scanner ./
+
+ENTRYPOINT ["/osv-scanner"]
diff --git a/pkg/lockfile/ecosystems.go b/pkg/lockfile/ecosystems.go
index 17a79d9..ec7c101 100644
--- a/pkg/lockfile/ecosystems.go
+++ b/pkg/lockfile/ecosystems.go
@@ -12,5 +12,6 @@ func KnownEcosystems() []Ecosystem {
 		MavenEcosystem,
 		PipEcosystem,
 		PubEcosystem,
+		AlpineEcosystem,
 	}
 }
diff --git a/pkg/lockfile/fixtures/apk/empty_installed b/pkg/lockfile/fixtures/apk/empty_installed
new file mode 100644
index 0000000..e69de29
diff --git a/pkg/lockfile/fixtures/apk/malformed_installed b/pkg/lockfile/fixtures/apk/malformed_installed
new file mode 100644
index 0000000..ac23d96
--- /dev/null
+++ b/pkg/lockfile/fixtures/apk/malformed_installed
@@ -0,0 +1,131 @@
+
+This is a malformed APK installed file
+
+
+
+  no package:
+
+Z:Q1olh8TpdAi2QnTl4FK3TjdUiSwTo=
+R:loader_attic.so
+R:afalg.so
+Z:Q1mcqLbO6iQe8TmQCoRDozFWScisQ=
+F:usr/lib
+r:libcrypto1.1
+F:etc
+F:lib
+F:usr/lib/ossl-modules
+R:libcrypto.so.3
+a:0:0:755
+Z:Q1AFDkJxxzzc5SCdOjdbK1BA/vbsY=
+o:openssl
+I:4206592
+R:legacy.so
+Z:Q1fqYq/iJ6x71cTpr8fcO4/6IgyQg=
+C:Q1TjmrAEa5PKd40PNz6OIpo+bBeW0=
+a:0:0:755
+R:openssl.cnf
+R:openssl.cnf.dist
+F:etc/ssl/private
+R:padlock.so
+Z:Q1zr8y7mYzOdgG1uz+DmGLBLOZ/jM=
+a:0:0:755
+R:capi.so
+S:1707237
+F:etc/ssl
+a:0:0:777
+p:so:libcrypto.so.3=3
+R:ct_log_list.cnf.dist
+F:usr/lib/engines-3
+V:3.0.7-r0
+Z:Q1WjKZkr5xeMyOxhVrCQfW04JiiME=
+F:etc/ssl/certs
+c:37a47708fdd97644624ff4b7238bb3299e037eaf
+a:0:0:777
+t:1667317778
+a:0:0:755
+Z:Q1op76VCo7av+GQqk9nT9kEezP1I8=
+L:Apache-2.0
+R:tsget
+a:0:0:755
+R:CA.pl
+R:ct_log_list.cnf
+D:so:libc.musl-x86_64.so.1
+Z:Q1XK8nt7AyX7GIGpMOLlkJk5dy81c=
+R:libcrypto.so.3
+T:Crypto library from openssl
+Z:Q1fqYq/iJ6x71cTpr8fcO4/6IgyQg=
+Z:Q1olh8TpdAi2QnTl4FK3TjdUiSwTo=
+F:usr
+Z:Q13NVgfr7dQUuGYxur0tNalH6EIjU=
+R:tsget.pl
+Z:Q1yzxstq05Nm+4DAS0gR/XScMthRY=
+a:0:0:755
+a:0:0:755
+a:0:0:755
+Z:Q1nwKfkE6NiHpVJ8wZRoQYglMEYwQ=
+U:https://www.openssl.org/
+F:etc/ssl/misc
+A:x86_64
+Z:Q1Uv35WBwtuePGrdxQuLDKbHVVTT4=
+
+nothing here:
+Z:Q1yVNLeeB7VouhCO/kz+dbfL3dY4c=
+F:sbin
+Z:Q1EgLFjj67ou3eMqp4m3r2ZjnQ7QU=
+Z:Q1ORf+lPRKuYgdkBBcKoevR1t60Q4=
+M:0:0:1777
+Z:Q1mB95Hq2NUTZ599RDiSsj9w5FrOU=
+F:etc
+F:var
+F:etc/network/if-pre-up.d
+Z:Q1HWpG3eQD8Uoi4mks2E3SSvOAUhY=
+F:usr/sbin
+
+
+
+
+
+
+
+
+no version
+F:var/lib
+A:x86_64
+F:var/cache
+F:etc/network/if-up.d
+F:usr/share
+t:1668852790
+F:etc/network/if-down.d
+a:0:0:755
+C:Q1NN3sp0yr99btRysqty3nQUrWHaY=
+S:509600
+a:0:0:755
+F:etc/network/if-post-up.d
+o:busybox
+R:udhcpd.conf
+U:https://busybox.net/
+F:var/cache/misc
+a:0:0:775
+F:usr/share/udhcpc
+p:cmd:busybox=1.35.0-r29
+R:dad
+R:securetty
+F:etc/network/if-post-down.d
+I:962560
+F:etc/logrotate.d
+D:so:libc.musl-x86_64.so.1
+F:bin
+c:1dbf7a793afae640ea643a055b6dd4f430ac116b
+F:var/lib/udhcpd
+F:tmp
+Z:Q1TylyCINVmnS+A/Tead4vZhE7Bks=
+T:Size optimized toolbox of many common UNIX utilities
+F:etc/network/if-pre-down.d
+R:default.script
+R:acpid
+r:busybox-initscripts
+R:busybox
+L:GPL-2.0-only
+F:usr
+F:etc/network
+P:busybox
\ No newline at end of file
diff --git a/pkg/lockfile/fixtures/apk/multiple_installed b/pkg/lockfile/fixtures/apk/multiple_installed
new file mode 100644
index 0000000..9d0fa07
--- /dev/null
+++ b/pkg/lockfile/fixtures/apk/multiple_installed
@@ -0,0 +1,123 @@
+C:Q1/JgpM8J6DWI/541tUX+uHEzSjqo=
+P:alpine-baselayout-data
+V:3.4.0-r0
+A:x86_64
+S:11664
+I:77824
+T:Alpine base dir structure and init scripts
+U:https://git.alpinelinux.org/cgit/aports/tree/main/alpine-baselayout
+L:GPL-2.0-only
+o:alpine-baselayout
+m:redacted <redacted@redacted.com
+t:1667573027
+c:bd965a7ebf7fd8f07d7a0cc0d7375bf3e4eb9b24
+r:alpine-baselayout
+F:etc
+R:fstab
+Z:Q11Q7hNe8QpDS531guqCdrXBzoA/o=
+R:group
+Z:Q13K+olJg5ayzHSVNUkggZJXuB+9Y=
+R:hostname
+Z:Q16nVwYVXP/tChvUPdukVD2ifXOmc=
+R:hosts
+Z:Q1BD6zJKZTRWyqGnPi4tSfd3krsMU=
+R:inittab
+Z:Q1TsthbhW7QzWRe1E/NKwTOuD4pHc=
+R:modules
+Z:Q1toogjUipHGcMgECgPJX64SwUT1M=
+R:mtab
+a:0:0:777
+Z:Q1kiljhXXH1LlQroHsEJIkPZg2eiw=
+R:nsswitch.conf
+Z:Q19DBsMnv0R2fajaTjoTv0C91NOqo=
+R:passwd
+Z:Q1TchuuLUfur0izvfZQZxgN/LJhB8=
+R:profile
+Z:Q1Ia5UTXvRkAH1lTZK8lm8qRBdRF4=
+R:protocols
+Z:Q11fllRTkIm5bxsZVoSNeDUn2m+0c=
+R:services
+Z:Q1oNeiKb8En3/hfoRFImI25AJFNdA=
+R:shadow
+a:0:42:640
+Z:Q1ltrPIAW2zHeDiajsex2Bdmq3uqA=
+R:shells
+Z:Q1ojm2YdpCJ6B/apGDaZ/Sdb2xJkA=
+R:sysctl.conf
+Z:Q14upz3tfnNxZkIEsUhWn7Xoiw96g=
+
+C:Q1Pk7x1woArbB1nzkMPJPq1TECwus=
+P:musl
+V:1.2.3-r4
+A:x86_64
+S:388955
+I:634880
+T:the musl c library (libc) implementation
+U:https://musl.libc.org/
+L:MIT
+o:musl
+m:redacted <redacted@redacted.com>
+t:1668104640
+c:f93af038c3de7146121c2ea8124ba5ce29b4b058
+p:so:libc.musl-x86_64.so.1=1
+F:lib
+R:ld-musl-x86_64.so.1
+a:0:0:755
+Z:Q1tGxgx2FLrD+0Uk03NUBwbbEiRCU=
+R:libc.musl-x86_64.so.1
+a:0:0:777
+Z:Q17yJ3JFNypA4mxhJJr0ou6CzsJVI=
+
+C:Q1NN3sp0yr99btRysqty3nQUrWHaY=
+P:busybox
+V:1.35.0-r29
+A:x86_64
+S:509600
+I:962560
+T:Size optimized toolbox of many common UNIX utilities
+U:https://busybox.net/
+L:GPL-2.0-only
+o:busybox
+m:redacted <redacted@redacted.com>
+t:1668852790
+c:1dbf7a793afae640ea643a055b6dd4f430ac116b
+D:so:libc.musl-x86_64.so.1
+p:cmd:busybox=1.35.0-r29
+r:busybox-initscripts
+F:bin
+R:busybox
+a:0:0:755
+Z:Q1yVNLeeB7VouhCO/kz+dbfL3dY4c=
+F:etc
+R:securetty
+Z:Q1mB95Hq2NUTZ599RDiSsj9w5FrOU=
+R:udhcpd.conf
+Z:Q1EgLFjj67ou3eMqp4m3r2ZjnQ7QU=
+F:etc/logrotate.d
+R:acpid
+Z:Q1TylyCINVmnS+A/Tead4vZhE7Bks=
+F:etc/network
+F:etc/network/if-down.d
+F:etc/network/if-post-down.d
+F:etc/network/if-post-up.d
+F:etc/network/if-pre-down.d
+F:etc/network/if-pre-up.d
+F:etc/network/if-up.d
+R:dad
+a:0:0:775
+Z:Q1ORf+lPRKuYgdkBBcKoevR1t60Q4=
+F:sbin
+F:tmp
+M:0:0:1777
+F:usr
+F:usr/sbin
+F:usr/share
+F:usr/share/udhcpc
+R:default.script
+a:0:0:755
+Z:Q1HWpG3eQD8Uoi4mks2E3SSvOAUhY=
+F:var
+F:var/cache
+F:var/cache/misc
+F:var/lib
+F:var/lib/udhcpd
diff --git a/pkg/lockfile/fixtures/apk/not_installed b/pkg/lockfile/fixtures/apk/not_installed
new file mode 100644
index 0000000..0302b39
--- /dev/null
+++ b/pkg/lockfile/fixtures/apk/not_installed
@@ -0,0 +1,12 @@
+Not an APK file!
+[[package]]
+name = "hello_world"
+version = "0.1.0"
+dependencies = [
+ "regex 1.5.0 (git+https://github.com/rust-lang/regex.git#9f9f693768c584971a4d53bc3c586c33ed3a6831)",
+]
+
+[[package]]
+name = "regex"
+version = "1.5.0"
+source = "git+https://github.com/rust-lang/regex.git#9f9f693768c584971a4d53bc3c586c33ed3a6831"
diff --git a/pkg/lockfile/fixtures/apk/shuffled_installed b/pkg/lockfile/fixtures/apk/shuffled_installed
new file mode 100644
index 0000000..c1cbf66
--- /dev/null
+++ b/pkg/lockfile/fixtures/apk/shuffled_installed
@@ -0,0 +1,32 @@
+F:lib/apk/exec
+F:var/lib
+F:sbin
+L:GPL-2.0-only
+D:musl>=1.2 ca-certificates-bundle so:libc.musl-x86_64.so.1 so:libcrypto.so.3 so:libssl.so.3 so:libz.so.1
+m:Natanael Copa <ncopa@alpinelinux.org>
+Z:Q1/4bmOPe/H1YhHRzlrj27oufThMw=
+I:307200
+S:120973
+c:0188f510baadbae393472103427b9c1875117136
+F:etc
+t:1666552494
+A:x86_64
+V:2.12.10-r1
+F:lib/apk
+F:var/lib/apk
+a:0:0:755
+C:Q1Ef3iwt+cMdGngEgaFr2URIJhKzQ=
+T:Alpine Package Keeper - package manager for alpine
+R:apk
+P:apk-tools
+F:etc/apk/protected_paths.d
+F:etc/apk/keys
+F:lib
+Z:Q1opjpYqXgzmOVo7EbNe8l5Xol08g=
+F:var
+p:so:libapk.so.3.12.0=3.12.0 cmd:apk=2.12.10-r1
+R:libapk.so.3.12.0
+a:0:0:755
+F:etc/apk
+o:apk-tools
+U:https://gitlab.alpinelinux.org/alpine/apk-tools
\ No newline at end of file
diff --git a/pkg/lockfile/fixtures/apk/single_installed b/pkg/lockfile/fixtures/apk/single_installed
new file mode 100644
index 0000000..181245d
--- /dev/null
+++ b/pkg/lockfile/fixtures/apk/single_installed
@@ -0,0 +1,32 @@
+C:Q1Ef3iwt+cMdGngEgaFr2URIJhKzQ=
+P:apk-tools
+V:2.12.10-r1
+A:x86_64
+S:120973
+I:307200
+T:Alpine Package Keeper - package manager for alpine
+U:https://gitlab.alpinelinux.org/alpine/apk-tools
+L:GPL-2.0-only
+o:apk-tools
+m:Natanael Copa <ncopa@alpinelinux.org>
+t:1666552494
+c:0188f510baadbae393472103427b9c1875117136
+D:musl>=1.2 ca-certificates-bundle so:libc.musl-x86_64.so.1 so:libcrypto.so.3 so:libssl.so.3 so:libz.so.1
+p:so:libapk.so.3.12.0=3.12.0 cmd:apk=2.12.10-r1
+F:etc
+F:etc/apk
+F:etc/apk/keys
+F:etc/apk/protected_paths.d
+F:lib
+R:libapk.so.3.12.0
+a:0:0:755
+Z:Q1opjpYqXgzmOVo7EbNe8l5Xol08g=
+F:lib/apk
+F:lib/apk/exec
+F:sbin
+R:apk
+a:0:0:755
+Z:Q1/4bmOPe/H1YhHRzlrj27oufThMw=
+F:var
+F:var/lib
+F:var/lib/apk
\ No newline at end of file
diff --git a/pkg/lockfile/parse-apk-installed.go b/pkg/lockfile/parse-apk-installed.go
new file mode 100644
index 0000000..1ee431b
--- /dev/null
+++ b/pkg/lockfile/parse-apk-installed.go
@@ -0,0 +1,104 @@
+package lockfile
+
+import (
+	"bufio"
+	"fmt"
+	"os"
+	"strings"
+)
+
+const AlpineEcosystem Ecosystem = "Alpine"
+
+func groupApkPackageLines(scanner *bufio.Scanner) [][]string {
+	var groups [][]string
+	var group []string
+
+	for scanner.Scan() {
+		line := scanner.Text()
+
+		if line == "" {
+			if len(group) > 0 {
+				groups = append(groups, group)
+			}
+			group = make([]string, 0)
+			continue
+		}
+		group = append(group, line)
+	}
+
+	if len(group) > 0 {
+		groups = append(groups, group)
+	}
+
+	return groups
+}
+
+func parseApkPackageGroup(group []string, pathToLockfile string) PackageDetails {
+	var pkg = PackageDetails{
+		Ecosystem: AlpineEcosystem,
+		CompareAs: AlpineEcosystem,
+	}
+
+	// File SPECS: https://wiki.alpinelinux.org/wiki/Apk_spec
+	for _, line := range group {
+		switch {
+		case strings.HasPrefix(line, "P:"):
+			pkg.Name = strings.TrimPrefix(line, "P:")
+		case strings.HasPrefix(line, "V:"):
+			pkg.Version = strings.TrimPrefix(line, "V:")
+		case strings.HasPrefix(line, "c:"):
+			pkg.Commit = strings.TrimPrefix(line, "c:")
+		}
+	}
+
+	if pkg.Version == "" {
+		var pkgPrintName string = pkg.Name
+		if pkgPrintName == "" {
+			pkgPrintName = "<unknown>"
+		}
+
+		_, _ = fmt.Fprintf(
+			os.Stderr,
+			"warning: malformed APK installed file. Found no version number in record. Package %s. File: %s\n",
+			pkgPrintName,
+			pathToLockfile,
+		)
+	}
+
+	return pkg
+}
+
+func ParseApkInstalled(pathToLockfile string) ([]PackageDetails, error) {
+	file, err := os.Open(pathToLockfile)
+	if err != nil {
+		return []PackageDetails{}, fmt.Errorf("could not open %s: %w", pathToLockfile, err)
+	}
+	defer file.Close()
+
+	scanner := bufio.NewScanner(file)
+
+	packageGroups := groupApkPackageLines(scanner)
+
+	packages := make([]PackageDetails, 0, len(packageGroups))
+
+	for _, group := range packageGroups {
+		pkg := parseApkPackageGroup(group, pathToLockfile)
+
+		if pkg.Name == "" {
+			_, _ = fmt.Fprintf(
+				os.Stderr,
+				"warning: malformed APK installed file. Found no package name in record. File: %s\n",
+				pathToLockfile,
+			)
+			continue
+		}
+
+		packages = append(packages, pkg)
+	}
+
+	if err := scanner.Err(); err != nil {
+		return packages, fmt.Errorf("error while scanning %s: %w", pathToLockfile, err)
+	}
+
+	return packages, nil
+}
diff --git a/pkg/lockfile/parse-yarn-lock.go b/pkg/lockfile/parse-yarn-lock.go
index 3603269..b227178 100644
--- a/pkg/lockfile/parse-yarn-lock.go
+++ b/pkg/lockfile/parse-yarn-lock.go
@@ -15,7 +15,7 @@ func shouldSkipYarnLine(line string) bool {
 	return line == "" || strings.HasPrefix(line, "#")
 }
 
-func groupPackageLines(scanner *bufio.Scanner) [][]string {
+func groupYarnPackageLines(scanner *bufio.Scanner) [][]string {
 	var groups [][]string
 	var group []string
 
@@ -148,7 +148,7 @@ func tryExtractCommit(resolution string) string {
 	return ""
 }
 
-func parsePackageGroup(group []string) PackageDetails {
+func parseYarnPackageGroup(group []string) PackageDetails {
 	name := extractYarnPackageName(group[0])
 	version := determineYarnPackageVersion(group)
 	resolution := determineYarnPackageResolution(group)
@@ -179,7 +179,7 @@ func ParseYarnLock(pathToLockfile string) ([]PackageDetails, error) {
 
 	scanner := bufio.NewScanner(file)
 
-	packageGroups := groupPackageLines(scanner)
+	packageGroups := groupYarnPackageLines(scanner)
 
 	if err := scanner.Err(); err != nil {
 		return []PackageDetails{}, fmt.Errorf("error while scanning %s: %w", pathToLockfile, err)
@@ -192,7 +192,7 @@ func ParseYarnLock(pathToLockfile string) ([]PackageDetails, error) {
 			continue
 		}
 
-		packages = append(packages, parsePackageGroup(group))
+		packages = append(packages, parseYarnPackageGroup(group))
 	}
 
 	return packages, nil
diff --git a/pkg/lockfile/parse.go b/pkg/lockfile/parse.go
index 3a34b22..579b10c 100644
--- a/pkg/lockfile/parse.go
+++ b/pkg/lockfile/parse.go
@@ -24,6 +24,7 @@ var parsers = map[string]PackageDetailsParser{
 	"Gemfile.lock":                ParseGemfileLock,
 	"go.mod":                      ParseGoLock,
 	"gradle.lockfile":             ParseGradleLock,
+        "installed":                   ParseApkInstalled,
 	"mix.lock":                    ParseMixLock,
 	"Pipfile.lock":                ParsePipenvLock,
 	"package-lock.json":           ParseNpmLock,
diff --git a/renovate.json b/renovate.json
index 1b34bf1..00dd57a 100644
--- a/renovate.json
+++ b/renovate.json
@@ -6,6 +6,7 @@
   "timezone": "Australia/Sydney",
   "schedule": ["before 6am on monday"],
   "labels": ["dependencies"],
+  "postUpdateOptions": ["gomodTidy"],
   "packageRules": [
     {
       "matchUpdateTypes": ["major"],


Test patch:
diff --git a/pkg/lockfile/parse-apk-installed_test.go b/pkg/lockfile/parse-apk-installed_test.go
new file mode 100644
index 0000000..1b38774
--- /dev/null
+++ b/pkg/lockfile/parse-apk-installed_test.go
@@ -0,0 +1,133 @@
+package lockfile_test
+
+import (
+	"github.com/google/osv-scanner/pkg/lockfile"
+	"testing"
+)
+
+func TestParseApkInstalled_FileDoesNotExist(t *testing.T) {
+	t.Parallel()
+
+	packages, err := lockfile.ParseRequirementsTxt("fixtures/apk/does-not-exist")
+
+	expectErrContaining(t, err, "could not open")
+	expectPackages(t, packages, []lockfile.PackageDetails{})
+}
+
+func TestParseApkInstalled_Empty(t *testing.T) {
+	t.Parallel()
+
+	packages, err := lockfile.ParseApkInstalled("fixtures/apk/empty_installed")
+
+	if err != nil {
+		t.Errorf("Got unexpected error: %v", err)
+	}
+
+	expectPackages(t, packages, []lockfile.PackageDetails{})
+}
+
+func TestParseApkInstalled_NotAnInstalled(t *testing.T) {
+	t.Parallel()
+
+	packages, err := lockfile.ParseApkInstalled("fixtures/apk/not_installed")
+
+	if err != nil {
+		t.Errorf("Got unexpected error: %v", err)
+	}
+
+	expectPackages(t, packages, []lockfile.PackageDetails{})
+}
+
+func TestParseApkInstalled_Malformed(t *testing.T) {
+	t.Parallel()
+
+	packages, err := lockfile.ParseApkInstalled("fixtures/apk/malformed_installed")
+
+	if err != nil {
+		t.Errorf("Got unexpected error: %v", err)
+	}
+
+	expectPackages(t, packages, []lockfile.PackageDetails{
+		{
+			Name:      "busybox",
+			Version:   "",
+			Commit:    "1dbf7a793afae640ea643a055b6dd4f430ac116b",
+			Ecosystem: lockfile.AlpineEcosystem,
+			CompareAs: lockfile.AlpineEcosystem,
+		},
+	})
+}
+
+func TestParseApkInstalled_Single(t *testing.T) {
+	t.Parallel()
+
+	packages, err := lockfile.ParseApkInstalled("fixtures/apk/single_installed")
+
+	if err != nil {
+		t.Errorf("Got unexpected error: %v", err)
+	}
+
+	expectPackages(t, packages, []lockfile.PackageDetails{
+		{
+			Name:      "apk-tools",
+			Version:   "2.12.10-r1",
+			Commit:    "0188f510baadbae393472103427b9c1875117136",
+			Ecosystem: lockfile.AlpineEcosystem,
+			CompareAs: lockfile.AlpineEcosystem,
+		},
+	})
+}
+
+func TestParseApkInstalled_Shuffled(t *testing.T) {
+	t.Parallel()
+
+	packages, err := lockfile.ParseApkInstalled("fixtures/apk/shuffled_installed")
+
+	if err != nil {
+		t.Errorf("Got unexpected error: %v", err)
+	}
+
+	expectPackages(t, packages, []lockfile.PackageDetails{
+		{
+			Name:      "apk-tools",
+			Version:   "2.12.10-r1",
+			Commit:    "0188f510baadbae393472103427b9c1875117136",
+			Ecosystem: lockfile.AlpineEcosystem,
+			CompareAs: lockfile.AlpineEcosystem,
+		},
+	})
+}
+
+func TestParseApkInstalled_Multiple(t *testing.T) {
+	t.Parallel()
+
+	packages, err := lockfile.ParseApkInstalled("fixtures/apk/multiple_installed")
+
+	if err != nil {
+		t.Errorf("Got unexpected error: %v", err)
+	}
+
+	expectPackages(t, packages, []lockfile.PackageDetails{
+		{
+			Name:      "alpine-baselayout-data",
+			Version:   "3.4.0-r0",
+			Commit:    "bd965a7ebf7fd8f07d7a0cc0d7375bf3e4eb9b24",
+			Ecosystem: lockfile.AlpineEcosystem,
+			CompareAs: lockfile.AlpineEcosystem,
+		},
+		{
+			Name:      "musl",
+			Version:   "1.2.3-r4",
+			Commit:    "f93af038c3de7146121c2ea8124ba5ce29b4b058",
+			Ecosystem: lockfile.AlpineEcosystem,
+			CompareAs: lockfile.AlpineEcosystem,
+		},
+		{
+			Name:      "busybox",
+			Version:   "1.35.0-r29",
+			Commit:    "1dbf7a793afae640ea643a055b6dd4f430ac116b",
+			Ecosystem: lockfile.AlpineEcosystem,
+			CompareAs: lockfile.AlpineEcosystem,
+		},
+	})
+}
diff --git a/pkg/lockfile/parse_test.go b/pkg/lockfile/parse_test.go
index b890701..1a1acb6 100644
--- a/pkg/lockfile/parse_test.go
+++ b/pkg/lockfile/parse_test.go
@@ -106,6 +106,7 @@ func TestParse_FindsExpectedParsers(t *testing.T) {
 		"poetry.lock",
 		"pubspec.lock",
 		"requirements.txt",
+		"installed",
 	}
 
 	count := 0