Skip to content

Commit 5e97caa

Browse files
authored
fix: flatten errors correctly for directory based CSAF providers (#76)
## What Flatten errors correctly for directory based CSAF poviders. Aligned error structure for directory based CSAF poviders. Previously `errors.FlattenError` did only support ROLIE feed based csaf providers as this feature was only targeted at restricted CSAF sources which are always ROLIE feed based. This was not clearly enough stated in the function doc comment which can easily lead to incorrect results when using this function with directory based CSAF providers. In this case `errors.FlattenError` was a no-op just returning the original error. ## Why We want to support fine grained error handling also for directory based CSAF providers and avoid traps for the caller of `errors.FlattenError`. The caller usually won't know if the CSAF provider is ROLIE or directory based, unless it is a restricted CSAF provider which rules out the latter case. Thus this would make it hard to rely on the output.
2 parents 9eaae1c + 118c845 commit 5e97caa

4 files changed

Lines changed: 58 additions & 37 deletions

File tree

cmd/csaf_downloader/downloader.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -492,11 +492,15 @@ func (dc *downloadContext) downloadAdvisory(
492492
switch {
493493
case resp.StatusCode == http.StatusUnauthorized:
494494
errorCh <- csafErrs.ErrInvalidCredentials{Message: fmt.Sprintf("invalid credentials to retrieve CSAF document %s at URL %s: %s", filename, file.URL(), resp.Status)}
495+
case resp.StatusCode == http.StatusForbidden:
496+
// if we have access to the feed containing the document, we also must have access to itself, otherwise this indicates a problem with the provider
497+
errorCh <- csafErrs.ErrCsafProviderIssue{Message: fmt.Sprintf("access denied to CSAF document %s at URL %s: %s", filename, file.URL(), resp.Status)}
495498
case resp.StatusCode == http.StatusNotFound:
496499
errorCh <- csafErrs.ErrCsafProviderIssue{Message: fmt.Sprintf("could not find CSAF document %s listed in table of content at URL %s: %s ", filename, file.URL(), resp.Status)}
497500
case resp.StatusCode >= 500:
498-
errorCh <- fmt.Errorf("could not retrieve CSAF document %s at URL %s: %s %w", filename, file.URL(), resp.Status, csafErrs.ErrRetryable) // mark as retryable error
499-
default:
501+
providerErr := csafErrs.ErrCsafProviderIssue{Message: fmt.Sprintf("could not retrieve CSAF document %s at URL %s: %s", filename, file.URL(), resp.Status)}
502+
errorCh <- fmt.Errorf("%w %w", providerErr, csafErrs.ErrRetryable) // mark error as retryable as failure for server side errors are often temporary
503+
default: // client error or fringe case
500504
errorCh <- fmt.Errorf("could not retrieve CSAF document %s at URL %s: %s", filename, file.URL(), resp.Status)
501505
}
502506
dc.stats.downloadFailed++

csaf/advisories.go

Lines changed: 36 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@ func (afp *AdvisoryFileProcessor) Process(
187187
dirURLs = []string{baseURL}
188188
}
189189

190+
feedErrs := []error{} // errors encountered while processing directories/feeds
190191
for _, base := range dirURLs {
191192
if base == "" {
192193
continue
@@ -195,13 +196,18 @@ func (afp *AdvisoryFileProcessor) Process(
195196
// Use changes.csv to be able to filter by age.
196197
files, err := afp.loadChanges(base, lg)
197198
if err != nil {
198-
return err
199+
feedErrs = append(feedErrs, err)
200+
continue
199201
}
200202
// XXX: Is treating as white okay? better look into the advisories?
201203
if err := fn(TLPLabelWhite, files); err != nil {
202-
return err
204+
feedErrs = append(feedErrs, err)
203205
}
204206
}
207+
208+
if len(feedErrs) > 0 {
209+
return &errs.CompositeErrFeed{Errs: feedErrs}
210+
}
205211
} // TODO: else scan directories?
206212
return nil
207213
}
@@ -214,23 +220,37 @@ func (afp *AdvisoryFileProcessor) loadChanges(
214220
) ([]AdvisoryFile, error) {
215221
base, err := url.Parse(baseURL)
216222
if err != nil {
217-
return nil, err
223+
return nil, errs.ErrCsafProviderIssue{Message: fmt.Sprintf("invalid directory url %s: %v", baseURL, err)}
218224
}
219225
changesURL := base.JoinPath("changes.csv").String()
220226

221227
resp, err := afp.client.Get(changesURL)
222228
if err != nil {
223-
return nil, err
229+
return nil, errs.ErrNetwork{Message: fmt.Sprintf("failed get request for url %s: %v", changesURL, err)}
224230
}
225231

226232
if resp.StatusCode != http.StatusOK {
227-
return nil, fmt.Errorf("fetching %s failed. Status code %d (%s)",
228-
changesURL, resp.StatusCode, resp.Status)
233+
switch { // we don't expect 401 and 403, as directory based feeds are supposed to be public, but just to be on the safe side
234+
case resp.StatusCode == http.StatusUnauthorized:
235+
return nil, errs.ErrInvalidCredentials{Message: fmt.Sprintf("invalid credentials for accessing %s: %s", changesURL, resp.Status)}
236+
case resp.StatusCode == http.StatusForbidden:
237+
return []AdvisoryFile{}, nil // user has insufficient permissions to access feed, no error
238+
case resp.StatusCode == http.StatusNotFound:
239+
return nil, errs.ErrCsafProviderIssue{Message: fmt.Sprintf("could not find changes.csv at %s: %s", changesURL, resp.Status)}
240+
case resp.StatusCode >= 500:
241+
providerErr := errs.ErrCsafProviderIssue{Message: fmt.Sprintf("could not retrieve changes.csv at %s: %s", changesURL, resp.Status)}
242+
return nil, fmt.Errorf("%w %w", providerErr, errs.ErrRetryable) // mark error as retryable as failure for server side errors are often temporary
243+
default: // client error or fringe case
244+
return nil, fmt.Errorf("could not retrieve changes.csv at %s: %s", changesURL, resp.Status)
245+
}
229246
}
230247

231248
defer resp.Body.Close()
232249
var files []AdvisoryFile
233250
c := csv.NewReader(resp.Body)
251+
// format specification:
252+
// https://docs.oasis-open.org/csaf/csaf/v2.0/os/csaf-v2.0-os.html#7113-requirement-13-changescsv
253+
c.FieldsPerRecord = 2
234254
const (
235255
pathColumn = 0
236256
timeColumn = 1
@@ -241,16 +261,12 @@ func (afp *AdvisoryFileProcessor) loadChanges(
241261
break
242262
}
243263
if err != nil {
244-
return nil, err
245-
}
246-
if len(r) < 2 {
247-
lg(slog.LevelError, "Not enough columns", "line", line)
248-
continue
264+
return nil, errs.ErrCsafProviderIssue{Message: fmt.Sprintf("could not read record from changes.csv: %v", err)}
249265
}
250266
t, err := time.Parse(time.RFC3339, r[timeColumn])
251267
if err != nil {
252268
lg(slog.LevelError, "Invalid time stamp in line", "url", changesURL, "line", line, "err", err)
253-
continue
269+
return nil, errs.ErrCsafProviderIssue{Message: fmt.Sprintf("could not read timestamp from changes.csv: %v", err)}
254270
}
255271
// Apply date range filtering.
256272
if afp.AgeAccept != nil && !afp.AgeAccept(t) {
@@ -259,7 +275,7 @@ func (afp *AdvisoryFileProcessor) loadChanges(
259275
path := r[pathColumn]
260276
if _, err := url.Parse(path); err != nil {
261277
lg(slog.LevelError, "Contains an invalid URL", "url", changesURL, "path", path, "line", line)
262-
continue
278+
return nil, errs.ErrCsafProviderIssue{Message: fmt.Sprintf("could not read url from changes.csv: %v", err)}
263279
}
264280

265281
files = append(files,
@@ -320,13 +336,14 @@ func (afp *AdvisoryFileProcessor) processROLIE(
320336
switch {
321337
case res.StatusCode == http.StatusUnauthorized:
322338
feedErrs = append(feedErrs, errs.ErrInvalidCredentials{Message: fmt.Sprintf("invalid credentials for TLP:%s ROLIE feed at %s: %s", label, feedURL.String(), res.Status)})
323-
case res.StatusCode == http.StatusNotFound:
324-
feedErrs = append(feedErrs, errs.ErrCsafProviderIssue{Message: fmt.Sprintf("could not find TLP:%s ROLIE feed at %s: %s", label, feedURL.String(), res.Status)})
325339
case res.StatusCode == http.StatusForbidden:
326340
// user has insufficient permissions to access feed, no error
327-
case res.StatusCode > 500:
328-
feedErrs = append(feedErrs, fmt.Errorf("could not retrieve TLP:%s ROLIE feed at %s: %s %w", label, feedURL.String(), res.Status, errs.ErrRetryable)) // mark error as retryable
329-
default:
341+
case res.StatusCode == http.StatusNotFound:
342+
feedErrs = append(feedErrs, errs.ErrCsafProviderIssue{Message: fmt.Sprintf("could not find TLP:%s ROLIE feed at %s: %s", label, feedURL.String(), res.Status)})
343+
case res.StatusCode >= 500:
344+
providerErr := errs.ErrCsafProviderIssue{Message: fmt.Sprintf("could not retrieve TLP:%s ROLIE feed at %s: %s", label, feedURL.String(), res.Status)}
345+
feedErrs = append(feedErrs, fmt.Errorf("%w %w", providerErr, errs.ErrRetryable)) // mark error as retryable as failure for server side errors are often temporary
346+
default: // client error or fringe case
330347
feedErrs = append(feedErrs, fmt.Errorf("could not retrieve TLP:%s ROLIE feed at %s: %s", label, feedURL.String(), res.Status))
331348
}
332349
continue
@@ -427,7 +444,7 @@ func (afp *AdvisoryFileProcessor) processROLIE(
427444
}
428445
}
429446
if len(feedErrs) > 0 {
430-
return &errs.CompositeErrRolieFeed{Errs: feedErrs}
447+
return &errs.CompositeErrFeed{Errs: feedErrs}
431448
}
432449
return nil
433450
}

pkg/errs/errors.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -47,14 +47,14 @@ func (e ErrInvalidCredentials) Error() string {
4747

4848
var ErrRetryable = errors.New("(retryable error)")
4949

50-
// CompositeErrRolieFeed holds an array of errors which encountered during processing rolie feeds
51-
type CompositeErrRolieFeed struct {
50+
// CompositeErrFeed holds an array of errors which encountered during processing rolie feeds
51+
type CompositeErrFeed struct {
5252
Errs []error
5353
}
5454

55-
func (e *CompositeErrRolieFeed) Error() string {
55+
func (e *CompositeErrFeed) Error() string {
5656
if len(e.Errs) == 0 {
57-
return "empty CompositeErrRolieFeed"
57+
return "empty CompositeErrFeed"
5858
}
5959

6060
messages := make([]string, 0, len(e.Errs))
@@ -64,7 +64,7 @@ func (e *CompositeErrRolieFeed) Error() string {
6464
return strings.Join(messages, "\n")
6565
}
6666

67-
func (e *CompositeErrRolieFeed) Unwrap() []error {
67+
func (e *CompositeErrFeed) Unwrap() []error {
6868
return e.Errs
6969
}
7070

@@ -89,10 +89,10 @@ func (e *CompositeErrCsafDownload) Unwrap() []error {
8989
return e.Errs
9090
}
9191

92-
// FlattenError flattens out all composite errors (note: discards the errors wrapped around [CompositeErrRolieFeed] or [CompositeErrCsafDownload])
93-
// The assumed structure is CompositeErrRolieFeed{Errs: []error{...,CompositeErrCsafDownload,...,CompositeErrCsafDownload,...}}.
92+
// FlattenError flattens out all composite errors (note: discards the errors wrapped around [CompositeErrFeed] or [CompositeErrCsafDownload])
93+
// The assumed structure is CompositeErrFeed{Errs: []error{...,CompositeErrCsafDownload,...,CompositeErrCsafDownload,...}}.
9494
func FlattenError(err error) (flattenedErrors []error) {
95-
var rolieErrs *CompositeErrRolieFeed
95+
var rolieErrs *CompositeErrFeed
9696
if errors.As(err, &rolieErrs) {
9797
for _, rolieErr := range rolieErrs.Unwrap() {
9898
var csafDlErrs *CompositeErrCsafDownload

pkg/errs/errors_test.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,21 +26,21 @@ func TestFlattenError(t *testing.T) {
2626

2727
compositeErrCsafDownload := &CompositeErrCsafDownload{Errs: csafDownloadErrsFlat}
2828

29-
singleRolieFeedErrs := []error{
30-
errors.New("single error rolie feed 1"),
31-
errors.New("single error rolie feed 2"),
29+
singleFeedErrs := []error{
30+
errors.New("single error feed 1"),
31+
errors.New("single error feed 2"),
3232
}
3333

34-
rolieFeedCompositeErr := CompositeErrRolieFeed{
34+
feedCompositeErr := CompositeErrFeed{
3535
Errs: append(
36-
singleRolieFeedErrs,
37-
fmt.Errorf("issues during downloader of rolie: %w", compositeErrCsafDownload),
36+
singleFeedErrs,
37+
fmt.Errorf("issues during download of feed: %w", compositeErrCsafDownload),
3838
compositeErrCsafDownload,
3939
),
4040
}
41-
wantFlattenedErrors := slices.Concat(singleRolieFeedErrs, csafDownloadErrsFlat, csafDownloadErrsFlat)
41+
wantFlattenedErrors := slices.Concat(singleFeedErrs, csafDownloadErrsFlat, csafDownloadErrsFlat)
4242

43-
gotFlattenedErrors := FlattenError(fmt.Errorf("wrap rolie feed composite err: %w", &rolieFeedCompositeErr))
43+
gotFlattenedErrors := FlattenError(fmt.Errorf("wrap feed composite err: %w", &feedCompositeErr))
4444

4545
assert.ElementsMatch(t, wantFlattenedErrors, gotFlattenedErrors)
4646
})

0 commit comments

Comments
 (0)