Skip to content

feat(correlation): evidence-based correlation engine - #2693

Merged
ctron merged 52 commits into
guacsec:mainfrom
ctron:feature/correlation_engine_2
Oct 26, 2026
Merged

ctron merged 52 commits into
guacsec:mainfrom
ctron:feature/correlation_engine_2

Conversation

@ctron

@ctron ctron commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

Depends on #2775 (utoipa 6 upgrade). Merge #2775 first.

Summary

An evidence-based correlation engine (ADR 00023): ingestion stores what documents state,
extractors turn those statements into evidence (component × vulnerability, with match
dimension, confidence and provenance), and verdicts are resolved from the evidence.
This covers stages 1–3 of the ADR roadmap, plus what real-world data required on top.

Correlation (modules/correlation)

  • correlation_evidence table, extraction worker, and verdict resolution (Fixed > NotAffected >
    Affected > UnderInvestigation).
  • Extractors:
    • Digest and product identifiers (SKU, model number, serial number);
    • PURL: version ranges per PURL type, CSAF vls ranges, RPM release streams (incl. RHEL
      mainline minors, EUS/AUS/E4S as separate products), SUSE codestreams;
    • CPE: version ranges and extended attributes.
  • Product context: evidence is scoped to the product a statement is made for (from describing
    CPEs, ancestors, and RPM PURLs), so statements of other products/streams don't leak.
  • Correlation options per publisher, e.g. versionless PURLs, or fixed_is_first_fixed (Red Hat
    states fixed only for the fixing build; enabled for Red Hat by default).
  • API: GET /v3/correlation/sbom/{id}, POST /v3/correlation/verdict-counts,
    GET /v3/correlation/query, GET /v3/correlation/advisory/{id}/statement,
    CRUD on /v3/correlation/option.

Ingestion

  • CSAF: all product statuses (incl. recommended, first_fixed, first_affected,
    last_affected, under_investigation), version ranges as stated, product context of
    statements, products defined outside of branches.
  • Red Hat's "fixed in X ⇒ affected before X" is no longer derived at ingestion (now a correlation
    rule). The fundamental SBOM, PURL and vulnerability APIs still report those "affected"
    statuses: as a bridge until they use the correlation verdicts, they read the views
    purl_status_effective / version_range_effective (m0002560), which add them at query time
    for publishers enabling fixed_is_first_fixed (nothing inferred is stored).
  • Compressed uploads from the UI and URLs.

UI (ui/, Yew/PatternFly)

SBOM list with verdict counts, correlation results with evidence and product context, advisory
viewer with stored statements, management of correlation options.

Other

  • Migrations m0002380 … m0002560. Advisories ingested before m0002510/m0002540 need to be
    re-ingested for product contexts and the additional CSAF statuses.
  • deny.toml exceptions for the correlation UI (wasm, not shipped with the server).
    Policy changes — please confirm:
  • The supply chain audit still fails on RUSTSEC-2024-0449 (unsafe-libyaml via serde_yaml_ng),
    which affects main as well and is left to be handled there.
  • Known gaps and follow-ups are tracked in TODO.md.

Review

This PR is large (~23k lines of code; ~118k lines are test data under etc/test-data). It could
be split into ingestion changes / correlation engine / UI if that helps reviewing.

Test plan

  • cargo test -p trustify-module-correlation: scenario tests (etc/test-data/scenarios, each
    run in both ingestion orders) against real advisories (Red Hat, SUSE, OSV, CVE, CERT@VDE), incl.
    S24, a syft SBOM of a UBI 10 image
  • cargo xtask precommit (includes the UI crate)
  • CI green

Try it out

  1. Start the database, from the repository root:
    podman compose -f etc/deploy/compose/compose.yaml up
  2. Start the server (with the embedded OIDC server), from the repository root:
    cargo run -p trustify-trustd -- api --devmode --embedded-oidc
  3. Start the UI, a Yew/PatternFly app in ui/ (needs rustup target add wasm32-unknown-unknown,
    cargo install trunk, and Node.js; see ui/README.md):
    cd ui
    npm ci
    trunk serve --port 8081
    and open http://localhost:8081.

🤖 Generated with Claude Code

@sourcery-ai

sourcery-ai Bot commented Sep 22, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

This PR establishes an evidence-first correlation foundation: it persists extractor-produced component/vulnerability links, resolves them into precedence-based verdicts, exposes those results through a new SBOM correlation endpoint, documents the architecture, and wires in initial service and endpoint tests.

Sequence diagram for SBOM correlation verdict retrieval

sequenceDiagram
    participant Client
    participant API as CorrelationEndpoint
    participant Service as CorrelationService
    participant DB as ReadOnlyDatabase

    Client->>API: GET /v3/correlation/sbom/{id}
    API->>DB: Query correlation_evidence for sbom_id
    DB-->>Service: Evidence rows
    Service->>DB: Load nodes, checksums, advisories, vulnerabilities
    DB-->>Service: Correlation metadata
    Service->>Service: correlate_sbom(sbom_id, connection)
    Service-->>API: CorrelationResult with verdicts and evidence
    API-->>Client: 200 JSON response
Loading

Entity relationship diagram for correlation evidence storage

erDiagram
    SBOM ||--o{ CORRELATION_EVIDENCE : contains
    ADVISORY ||--o{ CORRELATION_EVIDENCE : provides
    CORRELATION_EVIDENCE {
        uuid id PK
        uuid sbom_id FK
        text node_id
        uuid advisory_id FK
        text vulnerability_id
        assertion_status status
        match_dimension match_dimension
        float confidence
        text extractor
    }
Loading

File-Level Changes

Change Details Files
Defines the persisted evidence model and database schema for component-to-vulnerability correlation.
  • Adds the correlation evidence entity with assertion status and match-dimension enums.
  • Adds a migration creating the evidence table, PostgreSQL enums, indexes, foreign keys, and extractor-scoped uniqueness.
  • Records the correlation architecture and staged extractor roadmap in ADR 00022.
docs/adrs/00022-correlation-engine.md
entity/src/correlation_evidence.rs
entity/src/lib.rs
migration/src/lib.rs
migration/src/m0002380_create_correlation_evidence.rs
Introduces a correlation service that converts stored evidence into API-ready verdicts.
  • Loads evidence and related SBOM nodes, checksums, advisories, and vulnerabilities.
  • Groups evidence by component and vulnerability and resolves statuses using Fixed > NotAffected > Affected > UnderInvestigation precedence.
  • Returns evidence provenance and component/vulnerability metadata, with empty verdicts when no evidence exists.
modules/correlation/src/service/mod.rs
modules/correlation/src/model/mod.rs
modules/correlation/src/error.rs
Exposes the evidence-backed correlation result through the server API and OpenAPI contract.
  • Adds GET /v3/correlation/sbom/{id} and wires the correlation module into the server profile.
  • Defines serializable response models for verdicts, evidence, components, vulnerabilities, statuses, and match dimensions.
  • Updates the checked-in OpenAPI specification with the endpoint and schemas.
modules/correlation/src/endpoints/mod.rs
modules/correlation/src/endpoints/test.rs
modules/correlation/src/lib.rs
server/src/profile/api.rs
server/Cargo.toml
openapi.yaml
Adds initial integration coverage for empty and evidence-backed SBOM correlation flows.
  • Verifies service and endpoint behavior for an SBOM with no evidence.
  • Ingests sample SBOM/advisory data, inserts digest evidence, and verifies the resolved affected verdict and provenance.
modules/correlation/src/service/test.rs
modules/correlation/src/endpoints/test.rs
Registers the new correlation crate as a workspace and server dependency.
  • Adds the module to the workspace members and shared dependency declarations.
  • Adds the module's runtime and test dependencies.
Cargo.toml
Cargo.lock
modules/correlation/Cargo.toml

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 force-pushed the feature/correlation_engine_2 branch 3 times, most recently from 171fa8c to 4b41bde Compare October 7, 2026 08:03
@ctron
ctron force-pushed the feature/correlation_engine_2 branch from 1aef529 to dce6eb7 Compare October 12, 2026 12:26
@ctron ctron changed the title feat(correlation): evidence-based correlation engine foundation feat(correlation): evidence-based correlation engine Oct 13, 2026
@ctron
ctron force-pushed the feature/correlation_engine_2 branch from 1139747 to ffa813a Compare October 13, 2026 08:03
@ctron
ctron force-pushed the feature/correlation_engine_2 branch 3 times, most recently from f683eba to c6deadc Compare October 19, 2026 18:09
@ctron
ctron marked this pull request as ready for review October 20, 2026 11:15

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

Sorry, we are unable to review this pull request

The GitHub API does not allow us to fetch diffs exceeding 300 files, and this pull request has 418

@ctron
ctron force-pushed the feature/correlation_engine_2 branch 7 times, most recently from 5c3ce95 to 2654e41 Compare October 26, 2026 07:12
@@ -0,0 +1,3 @@
DROP TABLE remediation_product_identifier_assertion;

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.

there is now a m0002390_create_crypto_remediation

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in feadf62. The PR migrations are now numbered sequentially from m0002410 through m0002590, after upstream's crypto remediation and base PURL name index migrations. Updated the SQL include paths, tests, and documentation references as well. Formatting and compilation checks for the migration and correlation test targets passed.

@@ -0,0 +1,63 @@
use sea_orm_migration::prelude::*;

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.

there is now a m0002390_create_crypto_remediation

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in feadf62: the correlation evidence migration is now m0002410_create_correlation_evidence, and the remaining PR migrations follow sequentially through m0002590. Upstream's existing migration names are preserved.

#[post("/v3/upload/from-url")]
#[allow(clippy::too_many_arguments)]
/// Download a document from a URL and ingest it with auto-detected format.
pub async fn upload_document_from_url(

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.

I tihnk auth runs after download ... require_permission() is only called once format has been detected from fetched bytes so any authed user can make server issue requests

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Authentication already runs in the /api middleware before the handler, but you are right that the format-specific authorization check happened after downloading. Fixed in 2803f79 by adding Require before the handler runs. It requires at least one of create.sbom, create.advisory, create.weakness, or create.exploit before any outbound request. After downloading and detecting the format, we still require that format's specific permission before ingestion. Regression tests verify that anonymous users and users with no upload permission (including unrelated create permissions) trigger zero outbound requests, and that a different upload permission does not authorize ingestion. All 12 upload tests pass.

@@ -0,0 +1,75 @@
use migration::{

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.

I think this migration is 'one way' ...

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

I found all the sql to have no kind of injection paths ;) which is good ... I provided a few comments, otherwise this PR is good enough to go into main ... I would rather we get it in before it grows any bigger !

@ctron
ctron force-pushed the feature/correlation_engine_2 branch from 77983fb to 126612e Compare October 26, 2026 14:47
@ctron
ctron enabled auto-merge October 26, 2026 15:36
ctron added 2 commits October 26, 2026 16:39
…ion UI

Consolidates the following changes in their original order:

- feat(correlation): add correlation engine foundation with evidence table and verdict API
- docs(correlation): add README with API examples
- feat(correlation): add digest-based correlation extractor
- feat(correlation): add identifier query, product identifier ingestion, and URL-based advisory ingest
- refactor(correlation): unify query endpoint with verdict pipeline
- feat(correlation): show unmatched SBOM components and populate PURLs/CPEs
- fix(ui): use origin as OIDC redirect_uri for deep-link support
- feat(correlation): add product identifier extractor, document upload, and advisory UI
- feat(correlation): add matched_value to evidence and show all component types
- fix(correlation): adapt csaf hash/identifier extraction to csaf-rs 0.5 types
- chore(deps): switch garage-door from local path to 0.2.1
- chore(correlation): add the S18 and S19 scenario data
- refactor(correlation): merge unmatched component type columns into single Identifiers column
- refactor(correlation): isolate identifier types behind an Extractor trait
- refactor(correlation): use named result types for extractor matches
- chore: ignore trunk lock file and xtask SBOM output
- refactor(correlation): switch to usize
- chore(ui): include correlation UI in precommit clippy and check
- refactor(correlation): require explicit identifier kind for queries
- feat(ui): support uploading multiple files via drag and drop
- feat(ingestor): accept compressed documents from the UI and URLs

Assisted-by: Codex
…eams

Consolidates the following changes in their original order:

- feat(correlation): add CPE extractor with version range matching
- feat(correlation): show verdict counts in the SBOM list
- docs(correlation): document the matching approach of the extractors
- feat(correlation): implement PURL-based matching in PurlExtractor
- chore(deps): update garage-door to 0.3.0
- feat(correlation): scope RPM PURL matches by release stream
- refactor(correlation): derive Red Hat's implied affected ranges in correlation
- fix(cpe): drop a CPE language which isn't a language tag
- feat(csaf): ingest the "recommended" product status
- feat(correlation): keep one evidence record per status
- feat(correlation): match SUSE PURLs by codestream
- fix(csaf): store version ranges as stated
- feat(correlation): evaluate CSAF vls ranges by PURL type
- fix(csaf): resolve products defined outside of branches
- fix: replace deprecated fetch_update with try_update
- chore(ui): replace deprecated Sass @import with @use

Assisted-by: Codex
ctron and others added 21 commits October 26, 2026 16:39
The correlation details page already shows an alert when results are
outdated, so the per-SBOM label in the list is just noise.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
While a refresh covering the outdated results is queued or running,
there's nothing the user can do but wait. Show an info alert pointing
to the refresh progress instead of the warning asking for a refresh.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add an endpoint listing all factors a match confidence can be made of,
built from the same constants and rules the extractors use, and a UI
page explaining how they combine and how confidence ranks evidence.
Confidence breakdowns link to it, and it lives in a new "Documentation"
nav group, together with the API documentation link.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
List importers with their state, progress and last runs, enable,
run, delete and create them, and show an importer's run reports and
edit its configuration as JSON. Importers are listed under
"Administration", which is now shown without correlation as well.

Assisted-by: Claude Code
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Replace the switch and buttons in importer rows with a kebab menu in
the table's action cell, and show the same menu as an "Actions" toggle
in the importer page header, as PatternFly recommends. Confirm deleting
an importer with a new, shared confirmation dialog.

Assisted-by: Claude Code
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Assisted-by: Claude Code
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add GET /api/v4/importer, which accepts the common query and pagination
parameters and returns paginated results ordered by name, keeping pages
stable while importers run. Deprecate GET /api/v3/importer, which
returns all importers as an array, and use the new listing in the load
tests.

Assisted-by: Claude Code
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fetch pages of importers from the paginated listing, with the total.

Assisted-by: Claude Code
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Summarize an importer in compact cards, reusing those of the advisory
page, and add a "Last run" tab with the timing, results and messages of
its most recent run. Show the messages of runs as a paginated table.

Assisted-by: Claude Code
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Like the API command, --devmode creates the storage directory and
applies database migrations, so the importer can be started on its
own against a fresh database.

Assisted-by: Claude Code
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Without any processed item, the rate is zero and the estimated
remaining time infinite, which overflowed computing the estimated
completion and panicked when reading such an importer.

Assisted-by: Claude Code
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Expand past runs in the reports to review their details and messages,
like the most recent run, and summarize them by result and message
counts. Show the remaining time and estimated completion of running
importers.

Assisted-by: Claude Code
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Report how long each ingestion source, the correlation extraction and
the dump took, including the extraction throughput. Migrate the dump
generator from log to tracing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Assisted-by: Claude Code
Without a working directory, the database, storage and importer
checkouts went to /tmp, which may be a size-limited tmpfs. The working
directory can now also be set with GENERATE_DUMP_WORKING_DIR. Without
one, use a temporary directory in Cargo's target directory, and fall
back to the system's temporary directory only when not running through
Cargo. Keep the shared PostgreSQL installation.

Also limit the storage archive to the storage directory, instead of
the whole working directory.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Assisted-by: Claude Code
@ctron
ctron added this pull request to the merge queue Oct 26, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch Oct 26, 2026
@ctron
ctron force-pushed the feature/correlation_engine_2 branch from 2803f79 to 26020b4 Compare October 26, 2026 15:43
@ctron
ctron enabled auto-merge October 26, 2026 16:20
@ctron
ctron added this pull request to the merge queue Oct 26, 2026
Merged via the queue into guacsec:main with commit e537d55 Oct 26, 2026
10 checks passed
@ctron
ctron deleted the feature/correlation_engine_2 branch October 26, 2026 17:04
@github-project-automation github-project-automation Bot moved this to Done in Trustify Oct 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants