Repository: google/osv-scanner
Pull request #506 (2023-08-27 14:31:15)

fix(deps): update osv-scanner minor
[![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [github.com/CycloneDX/cyclonedx-go](https://togithub.com/CycloneDX/cyclonedx-go) | require | patch | `v0.7.1` -> `v0.7.2` |
| [github.com/jedib0t/go-pretty/v6](https://togithub.com/jedib0t/go-pretty) | require | patch | `v6.4.6` -> `v6.4.7` |

---

### Release Notes

<details>
<summary>CycloneDX/cyclonedx-go (github.com/CycloneDX/cyclonedx-go)</summary>

### [`v0.7.2`](https://togithub.com/CycloneDX/cyclonedx-go/releases/tag/v0.7.2)

[Compare Source](https://togithub.com/CycloneDX/cyclonedx-go/compare/v0.7.1...v0.7.2)

This is a bugfix release that ships with minimal support for the CycloneDX v1.5 specification.

Full support is being worked on and planned to be released soon. The progress may be tracked in [#&#8203;90](https://togithub.com/CycloneDX/cyclonedx-go/issues/90).

The reason for publishing partial support like this is to allow the consumption of v1.5 BOMs, which fails with `cyclonedx-go` <= v0.7.1.

> **Warning**\
> The default [`SpecVersion`](https://pkg.go.dev/github.com/CycloneDX/cyclonedx-go#SpecVersion) has been updated to `SpecVersion1_5`. If your application *generates* BOMs, and you're not ready (or willing) to distribute BOMs following the v1.5 specification yet, consider using [`EncodeVersion`](https://togithub.com/CycloneDX/cyclonedx-go/blob/83031d6697bd6d8b20bce2a0326347a0ea7691c7/encode.go#L31-L34) to generate output for an older version of the spec.

#### Changelog

##### Features

-   [`7128a92`](https://togithub.com/CycloneDX/cyclonedx-go/commit/7128a921f3e83a43feef75bc8ab95642c236ef82): feat: raise baseline go version to 1.18 ([@&#8203;nscuro](https://togithub.com/nscuro))

##### Fixes

-   [`ff719b6`](https://togithub.com/CycloneDX/cyclonedx-go/commit/ff719b64835af6e75dcfd6e7ff90d070f271ae07): fix: unmarshal bom on v1.5 return invalid specification version ([@&#8203;chen-keinan](https://togithub.com/chen-keinan))

##### Building and Packaging

-   [`966c223`](https://togithub.com/CycloneDX/cyclonedx-go/commit/966c223154527621395473cc045a7672609c879f): build(deps): bump CycloneDX/gh-gomod-generate-sbom from 1.1.0 to 2.0.0 ([@&#8203;dependabot](https://togithub.com/dependabot)\[bot])
-   [`1e83e85`](https://togithub.com/CycloneDX/cyclonedx-go/commit/1e83e8598d07b6303522cb63458be2577223f8d3): build(deps): bump actions/checkout from 3.5.0 to 3.5.1 ([@&#8203;dependabot](https://togithub.com/dependabot)\[bot])
-   [`78f6593`](https://togithub.com/CycloneDX/cyclonedx-go/commit/78f6593ed81da036aec671c19ea937b3a80586bf): build(deps): bump actions/checkout from 3.5.1 to 3.5.2 ([@&#8203;dependabot](https://togithub.com/dependabot)\[bot])
-   [`868f6db`](https://togithub.com/CycloneDX/cyclonedx-go/commit/868f6db7d03da581dbe9b6d283acd6c477529c0a): build(deps): bump actions/checkout from 3.5.2 to 3.5.3 ([@&#8203;dependabot](https://togithub.com/dependabot)\[bot])
-   [`5885827`](https://togithub.com/CycloneDX/cyclonedx-go/commit/5885827e4246b82e08d37f6f0b95c6c0a4ef821b): build(deps): bump actions/setup-go from 4.0.0 to 4.0.1 ([@&#8203;dependabot](https://togithub.com/dependabot)\[bot])
-   [`d772b54`](https://togithub.com/CycloneDX/cyclonedx-go/commit/d772b5438430be7879f3a4e7064c1ccbdbf153a1): build(deps): bump actions/setup-go from 4.0.1 to 4.1.0 ([@&#8203;dependabot](https://togithub.com/dependabot)\[bot])
-   [`578e862`](https://togithub.com/CycloneDX/cyclonedx-go/commit/578e8621c93869b9e0368eebb619cd96c7e9e2bb): build(deps): bump github.com/stretchr/testify from 1.8.2 to 1.8.4 ([@&#8203;dependabot](https://togithub.com/dependabot)\[bot])
-   [`f83e6a7`](https://togithub.com/CycloneDX/cyclonedx-go/commit/f83e6a7c9d196eff9f99ecf8291cd4adeabce31a): build(deps): bump gitpod/workspace-go from `2be827f` to `910daeb` ([@&#8203;dependabot](https://togithub.com/dependabot)\[bot])
-   [`cd7b23a`](https://togithub.com/CycloneDX/cyclonedx-go/commit/cd7b23a68ff1c7467e211c9c69f9fb67c2244043): build(deps): bump gitpod/workspace-go from `910daeb` to `d7a41f5` ([@&#8203;dependabot](https://togithub.com/dependabot)\[bot])
-   [`668553d`](https://togithub.com/CycloneDX/cyclonedx-go/commit/668553d1667110b8b34c7a4a954c3ac4707816ba): build(deps): bump gitpod/workspace-go from `d7a41f5` to `f37c673` ([@&#8203;dependabot](https://togithub.com/dependabot)\[bot])
-   [`d9a5f8c`](https://togithub.com/CycloneDX/cyclonedx-go/commit/d9a5f8cf07fa834c02969fba2128bdb14c0865ff): build(deps): bump golangci/golangci-lint-action from 3.4.0 to 3.5.0 ([@&#8203;dependabot](https://togithub.com/dependabot)\[bot])
-   [`66f96df`](https://togithub.com/CycloneDX/cyclonedx-go/commit/66f96dfacf866f8d2ca686659e964fc535c72f92): build(deps): bump golangci/golangci-lint-action from 3.5.0 to 3.6.0 ([@&#8203;dependabot](https://togithub.com/dependabot)\[bot])
-   [`8b51c39`](https://togithub.com/CycloneDX/cyclonedx-go/commit/8b51c39974573c22ba0a14ba1d5a0cd5b50c68fa): build(deps): bump golangci/golangci-lint-action from 3.6.0 to 3.7.0 ([@&#8203;dependabot](https://togithub.com/dependabot)\[bot])
-   [`e44f7de`](https://togithub.com/CycloneDX/cyclonedx-go/commit/e44f7de374a51cd1228117d43ccedfdcbe50cd73): build(deps): bump goreleaser/goreleaser-action from 4.2.0 to 4.3.0 ([@&#8203;dependabot](https://togithub.com/dependabot)\[bot])
-   [`6360fe1`](https://togithub.com/CycloneDX/cyclonedx-go/commit/6360fe1474853e461a6af83fc6214882b4647f09): build(deps): bump goreleaser/goreleaser-action from 4.3.0 to 4.4.0 ([@&#8203;dependabot](https://togithub.com/dependabot)\[bot])

##### Others

-   [`a069906`](https://togithub.com/CycloneDX/cyclonedx-go/commit/a06990657b338db19fec11a677ea915eea2b5c74): feat(spec1-5): add initial support for spec v1.5 ([@&#8203;nscuro](https://togithub.com/nscuro))
-   [`67a7567`](https://togithub.com/CycloneDX/cyclonedx-go/commit/67a7567143eb3373099f100bbe17143239cf5d4e): feat(spec1-5): add licensing, license properties, and license bom-ref ([@&#8203;nscuro](https://togithub.com/nscuro))
-   [`d2f3bb9`](https://togithub.com/CycloneDX/cyclonedx-go/commit/d2f3bb95bf740da7a6d36c6a1c324356afed5356): feat(spec1-5): add lifecycle support ([@&#8203;nscuro](https://togithub.com/nscuro))
-   [`eb041b5`](https://togithub.com/CycloneDX/cyclonedx-go/commit/eb041b55b2eb8685a37be6f7a9c265fb6528377b): feat(spec1-5): add new component types ([@&#8203;nscuro](https://togithub.com/nscuro))
-   [`c45ba61`](https://togithub.com/CycloneDX/cyclonedx-go/commit/c45ba618028d9f0cb593784e6483f4392a78ff3b): feat(spec1-5): add new external reference types ([@&#8203;nscuro](https://togithub.com/nscuro))
-   [`d84947d`](https://togithub.com/CycloneDX/cyclonedx-go/commit/d84947d74d7df97f851211bf7b72786e3583b9e3): feat(spec1-5): add support for annotations ([@&#8203;nscuro](https://togithub.com/nscuro))
-   [`0ba0496`](https://togithub.com/CycloneDX/cyclonedx-go/commit/0ba04965ce8c5df710eb2a1cae1e7546ffb6321b): feat(spec1-5): bump schema to 1.5 for round-trip tests ([@&#8203;nscuro](https://togithub.com/nscuro))
-   [`4e20914`](https://togithub.com/CycloneDX/cyclonedx-go/commit/4e20914ebfc2aa80fbe0fa32650567554ebaaf49): misc(dx): add project icon for intellij and goland ([@&#8203;nscuro](https://togithub.com/nscuro))

</details>

<details>
<summary>jedib0t/go-pretty (github.com/jedib0t/go-pretty/v6)</summary>

### [`v6.4.7`](https://togithub.com/jedib0t/go-pretty/releases/tag/v6.4.7)

[Compare Source](https://togithub.com/jedib0t/go-pretty/compare/v6.4.6...v6.4.7)

### Features

-   **text**
    -   add hyperlink formatter ([#&#8203;263](https://togithub.com/jedib0t/go-pretty/issues/263)) // thanks [@&#8203;sanyatuning](https://togithub.com/sanyatuning)
-   **progress** // thanks [@&#8203;NathanBaulch](https://togithub.com/NathanBaulch) for all of the following
    -   support rendering trackers that haven't started yet ([#&#8203;270](https://togithub.com/jedib0t/go-pretty/issues/270))
    -   secondary sort by start time ([#&#8203;271](https://togithub.com/jedib0t/go-pretty/issues/271))
    -   stability fix for descending sorts ([#&#8203;272](https://togithub.com/jedib0t/go-pretty/issues/272))
    -   render never started trackers properly ([#&#8203;273](https://togithub.com/jedib0t/go-pretty/issues/273))
    -   overall ETA never less than max ETA ([#&#8203;274](https://togithub.com/jedib0t/go-pretty/issues/274))

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "before 6am on monday" in timezone Australia/Sydney, Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get [config help](https://togithub.com/renovatebot/renovate/discussions) if that's undesired.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://developer.mend.io/github/google/osv-scanner).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNi42NC44IiwidXBkYXRlZEluVmVyIjoiMzYuNjQuOCIsInRhcmdldEJyYW5jaCI6Im1haW4ifQ==-->



Patch:
diff --git a/go.mod b/go.mod
index 909fbe7..0800cda 100644
--- a/go.mod
+++ b/go.mod
@@ -4,12 +4,12 @@ go 1.19
 
 require (
 	github.com/BurntSushi/toml v1.3.2
-	github.com/CycloneDX/cyclonedx-go v0.7.1
+	github.com/CycloneDX/cyclonedx-go v0.7.2
 	github.com/go-git/go-billy/v5 v5.4.1
 	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/jedib0t/go-pretty/v6 v6.4.7
 	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
diff --git a/go.sum b/go.sum
index fdaaffe..b2764ad 100644
--- a/go.sum
+++ b/go.sum
@@ -2,8 +2,8 @@ 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/CycloneDX/cyclonedx-go v0.7.2 h1:kKQ0t1dPOlugSIYVOMiMtFqeXI2wp/f5DBIdfux8gnQ=
+github.com/CycloneDX/cyclonedx-go v0.7.2/go.mod h1:K2bA+324+Og0X84fA8HhN2X066K7Bxz4rpMQ4ZhjtSk=
 github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
 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=
@@ -52,8 +52,8 @@ github.com/ianlancetaylor/demangle v0.0.0-20230524184225-eabc099b10ab h1:BA4a7pe
 github.com/ianlancetaylor/demangle v0.0.0-20230524184225-eabc099b10ab/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw=
 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=
-github.com/jedib0t/go-pretty/v6 v6.4.6/go.mod h1:Ndk3ase2CkQbXLLNf5QDHoYb6J9WtVfmHZu9n8rk2xs=
+github.com/jedib0t/go-pretty/v6 v6.4.7 h1:lwiTJr1DEkAgzljsUsORmWsVn5MQjt1BPJdPCtJ6KXE=
+github.com/jedib0t/go-pretty/v6 v6.4.7/go.mod h1:Ndk3ase2CkQbXLLNf5QDHoYb6J9WtVfmHZu9n8rk2xs=
 github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
 github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
@@ -109,12 +109,16 @@ github.com/stretchr/testify v1.7.4/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
 github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
 github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/terminalstatic/go-xsd-validate v0.1.5 h1:RqpJnf6HGE2CB/lZB1A8BYguk8uRtcvYAPLCF15qguo=
 github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs=
 github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
 github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4=
 github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI=
 github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
 github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
+github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c=
+github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
+github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
 github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
 github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
diff --git a/internal/local/zip.go b/internal/local/zip.go
index 6b19120..cdd5027 100644
--- a/internal/local/zip.go
+++ b/internal/local/zip.go
@@ -16,6 +16,7 @@ import (
 	"path"
 	"strings"
 
+	"github.com/google/osv-scanner/internal/utility/vulns"
 	"github.com/google/osv-scanner/pkg/lockfile"
 	"github.com/google/osv-scanner/pkg/models"
 	"github.com/google/osv-scanner/pkg/osv"
@@ -235,7 +236,7 @@ func (db *ZipDB) VulnerabilitiesAffectingPackage(pkg lockfile.PackageDetails) mo
 	var vulnerabilities models.Vulnerabilities
 
 	for _, vulnerability := range db.Vulnerabilities(false) {
-		if vulnerability.IsAffected(pkg) && !vulnerabilities.Includes(vulnerability) {
+		if vulns.IsAffected(vulnerability, pkg) && !vulns.Include(vulnerabilities, vulnerability) {
 			vulnerabilities = append(vulnerabilities, vulnerability)
 		}
 	}
diff --git a/internal/output/table.go b/internal/output/table.go
index 871f9d6..69030bc 100644
--- a/internal/output/table.go
+++ b/internal/output/table.go
@@ -13,12 +13,15 @@ import (
 	v3_metric "github.com/goark/go-cvss/v3/metric"
 
 	"github.com/google/osv-scanner/pkg/models"
-	"github.com/google/osv-scanner/pkg/osv"
 
 	"github.com/jedib0t/go-pretty/v6/table"
 	"github.com/jedib0t/go-pretty/v6/text"
 )
 
+// OSVBaseVulnerabilityURL is the base URL for detailed vulnerability views.
+// Copied in from osv package to avoid referencing the osv package unnecessarily
+const OSVBaseVulnerabilityURL = "https://osv.dev/"
+
 // PrintTableResults prints the osv scan results into a human friendly table.
 func PrintTableResults(vulnResult *models.VulnerabilityResults, outputWriter io.Writer, terminalWidth int) {
 	outputTable := table.NewWriter()
@@ -96,9 +99,9 @@ func tableBuilderInner(vulnResult *models.VulnerabilityResults, addStyling bool,
 
 				for _, vuln := range group.IDs {
 					if addStyling {
-						links = append(links, osv.BaseVulnerabilityURL+text.Bold.EscapeSeq()+vuln+text.Reset.EscapeSeq())
+						links = append(links, OSVBaseVulnerabilityURL+text.Bold.EscapeSeq()+vuln+text.Reset.EscapeSeq())
 					} else {
-						links = append(links, osv.BaseVulnerabilityURL+vuln)
+						links = append(links, OSVBaseVulnerabilityURL+vuln)
 					}
 				}
 
diff --git a/internal/utility/vulns/vulnerabilities.go b/internal/utility/vulns/vulnerabilities.go
new file mode 100644
index 0000000..5e45e2d
--- /dev/null
+++ b/internal/utility/vulns/vulnerabilities.go
@@ -0,0 +1,20 @@
+package vulns
+
+import "github.com/google/osv-scanner/pkg/models"
+
+func Include(vs models.Vulnerabilities, vulnerability models.Vulnerability) bool {
+	for _, vuln := range vs {
+		if vuln.ID == vulnerability.ID {
+			return true
+		}
+
+		if isAliasOf(vuln, vulnerability) {
+			return true
+		}
+		if isAliasOf(vulnerability, vuln) {
+			return true
+		}
+	}
+
+	return false
+}
diff --git a/internal/utility/vulns/vulnerability.go b/internal/utility/vulns/vulnerability.go
new file mode 100644
index 0000000..25f103d
--- /dev/null
+++ b/internal/utility/vulns/vulnerability.go
@@ -0,0 +1,152 @@
+package vulns
+
+import (
+	"fmt"
+	"os"
+	"sort"
+
+	"github.com/google/osv-scanner/internal/semantic"
+	"github.com/google/osv-scanner/pkg/lockfile"
+	"github.com/google/osv-scanner/pkg/models"
+	"golang.org/x/exp/slices"
+)
+
+func eventVersion(e models.Event) string {
+	if e.Introduced != "" {
+		return e.Introduced
+	}
+
+	if e.Fixed != "" {
+		return e.Fixed
+	}
+
+	if e.Limit != "" {
+		return e.Limit
+	}
+
+	if e.LastAffected != "" {
+		return e.LastAffected
+	}
+
+	return ""
+}
+
+func rangeContainsVersion(ar models.Range, pkg lockfile.PackageDetails) bool {
+	if ar.Type != models.RangeEcosystem && ar.Type != models.RangeSemVer {
+		return false
+	}
+	// todo: we should probably warn here
+	if len(ar.Events) == 0 {
+		return false
+	}
+
+	vp := semantic.MustParse(pkg.Version, pkg.CompareAs)
+
+	sort.Slice(ar.Events, func(i, j int) bool {
+		a := ar.Events[i]
+		b := ar.Events[j]
+
+		if a.Introduced == "0" {
+			return true
+		}
+
+		if b.Introduced == "0" {
+			return false
+		}
+
+		return semantic.MustParse(eventVersion(a), pkg.CompareAs).CompareStr(eventVersion(b)) < 0
+	})
+
+	var affected bool
+	for _, e := range ar.Events {
+		if affected {
+			if e.Fixed != "" {
+				affected = vp.CompareStr(e.Fixed) < 0
+			} else if e.LastAffected != "" {
+				affected = e.LastAffected == pkg.Version || vp.CompareStr(e.LastAffected) <= 0
+			}
+		} else if e.Introduced != "" {
+			affected = e.Introduced == "0" || vp.CompareStr(e.Introduced) >= 0
+		}
+	}
+
+	return affected
+}
+
+// rangeAffectsVersion checks if the given version is within the range
+// specified by the events of any "Ecosystem" or "Semver" type ranges
+func rangeAffectsVersion(a []models.Range, pkg lockfile.PackageDetails) bool {
+	for _, r := range a {
+		if r.Type != models.RangeEcosystem && r.Type != models.RangeSemVer {
+			return false
+		}
+		if rangeContainsVersion(r, pkg) {
+			return true
+		}
+	}
+
+	return false
+}
+
+func isAliasOfID(v models.Vulnerability, id string) bool {
+	for _, alias := range v.Aliases {
+		if alias == id {
+			return true
+		}
+	}
+
+	return false
+}
+
+func isAliasOf(v models.Vulnerability, vulnerability models.Vulnerability) bool {
+	for _, alias := range vulnerability.Aliases {
+		if v.ID == alias || isAliasOfID(v, alias) {
+			return true
+		}
+	}
+
+	return false
+}
+
+func AffectsEcosystem(v models.Vulnerability, ecosystem lockfile.Ecosystem) bool {
+	for _, affected := range v.Affected {
+		if string(affected.Package.Ecosystem) == string(ecosystem) {
+			return true
+		}
+	}
+
+	return false
+}
+
+func IsAffected(v models.Vulnerability, pkg lockfile.PackageDetails) bool {
+	for _, affected := range v.Affected {
+		if string(affected.Package.Ecosystem) == string(pkg.Ecosystem) &&
+			affected.Package.Name == pkg.Name {
+			if len(affected.Ranges) == 0 && len(affected.Versions) == 0 {
+				_, _ = fmt.Fprintf(
+					os.Stderr,
+					"%s does not have any ranges or versions - this is probably a mistake!\n",
+					v.ID,
+				)
+
+				continue
+			}
+
+			if slices.Contains(affected.Versions, pkg.Version) {
+				return true
+			}
+
+			if rangeAffectsVersion(affected.Ranges, pkg) {
+				return true
+			}
+
+			// if a package does not have a version, assume it is vulnerable
+			// as false positives are better than false negatives here
+			if pkg.Version == "" {
+				return true
+			}
+		}
+	}
+
+	return false
+}
diff --git a/pkg/models/vulnerabilities.go b/pkg/models/vulnerabilities.go
index 53c9470..ea20967 100644
--- a/pkg/models/vulnerabilities.go
+++ b/pkg/models/vulnerabilities.go
@@ -7,23 +7,6 @@ import (
 
 type Vulnerabilities []Vulnerability
 
-func (vs Vulnerabilities) Includes(vulnerability Vulnerability) bool {
-	for _, vuln := range vs {
-		if vuln.ID == vulnerability.ID {
-			return true
-		}
-
-		if vuln.isAliasOf(vulnerability) {
-			return true
-		}
-		if vulnerability.isAliasOf(vuln) {
-			return true
-		}
-	}
-
-	return false
-}
-
 // MarshalJSON ensures that if there are no vulnerabilities,
 // an empty array is used as the value instead of "null"
 func (vs Vulnerabilities) MarshalJSON() ([]byte, error) {
diff --git a/pkg/models/vulnerability.go b/pkg/models/vulnerability.go
index 1d92217..db7a0e1 100644
--- a/pkg/models/vulnerability.go
+++ b/pkg/models/vulnerability.go
@@ -2,14 +2,7 @@ package models
 
 import (
 	"encoding/json"
-	"fmt"
-	"os"
-	"sort"
 	"time"
-
-	"github.com/google/osv-scanner/internal/semantic"
-	"github.com/google/osv-scanner/pkg/lockfile"
-	"golang.org/x/exp/slices"
 )
 
 // Package identifies the affected code library or command provided by the
@@ -174,143 +167,3 @@ func (v Vulnerability) MarshalYAML() (interface{}, error) {
 
 	return raw, nil
 }
-
-func (e Event) version() string {
-	if e.Introduced != "" {
-		return e.Introduced
-	}
-
-	if e.Fixed != "" {
-		return e.Fixed
-	}
-
-	if e.Limit != "" {
-		return e.Limit
-	}
-
-	if e.LastAffected != "" {
-		return e.LastAffected
-	}
-
-	return ""
-}
-
-func (ar Range) containsVersion(pkg lockfile.PackageDetails) bool {
-	if ar.Type != RangeEcosystem && ar.Type != RangeSemVer {
-		return false
-	}
-	// todo: we should probably warn here
-	if len(ar.Events) == 0 {
-		return false
-	}
-
-	vp := semantic.MustParse(pkg.Version, pkg.CompareAs)
-
-	sort.Slice(ar.Events, func(i, j int) bool {
-		a := ar.Events[i]
-		b := ar.Events[j]
-
-		if a.Introduced == "0" {
-			return true
-		}
-
-		if b.Introduced == "0" {
-			return false
-		}
-
-		return semantic.MustParse(a.version(), pkg.CompareAs).CompareStr(b.version()) < 0
-	})
-
-	var affected bool
-	for _, e := range ar.Events {
-		if affected {
-			if e.Fixed != "" {
-				affected = vp.CompareStr(e.Fixed) < 0
-			} else if e.LastAffected != "" {
-				affected = e.LastAffected == pkg.Version || vp.CompareStr(e.LastAffected) <= 0
-			}
-		} else if e.Introduced != "" {
-			affected = e.Introduced == "0" || vp.CompareStr(e.Introduced) >= 0
-		}
-	}
-
-	return affected
-}
-
-// affectsVersion checks if the given version is within the range
-// specified by the events of any "Ecosystem" or "Semver" type ranges
-func affectsVersion(a []Range, pkg lockfile.PackageDetails) bool {
-	for _, r := range a {
-		if r.Type != RangeEcosystem && r.Type != RangeSemVer {
-			return false
-		}
-		if r.containsVersion(pkg) {
-			return true
-		}
-	}
-
-	return false
-}
-
-func (v Vulnerability) isAliasOfID(id string) bool {
-	for _, alias := range v.Aliases {
-		if alias == id {
-			return true
-		}
-	}
-
-	return false
-}
-
-func (v Vulnerability) isAliasOf(vulnerability Vulnerability) bool {
-	for _, alias := range vulnerability.Aliases {
-		if v.ID == alias || v.isAliasOfID(alias) {
-			return true
-		}
-	}
-
-	return false
-}
-
-func (v Vulnerability) AffectsEcosystem(ecosystem lockfile.Ecosystem) bool {
-	for _, affected := range v.Affected {
-		if string(affected.Package.Ecosystem) == string(ecosystem) {
-			return true
-		}
-	}
-
-	return false
-}
-
-func (v Vulnerability) IsAffected(pkg lockfile.PackageDetails) bool {
-	for _, affected := range v.Affected {
-		if string(affected.Package.Ecosystem) == string(pkg.Ecosystem) &&
-			affected.Package.Name == pkg.Name {
-			if len(affected.Ranges) == 0 && len(affected.Versions) == 0 {
-				_, _ = fmt.Fprintf(
-					os.Stderr,
-					"%s does not have any ranges or versions - this is probably a mistake!\n",
-					v.ID,
-				)
-
-				continue
-			}
-
-			if slices.Contains(affected.Versions, pkg.Version) {
-				return true
-			}
-
-			if affectsVersion(affected.Ranges, pkg) {
-				return true
-			}
-
-			// if a package does not have a version, assume it is vulnerable
-			// as false positives are better than false negatives here
-			if pkg.Version == "" {
-				return true
-			}
-		}
-	}
-
-	return false
-}


Test patch:
diff --git a/internal/utility/vulns/vulnerabilities_test.go b/internal/utility/vulns/vulnerabilities_test.go
new file mode 100644
index 0000000..6a15ca1
--- /dev/null
+++ b/internal/utility/vulns/vulnerabilities_test.go
@@ -0,0 +1,129 @@
+package vulns_test
+
+import (
+	"testing"
+
+	"github.com/google/osv-scanner/internal/utility/vulns"
+	"github.com/google/osv-scanner/pkg/models"
+)
+
+func TestVulnerabilities_Includes(t *testing.T) {
+	t.Parallel()
+
+	type args struct {
+		osv models.Vulnerability
+	}
+	tests := []struct {
+		name string
+		vs   models.Vulnerabilities
+		args args
+		want bool
+	}{
+		{
+			name: "",
+			vs: models.Vulnerabilities{
+				models.Vulnerability{
+					ID:      "GHSA-1",
+					Aliases: []string{},
+				},
+			},
+			args: args{
+				osv: models.Vulnerability{
+					ID:      "GHSA-2",
+					Aliases: []string{},
+				},
+			},
+			want: false,
+		},
+		{
+			name: "",
+			vs: models.Vulnerabilities{
+				models.Vulnerability{
+					ID:      "GHSA-1",
+					Aliases: []string{},
+				},
+			},
+			args: args{
+				osv: models.Vulnerability{
+					ID:      "GHSA-1",
+					Aliases: []string{},
+				},
+			},
+			want: true,
+		},
+		{
+			name: "",
+			vs: models.Vulnerabilities{
+				models.Vulnerability{
+					ID:      "GHSA-1",
+					Aliases: []string{"GHSA-2"},
+				},
+			},
+			args: args{
+				osv: models.Vulnerability{
+					ID:      "GHSA-2",
+					Aliases: []string{},
+				},
+			},
+			want: true,
+		},
+		{
+			name: "",
+			vs: models.Vulnerabilities{
+				models.Vulnerability{
+					ID:      "GHSA-1",
+					Aliases: []string{},
+				},
+			},
+			args: args{
+				osv: models.Vulnerability{
+					ID:      "GHSA-2",
+					Aliases: []string{"GHSA-1"},
+				},
+			},
+			want: true,
+		},
+		{
+			name: "",
+			vs: models.Vulnerabilities{
+				models.Vulnerability{
+					ID:      "GHSA-1",
+					Aliases: []string{"CVE-1"},
+				},
+			},
+			args: args{
+				osv: models.Vulnerability{
+					ID:      "GHSA-2",
+					Aliases: []string{"CVE-1"},
+				},
+			},
+			want: true,
+		},
+		{
+			name: "",
+			vs: models.Vulnerabilities{
+				models.Vulnerability{
+					ID:      "GHSA-1",
+					Aliases: []string{"CVE-2"},
+				},
+			},
+			args: args{
+				osv: models.Vulnerability{
+					ID:      "GHSA-2",
+					Aliases: []string{"CVE-2"},
+				},
+			},
+			want: true,
+		},
+	}
+	for _, tt := range tests {
+		tt := tt
+		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
+
+			if got := vulns.Include(tt.vs, tt.args.osv); got != tt.want {
+				t.Errorf("Includes() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
diff --git a/internal/utility/vulns/vulnerability_test.go b/internal/utility/vulns/vulnerability_test.go
new file mode 100644
index 0000000..9e64ee1
--- /dev/null
+++ b/internal/utility/vulns/vulnerability_test.go
@@ -0,0 +1,706 @@
+package vulns_test
+
+import (
+	"testing"
+	"time"
+
+	"github.com/google/osv-scanner/internal/utility/vulns"
+	"github.com/google/osv-scanner/pkg/lockfile"
+	"github.com/google/osv-scanner/pkg/models"
+)
+
+func expectIsAffected(t *testing.T, vuln models.Vulnerability, version string, expectAffected bool) {
+	t.Helper()
+
+	pkg := lockfile.PackageDetails{
+		Name:      "my-package",
+		Version:   version,
+		Ecosystem: lockfile.NpmEcosystem,
+		CompareAs: lockfile.NpmEcosystem,
+	}
+
+	if vulns.IsAffected(vuln, pkg) != expectAffected {
+		if expectAffected {
+			t.Errorf("Expected OSV to affect package version %s but it did not", version)
+		} else {
+			t.Errorf("Expected OSV not to affect package version %s but it did", version)
+		}
+	}
+}
+
+func buildOSVWithAffected(affected ...models.Affected) models.Vulnerability {
+	return models.Vulnerability{
+		ID:        "1",
+		Published: time.Time{},
+		Modified:  time.Time{},
+		Details:   "This is an open source vulnerability!",
+		Affected:  affected,
+	}
+}
+
+func buildEcosystemAffectsRange(events ...models.Event) models.Range {
+	return models.Range{Type: models.RangeEcosystem, Events: events}
+}
+
+func buildSemverAffectsRange(events ...models.Event) models.Range {
+	return models.Range{Type: models.RangeSemVer, Events: events}
+}
+
+func TestOSV_AffectsEcosystem(t *testing.T) {
+	t.Parallel()
+
+	type AffectsTest struct {
+		Affected  []models.Affected
+		Ecosystem lockfile.Ecosystem
+		Expected  bool
+	}
+
+	tests := []AffectsTest{
+		{Affected: nil, Ecosystem: "Go", Expected: false},
+		{Affected: nil, Ecosystem: "npm", Expected: false},
+		{Affected: nil, Ecosystem: "PyPI", Expected: false},
+		{Affected: nil, Ecosystem: "", Expected: false},
+		{
+			Affected: []models.Affected{
+				{Package: models.Package{Ecosystem: "crates.io"}},
+				{Package: models.Package{Ecosystem: "npm"}},
+				{Package: models.Package{Ecosystem: "PyPI"}},
+			},
+			Ecosystem: "Packagist",
+			Expected:  false,
+		},
+		{
+			Affected: []models.Affected{
+				{Package: models.Package{Ecosystem: "NuGet"}},
+			},
+			Ecosystem: "NuGet",
+			Expected:  true,
+		},
+		{
+			Affected: []models.Affected{
+				{Package: models.Package{Ecosystem: "npm"}},
+				{Package: models.Package{Ecosystem: "npm"}},
+			},
+			Ecosystem: "npm",
+			Expected:  true,
+		},
+	}
+
+	for i, test := range tests {
+		vuln := models.Vulnerability{
+			ID:        "1",
+			Published: time.Time{},
+			Modified:  time.Time{},
+			Details:   "This is an open source vulnerability!",
+			Affected:  test.Affected,
+		}
+
+		if vulns.AffectsEcosystem(vuln, test.Ecosystem) != test.Expected {
+			t.Errorf(
+				"Test #%d: Expected OSV to return %t but it returned %t",
+				i,
+				test.Expected,
+				!test.Expected,
+			)
+		}
+	}
+
+	// test when the OSV doesn't have an "Affected"
+	vuln := models.Vulnerability{
+		ID:        "1",
+		Published: time.Time{},
+		Modified:  time.Time{},
+		Details:   "This is an open source vulnerability!",
+		Affected:  nil,
+	}
+
+	if vulns.AffectsEcosystem(vuln, "npm") {
+		t.Errorf(
+			"Expected OSV to report 'false' when it doesn't have an Affected, but it reported true!",
+		)
+	}
+}
+
+func TestOSV_IsAffected_AffectsWithEcosystem_DifferentEcosystem(t *testing.T) {
+	t.Parallel()
+
+	vuln := buildOSVWithAffected(
+		models.Affected{
+			Package: models.Package{Ecosystem: models.EcosystemPyPI, Name: "my-package"},
+			Ranges: []models.Range{
+				buildEcosystemAffectsRange(models.Event{Introduced: "0"}),
+			},
+		},
+	)
+
+	for _, v := range []string{"1.0.0", "1.1.1", "2.0.0"} {
+		expectIsAffected(t, vuln, v, false)
+	}
+}
+
+func TestOSV_IsAffected_AffectsWithEcosystem_SingleAffected(t *testing.T) {
+	t.Parallel()
+
+	var vuln models.Vulnerability
+
+	// "Introduced: 0" means everything is affected
+	vuln = buildOSVWithAffected(
+		models.Affected{
+			Package: models.Package{Ecosystem: models.EcosystemNPM, Name: "my-package"},
+			Ranges: []models.Range{
+				buildEcosystemAffectsRange(models.Event{Introduced: "0"}),
+			},
+		},
+	)
+
+	for _, v := range []string{"1.0.0", "1.1.1", "2.0.0"} {
+		expectIsAffected(t, vuln, v, true)
+	}
+
+	// an empty version should always be treated as affected
+	expectIsAffected(t, vuln, "", true)
+
+	// "Fixed: 1" means all versions after this are not vulnerable
+	vuln = buildOSVWithAffected(
+		models.Affected{
+			Package: models.Package{Ecosystem: models.EcosystemNPM, Name: "my-package"},
+			Ranges: []models.Range{
+				buildEcosystemAffectsRange(
+					models.Event{Introduced: "0"},
+					models.Event{Fixed: "1"},
+				),
+			},
+		},
+	)
+
+	for _, v := range []string{"0.0.0", "0.1.0", "0.0.0.1", "1.0.0-rc"} {
+		expectIsAffected(t, vuln, v, true)
+	}
+
+	for _, v := range []string{"1.0.0", "1.1.0", "2.0.0"} {
+		expectIsAffected(t, vuln, v, false)
+	}
+
+	// an empty version should always be treated as affected
+	expectIsAffected(t, vuln, "", true)
+
+	// multiple fixes and introduced
+	vuln = buildOSVWithAffected(
+		models.Affected{
+			Package: models.Package{Ecosystem: models.EcosystemNPM, Name: "my-package"},
+			Ranges: []models.Range{
+				buildEcosystemAffectsRange(
+					models.Event{Introduced: "0"},
+					models.Event{Fixed: "1"},
+					models.Event{Introduced: "2.1.0"},
+					models.Event{Fixed: "3.2.0"},
+				),
+			},
+		},
+	)
+
+	for _, v := range []string{"0.0.0", "0.1.0", "0.0.0.1", "1.0.0-rc"} {
+		expectIsAffected(t, vuln, v, true)
+	}
+
+	for _, v := range []string{"1.0.0", "1.1.0", "2.0.0rc2", "2.0.1"} {
+		expectIsAffected(t, vuln, v, false)
+	}
+
+	for _, v := range []string{"2.1.1", "2.3.4", "3.0.0", "3.0.0-rc"} {
+		expectIsAffected(t, vuln, v, true)
+	}
+
+	for _, v := range []string{"3.2.0", "3.2.1", "4.0.0"} {
+		expectIsAffected(t, vuln, v, false)
+	}
+
+	// an empty version should always be treated as affected
+	expectIsAffected(t, vuln, "", true)
+
+	// "LastAffected: 1" means all versions after this are not vulnerable
+	vuln = buildOSVWithAffected(
+		models.Affected{
+			Package: models.Package{Ecosystem: models.EcosystemNPM, Name: "my-package"},
+			Ranges: []models.Range{
+				buildEcosystemAffectsRange(
+					models.Event{Introduced: "0"},
+					models.Event{LastAffected: "1"},
+				),
+			},
+		},
+	)
+
+	for _, v := range []string{"0.0.0", "0.1.0", "0.0.0.1", "1.0.0-rc", "1.0.0"} {
+		expectIsAffected(t, vuln, v, true)
+	}
+
+	for _, v := range []string{"1.0.1", "1.1.0", "2.0.0"} {
+		expectIsAffected(t, vuln, v, false)
+	}
+
+	// an empty version should always be treated as affected
+	expectIsAffected(t, vuln, "", true)
+
+	// mix of fixes, last_known_affected, and introduced
+	vuln = buildOSVWithAffected(
+		models.Affected{
+			Package: models.Package{Ecosystem: models.EcosystemNPM, Name: "my-package"},
+			Ranges: []models.Range{
+				buildEcosystemAffectsRange(
+					models.Event{Introduced: "0"},
+					models.Event{Fixed: "1"},
+					models.Event{Introduced: "2.1.0"},
+					models.Event{LastAffected: "3.1.9"},
+				),
+			},
+		},
+	)
+
+	for _, v := range []string{"0.0.0", "0.1.0", "0.0.0.1", "1.0.0-rc"} {
+		expectIsAffected(t, vuln, v, true)
+	}
+
+	for _, v := range []string{"1.0.0", "1.1.0", "2.0.0rc2", "2.0.1"} {
+		expectIsAffected(t, vuln, v, false)
+	}
+
+	for _, v := range []string{"2.1.1", "2.3.4", "3.0.0", "3.0.0-rc", "3.1.9"} {
+		expectIsAffected(t, vuln, v, true)
+	}
+
+	for _, v := range []string{"3.2.0", "3.2.1", "4.0.0"} {
+		expectIsAffected(t, vuln, v, false)
+	}
+
+	// an empty version should always be treated as affected
+	expectIsAffected(t, vuln, "", true)
+}
+
+func TestOSV_IsAffected_AffectsWithEcosystem_MultipleAffected(t *testing.T) {
+	t.Parallel()
+
+	vuln := buildOSVWithAffected(
+		models.Affected{
+			Package: models.Package{Ecosystem: models.EcosystemNPM, Name: "my-package"},
+			Ranges: []models.Range{
+				buildEcosystemAffectsRange(
+					models.Event{Introduced: "0"},
+					models.Event{Fixed: "1"},
+				),
+			},
+		},
+		models.Affected{
+			Package: models.Package{Ecosystem: models.EcosystemNPM, Name: "my-package"},
+			Ranges: []models.Range{
+				buildEcosystemAffectsRange(
+					models.Event{Introduced: "2.1.0"},
+					models.Event{Fixed: "3.2.0"},
+				),
+			},
+		},
+		models.Affected{
+			Package: models.Package{Ecosystem: models.EcosystemNPM, Name: "my-package"},
+			Ranges: []models.Range{
+				buildEcosystemAffectsRange(
+					models.Event{Introduced: "3.3.0"},
+					models.Event{LastAffected: "3.5.0"},
+				),
+			},
+		},
+	)
+
+	for _, v := range []string{"0.0.0", "0.1.0", "0.0.0.1", "1.0.0-rc"} {
+		expectIsAffected(t, vuln, v, true)
+	}
+
+	for _, v := range []string{"1.0.0", "1.1.0", "2.0.0rc2", "2.0.1"} {
+		expectIsAffected(t, vuln, v, false)
+	}
+
+	for _, v := range []string{"2.1.1", "2.3.4", "3.0.0", "3.0.0-rc"} {
+		expectIsAffected(t, vuln, v, true)
+	}
+
+	for _, v := range []string{"3.2.0", "3.2.1", "4.0.0"} {
+		expectIsAffected(t, vuln, v, false)
+	}
+
+	for _, v := range []string{"3.3.1", "3.4.5"} {
+		expectIsAffected(t, vuln, v, true)
+	}
+
+	// an empty version should always be treated as affected
+	expectIsAffected(t, vuln, "", true)
+}
+
+func TestOSV_IsAffected_AffectsWithEcosystem_Unsorted(t *testing.T) {
+	t.Parallel()
+
+	vuln := buildOSVWithAffected(
+		models.Affected{
+			Package: models.Package{Ecosystem: models.EcosystemNPM, Name: "my-package"},
+			Ranges: []models.Range{
+				buildEcosystemAffectsRange(
+					models.Event{Introduced: "0"},
+					models.Event{Introduced: "2.1.0"},
+					models.Event{Fixed: "1"},
+					models.Event{LastAffected: "3.1.9"},
+				),
+			},
+		},
+	)
+
+	for _, v := range []string{"0.0.0", "0.1.0", "0.0.0.1", "1.0.0-rc"} {
+		expectIsAffected(t, vuln, v, true)
+	}
+
+	for _, v := range []string{"1.0.0", "1.1.0", "2.0.0rc2", "2.0.1"} {
+		expectIsAffected(t, vuln, v, false)
+	}
+
+	for _, v := range []string{"2.1.1", "2.3.4", "3.0.0", "3.0.0-rc", "3.1.9"} {
+		expectIsAffected(t, vuln, v, true)
+	}
+
+	for _, v := range []string{"3.2.0", "3.2.1", "4.0.0"} {
+		expectIsAffected(t, vuln, v, false)
+	}
+
+	// an empty version should always be treated as affected
+	expectIsAffected(t, vuln, "", true)
+
+	// zeros with build strings
+	vuln = buildOSVWithAffected(
+		models.Affected{
+			// golang.org/x/sys
+			Package: models.Package{Ecosystem: models.EcosystemNPM, Name: "my-package"},
+			Ranges: []models.Range{
+				buildEcosystemAffectsRange(
+					models.Event{Fixed: "0.0.0-20220412211240-33da011f77ad"},
+					models.Event{Introduced: "0"},
+				),
+			},
+		},
+		models.Affected{
+			// golang.org/x/net
+			Package: models.Package{Ecosystem: models.EcosystemNPM, Name: "my-package"},
+			Ranges: []models.Range{
+				buildEcosystemAffectsRange(
+					models.Event{Introduced: "0.0.0-20180925071336-cf3bd585ca2a"},
+					models.Event{Fixed: "0"},
+				),
+			},
+		},
+	)
+
+	for _, v := range []string{"0.0.0", "0.14.0"} {
+		expectIsAffected(t, vuln, v, false)
+	}
+
+	for _, v := range []string{"0.0.0-20180925071336-cf3bd585ca2a"} {
+		expectIsAffected(t, vuln, v, true)
+	}
+
+	// an empty version should always be treated as affected
+	expectIsAffected(t, vuln, "", true)
+}
+
+func TestOSV_IsAffected_AffectsWithSemver_DifferentEcosystem(t *testing.T) {
+	t.Parallel()
+
+	vuln := buildOSVWithAffected(
+		models.Affected{
+			Package: models.Package{Ecosystem: models.EcosystemPyPI, Name: "my-package"},
+			Ranges: []models.Range{
+				buildSemverAffectsRange(models.Event{Introduced: "0"}),
+			},
+		},
+	)
+
+	for _, v := range []string{"1.0.0", "1.1.1", "2.0.0"} {
+		expectIsAffected(t, vuln, v, false)
+	}
+}
+
+func TestOSV_IsAffected_AffectsWithSemver_SingleAffected(t *testing.T) {
+	t.Parallel()
+
+	var vuln models.Vulnerability
+
+	// "Introduced: 0" means everything is affected
+	vuln = buildOSVWithAffected(
+		models.Affected{
+			Package: models.Package{Ecosystem: models.EcosystemNPM, Name: "my-package"},
+			Ranges: []models.Range{
+				buildSemverAffectsRange(models.Event{Introduced: "0"}),
+			},
+		},
+	)
+
+	for _, v := range []string{"v1.0.0", "v1.1.1", "v2.0.0"} {
+		expectIsAffected(t, vuln, v, true)
+	}
+
+	// "Fixed: 1" means all versions after this are not vulnerable
+	vuln = buildOSVWithAffected(
+		models.Affected{
+			Package: models.Package{Ecosystem: models.EcosystemNPM, Name: "my-package"},
+			Ranges: []models.Range{
+				buildSemverAffectsRange(
+					models.Event{Introduced: "0"},
+					models.Event{Fixed: "1.0.0"},
+				),
+			},
+		},
+	)
+
+	for _, v := range []string{"0.0.0", "0.1.0", "0.0.0.1", "1.0.0-rc"} {
+		expectIsAffected(t, vuln, v, true)
+	}
+
+	for _, v := range []string{"1.0.0", "1.1.0", "2.0.0"} {
+		expectIsAffected(t, vuln, v, false)
+	}
+
+	// multiple fixes and introduced
+	vuln = buildOSVWithAffected(
+		models.Affected{
+			Package: models.Package{Ecosystem: models.EcosystemNPM, Name: "my-package"},
+			Ranges: []models.Range{
+				buildSemverAffectsRange(
+					models.Event{Introduced: "0"},
+					models.Event{Fixed: "1"},
+					models.Event{Introduced: "2.1.0"},
+					models.Event{Fixed: "3.2.0"},
+				),
+			},
+		},
+	)
+
+	for _, v := range []string{"0.0.0", "0.1.0", "0.0.0.1", "1.0.0-rc"} {
+		expectIsAffected(t, vuln, v, true)
+	}
+
+	for _, v := range []string{"1.0.0", "1.1.0", "2.0.0rc2", "2.0.1"} {
+		expectIsAffected(t, vuln, v, false)
+	}
+
+	for _, v := range []string{"2.1.1", "2.3.4", "3.0.0", "3.0.0-rc"} {
+		expectIsAffected(t, vuln, v, true)
+	}
+
+	for _, v := range []string{"3.2.0", "3.2.1", "4.0.0"} {
+		expectIsAffected(t, vuln, v, false)
+	}
+
+	// an empty version should always be treated as affected
+	expectIsAffected(t, vuln, "", true)
+
+	// "LastAffected: 1" means all versions after this are not vulnerable
+	vuln = buildOSVWithAffected(
+		models.Affected{
+			Package: models.Package{Ecosystem: models.EcosystemNPM, Name: "my-package"},
+			Ranges: []models.Range{
+				buildSemverAffectsRange(
+					models.Event{Introduced: "0"},
+					models.Event{LastAffected: "1.0.0"},
+				),
+			},
+		},
+	)
+
+	for _, v := range []string{"0.0.0", "0.1.0", "0.0.0.1", "1.0.0-rc", "1.0.0"} {
+		expectIsAffected(t, vuln, v, true)
+	}
+
+	for _, v := range []string{"1.0.1", "1.1.0", "2.0.0"} {
+		expectIsAffected(t, vuln, v, false)
+	}
+
+	// mix of fixes, last_known_affected, and introduced
+	vuln = buildOSVWithAffected(
+		models.Affected{
+			Package: models.Package{Ecosystem: models.EcosystemNPM, Name: "my-package"},
+			Ranges: []models.Range{
+				buildSemverAffectsRange(
+					models.Event{Introduced: "0"},
+					models.Event{Fixed: "1"},
+					models.Event{Introduced: "2.1.0"},
+					models.Event{LastAffected: "3.1.9"},
+				),
+			},
+		},
+	)
+
+	for _, v := range []string{"0.0.0", "0.1.0", "0.0.0.1", "1.0.0-rc"} {
+		expectIsAffected(t, vuln, v, true)
+	}
+
+	for _, v := range []string{"1.0.0", "1.1.0", "2.0.0rc2", "2.0.1"} {
+		expectIsAffected(t, vuln, v, false)
+	}
+
+	for _, v := range []string{"2.1.1", "2.3.4", "3.0.0", "3.0.0-rc"} {
+		expectIsAffected(t, vuln, v, true)
+	}
+
+	for _, v := range []string{"3.2.0", "3.2.1", "4.0.0"} {
+		expectIsAffected(t, vuln, v, false)
+	}
+
+	// an empty version should always be treated as affected
+	expectIsAffected(t, vuln, "", true)
+}
+
+func TestOSV_IsAffected_AffectsWithSemver_MultipleAffected(t *testing.T) {
+	t.Parallel()
+
+	vuln := buildOSVWithAffected(
+		models.Affected{
+			Package: models.Package{Ecosystem: models.EcosystemNPM, Name: "my-package"},
+			Ranges: []models.Range{
+				buildSemverAffectsRange(
+					models.Event{Introduced: "0"},
+					models.Event{Fixed: "1"},
+				),
+			},
+		},
+		models.Affected{
+			Package: models.Package{Ecosystem: models.EcosystemNPM, Name: "my-package"},
+			Ranges: []models.Range{
+				buildSemverAffectsRange(
+					models.Event{Introduced: "2.1.0"},
+					models.Event{Fixed: "3.2.0"},
+				),
+			},
+		},
+		models.Affected{
+			Package: models.Package{Ecosystem: models.EcosystemNPM, Name: "my-package"},
+			Ranges: []models.Range{
+				buildSemverAffectsRange(
+					models.Event{Introduced: "3.3.0"},
+					models.Event{LastAffected: "3.5.0"},
+				),
+			},
+		},
+	)
+
+	for _, v := range []string{"0.0.0", "0.1.0", "0.0.0.1", "1.0.0-rc"} {
+		expectIsAffected(t, vuln, v, true)
+	}
+
+	for _, v := range []string{"1.0.0", "1.1.0", "2.0.0rc2", "2.0.1"} {
+		expectIsAffected(t, vuln, v, false)
+	}
+
+	for _, v := range []string{"2.1.1", "2.3.4", "3.0.0", "3.0.0-rc"} {
+		expectIsAffected(t, vuln, v, true)
+	}
+
+	for _, v := range []string{"3.2.0", "3.2.1", "4.0.0"} {
+		expectIsAffected(t, vuln, v, false)
+	}
+
+	for _, v := range []string{"3.3.1", "3.4.5", "3.5.0"} {
+		expectIsAffected(t, vuln, v, true)
+	}
+
+	// an empty version should always be treated as affected
+	expectIsAffected(t, vuln, "", true)
+}
+
+func TestOSV_IsAffected_AffectsWithSemver_Unsorted(t *testing.T) {
+	t.Parallel()
+
+	// mix of fixes, last_known_affected, and introduced
+	vuln := buildOSVWithAffected(
+		models.Affected{
+			Package: models.Package{Ecosystem: models.EcosystemNPM, Name: "my-package"},
+			Ranges: []models.Range{
+				buildSemverAffectsRange(
+					models.Event{Introduced: "0"},
+					models.Event{Introduced: "2.1.0"},
+					models.Event{Fixed: "1"},
+					models.Event{LastAffected: "3.1.9"},
+				),
+			},
+		},
+	)
+
+	for _, v := range []string{"0.0.0", "0.1.0", "0.0.0.1", "1.0.0-rc"} {
+		expectIsAffected(t, vuln, v, true)
+	}
+
+	for _, v := range []string{"1.0.0", "1.1.0", "2.0.0rc2", "2.0.1"} {
+		expectIsAffected(t, vuln, v, false)
+	}
+
+	for _, v := range []string{"2.1.1", "2.3.4", "3.0.0", "3.0.0-rc"} {
+		expectIsAffected(t, vuln, v, true)
+	}
+
+	for _, v := range []string{"3.2.0", "3.2.1", "4.0.0"} {
+		expectIsAffected(t, vuln, v, false)
+	}
+
+	// an empty version should always be treated as affected
+	expectIsAffected(t, vuln, "", true)
+
+	// an empty version should always be treated as affected
+	expectIsAffected(t, vuln, "", true)
+
+	// zeros with build strings
+	vuln = buildOSVWithAffected(
+		models.Affected{
+			// golang.org/x/sys
+			Package: models.Package{Ecosystem: models.EcosystemNPM, Name: "my-package"},
+			Ranges: []models.Range{
+				buildSemverAffectsRange(
+					models.Event{Fixed: "0.0.0-20220412211240-33da011f77ad"},
+					models.Event{Introduced: "0"},
+				),
+			},
+		},
+		models.Affected{
+			// golang.org/x/net
+			Package: models.Package{Ecosystem: models.EcosystemNPM, Name: "my-package"},
+			Ranges: []models.Range{
+				buildSemverAffectsRange(
+					models.Event{Introduced: "0.0.0-20180925071336-cf3bd585ca2a"},
+					models.Event{Fixed: "0"},
+				),
+			},
+		},
+	)
+
+	for _, v := range []string{"0.0.0", "0.14.0"} {
+		expectIsAffected(t, vuln, v, false)
+	}
+
+	for _, v := range []string{"0.0.0-20180925071336-cf3bd585ca2a"} {
+		expectIsAffected(t, vuln, v, true)
+	}
+
+	// an empty version should always be treated as affected
+	expectIsAffected(t, vuln, "", true)
+}
+
+func TestOSV_IsAffected_OnlyVersions(t *testing.T) {
+	t.Parallel()
+
+	vuln := buildOSVWithAffected(
+		models.Affected{
+			Package:  models.Package{Ecosystem: models.EcosystemNPM, Name: "my-package"},
+			Versions: []string{"1.0.0"},
+		},
+	)
+
+	expectIsAffected(t, vuln, "0.0.0", false)
+	expectIsAffected(t, vuln, "1.0.0", true)
+	expectIsAffected(t, vuln, "1.0.0-beta1", false)
+	expectIsAffected(t, vuln, "1.1.0", false)
+
+	// an empty version should always be treated as affected
+	expectIsAffected(t, vuln, "", true)
+}
diff --git a/pkg/models/vulnerabilities_test.go b/pkg/models/vulnerabilities_test.go
index 2430283..2d06f2d 100644
--- a/pkg/models/vulnerabilities_test.go
+++ b/pkg/models/vulnerabilities_test.go
@@ -7,127 +7,6 @@ import (
 	"github.com/google/osv-scanner/pkg/models"
 )
 
-func TestVulnerabilities_Includes(t *testing.T) {
-	t.Parallel()
-
-	type args struct {
-		osv models.Vulnerability
-	}
-	tests := []struct {
-		name string
-		vs   models.Vulnerabilities
-		args args
-		want bool
-	}{
-		{
-			name: "",
-			vs: models.Vulnerabilities{
-				models.Vulnerability{
-					ID:      "GHSA-1",
-					Aliases: []string{},
-				},
-			},
-			args: args{
-				osv: models.Vulnerability{
-					ID:      "GHSA-2",
-					Aliases: []string{},
-				},
-			},
-			want: false,
-		},
-		{
-			name: "",
-			vs: models.Vulnerabilities{
-				models.Vulnerability{
-					ID:      "GHSA-1",
-					Aliases: []string{},
-				},
-			},
-			args: args{
-				osv: models.Vulnerability{
-					ID:      "GHSA-1",
-					Aliases: []string{},
-				},
-			},
-			want: true,
-		},
-		{
-			name: "",
-			vs: models.Vulnerabilities{
-				models.Vulnerability{
-					ID:      "GHSA-1",
-					Aliases: []string{"GHSA-2"},
-				},
-			},
-			args: args{
-				osv: models.Vulnerability{
-					ID:      "GHSA-2",
-					Aliases: []string{},
-				},
-			},
-			want: true,
-		},
-		{
-			name: "",
-			vs: models.Vulnerabilities{
-				models.Vulnerability{
-					ID:      "GHSA-1",
-					Aliases: []string{},
-				},
-			},
-			args: args{
-				osv: models.Vulnerability{
-					ID:      "GHSA-2",
-					Aliases: []string{"GHSA-1"},
-				},
-			},
-			want: true,
-		},
-		{
-			name: "",
-			vs: models.Vulnerabilities{
-				models.Vulnerability{
-					ID:      "GHSA-1",
-					Aliases: []string{"CVE-1"},
-				},
-			},
-			args: args{
-				osv: models.Vulnerability{
-					ID:      "GHSA-2",
-					Aliases: []string{"CVE-1"},
-				},
-			},
-			want: true,
-		},
-		{
-			name: "",
-			vs: models.Vulnerabilities{
-				models.Vulnerability{
-					ID:      "GHSA-1",
-					Aliases: []string{"CVE-2"},
-				},
-			},
-			args: args{
-				osv: models.Vulnerability{
-					ID:      "GHSA-2",
-					Aliases: []string{"CVE-2"},
-				},
-			},
-			want: true,
-		},
-	}
-	for _, tt := range tests {
-		tt := tt
-		t.Run(tt.name, func(t *testing.T) {
-			t.Parallel()
-
-			if got := tt.vs.Includes(tt.args.osv); got != tt.want {
-				t.Errorf("Includes() = %v, want %v", got, tt.want)
-			}
-		})
-	}
-}
-
 func TestVulnerabilities_MarshalJSON(t *testing.T) {
 	t.Parallel()
 
diff --git a/pkg/models/vulnerability_test.go b/pkg/models/vulnerability_test.go
index 1303e07..11171ca 100644
--- a/pkg/models/vulnerability_test.go
+++ b/pkg/models/vulnerability_test.go
@@ -5,7 +5,6 @@ import (
 	"testing"
 	"time"
 
-	"github.com/google/osv-scanner/pkg/lockfile"
 	"github.com/google/osv-scanner/pkg/models"
 	"gopkg.in/yaml.v3"
 )
@@ -232,699 +231,3 @@ func TestVulnerability_MarshalYAMLTimes(t *testing.T) {
 		})
 	}
 }
-
-func expectIsAffected(t *testing.T, vuln models.Vulnerability, version string, expectAffected bool) {
-	t.Helper()
-
-	pkg := lockfile.PackageDetails{
-		Name:      "my-package",
-		Version:   version,
-		Ecosystem: lockfile.NpmEcosystem,
-		CompareAs: lockfile.NpmEcosystem,
-	}
-
-	if vuln.IsAffected(pkg) != expectAffected {
-		if expectAffected {
-			t.Errorf("Expected OSV to affect package version %s but it did not", version)
-		} else {
-			t.Errorf("Expected OSV not to affect package version %s but it did", version)
-		}
-	}
-}
-
-func buildOSVWithAffected(affected ...models.Affected) models.Vulnerability {
-	return models.Vulnerability{
-		ID:        "1",
-		Published: time.Time{},
-		Modified:  time.Time{},
-		Details:   "This is an open source vulnerability!",
-		Affected:  affected,
-	}
-}
-
-func buildEcosystemAffectsRange(events ...models.Event) models.Range {
-	return models.Range{Type: models.RangeEcosystem, Events: events}
-}
-
-func buildSemverAffectsRange(events ...models.Event) models.Range {
-	return models.Range{Type: models.RangeSemVer, Events: events}
-}
-
-func TestOSV_AffectsEcosystem(t *testing.T) {
-	t.Parallel()
-
-	type AffectsTest struct {
-		Affected  []models.Affected
-		Ecosystem lockfile.Ecosystem
-		Expected  bool
-	}
-
-	tests := []AffectsTest{
-		{Affected: nil, Ecosystem: "Go", Expected: false},
-		{Affected: nil, Ecosystem: "npm", Expected: false},
-		{Affected: nil, Ecosystem: "PyPI", Expected: false},
-		{Affected: nil, Ecosystem: "", Expected: false},
-		{
-			Affected: []models.Affected{
-				{Package: models.Package{Ecosystem: "crates.io"}},
-				{Package: models.Package{Ecosystem: "npm"}},
-				{Package: models.Package{Ecosystem: "PyPI"}},
-			},
-			Ecosystem: "Packagist",
-			Expected:  false,
-		},
-		{
-			Affected: []models.Affected{
-				{Package: models.Package{Ecosystem: "NuGet"}},
-			},
-			Ecosystem: "NuGet",
-			Expected:  true,
-		},
-		{
-			Affected: []models.Affected{
-				{Package: models.Package{Ecosystem: "npm"}},
-				{Package: models.Package{Ecosystem: "npm"}},
-			},
-			Ecosystem: "npm",
-			Expected:  true,
-		},
-	}
-
-	for i, test := range tests {
-		vuln := models.Vulnerability{
-			ID:        "1",
-			Published: time.Time{},
-			Modified:  time.Time{},
-			Details:   "This is an open source vulnerability!",
-			Affected:  test.Affected,
-		}
-
-		if vuln.AffectsEcosystem(test.Ecosystem) != test.Expected {
-			t.Errorf(
-				"Test #%d: Expected OSV to return %t but it returned %t",
-				i,
-				test.Expected,
-				!test.Expected,
-			)
-		}
-	}
-
-	// test when the OSV doesn't have an "Affected"
-	vuln := models.Vulnerability{
-		ID:        "1",
-		Published: time.Time{},
-		Modified:  time.Time{},
-		Details:   "This is an open source vulnerability!",
-		Affected:  nil,
-	}
-
-	if vuln.AffectsEcosystem("npm") {
-		t.Errorf(
-			"Expected OSV to report 'false' when it doesn't have an Affected, but it reported true!",
-		)
-	}
-}
-
-func TestOSV_IsAffected_AffectsWithEcosystem_DifferentEcosystem(t *testing.T) {
-	t.Parallel()
-
-	vuln := buildOSVWithAffected(
-		models.Affected{
-			Package: models.Package{Ecosystem: models.EcosystemPyPI, Name: "my-package"},
-			Ranges: []models.Range{
-				buildEcosystemAffectsRange(models.Event{Introduced: "0"}),
-			},
-		},
-	)
-
-	for _, v := range []string{"1.0.0", "1.1.1", "2.0.0"} {
-		expectIsAffected(t, vuln, v, false)
-	}
-}
-
-func TestOSV_IsAffected_AffectsWithEcosystem_SingleAffected(t *testing.T) {
-	t.Parallel()
-
-	var vuln models.Vulnerability
-
-	// "Introduced: 0" means everything is affected
-	vuln = buildOSVWithAffected(
-		models.Affected{
-			Package: models.Package{Ecosystem: models.EcosystemNPM, Name: "my-package"},
-			Ranges: []models.Range{
-				buildEcosystemAffectsRange(models.Event{Introduced: "0"}),
-			},
-		},
-	)
-
-	for _, v := range []string{"1.0.0", "1.1.1", "2.0.0"} {
-		expectIsAffected(t, vuln, v, true)
-	}
-
-	// an empty version should always be treated as affected
-	expectIsAffected(t, vuln, "", true)
-
-	// "Fixed: 1" means all versions after this are not vulnerable
-	vuln = buildOSVWithAffected(
-		models.Affected{
-			Package: models.Package{Ecosystem: models.EcosystemNPM, Name: "my-package"},
-			Ranges: []models.Range{
-				buildEcosystemAffectsRange(
-					models.Event{Introduced: "0"},
-					models.Event{Fixed: "1"},
-				),
-			},
-		},
-	)
-
-	for _, v := range []string{"0.0.0", "0.1.0", "0.0.0.1", "1.0.0-rc"} {
-		expectIsAffected(t, vuln, v, true)
-	}
-
-	for _, v := range []string{"1.0.0", "1.1.0", "2.0.0"} {
-		expectIsAffected(t, vuln, v, false)
-	}
-
-	// an empty version should always be treated as affected
-	expectIsAffected(t, vuln, "", true)
-
-	// multiple fixes and introduced
-	vuln = buildOSVWithAffected(
-		models.Affected{
-			Package: models.Package{Ecosystem: models.EcosystemNPM, Name: "my-package"},
-			Ranges: []models.Range{
-				buildEcosystemAffectsRange(
-					models.Event{Introduced: "0"},
-					models.Event{Fixed: "1"},
-					models.Event{Introduced: "2.1.0"},
-					models.Event{Fixed: "3.2.0"},
-				),
-			},
-		},
-	)
-
-	for _, v := range []string{"0.0.0", "0.1.0", "0.0.0.1", "1.0.0-rc"} {
-		expectIsAffected(t, vuln, v, true)
-	}
-
-	for _, v := range []string{"1.0.0", "1.1.0", "2.0.0rc2", "2.0.1"} {
-		expectIsAffected(t, vuln, v, false)
-	}
-
-	for _, v := range []string{"2.1.1", "2.3.4", "3.0.0", "3.0.0-rc"} {
-		expectIsAffected(t, vuln, v, true)
-	}
-
-	for _, v := range []string{"3.2.0", "3.2.1", "4.0.0"} {
-		expectIsAffected(t, vuln, v, false)
-	}
-
-	// an empty version should always be treated as affected
-	expectIsAffected(t, vuln, "", true)
-
-	// "LastAffected: 1" means all versions after this are not vulnerable
-	vuln = buildOSVWithAffected(
-		models.Affected{
-			Package: models.Package{Ecosystem: models.EcosystemNPM, Name: "my-package"},
-			Ranges: []models.Range{
-				buildEcosystemAffectsRange(
-					models.Event{Introduced: "0"},
-					models.Event{LastAffected: "1"},
-				),
-			},
-		},
-	)
-
-	for _, v := range []string{"0.0.0", "0.1.0", "0.0.0.1", "1.0.0-rc", "1.0.0"} {
-		expectIsAffected(t, vuln, v, true)
-	}
-
-	for _, v := range []string{"1.0.1", "1.1.0", "2.0.0"} {
-		expectIsAffected(t, vuln, v, false)
-	}
-
-	// an empty version should always be treated as affected
-	expectIsAffected(t, vuln, "", true)
-
-	// mix of fixes, last_known_affected, and introduced
-	vuln = buildOSVWithAffected(
-		models.Affected{
-			Package: models.Package{Ecosystem: models.EcosystemNPM, Name: "my-package"},
-			Ranges: []models.Range{
-				buildEcosystemAffectsRange(
-					models.Event{Introduced: "0"},
-					models.Event{Fixed: "1"},
-					models.Event{Introduced: "2.1.0"},
-					models.Event{LastAffected: "3.1.9"},
-				),
-			},
-		},
-	)
-
-	for _, v := range []string{"0.0.0", "0.1.0", "0.0.0.1", "1.0.0-rc"} {
-		expectIsAffected(t, vuln, v, true)
-	}
-
-	for _, v := range []string{"1.0.0", "1.1.0", "2.0.0rc2", "2.0.1"} {
-		expectIsAffected(t, vuln, v, false)
-	}
-
-	for _, v := range []string{"2.1.1", "2.3.4", "3.0.0", "3.0.0-rc", "3.1.9"} {
-		expectIsAffected(t, vuln, v, true)
-	}
-
-	for _, v := range []string{"3.2.0", "3.2.1", "4.0.0"} {
-		expectIsAffected(t, vuln, v, false)
-	}
-
-	// an empty version should always be treated as affected
-	expectIsAffected(t, vuln, "", true)
-}
-
-func TestOSV_IsAffected_AffectsWithEcosystem_MultipleAffected(t *testing.T) {
-	t.Parallel()
-
-	vuln := buildOSVWithAffected(
-		models.Affected{
-			Package: models.Package{Ecosystem: models.EcosystemNPM, Name: "my-package"},
-			Ranges: []models.Range{
-				buildEcosystemAffectsRange(
-					models.Event{Introduced: "0"},
-					models.Event{Fixed: "1"},
-				),
-			},
-		},
-		models.Affected{
-			Package: models.Package{Ecosystem: models.EcosystemNPM, Name: "my-package"},
-			Ranges: []models.Range{
-				buildEcosystemAffectsRange(
-					models.Event{Introduced: "2.1.0"},
-					models.Event{Fixed: "3.2.0"},
-				),
-			},
-		},
-		models.Affected{
-			Package: models.Package{Ecosystem: models.EcosystemNPM, Name: "my-package"},
-			Ranges: []models.Range{
-				buildEcosystemAffectsRange(
-					models.Event{Introduced: "3.3.0"},
-					models.Event{LastAffected: "3.5.0"},
-				),
-			},
-		},
-	)
-
-	for _, v := range []string{"0.0.0", "0.1.0", "0.0.0.1", "1.0.0-rc"} {
-		expectIsAffected(t, vuln, v, true)
-	}
-
-	for _, v := range []string{"1.0.0", "1.1.0", "2.0.0rc2", "2.0.1"} {
-		expectIsAffected(t, vuln, v, false)
-	}
-
-	for _, v := range []string{"2.1.1", "2.3.4", "3.0.0", "3.0.0-rc"} {
-		expectIsAffected(t, vuln, v, true)
-	}
-
-	for _, v := range []string{"3.2.0", "3.2.1", "4.0.0"} {
-		expectIsAffected(t, vuln, v, false)
-	}
-
-	for _, v := range []string{"3.3.1", "3.4.5"} {
-		expectIsAffected(t, vuln, v, true)
-	}
-
-	// an empty version should always be treated as affected
-	expectIsAffected(t, vuln, "", true)
-}
-
-func TestOSV_IsAffected_AffectsWithEcosystem_Unsorted(t *testing.T) {
-	t.Parallel()
-
-	vuln := buildOSVWithAffected(
-		models.Affected{
-			Package: models.Package{Ecosystem: models.EcosystemNPM, Name: "my-package"},
-			Ranges: []models.Range{
-				buildEcosystemAffectsRange(
-					models.Event{Introduced: "0"},
-					models.Event{Introduced: "2.1.0"},
-					models.Event{Fixed: "1"},
-					models.Event{LastAffected: "3.1.9"},
-				),
-			},
-		},
-	)
-
-	for _, v := range []string{"0.0.0", "0.1.0", "0.0.0.1", "1.0.0-rc"} {
-		expectIsAffected(t, vuln, v, true)
-	}
-
-	for _, v := range []string{"1.0.0", "1.1.0", "2.0.0rc2", "2.0.1"} {
-		expectIsAffected(t, vuln, v, false)
-	}
-
-	for _, v := range []string{"2.1.1", "2.3.4", "3.0.0", "3.0.0-rc", "3.1.9"} {
-		expectIsAffected(t, vuln, v, true)
-	}
-
-	for _, v := range []string{"3.2.0", "3.2.1", "4.0.0"} {
-		expectIsAffected(t, vuln, v, false)
-	}
-
-	// an empty version should always be treated as affected
-	expectIsAffected(t, vuln, "", true)
-
-	// zeros with build strings
-	vuln = buildOSVWithAffected(
-		models.Affected{
-			// golang.org/x/sys
-			Package: models.Package{Ecosystem: models.EcosystemNPM, Name: "my-package"},
-			Ranges: []models.Range{
-				buildEcosystemAffectsRange(
-					models.Event{Fixed: "0.0.0-20220412211240-33da011f77ad"},
-					models.Event{Introduced: "0"},
-				),
-			},
-		},
-		models.Affected{
-			// golang.org/x/net
-			Package: models.Package{Ecosystem: models.EcosystemNPM, Name: "my-package"},
-			Ranges: []models.Range{
-				buildEcosystemAffectsRange(
-					models.Event{Introduced: "0.0.0-20180925071336-cf3bd585ca2a"},
-					models.Event{Fixed: "0"},
-				),
-			},
-		},
-	)
-
-	for _, v := range []string{"0.0.0", "0.14.0"} {
-		expectIsAffected(t, vuln, v, false)
-	}
-
-	for _, v := range []string{"0.0.0-20180925071336-cf3bd585ca2a"} {
-		expectIsAffected(t, vuln, v, true)
-	}
-
-	// an empty version should always be treated as affected
-	expectIsAffected(t, vuln, "", true)
-}
-
-func TestOSV_IsAffected_AffectsWithSemver_DifferentEcosystem(t *testing.T) {
-	t.Parallel()
-
-	vuln := buildOSVWithAffected(
-		models.Affected{
-			Package: models.Package{Ecosystem: models.EcosystemPyPI, Name: "my-package"},
-			Ranges: []models.Range{
-				buildSemverAffectsRange(models.Event{Introduced: "0"}),
-			},
-		},
-	)
-
-	for _, v := range []string{"1.0.0", "1.1.1", "2.0.0"} {
-		expectIsAffected(t, vuln, v, false)
-	}
-}
-
-func TestOSV_IsAffected_AffectsWithSemver_SingleAffected(t *testing.T) {
-	t.Parallel()
-
-	var vuln models.Vulnerability
-
-	// "Introduced: 0" means everything is affected
-	vuln = buildOSVWithAffected(
-		models.Affected{
-			Package: models.Package{Ecosystem: models.EcosystemNPM, Name: "my-package"},
-			Ranges: []models.Range{
-				buildSemverAffectsRange(models.Event{Introduced: "0"}),
-			},
-		},
-	)
-
-	for _, v := range []string{"v1.0.0", "v1.1.1", "v2.0.0"} {
-		expectIsAffected(t, vuln, v, true)
-	}
-
-	// "Fixed: 1" means all versions after this are not vulnerable
-	vuln = buildOSVWithAffected(
-		models.Affected{
-			Package: models.Package{Ecosystem: models.EcosystemNPM, Name: "my-package"},
-			Ranges: []models.Range{
-				buildSemverAffectsRange(
-					models.Event{Introduced: "0"},
-					models.Event{Fixed: "1.0.0"},
-				),
-			},
-		},
-	)
-
-	for _, v := range []string{"0.0.0", "0.1.0", "0.0.0.1", "1.0.0-rc"} {
-		expectIsAffected(t, vuln, v, true)
-	}
-
-	for _, v := range []string{"1.0.0", "1.1.0", "2.0.0"} {
-		expectIsAffected(t, vuln, v, false)
-	}
-
-	// multiple fixes and introduced
-	vuln = buildOSVWithAffected(
-		models.Affected{
-			Package: models.Package{Ecosystem: models.EcosystemNPM, Name: "my-package"},
-			Ranges: []models.Range{
-				buildSemverAffectsRange(
-					models.Event{Introduced: "0"},
-					models.Event{Fixed: "1"},
-					models.Event{Introduced: "2.1.0"},
-					models.Event{Fixed: "3.2.0"},
-				),
-			},
-		},
-	)
-
-	for _, v := range []string{"0.0.0", "0.1.0", "0.0.0.1", "1.0.0-rc"} {
-		expectIsAffected(t, vuln, v, true)
-	}
-
-	for _, v := range []string{"1.0.0", "1.1.0", "2.0.0rc2", "2.0.1"} {
-		expectIsAffected(t, vuln, v, false)
-	}
-
-	for _, v := range []string{"2.1.1", "2.3.4", "3.0.0", "3.0.0-rc"} {
-		expectIsAffected(t, vuln, v, true)
-	}
-
-	for _, v := range []string{"3.2.0", "3.2.1", "4.0.0"} {
-		expectIsAffected(t, vuln, v, false)
-	}
-
-	// an empty version should always be treated as affected
-	expectIsAffected(t, vuln, "", true)
-
-	// "LastAffected: 1" means all versions after this are not vulnerable
-	vuln = buildOSVWithAffected(
-		models.Affected{
-			Package: models.Package{Ecosystem: models.EcosystemNPM, Name: "my-package"},
-			Ranges: []models.Range{
-				buildSemverAffectsRange(
-					models.Event{Introduced: "0"},
-					models.Event{LastAffected: "1.0.0"},
-				),
-			},
-		},
-	)
-
-	for _, v := range []string{"0.0.0", "0.1.0", "0.0.0.1", "1.0.0-rc", "1.0.0"} {
-		expectIsAffected(t, vuln, v, true)
-	}
-
-	for _, v := range []string{"1.0.1", "1.1.0", "2.0.0"} {
-		expectIsAffected(t, vuln, v, false)
-	}
-
-	// mix of fixes, last_known_affected, and introduced
-	vuln = buildOSVWithAffected(
-		models.Affected{
-			Package: models.Package{Ecosystem: models.EcosystemNPM, Name: "my-package"},
-			Ranges: []models.Range{
-				buildSemverAffectsRange(
-					models.Event{Introduced: "0"},
-					models.Event{Fixed: "1"},
-					models.Event{Introduced: "2.1.0"},
-					models.Event{LastAffected: "3.1.9"},
-				),
-			},
-		},
-	)
-
-	for _, v := range []string{"0.0.0", "0.1.0", "0.0.0.1", "1.0.0-rc"} {
-		expectIsAffected(t, vuln, v, true)
-	}
-
-	for _, v := range []string{"1.0.0", "1.1.0", "2.0.0rc2", "2.0.1"} {
-		expectIsAffected(t, vuln, v, false)
-	}
-
-	for _, v := range []string{"2.1.1", "2.3.4", "3.0.0", "3.0.0-rc"} {
-		expectIsAffected(t, vuln, v, true)
-	}
-
-	for _, v := range []string{"3.2.0", "3.2.1", "4.0.0"} {
-		expectIsAffected(t, vuln, v, false)
-	}
-
-	// an empty version should always be treated as affected
-	expectIsAffected(t, vuln, "", true)
-}
-
-func TestOSV_IsAffected_AffectsWithSemver_MultipleAffected(t *testing.T) {
-	t.Parallel()
-
-	vuln := buildOSVWithAffected(
-		models.Affected{
-			Package: models.Package{Ecosystem: models.EcosystemNPM, Name: "my-package"},
-			Ranges: []models.Range{
-				buildSemverAffectsRange(
-					models.Event{Introduced: "0"},
-					models.Event{Fixed: "1"},
-				),
-			},
-		},
-		models.Affected{
-			Package: models.Package{Ecosystem: models.EcosystemNPM, Name: "my-package"},
-			Ranges: []models.Range{
-				buildSemverAffectsRange(
-					models.Event{Introduced: "2.1.0"},
-					models.Event{Fixed: "3.2.0"},
-				),
-			},
-		},
-		models.Affected{
-			Package: models.Package{Ecosystem: models.EcosystemNPM, Name: "my-package"},
-			Ranges: []models.Range{
-				buildSemverAffectsRange(
-					models.Event{Introduced: "3.3.0"},
-					models.Event{LastAffected: "3.5.0"},
-				),
-			},
-		},
-	)
-
-	for _, v := range []string{"0.0.0", "0.1.0", "0.0.0.1", "1.0.0-rc"} {
-		expectIsAffected(t, vuln, v, true)
-	}
-
-	for _, v := range []string{"1.0.0", "1.1.0", "2.0.0rc2", "2.0.1"} {
-		expectIsAffected(t, vuln, v, false)
-	}
-
-	for _, v := range []string{"2.1.1", "2.3.4", "3.0.0", "3.0.0-rc"} {
-		expectIsAffected(t, vuln, v, true)
-	}
-
-	for _, v := range []string{"3.2.0", "3.2.1", "4.0.0"} {
-		expectIsAffected(t, vuln, v, false)
-	}
-
-	for _, v := range []string{"3.3.1", "3.4.5", "3.5.0"} {
-		expectIsAffected(t, vuln, v, true)
-	}
-
-	// an empty version should always be treated as affected
-	expectIsAffected(t, vuln, "", true)
-}
-
-func TestOSV_IsAffected_AffectsWithSemver_Unsorted(t *testing.T) {
-	t.Parallel()
-
-	// mix of fixes, last_known_affected, and introduced
-	vuln := buildOSVWithAffected(
-		models.Affected{
-			Package: models.Package{Ecosystem: models.EcosystemNPM, Name: "my-package"},
-			Ranges: []models.Range{
-				buildSemverAffectsRange(
-					models.Event{Introduced: "0"},
-					models.Event{Introduced: "2.1.0"},
-					models.Event{Fixed: "1"},
-					models.Event{LastAffected: "3.1.9"},
-				),
-			},
-		},
-	)
-
-	for _, v := range []string{"0.0.0", "0.1.0", "0.0.0.1", "1.0.0-rc"} {
-		expectIsAffected(t, vuln, v, true)
-	}
-
-	for _, v := range []string{"1.0.0", "1.1.0", "2.0.0rc2", "2.0.1"} {
-		expectIsAffected(t, vuln, v, false)
-	}
-
-	for _, v := range []string{"2.1.1", "2.3.4", "3.0.0", "3.0.0-rc"} {
-		expectIsAffected(t, vuln, v, true)
-	}
-
-	for _, v := range []string{"3.2.0", "3.2.1", "4.0.0"} {
-		expectIsAffected(t, vuln, v, false)
-	}
-
-	// an empty version should always be treated as affected
-	expectIsAffected(t, vuln, "", true)
-
-	// an empty version should always be treated as affected
-	expectIsAffected(t, vuln, "", true)
-
-	// zeros with build strings
-	vuln = buildOSVWithAffected(
-		models.Affected{
-			// golang.org/x/sys
-			Package: models.Package{Ecosystem: models.EcosystemNPM, Name: "my-package"},
-			Ranges: []models.Range{
-				buildSemverAffectsRange(
-					models.Event{Fixed: "0.0.0-20220412211240-33da011f77ad"},
-					models.Event{Introduced: "0"},
-				),
-			},
-		},
-		models.Affected{
-			// golang.org/x/net
-			Package: models.Package{Ecosystem: models.EcosystemNPM, Name: "my-package"},
-			Ranges: []models.Range{
-				buildSemverAffectsRange(
-					models.Event{Introduced: "0.0.0-20180925071336-cf3bd585ca2a"},
-					models.Event{Fixed: "0"},
-				),
-			},
-		},
-	)
-
-	for _, v := range []string{"0.0.0", "0.14.0"} {
-		expectIsAffected(t, vuln, v, false)
-	}
-
-	for _, v := range []string{"0.0.0-20180925071336-cf3bd585ca2a"} {
-		expectIsAffected(t, vuln, v, true)
-	}
-
-	// an empty version should always be treated as affected
-	expectIsAffected(t, vuln, "", true)
-}
-
-func TestOSV_IsAffected_OnlyVersions(t *testing.T) {
-	t.Parallel()
-
-	vuln := buildOSVWithAffected(
-		models.Affected{
-			Package:  models.Package{Ecosystem: models.EcosystemNPM, Name: "my-package"},
-			Versions: []string{"1.0.0"},
-		},
-	)
-
-	expectIsAffected(t, vuln, "0.0.0", false)
-	expectIsAffected(t, vuln, "1.0.0", true)
-	expectIsAffected(t, vuln, "1.0.0-beta1", false)
-	expectIsAffected(t, vuln, "1.1.0", false)
-
-	// an empty version should always be treated as affected
-	expectIsAffected(t, vuln, "", true)
-}