mirror of
https://github.com/aquasecurity/trivy.git
synced 2025-12-12 15:50:15 -08:00
Signed-off-by: juan131 <jariza@vmware.com> Signed-off-by: knqyf263 <knqyf263@gmail.com> Co-authored-by: DmitriyLewen <dmitriy.lewen@smartforce.io> Co-authored-by: DmitriyLewen <91113035+DmitriyLewen@users.noreply.github.com> Co-authored-by: Nikita Pivkin <nikita.pivkin@smartforce.io> Co-authored-by: knqyf263 <knqyf263@gmail.com>
243 lines
6.6 KiB
Go
243 lines
6.6 KiB
Go
package redhat
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
version "github.com/knqyf263/go-rpm-version"
|
|
"golang.org/x/exp/maps"
|
|
"golang.org/x/exp/slices"
|
|
"golang.org/x/xerrors"
|
|
"k8s.io/utils/clock"
|
|
|
|
dbTypes "github.com/aquasecurity/trivy-db/pkg/types"
|
|
ustrings "github.com/aquasecurity/trivy-db/pkg/utils/strings"
|
|
redhat "github.com/aquasecurity/trivy-db/pkg/vulnsrc/redhat-oval"
|
|
"github.com/aquasecurity/trivy-db/pkg/vulnsrc/vulnerability"
|
|
osver "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/scanner/utils"
|
|
"github.com/aquasecurity/trivy/pkg/types"
|
|
)
|
|
|
|
var (
|
|
defaultContentSets = map[string][]string{
|
|
"6": {
|
|
"rhel-6-server-rpms",
|
|
"rhel-6-server-extras-rpms",
|
|
},
|
|
"7": {
|
|
"rhel-7-server-rpms",
|
|
"rhel-7-server-extras-rpms",
|
|
},
|
|
"8": {
|
|
"rhel-8-for-x86_64-baseos-rpms",
|
|
"rhel-8-for-x86_64-appstream-rpms",
|
|
},
|
|
"9": {
|
|
"rhel-9-for-x86_64-baseos-rpms",
|
|
"rhel-9-for-x86_64-appstream-rpms",
|
|
},
|
|
}
|
|
redhatEOLDates = map[string]time.Time{
|
|
"4": time.Date(2017, 5, 31, 23, 59, 59, 0, time.UTC),
|
|
"5": time.Date(2020, 11, 30, 23, 59, 59, 0, time.UTC),
|
|
"6": time.Date(2024, 6, 30, 23, 59, 59, 0, time.UTC),
|
|
// N/A
|
|
"7": time.Date(3000, 1, 1, 23, 59, 59, 0, time.UTC),
|
|
"8": time.Date(3000, 1, 1, 23, 59, 59, 0, time.UTC),
|
|
"9": time.Date(3000, 1, 1, 23, 59, 59, 0, time.UTC),
|
|
}
|
|
centosEOLDates = map[string]time.Time{
|
|
"3": time.Date(2010, 10, 31, 23, 59, 59, 0, time.UTC),
|
|
"4": time.Date(2012, 2, 29, 23, 59, 59, 0, time.UTC),
|
|
"5": time.Date(2017, 3, 31, 23, 59, 59, 0, time.UTC),
|
|
"6": time.Date(2020, 11, 30, 23, 59, 59, 0, time.UTC),
|
|
"7": time.Date(2024, 6, 30, 23, 59, 59, 0, time.UTC),
|
|
"8": time.Date(2021, 12, 31, 23, 59, 59, 0, time.UTC),
|
|
}
|
|
excludedVendorsSuffix = []string{
|
|
".remi",
|
|
}
|
|
)
|
|
|
|
type options struct {
|
|
clock clock.Clock
|
|
}
|
|
|
|
type option func(*options)
|
|
|
|
func WithClock(c clock.Clock) option {
|
|
return func(opts *options) {
|
|
opts.clock = c
|
|
}
|
|
}
|
|
|
|
// Scanner implements the RedHat scanner
|
|
type Scanner struct {
|
|
vs redhat.VulnSrc
|
|
*options
|
|
}
|
|
|
|
// NewScanner is the factory method for Scanner
|
|
func NewScanner(opts ...option) *Scanner {
|
|
o := &options{
|
|
clock: clock.RealClock{},
|
|
}
|
|
|
|
for _, opt := range opts {
|
|
opt(o)
|
|
}
|
|
return &Scanner{
|
|
vs: redhat.NewVulnSrc(),
|
|
options: o,
|
|
}
|
|
}
|
|
|
|
// Detect scans and returns redhat vulnerabilities
|
|
func (s *Scanner) Detect(osVer string, _ *ftypes.Repository, pkgs []ftypes.Package) ([]types.DetectedVulnerability, error) {
|
|
log.Logger.Info("Detecting RHEL/CentOS vulnerabilities...")
|
|
|
|
osVer = osver.Major(osVer)
|
|
log.Logger.Debugf("Red Hat: os version: %s", osVer)
|
|
log.Logger.Debugf("Red Hat: the number of packages: %d", len(pkgs))
|
|
|
|
var vulns []types.DetectedVulnerability
|
|
for _, pkg := range pkgs {
|
|
if !isFromSupportedVendor(pkg) {
|
|
log.Logger.Debugf("Skipping %s: unsupported vendor", pkg.Name)
|
|
continue
|
|
}
|
|
|
|
detectedVulns, err := s.detect(osVer, pkg)
|
|
if err != nil {
|
|
return nil, xerrors.Errorf("redhat vulnerability detection error: %w", err)
|
|
}
|
|
vulns = append(vulns, detectedVulns...)
|
|
}
|
|
return vulns, nil
|
|
}
|
|
|
|
func (s *Scanner) detect(osVer string, pkg ftypes.Package) ([]types.DetectedVulnerability, error) {
|
|
// For Red Hat OVAL v2 containing only binary package names
|
|
pkgName := addModularNamespace(pkg.Name, pkg.Modularitylabel)
|
|
|
|
var contentSets []string
|
|
var nvr string
|
|
if pkg.BuildInfo == nil {
|
|
contentSets = defaultContentSets[osVer]
|
|
} else {
|
|
contentSets = pkg.BuildInfo.ContentSets
|
|
nvr = fmt.Sprintf("%s-%s", pkg.BuildInfo.Nvr, pkg.BuildInfo.Arch)
|
|
}
|
|
|
|
advisories, err := s.vs.Get(pkgName, contentSets, []string{nvr})
|
|
if err != nil {
|
|
return nil, xerrors.Errorf("failed to get Red Hat advisories: %w", err)
|
|
}
|
|
|
|
installed := utils.FormatVersion(pkg)
|
|
installedVersion := version.NewVersion(installed)
|
|
|
|
uniqVulns := make(map[string]types.DetectedVulnerability)
|
|
for _, adv := range advisories {
|
|
// if Arches for advisory is empty or pkg.Arch is "noarch", then any Arches are affected
|
|
if len(adv.Arches) != 0 && pkg.Arch != "noarch" {
|
|
if !slices.Contains(adv.Arches, pkg.Arch) {
|
|
continue
|
|
}
|
|
}
|
|
|
|
vulnID := adv.VulnerabilityID
|
|
vuln := types.DetectedVulnerability{
|
|
VulnerabilityID: vulnID,
|
|
PkgID: pkg.ID,
|
|
PkgName: pkg.Name,
|
|
InstalledVersion: utils.FormatVersion(pkg),
|
|
PkgRef: pkg.Ref,
|
|
PkgIdentifier: pkg.Identifier,
|
|
Status: adv.Status,
|
|
Layer: pkg.Layer,
|
|
SeveritySource: vulnerability.RedHat,
|
|
Vulnerability: dbTypes.Vulnerability{
|
|
Severity: adv.Severity.String(),
|
|
},
|
|
Custom: adv.Custom,
|
|
}
|
|
|
|
// unpatched vulnerabilities
|
|
if adv.FixedVersion == "" {
|
|
// Red Hat may contain several advisories for the same vulnerability (RHSA advisories).
|
|
// To avoid overwriting the fixed version by mistake, we should skip unpatched vulnerabilities if they were added earlier
|
|
if _, ok := uniqVulns[vulnID]; !ok {
|
|
uniqVulns[vulnID] = vuln
|
|
}
|
|
continue
|
|
}
|
|
|
|
// patched vulnerabilities
|
|
fixedVersion := version.NewVersion(adv.FixedVersion)
|
|
if installedVersion.LessThan(fixedVersion) {
|
|
vuln.VendorIDs = adv.VendorIDs
|
|
vuln.FixedVersion = fixedVersion.String()
|
|
|
|
if v, ok := uniqVulns[vulnID]; ok {
|
|
// In case two advisories resolve the same CVE-ID.
|
|
// e.g. The first fix might be incomplete.
|
|
v.VendorIDs = ustrings.Unique(append(v.VendorIDs, vuln.VendorIDs...))
|
|
|
|
// The newer fixed version should be taken.
|
|
if version.NewVersion(v.FixedVersion).LessThan(fixedVersion) {
|
|
v.FixedVersion = vuln.FixedVersion
|
|
}
|
|
uniqVulns[vulnID] = v
|
|
} else {
|
|
uniqVulns[vulnID] = vuln
|
|
}
|
|
}
|
|
}
|
|
|
|
vulns := maps.Values(uniqVulns)
|
|
sort.Slice(vulns, func(i, j int) bool {
|
|
return vulns[i].VulnerabilityID < vulns[j].VulnerabilityID
|
|
})
|
|
|
|
return vulns, nil
|
|
}
|
|
|
|
// IsSupportedVersion checks is OSFamily can be scanned with Redhat scanner
|
|
func (s *Scanner) IsSupportedVersion(osFamily ftypes.OSType, osVer string) bool {
|
|
osVer = osver.Major(osVer)
|
|
if osFamily == ftypes.CentOS {
|
|
return osver.Supported(s.clock, centosEOLDates, osFamily, osVer)
|
|
}
|
|
|
|
return osver.Supported(s.clock, redhatEOLDates, osFamily, osVer)
|
|
}
|
|
|
|
func isFromSupportedVendor(pkg ftypes.Package) bool {
|
|
for _, suffix := range excludedVendorsSuffix {
|
|
if strings.HasSuffix(pkg.Release, suffix) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func addModularNamespace(name, label string) string {
|
|
// e.g. npm, nodejs:12:8030020201124152102:229f0a1c => nodejs:12::npm
|
|
var count int
|
|
for i, r := range label {
|
|
if r == ':' {
|
|
count++
|
|
}
|
|
if count == 2 {
|
|
return label[:i] + "::" + name
|
|
}
|
|
}
|
|
return name
|
|
}
|