Function Map V1 Contract
Predicate Type (canonical):
https://stella.ops/predicates/function-map/v1Predicate Type (legacy alias):stella.ops/functionMap@v1DSSE Payload Type:application/vnd.stellaops.function-map.v1+jsonJSON Schema URI:https://stellaops.org/schemas/function-map-v1.jsonSchema Version:1.0.0
Field-naming note: The predicate (subject + predicate payload) serializes with camelCase JSON property names (
buildId,expectedPaths,nodeHash, …). The runtime observation record and the verification result serialize with snake_case property names (node_hash,function_name,observation_rate, …). Both conventions are intentional and are preserved below.
Overview
This contract is for engineers generating, signing, or verifying function maps in the Stella Ops reachability pipeline, and for clients of the Platform function-map API and CLI. A function map predicate declares the expected call paths for a service component, enabling verification of runtime behavior (eBPF observations) against static analysis. It serves as the “contract” that runtime observations are verified against, and is typically generated from SBOM + static analysis, then signed for attestation. It follows the in-toto attestation framework.
Ground-truth implementation:
src/Scanner/__Libraries/StellaOps.Scanner.Reachability/FunctionMap/(predicate model, generator, claim verifier).src/Platform/StellaOps.Platform.WebService/Endpoints/FunctionMapEndpoints.cs(HTTP API).src/Cli/StellaOps.Cli/Commands/FunctionMap/FunctionMapCommandGroup.cs(CLI).src/Scanner/__Libraries/StellaOps.Scanner.Storage/Postgres/Migrations/023_runtime_observations.sql(observation persistence).
Predicate Schema
The top-level predicate type field is serialized as _type (not type).
{
"_type": "https://stella.ops/predicates/function-map/v1",
"subject": {
"purl": "pkg:oci/my-service@sha256:abc123...",
"digest": { "sha256": "abc123..." },
"name": "my-service"
},
"predicate": {
"schemaVersion": "1.0.0",
"service": "my-backend",
"buildId": "build-456",
"generatedFrom": {
"sbomRef": "sha256:...",
"staticAnalysisRef": "sha256:...",
"binaryAnalysisRef": "sha256:...",
"hotFunctionPatterns": ["SSL_*", "EVP_*"]
},
"expectedPaths": [...],
"coverage": {
"minObservationRate": 0.95,
"windowSeconds": 1800,
"minObservationCount": null,
"failOnUnexpected": false
},
"generatedAt": "2026-01-23T10:00:00Z",
"generator": {
"name": "StellaOps.Scanner.Reachability.FunctionMap",
"version": "2.0.0",
"commit": "abc123"
},
"metadata": {}
}
}
Subject
Source: FunctionMapSubject in FunctionMapPredicate.cs.
| Field | Type | Required | Description |
|---|---|---|---|
purl | string | Yes | Package URL of the subject artifact |
digest | object | Yes | Map of algorithm (sha256, sha512) to hex-encoded hash |
name | string | No | Optional artifact name |
Predicate Fields
Source: FunctionMapPredicatePayload in FunctionMapPredicate.cs.
| Field | Type | Required | Description |
|---|---|---|---|
schemaVersion | string | No | Schema version; defaults to 1.0.0 |
service | string | Yes | Service name that this function map applies to |
buildId | string | No | Build ID/version, for correlating with a specific build |
generatedFrom | object | No | Source references (SBOM, static/binary analysis, hot-function patterns) |
expectedPaths | array | Yes | List of expected call paths |
coverage | object | Yes | Coverage thresholds for verification |
generatedAt | ISO 8601 | Yes | Generation timestamp |
generator | object | No | Tool that generated the predicate (name, version, commit) |
metadata | object | No | Free-form extension metadata |
Generated From
Source: FunctionMapGeneratedFrom.
| Field | Type | Required | Description |
|---|---|---|---|
sbomRef | string | No | SHA-256 digest (sha256:...) of the SBOM used |
staticAnalysisRef | string | No | SHA-256 digest of the static-analysis results used |
binaryAnalysisRef | string | No | SHA-256 digest of the binary-analysis results used |
hotFunctionPatterns | array | No | Glob patterns used to filter hot functions |
Expected Path
Source: ExpectedPath / PathEntrypoint in ExpectedPath.cs. Each expected path represents a call chain starting from an entrypoint:
{
"pathId": "path-001",
"description": "TLS handshake via OpenSSL",
"entrypoint": {
"symbol": "handleRequest",
"nodeHash": "sha256:...",
"purl": "pkg:oci/my-service@sha256:..."
},
"expectedCalls": [
{
"symbol": "crypto_sign",
"purl": "pkg:deb/libcrypto3@3.0.0",
"nodeHash": "sha256:...",
"probeTypes": ["uprobe"],
"optional": false,
"description": "libsodium signing",
"functionAddress": null,
"binaryPath": "/usr/lib/libcrypto.so.3"
}
],
"pathHash": "sha256:...",
"optional": false,
"strictOrdering": false,
"tags": ["crypto"]
}
| Field | Type | Required | Description |
|---|---|---|---|
pathId | string | Yes | Unique path identifier |
description | string | No | Human-readable description of the call path |
entrypoint | object | Yes | Path entry point (symbol, nodeHash, optional purl) |
expectedCalls | array | Yes | List of expected function calls |
pathHash | string | Yes | SHA-256(entrypoint.nodeHash || sorted call node hashes) (sha256:...) |
optional | boolean | No | Whether this path is optional (default false). Optional paths are skipped in the coverage calculation |
strictOrdering | boolean | No | Ordered sequence vs unordered set (default false). NOTE: declared in the schema but not yet enforced by ClaimVerifier (set semantics are always used) |
tags | array | No | Categorization tags (crypto, auth, network, etc.) |
Path Entrypoint
| Field | Type | Required | Description |
|---|---|---|---|
symbol | string | Yes | Symbol name of the entrypoint function |
nodeHash | string | Yes | Node hash for this entrypoint (sha256:...) |
purl | string | No | PURL of the component containing the entrypoint |
Expected Call
Source: ExpectedCall in ExpectedCall.cs.
| Field | Type | Required | Description |
|---|---|---|---|
symbol | string | Yes | Function name (e.g. SSL_connect) |
purl | string | Yes | Package URL of the component containing this function |
nodeHash | string | Yes | SHA-256(normalized PURL + ":" + normalized symbol) (sha256:...) |
probeTypes | array | Yes | Acceptable probe types for observation |
optional | boolean | No | Whether this call is optional (default false). Optional calls do not count toward coverage |
description | string | No | Human-readable description of the expected call |
functionAddress | uint64 | No | Address hint for probe attachment (numeric, not string) |
binaryPath | string | No | Binary path for uprobe attachment in containerized environments |
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
Source: CoverageThresholds in FunctionMapPredicate.cs.
| Field | Type | Default | Description |
|---|---|---|---|
minObservationRate | double | 0.95 | Minimum fraction of expected (non-optional) calls that must be observed; range 0.0–1.0 |
windowSeconds | integer | 1800 | Observation window duration in seconds |
minObservationCount | integer | null | Minimum number of observations required before verification can succeed (optional; declared in the schema but not yet enforced by ClaimVerifier) |
failOnUnexpected | boolean | false | Whether unexpected symbols cause verification failure |
Node Hash Recipe
Node hashes provide content-addressable identifiers for function calls. The recipe is implemented in FunctionMapGenerator.ComputeNodeHash and intentionally matches PathWitnessBuilder (see Witness V1):
nodeHash = "sha256:" + lowerhex(SHA-256(normalize_purl(PURL) + ":" + normalize_symbol(symbol)))
Where:
normalize_purl(PURL)=trim()thentoLowerInvariant()(lower-cased).normalize_symbol(symbol)=trim()only.
The symbol is not demangled, not lower-cased, and leading underscores are not stripped — only surrounding whitespace is trimmed. (An earlier revision of this doc described a demangle/lowercase/strip-underscores recipe that the code does not implement.)
The output digest is hex-encoded lower-case and prefixed with sha256:.
Path Hash Recipe
Source: FunctionMapGenerator.ComputePathHash.
pathHash = "sha256:" + lowerhex(SHA-256(
join(":", [entrypoint_hex] + sort(call_node_hashes_hex))))
Note the actual algorithm strips the sha256: prefix from each component hash before joining, sorts the call hashes (ordinal) while keeping the entrypoint hash first, and joins with :. The entrypoint hash is always first and is not part of the sort. Sorting is applied regardless of strictOrdering (the generator does not currently vary hashing on strictOrdering).
Coverage Calculation Algorithm
Source: ClaimVerifier.ComputeCoverage. Coverage is computed over expected calls (node hashes), not over whole paths:
for each non-optional path:
for each non-optional expected_call:
total_expected_calls += 1
if observation exists for call.nodeHash:
observed_calls += 1
mark path as observed
coverageRate = observed_calls / total_expected_calls
= 0.0 if total_expected_calls == 0
ComputeCoverage also reports totalPaths, observedPaths (paths with at least one observed call), and unexpectedSymbolCount (distinct observation node hashes not in the expected set).
ComputeCoveragematches purely onnodeHashpresence and does not filter by time window or probe type. The fullVerifyAsyncpath (below) applies window and probe-type matching.
Verification Algorithm
Source: ClaimVerifier.VerifyAsync.
VERIFY(functionMap, observations, options):
1. Resolve effective thresholds (options overrides win over coverage thresholds):
minObservationRate, windowSeconds, failOnUnexpected
2. windowEnd = options.To ?? now
windowStart = options.From ?? windowEnd - windowSeconds
3. Filter observations to [windowStart, windowEnd] and, if set,
to options.ContainerIdFilter / options.PodNameFilter
4. For each non-optional expected_path:
For each non-optional expected_call:
- Look up observations by call.nodeHash
- If an observation's probeType is in call.probeTypes -> matched (probeTypeMatched=true)
- If observations exist but probe type differs -> still counted as matched,
flagged as probe-type mismatch (probeTypeMatched=false)
- Otherwise -> missing
path.observed = (no missing calls)
5. unexpected = observed function names whose nodeHash is in no expected path/call
6. observationRate = totalMatchedNodeHashes / (totalMatched + totalMissing)
7. verified = observationRate >= minObservationRate
AND (NOT failOnUnexpected OR unexpected.count == 0)
8. Build evidence (functionMapDigest, observationsDigest, observationCount,
window, verifierVersion) and warnings; return ClaimVerificationResult
Key behaviors that differ from a naive reading:
- The observation rate is call-based (node-hash level), not path-based.
- Optional paths and optional calls are skipped entirely in the rate calculation.
- A call observed with the wrong probe type still counts as observed but raises a warning; it does not by itself fail verification.
Media Types
Source: FunctionMapSchema (DSSE payload type / schema URI), FunctionMapCommandGroup (CLI sign/attest path).
| Usage | Media Type |
|---|---|
DSSE payload type (canonical, FunctionMapSchema.DssePayloadType) | application/vnd.stellaops.function-map.v1+json |
DSSE payload type used by CLI --sign / --attest (FunctionMapCommandGroup) | application/vnd.stellaops.function-map+json |
JSON schema URI (FunctionMapSchema.JsonSchemaUri) | https://stellaops.org/schemas/function-map-v1.json |
Offline observations file (CLI --observations) | NDJSON (one ClaimObservation JSON object per line) |
Discrepancy flagged: the canonical
DssePayloadTypeconst (application/vnd.stellaops.function-map.v1+json) and the payload type hard-coded in the CLI sign/attest path (application/vnd.stellaops.function-map+json, no.v1) do not match. An earlier revision of this doc listedapplication/vnd.stella.function-map+jsonandapplication/vnd.stella.verification-report+json, neither of which appears in the code base; those have been removed.
Observation Record (NDJSON)
Source: ClaimObservation in Verification/IClaimVerifier.cs. Unlike the predicate, observation fields use snake_case JSON property names. Each line in an offline observations file (CLI --offline --observations) is one such object:
{
"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_us": 150
}
| Field | Type | Required | Description |
|---|---|---|---|
observation_id | string | Yes | Unique observation ID (used for dedup and deterministic hashing) |
node_hash | string | Yes | Node hash (sha256:...) computed from PURL + normalized symbol |
function_name | string | Yes | Observed function name |
probe_type | string | Yes | Probe type that generated the observation |
observed_at | ISO 8601 | Yes | When the observation occurred |
observation_count | integer | No | Aggregated count for batched observations (default 1) |
container_id | string | No | Container ID where the observation occurred |
pod_name | string | No | Kubernetes pod name |
namespace | string | No | Kubernetes namespace |
duration_us | int64 | No | Call duration in microseconds (serialized as duration_us, not duration_microseconds) |
The Platform API
ObservationDtocarries the same snake_case fields except it omitsduration_us.
Probe Types
Source: FunctionMapSchema.ProbeTypes.
| 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 |
FunctionMapSchema.ProbeTypes.IsValid validates probe types case-insensitively; the generator validation step rejects any probeTypes value outside this set.
Verification Result
Source: ClaimVerificationResult in Verification/IClaimVerifier.cs. Returned by the claim verifier and by the CLI --format json. Fields use snake_case.
| Field | Type | Description |
|---|---|---|
verified | boolean | Whether verification passed |
observation_rate | double | Overall call-based observation rate (0.0–1.0) |
target_rate | double | Effective minimum observation rate |
paths | array | Per-path results (path_id, observed, observation_rate, observation_count, matched_node_hashes, missing_node_hashes, optional call_details) |
unexpected_symbols | array | Observed function names not present in the function map |
missing_expected_symbols | array | Expected symbols with no matching observation |
evidence | object | function_map_digest, observations_digest, observation_count, window_start, window_end, verifier_version |
verified_at | ISO 8601 | When verification was performed |
warnings | array | Optional verification warnings (low coverage, probe-type mismatches, unexpected symbols) |
Per-call detail (call_details[], present when IncludeBreakdown is true): symbol, node_hash, observed, observation_count, matched_probe_type, probe_type_matched.
HTTP API (Platform)
Source: src/Platform/StellaOps.Platform.WebService/Endpoints/FunctionMapEndpoints.cs. All routes are mounted under /api/v1/function-maps, require a resolved tenant (RequireTenant()), and are guarded by Platform authorization policies that map to the Platform scopes below.
| Method | Route | Policy / scope | Description |
|---|---|---|---|
POST | /api/v1/function-maps | FunctionMapWrite (functionmap.write) | Create a function map from an SBOM ref + hot-function patterns (CreateFunctionMapRequest) |
GET | /api/v1/function-maps?limit&offset | FunctionMapRead (functionmap.read) | List function maps for the tenant (FunctionMapSummary) |
GET | /api/v1/function-maps/{id} | FunctionMapRead (functionmap.read) | Get a function map by ID (FunctionMapDetail) |
DELETE | /api/v1/function-maps/{id} | FunctionMapWrite (functionmap.write) | Delete a function map by ID |
POST | /api/v1/function-maps/{id}/verify | FunctionMapVerify (functionmap.verify) | Verify observations against a map (VerifyFunctionMapRequest → FunctionMapVerifyResponse) |
GET | /api/v1/function-maps/{id}/coverage | FunctionMapRead (functionmap.read) | Current coverage statistics (FunctionMapCoverageResponse) |
Scope note: these are Platform-local scopes declared in
PlatformScopes.cs(functionmap.read/.write/.verify) and their policies inPlatformPolicies.cs(platform.functionmap.*). They are not present in the canonical Authority scope catalog (StellaOpsScopes.cs); there is nofunction-mapentry there.
Request/response DTOs are defined in src/Platform/StellaOps.Platform.WebService/Contracts/FunctionMapModels.cs and use camelCase property names, except the embedded ObservationDto (snake_case, as noted above). All responses are wrapped in the platform envelope (PlatformItemResponse<T> / PlatformListResponse<T>) carrying tenantId, actorId, dataAsOf, cached, and cacheTtlSeconds.
CLI
Source: src/Cli/StellaOps.Cli/Commands/FunctionMap/FunctionMapCommandGroup.cs. Command group: stella function-map (alias stella fmap).
stella function-map generate
Generates a function_map predicate from an SBOM.
| Option | Alias | Default | Description |
|---|---|---|---|
--sbom | -s | (required) | Path to SBOM file (CycloneDX/SPDX JSON) |
--service | (required) | Service name for the function map | |
--subject | derived pkg:generic/{service} | Subject artifact PURL | |
--static-analysis | Path to static-analysis results | ||
--hot-functions | -H | [] | Glob pattern(s) for hot functions (repeatable) |
--min-rate | 0.95 | Minimum observation rate threshold (0.0–1.0) | |
--window | 1800 | Observation window in seconds | |
--fail-on-unexpected | false | Fail verification if unexpected symbols are observed | |
--output | -o | stdout | Output file path |
--format | -f | json | Output format: json or yaml |
--sign | false | Sign the predicate as a DSSE envelope (requires a registered ISigner) | |
--attest | false | Create a DSSE envelope and submit to a Rekor transparency log | |
--build-id | Build ID to embed in the predicate |
stella function-map verify
Verifies runtime observations against a function_map.
| Option | Alias | Default | Description |
|---|---|---|---|
--function-map | -m | (required) | Path or OCI reference to the function_map predicate |
--container | -c | all | Container ID to filter observations |
--from | 30 min ago | Start of observation window (ISO 8601) | |
--to | now | End of observation window (ISO 8601) | |
--output | -o | stdout | Output verification report path |
--format | -f | table | Output format: json, table, or md |
--strict | false | Fail on any unexpected symbols (failOnUnexpected override) | |
--sign | false | Sign the verification report (not yet implemented; emits a warning) | |
--offline | false | Offline mode (read observations from a bundled file) | |
--observations | Path to an NDJSON observations file (required with --offline) |
Not yet implemented: online observation query (without
--offline) is a stub that emits a warning and verifies against an empty observation set;verify --signis a stub.
Exit codes (FunctionMapExitCodes)
| Code | Meaning |
|---|---|
0 | Success |
10 | File not found |
20 | Validation failed |
25 | Verification failed |
30 | Signing failed |
40 | Attestation failed |
99 | System error |
Persistence
Source: src/Scanner/__Libraries/StellaOps.Scanner.Storage/Postgres/Migrations/023_runtime_observations.sql. Runtime observations are stored in scanner.runtime_observations and queried via IRuntimeObservationStore (PostgresRuntimeObservationStore).
| Column | Type | Notes |
|---|---|---|
id | UUID PK | gen_random_uuid() |
observation_id | TEXT | NOT NULL UNIQUE (dedup) |
node_hash | TEXT | NOT NULL; indexed |
function_name | TEXT | NOT NULL; indexed |
container_id | TEXT | partial index where not null |
pod_name | TEXT | partial index (pod_name, namespace) where not null |
namespace | TEXT | |
probe_type | TEXT | NOT NULL |
observation_count | INTEGER | default 1 |
duration_us | BIGINT | call duration in microseconds |
observed_at | TIMESTAMPTZ | NOT NULL; BRIN index for time-range queries/pruning |
created_at | TIMESTAMPTZ | default now() |
A composite (node_hash, observed_at DESC) index supports the common lookup pattern. IRuntimeObservationStore exposes store/batch-store, query-by node-hash/container/pod, generic QueryAsync(ObservationQuery), GetSummaryAsync, and PruneOlderThanAsync.
