Skip to content

feat: add endpoint for returning all AIBOM's, optionally filtered#2325

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

feat: add endpoint for returning all AIBOM's, optionally filtered#2325
jcrossley3 wants to merge 2 commits intoguacsec:mainfrom
jcrossley3:2324

Conversation

@jcrossley3
Copy link
Copy Markdown
Contributor

@jcrossley3 jcrossley3 commented Apr 14, 2026

Fixes #2324

Summary by Sourcery

Add a new API endpoint to list all AI SBOM models with flexible querying and pagination, alongside adapting existing model retrieval logic to support both per-SBOM and global queries.

New Features:

  • Expose a /api/v2/sbom/models endpoint to search across all AI models with query, sorting, offset, and limit parameters.

Enhancements:

  • Generalize SbomService model-fetching to accept an optional SBOM id so it can be reused for both single-SBOM and global model searches.
  • Register the new all-models handler in the SBOM endpoints configuration and document it in the OpenAPI/utoipa specs.

Tests:

  • Add integration tests covering various positive and negative query patterns for the new all-models AI SBOM models endpoint.

@sourcery-ai
Copy link
Copy Markdown
Contributor

sourcery-ai bot commented Apr 14, 2026

Reviewer's Guide

Adds a new v2 endpoint and OpenAPI definition to list all AI SBOM models across SBOMs with existing query/sort/pagination semantics, refactors the service method to accept an optional SBOM id so it can be reused by both per-SBOM and global listings, and introduces tests that validate the new endpoint’s query behavior using ingested AIBOM fixtures.

Sequence diagram for the new listAllModels v2 endpoint

sequenceDiagram
    actor ApiClient
    participant ActixRouter
    participant AllModelsHandler as all_models
    participant SbomService
    participant Database

    ApiClient->>ActixRouter: GET /api/v2/sbom/models?q&sort&offset&limit
    ActixRouter->>AllModelsHandler: Route request
    AllModelsHandler->>Database: begin_read()
    Database-->>AllModelsHandler: read_transaction
    AllModelsHandler->>SbomService: fetch_sbom_models(None, search, paginated, read_transaction)
    SbomService->>Database: execute AI models query with optional sbom_id filter
    Database-->>SbomService: PaginatedResults<SbomModel>
    SbomService-->>AllModelsHandler: PaginatedResults<SbomModel>
    AllModelsHandler-->>ApiClient: 200 OK (application/json)
Loading

Updated class diagram for SbomService and AI model listing endpoints

classDiagram
    class SbomService {
        +fetch_sbom_models(sbom_id: Option_Uuid, search: Query, paginated: Paginated, connection: ConnectionTrait) PaginatedResults_SbomModel
    }

    class SbomEndpoints {
        +models(fetch: SbomService, db: Database, search: Query, paginated: Paginated, read_guard: ReadSbom) HttpResponse
        +all_models(fetch: SbomService, db: Database, search: Query, paginated: Paginated, read_guard: ReadSbom) HttpResponse
    }

    class Database {
        +begin_read() Transaction
    }

    class Query {
    }

    class Paginated {
        +offset: int64
        +limit: int64
    }

    class PaginatedResults_SbomModel {
        +items: List_SbomModel
        +total: int64
        +offset: int64
        +limit: int64
    }

    class SbomModel {
        +id: string
    }

    class ReadSbom {
    }

    class ConnectionTrait {
    }

    class Transaction {
    }

    SbomEndpoints --> SbomService
    SbomEndpoints --> Database
    SbomEndpoints --> Query
    SbomEndpoints --> Paginated
    SbomEndpoints --> ReadSbom

    SbomService ..> ConnectionTrait
    SbomService --> PaginatedResults_SbomModel
    PaginatedResults_SbomModel "1" o-- "*" SbomModel
    Database --> Transaction
Loading

File-Level Changes

Change Details Files
Expose a new REST API endpoint to list all AI models with search and pagination.
  • Extend OpenAPI spec with /api/v2/sbom/models GET, documenting q, sort, offset, and limit query parameters and PaginatedResults_SbomModel response.
  • Add all_models handler in sbom endpoints module, wired into the Actix web configuration at /v2/sbom/models, reusing Query and Paginated params and ReadSbom authorization.
openapi.yaml
modules/fundamental/src/sbom/endpoints/mod.rs
Refactor SBOM model fetching logic to support both per-SBOM and global queries.
  • Change SbomService::fetch_sbom_models signature to take Option instead of Uuid.
  • Initialize the underlying SeaORM query without an sbom_id filter and conditionally apply sbom_ai::Column::SbomId.eq(id) when an id is provided.
  • Update existing models endpoint to pass Some(id) so behavior for per-SBOM queries is preserved while enabling None for global queries.
modules/fundamental/src/sbom/service/sbom.rs
modules/fundamental/src/sbom/endpoints/mod.rs
Add tests for querying all AI BOM models via the new endpoint using various q filters.
  • Introduce query_all_aibom_models rstest using TrustifyContext that ingests two AI BOM fixtures and exercises multiple positive and negative q patterns against /api/v2/sbom/models.
  • Assert that the total field in the paginated JSON response matches the expected count for each query case.
modules/fundamental/src/sbom/endpoints/test.rs

Assessment against linked issues

Issue Objective Addressed Explanation
#2324 Add an API endpoint that returns all AI models in the system, supporting optional filtering via the q query parameter.

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

Copy link
Copy Markdown
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

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

## Individual Comments

### Comment 1
<location path="modules/fundamental/src/sbom/endpoints/test.rs" line_range="1861-1870" />
<code_context>
+#[test_context(TrustifyContext)]
</code_context>
<issue_to_address>
**suggestion (testing):** Add a test case for calling `/api/v2/sbom/models` without a `q` parameter to verify the default behavior returns all models and aligns with pagination defaults.

The current parameterized tests only cover variations of the `q` filter and never call `/api/v2/sbom/models` without it (or with an empty string). Please add a dedicated test (or rstest case) that omits `q` and asserts the expected `total` and pagination metadata for the default "list all" behavior to protect against regressions in default pagination handling.

Suggested implementation:

```rust
#[test_context(TrustifyContext)]
#[rstest]
#[case("hugging", 2)]
#[case("granite", 1)]
#[case("pkg:huggingface/ibm-granite", 1)]
#[case("pkg:huggingface/ibm-granite/granite-docling-258M", 1)]
#[case("pkg:huggingface/ibm-granite/granite-docling-258M@1.0", 1)]
#[case("purl=pkg:huggingface/ibm-granite/granite-docling-258M@1.0", 1)]
#[case("purl~granite", 1)]
#[case("purl:namespace=ibm-granite&purl:version=1.0&purl:type=huggingface", 1)]
#[case("name~granite", 1)]

#[test_context(TrustifyContext)]
#[tokio::test]
async fn list_models_without_q_returns_default_pagination(ctx: &TrustifyContext) -> anyhow::Result<()> {
    // When: calling `/api/v2/sbom/models` without a `q` parameter, we expect the default
    // "list all models" behavior and default pagination metadata.
    let response = ctx
        .client
        .get("/api/v2/sbom/models")
        .send()
        .await?;

    assert_eq!(response.status(), reqwest::StatusCode::OK);

    #[derive(serde::Deserialize)]
    struct ModelsPage<T> {
        total: u64,
        items: Vec<T>,
        offset: u64,
        limit: u64,
    }

    // Use the minimal shape needed for the assertions; the full model type is not required here.
    #[derive(serde::Deserialize)]
    struct ModelSummary {
        id: String,
    }

    let page: ModelsPage<ModelSummary> = response.json().await?;

    // Then: verify default pagination semantics for "list all".
    //
    // `total` should reflect the total number of models in the system and must be at
    // least as large as the number of items in this page.
    assert!(page.total >= page.items.len() as u64);

    // Default listing should start at the beginning.
    assert_eq!(page.offset, 0);

    // `limit` should be the configured default page size; at minimum it must be at
    // least as large as the number of items returned.
    assert!(page.limit >= page.items.len() as u64);

    Ok(())
}

```

Depending on the existing test harness, you may need to:

1. Adjust the way the HTTP client is accessed:
   - If `TrustifyContext` exposes the client differently (e.g. `ctx.http`, `ctx.api`, `ctx.app`, or a helper like `ctx.get("/path").await`), replace `ctx.client.get(...).send().await?` with the appropriate call.
2. Align the HTTP client and status imports:
   - If you are not already using `reqwest`, replace `reqwest::StatusCode` with the status type you use elsewhere (for example, `actix_web::http::StatusCode` or `hyper::StatusCode`), and ensure any necessary `use` statements are present at the top of the file.
3. Reuse existing pagination/model types if available:
   - If the project already defines shared pagination or model summary DTOs (e.g. `Page<T>`, `PaginatedResponse<T>`, or a `ModelSummary` type), remove the local `ModelsPage` and `ModelSummary` structs and deserialize directly into those shared types instead, updating the field names in the assertions if they differ (`total_count` vs `total`, `data` vs `items`, etc.).
4. Keep test attributes consistent:
   - If other tests in this file use a different asynchronous test attribute (e.g. `#[actix_rt::test]` or `#[tokio::test(flavor = "multi_thread")]`), update the new test's attribute to match.
</issue_to_address>

### Comment 2
<location path="modules/fundamental/src/sbom/endpoints/test.rs" line_range="1902-1904" />
<code_context>
+
+    let uri = format!("/api/v2/sbom/models?q={}", encode(q));
+    let req = TestRequest::get().uri(&uri).to_request();
+    let response: Value = app.call_and_read_body_json(req).await;
+
+    assert_eq!(response["total"].as_i64(), Some(count), "q: {q}");
+
+    Ok(())
</code_context>
<issue_to_address>
**suggestion (testing):** Strengthen assertions by checking the actual returned models (e.g., IDs or names) in addition to the `total` count.

Asserting only on `response["total"]` would still pass if the wrong models are returned but the count matches. Please also validate the contents of `items` (e.g., expected IDs/names/purls present and unexpected ones absent, or a sorted list of IDs/names) so the test actually checks the filtering behavior, not just the count.

Suggested implementation:

```rust
    let uri = format!("/api/v2/sbom/models?q={}", encode(q));
    let req = TestRequest::get().uri(&uri).to_request();
    let response: Value = app.call_and_read_body_json(req).await;

    // Keep existing count assertion
    assert_eq!(response["total"].as_i64(), Some(count), "q: {q}");

    // Assert items length matches reported total
    let items = response["items"]
        .as_array()
        .expect("response.items should be an array");

    assert_eq!(
        items.len() as i64,
        count,
        "items length should match total for q: {q}"
    );

    // Collect a string identifier for each item (prefer purl, fall back to name)
    let item_strings: Vec<String> = items
        .iter()
        .map(|item| {
            item["purl"]
                .as_str()
                .or_else(|| item["name"].as_str())
                .unwrap_or_default()
                .to_string()
        })
        .collect();

    // Strengthen assertions by validating which models are returned
    match q {
        // Broad search, should include both models
        "hugging" => {
            assert!(
                item_strings
                    .iter()
                    .any(|s| s.contains("granite-docling-258M")),
                "expected granite-docling-258M in results for q: {q}"
            );
            assert!(
                item_strings.iter().any(|s| s.contains("canary-1b-v2")),
                "expected canary-1b-v2 in results for q: {q}"
            );
        }
        // All other cases in this test are granite-specific queries
        _ => {
            // Only the granite model should be returned
            assert!(
                item_strings
                    .iter()
                    .all(|s| s.contains("granite-docling-258M")),
                "only granite-docling-258M should match for q: {q}"
            );
            assert!(
                !item_strings.iter().any(|s| s.contains("canary-1b-v2")),
                "canary-1b-v2 should not be returned for q: {q}"
            );
        }
    }

    Ok(())
}

```

1. Ensure `serde_json::Value` is imported at the top of the file, if it is not already:
   `use serde_json::Value;`
2. If the actual identifying field is not `purl` or `name`, adjust the `item_strings` extraction to use the appropriate key (e.g., `id` or another field).
3. If the model identifiers differ from `"granite-docling-258M"` and `"canary-1b-v2"` in the actual API response, update the `contains(...)` substrings accordingly to match the real values (IDs, names, or purls).
</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.

Comment thread modules/fundamental/src/sbom/endpoints/test.rs Outdated
Comment thread modules/fundamental/src/sbom/endpoints/test.rs Outdated
@codecov
Copy link
Copy Markdown

codecov bot commented Apr 14, 2026

Codecov Report

❌ Patch coverage is 88.23529% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 68.99%. Comparing base (18543a1) to head (82a7f63).

Files with missing lines Patch % Lines
modules/fundamental/src/sbom/endpoints/mod.rs 86.66% 0 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2325      +/-   ##
==========================================
+ Coverage   68.96%   68.99%   +0.02%     
==========================================
  Files         435      435              
  Lines       24366    24381      +15     
  Branches    24366    24381      +15     
==========================================
+ Hits        16805    16822      +17     
+ Misses       6678     6667      -11     
- Partials      883      892       +9     

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

Copy link
Copy Markdown
Contributor

@ctron ctron left a comment

Choose a reason for hiding this comment

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

I think the AI is right. So I think it makes sense:

  • add a test variant without q
  • not only test for the number of results, but for the actual models returned (which implies the number then)
  • also: add some tests to the scale-testing repository using this new endpoint. Not only with one case, but using a few with q (and without)

@jcrossley3
Copy link
Copy Markdown
Contributor Author

I think the AI is right. So I think it makes sense:

  • add a test variant without q
  • not only test for the number of results, but for the actual models returned (which implies the number then)
  • also: add some tests to the scale-testing repository using this new endpoint. Not only with one case, but using a few with q (and without)

done

@ctron
Copy link
Copy Markdown
Contributor

ctron commented Apr 15, 2026

I think the AI is right. So I think it makes sense:

  • add a test variant without q
  • not only test for the number of results, but for the actual models returned (which implies the number then)
  • also: add some tests to the scale-testing repository using this new endpoint. Not only with one case, but using a few with q (and without)

done

Sorry I can't find the new tests in the scale-testing repository. Could you please point me towards them.

Also (not a blocker), I'm wondering how the user can get a reference on the SBOM with the search results of the models? There seems to be no "sbom ID" as part of the response.

Copy link
Copy Markdown
Contributor

@carlosthe19916 carlosthe19916 left a comment

Choose a reason for hiding this comment

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

@jcrossley3 sorry for my late review. Looks good; only one thing missing

The mockups (image below) contains the column SBOMs which is supposed to count the number of SBOMs that contain a particular AI Model. How can I filter SBOMs based on a Model?

Image

@jcrossley3
Copy link
Copy Markdown
Contributor Author

Sorry I can't find the new tests in the scale-testing repository. Could you please point me towards them.

I created an issue in that repo: guacsec/trustify-scale-testing#99

This would have to me merged before work can proceed on that, right?

@jcrossley3
Copy link
Copy Markdown
Contributor Author

The mockups (image below) contains the column SBOMs which is supposed to count the number of SBOMs that contain a particular AI Model. How can I filter SBOMs based on a Model?

Hmmm. I totally missed that. I assume your question is because that's what's required in response to a user clicking the "sbom count" link?

@carlosthe19916
Copy link
Copy Markdown
Contributor

The mockups (image below) contains the column SBOMs which is supposed to count the number of SBOMs that contain a particular AI Model. How can I filter SBOMs based on a Model?

Hmmm. I totally missed that. I assume your question is because that's what's required in response to a user clicking the "sbom count" link?

Following the Licenses Page Pattern, if the user clicks on the "SBOM Count" then he would be redirected to the SBOM List page with a pre-defined filter that renders only those SBOMs.

Watch the video below of the licenses.

In the UI side:

  • The SBOM List page needs a new filter: Filter by AI Model
  • The Model List page needs a column with "sbom count"
Screencast.From.2026-04-15.15-22-27.mp4

@carlosthe19916
Copy link
Copy Markdown
Contributor

@jcrossley3 If it help I think we can tackle the "sbom count" in a separate PR so the UI takes this one and move things forward. If you want to do everything here it is also fine, whatever generates less problems for you

@jcrossley3
Copy link
Copy Markdown
Contributor Author

@jcrossley3 If it help I think we can tackle the "sbom count" in a separate PR so the UI takes this one and move things forward. If you want to do everything here it is also fine, whatever generates less problems for you

Yes, please. It's not a trivial change, so deserving of its own PR, I think.

Copy link
Copy Markdown
Contributor

@carlosthe19916 carlosthe19916 left a comment

Choose a reason for hiding this comment

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

LGTM, I tested it locally and there is this PR on the UI side that renders the models guacsec/trustify-ui#995

As agreed with @jcrossley3 the column SBOM Count can be done in a separate PR

@ctron
Copy link
Copy Markdown
Contributor

ctron commented Apr 16, 2026

Sorry I can't find the new tests in the scale-testing repository. Could you please point me towards them.

I created an issue in that repo: guacsec/trustify-scale-testing#99

This would have to me merged before work can proceed on that, right?

I'd say it can be run in parallel. And you should be able to run a test locally, just to show some numbers.

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.

Complete work on AI endpoint

3 participants