Skip to content

chore(scanner): Add retries to Scanner V4 CSAF enricher#21885

Draft
dcaravel wants to merge 1 commit into
masterfrom
dc/csaf-enricher-resil
Draft

chore(scanner): Add retries to Scanner V4 CSAF enricher#21885
dcaravel wants to merge 1 commit into
masterfrom
dc/csaf-enricher-resil

Conversation

@dcaravel

@dcaravel dcaravel commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Description

Adds retries when processing entries from changes.csv in the CSAF enricher. Recent vuln bundle generation has been failing (example)

Also logs the URI of the failing request to ease troubleshooting.

User-facing documentation

Testing and quality

  • the change is production ready: the change is GA, or otherwise the functionality is gated by a feature flag
  • CI results are inspected

Automated testing

  • added unit tests
  • added e2e tests
  • added regression tests
  • added compatibility tests
  • modified existing tests

How I validated my change

change me!

@dcaravel dcaravel added the pr-update-scanner-vulns Update Scanner's vulnerability data for this PR label Jul 22, 2026
@openshift-ci

openshift-ci Bot commented Jul 22, 2026

Copy link
Copy Markdown

Skipping CI for Draft Pull Request.
If you want CI signal for your change, please convert it to an actual PR.
You can still manually trigger a test run with /test all

@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Central YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Enterprise

Run ID: dea6008f-7ce9-4709-870d-db5d5c1454b4

📥 Commits

Reviewing files that changed from the base of the PR and between dd58bb7 and 5252ce2.

📒 Files selected for processing (2)
  • scanner/enricher/csaf/fetcher.go
  • scanner/enricher/csaf/fetcher_test.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • scanner/enricher/csaf/fetcher.go
  • scanner/enricher/csaf/fetcher_test.go

📝 Walkthrough

Summary by CodeRabbit

  • Bug Fixes
    • Improved reliability when retrieving security advisories by retrying temporary failures.
    • Unavailable or invalid advisories are now skipped without interrupting processing of other advisories.
    • Prevented unsuccessful advisory requests from being marked as changed or included in output.

Walkthrough

CSAF enrichment adds retried advisory fetching with exponential backoff. processChanges skips unavailable advisories while continuing to process remaining entries, and marks advisories changed only after successful output. Tests cover retries, persistent failures, and mixed availability.

Changes

CSAF advisory fetching

Layer / File(s) Summary
Retried advisory fetch helper
scanner/enricher/csaf/fetcher.go, scanner/enricher/csaf/fetcher_test.go
Adds configurable retry behavior, response validation, compact JSON output, and tests for transient and persistent request failures.
Per-advisory processing integration
scanner/enricher/csaf/fetcher.go, scanner/enricher/csaf/fetcher_test.go
Routes advisory retrieval through the helper, skips unavailable advisories, and updates change tracking only for successful fetches.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant processChanges
  participant fetchAdvisory
  participant AdvisoryEndpoint as Advisory HTTP endpoint
  participant Output as Advisory output writer

  processChanges->>fetchAdvisory: Fetch advisory
  fetchAdvisory->>AdvisoryEndpoint: GET advisory
  AdvisoryEndpoint-->>fetchAdvisory: Success or error response
  fetchAdvisory->>AdvisoryEndpoint: Retry failed request with backoff
  AdvisoryEndpoint-->>fetchAdvisory: Advisory JSON
  fetchAdvisory->>Output: Write compacted advisory
  fetchAdvisory-->>processChanges: Success or fetch error
  processChanges->>processChanges: Mark successful advisory changed
Loading
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description covers the change, but the validation section is still a placeholder and testing details are not filled in. Replace the placeholders with a brief validation summary and note the added tests or why any checklist items are unchecked.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the main change: adding retries to the Scanner V4 CSAF enricher.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dc/csaf-enricher-resil

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scanner/enricher/csaf/fetcher.go`:
- Around line 386-395: Handle and propagate the error returned by w.Write in the
fetch response-body flow instead of discarding it. Update the surrounding
function to return a descriptive wrapped error when writing bc.Bytes() fails,
ensuring fetchAdvisory/processChanges do not treat an unsuccessful write as a
successful fetch.
- Around line 397-407: Update the retry flow around BetweenAttempts and
checkResponse so advisory retries stop promptly when ctx is canceled and do not
retry permanent 4xx responses. Replace the blocking backoff sleep with
context-aware waiting using ctx.Done(), and classify non-retryable 4xx errors so
retry.WithRetry exits immediately while preserving retries for transient
failures.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Central YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Enterprise

Run ID: adef1e7f-2914-4913-a5df-48eb87585779

📥 Commits

Reviewing files that changed from the base of the PR and between ca53a90 and dd58bb7.

📒 Files selected for processing (2)
  • scanner/enricher/csaf/fetcher.go
  • scanner/enricher/csaf/fetcher_test.go

Comment thread scanner/enricher/csaf/fetcher.go
Comment on lines +397 to +407
return retry.WithRetry(fetch,
retry.WithContext(ctx),
retry.Tries(advisoryFetchRetries),
retry.BetweenAttempts(func(attempt int) {
time.Sleep(advisoryFetchRetryBaseDelay * time.Duration(1<<attempt))
}),
retry.OnFailedAttempts(func(err error) {
slog.WarnContext(ctx, "advisory fetch attempt failed",
"advisory", advisoryURI, "reason", err)
}),
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect retry package internals for ctx-cancellation handling and early-termination support.
fd retry.go --exec cat -n {}

Repository: stackrox/stackrox

Length of output: 16230


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate fetcher.go =="
fd '^fetcher\.go$' scanner/enricher/csaf || true

echo "== targeted fetcher section =="
cat -n scanner/enricher/csaf/fetcher.go | sed -n '360,425p'

echo "== search checkResponse and related symbols =="
rg -n "func checkResponse|checkResponse\\(|advisoryFetchRetries|advisoryFetchRetryBaseDelay|compressedFileTimeout|processChanges|context\\.With" scanner/enricher/csaf -S

echo "== deterministic retry behavior model =="
python3 - <<'PY'
from dataclasses import dataclass
from typing import Callable, Optional

`@dataclass`
class Event:
    t: float
    kind: str

def simulate(ctx_canceled_at: Optional[float], errors: list, base=0.5, cap=0):
    attempts = max(1, 5)  # Tries(5)
    sleeps = []
    events = []
    canceled = False
    t = 0.0
    for attempt in range(attempts):
        if not canceled and ctx_canceled_at is not None and t >= ctx_canceled_at:
            canceled = True
        if canceled:
            return events + [Event(t, "ctx_err")]

        err = errors[attempt] if attempt < len(errors) else None
        if err is None:
            return events + [Event(t, "success")] if attempt == len(errors) else events + [Event(t, "success")]
        if err == "4xx":
            return events + [Event(t, "4xx_no_retry")]

        delta = cap if cap else base * (1 << attempt)
        attempts_remaining = attempts - attempt
        # retry loop increments i and only does attempts_remaining sleeps for a Tries(5) error-only case.
        sleeps_delta = base * ((1 << (attempts - 1)) - (1 << attempt))
        while delta > t or delta >= t:
            delta += 0.01
        # exact backoff not precise
        sleep_amt = base * ((1 << attempts_remaining) - (1 << attempt)) # approximate count from source does (2+4+8+16=30) as comment.
        # use actual formula per fixed.
PY

Repository: stackrox/stackrox

Length of output: 4954


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== processChanges context/deadline and advisory fetch call =="
cat -n scanner/enricher/csaf/fetcher.go | sed -n '100,345p'

echo "== retry package exact behavior probe =="
python3 - <<'PY'
# Mirrors pkg/retry/retry.go: Tries(5), base sleeps in BetweenAttempts, no explicit Backoff option.
def sleep_total_before_failure(errors):
    total_sleep = 0.0
    attempt_sleeps = {}
    for i, err in enumerate(errors):
        if err is None:
            return False, total_sleep, i, attempt_sleeps
        if err == "4xx":
            return False, total_sleep, i, attempt_sleeps
        # between(i) runs before backoff only on retryable/error path; same retry library without backoff option
        # only executes BetweenAttempts callback, so current implementation sleeps.
        sleep = 0.5 * (1<<i)
        attempt_sleeps[i] = sleep
        total_sleep += sleep
    return True, total_sleep, len(errors), attempt_sleeps

# non-retryable (4xx) errors
for n in range(5):
    print("4xx after attempts", n, "total_sleep", sleep_total_before_failure([4]*n + [404])[1])
print()
# transient-only errors returning 5 failures
ok, total_sleep, final_attempt, sleeps = sleep_total_before_failure([503]*5)
print("retries exhausted", ok, "total_sleep", total_sleep, "attempts", final_attempt, "sleeps", sleeps)
PY

Repository: stackrox/stackrox

Length of output: 9107


Avoid blocking advisory retries on context cancellation and on permanent 4xx responses.

BetweenAttempts runs time.Sleep(...) synchronously, so each retry backoff can block until it returns even after ctx is canceled; use a select with time.After and ctx.Done() so cancellation takes effect during backoff. Also, checkResponse returns plain errors for non-OK states, including 4xx, so permanently-missing advisories still consume the full retry/backoff budget in processChanges, which has no deadline of its own while later compressed fetching uses compressedFileTimeout. Short-circuit non-retryable 4xx responses, or add an explicit timeout/deadline around the changes/advisory retry phase.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scanner/enricher/csaf/fetcher.go` around lines 397 - 407, Update the retry
flow around BetweenAttempts and checkResponse so advisory retries stop promptly
when ctx is canceled and do not retry permanent 4xx responses. Replace the
blocking backoff sleep with context-aware waiting using ctx.Done(), and classify
non-retryable 4xx errors so retry.WithRetry exits immediately while preserving
retries for transient failures.

@github-actions

github-actions Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

🚀 Build Images Ready

Images are ready for commit 5252ce2. To use with deploy scripts:

export MAIN_IMAGE_TAG=4.12.x-565-g5252ce2df6

@dcaravel
dcaravel force-pushed the dc/csaf-enricher-resil branch from dd58bb7 to 5252ce2 Compare July 22, 2026 19:49
@codecov

codecov Bot commented Jul 22, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 72.50000% with 11 lines in your changes missing coverage. Please review.
✅ Project coverage is 51.35%. Comparing base (25e02cc) to head (5252ce2).
⚠️ Report is 2 commits behind head on master.

Files with missing lines Patch % Lines
scanner/enricher/csaf/fetcher.go 72.50% 6 Missing and 5 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master   #21885      +/-   ##
==========================================
- Coverage   51.38%   51.35%   -0.03%     
==========================================
  Files        2856     2857       +1     
  Lines      178637   178806     +169     
==========================================
+ Hits        91794    91828      +34     
- Misses      78832    78947     +115     
- Partials     8011     8031      +20     
Flag Coverage Δ
go-unit-tests 51.35% <72.50%> (-0.03%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant