mirror of
https://github.com/aquasecurity/trivy.git
synced 2025-12-12 15:50:15 -08:00
feat(vuln): add Root.io support for container image scanning (#9073)
Co-authored-by: DmitriyLewen <dmitriy.lewen@smartforce.io>
This commit is contained in:
@@ -15,6 +15,7 @@ Trivy supports them for
|
||||
| [Bitnami packages](bitnami.md) | `/opt/bitnami/<component>/.spdx-<component>.spdx` | ✅ | ✅ | - | - |
|
||||
| [Conda](conda.md) | `<conda-root>/envs/<env>/conda-meta/<package>.json` | ✅ | ✅ | - | - |
|
||||
| | `environment.yml` | - | - | ✅ | ✅ |
|
||||
| [Root.io images](rootio.md) | - | ✅ | ✅ | - | - |
|
||||
| [RPM Archives](rpm.md) | `*.rpm` | ✅[^5] | ✅[^5] | ✅[^5] | ✅[^5] |
|
||||
|
||||
[sbom]: ../../supply-chain/sbom.md
|
||||
|
||||
20
docs/docs/coverage/others/rootio.md
Normal file
20
docs/docs/coverage/others/rootio.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# Root.io
|
||||
|
||||
!!! warning "EXPERIMENTAL"
|
||||
Scanning results may be inaccurate.
|
||||
|
||||
While it is not an OS, this page describes the details of [Root.io](https://root.io/) patch distribution service.
|
||||
Root.io provides security patches for [Debian](../os/debian.md), [Ubuntu](../os/ubuntu.md), and [Alpine](../os/alpine.md)-based container images.
|
||||
Root.io patches are detected when Trivy finds packages with specific version suffixes:
|
||||
|
||||
- **Debian/Ubuntu**: packages with `.root.io` in version string
|
||||
- **Alpine**: packages with `-r\d007\d` pattern in version string (e.g., `-r10071`, `-r20072`)
|
||||
|
||||
When Root.io patches are detected, Trivy automatically switches to Root.io scanning mode for vulnerability detection.
|
||||
Even when the original OS distributor (Debian, Ubuntu, Alpine) has not provided a patch for a vulnerability, Trivy will display Root.io patches if they are available.
|
||||
|
||||
For detailed information about supported scanners, features, and functionality, please refer to the documentation for the underlying OS:
|
||||
|
||||
- [Debian](../os/debian.md)
|
||||
- [Ubuntu](../os/ubuntu.md)
|
||||
- [Alpine](../os/alpine.md)
|
||||
@@ -169,6 +169,7 @@ trivy filesystem [flags] PATH
|
||||
- govulndb
|
||||
- echo
|
||||
- minimos
|
||||
- rootio
|
||||
- auto
|
||||
(default [auto])
|
||||
```
|
||||
|
||||
@@ -190,6 +190,7 @@ trivy image [flags] IMAGE_NAME
|
||||
- govulndb
|
||||
- echo
|
||||
- minimos
|
||||
- rootio
|
||||
- auto
|
||||
(default [auto])
|
||||
```
|
||||
|
||||
@@ -178,6 +178,7 @@ trivy kubernetes [flags] [CONTEXT]
|
||||
- govulndb
|
||||
- echo
|
||||
- minimos
|
||||
- rootio
|
||||
- auto
|
||||
(default [auto])
|
||||
```
|
||||
|
||||
@@ -168,6 +168,7 @@ trivy repository [flags] (REPO_PATH | REPO_URL)
|
||||
- govulndb
|
||||
- echo
|
||||
- minimos
|
||||
- rootio
|
||||
- auto
|
||||
(default [auto])
|
||||
```
|
||||
|
||||
@@ -170,6 +170,7 @@ trivy rootfs [flags] ROOTDIR
|
||||
- govulndb
|
||||
- echo
|
||||
- minimos
|
||||
- rootio
|
||||
- auto
|
||||
(default [auto])
|
||||
```
|
||||
|
||||
@@ -139,6 +139,7 @@ trivy sbom [flags] SBOM_PATH
|
||||
- govulndb
|
||||
- echo
|
||||
- minimos
|
||||
- rootio
|
||||
- auto
|
||||
(default [auto])
|
||||
```
|
||||
|
||||
@@ -155,6 +155,7 @@ trivy vm [flags] VM_IMAGE
|
||||
- govulndb
|
||||
- echo
|
||||
- minimos
|
||||
- rootio
|
||||
- auto
|
||||
(default [auto])
|
||||
```
|
||||
|
||||
@@ -37,6 +37,7 @@ See [here](../coverage/os/index.md#supported-os) for the supported OSes.
|
||||
| Azure Linux (CBL-Mariner) | [OVAL][azure] |
|
||||
| OpenSUSE/SLES | [CVRF][suse] |
|
||||
| Photon OS | [Photon Security Advisory][photon] |
|
||||
| Root.io | [Root.io Patch Feed][rootio] |
|
||||
|
||||
#### Data Source Selection
|
||||
Trivy **only** consumes security advisories from the sources listed in the above table.
|
||||
@@ -394,6 +395,7 @@ Example logic for the following vendor severity levels when scanning an Alpine i
|
||||
[suse]: http://ftp.suse.com/pub/projects/security/cvrf/
|
||||
[photon]: https://packages.vmware.com/photon/photon_cve_metadata/
|
||||
[azure]: https://github.com/microsoft/AzureLinuxVulnerabilityData/
|
||||
[rootio]: https://api.root.io/external/patch_feed
|
||||
|
||||
[php-ghsa]: https://github.com/advisories?query=ecosystem%3Acomposer
|
||||
[python-ghsa]: https://github.com/advisories?query=ecosystem%3Apip
|
||||
|
||||
6
go.mod
6
go.mod
@@ -24,7 +24,7 @@ require (
|
||||
github.com/aquasecurity/testdocker v0.0.0-20250616060700-ba6845ac6d17
|
||||
github.com/aquasecurity/tml v0.6.1
|
||||
github.com/aquasecurity/trivy-checks v1.11.3-0.20250604022615-9a7efa7c9169
|
||||
github.com/aquasecurity/trivy-db v0.0.0-20250529093513-a12dfc204b6e
|
||||
github.com/aquasecurity/trivy-db v0.0.0-20250627124416-ca81c496a932
|
||||
github.com/aquasecurity/trivy-java-db v0.0.0-20240109071736-184bd7481d48
|
||||
github.com/aquasecurity/trivy-kubernetes v0.9.0
|
||||
github.com/aws/aws-sdk-go-v2 v1.36.5
|
||||
@@ -323,7 +323,7 @@ require (
|
||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||
github.com/nozzle/throttler v0.0.0-20180817012639-2ea982251481 // indirect
|
||||
github.com/oklog/ulid v1.3.1 // indirect
|
||||
github.com/oklog/ulid/v2 v2.1.0 // indirect
|
||||
github.com/oklog/ulid/v2 v2.1.1 // indirect
|
||||
github.com/opencontainers/runtime-spec v1.2.1 // indirect
|
||||
github.com/opencontainers/selinux v1.12.0 // indirect
|
||||
github.com/opentracing/opentracing-go v1.2.0 // indirect
|
||||
@@ -347,7 +347,7 @@ require (
|
||||
github.com/rubenv/sql-migrate v1.8.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/sagikazarmark/locafero v0.7.0 // indirect
|
||||
github.com/samber/oops v1.16.1 // indirect
|
||||
github.com/samber/oops v1.18.1 // indirect
|
||||
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 // indirect
|
||||
github.com/sassoftware/relic v7.2.1+incompatible // indirect
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
|
||||
|
||||
12
go.sum
12
go.sum
@@ -798,8 +798,8 @@ github.com/aquasecurity/tml v0.6.1 h1:y2ZlGSfrhnn7t4ZJ/0rotuH+v5Jgv6BDDO5jB6A9gw
|
||||
github.com/aquasecurity/tml v0.6.1/go.mod h1:OnYMWY5lvI9ejU7yH9LCberWaaTBW7hBFsITiIMY2yY=
|
||||
github.com/aquasecurity/trivy-checks v1.11.3-0.20250604022615-9a7efa7c9169 h1:TckzIxUX7lZaU9f2lNxCN0noYYP8fzmSQf6a4JdV83w=
|
||||
github.com/aquasecurity/trivy-checks v1.11.3-0.20250604022615-9a7efa7c9169/go.mod h1:nT69xgRcBD4NlHwTBpWMYirpK5/Zpl8M+XDOgmjMn2k=
|
||||
github.com/aquasecurity/trivy-db v0.0.0-20250529093513-a12dfc204b6e h1:+B/in1DQDGwQbKhW5pWL8XxBgnZKxXhUznylJ2NCyvs=
|
||||
github.com/aquasecurity/trivy-db v0.0.0-20250529093513-a12dfc204b6e/go.mod h1:4zd4qZcjhNAHASz5I0O7qapv5h5gSJzSEaZXv/IPOGc=
|
||||
github.com/aquasecurity/trivy-db v0.0.0-20250627124416-ca81c496a932 h1:5GKQ53uGGHEEtZ/FX94jcIdfEGeyiHZ7tmJ5nCtDz5c=
|
||||
github.com/aquasecurity/trivy-db v0.0.0-20250627124416-ca81c496a932/go.mod h1:Ubl2YWA6Zg7eaojg4MDmeDdYU4+PiGPsnwo6B5UIwqw=
|
||||
github.com/aquasecurity/trivy-java-db v0.0.0-20240109071736-184bd7481d48 h1:JVgBIuIYbwG+ekC5lUHUpGJboPYiCcxiz06RCtz8neI=
|
||||
github.com/aquasecurity/trivy-java-db v0.0.0-20240109071736-184bd7481d48/go.mod h1:Ldya37FLi0e/5Cjq2T5Bty7cFkzUDwTcPeQua+2M8i8=
|
||||
github.com/aquasecurity/trivy-kubernetes v0.9.0 h1:rp8RuXwKfFWUPR/ULksA2WpD0z6rslVkzLmPGQr61Wc=
|
||||
@@ -1617,8 +1617,8 @@ github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY=
|
||||
github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc=
|
||||
github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
|
||||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||
github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU=
|
||||
github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=
|
||||
github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s=
|
||||
github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=
|
||||
github.com/oleiade/reflections v1.0.1 h1:D1XO3LVEYroYskEsoSiGItp9RUxG6jWnCVvrqH0HHQM=
|
||||
github.com/oleiade/reflections v1.0.1/go.mod h1:rdFxbxq4QXVZWj0F+e9jqjDkc7dbp97vkRixKo2JR60=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
@@ -1743,8 +1743,8 @@ github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsF
|
||||
github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k=
|
||||
github.com/samber/lo v1.51.0 h1:kysRYLbHy/MB7kQZf5DSN50JHmMsNEdeY24VzJFu7wI=
|
||||
github.com/samber/lo v1.51.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
|
||||
github.com/samber/oops v1.16.1 h1:XlKkXsWM5g8hE4C+sEV9n0X282fZn3XabVmAKU2RiHI=
|
||||
github.com/samber/oops v1.16.1/go.mod h1:8eXgMAJcDXRAijQsFRhfy/EHDOTiSvwkg6khFqFK078=
|
||||
github.com/samber/oops v1.18.1 h1:qjhZbqbdyhWBKntkY8sxrDNKA8b4c5VHlmI1rli7X7M=
|
||||
github.com/samber/oops v1.18.1/go.mod h1:xYqvimigkKV70HyLXiBZJFpIWi2CGcc6Xx7eV+2HycI=
|
||||
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4=
|
||||
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY=
|
||||
github.com/sassoftware/go-rpmutils v0.4.0 h1:ojND82NYBxgwrV+mX1CWsd5QJvvEZTKddtCdFLPWhpg=
|
||||
|
||||
@@ -117,6 +117,7 @@ nav:
|
||||
- Overview: docs/coverage/others/index.md
|
||||
- Bitnami Images: docs/coverage/others/bitnami.md
|
||||
- Conda: docs/coverage/others/conda.md
|
||||
- Root.io Images: docs/coverage/others/rootio.md
|
||||
- RPM Archives: docs/coverage/others/rpm.md
|
||||
- Kubernetes: docs/coverage/kubernetes.md
|
||||
- Configuration:
|
||||
|
||||
@@ -14,12 +14,14 @@ import (
|
||||
"github.com/aquasecurity/trivy/pkg/detector/ospkg/bottlerocket"
|
||||
"github.com/aquasecurity/trivy/pkg/detector/ospkg/chainguard"
|
||||
"github.com/aquasecurity/trivy/pkg/detector/ospkg/debian"
|
||||
"github.com/aquasecurity/trivy/pkg/detector/ospkg/driver"
|
||||
"github.com/aquasecurity/trivy/pkg/detector/ospkg/echo"
|
||||
"github.com/aquasecurity/trivy/pkg/detector/ospkg/minimos"
|
||||
"github.com/aquasecurity/trivy/pkg/detector/ospkg/oracle"
|
||||
"github.com/aquasecurity/trivy/pkg/detector/ospkg/photon"
|
||||
"github.com/aquasecurity/trivy/pkg/detector/ospkg/redhat"
|
||||
"github.com/aquasecurity/trivy/pkg/detector/ospkg/rocky"
|
||||
"github.com/aquasecurity/trivy/pkg/detector/ospkg/rootio"
|
||||
"github.com/aquasecurity/trivy/pkg/detector/ospkg/suse"
|
||||
"github.com/aquasecurity/trivy/pkg/detector/ospkg/ubuntu"
|
||||
"github.com/aquasecurity/trivy/pkg/detector/ospkg/wolfi"
|
||||
@@ -32,7 +34,7 @@ var (
|
||||
// ErrUnsupportedOS defines error for unsupported OS
|
||||
ErrUnsupportedOS = xerrors.New("unsupported os")
|
||||
|
||||
drivers = map[ftypes.OSType]Driver{
|
||||
drivers = map[ftypes.OSType]driver.Driver{
|
||||
ftypes.Alpine: alpine.NewScanner(),
|
||||
ftypes.Alma: alma.NewScanner(),
|
||||
ftypes.Amazon: amazon.NewScanner(),
|
||||
@@ -55,36 +57,36 @@ var (
|
||||
ftypes.Echo: echo.NewScanner(),
|
||||
ftypes.MinimOS: minimos.NewScanner(),
|
||||
}
|
||||
|
||||
// providers dynamically generate drivers based on package information
|
||||
// and environment detection. They are tried before standard OS-specific drivers.
|
||||
providers = []driver.Provider{
|
||||
rootio.Provider,
|
||||
}
|
||||
)
|
||||
|
||||
// RegisterDriver is defined for extensibility and not supposed to be used in Trivy.
|
||||
func RegisterDriver(name ftypes.OSType, driver Driver) {
|
||||
drivers[name] = driver
|
||||
}
|
||||
|
||||
// Driver defines operations for OS package scan
|
||||
type Driver interface {
|
||||
Detect(context.Context, string, *ftypes.Repository, []ftypes.Package) ([]types.DetectedVulnerability, error)
|
||||
IsSupportedVersion(context.Context, ftypes.OSType, string) bool
|
||||
func RegisterDriver(name ftypes.OSType, drv driver.Driver) {
|
||||
drivers[name] = drv
|
||||
}
|
||||
|
||||
// Detect detects the vulnerabilities
|
||||
func Detect(ctx context.Context, _, osFamily ftypes.OSType, osName string, repo *ftypes.Repository, _ time.Time, pkgs []ftypes.Package) ([]types.DetectedVulnerability, bool, error) {
|
||||
ctx = log.WithContextPrefix(ctx, string(osFamily))
|
||||
|
||||
driver, err := newDriver(osFamily)
|
||||
d, err := newDriver(osFamily, pkgs)
|
||||
if err != nil {
|
||||
return nil, false, ErrUnsupportedOS
|
||||
}
|
||||
|
||||
eosl := !driver.IsSupportedVersion(ctx, osFamily, osName)
|
||||
eosl := !d.IsSupportedVersion(ctx, osFamily, osName)
|
||||
|
||||
// Package `gpg-pubkey` doesn't use the correct version.
|
||||
// We don't need to find vulnerabilities for this package.
|
||||
filteredPkgs := lo.Filter(pkgs, func(pkg ftypes.Package, _ int) bool {
|
||||
return pkg.Name != "gpg-pubkey"
|
||||
})
|
||||
vulns, err := driver.Detect(ctx, osName, repo, filteredPkgs)
|
||||
vulns, err := d.Detect(ctx, osName, repo, filteredPkgs)
|
||||
if err != nil {
|
||||
return nil, false, xerrors.Errorf("failed detection: %w", err)
|
||||
}
|
||||
@@ -92,9 +94,17 @@ func Detect(ctx context.Context, _, osFamily ftypes.OSType, osName string, repo
|
||||
return vulns, eosl, nil
|
||||
}
|
||||
|
||||
func newDriver(osFamily ftypes.OSType) (Driver, error) {
|
||||
if driver, ok := drivers[osFamily]; ok {
|
||||
return driver, nil
|
||||
func newDriver(osFamily ftypes.OSType, pkgs []ftypes.Package) (driver.Driver, error) {
|
||||
// Try providers first
|
||||
for _, provider := range providers {
|
||||
if d := provider(osFamily, pkgs); d != nil {
|
||||
return d, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to standard drivers
|
||||
if d, ok := drivers[osFamily]; ok {
|
||||
return d, nil
|
||||
}
|
||||
|
||||
log.Warn("Unsupported os", log.String("family", string(osFamily)))
|
||||
|
||||
17
pkg/detector/ospkg/driver/driver.go
Normal file
17
pkg/detector/ospkg/driver/driver.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package driver
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
|
||||
"github.com/aquasecurity/trivy/pkg/types"
|
||||
)
|
||||
|
||||
// Driver defines operations for OS package scan
|
||||
type Driver interface {
|
||||
Detect(context.Context, string, *ftypes.Repository, []ftypes.Package) ([]types.DetectedVulnerability, error)
|
||||
IsSupportedVersion(context.Context, ftypes.OSType, string) bool
|
||||
}
|
||||
|
||||
// Provider creates a specialized driver based on the environment
|
||||
type Provider func(osFamily ftypes.OSType, pkgs []ftypes.Package) Driver
|
||||
45
pkg/detector/ospkg/rootio/provider.go
Normal file
45
pkg/detector/ospkg/rootio/provider.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package rootio
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
"github.com/aquasecurity/trivy/pkg/detector/ospkg/driver"
|
||||
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
|
||||
)
|
||||
|
||||
var (
|
||||
// debianRootIOPattern matches Debian/Ubuntu Root.io version pattern: .root.io
|
||||
debianRootIOPattern = regexp.MustCompile(`\.root\.io`)
|
||||
// alpineRootIOPattern matches Alpine Root.io version pattern: -r\d007\d (e.g., -r10071, -r20072)
|
||||
alpineRootIOPattern = regexp.MustCompile(`-r\d007\d`)
|
||||
)
|
||||
|
||||
// Provider creates a Root.io driver if Root.io packages are detected
|
||||
func Provider(osFamily ftypes.OSType, pkgs []ftypes.Package) driver.Driver {
|
||||
if !isRootIOEnvironment(osFamily, pkgs) {
|
||||
return nil
|
||||
}
|
||||
return NewScanner(osFamily)
|
||||
}
|
||||
|
||||
// isRootIOEnvironment detects if the environment is Root.io based on package suffixes
|
||||
func isRootIOEnvironment(osFamily ftypes.OSType, pkgs []ftypes.Package) bool {
|
||||
switch osFamily {
|
||||
case ftypes.Debian, ftypes.Ubuntu:
|
||||
return hasPackageWithPattern(pkgs, debianRootIOPattern)
|
||||
case ftypes.Alpine:
|
||||
return hasPackageWithPattern(pkgs, alpineRootIOPattern)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// hasPackageWithPattern checks if any package version matches the specified pattern
|
||||
func hasPackageWithPattern(pkgs []ftypes.Package, pattern *regexp.Regexp) bool {
|
||||
for _, pkg := range pkgs {
|
||||
if pattern.MatchString(pkg.Version) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
118
pkg/detector/ospkg/rootio/provider_test.go
Normal file
118
pkg/detector/ospkg/rootio/provider_test.go
Normal file
@@ -0,0 +1,118 @@
|
||||
package rootio_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/aquasecurity/trivy/pkg/detector/ospkg/rootio"
|
||||
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
|
||||
)
|
||||
|
||||
func TestProvider(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
osFamily ftypes.OSType
|
||||
pkgs []ftypes.Package
|
||||
want bool // true if driver should be returned, false if nil
|
||||
}{
|
||||
{
|
||||
name: "Debian with .root.io package",
|
||||
osFamily: ftypes.Debian,
|
||||
pkgs: []ftypes.Package{
|
||||
{Name: "libc6", Version: "2.31-13+deb11u4.root.io"},
|
||||
{Name: "bash", Version: "5.1-2+deb11u1"},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "Ubuntu with .root.io package",
|
||||
osFamily: ftypes.Ubuntu,
|
||||
pkgs: []ftypes.Package{
|
||||
{Name: "libc6", Version: "2.31-0ubuntu9.9.root.io"},
|
||||
{Name: "bash", Version: "5.1-6ubuntu1"},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "Alpine with Root.io pattern package",
|
||||
osFamily: ftypes.Alpine,
|
||||
pkgs: []ftypes.Package{
|
||||
{Name: "musl", Version: "1.2.3-r10071"},
|
||||
{Name: "busybox", Version: "1.35.0-r17"},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "Debian without .root.io package",
|
||||
osFamily: ftypes.Debian,
|
||||
pkgs: []ftypes.Package{
|
||||
{Name: "libc6", Version: "2.31-13+deb11u4"},
|
||||
{Name: "bash", Version: "5.1-2+deb11u1"},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "Ubuntu without .root.io package",
|
||||
osFamily: ftypes.Ubuntu,
|
||||
pkgs: []ftypes.Package{
|
||||
{Name: "libc6", Version: "2.31-0ubuntu9.9"},
|
||||
{Name: "bash", Version: "5.1-6ubuntu1"},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "Alpine without Root.io pattern package",
|
||||
osFamily: ftypes.Alpine,
|
||||
pkgs: []ftypes.Package{
|
||||
{Name: "musl", Version: "1.2.3-r0"},
|
||||
{Name: "busybox", Version: "1.35.0-r17"},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "Unsupported OS family",
|
||||
osFamily: ftypes.RedHat,
|
||||
pkgs: []ftypes.Package{
|
||||
{Name: "glibc", Version: "2.28-151.el8.root.io"},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "Empty package list",
|
||||
osFamily: ftypes.Debian,
|
||||
pkgs: []ftypes.Package{},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "Multiple .root.io packages",
|
||||
osFamily: ftypes.Debian,
|
||||
pkgs: []ftypes.Package{
|
||||
{Name: "libc6", Version: "2.31-13+deb11u4.root.io"},
|
||||
{Name: "openssl", Version: "1.1.1n-0+deb11u3.root.io"},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "Multiple Alpine Root.io pattern packages",
|
||||
osFamily: ftypes.Alpine,
|
||||
pkgs: []ftypes.Package{
|
||||
{Name: "musl", Version: "1.2.3-r20072"},
|
||||
{Name: "openssl", Version: "1.1.1t-r10071"},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
driver := rootio.Provider(tt.osFamily, tt.pkgs)
|
||||
if tt.want {
|
||||
require.NotNil(t, driver, "Provider should return a driver for Root.io environment")
|
||||
} else {
|
||||
assert.Nil(t, driver, "Provider should return nil for non-Root.io environment")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
140
pkg/detector/ospkg/rootio/rootio.go
Normal file
140
pkg/detector/ospkg/rootio/rootio.go
Normal file
@@ -0,0 +1,140 @@
|
||||
package rootio
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
dbTypes "github.com/aquasecurity/trivy-db/pkg/types"
|
||||
"github.com/aquasecurity/trivy-db/pkg/vulnsrc/rootio"
|
||||
"github.com/aquasecurity/trivy-db/pkg/vulnsrc/vulnerability"
|
||||
"github.com/aquasecurity/trivy/pkg/detector/ospkg/version"
|
||||
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
|
||||
"github.com/aquasecurity/trivy/pkg/log"
|
||||
"github.com/aquasecurity/trivy/pkg/scan/utils"
|
||||
"github.com/aquasecurity/trivy/pkg/types"
|
||||
)
|
||||
|
||||
// Scanner implements the Root.io scanner
|
||||
type Scanner struct {
|
||||
comparer version.Comparer
|
||||
vsg rootio.VulnSrcGetter
|
||||
versionTrimmer func(string) string
|
||||
logger *log.Logger
|
||||
}
|
||||
|
||||
// NewScanner is the factory method for Scanner
|
||||
func NewScanner(baseOS ftypes.OSType) *Scanner {
|
||||
var comparer version.Comparer
|
||||
var vsg rootio.VulnSrcGetter
|
||||
var versionTrimmer func(string) string
|
||||
|
||||
switch baseOS {
|
||||
case ftypes.Debian:
|
||||
comparer = version.NewDEBComparer()
|
||||
vsg = rootio.NewVulnSrcGetter(vulnerability.Debian)
|
||||
versionTrimmer = version.Major
|
||||
case ftypes.Ubuntu:
|
||||
comparer = version.NewDEBComparer()
|
||||
vsg = rootio.NewVulnSrcGetter(vulnerability.Ubuntu)
|
||||
versionTrimmer = version.Minor
|
||||
case ftypes.Alpine:
|
||||
comparer = version.NewAPKComparer()
|
||||
vsg = rootio.NewVulnSrcGetter(vulnerability.Alpine)
|
||||
versionTrimmer = version.Minor
|
||||
default:
|
||||
// Should never happen as it's validated in the provider
|
||||
comparer = version.NewDEBComparer()
|
||||
vsg = rootio.NewVulnSrcGetter(vulnerability.Debian)
|
||||
versionTrimmer = version.Major
|
||||
}
|
||||
|
||||
return &Scanner{
|
||||
comparer: comparer,
|
||||
vsg: vsg,
|
||||
versionTrimmer: versionTrimmer,
|
||||
logger: log.WithPrefix("rootio"),
|
||||
}
|
||||
}
|
||||
|
||||
// Detect vulnerabilities in package using Root.io scanner
|
||||
func (s *Scanner) Detect(ctx context.Context, osVer string, _ *ftypes.Repository, pkgs []ftypes.Package) ([]types.DetectedVulnerability, error) {
|
||||
// Trim patch/minor part of osVer.
|
||||
// e.g. "12.0.1" -> "12" (Debian), "24.04.1" -> "24.04" (Ubuntu), "3.17.2" -> "3.17" (Alpine)
|
||||
osVer = s.versionTrimmer(osVer)
|
||||
log.InfoContext(ctx, "Detecting vulnerabilities...", log.String("os_version", osVer), log.Int("pkg_num", len(pkgs)))
|
||||
|
||||
var vulns []types.DetectedVulnerability
|
||||
for _, pkg := range pkgs {
|
||||
srcName := pkg.SrcName
|
||||
if srcName == "" {
|
||||
srcName = pkg.Name
|
||||
}
|
||||
|
||||
advisories, err := s.vsg.Get(osVer, srcName)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("failed to get Root.io advisories: %w", err)
|
||||
}
|
||||
|
||||
for _, adv := range advisories {
|
||||
if !s.isVulnerable(ctx, utils.FormatSrcVersion(pkg), adv) {
|
||||
continue
|
||||
}
|
||||
vulns = append(vulns, types.DetectedVulnerability{
|
||||
VulnerabilityID: adv.VulnerabilityID,
|
||||
PkgID: pkg.ID,
|
||||
PkgName: pkg.Name,
|
||||
InstalledVersion: utils.FormatVersion(pkg),
|
||||
FixedVersion: strings.Join(adv.PatchedVersions, ", "),
|
||||
Layer: pkg.Layer,
|
||||
PkgIdentifier: pkg.Identifier,
|
||||
Custom: adv.Custom,
|
||||
DataSource: adv.DataSource,
|
||||
})
|
||||
}
|
||||
}
|
||||
return vulns, nil
|
||||
}
|
||||
|
||||
func (s *Scanner) isVulnerable(ctx context.Context, installedVersion string, adv dbTypes.Advisory) bool {
|
||||
// Handle unfixed vulnerabilities
|
||||
if len(adv.VulnerableVersions) == 0 {
|
||||
// If no vulnerable versions are specified, it means the package is always vulnerable
|
||||
return true
|
||||
}
|
||||
|
||||
// For fixed vulnerabilities, check if installed version satisfies the constraint
|
||||
return s.checkConstraints(ctx, installedVersion, adv.VulnerableVersions)
|
||||
}
|
||||
|
||||
func (s *Scanner) checkConstraints(ctx context.Context, installedVersion string, constraintsStr []string) bool {
|
||||
if installedVersion == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, constraintStr := range constraintsStr {
|
||||
constraints, err := version.NewConstraints(constraintStr, s.comparer)
|
||||
if err != nil {
|
||||
s.logger.DebugContext(ctx, "Failed to parse constraints",
|
||||
log.String("constraints", constraintStr), log.Err(err))
|
||||
return false
|
||||
}
|
||||
|
||||
if satisfied, err := constraints.Check(installedVersion); err != nil {
|
||||
s.logger.DebugContext(ctx, "Failed to check version constraints",
|
||||
log.String("version", installedVersion),
|
||||
log.String("constraints", constraintStr), log.Err(err))
|
||||
return false
|
||||
} else if satisfied {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IsSupportedVersion checks if the version is supported.
|
||||
// Root.io creates fixes for EOL distributions, so we assume all versions are supported.
|
||||
func (s *Scanner) IsSupportedVersion(_ context.Context, _ ftypes.OSType, _ string) bool {
|
||||
return true
|
||||
}
|
||||
143
pkg/detector/ospkg/rootio/rootio_private_test.go
Normal file
143
pkg/detector/ospkg/rootio/rootio_private_test.go
Normal file
@@ -0,0 +1,143 @@
|
||||
package rootio
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/aquasecurity/trivy-db/pkg/types"
|
||||
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
|
||||
)
|
||||
|
||||
func TestScanner_IsVulnerable(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
installedVersion string
|
||||
vulnerableRanges []string
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "Installed vulnerable vendor version. There is no fix",
|
||||
installedVersion: "1.0.0",
|
||||
vulnerableRanges: []string{},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "Installed vulnerable vendor version, fix by vendor",
|
||||
installedVersion: "1.0.0",
|
||||
vulnerableRanges: []string{
|
||||
"<1.0.0-2",
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "Installed non-vulnerable vendor version, fix by vendor",
|
||||
installedVersion: "1.0.0-2",
|
||||
vulnerableRanges: []string{
|
||||
"<1.0.0-2",
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "Installed vulnerable vendor version, fix by root.io (root.io version)",
|
||||
installedVersion: "1.0.0-2",
|
||||
vulnerableRanges: []string{
|
||||
"<1.0.0-2.root.io",
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "Installed non-vulnerable vendor version, fix by root.io (root.io version)",
|
||||
installedVersion: "1.0.0-3",
|
||||
vulnerableRanges: []string{
|
||||
"<1.0.0-2.root.io",
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "Installed vulnerable vendor version, fix by root.io (root.io + vendor versions)",
|
||||
installedVersion: "1.0.0-2",
|
||||
vulnerableRanges: []string{
|
||||
"<1.0.0-2.root.io",
|
||||
">=1.0.0-2 <1.0.0-3",
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "Installed non-vulnerable vendor version, fix by root.io (root.io + vendor versions)",
|
||||
installedVersion: "1.0.0-3",
|
||||
vulnerableRanges: []string{
|
||||
"<1.0.0-2.root.io",
|
||||
">=1.0.0-2 <1.0.0-3",
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "Installed vulnerable root.io version, fix by root.io",
|
||||
installedVersion: "1.0.0-1.root.io",
|
||||
vulnerableRanges: []string{
|
||||
"<1.0.0-2.root.io",
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "Installed non-vulnerable root.io version, fix by root.io",
|
||||
installedVersion: "1.0.0-2.root.io",
|
||||
vulnerableRanges: []string{
|
||||
"<1.0.0-2.root.io",
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "Installed vulnerable root.io version, fix by vendor",
|
||||
installedVersion: "1.0.0-1.root.io",
|
||||
vulnerableRanges: []string{
|
||||
"<1.0.0-2",
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "Installed non-vulnerable root.io version, fix by vendor",
|
||||
installedVersion: "1.0.0-2.root.io",
|
||||
vulnerableRanges: []string{
|
||||
"<1.0.0-1",
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "Installed vulnerable root.io version, fix by root.io (root.io + vendor versions)",
|
||||
installedVersion: "1.0.0-1.root.io",
|
||||
vulnerableRanges: []string{
|
||||
"<1.0.0-2.root.io",
|
||||
">=1.0.0-2 <1.0.0-2",
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "Installed non-vulnerable root.io version, fix by root.io (root.io + vendor versions)",
|
||||
installedVersion: "1.0.0-2.root.io",
|
||||
vulnerableRanges: []string{
|
||||
"<1.0.0-2.root.io",
|
||||
">=1.0.0-2 <1.0.0-2",
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "Installed non-vulnerable root.io version, fix by root.io (root.io + root.io + vendor versions)",
|
||||
installedVersion: "1.0.0-2.root.io",
|
||||
vulnerableRanges: []string{
|
||||
"<1.0.0-2.root.io",
|
||||
">1.0.0-2.root.io <1.0.0-2",
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
scanner := NewScanner(ftypes.Debian)
|
||||
vulnerable := scanner.isVulnerable(t.Context(), tt.installedVersion, types.Advisory{VulnerableVersions: tt.vulnerableRanges})
|
||||
require.Equal(t, tt.want, vulnerable)
|
||||
})
|
||||
}
|
||||
}
|
||||
163
pkg/detector/ospkg/rootio/rootio_test.go
Normal file
163
pkg/detector/ospkg/rootio/rootio_test.go
Normal file
@@ -0,0 +1,163 @@
|
||||
package rootio_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/aquasecurity/trivy-db/pkg/db"
|
||||
dbTypes "github.com/aquasecurity/trivy-db/pkg/types"
|
||||
"github.com/aquasecurity/trivy-db/pkg/vulnsrc/vulnerability"
|
||||
"github.com/aquasecurity/trivy/internal/dbtest"
|
||||
"github.com/aquasecurity/trivy/pkg/detector/ospkg/rootio"
|
||||
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
|
||||
"github.com/aquasecurity/trivy/pkg/types"
|
||||
)
|
||||
|
||||
func TestScanner_Detect(t *testing.T) {
|
||||
type args struct {
|
||||
osVer string
|
||||
pkgs []ftypes.Package
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
baseOS ftypes.OSType
|
||||
fixtures []string
|
||||
args args
|
||||
want []types.DetectedVulnerability
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "Debian scanner",
|
||||
baseOS: ftypes.Debian,
|
||||
fixtures: []string{
|
||||
"testdata/fixtures/rootio.yaml",
|
||||
"testdata/fixtures/data-source.yaml",
|
||||
},
|
||||
args: args{
|
||||
osVer: "12",
|
||||
pkgs: []ftypes.Package{
|
||||
{
|
||||
Name: "openssl",
|
||||
Version: "3.0.15-1~deb12u1.root.io.0",
|
||||
SrcName: "openssl",
|
||||
SrcVersion: "3.0.15-1~deb12u1.root.io.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: []types.DetectedVulnerability{
|
||||
{
|
||||
PkgName: "openssl",
|
||||
VulnerabilityID: "CVE-2024-13176",
|
||||
InstalledVersion: "3.0.15-1~deb12u1.root.io.0",
|
||||
FixedVersion: "3.0.15-1~deb12u1.root.io.1, 3.0.16-1~deb12u1",
|
||||
DataSource: &dbTypes.DataSource{
|
||||
ID: vulnerability.RootIO,
|
||||
Name: "Root.io Security Patches",
|
||||
URL: "https://api.root.io/external/patch_feed",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Ubuntu scanner",
|
||||
baseOS: ftypes.Ubuntu,
|
||||
fixtures: []string{
|
||||
"testdata/fixtures/rootio.yaml",
|
||||
"testdata/fixtures/data-source.yaml",
|
||||
},
|
||||
args: args{
|
||||
osVer: "20.04",
|
||||
pkgs: []ftypes.Package{
|
||||
{
|
||||
Name: "nginx",
|
||||
Version: "1.22.1-9+deb12u2.root.io.0",
|
||||
SrcName: "nginx",
|
||||
SrcVersion: "1.22.1-9+deb12u2.root.io.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: []types.DetectedVulnerability{
|
||||
{
|
||||
PkgName: "nginx",
|
||||
VulnerabilityID: "CVE-2023-44487",
|
||||
InstalledVersion: "1.22.1-9+deb12u2.root.io.0",
|
||||
FixedVersion: "1.22.1-9+deb12u2.root.io.1",
|
||||
DataSource: &dbTypes.DataSource{
|
||||
ID: vulnerability.RootIO,
|
||||
Name: "Root.io Security Patches",
|
||||
URL: "https://api.root.io/external/patch_feed",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Alpine scanner",
|
||||
baseOS: ftypes.Alpine,
|
||||
fixtures: []string{
|
||||
"testdata/fixtures/rootio.yaml",
|
||||
"testdata/fixtures/data-source.yaml",
|
||||
},
|
||||
args: args{
|
||||
osVer: "3.19.3",
|
||||
pkgs: []ftypes.Package{
|
||||
{
|
||||
Name: "less",
|
||||
Version: "643-r00072",
|
||||
SrcName: "less",
|
||||
SrcVersion: "643-r00072",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: []types.DetectedVulnerability{
|
||||
{
|
||||
PkgName: "less",
|
||||
VulnerabilityID: "CVE-2024-32487",
|
||||
InstalledVersion: "643-r00072",
|
||||
FixedVersion: "643-r10072",
|
||||
DataSource: &dbTypes.DataSource{
|
||||
ID: vulnerability.RootIO,
|
||||
Name: "Root.io Security Patches",
|
||||
URL: "https://api.root.io/external/patch_feed",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Get returns an error",
|
||||
baseOS: ftypes.Alpine,
|
||||
fixtures: []string{
|
||||
"testdata/fixtures/invalid.yaml",
|
||||
"testdata/fixtures/data-source.yaml",
|
||||
},
|
||||
args: args{
|
||||
osVer: "3.20",
|
||||
pkgs: []ftypes.Package{
|
||||
{
|
||||
Name: "jq",
|
||||
Version: "1.5-12",
|
||||
SrcName: "jq",
|
||||
SrcVersion: "1.5-12",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: "failed to get Root.io advisories",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_ = dbtest.InitDB(t, tt.fixtures)
|
||||
defer db.Close()
|
||||
|
||||
scanner := rootio.NewScanner(tt.baseOS)
|
||||
got, err := scanner.Detect(t.Context(), tt.args.osVer, nil, tt.args.pkgs)
|
||||
if tt.wantErr != "" {
|
||||
require.ErrorContains(t, err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
17
pkg/detector/ospkg/rootio/testdata/fixtures/data-source.yaml
vendored
Normal file
17
pkg/detector/ospkg/rootio/testdata/fixtures/data-source.yaml
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
- bucket: data-source
|
||||
pairs:
|
||||
- key: root.io debian 12
|
||||
value:
|
||||
ID: "rootio"
|
||||
Name: "Root.io Security Patches"
|
||||
URL: "https://api.root.io/external/patch_feed"
|
||||
- key: root.io ubuntu 20.04
|
||||
value:
|
||||
ID: "rootio"
|
||||
Name: "Root.io Security Patches"
|
||||
URL: "https://api.root.io/external/patch_feed"
|
||||
- key: root.io alpine 3.19
|
||||
value:
|
||||
ID: "rootio"
|
||||
Name: "Root.io Security Patches"
|
||||
URL: "https://api.root.io/external/patch_feed"
|
||||
9
pkg/detector/ospkg/rootio/testdata/fixtures/invalid.yaml
vendored
Normal file
9
pkg/detector/ospkg/rootio/testdata/fixtures/invalid.yaml
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
- bucket: root.io alpine 3.20
|
||||
pairs:
|
||||
- bucket: jq
|
||||
pairs:
|
||||
- key: CVE-2020-8177
|
||||
value:
|
||||
FixedVersion:
|
||||
- foo
|
||||
- bar
|
||||
32
pkg/detector/ospkg/rootio/testdata/fixtures/rootio.yaml
vendored
Normal file
32
pkg/detector/ospkg/rootio/testdata/fixtures/rootio.yaml
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
- bucket: root.io debian 12
|
||||
pairs:
|
||||
- bucket: openssl
|
||||
pairs:
|
||||
- key: CVE-2024-13176
|
||||
value:
|
||||
VulnerableVersions:
|
||||
- "<3.0.15-1~deb12u1.root.io.1"
|
||||
- ">3.0.15-1~deb12u1.root.io.1 <3.0.16-1~deb12u1"
|
||||
PatchedVersions:
|
||||
- "3.0.15-1~deb12u1.root.io.1"
|
||||
- "3.0.16-1~deb12u1"
|
||||
- bucket: root.io ubuntu 20.04
|
||||
pairs:
|
||||
- bucket: nginx
|
||||
pairs:
|
||||
- key: CVE-2023-44487
|
||||
value:
|
||||
VulnerableVersions:
|
||||
- "<1.22.1-9+deb12u2.root.io.1"
|
||||
PatchedVersions:
|
||||
- "1.22.1-9+deb12u2.root.io.1"
|
||||
- bucket: root.io alpine 3.19
|
||||
pairs:
|
||||
- bucket: less
|
||||
pairs:
|
||||
- key: CVE-2024-32487
|
||||
value:
|
||||
VulnerableVersions:
|
||||
- "<643-r10072"
|
||||
PatchedVersions:
|
||||
- "643-r10072"
|
||||
65
pkg/detector/ospkg/version/compare.go
Normal file
65
pkg/detector/ospkg/version/compare.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package version
|
||||
|
||||
import (
|
||||
apkver "github.com/knqyf263/go-apk-version"
|
||||
debver "github.com/knqyf263/go-deb-version"
|
||||
)
|
||||
|
||||
// Comparer defines the interface for version comparison
|
||||
type Comparer interface {
|
||||
Compare(version1, version2 string) (int, error)
|
||||
}
|
||||
|
||||
// DEBComparer implements Comparer for Debian/Ubuntu packages
|
||||
type DEBComparer struct{}
|
||||
|
||||
// NewDEBComparer creates a new DEB version comparer
|
||||
func NewDEBComparer() *DEBComparer {
|
||||
return &DEBComparer{}
|
||||
}
|
||||
|
||||
// Compare compares two Debian package versions
|
||||
// Returns:
|
||||
// - positive if version1 > version2
|
||||
// - negative if version1 < version2
|
||||
// - zero if version1 == version2
|
||||
func (c *DEBComparer) Compare(version1, version2 string) (int, error) {
|
||||
v1, err := debver.NewVersion(version1)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
v2, err := debver.NewVersion(version2)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return v1.Compare(v2), nil
|
||||
}
|
||||
|
||||
// APKComparer implements Comparer for Alpine packages
|
||||
type APKComparer struct{}
|
||||
|
||||
// NewAPKComparer creates a new APK version comparer
|
||||
func NewAPKComparer() *APKComparer {
|
||||
return &APKComparer{}
|
||||
}
|
||||
|
||||
// Compare compares two Alpine package versions
|
||||
// Returns:
|
||||
// - positive if version1 > version2
|
||||
// - negative if version1 < version2
|
||||
// - zero if version1 == version2
|
||||
func (c *APKComparer) Compare(version1, version2 string) (int, error) {
|
||||
v1, err := apkver.NewVersion(version1)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
v2, err := apkver.NewVersion(version2)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return v1.Compare(v2), nil
|
||||
}
|
||||
152
pkg/detector/ospkg/version/compare_test.go
Normal file
152
pkg/detector/ospkg/version/compare_test.go
Normal file
@@ -0,0 +1,152 @@
|
||||
package version_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/aquasecurity/trivy/pkg/detector/ospkg/version"
|
||||
)
|
||||
|
||||
func TestDEBComparer_Compare(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
version1 string
|
||||
version2 string
|
||||
want int
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "equal versions",
|
||||
version1: "1.2.3",
|
||||
version2: "1.2.3",
|
||||
want: 0,
|
||||
},
|
||||
{
|
||||
name: "version1 greater",
|
||||
version1: "1.2.4",
|
||||
version2: "1.2.3",
|
||||
want: 1,
|
||||
},
|
||||
{
|
||||
name: "version1 less",
|
||||
version1: "1.2.2",
|
||||
version2: "1.2.3",
|
||||
want: -1,
|
||||
},
|
||||
{
|
||||
name: "with debian revision - equal base, different revision",
|
||||
version1: "1.2.3-1",
|
||||
version2: "1.2.3-2",
|
||||
want: -1,
|
||||
},
|
||||
{
|
||||
name: "with epoch - different epoch",
|
||||
version1: "1:1.2.3",
|
||||
version2: "2:1.2.3",
|
||||
want: -1,
|
||||
},
|
||||
{
|
||||
name: "with epoch and revision",
|
||||
version1: "1:1.2.3-1",
|
||||
version2: "1:1.2.3-2",
|
||||
want: -1,
|
||||
},
|
||||
{
|
||||
name: "ubuntu specific version",
|
||||
version1: "1.2.3-1ubuntu1",
|
||||
version2: "1.2.3-1ubuntu2",
|
||||
want: -1,
|
||||
},
|
||||
{
|
||||
name: "invalid version1",
|
||||
version1: "invalid_version",
|
||||
version2: "1.2.3",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid version2",
|
||||
version1: "1.2.3",
|
||||
version2: "invalid_version",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := version.NewDEBComparer()
|
||||
got, err := c.Compare(tt.version1, tt.version2)
|
||||
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPKComparer_Compare(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
version1 string
|
||||
version2 string
|
||||
want int
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "equal versions",
|
||||
version1: "1.2.3",
|
||||
version2: "1.2.3",
|
||||
want: 0,
|
||||
},
|
||||
{
|
||||
name: "version1 greater",
|
||||
version1: "1.2.4",
|
||||
version2: "1.2.3",
|
||||
want: 1,
|
||||
},
|
||||
{
|
||||
name: "version1 less",
|
||||
version1: "1.2.2",
|
||||
version2: "1.2.3",
|
||||
want: -1,
|
||||
},
|
||||
{
|
||||
name: "with alpine revision - equal base, different revision",
|
||||
version1: "1.2.3-r0",
|
||||
version2: "1.2.3-r1",
|
||||
want: -1,
|
||||
},
|
||||
{
|
||||
name: "pre-release versions",
|
||||
version1: "1.2.3_pre1",
|
||||
version2: "1.2.3",
|
||||
want: -1,
|
||||
},
|
||||
{
|
||||
name: "complex alpine version",
|
||||
version1: "1.2.3-r0",
|
||||
version2: "1.2.3_pre1-r0",
|
||||
want: 1,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := version.NewAPKComparer()
|
||||
got, err := c.Compare(tt.version1, tt.version2)
|
||||
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
195
pkg/detector/ospkg/version/constraint.go
Normal file
195
pkg/detector/ospkg/version/constraint.go
Normal file
@@ -0,0 +1,195 @@
|
||||
package version
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
// operatorFunc defines the signature for constraint operator functions
|
||||
type operatorFunc func(v, c string, comparer Comparer) (bool, error)
|
||||
|
||||
// constraintOperators maps operator strings to their corresponding functions
|
||||
var constraintOperators = map[string]operatorFunc{
|
||||
"": constraintEqual,
|
||||
"=": constraintEqual,
|
||||
"==": constraintEqual,
|
||||
"!=": constraintNotEqual,
|
||||
">": constraintGreaterThan,
|
||||
"<": constraintLessThan,
|
||||
">=": constraintGreaterThanEqual,
|
||||
"<=": constraintLessThanEqual,
|
||||
}
|
||||
|
||||
// constraintRegex matches constraint patterns like ">=1.2.3", "<2.0.0", "==1.0.0"
|
||||
// Version can contain numbers, dots, hyphens, plus signs, tildes, colons, and alphanumeric characters
|
||||
var constraintRegex = regexp.MustCompile(`^(>=|<=|>|<|==|!=|=)?\s*([0-9]+[0-9a-zA-Z.\-+~:_]*)$`)
|
||||
|
||||
// constraint represents a single version constraint
|
||||
type constraint struct {
|
||||
version string
|
||||
operator operatorFunc
|
||||
original string
|
||||
}
|
||||
|
||||
// Constraints represents a collection of constraints with a comparer
|
||||
type Constraints struct {
|
||||
constraints []*constraint
|
||||
comparer Comparer
|
||||
}
|
||||
|
||||
// NewConstraints creates a new Constraints from a constraint string and comparer
|
||||
// Multiple constraints can be separated by commas or spaces
|
||||
func NewConstraints(constraints string, comparer Comparer) (*Constraints, error) {
|
||||
if constraints == "" {
|
||||
return nil, xerrors.New("constraints string is empty")
|
||||
}
|
||||
|
||||
var cs []*constraint
|
||||
constraintList := splitConstraints(constraints)
|
||||
for _, constraintStr := range constraintList {
|
||||
constraintStr = strings.TrimSpace(constraintStr)
|
||||
if constraintStr == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
c, err := newConstraint(constraintStr)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("invalid constraint '%s': %w", constraintStr, err)
|
||||
}
|
||||
|
||||
cs = append(cs, c)
|
||||
}
|
||||
|
||||
return &Constraints{
|
||||
constraints: cs,
|
||||
comparer: comparer,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// splitConstraints splits constraint string by comma or space, preferring comma
|
||||
func splitConstraints(constraints string) []string {
|
||||
// If contains comma, split by comma
|
||||
if strings.Contains(constraints, ",") {
|
||||
return strings.Split(constraints, ",")
|
||||
}
|
||||
// Otherwise, split by spaces
|
||||
return strings.Fields(constraints)
|
||||
}
|
||||
|
||||
// newConstraint creates a new constraint from a constraint string
|
||||
func newConstraint(constraintStr string) (*constraint, error) {
|
||||
constraintStr = strings.TrimSpace(constraintStr)
|
||||
matches := constraintRegex.FindStringSubmatch(constraintStr)
|
||||
if len(matches) != 3 {
|
||||
return nil, xerrors.Errorf("invalid constraint format: %s", constraintStr)
|
||||
}
|
||||
|
||||
op := matches[1]
|
||||
version := strings.TrimSpace(matches[2])
|
||||
|
||||
operator, ok := constraintOperators[op]
|
||||
if !ok {
|
||||
return nil, xerrors.Errorf("unsupported operator: %s", op)
|
||||
}
|
||||
|
||||
return &constraint{
|
||||
version: version,
|
||||
operator: operator,
|
||||
original: constraintStr,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Check returns true if the given version satisfies the constraint
|
||||
func (c *constraint) check(version string, comparer Comparer) (bool, error) {
|
||||
return c.operator(version, c.version, comparer)
|
||||
}
|
||||
|
||||
// String returns the original constraint string
|
||||
func (c *constraint) String() string {
|
||||
return c.original
|
||||
}
|
||||
|
||||
// Check returns true if the given version satisfies any of the constraints
|
||||
// Multiple constraints are combined with AND logic
|
||||
func (cs *Constraints) Check(version string) (bool, error) {
|
||||
if version == "" {
|
||||
return false, xerrors.New("version is empty")
|
||||
}
|
||||
|
||||
if len(cs.constraints) == 0 {
|
||||
return false, xerrors.New("no constraints specified")
|
||||
}
|
||||
|
||||
for _, c := range cs.constraints {
|
||||
satisfied, err := c.check(version, cs.comparer)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if !satisfied {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// String returns the string representation of constraints
|
||||
func (cs *Constraints) String() string {
|
||||
var strs []string
|
||||
for _, c := range cs.constraints {
|
||||
strs = append(strs, c.String())
|
||||
}
|
||||
return strings.Join(strs, ", ")
|
||||
}
|
||||
|
||||
// Constraint operator functions
|
||||
|
||||
func constraintEqual(v, c string, comparer Comparer) (bool, error) {
|
||||
result, err := comparer.Compare(v, c)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return result == 0, nil
|
||||
}
|
||||
|
||||
func constraintNotEqual(v, c string, comparer Comparer) (bool, error) {
|
||||
result, err := comparer.Compare(v, c)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return result != 0, nil
|
||||
}
|
||||
|
||||
func constraintGreaterThan(v, c string, comparer Comparer) (bool, error) {
|
||||
result, err := comparer.Compare(v, c)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return result > 0, nil
|
||||
}
|
||||
|
||||
func constraintLessThan(v, c string, comparer Comparer) (bool, error) {
|
||||
result, err := comparer.Compare(v, c)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return result < 0, nil
|
||||
}
|
||||
|
||||
func constraintGreaterThanEqual(v, c string, comparer Comparer) (bool, error) {
|
||||
result, err := comparer.Compare(v, c)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return result >= 0, nil
|
||||
}
|
||||
|
||||
func constraintLessThanEqual(v, c string, comparer Comparer) (bool, error) {
|
||||
result, err := comparer.Compare(v, c)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return result <= 0, nil
|
||||
}
|
||||
320
pkg/detector/ospkg/version/constraint_test.go
Normal file
320
pkg/detector/ospkg/version/constraint_test.go
Normal file
@@ -0,0 +1,320 @@
|
||||
package version_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/aquasecurity/trivy/pkg/detector/ospkg/version"
|
||||
)
|
||||
|
||||
func TestNewConstraints(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
constraints string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "empty constraint returns error",
|
||||
constraints: "",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "single constraint with operator",
|
||||
constraints: ">=1.2.3",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "single constraint without operator",
|
||||
constraints: "1.2.3",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "multiple constraints with comma",
|
||||
constraints: ">=1.2.3, <2.0.0",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "multiple constraints with space",
|
||||
constraints: ">=1.2.3 <2.0.0",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "mixed operators",
|
||||
constraints: ">1.0.0, <=2.0.0, ==1.5.0",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "invalid constraint format",
|
||||
constraints: ">>>1.2.3",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "constraints with extra spaces",
|
||||
constraints: " >=1.2.3 , <2.0.0 ",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "not equal operator",
|
||||
constraints: "!=1.2.3",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "equal operator variations",
|
||||
constraints: "=1.2.3, ==1.2.4",
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, err := version.NewConstraints(tt.constraints, version.NewDEBComparer())
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConstraints_Check(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
constraints string
|
||||
version string
|
||||
want bool
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "empty version returns error",
|
||||
constraints: ">=1.2.3",
|
||||
version: "",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "equal constraint satisfied",
|
||||
constraints: "1.2.3",
|
||||
version: "1.2.3",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "equal constraint not satisfied",
|
||||
constraints: "1.2.3",
|
||||
version: "1.2.4",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "greater than satisfied",
|
||||
constraints: ">1.2.3",
|
||||
version: "1.2.4",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "greater than not satisfied",
|
||||
constraints: ">1.2.3",
|
||||
version: "1.2.3",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "greater than not satisfied (lower)",
|
||||
constraints: ">1.2.3",
|
||||
version: "1.2.2",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "greater than or equal satisfied (equal)",
|
||||
constraints: ">=1.2.3",
|
||||
version: "1.2.3",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "greater than or equal satisfied (greater)",
|
||||
constraints: ">=1.2.3",
|
||||
version: "1.2.4",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "less than satisfied",
|
||||
constraints: "<2.0.0",
|
||||
version: "1.9.9",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "less than not satisfied",
|
||||
constraints: "<2.0.0",
|
||||
version: "2.0.0",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "less than or equal satisfied (equal)",
|
||||
constraints: "<=2.0.0",
|
||||
version: "2.0.0",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "less than or equal satisfied (less)",
|
||||
constraints: "<=2.0.0",
|
||||
version: "1.9.9",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "not equal satisfied",
|
||||
constraints: "!=1.2.3",
|
||||
version: "1.2.4",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "not equal not satisfied",
|
||||
constraints: "!=1.2.3",
|
||||
version: "1.2.3",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "multiple constraints AND logic (before first constraint)",
|
||||
constraints: ">=1.0.0, <2.0.0",
|
||||
version: "0.9.0",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "multiple constraints AND logic (after second constraint)",
|
||||
constraints: ">=1.0.0, <2.0.0",
|
||||
version: "2.1.0",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "multiple constraints AND logic (satisfied)",
|
||||
constraints: ">=1.0.0, <2.0.0",
|
||||
version: "1.5.0",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "range constraint (satisfied)",
|
||||
constraints: ">=1.2.3, <2.0.0",
|
||||
version: "1.5.0",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "multiple constraints with space separator",
|
||||
constraints: ">=1.2.3 <2.0.0",
|
||||
version: "1.5.0",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "debian version with revision",
|
||||
constraints: ">=1.2.3-1",
|
||||
version: "1.2.3-2",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "debian version with epoch",
|
||||
constraints: ">=1:1.2.3",
|
||||
version: "1:1.2.4",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "debian version with epoch and revision",
|
||||
constraints: ">=1:1.2.3-1",
|
||||
version: "1:1.2.3-2",
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
comparer := version.NewDEBComparer()
|
||||
constraints, err := version.NewConstraints(tt.constraints, comparer)
|
||||
require.NoError(t, err)
|
||||
|
||||
got, err := constraints.Check(tt.version)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConstraints_String(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
constraints string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "single constraint",
|
||||
constraints: ">=1.2.3",
|
||||
want: ">=1.2.3",
|
||||
},
|
||||
{
|
||||
name: "multiple constraints",
|
||||
constraints: ">=1.2.3, <2.0.0",
|
||||
want: ">=1.2.3, <2.0.0",
|
||||
},
|
||||
{
|
||||
name: "constraints with extra spaces",
|
||||
constraints: " >=1.2.3 , <2.0.0 ",
|
||||
want: ">=1.2.3, <2.0.0",
|
||||
},
|
||||
{
|
||||
name: "space separated constraints",
|
||||
constraints: ">=1.2.3 <2.0.0",
|
||||
want: ">=1.2.3, <2.0.0",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
comparer := version.NewDEBComparer()
|
||||
constraints, err := version.NewConstraints(tt.constraints, comparer)
|
||||
require.NoError(t, err)
|
||||
|
||||
got := constraints.String()
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConstraints_CheckWithAPKComparer(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
constraints string
|
||||
version string
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "alpine version comparison",
|
||||
constraints: ">=1.2.3-r0",
|
||||
version: "1.2.3-r1",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "alpine version with pre-release",
|
||||
constraints: "<1.2.3_pre1",
|
||||
version: "1.2.2",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "alpine version range",
|
||||
constraints: ">=1.2.3-r0, <2.0.0",
|
||||
version: "1.5.0-r2",
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
comparer := version.NewAPKComparer()
|
||||
constraints, err := version.NewConstraints(tt.constraints, comparer)
|
||||
require.NoError(t, err)
|
||||
|
||||
got, err := constraints.Check(tt.version)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user