github.com/devseccon/trivy@v0.47.1-0.20231123133102-bd902a0bd996/pkg/detector/ospkg/redhat/redhat.go (about)

     1  package redhat
     2  
     3  import (
     4  	"fmt"
     5  	"sort"
     6  	"strings"
     7  	"time"
     8  
     9  	version "github.com/knqyf263/go-rpm-version"
    10  	"golang.org/x/exp/maps"
    11  	"golang.org/x/exp/slices"
    12  	"golang.org/x/xerrors"
    13  	"k8s.io/utils/clock"
    14  
    15  	dbTypes "github.com/aquasecurity/trivy-db/pkg/types"
    16  	ustrings "github.com/aquasecurity/trivy-db/pkg/utils/strings"
    17  	redhat "github.com/aquasecurity/trivy-db/pkg/vulnsrc/redhat-oval"
    18  	"github.com/aquasecurity/trivy-db/pkg/vulnsrc/vulnerability"
    19  	osver "github.com/devseccon/trivy/pkg/detector/ospkg/version"
    20  	ftypes "github.com/devseccon/trivy/pkg/fanal/types"
    21  	"github.com/devseccon/trivy/pkg/log"
    22  	"github.com/devseccon/trivy/pkg/scanner/utils"
    23  	"github.com/devseccon/trivy/pkg/types"
    24  )
    25  
    26  var (
    27  	defaultContentSets = map[string][]string{
    28  		"6": {
    29  			"rhel-6-server-rpms",
    30  			"rhel-6-server-extras-rpms",
    31  		},
    32  		"7": {
    33  			"rhel-7-server-rpms",
    34  			"rhel-7-server-extras-rpms",
    35  		},
    36  		"8": {
    37  			"rhel-8-for-x86_64-baseos-rpms",
    38  			"rhel-8-for-x86_64-appstream-rpms",
    39  		},
    40  		"9": {
    41  			"rhel-9-for-x86_64-baseos-rpms",
    42  			"rhel-9-for-x86_64-appstream-rpms",
    43  		},
    44  	}
    45  	redhatEOLDates = map[string]time.Time{
    46  		"4": time.Date(2017, 5, 31, 23, 59, 59, 0, time.UTC),
    47  		"5": time.Date(2020, 11, 30, 23, 59, 59, 0, time.UTC),
    48  		"6": time.Date(2024, 6, 30, 23, 59, 59, 0, time.UTC),
    49  		// N/A
    50  		"7": time.Date(3000, 1, 1, 23, 59, 59, 0, time.UTC),
    51  		"8": time.Date(3000, 1, 1, 23, 59, 59, 0, time.UTC),
    52  		"9": time.Date(3000, 1, 1, 23, 59, 59, 0, time.UTC),
    53  	}
    54  	centosEOLDates = map[string]time.Time{
    55  		"3": time.Date(2010, 10, 31, 23, 59, 59, 0, time.UTC),
    56  		"4": time.Date(2012, 2, 29, 23, 59, 59, 0, time.UTC),
    57  		"5": time.Date(2017, 3, 31, 23, 59, 59, 0, time.UTC),
    58  		"6": time.Date(2020, 11, 30, 23, 59, 59, 0, time.UTC),
    59  		"7": time.Date(2024, 6, 30, 23, 59, 59, 0, time.UTC),
    60  		"8": time.Date(2021, 12, 31, 23, 59, 59, 0, time.UTC),
    61  	}
    62  	excludedVendorsSuffix = []string{
    63  		".remi",
    64  	}
    65  )
    66  
    67  type options struct {
    68  	clock clock.Clock
    69  }
    70  
    71  type option func(*options)
    72  
    73  func WithClock(c clock.Clock) option {
    74  	return func(opts *options) {
    75  		opts.clock = c
    76  	}
    77  }
    78  
    79  // Scanner implements the RedHat scanner
    80  type Scanner struct {
    81  	vs redhat.VulnSrc
    82  	*options
    83  }
    84  
    85  // NewScanner is the factory method for Scanner
    86  func NewScanner(opts ...option) *Scanner {
    87  	o := &options{
    88  		clock: clock.RealClock{},
    89  	}
    90  
    91  	for _, opt := range opts {
    92  		opt(o)
    93  	}
    94  	return &Scanner{
    95  		vs:      redhat.NewVulnSrc(),
    96  		options: o,
    97  	}
    98  }
    99  
   100  // Detect scans and returns redhat vulnerabilities
   101  func (s *Scanner) Detect(osVer string, _ *ftypes.Repository, pkgs []ftypes.Package) ([]types.DetectedVulnerability, error) {
   102  	log.Logger.Info("Detecting RHEL/CentOS vulnerabilities...")
   103  
   104  	osVer = osver.Major(osVer)
   105  	log.Logger.Debugf("Red Hat: os version: %s", osVer)
   106  	log.Logger.Debugf("Red Hat: the number of packages: %d", len(pkgs))
   107  
   108  	var vulns []types.DetectedVulnerability
   109  	for _, pkg := range pkgs {
   110  		if !isFromSupportedVendor(pkg) {
   111  			log.Logger.Debugf("Skipping %s: unsupported vendor", pkg.Name)
   112  			continue
   113  		}
   114  
   115  		detectedVulns, err := s.detect(osVer, pkg)
   116  		if err != nil {
   117  			return nil, xerrors.Errorf("redhat vulnerability detection error: %w", err)
   118  		}
   119  		vulns = append(vulns, detectedVulns...)
   120  	}
   121  	return vulns, nil
   122  }
   123  
   124  func (s *Scanner) detect(osVer string, pkg ftypes.Package) ([]types.DetectedVulnerability, error) {
   125  	// For Red Hat OVAL v2 containing only binary package names
   126  	pkgName := addModularNamespace(pkg.Name, pkg.Modularitylabel)
   127  
   128  	var contentSets []string
   129  	var nvr string
   130  	if pkg.BuildInfo == nil {
   131  		contentSets = defaultContentSets[osVer]
   132  	} else {
   133  		contentSets = pkg.BuildInfo.ContentSets
   134  		nvr = fmt.Sprintf("%s-%s", pkg.BuildInfo.Nvr, pkg.BuildInfo.Arch)
   135  	}
   136  
   137  	advisories, err := s.vs.Get(pkgName, contentSets, []string{nvr})
   138  	if err != nil {
   139  		return nil, xerrors.Errorf("failed to get Red Hat advisories: %w", err)
   140  	}
   141  
   142  	installed := utils.FormatVersion(pkg)
   143  	installedVersion := version.NewVersion(installed)
   144  
   145  	uniqVulns := make(map[string]types.DetectedVulnerability)
   146  	for _, adv := range advisories {
   147  		// if Arches for advisory is empty or pkg.Arch is "noarch", then any Arches are affected
   148  		if len(adv.Arches) != 0 && pkg.Arch != "noarch" {
   149  			if !slices.Contains(adv.Arches, pkg.Arch) {
   150  				continue
   151  			}
   152  		}
   153  
   154  		vulnID := adv.VulnerabilityID
   155  		vuln := types.DetectedVulnerability{
   156  			VulnerabilityID:  vulnID,
   157  			PkgID:            pkg.ID,
   158  			PkgName:          pkg.Name,
   159  			InstalledVersion: utils.FormatVersion(pkg),
   160  			PkgRef:           pkg.Ref,
   161  			Status:           adv.Status,
   162  			Layer:            pkg.Layer,
   163  			SeveritySource:   vulnerability.RedHat,
   164  			Vulnerability: dbTypes.Vulnerability{
   165  				Severity: adv.Severity.String(),
   166  			},
   167  			Custom: adv.Custom,
   168  		}
   169  
   170  		// unpatched vulnerabilities
   171  		if adv.FixedVersion == "" {
   172  			// Red Hat may contain several advisories for the same vulnerability (RHSA advisories).
   173  			// To avoid overwriting the fixed version by mistake, we should skip unpatched vulnerabilities if they were added earlier
   174  			if _, ok := uniqVulns[vulnID]; !ok {
   175  				uniqVulns[vulnID] = vuln
   176  			}
   177  			continue
   178  		}
   179  
   180  		// patched vulnerabilities
   181  		fixedVersion := version.NewVersion(adv.FixedVersion)
   182  		if installedVersion.LessThan(fixedVersion) {
   183  			vuln.VendorIDs = adv.VendorIDs
   184  			vuln.FixedVersion = fixedVersion.String()
   185  
   186  			if v, ok := uniqVulns[vulnID]; ok {
   187  				// In case two advisories resolve the same CVE-ID.
   188  				// e.g. The first fix might be incomplete.
   189  				v.VendorIDs = ustrings.Unique(append(v.VendorIDs, vuln.VendorIDs...))
   190  
   191  				// The newer fixed version should be taken.
   192  				if version.NewVersion(v.FixedVersion).LessThan(fixedVersion) {
   193  					v.FixedVersion = vuln.FixedVersion
   194  				}
   195  				uniqVulns[vulnID] = v
   196  			} else {
   197  				uniqVulns[vulnID] = vuln
   198  			}
   199  		}
   200  	}
   201  
   202  	vulns := maps.Values(uniqVulns)
   203  	sort.Slice(vulns, func(i, j int) bool {
   204  		return vulns[i].VulnerabilityID < vulns[j].VulnerabilityID
   205  	})
   206  
   207  	return vulns, nil
   208  }
   209  
   210  // IsSupportedVersion checks is OSFamily can be scanned with Redhat scanner
   211  func (s *Scanner) IsSupportedVersion(osFamily ftypes.OSType, osVer string) bool {
   212  	osVer = osver.Major(osVer)
   213  	if osFamily == ftypes.CentOS {
   214  		return osver.Supported(s.clock, centosEOLDates, osFamily, osVer)
   215  	}
   216  
   217  	return osver.Supported(s.clock, redhatEOLDates, osFamily, osVer)
   218  }
   219  
   220  func isFromSupportedVendor(pkg ftypes.Package) bool {
   221  	for _, suffix := range excludedVendorsSuffix {
   222  		if strings.HasSuffix(pkg.Release, suffix) {
   223  			return false
   224  		}
   225  	}
   226  	return true
   227  }
   228  
   229  func addModularNamespace(name, label string) string {
   230  	// e.g. npm, nodejs:12:8030020201124152102:229f0a1c => nodejs:12::npm
   231  	var count int
   232  	for i, r := range label {
   233  		if r == ':' {
   234  			count++
   235  		}
   236  		if count == 2 {
   237  			return label[:i] + "::" + name
   238  		}
   239  	}
   240  	return name
   241  }