# SBOM/VEX Deterministic Diff Rules (SP5) Status: Draft · Date: 2025-12-04 Scope: Define deterministic diff rules and fixtures for SBOM/VEX deltas, ensuring reproducible comparison results and stable hash expectations. ## Objectives - Enable deterministic diffing between SBOM/VEX versions. - Define canonical ordering for diff output. - Provide fixtures for validating diff implementations. - Ensure diff results are hash-stable. ## Diff Operations ### Supported Operations | Operation | Description | Output Format | |-----------|-------------|---------------| | `component-diff` | Compare component lists between SBOMs | JSON Patch | | `vulnerability-diff` | Compare vulnerability lists | JSON Patch | | `vex-diff` | Compare VEX statements | JSON Patch | | `full-diff` | Complete SBOM/VEX comparison | Combined JSON Patch | ### JSON Patch Format Diff output uses RFC 6902 JSON Patch format: ```json { "patch": [ { "op": "add", "path": "/components/2", "value": { "type": "library", "name": "new-lib", "version": "1.0.0", "purl": "pkg:npm/new-lib@1.0.0" } }, { "op": "remove", "path": "/components/0" }, { "op": "replace", "path": "/components/1/version", "value": "2.0.0" } ], "meta": { "source": "sbom-v1.json", "target": "sbom-v2.json", "sourceHash": "b3:...", "targetHash": "b3:...", "patchHash": "b3:...", "timestamp": "2025-12-04T00:00:00Z" } } ``` ## Determinism Rules ### Ordering 1. **Operations**: `remove` first (descending path order), then `replace`, then `add` 2. **Paths**: Lexicographic sort within operation type 3. **Array indices**: Stable indices based on sort keys (purl for components, id for vulns) ### Canonical Comparison When comparing elements for diff: | Element Type | Sort Keys | Tie Breakers | |--------------|-----------|--------------| | Component | `purl` | `name`, `version` | | Vulnerability | `id` | `source.name`, `ratings[0].score` | | VEX Statement | `vulnerability` | `products[0].purl`, `timestamp` | | Service | `name` | `version` | | Property | `name` | - | ### Hash Computation Diff output hash computed as: 1. Serialize patch array to canonical JSON (sorted keys, no whitespace) 2. Compute BLAKE3-256 over UTF-8 bytes 3. Record in `meta.patchHash` ## Component Diff ### Input ```json // sbom-v1.json { "components": [ {"name": "lib-a", "version": "1.0.0", "purl": "pkg:npm/lib-a@1.0.0"}, {"name": "lib-b", "version": "1.0.0", "purl": "pkg:npm/lib-b@1.0.0"} ] } // sbom-v2.json { "components": [ {"name": "lib-a", "version": "1.0.1", "purl": "pkg:npm/lib-a@1.0.1"}, {"name": "lib-c", "version": "1.0.0", "purl": "pkg:npm/lib-c@1.0.0"} ] } ``` ### Output ```json { "patch": [ { "op": "remove", "path": "/components/1", "comment": "removed pkg:npm/lib-b@1.0.0" }, { "op": "replace", "path": "/components/0/version", "value": "1.0.1", "comment": "upgraded pkg:npm/lib-a@1.0.0 -> 1.0.1" }, { "op": "replace", "path": "/components/0/purl", "value": "pkg:npm/lib-a@1.0.1" }, { "op": "add", "path": "/components/1", "value": {"name": "lib-c", "version": "1.0.0", "purl": "pkg:npm/lib-c@1.0.0"}, "comment": "added pkg:npm/lib-c@1.0.0" } ] } ``` ## Vulnerability Diff ### Added/Removed Vulnerabilities ```json { "patch": [ { "op": "add", "path": "/vulnerabilities/-", "value": { "id": "CVE-2025-0002", "ratings": [{"method": "CVSSv4", "score": 5.3}] }, "comment": "new vulnerability CVE-2025-0002" }, { "op": "remove", "path": "/vulnerabilities/0", "comment": "resolved CVE-2025-0001" } ] } ``` ### Rating Changes ```json { "patch": [ { "op": "replace", "path": "/vulnerabilities/0/ratings/0/score", "value": 9.0, "comment": "CVE-2025-0001 score updated 8.5 -> 9.0" } ] } ``` ## VEX Diff ### Statement Status Changes ```json { "patch": [ { "op": "replace", "path": "/statements/0/status", "value": "not_affected", "comment": "CVE-2025-0001 status changed affected -> not_affected" }, { "op": "add", "path": "/statements/0/justification", "value": { "category": "vulnerable_code_not_present", "details": "Function patched in v2.0.1" } } ] } ``` ## Fixtures ### Directory Structure ``` docs/modules/policy/fixtures/diff-rules/ ├── component-diff/ │ ├── input-v1.json │ ├── input-v2.json │ ├── expected-diff.json │ └── hashes.txt ├── vulnerability-diff/ │ ├── input-v1.json │ ├── input-v2.json │ ├── expected-diff.json │ └── hashes.txt ├── vex-diff/ │ ├── input-v1.json │ ├── input-v2.json │ ├── expected-diff.json │ └── hashes.txt └── full-diff/ ├── sbom-v1.json ├── sbom-v2.json ├── expected-diff.json └── hashes.txt ``` ### Sample Fixture (Component Diff) ```json // docs/modules/policy/fixtures/diff-rules/component-diff/expected-diff.json { "patch": [ {"op": "remove", "path": "/components/1"}, {"op": "replace", "path": "/components/0/version", "value": "1.0.1"}, {"op": "replace", "path": "/components/0/purl", "value": "pkg:npm/lib-a@1.0.1"}, {"op": "add", "path": "/components/1", "value": {"name": "lib-c", "version": "1.0.0", "purl": "pkg:npm/lib-c@1.0.0"}} ], "meta": { "sourceHash": "b3:...", "targetHash": "b3:...", "patchHash": "b3:..." } } ``` ## CI Validation ```bash #!/bin/bash # scripts/policy/validate-diff-fixtures.sh FIXTURE_DIR="docs/modules/policy/fixtures/diff-rules" for category in component-diff vulnerability-diff vex-diff full-diff; do echo "Validating ${category}..." # Run diff stellaops-diff \ --source "${FIXTURE_DIR}/${category}/input-v1.json" \ --target "${FIXTURE_DIR}/${category}/input-v2.json" \ --output /tmp/actual-diff.json # Compare with expected expected_hash=$(grep "expected-diff.json" "${FIXTURE_DIR}/${category}/hashes.txt" | awk '{print $2}') actual_hash=$(b3sum /tmp/actual-diff.json | cut -d' ' -f1) if [[ "${actual_hash}" != "${expected_hash}" ]]; then echo "FAIL: ${category} diff hash mismatch" diff <(jq -S . "${FIXTURE_DIR}/${category}/expected-diff.json") <(jq -S . /tmp/actual-diff.json) exit 1 fi echo "PASS: ${category}" done ``` ## API Integration ### Diff Endpoint ```http POST /api/v1/sbom/diff Content-Type: application/json { "source": "", "target": "", "options": { "includeComments": true, "format": "json-patch" } } ``` ### Response ```json { "diff": { "patch": [...], "meta": { "sourceHash": "b3:...", "targetHash": "b3:...", "patchHash": "b3:...", "componentChanges": { "added": 1, "removed": 1, "modified": 1 }, "vulnerabilityChanges": { "added": 0, "removed": 1, "modified": 0 } } } } ``` ## Links - Sprint: `docs/implplan/SPRINT_0186_0001_0001_record_deterministic_execution.md` (SP5) - Spine Versioning: `docs/modules/policy/contracts/spine-versioning-plan.md` (SP1)