github.com/quay/claircore@v1.5.28/rhel/rhcc/updater.go (about) 1 package rhcc 2 3 import ( 4 "compress/bzip2" 5 "context" 6 "encoding/xml" 7 "fmt" 8 "io" 9 "net/http" 10 "sort" 11 12 "github.com/quay/zlog" 13 "golang.org/x/text/cases" 14 "golang.org/x/text/language" 15 16 "github.com/quay/claircore" 17 "github.com/quay/claircore/internal/xmlutil" 18 "github.com/quay/claircore/libvuln/driver" 19 "github.com/quay/claircore/pkg/rhctag" 20 "github.com/quay/claircore/pkg/tmp" 21 "github.com/quay/claircore/rhel/internal/common" 22 "github.com/quay/claircore/toolkit/types/cpe" 23 ) 24 25 //doc:url updater 26 const ( 27 dbURL = "https://access.redhat.com/security/data/metrics/cvemap.xml" 28 cveURL = "https://access.redhat.com/security/cve/" 29 ) 30 31 var ( 32 _ driver.Updater = (*updater)(nil) 33 _ driver.Configurable = (*updater)(nil) 34 ) 35 36 // updater fetches and parses cvemap.xml 37 type updater struct { 38 client *http.Client 39 url string 40 bzipped bool 41 } 42 43 // UpdaterConfig is the configuration for the container catalog's updater. 44 // 45 // By convention, this is in a "rhel-container-updater" key. 46 type UpdaterConfig struct { 47 // URL is the URL to a "cvemap.xml" file. 48 // 49 // The Updater's configuration hook will check for a version with an 50 // additional ".bz2" extension. 51 URL string `json:"url" yaml:"url"` 52 } 53 54 const updaterName = "rhel-container-updater" 55 56 func (*updater) Name() string { 57 return updaterName 58 } 59 60 // UpdaterSet returns the rhcc UpdaterSet. 61 func UpdaterSet(_ context.Context) (driver.UpdaterSet, error) { 62 us := driver.NewUpdaterSet() 63 if err := us.Add(&updater{}); err != nil { 64 return us, err 65 } 66 return us, nil 67 } 68 69 // Configure implements [driver.Configurable]. 70 func (u *updater) Configure(ctx context.Context, f driver.ConfigUnmarshaler, c *http.Client) error { 71 u.url = dbURL 72 u.client = c 73 var cfg UpdaterConfig 74 if err := f(&cfg); err != nil { 75 return err 76 } 77 if cfg.URL != "" { 78 u.url = cfg.URL 79 } 80 81 // This could check the reported content type perhaps, but just relying on 82 // the extension is quicker and we have inside information that it's 83 // correct. 84 tryURL := cfg.URL + ".bz2" 85 req, err := http.NewRequestWithContext(ctx, http.MethodHead, tryURL, nil) 86 if err != nil { 87 // WTF? 88 return err 89 } 90 res, err := u.client.Do(req) 91 if err == nil { // NB swapped conditional 92 res.Body.Close() 93 if res.StatusCode == http.StatusOK { 94 u.url = tryURL 95 u.bzipped = true 96 } 97 } 98 return nil 99 } 100 101 // Fetch implements [driver.Updater]. 102 func (u *updater) Fetch(ctx context.Context, hint driver.Fingerprint) (io.ReadCloser, driver.Fingerprint, error) { 103 ctx = zlog.ContextWithValues(ctx, "component", "rhel/rhcc/Updater.Fetch") 104 105 zlog.Info(ctx).Str("database", u.url).Msg("starting fetch") 106 req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.url, nil) 107 if err != nil { 108 return nil, hint, fmt.Errorf("rhcc: unable to construct request: %w", err) 109 } 110 111 if hint != "" { 112 zlog.Debug(ctx). 113 Str("hint", string(hint)). 114 Msg("using hint") 115 req.Header.Set("if-none-match", string(hint)) 116 } 117 118 res, err := u.client.Do(req) 119 if err != nil { 120 return nil, hint, fmt.Errorf("rhcc: error making request: %w", err) 121 } 122 defer res.Body.Close() 123 124 switch res.StatusCode { 125 case http.StatusOK: 126 if t := string(hint); t == "" || t != res.Header.Get("etag") { 127 break 128 } 129 fallthrough 130 case http.StatusNotModified: 131 zlog.Info(ctx).Msg("database unchanged since last fetch") 132 return nil, hint, driver.Unchanged 133 default: 134 return nil, hint, fmt.Errorf("rhcc: http response error: %s %d", res.Status, res.StatusCode) 135 } 136 zlog.Debug(ctx).Msg("successfully requested database") 137 138 tf, err := tmp.NewFile("", updaterName+".") 139 if err != nil { 140 return nil, hint, fmt.Errorf("rhcc: unable to open tempfile: %w", err) 141 } 142 zlog.Debug(ctx). 143 Str("name", tf.Name()). 144 Msg("created tempfile") 145 var success bool 146 defer func() { 147 if !success { 148 if err := tf.Close(); err != nil { 149 zlog.Warn(ctx).Err(err).Msg("unable to close spool") 150 } 151 } 152 }() 153 154 var r io.Reader = res.Body 155 if u.bzipped { 156 // No cleanup/pooling. 157 r = bzip2.NewReader(res.Body) 158 } 159 if _, err := io.Copy(tf, r); err != nil { 160 return nil, hint, fmt.Errorf("rhcc: unable to copy resp body to tempfile: %w", err) 161 } 162 if n, err := tf.Seek(0, io.SeekStart); err != nil || n != 0 { 163 return nil, hint, fmt.Errorf("rhcc: unable to seek database to start: %w", err) 164 } 165 zlog.Debug(ctx).Msg("decompressed and buffered database") 166 167 success = true 168 hint = driver.Fingerprint(res.Header.Get("etag")) 169 zlog.Debug(ctx). 170 Str("hint", string(hint)). 171 Msg("using new hint") 172 173 return tf, hint, nil 174 } 175 176 // Parse implements [driver.Updater]. 177 func (u *updater) Parse(ctx context.Context, r io.ReadCloser) ([]*claircore.Vulnerability, error) { 178 ctx = zlog.ContextWithValues(ctx, "component", "rhel/rhcc/Updater.Parse") 179 zlog.Info(ctx).Msg("parse start") 180 defer r.Close() 181 defer zlog.Info(ctx).Msg("parse done") 182 183 var cvemap cveMap 184 dec := xml.NewDecoder(r) 185 dec.CharsetReader = xmlutil.CharsetReader 186 if err := dec.Decode(&cvemap); err != nil { 187 return nil, fmt.Errorf("rhel: unable to decode cvemap: %w", err) 188 } 189 zlog.Debug(ctx).Msg("xml decoded") 190 191 zlog.Debug(ctx). 192 Int("count", len(cvemap.RedHatVulnerabilities)).Msg("found raw entries") 193 194 vs := []*claircore.Vulnerability{} 195 for _, vuln := range cvemap.RedHatVulnerabilities { 196 description := getDescription(vuln.Details) 197 versionsByContainer := make(map[string]map[rhctag.Version]*consolidatedRelease) 198 for _, release := range vuln.AffectedReleases { 199 match, packageName, version := parseContainerPackage(release.Package) 200 if !match { 201 continue 202 } 203 // parse version 204 v, err := rhctag.Parse(version) 205 if err != nil { 206 zlog.Debug(ctx). 207 Str("package", packageName). 208 Str("version", version). 209 Err(err). 210 Msgf("tag parse error") 211 continue 212 } 213 // parse severity 214 var severity string 215 if release.Impact != "" { 216 severity = release.Impact 217 } else { 218 severity = vuln.ThreatSeverity 219 } 220 titleCase := cases.Title(language.Und) 221 severity = titleCase.String(severity) 222 // parse cpe 223 cpe, err := cpe.Unbind(release.Cpe) 224 if err != nil { 225 zlog.Warn(ctx). 226 Err(err). 227 Str("cpe", release.Cpe). 228 Msg("could not unbind cpe") 229 continue 230 } 231 // collect minor keys 232 minorKey := v.MinorStart() 233 // initialize and update the minorKey to consolidated release map 234 if versionsByContainer[packageName] == nil { 235 versionsByContainer[packageName] = make(map[rhctag.Version]*consolidatedRelease) 236 } 237 versionsByContainer[packageName][minorKey] = &consolidatedRelease{ 238 Cpe: cpe, 239 Issued: release.ReleaseDate.time, 240 Severity: severity, 241 AdvisoryLink: release.Advisory.URL, 242 AdvisoryName: release.Advisory.Text, 243 } 244 // initialize and update the fixed in versions slice 245 if versionsByContainer[packageName][minorKey].FixedInVersions == nil { 246 vs := make(rhctag.Versions, 0) 247 versionsByContainer[packageName][minorKey].FixedInVersions = &vs 248 } 249 newVersions := versionsByContainer[packageName][minorKey].FixedInVersions.Append(v) 250 versionsByContainer[packageName][minorKey].FixedInVersions = &newVersions 251 } 252 253 // Build the Vulnerability slice 254 for pkg, releasesByMinor := range versionsByContainer { 255 p := &claircore.Package{ 256 Name: pkg, 257 Kind: claircore.BINARY, 258 } 259 // sort minor keys 260 minorKeys := make(rhctag.Versions, 0) 261 for k := range releasesByMinor { 262 minorKeys = append(minorKeys, k) 263 } 264 sort.Sort(minorKeys) 265 // iterate minor key map in order 266 for idx, minor := range minorKeys { 267 // sort the fixed in versions 268 sort.Sort(releasesByMinor[minor].FixedInVersions) 269 // The first minor version range should match all previous versions 270 start := minor 271 if idx == 0 { 272 start = rhctag.Version{} 273 } 274 // For containers such as openshift-logging/elasticsearch6-rhel8 we need to match 275 // the first Fixed in Version here. 276 // Most of the time this will return the only Fixed In Version for minor version 277 firstPatch, _ := releasesByMinor[minor].FixedInVersions.First() 278 r := &claircore.Range{ 279 Lower: start.Version(true), 280 Upper: firstPatch.Version(false), 281 } 282 links := fmt.Sprintf("%s %s%s", releasesByMinor[minor].AdvisoryLink, cveURL, vuln.Name) 283 v := &claircore.Vulnerability{ 284 Updater: updaterName, 285 Name: releasesByMinor[minor].AdvisoryName, 286 Description: description, 287 Issued: releasesByMinor[minor].Issued, 288 Severity: releasesByMinor[minor].Severity, 289 NormalizedSeverity: common.NormalizeSeverity(releasesByMinor[minor].Severity), 290 Package: p, 291 Repo: &goldRepo, 292 Links: links, 293 FixedInVersion: firstPatch.Original, 294 Range: r, 295 } 296 vs = append(vs, v) 297 } 298 } 299 } 300 zlog.Debug(ctx). 301 Int("count", len(vs)). 302 Msg("found vulnerabilities") 303 return vs, nil 304 }