// ----------------------------------------------------------------------------- // ParsedSbomParserTests.cs // Sprint: SPRINT_20260119_015_Concelier_sbom_full_extraction // Task: TASK-015-008, TASK-015-009 - Parsed SBOM parsing tests // Description: Unit tests for enriched SBOM parsing // ----------------------------------------------------------------------------- using System.Text; using System.Linq; using FluentAssertions; using Microsoft.Extensions.Logging; using Moq; using StellaOps.Concelier.SbomIntegration.Models; using StellaOps.Concelier.SbomIntegration.Parsing; using StellaOps.TestKit; using Xunit; namespace StellaOps.Concelier.SbomIntegration.Tests; public sealed class ParsedSbomParserTests { private readonly ParsedSbomParser _parser; public ParsedSbomParserTests() { var loggerMock = new Mock>(); _parser = new ParsedSbomParser(loggerMock.Object); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task ParseAsync_CycloneDx_ExtractsMetadataComponentsAndServices() { var content = """ { "bomFormat": "CycloneDX", "specVersion": "1.7", "serialNumber": "urn:uuid:1234", "metadata": { "timestamp": "2026-01-20T00:00:00Z", "component": { "bom-ref": "app", "name": "myapp", "version": "1.0.0" }, "tools": [ { "name": "stella-scanner" } ], "authors": [ { "name": "dev@example.com" } ], "supplier": { "name": "Acme" }, "manufacturer": { "name": "AcmeManu" } }, "components": [ { "bom-ref": "lib", "name": "lib", "version": "2.0.0", "purl": "pkg:npm/lib@2.0.0", "scope": "optional", "modified": true, "supplier": { "name": "LibSupplier", "url": "https://supplier.example.com", "contact": [ { "name": "Supplier Contact", "email": "contact@example.com" } ] }, "manufacturer": { "name": "LibManufacturer" }, "evidence": { "identity": { "field": "purl", "confidence": 0.9, "value": "pkg:npm/lib@2.0.0" }, "occurrences": [ { "location": "src/lib.js", "line": 10, "offset": 4, "symbol": "libfn", "additionalContext": "ctx" } ], "callstack": { "frames": [ { "package": "pkg", "module": "mod", "function": "fn", "parameters": ["a", "b"], "line": 20, "column": 2, "fullFilename": "/src/lib.js" } ] }, "licenses": [ { "expression": "MIT" } ], "copyright": [ "Copyright 2026" ] }, "pedigree": { "ancestors": [ { "bom-ref": "ancestor", "version": "0.1", "description": "base" } ], "variants": [ { "bom-ref": "variant", "version": "2.0" } ], "commits": [ { "uid": "abc123", "message": "fix" } ], "patches": [ { "type": "backport", "diff": { "text": "diff", "url": "https://example.com/diff" } } ], "notes": ["note1", "note2"] }, "cryptoProperties": { "assetType": "algorithm", "algorithmProperties": { "primitive": "hash", "parameterSetIdentifier": "ps1", "curve": "P-256", "executionEnvironment": "software", "certificationLevel": "fips140-2", "mode": "gcm", "padding": "pkcs7", "cryptoFunctions": ["digest"], "classicalSecurityLevel": 128, "nistQuantumSecurityLevel": 1, "keySize": 256 }, "certificateProperties": { "subjectName": "CN=Test", "issuerName": "CA", "notValidBefore": "2026-01-01T00:00:00Z", "notValidAfter": "2027-01-01T00:00:00Z", "signatureAlgorithmRef": "sha256", "subjectPublicKeyRef": "key", "certificateFormat": "x509", "certificateExtension": "ext" }, "protocolProperties": { "type": "tls", "version": "1.3", "cipherSuites": ["TLS_AES_128_GCM_SHA256"], "ikev2TransformTypes": ["aes"], "cryptoRefArray": ["ref1"] }, "relatedCryptoMaterialProperties": { "type": "key", "id": "key-1", "algorithmRef": "alg-1", "securedBy": ["sec1"], "relatedCryptographicAssets": [ { "type": "certificate", "ref": "cert-1" } ] }, "oid": "1.2.3.4" }, "modelCard": { "bom-ref": "model-1", "modelParameters": { "task": "classification", "architectureFamily": "cnn", "modelArchitecture": "resnet", "approach": { "type": "supervised" }, "datasets": [ { "name": "dataset1", "version": "1.0", "url": "https://example.com/ds", "hashes": [ { "alg": "SHA-256", "content": "abcd" } ] } ], "inputs": [ { "format": "image", "description": "jpg" } ], "outputs": [ { "format": "label", "description": "class" } ] }, "quantitativeAnalysis": { "performanceMetrics": [ { "type": "accuracy", "value": "0.9", "slice": "overall", "confidenceInterval": { "lowerBound": "0.88", "upperBound": "0.92" } } ], "graphics": { "collection": [ { "name": "roc", "image": "data:image/png;base64,aa", "description": "ROC" } ] } }, "considerations": { "users": ["devs"], "useCases": ["testing"], "technicalLimitations": ["small dataset"], "ethicalConsiderations": [ { "name": "bias", "mitigationStrategy": "review" } ], "fairnessAssessments": [ { "groupAtRisk": "group1", "benefits": "benefit", "harms": "harm", "mitigationStrategy": "mit" } ], "environmentalConsiderations": { "energyConsumptions": [ { "activity": "training", "activityEnergyCost": "10kWh", "co2CostEquivalent": "5kg", "co2CostOffset": "1kg", "properties": [ { "name": "region", "value": "us" } ], "energyProviders": [ { "bom-ref": "prov", "description": "provider", "organization": { "name": "EnergyCo" }, "energySource": "solar", "energyProvided": "5kWh", "externalReferences": [ { "type": "website", "url": "https://energy.example.com" } ] } ] } ], "properties": [ { "name": "note", "value": "env" } ] } } }, "licenses": [ { "license": { "id": "MIT", "name": "MIT License", "url": "https://example.com/license", "text": { "content": "TUlU", "encoding": "base64" }, "licensing": { "licensor": { "name": "Acme" }, "licensee": "Consumer", "purchaseOrder": "PO-123" } } }, { "expression": "MIT AND (Apache-2.0 OR BSD-3-Clause)" } ], "externalReferences": [ { "type": "website", "url": "https://example.com/lib", "comment": "home" } ] } ], "dependencies": [ { "ref": "app", "dependsOn": ["lib"] } ], "services": [ { "bom-ref": "svc", "name": "api", "version": "1.0.0", "authenticated": true, "x-trust-boundary": true, "endpoints": ["https://api.example.com"], "licenses": [ { "expression": "Apache-2.0" } ], "externalReferences": [ { "type": "documentation", "url": "https://example.com/api-docs" } ] } ], "formulation": [ { "bom-ref": "form-1", "components": [ "lib", { "ref": "app", "properties": [ { "name": "stage", "value": "build" } ] } ], "workflows": [ { "name": "build", "description": "build pipeline", "inputs": ["src"], "outputs": ["artifact"], "tasks": [ { "name": "compile", "description": "compile sources", "inputs": ["src"], "outputs": ["bin"], "parameters": [ { "name": "opt", "value": "O2" } ], "properties": [ { "name": "runner", "value": "msbuild" } ] } ], "properties": [ { "name": "workflow", "value": "ci" } ] } ], "tasks": [ { "name": "package", "description": "package app", "inputs": ["bin"], "outputs": ["artifact"], "parameters": [ { "name": "format", "value": "zip" } ] } ], "properties": [ { "name": "formulation", "value": "v1" } ] } ] } """; using var stream = new MemoryStream(Encoding.UTF8.GetBytes(content)); var result = await _parser.ParseAsync(stream, SbomFormat.CycloneDX); result.Format.Should().Be("cyclonedx"); result.SpecVersion.Should().Be("1.7"); result.SerialNumber.Should().Be("urn:uuid:1234"); result.Metadata.Name.Should().Be("myapp"); result.Metadata.Version.Should().Be("1.0.0"); result.Metadata.Supplier.Should().Be("Acme"); result.Metadata.Manufacturer.Should().Be("AcmeManu"); result.Components.Should().Contain(c => c.BomRef == "app"); result.Components.Should().Contain(c => c.Purl == "pkg:npm/lib@2.0.0"); var lib = result.Components.Single(c => c.BomRef == "lib"); lib.ExternalReferences.Should().ContainSingle(r => r.Type == "website" && r.Url == "https://example.com/lib"); lib.Scope.Should().Be(ComponentScope.Optional); lib.Modified.Should().BeTrue(); lib.Supplier.Should().NotBeNull(); lib.Supplier!.Name.Should().Be("LibSupplier"); lib.Supplier!.Url.Should().Be("https://supplier.example.com"); lib.Manufacturer.Should().NotBeNull(); lib.Manufacturer!.Name.Should().Be("LibManufacturer"); lib.Evidence.Should().NotBeNull(); lib.Evidence!.Identity.Should().NotBeNull(); lib.Evidence!.Identity!.Field.Should().Be("purl"); lib.Evidence!.Identity!.Confidence.Should().Be(0.9); lib.Evidence!.Occurrences.Should().ContainSingle(o => o.Location == "src/lib.js"); lib.Evidence!.Callstack.Should().NotBeNull(); lib.Evidence!.Callstack!.Frames.Should().ContainSingle(f => f.Function == "fn"); lib.Evidence!.Licenses.Should().ContainSingle(l => l.Expression != null); lib.Evidence!.Copyrights.Should().ContainSingle(c => c == "Copyright 2026"); lib.Pedigree.Should().NotBeNull(); lib.Pedigree!.Ancestors.Should().ContainSingle(a => a.BomRef == "ancestor"); lib.Pedigree!.Variants.Should().ContainSingle(v => v.BomRef == "variant"); lib.Pedigree!.Commits.Should().ContainSingle(c => c.BomRef == "abc123"); lib.Pedigree!.Patches.Should().ContainSingle(p => p.Type == "backport"); lib.Pedigree!.Notes.Should().Contain(n => n == "note1"); lib.CryptoProperties.Should().NotBeNull(); lib.CryptoProperties!.AssetType.Should().Be(CryptoAssetType.Algorithm); lib.CryptoProperties!.AlgorithmProperties.Should().NotBeNull(); lib.CryptoProperties!.AlgorithmProperties!.Primitive.Should().Be(CryptoPrimitive.Hash); lib.CryptoProperties!.CertificateProperties.Should().NotBeNull(); lib.CryptoProperties!.CertificateProperties!.SubjectName.Should().Be("CN=Test"); lib.CryptoProperties!.ProtocolProperties.Should().NotBeNull(); lib.CryptoProperties!.ProtocolProperties!.Type.Should().Be("tls"); lib.CryptoProperties!.RelatedCryptoMaterial.Should().NotBeNull(); lib.CryptoProperties!.RelatedCryptoMaterial!.Reference.Should().Be("key-1"); lib.CryptoProperties!.RelatedCryptoMaterial!.MaterialRefs.Should().Contain("cert-1"); lib.ModelCard.Should().NotBeNull(); lib.ModelCard!.BomRef.Should().Be("model-1"); lib.ModelCard!.ModelParameters.Should().NotBeNull(); lib.ModelCard!.ModelParameters!.Task.Should().Be("classification"); lib.ModelCard!.ModelParameters!.Datasets.Should().ContainSingle(d => d.Name == "dataset1"); lib.ModelCard!.QuantitativeAnalysis.Should().NotBeNull(); lib.ModelCard!.QuantitativeAnalysis!.PerformanceMetrics.Should().ContainSingle(m => m.Type == "accuracy"); lib.ModelCard!.Considerations.Should().NotBeNull(); lib.ModelCard!.Considerations!.Users.Should().ContainSingle(u => u == "devs"); lib.Licenses.Should().HaveCount(2); lib.Licenses[0].SpdxId.Should().Be("MIT"); lib.Licenses[0].Name.Should().Be("MIT License"); lib.Licenses[0].Url.Should().Be("https://example.com/license"); lib.Licenses[0].Text.Should().Be("MIT"); lib.Licenses[0].Licensing.Should().NotBeNull(); lib.Licenses[0].Licensing!.Licensor.Should().Be("Acme"); lib.Licenses[0].Licensing!.Licensee.Should().Be("Consumer"); lib.Licenses[0].Licensing!.PurchaseOrder.Should().Be("PO-123"); lib.Licenses[1].Expression.Should().BeOfType(); var andExpr = (ConjunctiveSet)lib.Licenses[1].Expression!; andExpr.Members.Should().HaveCount(2); andExpr.Members[0].Should().BeOfType() .Which.Id.Should().Be("MIT"); andExpr.Members[1].Should().BeOfType(); var orExpr = (DisjunctiveSet)andExpr.Members[1]; orExpr.Members.OfType() .Should() .ContainSingle(license => license.Id == "Apache-2.0"); result.Dependencies.Should().ContainSingle(d => d.SourceRef == "app"); result.Services.Should().ContainSingle(s => s.Name == "api"); result.Services[0].Authenticated.Should().BeTrue(); result.Services[0].CrossesTrustBoundary.Should().BeTrue(); result.Services[0].Licenses.Should().ContainSingle(); result.Services[0].ExternalReferences.Should().ContainSingle(r => r.Type == "documentation" && r.Url == "https://example.com/api-docs"); result.Formulation.Should().NotBeNull(); result.Formulation!.BomRef.Should().Be("form-1"); result.Formulation.Components.Should().HaveCount(2); result.Formulation.Components.Should().ContainSingle(c => c.BomRef == "lib"); result.Formulation.Components.Should().ContainSingle(c => c.BomRef == "app" && c.Properties.ContainsKey("stage")); result.Formulation.Workflows.Should().ContainSingle(w => w.Name == "build"); var workflow = result.Formulation.Workflows.Single(w => w.Name == "build"); workflow.InputRefs.Should().Contain("src"); workflow.OutputRefs.Should().Contain("artifact"); workflow.Tasks.Should().ContainSingle(t => t.Name == "compile"); workflow.Tasks[0].Parameters.Should().ContainKey("opt").WhoseValue.Should().Be("O2"); workflow.Tasks[0].Properties.Should().ContainKey("runner").WhoseValue.Should().Be("msbuild"); result.Formulation.Tasks.Should().ContainSingle(t => t.Name == "package"); result.Formulation.Tasks[0].Parameters.Should().ContainKey("format").WhoseValue.Should().Be("zip"); result.Formulation.Properties.Should().ContainKey("formulation").WhoseValue.Should().Be("v1"); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task ParseAsync_Spdx3_ExtractsDocumentAndPackageMetadata() { var content = """ { "@context": "https://spdx.org/rdf/3.0.1/spdx-context.jsonld", "@graph": [ { "@type": "SpdxDocument", "spdxId": "urn:doc", "name": "sbom-doc", "creationInfo": { "specVersion": "3.0.1", "created": "2026-01-20T00:00:00Z", "createdBy": ["org:Acme"], "createdUsing": ["tool:stella"], "profile": ["https://spdx.org/rdf/3.0.1/terms/Core/ProfileIdentifierType/core"] }, "rootElement": ["spdx:pkg:root"], "namespaceMap": [ { "prefix": "ex", "namespace": "https://example.com" } ], "import": [ { "externalSpdxId": "urn:ext" } ] }, { "@type": "build_Build", "spdxId": "spdx:build:1", "buildId": "build-123", "buildType": "release", "buildStartTime": "2026-01-20T01:00:00Z", "buildEndTime": "2026-01-20T01:30:00Z", "configSourceEntrypoint": "build.yaml", "configSourceDigest": "sha256:abcd", "configSourceUri": "https://example.com/build.yaml", "environment": [ { "name": "OS", "value": "linux" } ], "parameters": { "opt": "O2", "threads": 8 } }, { "@type": "software_Package", "spdxId": "spdx:pkg:root", "name": "root", "software_packageVersion": "1.0.0", "packageUrl": "pkg:npm/root@1.0.0", "simplelicensing_licenseExpression": "Apache-2.0 WITH LLVM-exception", "verifiedUsing": [ { "algorithm": "SHA256", "hashValue": "abc" } ], "externalRef": [ { "externalRefType": "purl", "locator": "pkg:npm/root@1.0.0", "comment": "source" } ], "externalIdentifier": [ { "externalIdentifierType": "cpe23", "identifier": "cpe:2.3:a:root" } ] } ] } """; using var stream = new MemoryStream(Encoding.UTF8.GetBytes(content)); var result = await _parser.ParseAsync(stream, SbomFormat.SPDX); result.Format.Should().Be("spdx"); result.SpecVersion.Should().Be("3.0.1"); result.SerialNumber.Should().Be("urn:doc"); result.Metadata.Name.Should().Be("sbom-doc"); result.Metadata.RootComponentRef.Should().Be("spdx:pkg:root"); result.Metadata.NamespaceMap.Should().ContainSingle(map => map.Prefix == "ex"); result.BuildInfo.Should().NotBeNull(); result.BuildInfo!.BuildId.Should().Be("build-123"); result.BuildInfo.BuildType.Should().Be("release"); result.BuildInfo.BuildStartTime.Should().Be(DateTimeOffset.Parse("2026-01-20T01:00:00Z")); result.BuildInfo.BuildEndTime.Should().Be(DateTimeOffset.Parse("2026-01-20T01:30:00Z")); result.BuildInfo.ConfigSourceEntrypoint.Should().Be("build.yaml"); result.BuildInfo.ConfigSourceDigest.Should().Be("sha256:abcd"); result.BuildInfo.ConfigSourceUri.Should().Be("https://example.com/build.yaml"); result.BuildInfo.Environment.Should().ContainKey("OS").WhoseValue.Should().Be("linux"); result.BuildInfo.Parameters.Should().ContainKey("opt").WhoseValue.Should().Be("O2"); result.BuildInfo.Parameters.Should().ContainKey("threads").WhoseValue.Should().Be("8"); var root = result.Components.Single(c => c.Purl == "pkg:npm/root@1.0.0"); root.Cpe.Should().Be("cpe:2.3:a:root"); root.Hashes.Should().ContainSingle(h => h.Algorithm == "SHA256" && h.Value == "abc"); root.ExternalReferences.Should().ContainSingle(r => r.Type == "purl" && r.Url == "pkg:npm/root@1.0.0"); root.Licenses.Should().ContainSingle(); root.Licenses[0].Expression.Should().BeOfType(); var withExpr = (WithException)root.Licenses[0].Expression!; withExpr.Exception.Should().Be("LLVM-exception"); withExpr.License.Should().BeOfType() .Which.Id.Should().Be("Apache-2.0"); } }