feat: API endpoint for fetching an SBOM's AI models#2255
feat: API endpoint for fetching an SBOM's AI models#2255jcrossley3 wants to merge 2 commits intoguacsec:mainfrom
Conversation
Reviewer's GuideImplements 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 listingsequenceDiagram
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)
Entity relationship diagram for SBOM AI models and PURLserDiagram
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
Entity relationship diagram for migration adding QualifiedPurlId to sbom_aierDiagram
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
Class diagram for SbomModel service and ingestion changesclassDiagram
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
File-Level Changes
Assessment against linked issues
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
Codecov Report❌ Patch coverage is
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. 🚀 New features to boost your workflow:
|
060d21f to
d95c3ab
Compare
a31eddb to
bfa013f
Compare
There was a problem hiding this comment.
Hey - I've found 3 issues, and left some high level feedback:
- The new
qualified_purl_idcolumn onsbom_aiis created asNOT NULLwithout 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 toCanonicalPurlfails you replace the original value withNull, which silently discards data; consider preserving the original value on error (e.g. logging and returningselfunchanged) instead of overwriting it. - The OpenAPI
PaginatedResults_SbomModelandSbomModelschemas diverge from the Rust model (e.g.purlis documented as a string while the struct usesserde_json::Value, andproperties/purllack explicit types in the YAML); aligning the OpenAPI definitions with the RustSbomModel(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>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
Fixes #2254
Summary by Sourcery
Add support for querying AI models associated with an SBOM, including API, service, and persistence changes.
New Features:
Enhancements:
Tests: