238 lines
7.0 KiB
Markdown
238 lines
7.0 KiB
Markdown
# Function Map V1 Contract
|
|
|
|
> **Predicate Type:** `https://stella.ops/predicates/function-map/v1`
|
|
> **DSSE Payload Type:** `application/vnd.stellaops.function-map.v1+json`
|
|
> **Schema Version:** `1.0.0`
|
|
|
|
## Overview
|
|
|
|
A function map predicate declares the expected call paths for a service component, enabling verification of runtime behavior against static analysis. It follows the [in-toto attestation](https://github.com/in-toto/attestation) framework.
|
|
|
|
---
|
|
|
|
## Predicate Schema
|
|
|
|
```json
|
|
{
|
|
"type": "https://stella.ops/predicates/function-map/v1",
|
|
"subject": {
|
|
"purl": "pkg:oci/my-service@sha256:abc123...",
|
|
"digest": { "sha256": "abc123..." }
|
|
},
|
|
"predicate": {
|
|
"service": "my-backend",
|
|
"build_id": "build-456",
|
|
"expected_paths": [...],
|
|
"coverage": {
|
|
"min_observation_rate": 0.95,
|
|
"window_seconds": 1800,
|
|
"fail_on_unexpected": false
|
|
},
|
|
"generated_at": "2026-01-23T10:00:00Z",
|
|
"generated_from": {
|
|
"sbom_ref": "oci://registry/sbom@sha256:...",
|
|
"static_analysis_ref": "oci://registry/analysis@sha256:..."
|
|
},
|
|
"generator": {
|
|
"name": "stella-cli",
|
|
"version": "2.0.0",
|
|
"commit": "abc123"
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Subject
|
|
|
|
| Field | Type | Required | Description |
|
|
|-------|------|----------|-------------|
|
|
| `purl` | string | Yes | Package URL of the subject artifact |
|
|
| `digest` | object | Yes | Content digest (sha256, sha512, etc.) |
|
|
|
|
---
|
|
|
|
## Predicate Fields
|
|
|
|
| Field | Type | Required | Description |
|
|
|-------|------|----------|-------------|
|
|
| `service` | string | Yes | Service name for correlation |
|
|
| `build_id` | string | No | Build identifier for provenance correlation |
|
|
| `expected_paths` | array | Yes | List of expected call paths |
|
|
| `coverage` | object | Yes | Coverage thresholds for verification |
|
|
| `generated_at` | ISO 8601 | Yes | Generation timestamp |
|
|
| `generated_from` | object | No | Source references (SBOM, static analysis) |
|
|
| `generator` | object | No | Tool that generated the predicate |
|
|
|
|
---
|
|
|
|
## Expected Path
|
|
|
|
Each expected path represents a call chain starting from an entrypoint:
|
|
|
|
```json
|
|
{
|
|
"path_id": "path-001",
|
|
"entrypoint": {
|
|
"symbol": "handleRequest",
|
|
"node_hash": "sha256:..."
|
|
},
|
|
"expected_calls": [
|
|
{
|
|
"symbol": "crypto_sign",
|
|
"purl": "pkg:deb/libcrypto3@3.0.0",
|
|
"node_hash": "sha256:...",
|
|
"probe_types": ["uprobe"],
|
|
"optional": false,
|
|
"function_address": null,
|
|
"binary_path": "/usr/lib/libcrypto.so.3"
|
|
}
|
|
],
|
|
"path_hash": "sha256:...",
|
|
"optional": false,
|
|
"strict_ordering": false,
|
|
"tags": ["crypto"]
|
|
}
|
|
```
|
|
|
|
| Field | Type | Required | Description |
|
|
|-------|------|----------|-------------|
|
|
| `path_id` | string | Yes | Unique path identifier |
|
|
| `entrypoint` | object | Yes | Path entry point (symbol + node_hash) |
|
|
| `expected_calls` | array | Yes | List of expected function calls |
|
|
| `path_hash` | string | Yes | SHA-256(entrypoint \|\| sorted calls) |
|
|
| `optional` | boolean | No | Whether this path is optional (default false) |
|
|
| `strict_ordering` | boolean | No | Ordered sequence vs unordered set (default false) |
|
|
| `tags` | array | No | Categorization tags (crypto, auth, network, etc.) |
|
|
|
|
---
|
|
|
|
## Expected Call
|
|
|
|
| Field | Type | Required | Description |
|
|
|-------|------|----------|-------------|
|
|
| `symbol` | string | Yes | Function name (demangled) |
|
|
| `purl` | string | Yes | Package URL of the component containing this function |
|
|
| `node_hash` | string | Yes | SHA-256(PURL + normalized symbol) |
|
|
| `probe_types` | array | Yes | Acceptable probe types for observation |
|
|
| `optional` | boolean | No | Whether this call is optional (default false) |
|
|
| `function_address` | string | No | Address hint for probe attachment |
|
|
| `binary_path` | string | No | Binary path for uprobe attachment |
|
|
|
|
### Probe Types
|
|
|
|
| Type | Description |
|
|
|------|-------------|
|
|
| `kprobe` | Kernel function entry |
|
|
| `kretprobe` | Kernel function return |
|
|
| `uprobe` | User-space function entry |
|
|
| `uretprobe` | User-space function return |
|
|
| `tracepoint` | Kernel tracepoint |
|
|
| `usdt` | User-space statically defined tracing |
|
|
|
|
---
|
|
|
|
## Coverage Thresholds
|
|
|
|
| Field | Type | Default | Description |
|
|
|-------|------|---------|-------------|
|
|
| `min_observation_rate` | double | 0.95 | Minimum fraction of paths that must be observed |
|
|
| `window_seconds` | integer | 1800 | Observation window duration |
|
|
| `fail_on_unexpected` | boolean | false | Whether unexpected symbols cause verification failure |
|
|
|
|
---
|
|
|
|
## Node Hash Recipe
|
|
|
|
Node hashes provide content-addressable identifiers for function calls, matching the [Witness V1](witness-v1.md) convention:
|
|
|
|
```
|
|
node_hash = SHA-256(PURL + ":" + normalize(symbol))
|
|
```
|
|
|
|
Where `normalize(symbol)`:
|
|
1. Demangle C++/Rust symbols
|
|
2. Strip leading underscores (platform convention)
|
|
3. Lowercase the result
|
|
4. Remove whitespace
|
|
|
|
### Path Hash Recipe
|
|
|
|
```
|
|
path_hash = SHA-256(entrypoint.node_hash + ":" + sort(calls.map(c => c.node_hash)).join(":"))
|
|
```
|
|
|
|
The path hash is independent of call ordering (sorted) unless `strict_ordering` is true, in which case calls are not sorted before hashing.
|
|
|
|
---
|
|
|
|
## Coverage Calculation Algorithm
|
|
|
|
```
|
|
total_required = count(paths where optional == false)
|
|
observed_required = count(paths where optional == false AND has_matching_observation)
|
|
|
|
observation_rate = observed_required / total_required
|
|
= 0.0 if total_required == 0
|
|
|
|
verified = observation_rate >= coverage.min_observation_rate
|
|
```
|
|
|
|
For each path, an observation "matches" when:
|
|
- At least one observation has a `node_hash` matching any call in the path
|
|
- The observation falls within the time window
|
|
- The probe type is in the call's `probe_types` list
|
|
|
|
---
|
|
|
|
## Verification Algorithm
|
|
|
|
```
|
|
VERIFY(predicate, observations, options):
|
|
1. Filter observations to time window [now - window_seconds, now]
|
|
2. For each required expected_path:
|
|
a. For each expected_call in path:
|
|
- Find observations matching node_hash AND probe_type
|
|
- Mark call as "observed" if any match found
|
|
b. Mark path as "covered" if entrypoint OR any call observed
|
|
3. Compute observation_rate = covered_paths / required_paths
|
|
4. Collect unexpected = observations not matching any expected call
|
|
5. Collect missing = required calls with no matching observation
|
|
6. verified = observation_rate >= min_observation_rate
|
|
AND (NOT fail_on_unexpected OR unexpected.count == 0)
|
|
7. Return result with breakdown, unexpected, missing
|
|
```
|
|
|
|
---
|
|
|
|
## Media Types
|
|
|
|
| Usage | Media Type |
|
|
|-------|-----------|
|
|
| Function map predicate | `application/vnd.stella.function-map+json` |
|
|
| DSSE-signed predicate | `application/vnd.dsse+json` |
|
|
| Observations | `application/x-ndjson` |
|
|
| Verification report | `application/vnd.stella.verification-report+json` |
|
|
|
|
---
|
|
|
|
## Observation Record (NDJSON)
|
|
|
|
Each line in an observations file:
|
|
|
|
```json
|
|
{
|
|
"observation_id": "obs-123",
|
|
"node_hash": "sha256:...",
|
|
"function_name": "crypto_sign",
|
|
"probe_type": "uprobe",
|
|
"observed_at": "2026-01-23T10:05:00Z",
|
|
"observation_count": 42,
|
|
"container_id": "abc123",
|
|
"pod_name": "my-service-pod-xyz",
|
|
"namespace": "production",
|
|
"duration_microseconds": 150
|
|
}
|
|
```
|