Skip to content

feat(ingestor): extract additional CycloneDX 1.6 model card considerations#2399

Open
ikanias wants to merge 2 commits into
mainfrom
TC-4796
Open

feat(ingestor): extract additional CycloneDX 1.6 model card considerations#2399
ikanias wants to merge 2 commits into
mainfrom
TC-4796

Conversation

@ikanias

@ikanias ikanias commented Jun 12, 2026

Copy link
Copy Markdown

Summary

  • Extract 5 additional CycloneDX 1.6 ModelCardConsiderations fields into sbom_ai.properties during AIBOM ingestion
  • ethicalConsiderationssafetyRiskAssessment (JSON array of {name, mitigationStrategy} objects)
  • fairnessAssessmentsfairnessAssessments (JSON array of {groupAtRisk, benefits, harms, mitigationStrategy} objects)
  • performanceTradeoffsperformanceTradeoffs (semicolon-joined string)
  • useCasesuseCases (semicolon-joined string)
  • usersusers (semicolon-joined string)

Test plan

  • Extended existing ingest_ai_cyclonedx_structured_fields test with assertions for all 5 new fields
  • Updated test_structured_model_card.json fixture with CycloneDX 1.6 considerations data
  • Verified all 5 AI CycloneDX tests pass with no regressions
  • Clippy clean, rustfmt clean

Implements TC-4796

🤖 Generated with Claude Code

Summary by Sourcery

Map CycloneDX AI model card structured fields into sbom_ai properties and validate ingestion across multiple vendor fixtures.

New Features:

  • Populate sbom_ai.properties from CycloneDX modelParameters and considerations fields, including primaryPurpose, typeOfModel, limitations, safety risk assessments, fairness assessments, performance tradeoffs, use cases, and users.

Enhancements:

  • Ensure generic model card properties override derived structured fields when both are present during ingestion of CycloneDX AI SBOMs.

Tests:

  • Add and extend CycloneDX AI ingestion tests, including a structured model card fixture and vendor-specific cases, to verify property precedence and new consideration field mappings.

ikanias and others added 2 commits June 10, 2026 18:49
…bom_ai.properties

Extract structured fields from CycloneDX ModelCard into sbom_ai.properties
as frontend-friendly keys: model_parameters.task → primaryPurpose,
model_parameters.approach.type → typeOfModel, and
considerations.technical_limitations → limitation (joined with "; ").
Generic model_card.properties overlay on top, preserving precedence.

Implements TC-4516

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…tions

Extract 5 additional CycloneDX 1.6 ModelCardConsiderations fields into
sbom_ai.properties during AIBOM ingestion: ethicalConsiderations as
safetyRiskAssessment (JSON array), fairnessAssessments (JSON array),
performanceTradeoffs, useCases, and users (joined strings).

Implements TC-4796

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Assisted-by: Claude Code
@sourcery-ai

sourcery-ai Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Extends AI CycloneDX AIBOM ingestion to derive additional structured model card considerations into sbom_ai.properties, while ensuring generic model card properties override structured fields, and adds targeted tests/fixtures to validate precedence and new mappings.

Flow diagram for building ModelCard properties from CycloneDX model_card

flowchart TD
    A[Component]
    A --> B[model_card]

    B --> C[model_parameters]
    C --> C1[primaryPurpose from task]
    C --> C2[typeOfModel from approach.type_]

    B --> D[considerations]
    D --> D1[limitation from technical_limitations join]
    D --> D2[safetyRiskAssessment from ethical_considerations]
    D --> D3[fairnessAssessments from fairness_assessments]
    D --> D4[performanceTradeoffs from performance_tradeoffs join]
    D --> D5[useCases from use_cases join]
    D --> D6[users from users join]

    B --> E[properties]
    E -->|overrides structured fields| F[final properties map]

    C1 --> F
    C2 --> F
    D1 --> F
    D2 --> F
    D3 --> F
    D4 --> F
    D5 --> F
    D6 --> F

    F --> G[ModelCard.properties in sbom_ai.Entity]
Loading

File-Level Changes

Change Details Files
Derive structured model card fields from CycloneDX components into a normalized ModelCard.properties map, including new considerations mappings and precedence rules.
  • Replaced direct passthrough of model_card.properties with construction of a JSON object from structured model_parameters and considerations fields when present.
  • Mapped modelParameters.task to primaryPurpose and modelParameters.approach.type_ to typeOfModel.
  • Mapped considerations.technicalLimitations into limitation as a semicolon-joined string when non-empty.
  • Mapped considerations.ethicalConsiderations into safetyRiskAssessment as an array of {name, mitigationStrategy} objects, omitting missing fields.
  • Mapped considerations.fairnessAssessments into fairnessAssessments as an array of {groupAtRisk, benefits, harms, mitigationStrategy} objects.
  • Mapped considerations.performanceTradeoffs, useCases, and users into performanceTradeoffs, useCases, and users respectively as semicolon-joined strings when non-empty.
  • Applied generic model card properties last so they override any structured-field-derived entries, and returned Value::Null when no properties exist.
modules/ingestor/src/graph/sbom/common/machine_learning_model.rs
Add and extend ingestion tests to cover generic vs structured precedence and the new CycloneDX 1.6 considerations mappings.
  • Added ingest_ai_cyclonedx_nvidia_properties test to confirm generic properties override structured task and populate typeOfModel, limitation, and other generic fields.
  • Added ingest_ai_cyclonedx_ibm_properties test to validate generic primaryPurpose and typeOfModel handling and absence of limitation when not provided.
  • Extended ingest_ai_cyclonedx_structured_fields test to assert all new structured mappings for safetyRiskAssessment, fairnessAssessments, performanceTradeoffs, useCases, and users, as well as existing primaryPurpose/typeOfModel/limitation behavior.
  • Introduced/updated CycloneDX AI test fixtures, including test_structured_model_card.json, to exercise structured model card considerations content.
modules/ingestor/src/service/sbom/cyclonedx.rs
etc/test-data/cyclonedx/ai/test_structured_model_card.json

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

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

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

  • The new ModelCard conversion now returns Value::Null for cards that only have an empty properties list (and no structured fields), whereas previously this resulted in an empty object; consider whether this behavioral change is intentional and, if not, preserve the prior semantics for empty properties.
  • The structured-field-to-properties mapping logic in impl From<&Component> for ModelCard has grown quite large; consider extracting this into a dedicated helper (e.g., fn build_properties(card: &ModelCardType) -> Map<String, Value>) to improve readability and make the mapping easier to unit-test in isolation.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The new `ModelCard` conversion now returns `Value::Null` for cards that only have an empty `properties` list (and no structured fields), whereas previously this resulted in an empty object; consider whether this behavioral change is intentional and, if not, preserve the prior semantics for empty `properties`.
- The structured-field-to-`properties` mapping logic in `impl From<&Component> for ModelCard` has grown quite large; consider extracting this into a dedicated helper (e.g., `fn build_properties(card: &ModelCardType) -> Map<String, Value>`) to improve readability and make the mapping easier to unit-test in isolation.

## Individual Comments

### Comment 1
<location path="modules/ingestor/src/graph/sbom/common/machine_learning_model.rs" line_range="124-125" />
<code_context>
+                    }
+                }
+
+                if map.is_empty() {
+                    Value::Null
+                } else {
+                    Value::Object(map)
</code_context>
<issue_to_address>
**issue (bug_risk):** Revisit behavior change for model cards with only empty generic properties

Previously, an empty `card.properties` vector resulted in `Some(Map::new())` and thus `Value::Object({})`. With the new check, the same case now yields `Value::Null`. This changes observable behavior for model cards that exist but have no effective properties and may break callers that distinguish between an empty object and `null`. If that distinction matters, consider still returning `Value::Object(map)` when `card.properties` was `Some(_)`, even if the map is empty.
</issue_to_address>

### Comment 2
<location path="modules/ingestor/src/graph/sbom/common/machine_learning_model.rs" line_range="45-54" />
<code_context>
+                    if let Some(risks) = &considerations.ethical_considerations
</code_context>
<issue_to_address>
**suggestion:** Consider skipping fully-empty risk objects when all fields are `None`

Right now each `ethical_considerations` entry becomes an object even when both `name` and `mitigation_strategy` are `None`, producing `{}` entries. This may be unexpected for consumers. Consider either dropping objects that end up empty or only inserting when `name` is set, whichever best matches your schema.

Suggested implementation:

```rust
                    if let Some(risks) = &considerations.ethical_considerations
                        && !risks.is_empty()
                    {
                        let arr: Vec<Value> = risks
                            .iter()
                            .filter_map(|r| {
                                let mut obj = Map::new();
                                if let Some(name) = &r.name {
                                    obj.insert("name".to_string(), Value::String(name.clone()));
                                }
                                if let Some(ms) = &r.mitigation_strategy {

```

To fully implement the behavior of skipping fully-empty risk objects, adjust the rest of the closure so that instead of unconditionally returning `Value::Object(obj)`, it returns `None` when `obj.is_empty()`:

```rust
if let Some(ms) = &r.mitigation_strategy {
    obj.insert("mitigation_strategy".to_string(), Value::String(ms.clone()));
}
if obj.is_empty() {
    None
} else {
    Some(Value::Object(obj))
}
```

This should replace the previous `Value::Object(obj)` expression at the end of the closure.
</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 on lines +124 to +125
if map.is_empty() {
Value::Null

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.

issue (bug_risk): Revisit behavior change for model cards with only empty generic properties

Previously, an empty card.properties vector resulted in Some(Map::new()) and thus Value::Object({}). With the new check, the same case now yields Value::Null. This changes observable behavior for model cards that exist but have no effective properties and may break callers that distinguish between an empty object and null. If that distinction matters, consider still returning Value::Object(map) when card.properties was Some(_), even if the map is empty.

Comment on lines +45 to +54
if let Some(risks) = &considerations.ethical_considerations
&& !risks.is_empty()
{
let arr: Vec<Value> = risks
.iter()
.map(|r| {
let mut obj = Map::new();
if let Some(name) = &r.name {
obj.insert("name".to_string(), Value::String(name.clone()));
}

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.

suggestion: Consider skipping fully-empty risk objects when all fields are None

Right now each ethical_considerations entry becomes an object even when both name and mitigation_strategy are None, producing {} entries. This may be unexpected for consumers. Consider either dropping objects that end up empty or only inserting when name is set, whichever best matches your schema.

Suggested implementation:

                    if let Some(risks) = &considerations.ethical_considerations
                        && !risks.is_empty()
                    {
                        let arr: Vec<Value> = risks
                            .iter()
                            .filter_map(|r| {
                                let mut obj = Map::new();
                                if let Some(name) = &r.name {
                                    obj.insert("name".to_string(), Value::String(name.clone()));
                                }
                                if let Some(ms) = &r.mitigation_strategy {

To fully implement the behavior of skipping fully-empty risk objects, adjust the rest of the closure so that instead of unconditionally returning Value::Object(obj), it returns None when obj.is_empty():

if let Some(ms) = &r.mitigation_strategy {
    obj.insert("mitigation_strategy".to_string(), Value::String(ms.clone()));
}
if obj.is_empty() {
    None
} else {
    Some(Value::Object(obj))
}

This should replace the previous Value::Object(obj) expression at the end of the closure.

@codecov

codecov Bot commented Jun 12, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.19608% with 15 lines in your changes missing coverage. Please review.
✅ Project coverage is 71.50%. Comparing base (5f31950) to head (5bba971).
⚠️ Report is 11 commits behind head on main.

Files with missing lines Patch % Lines
...or/src/graph/sbom/common/machine_learning_model.rs 84.37% 5 Missing and 10 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2399      +/-   ##
==========================================
+ Coverage   71.37%   71.50%   +0.12%     
==========================================
  Files         441      441              
  Lines       26747    26886     +139     
  Branches    26747    26886     +139     
==========================================
+ Hits        19092    19224     +132     
+ Misses       6541     6533       -8     
- Partials     1114     1129      +15     

☔ View full report in Codecov by Harness.
📢 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.

@ctron

ctron commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

It feels a bit weird taking some fields from the original structure, stuffing then in some JSON column, renaming part of the fields in the process.

I could see value in just taking in the while model card as JSONB (depending on how big they can get). Or using dedicated columns in the database.

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