Guardian Runtime — Drift Detection & SIEM Export
Guardian Runtime is the continuous monitoring component of CipherShift365. It detects cryptographic drift in production, exports canonical events to SIEM platforms, and self-protects against impacting the host application.
Table of Contents
- What It Does
- Why It's Needed
- Architecture Overview
- Installation
- How to Run / Use
- User Manual
- Developer Manual
- Code Examples
- API Reference
- Configuration Options
- Value Showcase
What It Does
Guardian Runtime runs inside your .NET application process and continuously monitors cryptographic activity against a signed baseline:
| Capability | Description |
|---|---|
| Drift Engine | Compares live crypto events against baseline. Detects new, changed, or resolved findings. |
| DiagnosticSource Listener | Subscribes to .NET DiagnosticSource / EventSource events from System.Security.Cryptography and the Vault SDK. No CLR Profiler required. |
| Sampling Controller | Rate-limits processing to bounded resources. Drops events under load, records when sampling is active. |
| Health Monitor | Self-disables after 3 consecutive faults. Emits a high-severity health event. Host application continues unaffected. |
| Canonical Events | Normalizes all crypto events into a structured CanonicalEvent model traceable back to a CBOM asset. |
| SIEM Export | Native formatters for Microsoft Sentinel, Splunk, and Elastic — no custom parsers needed. |
| APM Coexistence | Uses DiagnosticListener.AllListeners.Subscribe(). No CLR Profiler slot consumed — coexists with Datadog, Dynatrace, New Relic, etc. |
Drift Categories Detected
| Category | What It Means |
|---|---|
NewFinding |
A crypto asset detected that has no corresponding entry in the baseline |
RiskChanged |
An algorithm's risk level has worsened (or improved) since baseline |
ConfidenceChanged |
Detection confidence has changed since baseline |
HealthEvent |
Guardian Runtime self-disabled due to consecutive faults |
SamplingActive |
Event rate exceeded sampling threshold |
Why It's Needed
The Problem
Build-time gates (Guardian Gate) prevent violations from reaching production. But what about:
- Third-party NuGet packages that add crypto at runtime?
- Feature flags that activate dormant crypto paths?
- Configuration changes that switch to weaker algorithms?
- Library updates that silently change cryptographic defaults?
- Runtime-generated code or dynamically loaded assemblies?
Without runtime monitoring, you have blind spots between gate checks.
The Value Proposition
| Stakeholder | Benefit |
|---|---|
| SOC Analysts | Canonical events in their native SIEM format — no learning curve |
| DevOps / SRE | Bounded resource usage, self-disabling safety — will never crash the host |
| Security Engineers | Drift detection surfaces configuration drift, library upgrade risks, and feature-flag surprises |
| Compliance Auditors | Drift events provide a continuous audit trail between gate evaluations |
| Developers | No code changes needed. Subscribes to existing .NET diagnostics infrastructure. |
Guardian Runtime is the "always-on" counterpart to Guardian Gate's "checkpoint" enforcement. Together they provide defense-in-depth for cryptographic posture.
Architecture Overview
Event Flow
System.Security.Cryptographyor the Vault SDK emits aDiagnosticSourceeventRuntimeListener'sCryptoDiagnosticObserverfilters for crypto-related event namesHealthMonitor.TryEnter()gates if the listener is in a faulted stateSamplingController.ShouldProcess()rate-limits to ≤1000 events/secondDriftEngine.Evaluate()compares the finding against the loadedSignedBaseline- If drift is detected, a
CanonicalEventis raised via theonEventcallback - The SIEM exporter formats the event for the target platform
Safety Properties
| Property | Guarantee |
|---|---|
| Host impact on fault | None — self-disables, host continues |
| Resource bound | ≤1000 events/sec (configurable) |
| Profiler slot | Not used — works alongside APMs |
| Hash collision resilience | Baseline uses LogicalId (content-hash), not filenames |
| Startup hook | No code changes in the host application (if using .NET startup hooks) |
Installation
dotnet add package CipherShift365.Guardian
Programmatic Registration
using CipherShift365.Guardian.Runtime;
// Create components
var drift = new DriftEngine(identityService);
var sampling = new SamplingController(maxEventsPerSecond: 1000);
var health = new HealthMonitor();
health.OnFault += e => Console.Error.WriteLine($"GUARDIAN FAULT: {e.Description}");
// Load baseline
drift.LoadBaseline(signedBaseline);
// Configure SIEM export
var listener = new RuntimeListener(drift, sampling, health, onEvent: canonicalEvent =>
{
// Send to SIEM
await siemClient.SendAsync(EventExporter.ToSentinelJson(canonicalEvent));
});
listener.Start();
.NET Startup Hook (No Code Changes)
Add to your application's .csproj:
<ItemGroup>
<RuntimeHostConfigurationOption Include="STARTUP_HOOKS"
Value="path\to\CipherShift365.Guardian.Runtime.dll" />
</ItemGroup>
This injects RuntimeListener.Start() before Main() without requiring any application code changes.
Dependencies
CipherShift365.Guardian
├── CipherShift365.Core.Analysis
└── CipherShift365.Core.Runtime (shared contracts + types)
How to Run / Use
Minimal Integration
// Setup (once at startup)
var drift = new DriftEngine(identityService);
var sampling = new SamplingController(500); // 500 events/sec
var health = new HealthMonitor();
var listener = new RuntimeListener(drift, sampling, health, onEvent: evt =>
{
// Log to console, file, or SIEM
Console.WriteLine($"[DRIFT] {evt.Category}: {evt.Algorithm} — {evt.Description}");
});
listener.Start();
// No further interaction needed — listener runs for the process lifetime
With SIEM Export
var listener = new RuntimeListener(drift, sampling, health, onEvent: evt =>
{
string json = targetSiem switch
{
"sentinel" => EventExporter.ToSentinelJson(evt),
"splunk" => EventExporter.ToSplunkJson(evt),
"elastic" => EventExporter.ToElasticJson(evt),
_ => JsonSerializer.Serialize(evt)
};
// Send via your SIEM agent's forwarder
siemForwarder.Send(json);
});
Monitoring Health
// Periodically check if Guardian has faulted
if (health.IsFaulted)
{
Console.Error.WriteLine(
$"Guardian disabled at {health.LastFaultAt} after {health.FaultCount} faults. " +
"Host continues normally but drift detection is unavailable until reset.");
}
User Manual
Understanding Drift
Cryptographic drift is any change in the cryptographic posture of your application relative to the signed baseline:
| Drift Type | Example | Action |
|---|---|---|
| NewFinding | A NuGet update added ECDSA usage |
Add to baseline if approved, or remediate |
| RiskChanged | AES-128 was accepted, now DES appears under same LogicalId |
Investigate and remediate |
| ConfidenceChanged | Detection confidence went from High to Low |
Verify the finding manually |
| HealthEvent | Guardian faulted 3 times | Restart listener or redeploy |
Loading a Baseline
The DriftEngine requires a SignedBaseline loaded at startup:
// Load the same signed baseline used by Guardian Gate
byte[] blob = File.ReadAllBytes("baseline.csb");
var sb = BaselineManager.ImportFromDistribution(blob, verifier);
if (sb is null) throw new SecurityException("Baseline rejected.");
drift.LoadBaseline(sb);
If no baseline is loaded, DriftEngine.Evaluate() returns BaselineNotLoaded for every event — drift detection is unavailable but monitoring continues without errors.
SIEM Field Mapping
Microsoft Sentinel
{
"TimeGenerated": "2026-06-29T12:00:00.000Z",
"AssetId": "crypto-ml-dsa-87",
"Algorithm": "ML-DSA-87",
"Severity": "QuantumSafe",
"Category": "NewFinding",
"CorrelationId": "a1b2c3d4",
"Description": "New crypto asset detected: ML-DSA-87 (Signature)",
"Source": "CipherShift365.Guardian"
}
Splunk
{
"timestamp": "2026-06-29T12:00:00.000Z",
"event": "pqc_drift",
"fields": {
"asset_id": "crypto-ml-dsa-87",
"algorithm": "ML-DSA-87",
"severity": "QuantumSafe",
"category": "NewFinding",
"correlation_id": "a1b2c3d4"
}
}
Elastic
{
"@timestamp": "2026-06-29T12:00:00.000Z",
"event": {
"action": "pqc_drift",
"category": "NewFinding",
"severity": 3
},
"asset": {
"id": "crypto-ml-dsa-87",
"algorithm": "ML-DSA-87"
},
"ciphershift": {
"correlation_id": "a1b2c3d4"
}
}
Health Monitoring Events
When the HealthMonitor self-disables, it emits a CanonicalEvent with:
- Algorithm:
"HealthMonitor" - Severity:
QuantumBroken(highest) - Category:
HealthEvent - Description: Includes fault count and final reason
- RecommendedAction:
"Restart the Guardian listener or redeploy. Investigate root cause."
This ensures that silent failures are impossible — a security monitoring tool that stops working should raise alarms, not disappear quietly.
Developer Manual
Class Hierarchy
RuntimeListener (IDisposable)
├── Constructor: DriftEngine, SamplingController, HealthMonitor, Action<CanonicalEvent>?
├── Start() — Subscribes to DiagnosticListener.AllListeners
├── ProcessCryptoEvent() — Core event processing pipeline
└── Dispose() — Unsubscribes all listeners
CryptoDiagnosticObserver : IObserver<DiagnosticListener>
├── OnNext() — Filters for CipherShift365 / ASP.NET sources
└── Subscribes CryptoEventObserver for crypto-related events
CryptoEventObserver : IObserver<KeyValuePair<string, object?>>
└── Forwards to RuntimeListener.ProcessCryptoEvent()
DriftEngine
├── Constructor: IIdentityService
├── LoadBaseline(SignedBaseline)
├── Evaluate(Finding) → DriftResult
└── GetCurrentBaseline() → Baseline?
SamplingController
├── Constructor: int maxEventsPerSecond (default 1000)
├── ShouldProcess() → bool
├── IsSampling : bool
└── SampleCount : int
HealthMonitor
├── IsFaulted : bool (true after 3 consecutive faults)
├── FaultCount : int
├── LastFaultAt : DateTimeOffset?
├── OnFault : event Action<CanonicalEvent>
├── RecordFault(string reason)
├── TryEnter() → bool
└── Reset()
CanonicalEvent (record)
├── EventId, AssetId, Algorithm, Severity, Category
├── CorrelationId, Timestamp
└── FindingLocationalId?, BaselineLogicalId?, ObservedLogicalId?, Description?, RecommendedAction?
EventExporter (static)
├── ToSentinelJson(CanonicalEvent) → string
├── ToSplunkJson(CanonicalEvent) → string
└── ToElasticJson(CanonicalEvent) → string
DriftResult (readonly record struct)
├── HasDrift : bool, Category : DriftCategory
├── LogicalId, Algorithm, Description
├── Risk, Confidence
└── Static factories: NewFinding(), RiskChanged(), ConfidenceChanged(), NoDrift(), BaselineNotLoaded()
Processing Pipeline (Detailed)
The ProcessCryptoEvent method follows this sequence:
1. HealthMonitor.TryEnter()
└─ If faulted → drop event immediately
2. SamplingController.ShouldProcess()
└─ If sampling → drop event, record sample count
3. Extract algorithm from CryptoEvent
└─ If null → return (no-op for non-crypto events)
4. Construct Finding from event data
└─ Maps event.Role, event.Algorithm, event.Details
5. DriftEngine.Evaluate(finding)
└─ Computes LogicalId from (Role, Algorithm, Parameters)
└─ Compares against baseline entries by LogicalId
└─ Returns DriftResult with category and description
6. If DriftResult.HasDrift == true:
└─ Construct CanonicalEvent
└─ Invoke onEvent callback → SIEM export
Adding a New SIEM Exporter
public static class EventExporter
{
public static string ToYourSiemJson(CanonicalEvent e)
{
return JsonSerializer.Serialize(new
{
time = e.Timestamp,
// ... map fields to your SIEM's schema
});
}
}
The DiagnosticSource Advantage
Unlike CLR Profilers (which consume the single available profiling slot and break APM tools), Guardian Runtime uses DiagnosticListener.AllListeners.Subscribe():
| Approach | Pros | Cons |
|---|---|---|
| CLR Profiler | Deepest instrumentation | Consumes the profiler slot. Conflicts with APMs. |
| Mono.Cecil weaving | Broad coverage | Requires build integration. Brittle across updates. |
| DiagnosticSource (Guardian Runtime) | Zero config, coexists with APMs, works in-proc | Relies on libraries emitting events (Vault SDK does; BCL does) |
Thread Safety
DriftEngine._observedusesConcurrentDictionary<LogicalId, Finding>— safe for concurrent writesSamplingControllerusesInterlocked.Increment— lock-freeHealthMonitor._faultedisvolatile bool— safe for multi-threaded accessRuntimeListeneris not designed for concurrentStart()calls — call once at startup
Code Examples
Example 1: Console-Based Drift Monitor
var drift = new DriftEngine(identity);
var sampling = new SamplingController(500);
var health = new HealthMonitor();
// Self-disable on fault
health.OnFault += e => Console.Error.WriteLine($"GUARDIAN DISABLED: {e.Description}");
// Drift to console
var listener = new RuntimeListener(drift, sampling, health, onEvent: e =>
{
Console.WriteLine($"[{e.Timestamp:HH:mm:ss}] [{e.Category}] {e.Algorithm}: {e.Description}");
if (e.RecommendedAction is not null)
Console.WriteLine($" → Action: {e.RecommendedAction}");
});
listener.Start();
Console.WriteLine("Guardian Runtime active. Press any key to stop.");
Console.ReadKey();
listener.Dispose();
Example 2: Sentinel Integration with HttpClient
var drift = new DriftEngine(identity);
drift.LoadBaseline(signedBaseline);
var sampling = new SamplingController(1000);
var health = new HealthMonitor();
var httpClient = new HttpClient();
httpClient.BaseAddress = new Uri(Environment.GetEnvironmentVariable("SENTINEL_DCR_URL")!);
var listener = new RuntimeListener(drift, sampling, health, async evt =>
{
var json = EventExporter.ToSentinelJson(evt);
var content = new StringContent(json, Encoding.UTF8, "application/json");
await httpClient.PostAsync("/api/logs", content);
});
listener.Start();
Example 3: Splunk HEC Forwarder
var listener = new RuntimeListener(drift, sampling, health, async evt =>
{
var splunkPayload = EventExporter.ToSplunkJson(evt);
var request = new HttpRequestMessage(HttpMethod.Post, "https://splunk-hec:8088/services/collector")
{
Content = new StringContent(splunkPayload, Encoding.UTF8, "application/json"),
Headers = { { "Authorization", $"Splunk {hecToken}" } }
};
await httpClient.SendAsync(request);
});
Example 4: Resilience — Self-Disable and Recovery
// Guardian Runtime is resilient by design
var health = new HealthMonitor();
var listener = new RuntimeListener(drift, sampling, health);
// Simulate a fault in an external callback
health.OnFault += e =>
{
// Forward the health event to your alerting system
alertingService.SendCritical(e.Description!);
// After 3 faults, Guardian self-disables
// Your application continues running normally
// Only drift detection stops — not your business logic
};
listener.Start();
// Later, if you want to reset:
if (health.IsFaulted)
{
health.Reset();
listener.Start(); // Re-enable monitoring
}
Example 5: Custom Drift Handler with Filtering
var listener = new RuntimeListener(drift, sampling, health, onEvent: e =>
{
// Only alert on high-severity drift
if (e.Severity == RiskLevel.QuantumBroken || e.Severity == RiskLevel.QuantumVulnerable)
{
// Send to alerting system
pagerDuty.Trigger(new Alert
{
Severity = e.Severity.ToString(),
Source = "CipherShift365.Guardian",
Summary = e.Description ?? $"Drift: {e.Category}",
Details = new
{
e.AssetId, e.Algorithm, e.CorrelationId,
Action = e.RecommendedAction
}
});
}
// Always log to SIEM regardless of severity
siemForwarder.Send(EventExporter.ToElasticJson(e));
});
API Reference
RuntimeListener
| Member | Type | Description |
|---|---|---|
RuntimeListener(DriftEngine, SamplingController, HealthMonitor, Action<CanonicalEvent>?) |
Constructor | Creates a listener. onEvent receives drift events. |
Start() |
Method | Subscribes to DiagnosticListener.AllListeners. Idempotent (no-op if already started). |
Dispose() |
Method | Unsubscribes all listeners. Safe to call multiple times. |
DriftEngine
| Member | Type | Description |
|---|---|---|
DriftEngine(IIdentityService) |
Constructor | Requires identity service for LogicalId computation |
LoadBaseline(SignedBaseline) |
Method | Loads a baseline for comparison. Call before Evaluate(). |
Evaluate(Finding) |
Method | Compares a finding against the baseline. Returns DriftResult. |
GetCurrentBaseline() |
Method | Returns Baseline? from the loaded signed baseline |
SamplingController
| Property / Method | Type | Description |
|---|---|---|
SamplingController(int maxEventsPerSecond = 1000) |
Constructor | Sets the rate limit |
ShouldProcess() |
bool |
Returns true if event should be processed; false if it should be dropped |
IsSampling |
bool |
true when sampling is active (events being dropped) |
MaxEventsPerSecond |
int |
Configured rate limit |
SampleCount |
int |
Number of events dropped in the current window |
HealthMonitor
| Property / Method / Event | Type | Description |
|---|---|---|
IsFaulted |
bool |
true after 3 consecutive faults. Listener stops processing. |
FaultCount |
int |
Total number of recorded faults |
LastFaultAt |
DateTimeOffset? |
Timestamp of the most recent fault |
RecordFault(string reason) |
Method | Records a fault. Auto-disables at count 3. |
TryEnter() |
bool |
Returns false if faulted (events should be dropped) |
Reset() |
Method | Clears fault state and count |
OnFault |
event Action<CanonicalEvent> |
Fires when self-disabling triggers |
DriftResult
| Property | Type | Description |
|---|---|---|
HasDrift |
bool |
true if drift was detected |
Category |
DriftCategory |
Type of drift |
LogicalId |
LogicalId |
Content-based identifier |
Algorithm |
string |
Algorithm name |
Description |
string? |
Human-readable description |
Risk |
RiskLevel |
Current risk level |
Confidence |
Confidence |
Current confidence level |
Observed |
Finding? |
The newly observed finding |
BaselineEntry |
Finding? |
The matching baseline entry (if any) |
| Static Factory | Returns | Use Case |
|---|---|---|
NewFinding(LogicalId, Finding) |
DriftResult |
No baseline entry exists |
RiskChanged(LogicalId, Finding, Finding) |
DriftResult |
Risk level differs from baseline |
ConfidenceChanged(LogicalId, Finding, Finding) |
DriftResult |
Confidence differs from baseline |
NoDrift(LogicalId, string) |
DriftResult |
Finding matches baseline |
BaselineNotLoaded(LogicalId, string) |
DriftResult |
No baseline available |
DriftCategory
| Value | Description |
|---|---|
NewFinding |
Crypto asset detected with no baseline entry |
ResolvedFinding |
(Reserved) Finding present in baseline but not observed |
RiskChanged |
Risk level changed since baseline |
ConfidenceChanged |
Detection confidence changed since baseline |
BaselineDrift |
(Reserved) General baseline mismatch |
HealthEvent |
Guardian Runtime health status change |
SamplingActive |
Event sampling activated due to rate limit |
CanonicalEvent
| Property | Type | Description |
|---|---|---|
EventId |
string |
Unique event identifier |
AssetId |
string |
CBOM asset identifier for traceability |
Algorithm |
string |
Crypto algorithm name |
Severity |
RiskLevel |
Severity level |
Category |
DriftCategory |
Type of drift |
CorrelationId |
string |
Cross-event correlation ID |
Timestamp |
DateTimeOffset |
Event timestamp (UTC) |
FindingLocationalId |
string? |
Source location of the finding |
BaselineLogicalId |
string? |
Baseline's logical identifier |
ObservedLogicalId |
string? |
Observed logical identifier |
Description |
string? |
Human-readable event description |
RecommendedAction |
string? |
Suggested remediation step |
EventExporter (Static)
| Method | Returns | Target |
|---|---|---|
ToSentinelJson(CanonicalEvent) |
string |
Microsoft Sentinel custom log |
ToSplunkJson(CanonicalEvent) |
string |
Splunk HEC event endpoint |
ToElasticJson(CanonicalEvent) |
string |
Elasticsearch index |
Configuration Options
Sampling Rate
// Default: 1000 events per second
var sampling = new SamplingController(maxEventsPerSecond: 1000);
// Low-resource environments: 100 events per second
var sampling = new SamplingController(maxEventsPerSecond: 100);
// High-throughput services: 5000 events per second
var sampling = new SamplingController(maxEventsPerSecond: 5000);
Health Monitor Threshold
| Threshold | Behavior |
|---|---|
| Fault count < 3 | Events logged but processing continues |
| Fault count = 3 | Self-disable. Health event emitted. TryEnter() returns false. |
After Reset() |
cleared, processing can resume on next Start() |
The threshold of 3 is hardcoded to prevent flapping (one transient error shouldn't disable monitoring) while being aggressive enough to catch persistent issues.
SIEM Configuration
| SIEM | Endpoint Type | Authentication |
|---|---|---|
| Microsoft Sentinel | DCR Log Ingestion API | Entra ID (Managed Identity or Service Principal) |
| Splunk | HTTP Event Collector (HEC) | HEC Token |
| Elastic | Elasticsearch _bulk API |
API Key or Basic Auth |
Memory & Performance
| Metric | Bound |
|---|---|
| Maximum events per second | maxEventsPerSecond (default 1000) |
| DriftEngine concurrent entries | ConcurrentDictionary — grows with unique LogicalIds |
| Canonical Event allocation | One allocation per drift event (not per crypto operation) |
| DiagnosticSource overhead | Negligible — subscription-based, not polling |
The DriftEngine._observed dictionary grows cumulatively over the process lifetime. For long-running services (days/weeks), consider periodically purging entries that have been in a steady state:
// Optional: periodic cleanup (not built-in)
if (DateTimeOffset.UtcNow - lastPurge > TimeSpan.FromHours(24))
{
// Custom purge logic if needed
}
DiagnosticSource Event Filtering
By default, CryptoDiagnosticObserver subscribes to sources starting with:
"CipherShift365"(case-insensitive)"Microsoft.AspNetCore"(case-insensitive)
And filters events containing:
"Crypto","Sign", or"Encapsulate"(case-insensitive)
This minimizes overhead from non-crypto diagnostic events.
Value Showcase
Before Guardian Runtime
Q: "Has the production deployment drifted from our crypto baseline?"
A: "We don't know. We run a gate at build time. What happens at runtime is anyone's guess."
Risk: HIGH — unknown runtime behavior, no telemetry
After Guardian Runtime
Q: "Has the production deployment drifted from our crypto baseline?"
A: "Guardian Runtime has detected 0 drift events in the last 24 hours.
Baseline v12 loaded, 47 crypto events processed, 0 sampling drops.
Health: green. Full event log available in Sentinel."
Risk: LOW — continuous verification with real-time SIEM visibility
Quantitative Impact
| Metric | Without Guardian Runtime | With Guardian Runtime |
|---|---|---|
| Time to detect runtime crypto change | Never (no monitoring) | <1 second |
| SIEM integration effort | Custom parser development required | Native JSON formatting, zero custom parsers |
| Host application risk | N/A | Zero — self-disabling, bounded resources |
| APM tool compatibility | N/A | Full — no CLR Profiler slot consumed |
| Cross-team visibility | Siloed in individual teams' logs | Canonical events in centralized SIEM |
Compliance Alignment
| Framework | How Guardian Runtime Helps |
|---|---|
| CNSA 2.0 | Continuous verification that only approved algorithms are used at runtime |
| NSM-10 | Ongoing monitoring of quantum-resistance posture in operational systems |
| NIST SP 800-137 | Continuous monitoring for information security |
| FedRAMP Continuous Monitoring | Cryptographic drift events feed into the continuous monitoring strategy |
| MITRE ATT&CK (T1573 — Encrypted Channel) | Detects unexpected cryptographic usage patterns |
Next: See Knowledge Base for the signed+encrypted KB package format that drives both Gate and Runtime.