Asset Registry v1 Contract
Contract ID: CONTRACT-ASSET-REGISTRY-V1-016 · Schema: asset-registry.v1 · Status: Published · Owner: Graph Indexer Guild · Last updated: 2026-04-30
Purpose
Asset Registry v1 is the shared asset substrate for NIS2 asset inventory, DORA Register of Information projections, and TLPT scoping. Owned by Graph, it is represented as deterministic Graph Indexer asset nodes plus an append-only stream of asset registry events.
Audience: Graph Indexer implementers and the compliance consumers (NIS2, DORA, TLPT) that project over the asset graph. For the offline/federated transport of these events, see Asset Inventory Sync v1.
Schema Pinning
- Schema version is pinned to
asset-registry.v1(AssetRegistryV1.SchemaVersion). - JSON schema:
docs/contracts/schemas/asset-registry-v1.schema.json. - Schema constants (version, node/edge kinds, type and event enums, validators):
src/Graph/StellaOps.Graph.Indexer/Assets/AssetRegistryV1.cs. - Runtime node/edge/event helper:
src/Graph/StellaOps.Graph.Indexer/Assets/AssetRegistryDocuments.cs(AssetGraphDocumentFactory,AssetRegistryEvent). - Source-to-asset adapter and indexing pipeline:
src/Graph/StellaOps.Graph.Indexer/Assets/AssetIndexingPipeline.cs(AssetSourceAdapter,AssetIndexTransformer,AssetIndexProcessor). - Persistence migration:
src/Graph/__Libraries/StellaOps.Graph.Indexer.Persistence/Migrations/003_asset_registry_events.sql. - Breaking changes require a new schema version and migration; v1 consumers must not auto-follow later enum changes.
Asset Node Shape
{
"schemaVersion": "asset-registry.v1",
"assetId": "gn:tenant-a:asset:...",
"type": "integration",
"tenantId": "tenant-a",
"attributes": {
"provider": "github",
"credential_ref": "cred:github-prod"
},
"ownership": {
"ownerActorRef": "actor:release-owner",
"ownerTeamRef": "team:platform",
"credentialRefs": ["cred:github-prod"]
},
"criticality": "high",
"dependsOn": ["gn:tenant-a:service:payments-api"],
"firstSeen": "2026-04-30T08:00:00Z",
"lastSeen": "2026-04-30T08:05:00Z",
"tombstone": false
}
Graph node mapping (the camelCase shape above is the external/API representation; the underlying Graph node stores snake_case attributes, see AssetGraphDocumentFactory.CreateAssetNode):
kind:assetid: deterministic node id fromGraphIdentity.ComputeNodeId(tenant, "asset", canonical_key), exposed asassetIdcanonical_key:tenant,type,source,external_idvalid_from:firstSeenvalid_to:lastSeenonly for tombstones; otherwisenull- node
attributes(snake_case):schema_version(asset-registry.v1),asset_id,tenant_id,type,source,external_id,criticality,ownership(owner_actor_ref,owner_team_ref,credential_refs),depends_on,asset_attributes(source-specific string attributes),first_seen,last_seen,tombstone
V1 Taxonomy
Asset v1 types are exactly:
hostcontainerimageserviceintegrationplugin
The enum is closed for v1 (AssetRegistryV1.AssetTypes) and is enforced both at write time (AssetRegistryV1.NormalizeType / IsAssetType) and in the persistence layer (the chk_asset_registry_events_asset_type CHECK constraint in both the Graph and Findings Ledger tables). Software components remain in the dependency/SBOM graph and may be linked to image or service assets, but they are not v1 asset types.
The first-party indexer (AssetSourceAdapter in AssetIndexingPipeline.cs) currently produces image and container assets from SBOM scans (sources scanner.sbom, scanner.sbom.container), integration assets from integration profiles (integrations.profile), service assets from runtime signals (signals.runtime), and plugin assets from the plugin catalogue (plugin.catalog). The host type is part of the closed v1 enum but has no first-party producer in the current pipeline; it is reserved for external producers that write asset nodes directly through AssetGraphDocumentFactory.
Excluded Types
The following are not asset nodes:
tenant-integration-credentialhuman-actorsoftware-componentvulnerabilitypolicy-control
Credentials and people are sensitive authority/security objects. The asset registry stores only opaque references such as credential_ref, credentialRefs, ownerActorRef, or ownerTeamRef; it must not store secret values, names, email addresses, or dereferenced PII.
Lineage and Events
Asset lifecycle is append-only. Deletions are tombstones, not physical deletes.
Graph edge mapping:
- Edge kind:
ASSET_VERSION(AssetRegistryV1.AssetVersionEdgeKind). - Event types:
asset.created,asset.updated,asset.removed(the closedAssetRegistryV1.EventTypesset). - Removed events require
tombstone: true; the indexer infersasset.removedfor any tombstone observation and rejects tombstones declared as create/update. - Edge attributes include
schema_version,asset_id,event_type,previous_asset_hash,current_asset_hash,effective_at, andtombstone. Theprevious_asset_hashisnonewhen there is no prior version; hashes are lower-case hex.
Persistence event table: graph.asset_registry_events.
Event fields:
event_id:asset-event:<eventHash>tenant_idasset_idasset_typeevent_typepayload_json: canonical asset node JSONpayload_hash: SHA-256 ofpayload_jsonprevious_event_hash: prior event hash for the same asset, when presentmerkle_leaf_hash: SHA-256 leaf hash for downstream ledger anchoringevent_hash: deterministic hash over event metadata (schema version, tenant, asset id/type, event type,payload_hash,previous_event_hash,merkle_leaf_hash,occurred_at)occurred_at: observation time supplied by the writer; participates inevent_hashrecorded_at: server-side append time,DEFAULT NOW()on thegraph.asset_registry_eventstable. It is not part of the canonical event payload orevent_hash, and is not carried on theAssetRegistryEventrecord; it exists only to drive the Findings Ledger append-order checkpoint below.
The Graph-side event table is the source for downstream Findings Ledger visibility. Findings Ledger mirrors the source rows into findings.asset_registry_events and preserves these hashes unchanged. The projection checkpoint follows Graph append order (recorded_at, event_id) while visibility queries retain replay order (occurred_at, event_hash).
Determinism and Offline Replay
- IDs and hashes are deterministic from canonical JSON with sorted object keys.
- Arrays that are sets, such as
dependsOnandcredentialRefs, are sorted with ordinal string ordering. - Timestamps are UTC ISO-8601.
- Tests run offline from in-repo code and migrations:
src/Graph/__Tests/StellaOps.Graph.Indexer.Tests/AssetRegistrySchemaTests.cssrc/Graph/__Tests/StellaOps.Graph.Indexer.Persistence.Tests/AssetRegistryEventStoreTests.cs
- Offline kits replay asset state by ordering
asset_registry_eventsbyoccurred_at ASC, event_hash ASCand followingprevious_event_hash.
Consumers
- NIS2 inventory and control evidence: asset list and point-in-time snapshots.
- DORA RoI: projections over
integrationandserviceassets, with operator-supplied contract metadata in later sprints. - TLPT scoping: queries over assets by tenant, criticality, dependency, and tombstone state.
Runtime API
Graph exposes live asset registry reads under (see src/Graph/StellaOps.Graph.Api/Endpoints/AssetRegistryEndpoints.cs):
| Method | Route | Required scope | Notes |
|---|---|---|---|
GET | /graph/assets | graph:read or graph:query | List/query via query-string params (type CSV, query, criticality, includeTombstones, includeLineage, limit, cursor, ordering). |
POST | /graph/assets/query | graph:read or graph:query | Body is AssetRegistryQueryRequest. |
GET | /graph/assets/{assetId} | graph:read or graph:query | `?includeLineage=true |
POST | /graph/assets/export | graph:export | Body is AssetRegistryExportRequest; format is ndjson (default) or json. |
All four endpoints require a resolved tenant (.RequireTenant()); reads/query/get authorize against GraphPolicies.ReadOrQuery and export against GraphPolicies.Export. The scopes graph:read and graph:export are canonical (StellaOpsScopes.GraphRead / StellaOpsScopes.GraphExport); graph:query is a Graph-API-local policy scope (GraphPolicies.GraphQueryScope) that is not part of the central StellaOpsScopes catalog.
The runtime API returns asset-registry.v1 records (the response DTO pins schemaVersion to asset-registry.v1), supports tombstone lineage when includeLineage is requested, and preserves the opaque-reference boundary for credentials and human actors. List/query results page via an opaque cursor (default limit 100, max 500) and order by asset type then asset id (or by asset id when ordering=assetId). Export responses include X-Content-SHA256 (lower-case hex of the payload) and X-Asset-Count headers, and export rows are ordered by asset type then asset id. Validation failures return 400 ASSET_REGISTRY_VALIDATION_FAILED.
Detailed request/response shapes are in eu-runtime-api-contracts-v1.md.
Change Control
- Do not add asset types to v1.
- Do not promote credentials, people, vulnerabilities, policy controls, or software components into v1 asset types without a new migration and contract version.
- Do not fetch regulator schemas or upstream references at runtime for this contract; all schema and tests are in-repo for offline determinism.
