Skip to content

feat: API endpoint for fetching an SBOM's AI models#2255

Open
jcrossley3 wants to merge 2 commits intoguacsec:mainfrom
jcrossley3:2254
Open

feat: API endpoint for fetching an SBOM's AI models#2255
jcrossley3 wants to merge 2 commits intoguacsec:mainfrom
jcrossley3:2254

Conversation

@jcrossley3
Copy link
Contributor

@jcrossley3 jcrossley3 commented Feb 24, 2026

Fixes #2254

Summary by Sourcery

Add support for querying AI models associated with an SBOM, including API, service, and persistence changes.

New Features:

  • Expose a new /api/v2/sbom/{id}/models endpoint to search AI models for a given SBOM with filtering, sorting, and pagination.
  • Introduce an SbomModel API/model type and paginated response schema for returning AI model details, including properties and PURLs.

Enhancements:

  • Extend SBOM service and ingestor logic to store AI model PURLs and relate AI models to qualified PURLs for querying.
  • Wire the new models endpoint into the SBOM HTTP router and OpenAPI/utoipa documentation.

Tests:

  • Add integration tests for fetching AI models from an AI SBOM and verifying various query patterns return the expected single model.

@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Feb 24, 2026

Reviewer's Guide

Implements a new paginated /api/v2/sbom/{id}/models endpoint (and corresponding OpenAPI schema) to list AI models associated with an SBOM, backed by new SbomModel domain DTO, a service method that queries sbom_ai joined with nodes and qualified_purl, updated ingestion to persist model PURLs, and a database migration plus entity wiring to store and relate AI models to qualified PURLs; includes tests for retrieval and querying of AI models from an SBOM.

Sequence diagram for GET /v2/sbom/{id}/models AI model listing

sequenceDiagram
    actor Client
    participant Api as SbomEndpoints_models
    participant Service as SbomService
    participant DB as Database
    participant ORM as SeaOrm

    Client->>Api: GET /v2/sbom/{id}/models?q=...&offset=&limit=
    Api->>DB: begin_read()
    DB-->>Api: Tx
    Api->>Service: fetch_sbom_models(sbom_id, Query, Paginated, Tx)
    Service->>ORM: sbom_ai::Entity::find()
    Service->>ORM: filter(SbomId = sbom_id)
    Service->>ORM: join(Node, QualifiedPurl)
    Service->>ORM: filtering_with(Query, Columns)
    Service->>ORM: limit_selector(offset, limit)
    ORM-->>Service: items (raw SbomModel rows), total
    Service->>Service: SbomModel::stringify_purl() on each item
    Service-->>Api: PaginatedResults<SbomModel>
    Api-->>Client: 200 OK (JSON PaginatedResults_SbomModel)
Loading

Entity relationship diagram for SBOM AI models and PURLs

erDiagram
    SBOM {
        uuid SbomId PK
    }

    SBOM_AI {
        uuid SbomId FK
        string NodeId PK
        json Properties
        uuid QualifiedPurlId FK
    }

    QUALIFIED_PURL {
        uuid Id PK
    }

    SBOM ||--o{ SBOM_AI : has_models
    QUALIFIED_PURL ||--o{ SBOM_AI : referenced_by

    SBOM_AI }o--|| QUALIFIED_PURL : has_one_purl
Loading

Entity relationship diagram for migration adding QualifiedPurlId to sbom_ai

erDiagram
    SBOM_AI {
        string NodeId PK
        uuid SbomId
        json Properties
        uuid QualifiedPurlId
    }

    MIGRATION_m0002120_add_ai_model_purl {
        up_add_column_QualifiedPurlId
        down_drop_column_QualifiedPurlId
    }

    MIGRATION_m0002120_add_ai_model_purl ||--o{ SBOM_AI : alters_table
Loading

Class diagram for SbomModel service and ingestion changes

classDiagram
    class SbomModel {
        +String id
        +String name
        +serde_json_Value purl
        +serde_json_Value properties
        +SbomModel stringify_purl()
    }

    class PaginatedResults_SbomModel {
        +Vec~SbomModel~ items
        +i64 total
    }

    class SbomService {
        +fetch_sbom_models(sbom_id: Uuid, search: Query, paginated: Paginated, connection: ConnectionTrait) Result~PaginatedResults_SbomModel, Error~
    }

    class SbomEndpoints_models {
        +models(fetch: SbomService, db: Database, id: Uuid, search: Query, paginated: Paginated, auth: ReadSbom) actix_web_Result
    }

    class MachineLearningModelCreator {
        -Uuid sbom_id
        -NodeCreator nodes
        -Vec~sbom_ai_ActiveModel~ models
        +add(node_id: String, name: String, purl: Option~String~, checksums: IntoIterator, model_card: ModelCard) void
    }

    class SbomAi_Entity {
        +Uuid sbom_id
        +String node_id
        +serde_json_Value properties
        +Uuid qualified_purl_id
    }

    class QualifiedPurl_Entity {
        +Uuid id
    }

    SbomService --> SbomModel : returns
    SbomService --> PaginatedResults_SbomModel : wraps
    SbomEndpoints_models --> SbomService : uses

    MachineLearningModelCreator --> SbomAi_Entity : builds_ActiveModel
    SbomAi_Entity --> QualifiedPurl_Entity : qualified_purl_id

    PaginatedResults_SbomModel o--> SbomModel : contains
Loading

File-Level Changes

Change Details Files
Add REST API surface and OpenAPI schema for listing AI models on an SBOM.
  • Define GET /api/v2/sbom/{id}/models in openapi.yaml with q, sort, offset, and limit query parameters and 200 PaginatedResults_SbomModel response.
  • Introduce PaginatedResults_SbomModel schema and SbomModel schema in OpenAPI components mirroring model id, name, purl, and properties fields.
  • Ensure endpoint is tagged under sbom and aligned with existing pagination and filtering conventions.
openapi.yaml
Expose an SbomModel DTO and service-layer method returning paginated AI models for an SBOM.
  • Introduce SbomModel struct in sbom model module with id, name, purl (JSON), and properties plus helper stringify_purl to convert structured PURLs into string form.
  • Add SbomService::fetch_sbom_models that queries sbom_ai by sbom_id, joins sbom_node and qualified_purl, applies generic Query filtering (including special handling for purl and purl:type), paginates with limit_selector, and maps rows into SbomModel with stringified PURLs.
  • Wire the new models endpoint into the SBOM endpoints module, including utoipa path metadata and handler that pulls Query and Paginated from query parameters, calls fetch_sbom_models inside a read transaction, and returns PaginatedResults as JSON.
modules/fundamental/src/sbom/model/mod.rs
modules/fundamental/src/sbom/service/sbom.rs
modules/fundamental/src/sbom/endpoints/mod.rs
Persist AI model PURLs during ingestion and relate sbom_ai rows to qualified_purl records.
  • Extend MachineLearningModelCreator::add to accept an optional PURL string, parse it to a Purl, derive a qualifier UUID, and store it as qualified_purl_id in sbom_ai::ActiveModel, defaulting to Uuid::nil on parse failure or absence.
  • Pass component PURL from CycloneDX component ingestion into MachineLearningModelCreator::add so AI models created from CycloneDX have their PURLs persisted.
  • Update sbom_ai entity model to include a non-null qualified_purl_id column and change its Purl relation to has_one qualified_purl::Entity; add corresponding reverse relation from qualified_purl to sbom_ai.
  • Add migration m0002120_add_ai_model_purl to alter the sbom_ai table by introducing a non-null qualified_purl_id UUID column and register this migration in the global Migrator.
modules/ingestor/src/graph/sbom/common/machine_learning_model.rs
modules/ingestor/src/graph/sbom/cyclonedx.rs
entity/src/sbom_ai.rs
entity/src/qualified_purl.rs
migration/src/m0002120_add_ai_model_purl.rs
migration/src/lib.rs
Add tests for the new AI model listing and querying behavior and align existing AI SBOM tests with new endpoint naming.
  • Rename existing get_aibom test to get_aibom_packages to clarify its focus on packages and avoid confusion with models.
  • Add get_aibom_models test that ingests a CycloneDX AI SBOM, calls /api/v2/sbom/{id}/models, and asserts the returned item structure (id, name, string PURL, and properties including several known keys) and total=1.
  • Add parameterized query_aibom_models tests that exercise various q filter patterns over purl, purl namespace/version/type, name, and properties to verify the search/filtering integration returns one result for each query case.
modules/fundamental/src/sbom/endpoints/test.rs

Assessment against linked issues

Issue Objective Addressed Explanation
#2254 Expose an API endpoint to fetch AI models associated with a given SBOM, including OpenAPI documentation and standard query/pagination parameters.
#2254 Implement backend logic and data model changes so that AI models linked to an SBOM are stored and can be queried and returned by the new endpoint.

Possibly linked issues


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

@codecov
Copy link

codecov bot commented Feb 24, 2026

Codecov Report

❌ Patch coverage is 93.10345% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 68.18%. Comparing base (dd31dae) to head (be63410).

Files with missing lines Patch % Lines
modules/fundamental/src/sbom/endpoints/mod.rs 86.66% 0 Missing and 2 partials ⚠️
modules/fundamental/src/sbom/model/mod.rs 77.77% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2255      +/-   ##
==========================================
+ Coverage   68.10%   68.18%   +0.07%     
==========================================
  Files         425      426       +1     
  Lines       24886    24942      +56     
  Branches    24886    24942      +56     
==========================================
+ Hits        16949    17007      +58     
+ Misses       7018     7008      -10     
- Partials      919      927       +8     

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

@jcrossley3 jcrossley3 force-pushed the 2254 branch 3 times, most recently from 060d21f to d95c3ab Compare March 9, 2026 13:23
@jcrossley3 jcrossley3 force-pushed the 2254 branch 2 times, most recently from a31eddb to bfa013f Compare March 13, 2026 16:25
@jcrossley3 jcrossley3 marked this pull request as ready for review March 13, 2026 16:36
@jcrossley3 jcrossley3 requested review from ctron, i386x and mrizzi March 13, 2026 16:36
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 3 issues, and left some high level feedback:

  • The new qualified_purl_id column on sbom_ai is created as NOT NULL without a default or backfill, which will break migrations on non-empty databases; consider either making it nullable initially and backfilling or providing a default/UPDATE before enforcing NOT NULL.
  • In SbomModel::stringify_purl, when deserialization to CanonicalPurl fails you replace the original value with Null, which silently discards data; consider preserving the original value on error (e.g. logging and returning self unchanged) instead of overwriting it.
  • The OpenAPI PaginatedResults_SbomModel and SbomModel schemas diverge from the Rust model (e.g. purl is documented as a string while the struct uses serde_json::Value, and properties/purl lack explicit types in the YAML); aligning the OpenAPI definitions with the Rust SbomModel (or reusing it via $ref) will avoid client/server contract mismatches.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The new `qualified_purl_id` column on `sbom_ai` is created as `NOT NULL` without a default or backfill, which will break migrations on non-empty databases; consider either making it nullable initially and backfilling or providing a default/UPDATE before enforcing NOT NULL.
- In `SbomModel::stringify_purl`, when deserialization to `CanonicalPurl` fails you replace the original value with `Null`, which silently discards data; consider preserving the original value on error (e.g. logging and returning `self` unchanged) instead of overwriting it.
- The OpenAPI `PaginatedResults_SbomModel` and `SbomModel` schemas diverge from the Rust model (e.g. `purl` is documented as a string while the struct uses `serde_json::Value`, and `properties`/`purl` lack explicit types in the YAML); aligning the OpenAPI definitions with the Rust `SbomModel` (or reusing it via `$ref`) will avoid client/server contract mismatches.

## Individual Comments

### Comment 1
<location path="modules/fundamental/src/sbom/service/sbom.rs" line_range="446-452" />
<code_context>
+        paginated: Paginated,
+        connection: &C,
+    ) -> Result<PaginatedResults<SbomModel>, Error> {
+        let query = sbom_ai::Entity::find()
+            .filter(sbom_ai::Column::SbomId.eq(sbom_id))
+            .select_only()
+            .column_as(sbom_ai::Column::NodeId, "id")
+            .column(sbom_node::Column::Name)
+            .column(sbom_ai::Column::Properties)
+            .column(qualified_purl::Column::Purl)
+            .join(JoinType::LeftJoin, sbom_ai::Relation::Node.def())
+            .join(JoinType::LeftJoin, sbom_ai::Relation::Purl.def())
</code_context>
<issue_to_address>
**issue (bug_risk):** The selected column order does not match `SbomModel` field order, which will break `FromQueryResult` mapping.

Because `SbomModel` is `{ id, name, purl, properties }` and derives `FromQueryResult`, SeaORM will map columns in the selected order: `id`, `name`, `properties`, `purl`. This will populate `purl` and `properties` incorrectly. Please move `.column(qualified_purl::Column::Purl)` before `.column(sbom_ai::Column::Properties)`, or add explicit `FromQueryResult` annotations if a different mapping is intended.
</issue_to_address>

### Comment 2
<location path="migration/src/m0002120_add_ai_model_purl.rs" line_range="13-16" />
<code_context>
+            .alter_table(
+                Table::alter()
+                    .table(SbomAi::Table)
+                    .add_column(
+                        ColumnDef::new(SbomAi::QualifiedPurlId)
+                            .uuid()
+                            .not_null()
+                            .to_owned(),
+                    )
</code_context>
<issue_to_address>
**issue (bug_risk):** Adding a non-null `qualified_purl_id` column without a default can break migration on existing data and makes "no purl" unrepresentable.

The ingestor currently uses `Uuid::nil()` when there is no valid PURL, which likely won’t correspond to any `qualified_purl` row, leading to invalid references. Please either make this column nullable and use `Option<Uuid>` in the entity, or add a default and ensure a matching `qualified_purl` row exists for that sentinel value.
</issue_to_address>

### Comment 3
<location path="entity/src/qualified_purl.rs" line_range="73-78" />
<code_context>
         to = "super::sbom_package_purl_ref::Column::QualifiedPurlId"
     )]
     SbomPackage,
+    #[sea_orm(
+        belongs_to = "super::sbom_ai::Entity",
+        from = "Column::Id",
+        to = "super::sbom_ai::Column::QualifiedPurlId"
+    )]
+    AI,
 }

</code_context>
<issue_to_address>
**issue (bug_risk):** The `qualified_purl``sbom_ai` relation direction looks inverted; `sbom_ai` owns the `qualified_purl_id` FK, so `qualified_purl` should likely `has_many` AI models.

`sbom_ai::Model` has a `qualified_purl_id` and `sbom_ai` declares `#[sea_orm(has_one = "super::qualified_purl::Entity")] Purl`, so `sbom_ai` is the side holding the FK. Declaring `qualified_purl` as `belongs_to sbom_ai` implies the opposite ownership and can cause incorrect relation-based joins. To align with the schema, `qualified_purl` should expose a `has_many` relation to `sbom_ai` instead.
</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.

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.

Implement api endpoint for fetching the AI models associated with an SBOM

1 participant