diff --git a/etc/test-data/cyclonedx/ai/TC-4427/ComponentLevelExternalBOMReference/README b/etc/test-data/cyclonedx/ai/TC-4427/ComponentLevelExternalBOMReference/README new file mode 100644 index 000000000..23d6ad11d --- /dev/null +++ b/etc/test-data/cyclonedx/ai/TC-4427/ComponentLevelExternalBOMReference/README @@ -0,0 +1,11 @@ +Recommended Directory Layout: + + sboms/ + ├── medical-scribe-app/ + │ └── 5.0.0/ + │ └── medical-scribe-app-5.0.0.sbom.json + │ + └── aiboms/ + └── scribe-gpt-medical/ + └── 2026.05/ + └── scribe-gpt-medical-2026.05.aibom.json \ No newline at end of file diff --git a/etc/test-data/cyclonedx/ai/TC-4427/ComponentLevelExternalBOMReference/medical-scribe-app-5.0.0.sbom.json b/etc/test-data/cyclonedx/ai/TC-4427/ComponentLevelExternalBOMReference/medical-scribe-app-5.0.0.sbom.json new file mode 100644 index 000000000..5c969d82d --- /dev/null +++ b/etc/test-data/cyclonedx/ai/TC-4427/ComponentLevelExternalBOMReference/medical-scribe-app-5.0.0.sbom.json @@ -0,0 +1,70 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.6", + "version": 1, + "serialNumber": "urn:uuid:9e2aa7c3-62cb-4f41-9e4f-111111111111", + + "metadata": { + "timestamp": "2026-05-14T12:00:00Z", + "component": { + "type": "application", + "name": "medical-scribe-app", + "version": "5.0.0", + "bom-ref": "medical-scribe-app" + } + }, + + "components": [ + { + "type": "application", + "name": "medical-scribe-app", + "version": "5.0.0", + "bom-ref": "medical-scribe-app", + "purl": "pkg:docker/acme/medical-scribe-app@5.0.0" + }, + + { + "type": "library", + "name": "fastapi", + "version": "0.115.0", + "bom-ref": "fastapi-lib", + "purl": "pkg:pypi/fastapi@0.115.0" + }, + + { + "type": "library", + "name": "transformers", + "version": "4.41.2", + "bom-ref": "transformers-lib", + "purl": "pkg:pypi/transformers@4.41.2" + }, + + { + "type": "machine-learning-model", + "name": "scribe-gpt-medical", + "version": "2026.05", + "bom-ref": "scribe-model", + + "description": "External medical transcription and summarization model", + + "externalReferences": [ + { + "type": "bom", + "url": "./scribe-gpt-medical-2026.05.aibom.cdx.json", + "comment": "External AIBOM describing the AI model supply chain" + } + ] + } + ], + + "dependencies": [ + { + "ref": "medical-scribe-app", + "dependsOn": [ + "fastapi-lib", + "transformers-lib", + "scribe-model" + ] + } + ] +} \ No newline at end of file diff --git a/etc/test-data/cyclonedx/ai/TC-4427/ComponentLevelExternalBOMReference/scribe-gpt-medical-2026.05.aibom.json b/etc/test-data/cyclonedx/ai/TC-4427/ComponentLevelExternalBOMReference/scribe-gpt-medical-2026.05.aibom.json new file mode 100644 index 000000000..25b950bfd --- /dev/null +++ b/etc/test-data/cyclonedx/ai/TC-4427/ComponentLevelExternalBOMReference/scribe-gpt-medical-2026.05.aibom.json @@ -0,0 +1,109 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.6", + "version": 3, + "serialNumber": "urn:uuid:5ab8e98b-3b35-44d5-92d9-222222222222", + + "metadata": { + "timestamp": "2026-05-14T12:00:00Z", + + "component": { + "type": "machine-learning-model", + "name": "scribe-gpt-medical", + "version": "2026.05", + "bom-ref": "scribe-model" + }, + + "authors": [ + { + "name": "Acme AI Research" + } + ] + }, + + "components": [ + { + "type": "machine-learning-model", + "name": "scribe-gpt-medical", + "version": "2026.05", + "bom-ref": "scribe-model", + + "publisher": "Acme AI Research", + + "description": "Fine-tuned medical summarization and transcription model", + + "licenses": [ + { + "license": { + "id": "Apache-2.0" + } + } + ], + + "properties": [ + { + "name": "model:architecture", + "value": "Llama-derived Transformer" + }, + { + "name": "model:parameters", + "value": "13B" + }, + { + "name": "model:framework", + "value": "PyTorch" + }, + { + "name": "model:precision", + "value": "fp16" + }, + { + "name": "model:context-window", + "value": "32768" + }, + { + "name": "model:fine-tuned-from", + "value": "llama3-13b" + } + ], + + "externalReferences": [ + { + "type": "model-card", + "url": "https://models.acme.example/scribe-gpt-medical/model-card" + } + ] + }, + + { + "type": "data", + "name": "medical-transcripts-training-dataset", + "version": "2026-Q1", + "bom-ref": "training-dataset", + + "properties": [ + { + "name": "dataset:source", + "value": "Curated anonymized transcripts" + } + ] + }, + + { + "type": "data", + "name": "medical-tokenizer", + "version": "3.1", + "bom-ref": "medical-tokenizer" + } + ], + + "dependencies": [ + { + "ref": "scribe-model", + "dependsOn": [ + "training-dataset", + "medical-tokenizer" + ] + } + ] +} \ No newline at end of file diff --git a/etc/test-data/cyclonedx/ai/TC-4427/CycloneDXBOM-LinkCross-BOMreferences/README b/etc/test-data/cyclonedx/ai/TC-4427/CycloneDXBOM-LinkCross-BOMreferences/README new file mode 100644 index 000000000..4b36b7a28 --- /dev/null +++ b/etc/test-data/cyclonedx/ai/TC-4427/CycloneDXBOM-LinkCross-BOMreferences/README @@ -0,0 +1,36 @@ +Understanding the BOM-Link URN + +The key line is: +{ + "url": "urn:cdx:aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/2" +} + + +This means: + +Part Meaning +==== ======= +urn:cdx: CycloneDX BOM-Link namespace +aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee Target BOM serial number UUID +/2 BOM version + +The referenced BOM must therefore contain: +{ + "serialNumber": "urn:uuid:aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", + "version": 2 +} + +This creates a deterministic immutable link. + +Recommended Repository Layout + + sboms/ + ├── applications/ + │ └── advisor-chatbot/ + │ └── 1.9.0/ + │ └── advisor-chatbot-1.9.0.sbom.json + │ + └── aiboms/ + └── advisor-llm/ + └── 8.1/ + └── advisor-llm-8.1.aibom.json \ No newline at end of file diff --git a/etc/test-data/cyclonedx/ai/TC-4427/CycloneDXBOM-LinkCross-BOMreferences/advisor-chatbot-1.9.0.sbom.json b/etc/test-data/cyclonedx/ai/TC-4427/CycloneDXBOM-LinkCross-BOMreferences/advisor-chatbot-1.9.0.sbom.json new file mode 100644 index 000000000..994fd22ac --- /dev/null +++ b/etc/test-data/cyclonedx/ai/TC-4427/CycloneDXBOM-LinkCross-BOMreferences/advisor-chatbot-1.9.0.sbom.json @@ -0,0 +1,68 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.6", + "version": 1, + + "serialNumber": "urn:uuid:11111111-2222-3333-4444-555555555555", + + "metadata": { + "timestamp": "2026-05-14T12:00:00Z", + + "component": { + "type": "application", + "name": "advisor-chatbot", + "version": "1.9.0", + "bom-ref": "advisor-chatbot-app" + } + }, + + "components": [ + { + "type": "application", + "name": "advisor-chatbot", + "version": "1.9.0", + "bom-ref": "advisor-chatbot-app", + + "purl": "pkg:docker/acme/advisor-chatbot@1.9.0" + }, + + { + "type": "library", + "name": "langchain", + "version": "0.3.0", + "bom-ref": "langchain-lib", + + "purl": "pkg:pypi/langchain@0.3.0" + }, + + { + "type": "machine-learning-model", + "name": "advisor-llm", + "version": "8.1", + "bom-ref": "advisor-llm-component", + + "description": "Enterprise advisory language model", + + "externalReferences": [ + { + "type": "bom", + + "url": "urn:cdx:aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/2", + + "comment": "BOM-Link reference to the external AI BOM" + } + ] + } + ], + + "dependencies": [ + { + "ref": "advisor-chatbot-app", + + "dependsOn": [ + "langchain-lib", + "advisor-llm-component" + ] + } + ] +} \ No newline at end of file diff --git a/etc/test-data/cyclonedx/ai/TC-4427/CycloneDXBOM-LinkCross-BOMreferences/advisor-llm-8.1.aibom.json b/etc/test-data/cyclonedx/ai/TC-4427/CycloneDXBOM-LinkCross-BOMreferences/advisor-llm-8.1.aibom.json new file mode 100644 index 000000000..9198c50d8 --- /dev/null +++ b/etc/test-data/cyclonedx/ai/TC-4427/CycloneDXBOM-LinkCross-BOMreferences/advisor-llm-8.1.aibom.json @@ -0,0 +1,113 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.6", + + "version": 2, + + "serialNumber": "urn:uuid:aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", + + "metadata": { + "timestamp": "2026-05-14T12:00:00Z", + + "component": { + "type": "machine-learning-model", + "name": "advisor-llm", + "version": "8.1", + + "bom-ref": "advisor-llm-root" + }, + + "authors": [ + { + "name": "Acme AI Platform Team" + } + ] + }, + + "components": [ + { + "type": "machine-learning-model", + + "name": "advisor-llm", + "version": "8.1", + + "bom-ref": "advisor-llm-root", + + "publisher": "Acme AI Platform Team", + + "description": "Instruction-tuned enterprise advisory model", + + "licenses": [ + { + "license": { + "id": "Apache-2.0" + } + } + ], + + "properties": [ + { + "name": "model:architecture", + "value": "Transformer" + }, + { + "name": "model:parameters", + "value": "70B" + }, + { + "name": "model:framework", + "value": "PyTorch" + }, + { + "name": "model:precision", + "value": "bf16" + }, + { + "name": "model:context-window", + "value": "131072" + }, + { + "name": "model:fine-tuned-from", + "value": "llama3-70b" + } + ], + + "externalReferences": [ + { + "type": "model-card", + + "url": "https://models.acme.example/advisor-llm/model-card" + } + ] + }, + + { + "type": "data", + + "name": "enterprise-advisory-training-set", + "version": "2026-Q1", + + "bom-ref": "advisor-training-data" + }, + + { + "type": "data", + + "name": "advisor-tokenizer", + "version": "5.2", + + "bom-ref": "advisor-tokenizer" + } + ], + + "dependencies": [ + { + "ref": "advisor-llm-root", + + "dependsOn": [ + "advisor-training-data", + "advisor-tokenizer" + ] + } + ] +} \ No newline at end of file diff --git a/etc/test-data/cyclonedx/ai/TC-4427/ExternalAibomViaExternalReferences/claims-assistant-2.0.0.json b/etc/test-data/cyclonedx/ai/TC-4427/ExternalAibomViaExternalReferences/claims-assistant-2.0.0.json new file mode 100644 index 000000000..bd1e6994d --- /dev/null +++ b/etc/test-data/cyclonedx/ai/TC-4427/ExternalAibomViaExternalReferences/claims-assistant-2.0.0.json @@ -0,0 +1,29 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.6", + "version": 1, + "serialNumber": "urn:uuid:22222222-2222-2222-2222-222222222222", + "metadata": { + "component": { + "type": "application", + "name": "claims-assistant", + "version": "2.0.0", + "bom-ref": "claims-app" + } + }, + "components": [ + { + "type": "application", + "name": "claims-assistant", + "version": "2.0.0", + "bom-ref": "claims-app" + } + ], + "externalReferences": [ + { + "type": "bom", + "url": "https://sbom.example.com/aiboms/claims-llm-7b.cdx.json", + "comment": "External AI BOM describing the hosted model" + } + ] +} \ No newline at end of file diff --git a/etc/test-data/cyclonedx/ai/TC-4427/ExternalAibomViaExternalReferences/claims-llm-7b.json b/etc/test-data/cyclonedx/ai/TC-4427/ExternalAibomViaExternalReferences/claims-llm-7b.json new file mode 100644 index 000000000..5832c9851 --- /dev/null +++ b/etc/test-data/cyclonedx/ai/TC-4427/ExternalAibomViaExternalReferences/claims-llm-7b.json @@ -0,0 +1,37 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.6", + "version": 3, + "serialNumber": "urn:uuid:33333333-3333-3333-3333-333333333333", + "metadata": { + "component": { + "type": "machine-learning-model", + "name": "claims-llm-7b", + "version": "2026.04", + "bom-ref": "claims-llm" + } + }, + "components": [ + { + "type": "machine-learning-model", + "name": "claims-llm-7b", + "version": "2026.04", + "bom-ref": "claims-llm", + "properties": [ + { + "name": "model:architecture", + "value": "Llama-derived" + }, + { + "name": "model:parameters", + "value": "7B" + } + ] + }, + { + "type": "data", + "name": "claims-training-dataset", + "version": "2026-Q1" + } + ] +} \ No newline at end of file diff --git a/etc/test-data/cyclonedx/ai/TC-4427/SelfContained/fraud-detection-api-4.2.0.json b/etc/test-data/cyclonedx/ai/TC-4427/SelfContained/fraud-detection-api-4.2.0.json new file mode 100644 index 000000000..25a4e04ef --- /dev/null +++ b/etc/test-data/cyclonedx/ai/TC-4427/SelfContained/fraud-detection-api-4.2.0.json @@ -0,0 +1,94 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.6", + "version": 1, + "serialNumber": "urn:uuid:7c2d9a48-1c17-4d0d-9cb0-111111111111", + "metadata": { + "timestamp": "2026-05-14T12:00:00Z", + "component": { + "type": "application", + "name": "fraud-detection-api", + "version": "4.2.0", + "bom-ref": "app-fraud-api" + }, + "tools": { + "components": [ + { + "type": "application", + "name": "cdxgen", + "version": "10.10.0" + } + ] + } + }, + "components": [ + { + "type": "application", + "name": "fraud-detection-api", + "version": "4.2.0", + "bom-ref": "app-fraud-api", + "purl": "pkg:docker/acme/fraud-api@4.2.0" + }, + { + "type": "library", + "name": "transformers", + "version": "4.41.2", + "bom-ref": "lib-transformers", + "purl": "pkg:pypi/transformers@4.41.2" + }, + { + "type": "machine-learning-model", + "name": "fraudbert-base", + "version": "1.3.1", + "bom-ref": "model-fraudbert", + "publisher": "Acme AI Labs", + "description": "Fine-tuned BERT model for fraud classification", + "licenses": [ + { + "license": { + "id": "Apache-2.0" + } + } + ], + "properties": [ + { + "name": "model:architecture", + "value": "BERT" + }, + { + "name": "model:framework", + "value": "PyTorch" + }, + { + "name": "model:precision", + "value": "fp16" + }, + { + "name": "model:input", + "value": "transaction text" + } + ], + "externalReferences": [ + { + "type": "model-card", + "url": "https://models.acme.example/fraudbert-base/model-card" + } + ] + }, + { + "type": "data", + "name": "fraudbert-tokenizer", + "version": "1.3.1", + "bom-ref": "tokenizer-fraudbert" + } + ], + "dependencies": [ + { + "ref": "app-fraud-api", + "dependsOn": [ + "lib-transformers", + "model-fraudbert" + ] + } + ] +} diff --git a/modules/fundamental/src/sbom/endpoints/test.rs b/modules/fundamental/src/sbom/endpoints/test.rs index 9e34cd263..44a5efed7 100644 --- a/modules/fundamental/src/sbom/endpoints/test.rs +++ b/modules/fundamental/src/sbom/endpoints/test.rs @@ -1814,6 +1814,35 @@ async fn get_aibom_models(ctx: &TrustifyContext) -> Result<(), anyhow::Error> { Ok(()) } +#[test_context(TrustifyContext)] +#[test(actix_web::test)] +async fn aibom_self_contained_model(ctx: &TrustifyContext) -> Result<(), anyhow::Error> { + let app = caller(ctx).await?; + + let id = ctx + .ingest_document("cyclonedx/ai/TC-4427/SelfContained/fraud-detection-api-4.2.0.json") + .await? + .id + .to_string(); + + let uri = format!("/api/v3/sbom/{id}/models?total=true&counts=true"); + let req = TestRequest::get().uri(&uri).to_request(); + let response: Value = app.call_and_read_body_json(req).await; + log::info!("response:\n{:#}", json!(response)); + let expected = json!({ + "items": [ + { + "id": "model-fraudbert", + "name": "fraudbert-base", + }, + ], + "total": 1 + }); + assert!(response.contains_subset(expected)); + + Ok(()) +} + #[test_context(TrustifyContext)] #[rstest] #[case("hugging", 1)]