Skip to content

feat: CSAF remediation support in api/v3/purl/recommend and api/v2/vulnerability/analyze endpoints#2234

Open
Strum355 wants to merge 3 commits intoguacsec:mainfrom
Strum355:nsc/csaf-remediation
Open

feat: CSAF remediation support in api/v3/purl/recommend and api/v2/vulnerability/analyze endpoints#2234
Strum355 wants to merge 3 commits intoguacsec:mainfrom
Strum355:nsc/csaf-remediation

Conversation

@Strum355
Copy link
Member

@Strum355 Strum355 commented Feb 5, 2026

Summary by Sourcery

Add CSAF remediation ingestion, persistence, and exposure across APIs, enriching vulnerability and purl analysis with remediation metadata.

New Features:

  • Ingest and store CSAF remediation entries linked to advisory/vulnerability, product statuses, and purl statuses.
  • Expose remediation information alongside purl status analysis in v3 vulnerability analysis and purl recommendation APIs.
  • Introduce remediation entities, enums, and summaries, including OpenAPI schema definitions for remediation categories and data returned to clients.

Enhancements:

  • Extend CSAF loader and status creation to build mappings between products, purls, and associated remediation data.
  • Augment versioned purl and vulnerability analysis models to carry remediation summaries per status, preserving existing status behavior.

Tests:

  • Add integration tests validating CSAF remediation ingestion, linkage to product and purl statuses, and availability via analysis endpoints.

@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Feb 5, 2026

Reviewer's Guide

Adds CSAF remediation ingestion and exposure across the data model and APIs, including new remediation entities, migrations, and wiring them into CSAF loading, PURL analysis (v2/v3), and OpenAPI schemas.

Sequence diagram for CSAF remediation ingestion flow

sequenceDiagram
    actor UpstreamProvider
    participant CsafLoader
    participant StatusCreator
    participant RemediationCreator
    participant DB

    UpstreamProvider->>CsafLoader: load(csaf_document)
    CsafLoader->>StatusCreator: new(advisory_id, vulnerability_id)
    CsafLoader->>StatusCreator: add_all(product_status.*)
    CsafLoader->>StatusCreator: create(graph, connection)
    activate StatusCreator
    StatusCreator->>DB: insert organizations, products, product_status, purl_status
    StatusCreator-->>CsafLoader: HashMap~product_id, ProductIdStatusMapping~
    deactivate StatusCreator

    alt vulnerability_has_remediations
        CsafLoader->>RemediationCreator: new(advisory_id, vulnerability_id, product_id_mapping)
        loop remediations
            CsafLoader->>RemediationCreator: add(remediation)
        end
        RemediationCreator->>DB: insert remediation rows
        RemediationCreator->>DB: insert remediation_purl_status rows
        RemediationCreator->>DB: insert remediation_product_status rows
        RemediationCreator-->>CsafLoader: Ok
    end

    CsafLoader-->>UpstreamProvider: load result
Loading

ER diagram for new remediation tables and relationships

erDiagram
    remediation {
        uuid id PK
        uuid advisory_id
        string vulnerability_id
        remediation_category category
        string details
        string url
        jsonb data
    }

    remediation_purl_status {
        uuid remediation_id PK, FK
        uuid purl_status_id PK, FK
    }

    remediation_product_status {
        uuid remediation_id PK, FK
        uuid product_status_id PK, FK
    }

    advisory_vulnerability {
        uuid advisory_id PK, FK
        string vulnerability_id PK, FK
    }

    purl_status {
        uuid id PK
        uuid base_purl_id
        uuid vulnerability_id
        uuid status_id
        uuid version_range_id
    }

    product_status {
        uuid id PK
        uuid advisory_id
        string vulnerability_id
        uuid product_version_range_id
    }

    remediation ||--o{ remediation_purl_status : links
    remediation ||--o{ remediation_product_status : links

    remediation_purl_status }o--|| purl_status : targets
    remediation_product_status }o--|| product_status : targets

    advisory_vulnerability ||--o{ remediation : owns
    purl_status }o--|| advisory_vulnerability : via_vulnerability
    product_status }o--|| advisory_vulnerability : via_vulnerability
Loading

Class diagram for new remediation domain model and API exposure

classDiagram
    class RemediationCategory {
        <<enumeration>>
        +VendorFix
        +Workaround
        +Mitigation
        +NoFixPlanned
        +NoneAvailable
        +WillNotFix
    }

    class RemediationModel {
        +Uuid id
        +Uuid advisory_id
        +String vulnerability_id
        +RemediationCategory category
        +Option~String~ details
        +Option~String~ url
        +serde_json::Value data
    }

    class RemediationPurlStatusModel {
        +Uuid remediation_id
        +Uuid purl_status_id
    }

    class RemediationProductStatusModel {
        +Uuid remediation_id
        +Uuid product_status_id
    }

    class RemediationSummary {
        +Uuid id
        +RemediationCategory category
        +Option~String~ details
        +Option~String~ url
        +serde_json::Value data
        +from_entities(remediations: &[remediation::Model]) RemediationSummary[]
    }

    class VersionedPurlStatus {
        +VulnerabilityHead vulnerability
        +String status
        +Vec~RemediationSummary~ remediations
        +from_entity(vuln: vulnerability::Model, status_model: Option~status::Model~, remediations: &[remediation::Model], tx: ConnectionTrait) Result~VersionedPurlStatus, Error~
    }

    class AnalysisPurlStatus {
        +PurlStatus purl_status
        +Vec~RemediationSummary~ remediations
    }

    class AnalysisDetailsV3 {
        +VulnerabilityHead head
        +Vec~AnalysisPurlStatus~ purl_statuses
    }

    class VulnerabilityStatus {
        +String id
        +Option~VexStatus~ status
        +Option~VexJustification~ justification
        +Vec~RemediationSummary~ remediations
    }

    class PurlStatus {
    }

    class ProductStatusModel {
    }

    class AdvisoryVulnerabilityModel {
        +Uuid advisory_id
        +String vulnerability_id
    }

    RemediationModel --> RemediationCategory
    RemediationModel --> AdvisoryVulnerabilityModel : belongs_to
    RemediationPurlStatusModel --> RemediationModel : many_to_one
    RemediationPurlStatusModel --> PurlStatus : many_to_one
    RemediationProductStatusModel --> RemediationModel : many_to_one
    RemediationProductStatusModel --> ProductStatusModel : many_to_one

    RemediationSummary ..> RemediationModel : from_entities
    VersionedPurlStatus --> RemediationSummary : uses
    AnalysisPurlStatus --> RemediationSummary : has
    AnalysisDetailsV3 --> AnalysisPurlStatus : has
    VulnerabilityStatus --> RemediationSummary : has
Loading

File-Level Changes

Change Details Files
Track product/purl status IDs during CSAF ingestion to support remediation linking and introduce a RemediationCreator to persist CSAF remediations and their associations.
  • Extend StatusCreator to map CSAF product_ids to ProductStatus and PurlStatus UUIDs, returning this mapping from create() instead of just ().
  • Introduce ProductIdStatusMapping struct plus internal maps from product_id→product and product→purl_statuses to later resolve remediation scopes.
  • Add RemediationCreator that deterministically UUIDs remediations, stores them in remediation table, and bulk‑inserts join records into remediation_purl_status and remediation_product_status tables.
  • Wire RemediationCreator into CsafLoader.ingest_product_statuses so CSAF vulnerability.remediations are saved and linked to the appropriate product/purl statuses after product_status creation.
  • Add CSAF loader tests to assert remediations and join rows are correctly created and linked for both product-only and purl-based CSF documents.
modules/ingestor/src/service/advisory/csaf/creator.rs
modules/ingestor/src/service/advisory/csaf/loader.rs
Add remediation persistence layer (tables, entities, relations) and expose them through SeaORM relations.
  • Create migration m0001210_csaf_remediations to define remediation table, enum remediation_category, and join tables remediation_purl_status and remediation_product_status with indexes and cascading FKs.
  • Add new entity modules remediation, remediation_product_status, and remediation_purl_status with appropriate SeaORM relations to advisory, vulnerability, purl_status, and product_status.
  • Extend purl_status entity with Relatedremediation::Entity via remediation_purl_status join.
  • Register new remediation modules in entity::lib and add csaf crate dependency for category mapping.
migration/src/m0001210_csaf_remediations.rs
entity/src/remediation.rs
entity/src/remediation_product_status.rs
entity/src/remediation_purl_status.rs
entity/src/purl_status.rs
entity/src/lib.rs
entity/Cargo.toml
Surface remediations in PURL- and vulnerability-centric APIs, including new models and SQL wiring for api/v3/purl/recommend and api/v2/vulnerability/analyze.
  • Update vulnerability service analysis query to LEFT JOIN remediation_purl_status/remediation, aggregate remediations JSONB alongside advisories, and group appropriately.
  • Introduce RemediationSummary and RemediationCategory exposure in purl/vulnerability models and wire them into VersionedPurlStatus, VulnerabilityStatus, and Analysis models.
  • Wrap PurlStatus in a new AnalysisPurlStatus that carries remediations, and extend AnalysisDetailsV3 to also carry VulnerabilityHead; adapt analysis building logic accordingly.
  • Update PurlService.recommend to propagate remediations into VulnerabilityStatus objects and adjust PurlDetails/VersionedPurlAdvisory to load many-to-many remediations via SeaORM.
  • Adjust tests (including new analyze_purls_remediations) to assert remediations flow through v2/v3 analysis and recommendations and that field access goes through nested purl_status where required.
modules/fundamental/src/vulnerability/service/mod.rs
modules/fundamental/src/vulnerability/model/analyze.rs
modules/fundamental/src/purl/model/summary/remediation.rs
modules/fundamental/src/purl/model/details/versioned_purl.rs
modules/fundamental/src/purl/model/mod.rs
modules/fundamental/src/purl/service/mod.rs
modules/fundamental/src/purl/model/details/purl.rs
modules/fundamental/src/vulnerability/service/test.rs
modules/fundamental/tests/vuln/mod.rs
Extend OpenAPI schema to describe remediation payloads and the enriched structures that now include remediations.
  • Change AnalysisDetailsV3 to allOf VulnerabilityHead and introduce AnalysisPurlStatus as PurlStatus + remediations in the OpenAPI spec.
  • Define RemediationCategory enum and RemediationSummary schema, and reference them from AnalysisPurlStatus and VulnerabilityStatus schemas.
  • Update schemas that include VulnerabilityStatus to require remediations property and adjust descriptions to reflect remediations in responses.
openapi.yaml
Miscellaneous plumbing and test-data updates to support the new remediation behavior.
  • Make CSAF util module public within ingestor to allow reuse after new wiring.
  • Add synthetic CSAF test document containing remediations referencing purls for functional tests.
  • Tighten/normalize various tests to account for new AnalysisPurlStatus wrapping and to tweak formatting/commas where necessary for compilation.
modules/ingestor/src/service/advisory/csaf/mod.rs
modules/fundamental/tests/advisory/csaf/reingest.rs
modules/fundamental/tests/advisory/csaf/delete.rs
modules/fundamental/tests/advisory/osv/reingest.rs
modules/fundamental/tests/advisory/csaf/reingest.rs
modules/fundamental/tests/vuln/mod.rs
etc/test-data/csaf/synthetic-affected-with-purl.json
Cargo.lock

Possibly linked issues

  • #Extract remediation information from advisories: The PR adds CSAF remediation ingestion and API exposure, directly implementing the remediation extraction requested in the issue.

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

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 2 issues, and left some high level feedback:

  • In RemediationCreator::generate_remediation_uuid you base the UUID on format!("{:?}", rem.category), which depends on the Debug representation; consider using a stable string value (e.g. the serialized enum string or DB value) to avoid UUID changes if the enum’s Debug output ever changes.
  • The RemediationCategory ActiveEnum and migration both define a will_not_fix variant, but the From<&csaf::vulnerability::RemediationCategory> for RemediationCategory impl does not handle this case (comment says CSAF 2.1 not supported); if/when the upstream library adds it, this will silently map to nothing, so it may be worth adding a placeholder arm or explicit TODO to avoid future mismatches.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `RemediationCreator::generate_remediation_uuid` you base the UUID on `format!("{:?}", rem.category)`, which depends on the `Debug` representation; consider using a stable string value (e.g. the serialized enum string or DB value) to avoid UUID changes if the enum’s `Debug` output ever changes.
- The `RemediationCategory` ActiveEnum and migration both define a `will_not_fix` variant, but the `From<&csaf::vulnerability::RemediationCategory> for RemediationCategory` impl does not handle this case (comment says CSAF 2.1 not supported); if/when the upstream library adds it, this will silently map to nothing, so it may be worth adding a placeholder arm or explicit TODO to avoid future mismatches.

## Individual Comments

### Comment 1
<location> `modules/fundamental/src/vulnerability/service/mod.rs:477-481` </location>
<code_context>
   INNER JOIN version_range ON purl_status.version_range_id = version_range.id
   LEFT JOIN vulnerability ON purl_status.vulnerability_id = vulnerability.id
   INNER JOIN status ON purl_status.status_id = status.id
+  LEFT JOIN remediation_purl_status rps ON rps.purl_status_id = purl_status.id
+  LEFT JOIN remediation ON remediation.id = rps.remediation_id
 WHERE {ns_condition}
</code_context>

<issue_to_address>
**issue (bug_risk):** Remediations get associated per advisory, not per purl status, which can misrepresent purl-specific remediations.

The SQL correctly joins `remediation_purl_status` via `purl_status.id`, but the aggregation later stores remediations only in `remediations_by_advisory: HashMap<Uuid, Vec<RemediationSummary>>`, keyed by `advisory_id`. As a result, every `AnalysisPurlStatus` for an advisory gets the same remediation list, even though `remediation_purl_status` is scoped to specific `purl_status` rows.

If remediations are meant to be purl-specific, consider keying by `purl_status` (or `(advisory_id, purl_status_id)`) and attaching only the corresponding remediations to each `AnalysisPurlStatus` instead of sharing them across the advisory.
</issue_to_address>

### Comment 2
<location> `modules/ingestor/src/service/advisory/csaf/creator.rs:483` </location>
<code_context>
+    fn generate_remediation_uuid(&self, rem: &Remediation) -> Uuid {
+        let mut result = Uuid::new_v5(&REMEDIATION_NAMESPACE, self.advisory_id.as_bytes());
+        result = Uuid::new_v5(&result, self.vulnerability_id.as_bytes());
+        result = Uuid::new_v5(&result, format!("{:?}", rem.category).as_bytes());
+        result = Uuid::new_v5(&result, rem.details.as_bytes());
+        if let Some(url) = &rem.url {
</code_context>

<issue_to_address>
**suggestion (bug_risk):** Using `Debug` formatting of the remediation category in UUID generation is brittle and may change if the upstream enum implementation changes.

Deriving the UUID from `format!("{:?}", rem.category)` ties its stability to the `Debug` implementation of `csaf::RemediationCategory`, which can change (e.g., variant rename or formatting tweak), silently changing UUIDs and breaking idempotency. Prefer a stable representation instead, such as first mapping to your own `RemediationCategory` and using its database string (or another explicitly maintained string mapping) as the `new_v5` input.

Suggested implementation:

```rust
        let mut result = Uuid::new_v5(&REMEDIATION_NAMESPACE, self.advisory_id.as_bytes());
        result = Uuid::new_v5(&result, self.vulnerability_id.as_bytes());

        // Use a stable, non-Debug string representation of the remediation category
        // so that UUIDs do not change with Debug formatting tweaks.
        let category_key = rem.category.to_string();
        result = Uuid::new_v5(&result, category_key.as_bytes());

        result = Uuid::new_v5(&result, rem.details.as_bytes());

```

If your codebase already defines an internal, stable representation of remediation categories (e.g., your own `RemediationCategory` enum or a DB string mapping), you can further improve stability by:
1. Mapping `rem.category` to that internal representation, and
2. Using that explicit mapping as the `category_key` instead of `rem.category.to_string()`.

For example:
- Introduce a helper like `fn remediation_category_key(rem: &Remediation) -> &'static str` that returns your own stable strings.
- Replace `let category_key = rem.category.to_string();` with `let category_key = remediation_category_key(rem);`.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@Strum355 Strum355 requested a review from dejanb February 5, 2026 14:47
Copy link
Contributor

@dejanb dejanb left a comment

Choose a reason for hiding this comment

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

This is going in a good direction. Thanks @Strum355

I generally agree with sourcery comments, so let's try to address them

In general, I think we should add more tests to cover cases like this

I opened #2236 for the next step in supporting product statuses, which should provide us with more realistic test data. But even with current synthetic data we should cover multiple purls-advisories combinations to make sure that queries and APIs cover them properly.

@codecov
Copy link

codecov bot commented Feb 6, 2026

Codecov Report

❌ Patch coverage is 71.08434% with 48 lines in your changes missing coverage. Please review.
✅ Project coverage is 69.25%. Comparing base (bc3f83a) to head (920fd0b).

Files with missing lines Patch % Lines
entity/src/remediation.rs 37.20% 27 Missing ⚠️
entity/src/remediation_product_status.rs 0.00% 6 Missing ⚠️
entity/src/remediation_purl_status.rs 0.00% 6 Missing ⚠️
.../fundamental/src/purl/model/summary/remediation.rs 50.00% 6 Missing ⚠️
...ndamental/src/purl/model/details/versioned_purl.rs 83.33% 0 Missing and 2 partials ⚠️
...ules/fundamental/src/vulnerability/service/test.rs 83.33% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2234      +/-   ##
==========================================
+ Coverage   69.21%   69.25%   +0.03%     
==========================================
  Files         405      410       +5     
  Lines       23038    23197     +159     
  Branches    23038    23197     +159     
==========================================
+ Hits        15946    16065     +119     
- Misses       6188     6223      +35     
- Partials      904      909       +5     

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

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

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