feat(telemetry): add telemetry client and services for tracking events
- Implemented TelemetryClient to handle event queuing and flushing to the telemetry endpoint. - Created TtfsTelemetryService for emitting specific telemetry events related to TTFS. - Added tests for TelemetryClient to ensure event queuing and flushing functionality. - Introduced models for reachability drift detection, including DriftResult and DriftedSink. - Developed DriftApiService for interacting with the drift detection API. - Updated FirstSignalCardComponent to emit telemetry events on signal appearance. - Enhanced localization support for first signal component with i18n strings.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
using System.CommandLine;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Cli.Extensions;
|
||||
|
||||
namespace StellaOps.Cli.Commands.Proof;
|
||||
|
||||
@@ -32,28 +33,33 @@ public class KeyRotationCommandGroup
|
||||
{
|
||||
var keyCommand = new Command("key", "Key management and rotation commands");
|
||||
|
||||
keyCommand.AddCommand(BuildListCommand());
|
||||
keyCommand.AddCommand(BuildAddCommand());
|
||||
keyCommand.AddCommand(BuildRevokeCommand());
|
||||
keyCommand.AddCommand(BuildRotateCommand());
|
||||
keyCommand.AddCommand(BuildStatusCommand());
|
||||
keyCommand.AddCommand(BuildHistoryCommand());
|
||||
keyCommand.AddCommand(BuildVerifyCommand());
|
||||
keyCommand.Add(BuildListCommand());
|
||||
keyCommand.Add(BuildAddCommand());
|
||||
keyCommand.Add(BuildRevokeCommand());
|
||||
keyCommand.Add(BuildRotateCommand());
|
||||
keyCommand.Add(BuildStatusCommand());
|
||||
keyCommand.Add(BuildHistoryCommand());
|
||||
keyCommand.Add(BuildVerifyCommand());
|
||||
|
||||
return keyCommand;
|
||||
}
|
||||
|
||||
private Command BuildListCommand()
|
||||
{
|
||||
var anchorArg = new Argument<Guid>("anchorId", "Trust anchor ID");
|
||||
var includeRevokedOption = new Option<bool>(
|
||||
name: "--include-revoked",
|
||||
getDefaultValue: () => false,
|
||||
description: "Include revoked keys in output");
|
||||
var outputOption = new Option<string>(
|
||||
name: "--output",
|
||||
getDefaultValue: () => "text",
|
||||
description: "Output format: text, json");
|
||||
var anchorArg = new Argument<Guid>("anchorId")
|
||||
{
|
||||
Description = "Trust anchor ID"
|
||||
};
|
||||
|
||||
var includeRevokedOption = new Option<bool>("--include-revoked")
|
||||
{
|
||||
Description = "Include revoked keys in output"
|
||||
}.SetDefaultValue(false);
|
||||
|
||||
var outputOption = new Option<string>("--output")
|
||||
{
|
||||
Description = "Output format: text, json"
|
||||
}.SetDefaultValue("text").FromAmong("text", "json");
|
||||
|
||||
var listCommand = new Command("list", "List keys for a trust anchor")
|
||||
{
|
||||
@@ -62,12 +68,12 @@ public class KeyRotationCommandGroup
|
||||
outputOption
|
||||
};
|
||||
|
||||
listCommand.SetHandler(async (context) =>
|
||||
listCommand.SetAction(async (parseResult, ct) =>
|
||||
{
|
||||
var anchorId = context.ParseResult.GetValueForArgument(anchorArg);
|
||||
var includeRevoked = context.ParseResult.GetValueForOption(includeRevokedOption);
|
||||
var output = context.ParseResult.GetValueForOption(outputOption) ?? "text";
|
||||
context.ExitCode = await ListKeysAsync(anchorId, includeRevoked, output, context.GetCancellationToken());
|
||||
var anchorId = parseResult.GetValue(anchorArg);
|
||||
var includeRevoked = parseResult.GetValue(includeRevokedOption);
|
||||
var output = parseResult.GetValue(outputOption) ?? "text";
|
||||
Environment.ExitCode = await ListKeysAsync(anchorId, includeRevoked, output, ct).ConfigureAwait(false);
|
||||
});
|
||||
|
||||
return listCommand;
|
||||
@@ -75,18 +81,30 @@ public class KeyRotationCommandGroup
|
||||
|
||||
private Command BuildAddCommand()
|
||||
{
|
||||
var anchorArg = new Argument<Guid>("anchorId", "Trust anchor ID");
|
||||
var keyIdArg = new Argument<string>("keyId", "New key ID");
|
||||
var algorithmOption = new Option<string>(
|
||||
aliases: ["-a", "--algorithm"],
|
||||
getDefaultValue: () => "Ed25519",
|
||||
description: "Key algorithm: Ed25519, ES256, ES384, RS256");
|
||||
var publicKeyOption = new Option<string?>(
|
||||
name: "--public-key",
|
||||
description: "Path to public key file (PEM format)");
|
||||
var notesOption = new Option<string?>(
|
||||
name: "--notes",
|
||||
description: "Human-readable notes about the key");
|
||||
var anchorArg = new Argument<Guid>("anchorId")
|
||||
{
|
||||
Description = "Trust anchor ID"
|
||||
};
|
||||
|
||||
var keyIdArg = new Argument<string>("keyId")
|
||||
{
|
||||
Description = "New key ID"
|
||||
};
|
||||
|
||||
var algorithmOption = new Option<string>("--algorithm", new[] { "-a" })
|
||||
{
|
||||
Description = "Key algorithm: Ed25519, ES256, ES384, RS256"
|
||||
}.SetDefaultValue("Ed25519").FromAmong("Ed25519", "ES256", "ES384", "RS256");
|
||||
|
||||
var publicKeyOption = new Option<string?>("--public-key")
|
||||
{
|
||||
Description = "Path to public key file (PEM format)"
|
||||
};
|
||||
|
||||
var notesOption = new Option<string?>("--notes")
|
||||
{
|
||||
Description = "Human-readable notes about the key"
|
||||
};
|
||||
|
||||
var addCommand = new Command("add", "Add a new key to a trust anchor")
|
||||
{
|
||||
@@ -97,14 +115,14 @@ public class KeyRotationCommandGroup
|
||||
notesOption
|
||||
};
|
||||
|
||||
addCommand.SetHandler(async (context) =>
|
||||
addCommand.SetAction(async (parseResult, ct) =>
|
||||
{
|
||||
var anchorId = context.ParseResult.GetValueForArgument(anchorArg);
|
||||
var keyId = context.ParseResult.GetValueForArgument(keyIdArg);
|
||||
var algorithm = context.ParseResult.GetValueForOption(algorithmOption) ?? "Ed25519";
|
||||
var publicKeyPath = context.ParseResult.GetValueForOption(publicKeyOption);
|
||||
var notes = context.ParseResult.GetValueForOption(notesOption);
|
||||
context.ExitCode = await AddKeyAsync(anchorId, keyId, algorithm, publicKeyPath, notes, context.GetCancellationToken());
|
||||
var anchorId = parseResult.GetValue(anchorArg);
|
||||
var keyId = parseResult.GetValue(keyIdArg);
|
||||
var algorithm = parseResult.GetValue(algorithmOption) ?? "Ed25519";
|
||||
var publicKeyPath = parseResult.GetValue(publicKeyOption);
|
||||
var notes = parseResult.GetValue(notesOption);
|
||||
Environment.ExitCode = await AddKeyAsync(anchorId, keyId, algorithm, publicKeyPath, notes, ct).ConfigureAwait(false);
|
||||
});
|
||||
|
||||
return addCommand;
|
||||
@@ -112,19 +130,30 @@ public class KeyRotationCommandGroup
|
||||
|
||||
private Command BuildRevokeCommand()
|
||||
{
|
||||
var anchorArg = new Argument<Guid>("anchorId", "Trust anchor ID");
|
||||
var keyIdArg = new Argument<string>("keyId", "Key ID to revoke");
|
||||
var reasonOption = new Option<string>(
|
||||
aliases: ["-r", "--reason"],
|
||||
getDefaultValue: () => "rotation-complete",
|
||||
description: "Reason for revocation");
|
||||
var effectiveOption = new Option<DateTimeOffset?>(
|
||||
name: "--effective-at",
|
||||
description: "Effective revocation time (default: now). ISO-8601 format.");
|
||||
var forceOption = new Option<bool>(
|
||||
name: "--force",
|
||||
getDefaultValue: () => false,
|
||||
description: "Skip confirmation prompt");
|
||||
var anchorArg = new Argument<Guid>("anchorId")
|
||||
{
|
||||
Description = "Trust anchor ID"
|
||||
};
|
||||
|
||||
var keyIdArg = new Argument<string>("keyId")
|
||||
{
|
||||
Description = "Key ID to revoke"
|
||||
};
|
||||
|
||||
var reasonOption = new Option<string>("--reason", new[] { "-r" })
|
||||
{
|
||||
Description = "Reason for revocation"
|
||||
}.SetDefaultValue("rotation-complete");
|
||||
|
||||
var effectiveOption = new Option<DateTimeOffset?>("--effective-at")
|
||||
{
|
||||
Description = "Effective revocation time (default: now). ISO-8601 format."
|
||||
};
|
||||
|
||||
var forceOption = new Option<bool>("--force")
|
||||
{
|
||||
Description = "Skip confirmation prompt"
|
||||
}.SetDefaultValue(false);
|
||||
|
||||
var revokeCommand = new Command("revoke", "Revoke a key from a trust anchor")
|
||||
{
|
||||
@@ -135,14 +164,14 @@ public class KeyRotationCommandGroup
|
||||
forceOption
|
||||
};
|
||||
|
||||
revokeCommand.SetHandler(async (context) =>
|
||||
revokeCommand.SetAction(async (parseResult, ct) =>
|
||||
{
|
||||
var anchorId = context.ParseResult.GetValueForArgument(anchorArg);
|
||||
var keyId = context.ParseResult.GetValueForArgument(keyIdArg);
|
||||
var reason = context.ParseResult.GetValueForOption(reasonOption) ?? "rotation-complete";
|
||||
var effectiveAt = context.ParseResult.GetValueForOption(effectiveOption) ?? DateTimeOffset.UtcNow;
|
||||
var force = context.ParseResult.GetValueForOption(forceOption);
|
||||
context.ExitCode = await RevokeKeyAsync(anchorId, keyId, reason, effectiveAt, force, context.GetCancellationToken());
|
||||
var anchorId = parseResult.GetValue(anchorArg);
|
||||
var keyId = parseResult.GetValue(keyIdArg);
|
||||
var reason = parseResult.GetValue(reasonOption) ?? "rotation-complete";
|
||||
var effectiveAt = parseResult.GetValue(effectiveOption) ?? DateTimeOffset.UtcNow;
|
||||
var force = parseResult.GetValue(forceOption);
|
||||
Environment.ExitCode = await RevokeKeyAsync(anchorId, keyId, reason, effectiveAt, force, ct).ConfigureAwait(false);
|
||||
});
|
||||
|
||||
return revokeCommand;
|
||||
@@ -150,20 +179,35 @@ public class KeyRotationCommandGroup
|
||||
|
||||
private Command BuildRotateCommand()
|
||||
{
|
||||
var anchorArg = new Argument<Guid>("anchorId", "Trust anchor ID");
|
||||
var oldKeyIdArg = new Argument<string>("oldKeyId", "Old key ID to replace");
|
||||
var newKeyIdArg = new Argument<string>("newKeyId", "New key ID");
|
||||
var algorithmOption = new Option<string>(
|
||||
aliases: ["-a", "--algorithm"],
|
||||
getDefaultValue: () => "Ed25519",
|
||||
description: "Key algorithm: Ed25519, ES256, ES384, RS256");
|
||||
var publicKeyOption = new Option<string?>(
|
||||
name: "--public-key",
|
||||
description: "Path to new public key file (PEM format)");
|
||||
var overlapOption = new Option<int>(
|
||||
name: "--overlap-days",
|
||||
getDefaultValue: () => 30,
|
||||
description: "Days to keep both keys active before revoking old");
|
||||
var anchorArg = new Argument<Guid>("anchorId")
|
||||
{
|
||||
Description = "Trust anchor ID"
|
||||
};
|
||||
|
||||
var oldKeyIdArg = new Argument<string>("oldKeyId")
|
||||
{
|
||||
Description = "Old key ID to replace"
|
||||
};
|
||||
|
||||
var newKeyIdArg = new Argument<string>("newKeyId")
|
||||
{
|
||||
Description = "New key ID"
|
||||
};
|
||||
|
||||
var algorithmOption = new Option<string>("--algorithm", new[] { "-a" })
|
||||
{
|
||||
Description = "Key algorithm: Ed25519, ES256, ES384, RS256"
|
||||
}.SetDefaultValue("Ed25519").FromAmong("Ed25519", "ES256", "ES384", "RS256");
|
||||
|
||||
var publicKeyOption = new Option<string?>("--public-key")
|
||||
{
|
||||
Description = "Path to new public key file (PEM format)"
|
||||
};
|
||||
|
||||
var overlapOption = new Option<int>("--overlap-days")
|
||||
{
|
||||
Description = "Days to keep both keys active before revoking old"
|
||||
}.SetDefaultValue(30);
|
||||
|
||||
var rotateCommand = new Command("rotate", "Rotate a key (add new, schedule old revocation)")
|
||||
{
|
||||
@@ -175,15 +219,15 @@ public class KeyRotationCommandGroup
|
||||
overlapOption
|
||||
};
|
||||
|
||||
rotateCommand.SetHandler(async (context) =>
|
||||
rotateCommand.SetAction(async (parseResult, ct) =>
|
||||
{
|
||||
var anchorId = context.ParseResult.GetValueForArgument(anchorArg);
|
||||
var oldKeyId = context.ParseResult.GetValueForArgument(oldKeyIdArg);
|
||||
var newKeyId = context.ParseResult.GetValueForArgument(newKeyIdArg);
|
||||
var algorithm = context.ParseResult.GetValueForOption(algorithmOption) ?? "Ed25519";
|
||||
var publicKeyPath = context.ParseResult.GetValueForOption(publicKeyOption);
|
||||
var overlapDays = context.ParseResult.GetValueForOption(overlapOption);
|
||||
context.ExitCode = await RotateKeyAsync(anchorId, oldKeyId, newKeyId, algorithm, publicKeyPath, overlapDays, context.GetCancellationToken());
|
||||
var anchorId = parseResult.GetValue(anchorArg);
|
||||
var oldKeyId = parseResult.GetValue(oldKeyIdArg);
|
||||
var newKeyId = parseResult.GetValue(newKeyIdArg);
|
||||
var algorithm = parseResult.GetValue(algorithmOption) ?? "Ed25519";
|
||||
var publicKeyPath = parseResult.GetValue(publicKeyOption);
|
||||
var overlapDays = parseResult.GetValue(overlapOption);
|
||||
Environment.ExitCode = await RotateKeyAsync(anchorId, oldKeyId, newKeyId, algorithm, publicKeyPath, overlapDays, ct).ConfigureAwait(false);
|
||||
});
|
||||
|
||||
return rotateCommand;
|
||||
@@ -191,11 +235,15 @@ public class KeyRotationCommandGroup
|
||||
|
||||
private Command BuildStatusCommand()
|
||||
{
|
||||
var anchorArg = new Argument<Guid>("anchorId", "Trust anchor ID");
|
||||
var outputOption = new Option<string>(
|
||||
name: "--output",
|
||||
getDefaultValue: () => "text",
|
||||
description: "Output format: text, json");
|
||||
var anchorArg = new Argument<Guid>("anchorId")
|
||||
{
|
||||
Description = "Trust anchor ID"
|
||||
};
|
||||
|
||||
var outputOption = new Option<string>("--output")
|
||||
{
|
||||
Description = "Output format: text, json"
|
||||
}.SetDefaultValue("text").FromAmong("text", "json");
|
||||
|
||||
var statusCommand = new Command("status", "Show key rotation status and warnings")
|
||||
{
|
||||
@@ -203,11 +251,11 @@ public class KeyRotationCommandGroup
|
||||
outputOption
|
||||
};
|
||||
|
||||
statusCommand.SetHandler(async (context) =>
|
||||
statusCommand.SetAction(async (parseResult, ct) =>
|
||||
{
|
||||
var anchorId = context.ParseResult.GetValueForArgument(anchorArg);
|
||||
var output = context.ParseResult.GetValueForOption(outputOption) ?? "text";
|
||||
context.ExitCode = await ShowStatusAsync(anchorId, output, context.GetCancellationToken());
|
||||
var anchorId = parseResult.GetValue(anchorArg);
|
||||
var output = parseResult.GetValue(outputOption) ?? "text";
|
||||
Environment.ExitCode = await ShowStatusAsync(anchorId, output, ct).ConfigureAwait(false);
|
||||
});
|
||||
|
||||
return statusCommand;
|
||||
@@ -215,18 +263,25 @@ public class KeyRotationCommandGroup
|
||||
|
||||
private Command BuildHistoryCommand()
|
||||
{
|
||||
var anchorArg = new Argument<Guid>("anchorId", "Trust anchor ID");
|
||||
var keyIdOption = new Option<string?>(
|
||||
aliases: ["-k", "--key-id"],
|
||||
description: "Filter by specific key ID");
|
||||
var limitOption = new Option<int>(
|
||||
name: "--limit",
|
||||
getDefaultValue: () => 50,
|
||||
description: "Maximum entries to show");
|
||||
var outputOption = new Option<string>(
|
||||
name: "--output",
|
||||
getDefaultValue: () => "text",
|
||||
description: "Output format: text, json");
|
||||
var anchorArg = new Argument<Guid>("anchorId")
|
||||
{
|
||||
Description = "Trust anchor ID"
|
||||
};
|
||||
|
||||
var keyIdOption = new Option<string?>("--key-id", new[] { "-k" })
|
||||
{
|
||||
Description = "Filter by specific key ID"
|
||||
};
|
||||
|
||||
var limitOption = new Option<int>("--limit")
|
||||
{
|
||||
Description = "Maximum entries to show"
|
||||
}.SetDefaultValue(50);
|
||||
|
||||
var outputOption = new Option<string>("--output")
|
||||
{
|
||||
Description = "Output format: text, json"
|
||||
}.SetDefaultValue("text").FromAmong("text", "json");
|
||||
|
||||
var historyCommand = new Command("history", "Show key audit history")
|
||||
{
|
||||
@@ -236,13 +291,13 @@ public class KeyRotationCommandGroup
|
||||
outputOption
|
||||
};
|
||||
|
||||
historyCommand.SetHandler(async (context) =>
|
||||
historyCommand.SetAction(async (parseResult, ct) =>
|
||||
{
|
||||
var anchorId = context.ParseResult.GetValueForArgument(anchorArg);
|
||||
var keyId = context.ParseResult.GetValueForOption(keyIdOption);
|
||||
var limit = context.ParseResult.GetValueForOption(limitOption);
|
||||
var output = context.ParseResult.GetValueForOption(outputOption) ?? "text";
|
||||
context.ExitCode = await ShowHistoryAsync(anchorId, keyId, limit, output, context.GetCancellationToken());
|
||||
var anchorId = parseResult.GetValue(anchorArg);
|
||||
var keyId = parseResult.GetValue(keyIdOption);
|
||||
var limit = parseResult.GetValue(limitOption);
|
||||
var output = parseResult.GetValue(outputOption) ?? "text";
|
||||
Environment.ExitCode = await ShowHistoryAsync(anchorId, keyId, limit, output, ct).ConfigureAwait(false);
|
||||
});
|
||||
|
||||
return historyCommand;
|
||||
@@ -250,11 +305,20 @@ public class KeyRotationCommandGroup
|
||||
|
||||
private Command BuildVerifyCommand()
|
||||
{
|
||||
var anchorArg = new Argument<Guid>("anchorId", "Trust anchor ID");
|
||||
var keyIdArg = new Argument<string>("keyId", "Key ID to verify");
|
||||
var signedAtOption = new Option<DateTimeOffset?>(
|
||||
aliases: ["-t", "--signed-at"],
|
||||
description: "Verify key was valid at this time (ISO-8601)");
|
||||
var anchorArg = new Argument<Guid>("anchorId")
|
||||
{
|
||||
Description = "Trust anchor ID"
|
||||
};
|
||||
|
||||
var keyIdArg = new Argument<string>("keyId")
|
||||
{
|
||||
Description = "Key ID to verify"
|
||||
};
|
||||
|
||||
var signedAtOption = new Option<DateTimeOffset?>("--signed-at", new[] { "-t" })
|
||||
{
|
||||
Description = "Verify key was valid at this time (ISO-8601)"
|
||||
};
|
||||
|
||||
var verifyCommand = new Command("verify", "Verify a key's validity at a point in time")
|
||||
{
|
||||
@@ -263,12 +327,12 @@ public class KeyRotationCommandGroup
|
||||
signedAtOption
|
||||
};
|
||||
|
||||
verifyCommand.SetHandler(async (context) =>
|
||||
verifyCommand.SetAction(async (parseResult, ct) =>
|
||||
{
|
||||
var anchorId = context.ParseResult.GetValueForArgument(anchorArg);
|
||||
var keyId = context.ParseResult.GetValueForArgument(keyIdArg);
|
||||
var signedAt = context.ParseResult.GetValueForOption(signedAtOption) ?? DateTimeOffset.UtcNow;
|
||||
context.ExitCode = await VerifyKeyAsync(anchorId, keyId, signedAt, context.GetCancellationToken());
|
||||
var anchorId = parseResult.GetValue(anchorArg);
|
||||
var keyId = parseResult.GetValue(keyIdArg);
|
||||
var signedAt = parseResult.GetValue(signedAtOption) ?? DateTimeOffset.UtcNow;
|
||||
Environment.ExitCode = await VerifyKeyAsync(anchorId, keyId, signedAt, ct).ConfigureAwait(false);
|
||||
});
|
||||
|
||||
return verifyCommand;
|
||||
|
||||
Reference in New Issue
Block a user