github.com/quay/claircore@v1.5.28/ubuntu/updater.go (about) 1 package ubuntu 2 3 import ( 4 "compress/bzip2" 5 "context" 6 "encoding/xml" 7 "fmt" 8 "io" 9 "net/http" 10 "net/url" 11 12 "github.com/quay/goval-parser/oval" 13 "github.com/quay/zlog" 14 15 "github.com/quay/claircore" 16 "github.com/quay/claircore/internal/xmlutil" 17 "github.com/quay/claircore/libvuln/driver" 18 "github.com/quay/claircore/pkg/ovalutil" 19 "github.com/quay/claircore/pkg/tmp" 20 ) 21 22 var ( 23 _ driver.Updater = (*updater)(nil) 24 _ driver.Configurable = (*updater)(nil) 25 ) 26 27 // Updater fetches and parses Ubuntu-flavored OVAL. 28 // 29 // Updaters are constructed exclusively by the [Factory]. 30 type updater struct { 31 // the url to fetch the OVAL db from 32 url string 33 useBzip2 bool 34 name string 35 id string 36 c *http.Client 37 } 38 39 // Name implements [driver.Updater]. 40 func (u *updater) Name() string { 41 return fmt.Sprintf("ubuntu/updater/%s", u.name) 42 } 43 44 // Configure implements [driver.Configurable]. 45 func (u *updater) Configure(ctx context.Context, f driver.ConfigUnmarshaler, c *http.Client) error { 46 ctx = zlog.ContextWithValues(ctx, 47 "component", "ubuntu/Updater.Configure", 48 "updater", u.Name()) 49 u.c = c 50 51 var cfg UpdaterConfig 52 if err := f(&cfg); err != nil { 53 return err 54 } 55 56 if cfg.URL != "" { 57 if _, err := url.Parse(cfg.URL); err != nil { 58 return err 59 } 60 u.url = cfg.URL 61 zlog.Info(ctx). 62 Msg("configured database URL") 63 } 64 if cfg.UseBzip2 != nil { 65 u.useBzip2 = *cfg.UseBzip2 66 } 67 68 return nil 69 } 70 71 // UpdaterConfig is the configuration for the updater. 72 // 73 // By convention, this is in a map called "ubuntu/updater/${RELEASE}", e.g. 74 // "ubuntu/updater/focal". 75 type UpdaterConfig struct { 76 URL string `json:"url" yaml:"url"` 77 UseBzip2 *bool `json:"use_bzip2" yaml:"use_bzip2"` 78 } 79 80 // Fetch implements [driver.Updater]. 81 func (u *updater) Fetch(ctx context.Context, fingerprint driver.Fingerprint) (io.ReadCloser, driver.Fingerprint, error) { 82 ctx = zlog.ContextWithValues(ctx, 83 "component", "ubuntu/Updater.Fetch", 84 "database", u.url) 85 86 req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.url, nil) 87 if err != nil { 88 return nil, "", fmt.Errorf("failed to create request") 89 } 90 if fingerprint != "" { 91 req.Header.Set("if-none-match", string(fingerprint)) 92 } 93 94 // fetch OVAL xml database 95 resp, err := u.c.Do(req) 96 if err != nil { 97 return nil, "", fmt.Errorf("ubuntu: failed to retrieve OVAL database: %w", err) 98 } 99 defer resp.Body.Close() 100 101 switch resp.StatusCode { 102 case http.StatusOK: 103 if fp := string(fingerprint); fp == "" || fp != resp.Header.Get("etag") { 104 zlog.Info(ctx).Msg("fetching latest oval database") 105 break 106 } 107 fallthrough 108 case http.StatusNotModified: 109 return nil, fingerprint, driver.Unchanged 110 default: 111 return nil, "", fmt.Errorf("ubuntu: unexpected response: %s", resp.Status) 112 } 113 114 fp := resp.Header.Get("etag") 115 f, err := tmp.NewFile("", "ubuntu.") 116 if err != nil { 117 return nil, "", err 118 } 119 var success bool 120 defer func() { 121 if !success { 122 if err := f.Close(); err != nil { 123 zlog.Warn(ctx).Err(err).Msg("unable to close spool") 124 } 125 } 126 }() 127 var r io.Reader = resp.Body 128 if u.useBzip2 { 129 r = bzip2.NewReader(r) 130 } 131 if _, err := io.Copy(f, r); err != nil { 132 return nil, "", fmt.Errorf("ubuntu: failed to read http body: %w", err) 133 } 134 if _, err := f.Seek(0, io.SeekStart); err != nil { 135 return nil, "", fmt.Errorf("ubuntu: failed to seek body: %w", err) 136 } 137 138 success = true 139 zlog.Info(ctx).Msg("fetched latest oval database successfully") 140 return f, driver.Fingerprint(fp), err 141 } 142 143 // Parse implements [driver.Updater]. 144 func (u *updater) Parse(ctx context.Context, r io.ReadCloser) ([]*claircore.Vulnerability, error) { 145 ctx = zlog.ContextWithValues(ctx, 146 "component", "ubuntu/Updater.Parse") 147 zlog.Info(ctx).Msg("starting parse") 148 defer r.Close() 149 root := oval.Root{} 150 dec := xml.NewDecoder(r) 151 dec.CharsetReader = xmlutil.CharsetReader 152 if err := dec.Decode(&root); err != nil { 153 return nil, fmt.Errorf("ubuntu: unable to decode OVAL document: %w", err) 154 } 155 zlog.Debug(ctx).Msg("xml decoded") 156 157 nameLookupFunc := func(def oval.Definition, name *oval.DpkgName) []string { 158 // if the dpkginfo_object>name field has a var_ref it indicates 159 // a variable lookup for all packages affected by this vuln is necessary. 160 // 161 // if the name.Ref field is empty it indicates a single package is affected 162 // by the vuln and that package's name is in name.Body. 163 var ns []string 164 if len(name.Ref) == 0 { 165 ns = append(ns, name.Body) 166 return ns 167 } 168 _, i, err := root.Variables.Lookup(name.Ref) 169 if err != nil { 170 zlog.Error(ctx).Err(err).Msg("could not lookup variable id") 171 return ns 172 } 173 consts := root.Variables.ConstantVariables[i] 174 for _, v := range consts.Values { 175 ns = append(ns, v.Body) 176 } 177 return ns 178 } 179 180 protoVulns := func(def oval.Definition) ([]*claircore.Vulnerability, error) { 181 vs := []*claircore.Vulnerability{} 182 v := &claircore.Vulnerability{ 183 Updater: u.Name(), 184 Name: def.Title, 185 Description: def.Description, 186 Issued: def.Advisory.Issued.Date, 187 Links: ovalutil.Links(def), 188 NormalizedSeverity: normalizeSeverity(def.Advisory.Severity), 189 Dist: lookupDist(u.id), 190 } 191 vs = append(vs, v) 192 return vs, nil 193 } 194 vulns, err := ovalutil.DpkgDefsToVulns(ctx, &root, protoVulns, nameLookupFunc) 195 if err != nil { 196 return nil, err 197 } 198 return vulns, nil 199 } 200 201 func normalizeSeverity(severity string) claircore.Severity { 202 switch severity { 203 case "Negligible": 204 return claircore.Negligible 205 case "Low": 206 return claircore.Low 207 case "Medium": 208 return claircore.Medium 209 case "High": 210 return claircore.High 211 case "Critical": 212 return claircore.Critical 213 default: 214 } 215 return claircore.Unknown 216 }