Files
git.stella-ops.org/docs/modules/policy/contracts/sbom-vex-diff-rules.md
StellaOps Bot 8768c27f30
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Signals DSSE Sign & Evidence Locker / sign-signals-artifacts (push) Has been cancelled
Signals DSSE Sign & Evidence Locker / verify-signatures (push) Has been cancelled
Add signal contracts for reachability, exploitability, trust, and unknown symbols
- Introduced `ReachabilityState`, `RuntimeHit`, `ExploitabilitySignal`, `ReachabilitySignal`, `SignalEnvelope`, `SignalType`, `TrustSignal`, and `UnknownSymbolSignal` records to define various signal types and their properties.
- Implemented JSON serialization attributes for proper data interchange.
- Created project files for the new signal contracts library and corresponding test projects.
- Added deterministic test fixtures for micro-interaction testing.
- Included cryptographic keys for secure operations with cosign.
2025-12-05 00:27:00 +02:00

7.3 KiB

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:

{
  "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

// 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

{
  "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

{
  "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

{
  "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

{
  "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)

// 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

#!/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

POST /api/v1/sbom/diff
Content-Type: application/json

{
  "source": "<base64-encoded-sbom-v1>",
  "target": "<base64-encoded-sbom-v2>",
  "options": {
    "includeComments": true,
    "format": "json-patch"
  }
}

Response

{
  "diff": {
    "patch": [...],
    "meta": {
      "sourceHash": "b3:...",
      "targetHash": "b3:...",
      "patchHash": "b3:...",
      "componentChanges": {
        "added": 1,
        "removed": 1,
        "modified": 1
      },
      "vulnerabilityChanges": {
        "added": 0,
        "removed": 1,
        "modified": 0
      }
    }
  }
}
  • Sprint: docs/implplan/SPRINT_0186_0001_0001_record_deterministic_execution.md (SP5)
  • Spine Versioning: docs/modules/policy/contracts/spine-versioning-plan.md (SP1)