@@ -22,13 +22,17 @@ import (
2222 "github.com/klauspost/compress/snappy"
2323 "github.com/quay/claircore/libvuln/driver"
2424 "github.com/quay/claircore/pkg/tmp"
25+ "github.com/stackrox/rox/pkg/retry"
2526 "github.com/stackrox/rox/pkg/utils"
2627 "github.com/stackrox/rox/scanner/enricher/csaf/internal/zreader"
2728)
2829
2930var (
3031 // compressedFileTimeout matches Claircore's VEX https://github.com/quay/claircore/blob/v1.5.34/rhel/vex/fetcher.go.
3132 compressedFileTimeout = 2 * time .Minute
33+
34+ advisoryFetchRetries = 5
35+ advisoryFetchRetryBaseDelay = 500 * time .Millisecond
3236)
3337
3438// fingerprint is used to track the state of the changes.csv and deletions.csv endpoints.
@@ -339,49 +343,22 @@ func (e *Enricher) processChanges(ctx context.Context, w io.Writer, fp *fingerpr
339343 continue
340344 }
341345
342- changed [path .Base (cvePath )] = true
343-
344346 advisoryURI , err := e .base .Parse (cvePath )
345347 if err != nil {
346348 return err
347349 }
348- req , err := http .NewRequestWithContext (ctx , http .MethodGet , advisoryURI .String (), nil )
349- if err != nil {
350- return fmt .Errorf ("error creating advisory request %w" , err )
351- }
352-
353- // Use a func here as we're in a loop and want to make sure the
354- // body is closed in all events.
355- err = func () error {
356- res , err := e .c .Do (req )
357- if err != nil {
358- return fmt .Errorf ("error making advisory request %w" , err )
359- }
360- defer utils .IgnoreError (res .Body .Close )
361- err = checkResponse (res , http .StatusOK )
362- if err != nil {
363- return fmt .Errorf ("unexpected response: %w" , err )
364- }
365350
366- // Add compacted JSON to buffer.
367- _ , err = buf .ReadFrom (res .Body )
368- if err != nil {
369- return fmt .Errorf ("error reading from buffer: %w" , err )
370- }
371- slog .DebugContext (ctx , "copying body to file" , "url" , advisoryURI .String ())
372- err = json .Compact (& bc , buf .Bytes ())
373- if err != nil {
374- return fmt .Errorf ("error compressing JSON: %w" , err )
375- }
376-
377- bc .WriteByte ('\n' )
378- _ , _ = w .Write (bc .Bytes ())
379- l ++
380- return nil
381- }()
382- if ! errors .Is (err , nil ) {
383- return err
351+ // Fetch individual advisory with retries. If the advisory is
352+ // unavailable after all attempts, skip it — the compressed
353+ // archive (downloaded next) will likely container a version of it.
354+ advisoryErr := e .fetchAdvisory (ctx , advisoryURI , & buf , & bc , w )
355+ if advisoryErr != nil {
356+ slog .WarnContext (ctx , "skipping advisory due to fetch failure; will fall back to archive" ,
357+ "advisory" , cvePath , "reason" , advisoryErr )
358+ continue
384359 }
360+ changed [path .Base (cvePath )] = true
361+ l ++
385362 }
386363
387364 if ! errors .Is (err , io .EOF ) {
@@ -390,6 +367,46 @@ func (e *Enricher) processChanges(ctx context.Context, w io.Writer, fp *fingerpr
390367 return nil
391368}
392369
370+ func (e * Enricher ) fetchAdvisory (ctx context.Context , advisoryURI * url.URL , buf , bc * bytes.Buffer , w io.Writer ) error {
371+ fetch := func () error {
372+ buf .Reset ()
373+ bc .Reset ()
374+ req , err := http .NewRequestWithContext (ctx , http .MethodGet , advisoryURI .String (), nil )
375+ if err != nil {
376+ return err
377+ }
378+ res , err := e .c .Do (req )
379+ if err != nil {
380+ return err
381+ }
382+ defer utils .IgnoreError (res .Body .Close )
383+ if err := checkResponse (res , http .StatusOK ); err != nil {
384+ return err
385+ }
386+ if _ , err := buf .ReadFrom (res .Body ); err != nil {
387+ return fmt .Errorf ("error reading response body: %w" , err )
388+ }
389+ if err := json .Compact (bc , buf .Bytes ()); err != nil {
390+ return fmt .Errorf ("error compacting JSON: %w" , err )
391+ }
392+ bc .WriteByte ('\n' )
393+ _ , _ = w .Write (bc .Bytes ())
394+ return nil
395+ }
396+
397+ return retry .WithRetry (fetch ,
398+ retry .WithContext (ctx ),
399+ retry .Tries (advisoryFetchRetries ),
400+ retry .BetweenAttempts (func (attempt int ) {
401+ time .Sleep (advisoryFetchRetryBaseDelay * time .Duration (1 << attempt ))
402+ }),
403+ retry .OnFailedAttempts (func (err error ) {
404+ slog .WarnContext (ctx , "advisory fetch attempt failed" ,
405+ "advisory" , advisoryURI , "reason" , err )
406+ }),
407+ )
408+ }
409+
393410// checkResponse takes a http.Response and a variadic of ints representing
394411// acceptable http status codes. The error returned will attempt to include
395412// some content from the server's response.
0 commit comments