github.com/quay/claircore@v1.5.28/ubuntu/updaterset.go (about) 1 package ubuntu 2 3 import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "net/http" 8 "net/url" 9 "path" 10 "strings" 11 "sync" 12 13 "github.com/quay/zlog" 14 "golang.org/x/sync/errgroup" 15 16 "github.com/quay/claircore" 17 "github.com/quay/claircore/libvuln/driver" 18 ) 19 20 var ( 21 _ driver.Configurable = (*Factory)(nil) 22 _ driver.UpdaterSetFactory = (*Factory)(nil) 23 ) 24 25 //doc:url updater 26 const ( 27 defaultAPI = `https://api.launchpad.net/1.0/` 28 ovalURL = `https://security-metadata.canonical.com/oval/com.ubuntu.%s.cve.oval.xml` 29 ) 30 const defaultName = `ubuntu` 31 32 // NewFactory constructs a Factory. 33 // 34 // The returned Factory must have [Configure] called before [UpdaterSet]. 35 func NewFactory(ctx context.Context) (*Factory, error) { 36 return &Factory{}, nil 37 } 38 39 // Factory implements [driver.UpdaterSetFactory]. 40 // 41 // [Configure] must be called before [UpdaterSet]. 42 type Factory struct { 43 c *http.Client 44 api string 45 force [][2]string 46 } 47 48 // FactoryConfig is the configuration for Factories. 49 type FactoryConfig struct { 50 // URL should be the address of a [Launchpad API] server. 51 // 52 // [Launchpad API]: https://launchpad.net/+apidoc/1.0.html 53 URL string `json:"url" yaml:"url"` 54 // Name is the distribution name, as used in talking to the Launchpad API. 55 Name string `json:"name" yaml:"name"` 56 // Force is a list of name, version pairs to put in the resulting UpdaterSet regardless 57 // of their existence or "active" status in the API response. The resulting Updaters 58 // will have guesses at reasonable settings, but the individual Updater's configuration 59 // should be used to ensure correct parameters. 60 // 61 // For example, the name, version pair for Ubuntu 20.04 would be "focal", "20.04". 62 Force []struct { 63 Name string `json:"name" yaml:"name"` 64 Version string `json:"version" yaml:"version"` 65 } 66 } 67 68 // Configure implements [driver.Configurable]. 69 func (f *Factory) Configure(ctx context.Context, cf driver.ConfigUnmarshaler, c *http.Client) error { 70 ctx = zlog.ContextWithValues(ctx, 71 "component", "ubuntu/Factory.Configure") 72 var cfg FactoryConfig 73 if err := cf(&cfg); err != nil { 74 return err 75 } 76 f.c = c 77 78 u, err := url.Parse(defaultAPI) 79 if err != nil { 80 panic("programmer error: " + err.Error()) 81 } 82 if cfg.URL != "" { 83 u, err = url.Parse(cfg.URL) 84 if err != nil { 85 return fmt.Errorf("ubuntu: unable to parse provided URL: %w", err) 86 } 87 zlog.Info(ctx). 88 Msg("configured URL") 89 } 90 n := defaultName 91 if cfg.Name != "" { 92 n = cfg.Name 93 } 94 u, err = u.Parse(path.Join(n, "series")) 95 if err != nil { 96 return fmt.Errorf("ubuntu: unable to parse constructed URL: %w", err) 97 } 98 f.api = u.String() 99 100 for _, p := range cfg.Force { 101 f.force = append(f.force, [2]string{p.Name, p.Version}) 102 } 103 104 return nil 105 } 106 107 // UpdaterSet implements [driver.UpdaterSetFactory] 108 func (f *Factory) UpdaterSet(ctx context.Context) (driver.UpdaterSet, error) { 109 ctx = zlog.ContextWithValues(ctx, 110 "component", "ubuntu/Factory.UpdaterSet") 111 112 set := driver.NewUpdaterSet() 113 req, err := http.NewRequestWithContext(ctx, http.MethodGet, f.api, nil) 114 if err != nil { 115 return set, fmt.Errorf("ubuntu: unable to construct request: %w", err) 116 } 117 // There's no way to do conditional requests to this endpoint, as per [the docs]. 118 // It should change very slowly, but it seems like there's no alternative to asking for 119 // a few KB of JSON every so often. 120 // 121 // [the docs]: https://help.launchpad.net/API/Hacking 122 req.Header.Set(`TE`, `gzip`) 123 req.Header.Set(`Accept`, `application/json`) 124 res, err := f.c.Do(req) 125 if err != nil { 126 return set, fmt.Errorf("ubuntu: error requesting series collection: %w", err) 127 } 128 defer res.Body.Close() 129 switch res.StatusCode { 130 case http.StatusOK: 131 default: 132 return set, fmt.Errorf("ubuntu: unexpected status requesting %q: %q", f.api, res.Status) 133 } 134 var series seriesResponse 135 if err := json.NewDecoder(res.Body).Decode(&series); err != nil { 136 return set, fmt.Errorf("ubuntu: error requesting series collection: %w", err) 137 } 138 139 eg, ctx := errgroup.WithContext(ctx) 140 ch := make(chan *distroSeries) 141 us := make(chan *updater) 142 eg.Go(func() error { 143 // Send active distribution series down the channel. 144 defer close(ch) 145 for i := range series.Entries { 146 e := &series.Entries[i] 147 mkDist(e.Version, e.Name) 148 if !e.Active { 149 zlog.Debug(ctx).Str("release", e.Name).Msg("release not active") 150 continue 151 } 152 select { 153 case ch <- e: 154 case <-ctx.Done(): 155 return ctx.Err() 156 } 157 } 158 return nil 159 }) 160 eg.Go(func() error { 161 // Double-check the distribution. 162 defer close(us) 163 for e := range ch { 164 url := fmt.Sprintf(ovalURL, e.Name) 165 req, err := http.NewRequestWithContext(ctx, http.MethodHead, url, nil) 166 if err != nil { 167 return fmt.Errorf("ubuntu: unable to construct request: %w", err) 168 } 169 req.Header.Set(`accept`, `application/x-bzip2,application/xml;q=0.9`) 170 res, err := f.c.Do(req) 171 if err != nil { 172 return fmt.Errorf("ubuntu: error requesting inspecting OVAL feed: %w", err) 173 } 174 defer res.Body.Close() 175 switch res.StatusCode { 176 case http.StatusOK: 177 case http.StatusNotFound: 178 zlog.Debug(ctx). 179 Str("name", e.Name). 180 Str("version", e.Version). 181 Msg("OVAL database missing, skipping") 182 continue 183 default: 184 return fmt.Errorf("ubuntu: unexpected status requesting %q: %q", url, res.Status) 185 } 186 next, err := res.Request.URL.Parse(res.Header.Get(`content-location`)) 187 if err != nil { 188 return fmt.Errorf(`ubuntu: unable to parse "Content-Location": %w`, err) 189 } 190 us <- &updater{ 191 url: next.String(), 192 useBzip2: strings.EqualFold(`application/x-bzip2`, res.Header.Get(`content-type`)), 193 name: e.Name, 194 id: e.Version, 195 } 196 } 197 return nil 198 }) 199 eg.Go(func() error { 200 // Construct the set 201 for u := range us { 202 if err := set.Add(u); err != nil { 203 return err 204 } 205 } 206 return nil 207 }) 208 if err := eg.Wait(); err != nil { 209 return set, err 210 } 211 212 if len(f.force) != 0 { 213 zlog.Info(ctx).Msg("configuring manually specified updaters") 214 ns := make([]string, 0, len(f.force)) 215 for _, p := range f.force { 216 u := &updater{ 217 url: fmt.Sprintf(ovalURL+".bz2", p[0]), 218 useBzip2: true, 219 name: p[0], 220 id: p[1], 221 } 222 if err := set.Add(u); err != nil { 223 // Already exists, skip. 224 zlog.Debug(ctx).Err(err).Msg("skipping updater") 225 continue 226 } 227 ns = append(ns, u.Name()) 228 } 229 zlog.Info(ctx).Strs("updaters", ns).Msg("added specified updaters") 230 } 231 232 return set, nil 233 } 234 235 type seriesResponse struct { 236 Entries []distroSeries `json:"entries"` 237 } 238 239 type distroSeries struct { 240 Active bool `json:"active"` 241 Name string `json:"name"` 242 Version string `json:"version"` 243 } 244 245 var releases sync.Map 246 247 func mkDist(ver, name string) *claircore.Distribution { 248 v, _ := releases.LoadOrStore(ver, &claircore.Distribution{ 249 Name: "Ubuntu", 250 DID: "ubuntu", 251 VersionID: ver, 252 PrettyName: "Ubuntu " + ver, 253 VersionCodeName: name, 254 Version: fmt.Sprintf("%s (%s)", ver, strings.Title(name)), 255 }) 256 return v.(*claircore.Distribution) 257 } 258 259 func lookupDist(id string) *claircore.Distribution { 260 v, ok := releases.Load(id) 261 if !ok { 262 panic(fmt.Sprintf("programmer error: unknown key %q", id)) 263 } 264 return v.(*claircore.Distribution) 265 }