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 }