feat(vuln): add Root.io support for container image scanning (#9073)

Co-authored-by: DmitriyLewen <dmitriy.lewen@smartforce.io>
This commit is contained in:
Teppei Fukuda
2025-06-27 19:17:39 +04:00
committed by GitHub
parent 41d0f949c8
commit 3a0ec0f2ac
27 changed files with 1481 additions and 24 deletions

View File

@@ -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

View 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)

View File

@@ -169,6 +169,7 @@ trivy filesystem [flags] PATH
- govulndb
- echo
- minimos
- rootio
- auto
(default [auto])
```

View File

@@ -190,6 +190,7 @@ trivy image [flags] IMAGE_NAME
- govulndb
- echo
- minimos
- rootio
- auto
(default [auto])
```

View File

@@ -178,6 +178,7 @@ trivy kubernetes [flags] [CONTEXT]
- govulndb
- echo
- minimos
- rootio
- auto
(default [auto])
```

View File

@@ -168,6 +168,7 @@ trivy repository [flags] (REPO_PATH | REPO_URL)
- govulndb
- echo
- minimos
- rootio
- auto
(default [auto])
```

View File

@@ -170,6 +170,7 @@ trivy rootfs [flags] ROOTDIR
- govulndb
- echo
- minimos
- rootio
- auto
(default [auto])
```

View File

@@ -139,6 +139,7 @@ trivy sbom [flags] SBOM_PATH
- govulndb
- echo
- minimos
- rootio
- auto
(default [auto])
```

View File

@@ -155,6 +155,7 @@ trivy vm [flags] VM_IMAGE
- govulndb
- echo
- minimos
- rootio
- auto
(default [auto])
```

View File

@@ -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
View File

@@ -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
View File

@@ -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=

View File

@@ -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:

View File

@@ -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)))

View 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

View 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
}

View 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")
}
})
}
}

View 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
}

View 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)
})
}
}

View 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)
})
}
}

View 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"

View File

@@ -0,0 +1,9 @@
- bucket: root.io alpine 3.20
pairs:
- bucket: jq
pairs:
- key: CVE-2020-8177
value:
FixedVersion:
- foo
- bar

View 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"

View 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
}

View 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)
})
}
}

View 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
}

View 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)
})
}
}