Offline Sync Flow
Audience: operators and integrators running StellaOps in air-gapped or disconnected estates, and developers working on the AirGap modules (
src/AirGap).
Overview
The Offline Sync Flow describes how StellaOps supports air-gapped and disconnected environments through the Offline Kit. This flow covers knowledge-bundle generation, secure transfer, verification, and import in environments with no external network connectivity.
Business Value: Enable full vulnerability scanning and policy evaluation capabilities in highly secure, air-gapped environments while maintaining audit trails and cryptographic verification.
Actors
Reconciled against
src/AirGap(Controller, Importer, Time,__Libraries/StellaOps.AirGap.Bundle,__Libraries/StellaOps.AirGap.Sync) and the scope catalogsrc/Authority/.../StellaOps.Auth.Abstractions/StellaOpsScopes.cs. The original actor list attributed offline-bundle sealing/signing to EvidenceLocker and a standalone Signer service; that is INCORRECT. Offline knowledge bundles are built and DSSE-signed inside the AirGap.Bundle library (SnapshotBundleWriter+SnapshotManifestSigner), not by EvidenceLocker (which seals decision capsules, a separate concern — seesrc/EvidenceLocker/.../Api/Capsules).
| Actor | Type | Role |
|---|---|---|
| Online Admin | Human | Generates and exports offline knowledge bundles (stella airgap export) |
| Offline Admin | Human | Imports and verifies bundles (stella airgap import [--verify-only]) |
| Concelier (mirror) | Service | Source of advisory raw data snapshotted into bundles (StellaOps.Concelier.MirrorSeedTool) |
AirGap.Bundle (SnapshotBundleWriter) | Library | Assembles the snapshot, computes the Merkle root, writes manifest.json, and DSSE-signs manifest.sig |
| AirGap Controller | Service | Seal/unseal state machine + status/verify APIs under /system/airgap |
| AirGap Importer | Library | Validates bundles (TUF → DSSE → Merkle → rotation → referrer → version monotonicity) and quarantines failures |
| AirGap Time | Service | Time-anchor ingest/status (/api/v1/time); Roughtime + RFC3161 verification; staleness budgets |
| Import targets | Adapters | Load verified content into Concelier (advisories), Excititor (VEX), and the Policy registry |
Prerequisites
Online Environment
- Access to vulnerability feeds (NVD, GHSA, etc.)
- Signing keys configured
- Bundle generation scheduled
Offline Environment
- AirGap services deployed (Controller, Time; Importer library hosted by consuming services)
- Trust anchors configured (DSSE public keys + fingerprints; see
TrustRootConfig) - Secure transfer mechanism available
Authority Scopes
Source of truth:
src/Authority/.../StellaOps.Auth.Abstractions/StellaOpsScopes.cs.
| Scope | Constant | Used by |
|---|---|---|
airgap:status:read | AirgapStatusRead | GET /system/airgap/status, GET /api/v1/time/status |
airgap:seal | AirgapSeal | POST /system/airgap/seal, POST /system/airgap/unseal, POST /api/v1/time/anchor (write) |
airgap:import | AirgapImport | Offline bundle import while sealed |
The time-anchor write endpoint (
POST /api/v1/time/anchor) is guarded by the ASP.NET policyAirGapTime.AnchorWrite, whose backing scope isairgap:seal(AirGapTimePolicies.AnchorWriteScope = StellaOpsScopes.AirgapSeal) — there is no distinctairgap:anchor:writescope.NOTE:
POST /system/airgap/verifyis registered against theAirGapPolicies.Verifypolicy, whose backing scope isairgap:status:read(StellaOpsScopes.AirgapStatusRead,Program.cs:59-69) — verification is a read-only integrity check, so it shares the air-gap read scope. There is noairgap:verifyscope; only the three scopes above are catalogued. (airgap:writestill appears as an expected import scope inAirGapControllerContractTests:224and is absent from the catalog — that one remains a code/catalog gap.)
Bundle Types
In practice these are sections of one knowledge snapshot bundle (KnowledgeSnapshotManifest carries advisories, vexStatements, policies, ruleBundles, trustRoots, and an optional timeAnchor), rather than five independently scheduled artifacts. The “Frequency” column is operator convention, not an enforced schedule.
| Section | Maps to | Manifest field |
|---|---|---|
| Advisories | per-feed advisory files (advisories/<feedId>/) | advisories (AdvisorySnapshotEntry) |
| VEX | per-source VEX files (vex/<sourceId>/) | vexStatements (VexSnapshotEntry) |
| Policies | policy files (policies/) | policies (PolicySnapshotEntry) |
| Rule bundles | rule files (rules/<bundleId>) | ruleBundles (RuleBundleSnapshotEntry) |
| Trust roots | DSSE trust material (trust/) | trustRoots (TrustRootSnapshotEntry) |
| Time anchor | Roughtime or RFC3161 token | timeAnchor (TimeAnchorEntry) |
The original “Time Anchor … NTP alternatives” entry is corrected: implemented anchor formats are Roughtime and RFC3161 only (
TimeTokenFormat); plain NTP is not a supported anchor source.
Flow Diagram
NOTE: the diagram is conceptual. The “Evidence Locker” / “Sign” / “Verif” boxes predate reconciliation — in the implemented system the online side is
SnapshotBundleWriter+SnapshotManifestSigner(DSSE), and the offline side is the AirGap Controller (seal/verify) plus the Importer validation chain. See the Actors table and step-by-step for accurate component names.
┌─────────────────────────────────────────────────────────────────────────────────┐
│ Offline Sync Flow │
└─────────────────────────────────────────────────────────────────────────────────┘
ONLINE ENVIRONMENT OFFLINE ENVIRONMENT
┌────────────────────────────────┐ ┌────────────────────────────────┐
│ │ │ │
│ ┌────────┐ ┌────────┐ ┌────┐│ │┌────┐ ┌─────────┐ ┌────────┐│
│ │ Mirror │ │Evidence│ │Sign││ ││Verif│ │ AirGap │ │Concelier││
│ │ │ │ Locker │ │ ││ ││ │ │Importer │ │ ││
│ └───┬────┘ └───┬────┘ └──┬─┘│ │└──┬──┘ └────┬────┘ └───┬────┘│
│ │ │ │ │ │ │ │ │ │
│ │ Snapshot │ │ │ │ │ │ │ │
│ │ advisories│ │ │ │ │ │ │ │
│ │───┐ │ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │ │ │ │
│ │<──┘ │ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │ │ │
│ │ Create │ │ │ │ │ │ │ │
│ │ bundle │ │ │ │ │ │ │ │
│ │──────────>│ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │ │ │
│ │ │ Seal │ │ │ │ │ │ │
│ │ │──────────> │ │ │ │ │ │
│ │ │ │ │ │ │ │ │ │
│ │ │ Signed │ │ │ │ │ │ │
│ │ │ bundle │ │ │ │ │ │ │
│ │ │<────────── │ │ │ │ │ │
│ │ │ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │ │ │
│ │ [Export to ] │ │ │ │ │ │
│ │ [removable media ] │ =========> [Import from│ │ │
│ │ │ │ │ │ │removable]│ │ │
│ │ │ │ │ │ │ │ │ │
│ │ │ │ │ │ │ Verify │ │ │
│ │ │ │ │ │ │ signature│ │ │
│ │ │ │ │ │ │───┐ │ │ │
│ │ │ │ │ │ │ │ │ │ │
│ │ │ │ │ │ │<──┘ │ │ │
│ │ │ │ │ │ │ │ │ │
│ │ │ │ │ │ │ Verify │ │ │
│ │ │ │ │ │ │ Merkle │ │ │
│ │ │ │ │ │ │───┐ │ │ │
│ │ │ │ │ │ │ │ │ │ │
│ │ │ │ │ │ │<──┘ │ │ │
│ │ │ │ │ │ │ │ │ │
│ │ │ │ │ │ │ Import │ │ │
│ │ │ │ │ │ │──────────> │ │
│ │ │ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │ Unpack │ │
│ │ │ │ │ │ │ │ advisories│ │
│ │ │ │ │ │ │ │───┐ │ │
│ │ │ │ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │<──┘ │ │
│ │ │ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │ Merge to │ │
│ │ │ │ │ │ │ │ Concelier │ │
│ │ │ │ │ │ │ │──────────>│ │
│ │ │ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │ │ │
└────────────────────────────────┘ └────────────────────────────────┘
Step-by-Step
1. Advisory Snapshot (Online)
Source advisory data comes from Concelier’s raw advisory store (mirror seeding via StellaOps.Concelier.MirrorSeedTool). The illustrative snapshot_id / delta_from envelope below is conceptual — there is no snapshot.json of this shape in src/AirGap; per-feed snapshots are captured into the manifest as AdvisorySnapshotEntry rows (feedId, relativePath, digest, recordCount, snapshotAt) when SnapshotBundleWriter builds the bundle.
{
"snapshot_id": "snap-20241229",
"created_at": "2024-12-29T00:00:00Z",
"sources": [
{"name": "nvd", "last_sync": "2024-12-29T00:00:00Z", "count": 245678},
{"name": "ghsa", "last_sync": "2024-12-28T23:45:00Z", "count": 45678},
{"name": "osv", "last_sync": "2024-12-28T23:30:00Z", "count": 89012}
],
"delta_from": "snap-20241228",
"advisories": {
"new": 127,
"updated": 456,
"unchanged": 245095
}
}
2. Bundle Generation (Online)
SnapshotBundleWriter (in StellaOps.AirGap.Bundle) assembles the snapshot and writes a manifest.json (KnowledgeSnapshotManifest). The manifest below reflects the actual schema — the original example invented an integrity/contents block that does not exist in code:
{
"bundleId": "knowledge-2024-12-29",
"name": "knowledge-2024-12-29",
"version": "1.0.0",
"createdAt": "2024-12-29T01:00:00Z",
"schemaVersion": "1.0.0",
"merkleRoot": "sha256:abc123...",
"totalSizeBytes": 131072000,
"entryCount": 15,
"advisories": [
{ "feedId": "nvd", "relativePath": "advisories/nvd/nvd.ndjson", "digest": "sha256:...", "sizeBytes": 0, "recordCount": 245678 }
],
"vexStatements": [],
"policies": [],
"trustRoots": [],
"ruleBundles": [],
"timeAnchor": null
}
The Merkle root is computed over the per-entry digests (
ComputeMerkleRoot) and stored inside the manifest — there is no separatemerkle/tree.jsonartifact. The archive is a deterministic PAXtar.gz(TarEntryFormat.Pax, written bySnapshotBundleWriter.Tar.cs) with a fixed modification time and ordinal path ordering. The on-disk extension is.tar.gz(NormalizeOutputPathappends.tar.gzif missing); the CLI default output name isknowledge-<yyyyMMdd>.tar.gz. There is no.stella.bundle.tgzextension in the code.
3. Bundle Signing (Online)
SnapshotManifestSigner produces a DSSE envelope over the canonical manifest bytes and writes it to manifest.sig. This corrects the original “detached PKCS#7 signature + full cert chain + external TSA” model, which is not what the code does:
- Signing algorithms (
SnapshotManifestSigner.Crypto.Sign.cs): ECDSA —ES256/ES384/ES512(curve-dependent), or RSARS256; an ephemeralES256key is used when none is supplied. - The offline Importer’s
DsseVerifierverifies RSA-PSS / SHA-256 (PS256) envelopes against trusted public keys, matching by key fingerprint within a configured trust window. - There is no built-in network TSA. Time provenance is carried separately by time anchors (Roughtime / RFC3161 tokens) via the AirGap Time service, not by a
timestamp.stellaops.ioTSA.
{
"payloadType": "application/vnd.stellaops.knowledge-snapshot+json",
"payload": "base64(manifest.json)...",
"signatures": [
{ "keyid": "<sha256 fingerprint of public key>", "sig": "base64(signature)..." }
]
}
4. Export Package
Final package layout reflects the directories actually created by SnapshotBundleWriter (Sections.* partials). The original tree listed a merkle/tree.json, verify.sh, and README.md that the writer does not emit:
knowledge-<yyyyMMdd>.tar.gz (deterministic PAX tar.gz)
├── manifest.json # KnowledgeSnapshotManifest (includes merkleRoot, entries)
├── manifest.sig # DSSE envelope over the manifest (present only when signed)
├── advisories/
│ └── <feedId>/<fileName> # e.g. advisories/nvd/nvd.ndjson
├── vex/
│ └── <sourceId>/<fileName>
├── policies/
│ └── <fileName>
├── rules/
│ └── <ruleBundleId>/<fileName> # rule files live under the bundle dir
└── trust/
└── <fileName> # trust-root material referenced by the manifest
A
timeAnchorentry, when present, is recorded inmanifest.json(TimeAnchorEntry) rather than as a fixed top-level directory.
5. Secure Transfer
Transfer via approved mechanism:
- USB drive (encrypted)
- Optical media (write-once)
- Data diode (one-way network)
- Secure courier
6. Import Verification (Offline)
The CLI binary is stella (the airgap command group lives in AirGapCommandGroup.cs); there is no stellaops-airgap binary. The airgap group exposes export, import, diff, status, and a jobs subgroup (export/import/list) — there is no airgap verify-bundle subcommand. Verification-only is the import command with --verify-only, and import takes the bundle as a positional argument, not --bundle. Verification runs through ImportValidator.ValidateAsync, which executes a fixed ordered chain and quarantines the bundle on any failure (under /updates/quarantine/<tenantId>/...):
# Verify a bundle without applying changes (positional bundle arg)
stella airgap import /mnt/media/knowledge-20241229.tar.gz --verify-only
# Import (verifies, then activates) — see step 8
stella airgap import /mnt/media/knowledge-20241229.tar.gz
A separate
stella verify bundle --bundle <path>command exists (VerifyCommandGroup.cs), but it verifies E2E evidence bundles (input-hash + verdict replay), not knowledge-snapshot bundles — do not confuse the two.
ImportValidator chain (each step fails closed; reason codes are emitted as structured logs):
- TUF metadata —
TufMetadataValidator.Validate(root, snapshot, timestamp)(TUF_INVALID). - DSSE signature —
DsseVerifier.Verifyagainst trusted fingerprints/keys + trust window (DSSE_INVALID). - Merkle root —
MerkleRootCalculator.ComputeRootover payload entries must equal the manifest’smerkleRoot(MERKLE_ROOT_MISSING/MERKLE_ROOT_MISMATCH). - Root rotation policy —
RootRotationPolicy.Validateover active/pending keys + approver IDs (ROTATION_INVALID). - Referrer validation — only for
mirror-bundle/offline-kittypes (REFERRER_VALIDATION_FAILED; counts missing/checksum/size mismatches). - Version monotonicity —
IVersionMonotonicityCheckerblocks rollback per tenant/type (VERSION_NON_MONOTONIC); overridable only with--forceand aforceActivateReason.
The original example showed an X.509 certificate-chain check and a TSA timestamp window. Neither matches the code: trust is fingerprint-based DSSE, and freshness is enforced separately by the Time service’s staleness budgets (see step 7), not as part of bundle signature verification.
7. Time Anchor Verification (Offline)
The AirGap Time service (/api/v1/time) ingests a time anchor and reports staleness without network time. Anchors are Roughtime or RFC3161 tokens (TimeTokenFormat), verified by RoughtimeVerifier / Rfc3161Verifier. Freshness is expressed as a staleness budget (StalenessBudget: warningSeconds, breachSeconds; default 3600 / 7200), evaluated into a StalenessEvaluation (isWarning / isBreach) — there is no FRESH/STALE/EXPIRED enum.
# Read status requires airgap:status:read; write (set anchor) requires airgap:seal
# (policy AirGapTime.AnchorWrite → scope airgap:seal). The body maps to SetAnchorRequest;
# trustRootKeyId / trustRootAlgorithm / trustRootPublicKeyBase64 are optional and only
# honoured outside Production (AllowRequestSuppliedTrustRoots).
POST /api/v1/time/anchor
{ "tenantId": "...", "hexToken": "<roughtime|rfc3161 token hex>", "format": "Roughtime",
"warningSeconds": 3600, "breachSeconds": 7200 }
GET /api/v1/time/status returns a TimeStatusDto. The actual shape is wider than the staleness evaluation alone — it embeds anchor identity plus per-content staleness. Note secondsRemaining is a computed property on StalenessEvaluation, not a field of TimeStatusDto:
{
"anchorTime": "2024-12-28T16:00:00.0000000Z",
"format": "Roughtime",
"source": "roughtime:<server>",
"fingerprint": "<signing key fingerprint>",
"digest": "<token digest>",
"ageSeconds": 32400,
"warningSeconds": 3600,
"breachSeconds": 7200,
"isWarning": true,
"isBreach": true,
"contentStaleness": { "advisories": { "ageSeconds": 32400, "warningSeconds": 3600, "breachSeconds": 7200, "isWarning": true, "isBreach": true } },
"evaluatedAtUtc": "2024-12-29T01:00:00.0000000Z"
}
The seal flow (
POST /system/airgap/seal) records adriftBaselineSeconds(wall clock vs. anchor at seal) and per-content staleness budgets (advisories / vex / policy). Request-supplied trust roots are rejected outside Development/Testing (AllowRequestSuppliedTrustRoots).
8. Advisory Import (Offline)
KnowledgeSnapshotImporter loads verified content into the registered import targets:
- Advisories → Concelier via
ConcelierAdvisoryImportTarget(IAdvisoryRawRepository). - VEX → Excititor via
ExcititorVexImportTarget(IVexRawDocumentSink). - Policies → the Policy registry via
PolicyRegistryImportTarget.
The result shape is SnapshotImportResult. The per-module processed/created/updated/failed counts live in a nested Statistics object (ImportStatistics), not flat at the root; the root carries success, bundleId, startedAt, completedAt, errors, and error. ImportStatistics also exposes computed totalProcessed/totalCreated/totalUpdated/totalFailed rollups. The original example’s import_id / advisories_imported / snapshot-state fields are not the actual contract:
{
"success": true,
"bundleId": "knowledge-2024-12-29",
"startedAt": "2024-12-29T02:00:00Z",
"completedAt": "2024-12-29T02:00:42Z",
"statistics": {
"advisoriesProcessed": 583,
"advisoriesCreated": 127,
"advisoriesUpdated": 456,
"advisoriesFailed": 0,
"vexProcessed": 0,
"vexCreated": 0,
"vexUpdated": 0,
"vexFailed": 0,
"policiesProcessed": 0,
"policiesCreated": 0,
"policiesUpdated": 0,
"policiesFailed": 0
},
"errors": [],
"error": null
}
Bundle Freshness Policies
DRAFT / illustrative. The YAML profiles below are an operator-facing convention and do not map to a config schema in
src/AirGap. The implemented freshness control is the per-tenantStalenessBudget(warningSeconds/breachSeconds, default3600/7200) set on the time anchor and at seal time, plus optional per-content budgets (advisories / vex / policy). Treat themode/max_age/require_tsakeys as roadmap, not current behavior.
Strict (High Security)
freshness_policy:
mode: strict
max_age:
advisory: 24h
vex: 24h
policy: 7d
trust: 30d
require_tsa: true
reject_stale: true
Standard
freshness_policy:
mode: standard
max_age:
advisory: 7d
vex: 7d
policy: 30d
trust: 90d
require_tsa: false
warn_stale: true
Permissive (Disconnected Operations)
freshness_policy:
mode: permissive
max_age:
advisory: 30d
vex: 30d
policy: 90d
trust: 365d
require_tsa: false
warn_stale: true
allow_manual_override: true
Data Contracts
The TypeScript interfaces below are an illustrative/aspirational view. The authoritative schemas live in C# under
StellaOps.AirGap.Bundle.Models: the on-disk knowledge manifest isKnowledgeSnapshotManifest(see step 2 —bundleId/name/version/merkleRoot/entryCountplus typed entry lists), and import results areSnapshotImportResult(step 8). Field names below use snake_case and a flatterOfflineBundleshape that does not match the implemented camelCase records; retained here as a conceptual summary, flagged as not-implemented-as-written.
Bundle Manifest Schema (conceptual — see KnowledgeSnapshotManifest for the real schema)
interface OfflineBundle {
bundle_id: string;
bundle_type: 'advisory' | 'vex' | 'policy' | 'trust' | 'time_anchor';
version: string;
created_at: string;
created_by: string;
contents: {
files: Array<{
path: string;
size: number;
sha256: string;
}>;
metadata: Record<string, unknown>;
};
integrity: {
merkle_root: string;
algorithm: 'sha256';
tree_path: string;
};
signature: {
keyid: string;
algorithm: string;
sig: string;
};
freshness: {
timestamp: string;
tsa_timestamp?: string;
valid_until?: string;
};
}
Import Result Schema (conceptual — see SnapshotImportResult for the real schema)
interface ImportResult {
import_id: string;
bundle_id: string;
status: 'success' | 'partial' | 'failed';
started_at: string;
completed_at: string;
results: {
records_imported: number;
records_updated: number;
records_new: number;
conflicts: number;
errors: number;
};
verification: {
signature_valid: boolean;
merkle_verified: boolean;
freshness_check: 'FRESH' | 'STALE' | 'EXPIRED';
};
audit_log_entry: string;
}
Scheduling Strategies
DRAFT / illustrative. The
offline_sync:YAML below is an operator convention; there is no such config schema insrc/AirGap. Bundle export is invoked on demand viastella airgap export(and can be wrapped in any external scheduler/cron).type: deltavs. full andretentionare roadmap conventions, not implemented config keys.
Daily Sync
offline_sync:
advisory_bundle:
schedule: "0 1 * * *" # Daily at 1 AM
type: delta
retention: 7
vex_bundle:
schedule: "0 2 * * *" # Daily at 2 AM
type: delta
retention: 7
Weekly Full + Daily Delta
offline_sync:
advisory_bundle:
full:
schedule: "0 0 * * SUN" # Weekly full on Sunday
retention: 4
delta:
schedule: "0 1 * * MON-SAT" # Daily delta Mon-Sat
retention: 7
Error Handling
Reconciled to ImportValidator reason codes. Any failure in the validation chain is logged with a reason_code and the bundle is moved to quarantine (/updates/quarantine/<tenantId>/...) for triage rather than partially imported.
| Failure | Reason code | Behavior |
|---|---|---|
| TUF metadata invalid | TUF_INVALID | Reject + quarantine |
| DSSE signature untrusted/invalid | DSSE_INVALID | Reject + quarantine |
| Computed Merkle root empty | HASH_MISMATCH (reason merkle-empty) | Reject + quarantine |
| Merkle root missing/mismatch | MERKLE_ROOT_MISSING / MERKLE_ROOT_MISMATCH | Reject + quarantine |
| Trust-root rotation invalid | ROTATION_INVALID | Reject + quarantine |
| Referrer validation failed | REFERRER_VALIDATION_FAILED | Reject + quarantine (mirror/offline-kit types) |
| Version rollback (non-monotonic) | VERSION_NON_MONOTONIC | Reject unless ForceActivate + ForceActivateReason |
| Force activate without reason | FORCE_ACTIVATE_REASON_REQUIRED | Reject + quarantine |
| Manifest version unparseable | VERSION_PARSE_FAILED | Reject + quarantine |
| Version store write failed | (reason version-store-write-failed:*) | Reject + quarantine |
| Time anchor over budget | (Time service) | isWarning / isBreach on staleness evaluation |
Observability
Metric names reconciled against
AirGapTelemetry(Controller meterStellaOps.AirGap.Controller) andOfflineKitMetrics(Importer meterStellaOps.AirGap.Importer). The originaloffline_*metric names were invented and have been replaced with the emitted instruments.
Metrics — Controller (StellaOps.AirGap.Controller)
| Metric | Type | Labels / Tags |
|---|---|---|
airgap_seal_total | Counter | tenant, sealed |
airgap_unseal_total | Counter | tenant, sealed |
airgap_startup_blocked_total | Counter | tenant, reason |
airgap_time_anchor_age_seconds | ObservableGauge | tenant |
airgap_staleness_budget_seconds | ObservableGauge | tenant |
Metrics — Importer (StellaOps.AirGap.Importer)
| Metric | Type | Labels / Tags |
|---|---|---|
offlinekit_import_total | Counter | status, tenant_id |
offlinekit_attestation_verify_latency_seconds | Histogram | attestation_type, success |
attestor_rekor_success_total | Counter | mode |
attestor_rekor_retry_total | Counter | reason |
rekor_inclusion_latency | Histogram | success |
Key Log Events
Structured log message templates as emitted in code (see
AirGapTelemetry/ImportValidator).
| Event | Level | Fields |
|---|---|---|
airgap.sealed | INFO | tenant, policy_hash, anchor_source, anchor_digest, age_seconds |
airgap.unsealed | INFO | tenant, last_transition_at |
airgap.startup.validation (passed/failed) | INFO / CRITICAL | tenant, allowlist_count / reason |
offlinekit.import.validation (succeeded/failed) | INFO / WARN | tenant_id, bundle_type, bundle_digest, reason_code |
offlinekit.import.force_activation | WARN | tenant_id, bundle_type, incoming_version, current_version, force_activate_reason |
offlinekit.import.quarantine | ERROR | tenant_id, bundle_path, reason_code |
Related Flows
- Evidence Bundle Export Flow — DSSE sealing of decision capsules (a separate concern from knowledge-bundle sealing; see the Actors note above).
- Advisory Drift Re-scan Flow — consumes the advisories imported by this flow.
- Scan Submission Flow — scans evaluate against the imported advisory/VEX content.
