- 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.
332 lines
7.3 KiB
Markdown
332 lines
7.3 KiB
Markdown
# 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": "<base64-encoded-sbom-v1>",
|
|
"target": "<base64-encoded-sbom-v2>",
|
|
"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)
|