Skip to content

feat(correlation): paginate SBOM correlation results - #2811

Open
ctron wants to merge 19 commits into
guacsec:mainfrom
ctron:feature/correlation-pagination
Open

ctron wants to merge 19 commits into
guacsec:mainfrom
ctron:feature/correlation-pagination

Conversation

@ctron

@ctron ctron commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

SBOM correlation reads currently return all verdicts and unmatched components in one response. Add independent server pagination for verdicts, unmatched components, and remediations, with optional totals, zero-sized pages, configured maximum limits, and deterministic default ordering. Verdict pages select complete evidence groups before resolution; remediation pages preserve source provenance and legacy matching while scanning identities in bounded batches.

Update the correlation UI to page each collection independently and obtain SBOM-wide summaries from the count API. Document the query parameters, ordering, and client migration in OpenAPI and the correlation README.

API changes:

  • GET /api/v3/correlation/sbom/{id} returns items and nullable total alongside SBOM metadata and freshness. Defaults are offset=0, limit=25, and total=false.
  • Unmatched components move from include_unmatched to /sbom/{id}/unmatched; /sbom/{id}/remediations provides independently paginated remediation rows.
  • Unknown query parameters are ignored, following the usual paginated API pattern. Preview and identifier-query response shapes stay unchanged.

Validation:

  • Correlation module suite: 312 tests passed; the updated unknown-parameter expectation and added batch/count-only regression passed in targeted reruns.
  • API tests: 7 passed.
  • Backend and WebAssembly UI Clippy with warnings denied.
  • WebAssembly UI build, formatting, and diff checks.

Browser interaction was not tested.

Summary by Sourcery

Paginate SBOM correlation verdicts, unmatched components, and remediations independently while updating clients and documentation for the new API behavior.

New Features:

  • Add independently paginated API endpoints for SBOM verdicts, unmatched components, and remediation results with optional totals and count-only requests.
  • Update the correlation UI to load and paginate each result collection independently while using count APIs for SBOM-wide summaries.

Bug Fixes:

  • Preserve complete verdict evidence groups during pagination and resolve remediation status from full verdict evidence.
  • Ensure remediation results retain source provenance and avoid reconstructing matches for evidence without recorded sources.

Enhancements:

  • Define deterministic ordering, pagination limits, filtering behavior, and handling for unknown parameters across correlation collections.
  • Optimize paged correlation resolution and identifier loading by restricting work to selected component and verdict groups.

Documentation:

  • Document the new correlation pagination API, response shapes, ordering, migration guidance, and remediation behavior in the README and OpenAPI specification.

Tests:

  • Add coverage for pagination ordering, filtering, count-only requests, maximum limits, unmatched components, remediation deduplication, source provenance, and HTTP contracts.

Chores:

  • Increase the embedded database startup timeout for busy CI environments and standardize embedded database logging.

@sourcery-ai

sourcery-ai Bot commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Adds server-side pagination for all SBOM correlation collections with deterministic ordering, optional cached totals, bounded remediation traversal, and complete-evidence semantics, then migrates the correlation UI and documentation to consume the independently paginated API.

Sequence diagram for paginated verdict resolution

sequenceDiagram
    participant UI as Correlation UI
    participant API as Correlation API
    participant Service as CorrelationService
    participant DB as Database

    UI->>API: GET /api/v3/correlation/sbom/{id}?offset=0&limit=25&total=true
    API->>Service: verdict_page(sbom_id, request, paginated, cache, tx)
    Service->>DB: Select ordered evidence groups
    DB-->>Service: Complete node/vulnerability groups
    Service->>DB: Load evidence for selected groups
    DB-->>Service: Evidence rows
    Service->>Service: verdicts(sbom_id, request, evidence_rows, tx)
    Service-->>API: SbomCorrelationPage<VerdictSummary>
    API-->>UI: items, freshness, optional total
Loading

Sequence diagram for independently paginated correlation collections

sequenceDiagram
    participant UI as Correlation UI
    participant API as Correlation API
    participant Service as CorrelationService
    participant DB as Database

    par Verdicts
        UI->>API: fetch_correlation_page(sbom_id, '', offset, limit, total)
        API->>Service: verdict_page(...)
        Service-->>API: Verdict page
        API-->>UI: items and total
    and Unmatched components
        UI->>API: fetch_correlation_page(sbom_id, '/unmatched', offset, limit, total)
        API->>Service: unmatched_page(...)
        Service->>DB: Select unmatched nodes ordered by node_id
        DB-->>Service: Component page
        Service-->>API: Unmatched page
        API-->>UI: items and total
    and Remediations
        UI->>API: fetch_correlation_page(sbom_id, '/remediations', offset, limit, total)
        API->>Service: remediation_page(...)
        Service->>Service: remediation_keys(...)
        Service->>DB: Scan identity groups in bounded batches
        DB-->>Service: Remediation keys
        Service-->>API: Remediation page
        API-->>UI: items and total
    end
Loading

Flow diagram for complete-evidence verdict pagination

flowchart TD
    START[Paginated verdict request]
    GROUPS[Select complete evidence groups]
    PAGE[Apply offset and limit to groups]
    EVIDENCE[Load all evidence for selected groups]
    RESOLVE[Resolve verdicts with full group evidence]
    RESPONSE[Return verdict items and optional total]

    START --> GROUPS --> PAGE --> EVIDENCE --> RESOLVE --> RESPONSE
Loading

File-Level Changes

Change Details Files
Introduces independently paginated SBOM correlation API responses for verdicts, unmatched components, and remediations.
  • Adds a shared page envelope with SBOM metadata, freshness, items, and optional totals.
  • Adds dedicated unmatched and remediation endpoints while preserving preview and identifier-query shapes.
  • Supports defaults, zero-sized and count-only pages, configured maximum limits, ignored unknown parameters, filters, caching, and deterministic ordering.
  • Documents response shapes, ordering, pagination behavior, and client migration.
api/src/correlation.rs
modules/correlation/src/endpoints/mod.rs
modules/correlation/src/endpoints/test.rs
modules/correlation/README.md
Implements bounded, deterministic server-side pagination while preserving complete verdict evidence and remediation provenance.
  • Selects verdict pages by complete node/vulnerability evidence groups before resolution.
  • Adds unmatched-component queries that exclude the SBOM root and identify nodes with no evidence.
  • Enumerates remediation identities in bounded batches, deduplicates rows, preserves source assertion and legacy PURL matching, and resolves status from complete verdict groups.
  • Adds scoped extractor lookups to hydrate only selected nodes and regression coverage for ordering, filters, batching, count-only requests, and malformed deferred data.
modules/correlation/src/service/mod.rs
modules/correlation/src/service/pagination.rs
modules/correlation/src/service/pagination/tests.rs
modules/correlation/src/service/remediation/tests.rs
modules/correlation/src/extractor/mod.rs
modules/correlation/src/extractor/registry.rs
modules/correlation/src/extractor/cpe/mod.rs
modules/correlation/src/extractor/digest.rs
modules/correlation/src/extractor/product_identifier/mod.rs
modules/correlation/src/extractor/purl/mod.rs
Migrates the correlation UI to independently load and paginate each collection from the new API.
  • Adds generic collection fetching with per-list pagination, totals, loading/error states, cancellation, and page controls.
  • Loads verdicts, unmatched components, and remediations through separate endpoints and uses the verdict-count API for SBOM-wide status summaries.
  • Updates UI models and renderers to consume server-provided remediation rows and paged verdict envelopes.
ui/src/api.rs
ui/src/correlation.rs
ui/src/correlation/pagination.rs
ui/src/model.rs

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@ctron
ctron requested a review from a team October 27, 2026 09:27

@sourcery-ai sourcery-ai 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.

Hey - I've found 5 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="modules/correlation/src/service/pagination.rs" line_range="268-269" />
<code_context>
+            groups(sbom_id, request).limiting_as::<EvidenceGroup>(connection, paginated, cache)?;
+        let total = if paginated.total() {
+            let key = format!(
+                "correlation-remediations:{}",
+                groups(sbom_id, request).build(DbBackend::Postgres)
+            );
+            Some(
</code_context>
<issue_to_address>
**Totals use another query's count**

When total requests for different SBOMs or filters have the same query shape before the cache entry expires, `groups(sbom_id, request).build(DbBackend::Postgres)` keys `cached_total` by parameterized SQL without bound values, so requests share a cached count and callers receive another query's total.

Include the bound SBOM and filter values in the cache key, as the shared limiter does.
</issue_to_address>

### Comment 2
<location path="modules/correlation/src/endpoints/mod.rs" line_range="77" />
<code_context>
     ),
     responses(
         (status = 422, description = "Correlation capability disabled"),
-        (status = 200, description = "Correlation verdicts for the SBOM", body = CorrelationResult),
+        (status = 200, description = "Correlation verdicts for the SBOM", body = SbomCorrelationPage<VerdictSummary>),
+        (status = 400, description = "Invalid pagination or query parameters"),
</code_context>
<issue_to_address>
**Existing v3 clients break**

When an existing v3 client consumes the previous `CorrelationResult` response, clients calling the existing v3 endpoint and reading `verdicts` or `unmatched_components` receive neither field under the new response shape; the same route now returns `items` and requires a separate unmatched request, so existing integrations fail without a versioned migration path.

Preserve the old contract on v3 or introduce a versioned endpoint for the breaking response change.
</issue_to_address>

### Comment 3
<location path="modules/correlation/src/service/pagination.rs" line_range="208-211" />
<code_context>
+        }
+        loop {
+            let batch = groups(sbom_id, request)
+                .offset(group_offset)
+                .limit(100)
+                .into_model::<EvidenceGroup>()
+                .all(connection)
+                .await?;
+            if batch.is_empty() {
</code_context>
<issue_to_address>
**Remediation totals scan every group**

When a remediation-page request has no cached total and the SBOM has enough evidence groups for the full scan to exceed the request’s available time, on a cache miss, `remediation_keys` reads groups in batches until the entire matching set is exhausted to compute the total. The UI requests totals on every page, so the first remediation-page request for a large SBOM does full-set work before returning its small page and can exceed request time limits.

Avoid forcing an uncached full scan on the interactive path, for example by using a bounded/async total strategy or letting the UI request totals separately.

Also at `ui/src/correlation/pagination.rs:59`.
</issue_to_address>

### Comment 4
<location path="ui/src/correlation/pagination.rs" line_range="148" />
<code_context>
+                    return;
+                }
+                match response {
+                    Ok(data) => counts.set(data.into_iter().next()),
+                    Err(err) => error.set(Some(err.to_string())),
+                }
</code_context>
<issue_to_address>
**Successful retry keeps failure alert**

When a summary request fails and a later request succeeds, `CorrelationSummary` updates `counts` on success without clearing the prior `error`, and the render checks `error` first, so it keeps showing the failure alert instead of the counts.

Clear `error` when a summary request succeeds.

Also at `ui/src/correlation/pagination.rs:149`.
</issue_to_address>

### Comment 5
<location path="modules/correlation/src/endpoints/mod.rs" line_range="94" />
<code_context>
     responses(
         (status = 422, description = "Correlation capability disabled"),
-        (status = 200, description = "Correlation verdicts for the SBOM", body = CorrelationResult),
+        (status = 200, description = "Correlation verdicts for the SBOM", body = SbomCorrelationPage<VerdictSummary>),
+        (status = 400, description = "Invalid pagination or query parameters"),
         (status = 401, description = "The user was not authenticated"),
</code_context>
<issue_to_address>
**Published OpenAPI stays stale**

When a client or tool uses the checked-in OpenAPI document to generate or validate correlation requests, generated clients and spec consumers still see `CorrelationResult` with `verdicts` and `include_unmatched`, and cannot discover the new unmatched and remediation routes. They generate requests and parse responses against the old contract.

Regenerate and commit `openapi.yaml` from the updated endpoint annotations.
</issue_to_address>

Sourcery assessment

Approval pending. 5 findings to address first.

Blocking findings: modules/correlation/src/service/pagination.rs:269, modules/correlation/src/endpoints/mod.rs:77, modules/correlation/src/service/pagination.rs:211, ui/src/correlation/pagination.rs:148, modules/correlation/src/endpoints/mod.rs:94


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment thread modules/correlation/src/service/pagination.rs Outdated
Comment thread modules/correlation/src/endpoints/mod.rs
Comment thread modules/correlation/src/service/pagination.rs Outdated
Comment thread ui/src/correlation/pagination.rs Outdated
Comment thread modules/correlation/src/endpoints/mod.rs Outdated
@ctron
ctron force-pushed the feature/correlation-pagination branch from 3268205 to 6727dd7 Compare October 27, 2026 14:44

@rh-jfuller rh-jfuller 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.

looking at the code I believe remediation pagination grows quadratically (on large SBOMs this might be painful) but did not have time to check.

The SBOM GET response seems to drop freshness, sbom_name and sbom_id ... eg. the PaginatedResults only has items and total which I guess is in line with things but wanted to mention that I saw this change.

I believe removing deny_unknown_fields could make filter typos silently widen results. I understand why: Paginated and SbomCorrelationParams now both read the same query string. But the consequences could be bad.

lastly you might have missed that perf test no longer exercises unmatched components (tools/perf/users/v3/correlation.py:38). It still calls ?include_unmatched=true which is now moved to /unmatched.

otherwise LGTM

ctron added 7 commits October 28, 2026 13:28
…emediations

Signed-off-by: Jens Reimann <ctron@dentrassi.de>
Signed-off-by: Jens Reimann <ctron@dentrassi.de>
Assisted-by: Claude Code
Signed-off-by: Jens Reimann <ctron@dentrassi.de>
Assisted-by: Codex
Signed-off-by: Jens Reimann <ctron@dentrassi.de>
Assisted-by: Codex
Signed-off-by: Jens Reimann <ctron@dentrassi.de>
Assisted-by: Codex
…ode filters

Signed-off-by: Jens Reimann <ctron@dentrassi.de>
Assisted-by: Codex
Signed-off-by: Jens Reimann <ctron@dentrassi.de>
Assisted-by: Codex
@ctron
ctron force-pushed the feature/correlation-pagination branch from 1851d47 to aef8a2d Compare October 28, 2026 12:37
ctron and others added 12 commits October 28, 2026 14:15
Signed-off-by: Jens Reimann <ctron@dentrassi.de>
Assisted-by: Codex
Signed-off-by: Jens Reimann <ctron@dentrassi.de>
Assisted-by: Codex
Signed-off-by: Jens Reimann <ctron@dentrassi.de>
Assisted-by: Codex
Signed-off-by: Jens Reimann <ctron@dentrassi.de>
Assisted-by: Codex
Signed-off-by: Jens Reimann <ctron@dentrassi.de>
Assisted-by: Codex
Signed-off-by: Jens Reimann <ctron@dentrassi.de>
Assisted-by: Codex
Signed-off-by: Jens Reimann <ctron@dentrassi.de>
Assisted-by: Codex
The Cargo build script ran Trunk in the source tree, so its pre-build
hook ran `npm ci` in ui/node_modules. Concurrently with `trunk serve`,
or a build in another target directory, both installs removed each
other's files and failed with ENOTEMPTY.

The build script now stages the web inputs in OUT_DIR, installs only
the runtime npm dependencies there, and runs Trunk without the
development hook. Setting TRUSTIFY_UI_DIST to a pre-built distribution
skips npm and Trunk entirely.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Assisted-by: Claude Code
The processing overview was always rendered as plain paragraphs above
the SBOM list and verdicts, even with nothing outstanding, and printed
raw timestamps with nanoseconds and offset.

Summarize outstanding verdict, extraction, and refresh work in a single
inline alert, shown only while there is any, and format the processing
timestamps with the shared timestamp helper.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Assisted-by: Claude Code

This branch has not been deployed

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

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

2 participants